incubator-bloodhound-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From g..@apache.org
Subject svn commit: r1383477 - in /incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard: tests/test_webui.py web_ui.py widgets/containers.py widgets/templates/widget_timeline.html widgets/timeline.py
Date Tue, 11 Sep 2012 16:54:47 GMT
Author: gjm
Date: Tue Sep 11 16:54:46 2012
New Revision: 1383477

URL: http://svn.apache.org/viewvc?rev=1383477&view=rev
Log:
timeline filters API for dashboard - towards #94

Modified:
    incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/tests/test_webui.py
    incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/web_ui.py
    incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/containers.py
    incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/templates/widget_timeline.html
    incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/timeline.py

Modified: incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/tests/test_webui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/tests/test_webui.py?rev=1383477&r1=1383476&r2=1383477&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/tests/test_webui.py (original)
+++ incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/tests/test_webui.py Tue Sep
11 16:54:46 2012
@@ -118,7 +118,12 @@ __test__ = {
       """,
     'Rendering templates' : r"""
       >>> dbm = DashboardModule(env)
-      >>> pprint(dbm.expand_widget_data(auth_req))
+      >>> from trac.mimeview.api import Context
+      >>> context = Context.from_request(auth_req)
+      
+      #FIXME: This won't work. Missing schema
+
+      >>> pprint(dbm.expand_widget_data(context))
       [{'content': <genshi.core.Stream object at ...>, 
       'title': <Element "a">}]
       """,

Modified: incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/web_ui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/web_ui.py?rev=1383477&r1=1383476&r2=1383477&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/web_ui.py (original)
+++ incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/web_ui.py Tue Sep 11 16:54:46
2012
@@ -96,9 +96,10 @@ class DashboardModule(Component):
             add_ctxtnav(req, _('Custom Query'), req.href.query())
         if self.env[ReportModule] is not None:
             add_ctxtnav(req, _('Reports'), req.href.report())
-        template, layout_data = self.expand_layout_data(req, 
+        context = Context.from_request(req)
+        template, layout_data = self.expand_layout_data(context, 
             'bootstrap_grid', self.DASHBOARD_SCHEMA)
-        widgets = self.expand_widget_data(req, layout_data) 
+        widgets = self.expand_widget_data(context, layout_data) 
         return template, {
                     'context' : Context.from_request(req),
                     'layout' : layout_data,
@@ -221,15 +222,14 @@ class DashboardModule(Component):
         }
 
     # Public API
-    def expand_layout_data(self, req, layout_name, schema, embed=False):
+    def expand_layout_data(self, context, layout_name, schema, embed=False):
         """Determine the template needed to render a specific layout
         and the data needed to place the widgets at expected
         location.
         """
         layout = DashboardSystem(self.env).resolve_layout(layout_name)
 
-        ctx = Context.from_request(req)
-        template = layout.expand_layout(layout_name, ctx, {
+        template = layout.expand_layout(layout_name, context, {
                 'schema' : schema,
                 'embed' : embed
             })['template']
@@ -262,7 +262,7 @@ class DashboardModule(Component):
                     { 'title' : _('Widget error'), 'data' : data}, \
                     ctx
 
-    def expand_widget_data(self, req, schema):
+    def expand_widget_data(self, context, schema):
         """Expand raw widget data and format it for use in template
         """
         # TODO: Implement dynamic dashboard specification
@@ -272,10 +272,9 @@ class DashboardModule(Component):
                 for wnm in wp.get_widgets()
             )
         self.log.debug("Bloodhound: Widget index %s" % (widgets_index,))
-        ctx = Context.from_request(req)
         for w in widgets_spec.itervalues():
             w['c'] = widgets_index.get(w['args'][0])
-            w['args'][1] = ctx
+            w['args'][1] = context
         self.log.debug("Bloodhound: Widget specs %s" % (widgets_spec,))
         chrome = Chrome(self.env)
         render = chrome.render_template
@@ -323,8 +322,8 @@ class DashboardChrome:
             widgets = {}
         schema['widgets'] = widgets
         template, layout_data = dbmod.expand_layout_data(
-                context.req, layout, schema, True)
-        widgets = dbmod.expand_widget_data(context.req, layout_data)
+                context, layout, schema, True)
+        widgets = dbmod.expand_widget_data(context, layout_data)
         return Chrome(self.env).render_template(context.req, template, 
                 dict(context=context, layout=layout_data, 
                         widgets=widgets, title='',
@@ -345,7 +344,7 @@ class DashboardChrome:
         elif isinstance(argsdef, Stream):
             options['args'] = parse_args_tag(argsdef)
         return dbmod.expand_widget_data(
-                    context.req,
+                    context,
                     {'widgets' : { 0 : widget }}
                 )[0]
 

Modified: incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/containers.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/containers.py?rev=1383477&r1=1383476&r2=1383477&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/containers.py (original)
+++ incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/containers.py Tue
Sep 11 16:54:46 2012
@@ -64,7 +64,6 @@ class ContainerWidget(WidgetBase):
         """Count ocurrences of values assigned to given ticket field.
         """
         dbsys = DashboardSystem(self.env)
-        req = context.req
         params = ('layout', 'schema', 'show_captions', 'title')
         layout, schema, show_captions, title = \
                 self.bind_params(name, options, *params)
@@ -72,7 +71,7 @@ class ContainerWidget(WidgetBase):
         dbmod = DashboardModule(self.env)
         layout_data = lp.expand_layout(layout, context, 
                 { 'schema' : schema, 'embed' : True })
-        widgets = dbmod.expand_widget_data(req, schema)
+        widgets = dbmod.expand_widget_data(context, schema)
 
         return layout_data['template'], \
                 {

Modified: incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/templates/widget_timeline.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/templates/widget_timeline.html?rev=1383477&r1=1383476&r2=1383477&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/templates/widget_timeline.html
(original)
+++ incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/templates/widget_timeline.html
Tue Sep 11 16:54:46 2012
@@ -21,9 +21,11 @@
   xmlns="http://www.w3.org/1999/xhtml"
   xmlns:py="http://genshi.edgewall.org/"
   xmlns:xi="http://www.w3.org/2001/XInclude"
-  py:with="today = format_date(today); yesterday = format_date(yesterday)">
+  py:with="today = format_date(today); yesterday = format_date(yesterday)"
+  py:choose="">
 
-  <table py:for="day, events in groupby(events, key=lambda e: format_date(e.date))"
+  <table py:when="events"
+      py:for="day, events in groupby(events, key=lambda e: format_date(e.date))"
       class="table" id="activityfeed">
     <thead>
       <tr>
@@ -47,4 +49,15 @@
       </tr>
     </tbody>
   </table>
-</div>
\ No newline at end of file
+  <py:otherwise>
+    <py:def function="timeline_empty()">
+      No events reported for <em>${summary_of(context.resource)}</em> in the
+      last <em>$daysback</em> days since 
+      <span class="date">${format_date(fromdate)}</span>.
+      This may happen if system is not configured correctly. 
+      Please contact your administrator if you think this is the case.
+    </py:def>
+    <xi:include href="widget_alert.html" 
+        py:with="msglabel = 'Warning'; msgbody = timeline_empty()" />
+  </py:otherwise>
+</div>

Modified: incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/timeline.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/timeline.py?rev=1383477&r1=1383476&r2=1383477&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/timeline.py (original)
+++ incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/widgets/timeline.py Tue Sep
11 16:54:46 2012
@@ -26,10 +26,14 @@ Widgets displaying timeline data.
 
 from datetime import datetime, date, time, timedelta
 from itertools import imap, islice
+from types import MethodType
 
 from genshi.builder import tag
-from trac.core import implements, TracError
+from trac.core import Component, ExtensionPoint, implements, Interface, \
+        TracError
 from trac.config import IntOption
+from trac.mimeview.api import RenderingContext
+from trac.resource import Resource, resource_exists
 from trac.timeline.web_ui import TimelineModule
 from trac.util.translation import _
 from trac.web.chrome import add_stylesheet
@@ -40,12 +44,54 @@ from bhdashboard.util import WidgetBase,
                               merge_links, pretty_wrapper, trac_version, \
                               trac_tags
 
+__metaclass__ = type
+
+class ITimelineEventsFilter(Interface):
+    """Filter timeline events displayed in a rendering context
+    """
+    def supported_providers():
+        """List supported timeline providers. Filtering process will take 
+        place only for the events contributed by listed providers.
+        Return `None` and all events contributed by all timeline providers 
+        will be processed.
+        """
+    def filter_event(context, provider, event, filters):
+        """Decide whether a timeline event is relevant in a rendering context.
+
+        :param context: rendering context, used to determine events scope
+        :param provider: provider contributing event
+        :param event: target event
+        :param filters: active timeline filters
+        :return: the event resulting from the filtering process or 
+                  `None` if it has to be removed from the event stream or
+                  `NotImplemented` if the filter doesn't care about it.
+        """
+
 class TimelineWidget(WidgetBase):
     """Display activity feed.
     """
     default_count = IntOption('widget_activity', 'limit', 25, 
                         """Maximum number of items displayed by default""")
 
+    event_filters = ExtensionPoint(ITimelineEventsFilter)
+
+    _filters_map = None
+
+    @property
+    def filters_map(self):
+        """Quick access to timeline events filters to be applied for a 
+        given timeline provider.
+        """
+        if self._filters_map is None:
+            self._filters_map = {}
+            for _filter in self.event_filters:
+                providers = _filter.supported_providers()
+                if providers is None:
+                    providers = [None]
+                for p in providers:
+                    self._filters_map.setdefault(p, []).append(_filter)
+        return self._filters_map
+
     def get_widget_params(self, name):
         """Return a dictionary containing arguments specification for
         the widget with specified name.
@@ -74,6 +120,15 @@ class TimelineWidget(WidgetBase):
                         'desc' : """Limit the number of events displayed""",
                         'type' : int
                     },
