2222from django .core .urlresolvers import reverse
2323from django .contrib .sites .models import Site
2424from django .conf import settings
25- from patchwork .parser import hash_patch
25+ from django .utils .functional import cached_property
26+ from patchwork .parser import hash_patch , extract_tags
2627
2728import re
2829import datetime , time
2930import random
31+ from collections import Counter , OrderedDict
3032
3133class Person (models .Model ):
3234 email = models .CharField (max_length = 255 , unique = True )
@@ -56,6 +58,7 @@ class Project(models.Model):
5658 scm_url = models .CharField (max_length = 2000 , blank = True )
5759 webscm_url = models .CharField (max_length = 2000 , blank = True )
5860 send_notifications = models .BooleanField (default = False )
61+ use_tags = models .BooleanField (default = True )
5962
6063 def __unicode__ (self ):
6164 return self .name
@@ -65,6 +68,12 @@ def is_editable(self, user):
6568 return False
6669 return self in user .profile .maintainer_projects .all ()
6770
71+ @cached_property
72+ def tags (self ):
73+ if not self .use_tags :
74+ return []
75+ return list (Tag .objects .all ())
76+
6877 class Meta :
6978 ordering = ['linkname' ]
7079
@@ -165,9 +174,68 @@ def _construct(string = ''):
165174 def db_type (self , connection = None ):
166175 return 'char(%d)' % self .n_bytes
167176
177+ class Tag (models .Model ):
178+ name = models .CharField (max_length = 20 )
179+ pattern = models .CharField (max_length = 50 ,
180+ help_text = 'A simple regex to match the tag in the content of '
181+ 'a message. Will be used with MULTILINE and IGNORECASE '
182+ 'flags. eg. ^Acked-by:' )
183+ abbrev = models .CharField (max_length = 2 , unique = True ,
184+ help_text = 'Short (one-or-two letter) abbreviation for the tag, '
185+ 'used in table column headers' )
186+
187+ def __unicode__ (self ):
188+ return self .name
189+
190+ @property
191+ def attr_name (self ):
192+ return 'tag_%d_count' % self .id
193+
194+ class Meta :
195+ ordering = ['abbrev' ]
196+
197+ class PatchTag (models .Model ):
198+ patch = models .ForeignKey ('Patch' )
199+ tag = models .ForeignKey ('Tag' )
200+ count = models .IntegerField (default = 1 )
201+
202+ class Meta :
203+ unique_together = [('patch' , 'tag' )]
204+
168205def get_default_initial_patch_state ():
169206 return State .objects .get (ordering = 0 )
170207
208+ class PatchQuerySet (models .query .QuerySet ):
209+
210+ def with_tag_counts (self , project ):
211+ if not project .use_tags :
212+ return self
213+
214+ # We need the project's use_tags field loaded for Project.tags().
215+ # Using prefetch_related means we'll share the one instance of
216+ # Project, and share the project.tags cache between all patch.project
217+ # references.
218+ qs = self .prefetch_related ('project' )
219+ select = OrderedDict ()
220+ select_params = []
221+ for tag in project .tags :
222+ select [tag .attr_name ] = ("coalesce("
223+ "(SELECT count FROM patchwork_patchtag "
224+ "WHERE patchwork_patchtag.patch_id=patchwork_patch.id "
225+ "AND patchwork_patchtag.tag_id=%s), 0)" )
226+ select_params .append (tag .id )
227+
228+ return qs .extra (select = select , select_params = select_params )
229+
230+ class PatchManager (models .Manager ):
231+ use_for_related_fields = True
232+
233+ def get_queryset (self ):
234+ return PatchQuerySet (self .model , using = self .db )
235+
236+ def with_tag_counts (self , project ):
237+ return self .get_queryset ().with_tag_counts (project )
238+
171239class Patch (models .Model ):
172240 project = models .ForeignKey (Project )
173241 msgid = models .CharField (max_length = 255 )
@@ -182,13 +250,34 @@ class Patch(models.Model):
182250 pull_url = models .CharField (max_length = 255 , null = True , blank = True )
183251 commit_ref = models .CharField (max_length = 255 , null = True , blank = True )
184252 hash = HashField (null = True , blank = True )
253+ tags = models .ManyToManyField (Tag , through = PatchTag )
254+
255+ objects = PatchManager ()
185256
186257 def __unicode__ (self ):
187258 return self .name
188259
189260 def comments (self ):
190261 return Comment .objects .filter (patch = self )
191262
263+ def _set_tag (self , tag , count ):
264+ if count == 0 :
265+ self .patchtag_set .filter (tag = tag ).delete ()
266+ return
267+ (patchtag , _ ) = PatchTag .objects .get_or_create (patch = self , tag = tag )
268+ if patchtag .count != count :
269+ patchtag .count = count
270+ patchtag .save ()
271+
272+ def refresh_tag_counts (self ):
273+ tags = self .project .tags
274+ counter = Counter ()
275+ for comment in self .comment_set .all ():
276+ counter = counter + extract_tags (comment .content , tags )
277+
278+ for tag in tags :
279+ self ._set_tag (tag , counter [tag ])
280+
192281 def save (self ):
193282 try :
194283 s = self .state
@@ -239,6 +328,14 @@ def patch_responses(self):
239328 return '' .join ([ match .group (0 ) + '\n ' for match in
240329 self .response_re .finditer (self .content )])
241330
331+ def save (self , * args , ** kwargs ):
332+ super (Comment , self ).save (* args , ** kwargs )
333+ self .patch .refresh_tag_counts ()
334+
335+ def delete (self , * args , ** kwargs ):
336+ super (Comment , self ).delete (* args , ** kwargs )
337+ self .patch .refresh_tag_counts ()
338+
242339 class Meta :
243340 ordering = ['date' ]
244341 unique_together = [('msgid' , 'patch' )]
0 commit comments