Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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
authored May 18, 2009
3  ChangeLog
... ...
@@ -1,4 +1,7 @@
1 1
 # Itteco Trac Plugin Changelog #
  2
+IttecoTracPlugin 0.1.12 (May 19, 2009)
  3
+* Add support of modifiable reports
  4
+
2 5
 IttecoTracPlugin 0.1.12 (May 11, 2009)
3 6
 * Move widgets colorization to client side
4 7
 
2  itteco/__init__.py
... ...
@@ -1,2 +1,2 @@
1  
-__version__ = '0.1.12'
  1
+__version__ = '0.1.13'
2 2
 __package__ = 'itteco'
76  itteco/htdocs/css/report.css
... ...
@@ -0,0 +1,76 @@
  1
+body{
  2
+    background: url('./../images/droppable-hover.png') no-repeat;
  3
+    background-attachment: fixed;
  4
+}
  5
+#dropbox {
  6
+	font-size: 0.8em;
  7
+	font-size-adjust: none;
  8
+	font-stretch: normal;
  9
+	-x-system-font: none;
  10
+    background-color: transparent;
  11
+	border-top: 1px solid #b3b38d;
  12
+	border-left: 1px solid #b3b38d;
  13
+    border-right: 1px solid #72724c;
  14
+	border-bottom: 1px solid #72724c;
  15
+	color: #56562b;
  16
+    
  17
+	line-height: normal;
  18
+	margin-bottom: 10px;
  19
+	padding: 1px;
  20
+    position:fixed;
  21
+    width: 290px;    
  22
+    top: 150px; 
  23
+    right: -1%;
  24
+    overflow: hidden;
  25
+    z-index: 100;
  26
+}
  27
+.link-expanded, .link-collapsed {
  28
+	display: inline-block;
  29
+    height: 13px;
  30
+	margin: 0;
  31
+	padding: 0;
  32
+	width: 10px;
  33
+}
  34
+.link-expanded:hover, .link-collapsed:hover  {
  35
+	background-color: transparent;
  36
+}
  37
+.link-expanded {
  38
+	background: url(./../images/icon_expanded.png) no-repeat 0 5px;
  39
+}
  40
+.link-collapsed {    
  41
+	background: url(./../images/icon_collapsed.png) no-repeat 0 4px;
  42
+	width: 10px;
  43
+}
  44
+#dropbox .control-panel{background-color: #E3E3E3; font-size: 1.2em; opacity: 0.9}
  45
+#dropbox .content{
  46
+    background-color: #D1D1D1; 
  47
+    font-size: 1.3em;
  48
+    font-weight: bolder;
  49
+    opacity: 0.8;
  50
+}
  51
+#dropbox .content .odd{ 
  52
+    background: #D1D1D1;
  53
+    border-color: #ddd;
  54
+    color: #444;
  55
+   
  56
+    padding-top: 2px;
  57
+    padding-right: 1px;
  58
+    padding-left: 1px;
  59
+    padding-bottom: 1px;
  60
+}
  61
+
  62
+#dropbox .content .even{ background: #E3E3E3;  border-color: #ccc; color: #333}
  63
+
  64
+#dropbox .content .numrows{font-size: 0.8em; font-weight: normal;}
  65
+.droppable-active {
  66
+	background-color: white;
  67
+}
  68
+
  69
+.droppable-hover {
  70
+	background:  #90e224 url(./../images/droppable-hover.png) 0 0 !IMPORTANT;
  71
+}
  72
+#dragHelper{
  73
+	background-color: #90e224;
  74
+    cursor: move;
  75
+    z-index: 101;
  76
+}
124  itteco/htdocs/js/report.js
... ...
@@ -0,0 +1,124 @@
  1
+var dragHelper = '<div id="dragHelper"><div>Dragging <span class="quantity">0</span> tickets.</div></div>';
  2
+var draggable_options = {helper:function(e){return $(dragHelper).get();}, opacity: 0.8, start: function(e, ui){$('.quantity', ui.helper).text($(":checked").length || 1);}};
  3