+                'realm' : {
+                        'desc' : """Resource realm. Used to filter events""",
+                    },
+                'id' : {
+                        'desc' : """Resource ID. Used to filter events""",
+                    },
+                'version' : {
+                        'desc' : """Resource version. Used to filter events""",
+                    },
             }
     get_widget_params = pretty_wrapper(get_widget_params, check_widget_name)
 
@@ -83,9 +138,13 @@ class TimelineWidget(WidgetBase):
         data = None
         req = context.req
         try:
+            timemdl = self.env[TimelineModule]
+            if timemdl is None :
+                raise TracError('Timeline module not available (disabled?)')
+
             params = ('from', 'daysback', 'doneby', 'precision', 'filters', \
-                        'max')
-            start, days, user, precision, filters, count = \
+                        'max', 'realm', 'id')
+            start, days, user, precision, filters, count, realm, rid = \
                     self.bind_params(name, options, *params)
             if count is None:
                 count = self.default_count
@@ -101,14 +160,27 @@ class TimelineWidget(WidgetBase):
             if start is not None:
                 fakereq.args['from'] = start.strftime('%x %X')
 
-            timemdl = self.env[TimelineModule]
-            if timemdl is None :
-                raise TracError('Timeline module not available (disabled?)')
-
-            data = timemdl.process_request(fakereq)[1]
+            if (realm, rid) != (None, None):
+                # Override rendering context
+                resource = Resource(realm, rid)
+                if resource_exists(self.env, resource) or \
+                        realm == rid == '':
+                    context = RenderingContext(resource)
+                    context.req = req
+                else:
+                    self.log.warning("TimelineWidget: Resource %s not found",
+                            resource)
+            # FIXME: Filter also if existence check is not conclusive ?
+            if resource_exists(self.env, context.resource):
+                module = FilteredTimeline(self.env, context)
+                self.log.debug('Filtering timeline events for %s', \
+                        context.resource)
+            else:
+                module = timemdl
+            data = module.process_request(fakereq)[1]
         except TracError, exc:
             if data is not None:
