You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by ju...@apache.org on 2013/01/08 15:37:01 UTC

svn commit: r1430304 [1/2] - in /incubator/bloodhound/branches/bep_0003_multiproduct: ./ bloodhound_dashboard/bhdashboard/ bloodhound_dashboard/bhdashboard/widgets/ bloodhound_multiproduct/multiproduct/ bloodhound_multiproduct/tests/ bloodhound_search/...

Author: jure
Date: Tue Jan  8 14:37:00 2013
New Revision: 1430304

URL: http://svn.apache.org/viewvc?rev=1430304&view=rev
Log:
Sync merge from trunk


Added:
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/model.py
      - copied unchanged from r1430287, incubator/bloodhound/trunk/bloodhound_dashboard/bhdashboard/model.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/admin.py
      - copied unchanged from r1430287, incubator/bloodhound/trunk/bloodhound_search/bhsearch/admin.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/default-pages/
      - copied from r1430287, incubator/bloodhound/trunk/bloodhound_search/bhsearch/default-pages/
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/query_parser.py
      - copied unchanged from r1430287, incubator/bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/api.py
      - copied unchanged from r1430287, incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/api.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/index_with_whoosh.py
      - copied unchanged from r1430287, incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/index_with_whoosh.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/ticket_search.py
      - copied unchanged from r1430287, incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/ticket_search.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/utils.py
      - copied unchanged from r1430287, incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/utils.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py
      - copied unchanged from r1430287, incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py
      - copied unchanged from r1430287, incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/ticket_search.py
      - copied unchanged from r1430287, incubator/bloodhound/trunk/bloodhound_search/bhsearch/ticket_search.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/whoosh_backend.py
      - copied unchanged from r1430287, incubator/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py
Modified:
    incubator/bloodhound/branches/bep_0003_multiproduct/   (props changed)
    incubator/bloodhound/branches/bep_0003_multiproduct/.rat-ignore
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/admin.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/macros.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/web_ui.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/widgets/ticket.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/model.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/model.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/templates/bhsearch.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/js/theme.js
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_perms.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket_box.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bloodhound_theme.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/theme.py
    incubator/bloodhound/branches/bep_0003_multiproduct/installer/bloodhound_setup.py
    incubator/bloodhound/branches/bep_0003_multiproduct/trac/   (props changed)
    incubator/bloodhound/branches/bep_0003_multiproduct/trac/trac/mimeview/patch.py

Propchange: incubator/bloodhound/branches/bep_0003_multiproduct/
------------------------------------------------------------------------------
    svn:mergeinfo = /incubator/bloodhound/trunk:1420072-1430287

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/.rat-ignore
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/.rat-ignore?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/.rat-ignore (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/.rat-ignore Tue Jan  8 14:37:00 2013
@@ -1,6 +1,12 @@
-trac/contrib/
 .rat-ignore
+**/CHANGES
 **/MANIFEST.in
+**/TESTING_README
+**/TODO
 bloodhound_dashboard/bhdashboard/default-pages/
+bloodhound_search/bhsearch/default-pages/
+doc/html-templates/js/jquery-1.8.2.js
 doc/wireframes/src/
+installer/README.rst
+trac/
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/admin.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/admin.py?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/admin.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/admin.py Tue Jan  8 14:37:00 2013
@@ -23,16 +23,41 @@ r"""Project dashboard for Apache(TM) Blo
 
 Administration commands for Bloodhound Dashboard.
 """
+import json
 import pkg_resources
+from sys import stdout
 
 from trac.admin.api import IAdminCommandProvider, AdminCommandError
 from trac.core import Component, implements
+from trac.db_default import schema as tracschema
 from trac.util.text import printout
 from trac.util.translation import _
 from trac.wiki.admin import WikiAdmin
 from trac.wiki.model import WikiPage
 from bhdashboard import wiki
 
+try:
+    from multiproduct.model import Product, ProductResourceMap
+except ImportError:
+    Product = None
+    ProductResourceMap = None
+
+schema = tracschema[:]
+if Product is not None:
+    schema.extend([Product._get_schema(), ProductResourceMap._get_schema()])
+
+structure = dict([(table.name, [col.name for col in table.columns])
+                  for table in schema])
+
+# add product for any columns required
+for table in ['ticket',]:
+    structure[table].append('product')
+
+# probably no point in keeping data from these tables
+ignored = ['auth_cookie', 'session', 'session_attribute', 'cache']
+IGNORED_DB_STRUCTURE = dict([(k, structure[k]) for k in ignored])
+DB_STRUCTURE = dict([(k, structure[k]) for k in structure if k not in ignored])
+
 class BloodhoundAdmin(Component):
     """Bloodhound administration commands.
     """
@@ -47,6 +72,21 @@ class BloodhoundAdmin(Component):
                 'Move Trac* wiki pages to %s/*' % wiki.GUIDE_NAME,
                 None, self._do_wiki_upgrade)
 
+        yield ('devfixture dump', '[filename]',
+               """Dumps database to stdout in a form suitable for reloading
+
+               If a filename is not provided, data will be sent standard out.
+               """,
+               None, self._dump_as_fixture)
+
+        yield ('devfixture load', '<filename> <backedup>',
+               """Loads database fixture from json dump file
+
+               You need to specify a filename and confirm that you have backed
+               up your data.
+               """,
+               None, self._load_fixture_from_file)
+
     def _do_wiki_upgrade(self):
         """Move all wiki pages starting with Trac prefix to unbranded user
         guide pages.
@@ -95,3 +135,56 @@ class BloodhoundAdmin(Component):
                             WHERE name=%s
                             """, 
                          (re.sub(r'\b%s\b' % old_name, new_name, text), name))
+
+    def _get_tdump(self, db, table, fields):
+        """Dumps all the data from a table for a known set of fields"""
+        return db("SELECT %s from %s" %(', '.join(fields), table))
+
+    def _dump_as_fixture(self, *args):
+        """Dumps database to a json fixture"""
+        def dump_json(fp):
+            """Dump to json given a file"""
+            with self.env.db_query as db:
+                data = [(k, v, self._get_tdump(db, k, v))
+                        for k, v in DB_STRUCTURE.iteritems()]
+                jd = json.dumps(data, sort_keys=True, indent=2,
+                                separators=(',', ':'))
+                fp.write(jd)
+
+        if len(args):
+            f = open(args[0], mode='w+')
+            dump_json(f)
+            f.close()
+        else:
+            dump_json(stdout)
+
+    def _load_fixture_from_file(self, fname):
+        """Calls _load_fixture with an open file"""
+        try:
+            fp = open(fname, mode='r')
+            self._load_fixture(fp)
+            fp.close()
+        except IOError:
+            printout(_("The file '%(fname)s' does not exist", fname=fname))
+
+    def _load_fixture(self, fp):
+        """Extract fixture data from a file like object, expecting json"""
+        # Only delete if we think it unlikely that there is data to lose
+        with self.env.db_query as db:
+            if db('SELECT * FROM ticket'):
+                printout(_("This command is only intended to run on fresh "
+                           "environments as it will overwrite the database.\n"
+                           "If it is safe to lose bloodhound data, delete the "
+                           "environment and re-run python bloodhound_setup.py "
+                           "before attempting to load the fixture again."))
+                return
+        data = json.load(fp)
+        with self.env.db_transaction as db:
+            for tab, cols, vals in data:
+                db("DELETE FROM %s" %(tab))
+            for tab, cols, vals in data:
+                printout("Populating %s table" % tab)
+                db.executemany("INSERT INTO %s (%s) VALUES (%s)" % (tab,
+                        ','.join(cols), ','.join(['%s' for c in cols])), vals)
+                printout("%d records added" % len(vals))
+                

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/macros.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/macros.py?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/macros.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/macros.py Tue Jan  8 14:37:00 2013
@@ -25,7 +25,7 @@ from trac.util.translation import _, cle
 from trac.wiki.api import WikiSystem
 from trac.wiki.macros import WikiMacroBase
 
