Adding the Batch Create Tickets feature wiki macro to BH by merging the finalized changes on bep_0011_batch_create_tickets feature branch [1598681]:[1786358] into trunk
git-svn-id: https://svn.apache.org/repos/asf/bloodhound/trunk@1786396 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/bloodhound_theme/bhtheme/tests/theme.py b/bloodhound_theme/bhtheme/tests/theme.py
index 2ec72fa..ebfca86 100644
--- a/bloodhound_theme/bhtheme/tests/theme.py
+++ b/bloodhound_theme/bhtheme/tests/theme.py
@@ -17,24 +17,14 @@
# specific language governing permissions and limitations
# under the License.
-from trac.test import EnvironmentStub, Mock, MockPerm
-from trac.util.datefmt import utc
+from trac.test import EnvironmentStub
from trac.web.chrome import Chrome
-from trac.wiki.model import WikiPage
from bhdashboard.web_ui import DashboardModule
-from bhtheme.theme import BloodhoundTheme, BatchCreateTicketsMacro, CreatedTicketsMacro
+from bhtheme.theme import BloodhoundTheme
from bhtheme.tests import unittest
-try:
- from babel import Locale
-
- locale_en = Locale.parse('en_US')
-except ImportError:
- locale_en = None
-
-
class ThemeTestCase(unittest.TestCase):
def setUp(self):
@@ -42,16 +32,6 @@
default_data=True)
self.bhtheme = BloodhoundTheme(self.env)
- self.BatchCreateTicketsMacro = BatchCreateTicketsMacro(self.env)
- self.CreatedTicketsMacro = CreatedTicketsMacro(self.env)
-
- self.req = Mock(href=self.env.href, authname='anonymous', tz=utc,
- method='POST',
- args=dict(action='dummy'),
- locale=locale_en, lc_time=locale_en,
- chrome={'warnings': []})
- self.req.perm = MockPerm()
-
def tearDown(self):
self.env.reset_db()
@@ -61,49 +41,6 @@
for dir in self.bhtheme.get_templates_dirs():
self.assertIn(dir, chrome.get_all_templates_dirs())
- def test_batch_create(self):
- with self.env.db_transaction as db:
- db("CREATE TABLE bloodhound_product (prefix text,name text,description text,owner text,UNIQUE (prefix,name))")
- db("INSERT INTO bloodhound_product VALUES ('my','product001','test product 001','anonymous')")
- attr = {
- 'summary0': u's1',
- 'summary1': u's2',
- 'summary2': u's3',
- 'summary3': u's4',
- 'summary4': u's5',
- 'priority1': u'critical',
- 'priority0': u'blocker',
- 'priority3': u'minor',
- 'priority2': u'major',
- 'priority4': u'trivial',
- 'milestone0': u'milestone1',
- 'milestone1': u'milestone2',
- 'milestone2': u'milestone3',
- 'milestone3': u'milestone4',
- 'milestone4': u'milestone1',
- 'component4': u'component1',
- 'product4': u'my',
- 'product3': u'my',
- 'product2': u'my',
- 'product1': u'my',
- 'product0': u'my',
- 'component1': u'component2',
- 'component0': u'component1',
- 'component3': u'component2',
- 'component2': u'component1',
- 'description4': u'd5',
- 'description2': u'd3',
- 'description3': u'd4',
- 'description0': u'd1',
- 'description1': u'd2'}
-
- def test_update_wiki_content(self):
-
- w = WikiPage(self.env)
- w.name = 'temp_page'
- w.text = 'test the wiki replace function. ie: [[BatchCreateTickets(5)]] replaces with Created Tickets macro.'
- WikiPage.save(w, 'anonymous', 'comment 01', '127.0.0.1')
-
def suite():
suite = unittest.TestSuite()
diff --git a/bloodhound_theme/bhtheme/theme.py b/bloodhound_theme/bhtheme/theme.py
index 5ef5135..9a12e59 100644
--- a/bloodhound_theme/bhtheme/theme.py
+++ b/bloodhound_theme/bhtheme/theme.py
@@ -17,27 +17,16 @@
# specific language governing permissions and limitations
# under the License.
-import random
-import re
-import string
import sys
-from bhdashboard import wiki
-from bhdashboard.util import dummy_request
-from bhdashboard.web_ui import DashboardModule
-from bhtheme.translation import _, add_domain
from genshi.builder import tag
from genshi.core import TEXT
from genshi.filters.transform import Transformer
from genshi.output import DocType
-from multiproduct.env import ProductEnvironment
-from multiproduct.web_ui import PRODUCT_RE, ProductModule
-from pkg_resources import get_distribution
-from themeengine.api import ThemeBase, ThemeEngineSystem
+
from trac.config import ListOption, Option
from trac.core import Component, TracError, implements
from trac.mimeview.api import get_mimetype
-from trac.perm import IPermissionRequestor
from trac.resource import get_resource_url, Neighborhood, Resource
from trac.ticket.api import TicketSystem
from trac.ticket.model import Ticket, Milestone
@@ -45,15 +34,22 @@
from trac.ticket.web_ui import TicketModule
from trac.util.compat import set
from trac.util.presentation import to_json
-from trac.util.translation import cleandoc_
from trac.versioncontrol.web_ui.browser import BrowserModule
-from trac.web.api import IRequestFilter, ITemplateStreamFilter
+from trac.web.api import IRequestFilter, IRequestHandler, ITemplateStreamFilter
from trac.web.chrome import (add_stylesheet, add_warning, INavigationContributor,
ITemplateProvider, prevnext_nav, Chrome, add_script)
-from trac.web.main import IRequestHandler
from trac.wiki.admin import WikiAdmin
from trac.wiki.formatter import format_to_html
-from trac.wiki.macros import WikiMacroBase
+
+from themeengine.api import ThemeBase, ThemeEngineSystem
+
+from bhdashboard.util import dummy_request
+from bhdashboard.web_ui import DashboardModule
+from bhdashboard import wiki
+
+from multiproduct.env import ProductEnvironment
+from multiproduct.web_ui import PRODUCT_RE, ProductModule
+from bhtheme.translation import _, add_domain
try:
from multiproduct.ticket.web_ui import ProductTicketModule
@@ -384,7 +380,7 @@
self._modify_resource_breadcrumb(req, template, data, content_type,
is_active)
- # add a creation event to the changelog if the ticket exists
+ #add a creation event to the changelog if the ticket exists
ticket = data['ticket']
if ticket.exists:
data['changes'] = [{'comment': '',
@@ -396,7 +392,7 @@
'date': ticket['time'],
},
] + data['changes']
- # and set default order
+ #and set default order
if not req.session.get('ticket_comments_order'):
req.session['ticket_comments_order'] = 'newest'
@@ -468,7 +464,7 @@
SELECT product, value FROM bloodhound_productconfig
WHERE product IN (%s) AND section='project' AND
option='icon'""" % ', '.join(["%s"] * len(products)),
- tuple(p.prefix for p in products))
+ tuple(p.prefix for p in products))
icons = dict(icons)
data['thumbsize'] = 64
# FIXME: Gray icon for missing products
@@ -483,11 +479,11 @@
product_ctx(product),
product.description),
links={'extras': (([{'href': req.href.products(
- product.prefix, action='edit'),
- 'title': _('Edit product %(prefix)s',
- prefix=product.prefix),
- 'icon': tag.i(class_='icon-edit'),
- 'label': _('Edit')}, ]
+ product.prefix, action='edit'),
+ 'title': _('Edit product %(prefix)s',
+ prefix=product.prefix),
+ 'icon': tag.i(class_='icon-edit'),
+ 'label': _('Edit')},]
if 'PRODUCT_MODIFY' in req.perm
else []) +
[{'href': product.href(),
@@ -523,7 +519,6 @@
tag.a(_('Source'),
href=req.href.wiki('TracRepositoryAdmin')))
-
class QCTSelectFieldUpdate(Component):
implements(IRequestHandler)
@@ -535,8 +530,8 @@
fields_to_update = req.args.get('fields_to_update[]');
env = ProductEnvironment(self.env.parent, req.args.get('product'))
ticket_fields = TicketSystem(env).get_ticket_fields()
- data = dict([f['name'], f['options']] for f in ticket_fields
- if f['type'] == 'select' and f['name'] in fields_to_update)
+ data = dict([f['name'], f['options']] for f in ticket_fields
+ if f['type'] == 'select' and f['name'] in fields_to_update)
req.send(to_json(data), 'application/json')
@@ -545,7 +540,7 @@
qct_fields = ListOption('ticket', 'quick_create_fields',
'product, version, type',
- doc="""Multiple selection fields displayed in create ticket menu""",
+ doc="""Multiple selection fields displayed in create ticket menu""",
doc_domain='bhtheme')
def __init__(self, *args, **kwargs):
@@ -596,7 +591,8 @@
dict(value=p,
new_ticket_url=dum_req.href.products(p, 'newticket'),
description=ProductEnvironment.lookup_env(self.env, p)
- .product.name)
+ .product.name
+ )
for p in product_field['options']
if req.perm.has_permission('TICKET_CREATE',
Neighborhood('product', p)
@@ -614,7 +610,7 @@
'fields': [all_fields[k] for k in self.qct_fields
if k in all_fields],
'hidden_fields': [all_fields[k] for k in all_fields.keys()
- if k not in self.qct_fields]}
+ if k not in self.qct_fields] }
return template, data, content_type
# IRequestHandler methods
@@ -691,457 +687,5 @@
"of ticket #%s: %s" % (t.id, e))
return t['product'], t.id
+from pkg_resources import get_distribution
application_version = get_distribution('BloodhoundTheme').version
-
-
-class BatchCreateTicketsMacro(WikiMacroBase):
- def parse_macro(self, parser, name, content):
- pass
-
- implements(
- IRequestFilter,
- IRequestHandler,
- ITemplateStreamFilter,
- IPermissionRequestor)
- _description = cleandoc_(
- """
- Helps to create batch of tickets at once.
-
- This macro accepts only one argument, which should be an integer value equal to the number of tickets that is going to create as a batch.
-
- Example:
- {{{
- [[BatchCreateTickets(5)]] # This will create an empty table with 5 rows.
- }}}
-
- The empty table which will be created will contain the following tickets fields.
- * `Summary` //(This field is mandatory.)//
- * `Description`
- * `Product`
- * `Priority`
- * `Milestone`
- * `Component`
-
- `BatchCreateTickets` has also make it possible to increase or decrease the size of the empty table created. After
- filling the appropriate ticket fields you can create that batch of tickets and will be able to view the
- details of the created tickets as a ticket table.
- """)
-
- bct_fields = ListOption(
- 'ticket',
- 'batch_create_fields',
- 'product, version, type',
- doc="""Multiple selection fields displayed in create ticket menu""",
- doc_domain='bhtheme')
-
- def __init__(self, *args, **kwargs):
- import pkg_resources
- locale_dir = pkg_resources.resource_filename(__name__, 'locale')
- add_domain(self.env.path, locale_dir)
- self.rows = 0
- self.rqst = None
- self.file = None
- super(BatchCreateTicketsMacro, self).__init__(*args, **kwargs)
-
- # IPermissionRequestor methods
- def get_permission_actions(self):
- return ['TICKET_BATCH_CREATE']
-
- def expand_macro(self, formatter, name, args):
- """Set the number of rows for empty ticket table to be generated.
- Return none as the template will not changed at this point.
-
- `name` is the actual name of the macro. (here it'll be
- `'BatchCreateTickets'`),
- `args` is the text enclosed in parenthesis at the call of the macro.
- Note that if there are ''no'' parenthesis (like in, e.g.
- [[BatchCreateTickets]]), then `args` is `None` and its not a valid argument.
- Or if the argument is not a valid type(like in, e.g.
- [[BatchCreateTickets("Hello")]] "Hello" is a string which can't be parsed to an integer)
- then the bh will raise an error.
- """
- self.rows = args
- # check the permission conditions and allow the feature only on wiki formatted pages.
- if (
- self.env.product is not None) and (
- self.file == 'bh_wiki_view.html' or self.file == 'bh_wiki_edit.html' or self.file is None) and (
- self.rqst.perm.has_permission('TRAC_ADMIN') or self.rqst.perm.has_permission(
- 'TICKET_BATCH_CREATE')):
- # todo let the user select the product when creating tickets
- # generate the required data to be parsed to the js functions too create the empty ticket table.
-
- product_id = str(self.env.product.resource.id)
- milestones = self.env.db_query(
- "SELECT * FROM milestone WHERE product=%s", (product_id,))
- components = self.env.db_query(
- "SELECT * FROM component WHERE product=%s", (product_id,))
-
- random_string = '%s%s' % ('-', ''.join(random.choice(string.lowercase) for i in range(10)))
- form = tag.form(
- tag.div(
- tag.span(
- tag.script(
- type="text/javascript",
- charset="utf-8",
- src=str(self.rqst.href.chrome('theme/js/batchcreate.js'))),
- tag.script(
- # pass the relevant arguments to the js function as JSON parameters.
- "emptyTable(" + to_json(str(self.rows)) + "," + to_json(self.env.product._data['name']) + "," +
- to_json(milestones) + "," + to_json(components) + "," +
- to_json(self.rqst.href() + "/bct") + "," +
- to_json(str(self.rqst.environ["HTTP_COOKIE"])) + "," +
- to_json(random_string) + ")",
- id="js-caller" + random_string,
- type="text/javascript"),
- class_="input-group-btn"),
- class_='report',
- id="div-empty-table" + random_string),
- method="get",
- style="display:inline",
- id="batchcreate" + random_string)
- try:
- int(self.rows)
- except TracError:
- print "Enter a valid argument (integer) to the BatchCreateTickets macro."
-
- return form
- else:
- return None
-
- # IRequestFilter(Interface):
- def pre_process_request(self, req, handler):
- """Nothing to do.
- """
- self.rqst = req
- return handler
-
- def post_process_request(self, req, template, data, content_type):
- """Append necessary ticket data
- """
- try:
- tm = self._get_ticket_module()
- except TracError:
- # no ticket module so no create ticket button
- return template, data, content_type
-
- if (template, data, content_type) != (None,) * 3: # TODO: Check !
- if data is None:
- data = {}
- dum_req = dummy_request(self.env)
- dum_req.perm = req.perm
- ticket = Ticket(self.env)
- tm._populate(dum_req, ticket, False)
- all_fields = dict([f['name'], f]
- for f in tm._prepare_fields(dum_req, ticket)
- if f['type'] == 'select')
-
- product_field = all_fields.get('product')
- if product_field:
- # When at product scope, set the default selection to the
- # product at current scope. When at global scope the default
- # selection is determined by [ticket] default_product
- if self.env.product and \
- self.env.product.prefix in product_field['options']:
- product_field['value'] = self.env.product.prefix
- # Transform the options field to dictionary of product
- # attributes and filter out products for which user doesn't
- # have TICKET_CREATE permission
- product_field['options'] = [
- dict(value=p,
- new_ticket_url=dum_req.href.products(p, 'newticket'),
- description=ProductEnvironment.lookup_env(self.env, p)
- .product.name
- )
- for p in product_field['options']
- if req.perm.has_permission('TICKET_CREATE',
- Neighborhood('product', p)
- .child(None, None))]
- else:
- msg = _("Missing ticket field '%(field)s'.", field='product')
- if ProductTicketModule is not None and \
- self.env[ProductTicketModule] is not None:
- # Display warning alert to users
- add_warning(req, msg)
- else:
- # Include message in logs since this might be a failure
- self.log.warning(msg)
- data['bct'] = {
- 'fields': [all_fields[k] for k in self.bct_fields
- if k in all_fields],
- 'hidden_fields': [all_fields[k] for k in all_fields.keys()
- if k not in self.bct_fields]}
- return template, data, content_type
-
- # IRequestHandler methods
-
- def match_request(self, req):
- """Handle requests sent to /bct
- """
- m = PRODUCT_RE.match(req.path_info)
- return req.path_info == '/bct' or \
- (m and m.group('pathinfo').strip('/') == 'bct')
-
- def process_request(self, req):
-
- self.log.debug("BatchCreateTicketsModule: process_request entered")
- """Forward new ticket request to `trac.ticket.web_ui.TicketModule`
- but return plain text suitable for AJAX requests.
- """
- try:
- tm = self._get_ticket_module()
- req.perm.require('TICKET_BATCH_CREATE')
-
- attrs = dict([k[6:], v] for k, v in req.args.iteritems()
- if k.startswith('field_'))
- # new_tkts variable will contain the tickets that have been created as a batch
- # that information will be used to load the resultant query table
- product, tid, new_tkts = self.batch_create(
- req, attrs, True)
- except Exception as exc:
- self.log.exception("BH: Batch create tickets failed %s" % (exc,))
- req.send(str(exc), 'plain/text', 500)
- else:
- tkt_list = []
- tkt_dict = {}
- num_of_tkts = len(new_tkts)
- for i in range(0, num_of_tkts):
- tres = Neighborhood(
- 'product',
- new_tkts[i].values['product'])(
- 'ticket',
- tid -
- num_of_tkts +
- i +
- 1)
- href = req.href
- tkt_list.append(
- to_json(
- {
- 'product': new_tkts[i].values['product'],
- 'id': tid - num_of_tkts + i + 1,
- 'url': get_resource_url(
- self.env,
- tres,
- href),
- 'summary': new_tkts[i].values['summary'],
- 'status': new_tkts[i].values['status'],
- 'milestone': new_tkts[i].values['milestone'],
- 'component': new_tkts[i].values['component'],
- 'priority': new_tkts[i].values['priority'],
- 'description': new_tkts[i].values['description']}))
-
- tkt_dict["tickets"] = tkt_list
-
- self._update_wiki_content(num_of_tkts)
-
- # send the HTTP POST request
- req.send(to_json(tkt_dict), 'application/json')
-
- # Public API
- def _update_wiki_content(self, num_of_tkts):
- """Editing the wiki content
- After creating the tickets successfully the feature requires to display the details of the created tickets
- within the wiki.
- To do that at this point the wiki content will be updated.
- ie. [[BatchCreateTickets(x)]] to [[CreatedTickets(start id,end id)]]
- """
- max_uid = self.env.db_query("SELECT MAX(uid) FROM ticket")
- max_time = self.env.db_query("SELECT MAX(time) FROM wiki")
- wiki_content = self.env.db_query(
- "SELECT * FROM wiki WHERE time==%s", (max_time[0][0],))
- wiki_string = wiki_content[0][5]
- # regex pattern of the macro declaration.
- pattern = '\[\[BatchCreateTickets\([0-9]+\)\]\]'
- l = re.search(pattern, wiki_string)
- l1 = str(wiki_string[l.regs[0][0]:l.regs[0][1]])
- updated_wiki_content = wiki_string.replace(
- l1, "[[CreatedTickets(" + str(max_uid[0][0] - num_of_tkts) + "," + str(max_uid[0][0]) + ")]]")
- with self.env.db_transaction as db:
- db("UPDATE wiki SET text=%s WHERE time=%s",
- (updated_wiki_content, max_time[0][0]))
- return None
-
- def _get_ticket_module(self):
- ptm = None
- if ProductTicketModule is not None:
- ptm = self.env[ProductTicketModule]
- tm = self.env[TicketModule]
- if not (tm is None) ^ (ptm is None):
- raise TracError('Unable to load TicketModule (disabled)?')
- if tm is None:
- tm = ptm
- return tm
-
- # ITemplateStreamFilter methods
- def filter_stream(self, req, method, filename, stream, data):
- self.file = filename
- return stream
-
- # Public API
- def batch_create(self, req, attributes={}, notify=False):
- """ Create batch of tickets, returning created tickets.
- """
- num_of_tkts = attributes.__len__() / 5
- created_tickets = []
- loop_condition = True
- i = -1
- # iterate data row by row and create tickets using those user filled data fields.
- while loop_condition:
- if num_of_tkts <= 0:
- loop_condition = False
- break
- i += 1
-
- if 'summary' + str(i) not in attributes:
- continue
-
- if 'product' + str(i) in attributes:
- env = self.env.parent or self.env
- if attributes['product' + str(i)]:
- env = ProductEnvironment(
- env,
- attributes[
- 'product' +
- str(i)])
- else:
- env = self.env
- # If the summary field of a particular data row is empty skip creating that ticket.
- if attributes.get('summary' + str(i)) == "":
- num_of_tkts -= 1
- continue
-
- description = attributes.pop('description' + str(i))
- status = 'new'
- summary = attributes.pop('summary' + str(i))
- priority = attributes.pop('priority' + str(i))
- milestone = attributes.pop('milestone' + str(i))
- component = attributes.pop('component' + str(i))
-
- # Create the tickets using extracted data.
- t = Ticket(env)
- t['summary'] = summary
- t['description'] = description
- t['reporter'] = req.authname
- t['status'] = status
- t['resolution'] = ''
- t['product'] = self.env.product._data['prefix']
- t['priority'] = priority
- t['milestone'] = milestone
- t['component'] = component
- # Insert the data into the DB.
- t.insert()
- created_tickets.append(t)
-
- if notify:
- try:
- tn = TicketNotifyEmail(env)
- tn.notify(t, newticket=True)
- except Exception as e:
- self.log.exception(
- "Failure sending notification on creation "
- "of ticket #%s: %s" %
- (t.id, e))
- num_of_tkts -= 1
- return t['product'], t.id, created_tickets
-
-
-class CreatedTicketsMacro(WikiMacroBase):
- implements(ITemplateStreamFilter)
- _description = cleandoc_(
- """
- Helps to generate a ticket table within the wiki.
-
- You can use this macro in order to display the details of a batch of tickets. `CreatedTickets` macro takes exactly
- two integer arguments. The arguments defines the range of the tickets that will be displayed in the ticket table.
-
- Example:
- {{{
- [[CreatedTickets(10,15)]] # This will create a ticket table with tickets which has id's between 10 and 15.
- }}}
- """)
-
- def __init__(self, *args, **kwargs):
- import pkg_resources
- locale_dir = pkg_resources.resource_filename(__name__, 'locale')
- add_domain(self.env.path, locale_dir)
- self.rqst = None
- self.file = None
- super(CreatedTicketsMacro, self).__init__(*args, **kwargs)
-
- # Template Stream Filter methods
- def filter_stream(self, req, method, filename, stream, data):
- self.rqst = req
- self.file = filename
- return stream
-
- def expand_macro(self, formatter, name, args):
- """Set the number of rows for empty ticket table to be generated.
- Return none as the template will not changed at this point.
-
- `name` is the actual name of the macro. (here it'll be
- `'BatchCreateTickets'`),
- `args` is the text enclosed in parenthesis at the call of the macro.
- Note that if there are ''no'' parenthesis (like in, e.g.
- [[BatchCreateTickets]]), then `args` is `None` and its not a valid argument.
- Or if the argument is not a valid type(like in, e.g.
- [[BatchCreateTickets("Hello")]] "Hello" is a string which can't be parsed to an integer)
- then the bh will raise an error.
- """
- if self.file == 'bh_wiki_view.html' or self.file == 'bh_wiki_edit.html' or self.file is None:
- # Extract the macro arguments.
- id_range = args.split(',')
- start_id = int(id_range[0])
- end_id = int(id_range[1])
-
- display_tickets = self.env.db_query(
- "SELECT id,summary,product,status,milestone,component FROM ticket WHERE uid>=%s and uid<=%s",
- (start_id +
- 1,
- end_id),
- )
- display_tickets_list = []
- for i in range(0, end_id - start_id):
- tres = Neighborhood(
- 'product',
- display_tickets[i][2])(
- 'ticket',
- display_tickets[i][0])
- href = self.rqst.href
- display_tickets_list.append(
- to_json(
- {
- 'product': display_tickets[i][2],
- 'id': display_tickets[i][0],
- 'url': get_resource_url(
- self.env,
- tres,
- href),
- 'summary': display_tickets[i][1],
- 'status': display_tickets[i][3],
- 'milestone': display_tickets[i][4],
- 'component': display_tickets[i][5]
- }))
-
- random_string = '%s%s' % ('-', ''.join(random.choice(string.lowercase) for i in range(10)))
- # Send the ticket data to be displayed on the ticket table as JSON parameters.
- form = tag.form(
- tag.div(
- tag.span(
- tag.script(
- type='text/javascript',
- charset='utf-8',
- src=str(self.rqst.href.chrome('theme/js/batchcreate.js'))),
- tag.script(
- 'display_created_tickets(' + to_json(display_tickets_list) + ',' +
- to_json(random_string) + ',' + to_json(self.env.product._data['name']) + ')',
- id='js-caller' + random_string,
- type='text/javascript'),
- class_='input-group-btn'),
- class_='report',
- id='div-created-ticket-table' + random_string),
- method='get',
- style='display:inline',
- id='batchcreate' + random_string)
- return form
- else:
- return None