Author: matevz
Date: Tue Mar 19 17:22:51 2013
New Revision: 1458416

URL: http://svn.apache.org/r1458416
Log:
#325 - Multiproduct UI: Dashboard (ProductQuery added for ticket lists)

Modified:
    
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/web_ui.py
    
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/query.py
    
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/templates/widget_grid.html
    
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/ticket.py
    
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/ticket/query.py
    
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/theme.py

Modified: 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/web_ui.py
URL: 
http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/web_ui.py?rev=1458416&r1=1458415&r2=1458416&view=diff
==============================================================================
--- 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/web_ui.py
 (original)
+++ 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/web_ui.py
 Tue Mar 19 17:22:51 2013
@@ -28,7 +28,7 @@ __metaclass__ = type
 
 from itertools import izip
 import pkg_resources
-import re
+import re, copy
 from uuid import uuid4
 
 from genshi.builder import tag
@@ -104,7 +104,10 @@ class DashboardModule(Component):
             add_ctxtnav(req, _('Reports'), req.href.report())
         context = Context.from_request(req)
         template, layout_data = self.expand_layout_data(context, 
-            'bootstrap_grid', self.DASHBOARD_SCHEMA)
+            'bootstrap_grid',
+            self.DASHBOARD_SCHEMA if isinstance(self.env, ProductEnvironment)
+                else self.DASHBOARD_GLOBAL_SCHEMA
+            )
         widgets = self.expand_widget_data(context, layout_data) 
         return template, {
                     'context' : Context.from_request(req),
@@ -230,6 +233,17 @@ class DashboardModule(Component):
                }
         }
 
+    # global dashboard: add milestone column, group by product
+    DASHBOARD_GLOBAL_SCHEMA = copy.deepcopy(DASHBOARD_SCHEMA)
+    DASHBOARD_GLOBAL_SCHEMA['widgets']['active 
tickets']['args'][2]['args']['query'] = (
+        'status=!closed&group=product&col=id&col=summary&col=owner&col=status&'
+        'col=priority&order=priority&col=milestone'
+    )
+    DASHBOARD_GLOBAL_SCHEMA['widgets']['my 
tickets']['args'][2]['args']['query'] = (
+        'status=!closed&group=product&col=id&col=summary&col=owner&col=status&'
+        'col=priority&order=priority&col=milestone&owner=$USER&'
+    )
+
     # Public API
     def expand_layout_data(self, context, layout_name, schema, embed=False):
         """Determine the template needed to render a specific layout

Modified: 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/query.py
URL: 
http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/query.py?rev=1458416&r1=1458415&r2=1458416&view=diff
==============================================================================
--- 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/query.py
 (original)
+++ 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/query.py
 Tue Mar 19 17:22:51 2013
@@ -41,6 +41,9 @@ from bhdashboard.util import WidgetBase,
                               merge_links, pretty_wrapper, trac_version, \
                               trac_tags
 
+from multiproduct.env import ProductEnvironment
+from multiproduct.ticket.query import ProductQueryModule
+
 class TicketQueryWidget(WidgetBase):
     """Display tickets matching a TracQuery using a grid
     """
@@ -91,7 +94,9 @@ class TicketQueryWidget(WidgetBase):
             more_link_href = req.href('query', args)
             args.update({'page' : page, 'max': maxrows})
 
-            qrymdl = self.env[QueryModule]
+            qrymdl = self.env[QueryModule
+                if isinstance(self.env, ProductEnvironment)
+                else ProductQueryModule]
             if qrymdl is None :
                 raise TracError('Query module not available (disabled?)')
             data = qrymdl.process_request(fakereq)[1]
@@ -121,7 +126,8 @@ class TicketQueryWidget(WidgetBase):
                                         } \
                                     for hidx, h in enumerate(headers)]],
                                 'id' : t['id'],
-                                'resource' : Resource('ticket', t['id'])
+                                'resource' : Resource('ticket', t['id']),
+                                'href': t['href']
                             } for t in tickets]) \
                                 for group_value, tickets in data['groups'] ]))
             return 'widget_grid.html', \

Modified: 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/templates/widget_grid.html
URL: 
http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/templates/widget_grid.html?rev=1458416&r1=1458415&r2=1458416&view=diff
==============================================================================
--- 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/templates/widget_grid.html
 (original)
+++ 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/templates/widget_grid.html
 Tue Mar 19 17:22:51 2013
@@ -92,13 +92,13 @@
                       <!--! for the ticket listing -->
                       <py:when test="col in ('ticket', 'id')">
                         <td class="ticket" py:attrs="td_attrs">
-                          <a title="View ${row.resource.realm}" 
href="${url_of(row.resource)}">#$cell.value</a>
+                          <a title="View ${row.resource.realm}" 
href="$row.href">#$cell.value</a>
                         </td>
                       </py:when>
   
                       <py:when test="col == 'summary' and row.id">
                         <td class="$col" py:attrs="td_attrs">
-                          <a title="View ${row.resource.realm}" 
href="${url_of(row.resource)}">$cell.value</a>
+                          <a title="View ${row.resource.realm}" 
href="${row.href if row.href else url_of(row.resource)}">$cell.value</a>
                         </td>
                       </py:when>
   

Modified: 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/ticket.py
URL: 
http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/ticket.py?rev=1458416&r1=1458415&r2=1458416&view=diff
==============================================================================
--- 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/ticket.py
 (original)
+++ 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/ticket.py
 Tue Mar 19 17:22:51 2013
@@ -45,6 +45,8 @@ from bhdashboard.util import WidgetBase,
                             pretty_wrapper, resolve_ep_class, \
                             trac_version, trac_tags
 
+from multiproduct.env import Product, ProductEnvironment
+
 class TicketFieldValuesWidget(WidgetBase):
     """Display a tag cloud representing frequency of values assigned to 
     ticket fields.
