Commits

Anonymous committed d0eed01 Draft

Dashboard code import: BH_Dashboard: Initial version. (Simple) initialization and exception test cases ... PASS

Comments (0)

Files changed (20)

+
+What's new in version 1.0.0
+---------------------------
+
+- 
+
+include CHANGES COPYRIGHT NOTICE README TODO
+graft bhdashboard/templates
+graft bhdashboard/htdocs
+graft bhdashboard/widgets/templates
+graft bhdashboard/widgets/htdocs
+graft bhdashboard/tests/data
+
+= Project dashboard for Apache(TM) Bloodhound =
+
+ Add custom dashboards in multiple pages of Bloodhound sites.
+
+== !ToDo ==
+
+Outstanding tasks are :
+
+[[TicketQuery(component=bhdashboard&priority=major, format=list, rows=id|summary)]]
+
+== Dependencies ==
+
+This plugin depends on the following components to be installed:
+
+
+  - [http:trac.edgewall.org Trac]  ,,Since version 
+    ''' 1.0 ''',, . 
+
+
+== Installation ==
+
+This plugin has been tested with 
+[http://trac.edgewall.org/ Trac]  [http://trac.edgewall.org/wiki/0.11 0.11]  [http://trac.edgewall.org/wiki/0.12 0.12]  [http://trac.edgewall.org/wiki/0.13 0.13] .
+
+The first step to make it work is to [wiki:TracPlugins install this plugin] 
+either for a particular environment or otherwise make it available to
+all the environments:
+
+{{{
+$ easy_install /path/to/unpacked/BloodhoundDashboardPlugin-x.y.z.zip
+}}}
+
+,, where ''x.y.z'' is the version of the plugin,,
+
+... or alternately ...
+
+{{{
+$ easy_install BloodhoundDashboardPlugin
+}}}
+
+In case of having internet connection and access to 
+[http://pypi.python.org/pypi PyPI] or a simlar repository, both these 
+methods '''should''' automatically retrieve the [#Dependencies external 
+dependencies] from there.
+
+== Configuration ==
+
+In order to enable [wiki:/En/Devel/BloodhoundDashboardPlugin BloodhoundDashboardPlugin] plugin, 
+the only thing to do is to add the following lines to [wiki:TracIni trac.ini].
+
+{{{
+[components]
+bhdashboard.* = enabled
+}}}
+
+== Bug / feature requests ==
+
+Existing bugs and feature requests for [wiki:/En/Devel/BloodhoundDashboardPlugin BloodhoundDashboardPlugin] are
+[query:status=new|assigned|reopened&component=bhdashboard here].
+If you have any issues, please create a [/newticket?component=bhdashboard new ticket].
+
+
+
+= Testing Bloodhound Dashboard plugin =
+
+== Overview ==
+
+This plugin makes use of `setuptools` `test` command. Therefore all 
+test-support libraries needed to run the test suite should be installed 
+automatically (... considering the fact that they are listed in 
+`tests_require` and `install_requires` entries in `setup.ini` script ;).
+Recommended is the use of virtual Python environments. 
+
+== How to run tests ==
+
+All tests are written in files under `bhdashboard/tests` folder 
+(sub-modules of `bhdashboard.tests`) having names starting with prefix 
+`test_`. The following command should be enough so as to run tests 
+in one such module :
+
+{{{
+#!sh
+
+$ /path/to/python setup.py test -m bhdashboard.tests.test_<something>
+
+}}}
+
+... where `<something>` should be replaced to match the name of an existing 
+file containing tests e.g.
+
+{{{
+#!sh
+
+$ /path/to/python setup.py test -m bhdashboard.tests.test_report
+
+}}}
+
+== Continuous integration ==
+
+*TODO*
+
+== How do we run tests ==
+
+At present members of the team run tests on their computers as mentioned below :
+
+  - ''Python'' '''2.6''' virtual environment , Trac '''0.11.7''' .
+
+Besides there's a whole continuous integration infrastructure behind the 
+project (but that's TBD so far, should be documented later so that's in the 
+*TODO* list).
+
+PS: ... much more details missing here BTW ... should be added as soon 
+as possible ;) . Unfortunately weeks only have 7 days , 
+days only have 24 hours ... :-/
+
+
+Outstanding tasks
+-----------------
+
+- 

bhdashboard/__init__.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+
+r"""Project dashboard for Apache(TM) Bloodhound
+
+Add custom dashboards in multiple pages of Bloodhound sites.
+"""
+
+# Ignore errors to avoid Internal Server Errors
+from trac.core import TracError
+TracError.__str__ = lambda self: unicode(self).encode('ascii', 'ignore')
+
+try:
+    from bhdashboard import *
+    msg = 'Ok'
+except Exception, exc:
+#    raise
+    msg = "Exception %s raised: '%s'" % (exc.__class__.__name__, str(exc))

bhdashboard/api.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+
+r"""Project dashboard for Apache(TM) Bloodhound
+
+The core of the dashboard architecture.
+"""
+
+from trac.core import Component, ExtensionPoint, implements, \
+        Interface, TracError
+from trac.perm import IPermissionRequestor
+from trac.util.translation import _
+
+class IWidgetProvider(Interface):
+    r"""Extension point interface for components providing widgets.
+    These may be seen as web parts more sophisticated than WikiMacro 
+    as they expose much more meta-data, but more simple than gadgets
+    because they belong in the environment and are built on top of Trac
+    architecture. This makes them more suitable to be used in 
+    environments where flexibility and configurability is needed 
+    (i.e. dashboards).
+    """
+    def get_widgets():
+        """Return an iterable listing the names of the provided widgets."""
+
+    def get_widget_description(name):
+        """Return plain text description of the widget with specified name."""
+
+    def get_widget_params(name):
+        """Return a dictionary describing wigdet preference for the widget 
+        with specified name. Used to customize widget behavior."""
+
+    def render_widget(name, context, options):
+        """Render widget considering given options."""
+
+    # TODO: Add methods to specify widget metadata (e.g. parameters)
+
+class DashboardSystem(Component):
+    implements(IPermissionRequestor)
+
+    providers = ExtensionPoint(IWidgetProvider)
+
+    # IPermissionRequestor methods
+    def get_permission_actions(self):
+        return ['DASHBOARD_VIEW',
+                # 'DASHBOARD_CREATE', 'DASHBOARD_EDIT' <= Coming soon ;)
+               ]
+
+    # Public API
+    def bind_params(self, options, spec, *params):
+        """Extract values for widget arguments from `options` and ensure 
+        they are valid and properly formatted.
+        """
+        # Should this helper function be part of public API ?
+        def get_and_check(p):
+            try:
+                param_spec = spec[p]
+            except KeyError:
+                raise InvalidWidgetArgument("Unknown parameter `%s`" % (p,))
+            try:
+                argtype = param_spec.get('type') or unicode
+                return argtype(options['args'][p])
+            except KeyError:
+                if param_spec.get('required'):
+                    raise InvalidWidgetArgument(p,
+                            "Required parameter expected")
+        return (get_and_check(param) for param in params)
+
+# Maybe it is better to move these to a separate file 
+# (if this gets as big as it seems it will be)
+
+class WidgetException(TracError):
+    """Base class for all errors related to Trac widgets"""
+
+class InvalidIdentifier(WidgetException):
+    """Invalid value for a field used to identify an internal object"""
+
+    title = 'Invalid identifier'
+
+class InvalidWidgetArgument(WidgetException):
+    """Something went wrong with widget parameter"""
+    
+    title = 'Invalid Argument'
+    
+    def __init__(self, argname, message, title=None, show_traceback=False):
+        TracError.__init__(self, message, title, show_traceback)
+        self.argname = argname
+    
+    def __unicode__(self):
+        return unicode(_("Invalid argument `") + self.argname + "`. " + \
+                self.message)
+

bhdashboard/templates/dashboard.html

+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/"
+      xmlns:xi="http://www.w3.org/2001/XInclude">
+  <xi:include href="layout.html" />
+  <head>
+    <title>$title</title>
+  </head>
+
+  <body class="yui-skin-sam">
+    <div id="content" class="dashboard">
+      <div id="doc3" class="yui-t7">  
+        <div id="hd" role="banner"><h1>Header</h1></div>  
+        <div id="bd" role="main">  
+          <div class="yui-g">  
+            <div class="yui-u first">  
+              
+            </div>  
+            <div class="yui-u">  
+              
+            </div>  
+          </div>  
+        </div>  
+        <div id="ft" role="contentinfo"><p>Footer</p></div>  
+      </div>  
+    </div>
+  </body>
+</html>

bhdashboard/tests/__init__.py

+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+r"""Test artifacts.
+
+The test suites have been run using Trac=0.11.1 , Trac=0.11.5 , Trac=0.11.7
+"""
+
+__metaclass__ = type
+
+from trac.core import ComponentMeta
+from trac.db.api import _parse_db_str, DatabaseManager
+from trac.mimeview.api import Context
+from trac.test import EnvironmentStub
+from trac.util.compat import set
+
+import sys
+
+#------------------------------------------------------
+#    Trac environments used for testing purposes
+#------------------------------------------------------
+
+class EnvironmentStub(EnvironmentStub):
+  r"""Enhanced stub of the trac.env.Environment object for testing.
+  """
+
+  # Dont break lazy evaluation. Otherwise RPC calls misteriously fail.
+  @property
+  def _abs_href(self):
+    return self.abs_href
+
+  def enable_component(self, clsdef):
+    r"""Enable a plugin temporarily at testing time.
+    """
+    if clsdef not in self.enabled_components :
+      self.enabled_components.append(clsdef)
+
+  def disable_component(self, clsdef):
+    r"""Disable a plugin temporarily at testing time.
+    """
+    try:
+      self.enabled_components.remove(clsdef)
+    except ValueError :
+      self.log.warning("Component %s was not enabled", clsdef)
+
+  def rip_component(self, cls):
+    r"""Disable a plugin forever and RIP it using the super-laser beam.
+    """
+    self.disable_component(cls)
+    for reg in ComponentMeta._registry.itervalues():
+      try:
+        reg.remove(cls)
+      except ValueError :
+        pass
+
+  if not hasattr(EnvironmentStub, 'reset_db'):
+
+    # Copycat trac.test.EnvironmentStub.reset_db (Trac=0.11.5)
+    def reset_db(self, default_data=None):
+        r"""Remove all data from Trac tables, keeping the tables themselves.
+        :param default_data: after clean-up, initialize with default data
+        :return: True upon success
+        """
+        from trac import db_default
+
+        db = self.get_db_cnx()
+        db.rollback() # make sure there's no transaction in progress
+        cursor = db.cursor()
+
+        defdata = list(db_default.get_data(db))
+
+        for table, cols, vals in defdata:
+            cursor.execute("DELETE FROM %s" % (table,) )
+
+        db.commit()
+
+        if default_data:
+            for table, cols, vals in defdata:
+                cursor.executemany("INSERT INTO %s (%s) VALUES (%s)"
+                                   % (table, ','.join(cols),
+                                      ','.join(['%s' for c in cols])),
+                                   vals)
+        else:
+            cursor.execute("INSERT INTO system (name, value) "
+                           "VALUES (%s, %s)",
+                           ('database_version', str(db_default.db_version)))
+        db.commit()
+
+#------------------------------------------------------
+#    Minimalistic testing framework for Trac
+#------------------------------------------------------
+
+from dutest import DocTestLoader, DocTestSuiteFixture
+from os.path import dirname
+from types import MethodType
+
+from bhdashboard.util import dummy_request
+
+# Hide this module from tracebacks written into test results.
+__unittest = False
+
+class DocTestTracLoader(DocTestLoader):
+  r"""A generic XUnit loader that allows to load doctests written 
+  to check that Trac plugins behave as expected.
+  """
+  def set_env(self, env):
+    if self.extraglobs is None :
+      self.extraglobs = dict(env=env)
+    else :
+      self.extraglobs['env'] = env
+
+  env = property(lambda self : self.extraglobs.get('env'), set_env, \
+                  doc="""The Trac environment used in doctests.""")
+  del set_env
+
+  def __init__(self, dt_finder=None, globs=None, extraglobs=None, \
+                          load=None, default_data=False, enable=None, \
+                          **opts):
+    r"""Initialization. It basically works like `DocTestLoader`'s 
+    initializer but creates also the Trac environment used for 
+    testing purposes. The default behavior is to create an instance 
+    of `EnvironmentStub` class. Subclasses can add more specific 
+    keyword parameters in order to use them to create the 
+    environment. Next it loads (and | or) enables the components 
+    needed by the test suite.
+
+    The following variables are magically available at testing time. 
+    They can be used directly in doctests :
+
+    - req         A dummy request object setup for anonymous access.
+    - auth_req    A dummy request object setup like if user `murphy` was  
+                  accessing the site.
+    - env         the Trac environment used as a stub for testing 
+                  purposes (i.e. `self.env`).
+
+    @param dt_finder        see docs for `DocTestLoader.__init__` 
+                            method.
+    @param globs            see docs for `DocTestLoader.__init__` 
+                            method.
+    @param extraglobs       see docs for `DocTestLoader.__init__` 
+                            method.
+    @param load             a list of packages containing components 
+                            that will be loaded to ensure they are 
+                            available at testing time. It should be 
+                            the top level module in that package 
+                            (e.g. 'trac').
+    @param default_data     If true, populate the database with some 
+                            defaults. This parameter has to be 
+                            handled by `createTracEnv` method.
+    @param enable           a list of UNIX patterns specifying which 
+                            components need to be enabled by default 
+                            at testing time. This parameter should be 
+                            handled by `createTracEnv` method.
+    """
+    super(DocTestTracLoader, self).__init__(dt_finder, globs, \
+                                              extraglobs, **opts)
+    self.env = self.createTracEnv(default_data, enable, **opts)
+    self.load_components(load is None and self.default_packages or load)
+
+  # Load trac built-in components by default
+  default_packages = ['trac']
+
+  def createTracEnv(self, default_data=False, enable=None, **params):
+    r"""Create the Trac environment used for testing purposes. The 
+    default behavior is to create an instance of `EnvironmentStub` 
+    class. Subclasses can override this decision and add more specific 
+    keyword parameters in order to control environment creation in 
+    more detail. 
+
+    All parameters supplied at initialization time. By default they 
+    are ignored.
+    @param default_data     If True, populate the database with some 
+                            defaults.
+    @param enable           a list of UNIX patterns specifying which 
+                            components need to be enabled by default 
+                            at testing time.
+    @return                 the environment used for testing purpose.
+    """
+    return EnvironmentStub(default_data, enable)
+
+  def load_components(self, pkgs):
+    r"""Load some packages to ensure that the components they 
+    implement are available at testing time.
+    """
+    from trac.loader import load_components
+    for pkg in pkgs :
+      try :
+        __import__(pkg)
+      except ImportError :
+        pass                        # Skip pkg. What a shame !
+      else :
+        mdl = sys.modules[pkg]
+        load_components(self.env, dirname(dirname(mdl.__file__)))
+
+  class doctestSuiteClass(DocTestSuiteFixture):
+    r"""Prepare the global namespace before running all doctests 
+    in the suite. Reset the Trac environment.
+    """
+    username = 'murphy'
+
+    @property
+    def env(self):
+      r"""The Trac environment involved in this test. It is 
+      retrieved using the global namespace ;o).
+      """
+      return self.globalns['env']
+
+    def new_request(self, uname=None, args=None):
+      r"""Create and initialize a new request object.
+      """
+      req = dummy_request(self.env, uname)
+      if args is not None :
+        req.args = args
+      return req
+
+    def setUp(self):
+      r"""Include two (i.e. `req` anonymous and `auth_req` 
+      authenticated) request objects in the global namespace, before 
+      running the doctests. Besides, clean up environment data and 
+      include only default data.
+      """
+      globs = self.globalns
+      req = self.new_request(args=dict())
+      auth_req = self.new_request(uname=self.username, args=dict())
+      globs['req'] = req
+      globs['auth_req'] = auth_req
+      # TODO: If the source docstrings belong to a Trac component, 
+      #       then instantiate it and include in the global 
+      #       namespace.
+
+      # Delete data in Trac tables
+      from trac import db_default
+      db = self.env.get_db_cnx()
+      cursor = db.cursor()
+      for table in db_default.schema:
+        cursor.execute("DELETE FROM " + table.name)
+      db.commit()
+
+      self.env.reset_db(default_data=True)
+
+#------------------------------------------------------
+#    Test artifacts used to test widget providers
+#------------------------------------------------------
+
+from bhdashboard.api import InvalidIdentifier
+
+class DocTestWidgetLoader(DocTestTracLoader):
+  r"""Load doctests used to test Trac RPC handlers.
+  """
+  class doctestSuiteClass(DocTestTracLoader.doctestSuiteClass):
+    r"""Include the appropriate RPC handler in global namespace 
+    before running all test cases in the suite.
+    """
+
+    def ns_from_name(self):
+      r"""Extract the target namespace under test using the name
+      of the DocTest instance manipulated by the suite.
+      """
+      try :
+        return self._dt.name.split(':', 1)[0].split('|', 1)[-1]
+      except :
+        return None
+
+    def partial_setup(self):
+      r"""Perform partial setup due to some minor failure (e.g. 
+      namespace missing in test name).
+      """
+      globs = self.globalns
+      globs['widget'] = globs['ctx'] = globs['auth_ctx'] = None
+
+    def setup_widget(self, widgetns):
+      r"""(Insert | update) the IWidgetProvider in the global 
+      namespace.
+
+      @param widgetns             widget name.
+      @throws RuntimeError        if a widget with requested name cannot 
+                                  be found.
+      """
+      globs = self.globalns
+      globs['ctx'] = Context.from_request(globs['req'])
+      globs['auth_ctx'] = Context.from_request(globs['auth_req'])
+      for wp in self.dbsys.providers :
+        if widgetns in set(wp.get_widgets()) :
+          globs['widget'] = wp
+          break
+      else :
+        raise InvalidIdentifier('Cannot load widget provider for %s' % widgetns)
+
+    def setUp(self):
+      r"""Include the appropriate widget provider in global namespace 
+      before running all test cases in the suite. In this case three
+      objects are added to the global namespace :
+
+        - `widget`       the component implementing the widget under test
+        - `ctx`          context used to render the widget for 
+                         anonymous user
+        - `auth_ctx`     context used to render the widget for 
+                         authenticated user
+      """
+      # Fail here if BloodhoundDashboardPlugin is not available. Thus 
+      # this fact will be reported as a failure and subsequent test 
+      # cases will be run anyway.
+      from bhdashboard.api import DashboardSystem
+      self.dbsys = DashboardSystem(self.env)
+
+      # Add request objects
+      DocTestTracLoader.doctestSuiteClass.setUp(self)
+
+      widgetns = self.ns_from_name()
+      if widgetns is None :
+        # TODO: If doctests belong to a widget provider class then 
+        #       instantiate it. In the mean time ...
+        self.partial_setup()
+      else :
+        try :
+          self.setup_widget(widgetns)
+        except InvalidIdentifier:
+          self.partial_setup()
+
+#------------------------------------------------------
+#    Helper functions used in test cases
+#------------------------------------------------------
+
+def clear_perm_cache(_env, _req):
+  r"""Ensure that cache policies will not prevent test cases from 
+  altering user permissions right away.
+  """
+  from trac.perm import PermissionSystem, DefaultPermissionPolicy
+
+  _req.perm._cache.clear()            # Clear permission cache
+  for policy in PermissionSystem(_env).policies :
+    if isinstance(policy, DefaultPermissionPolicy):
+      policy.permission_cache.clear() # Clear policy cache
+      break
+
+#------------------------------------------------------
+#    Global test data
+#------------------------------------------------------
+
+from ConfigParser import RawConfigParser
+from pkg_resources import resource_stream
+
+def load_test_data(key):
+  r"""Load data used for testing purposes. Currently such data is 
+  stored in .INI files inside `data` directory.
+
+  @param key          currently the path to the file containing the 
+                      data, relative to `data` folder. 
+  """
+  fo = resource_stream(__name__, 'data/%s.ini' % key)
+  try :
+    p = RawConfigParser()
+    p.readfp(fo)
+    for section in p.sections():
+      yield section, dict(p.items(section))
+  finally :
+    fo.close()
+
+# The set of tickets used by test cases.
+ticket_data = [(attrs.pop('summary'), attrs.pop('description'), attrs) \
+                for _, attrs in sorted(load_test_data('ticket_data'))]
+

bhdashboard/tests/data/ticket_data.ini

+
+[ticket1]
+summary = Ticket 1
+description = Description 1
+priority = major
+milestone = milestone1
+type = defect
+owner = murphy
+status = accepted
+component = component1
+version = 1.0
+
+[ticket2]
+summary = Ticket 2
+description = Description 2
+priority = major
+milestone = milestone4
+type = task
+owner = murphy
+status = accepted
+
+[ticket3]
+summary = Ticket 3
+description = Description 3
+priority = critical
+milestone = milestone3
+type = enhancement
+owner = tester
+version = 2.0
+
+[ticket4]
+summary = Ticket 4
+description = Description 4
+priority = minor
+milestone = milestone3
+type = task
+owner = murphy
+status = closed
+component = component1
+version = 1.0
+
+[ticket5]
+summary = Ticket 5
+description = Description 5
+priority = minor
+milestone = milestone3
+type = task
+owner = murphy
+version = 2.0
+
+[ticket6]
+summary = Ticket 6
+description = Description 6
+priority = minor
+milestone = milestone1
+type = task
+owner = tester
+status = assigned
+component = component2
+version = 1.0
+
+[ticket7]
+summary = Ticket 7
+description = Description 7
+priority = critical
+type = enhancement
+owner = murphy
+status = closed
+
+[ticket8]
+summary = Ticket 8
+description = Description 8
+priority = major
+type = task
+owner = murphy
+status = closed
+component = component1
+
+[ticket9]
+summary = Ticket 9
+description = Description 9
+priority = minor
+type = enhancement
+owner = tester
+status = closed
+version = 2.0

bhdashboard/tests/test_report.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+
+r"""Project dashboard for Apache(TM) Bloodhound
+
+In this file you'll find part of the tests written to ensure that
+widgets displaying contents generated by TracReports behave as expected.
+
+Only the tests requiring minimal setup effort are included below. 
+This means that the environment used to run these tests contains the 
+barely minimal information included in an environment (i.e. only the 
+data specified by `trac.db_default.get_data`.).
+
+Once the tests are started all built-in components (except 
+trac.versioncontrol.* ) as well as widget system and extensions
+are loaded. Besides the following values are (auto-magically)
+made available in the global namespace (i.e. provided that 
+the test name be written like `|widget_name: Descriptive message`):
+
+  - req         A dummy request object setup for anonymous access.
+  - auth_req    A dummy request object setup like if user `murphy` was  
+                accessing the site.
+  - env         the Trac environment used as a stub for testing purposes.
+                This object is an instance of 
+                `bhdashboard.tests.EnvironmentStub`.
+  - widget      the widget provider under test.
+  - ctx         context used to render widget for anonymous user.
+  - ctx_auth    context used to render widget for `murphy` user.
+  - ticket_data A set of tickets used for testing purposes.
+"""
+
+#------------------------------------------------------
+#    Test artifacts
+#------------------------------------------------------
+
+def test_suite():
+  from doctest import NORMALIZE_WHITESPACE, ELLIPSIS, REPORT_UDIFF
+  from dutest import MultiTestLoader
+  from unittest import defaultTestLoader
+  
+  from __init__ import DocTestWidgetLoader, ticket_data
+  
+  magic_vars = dict(ticket_data=ticket_data)
+  l = MultiTestLoader(
+        [defaultTestLoader, \
+          DocTestWidgetLoader(extraglobs=magic_vars, \
+                            enable=['trac.[a-uw-z]*', 'bhdashboard.*'], \
+                            default_data=True, \
+                            optionflags=ELLIPSIS | # REPORT_UDIFF | \
+                                        NORMALIZE_WHITESPACE) \
+        ])
+  
+  import sys
+  return l.loadTestsFromModule(sys.modules[__name__])
+
+#------------------------------------------------------
+#    Helper functions
+#------------------------------------------------------
+
+from datetime import datetime, time, date
+from itertools import izip
+from pprint import pprint
+
+from __init__ import clear_perm_cache
+
+def print_report_metadata(report_desc):
+  for attrnm in ('id', 'title', 'description', 'query'):
+    print attrnm.capitalize()
+    print '-' * len(attrnm)
+    print report_desc[attrnm]
+
+def print_report_columns(cols):
+  for coldsc in cols:
+    print 'Column:', coldsc[0], 'Type:', coldsc[1] or '_', \
+          'Label:', 
+    try :
+      print coldsc[2] or '_'
+    except IndexError :
+      print '_'
+
+def print_report_result(cols, data):
+  for i, row in enumerate(data):
+    print '= Row', i, '='
+    for coldsc in cols:
+      colnm = coldsc[0]
+      print 'Column:', colnm, 'Value:', row.get(colnm) or None, ''
+
+TICKET_ATTRS = ('summary', 'description', 'priority', \
+                'milestone', 'type', 'owner', 'status', \
+                'component', 'version')
+
+def prepare_ticket_workflow(tcktrpc, ticket_data, auth_req):
+  r"""Set ticket status considering the actions defined in standard 
+  ticket workflow. Needed for TracRpc>=1.0.6
+  """
+  from time import sleep
+  
+  TICKET_ACTIONS = {'accepted': 'accept', 'closed' : 'resolve',
+                    'assigned': 'reassign'}
+  sleep(1)
+  for idx, (_, __, td) in enumerate(ticket_data) :
+    action = TICKET_ACTIONS.get(td.get('status'))
+    if action is not None :
+      aux_attrs = {'action' : action}
+      aux_attrs.update(td)
+      tcktrpc.update(auth_req, idx + 1, "", aux_attrs)
+  sleep(1)
+  for idx, (_, __, td) in enumerate(ticket_data) :
+    tcktrpc.update(auth_req, idx + 1, "", td)
+
+__test__ = {
+    'Initialization: Report widgets' : r"""
+      >>> from trac.core import ComponentMeta
+      >>> from bhdashboard.api import IWidgetProvider
+      >>> from bhdashboard.widgets.report import *
+      >>> allcls = ComponentMeta._registry.get(IWidgetProvider, [])
+      >>> for wpcls in (TicketReportWidget,):
+      ...   print wpcls in allcls
+      ...
+      True
+      """,
+    '|TicketReport: Metadata' : r"""
+      >>> list(widget.get_widgets())
+      ['TicketReport']
+      >>> params = widget.get_widget_params('TicketReport')
+      >>> pprint(params)
+      {'id': {'desc': 'Report number',
+                  'required': True,
+                  'type': <type 'int'>},
+       'limit': {'default': 0,
+                  'desc': 'Number of results to retrieve',
+                  'type': <type 'int'>}}
+      """,
+    '|TicketReport: Render My Tickets report' : r"""
+      >>> widget.render_widget('TicketReport', ctx, {
+      ...     'args' : {'id' : 7}
+      ...   })
+      ...
+      """,
+    '|TicketReport: Render a subset of My Tickets report' : r"""
+      >>> widget.render_widget('TicketReport', ctx, {
+      ...     'args' : {'id' : 7}
+      ...   })
+      ...
+      """,
+    '|TicketReport: Invalid widget name' : r"""
+      >>> widget.render_widget('OlkswSk', ctx, {
+      ...     'args' : {'id' : 1, 'limit' : 8}
+      ...   })
+      ...
+      Traceback (most recent call last):
+        ...
+      InvalidIdentifier: Widget name MUST match any of TicketReport
+      """,
+    '|TicketReport: Invalid report ID in arguments' : r"""
+      >>> widget.render_widget('TicketReport', ctx, {
+      ...     'args' : {'id' : 99999}
+      ...   })
+      ...
+      Traceback (most recent call last):
+        ...
+      InvalidIdentifier: Report 99999 does not exist
+      """,
+    '|TicketReport: Missing required arguments' : r"""
+      >>> widget.render_widget('TicketReport', ctx, {
+      ...     'args' : {}
+      ...   })
+      ...
+      Traceback (most recent call last):
+        ...
+      InvalidWidgetArgument: Invalid argument `id`. Required parameter expected
+
+      >>> widget.render_widget('TicketReport', ctx, {
+      ...     'args' : {}
+      ...   })
+      ...
+      Traceback (most recent call last):
+        ...
+      InvalidWidgetArgument: Invalid argument `id`. Required parameter expected
+      """,
+    '|TicketReport: Invalid widget parameter' : r"""
+      >>> widget.render_widget('TicketReport', ctx, {
+      ...     'args' : {'newjums' : 7, 'id' : 3}
+      ...   })
+      ...
+      """,
+    '|TicketReport: Invalid report definition' : r"""
+      >>> raise NotImplementedError()
+      """,
+  }
+

bhdashboard/util.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+
+r"""Project dashboard for Apache(TM) Bloodhound
+
+Helper functions and classes.
+"""
+
+from functools import update_wrapper
+import inspect
+from urlparse import urlparse
+
+from trac.core import Component, implements
+from trac.web.api import Request
+from trac.web.chrome import Chrome
+from trac.web.main import RequestDispatcher
+
+from bhdashboard.api import DashboardSystem, IWidgetProvider, InvalidIdentifier
+
+def dummy_request(env, uname=None):
+    environ = {
+                'trac.base_url' : str(env._abs_href()), 
+                'SCRIPT_NAME' : urlparse(str(env._abs_href())).path
+                }
+    req = Request(environ, lambda *args, **kwds: None)
+    # Intercept redirection
+    req.redirect = lambda *args, **kwds: None
+    # Setup user information
+    if uname is not None :
+      environ['REMOTE_USER'] = req.authname = uname
+    
+    rd = RequestDispatcher(env)
+    chrome = Chrome(env)
+    req.callbacks.update({
+        'authname': rd.authenticate,
+        'chrome': chrome.prepare_request,
+        'hdf': rd._get_hdf,
+        'perm': rd._get_perm,
+        'session': rd._get_session,
+        'tz': rd._get_timezone,
+        'form_token': rd._get_form_token
+    })
+    return req
+
+
+class WidgetBase(Component):
+    """Abstract base class for widgets"""
+
+    implements(IWidgetProvider)
+    abstract = True
+
+    def get_widgets(self):
+        """Yield the name of the widget based on the class name."""
+        name = self.__class__.__name__
+        if name.endswith('Widget'):
+            name = name[:-6]
+        yield name
+
+    def get_widget_description(self, name):
+        """Return the subclass's docstring."""
+        return to_unicode(inspect.getdoc(self.__class__))
+
+    def get_widget_params(self, name):
+        """Return a dictionary containing arguments specification for
+        the widget with specified name.
+        """
+        raise NotImplementedError
+
+    def render_widget(self, context, name, options):
+        """Render widget considering given options."""
+        raise NotImplementedError
+
+    # Helper methods
+    def bind_params(self, name, options, *params):
+        return DashboardSystem(self.env).bind_params(options, 
+                self.get_widget_params(name), *params)
+
+def check_widget_name(f):
+    """Decorator used to wrap methods of widget providers so as to ensure
+    widget names will match those listed by `get_widgets` method.
+    """
+    def widget_name_checker(self, name, *args, **kwargs):
+        names = set(self.get_widgets())
+        if name not in names: 
+            raise InvalidIdentifier('Widget name MUST match any of ' + 
+                        ', '.join(names), 
+                    title='Invalid widget identifier')
+        return f(self, name, *args, **kwargs)
+    return widget_name_checker
+
+def pretty_wrapper(wrapped, *decorators):
+    """Apply multiple decorators to a given function and make the result 
+    look like wrapped function.
+    """
+    wrapper = wrapped
+    for f in decorators:
+        wrapper = f(wrapper)
+    return update_wrapper(wrapper, wrapped)
+

bhdashboard/web_ui.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+
+r"""Project dashboard for Apache(TM) Bloodhound
+
+Implementing dashboard user interface.
+"""
+
+import pkg_resources
+import re
+
+from trac.core import Component, implements
+from trac.config import Option
+from trac.web.api import IRequestHandler
+from trac.web.chrome import INavigationContributor, ITemplateProvider
+
+class DashboardModule(Component):
+    implements(IRequestHandler, INavigationContributor, ITemplateProvider)
+
+    mainnav_label = Option('dashboard', 'mainnav', 'Dashboard', \
+                            """Dashboard label in mainnav""")
+
+    # IRequestHandler methods
+    def match_request(self, req):
+        """Match dashboard prefix"""
+        return bool(re.match(r'^/dashboard(/.)?', req.path_info))
+
+    def process_request(self, req):
+        """Initially this will render static widgets. With time it will be 
+        more and more dynamic and flexible.
+        """
+
+    # INavigationContributor methods
+    def get_active_navigation_item(self, req):
+        """Highlight dashboard mainnav item.
+        """
+        return 'dashboard'
+
+    def get_navigation_items(self, req):
+        """Add an item in mainnav to access global dashboard
+        """
+        if 'DASHBOARD_VIEW' in req.perm:
+            yield ('mainnav', 'dashboard', 
+                    tag.a(_(self.mainnav_label), href=req.href.dashboard()))
+
+    # ITemplateProvider methods
+    def get_htdocs_dirs(self):
+        """List `htdocs` dirs for dashboard and widgets.
+        """
+        resource_filename = pkg_resources.resource_filename
+        return [
+                ('dashboard', resource_filename('bhdashboard', 'htdocs')),
+                ('widgets', resource_filename('bhdashboard.widgets', 'htdocs'))]
+
+    def get_templates_dirs(self):
+        """List `templates` folders for dashboard and widgets.
+        """
+        resource_filename = pkg_resources.resource_filename
+        return [resource_filename('bhdashboard', 'templates'),
+                resource_filename('bhdashboard.widgets', 'templates')]
+

bhdashboard/widgets/__init__.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+
+r"""Project dashboard for Apache(TM) Bloodhound
+
+Available widgets.
+"""
+

bhdashboard/widgets/report.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+
+r"""Project dashboard for Apache(TM) Bloodhound
+
+Widgets displaying report data.
+"""
+
+from datetime import datetime, date, time
+from itertools import imap
+
+from trac.core import implements, TracError
+from trac.ticket.report import ReportModule
+from trac.util.translation import _
+
+from bhdashboard.util import WidgetBase, InvalidIdentifier, \
+                              check_widget_name, pretty_wrapper
+
+class TicketReportWidget(WidgetBase):
+    """Display tickets in saved report using a grid
+    """
+    def get_widget_params(self, name):
+        """Return a dictionary containing arguments specification for
+        the widget with specified name.
+        """
+        return {
+                'id' : {
+                        'desc' : """Report number""",
+                        'required' : True,
+                        'type' : int,
+                    },
+                'limit' : {
+                        'default' : 0,
+                        'desc' : """Number of results to retrieve""",
+                        'type' : int,
+                },
+            }
+    get_widget_params = pretty_wrapper(get_widget_params, check_widget_name)
+
+    def render_widget(self, name, context, options):
+        """Execute stored report and render data using a grid
+        """
+        metadata = data = None
+        req = context.req
+        try:
+            rptid, limit = self.bind_params(name, options, 'id', 'limit')
+            metadata = self.get(req, rptid)
+            # TODO: Should metadata also contain columns definition ?
+            data = list(self.execute(req, rptid))
+        except TracError, exc:
+            if metadata is not None :
+                exc.title = metadata.get('title', 'TracReports')
+            else:
+                exc.title = 'TracReports'
+            raise
+        else:
+            return 'widget_grid', data, context
+
+    render_widget = pretty_wrapper(render_widget, check_widget_name)
+
+    # Internal methods
+
+    # These have been imported verbatim from existing 
+    # `tracgviz.rpc.ReportRPC` class in TracGViz plugin ;)
+    def get(self, req, id):
+        r"""Return information about an specific report as a dict 
+        containing the following fields.
+        
+        - id :            the report ID.
+        - title:          the report title.
+        - description:    the report description.
+        - query:          the query string used to select the tickets 
+                          to be included in this report. This field 
+                          is returned only if `REPORT_SQL_VIEW` has 
+                          been granted to the client performing the 
+                          request. Otherwise it is empty.
+        """
+        if 'REPORT_SQL_VIEW' in req.perm:
+            sql = "SELECT id,title,query,description from report " \
+                   "WHERE id=%s" % (id,)
+        else :
+            sql = "SELECT id,title,NULL,description from report " \
+                   "WHERE id=%s" % (id,)
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        try:
+            cursor.execute(sql)
+            for report_info in cursor:
+                return dict(zip(['id','title','query','description'], report_info))
+            else:
+                return None
+        finally:
+            cursor.close()
+
+    def _execute_sql(self, req, id, sql, limit=0):
+        r"""Execute a SQL report and return no more than `limit` rows 
+        (or all rows if limit == 0).
+        """
+        repmdl = ReportModule(self.env)
+        db = self.env.get_db_cnx()
+        try:
+            args = repmdl.get_var_args(req)
+        except ValueError,e:
+            raise ValueError(_('Report failed: %(error)s', error=e))
+        try:
+            try:
+                # Paginated exec (>=0.11)
+                exec_proc = repmdl.execute_paginated_report
+                kwargs = dict(limit=limit)
+            except AttributeError:
+                # Legacy exec (<=0.10)
+                exec_proc = repmdl.execute_report
+                kwargs = {}
+            return exec_proc(req, db, id, sql, args, **kwargs)[:2]
+        except Exception, e:
+            db.rollback()
+            raise 
+
+    def execute(self, req, id, limit=0):
+        r"""Execute a Trac report.
+
+        @param id     the report ID.
+        @return       a list containing the data provided by the 
+                      target report.
+        @throws       `NotImplementedError` if the report definition 
+                      consists of saved custom query specified 
+                      using a URL.
+        @throws       `QuerySyntaxError` if the report definition 
+                      consists of a `TracQuery` containing syntax errors.
+        @throws       `Exception` in case of detecting any other error.
+        """
+        report_spec = self.get(req, id)
+        if report_spec is None:
+            raise InvalidIdentifier('Report %s does not exist' % (id,))
+        sql = report_spec['query']
+        query = ''.join([line.strip() for line in sql.splitlines()])
+        if query and (query[0] == '?' or query.startswith('query:?')):
+            raise NotImplementedError('Saved custom queries specified ' \
+                                  'using URLs are not supported.')
+        elif query.startswith('query:'):
+            query = Query.from_string(self.env, query[6:], report=id)
+            server_url = urlparse(req.base_url)
+            server_href = Href(urlunparse((server_url.scheme, \
+                                        server_url.netloc, \
+                                        '', '', '', '')))
+            def rel2abs(row):
+                """Turn relative value in 'href' into absolute URLs."""
+                self.log.debug('IG: Query Row %s', row)
+                url = row['href']
+                urlobj = urlparse(url)
+                if not urlobj.netloc:
+                    row['href'] = server_href(url)
+                return row
+            
+            return imap(rel2abs, query.execute(req))
+        else:
+            cols, results = self._execute_sql(req, id, sql, limit=limit)
+            return (dict(zip(cols, list(row))) for row in results)
+
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
+[sdist]
+formats = gztar,bztar,ztar,tar,zip
+#!/usr/bin/env python
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+
+try:
+    from setuptools import setup
+except ImportError:
+    from distutils.core import setup
+
+from bhdashboard.__init__ import __doc__ as DESC
+
+versions = [
+    (1, 0, 0),
+    ]
+    
+latest = '.'.join(str(x) for x in versions[-1])
+
+status = {
+            'planning' :  "Development Status :: 1 - Planning",
+            'pre-alpha' : "Development Status :: 2 - Pre-Alpha",
+            'alpha' :     "Development Status :: 3 - Alpha",
+            'beta' :      "Development Status :: 4 - Beta",
+            'stable' :    "Development Status :: 5 - Production/Stable",
+            'mature' :    "Development Status :: 6 - Mature",
+            'inactive' :  "Development Status :: 7 - Inactive"
+         }
+dev_status = status["alpha"]
+
+cats = [
+      dev_status,
+      "Environment :: Plugins", 
+      "Environment :: Web Environment", 
+      "Framework :: Trac", 
+      "Intended Audience :: Developers", 
+      "Intended Audience :: Information Technology", 
+      "Intended Audience :: Other Audience", 
+      "Intended Audience :: System Administrators", 
+      "License :: Unknown", 
+      "Operating System :: OS Independent", 
+      "Programming Language :: Python", 
+      "Programming Language :: Python :: 2.5", 
+      "Programming Language :: Python :: 2.6",
+      "Programming Language :: Python :: 2.7",
+      "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", 
+      "Topic :: Internet :: WWW/HTTP :: HTTP Servers", 
+      "Topic :: Internet :: WWW/HTTP :: WSGI", 
+      "Topic :: Software Development :: Bug Tracking", 
+      "Topic :: Software Development :: Libraries :: Application Frameworks", 
+      "Topic :: Software Development :: Libraries :: Python Modules", 
+      "Topic :: Software Development :: User Interfaces",
+      "Topic :: Software Development :: Widget Sets"
+    ]
+
+# Be compatible with older versions of Python
+from sys import version
+if version < '2.2.3':
+    from distutils.dist import DistributionMetadata
+    DistributionMetadata.classifiers = None
+    DistributionMetadata.download_url = None
+
+# Add the change log to the package description.
+chglog = None
+try:
+    from os.path import dirname, join
+    chglog = open(join(dirname(__file__), "CHANGES"))
+    DESC+= ('\n\n' + chglog.read())
+finally:
+    if chglog:
+        chglog.close()
+
+DIST_NM = 'BloodhoundDashboardPlugin'
+PKG_INFO = {'bhdashboard' : ('bhdashboard',                     # Package dir
+                            # Package data
+                            ['../CHANGES', '../TODO', '../COPYRIGHT', 
+                              '../NOTICE', '../README', '../TESTING_README'
+                              'templates/*', 'htdocs/*'],
+                          ), 
+            'bhdashboard.widgets' : ('bhdashboard/widgets',     # Package dir
+                            # Package data
+                            ['templates/*', 'htdocs/*'],
+                          ), 
+            'bhdashboard.tests' : ('bhdashboard/tests',     # Package dir
+                            # Package data
+                            ['data/**'],
+                          ), 
+            }
+
+ENTRY_POINTS = r"""
+               [trac.plugins]
+               bhdashboard.api = bhdashboard.api
+               bhdashboard.web_ui = bhdashboard.web_ui
+               bhdashboard.widgets.report = bhdashboard.widgets.report
+               """
+
+setup(
+    name=DIST_NM,
+    version=latest,
+    description=DESC.split('\n', 1)[0],
+    requires = ['trac'],
+    tests_require = ['dutest>=0.2.4'],
+    install_requires = [
+        'setuptools>=0.6b1',
+        'Trac>=0.11',
+    ],
+    package_dir = dict([p, i[0]] for p, i in PKG_INFO.iteritems()),
+    packages = PKG_INFO.keys(),
+    package_data = dict([p, i[1]] for p, i in PKG_INFO.iteritems()),
+    include_package_data=True,
+    provides = ['%s (%s)' % (p, latest) for p in PKG_INFO.keys()],
+    obsoletes = ['%s (>=%s.0.0, <%s)' % (p, versions[-1][0], latest) \
+                  for p in PKG_INFO.keys()],
+    entry_points = ENTRY_POINTS,
+    classifiers = cats,
+    long_description= DESC
+    )
+

void/__init__.py

Empty file added.