-from bhdashboard.admin import GUIDE_NAME
+from bhdashboard.wiki import GUIDE_NAME
 
 class UserGuideTocMacro(WikiMacroBase):
     _description = cleandoc_("""Display a Guide table of contents

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=1430304&r1=1430303&r2=1430304&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 Jan  8 14:37:00 2013
@@ -196,7 +196,7 @@ class DashboardModule(Component):
                                     'query' : 'status=!closed&group=milestone'\
                                         '&col=id&col=summary&col=owner' \
                                         '&col=status&col=priority&' \
-                                        'order=priority&desc=1',
+                                        'order=priority',
                                     'title' : 'Active Tickets'}}],
                             'altlinks' : False
                         },
@@ -209,7 +209,7 @@ class DashboardModule(Component):
                                     'query' : 'status=!closed&group=milestone'\
                                         '&col=id&col=summary&col=owner' \
                                         '&col=status&col=priority&' \
-                                        'order=priority&desc=1&' \
+                                        'order=priority&' \
                                         'owner=$USER',
                                     'title' : 'My Tickets'}
                                 }],

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=1430304&r1=1430303&r2=1430304&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 Jan  8 14:37:00 2013
@@ -143,7 +143,7 @@ class TicketFieldValuesWidget(WidgetBase
                     admin_suffix = field_maps.get(fieldnm)['admin_url']
                     if 'TICKET_ADMIN' in req.perm and admin_suffix is not None:
                         hint = _('You can add one or more '
-                                 '<a href="%(url)s">here</a>',
+                                 '<a href="%(url)s">here</a>.',
                                 url=req.href.admin('ticket', admin_suffix))
                     else:
                         hint = _('Contact your administrator for further details')
@@ -153,9 +153,10 @@ class TicketFieldValuesWidget(WidgetBase
                                             field=field_maps[fieldnm]['title'])),
                                 'data' : dict(msgtype='info',
                                     msglabel="Note",
-                                    msgbody=Markup(_('''There is no value defined
-                                        for ticket field <em>%(field)s</em>. 
-                                        %(hint)s''', field=fieldnm, hint=hint) )
+                                    msgbody=Markup(_('''No values are
+                                        defined for ticket field
+                                        <em>%(field)s</em>. %(hint)s''',
+                                        field=fieldnm, hint=hint))
                                     )
                             }, context
                 else:

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py Tue Jan  8 14:37:00 2013
@@ -31,7 +31,7 @@ from trac.ticket.api import ITicketField
 from trac.util.translation import _, N_
 from trac.web.chrome import ITemplateProvider
 
-from multiproduct.model import Product
+from multiproduct.model import Product, ProductResourceMap
 
 DB_VERSION = 3
 DB_SYSTEM_KEY = 'bloodhound_multi_product_version'
@@ -43,20 +43,8 @@ class MultiProductSystem(Component):
     implements(IEnvironmentSetupParticipant, ITemplateProvider,
             IPermissionRequestor, ITicketFieldProvider, IResourceManager)
     
-    SCHEMA = [
-        Table('bloodhound_product', key = ['prefix', 'name']) [
-            Column('prefix'),
-            Column('name'),
-            Column('description'),
-            Column('owner'),
-            ],
-        Table('bloodhound_productresourcemap', key = 'id') [
-            Column('id', auto_increment=True),
-            Column('product_id'),
-            Column('resource_type'),
-            Column('resource_id'),
-            ]
-        ]
+    SCHEMA = [mcls._get_schema() for mcls in (Product, ProductResourceMap)]
+    del mcls
     
     def get_version(self):
         """Finds the current version of the bloodhound database schema"""

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/model.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/model.py?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/model.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/model.py Tue Jan  8 14:37:00 2013
@@ -19,219 +19,14 @@
 """Models to support multi-product"""
 from datetime import datetime
 
-from pkg_resources import resource_filename
 from trac.core import TracError
-from trac.resource import ResourceNotFound
-from trac.db import Table, Column, DatabaseManager
 from trac.resource import Resource
-from trac.ticket.api import TicketSystem
 from trac.ticket.model import Ticket
 from trac.ticket.query import Query
 from trac.util.datefmt import utc
 
-def dict_to_kv_str(data=None, sep=' AND '):
-    """Converts a dictionary into a string and a list suitable for using as part
-    of an SQL where clause like:
-        ('key0=%s AND key1=%s', ['value0','value1'])
-    The sep argument allows ' AND ' to be changed for ',' for UPDATE purposes
-    """
-    if data is None:
-        return ('', [])
-    return (sep.join(['%s=%%s' % k for k in data.keys()]), data.values())
+from bhdashboard.model import ModelBase
 
-def fields_to_kv_str(fields, data, sep=' AND '):
-    """Converts a list of fields and a dictionary containing those fields into a
-    string and a list suitable for using as part of an SQL where clause like:
-        ('key0=%s,key1=%s', ['value0','value1'])
-    """
-    return dict_to_kv_str(dict([(f, data[f]) for f in fields]), sep)
-
-class ModelBase(object):
-    """Base class for the models to factor out common features
-    Derived classes should provide a meta dictionary to describe the table like:
-    
-    _meta = {'table_name':'mytable',
-             'object_name':'WhatIWillCallMyselfInMessages',
-             'key_fields':['id','id2'],
-             'non_key_fields':['thing','anotherthing'],
-             }
-    """
-    
-    def __init__(self, env, keys=None):
-        """Initialisation requires an environment to be specified.
-        If keys are provided, the Model will initialise from the database
-        """
-        # make this impossible to instantiate without telling the class details
-        # about itself in the self.meta dictionary
-        self._old_data = {}
-        self._data = {}
-        self._exists = False
-        self._env = env
-        self._all_fields = self._meta['key_fields'] + \
-                           self._meta['non_key_fields']
-        if keys is not None:
-            self._get_row(keys)
-        else:
-            self._update_from_row(None)
-    
-    def update_field_dict(self, field_dict):
-        """Updates the object's copy of the db fields (no db transaction)"""
-        self._data.update(field_dict)
-    
-    def __getattr__(self, name):
-        """Overridden to allow table.field style field access."""
-        try:
-            if name in self._all_fields:
-                return self._data[name]
-        except KeyError:
-            raise AttributeError(name)
-        raise AttributeError(name)
-    
-    def __setattr__(self, name, value):
-        """Overridden to allow table.field = value style field setting."""
-        data = self.__dict__.get('data')
-        fields = self.__dict__.get('fields')
-        
-        if data and fields and name in fields:
-            self._data[name] = value
-        else:
-            dict.__setattr__(self, name, value)
-            
-    
-    def _update_from_row(self, row = None):
-        """uses a provided database row to update the model"""
-        fields = self._meta['key_fields']+self._meta['non_key_fields']
-        self._exists = row is not None
-        if row is None:
-            row = [None]*len(fields)
-        self._data = dict([(fields[i], row[i]) for i in range(len(row))])
-        self._old_data = {}
-        self._old_data.update(self._data)
-    
-    def _get_row(self, keys):
-        """queries the database and stores the result in the model"""
-        row = None
-        where, values = fields_to_kv_str(self._meta['key_fields'], keys)
-        fields = ','.join(self._meta['key_fields']+self._meta['non_key_fields'])
-        sdata = {'fields':fields,
-                 'where':where}
-        sdata.update(self._meta)
-        
-        sql = """SELECT %(fields)s FROM %(table_name)s
-                 WHERE %(where)s""" % sdata
-        with self._env.db_query as db:
-            for row in db(sql, values):
-                self._update_from_row(row)
-                break
-            else:
-                raise ResourceNotFound('No %(object_name)s with %(where)s' %
-                                sdata)
-    
-    def delete(self):
-        """Deletes the matching record from the database"""
-        if not self._exists:
-            raise TracError('%(object_name)s does not exist' % self._meta)
-        where, values = fields_to_kv_str(self._meta['key_fields'], self._data)
-        sdata = {'where': where}
-        sdata.update(self._meta)
-        sql = """DELETE FROM %(table_name)s
-                 WHERE %(where)s""" % sdata
-        with self._env.db_transaction as db:
-            db(sql, values)
-            self._exists = False
-            self._data = dict([(k, None) for k in self._data.keys()])
-            self._old_data.update(self._data)
-            TicketSystem(self._env).reset_ticket_fields()
-    
-    def insert(self):
-        """Create new record in the database"""
-        sdata = None
-        if self._exists or len(self.select(self._env, where =
-                                dict([(k,self._data[k])
-                                      for k in self._meta['key_fields']]))):
-            sdata = {'keys':','.join(["%s='%s'" % (k, self._data[k])
-                                     for k in self._meta['key_fields']])}
-        elif len(self.select(self._env, where =
-                                dict([(k,self._data[k])
-                                      for k in self._meta['unique_fields']]))):
-            sdata = {'keys':','.join(["%s='%s'" % (k, self._data[k])
-                                     for k in self._meta['unique_fields']])}
-        if sdata:
-            sdata.update(self._meta)
-            raise TracError('%(object_name)s %(keys)s already exists' %
-                            sdata)
-            
-        for key in self._meta['key_fields']:
-            if not self._data[key]:
-                sdata = {'key':key}
-                sdata.update(self._meta)
-                raise TracError('%(key)s required for %(object_name)s' %
-                                sdata)
-        fields = self._meta['key_fields']+self._meta['non_key_fields']
-        sdata = {'fields':','.join(fields),
-                 'values':','.join(['%s'] * len(fields))}
-        sdata.update(self._meta)
-        
-        sql = """INSERT INTO %(table_name)s (%(fields)s)
-                 VALUES (%(values)s)""" % sdata
-        with self._env.db_transaction as db:
-            db(sql, [self._data[f] for f in fields])
-            self._exists = True
-            self._old_data.update(self._data)
-            TicketSystem(self._env).reset_ticket_fields()
-
-    def _update_relations(self, db):
-        """Extra actions due to update"""
-        pass
-    
-    def update(self):
-        """Update the matching record in the database"""
-        if self._old_data == self._data:
-            return 
-        if not self._exists:
-            raise TracError('%(object_name)s does not exist' % self._meta)
-        for key in self._meta['no_change_fields']:
-            if self._data[key] != self._old_data[key]:
-                raise TracError('%s cannot be changed' % key)
-        for key in self._meta['key_fields'] + self._meta['unique_fields']:
-            if self._data[key] != self._old_data[key]:
-                if len(self.select(self._env, where = {key:self._data[key]})):
-                    raise TracError('%s already exists' % key)
-        
-        setsql, setvalues = fields_to_kv_str(self._meta['non_key_fields'],
-                                             self._data, sep=',')
-        where, values = fields_to_kv_str(self._meta['key_fields'], self._data)
-        
-        sdata = {'where': where,
-                 'values': setsql}
-        sdata.update(self._meta)
-        sql = """UPDATE %(table_name)s SET %(values)s
-                 WHERE %(where)s""" % sdata
-        with self._env.db_transaction as db:
-            db(sql, setvalues + values)
-            self._update_relations(db)
-            self._old_data.update(self._data)
-            TicketSystem(self._env).reset_ticket_fields()
-    
-    @classmethod
-    def select(cls, env, db=None, where=None):
-        """Query the database to get a set of records back"""
-        rows = []
-        fields = cls._meta['key_fields']+cls._meta['non_key_fields']
-        
-        sdata = {'fields':','.join(fields),}
-        sdata.update(cls._meta)
-        sql = r'SELECT %(fields)s FROM %(table_name)s' % sdata
-        wherestr, values = dict_to_kv_str(where)
-        if wherestr:
-            wherestr = ' WHERE ' + wherestr
-        for row in env.db_query(sql + wherestr, values):
-            # we won't know which class we need until called
-            model = cls.__new__(cls)
-            data = dict([(fields[i], row[i]) for i in range(len(fields))])
-            model.__init__(env, data)
-            rows.append(model)
-        return rows
 
 class Product(ModelBase):
     """The Product table"""
@@ -305,6 +100,7 @@ class ProductResourceMap(ModelBase):
             'non_key_fields':['product_id','resource_type','resource_id',],
             'no_change_fields':['id',],
             'unique_fields':[],
+            'auto_inc_fields': ['id'],
             }
     
     def reparent_resource(self, product=None):

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/model.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/model.py?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/model.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/model.py Tue Jan  8 14:37:00 2013
@@ -25,6 +25,7 @@ from sqlite3 import OperationalError
 
 from trac.test import EnvironmentStub
 from trac.core import TracError
