Commits

mat...@13f79535-47bb-0310-9956-ffa450edef68  committed 7319734

sync merge from trunk

  • Participants
  • Parent commits 8ac17e5
  • Branches bep_0010_ticket_numbering

Comments (0)

Files changed (12)

File bloodhound_dashboard/bhdashboard/widgets/product.py

 from bhdashboard.util import WidgetBase, check_widget_name, pretty_wrapper
 
 from multiproduct.env import Product, ProductEnvironment
-from multiproduct.hooks import ProductizedHref
 
 
 __metaclass__ = type
 
+
 class ProductWidget(WidgetBase):
     """Display products available to the user.
     """
+
     def get_widget_params(self, name):
         """Return a dictionary containing arguments specification for
         the widget with specified name.
         """
-        return {'max' : {'desc' : """Limit the number of products displayed""",
-                         'type' : int},
-                'cols' : {'desc' : """Number of columns""",
-                          'type' : int}
-                }
+        return {
+            'max': {'desc': """Limit the number of products displayed""",
+                    'type': int},
+            'cols': {'desc': """Number of columns""",
+                     'type': int}
+        }
 
     get_widget_params = pretty_wrapper(get_widget_params, check_widget_name)
 
-    def _get_product_info(self, product, resource, max_):
+    COMMON_QUERY = 'order=priority&status=!closed&col=id&col=summary' \
+                   '&col=owner&col=type&col=status&col=priority&col=product'
+
+    def _get_product_info(self, product, href, resource, max_):
         penv = ProductEnvironment(self.env, product.prefix)
-        href = ProductizedHref(self.env, penv.href.base)
         results = []
 
         # some queries return a list/tuple, some a generator,
 
         query = resource['type'].select(penv)
         for q in itertools.islice(query, max_):
-            q.url = href(resource['name'], q.name) if resource.get('hrefurl') \
-                else Query.from_string(penv, 'order=priority&%s=%s' %
-                    (resource['name'], q.name)).get_href(href)
-            q.ticket_count = penv.db_query(
-                """SELECT COUNT(*) FROM ticket WHERE ticket.%s='%s'
-                   AND ticket.status <> 'closed'""" % (resource['name'], q.name))[0][0]
+            q.url = href(resource['name'], q.name) \
+                if resource.get('hrefurl') \
+                else Query.from_string(penv,
+                    '%s=%s&%s&col=%s' % (resource['name'], q.name,
+                                         self.COMMON_QUERY, resource['name'])
+            ).get_href(href)
+            q.ticket_count = penv.db_query("""
+                SELECT COUNT(*) FROM ticket WHERE ticket.%s='%s'
+                AND ticket.status <> 'closed'
+                """ % (resource['name'], q.name))[0][0]
 
             results.append(q)
 
             q = resource['type'](penv)
             q.name = '(No %s)' % (resource['name'],)
             q.url = Query.from_string(penv,
-                        'status=!closed&col=id&col=summary&col=owner'
-                        '&col=status&col=priority&order=priority&%s=' %
-                        (resource['name'],)).get_href(href)
+               'status=!closed&col=id&col=summary&col=owner'
+               '&col=status&col=priority&order=priority&%s='
+               % (resource['name'],)
+            ).get_href(href)
             q.ticket_count = ticket_count
             results.append(q)
 
             q.name = _('... more')
             q.ticket_count = None
             q.url = href(resource['name']) if resource.get('hrefurl') \
-                else href.product(product.prefix)
+                else href.dashboard()
             results.append(q)
 
         return results
         params = ('max', 'cols')
         max_, cols = self.bind_params(name, options, *params)
 
-        if not isinstance(req.perm.env, ProductEnvironment):
+        if not isinstance(self.env, ProductEnvironment):
             for p in Product.select(self.env):
                 if 'PRODUCT_VIEW' in req.perm(Neighborhood('product', p.prefix)):
+                    penv = ProductEnvironment(self.env, p.prefix)
+                    phref = ProductEnvironment.resolve_href(penv, self.env)
                     for resource in (
-                        { 'type': Milestone, 'name': 'milestone', 'hrefurl': True },
-                        { 'type': Component, 'name': 'component' },
-                        { 'type': Version, 'name': 'version' },
+                        {'type': Milestone, 'name': 'milestone', 'hrefurl': True},
+                        {'type': Component, 'name': 'component'},
+                        {'type': Version, 'name': 'version'},
                     ):
                         setattr(p, resource['name'] + 's',
-                            self._get_product_info(p, resource, max_))
-                    p.owner_link = Query.from_string(self.env, 'status!=closed&'
-                        'col=id&col=summary&col=owner&col=status&col=priority&'
-                        'order=priority&group=product&owner=%s'
-                        % (p._data['owner'] or '', )).get_href(req.href)
+                                self._get_product_info(p, phref, resource, max_))
+                    p.owner_link = Query.from_string(self.env,
+                        'status!=closed&col=id&col=summary&col=owner'
+                        '&col=status&col=priority&order=priority'
+                        '&group=product&owner=%s' % (p._data['owner'] or '', )
+                    ).get_href(phref)
+                    p.href = phref()
                     data.setdefault('product_list', []).append(p)
             title = _('Products')
 
         data['colseq'] = itertools.cycle(xrange(cols - 1, -1, -1)) if cols \
