trac-mastertickets-3.0.2+20111224/0000755000175000017500000000000011677657652014377 5ustar wmbwmbtrac-mastertickets-3.0.2+20111224/.gitignore0000644000175000017500000000003411675460244016347 0ustar wmbwmb*.pyc *.egg-info dist build trac-mastertickets-3.0.2+20111224/setup.py0000644000175000017500000000273011675460244016076 0ustar wmbwmb#!/usr/bin/env python # -*- coding: iso-8859-1 -*- import os from setuptools import setup setup( name = 'TracMasterTickets', version = '3.0.2', packages = ['mastertickets'], package_data = { 'mastertickets': ['templates/*.html', 'htdocs/*.js', 'htdocs/*.css' ] }, author = 'Noah Kantrowitz', author_email = 'noah@coderanger.net', description = 'Provides support for ticket dependencies and master tickets.', long_description = open(os.path.join(os.path.dirname(__file__), 'README')).read(), license = 'BSD', keywords = 'trac plugin ticket dependencies master', url = 'http://github.com/coderanger/trac-mastertickets', classifiers = [ 'Framework :: Trac', #'Development Status :: 1 - Planning', # 'Development Status :: 2 - Pre-Alpha', # 'Development Status :: 3 - Alpha', # 'Development Status :: 4 - Beta', 'Development Status :: 5 - Production/Stable', # 'Development Status :: 6 - Mature', # 'Development Status :: 7 - Inactive', 'Environment :: Web Environment', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', ], install_requires = ['Trac>=0.12'], entry_points = { 'trac.plugins': [ 'mastertickets.web_ui = mastertickets.web_ui', 'mastertickets.api = mastertickets.api', ] } ) trac-mastertickets-3.0.2+20111224/README0000644000175000017500000000707411675460244015252 0ustar wmbwmbNotes ===== Adds basic ticket dependencies for Trac. Note: MasterTickets 3.0 requires Trac 0.12 or higher. What is it? ----------- This plugin adds "blocking" and "blocked_by" fields to each ticket, enabling you to express dependencies between tickets. It also provides a graphviz-based dependency-graph feature for those tickets having dependencies specified, allowing you to visually understand the dependency tree. The dependency graph is viewable by clicking 'depgraph' in the context (in the upper right corner) menu when viewing a ticket that blocks or is blocked by another ticket. What is it not? --------------- * It does not provide ticket-hiding for sub-tasks of a top-level ticket. * There is no orthogonal parent/child relationship possible * You cannot view the descriptions of tickets depending on the current ticket * In fact, there are no explicit features that can assist you with sub-task management * Although it would be cool. * It does not allow you to create a dependent ticket from the current ticket * It does not include reporting features to show how tasks are interrelated (other than the dependency graph already described above). Configuration ============= To use this plugin you must configure two custom fields named ``blocking`` and ``blocked_by``. All other configuration options go in the ``[mastertickets]`` section. ``dot_path`` : *optional, default: dot* Path to the dot executable. This is only used for the dependency graph. ``use_gs`` : *optional, default: False* If enabled, use ghostscript to produce a nicer dependency graph. ``gs_path`` : *optional, default: gs* Path to the ghostscript executable. ``closed_color`` : *optional, default: green* Color of closed tickets ``opened_color`` : *optional, default: red* Color of opened tickets ``graph_direction`` : *optional, default: TD* Direction of the dependency graph (TD = Top Down, DT = Down Top, LR = Left Right, RL = Right Left) To enable the plugin:: [components] mastertickets.* = enabled [ticket-custom] blocking = text blocking.label = Blocking blockedby = text blockedby.label = Blocked By Custom fields ------------- While the two field names must be ``blocking`` and ``blocked_by``, you are free to use any text for the field labels. Example ======= To use a locally-built graphviz:: [mastertickets] dot_path = /usr/local/bin/dot [components] mastertickets.* = enabled [ticket-custom] blocking = text blocking.label = Blocking blockedby = text blockedby.label = Blocked By Example reports -------------- To only show the tickets that are currently not blocked by other non-closed tickets, use this SQL (eg in a new report): SELECT p.value AS __color__, id AS ticket, summary, component, version, milestone, t.type AS type, owner, status, time AS created, changetime AS _changetime, description AS _description, reporter AS _reporter, (SELECT COUNT(*) FROM mastertickets m, ticket t2 WHERE t.id=m.dest AND m.source=t2.id AND t2.status <> 'closed') AS _blocked FROM ticket t LEFT JOIN enum p ON p.name = t.priority AND p.type = 'priority' WHERE status <> 'closed' AND _blocked = 0 ORDER BY CAST(p.value AS integer), milestone, t.type, time Basically it is the default report, with an aditional, hidden field (the sub query), named _blocked. The value of this field is checked to be zero. Using this method, other reports can be modified as well, eg to show the number of tickets blocking a ticket and/or the number of tickets that the ticked is blocking itself. trac-mastertickets-3.0.2+20111224/mastertickets/0000755000175000017500000000000011675460244017244 5ustar wmbwmbtrac-mastertickets-3.0.2+20111224/mastertickets/__init__.py0000644000175000017500000000000011675460244021343 0ustar wmbwmbtrac-mastertickets-3.0.2+20111224/mastertickets/templates/0000755000175000017500000000000011675460244021242 5ustar wmbwmbtrac-mastertickets-3.0.2+20111224/mastertickets/templates/depgraph.html0000644000175000017500000000253211675460244023724 0ustar wmbwmb Dependency Graph #$tkt.id Dependency Graph for milestone $milestone

Dependency Graph for milestone $milestone

Dependency graph

Dependency Graph for Ticket #$tkt.id