+
 from multiproduct.model import Product
 from multiproduct.api import MultiProductSystem
 
@@ -158,6 +159,6 @@ class ProductTestCase(unittest.TestCase)
         product.description = new_description
         self.assertEqual(new_description, product.description)
 
-if __name__ == '__main_':
+if __name__ == '__main__':
     unittest.main()
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py Tue Jan  8 14:37:00 2013
@@ -18,66 +18,177 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
-"""Core Bloodhound Search components"""
+r"""Core Bloodhound Search components."""
 
 from trac.core import *
+from trac.config import ExtensionOption
 
-class BloodhoundQuerySystem(Component):
-    """Implements core query functionality.
+ASC = "asc"
+DESC = "desc"
+SCORE = "score"
+
+class QueryResult(object):
+    def __init__(self):
+        self.hits = 0
+        self.page_count = 0
+        self.page_number = 0
+        self.offset = 0
+        self.docs = []
+        self.facets = None
+
+
+class ISearchBackend(Interface):
+    """Extension point interface for search backend systems.
     """
 
-    def query(self, query, sort = None, boost = None, filters = None,
-              facets = None, start = 0, rows = None):
-        """Return query result from on underlying backend.
+    def add_doc(self, doc, commit=True):
+        """
+        Called when new document instance must be added
+
+        :param doc: document to add
+        :param commit: flag if commit should be automatically called
+        """
+
+    def delete_doc(self, doc, commit=True):
+        """
+        Delete document from index
+
+        :param doc: document to delete
+        :param commit: flag if commit should be automatically called
+        """
+
+    def commit(self):
+        """
+        Commits changes
+        """
+
+    def optimize(self):
+        """
+        Optimize index if needed
+        """
+
+    def recreate_index(self):
+        """
+        Create a new index, if index exists, it will be deleted
+        """
+
+    def open_or_create_index_if_missing(self):
+        """
+        Open existing index, if index does not exist, create new one
+        """
+    def query(self, query, sort = None, fields = None, boost = None, filters = None,
+                  facets = None, pagenum = 1, pagelen = 20):
+        """
+        Perform query implementation
+
+        :param query:
+        :param sort:
+        :param fields:
+        :param boost:
+        :param filters:
+        :param facets:
+        :param pagenum:
+        :param pagelen:
+        :return: TBD!!!
+        """
+        pass
+
+class ISearchParticipant(Interface):
+    """Extension point interface for components that should be searched.
+    """
+
+    def get_search_filters(req):
+        """Called when we want to build the list of components with search.
+        Passes the request object to do permission checking."""
+        pass
+
+    def build_search_index(backend):
+        """Called when we want to rebuild the entire index.
+        :type backend: ISearchBackend
+        """
+        pass
+
+    def format_search_results(contents):
+        """Called to see if the module wants to format the search results."""
+
+class IQueryParser(Interface):
+    """Extension point for Bloodhound Search query parser.
+    """
+
+    def parse(query_string, req = None):
+        pass
+
+class BloodhoundSearchApi(Component):
+    """Implements core indexing functionality, provides methods for
+    searching, adding and deleting documents from index.
+    """
+    backend = ExtensionOption('bhsearch', 'search_backend',
+        ISearchBackend, 'WhooshBackend',
+        'Name of the component implementing Bloodhound Search backend \
+        interface: ISearchBackend.')
+
+    parser = ExtensionOption('bhsearch', 'query_parser',
+        IQueryParser, 'DefaultQueryParser',
+        'Name of the component implementing Bloodhound Search query \
+        parser.')
+
+    search_participants = ExtensionPoint(ISearchParticipant)
+
+    def query(self, query, req = None, sort = None, fields = None, boost = None, filters = None,
+                  facets = None, pagenum = 1, pagelen = 20):
+        """Return query result from an underlying search backend.
 
         Arguments:
-        query -- query string e.g. “bla status:closed” or a parsed
+        :param query: query string e.g. “bla status:closed” or a parsed
             representation of the query.
-        sort -- optional sorting
-        boost -- optional list of fields with boost values e.g.
+        :param sort: optional sorting
+        :param boost: optional list of fields with boost values e.g.
             {“id”: 1000, “subject” :100, “description”:10}.
-        filters -- optional list of terms. Usually can be cached by underlying
+        :param filters: optional list of terms. Usually can be cached by underlying
             search framework. For example {“type”: “wiki”}
-        facets - optional list of facet terms, can be field or expression.
-        start, rows -- paging support
+        :param facets: optional list of facet terms, can be field or expression.
+        :param page: paging support
+        :param pagelen: paging support
 
-        The result is returned as the following dictionary: {
-            "docs": [
-                {
-                    "id": "ticket:123",
-                    "resource_id": "123",
-                    "type": "ticket",
-                    ...
-                }
-            ],
-            "numFound":3,"
-            "start":0,
-            "facet_counts":{
-                "facet_fields":{
-                    "cat":[ "electronics",3, "card",2, "graphics",2, "music",1]
-                }
-            },
-        }
+        :return: result QueryResult
         """
         self.env.log.debug("Receive query request: %s", locals())
 
