Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add support of modifiable reports

git-svn-id: http://tracplugin.itteco.com/svn/trunk@44 7a303b46-347f-4bab-8b20-7d2844dc3dc5
  • Loading branch information...
commit 15c4ea52034da4cb5a6eefce1ccf38d60d9ddfe9 1 parent f3d9ea7
andrew.starkoff authored
View
3  ChangeLog
@@ -1,4 +1,7 @@
# Itteco Trac Plugin Changelog #
+IttecoTracPlugin 0.1.12 (May 19, 2009)
+* Add support of modifiable reports
+
IttecoTracPlugin 0.1.12 (May 11, 2009)
* Move widgets colorization to client side
View
2  itteco/__init__.py
@@ -1,2 +1,2 @@
-__version__ = '0.1.12'
+__version__ = '0.1.13'
__package__ = 'itteco'
View
76 itteco/htdocs/css/report.css
@@ -0,0 +1,76 @@
+body{
+ background: url('./../images/droppable-hover.png') no-repeat;
+ background-attachment: fixed;
+}
+#dropbox {
+ font-size: 0.8em;
+ font-size-adjust: none;
+ font-stretch: normal;
+ -x-system-font: none;
+ background-color: transparent;
+ border-top: 1px solid #b3b38d;
+ border-left: 1px solid #b3b38d;
+ border-right: 1px solid #72724c;
+ border-bottom: 1px solid #72724c;
+ color: #56562b;
+
+ line-height: normal;
+ margin-bottom: 10px;
+ padding: 1px;
+ position:fixed;
+ width: 290px;
+ top: 150px;
+ right: -1%;
+ overflow: hidden;
+ z-index: 100;
+}
+.link-expanded, .link-collapsed {
+ display: inline-block;
+ height: 13px;
+ margin: 0;
+ padding: 0;
+ width: 10px;
+}
+.link-expanded:hover, .link-collapsed:hover {
+ background-color: transparent;
+}
+.link-expanded {
+ background: url(./../images/icon_expanded.png) no-repeat 0 5px;
+}
+.link-collapsed {
+ background: url(./../images/icon_collapsed.png) no-repeat 0 4px;
+ width: 10px;
+}
+#dropbox .control-panel{background-color: #E3E3E3; font-size: 1.2em; opacity: 0.9}
+#dropbox .content{
+ background-color: #D1D1D1;
+ font-size: 1.3em;
+ font-weight: bolder;
+ opacity: 0.8;
+}
+#dropbox .content .odd{
+ background: #D1D1D1;
+ border-color: #ddd;
+ color: #444;
+
+ padding-top: 2px;
+ padding-right: 1px;
+ padding-left: 1px;
+ padding-bottom: 1px;
+}
+
+#dropbox .content .even{ background: #E3E3E3; border-color: #ccc; color: #333}
+
+#dropbox .content .numrows{font-size: 0.8em; font-weight: normal;}
+.droppable-active {
+ background-color: white;
+}
+
+.droppable-hover {
+ background: #90e224 url(./../images/droppable-hover.png) 0 0 !IMPORTANT;
+}
+#dragHelper{
+ background-color: #90e224;
+ cursor: move;
+ z-index: 101;
+}
View
124 itteco/htdocs/js/report.js
@@ -0,0 +1,124 @@
+var dragHelper = '<div id="dragHelper"><div>Dragging <span class="quantity">0</span> tickets.</div></div>';
+var draggable_options = {helper:function(e){return $(dragHelper).get();}, opacity: 0.8, start: function(e, ui){$('.quantity', ui.helper).text($(":checked").length || 1);}};
+decorateDropBox = function(){
+ var box = $('#dropbox');
+ $('.control-panel a', box).bind('click', function(e){
+ var content = $('#dropbox .content');
+ if(content.is(":visible")){
+ console.log('saving height',box.height());
+ box.attr('savedHeight', box.height());
+ }
+ var cp = $(this).parent();
+ cp.siblings().andSelf().toggle();
+ content.toggle();
+ if(content.is(":visible")){
+ box.resizable("enable");
+ if(box.is('[savedHeight]')){
+ console.log('restoring height',box.attr('savedHeight'));
+ box.height(box.attr('savedHeight')+'px');
+ }
+ }else{
+ box.resizable("disable");
+ box.height(cp.height()+3);
+ }
+ });
+ $('.report-result:odd', box).addClass('odd');
+ $('.report-result:even', box).addClass('even');
+ box.draggable();
+ box.resizable();
+}
+syncGroupIds = function(){
+ var bodyGroups = $('#content .report-result');
+ $('#dropbox .content .report-result').each(function(i){
+ var dropBoxGroup = $(this);
+ var pattern = dropBoxGroup.text().replace(/\n\s*/, '');
+ dropBoxGroup.attr('idx', 'group_'+i);
+
+ bodyGroups.not('[idx]').each(function(i){
+ var contentGroup = $(this);
+ if(contentGroup.text().replace(/\n\s*/, '')==pattern){
+ contentGroup.attr('idx', dropBoxGroup.attr('idx'));
+ return true;
+ }
+ });
+ });
+}
+disableTickets = function(tkts){
+ $(":checked", tkts).attr('disabled', 'disabled');
+}
+enableTickets = function(tkts){
+ $(":checked", tkts).removeAttr('disabled').attr('checked', false);
+ console.log(tkts);
+ $('td.ticket',tkts).draggable(draggable_options);
+}
+enableDragAndDrop = function(){
+ var tkts = $('.listing tbody td.ticket');
+ tkts.css('cursor','move').prepend('<input type="checkbox"/>');
+ tkts.each(function(i){var t = $(this); t.attr('ticket', $('a', t).text().substring(1)); });
+ tkts.draggable(draggable_options);
+ $('#dropbox .report-result').droppable({accept:'.ticket', activeClass: 'droppable-active', hoverClass: 'droppable-hover', drop: dropTicketsToGroup});
+}
+dropTicketsToGroup = function(e, ui){
+ var tkts = $('.listing tbody td.ticket:has(:checked)');
+ if(tkts.length==0){
+ tkts = ui.draggable;
+ }
+ disableTickets(tkts)
+ executeAjaxAction(tkts, $(this));
+}
+getMatchesText = function(cnt){
+ var txt = '(No matches)';
+ if (cnt>0){
+ if(cnt==1){
+ txt = '(1 match)';
+ }else{
+ txt = '('+cnt+' matches)';
+ }
+ }
+ return txt;
+}
+calculateGroupMatches= function(){
+ var groupBoxGroups= $('#dropbox .content .report-result');
+
+ $('#content .report-result').each(function(i){
+ var o = $(this);
+ var txt = getMatchesText($('td.ticket', o.next()).length);
+ $('.numrows', o).text(txt);
+ $('.numrows', groupBoxGroups.filter('[idx="'+o.attr('idx') +'"]')).text(txt);
+ })
+}
+executeAjaxAction = function(tickets, target){
+ var ids = $.map(tickets,function(tkt, i){return $('a',tkt).text().substring(1)});
+ $.getJSON(document.location.pathname, {action: 'execute', tickets: ids.join(','), presets: target.attr('preset')},
+ function(data){
+ if (data){
+ if(data.tickets){
+ var selectors = [];
+ $.each(data.tickets, function(){
+ selectors.push(' td.ticket[ticket="'+this+'"]');
+ });
+ var rows = $(selectors.join(','), '.listing tbody').parent().remove();
+
+ var target_table = $('#content .report-result[idx="'+target.attr('idx') +'"]').next();
+ if(target_table && target_table.length>0){
+ $('tbody', target_table).append(rows);
+ }else{
+ var sMatch = $('.numrows', target);
+ var s = sMatch.text().match(/\d+/);
+ var currQuantity = 0;
+ if (s && s.length>0){
+ currQuantity = parseInt(s[0], 10);
+ }
+ sMatch.text(getMatchesText(currQuantity+rows.length));
+ }
+ calculateGroupMatches();
+ enableTickets(rows);
+ }
+ }
+ });
+}
+$(document).ready(function(){
+ decorateDropBox();
+ syncGroupIds();
+ enableDragAndDrop();
+});
View
2  itteco/templates/itteco_whiteboard_utils.html
@@ -22,7 +22,7 @@
"statuses" : ${as_js_dict(config['statuses'])},
"overall_completion" : ${config['overall_completion'] or 0},
"label" : "${config['label'] or config['name']}",
- "css_class" : "${config['css_class']}"},
+ "css_class" : "${config['css_class'] or config['name']}"},
</py:for>
"x":1
};
View
113 itteco/ticket/report.py
@@ -0,0 +1,113 @@
+from genshi.builder import tag
+from genshi.filters.transform import Transformer
+
+from trac.core import implements, Component
+from trac.ticket.model import Ticket
+from trac.ticket.report import ReportModule
+from trac.util.translation import _
+from trac.web.api import ITemplateStreamFilter, IRequestFilter
+
+from itteco.utils import json
+
+class IttecoReportModule(Component):
+ implements(ITemplateStreamFilter, IRequestFilter)
+
+ mandatory_cols = ["__group__", "__group_preset__"]
+
+ #ITemplateStreamFilter methods
+ def filter_stream(self, req, method, filename, stream, data):
+ if filename=='report_view.html':
+ self.env.log.debug("report data='%s'" % (data,))
+ id = req.args.get('id')
+ action = req.args.get('action', 'view')
+ header_groups = data.get('header_groups')
+
+ if id and action=='view' and header_groups and len(header_groups)>0:
+ try:
+ if self._are_all_mandatory_fields_found(header_groups[0]):
+ link_builder = req.href.chrome
+ script_tag = lambda x: tag.script(type="text/javascript", src=link_builder(x))
+ stream |= Transformer("//head").append(tag(
+ # TODO fix scripts base path
+ tag.link(type="text/css", rel="stylesheet", href=link_builder("itteco/css/report.css")),
+ script_tag("itteco/js/jquery.ui/ui.core.js"),
+ script_tag("itteco/js/jquery.ui/ui.draggable.js"),
+ script_tag("itteco/js/jquery.ui/ui.droppable.js"),
+ script_tag("itteco/js/jquery.ui/ui.resizable.js"),
+ script_tag("itteco/js/report.js")))
+
+ args = ReportModule(self.env).get_var_args(req)
+ db = self.env.get_db_cnx()
+ cursor = db.cursor()
+ cursor.execute("SELECT query FROM report WHERE id=%s", (id,))
+ sql, = cursor.fetchone()
+ sql, args = ReportModule(self.env).sql_sub_vars(sql, args, db)
+ cursor.execute("SELECT DISTINCT __group__, __group_preset__, count(*) "+\
+ "FROM (%s) as group_config GROUP BY __group__, __group_preset__" % sql, args)
+ tags = []
+ for group, preset, quantity in cursor:
+ if preset:
+ tags.append(tag.div(group+'\n', \
+ tag.span(
+ quantity and '(%d match%s)' % (quantity, quantity!=1 and 'es' or '') \
+ or '(No matches)', class_='numrows'), \
+ preset=preset, class_='report-result'))
+ stream |= Transformer("//*[@id='main']").after(
+ tag.div(_render_conrol_panel(), tag.div(class_='content', *tags), id="dropbox"))
+ except ValueError,e:
+ #we do not fail the report it self, may be it works in read only mode
+ self.env.log.debug('Report decoration failed: %s' % e)
+ return stream
+
+ def _are_all_mandatory_fields_found(self, cols):
+ found_fields = [col for col in cols if col['col'] in self.mandatory_cols]
+ return len(found_fields)==len(self.mandatory_cols)
+
+ #IRequestFilter methods
+ def pre_process_request(self, req, handler):
+ if req.path_info.startswith('/report/') and req.args.get('action')=='execute':
+ return self
+ return handler
+
+ def post_process_request(self, req, template, content_type):
+ return (template, content_type)
+
+ def post_process_request(self, req, template, data, content_type):
+ return (template, data, content_type)
+
+ #IRequestHandler mathod for action processing
+ def process_request(self, req):
+ tickets = req.args.get('tickets','').split(',')
+ presets = [kw.split('=', 1) for kw in req.args.get('presets','').split('&')]
+ warn = []
+ modified_tickets = []
+ if tickets and presets:
+ db = self.env.get_db_cnx()
+ for ticket_id in tickets:
+ if 'TICKET_CHGPROP' in req.perm('ticket', ticket_id):
+ ticket = Ticket(self.env, ticket_id, db)
+ for preset in presets:
+ field = value = None
+ if len(preset)==2:
+ field, value = preset
+ else:
+ field, = preset
+ ticket[field] = value
+ ticket.save_changes(req.authname, _("Changed from executable report"), db=db)
+ modified_tickets.append(ticket_id)
+ else:
+ warn.append(_("You have no permission to modify ticket '%(ticket)s'", ticket=ticket_id))
+ db.commit()
+ req.write(json.write({'tickets':modified_tickets, 'warnings': warn}))
+
+def _render_conrol_panel():
+ return tag.div(
+ tag.span(
+ tag.a(' ', class_='link-expanded', href='#'),
+ tag.a(_('Collapse Control Panel'), href='#')),
+ tag.span(
+ tag.a(' ',class_='link-collapsed', href='#'),
+ tag.a(_('Expand Control Panel'), href='#'),
+ style="display: none;"),
+ class_='control-panel'
+ )
View
1  setup.py
@@ -27,6 +27,7 @@
'itteco.scrum.web_ui = itteco.scrum.web_ui',
'itteco.scrum.burndown = itteco.scrum.burndown',
'itteco.ticket.admin = itteco.ticket.admin',
+ 'itteco.ticket.report = itteco.ticket.report',
'itteco.ticket.roadmap = itteco.ticket.roadmap',
'itteco.ticket.web_ui = itteco.ticket.web_ui']},
install_requires=['trac >= 0.11.3','genshi >= 0.5.1'],
Please sign in to comment.
Something went wrong with that request. Please try again.