-                         else itertools.repeat(1)
+            else itertools.repeat(1)
 
-        return 'widget_product.html', \
-            {
-                'title': title,
-                'data': data,
-                'ctxtnav' : [
-                    tag.a(_('More'), 
-                    href = context.req.href('products'))],
-            }, \
-            context
+        return 'widget_product.html', {
+            'title': title,
+            'data': data,
+            'ctxtnav': [tag.a(_('More'), href=req.href('products'))],
+        }, context
 
     render_widget = pretty_wrapper(render_widget, check_widget_name)
-

File bloodhound_dashboard/bhdashboard/widgets/templates/widget_product.html

       <div class="span4">
         <div class="well product-well">
           <h4>
-            &#9734; <a href="${req.href.products(p.prefix)}">$p.name ($p.prefix)</a>
+            &#9734; <a href="${p.href}">$p.name ($p.prefix)</a>
             <py:if test="p.owner_link">
               <br />
               <small>owned by

File bloodhound_multiproduct/multiproduct/config.py

 from multiproduct.model import ProductSetting
 from multiproduct.perm import MultiproductPermissionPolicy
 
+
 class Configuration(Configuration):
     """Product-aware settings repository equivalent to instances of
     `trac.config.Configuration` (and thus `ConfigParser` from the
         for section, default_options in self.defaults(compmgr).items():
             for name, value in default_options.items():
                 if not ProductSetting.exists(self.env, self.product,
-                        section, name):
+                                             section, name):
                     if any(parent[section].contains(name, defaults=False)
                            for parent in self.parents):
                         value = None
             filename = Section._normalize_path(filename.strip(), self.env)
             self.parents.append(config.Configuration(filename))
 
+
 class Section(Section):
     """Proxy for a specific configuration section.
 
 
     @staticmethod
     def optionxform(optionstr):
-        return to_unicode(optionstr.lower());
+        return to_unicode(optionstr.lower())
 
     def __init__(self, config, name):
         self.config = config
         options = set()
         name_str = self.name
         for setting in ProductSetting.select(self.env,
-                where={'product':self.product, 'section':name_str}):
+                                             where={'product': self.product,
+                                                    'section': name_str}):
             option = self.optionxform(setting.option)
             options.add(option)
             yield option
     __iter__ = iterate
 
     def __repr__(self):
-        return '<%s [%s , %s]>' % (self.__class__.__name__, \
-                self.product, self.name)
+        return '<%s [%s , %s]>' % (self.__class__.__name__,
+                                   self.product, self.name)
 
     def get(self, key, default=''):
         """Return the value of the specified option.
             return cached
         name_str = self.name
         key_str = to_unicode(key)
-        settings = ProductSetting.select(self.env, 
-                where={'product':self.product, 'section':name_str,
-                        'option':key_str})
+        settings = ProductSetting.select(self.env,
+                                         where={'product': self.product,
+                                                'section': name_str,
+                                                'option': key_str})
         if len(settings) > 0:
             value = settings[0].value
         else:
         """
         key_str = self.optionxform(key)
         option_key = {
-                'product' : self.product, 
-                'section' : self.name,
-                'option' : key_str,
-            }
+            'product': self.product,
+            'section': self.name,
+            'option': key_str
+        }
         try:
             setting = ProductSetting(self.env, keys=option_key)
         except ResourceNotFound:
         value_str = to_unicode(value)
         self._cache.pop(key_str, None)
         option_key = {
-                'product' : self.product, 
-                'section' : self.name,
-                'option' : key_str,
-            }
+            'product': self.product,
+            'section': self.name,
+            'option': key_str,
+        }
         try:
             setting = ProductSetting(self.env, option_key)
         except ResourceNotFound:
             path = os.path.join(env.path, 'conf', path)
         return os.path.normcase(os.path.realpath(path))
 
+
 #--------------------
 # Option override classes
 #--------------------

File bloodhound_relations/bhrelations/api.py

             for listener in self.changing_listeners:
                 listener.adding_relation(relation)
 