-        #TODO: add implementation here
-        dummy_result = dict(docs = [
-            dict(
-                resource_id = "123",
-                summary = "Dummy result for query: " + (query or ''),
-            )
-        ])
-        return dummy_result
+        # TODO: add query parsers and meta keywords post-parsing
+
+        # TODO: apply security filters
+
+        parsed_query = self.parser.parse(query, req)
+
+        #some backend-independent logic will come here...
+        query_result = self.backend.query(
+            query = parsed_query,
+            sort = sort,
+            fields = fields,
+            filters = filters,
+            facets = facets,
+            pagenum = pagenum,
+            pagelen = pagelen,
+        )
+
+        return query_result
 
-class BloodhoundIndexSystem(Component):
-    """Implements core indexing functionality, provides methods for
-    adding and deleting documents form index.
-    """
 
     def rebuild_index(self):
-        """Erase the index if it exists. Then create a new index from scratch.
-        """
+        """Delete the index if it exists. Then create a new full index."""
+        self.log.info('Rebuilding the search index.')
+        self.backend.recreate_index()
+
+        for participant in self.search_participants:
+            participant.build_search_index(self.backend)
+        self.backend.commit()
+        self.backend.optimize()
+
+        #Erase the index if it exists. Then create a new index from scratch.
+
+        #erase ticket
+        #call reindex for each resource
+        #commit
         pass
 
     def optimize(self):
@@ -89,12 +200,14 @@ class BloodhoundIndexSystem(Component):
 
         The doc must be dictionary with obligatory "type" field
         """
-        pass
+        self.backend.add_doc(doc)
 
-    def delete_doc(self, doc):
+    def delete_doc(self, type, id):
         """Add a document from underlying search backend.
 
         The doc must be dictionary with obligatory "type" field
         """
         pass
 
+
+

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/templates/bhsearch.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/templates/bhsearch.html?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/templates/bhsearch.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/templates/bhsearch.html Tue Jan  8 14:37:00 2013
@@ -27,9 +27,15 @@
   <xi:include href="layout.html" />
   <head>
     <title py:choose="">
-      <py:when test="query">Search Results</py:when>
-      <py:otherwise>Search</py:otherwise>
+      <py:when test="query">Bloodhound Search Results</py:when>
+      <py:otherwise>Bloodhound Search</py:otherwise>
     </title>
+    <py:if test="results">
+        <meta name="startIndex" content="${results.span[0] + 1}"/>
+        <meta name="totalResults" content="$results.num_items"/>
+        <meta name="itemsPerPage" content="$results.max_per_page"/>
+    </py:if>
+
     <script type="text/javascript">
       jQuery(document).ready(function($) {$("#q").get(0).focus()});
     </script>
@@ -37,22 +43,35 @@
   <body>
     <div id="content" class="search">
 
-      <h1>This is dummy page. Implementation is coming...</h1>
+      <h1>This page provides prototype functionality. Implementation is coming...</h1>
       <h1><label for="q">Search</label></h1>
-      <form id="bhsearch" action="${href.bhsearch()}" method="get">
+      <form id="fullsearch" action="${href.bhsearch()}" method="get">
         <p>
           <input type="text" id="q" name="q" size="40" value="${query}" />
+          <input type="hidden" name="noquickjump" value="1" />
           <input type="submit" value="${_('Search')}" />
         </p>
       </form>
 
       <py:if test="results"><hr />
         <h2 py:if="results">
-          Results <span class="numresults">(${results.displayed_items()})</span>
+          Results <small>(${results.displayed_items()})</small>
         </h2>
-        <xi:include py:with="paginator = results" href="page_index.html" />
         <div>
           <dl id="results">
+
+            <!--This just a prototype stub. Should be replaced by proper ui mocks-->
+            <div>
+              <ul class="nav nav-tabs" id="mainnav">
+                <!--<li py:if="chrome.nav.mainnav"-->
+                    <!--py:for="idx, item in enumerate(i for i in chrome.nav.mainnav if i.name in mainnav_show)" -->
+                    <!--class="${classes(first_last(idx, chrome.nav.mainnav), active=item.active)}">${item.label}</li>-->
+                <li class="$active}"><a href="${page_href}">All (XXX)</a></li>
+                <li class=""><a href="${page_href}">Wiki (XXX)</a></li>
+                <li class=""><a href="${page_href}">Tickets (XXX)</a></li>
+              </ul>
+            </div>
+
             <py:for each="result in results">
               <dt><a href="${result.href}" class="searchable">${result.title}</a></dt>
               <dd class="searchable">${result.excerpt}</dd>
@@ -63,12 +82,21 @@
             </py:for>
           </dl>
         </div>
-        <xi:include py:with="paginator = results" href="page_index.html" />
+        <xi:include py:with="paginator = results" href="bh_page_index.html" />
       </py:if>
 
-      <div id="notfound" py:if="query and not (results)">
-        No matches found.
+      <div class="span12"
+          py:if="query and not (results or quickjump)">
+        <p id="notfound" class="alert">
+          No matches found.
+        </p>
       </div>
+
+      <div id="help" class="help-block pull-right" i18n:msg="">
+        <strong>Note:</strong> See <a href="${href.wiki('BloodhoundSearch')}">BloodhoundSearch</a>
+        for help on searching.
+      </div>
+
     </div>
   </body>
 </html>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py Tue Jan  8 14:37:00 2013
@@ -17,4 +17,18 @@
 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
+import doctest
+import unittest
+from bhsearch.tests import whoosh_backend, index_with_whoosh, web_ui, ticket_search, api
 
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(index_with_whoosh.suite())
+    suite.addTest(whoosh_backend.suite())
+    suite.addTest(web_ui.suite())
+    suite.addTest(ticket_search.suite())
+    suite.addTest(api.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
\ No newline at end of file

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py Tue Jan  8 14:37:00 2013
@@ -18,7 +18,7 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
-"""Bloodhound Search user interface"""
+r"""Bloodhound Search user interface."""
 
 import pkg_resources
 import re
@@ -26,12 +26,15 @@ import re
 from trac.core import *
 from genshi.builder import tag
 from trac.perm import IPermissionRequestor
+from trac.search import shorten_result
+from trac.util.presentation import Paginator
+from trac.util.datefmt import format_datetime, user_time
 from trac.web import IRequestHandler
 from trac.util.translation import _
 from trac.web.chrome import (INavigationContributor, ITemplateProvider,
                              add_link, add_stylesheet, add_warning,
                              web_context)
-from bhsearch.api import BloodhoundQuerySystem
+from bhsearch.api import BloodhoundSearchApi, ISearchParticipant, SCORE, ASC, DESC
 
 SEARCH_PERMISSION = 'SEARCH_VIEW'
 
@@ -43,6 +46,12 @@ class BloodhoundSearchModule(Component):
     #           IWikiSyntaxProvider #todo: implement later
     )
 
+    search_participants = ExtensionPoint(ISearchParticipant)
+
+    RESULTS_PER_PAGE = 10
+    DEFAULT_SORT = [(SCORE, ASC), ("time", DESC)]
+
+
     # INavigationContributor methods
     def get_active_navigation_item(self, req):
         return 'bhsearch'
@@ -65,18 +74,130 @@ class BloodhoundSearchModule(Component):
         req.perm.assert_permission(SEARCH_PERMISSION)
 
         query = req.args.get('q')
+        if query == None:
+            query = ""
+
+        #TODO add quick jump support
 
-        data = {}
-        if query:
-            data["query"] = query
-
-        #TODO: add implementation here
-        querySystem = BloodhoundQuerySystem(self.env)
-        result = querySystem.query(query)
+        #TODO: refactor filters or replace with facets
+        filters = []
+#        available_filters = filter(None, [p.get_search_filters(req) for p
+#            in self.search_participants])
+#        filters = [f[0] for f in available_filters if req.args.has_key(f[0])]
+#        if not filters:
+#            filters = [f[0] for f in available_filters
+#                       if f[0] not in self.default_disabled_filters and
+#                       (len(f) < 3 or len(f) > 2 and f[2])]
+#        data = {'filters': [{'name': f[0], 'label': f[1],
+#                             'active': f[0] in filters}
+#                            for f in available_filters],
+#                'quickjump': None,
+#                'results': []}
+
+        data = {
+      			'query': query,
+      		}
+
+        # Initial page request
+        #todo: filters check, tickets etc
+        if not any((query, )):
+            return self._return_data(req, data)
+
+        page = int(req.args.get('page', '1'))
+
+        #todo: retrieve sort from query string
+        sort = self.DEFAULT_SORT
+
+        #todo: add proper facets functionality
+#        facets = ("type", "status")
+        facets = ("type",)
+
+
+        querySystem = BloodhoundSearchApi(self.env)
+        query_result = querySystem.query(
+            query,
+            pagenum = page,
+            pagelen = self.RESULTS_PER_PAGE,
+            sort = sort,
+            facets = facets,
+        )
+        ui_docs = [self._process_doc(doc, req)
+                   for doc in query_result.docs]
+
+
+        results = Paginator(
+            ui_docs,
+            page - 1,
+            self.RESULTS_PER_PAGE,
+            query_result.hits,
+        )
+
+        results.shown_pages = self._prepare_shown_pages(
+            filters,
+            query,
+            req,
+            shown_pages = results.get_shown_pages(self.RESULTS_PER_PAGE))
+
+        results.current_page = {'href': None, 'class': 'current',
+                                'string': str(results.page + 1),
+                                'title':None}
+
+        if results.has_next_page:
+            next_href = req.href.bhsearch(zip(filters, ['on'] * len(filters)),
+                                        q=req.args.get('q'), page=page + 1,
+                                        noquickjump=1)
+            add_link(req, 'next', next_href, _('Next Page'))
+
+        if results.has_previous_page:
+            prev_href = req.href.bhsearch(zip(filters, ['on'] * len(filters)),
+                                        q=req.args.get('q'), page=page - 1,
+                                        noquickjump=1)
+            add_link(req, 'prev', prev_href, _('Previous Page'))
+
+        data['results'] = results
+
+        #add proper facet links
+        data['facets'] = query_result.facets
+
+        data['page_href'] = req.href.bhsearch(
+            zip(filters, ['on'] * len(filters)), q=req.args.get('q'),
+            noquickjump=1)
+        return self._return_data(req, data)
 
+    def _return_data(self, req, data):
         add_stylesheet(req, 'common/css/search.css')
         return 'bhsearch.html', data, None
 
+    def _process_doc(self, doc,req):
+        titlers = dict([(x.get_search_filters(req)[0], x.format_search_results)
+            for x in self.search_participants if x.get_search_filters(req)])
+
+        #todo: introduce copy by predefined value
+        ui_doc = dict(doc)
+
+        ui_doc["href"] = req.href(doc['type'], doc['id'])
+        #todo: perform content adaptation here
+        if doc.has_key('content'):
+            ui_doc['excerpt'] = shorten_result(doc['content'])
+        if doc.has_key('time'):
+            ui_doc['date'] = user_time(req, format_datetime, doc['time'])
+
+        ui_doc['title'] = titlers[doc['type']](doc)
+        return ui_doc
+
+    def _prepare_shown_pages(self, filters, query, req, shown_pages):
+        pagedata = []
+        for shown_page in shown_pages:
+            page_href = req.href.bhsearch([(f, 'on') for f in filters],
+                q=query,
+                page=shown_page, noquickjump=1)
+            pagedata.append([page_href, None, str(shown_page),
+                             'page ' + str(shown_page)])
+        fields = ['href', 'class', 'string', 'title']
+        result_shown_pages = [dict(zip(fields, p)) for p in pagedata]
+        return result_shown_pages
+
+
     # ITemplateProvider methods
     def get_htdocs_dirs(self):
 #        return [('bhsearch', pkg_resources.resource_filename(__name__, 'htdocs'))]
@@ -85,3 +206,4 @@ class BloodhoundSearchModule(Component):
     def get_templates_dirs(self):
         return [pkg_resources.resource_filename(__name__, 'templates')]
 
+

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py Tue Jan  8 14:37:00 2013
@@ -29,7 +29,7 @@ Add free text search and query functiona
 """
 
 versions = [
-    (0, 4, 0),
+    (0, 4, 1),
     ]
 
 latest = '.'.join(str(x) for x in versions[-1])
