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 <[email protected]>
 # 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 <[email protected]>
 
+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


Reply via email to