-            from bhrelations.notification import RelationNotifyEmail
-            RelationNotifyEmail(self.env).notify(relation)
-
     def delete(self, relation_id, when=None):
         if when is None:
             when = datetime.now(utc)

File bloodhound_relations/bhrelations/tests/base.py

 from _sqlite3 import OperationalError
 from tests.env import MultiproductTestCase
 from multiproduct.env import ProductEnvironment
-from bhrelations.api import RelationsSystem, EnvironmentSetup
+from bhrelations.api import RelationsSystem, EnvironmentSetup, \
+    RELATIONS_CONFIG_NAME
 from trac.test import EnvironmentStub, Mock, MockPerm
 from trac.ticket import Ticket
 from trac.util.datefmt import utc
                        'BlockerValidator')
         env.config.set('bhrelations', 'duplicate_relation',
                        'duplicateof')
-        config_name = RelationsSystem.RELATIONS_CONFIG_NAME
+        config_name = RELATIONS_CONFIG_NAME
         env.config.set(config_name, 'dependency', 'dependson,dependent')
         env.config.set(config_name, 'dependency.validators',
                        'NoCycles,SingleProduct')
 
         self.req = Mock(href=self.env.href, authname='anonymous', tz=utc,
                         args=dict(action='dummy'),
-                        locale=locale_en, lc_time=locale_en)
+                        locale=locale_en, lc_time=locale_en,
+                        chrome={'warnings': []})
         self.req.perm = MockPerm()
         self.relations_system = RelationsSystem(self.env)
         self._upgrade_env()

File bloodhound_relations/bhrelations/tests/notification.py

         relation = self.relations_system.add(
             ticket, ticket2, "dependent")
 
-        rn = RelationNotifyEmail(self.env)
-        rn.notify(relation)
+        self.notifier.notify(relation)
 
         recipients = self.smtpd.get_recipients()
         # checks there is no duplicate in the recipient list
         ticket = self._insert_and_load_ticket('Foo', reporter='anonymous')
         ticket2 = self._insert_and_load_ticket('Bar', reporter='anonymous')
 
-        self.relations_system.add(ticket, ticket2, "dependent")
+        relation = self.relations_system.add(ticket, ticket2, "dependent")
+        self.notifier.notify(relation)
 
         sender = self.smtpd.get_sender()
         recipients = self.smtpd.get_recipients()
         ticket2 = self._insert_and_load_ticket('Bar', reporter='anonymous')
 
         relation = self.relations_system.add(ticket, ticket2, "dependent")
+        self.notifier.notify(relation)
 
         relations = self.env.db_direct_query(
             "SELECT * FROM bloodhound_relations")

File bloodhound_relations/bhrelations/tests/web_ui.py

 
         self.assertEqual(len(data["relations"]), 1)
 
+    def test_failure_to_notify_does_not_result_in_error(self):
+        t2 = self._insert_ticket(self.env, "Bar")
+        self.req.args['add'] = True
+        self.req.args['dest_tid'] = str(t2)
+        self.req.args['reltype'] = 'dependson'
+        rlm = RelationManagementModule(self.env)
+        rlm.notify_relation_changed = self._failing_notification
+
+        url, data, x = rlm.process_request(self.req)
+        self.assertEqual(len(self.req.chrome['warnings']), 1)
+
+    def _failing_notification(self, relation):
+        raise Exception()
+
     def process_request(self):
         url, data, x = RelationManagementModule(self.env).process_request(
             self.req)

File bloodhound_relations/bhrelations/web_ui.py

 from trac.core import Component, implements, TracError
 from trac.resource import get_resource_url, Resource
 from trac.ticket.model import Ticket
+from trac.util import exception_to_unicode, to_unicode
 from trac.util.translation import _
 from trac.web import IRequestHandler, IRequestFilter
 from trac.web.chrome import ITemplateProvider, add_warning
                     except ValidationError as ex:
                         data['error'] = ex.message
 
+                # Notify
+                try:
+                    self.notify_relation_changed(relation)
+                except Exception, e:
+                    self.log.error("Failure sending notification on"
+                                   "creation of relation: %s",
+                                   exception_to_unicode(e))
+                    add_warning(req, _("The relation has been added, but an "
+                                       "error occurred while sending"
+                                       "notifications: " "%(message)s",
+                                       message=to_unicode(e)))
+
                 if 'error' in data:
                     data['relation'] = relation
             else:
         })
         return 'relations_manage.html', data, None
 
+    def notify_relation_changed(self, relation):
+        from bhrelations.notification import RelationNotifyEmail
+        RelationNotifyEmail(self.env).notify(relation)
+
     # ITemplateProvider methods
     def get_htdocs_dirs(self):
         return []

File bloodhound_theme/bhtheme/htdocs/bloodhound.css

  page-break-after: avoid;
 }
 