@@ -95,11 +95,11 @@ PKG_INFO = {'bhsearch' : ('bhsearch',   
                               'htdocs/img/*.*', 'htdocs/js/*.js',
                               'templates/*', 'default-pages/*'],
                           ),
-#            'search.widgets' : ('search/widgets',     # Package dir
+#            'search.widgets' : ('bhsearch/widgets',     # Package dir
 #                            # Package data
 #                            ['templates/*', 'htdocs/*.css'],
 #                          ),
-#            'search.layouts' : ('search/layouts',     # Package dir
+#            'search.layouts' : ('bhsearch/layouts',     # Package dir
 #                            # Package data
 #                            ['templates/*'],
 #                          ),
@@ -109,21 +109,38 @@ PKG_INFO = {'bhsearch' : ('bhsearch',   
                           ),
             }
 
-ENTRY_POINTS = r"""
-               [trac.plugins]
-               bhsearch.web_ui = bhsearch.web_ui
-               bhsearch.api = bhsearch.api
-               """
+#ENTRY_POINTS = r"""
+#               [trac.plugins]
+#               bhsearch.web_ui = bhsearch.web_ui
+#               bhsearch.api = bhsearch.api
+#               bhsearch.admin = bhsearch.admin
+#               bhsearch.ticket_search = bhsearch.ticket_search
+#               bhsearch.query_parser = bhsearch.query_parser
+#               bhsearch.whoosh_backend = bhsearch.whoosh_backend
+#               """
+ENTRY_POINTS = {
+        'trac.plugins': [
+            'bhsearch.web_ui = bhsearch.web_ui',
+            'bhsearch.api = bhsearch.api',
+            'bhsearch.admin = bhsearch.admin',
+            'bhsearch.ticket_search = bhsearch.ticket_search',
+            'bhsearch.query_parser = bhsearch.query_parser',
+            'bhsearch.whoosh_backend = bhsearch.whoosh_backend',
+        ],
+    }
+#bhsearch.whoosh_backend = bhsearch.whoosh_backend
+#bhsearch.ticket_search = bhsearch.ticket_search
 
 setup(
     name=DIST_NM,
     version=latest,
     description=DESC.split('\n', 1)[0],
     requires = ['trac'],
-    tests_require = ['dutest>=0.2.4', 'TracXMLRPC'],
+#    tests_require = ['dutest>=0.2.4', 'TracXMLRPC'],
     install_requires = [
         'setuptools>=0.6b1',
         'Trac>=0.11',
+        'whoosh>=2.4.1',
     ],
     package_dir = dict([p, i[0]] for p, i in PKG_INFO.iteritems()),
     packages = PKG_INFO.keys(),

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css Tue Jan  8 14:37:00 2013
@@ -144,6 +144,10 @@ div.reports form {
 
 }
 
+#activityfeed img {
+  display: none;
+}
+
 #activityfeed dt {
   font-weight: normal;
 }
@@ -182,6 +186,10 @@ textarea.wikitext {
   border-radius: 10px;
 }
 
+pre.wiki {
+  overflow: auto;
+}
+
 .trac-modifiedby {
   float: right;
 }
@@ -202,20 +210,25 @@ textarea.wikitext {
   width: 505px;
 }
 
-#field-summary {
-  width: 505px;
-}
-
-#field-reporter {
-  width: 505px;
+#qct-fieldset #field-description {
+  width: auto;
 }
 
 .bh-ticket-buttons {
   padding-left: 5px;
 }
 
-.ticket .properties h5 {
-  margin-bottom: 0px;
+.ticket .properties .enum h5 {
+  margin-top: 0px;
+}
+
+.ownership {
+  margin-left: 25px;
+}
+
+.ticket textarea {
+  height: auto;
+  width: auto;
 }
 
 /* @end */
@@ -276,6 +289,9 @@ h1, h2, h3, h4 {
   white-space: nowrap;
 }
 
+.clip.edit-active, .affix .clip-affic.edit-active {
+  overflow: visible;
+}
 /* @end */
 
 /* @group Alternate download links */
@@ -453,6 +469,10 @@ input[type="submit"].btn.btn-micro {
   }
 }
 