Dependency graph
${Markup(graph_render('cmapx').decode('utf8'))}
trac-mastertickets-3.0.2+20111224/mastertickets/model.py0000644000175000017500000001362311675460244020723 0ustar wmbwmb# Created by Noah Kantrowitz on 2007-07-04. # Copyright (c) 2007 Noah Kantrowitz. All rights reserved. import copy from datetime import datetime from trac.ticket.model import Ticket from trac.util.compat import set, sorted from trac.util.datefmt import utc, to_utimestamp class TicketLinks(object): """A model for the ticket links used MasterTickets.""" def __init__(self, env, tkt, db=None): self.env = env if not isinstance(tkt, Ticket): tkt = Ticket(self.env, tkt) self.tkt = tkt db = db or self.env.get_db_cnx() cursor = db.cursor() cursor.execute('SELECT dest FROM mastertickets WHERE source=%s ORDER BY dest', (self.tkt.id,)) self.blocking = set([int(num) for num, in cursor]) self._old_blocking = copy.copy(self.blocking) cursor.execute('SELECT source FROM mastertickets WHERE dest=%s ORDER BY source', (self.tkt.id,)) self.blocked_by = set([int(num) for num, in cursor]) self._old_blocked_by = copy.copy(self.blocked_by) def save(self, author, comment='', when=None, db=None): """Save new links.""" if when is None: when = datetime.now(utc) when_ts = to_utimestamp(when) handle_commit = False if db is None: db = self.env.get_db_cnx() handle_commit = True cursor = db.cursor() new_blocking = set(int(n) for n in self.blocking) new_blocked_by = set(int(n) for n in self.blocked_by) to_check = [ # new, old, field (new_blocking, self._old_blocking, 'blockedby', ('source', 'dest')), (new_blocked_by, self._old_blocked_by, 'blocking', ('dest', 'source')), ] for new_ids, old_ids, field, sourcedest in to_check: for n in new_ids | old_ids: update_field = None if n in new_ids and n not in old_ids: # New ticket added cursor.execute('INSERT INTO mastertickets (%s, %s) VALUES (%%s, %%s)'%sourcedest, (self.tkt.id, n)) update_field = lambda lst: lst.append(str(self.tkt.id)) elif n not in new_ids and n in old_ids: # Old ticket removed cursor.execute('DELETE FROM mastertickets WHERE %s=%%s AND %s=%%s'%sourcedest, (self.tkt.id, n)) update_field = lambda lst: lst.remove(str(self.tkt.id)) if update_field is not None: cursor.execute('SELECT value FROM ticket_custom WHERE ticket=%s AND name=%s', (n, str(field))) old_value = (cursor.fetchone() or ('',))[0] new_value = [x.strip() for x in old_value.split(',') if x.strip()] update_field(new_value) new_value = ', '.join(sorted(new_value, key=lambda x: int(x))) cursor.execute('INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) VALUES (%s, %s, %s, %s, %s, %s)', (n, when_ts, author, field, old_value, new_value)) if comment: cursor.execute('INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) VALUES (%s, %s, %s, %s, %s, %s)', (n, when_ts, author, 'comment', '', '(In #%s) %s'%(self.tkt.id, comment))) cursor.execute('UPDATE ticket_custom SET value=%s WHERE ticket=%s AND name=%s', (new_value, n, field)) # refresh the changetime to prevent concurrent edits cursor.execute('UPDATE ticket SET changetime=%s WHERE id=%s', (when_ts,n)) if not cursor.rowcount: cursor.execute('INSERT INTO ticket_custom (ticket, name, value) VALUES (%s, %s, %s)', (n, field, new_value)) # cursor.execute('DELETE FROM mastertickets WHERE source=%s OR dest=%s', (self.tkt.id, self.tkt.id)) # data = [] # for tkt in self.blocking: # if isinstance(tkt, Ticket): # tkt = tkt.id # data.append((self.tkt.id, tkt)) # for tkt in self.blocked_by: # if isisntance(tkt, Ticket): # tkt = tkt.id # data.append((tkt, self.tkt.id)) # # cursor.executemany('INSERT INTO mastertickets (source, dest) VALUES (%s, %s)', data) if handle_commit: db.commit() def __nonzero__(self): return bool(self.blocking) or bool(self.blocked_by) def __repr__(self): def l(arr): arr2 = [] for tkt in arr: if isinstance(tkt, Ticket): tkt = tkt.id arr2.append(str(tkt)) return '[%s]'%','.join(arr2) return ''% \ (self.tkt.id, l(getattr(self, 'blocking', [])), l(getattr(self, 'blocked_by', []))) @staticmethod def walk_tickets(env, tkt_ids): """Return an iterable of all links reachable directly above or below those ones.""" def visit(tkt, memo, next_fn): if tkt in memo: return False links = TicketLinks(env, tkt) memo[tkt] = links for n in next_fn(links): visit(n, memo, next_fn) memo1 = {} memo2 = {} for id in tkt_ids: visit(id, memo1, lambda links: links.blocking) visit(id, memo2, lambda links: links.blocked_by) memo1.update(memo2) return memo1.itervalues() trac-mastertickets-3.0.2+20111224/mastertickets/htdocs/0000755000175000017500000000000011675460244020530 5ustar wmbwmbtrac-mastertickets-3.0.2+20111224/mastertickets/htdocs/x.png0000644000175000017500000000130211675460244021501 0ustar wmbwmbPNG  IHDRaiCCPICC ProfilexkQ? ,iEQ!MЃIZcIH#M 6$MMԪ=`9ŃC=F.*XX& >3|ͼ.D= /^V5e#LsX .D}*c{xϊG8HQ@<e |.HU@6 =  z@K Ryt PfEj,en 3Қ_Kz7U >N6G/)Z?@RN@} |yx]8p`}I.̾;ƱTV48y  %!)MScϋvg  ם|r&J sj 逅nT Я:i%Sl. {YkCVgecɥ~"0IRhh(,#F kOA_7w.@)8JU慨[Jik9Q%0DƱaIDAT8ݓA 0'M_YXhs i81ph 쭿, sy!KzoP :|Ѻ%S;Wwt^OY0o[(OIENDB`trac-mastertickets-3.0.2+20111224/mastertickets/htdocs/linkify_blockedby.js0000644000175000017500000000013411675460244024547 0ustar wmbwmb$(function() { $('td[@headers=h_blockedby]').html($('#linkified_blockedby').html()); });trac-mastertickets-3.0.2+20111224/mastertickets/htdocs/ticket.css0000644000175000017500000000015511675460244022526 0ustar wmbwmbinput#blockedby { display: none; } label[for=blockedby] { display: none; } img.blockedby_icon { width: 1em; }trac-mastertickets-3.0.2+20111224/mastertickets/htdocs/disable_resolve.js0000644000175000017500000000021011675460244024221 0ustar wmbwmb$(function() { $("#action_resolve_resolve_resolution option[text='fixed']").attr('disabled', 'disabled').removeAttr('selected'); }); trac-mastertickets-3.0.2+20111224/mastertickets/htdocs/linkify_blocking.js0000644000175000017500000000013211675460244024377 0ustar wmbwmb$(function() { $('td[@headers=h_blocking]').html($('#linkified_blocking').html()); });trac-mastertickets-3.0.2+20111224/mastertickets/htdocs/exclaim.png0000644000175000017500000000124111675460244022656 0ustar wmbwmbPNG  IHDRaiCCPICC ProfilexkQ uQ+$fUR IK = <16x6vpR3-Rk &15[±yx.&h1k<̼=k4`4{/0xΝ.McGx pNMPb O4GC%S,E+ϛ[}wa`igjJ]r\6z8θlʸFXrz^idu#rr7;et.q&$I!##a0+H8 < FJh`lD׼n5 [fːݶٷ +$e: ~K2=IDAT8cd FF 4300000QgiQMHtI4Yk 6kPpIENDB`trac-mastertickets-3.0.2+20111224/mastertickets/htdocs/jquery.js0000644000175000017500000015724111675460244022417 0ustar wmbwmb/* prevent execution of jQuery if included more than once */ if(typeof window.jQuery == "undefined") { /* * jQuery 1.1 - New Wave Javascript * * Copyright (c) 2007 John Resig (jquery.com) * Dual licensed under the MIT (MIT-LICENSE.txt) * and GPL (GPL-LICENSE.txt) licenses. * * $Date: 2007-01-14 17:37:33 -0500 (Sun, 14 Jan 2007) $ * $Rev: 1073 $ */ // Global undefined variable window.undefined = window.undefined; var jQuery = function(a,c) { // If the context is global, return a new object if ( window == this ) return new jQuery(a,c); // Make sure that a selection was provided a = a || document; // HANDLE: $(function) // Shortcut for document ready // Safari reports typeof on DOM NodeLists as a function if ( jQuery.isFunction(a) && !a.nodeType && a[0] == undefined ) return new jQuery(document)[ jQuery.fn.ready ? "ready" : "load" ]( a ); // Handle HTML strings if ( typeof a == "string" ) { var m = /^[^<]*(<.+>)[^>]*$/.exec(a); a = m ? // HANDLE: $(html) -> $(array) jQuery.clean( [ m[1] ] ) : // HANDLE: $(expr) jQuery.find( a, c ); } return this.setArray( // HANDLE: $(array) a.constructor == Array && a || // HANDLE: $(arraylike) // Watch for when an array-like object is passed as the selector (a.jquery || a.length && a != window && !a.nodeType && a[0] != undefined && a[0].nodeType) && jQuery.makeArray( a ) || // HANDLE: $(*) [ a ] ); }; // Map over the $ in case of overwrite if ( typeof $ != "undefined" ) jQuery._$ = $; // Map the jQuery namespace to the '$' one var $ = jQuery; jQuery.fn = jQuery.prototype = { jquery: "1.1", size: function() { return this.length; }, length: 0, get: function( num ) { return num == undefined ? // Return a 'clean' array jQuery.makeArray( this ) : // Return just the object this[num]; }, pushStack: function( a ) { var ret = jQuery(this); ret.prevObject = this; return ret.setArray( a ); }, setArray: function( a ) { this.length = 0; [].push.apply( this, a ); return this; }, each: function( fn, args ) { return jQuery.each( this, fn, args ); }, index: function( obj ) { var pos = -1; this.each(function(i){ if ( this == obj ) pos = i; }); return pos; }, attr: function( key, value, type ) { var obj = key; // Look for the case where we're accessing a style value if ( key.constructor == String ) if ( value == undefined ) return jQuery[ type || "attr" ]( this[0], key ); else { obj = {}; obj[ key ] = value; } // Check to see if we're setting style values return this.each(function(){ // Set all the styles for ( var prop in obj ) jQuery.attr( type ? this.style : this, prop, jQuery.prop(this, obj[prop], type) ); }); }, css: function( key, value ) { return this.attr( key, value, "curCSS" ); }, text: function(e) { if ( typeof e == "string" ) return this.empty().append( document.createTextNode( e ) ); var t = ""; jQuery.each( e || this, function(){ jQuery.each( this.childNodes, function(){ if ( this.nodeType != 8 ) t += this.nodeType != 1 ? this.nodeValue : jQuery.fn.text([ this ]); }); }); return t; }, wrap: function() { // The elements to wrap the target around var a = jQuery.clean(arguments); // Wrap each of the matched elements individually return this.each(function(){ // Clone the structure that we're using to wrap var b = a[0].cloneNode(true); // Insert it before the element to be wrapped this.parentNode.insertBefore( b, this ); // Find the deepest point in the wrap structure while ( b.firstChild ) b = b.firstChild; // Move the matched element to within the wrap structure b.appendChild( this ); }); }, append: function() { return this.domManip(arguments, true, 1, function(a){ this.appendChild( a ); }); }, prepend: function() { return this.domManip(arguments, true, -1, function(a){ this.insertBefore( a, this.firstChild ); }); }, before: function() { return this.domManip(arguments, false, 1, function(a){ this.parentNode.insertBefore( a, this ); }); }, after: function() { return this.domManip(arguments, false, -1, function(a){ this.parentNode.insertBefore( a, this.nextSibling ); }); }, end: function() { return this.prevObject || jQuery([]); }, find: function(t) { return this.pushStack( jQuery.map( this, function(a){ return jQuery.find(t,a); }) ); }, clone: function(deep) { return this.pushStack( jQuery.map( this, function(a){ return a.cloneNode( deep != undefined ? deep : true ); }) ); }, filter: function(t) { return this.pushStack( jQuery.isFunction( t ) && jQuery.grep(this, function(el, index){ return t.apply(el, [index]) }) || jQuery.multiFilter(t,this) ); }, not: function(t) { return this.pushStack( t.constructor == String && jQuery.multiFilter(t,this,true) || jQuery.grep(this,function(a){ if ( t.constructor == Array || t.jquery ) return jQuery.inArray( t, a ) < 0; else return a != t; }) ); }, add: function(t) { return this.pushStack( jQuery.merge( this.get(), typeof t == "string" ? jQuery(t).get() : t ) ); }, is: function(expr) { return expr ? jQuery.filter(expr,this).r.length > 0 : false; }, val: function( val ) { return val == undefined ? ( this.length ? this[0].value : null ) : this.attr( "value", val ); }, html: function( val ) { return val == undefined ? ( this.length ? this[0].innerHTML : null ) : this.empty().append( val ); }, domManip: function(args, table, dir, fn){ var clone = this.length > 1; var a = jQuery.clean(args); if ( dir < 0 ) a.reverse(); return this.each(function(){ var obj = this; if ( table && this.nodeName.toUpperCase() == "TABLE" && a[0].nodeName.toUpperCase() == "TR" ) obj = this.getElementsByTagName("tbody")[0] || this.appendChild(document.createElement("tbody")); jQuery.each( a, function(){ fn.apply( obj, [ clone ? this.cloneNode(true) : this ] ); }); }); } }; jQuery.extend = jQuery.fn.extend = function() { // copy reference to target object var target = arguments[0], a = 1; // extend jQuery itself if only one argument is passed if ( arguments.length == 1 ) { target = this; a = 0; } var prop; while (prop = arguments[a++]) // Extend the base object for ( var i in prop ) target[i] = prop[i]; // Return the modified object return target; }; jQuery.extend({ noConflict: function() { if ( jQuery._$ ) $ = jQuery._$; }, isFunction: function( fn ) { return fn && typeof fn == "function"; }, // args is for internal usage only each: function( obj, fn, args ) { if ( obj.length == undefined ) for ( var i in obj ) fn.apply( obj[i], args || [i, obj[i]] ); else for ( var i = 0, ol = obj.length; i < ol; i++ ) if ( fn.apply( obj[i], args || [i, obj[i]] ) === false ) break; return obj; }, prop: function(elem, value, type){ // Handle executable functions if ( jQuery.isFunction( value ) ) return value.call( elem ); // Handle passing in a number to a CSS property if ( value.constructor == Number && type == "curCSS" ) return value + "px"; return value; }, className: { // internal only, use addClass("class") add: function( elem, c ){ jQuery.each( c.split(/\s+/), function(i, cur){ if ( !jQuery.className.has( elem.className, cur ) ) elem.className += ( elem.className ? " " : "" ) + cur; }); }, // internal only, use removeClass("class") remove: function( elem, c ){ elem.className = c ? jQuery.grep( elem.className.split(/\s+/), function(cur){ return !jQuery.className.has( c, cur ); }).join(" ") : ""; }, // internal only, use is(".class") has: function( t, c ) { t = t.className || t; return t && new RegExp("(^|\\s)" + c + "(\\s|$)").test( t ); } }, swap: function(e,o,f) { for ( var i in o ) { e.style["old"+i] = e.style[i]; e.style[i] = o[i]; } f.apply( e, [] ); for ( var i in o ) e.style[i] = e.style["old"+i]; }, css: function(e,p) { if ( p == "height" || p == "width" ) { var old = {}, oHeight, oWidth, d = ["Top","Bottom","Right","Left"]; jQuery.each( d, function(){ old["padding" + this] = 0; old["border" + this + "Width"] = 0; }); jQuery.swap( e, old, function() { if (jQuery.css(e,"display") != "none") { oHeight = e.offsetHeight; oWidth = e.offsetWidth; } else { e = jQuery(e.cloneNode(true)) .find(":radio").removeAttr("checked").end() .css({ visibility: "hidden", position: "absolute", display: "block", right: "0", left: "0" }).appendTo(e.parentNode)[0]; var parPos = jQuery.css(e.parentNode,"position"); if ( parPos == "" || parPos == "static" ) e.parentNode.style.position = "relative"; oHeight = e.clientHeight; oWidth = e.clientWidth; if ( parPos == "" || parPos == "static" ) e.parentNode.style.position = "static"; e.parentNode.removeChild(e); } }); return p == "height" ? oHeight : oWidth; } return jQuery.curCSS( e, p ); }, curCSS: function(elem, prop, force) { var ret; if (prop == "opacity" && jQuery.browser.msie) return jQuery.attr(elem.style, "opacity"); if (prop == "float" || prop == "cssFloat") prop = jQuery.browser.msie ? "styleFloat" : "cssFloat"; if (!force && elem.style[prop]) ret = elem.style[prop]; else if (document.defaultView && document.defaultView.getComputedStyle) { if (prop == "cssFloat" || prop == "styleFloat") prop = "float"; prop = prop.replace(/([A-Z])/g,"-$1").toLowerCase(); var cur = document.defaultView.getComputedStyle(elem, null); if ( cur ) ret = cur.getPropertyValue(prop); else if ( prop == "display" ) ret = "none"; else jQuery.swap(elem, { display: "block" }, function() { var c = document.defaultView.getComputedStyle(this, ""); ret = c && c.getPropertyValue(prop) || ""; }); } else if (elem.currentStyle) { var newProp = prop.replace(/\-(\w)/g,function(m,c){return c.toUpperCase();}); ret = elem.currentStyle[prop] || elem.currentStyle[newProp]; } return ret; }, clean: function(a) { var r = []; jQuery.each( a, function(i,arg){ if ( !arg ) return; if ( arg.constructor == Number ) arg = arg.toString(); // Convert html string into DOM nodes if ( typeof arg == "string" ) { // Trim whitespace, otherwise indexOf won't work as expected var s = jQuery.trim(arg), div = document.createElement("div"), tb = []; var wrap = // option or optgroup !s.indexOf("", ""] || (!s.indexOf("", ""] || !s.indexOf("", ""] || // matched above (!s.indexOf("", ""] || [0,"",""]; // Go to html and back, then peel off extra wrappers div.innerHTML = wrap[1] + s + wrap[2]; // Move to the right depth while ( wrap[0]-- ) div = div.firstChild; // Remove IE's autoinserted from table fragments if ( jQuery.browser.msie ) { // String was a , *may* have spurious if ( !s.indexOf(" or else if ( wrap[1] == "
" && s.indexOf("= 0 ; --n ) if ( tb[n].nodeName.toUpperCase() == "TBODY" && !tb[n].childNodes.length ) tb[n].parentNode.removeChild(tb[n]); } arg = div.childNodes; } if ( arg.length === 0 ) return; if ( arg[0] == undefined ) r.push( arg ); else r = jQuery.merge( r, arg ); }); return r; }, attr: function(elem, name, value){ var fix = { "for": "htmlFor", "class": "className", "float": jQuery.browser.msie ? "styleFloat" : "cssFloat", cssFloat: jQuery.browser.msie ? "styleFloat" : "cssFloat", innerHTML: "innerHTML", className: "className", value: "value", disabled: "disabled", checked: "checked", readonly: "readOnly", selected: "selected" }; // IE actually uses filters for opacity ... elem is actually elem.style if ( name == "opacity" && jQuery.browser.msie && value != undefined ) { // IE has trouble with opacity if it does not have layout // Force it by setting the zoom level elem.zoom = 1; // Set the alpha filter to set the opacity return elem.filter = elem.filter.replace(/alpha\([^\)]*\)/gi,"") + ( value == 1 ? "" : "alpha(opacity=" + value * 100 + ")" ); } else if ( name == "opacity" && jQuery.browser.msie ) return elem.filter ? parseFloat( elem.filter.match(/alpha\(opacity=(.*)\)/)[1] ) / 100 : 1; // Mozilla doesn't play well with opacity 1 if ( name == "opacity" && jQuery.browser.mozilla && value == 1 ) value = 0.9999; // Certain attributes only work when accessed via the old DOM 0 way if ( fix[name] ) { if ( value != undefined ) elem[fix[name]] = value; return elem[fix[name]]; } else if ( value == undefined && jQuery.browser.msie && elem.nodeName && elem.nodeName.toUpperCase() == "FORM" && (name == "action" || name == "method") ) return elem.getAttributeNode(name).nodeValue; // IE elem.getAttribute passes even for style else if ( elem.tagName ) { if ( value != undefined ) elem.setAttribute( name, value ); return elem.getAttribute( name ); } else { name = name.replace(/-([a-z])/ig,function(z,b){return b.toUpperCase();}); if ( value != undefined ) elem[name] = value; return elem[name]; } }, trim: function(t){ return t.replace(/^\s+|\s+$/g, ""); }, makeArray: function( a ) { var r = []; if ( a.constructor != Array ) for ( var i = 0, al = a.length; i < al; i++ ) r.push( a[i] ); else r = a.slice( 0 ); return r; }, inArray: function( b, a ) { for ( var i = 0, al = a.length; i < al; i++ ) if ( a[i] == b ) return i; return -1; }, merge: function(first, second) { var r = [].slice.call( first, 0 ); // Now check for duplicates between the two arrays // and only add the unique items for ( var i = 0, sl = second.length; i < sl; i++ ) // Check for duplicates if ( jQuery.inArray( second[i], r ) == -1 ) // The item is unique, add it first.push( second[i] ); return first; }, grep: function(elems, fn, inv) { // If a string is passed in for the function, make a function // for it (a handy shortcut) if ( typeof fn == "string" ) fn = new Function("a","i","return " + fn); var result = []; // Go through the array, only saving the items // that pass the validator function for ( var i = 0, el = elems.length; i < el; i++ ) if ( !inv && fn(elems[i],i) || inv && !fn(elems[i],i) ) result.push( elems[i] ); return result; }, map: function(elems, fn) { // If a string is passed in for the function, make a function // for it (a handy shortcut) if ( typeof fn == "string" ) fn = new Function("a","return " + fn); var result = [], r = []; // Go through the array, translating each of the items to their // new value (or values). for ( var i = 0, el = elems.length; i < el; i++ ) { var val = fn(elems[i],i); if ( val !== null && val != undefined ) { if ( val.constructor != Array ) val = [val]; result = result.concat( val ); } } var r = result.length ? [ result[0] ] : []; check: for ( var i = 1, rl = result.length; i < rl; i++ ) { for ( var j = 0; j < i; j++ ) if ( result[i] == r[j] ) continue check; r.push( result[i] ); } return r; } }); /* * Whether the W3C compliant box model is being used. * * @property * @name $.boxModel * @type Boolean * @cat JavaScript */ new function() { var b = navigator.userAgent.toLowerCase(); // Figure out what browser is being used jQuery.browser = { safari: /webkit/.test(b), opera: /opera/.test(b), msie: /msie/.test(b) && !/opera/.test(b), mozilla: /mozilla/.test(b) && !/(compatible|webkit)/.test(b) }; // Check to see if the W3C box model is being used jQuery.boxModel = !jQuery.browser.msie || document.compatMode == "CSS1Compat"; }; jQuery.each({ parent: "a.parentNode", parents: "jQuery.parents(a)", next: "jQuery.nth(a,2,'nextSibling')", prev: "jQuery.nth(a,2,'previousSibling')", siblings: "jQuery.sibling(a.parentNode.firstChild,a)", children: "jQuery.sibling(a.firstChild)" }, function(i,n){ jQuery.fn[ i ] = function(a) { var ret = jQuery.map(this,n); if ( a && typeof a == "string" ) ret = jQuery.multiFilter(a,ret); return this.pushStack( ret ); }; }); jQuery.each({ appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after" }, function(i,n){ jQuery.fn[ i ] = function(){ var a = arguments; return this.each(function(){ for ( var j = 0, al = a.length; j < al; j++ ) jQuery(a[j])[n]( this ); }); }; }); jQuery.each( { removeAttr: function( key ) { jQuery.attr( this, key, "" ); this.removeAttribute( key ); }, addClass: function(c){ jQuery.className.add(this,c); }, removeClass: function(c){ jQuery.className.remove(this,c); }, toggleClass: function( c ){ jQuery.className[ jQuery.className.has(this,c) ? "remove" : "add" ](this, c); }, remove: function(a){ if ( !a || jQuery.filter( a, [this] ).r.length ) this.parentNode.removeChild( this ); }, empty: function() { while ( this.firstChild ) this.removeChild( this.firstChild ); } }, function(i,n){ jQuery.fn[ i ] = function() { return this.each( n, arguments ); }; }); jQuery.each( [ "eq", "lt", "gt", "contains" ], function(i,n){ jQuery.fn[ n ] = function(num,fn) { return this.filter( ":" + n + "(" + num + ")", fn ); }; }); jQuery.each( [ "height", "width" ], function(i,n){ jQuery.fn[ n ] = function(h) { return h == undefined ? ( this.length ? jQuery.css( this[0], n ) : null ) : this.css( n, h.constructor == String ? h : h + "px" ); }; }); jQuery.extend({ expr: { "": "m[2]=='*'||a.nodeName.toUpperCase()==m[2].toUpperCase()", "#": "a.getAttribute('id')==m[2]", ":": { // Position Checks lt: "im[3]-0", nth: "m[3]-0==i", eq: "m[3]-0==i", first: "i==0", last: "i==r.length-1", even: "i%2==0", odd: "i%2", // Child Checks "nth-child": "jQuery.nth(a.parentNode.firstChild,m[3],'nextSibling',a)==a", "first-child": "jQuery.nth(a.parentNode.firstChild,1,'nextSibling')==a", "last-child": "jQuery.nth(a.parentNode.lastChild,1,'previousSibling')==a", "only-child": "jQuery.sibling(a.parentNode.firstChild).length==1", // Parent Checks parent: "a.firstChild", empty: "!a.firstChild", // Text Check contains: "jQuery.fn.text.apply([a]).indexOf(m[3])>=0", // Visibility visible: 'a.type!="hidden"&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden"', hidden: 'a.type=="hidden"||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden"', // Form attributes enabled: "!a.disabled", disabled: "a.disabled", checked: "a.checked", selected: "a.selected||jQuery.attr(a,'selected')", // Form elements text: "a.type=='text'", radio: "a.type=='radio'", checkbox: "a.type=='checkbox'", file: "a.type=='file'", password: "a.type=='password'", submit: "a.type=='submit'", image: "a.type=='image'", reset: "a.type=='reset'", button: 'a.type=="button"||a.nodeName=="BUTTON"', input: "/input|select|textarea|button/i.test(a.nodeName)" }, ".": "jQuery.className.has(a,m[2])", "@": { "=": "z==m[4]", "!=": "z!=m[4]", "^=": "z&&!z.indexOf(m[4])", "$=": "z&&z.substr(z.length - m[4].length,m[4].length)==m[4]", "*=": "z&&z.indexOf(m[4])>=0", "": "z", _resort: function(m){ return ["", m[1], m[3], m[2], m[5]]; }, _prefix: "z=a[m[3]]||jQuery.attr(a,m[3]);" }, "[": "jQuery.find(m[2],a).length" }, // The regular expressions that power the parsing engine parse: [ // Match: [@value='test'], [@foo] /^\[ *(@)([a-z0-9_-]*) *([!*$^=]*) *('?"?)(.*?)\4 *\]/i, // Match: [div], [div p] /^(\[)\s*(.*?(\[.*?\])?[^[]*?)\s*\]/, // Match: :contains('foo') /^(:)([a-z0-9_-]*)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/i, // Match: :even, :last-chlid /^([:.#]*)([a-z0-9_*-]*)/i ], token: [ /^(\/?\.\.)/, "a.parentNode", /^(>|\/)/, "jQuery.sibling(a.firstChild)", /^(\+)/, "jQuery.nth(a,2,'nextSibling')", /^(~)/, function(a){ var s = jQuery.sibling(a.parentNode.firstChild); return s.slice(0, jQuery.inArray(a,s)); } ], multiFilter: function( expr, elems, not ) { var old, cur = []; while ( expr && expr != old ) { old = expr; var f = jQuery.filter( expr, elems, not ); expr = f.t.replace(/^\s*,\s*/, "" ); cur = not ? elems = f.r : jQuery.merge( cur, f.r ); } return cur; }, find: function( t, context ) { // Quickly handle non-string expressions if ( typeof t != "string" ) return [ t ]; // Make sure that the context is a DOM Element if ( context && !context.nodeType ) context = null; // Set the correct context (if none is provided) context = context || document; // Handle the common XPath // expression if ( !t.indexOf("//") ) { context = context.documentElement; t = t.substr(2,t.length); // And the / root expression } else if ( !t.indexOf("/") ) { context = context.documentElement; t = t.substr(1,t.length); if ( t.indexOf("/") >= 1 ) t = t.substr(t.indexOf("/"),t.length); } // Initialize the search var ret = [context], done = [], last = null; // Continue while a selector expression exists, and while // we're no longer looping upon ourselves while ( t && last != t ) { var r = []; last = t; t = jQuery.trim(t).replace( /^\/\//i, "" ); var foundToken = false; // An attempt at speeding up child selectors that // point to a specific element tag var re = /^[\/>]\s*([a-z0-9*-]+)/i; var m = re.exec(t); if ( m ) { // Perform our own iteration and filter jQuery.each( ret, function(){ for ( var c = this.firstChild; c; c = c.nextSibling ) if ( c.nodeType == 1 && ( c.nodeName == m[1].toUpperCase() || m[1] == "*" ) ) r.push( c ); }); ret = r; t = jQuery.trim( t.replace( re, "" ) ); foundToken = true; } else { // Look for pre-defined expression tokens for ( var i = 0; i < jQuery.token.length; i += 2 ) { // Attempt to match each, individual, token in // the specified order var re = jQuery.token[i]; var m = re.exec(t); // If the token match was found if ( m ) { // Map it against the token's handler r = ret = jQuery.map( ret, jQuery.isFunction( jQuery.token[i+1] ) ? jQuery.token[i+1] : function(a){ return eval(jQuery.token[i+1]); }); // And remove the token t = jQuery.trim( t.replace( re, "" ) ); foundToken = true; break; } } } // See if there's still an expression, and that we haven't already // matched a token if ( t && !foundToken ) { // Handle multiple expressions if ( !t.indexOf(",") ) { // Clean the result set if ( ret[0] == context ) ret.shift(); // Merge the result sets jQuery.merge( done, ret ); // Reset the context r = ret = [context]; // Touch up the selector string t = " " + t.substr(1,t.length); } else { // Optomize for the case nodeName#idName var re2 = /^([a-z0-9_-]+)(#)([a-z0-9\\*_-]*)/i; var m = re2.exec(t); // Re-organize the results, so that they're consistent if ( m ) { m = [ 0, m[2], m[3], m[1] ]; } else { // Otherwise, do a traditional filter check for // ID, class, and element selectors re2 = /^([#.]?)([a-z0-9\\*_-]*)/i; m = re2.exec(t); } // Try to do a global search by ID, where we can if ( m[1] == "#" && ret[ret.length-1].getElementById ) { // Optimization for HTML document case var oid = ret[ret.length-1].getElementById(m[2]); // Do a quick check for node name (where applicable) so // that div#foo searches will be really fast ret = r = oid && (!m[3] || oid.nodeName == m[3].toUpperCase()) ? [oid] : []; } else { // Pre-compile a regular expression to handle class searches if ( m[1] == "." ) var rec = new RegExp("(^|\\s)" + m[2] + "(\\s|$)"); // We need to find all descendant elements, it is more // efficient to use getAll() when we are already further down // the tree - we try to recognize that here jQuery.each( ret, function(){ // Grab the tag name being searched for var tag = m[1] != "" || m[0] == "" ? "*" : m[2]; // Handle IE7 being really dumb about s if ( this.nodeName.toUpperCase() == "OBJECT" && tag == "*" ) tag = "param"; jQuery.merge( r, m[1] != "" && ret.length != 1 ? jQuery.getAll( this, [], m[1], m[2], rec ) : this.getElementsByTagName( tag ) ); }); // It's faster to filter by class and be done with it if ( m[1] == "." && ret.length == 1 ) r = jQuery.grep( r, function(e) { return rec.test(e.className); }); // Same with ID filtering if ( m[1] == "#" && ret.length == 1 ) { // Remember, then wipe out, the result set var tmp = r; r = []; // Then try to find the element with the ID jQuery.each( tmp, function(){ if ( this.getAttribute("id") == m[2] ) { r = [ this ]; return false; } }); } ret = r; } t = t.replace( re2, "" ); } } // If a selector string still exists if ( t ) { // Attempt to filter it var val = jQuery.filter(t,r); ret = r = val.r; t = jQuery.trim(val.t); } } // Remove the root context if ( ret && ret[0] == context ) ret.shift(); // And combine the results jQuery.merge( done, ret ); return done; }, filter: function(t,r,not) { // Look for common filter expressions while ( t && /^[a-z[({<*:.#]/i.test(t) ) { var p = jQuery.parse, m; jQuery.each( p, function(i,re){ // Look for, and replace, string-like sequences // and finally build a regexp out of it m = re.exec( t ); if ( m ) { // Remove what we just matched t = t.substring( m[0].length ); // Re-organize the first match if ( jQuery.expr[ m[1] ]._resort ) m = jQuery.expr[ m[1] ]._resort( m ); return false; } }); // :not() is a special case that can be optimized by // keeping it out of the expression list if ( m[1] == ":" && m[2] == "not" ) r = jQuery.filter(m[3], r, true).r; // Handle classes as a special case (this will help to // improve the speed, as the regexp will only be compiled once) else if ( m[1] == "." ) { var re = new RegExp("(^|\\s)" + m[2] + "(\\s|$)"); r = jQuery.grep( r, function(e){ return re.test(e.className || ""); }, not); // Otherwise, find the expression to execute } else { var f = jQuery.expr[m[1]]; if ( typeof f != "string" ) f = jQuery.expr[m[1]][m[2]]; // Build a custom macro to enclose it eval("f = function(a,i){" + ( jQuery.expr[ m[1] ]._prefix || "" ) + "return " + f + "}"); // Execute it against the current filter r = jQuery.grep( r, f, not ); } } // Return an array of filtered elements (r) // and the modified expression string (t) return { r: r, t: t }; }, getAll: function( o, r, token, name, re ) { for ( var s = o.firstChild; s; s = s.nextSibling ) if ( s.nodeType == 1 ) { var add = true; if ( token == "." ) add = s.className && re.test(s.className); else if ( token == "#" ) add = s.getAttribute("id") == name; if ( add ) r.push( s ); if ( token == "#" && r.length ) break; if ( s.firstChild ) jQuery.getAll( s, r, token, name, re ); } return r; }, parents: function( elem ){ var matched = []; var cur = elem.parentNode; while ( cur && cur != document ) { matched.push( cur ); cur = cur.parentNode; } return matched; }, nth: function(cur,result,dir,elem){ result = result || 1; var num = 0; for ( ; cur; cur = cur[dir] ) { if ( cur.nodeType == 1 ) num++; if ( num == result || result == "even" && num % 2 == 0 && num > 1 && cur == elem || result == "odd" && num % 2 == 1 && cur == elem ) return cur; } }, sibling: function( n, elem ) { var r = []; for ( ; n; n = n.nextSibling ) { if ( n.nodeType == 1 && (!elem || n != elem) ) r.push( n ); } return r; } }); /* * A number of helper functions used for managing events. * Many of the ideas behind this code orignated from * Dean Edwards' addEvent library. */ jQuery.event = { // Bind an event to an element // Original by Dean Edwards add: function(element, type, handler, data) { // For whatever reason, IE has trouble passing the window object // around, causing it to be cloned in the process if ( jQuery.browser.msie && element.setInterval != undefined ) element = window; // if data is passed, bind to handler if( data ) handler.data = data; // Make sure that the function being executed has a unique ID if ( !handler.guid ) handler.guid = this.guid++; // Init the element's event structure if (!element.events) element.events = {}; // Get the current list of functions bound to this event var handlers = element.events[type]; // If it hasn't been initialized yet if (!handlers) { // Init the event handler queue handlers = element.events[type] = {}; // Remember an existing handler, if it's already there if (element["on" + type]) handlers[0] = element["on" + type]; } // Add the function to the element's handler list handlers[handler.guid] = handler; // And bind the global event handler to the element element["on" + type] = this.handle; // Remember the function in a global list (for triggering) if (!this.global[type]) this.global[type] = []; this.global[type].push( element ); }, guid: 1, global: {}, // Detach an event or set of events from an element remove: function(element, type, handler) { if (element.events) if ( type && type.type ) delete element.events[ type.type ][ type.handler.guid ]; else if (type && element.events[type]) if ( handler ) delete element.events[type][handler.guid]; else for ( var i in element.events[type] ) delete element.events[type][i]; else for ( var j in element.events ) this.remove( element, j ); }, trigger: function(type,data,element) { // Clone the incoming data, if any data = jQuery.makeArray(data || []); // Handle a global trigger if ( !element ) { var g = this.global[type]; if ( g ) jQuery.each( g, function(){ jQuery.event.trigger( type, data, this ); }); // Handle triggering a single element } else if ( element["on" + type] ) { // Pass along a fake event data.unshift( this.fix({ type: type, target: element }) ); // Trigger the event var val = element["on" + type].apply( element, data ); if ( val !== false && jQuery.isFunction( element[ type ] ) ) element[ type ](); } }, handle: function(event) { if ( typeof jQuery == "undefined" ) return false; // Empty object is for triggered events with no data event = jQuery.event.fix( event || window.event || {} ); // returned undefined or false var returnValue; var c = this.events[event.type]; var args = [].slice.call( arguments, 1 ); args.unshift( event ); for ( var j in c ) { // Pass in a reference to the handler function itself // So that we can later remove it args[0].handler = c[j]; args[0].data = c[j].data; if ( c[j].apply( this, args ) === false ) { event.preventDefault(); event.stopPropagation(); returnValue = false; } } // Clean up added properties in IE to prevent memory leak if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null; return returnValue; }, fix: function(event) { // Fix target property, if necessary if ( !event.target && event.srcElement ) event.target = event.srcElement; // Calculate pageX/Y if missing and clientX/Y available if ( event.pageX == undefined && event.clientX != undefined ) { var e = document.documentElement, b = document.body; event.pageX = event.clientX + (e.scrollLeft || b.scrollLeft); event.pageY = event.clientY + (e.scrollTop || b.scrollTop); } // check if target is a textnode (safari) if (jQuery.browser.safari && event.target.nodeType == 3) { // store a copy of the original event object // and clone because target is read only var originalEvent = event; event = jQuery.extend({}, originalEvent); // get parentnode from textnode event.target = originalEvent.target.parentNode; // add preventDefault and stopPropagation since // they will not work on the clone event.preventDefault = function() { return originalEvent.preventDefault(); }; event.stopPropagation = function() { return originalEvent.stopPropagation(); }; } // fix preventDefault and stopPropagation if (!event.preventDefault) event.preventDefault = function() { this.returnValue = false; }; if (!event.stopPropagation) event.stopPropagation = function() { this.cancelBubble = true; }; return event; } }; jQuery.fn.extend({ bind: function( type, data, fn ) { return this.each(function(){ jQuery.event.add( this, type, fn || data, data ); }); }, one: function( type, data, fn ) { return this.each(function(){ jQuery.event.add( this, type, function(event) { jQuery(this).unbind(event); return (fn || data).apply( this, arguments); }, data); }); }, unbind: function( type, fn ) { return this.each(function(){ jQuery.event.remove( this, type, fn ); }); }, trigger: function( type, data ) { return this.each(function(){ jQuery.event.trigger( type, data, this ); }); }, toggle: function() { // Save reference to arguments for access in closure var a = arguments; return this.click(function(e) { // Figure out which function to execute this.lastToggle = this.lastToggle == 0 ? 1 : 0; // Make sure that clicks stop e.preventDefault(); // and execute the function return a[this.lastToggle].apply( this, [e] ) || false; }); }, hover: function(f,g) { // A private function for handling mouse 'hovering' function handleHover(e) { // Check if mouse(over|out) are still within the same parent element var p = (e.type == "mouseover" ? e.fromElement : e.toElement) || e.relatedTarget; // Traverse up the tree while ( p && p != this ) try { p = p.parentNode } catch(e) { p = this; }; // If we actually just moused on to a sub-element, ignore it if ( p == this ) return false; // Execute the right function return (e.type == "mouseover" ? f : g).apply(this, [e]); } // Bind the function to the two event listeners return this.mouseover(handleHover).mouseout(handleHover); }, ready: function(f) { // If the DOM is already ready if ( jQuery.isReady ) // Execute the function immediately f.apply( document, [jQuery] ); // Otherwise, remember the function for later else { // Add the function to the wait list jQuery.readyList.push( function() { return f.apply(this, [jQuery]) } ); } return this; } }); jQuery.extend({ /* * All the code that makes DOM Ready work nicely. */ isReady: false, readyList: [], // Handle when the DOM is ready ready: function() { // Make sure that the DOM is not already loaded if ( !jQuery.isReady ) { // Remember that the DOM is ready jQuery.isReady = true; // If there are functions bound, to execute if ( jQuery.readyList ) { // Execute all of them jQuery.each( jQuery.readyList, function(){ this.apply( document ); }); // Reset the list of functions jQuery.readyList = null; } // Remove event lisenter to avoid memory leak if ( jQuery.browser.mozilla || jQuery.browser.opera ) document.removeEventListener( "DOMContentLoaded", jQuery.ready, false ); } } }); new function(){ jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," + "mousedown,mouseup,mousemove,mouseover,mouseout,change,select," + "submit,keydown,keypress,keyup,error").split(","), function(i,o){ // Handle event binding jQuery.fn[o] = function(f){ return f ? this.bind(o, f) : this.trigger(o); }; }); // If Mozilla is used if ( jQuery.browser.mozilla || jQuery.browser.opera ) // Use the handy event callback document.addEventListener( "DOMContentLoaded", jQuery.ready, false ); // If IE is used, use the excellent hack by Matthias Miller // http://www.outofhanwell.com/blog/index.php?title=the_window_onload_problem_revisited else if ( jQuery.browser.msie ) { // Only works if you document.write() it document.write("<\/script>"); // Use the defer script hack var script = document.getElementById("__ie_init"); // script does not exist if jQuery is loaded dynamically if ( script ) script.onreadystatechange = function() { if ( this.readyState != "complete" ) return; this.parentNode.removeChild( this ); jQuery.ready(); }; // Clear from memory script = null; // If Safari is used } else if ( jQuery.browser.safari ) // Continually check to see if the document.readyState is valid jQuery.safariTimer = setInterval(function(){ // loaded and complete are both valid states if ( document.readyState == "loaded" || document.readyState == "complete" ) { // If either one are found, remove the timer clearInterval( jQuery.safariTimer ); jQuery.safariTimer = null; // and execute any waiting functions jQuery.ready(); } }, 10); // A fallback to window.onload, that will always work jQuery.event.add( window, "load", jQuery.ready ); }; // Clean up after IE to avoid memory leaks if (jQuery.browser.msie) jQuery(window).one("unload", function() { var global = jQuery.event.global; for ( var type in global ) { var els = global[type], i = els.length; if ( i && type != 'unload' ) do jQuery.event.remove(els[i-1], type); while (--i); } }); jQuery.fn.extend({ show: function(speed,callback){ var hidden = this.filter(":hidden"); return speed ? hidden.animate({ height: "show", width: "show", opacity: "show" }, speed, callback) : hidden.each(function(){ this.style.display = this.oldblock ? this.oldblock : ""; if ( jQuery.css(this,"display") == "none" ) this.style.display = "block"; }); }, hide: function(speed,callback){ var visible = this.filter(":visible"); return speed ? visible.animate({ height: "hide", width: "hide", opacity: "hide" }, speed, callback) : visible.each(function(){ this.oldblock = this.oldblock || jQuery.css(this,"display"); if ( this.oldblock == "none" ) this.oldblock = "block"; this.style.display = "none"; }); }, // Save the old toggle function _toggle: jQuery.fn.toggle, toggle: function( fn, fn2 ){ var args = arguments; return jQuery.isFunction(fn) && jQuery.isFunction(fn2) ? this._toggle( fn, fn2 ) : this.each(function(){ jQuery(this)[ jQuery(this).is(":hidden") ? "show" : "hide" ] .apply( jQuery(this), args ); }); }, slideDown: function(speed,callback){ return this.animate({height: "show"}, speed, callback); }, slideUp: function(speed,callback){ return this.animate({height: "hide"}, speed, callback); }, slideToggle: function(speed, callback){ return this.each(function(){ var state = jQuery(this).is(":hidden") ? "show" : "hide"; jQuery(this).animate({height: state}, speed, callback); }); }, fadeIn: function(speed, callback){ return this.animate({opacity: "show"}, speed, callback); }, fadeOut: function(speed, callback){ return this.animate({opacity: "hide"}, speed, callback); }, fadeTo: function(speed,to,callback){ return this.animate({opacity: to}, speed, callback); }, animate: function( prop, speed, easing, callback ) { return this.queue(function(){ this.curAnim = jQuery.extend({}, prop); var opt = jQuery.speed(speed, easing, callback); for ( var p in prop ) { var e = new jQuery.fx( this, opt, p ); if ( prop[p].constructor == Number ) e.custom( e.cur(), prop[p] ); else e[ prop[p] ]( prop ); } }); }, queue: function(type,fn){ if ( !fn ) { fn = type; type = "fx"; } return this.each(function(){ if ( !this.queue ) this.queue = {}; if ( !this.queue[type] ) this.queue[type] = []; this.queue[type].push( fn ); if ( this.queue[type].length == 1 ) fn.apply(this); }); } }); jQuery.extend({ speed: function(speed, easing, fn) { var opt = speed && speed.constructor == Object ? speed : { complete: fn || !fn && easing || jQuery.isFunction( speed ) && speed, duration: speed, easing: fn && easing || easing && easing.constructor != Function && easing }; opt.duration = (opt.duration && opt.duration.constructor == Number ? opt.duration : { slow: 600, fast: 200 }[opt.duration]) || 400; // Queueing opt.old = opt.complete; opt.complete = function(){ jQuery.dequeue(this, "fx"); if ( jQuery.isFunction( opt.old ) ) opt.old.apply( this ); }; return opt; }, easing: {}, queue: {}, dequeue: function(elem,type){ type = type || "fx"; if ( elem.queue && elem.queue[type] ) { // Remove self elem.queue[type].shift(); // Get next function var f = elem.queue[type][0]; if ( f ) f.apply( elem ); } }, /* * I originally wrote fx() as a clone of moo.fx and in the process * of making it small in size the code became illegible to sane * people. You've been warned. */ fx: function( elem, options, prop ){ var z = this; // The styles var y = elem.style; // Store display property var oldDisplay = jQuery.css(elem, "display"); // Set display property to block for animation y.display = "block"; // Make sure that nothing sneaks out y.overflow = "hidden"; // Simple function for setting a style value z.a = function(){ if ( options.step ) options.step.apply( elem, [ z.now ] ); if ( prop == "opacity" ) jQuery.attr(y, "opacity", z.now); // Let attr handle opacity else if ( parseInt(z.now) ) // My hate for IE will never die y[prop] = parseInt(z.now) + "px"; }; // Figure out the maximum number to run to z.max = function(){ return parseFloat( jQuery.css(elem,prop) ); }; // Get the current size z.cur = function(){ var r = parseFloat( jQuery.curCSS(elem, prop) ); return r && r > -10000 ? r : z.max(); }; // Start an animation from one number to another z.custom = function(from,to){ z.startTime = (new Date()).getTime(); z.now = from; z.a(); z.timer = setInterval(function(){ z.step(from, to); }, 13); }; // Simple 'show' function z.show = function(){ if ( !elem.orig ) elem.orig = {}; // Remember where we started, so that we can go back to it later elem.orig[prop] = this.cur(); options.show = true; // Begin the animation z.custom(0, elem.orig[prop]); // Stupid IE, look what you made me do if ( prop != "opacity" ) y[prop] = "1px"; }; // Simple 'hide' function z.hide = function(){ if ( !elem.orig ) elem.orig = {}; // Remember where we started, so that we can go back to it later elem.orig[prop] = this.cur(); options.hide = true; // Begin the animation z.custom(elem.orig[prop], 0); }; //Simple 'toggle' function z.toggle = function() { if ( !elem.orig ) elem.orig = {}; // Remember where we started, so that we can go back to it later elem.orig[prop] = this.cur(); if(oldDisplay == "none") { options.show = true; // Stupid IE, look what you made me do if ( prop != "opacity" ) y[prop] = "1px"; // Begin the animation z.custom(0, elem.orig[prop]); } else { options.hide = true; // Begin the animation z.custom(elem.orig[prop], 0); } }; // Each step of an animation z.step = function(firstNum, lastNum){ var t = (new Date()).getTime(); if (t > options.duration + z.startTime) { // Stop the timer clearInterval(z.timer); z.timer = null; z.now = lastNum; z.a(); if (elem.curAnim) elem.curAnim[ prop ] = true; var done = true; for ( var i in elem.curAnim ) if ( elem.curAnim[i] !== true ) done = false; if ( done ) { // Reset the overflow y.overflow = ""; // Reset the display y.display = oldDisplay; if (jQuery.css(elem, "display") == "none") y.display = "block"; // Hide the element if the "hide" operation was done if ( options.hide ) y.display = "none"; // Reset the properties, if the item has been hidden or shown if ( options.hide || options.show ) for ( var p in elem.curAnim ) if (p == "opacity") jQuery.attr(y, p, elem.orig[p]); else y[p] = ""; } // If a callback was provided, execute it if ( done && jQuery.isFunction( options.complete ) ) // Execute the complete function options.complete.apply( elem ); } else { var n = t - this.startTime; // Figure out where in the animation we are and set the number var p = n / options.duration; // If the easing function exists, then use it z.now = options.easing && jQuery.easing[options.easing] ? jQuery.easing[options.easing](p, n, firstNum, (lastNum-firstNum), options.duration) : // else use default linear easing ((-Math.cos(p*Math.PI)/2) + 0.5) * (lastNum-firstNum) + firstNum; // Perform the next step of the animation z.a(); } }; } }); jQuery.fn.extend({ loadIfModified: function( url, params, callback ) { this.load( url, params, callback, 1 ); }, load: function( url, params, callback, ifModified ) { if ( jQuery.isFunction( url ) ) return this.bind("load", url); callback = callback || function(){}; // Default to a GET request var type = "GET"; // If the second parameter was provided if ( params ) // If it's a function if ( jQuery.isFunction( params.constructor ) ) { // We assume that it's the callback callback = params; params = null; // Otherwise, build a param string } else { params = jQuery.param( params ); type = "POST"; } var self = this; // Request the remote document jQuery.ajax({ url: url, type: type, data: params, ifModified: ifModified, complete: function(res, status){ if ( status == "success" || !ifModified && status == "notmodified" ) // Inject the HTML into all the matched elements self.attr("innerHTML", res.responseText) // Execute all the scripts inside of the newly-injected HTML .evalScripts() // Execute callback .each( callback, [res.responseText, status, res] ); else callback.apply( self, [res.responseText, status, res] ); } }); return this; }, serialize: function() { return jQuery.param( this ); }, evalScripts: function() { return this.find("script").each(function(){ if ( this.src ) jQuery.getScript( this.src ); else jQuery.globalEval( this.text || this.textContent || this.innerHTML || "" ); }).end(); } }); // If IE is used, create a wrapper for the XMLHttpRequest object if ( jQuery.browser.msie && typeof XMLHttpRequest == "undefined" ) XMLHttpRequest = function(){ return new ActiveXObject("Microsoft.XMLHTTP"); }; // Attach a bunch of functions for handling common AJAX events jQuery.each( "ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","), function(i,o){ jQuery.fn[o] = function(f){ return this.bind(o, f); }; }); jQuery.extend({ get: function( url, data, callback, type, ifModified ) { // shift arguments if data argument was ommited if ( jQuery.isFunction( data ) ) { callback = data; data = null; } return jQuery.ajax({ url: url, data: data, success: callback, dataType: type, ifModified: ifModified }); }, getIfModified: function( url, data, callback, type ) { return jQuery.get(url, data, callback, type, 1); }, getScript: function( url, callback ) { return jQuery.get(url, null, callback, "script"); }, getJSON: function( url, data, callback ) { return jQuery.get(url, data, callback, "json"); }, post: function( url, data, callback, type ) { return jQuery.ajax({ type: "POST", url: url, data: data, success: callback, dataType: type }); }, // timeout (ms) //timeout: 0, ajaxTimeout: function( timeout ) { jQuery.ajaxSettings.timeout = timeout; }, ajaxSetup: function( settings ) { jQuery.extend( jQuery.ajaxSettings, settings ); }, ajaxSettings: { global: true, type: "GET", timeout: 0, contentType: "application/x-www-form-urlencoded", processData: true, async: true, data: null }, // Last-Modified header cache for next request lastModified: {}, ajax: function( s ) { // TODO introduce global settings, allowing the client to modify them for all requests, not only timeout s = jQuery.extend({}, jQuery.ajaxSettings, s); // if data available if ( s.data ) { // convert data if not already a string if (s.processData && typeof s.data != "string") s.data = jQuery.param(s.data); // append data to url for get requests if( s.type.toLowerCase() == "get" ) // "?" + data or "&" + data (in case there are already params) s.url += ((s.url.indexOf("?") > -1) ? "&" : "?") + s.data; } // Watch for a new set of requests if ( s.global && ! jQuery.active++ ) jQuery.event.trigger( "ajaxStart" ); var requestDone = false; // Create the request object var xml = new XMLHttpRequest(); // Open the socket xml.open(s.type, s.url, s.async); // Set the correct header, if data is being sent if ( s.data ) xml.setRequestHeader("Content-Type", s.contentType); // Set the If-Modified-Since header, if ifModified mode. if ( s.ifModified ) xml.setRequestHeader("If-Modified-Since", jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" ); // Set header so the called script knows that it's an XMLHttpRequest xml.setRequestHeader("X-Requested-With", "XMLHttpRequest"); // Make sure the browser sends the right content length if ( xml.overrideMimeType ) xml.setRequestHeader("Connection", "close"); // Allow custom headers/mimetypes if( s.beforeSend ) s.beforeSend(xml); if ( s.global ) jQuery.event.trigger("ajaxSend", [xml, s]); // Wait for a response to come back var onreadystatechange = function(isTimeout){ // The transfer is complete and the data is available, or the request timed out if ( xml && (xml.readyState == 4 || isTimeout == "timeout") ) { requestDone = true; var status; try { status = jQuery.httpSuccess( xml ) && isTimeout != "timeout" ? s.ifModified && jQuery.httpNotModified( xml, s.url ) ? "notmodified" : "success" : "error"; // Make sure that the request was successful or notmodified if ( status != "error" ) { // Cache Last-Modified header, if ifModified mode. var modRes; try { modRes = xml.getResponseHeader("Last-Modified"); } catch(e) {} // swallow exception thrown by FF if header is not available if ( s.ifModified && modRes ) jQuery.lastModified[s.url] = modRes; // process the data (runs the xml through httpData regardless of callback) var data = jQuery.httpData( xml, s.dataType ); // If a local callback was specified, fire it and pass it the data if ( s.success ) s.success( data, status ); // Fire the global callback if( s.global ) jQuery.event.trigger( "ajaxSuccess", [xml, s] ); } else jQuery.handleError(s, xml, status); } catch(e) { status = "error"; jQuery.handleError(s, xml, status, e); } // The request was completed if( s.global ) jQuery.event.trigger( "ajaxComplete", [xml, s] ); // Handle the global AJAX counter if ( s.global && ! --jQuery.active ) jQuery.event.trigger( "ajaxStop" ); // Process result if ( s.complete ) s.complete(xml, status); // Stop memory leaks xml.onreadystatechange = function(){}; xml = null; } }; xml.onreadystatechange = onreadystatechange; // Timeout checker if ( s.timeout > 0 ) setTimeout(function(){ // Check to see if the request is still happening if ( xml ) { // Cancel the request xml.abort(); if( !requestDone ) onreadystatechange( "timeout" ); } }, s.timeout); // save non-leaking reference var xml2 = xml; // Send the data try { xml2.send(s.data); } catch(e) { jQuery.handleError(s, xml, null, e); } // firefox 1.5 doesn't fire statechange for sync requests if ( !s.async ) onreadystatechange(); // return XMLHttpRequest to allow aborting the request etc. return xml2; }, handleError: function( s, xml, status, e ) { // If a local callback was specified, fire it if ( s.error ) s.error( xml, status, e ); // Fire the global callback if ( s.global ) jQuery.event.trigger( "ajaxError", [xml, s, e] ); }, // Counter for holding the number of active queries active: 0, // Determines if an XMLHttpRequest was successful or not httpSuccess: function( r ) { try { return !r.status && location.protocol == "file:" || ( r.status >= 200 && r.status < 300 ) || r.status == 304 || jQuery.browser.safari && r.status == undefined; } catch(e){} return false; }, // Determines if an XMLHttpRequest returns NotModified httpNotModified: function( xml, url ) { try { var xmlRes = xml.getResponseHeader("Last-Modified"); // Firefox always returns 200. check Last-Modified date return xml.status == 304 || xmlRes == jQuery.lastModified[url] || jQuery.browser.safari && xml.status == undefined; } catch(e){} return false; }, /* Get the data out of an XMLHttpRequest. * Return parsed XML if content-type header is "xml" and type is "xml" or omitted, * otherwise return plain text. * (String) data - The type of data that you're expecting back, * (e.g. "xml", "html", "script") */ httpData: function( r, type ) { var ct = r.getResponseHeader("content-type"); var data = !type && ct && ct.indexOf("xml") >= 0; data = type == "xml" || data ? r.responseXML : r.responseText; // If the type is "script", eval it in global context if ( type == "script" ) jQuery.globalEval( data ); // Get the JavaScript object, if JSON is used. if ( type == "json" ) eval( "data = " + data ); // evaluate scripts within html if ( type == "html" ) jQuery("
").html(data).evalScripts(); return data; }, // Serialize an array of form elements or a set of // key/values into a query string param: function( a ) { var s = []; // If an array was passed in, assume that it is an array // of form elements if ( a.constructor == Array || a.jquery ) // Serialize the form elements jQuery.each( a, function(){ s.push( encodeURIComponent(this.name) + "=" + encodeURIComponent( this.value ) ); }); // Otherwise, assume that it's an object of key/value pairs else // Serialize the key/values for ( var j in a ) // If the value is an array then the key names need to be repeated if ( a[j].constructor == Array ) jQuery.each( a[j], function(){ s.push( encodeURIComponent(j) + "=" + encodeURIComponent( this ) ); }); else s.push( encodeURIComponent(j) + "=" + encodeURIComponent( a[j] ) ); // Return the resulting serialization return s.join("&"); }, // evalulates a script in global context // not reliable for safari globalEval: function( data ) { if ( window.execScript ) window.execScript( data ); else if ( jQuery.browser.safari ) // safari doesn't provide a synchronous global eval window.setTimeout( data, 0 ); else eval.call( window, data ); } }); } trac-mastertickets-3.0.2+20111224/mastertickets/htdocs/caution.png0000644000175000017500000000130311675460244022675 0ustar wmbwmbPNG  IHDRaiCCPICC ProfilexkQ uQ+$fUR IK = <16x6vpR3-Rk &15[±yx.&h1k<̼=k4`4{/0xΝ.McGx pNMPb O4GC%S,E+ϛ[}wa`igjJ]r\6z8θlʸFXrz^idu#rr7;et.q&$I!##a0+H8 < FJh`lD׼n5 [fːݶٷ +$e: ~K2_IDAT8œI0 @Dm53f-"N lcǀCV4OЫ8~Q\vH.Q3^Q< gw|$ 5IENDB`trac-mastertickets-3.0.2+20111224/mastertickets/htdocs/checkmark.gif0000644000175000017500000000011411675460244023143 0ustar wmbwmbGIF89a_!,T uyqay^0P;trac-mastertickets-3.0.2+20111224/mastertickets/web_ui.py0000644000175000017500000003134311675460244021074 0ustar wmbwmbimport subprocess import re from pkg_resources import resource_filename from genshi.core import Markup, START, END, TEXT from genshi.builder import tag from trac.core import * from trac.web.api import IRequestHandler, IRequestFilter, ITemplateStreamFilter from trac.web.chrome import ITemplateProvider, add_stylesheet, add_script, \ add_ctxtnav from trac.ticket.api import ITicketManipulator from trac.ticket.model import Ticket from trac.ticket.query import Query from trac.config import Option, BoolOption, ChoiceOption from trac.resource import ResourceNotFound from trac.util import to_unicode from trac.util.html import html, Markup from trac.util.text import shorten_line from trac.util.compat import set, sorted, partial import graphviz from model import TicketLinks class MasterTicketsModule(Component): """Provides support for ticket dependencies.""" implements(IRequestHandler, IRequestFilter, ITemplateStreamFilter, ITemplateProvider, ITicketManipulator) dot_path = Option('mastertickets', 'dot_path', default='dot', doc='Path to the dot executable.') gs_path = Option('mastertickets', 'gs_path', default='gs', doc='Path to the ghostscript executable.') use_gs = BoolOption('mastertickets', 'use_gs', default=False, doc='If enabled, use ghostscript to produce nicer output.') closed_color = Option('mastertickets', 'closed_color', default='green', doc='Color of closed tickets') opened_color = Option('mastertickets', 'opened_color', default='red', doc='Color of opened tickets') graph_direction = ChoiceOption('mastertickets', 'graph_direction', choices = ['TD', 'LR', 'DT', 'RL'], doc='Direction of the dependency graph (TD = Top Down, DT = Down Top, LR = Left Right, RL = Right Left)') fields = set(['blocking', 'blockedby']) # IRequestFilter methods def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): if req.path_info.startswith('/ticket/'): # In case of an invalid ticket, the data is invalid if not data: return template, data, content_type tkt = data['ticket'] links = TicketLinks(self.env, tkt) for i in links.blocked_by: if Ticket(self.env, i)['status'] != 'closed': add_script(req, 'mastertickets/disable_resolve.js') break # Add link to depgraph if needed if links: add_ctxtnav(req, 'Depgraph', req.href.depgraph(tkt.id)) for change in data.get('changes', {}): if not change.has_key('fields'): continue for field, field_data in change['fields'].iteritems(): if field in self.fields: if field_data['new'].strip(): new = set([int(n) for n in field_data['new'].split(',')]) else: new = set() if field_data['old'].strip(): old = set([int(n) for n in field_data['old'].split(',')]) else: old = set() add = new - old sub = old - new elms = tag() if add: elms.append( tag.em(u', '.join([unicode(n) for n in sorted(add)])) ) elms.append(u' added') if add and sub: elms.append(u'; ') if sub: elms.append( tag.em(u', '.join([unicode(n) for n in sorted(sub)])) ) elms.append(u' removed') field_data['rendered'] = elms #add a link to generate a dependency graph for all the tickets in the milestone if req.path_info.startswith('/milestone/'): if not data: return template, data, content_type milestone=data['milestone'] add_ctxtnav(req, 'Depgraph', req.href.depgraph('milestone', milestone.name)) return template, data, content_type # ITemplateStreamFilter methods def filter_stream(self, req, method, filename, stream, data): if not data: return stream # We try all at the same time to maybe catch also changed or processed templates if filename in ["report_view.html", "query_results.html", "ticket.html", "query.html"]: # For ticket.html if 'fields' in data and isinstance(data['fields'], list): for field in data['fields']: for f in self.fields: if field['name'] == f and data['ticket'][f]: field['rendered'] = self._link_tickets(req, data['ticket'][f]) # For query_results.html and query.html if 'groups' in data and isinstance(data['groups'], list): for group, tickets in data['groups']: for ticket in tickets: for f in self.fields: if f in ticket: ticket[f] = self._link_tickets(req, ticket[f]) # For report_view.html if 'row_groups' in data and isinstance(data['row_groups'], list): for group, rows in data['row_groups']: for row in rows: if 'cell_groups' in row and isinstance(row['cell_groups'], list): for cells in row['cell_groups']: for cell in cells: # If the user names column in the report differently (blockedby AS "blocked by") then this will not find it if cell.get('header', {}).get('col') in self.fields: cell['value'] = self._link_tickets(req, cell['value']) return stream # ITicketManipulator methods def prepare_ticket(self, req, ticket, fields, actions): pass def validate_ticket(self, req, ticket): if req.args.get('action') == 'resolve' and req.args.get('action_resolve_resolve_resolution') == 'fixed': links = TicketLinks(self.env, ticket) for i in links.blocked_by: if Ticket(self.env, i)['status'] != 'closed': yield None, 'Ticket #%s is blocking this ticket'%i # ITemplateProvider methods def get_htdocs_dirs(self): """Return the absolute path of a directory containing additional static resources (such as images, style sheets, etc). """ return [('mastertickets', resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): """Return the absolute path of the directory containing the provided ClearSilver templates. """ return [resource_filename(__name__, 'templates')] # IRequestHandler methods def match_request(self, req): return req.path_info.startswith('/depgraph') def process_request(self, req): path_info = req.path_info[10:] if not path_info: raise TracError('No ticket specified') #list of tickets to generate the depgraph for tkt_ids=[] milestone=None split_path = path_info.split('/', 2) #Urls to generate the depgraph for a ticket is /depgraph/ticketnum #Urls to generate the depgraph for a milestone is /depgraph/milestone/milestone_name if split_path[0] == 'milestone': #we need to query the list of tickets in the milestone milestone = split_path[1] query=Query(self.env, constraints={'milestone' : [milestone]}, max=0) tkt_ids=[fields['id'] for fields in query.execute()] else: #the list is a single ticket tkt_ids = [int(split_path[0])] #the summary argument defines whether we place the ticket id or #it's summary in the node's label label_summary=0 if 'summary' in req.args: label_summary=int(req.args.get('summary')) g = self._build_graph(req, tkt_ids, label_summary=label_summary) if path_info.endswith('/depgraph.png') or 'format' in req.args: format = req.args.get('format') if format == 'text': #in case g.__str__ returns unicode, we need to convert it in ascii req.send(to_unicode(g).encode('ascii', 'replace'), 'text/plain') elif format == 'debug': import pprint req.send( pprint.pformat( [TicketLinks(self.env, tkt_id) for tkt_id in tkt_ids] ), 'text/plain') elif format is not None: req.send(g.render(self.dot_path, format), 'text/plain') if self.use_gs: ps = g.render(self.dot_path, 'ps2') gs = subprocess.Popen([self.gs_path, '-q', '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', '-sDEVICE=png16m', '-sOutputFile=%stdout%', '-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) img, err = gs.communicate(ps) if err: self.log.debug('MasterTickets: Error from gs: %s', err) else: img = g.render(self.dot_path) req.send(img, 'image/png') else: data = {} #add a context link to enable/disable labels in nodes if label_summary: add_ctxtnav(req, 'Without labels', req.href(req.path_info, summary=0)) else: add_ctxtnav(req, 'With labels', req.href(req.path_info, summary=1)) if milestone is None: tkt = Ticket(self.env, tkt_ids[0]) data['tkt'] = tkt add_ctxtnav(req, 'Back to Ticket #%s'%tkt.id, req.href.ticket(tkt.id)) else: add_ctxtnav(req, 'Back to Milestone %s'%milestone, req.href.milestone(milestone)) data['milestone'] = milestone data['graph'] = g data['graph_render'] = partial(g.render, self.dot_path) data['use_gs'] = self.use_gs return 'depgraph.html', data, None def _build_graph(self, req, tkt_ids, label_summary=0): g = graphviz.Graph() g.label_summary = label_summary g.attributes['rankdir'] = self.graph_direction node_default = g['node'] node_default['style'] = 'filled' edge_default = g['edge'] edge_default['style'] = '' # Force this to the top of the graph for id in tkt_ids: g[id] links = TicketLinks.walk_tickets(self.env, tkt_ids) links = sorted(links, key=lambda link: link.tkt.id) for link in links: tkt = link.tkt node = g[tkt.id] if label_summary: node['label'] = u'#%s %s' % (tkt.id, tkt['summary']) else: node['label'] = u'#%s'%tkt.id node['fillcolor'] = tkt['status'] == 'closed' and self.closed_color or self.opened_color node['URL'] = req.href.ticket(tkt.id) node['alt'] = u'Ticket #%s'%tkt.id node['tooltip'] = tkt['summary'] for n in link.blocking: node > g[n] return g def _link_tickets(self, req, tickets): items = [] for i, word in enumerate(re.split(r'([;,\s]+)', tickets)): if i % 2: items.append(word) elif word: ticketid = word word = '#%s' % word try: ticket = Ticket(self.env, ticketid) if 'TICKET_VIEW' in req.perm(ticket.resource): word = \ tag.a( '#%s' % ticket.id, class_=ticket['status'], href=req.href.ticket(int(ticket.id)), title=shorten_line(ticket['summary']) ) except ResourceNotFound: pass items.append(word) if items: return tag(items) else: return None trac-mastertickets-3.0.2+20111224/mastertickets/api.py0000644000175000017500000001545611675460244020402 0ustar wmbwmb# Created by Noah Kantrowitz on 2007-07-04. # Copyright (c) 2007 Noah Kantrowitz. All rights reserved. import re from trac.core import * from trac.env import IEnvironmentSetupParticipant from trac.db import DatabaseManager from trac.ticket.api import ITicketChangeListener, ITicketManipulator from trac.util.compat import set, sorted import db_default from model import TicketLinks from trac.ticket.model import Ticket class MasterTicketsSystem(Component): """Central functionality for the MasterTickets plugin.""" implements(IEnvironmentSetupParticipant, ITicketChangeListener, ITicketManipulator) NUMBERS_RE = re.compile(r'\d+', re.U) # IEnvironmentSetupParticipant methods def environment_created(self): self.found_db_version = 0 self.upgrade_environment(self.env.get_db_cnx()) def environment_needs_upgrade(self, db): cursor = db.cursor() cursor.execute("SELECT value FROM system WHERE name=%s", (db_default.name,)) value = cursor.fetchone() if not value: self.found_db_version = 0 return True else: self.found_db_version = int(value[0]) #self.log.debug('WeatherWidgetSystem: Found db version %s, current is %s' % (self.found_db_version, db_default.version)) if self.found_db_version < db_default.version: return True # Check for our custom fields if 'blocking' not in self.config['ticket-custom'] or 'blockedby' not in self.config['ticket-custom']: return True # Fall through return False def upgrade_environment(self, db): db_manager, _ = DatabaseManager(self.env)._get_connector() # Insert the default table old_data = {} # {table_name: (col_names, [row, ...]), ...} cursor = db.cursor() if not self.found_db_version: cursor.execute("INSERT INTO system (name, value) VALUES (%s, %s)",(db_default.name, db_default.version)) else: cursor.execute("UPDATE system SET value=%s WHERE name=%s",(db_default.version, db_default.name)) for tbl in db_default.tables: try: cursor.execute('SELECT * FROM %s'%tbl.name) old_data[tbl.name] = ([d[0] for d in cursor.description], cursor.fetchall()) except Exception, e: if 'OperationalError' not in e.__class__.__name__: raise e # If it is an OperationalError, keep going try: cursor.execute('DROP TABLE %s'%tbl.name) except Exception, e: if 'OperationalError' not in e.__class__.__name__: raise e # If it is an OperationalError, just move on to the next table for vers, migration in db_default.migrations: if self.found_db_version in vers: self.log.info('MasterTicketsSystem: Running migration %s', migration.__doc__) migration(old_data) for tbl in db_default.tables: for sql in db_manager.to_sql(tbl): cursor.execute(sql) # Try to reinsert any old data if tbl.name in old_data: data = old_data[tbl.name] sql = 'INSERT INTO %s (%s) VALUES (%s)' % \ (tbl.name, ','.join(data[0]), ','.join(['%s'] * len(data[0]))) for row in data[1]: try: cursor.execute(sql, row) except Exception, e: if 'OperationalError' not in e.__class__.__name__: raise e custom = self.config['ticket-custom'] config_dirty = False if 'blocking' not in custom: custom.set('blocking', 'text') custom.set('blocking.label', 'Blocking') config_dirty = True if 'blockedby' not in custom: custom.set('blockedby', 'text') custom.set('blockedby.label', 'Blocked By') config_dirty = True if config_dirty: self.config.save() # ITicketChangeListener methods def ticket_created(self, tkt): self.ticket_changed(tkt, '', tkt['reporter'], {}) def ticket_changed(self, tkt, comment, author, old_values): db = self.env.get_db_cnx() links = self._prepare_links(tkt, db) links.save(author, comment, tkt.time_changed, db) db.commit() def ticket_deleted(self, tkt): db = self.env.get_db_cnx() links = TicketLinks(self.env, tkt, db) links.blocking = set() links.blocked_by = set() links.save('trac', 'Ticket #%s deleted'%tkt.id, when=None, db=db) db.commit() # ITicketManipulator methods def prepare_ticket(self, req, ticket, fields, actions): pass def validate_ticket(self, req, ticket): db = self.env.get_db_cnx() cursor = db.cursor() id = unicode(ticket.id) links = self._prepare_links(ticket, db) # Check that ticket does not have itself as a blocker if id in links.blocking | links.blocked_by: yield 'blocked_by', 'This ticket is blocking itself' return # Check that there aren't any blocked_by in blocking or their parents blocking = links.blocking.copy() while len(blocking) > 0: if len(links.blocked_by & blocking) > 0: yield 'blocked_by', 'This ticket has circular dependencies' return new_blocking = set() for link in blocking: tmp_tkt = Ticket(self.env, link) new_blocking |= TicketLinks(self.env, tmp_tkt, db).blocking blocking = new_blocking for field in ('blocking', 'blockedby'): try: ids = self.NUMBERS_RE.findall(ticket[field] or '') for id in ids[:]: cursor.execute('SELECT id FROM ticket WHERE id=%s', (id,)) row = cursor.fetchone() if row is None: ids.remove(id) ticket[field] = ', '.join(sorted(ids, key=lambda x: int(x))) except Exception, e: self.log.debug('MasterTickets: Error parsing %s "%s": %s', field, ticket[field], e) yield field, 'Not a valid list of ticket IDs' # Internal methods def _prepare_links(self, tkt, db): links = TicketLinks(self.env, tkt, db) links.blocking = set(int(n) for n in self.NUMBERS_RE.findall(tkt['blocking'] or '')) links.blocked_by = set(int(n) for n in self.NUMBERS_RE.findall(tkt['blockedby'] or '')) return links trac-mastertickets-3.0.2+20111224/mastertickets/graphviz.py0000644000175000017500000000746511675460244021464 0ustar wmbwmb# -*- coding: utf-8 -*- # Created by Noah Kantrowitz on 2007-12-21. # Copyright (c) 2007 Noah Kantrowitz. All rights reserved. import os import subprocess import tempfile import time import itertools try: set = set except NameError: from sets import Set as set def _format_options(base_string, options): return u'%s [%s]'%(base_string, u', '.join([u'%s="%s"'%x for x in options.iteritems()])) class Edge(dict): """Model for an edge in a dot graph.""" def __init__(self, source, dest, **kwargs): self.source = source self.dest = dest dict.__init__(self, **kwargs) def __str__(self): ret = u'%s -> %s'%(self.source.name, self.dest.name) if self: ret = _format_options(ret, self) return ret def __hash__(self): return hash(id(self)) class Node(dict): """Model for a node in a dot graph.""" def __init__(self, name, **kwargs): self.name = unicode(name) self.edges = [] dict.__init__(self, **kwargs) def __str__(self): ret = self.name if self: ret = _format_options(ret, self) return ret def __gt__(self, other): """Allow node1 > node2 to add an edge.""" edge = Edge(self, other) self.edges.append(edge) other.edges.append(edge) return edge def __lt__(self, other): edge = Edge(other, self) self.edges.append(edge) other.edges.append(edge) return edge def __hash__(self): return hash(id(self)) class Graph(object): """A model object for a graphviz digraph.""" def __init__(self, name=u'graph'): super(Graph,self).__init__() self.name = name self.nodes = [] self._node_map = {} self.attributes={} self.edges = [] def add(self, obj): if isinstance(obj, Node): self.nodes.append(obj) self._node_map[obj.name] = obj elif isinstance(obj, Edge): self.edges.append(obj) def __getitem__(self, key): key = unicode(key) if key not in self._node_map: new_node = Node(key) self._node_map[key] = new_node self.nodes.append(new_node) return self._node_map[key] def __delitem__(self, key): key = unicode(key) node = self._node_map.pop(key) self.nodes.remove(node) def __str__(self): edges = [] nodes = [] memo = set() def process(lst): for obj in lst: if obj in memo: continue memo.add(obj) if isinstance(obj, Node): nodes.append(obj) process(obj.edges) elif isinstance(obj, Edge): edges.append(obj) if isinstance(obj.source, Node): process((obj.source,)) if isinstance(obj.dest, Node): process((obj.dest,)) process(self.nodes) process(self.edges) lines = [u'digraph "%s" {'%self.name] for att,value in self.attributes.iteritems(): lines.append(u'\t%s="%s";' % (att,value)) for obj in itertools.chain(nodes, edges): lines.append(u'\t%s;'%obj) lines.append(u'}') return u'\n'.join(lines) def render(self, dot_path='dot', format='png'): """Render a dot graph.""" proc = subprocess.Popen([dot_path, '-T%s'%format], stdin=subprocess.PIPE, stdout=subprocess.PIPE) out, _ = proc.communicate(unicode(self).encode('utf8')) return out if __name__ == '__main__': g = Graph() root = Node('me') root > Node('them') root < Node(u'Üs') g.add(root) print g.render() trac-mastertickets-3.0.2+20111224/mastertickets/db_default.py0000644000175000017500000000112211675460244021703 0ustar wmbwmb# Created by Noah Kantrowitz on 2007-07-04. # Copyright (c) 2007 Noah Kantrowitz. All rights reserved. from trac.db import Table, Column name = 'mastertickets' version = 2 tables = [ Table('mastertickets', key=('source','dest'))[ Column('source', type='integer'), Column('dest', type='integer'), ], ] def convert_to_int(data): """Convert both source and dest in the mastertickets table to ints.""" rows = data['mastertickets'][1] for i, (n1, n2) in enumerate(rows): rows[i] = [int(n1), int(n2)] migrations = [ (xrange(1,2), convert_to_int), ]