+h1, h2, h3 {
+ min-height: 40px;
+}
+
 .clip, .affix .clip-affix {
  overflow: hidden;
  text-overflow: ellipsis;
  border: 1px solid #DDDDDD;
  border-radius: 3px 3px 3px 3px;
  box-shadow: 0 1px 0 #FFFFFF inset;
+ display: none; /* can be removed after upgrading to jQuery UI 1.9 (http://bugs.jqueryui.com/ticket/4111 */
  list-style: none outside none;
  margin: 0 0 18px;
  padding: 7px 14px;
+ z-index: 1001 !important;
 }
 
 .ui-datepicker a.ui-datepicker-prev,

File bloodhound_theme/bhtheme/templates/bh_list_of_attachments.html

   <py:if test="alist.attachments or alist.can_create">
     <div id="attachments" py:choose="">
       <py:when test="compact and alist.attachments">
-        <h3 class="${'foldable' if foldable else None}">Attachments</h3>
+        <h3 class="${'foldable' if foldable else None}">Attachments (${len(alist.attachments)})</h3>
         <div>
           <ul>
             <py:for each="attachment in alist.attachments">
         </div>
       </py:when>
       <py:when test="not compact">
-        <h3 id="comment:attachments">Attachments</h3>
+        <h3 id="comment:attachments">Attachments (${len(alist.attachments) if alist.attachments else 0})</h3>
         <div py:if="alist.attachments or alist.can_create" class="attachments">
           <dl py:if="alist.attachments" class="attachments">
             <py:for each="attachment in alist.attachments">

File bloodhound_theme/bhtheme/templates/bloodhound_theme.html

                 <select id="field-${field.name}" name="field_${field.name}"
                     class="input-block-level" data-empty="true" data-field="${field.name}">
                   <option py:if="field.optional"></option>
-                  <option py:for="option in field.options"
+                  <option py:for="idx,option in enumerate(field.options)"
+                          py:with="description = field.options_desc[idx] if field.options_desc else option"
                           selected="${field.value == option or None}"
                           value = "$option"
-                          py:content="option"></option>
+                          py:content="description"></option>
                   <optgroup py:for="optgroup in field.optgroups"
                             py:if="optgroup.options"
                             label="${optgroup.label}">

File bloodhound_theme/bhtheme/theme.py

 from trac.util.translation import _
 from trac.versioncontrol.web_ui.browser import BrowserModule
 from trac.web.api import IRequestFilter, IRequestHandler, ITemplateStreamFilter
-from trac.web.chrome import (add_stylesheet, INavigationContributor,
+from trac.web.chrome import (add_stylesheet, add_warning, INavigationContributor,
                              ITemplateProvider, prevnext_nav, Chrome)
 from trac.wiki.admin import WikiAdmin
 
                                          is_active)
 
         #add a creation event to the changelog if the ticket exists
-        if data['ticket'].exists:
+        ticket = data['ticket']
+        if ticket.exists:
             data['changes'] = [{'comment': '',
-                                'author': data['author_id'],
+                                'author': ticket['reporter'],
                                 'fields': {u'reported': {'label': u'Reported'},
                                            },
                                 'permanent': 1,
                                 'cnum': 0,
-                                'date': data['start_time'],
+                                'date': ticket['time'],
                                 },
                                ] + data['changes']
         #and set default order
                               for f in tm._prepare_fields(req, ticket)
                               if f['type'] == 'select')
 
-            product_field = all_fields['product']
+            product_field = all_fields.get('product')
             if product_field:
                 if self.env.product:
                     product_field['value'] = self.env.product.prefix
                         product_field['value'] = product_field['options'][0]
                     else:
                         product_field['value'] = default_prefix
-
+                product_field['options_desc'] = [
+                    ProductEnvironment.lookup_env(self.env, p).product.name
+                        for p in product_field['options']
+                ]
+            else:
+                msg = _("Missing ticket field '%s'.", 'product')
+                if ProductTicketModule is not None and \
+                        self.env[ProductTicketModule] is not None:
+                    # Display warning alert to users
+                    add_warning(req, msg)
+                else:
+                    # Include message in logs since this might be a failure
+                    self.log.warning(msg)
             data['qct'] = {
                 'fields': [all_fields[k] for k in self.qct_fields
                            if k in all_fields],
         PS: Borrowed from XmlRpcPlugin.
         """
         if 'product' in attributes:
+            env = self.env.parent or self.env
             if attributes['product']:
-                env = ProductEnvironment(self.env, attributes['product'])
-            else:
-                # Global product
-                env = self.env.parent or self.env
+                env = ProductEnvironment(env, attributes['product'])
         else:
             env = self.env