+decorateDropBox = function(){
  4
+    var box = $('#dropbox');
  5
+    $('.control-panel a', box).bind('click', function(e){
  6
+        var content = $('#dropbox .content');
  7
+        if(content.is(":visible")){
  8
+            console.log('saving height',box.height());
  9
+            box.attr('savedHeight', box.height());
  10
+        }
  11
+        var cp = $(this).parent();
  12
+        cp.siblings().andSelf().toggle();
  13
+        content.toggle();
  14
+        if(content.is(":visible")){
  15
+            box.resizable("enable");
  16
+            if(box.is('[savedHeight]')){
  17
+                console.log('restoring height',box.attr('savedHeight'));
  18
+                box.height(box.attr('savedHeight')+'px');
  19
+            }
  20
+        }else{
  21
+            box.resizable("disable");
  22
+            box.height(cp.height()+3);
  23
+        }
  24
+    });
  25
+    $('.report-result:odd', box).addClass('odd');
  26
+    $('.report-result:even', box).addClass('even');
  27
+    box.draggable();
  28
+    box.resizable();
  29
+}
  30
+syncGroupIds = function(){
  31
+    var bodyGroups = $('#content .report-result');
  32
+    $('#dropbox .content .report-result').each(function(i){
  33
+        var dropBoxGroup = $(this);
  34
+        var pattern = dropBoxGroup.text().replace(/\n\s*/, '');
  35
+        dropBoxGroup.attr('idx', 'group_'+i);
  36
+        
  37
+        bodyGroups.not('[idx]').each(function(i){
  38
+            var contentGroup = $(this);
  39
+            if(contentGroup.text().replace(/\n\s*/, '')==pattern){
  40
+                contentGroup.attr('idx', dropBoxGroup.attr('idx'));
  41
+                return true;
  42
+            }
  43
+        });
  44
+    });
  45
+}
  46
+disableTickets = function(tkts){
  47
+    $(":checked", tkts).attr('disabled', 'disabled');
  48
+}
  49
+enableTickets = function(tkts){
  50
+    $(":checked", tkts).removeAttr('disabled').attr('checked', false);
  51
+    console.log(tkts);
  52
+    $('td.ticket',tkts).draggable(draggable_options);
  53
+}
  54
+enableDragAndDrop = function(){
  55
+  var tkts = $('.listing tbody td.ticket');
  56
+  tkts.css('cursor','move').prepend('<input type="checkbox"/>');
  57
+  tkts.each(function(i){var t = $(this); t.attr('ticket', $('a', t).text().substring(1)); });
  58
+  tkts.draggable(draggable_options);
  59
+  $('#dropbox .report-result').droppable({accept:'.ticket', activeClass: 'droppable-active', hoverClass: 'droppable-hover', drop: dropTicketsToGroup});
  60
+}
  61
+dropTicketsToGroup = function(e, ui){
  62
+    var tkts = $('.listing tbody td.ticket:has(:checked)');
  63
+    if(tkts.length==0){
  64
+        tkts = ui.draggable;
  65
+    }
  66
+    disableTickets(tkts)
  67
+    executeAjaxAction(tkts, $(this));
  68
+}
  69
+getMatchesText = function(cnt){
  70
+    var txt = '(No matches)';
  71
+    if (cnt>0){
  72
+        if(cnt==1){
  73
+            txt = '(1 match)';
  74
+        }else{
  75
+            txt = '('+cnt+' matches)';
  76
+        }
  77
+    }
  78
+    return txt;
  79
+}
  80
+calculateGroupMatches= function(){
  81
+    var groupBoxGroups= $('#dropbox .content .report-result');
  82
+    
  83
+    $('#content .report-result').each(function(i){
  84
+        var o = $(this);
  85
+        var txt = getMatchesText($('td.ticket', o.next()).length);
  86
+        $('.numrows', o).text(txt);
  87
+        $('.numrows', groupBoxGroups.filter('[idx="'+o.attr('idx') +'"]')).text(txt);
  88
+    })
  89
+}
  90