-                exc.title = data.get('title', 'TracReports')
+                exc.title = data.get('title', 'Activity')
             raise
         else:
             merge_links(srcreq=fakereq, dstreq=req,
@@ -116,6 +188,7 @@ class TimelineWidget(WidgetBase):
             add_stylesheet(req, 'dashboard/css/timeline.css')
             data['today'] = today = datetime.now(req.tz)
             data['yesterday'] = today - timedelta(days=1)
+            data['context'] = context
             return 'widget_timeline.html', \
                     {
                         'title' : _('Activity'),
@@ -127,3 +200,106 @@ class TimelineWidget(WidgetBase):
 
     render_widget = pretty_wrapper(render_widget, check_widget_name)
 
+class FilteredTimeline:
+    """This is a class (not a component ;) aimed at overriding some parts of
+    TimelineModule without patching it in order to inject code needed to filter
+    timeline events according to rendering context. It acts as a wrapper on top
+    of TimelineModule.
+    """
+    def __init__(self, env, context, keep_mismatched=False):
+        """Initialization
+
+        :param env: Environment object
+        :param context: Rendering context
+        """
+        self.env = env
+        self.context = context
+        self.keep_mismatched = keep_mismatched
+
+    # Access to TimelineModule's members
+
+    process_request = TimelineModule.__dict__['process_request']
+    _provider_failure = TimelineModule.__dict__['_provider_failure']
+    _event_data = TimelineModule.__dict__['_event_data']
+
+    @property
+    def event_providers(self):
+        """Introduce wrappers around timeline event providers in order to
+        filter event streams.
+        """
+        for p in TimelineModule(self.env).event_providers:
+            yield TimelineFilterAdapter(p, self.context, self.keep_mismatched)
+
+    def __getattr__(self, attrnm):
+        """Forward attribute access request to TimelineModule
+        """
+        try:
+            value = getattr(TimelineModule(self.env), attrnm)
+            if isinstance(value, MethodType):
+                raise AttributeError()
+        except AttributeError:
+            raise AttributeError("'%s' object has no attribute '%s'" % \
+                    (self.__class__.__name__, attrnm))
+        else:
+            return value
+
+class TimelineFilterAdapter:
+    """Wrapper class used to filter timeline event streams transparently.
+    Therefore it is compatible with `ITimelineEventProvider` interface 
+    and reuses the implementation provided by real provider.
+    """
+    def __init__(self, provider, context, keep_mismatched=False):
+        """Initialize wrapper object by providing real timeline events provider.
+        """
+        self.provider = provider
+        self.context = context
+        self.keep_mismatched = keep_mismatched
+
+    # ITimelineEventProvider methods
+
+    #def get_timeline_filters(self, req):
+    #def render_timeline_event(self, context, field, event):
+
+    def get_timeline_events(self, req, start, stop, filters):
+        """Filter timeline events according to context.
+        """
+        filters_map = TimelineWidget(self.env).filters_map
+        evfilters = filters_map.get(self.provider.__class__.__name__, []) + \
+                filters_map.get(None, [])
+        self.log.debug('Applying filters %s for %s against %s', evfilters, 
+                self.context.resource, self.provider)
+        if evfilters:
+            for event in self.provider.get_timeline_events(
+                    req, start, stop, filters):
+                match = False
+                for f in evfilters:
+                    new_event = f.filter_event(self.context, self.provider,
+                            event, filters)
+                    if new_event is None:
+                        event = None
+                        match = True
+                        break
+                    elif new_event is NotImplemented:
+                        pass
+                    else:
+                        event = new_event
+                        match = True
+                if event is not None and (match or self.keep_mismatched):
+                    yield event
+        else:
+            if self.keep_mismatched:
+                for event in self.provider.get_timeline_events(
+                        req, start, stop, filters):
+                    yield event
+
+    def __getattr__(self, attrnm):
+        """Forward attribute access request to real provider
+        """
+        try:
+            value = getattr(self.provider, attrnm)
+        except AttributeError:
+            raise AttributeError("'%s' object has no attribute '%s'" % \
+                    (self.__class__.__name__, attrnm))
+        else:
+            return value
+



Mime
View raw message