Skip to content
Permalink
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...
andrew.starkoff
andrew.starkoff committed May 18, 2009
1 parent f3d9ea7 commit 15c4ea52034da4cb5a6eefce1ccf38d60d9ddfe9
Showing with 319 additions and 2 deletions.
  1. +3 −0 ChangeLog
  2. +1 −1 itteco/__init__.py
  3. +76 −0 itteco/htdocs/css/report.css
  4. +124 −0 itteco/htdocs/js/report.js
  5. +1 −1 itteco/templates/itteco_whiteboard_utils.html
  6. +113 −0 itteco/ticket/report.py
  7. +1 −0 setup.py
@@ -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

@@ -1,2 +1,2 @@
__version__ = '0.1.12'
__version__ = '0.1.13'
__package__ = 'itteco'
@@ -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;
}
@@ -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();
});
@@ -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
};
@@ -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'
)
@@ -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'],

0 comments on commit 15c4ea5

Please sign in to comment.
You can’t perform that action at this time.