+executeAjaxAction = function(tickets, target){
  91
+    var ids = $.map(tickets,function(tkt, i){return $('a',tkt).text().substring(1)});
  92
+    $.getJSON(document.location.pathname, {action: 'execute', tickets: ids.join(','), presets: target.attr('preset')}, 
  93
+        function(data){ 
  94
+            if (data){
  95
+                if(data.tickets){
  96
+                    var selectors = [];
  97
+                    $.each(data.tickets, function(){
  98
+                        selectors.push(' td.ticket[ticket="'+this+'"]');
  99
+                    });
  100
+                    var rows = $(selectors.join(','), '.listing tbody').parent().remove();
  101
+                    
  102
+                    var target_table = $('#content .report-result[idx="'+target.attr('idx') +'"]').next();
  103
+                    if(target_table && target_table.length>0){
  104
+                        $('tbody', target_table).append(rows);
  105
+                    }else{
  106
+                        var sMatch = $('.numrows', target);
  107
+                        var s = sMatch.text().match(/\d+/);
  108
+                        var currQuantity = 0;
  109
+                        if (s && s.length>0){
  110
+                            currQuantity = parseInt(s[0], 10);                            
  111
+                        }
  112
+                        sMatch.text(getMatchesText(currQuantity+rows.length));
  113
+                    }
  114
+                    calculateGroupMatches();
  115
+                    enableTickets(rows);
  116
+                }
  117
+            }
  118
+        });
  119
+}
  120
+$(document).ready(function(){
  121
+    decorateDropBox();
  122
+    syncGroupIds();
  123
+    enableDragAndDrop();
  124
+});
2  itteco/templates/itteco_whiteboard_utils.html
@@ -22,7 +22,7 @@
22 22
             "statuses" : ${as_js_dict(config['statuses'])},
23 23
             "overall_completion" : ${config['overall_completion'] or 0},
24 24
             "label" : "${config['label'] or config['name']}",
25  
-            "css_class" : "${config['css_class']}"},
  25
+            "css_class" : "${config['css_class'] or config['name']}"},
26 26
         </py:for>
27 27
             "x":1
28 28
         };
113  itteco/ticket/report.py
... ...
@@ -0,0 +1,113 @@
  1
+from genshi.builder import tag
  2
+from genshi.filters.transform import Transformer
  3
+
  4
+from trac.core import implements, Component
  5
+from trac.ticket.model import Ticket
  6
+from trac.ticket.report import ReportModule
  7
+from trac.util.translation import _
  8
+from trac.web.api import ITemplateStreamFilter, IRequestFilter
  9
+
  10
+from itteco.utils import json
  11
+
  12
+class IttecoReportModule(Component):
  13
+    implements(ITemplateStreamFilter, IRequestFilter)
  14
+    
  15
+    mandatory_cols = ["__group__", "__group_preset__"]
  16
+    
  17
+    #ITemplateStreamFilter methods
  18
+    def filter_stream(self, req, method, filename, stream, data):
  19
+        if filename=='report_view.html':
  20
+            self.env.log.debug("report data='%s'" % (data,))
  21
+            id = req.args.get('id')
  22
+            action = req.args.get('action', 'view')
  23
+            header_groups = data.get('header_groups')
  24
+
  25
+            if id and action=='view' and header_groups and len(header_groups)>0:
  26
+                try:
  27
+                    if self._are_all_mandatory_fields_found(header_groups[0]):
  28
+                        link_builder = req.href.chrome
  29
+                        script_tag = lambda x: tag.script(type="text/javascript", src=link_builder(x))
  30
+                        stream |= Transformer("//head").append(tag(
  31
+                            # TODO fix scripts base path
  32
+                            tag.link(type="text/css", rel="stylesheet", href=link_builder("itteco/css/report.css")),
  33
+                            script_tag("itteco/js/jquery.ui/ui.core.js"),
  34
+                            script_tag("itteco/js/jquery.ui/ui.draggable.js"),
  35
+                            script_tag("itteco/js/jquery.ui/ui.droppable.js"),
  36
+                            script_tag("itteco/js/jquery.ui/ui.resizable.js"),
  37
+                            script_tag("itteco/js/report.js")))
  38
+                            
  39
+                        args = ReportModule(self.env).get_var_args(req)
  40
+                        db = self.env.get_db_cnx()
  41
+                        cursor = db.cursor()
  42
+                        cursor.execute("SELECT query FROM report WHERE id=%s", (id,))
  43
+                        sql, = cursor.fetchone()
  44
+                        sql, args = ReportModule(self.env).sql_sub_vars(sql, args, db)
  45