+.help-msg[title] {
+  cursor: help;
+}
+
 /* Revert some changes introduced in 2.1.0 */
 
 h6 {

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/js/theme.js
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/js/theme.js?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/js/theme.js (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/js/theme.js Tue Jan  8 14:37:00 2013
@@ -22,8 +22,8 @@ $( function () {
     var qct_timeout = null;
 
     // Do not close dropdown menu if user interacts with form controls
-    $('.dropdown-menu input, .dropdown-menu label, .dropdown-menu select')
-        .click(function (e) { e.stopPropagation(); });
+    $('.dropdown-menu input, .dropdown-menu label, .dropdown-menu select' +
+        ', .dropdown-menu textarea').click(function (e) { e.stopPropagation(); });
 
     // Install popover for create ticket shortcut
     // Important: Further options specified in markup
@@ -57,7 +57,7 @@ $( function () {
 
     // Clear input controls inside quick create box
     function qct_clearui() {
-      $('#qct-fieldset input, #qct-fieldset select').val('');
+      $('#qct-fieldset input, #qct-fieldset select, #qct-fieldset textarea').val('');
     }
 
     // We want to submit via #qct-create
@@ -115,28 +115,30 @@ function setup_sticky_panel(selector) {
   var h = target.height();
   target.parent('.stickyBox').height(h);
 
-  // Create style tag to fix anchor position
-  function _sticky_offset_rules(_h) {
-    return '.stickyBox~* form[id], .stickyBox~* div[id] { margin-top:-' +
-      _h + 'px; padding-top: ' + _h + 'px } ' +
-      '.stickyBox, .stickyBox [id] { margin-top: 0px ; padding-top: 0px }';
-  }
-  $('<style id="sticky-offset" /> ').text( _sticky_offset_rules(h) )
-      .appendTo('head');
-
   target = h = null;
   $(window).on('scroll.affix.data-api', function() {
-      var affix_data = $(selector).data('affix');
       var target = $(selector);
+      var affix_data = target.data('affix');
 
       if (affix_data && !affix_data.affixed) {
         var h = target.height();
         target.parent('.stickyBox').height(h);
-        $('style#sticky-offset').text(_sticky_offset_rules(h))
       }
       else {
         target.parent('.stickyBox').css('height', '');
       }
     })
+  $(function() {
+      var prev_onhashchange = window.onhashchange;
+
+      window.onhashchange = function() {
+        prev_onhashchange();
+        var target = $(selector);
+        var affix_data = target.data('affix');
+    
+        if (affix_data && !affix_data.affixed)
+          window.scrollBy(0, -target.height());
+      }
+    })
 }
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_perms.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_perms.html?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_perms.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_perms.html Tue Jan  8 14:37:00 2013
@@ -23,26 +23,27 @@
 <html xmlns="http://www.w3.org/1999/xhtml"
       xmlns:xi="http://www.w3.org/2001/XInclude"
       xmlns:py="http://genshi.edgewall.org/"
-      xmlns:i18n="http://genshi.edgewall.org/i18n">
+      xmlns:i18n="http://genshi.edgewall.org/i18n"
+      py:with="can_revoke = 'PERMISSION_REVOKE' in perm">
   <xi:include href="bh_admin.html" />
   <head>
     <title>Permissions</title>
   </head>
 
   <body>
-    <h2>Manage Permissions</h2>
+    <h2>Manage Permissions and Groups</h2>
 
     <div class="row">
-      <div class="${'PERMISSION_GRANT' in perm and 'span6' or 'span9'}">
-        <form id="revokeform" method="post" 
-            py:with="revoke_perm = 'PERMISSION_REVOKE' in perm" action="">
+      <div class="${can_revoke and 'span6' or 'span9'}">
+        <form id="revokeform" method="post" action="">
+          <h3>Permissions</h3>
           <table class="table table-bordered table-striped table-condensed" 
               id="permlist">
             <thead>
               <tr><th>Subject</th><th class="full-x">Action</th></tr>
             </thead>
             <tbody>
-              <tr py:for="idx, (subject, perm_group) in enumerate(groupby(sorted(perms), key=lambda tmp: tmp[0]))"
+              <tr py:for="idx, (subject, perm_group) in enumerate(groupby(sorted(perms), key=lambda p: p[0]))"
                   class="${'odd' if idx % 2 else 'even'}">
                 <td>$subject</td>
                 <td>
@@ -53,7 +54,7 @@
                     <!--! base64 make it safe to use ':' as separator when passing
                           both subject and action as one query parameter -->
                     <label for="$subject_action_id" class="checkbox inline">
-                      <input py:if="revoke_perm" type="checkbox"
+                      <input py:if="can_revoke" type="checkbox"
                              id="$subject_action_id"
                              name="sel" value="$subject_action" />
                       $action
@@ -63,8 +64,39 @@
               </tr>
             </tbody>
           </table>
+
+          <h3>Group Membership</h3>
+          <table class="table table-bordered table-striped table-condensed" 
+              id="grouplist">
+            <thead>
+              <tr><th>Subject</th><th class="full-x">Action</th></tr>
+            </thead>
+            <tbody>
+              <tr py:for="idx, (group, subj_group) in enumerate(groupby(sorted(groups, key=lambda p: p[1]),
+                                                                        key=lambda p: p[1]))"
+                  class="${'odd' if idx % 2 else 'even'}">
+                <td>$group</td>
+                <td>
+                  <py:for each="cnt, (subject,action) in enumerate(sorted(subj_group))"
+                          py:with="subject_action='%s:%s' % (unicode_to_base64(subject),
+                                                             unicode_to_base64(action));
+                                   subject_action_id='gmsa-%d-%d' % (idx, cnt)">
+                    <!--! base64 makes it safe to use ':' as separator when passing
+                          both subject and action as one query parameter -->
+                    <label for="$subject_action_id" class="checkbox inline">
+                      <input py:if="can_revoke" type="checkbox"
+                             id="$subject_action_id"
+                             name="sel" value="$subject_action" />
+                      $subject
+                    </label>
+                  </py:for>
+                </td>
+              </tr>
+              <tr py:if="not groups"><td colspan="2">No group memberships</td></tr>
+            </tbody>
+          </table>
           <br/>
-          <div class="control-group" py:if="revoke_perm">
+          <div class="control-group" py:if="can_revoke">
             <input class="btn" type="submit" name="remove"
                 value="${_('Remove selected items')}" />
           </div>
@@ -76,6 +108,7 @@
           as that is reserved for permission names.
         </p>
       </div>
+
       <div class="span3" py:if="'PERMISSION_GRANT' in perm">
         <form id="addperm" class="well" method="post" action="">
           <fieldset>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket.html?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket.html Tue Jan  8 14:37:00 2013
@@ -25,7 +25,16 @@
       xmlns:py="http://genshi.edgewall.org/"
       xmlns:i18n="http://genshi.edgewall.org/i18n"
       xmlns:bh="http://issues.apache.org/bloodhound/wiki/Ui/Dashboard"
-      py:with="preview_mode = 'preview' in req.args">
+      py:with="preview_mode = 'preview' in req.args;
+          can_append = 'TICKET_APPEND' in perm(ticket.resource);
+          can_create = 'TICKET_CREATE' in perm(ticket.resource) and not ticket.exists;
+          can_modify = 'TICKET_CHGPROP' in perm(ticket.resource);
+          can_edit = 'TICKET_EDIT_DESCRIPTION' in perm(ticket.resource);
+          only_for_admin = 'TICKET_ADMIN' in perm(ticket.resource);
+          has_edit_comment = 'TICKET_EDIT_COMMENT' in perm(ticket.resource);
+          has_property_editor = not version and version != 0 and not cnum_edit
+                                and (can_append or can_modify or can_edit or can_create);
+          colspan = 'span8' if bhdb else 'span12'">
   <xi:include href="layout.html" />
   <xi:include href="widget_macros.html" />
 
@@ -41,6 +50,84 @@
         $(".local-nav a").click(function() { $($(this).attr('href')).removeClass('collapsed').parent().removeClass("collapsed"); });
         $('.trac-nav').hide();
         $('.trac-topnav').hide();
+
+        <py:if test="has_property_editor">
+        // Install in place editing
+
+        var modify_elem = $('#modify');
+        modify_elem.parent().hide();
+  
+        function modify_ticket() {
+          if ($('#vc-summary').is('.edit-active'))
+            // Already in editable state
+            return;
+          $('[data-edit="inplace"]').each(function() {
+              var fc = $(this).addClass('edit-active');
+              var fieldnm = fc.attr('id').substr(3);
+              fc.attr('data-edit-orig', fc.html()).empty();
+              var editor = $('#properties #field-' + fieldnm);
+              if (editor.length == 0)
+                editor = $('#editor-' + fieldnm);
+              var fieldval = editor.val();
+              editor = editor.clone(false).appendTo(fc).val(fieldval);
+              if (editor.prop('tagName') === 'TEXTAREA') {
+                if (editor.is('.wikitext'))
+                  addWikiFormattingToolbar(editor.get(0));
+              }
+              if (fieldnm === 'summary') {
+                // Install inline edit form 
+                var submit_ticket = $('#tmpl-inplace-submit').html();
+                submit_ticket = $(submit_ticket).prepend(editor)
+                    .appendTo(fc);
+                submit_ticket.find('#edit-cancel').click(revert_ticket);
+                editor.wrap('<div class="btn-group"></div>')
+
+                // Workflow actions
+                var actions_box = submit_ticket.find('#workflow-actions')
+                    .click(function(e) { e.stopPropagation(); });
+                $('#action').children('div').each(function() {
+                    var action_ui = $(this).clone(false).prependTo(actions_box)
+                        .wrap('<li style="padding: 5px 10px"></li>');
+                    var action_trigger = action_ui.find('input[name=action]');
+
+                    function action_click() {
+                      var newlabel = action_ui.find('label[for^=action_]')
+                          .text();
+                      $('#submit-action-label').text(newlabel);
+
+                      // Enable | disable action controls
+                      actions_box.find('input[name=action]').each(function() {
+                          $(this).siblings().find("*[id]")
+                              .enable($(this).checked());
+                          $(this).siblings().filter("*[id]")
+                              .enable($(this).checked());
+                        });
+                    }
+                    action_trigger.click(action_click);
+                    if (action_trigger.attr('checked'))
+                      action_click();
+
+                    var action_help = action_ui.find('.help-block').detach()
+                        .text().replace(/\s+/g, ' ').replace(/^ Tip /g, 'Tip: ')
+                        .replace(/^\s$/, '');
+                    if (action_help)
+                        $('<i class="icon-info-sign"></i>').appendTo(action_ui)
+                            .attr('title', action_help);
+                  })
+              }
+            });
+        }
+
+        function revert_ticket() {
+          $('[data-edit="inplace"]').each(function() {
+              var fc = $(this).removeClass('edit-active');
+              fc.html(fc.attr('data-edit-orig'));
+            });
+        }
+
+        $('.local-nav a[href = "#inplace-edit"]').click(modify_ticket);
+        </py:if>
+
         $('body').scrollspy({ 
             'target' : '.local-nav' , 
             'offset' : $('.stickyBox').height() + 40
@@ -117,117 +204,157 @@
       <a href="#comment:$cnum" class="$cls">$prefix$cnum</a>
     </py:def>
 
-    <div id="content" class="ticket row"
-         py:with="can_append = 'TICKET_APPEND' in perm(ticket.resource);
-                  can_create = 'TICKET_CREATE' in perm(ticket.resource) and not ticket.exists;
-                  can_modify = 'TICKET_CHGPROP' in perm(ticket.resource);
-                  can_edit = 'TICKET_EDIT_DESCRIPTION' in perm(ticket.resource);
-                  only_for_admin = 'TICKET_ADMIN' in perm(ticket.resource);
-                  has_edit_comment = 'TICKET_EDIT_COMMENT' in perm(ticket.resource);
-                  has_property_editor = not version and version != 0 and not cnum_edit
-                                        and (can_append or can_modify or can_edit or can_create);
-                  colspan = 'span8' if bhdb else 'span12'">
+    <div id="content" class="ticket row">
       <div class="trac-topnav span2" py:if="ticket.exists and has_property_editor">
         <a href="#propertyform" title="Go to the ticket editor">Modify</a> &darr;
       </div>
       <br/>
 
       <div class="$colspan">
-        <div class="stickyBox">
-          <div id="overview" class="stickyStatus $colspan">
-            <div class="whitebox"></div>
-            <div class="properties">
-              <h2 class="summary searchable clip-affix" py:choose="">
-                <py:when test="ticket.exists">$ticket.summary</py:when>
-                <py:otherwise>Create Ticket</py:otherwise>
-              </h2>
-            </div>
-            <div class="row">
-              <div class="span3">
-                <h5 id="trac-ticket-title" py:choose="">
-                  <py:when test="ticket.exists">
-                    <a href="${href.ticket(ticket.id)}"
-                        i18n:msg="id">Ticket #${ticket.id}</a>
-                  </py:when>
-                  <py:otherwise>
-                    New Ticket <small><span py:if="preview_mode and ticket.type" class="status">(${ticket.type})</span></small>
-                  </py:otherwise>
-                </h5>
-              </div>
-              <div class="offset3">
-                <h6 class="date pull-right">
-                  <span i18n:msg="created" py:if="ticket.exists">Opened ${pretty_dateinfo(ticket.time)}</span>
-                  <py:if test="ticket.changetime != ticket.time">,
-                    <span i18n:msg="modified">Last modified ${pretty_dateinfo(ticket.changetime)}</span>
-                  </py:if>
-                  <span py:if="not ticket.exists" class="label label-warning">(ticket not yet created)</span>
-                </h6>
-                <h6 class="pull-right">
-                  <span id="h_reporter">Reported by
-                    ${reporter_link if defined('reporter_link') else authorinfo(ticket.reporter)}
-                  </span>,
-                  <span id="h_owner">Assigned to
-                    ${(owner_link if defined('owner_link') else authorinfo(ticket.owner)) if ticket.owner else ''}
-                  </span>
-                </h6>
+        <py:if test="ticket.exists">
+          <div class="row">
+            <div class="$colspan"><form py:strip="not has_property_editor" method="post" 
+                id="inplace-propertyform"
+                action="${href.ticket(ticket.id) + '#trac-add-comment' if ticket.exists
+                          else href.newticket() + '#ticket'}">
+              <py:if test="has_property_editor">
+                <input type="hidden" name="start_time" 
+                    value="${to_utimestamp(start_time)}" />
+                <input type="hidden" name="view_time" 
+                    value="${to_utimestamp(ticket['changetime'])}" />
+              </py:if>
+              <div class="stickyBox">
+                <div id="overview" class="stickyStatus $colspan">
+                  <div class="whitebox"></div>
+                  <div class="properties">
+                    <h2 class="summary searchable clip-affix" py:choose=""
+                        data-edit="${'inplace' if can_modify or can_create else None}"
+                        id="vc-summary">
+                      <py:when test="ticket.exists">&#9734; $ticket.summary</py:when>
+                      <py:otherwise>Create Ticket</py:otherwise>
+                    </h2>
+                  </div>
+                  <div class="row">
+                    <span class="ownership">
+                      <py:choose test="">
+                        <py:when test="ticket.exists">
+                          <a href="${href.ticket(ticket.id)}"
+                              i18n:msg="id">Ticket #${ticket.id}</a>
+                        </py:when>
+                        <py:otherwise>
+                          New Ticket <small><span py:if="preview_mode and ticket.type" class="status">(${ticket.type})</span></small>
+                        </py:otherwise>
+                      </py:choose>
+                      <py:if test="ticket.exists"> - 
+                        <py:if test="ticket.changetime != ticket.time">
+                          <span i18n:msg="modified">Last modified 
+                            <time datetime="${ticket.changetime.strftime('%Y-%m-%d')}">
+                              ${pretty_dateinfo(ticket.changetime)}
+                            </time>.
+                          </span>
+                        </py:if>
+                      </py:if>
+                      <span id="h_owner">Assigned to
+                        ${(owner_link if defined('owner_link') else authorinfo(ticket.owner)) if ticket.owner else ''}.
+                      </span>
+                    </span>
+                  </div>
+                  <div class="local-nav" py:if="ticket.exists"
+                      py:with="sections = (
+                              (_('Overview'), 'content', True, _('View ticket fields and description'), 'icon-list'),
+                              (_('Attachments'), 'attachments', attachments.attachments or attachments.can_create, _('Go to the list of attachments'), 'icon-file'),
+                              (_('Comments'), 'changelog', True, _('Go to the changelog'), 'icon-comment'),
+                              (_('Add comment'), 'propertyform', ticket.exists and can_append, _('Go to the ticket editor'), 'icon-plus-sign'),
+                              (_('Modify Ticket'), 'inplace-edit', can_modify or can_edit or can_create, _('Modify ticket fields and description'), 'icon-edit'),
+                          )">
+                    <div>
+                      <small>
+                        <ul class="nav btn-group">
+                          <li py:for="s in sections" py:if="s[2]" class="btn">
+                            <a href="#${s[1]}" title="${s[3]}">
+                              <i class="${s[4]}"></i>
+                              ${s[0]}
+                            </a>
+                          </li>
+                        </ul>
+                      </small>
+                    </div>
+                  </div>
+                  <div class="stickyEndMark"></div>
+                </div>
               </div>
-            </div>
-            <div class="local-nav" py:if="ticket.exists"
-                py:with="sections = (
-                        (_('Overview'), 'content', True, _('View ticket fields and description'), 'icon-list'),
-                        (_('Attachments'), 'attachments', attachments.attachments or attachments.can_create, _('Go to the list of attachments'), 'icon-file'),
-                        (_('Comments'), 'changelog', True, _('Go to the changelog'), 'icon-comment'),
-                        (_('Add comment'), 'propertyform', ticket.exists and can_append, _('Go to the ticket editor'), 'icon-plus-sign'),
-                        (_('Modify Ticket'), 'modify', can_modify or can_edit or can_create, _('Modify ticket fields and description'), 'icon-edit'),
-                    )">
+              <script type="text/javascript">
+                setup_sticky_panel('#overview');
+              </script>
+
               <div>
-                <small>
-                  <ul class="nav btn-group">
-                    <li py:for="s in sections" py:if="s[2]" class="btn">
-                      <a href="#${s[1]}" title="${s[3]}">
-                        <i class="${s[4]}"></i>
-                        ${s[0]}
-                      </a>
-                    </li>
-                  </ul>
-                </small>
+                <div class="row">
+                  <div class="span4">
+                    <div class="row">
+                      <div class="span2">
+                        <h5 id="h_reporter" class="pull-right">
+                          Reporter:
+                        </h5>
+                      </div>
+                      <div class="span2" id="vc-reporter"
+                          data-edit="${'inplace' if only_for_admin else None}">
+                        ${reporter_link if defined('reporter_link') else authorinfo(ticket.reporter)}
+                        &nbsp;
+                      </div>
+                    </div>
+                  </div>
+                  <div class="span4">
+                    <div class="row">
+                      <div class="span2">
+                        <h5 id="h_reporter" class="pull-right">
+                          Opened:
+                        </h5>
+                      </div>
+                      <div class="span2">
+                        <time datetime="${ticket.time.strftime('%Y-%m-%d')}">
+                          ${pretty_dateinfo(ticket.time)}
+                        </time>
+                        &nbsp;
+                      </div>
+                    </div>
+                  </div>
+                </div>
+                <div class="row">
+                  <div class="span4">
+                    <div class="row">
+                      <div class="span2">
+                        <h5 id="h_type" class="pull-right">
+                          Type:
+                        </h5>
+                      </div>
+                      <div id="vc-type" class="span2"
+                          data-edit="${'inplace' if can_modify or can_edit or can_create else None}">
+                        <small py:if="ticket.type">${ticket.type}</small>
+                        &nbsp;
+                      </div>
+                    </div>
+                  </div>
+                  <div class="span4">
+                    <div class="row">
+                      <div class="span2">
+                        <h5 id="h_status" class="pull-right">
+                          Status:
+                        </h5>
+                      </div>
+                      <div class="span2">
+                        <small>${ticket.status}
+                          <py:if test="ticket.resolution">: ${ticket.resolution}</py:if>
+                        </small>
+                        &nbsp;
+                      </div>
+                    </div>
+                  </div>
+                </div>
               </div>
-            </div>
-            <div class="stickyEndMark"></div>
-          </div>
-        </div>
-        <script type="text/javascript">
-          setup_sticky_panel('#overview');
-        </script>
-        <py:if test="ticket.exists">
-          <div class="row">
-            <div class="span6">
-              <h4 class="status"><small>${ticket.status}<py:if
-                  test="ticket.type"> ${ticket.type}</py:if><py:if
-                  test="ticket.resolution">: ${ticket.resolution}</py:if></small></h4>
-              <py:choose test="">
-                <py:when test="version is None" />
-                <py:when test="version == 0">
-                  &mdash;
-                  <small>
-                    <i18n:msg>at <a href="#comment:description">Initial Version</a></i18n:msg>
-                  </small>
-                </py:when>
-                <py:otherwise>
-                  &mdash;
-                  <small>
-                    <i18n:msg params="version">at <a href="#comment:$version">Version $version</a></i18n:msg>
-                  </small>
-                </py:otherwise>
-              </py:choose>
-            </div>
-          </div>
-          <div class="row">
-            <div class="$colspan">
               <xi:include href="bh_ticket_box.html"
                   py:with="preview_mode = change_preview.fields ; 
                       colcount = 4 if bhdb else 6"/>
-            </div>
+            </form></div>
 
             <!--! do not show attachments for old versions of this ticket or for new tickets -->
             <div class="$colspan" py:if="not version and version != 0 and ticket.exists">
@@ -333,7 +460,8 @@ ${comment}</textarea>
                         <th><label for="field-summary">Summary:</label></th>
                         <td class="fullrow" colspan="3">
                           <input type="text" id="field-summary" name="field_summary"
-                                 value="$ticket.summary" size="70" />
+                                 value="$ticket.summary" size="70" 
+                                 class="input-xlarge" />
                         </td>
                       </tr>
                       <py:if test="only_for_admin">
@@ -341,7 +469,7 @@ ${comment}</textarea>
                           <th><label for="field-reporter">Reporter:</label></th>
                           <td class="fullrow" colspan="3">
                             <input type="text" id="field-reporter" name="field_reporter"
-                                   value="${ticket.reporter}" size="70" />
+                                   value="${ticket.reporter}" class="input-medium" />
                           </td>
                         </tr>
                       </py:if>
@@ -373,9 +501,10 @@ ${ticket.description}</textarea>
                               field.edit_label or field.label or field.name}:</label>
                           </th>
                           <td class="col${idx + 1}" py:if="idx == 0 or not fullrow"
-                              colspan="${3 if fullrow else None}">
+                              colspan="${3 if fullrow else None}"
+                              id="${'editor-' + field.name if field else None}">
                             <py:choose test="field.type" py:if="field">
-                              <select py:when="'select'" id="field-${field.name}" name="field_${field.name}">
+                              <select py:when="'select'" id="field-${field.name}" name="field_${field.name}" class="input-medium">
                                 <option py:if="field.optional"></option>
                                 <option py:for="option in field.options"
                                         selected="${value == option or None}"
@@ -528,5 +657,28 @@ ${value}</textarea>
         </bh:widget>
       </div>
     </div>
+
+    <script type="text/x-tmpl" id="tmpl-inplace-submit" py:if="has_property_editor">
+      <div class="btn-toolbar" style="margin: 0px">
+      <div class="btn-group input-append">
+        <button id="edit-submit" class="btn btn-primary" type="submit" 
+            value="Submit changes" name="submit">
+          Update (<span id="submit-action-label"></span>)
+        </button>
+        <button class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
+          <span class="caret"></span>
+        </button>
+        <ul class="dropdown-menu">
+          <fieldset id="workflow-actions">
+          </fieldset>
+        </ul>
+      </div>
+      <div class="btn-group">
+        <button id="edit-cancel" class="btn-link" title="Discard changes">
+          Cancel
+        </button>
+      </div>
+      </div>
+    </script>
   </body>
 </html>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket_box.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket_box.html?rev=1430304&r1=1430303&r2=1430304&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket_box.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket_box.html Tue Jan  8 14:37:00 2013
@@ -47,7 +47,8 @@ Arguments:
               <py:if test="field"><i18n:msg params="field">${field.label or field.name}:</i18n:msg></py:if>
             </h5>
           </div>
-          <div class="${'span2' if is_inline else None}">
+          <div class="${'span2' if is_inline else None}" data-edit="inplace" 
+              id="${'vc-' + field.name if field else None}">
             <py:if test="field">
               <py:choose test="">
                 <py:when test="'rendered' in field">${field.rendered}</py:when>
@@ -66,7 +67,7 @@ Arguments:
       text_fields = [f for f in basefields if f.type == 'text' or f.name == 'cc'];
       area_fields = [f for f in basefields if f.type == 'textarea'];
       _colcount = colcount or 6">
-    <div class="properties" style="margin: 1.2em 0px">
+    <div class="properties" style="margin-bottom: 1.2em">
       <py:with vars="_fields, csscls, count, fontsize, is_inline = 
           (small_fields, 'span4', _colcount / 2, None, True)">
         <py:for each="fields_row in group(_fields, count)">
@@ -79,7 +80,8 @@ Arguments:
       <py:for each="field in fields">
         <py:if test="field.name == 'keywords'">
           <div title="Keywords">
-            <i class="icon-tags"></i> ${field.rendered}
+            <i class="icon-tags"></i>
+            <span data-edit="${'inplace' if can_modify or can_edit or can_create else None}" id="vc-keywords">${field.rendered}</span>
           </div>
         </py:if>
       </py:for>
@@ -104,7 +106,9 @@ Arguments:
                 title="Reply, quoting this description" />
           </form>
         </div>
-        <div py:if="ticket.description" class="searchable" xml:space="preserve">
+        <div py:if="ticket.description" class="searchable" xml:space="preserve"
+            data-edit="${'inplace' if can_edit or can_create else None}"
+            id="vc-description">
           ${wiki_to_html(context, ticket.description, escape_newlines=preserve_newlines)}
         </div>
         <br py:if="not ticket.description" style="clear: both" />