@@ -179,13 +181,14 @@ class TicketFieldValuesWidget(WidgetBase
                         "GROUP BY COALESCE(%(name)s, '')"
             sql = sql % field
             # TODO : Implement threshold and max
-            db = self.env.get_db_cnx()
-            try :
+
+            db_query = req.perm.env.db_query \
+                if isinstance(req.perm.env, ProductEnvironment) \
+                else req.perm.env.db_direct_query
+            with db_query as db:
                 cursor = db.cursor()
                 cursor.execute(sql)
                 items = cursor.fetchall()
-            finally:
-                cursor.close()
 
             QUERY_COLS = ['id', 'summary', 'owner', 'type', 'status', 
'priority']
             item_link= lambda item: req.href.query(col=QUERY_COLS + [fieldnm], 

Modified: 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/ticket/query.py
URL: 
http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/ticket/query.py?rev=1458416&r1=1458415&r2=1458416&view=diff
==============================================================================
--- 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/ticket/query.py
 (original)
+++ 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/ticket/query.py
 Tue Mar 19 17:22:51 2013
@@ -22,19 +22,27 @@ from __future__ import with_statement
 
 from itertools import groupby
 from math import ceil
+from datetime import datetime, timedelta
+import re
 
 from genshi.builder import tag
 
 from trac.db import get_column_names
-from trac.ticket.query import Query, TicketQueryMacro
+from trac.mimeview.api import Mimeview
+from trac.ticket.api import TicketSystem
+from trac.ticket.query import Query, QueryModule, TicketQueryMacro
 from trac.util import Ranges, as_bool
-from trac.util.datefmt import from_utimestamp
+from trac.util.datefmt import from_utimestamp, utc, to_timestamp
 from trac.util.text import shorten_line
 from trac.util.translation import _, tag_
-from trac.web.chrome import Chrome, add_stylesheet
+from trac.web.chrome import Chrome, add_stylesheet, add_link, web_context, \
+    add_script_data, add_script
 
 from multiproduct.dbcursor import GLOBAL_PRODUCT
-from multiproduct.env import lookup_product_env, resolve_product_href
+from multiproduct.env import lookup_product_env, resolve_product_href, \
+    Product, ProductEnvironment
+from multiproduct.hooks import ProductizedHref
+
 
 class ProductQuery(Query):
     """Product Overrides for TracQuery.
@@ -52,12 +60,17 @@ class ProductQuery(Query):
 
     def get_columns(self):
         super(ProductQuery, self).get_columns()
-        if not 'product' in self.cols:
+        if not 'product' in self.cols and self.group != 'product':
             # make sure 'product' is always present 
             # (needed for product context, href, permission checks ...)
+            # but don't implicitly include it if items are grouped by product
             self.cols.insert(0, 'product')
         return self.cols
 
+    def _get_ticket_href(self, prefix, tid):
+        product_env = ProductEnvironment(self.env, prefix)
+        return product_env.href.ticket(tid)
+
     def execute(self, req=None, db=None, cached_ids=None, authname=None,
                 tzinfo=None, href=None, locale=None):
         """Retrieve the list of matching tickets.
@@ -65,13 +78,13 @@ class ProductQuery(Query):
         :since 1.0: the `db` parameter is no longer needed and will be removed
         in version 1.1.1
         """
-        if req is not None:
-            href = req.href
         with self.env.db_direct_query as db:
             cursor = db.cursor()
 
             self.num_items = 0
             sql, args = self.get_sql(req, cached_ids, authname, tzinfo, locale)
+            if sql.startswith('SELECT ') and not sql.startswith('SELECT 
DISTINCT '):
+                sql = 'SELECT DISTINCT * FROM (' + sql + ')'
             self.num_items = self._count(sql, args)
 
             if self.num_items <= self.max:
@@ -96,6 +109,7 @@ class ProductQuery(Query):
                           [None]
             results = []
 
+            product_idx = columns.index('product')
             column_indices = range(len(columns))
             for row in cursor:
                 result = {}
@@ -105,8 +119,8 @@ class ProductQuery(Query):
                         val = val or 'anonymous'
                     elif name == 'id':
                         val = int(val)
-                        if href is not None:
-                            result['href'] = href.ticket(val)
+                        result['href'] = self._get_ticket_href(
+                            row[product_idx], val)
                     elif name in self.time_fields:
                         val = from_utimestamp(val)
                     elif field and field['type'] == 'checkbox':
@@ -121,6 +135,181 @@ class ProductQuery(Query):
             cursor.close()
             return results
 
+
+class ProductQueryModule(QueryModule):
+    def process_request(self, req):
+        req.perm.assert_permission('TICKET_VIEW')
+
+        constraints = self._get_constraints(req)
+        args = req.args
+        if not constraints and not 'order' in req.args:
+            # If no constraints are given in the URL, use the default ones.
+            if req.authname and req.authname != 'anonymous':
+                qstring = self.default_query
+                user = req.authname
+            else:
+                email = req.session.get('email')
+                name = req.session.get('name')
+                qstring = self.default_anonymous_query
+                user = email or name or None
+
+            self.log.debug('QueryModule: Using default query: %s', 
str(qstring))
+            if qstring.startswith('?'):
+                arg_list = parse_arg_list(qstring[1:])
+                args = arg_list_to_args(arg_list)
+                constraints = self._get_constraints(arg_list=arg_list)
+            else:
+                query = ProductQuery.from_string(self.env, qstring)
+                args = {'order': query.order, 'group': query.group,
+                        'col': query.cols, 'max': query.max}
+                if query.desc:
+                    args['desc'] = '1'
+                if query.groupdesc:
+                    args['groupdesc'] = '1'
+                constraints = query.constraints
+
+            # Substitute $USER, or ensure no field constraints that depend
+            # on $USER are used if we have no username.
+            for clause in constraints:
+                for field, vals in clause.items():
+                    for (i, val) in enumerate(vals):
+                        if user:
+                            vals[i] = val.replace('$USER', user)
+                        elif val.endswith('$USER'):
+                            del clause[field]
+                            break
+
+        cols = args.get('col')
+        if isinstance(cols, basestring):
+            cols = [cols]
+        # Since we don't show 'id' as an option to the user,
+        # we need to re-insert it here.
+        if cols and 'id' not in cols:
+            cols.insert(0, 'id')
+        rows = args.get('row', [])
+        if isinstance(rows, basestring):
+            rows = [rows]
+        format = req.args.get('format')
+        max = args.get('max')
+        if max is None and format in ('csv', 'tab'):
+            max = 0 # unlimited unless specified explicitly
+        query = ProductQuery(self.env, req.args.get('report'),
+                      constraints, cols, args.get('order'),
+                      'desc' in args, args.get('group'),
+                      'groupdesc' in args, 'verbose' in args,
+                      rows,
+                      args.get('page'),
+                      max)
+
+        if 'update' in req.args:
+            # Reset session vars
+            for var in ('query_constraints', 'query_time', 'query_tickets'):
+                if var in req.session:
+                    del req.session[var]
+            req.redirect(query.get_href(req.href))
+
+        # Add registered converters
+        for conversion in Mimeview(self.env).get_supported_conversions(
+                                             'trac.ticket.Query'):
+            add_link(req, 'alternate',
+                     query.get_href(req.href, format=conversion[0]),
+                     conversion[1], conversion[4], conversion[0])
+
+        if format:
+            filename = 'query' if format != 'rss' else None
+            Mimeview(self.env).send_converted(req, 'trac.ticket.Query', query,
+                                              format, filename=filename)
+
+        return self.display_html(req, query)
+
+    def display_html(self, req, query):
+        # The most recent query is stored in the user session;
+        orig_list = None
+        orig_time = datetime.now(utc)
+        query_time = int(req.session.get('query_time', 0))
+        query_time = datetime.fromtimestamp(query_time, utc)
+        query_constraints = unicode(query.constraints)
+        try:
+            if query_constraints != req.session.get('query_constraints') \
+                    or query_time < orig_time - timedelta(hours=1):
+                tickets = query.execute(req)
+                # New or outdated query, (re-)initialize session vars
+                req.session['query_constraints'] = query_constraints
+                req.session['query_tickets'] = ' '.join([str(t['id'])
+                                                         for t in tickets])
+            else:
+                orig_list = [int(id) for id
+                             in req.session.get('query_tickets', '').split()]
+                tickets = query.execute(req, cached_ids=orig_list)
+                orig_time = query_time
+        except QueryValueError, e:
+            tickets = []
+            for error in e.errors:
+                add_warning(req, error)
+
+        context = web_context(req, 'query')
+        owner_field = [f for f in query.fields if f['name'] == 'owner']
+        if owner_field:
+            TicketSystem(self.env).eventually_restrict_owner(owner_field[0])
+        data = query.template_data(context, tickets, orig_list, orig_time, req)
+
+        req.session['query_href'] = query.get_href(context.href)
+        req.session['query_time'] = to_timestamp(orig_time)
+        req.session['query_tickets'] = ' '.join([str(t['id'])
+                                                 for t in tickets])
+        title = _('Custom Query')
+
+        # Only interact with the report module if it is actually enabled.
+        #
+        # Note that with saved custom queries, there will be some convergence
+        # between the report module and the query module.
+        from trac.ticket.report import ReportModule
+        if 'REPORT_VIEW' in req.perm and \
+               self.env.is_component_enabled(ReportModule):
+            data['report_href'] = req.href.report()
+            add_ctxtnav(req, _('Available Reports'), req.href.report())
+            add_ctxtnav(req, _('Custom Query'), req.href.query())
+            if query.id:
+                for title, description in self.env.db_query("""
+                        SELECT title, description FROM report WHERE id=%s
+                        """, (query.id,)):
+                    data['report_resource'] = Resource('report', query.id)
+                    data['description'] = description
+        else:
+            data['report_href'] = None
+
+        # Only interact with the batch modify module it it is enabled
+        # TODO: fix this for multiproduct
+        """
+        from trac.ticket.batch import BatchModifyModule
+        if 'TICKET_BATCH_MODIFY' in req.perm and \
+                self.env.is_component_enabled(BatchModifyModule):
+            self.env[BatchModifyModule].add_template_data(req, data, tickets)
+        """
+
+        data.setdefault('report', None)
+        data.setdefault('description', None)
+        data['title'] = title
+
+        data['all_columns'] = query.get_all_columns()
+        # Don't allow the user to remove the id column
+        data['all_columns'].remove('id')
+        data['all_textareas'] = query.get_all_textareas()
+
+        properties = dict((name, dict((key, field[key])
+                                      for key in ('type', 'label', 'options',
+                                                  'optgroups')
+                                      if key in field))
+                          for name, field in data['fields'].iteritems())
+        add_script_data(req, properties=properties, modes=data['modes'])
+
+        add_stylesheet(req, 'common/css/report.css')
+        Chrome(self.env).add_jquery_ui(req)
+        add_script(req, 'common/js/query.js')
+
+        return 'query.html', data, None
+
+
 class ProductTicketQueryMacro(TicketQueryMacro):
     """TracQuery macro retrieving results across product boundaries. 
     """

Modified: 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/theme.py
URL: 
http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/theme.py?rev=1458416&r1=1458415&r2=1458416&view=diff
==============================================================================
--- 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/theme.py
 (original)
+++ 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/theme.py
 Tue Mar 19 17:22:51 2013
@@ -355,7 +355,6 @@ class BloodhoundTheme(ThemeBase):
         if href_fcn is None:
             href_fcn = req.href.products
         product_list = []
-        is_product_scope = isinstance(req.perm.env, ProductEnvironment)
         for product in Product.select(self.env):
             if 'PRODUCT_VIEW' in req.product_perm(product.prefix, 
product.resource):
                 product_list.append((product.prefix, product.name,


Reply via email to