+                        cursor.execute("SELECT DISTINCT __group__, __group_preset__, count(*) "+\
  46
+                            "FROM (%s) as group_config GROUP BY  __group__, __group_preset__" % sql, args)
  47
+                        tags = []
  48
+                        for group, preset, quantity in cursor:
  49
+                            if preset:
  50
+                                tags.append(tag.div(group+'\n', \
  51
+                                    tag.span(
  52
+                                        quantity and '(%d match%s)' % (quantity, quantity!=1 and 'es' or '') \
  53
+                                            or '(No matches)', class_='numrows'), \
  54
+                                    preset=preset, class_='report-result'))
  55
+                        stream |= Transformer("//*[@id='main']").after(
  56
+                            tag.div(_render_conrol_panel(), tag.div(class_='content', *tags), id="dropbox"))
  57
+                except ValueError,e:
  58
+                    #we do not fail the report it self, may be it works in read only mode
  59
+                    self.env.log.debug('Report decoration failed: %s' % e)
  60
+        return stream
  61
+
  62
+    def _are_all_mandatory_fields_found(self, cols):        
  63
+        found_fields = [col for col in cols if col['col'] in self.mandatory_cols]
  64
+        return len(found_fields)==len(self.mandatory_cols)
  65
+
  66
+    #IRequestFilter methods
  67
+    def pre_process_request(self, req, handler):
  68
+        if req.path_info.startswith('/report/') and req.args.get('action')=='execute':
  69
+            return self
  70
+        return handler
  71
+
  72
+    def post_process_request(self, req, template, content_type):
  73
+        return (template, content_type)
  74
+
  75
+    def post_process_request(self, req, template, data, content_type):
  76
+        return (template, data, content_type)
  77
+        
  78
+    #IRequestHandler mathod for action processing
  79
+    def process_request(self, req):
  80
+        tickets = req.args.get('tickets','').split(',')
  81
+        presets = [kw.split('=', 1) for kw in req.args.get('presets','').split('&')]
  82
+        warn = []
  83
+        modified_tickets = []
  84
+        if tickets and presets:
  85
+            db = self.env.get_db_cnx()
  86
+            for ticket_id in tickets:
  87
+                if 'TICKET_CHGPROP' in req.perm('ticket', ticket_id):
  88
+                    ticket  = Ticket(self.env, ticket_id, db)
  89
+                    for preset in presets:
  90
+                        field = value = None
  91
+                        if len(preset)==2:
  92
+                            field, value = preset
  93
+                        else:
  94
+                            field, = preset
  95
+                        ticket[field] = value
  96
+                    ticket.save_changes(req.authname, _("Changed from executable report"), db=db)
  97
+                    modified_tickets.append(ticket_id)
  98
+                else:
  99
+                    warn.append(_("You have no permission to modify ticket '%(ticket)s'", ticket=ticket_id))
  100
+            db.commit()
  101
+        req.write(json.write({'tickets':modified_tickets, 'warnings': warn}))
  102
+
  103
+def _render_conrol_panel():
  104
+    return tag.div(
  105
+      tag.span(
  106
+        tag.a(' ', class_='link-expanded', href='#'),
  107
+        tag.a(_('Collapse Control Panel'), href='#')),
  108
+      tag.span(
  109
+        tag.a(' ',class_='link-collapsed', href='#'),
  110
+        tag.a(_('Expand Control Panel'), href='#'),
  111
+        style="display: none;"),
  112
+      class_='control-panel'
  113
+    )
1  setup.py
@@ -27,6 +27,7 @@
27 27
                                      'itteco.scrum.web_ui = itteco.scrum.web_ui',
28 28
                                      'itteco.scrum.burndown = itteco.scrum.burndown',
29 29
                                      'itteco.ticket.admin = itteco.ticket.admin',
  30
+                                     'itteco.ticket.report = itteco.ticket.report',
30 31
                                      'itteco.ticket.roadmap = itteco.ticket.roadmap',
31 32
                                      'itteco.ticket.web_ui = itteco.ticket.web_ui']},
32 33
     install_requires=['trac >= 0.11.3','genshi >= 0.5.1'], 

0 notes on commit 15c4ea5

Please sign in to comment.
Something went wrong with that request. Please try again.