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> ↓
</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">☆ $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)}
+
+ </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>
+
+ </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>
+
+ </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>
+
+ </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">
- —
- <small>
- <i18n:msg>at <a href="#comment:description">Initial Version</a></i18n:msg>
- </small>
- </py:when>
- <py:otherwise>
- —
- <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" />