You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by rj...@apache.org on 2014/11/15 02:14:53 UTC
svn commit: r1639823 [4/29] - in
/bloodhound/branches/trac-1.0.2-integration/trac: ./ contrib/
contrib/cgi-bin/ contrib/workflow/ doc/ doc/dev/ doc/utils/ sample-plugins/
sample-plugins/permissions/ sample-plugins/workflow/ trac/ trac/admin/
trac/admin...
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/admin/web_ui.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/admin/web_ui.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/admin/web_ui.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/admin/web_ui.py Sat Nov 15 01:14:46 2014
@@ -22,11 +22,6 @@ import pkg_resources
import re
import shutil
-try:
- from babel.core import Locale
-except ImportError:
- Locale = None
-
from genshi import HTML
from genshi.builder import tag
@@ -34,10 +29,10 @@ from trac.admin.api import IAdminPanelPr
from trac.core import *
from trac.loader import get_plugin_info, get_plugins_dir
from trac.perm import PermissionSystem, IPermissionRequestor
-from trac.util.datefmt import all_timezones
+from trac.util.datefmt import all_timezones, pytz
from trac.util.text import exception_to_unicode, \
- unicode_to_base64, unicode_from_base64
-from trac.util.translation import _, get_available_locales, ngettext
+ unicode_to_base64, unicode_from_base64
+from trac.util.translation import _, Locale, get_available_locales, ngettext
from trac.web import HTTPNotFound, IRequestHandler
from trac.web.chrome import add_notice, add_stylesheet, \
add_warning, Chrome, INavigationContributor, \
@@ -76,8 +71,8 @@ class AdminModule(Component):
# admin panel is available
panels, providers = self._get_panels(req)
if panels:
- yield 'mainnav', 'admin', tag.a(_('Admin'), href=req.href.admin(),
- title=_('Administration'))
+ yield 'mainnav', 'admin', tag.a(_("Admin"), href=req.href.admin(),
+ title=_("Administration"))
# IRequestHandler methods
@@ -93,7 +88,7 @@ class AdminModule(Component):
def process_request(self, req):
panels, providers = self._get_panels(req)
if not panels:
- raise HTTPNotFound(_('No administration panels available'))
+ raise HTTPNotFound(_("No administration panels available"))
def _panel_order(p1, p2):
if p1[::2] == ('general', 'basics'):
@@ -116,14 +111,14 @@ class AdminModule(Component):
path_info = req.args.get('path_info')
if not panel_id:
try:
- panel_id = filter(
- lambda panel: panel[0] == cat_id, panels)[0][2]
+ panel_id = \
+ filter(lambda panel: panel[0] == cat_id, panels)[0][2]
except IndexError:
- raise HTTPNotFound(_('Unknown administration panel'))
+ raise HTTPNotFound(_("Unknown administration panel"))
provider = providers.get((cat_id, panel_id), None)
if not provider:
- raise HTTPNotFound(_('Unknown administration panel'))
+ raise HTTPNotFound(_("Unknown administration panel"))
if hasattr(provider, 'render_admin_panel'):
template, data = provider.render_admin_panel(req, cat_id, panel_id,
@@ -201,14 +196,14 @@ def _save_config(config, req, log, notic
try:
config.save()
if notices is None:
- notices = [_('Your changes have been saved.')]
+ notices = [_("Your changes have been saved.")]
for notice in notices:
add_notice(req, notice)
except Exception, e:
- log.error('Error writing to trac.ini: %s', exception_to_unicode(e))
- add_warning(req, _('Error writing to trac.ini, make sure it is '
- 'writable by the web server. Your changes have '
- 'not been saved.'))
+ log.error("Error writing to trac.ini: %s", exception_to_unicode(e))
+ add_warning(req, _("Error writing to trac.ini, make sure it is "
+ "writable by the web server. Your changes have "
+ "not been saved."))
class BasicsAdminPanel(Component):
@@ -218,19 +213,19 @@ class BasicsAdminPanel(Component):
# IAdminPanelProvider methods
def get_admin_panels(self, req):
- if 'TRAC_ADMIN' in req.perm:
- yield ('general', _('General'), 'basics', _('Basic Settings'))
+ if 'TRAC_ADMIN' in req.perm('admin', 'general/basics'):
+ yield ('general', _("General"), 'basics', _("Basic Settings"))
def render_admin_panel(self, req, cat, page, path_info):
- req.perm.require('TRAC_ADMIN')
-
if Locale:
- locales = [Locale.parse(locale)
- for locale in get_available_locales()]
- languages = sorted((str(locale), locale.display_name)
- for locale in locales)
+ locale_ids = get_available_locales()
+ locales = [Locale.parse(locale) for locale in locale_ids]
+ # don't use str(locale) to prevent storing expanded locale
+ # identifier, see #11258
+ languages = sorted((id, locale.display_name)
+ for id, locale in zip(locale_ids, locales))
else:
- locales, languages = [], []
+ locale_ids, locales, languages = [], [], []
if req.method == 'POST':
for option in ('name', 'url', 'descr'):
@@ -242,7 +237,7 @@ class BasicsAdminPanel(Component):
self.config.set('trac', 'default_timezone', default_timezone)
default_language = req.args.get('default_language')
- if default_language not in locales:
+ if default_language not in locale_ids:
default_language = ''
self.config.set('trac', 'default_language', default_language)
@@ -261,9 +256,11 @@ class BasicsAdminPanel(Component):
data = {
'default_timezone': default_timezone,
'timezones': all_timezones,
+ 'has_pytz': pytz is not None,
'default_language': default_language.replace('-', '_'),
'languages': languages,
'default_date_format': default_date_format,
+ 'has_babel': Locale is not None,
}
Chrome(self.env).add_textarea_grips(req)
return 'admin_basics.html', data
@@ -276,7 +273,8 @@ class LoggingAdminPanel(Component):
# IAdminPanelProvider methods
def get_admin_panels(self, req):
- if 'TRAC_ADMIN' in req.perm and not getattr(self.env, 'parent', None):
+ if 'TRAC_ADMIN' in req.perm('admin', 'general/logging') and \
+ not getattr(self.env, 'parent', None):
yield ('general', _('General'), 'logging', _('Logging'))
def render_admin_panel(self, req, cat, page, path_info):
@@ -288,16 +286,18 @@ class LoggingAdminPanel(Component):
log_dir = os.path.join(self.env.path, 'log')
log_types = [
- dict(name='none', label=_('None'), selected=log_type == 'none', disabled=False),
- dict(name='stderr', label=_('Console'),
+ dict(name='none', label=_("None"),
+ selected=log_type == 'none', disabled=False),
+ dict(name='stderr', label=_("Console"),
selected=log_type == 'stderr', disabled=False),
- dict(name='file', label=_('File'), selected=log_type == 'file',
- disabled=False),
- dict(name='syslog', label=_('Syslog'), disabled=os.name != 'posix',
- selected=log_type in ('unix', 'syslog')),
- dict(name='eventlog', label=_('Windows event log'),
- disabled=os.name != 'nt',
- selected=log_type in ('winlog', 'eventlog', 'nteventlog')),
+ dict(name='file', label=_("File"),
+ selected=log_type == 'file', disabled=False),
+ dict(name='syslog', label=_("Syslog"),
+ selected=log_type in ('unix', 'syslog'),
+ disabled=os.name != 'posix'),
+ dict(name='eventlog', label=_("Windows event log"),
+ selected=log_type in ('winlog', 'eventlog', 'nteventlog'),
+ disabled=os.name != 'nt'),
]
log_levels = ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']
@@ -308,8 +308,8 @@ class LoggingAdminPanel(Component):
new_type = req.args.get('log_type')
if new_type not in [t['name'] for t in log_types]:
raise TracError(
- _('Unknown log type %(type)s', type=new_type),
- _('Invalid log type')
+ _("Unknown log type %(type)s", type=new_type),
+ _("Invalid log type")
)
if new_type != log_type:
self.config.set('logging', 'log_type', new_type)
@@ -323,8 +323,8 @@ class LoggingAdminPanel(Component):
new_level = req.args.get('log_level')
if new_level not in log_levels:
raise TracError(
- _('Unknown log level %(level)s', level=new_level),
- _('Invalid log level'))
+ _("Unknown log level %(level)s", level=new_level),
+ _("Invalid log level"))
if new_level != log_level:
self.config.set('logging', 'log_level', new_level)
changed = True
@@ -337,8 +337,8 @@ class LoggingAdminPanel(Component):
changed = True
log_file = new_file
if not log_file:
- raise TracError(_('You must specify a log file'),
- _('Missing field'))
+ raise TracError(_("You must specify a log file"),
+ _("Missing field"))
else:
self.config.remove('logging', 'log_file')
changed = True
@@ -366,8 +366,9 @@ class PermissionAdminPanel(Component):
# IAdminPanelProvider methods
def get_admin_panels(self, req):
- if 'PERMISSION_GRANT' in req.perm or 'PERMISSION_REVOKE' in req.perm:
- yield ('general', _('General'), 'perm', _('Permissions'))
+ perm = req.perm('admin', 'general/perm')
+ if 'PERMISSION_GRANT' in perm or 'PERMISSION_REVOKE' in perm:
+ yield ('general', _("General"), 'perm', _("Permissions"))
def render_admin_panel(self, req, cat, page, path_info):
perm = PermissionSystem(self.env)
@@ -380,51 +381,57 @@ class PermissionAdminPanel(Component):
group = req.args.get('group', '').strip()
if subject and subject.isupper() or \
- group and group.isupper():
- raise TracError(_('All upper-cased tokens are reserved for '
- 'permission names'))
+ group and group.isupper():
+ raise TracError(_("All upper-cased tokens are reserved for "
+ "permission names"))
# Grant permission to subject
if req.args.get('add') and subject and action:
- req.perm.require('PERMISSION_GRANT')
+ req.perm('admin', 'general/perm').require('PERMISSION_GRANT')
if action not in all_actions:
- raise TracError(_('Unknown action'))
+ raise TracError(_("Unknown action"))
req.perm.require(action)
if (subject, action) not in all_permissions:
perm.grant_permission(subject, action)
- add_notice(req, _('The subject %(subject)s has been '
- 'granted the permission %(action)s.',
+ add_notice(req, _("The subject %(subject)s has been "
+ "granted the permission %(action)s.",
subject=subject, action=action))
req.redirect(req.href.admin(cat, page))
else:
- add_warning(req, _('The permission %(action)s was already '
- 'granted to %(subject)s.',
+ add_warning(req, _("The permission %(action)s was already "
+ "granted to %(subject)s.",
action=action, subject=subject))
# Add subject to group
elif req.args.get('add') and subject and group:
- req.perm.require('PERMISSION_GRANT')
+ req.perm('admin', 'general/perm').require('PERMISSION_GRANT')
for action in perm.get_user_permissions(group):
if not action in all_actions: # plugin disabled?
- self.env.log.warn("Adding %s to group %s: " \
- "Permission %s unavailable, skipping perm check." \
- % (subject, group, action))
+ self.env.log.warn("Adding %s to group %s: "
+ "Permission %s unavailable, skipping perm check.",
+ subject, group, action)
else:
- req.perm.require(action)
+ req.perm.require(action,
+ message=_("The subject %(subject)s was not added "
+ "to the group %(group)s because the "
+ "group has %(perm)s permission and "
+ "users cannot grant permissions they "
+ "don't possess.", subject=subject,
+ group=group, perm=action))
if (subject, group) not in all_permissions:
perm.grant_permission(subject, group)
- add_notice(req, _('The subject %(subject)s has been added '
- 'to the group %(group)s.',
+ add_notice(req, _("The subject %(subject)s has been added "
+ "to the group %(group)s.",
subject=subject, group=group))
req.redirect(req.href.admin(cat, page))
else:
- add_warning(req, _('The subject %(subject)s was already '
- 'added to the group %(group)s.',
+ add_warning(req, _("The subject %(subject)s was already "
+ "added to the group %(group)s.",
subject=subject, group=group))
# Remove permissions action
elif req.args.get('remove') and req.args.get('sel'):
- req.perm.require('PERMISSION_REVOKE')
+ req.perm('admin', 'general/perm').require('PERMISSION_REVOKE')
sel = req.args.get('sel')
sel = sel if isinstance(sel, list) else [sel]
for key in sel:
@@ -433,8 +440,8 @@ class PermissionAdminPanel(Component):
action = unicode_from_base64(action)
if (subject, action) in perm.get_all_permissions():
perm.revoke_permission(subject, action)
- add_notice(req, _('The selected permissions have been '
- 'revoked.'))
+ add_notice(req, _("The selected permissions have been "
+ "revoked."))
req.redirect(req.href.admin(cat, page))
perms = [perm for perm in all_permissions if perm[1].isupper()]
@@ -453,13 +460,14 @@ class PluginAdminPanel(Component):
# IAdminPanelProvider methods
def get_admin_panels(self, req):
- if 'TRAC_ADMIN' in req.perm and not getattr(self.env, 'parent', None):
+ if 'TRAC_ADMIN' in req.perm('admin', 'general/plugin') and \
+ not getattr(self.env, 'parent', None):
yield ('general', _('General'), 'plugin', _('Plugins'))
def render_admin_panel(self, req, cat, page, path_info):
if getattr(self.env, 'parent', None):
raise PermissionError()
- req.perm.require('TRAC_ADMIN')
+ req.perm('admin', 'general/plugin').require('TRAC_ADMIN')
if req.method == 'POST':
if 'install' in req.args:
@@ -469,7 +477,7 @@ class PluginAdminPanel(Component):
else:
self._do_update(req)
anchor = ''
- if req.args.has_key('plugin'):
+ if 'plugin' in req.args:
anchor = '#no%d' % (int(req.args.get('plugin')) + 1)
req.redirect(req.href.admin(cat, page) + anchor)
@@ -479,26 +487,26 @@ class PluginAdminPanel(Component):
def _do_install(self, req):
"""Install a plugin."""
- if not req.args.has_key('plugin_file'):
- raise TracError(_('No file uploaded'))
+ if 'plugin_file' not in req.args:
+ raise TracError(_("No file uploaded"))
upload = req.args['plugin_file']
if isinstance(upload, unicode) or not upload.filename:
- raise TracError(_('No file uploaded'))
+ raise TracError(_("No file uploaded"))
plugin_filename = upload.filename.replace('\\', '/').replace(':', '/')
plugin_filename = os.path.basename(plugin_filename)
if not plugin_filename:
- raise TracError(_('No file uploaded'))
+ raise TracError(_("No file uploaded"))
if not plugin_filename.endswith('.egg') and \
not plugin_filename.endswith('.py'):
- raise TracError(_('Uploaded file is not a Python source file or '
- 'egg'))
+ raise TracError(_("Uploaded file is not a Python source file or "
+ "egg"))
target_path = os.path.join(self.env.path, 'plugins', plugin_filename)
if os.path.isfile(target_path):
- raise TracError(_('Plugin %(name)s already installed',
+ raise TracError(_("Plugin %(name)s already installed",
name=plugin_filename))
- self.log.info('Installing plugin %s', plugin_filename)
+ self.log.info("Installing plugin %s", plugin_filename)
flags = os.O_CREAT + os.O_WRONLY + os.O_EXCL
try:
flags += os.O_BINARY
@@ -507,7 +515,7 @@ class PluginAdminPanel(Component):
pass
with os.fdopen(os.open(target_path, flags, 0666), 'w') as target_file:
shutil.copyfileobj(upload.file, target_file)
- self.log.info('Plugin %s installed to %s', plugin_filename,
+ self.log.info("Plugin %s installed to %s", plugin_filename,
target_path)
# TODO: Validate that the uploaded file is actually a valid Trac plugin
@@ -522,7 +530,7 @@ class PluginAdminPanel(Component):
plugin_path = os.path.join(self.env.path, 'plugins', plugin_filename)
if not os.path.isfile(plugin_path):
return
- self.log.info('Uninstalling plugin %s', plugin_filename)
+ self.log.info("Uninstalling plugin %s", plugin_filename)
os.remove(plugin_path)
# Make the environment reset itself on the next request
@@ -543,8 +551,8 @@ class PluginAdminPanel(Component):
if is_enabled != must_enable:
self.config.set('components', component,
'disabled' if is_enabled else 'enabled')
- self.log.info('%sabling component %s',
- 'Dis' if is_enabled else 'En', component)
+ self.log.info("%sabling component %s",
+ "Dis" if is_enabled else "En", component)
if must_enable:
added.append(component)
else:
@@ -562,13 +570,13 @@ class PluginAdminPanel(Component):
removed.sort()
notices = []
if removed:
- msg = ngettext('The following component has been disabled:',
- 'The following components have been disabled:',
+ msg = ngettext("The following component has been disabled:",
+ "The following components have been disabled:",
len(removed))
notices.append(tag(msg, make_list(removed)))
if added:
- msg = ngettext('The following component has been enabled:',
- 'The following components have been enabled:',
+ msg = ngettext("The following component has been enabled:",
+ "The following components have been enabled:",
len(added))
notices.append(tag(msg, make_list(added)))
@@ -581,7 +589,7 @@ class PluginAdminPanel(Component):
try:
return format_to_html(self.env, context, text)
except Exception, e:
- self.log.error('Unable to render component documentation: %s',
+ self.log.error("Unable to render component documentation: %s",
exception_to_unicode(e, traceback=True))
return tag.pre(text)
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/attachment.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/attachment.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/attachment.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/attachment.py Sat Nov 15 01:14:46 2014
@@ -38,7 +38,7 @@ from trac.mimeview import *
from trac.perm import PermissionError, IPermissionPolicy
from trac.resource import *
from trac.search import search_to_sql, shorten_result
-from trac.util import content_disposition, get_reporter_id
+from trac.util import content_disposition, create_zipinfo, get_reporter_id
from trac.util.compat import sha1
from trac.util.datefmt import format_datetime, from_utimestamp, \
to_datetime, to_utimestamp, utc
@@ -93,8 +93,9 @@ class IAttachmentManipulator(Interface):
attachment. Therefore, a return value of ``[]`` means
everything is OK."""
+
class ILegacyAttachmentPolicyDelegate(Interface):
- """Interface that can be used by plugins to seemlessly participate
+ """Interface that can be used by plugins to seamlessly participate
to the legacy way of checking for attachment permissions.
This should no longer be necessary once it becomes easier to
@@ -310,6 +311,12 @@ class Attachment(object):
t = to_datetime(t, utc)
self.date = t
+ parent_resource = self.resource.parent
+ if not resource_exists(self.env, parent_resource):
+ raise ResourceNotFound(
+ _("%(parent)s doesn't exist, can't create attachment",
+ parent=get_resource_name(self.env, parent_resource)))
+
# Make sure the path to the attachment is inside the environment
# attachments directory
attachments_dir = os.path.join(os.path.normpath(self.env.path),
@@ -341,7 +348,6 @@ class Attachment(object):
listener.attachment_added(self)
ResourceSystem(self.env).resource_created(self)
-
@classmethod
def select(cls, env, parent_realm, parent_id, db=None):
"""Iterator yielding all `Attachment` instances attached to
@@ -377,7 +383,8 @@ class Attachment(object):
os.rmdir(attachment_dir)
except OSError, e:
env.log.error("Can't delete attachment directory %s: %s",
- attachment_dir, exception_to_unicode(e, traceback=True))
+ attachment_dir,
+ exception_to_unicode(e, traceback=True))
@classmethod
def reparent_all(cls, env, parent_realm, parent_id, new_realm, new_id):
@@ -393,7 +400,8 @@ class Attachment(object):
os.rmdir(attachment_dir)
except OSError, e:
env.log.error("Can't delete attachment directory %s: %s",
- attachment_dir, exception_to_unicode(e, traceback=True))
+ attachment_dir,
+ exception_to_unicode(e, traceback=True))
def open(self):
path = self.path
@@ -436,8 +444,7 @@ class AttachmentModule(Component):
CHUNK_SIZE = 4096
max_size = IntOption('attachment', 'max_size', 262144,
- """Maximum allowed file size (in bytes) for ticket and wiki
- attachments.""")
+ """Maximum allowed file size (in bytes) for attachments.""")
max_zip_size = IntOption('attachment', 'max_zip_size', 2097152,
"""Maximum allowed total size (in bytes) for an attachment list to be
@@ -499,6 +506,10 @@ class AttachmentModule(Component):
parent_id, filename = path[:last_slash], path[last_slash + 1:]
parent = parent_realm(id=parent_id)
+ if not resource_exists(self.env, parent):
+ raise ResourceNotFound(
+ _("Parent resource %(parent)s doesn't exist",
+ parent=get_resource_name(self.env, parent)))
# Link the attachment page to parent resource
parent_name = get_resource_name(self.env, parent)
@@ -695,10 +706,6 @@ class AttachmentModule(Component):
def _do_save(self, req, attachment):
req.perm(attachment.resource).require('ATTACHMENT_CREATE')
parent_resource = attachment.resource.parent
- if not resource_exists(self.env, parent_resource):
- raise ResourceNotFound(
- _("%(parent)s doesn't exist, can't create attachment",
- parent=get_resource_name(self.env, parent_resource)))
if 'cancel' in req.args:
req.redirect(get_resource_url(self.env, parent_resource, req.href))
@@ -764,7 +771,7 @@ class AttachmentModule(Component):
try:
old_attachment = Attachment(self.env,
attachment.resource(id=filename))
- if not (req.authname and req.authname != 'anonymous' \
+ if not (req.authname and req.authname != 'anonymous'
and old_attachment.author == req.authname) \
and 'ATTACHMENT_DELETE' \
not in req.perm(attachment.resource):
@@ -774,7 +781,7 @@ class AttachmentModule(Component):
"attachments requires ATTACHMENT_DELETE permission.",
name=filename))
if (not attachment.description.strip() and
- old_attachment.description):
+ old_attachment.description):
attachment.description = old_attachment.description
old_attachment.delete()
except TracError:
@@ -806,7 +813,7 @@ class AttachmentModule(Component):
def _render_form(self, req, attachment):
req.perm(attachment.resource).require('ATTACHMENT_CREATE')
return {'mode': 'new', 'author': get_reporter_id(req),
- 'attachment': attachment, 'max_size': self.max_size}
+ 'attachment': attachment, 'max_size': self.max_size}
def _download_as_zip(self, req, parent, attachments=None):
if attachments is None:
@@ -823,19 +830,14 @@ class AttachmentModule(Component):
req.send_header('Content-Disposition',
content_disposition('inline', filename))
- from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED
+ from zipfile import ZipFile, ZIP_DEFLATED
buf = StringIO()
zipfile = ZipFile(buf, 'w', ZIP_DEFLATED)
for attachment in attachments:
- zipinfo = ZipInfo()
- zipinfo.filename = attachment.filename.encode('utf-8')
- zipinfo.flag_bits |= 0x800 # filename is encoded with utf-8
- zipinfo.date_time = attachment.date.utctimetuple()[:6]
- zipinfo.compress_type = ZIP_DEFLATED
- if attachment.description:
- zipinfo.comment = attachment.description.encode('utf-8')
- zipinfo.external_attr = 0644 << 16L # needed since Python 2.5
+ zipinfo = create_zipinfo(attachment.filename,
+ mtime=attachment.date,
+ comment=attachment.description)
try:
with attachment.open() as fd:
zipfile.writestr(zipinfo, fd.read())
@@ -999,7 +1001,7 @@ class LegacyAttachmentPolicy(Component):
else:
for d in self.delegates:
decision = d.check_attachment_permission(action, username,
- resource, perm)
+ resource, perm)
if decision is not None:
return decision
@@ -1113,4 +1115,3 @@ class AttachmentAdmin(Component):
finally:
if destination is not None:
output.close()
-
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/cache.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/cache.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/cache.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/cache.py Sat Nov 15 01:14:46 2014
@@ -13,6 +13,8 @@
from __future__ import with_statement
+import functools
+
from .core import Component
from .util import arity
from .util.concurrency import ThreadLocal, threading
@@ -34,12 +36,15 @@ def key_to_id(s):
return result
-class CachedPropertyBase(object):
- """Base class for cached property descriptors"""
+class CachedPropertyBase(property):
+ """Base class for cached property descriptors.
+
+ :since 1.0.2: inherits from `property`.
+ """
def __init__(self, retriever):
self.retriever = retriever
- self.__doc__ = retriever.__doc__
+ functools.update_wrapper(self, retriever)
def make_key(self, cls):
attr = self.retriever.__name__
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/config.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/config.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/config.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/config.py Sat Nov 15 01:14:46 2014
@@ -18,12 +18,13 @@ from ConfigParser import ConfigParser
from copy import deepcopy
import os.path
+from genshi.builder import tag
from trac.admin import AdminCommandError, IAdminCommandProvider
from trac.core import *
from trac.util import AtomicFile, as_bool
-from trac.util.compat import cleandoc
+from trac.util.compat import cleandoc, wait_for_file_mtime_change
from trac.util.text import printout, to_unicode, CRLF
-from trac.util.translation import _, N_
+from trac.util.translation import _, N_, tag_
__all__ = ['Configuration', 'ConfigSection', 'Option', 'BoolOption',
'IntOption', 'FloatOption', 'ListOption', 'ChoiceOption',
@@ -43,6 +44,12 @@ class ConfigurationError(TracError):
"""Exception raised when a value in the configuration file is not valid."""
title = N_('Configuration Error')
+ def __init__(self, message=None, title=None, show_traceback=False):
+ if message is None:
+ message = _("Look in the Trac log for more information.")
+ super(ConfigurationError, self).__init__(message, title,
+ show_traceback)
+
class Configuration(object):
"""Thin layer over `ConfigParser` from the Python standard library.
@@ -234,10 +241,12 @@ class Configuration(object):
# At this point, all the strings in `sections` are UTF-8 encoded `str`
try:
+ wait_for_file_mtime_change(self.filename)
with AtomicFile(self.filename, 'w') as fileobj:
fileobj.write('# -*- coding: utf-8 -*-\n\n')
- for section, options in sections:
- fileobj.write('[%s]\n' % section)
+ for section_str, options in sections:
+ fileobj.write('[%s]\n' % section_str)
+ section = to_unicode(section_str)
for key_str, val_str in options:
if to_unicode(key_str) in self[section].overridden:
fileobj.write('# %s = <inherited>\n' % key_str)
@@ -287,7 +296,8 @@ class Configuration(object):
def touch(self):
if self.filename and os.path.isfile(self.filename) \
- and os.access(self.filename, os.W_OK):
+ and os.access(self.filename, os.W_OK):
+ wait_for_file_mtime_change(self.filename)
os.utime(self.filename, None)
def set_defaults(self, compmgr=None):
@@ -296,14 +306,15 @@ class Configuration(object):
Values already set in the configuration are not overridden.
"""
- for section, default_options in self.defaults(compmgr).items():
- for name, value in default_options.items():
- if not self.parser.has_option(_to_utf8(section),
- _to_utf8(name)):
- if any(parent[section].contains(name, defaults=False)
- for parent in self.parents):
- value = None
- self.set(section, name, value)
+ for (section, name), option in Option.get_registry(compmgr).items():
+ if not self.parser.has_option(_to_utf8(section), _to_utf8(name)):
+ value = option.default
+ if any(parent[section].contains(name, defaults=False)
+ for parent in self.parents):
+ value = None
+ if value is not None:
+ value = option.dumps(value)
+ self.set(section, name, value)
class Section(object):
@@ -325,7 +336,7 @@ class Section(object):
for parent in self.config.parents:
if parent[self.name].contains(key, defaults=False):
return True
- return defaults and Option.registry.has_key((self.name, key))
+ return defaults and (self.name, key) in Option.registry
__contains__ = contains
@@ -608,27 +619,42 @@ class Option(object):
return value
def __set__(self, instance, value):
- raise AttributeError, 'can\'t set attribute'
+ raise AttributeError(_("Setting attribute is not allowed."))
def __repr__(self):
return '<%s [%s] "%s">' % (self.__class__.__name__, self.section,
self.name)
+ def dumps(self, value):
+ """Return the value as a string to write to a trac.ini file"""
+ if value is None:
+ return ''
+ if value is True:
+ return 'enabled'
+ if value is False:
+ return 'disabled'
+ if isinstance(value, unicode):
+ return value
+ return to_unicode(value)
+
class BoolOption(Option):
"""Descriptor for boolean configuration options."""
+
def accessor(self, section, name, default):
return section.getbool(name, default)
class IntOption(Option):
"""Descriptor for integer configuration options."""
+
def accessor(self, section, name, default):
return section.getint(name, default)
class FloatOption(Option):
"""Descriptor for float configuration options."""
+
def accessor(self, section, name, default):
return section.getfloat(name, default)
@@ -647,6 +673,11 @@ class ListOption(Option):
def accessor(self, section, name, default):
return section.getlist(name, default, self.sep, self.keep_empty)
+ def dumps(self, value):
+ if isinstance(value, (list, tuple)):
+ return self.sep.join(Option.dumps(self, v) or '' for v in value)
+ return Option.dumps(self, value)
+
class ChoiceOption(Option):
"""Descriptor for configuration options providing a choice among a list
@@ -678,11 +709,15 @@ class PathOption(Option):
Relative paths are resolved to absolute paths using the directory
containing the configuration file as the reference.
"""
+
def accessor(self, section, name, default):
return section.getpath(name, default)
class ExtensionOption(Option):
+ """Name of a component implementing `interface`. Raises a
+ `ConfigurationError` if the component cannot be found in the list of
+ active components implementing the interface."""
def __init__(self, section, name, interface, default=None, doc='',
doc_domain='tracini'):
@@ -696,11 +731,14 @@ class ExtensionOption(Option):
for impl in self.xtnpt.extensions(instance):
if impl.__class__.__name__ == value:
return impl
- raise AttributeError('Cannot find an implementation of the "%s" '
- 'interface named "%s". Please update the option '
- '%s.%s in trac.ini.'
- % (self.xtnpt.interface.__name__, value,
- self.section, self.name))
+ raise ConfigurationError(
+ tag_("Cannot find an implementation of the %(interface)s "
+ "interface named %(implementation)s. Please check "
+ "that the Component is enabled or update the option "
+ "%(option)s in trac.ini.",
+ interface=tag.tt(self.xtnpt.interface.__name__),
+ implementation=tag.tt(value),
+ option=tag.tt("[%s] %s" % (self.section, self.name))))
class OrderedExtensionsOption(ListOption):
@@ -722,9 +760,23 @@ class OrderedExtensionsOption(ListOption
return self
order = ListOption.__get__(self, instance, owner)
components = []
+ implementing_classes = []
for impl in self.xtnpt.extensions(instance):
+ implementing_classes.append(impl.__class__.__name__)
if self.include_missing or impl.__class__.__name__ in order:
components.append(impl)
+ not_found = sorted(set(order) - set(implementing_classes))
+ if not_found:
+ raise ConfigurationError(
+ tag_("Cannot find implementation(s) of the %(interface)s "
+ "interface named %(implementation)s. Please check "
+ "that the Component is enabled or update the option "
+ "%(option)s in trac.ini.",
+ interface=tag.tt(self.xtnpt.interface.__name__),
+ implementation=tag(
+ (', ' if idx != 0 else None, tag.tt(impl))
+ for idx, impl in enumerate(not_found)),
+ option=tag.tt("[%s] %s" % (self.section, self.name))))
def compare(x, y):
x, y = x.__class__.__name__, y.__class__.__name__
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/core.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/core.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/core.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/core.py Sat Nov 15 01:14:46 2014
@@ -141,6 +141,8 @@ class ComponentMeta(type):
return self
# The normal case where the component is not also the component manager
+ assert len(args) >= 1 and isinstance(args[0], ComponentManager), \
+ "First argument must be a ComponentManager instance"
compmgr = args[0]
self = compmgr.components.get(cls)
# Note that this check is racy, we intentionally don't use a
@@ -204,14 +206,14 @@ class ComponentManager(object):
"""Activate the component instance for the given class, or
return the existing instance if the component has already been
activated.
+
+ Note that `ComponentManager` components can't be activated
+ that way.
"""
if not self.is_enabled(cls):
return None
component = self.components.get(cls)
-
- # Leave other manager components out of extension point lists
- # see bh:comment:5:ticket:438 and ticket:11121
- if not component and not issubclass(cls, ComponentManager) :
+ if not component and not issubclass(cls, ComponentManager):
if cls not in ComponentMeta._components:
raise TracError('Component "%s" not registered' % cls.__name__)
try:
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/__init__.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/__init__.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/__init__.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/__init__.py Sat Nov 15 01:14:46 2014
@@ -1,2 +1,15 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2013 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+
from trac.db.api import *
from trac.db.schema import *
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/api.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/api.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/api.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/api.py Sat Nov 15 01:14:46 2014
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
-# Copyright (C)2005-2009 Edgewall Software
+# Copyright (C)2005-2014 Edgewall Software
# Copyright (C) 2005 Christopher Lenz <cm...@gmx.de>
# All rights reserved.
#
@@ -22,6 +22,7 @@ import urllib
from trac.config import BoolOption, IntOption, Option
from trac.core import *
+from trac.db.schema import Table
from trac.util.concurrency import ThreadLocal
from trac.util.text import unicode_passwd
from trac.util.translation import _
@@ -61,9 +62,9 @@ def with_transaction(env, db=None):
:deprecated: This decorator is in turn deprecated in favor of
context managers now that python 2.4 support has been
- dropped. Use instead the new context manager,
- `QueryContextManager` and
- `TransactionContextManager`, which makes for much
+ dropped. It will be removed in Trac 1.3.1. Use instead
+ the new context managers, `QueryContextManager` and
+ `TransactionContextManager`, which make for much
simpler to write code:
>>> def api_method(p1, p2):
@@ -250,10 +251,35 @@ class DatabaseManager(Component):
args['schema'] = schema
connector.init_db(**args)
+ def create_tables(self, schema):
+ """Create the specified tables.
+
+ :param schema: an iterable of table objects.
+
+ :since: version 1.0.2
+ """
+ connector = self.get_connector()[0]
+ with self.env.db_transaction as db:
+ for table in schema:
+ for sql in connector.to_sql(table):
+ db(sql)
+
+ def drop_tables(self, schema):
+ """Drop the specified tables.
+
+ :param schema: an iterable of `Table` objects or table names.
+
+ :since: version 1.0.2
+ """
+ with self.env.db_transaction as db:
+ for table in schema:
+ table_name = table.name if isinstance(table, Table) else table
+ db.drop_table(table_name)
+
def get_connection(self, readonly=False):
"""Get a database connection from the pool.
- If `readonly` is `True`, the returned connection will purposedly
+ If `readonly` is `True`, the returned connection will purposely
lack the `rollback` and `commit` methods.
"""
if not self._cnx_pool:
@@ -287,7 +313,7 @@ class DatabaseManager(Component):
backup_dir = os.path.join(self.env.path, backup_dir)
db_str = self.config.get('trac', 'database')
db_name, db_path = db_str.split(":", 1)
- dest_name = '%s.%i.%d.bak' % (db_name, self.env.get_version(),
+ dest_name = '%s.%i.%d.bak' % (db_name, self.env.database_version,
int(time.time()))
dest = os.path.join(backup_dir, dest_name)
else:
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/mysql_backend.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/mysql_backend.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/mysql_backend.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/mysql_backend.py Sat Nov 15 01:14:46 2014
@@ -14,14 +14,19 @@
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://trac.edgewall.org/log/.
-import os, re, types
+import os
+import re
+import sys
+import types
from genshi.core import Markup
from trac.core import *
from trac.config import Option
-from trac.db.api import IDatabaseConnector, _parse_db_str
+from trac.db.api import DatabaseManager, IDatabaseConnector, _parse_db_str, \
+ get_column_names
from trac.db.util import ConnectionWrapper, IterableCursor
+from trac.env import IEnvironmentSetupParticipant
from trac.util import as_int, get_pkginfo
from trac.util.compat import close_fds
from trac.util.text import exception_to_unicode, to_unicode
@@ -73,7 +78,7 @@ class MySQLConnector(Component):
* `read_default_group`: Configuration group to use from the default file
* `unix_socket`: Use a Unix socket at the given path to connect
"""
- implements(IDatabaseConnector)
+ implements(IDatabaseConnector, IEnvironmentSetupParticipant)
mysqldump_path = Option('trac', 'mysqldump_path', 'mysqldump',
"""Location of mysqldump for MySQL database backups""")
@@ -109,17 +114,29 @@ class MySQLConnector(Component):
host=None, port=None, params={}):
cnx = self.get_connection(path, log, user, password, host, port,
params)
+ self._verify_variables(cnx)
+ utf8_size = self._utf8_size(cnx)
cursor = cnx.cursor()
- utf8_size = {'utf8': 3, 'utf8mb4': 4}.get(cnx.charset)
if schema is None:
from trac.db_default import schema
for table in schema:
for stmt in self.to_sql(table, utf8_size=utf8_size):
self.log.debug(stmt)
cursor.execute(stmt)
+ self._verify_table_status(cnx)
cnx.commit()
- def _collist(self, table, columns, utf8_size=3):
+ def _utf8_size(self, cnx):
+ if cnx is None:
+ connector, args = DatabaseManager(self.env).get_connector()
+ cnx = connector.get_connection(**args)
+ charset = cnx.charset
+ cnx.close()
+ else:
+ charset = cnx.charset
+ return 4 if charset == 'utf8mb4' else 3
+
+ def _collist(self, table, columns, utf8_size):
"""Take a list of columns and impose limits on each so that indexing
works properly.
@@ -148,7 +165,9 @@ class MySQLConnector(Component):
cols.append(name)
return ','.join(cols)
- def to_sql(self, table, utf8_size=3):
+ def to_sql(self, table, utf8_size=None):
+ if utf8_size is None:
+ utf8_size = self._utf8_size(None)
sql = ['CREATE TABLE %s (' % table.name]
coldefs = []
for column in table.columns:
@@ -235,6 +254,83 @@ class MySQLConnector(Component):
raise TracError(_("No destination file created"))
return dest_file
+ # IEnvironmentSetupParticipant methods
+
+ def environment_created(self):
+ pass
+
+ def environment_needs_upgrade(self, db):
+ if getattr(self, 'required', False):
+ self._verify_table_status(db)
+ self._verify_variables(db)
+ return False
+
+ def upgrade_environment(self, db):
+ pass
+
+ UNSUPPORTED_ENGINES = ('MyISAM', 'EXAMPLE', 'ARCHIVE', 'CSV', 'ISAM')
+
+ def _verify_table_status(self, db):
+ from trac.db_default import schema
+ tables = [t.name for t in schema]
+ cursor = db.cursor()
+ cursor.execute("SHOW TABLE STATUS WHERE name IN (%s)" %
+ ','.join(('%s',) * len(tables)),
+ tables)
+ cols = get_column_names(cursor)
+ rows = [dict(zip(cols, row)) for row in cursor]
+
+ engines = [row['Name'] for row in rows
+ if row['Engine'] in self.UNSUPPORTED_ENGINES]
+ if engines:
+ raise TracError(_(
+ "All tables must be created as InnoDB or NDB storage engine "
+ "to support transactions. The following tables have been "
+ "created as storage engine which doesn't support "
+ "transactions: %(tables)s", tables=', '.join(engines)))
+
+ non_utf8bin = [row['Name'] for row in rows
+ if row['Collation'] not in ('utf8_bin', 'utf8mb4_bin',
+ None)]
+ if non_utf8bin:
+ raise TracError(_("All tables must be created with utf8_bin or "
+ "utf8mb4_bin as collation. The following tables "
+ "don't have the collations: %(tables)s",
+ tables=', '.join(non_utf8bin)))
+
+ SUPPORTED_COLLATIONS = (('utf8', 'utf8_bin'), ('utf8mb4', 'utf8mb4_bin'))
+
+ def _verify_variables(self, db):
+ cursor = db.cursor()
+ cursor.execute("SHOW VARIABLES WHERE variable_name IN ("
+ "'default_storage_engine','storage_engine',"
+ "'default_tmp_storage_engine',"
+ "'character_set_database','collation_database')")
+ vars = dict((row[0].lower(), row[1]) for row in cursor)
+
+ engine = vars.get('default_storage_engine') or \
+ vars.get('storage_engine')
+ if engine in self.UNSUPPORTED_ENGINES:
+ raise TracError(_("The current storage engine is %(engine)s. "
+ "It must be InnoDB or NDB storage engine to "
+ "support transactions.", engine=engine))
+
+ tmp_engine = vars.get('default_tmp_storage_engine')
+ if tmp_engine in self.UNSUPPORTED_ENGINES:
+ raise TracError(_("The current storage engine for TEMPORARY "
+ "tables is %(engine)s. It must be InnoDB or NDB "
+ "storage engine to support transactions.",
+ engine=tmp_engine))
+
+ charset = vars['character_set_database']
+ collation = vars['collation_database']
+ if (charset, collation) not in self.SUPPORTED_COLLATIONS:
+ raise TracError(_(
+ "The charset and collation of database are '%(charset)s' and "
+ "'%(collation)s'. The database must be created with one of "
+ "%(supported)s.", charset=charset, collation=collation,
+ supported=repr(self.SUPPORTED_COLLATIONS)))
+
class MySQLConnection(ConnectionWrapper):
"""Connection wrapper for MySQL."""
@@ -251,16 +347,21 @@ class MySQLConnection(ConnectionWrapper)
port = 3306
opts = {}
for name, value in params.iteritems():
- if name in ('init_command', 'read_default_file',
- 'read_default_group', 'unix_socket'):
- opts[name] = value
+ key = name.encode('utf-8')
+ if name == 'read_default_group':
+ opts[key] = value
+ elif name == 'init_command':
+ opts[key] = value.encode('utf-8')
+ elif name in ('read_default_file', 'unix_socket'):
+ opts[key] = value.encode(sys.getfilesystemencoding())
elif name in ('compress', 'named_pipe'):
- opts[name] = as_int(value, 0)
+ opts[key] = as_int(value, 0)
else:
self.log.warning("Invalid connection string parameter '%s'",
name)
cnx = MySQLdb.connect(db=path, user=user, passwd=password, host=host,
port=port, charset='utf8', **opts)
+ self.schema = path
if hasattr(cnx, 'encoders'):
# 'encoders' undocumented but present since 1.2.1 (r422)
cnx.encoders[Markup] = cnx.encoders[types.UnicodeType]
@@ -274,6 +375,24 @@ class MySQLConnection(ConnectionWrapper)
ConnectionWrapper.__init__(self, cnx, log)
self._is_closed = False
+ def cursor(self):
+ return IterableCursor(MySQLUnicodeCursor(self.cnx), self.log)
+
+ def rollback(self):
+ self.cnx.ping()
+ try:
+ self.cnx.rollback()
+ except MySQLdb.ProgrammingError:
+ self._is_closed = True
+
+ def close(self):
+ if not self._is_closed:
+ try:
+ self.cnx.close()
+ except MySQLdb.ProgrammingError:
+ pass # this error would mean it's already closed. So, ignore
+ self._is_closed = True
+
def cast(self, column, type):
if type == 'int' or type == 'int64':
type = 'signed'
@@ -284,6 +403,27 @@ class MySQLConnection(ConnectionWrapper)
def concat(self, *args):
return 'concat(%s)' % ', '.join(args)
+ def drop_table(self, table):
+ cursor = MySQLdb.cursors.Cursor(self.cnx)
+ cursor._defer_warnings = True # ignore "Warning: Unknown table ..."
+ cursor.execute("DROP TABLE IF EXISTS " + self.quote(table))
+
+ def get_column_names(self, table):
+ rows = self.execute("""
+ SELECT column_name FROM information_schema.columns
+ WHERE table_schema=%s AND table_name=%s
+ """, (self.schema, table))
+ return [row[0] for row in rows]
+
+ def get_last_id(self, cursor, table, column='id'):
+ return cursor.lastrowid
+
+ def get_table_names(self):
+ rows = self.execute("""
+ SELECT table_name FROM information_schema.tables
+ WHERE table_schema=%s""", (self.schema,))
+ return [row[0] for row in rows]
+
def like(self):
"""Return a case-insensitive LIKE clause."""
return "LIKE %%s COLLATE %s_general_ci ESCAPE '/'" % self.charset
@@ -291,31 +431,18 @@ class MySQLConnection(ConnectionWrapper)
def like_escape(self, text):
return _like_escape_re.sub(r'/\1', text)
+ def prefix_match(self):
+ """Return a case sensitive prefix-matching operator."""
+ return "LIKE %s ESCAPE '/'"
+
+ def prefix_match_value(self, prefix):
+ """Return a value for case sensitive prefix-matching operator."""
+ return self.like_escape(prefix) + '%'
+
def quote(self, identifier):
"""Return the quoted identifier."""
return "`%s`" % identifier.replace('`', '``')
- def get_last_id(self, cursor, table, column='id'):
- return cursor.lastrowid
-
def update_sequence(self, cursor, table, column='id'):
# MySQL handles sequence updates automagically
pass
-
- def rollback(self):
- self.cnx.ping()
- try:
- self.cnx.rollback()
- except MySQLdb.ProgrammingError:
- self._is_closed = True
-
- def close(self):
- if not self._is_closed:
- try:
- self.cnx.close()
- except MySQLdb.ProgrammingError:
- pass # this error would mean it's already closed. So, ignore
- self._is_closed = True
-
- def cursor(self):
- return IterableCursor(MySQLUnicodeCursor(self.cnx), self.log)
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/pool.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/pool.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/pool.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/pool.py Sat Nov 15 01:14:46 2014
@@ -26,7 +26,7 @@ from trac.util.text import exception_to_
from trac.util.translation import _
-class TimeoutError(Exception):
+class TimeoutError(TracError):
"""Exception raised by the connection pool when no connection has become
available after a given timeout."""
@@ -93,7 +93,7 @@ class ConnectionPoolBackend(object):
deferred = num == 1 and isinstance(cnx, tuple)
err = None
if deferred:
- # Potentially lenghty operations must be done without lock held
+ # Potentially lengthy operations must be done without lock held
op, cnx = cnx
try:
if op == 'ping':
@@ -214,4 +214,3 @@ class ConnectionPool(object):
def shutdown(self, tid=None):
_backend.shutdown(tid)
-
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/postgres_backend.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/postgres_backend.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/postgres_backend.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/postgres_backend.py Sat Nov 15 01:14:46 2014
@@ -22,7 +22,7 @@ from trac.core import *
from trac.config import Option
from trac.db.api import IDatabaseConnector, _parse_db_str
from trac.db.util import ConnectionWrapper, IterableCursor
-from trac.util import get_pkginfo
+from trac.util import get_pkginfo, lazy
from trac.util.compat import close_fds
from trac.util.text import empty, exception_to_unicode, to_unicode
from trac.util.translation import _
@@ -231,6 +231,9 @@ class PostgreSQLConnection(ConnectionWra
cnx.rollback()
ConnectionWrapper.__init__(self, cnx, log)
+ def cursor(self):
+ return IterableCursor(self.cnx.cursor(), self.log)
+
def cast(self, column, type):
# Temporary hack needed for the union of selects in the search module
return 'CAST(%s AS %s)' % (column, _type_map.get(type, type))
@@ -238,6 +241,37 @@ class PostgreSQLConnection(ConnectionWra
def concat(self, *args):
return '||'.join(args)
+ def drop_table(self, table):
+ if (self._version or '').startswith(('8.0.', '8.1.')):
+ cursor = self.cursor()
+ cursor.execute("""SELECT table_name FROM information_schema.tables
+ WHERE table_schema=current_schema()
+ AND table_name=%s""", (table,))
+ for row in cursor:
+ if row[0] == table:
+ self.execute("DROP TABLE " + self.quote(table))
+ break
+ else:
+ self.execute("DROP TABLE IF EXISTS " + self.quote(table))
+
+ def get_column_names(self, table):
+ rows = self.execute("""
+ SELECT column_name FROM information_schema.columns
+ WHERE table_schema=%s AND table_name=%s
+ """, (self.schema, table))
+ return [row[0] for row in rows]
+
+ def get_last_id(self, cursor, table, column='id'):
+ cursor.execute("SELECT CURRVAL(%s)",
+ (self.quote(self._sequence_name(table, column)),))
+ return cursor.fetchone()[0]
+
+ def get_table_names(self):
+ rows = self.execute("""
+ SELECT table_name FROM information_schema.tables
+ WHERE table_schema=%s""", (self.schema,))
+ return [row[0] for row in rows]
+
def like(self):
"""Return a case-insensitive LIKE clause."""
return "ILIKE %s ESCAPE '/'"
@@ -245,19 +279,31 @@ class PostgreSQLConnection(ConnectionWra
def like_escape(self, text):
return _like_escape_re.sub(r'/\1', text)
+ def prefix_match(self):
+ """Return a case sensitive prefix-matching operator."""
+ return "LIKE %s ESCAPE '/'"
+
+ def prefix_match_value(self, prefix):
+ """Return a value for case sensitive prefix-matching operator."""
+ return self.like_escape(prefix) + '%'
+
def quote(self, identifier):
"""Return the quoted identifier."""
return '"%s"' % identifier.replace('"', '""')
- def get_last_id(self, cursor, table, column='id'):
- cursor.execute("""SELECT CURRVAL('"%s_%s_seq"')""" % (table, column))
- return cursor.fetchone()[0]
-
def update_sequence(self, cursor, table, column='id'):
- cursor.execute("""
- SELECT setval('"%s_%s_seq"', (SELECT MAX(%s) FROM %s))
- """ % (table, column, column, table))
-
- def cursor(self):
- return IterableCursor(self.cnx.cursor(), self.log)
-
+ cursor.execute("SELECT SETVAL(%%s, (SELECT MAX(%s) FROM %s))"
+ % (self.quote(column), self.quote(table)),
+ (self.quote(self._sequence_name(table, column)),))
+
+ def _sequence_name(self, table, column):
+ return '%s_%s_seq' % (table, column)
+
+ @lazy
+ def _version(self):
+ cursor = self.cursor()
+ cursor.execute('SELECT version()')
+ for version, in cursor:
+ # retrieve "8.1.23" from "PostgreSQL 8.1.23 on ...."
+ if version.startswith('PostgreSQL '):
+ return version.split(' ', 2)[1]
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/sqlite_backend.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/sqlite_backend.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/sqlite_backend.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/sqlite_backend.py Sat Nov 15 01:14:46 2014
@@ -27,6 +27,8 @@ from trac.util.translation import _
_like_escape_re = re.compile(r'([/_%])')
+_glob_escape_re = re.compile(r'[*?\[]')
+
try:
import pysqlite2.dbapi2 as sqlite
have_pysqlite = 2
@@ -255,7 +257,8 @@ class SQLiteConnection(ConnectionWrapper
and sqlite.version_info >= (2, 5, 0)
def __init__(self, path, log=None, params={}):
- assert have_pysqlite > 0
+ if have_pysqlite == 0:
+ raise TracError(_("Cannot load Python bindings for SQLite"))
self.cnx = None
if path != ':memory:':
if not os.access(path, os.F_OK):
@@ -312,6 +315,24 @@ class SQLiteConnection(ConnectionWrapper
def concat(self, *args):
return '||'.join(args)
+ def drop_table(self, table):
+ cursor = self.cursor()
+ cursor.execute("DROP TABLE IF EXISTS " + self.quote(table))
+
+ def get_column_names(self, table):
+ cursor = self.cnx.cursor()
+ rows = cursor.execute("PRAGMA table_info(%s)"
+ % self.quote(table))
+ return [row[1] for row in rows]
+
+ def get_last_id(self, cursor, table, column='id'):
+ return cursor.lastrowid
+
+ def get_table_names(self):
+ rows = self.execute("""
+ SELECT name FROM sqlite_master WHERE type='table'""")
+ return [row[0] for row in rows]
+
def like(self):
"""Return a case-insensitive LIKE clause."""
if sqlite_version >= (3, 1, 0):
@@ -325,13 +346,18 @@ class SQLiteConnection(ConnectionWrapper
else:
return text
+ def prefix_match(self):
+ """Return a case sensitive prefix-matching operator."""
+ return 'GLOB %s'
+
+ def prefix_match_value(self, prefix):
+ """Return a value for case sensitive prefix-matching operator."""
+ return _glob_escape_re.sub(lambda m: '[%s]' % m.group(0), prefix) + '*'
+
def quote(self, identifier):
"""Return the quoted identifier."""
return "`%s`" % identifier.replace('`', '``')
- def get_last_id(self, cursor, table, column='id'):
- return cursor.lastrowid
-
def update_sequence(self, cursor, table, column='id'):
# SQLite handles sequence updates automagically
# http://www.sqlite.org/autoinc.html
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/__init__.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/__init__.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/__init__.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/__init__.py Sat Nov 15 01:14:46 2014
@@ -1,11 +1,23 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2014 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+
import unittest
from trac.db.tests import api, mysql_test, postgres_test, util
-
from trac.db.tests.functional import functionalSuite
-def suite():
+def suite():
suite = unittest.TestSuite()
suite.addTest(api.suite())
suite.addTest(mysql_test.suite())
@@ -15,4 +27,3 @@ def suite():
if __name__ == '__main__':
unittest.main(defaultTest='suite')
-
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/api.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/api.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/api.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/api.py Sat Nov 15 01:14:46 2014
@@ -1,12 +1,26 @@
# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2013 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
from __future__ import with_statement
import os
import unittest
+import trac.tests.compat
from trac.db.api import DatabaseManager, _parse_db_str, get_column_names, \
with_transaction
+from trac.db_default import schema as default_schema
+from trac.db.schema import Column, Table
from trac.test import EnvironmentStub, Mock
from trac.util.concurrency import ThreadLocal
@@ -28,7 +42,8 @@ class Error(Exception):
def make_env(get_cnx):
- return Mock(components={DatabaseManager:
+ from trac.core import ComponentManager
+ return Mock(ComponentManager, components={DatabaseManager:
Mock(get_connection=get_cnx,
_transaction_local=ThreadLocal(wdb=None, rdb=None))})
@@ -275,6 +290,9 @@ class StringsTestCase(unittest.TestCase)
def setUp(self):
self.env = EnvironmentStub()
+ def tearDown(self):
+ self.env.reset_db()
+
def test_insert_unicode(self):
self.env.db_transaction(
"INSERT INTO system (name,value) VALUES (%s,%s)",
@@ -306,48 +324,179 @@ class StringsTestCase(unittest.TestCase)
self.assertEqual(r'alpha\`\"\'\\beta``gamma""delta',
get_column_names(cursor)[0])
+ def test_quoted_id_with_percent(self):
+ db = self.env.get_read_db()
+ name = """%?`%s"%'%%"""
+
+ def test(db, logging=False):
+ cursor = db.cursor()
+ if logging:
+ cursor.log = self.env.log
+
+ cursor.execute('SELECT 1 AS ' + db.quote(name))
+ self.assertEqual(name, get_column_names(cursor)[0])
+ cursor.execute('SELECT %s AS ' + db.quote(name), (42,))
+ self.assertEqual(name, get_column_names(cursor)[0])
+ cursor.executemany("UPDATE system SET value=%s WHERE "
+ "1=(SELECT 0 AS " + db.quote(name) + ")",
+ [])
+ cursor.executemany("UPDATE system SET value=%s WHERE "
+ "1=(SELECT 0 AS " + db.quote(name) + ")",
+ [('42',), ('43',)])
+
+ test(db)
+ test(db, logging=True)
+
+ def test_prefix_match_case_sensitive(self):
+ @self.env.with_transaction()
+ def do_insert(db):
+ cursor = db.cursor()
+ cursor.executemany("INSERT INTO system (name,value) VALUES (%s,1)",
+ [('blahblah',), ('BlahBlah',), ('BLAHBLAH',),
+ (u'BlähBlah',), (u'BlahBläh',)])
+
+ db = self.env.get_read_db()
+ cursor = db.cursor()
+ cursor.execute("SELECT name FROM system WHERE name %s" %
+ db.prefix_match(),
+ (db.prefix_match_value('Blah'),))
+ names = sorted(name for name, in cursor)
+ self.assertEqual('BlahBlah', names[0])
+ self.assertEqual(u'BlahBläh', names[1])
+ self.assertEqual(2, len(names))
+
+ def test_prefix_match_metachars(self):
+ def do_query(prefix):
+ db = self.env.get_read_db()
+ cursor = db.cursor()
+ cursor.execute("SELECT name FROM system WHERE name %s "
+ "ORDER BY name" % db.prefix_match(),
+ (db.prefix_match_value(prefix),))
+ return [name for name, in cursor]
+
+ @self.env.with_transaction()
+ def do_insert(db):
+ values = ['foo*bar', 'foo*bar!', 'foo?bar', 'foo?bar!',
+ 'foo[bar', 'foo[bar!', 'foo]bar', 'foo]bar!',
+ 'foo%bar', 'foo%bar!', 'foo_bar', 'foo_bar!',
+ 'foo/bar', 'foo/bar!', 'fo*ob?ar[fo]ob%ar_fo/obar']
+ cursor = db.cursor()
+ cursor.executemany("INSERT INTO system (name,value) VALUES (%s,1)",
+ [(value,) for value in values])
+
+ self.assertEqual(['foo*bar', 'foo*bar!'], do_query('foo*'))
+ self.assertEqual(['foo?bar', 'foo?bar!'], do_query('foo?'))
+ self.assertEqual(['foo[bar', 'foo[bar!'], do_query('foo['))
+ self.assertEqual(['foo]bar', 'foo]bar!'], do_query('foo]'))
+ self.assertEqual(['foo%bar', 'foo%bar!'], do_query('foo%'))
+ self.assertEqual(['foo_bar', 'foo_bar!'], do_query('foo_'))
+ self.assertEqual(['foo/bar', 'foo/bar!'], do_query('foo/'))
+ self.assertEqual(['fo*ob?ar[fo]ob%ar_fo/obar'], do_query('fo*'))
+ self.assertEqual(['fo*ob?ar[fo]ob%ar_fo/obar'],
+ do_query('fo*ob?ar[fo]ob%ar_fo/obar'))
+
class ConnectionTestCase(unittest.TestCase):
def setUp(self):
self.env = EnvironmentStub()
+ self.schema = [
+ Table('HOURS', key='ID')[
+ Column('ID', auto_increment=True),
+ Column('AUTHOR')],
+ Table('blog', key='bid')[
+ Column('bid', auto_increment=True),
+ Column('author')
+ ]
+ ]
+ self.env.global_databasemanager.drop_tables(self.schema)
+ self.env.global_databasemanager.create_tables(self.schema)
def tearDown(self):
+ self.env.global_databasemanager.drop_tables(self.schema)
self.env.reset_db()
def test_get_last_id(self):
- id1 = id2 = None
q = "INSERT INTO report (author) VALUES ('anonymous')"
with self.env.db_transaction as db:
cursor = db.cursor()
cursor.execute(q)
# Row ID correct before...
id1 = db.get_last_id(cursor, 'report')
- self.assertNotEqual(0, id1)
db.commit()
cursor.execute(q)
# ... and after commit()
db.commit()
id2 = db.get_last_id(cursor, 'report')
- self.assertEqual(id1 + 1, id2)
- def test_update_sequence(self):
- self.env.db_transaction(
- "INSERT INTO report (id, author) VALUES (42, 'anonymous')")
+ self.assertNotEqual(0, id1)
+ self.assertEqual(id1 + 1, id2)
+
+ def test_update_sequence_default_column(self):
with self.env.db_transaction as db:
+ db("INSERT INTO report (id, author) VALUES (42, 'anonymous')")
cursor = db.cursor()
db.update_sequence(cursor, 'report', 'id')
+
self.env.db_transaction(
"INSERT INTO report (author) VALUES ('next-id')")
+
self.assertEqual(43, self.env.db_query(
"SELECT id FROM report WHERE author='next-id'")[0][0])
+ def test_update_sequence_nondefault_column(self):
+ with self.env.db_transaction as db:
+ cursor = db.cursor()
+ cursor.execute(
+ "INSERT INTO blog (bid, author) VALUES (42, 'anonymous')")
+ db.update_sequence(cursor, 'blog', 'bid')
+
+ self.env.db_transaction(
+ "INSERT INTO blog (author) VALUES ('next-id')")
+
+ self.assertEqual(43, self.env.db_query(
+ "SELECT bid FROM blog WHERE author='next-id'")[0][0])
+
+ def test_identifiers_need_quoting(self):
+ """Test for regression described in comment:4:ticket:11512."""
+ with self.env.db_transaction as db:
+ db("INSERT INTO %s (%s, %s) VALUES (42, 'anonymous')"
+ % (db.quote('HOURS'), db.quote('ID'), db.quote('AUTHOR')))
+ cursor = db.cursor()
+ db.update_sequence(cursor, 'HOURS', 'ID')
+
+ with self.env.db_transaction as db:
+ cursor = db.cursor()
+ cursor.execute(
+ "INSERT INTO %s (%s) VALUES ('next-id')"
+ % (db.quote('HOURS'), db.quote('AUTHOR')))
+ last_id = db.get_last_id(cursor, 'HOURS', 'ID')
+
+ self.assertEqual(43, last_id)
+
+ def test_table_names(self):
+ schema = default_schema + self.schema
+ with self.env.db_query as db:
+ db_tables = db.get_table_names()
+ self.assertEqual(len(schema), len(db_tables))
+ for table in schema:
+ self.assertIn(table.name, db_tables)
+
+ def test_get_column_names(self):
+ schema = default_schema + self.schema
+ with self.env.db_transaction as db:
+ for table in schema:
+ db_columns = db.get_column_names(table.name)
+ self.assertEqual(len(table.columns), len(db_columns))
+ for column in table.columns:
+ self.assertIn(column.name, db_columns)
+
def suite():
suite = unittest.TestSuite()
- suite.addTest(unittest.makeSuite(ParseConnectionStringTestCase, 'test'))
- suite.addTest(unittest.makeSuite(StringsTestCase, 'test'))
- suite.addTest(unittest.makeSuite(ConnectionTestCase, 'test'))
- suite.addTest(unittest.makeSuite(WithTransactionTest, 'test'))
+ suite.addTest(unittest.makeSuite(ParseConnectionStringTestCase))
+ suite.addTest(unittest.makeSuite(StringsTestCase))
+ suite.addTest(unittest.makeSuite(ConnectionTestCase))
+ suite.addTest(unittest.makeSuite(WithTransactionTest))
return suite
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/functional.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/functional.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/functional.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/functional.py Sat Nov 15 01:14:46 2014
@@ -1,4 +1,16 @@
-#!/usr/bin/python
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-2013 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
import os
from trac.tests.functional import *
@@ -18,12 +30,11 @@ class DatabaseBackupTestCase(FunctionalT
def functionalSuite(suite=None):
if not suite:
- import trac.tests.functional.testcases
- suite = trac.tests.functional.testcases.functionalSuite()
+ import trac.tests.functional
+ suite = trac.tests.functional.functionalSuite()
suite.addTest(DatabaseBackupTestCase())
return suite
if __name__ == '__main__':
unittest.main(defaultTest='functionalSuite')
-
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/mysql_test.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/mysql_test.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/mysql_test.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/mysql_test.py Sat Nov 15 01:14:46 2014
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
-# Copyright (C) 2009 Edgewall Software
+# Copyright (C) 2010-2013 Edgewall Software
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
@@ -13,8 +13,10 @@
import unittest
+import trac.tests.compat
from trac.db.mysql_backend import MySQLConnector
-from trac.test import EnvironmentStub
+from trac.db.schema import Table, Column, Index
+from trac.test import EnvironmentStub, Mock
class MySQLTableAlterationSQLTest(unittest.TestCase):
@@ -50,10 +52,31 @@ class MySQLTableAlterationSQLTest(unitte
{'due': ('int', 'int')})
self.assertEqual([], list(sql))
+ def test_utf8_size(self):
+ connector = MySQLConnector(self.env)
+ self.assertEqual(3, connector._utf8_size(Mock(charset='utf8')))
+ self.assertEqual(4, connector._utf8_size(Mock(charset='utf8mb4')))
+
+ def test_to_sql(self):
+ connector = MySQLConnector(self.env)
+ tab = Table('blah', key=('col1', 'col2'))[Column('col1'),
+ Column('col2'),
+ Index(['col2'])]
+
+ sql = list(connector.to_sql(tab, utf8_size=3))
+ self.assertEqual(2, len(sql))
+ self.assertIn(' PRIMARY KEY (`col1`(166),`col2`(166))', sql[0])
+ self.assertIn(' blah_col2_idx ON blah (`col2`(255))', sql[1])
+
+ sql = list(connector.to_sql(tab, utf8_size=4))
+ self.assertEqual(2, len(sql))
+ self.assertIn(' PRIMARY KEY (`col1`(125),`col2`(125))', sql[0])
+ self.assertIn(' blah_col2_idx ON blah (`col2`(191))', sql[1])
+
def suite():
suite = unittest.TestSuite()
- suite.addTest(unittest.makeSuite(MySQLTableAlterationSQLTest, 'test'))
+ suite.addTest(unittest.makeSuite(MySQLTableAlterationSQLTest))
return suite
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/postgres_test.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/postgres_test.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/postgres_test.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/postgres_test.py Sat Nov 15 01:14:46 2014
@@ -1,4 +1,15 @@
# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-2013 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
import re
import unittest
@@ -149,8 +160,8 @@ class PostgresTableAlterationSQLTest(uni
def suite():
suite = unittest.TestSuite()
- suite.addTest(unittest.makeSuite(PostgresTableCreationSQLTest, 'test'))
- suite.addTest(unittest.makeSuite(PostgresTableAlterationSQLTest, 'test'))
+ suite.addTest(unittest.makeSuite(PostgresTableCreationSQLTest))
+ suite.addTest(unittest.makeSuite(PostgresTableAlterationSQLTest))
return suite
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/util.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/util.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/util.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/tests/util.py Sat Nov 15 01:14:46 2014
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
-# Copyright (C) 2010 Edgewall Software
+# Copyright (C) 2010-2014 Edgewall Software
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
@@ -31,10 +31,41 @@ class SQLEscapeTestCase(unittest.TestCas
self.assertEqual("'%% %%'", sql_escape_percent("'% %'"))
self.assertEqual("'%%s %%i'", sql_escape_percent("'%s %i'"))
+ self.assertEqual("%", sql_escape_percent("%"))
+ self.assertEqual("`%%`", sql_escape_percent("`%`"))
+ self.assertEqual("``%``", sql_escape_percent("``%``"))
+ self.assertEqual("```%%```", sql_escape_percent("```%```"))
+ self.assertEqual("```%%`", sql_escape_percent("```%`"))
+ self.assertEqual("%s", sql_escape_percent("%s"))
+ self.assertEqual("% %", sql_escape_percent("% %"))
+ self.assertEqual("%s %i", sql_escape_percent("%s %i"))
+ self.assertEqual("`%%s`", sql_escape_percent("`%s`"))
+ self.assertEqual("`%% %%`", sql_escape_percent("`% %`"))
+ self.assertEqual("`%%s %%i`", sql_escape_percent("`%s %i`"))
+
+ self.assertEqual('%', sql_escape_percent('%'))
+ self.assertEqual('"%%"', sql_escape_percent('"%"'))
+ self.assertEqual('""%""', sql_escape_percent('""%""'))
+ self.assertEqual('"""%%"""', sql_escape_percent('"""%"""'))
+ self.assertEqual('"""%%"', sql_escape_percent('"""%"'))
+ self.assertEqual('%s', sql_escape_percent('%s'))
+ self.assertEqual('% %', sql_escape_percent('% %'))
+ self.assertEqual('%s %i', sql_escape_percent('%s %i'))
+ self.assertEqual('"%%s"', sql_escape_percent('"%s"'))
+ self.assertEqual('"%% %%"', sql_escape_percent('"% %"'))
+ self.assertEqual('"%%s %%i"', sql_escape_percent('"%s %i"'))
+
+ self.assertEqual("""'%%?''"%%s`%%i`%%%%"%%S'""",
+ sql_escape_percent("""'%?''"%s`%i`%%"%S'"""))
+ self.assertEqual("""`%%?``'%%s"%%i"%%%%'%%S`""",
+ sql_escape_percent("""`%?``'%s"%i"%%'%S`"""))
+ self.assertEqual('''"%%?""`%%s'%%i'%%%%`%%S"''',
+ sql_escape_percent('''"%?""`%s'%i'%%`%S"'''))
+
def suite():
suite = unittest.TestSuite()
- suite.addTest(unittest.makeSuite(SQLEscapeTestCase, 'test'))
+ suite.addTest(unittest.makeSuite(SQLEscapeTestCase))
return suite
if __name__ == '__main__':
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/db/util.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/db/util.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/db/util.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/db/util.py Sat Nov 15 01:14:46 2014
@@ -15,11 +15,18 @@
#
# Author: Christopher Lenz <cm...@gmx.de>
+import re
+
+_sql_escape_percent_re = re.compile("""
+ '(?:[^']+|'')*' |
+ `(?:[^`]+|``)*` |
+ "(?:[^"]+|"")*" """, re.VERBOSE)
+
def sql_escape_percent(sql):
- import re
- return re.sub("'((?:[^']|(?:''))*)'",
- lambda m: m.group(0).replace('%', '%%'), sql)
+ def repl(match):
+ return match.group(0).replace('%', '%%')
+ return _sql_escape_percent_re.sub(repl, sql)
class IterableCursor(object):
@@ -118,7 +125,7 @@ class ConnectionWrapper(object):
"""
dql = self.check_select(query)
cursor = self.cnx.cursor()
- cursor.execute(query, params)
+ cursor.execute(query, params if params is not None else [])
rows = cursor.fetchall() if dql else None
cursor.close()
return rows