source: ticketmodifiedfilesplugin/0.11/ticketmodifiedfiles/ticketmodifiedfiles.py @ 11457

Last change on this file since 11457 was 11457, checked in by Ryan J Ollos, 12 years ago

Refs #8688: Modified Genshi transformer so that it can find a match in Trac 0.12 and inject the modified files link below the Change History, as it does in Trac 0.11. This change should preserve the functionality in Trac 0.11.

File size: 9.5 KB
Line 
1# TicketModifiedFiles plugin
2# This software is licensed as described in the file COPYING.txt, which you
3# should have received as part of this distribution.
4
5import re
6
7from trac.core import *
8from trac.ticket.model import Ticket
9from trac.web import IRequestHandler
10from trac.web.api import IRequestFilter, ITemplateStreamFilter
11from trac.web.chrome import ITemplateProvider, add_stylesheet, add_script, add_ctxtnav
12from trac.util.datefmt import format_time
13
14#WARNING: genshi.filters.Transformer requires Genshi 0.5+
15from genshi.filters import Transformer
16from genshi.builder import tag
17
18class TicketModifiedFilesPlugin(Component):
19    implements(IRequestHandler, IRequestFilter, ITemplateProvider, ITemplateStreamFilter)
20   
21    # IRequestHandler methods
22    def match_request(self, req):
23        match = re.match(r'/modifiedfiles/([0-9]+)$', req.path_info)
24        if match:
25            req.args['id'] = match.group(1)
26            return True
27   
28    def process_request(self, req):
29        #Retrieve the information needed to display in the /modifiedfiles/ page
30        (id, files, deletedfiles, ticketsperfile, filestatus, conflictingtickets, ticketisclosed, revisions, ticketsdescription) = self.__process_ticket_request(req)
31        #Pack the information to send to the html file
32        data = {'ticketid':id, 'files':files, 'deletedfiles':deletedfiles, 'ticketsperfile':ticketsperfile, 'filestatus':filestatus, 'conflictingtickets':conflictingtickets, 'ticketisclosed':ticketisclosed, 'revisions':revisions, 'ticketsdescription':ticketsdescription}
33
34        add_ctxtnav(req, 'Back to Ticket #%s' % id, req.href.ticket(id))
35
36        #Add the custom stylesheet
37        add_stylesheet(req, 'common/css/timeline.css')
38        add_stylesheet(req, 'tmf/css/ticketmodifiedfiles.css')
39        add_script(req, 'tmf/js/ticketmodifiedfiles.js')
40        return 'ticketmodifiedfiles.html', data, None
41   
42    # IRequestFilter methods
43    def pre_process_request(self, req, handler):
44        return handler
45   
46    def post_process_request(self, req, template, data, content_type):
47        match = re.match(r'/ticket/([0-9]+)$', req.path_info)
48        if match:
49            data['modifiedfiles'] = int(match.group(1))
50        return template, data, content_type
51
52    # ITemplateProvider methods
53    # Used to add the plugin's templates and htdocs
54    def get_templates_dirs(self):
55        from pkg_resources import resource_filename
56        return [resource_filename(__name__, 'templates')]
57   
58    def get_htdocs_dirs(self):
59        """Return a list of directories with static resources (such as style
60        sheets, images, etc.)
61
62        Each item in the list must be a `(prefix, abspath)` tuple. The
63        `prefix` part defines the path in the URL that requests to these
64        resources are prefixed with.
65
66        The `abspath` is the absolute path to the directory containing the
67        resources on the local file system.
68        """
69        from pkg_resources import resource_filename
70        return [('tmf', resource_filename(__name__, 'htdocs'))]
71   
72    # ITemplateStreamFilter methods
73    def filter_stream(self, req, method, filename, stream, data):
74        if 'modifiedfiles' in data:
75            numconflictingtickets = self.__process_ticket_request(req, True)
76            #Display a warning message if there are conflicting tickets
77            if numconflictingtickets > 0:
78                if numconflictingtickets == 1:
79                    text = " There is one ticket in conflict!"
80                else:
81                    text = " There are %s tickets in conflict!" % str(numconflictingtickets)
82                stream |= Transformer("//div[@id='changelog']").before(tag.p(tag.strong("Warning:"), text, style='background: #def; border: 2px solid #00d; padding: 3px;'))
83           
84            #Display the link to this ticket's modifiedfiles page
85            stream |= Transformer("//div[@id='changelog']").before(
86                       tag.p(
87                             'Have a look at the ',
88                             tag.a("list of modified files", href="../modifiedfiles/" + str(data["modifiedfiles"])),
89                             ' related to this ticket.'
90                             )
91                       )
92        return stream
93
94    # Internal methods
95    def __process_ticket_request(self, req, justnumconflictingtickets = False):
96        id = int(req.args.get('id'))
97        req.perm('ticket', id, None).require('TICKET_VIEW')
98        #Get the list of status that have to be ignored when looking for conflicts
99        ignored_statuses = self.__striplist(self.env.config.get("modifiedfiles", "ignored_statuses", "closed").split(","))
100       
101        #Check if the ticket exists (throws an exception if the ticket does not exist)
102        thisticket = Ticket(self.env, id)
103       
104        #Tickets that are in the ignored states can not be in conflict
105        if justnumconflictingtickets and thisticket['status'] in ignored_statuses:
106            return 0
107       
108        files = []
109        revisions = []
110        ticketsperfile = {}
111       
112        db = self.env.get_db_cnx()
113        cursor = db.cursor()
114        #Retrieve all the revisions which's messages contain "#<TICKETID>"
115        cursor.execute("SELECT rev, time, author, message FROM revision WHERE message LIKE '%%#%s%%'" % id)
116        repos = self.env.get_repository()
117        for rev, time, author, message, in cursor:
118            #Filter out non-related revisions.
119            #for instance, you are lookink for #19, so you don't want #190, #191, #192, etc. to interfere
120            #To filter, check what the eventual char after "#19" is.
121            #If it's a number, we dont' want it (validrevision = False), but if it's text, keep this revision
122            validrevision = True
123            tempstr = message.split("#" + str(id), 1)
124            if len(tempstr[1]) > 0:
125                try:
126                    int(tempstr[1][0])
127                    validrevision = False
128                except:
129                    pass
130               
131            if validrevision:
132                if not justnumconflictingtickets:
133                    date = "(" + format_time(time, str('%d/%m/%Y - %H:%M')) + ")"
134                    revisions.append((rev, author, date))
135                for node_change in repos.get_changeset(rev).get_changes():
136                    files.append(node_change[0])
137                   
138       
139        #Remove duplicated values
140        files = self.__remove_duplicated_elements_and_sort(files)
141       
142        filestatus = {}
143       
144        for file in files:
145            #Get the last status of each file
146            if not justnumconflictingtickets:
147                try:
148                    node = repos.get_node(file)
149                    filestatus[file] = node.get_history().next()[2]
150                except:
151                    #If the node doesn't exist (in the last revision) it means that it has been deleted
152                    filestatus[file] = "delete"
153       
154            #Get the list of conflicting tickets per file
155            tempticketslist = []
156            cursor.execute("SELECT message FROM revision WHERE rev IN (SELECT rev FROM node_change WHERE path='%s')" % file)
157            for message, in cursor:
158                #Extract the ticket number
159                match = re.search(r'#([0-9]+)', message)
160                if match:
161                    ticket = int(match.group(1))
162                    #Don't add yourself
163                    if ticket != id:
164                        tempticketslist.append(ticket)
165            tempticketslist = self.__remove_duplicated_elements_and_sort(tempticketslist)
166           
167            ticketsperfile[file] = []
168            #Keep only the active tickets
169            for ticket in tempticketslist:
170                try:
171                    if Ticket(self.env, ticket)['status'] not in ignored_statuses:
172                        ticketsperfile[file].append(ticket)
173                except:
174                    pass
175       
176        #Get the global list of conflicting tickets
177        #Only if the ticket is not already closed
178        conflictingtickets=[]
179        ticketsdescription={}
180        ticketsdescription[id] = thisticket['summary']
181        ticketisclosed = True
182        if thisticket['status'] not in ignored_statuses:
183            ticketisclosed = False
184            for fn, relticketids in ticketsperfile.items():
185                for relticketid in relticketids:
186                    tick = Ticket(self.env, relticketid)
187                    conflictingtickets.append((relticketid, tick['status'], tick['owner']))
188                    ticketsdescription[relticketid] = tick['summary']
189   
190            #Remove duplicated values
191            conflictingtickets = self.__remove_duplicated_elements_and_sort(conflictingtickets)
192       
193        #Close the repository
194        repos.close()
195       
196        #Return only the number of conflicting tickets (if asked for)
197        if justnumconflictingtickets:
198            return len(conflictingtickets)
199       
200        #Separate the deleted files from the others
201        deletedfiles = []
202        for file in files:
203            if filestatus[file] == "delete":
204                deletedfiles.append(file)
205        for deletedfile in deletedfiles:
206            files.remove(deletedfile)
207       
208        #Return all the needed information
209        return (id, files, deletedfiles, ticketsperfile, filestatus, conflictingtickets, ticketisclosed, revisions, ticketsdescription)
210   
211    def __remove_duplicated_elements_and_sort(self, list):
212        d = {}
213        for x in list: d[x]=1
214        return sorted(d.keys())
215   
216    def __striplist(self, l):
217        return([x.strip() for x in l])
Note: See TracBrowser for help on using the repository browser.