]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/models.py
a5f60cb8f94268a4aed8cb0ab9da937a574e9614
[patchwork] / apps / patchwork / models.py
1 # Patchwork - automated patch tracking system
2 # Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
3 #
4 # This file is part of the Patchwork package.
5 #
6 # Patchwork is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # Patchwork is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with Patchwork; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 from django.db import models
21 from django.contrib.auth.models import User
22 from django.core.urlresolvers import reverse
23 from django.contrib.sites.models import Site
24 from django.conf import settings
25 from patchwork.parser import hash_patch
26
27 import re
28 import datetime, time
29 import random
30
31 class Person(models.Model):
32     email = models.CharField(max_length=255, unique = True)
33     name = models.CharField(max_length=255, null = True, blank = True)
34     user = models.ForeignKey(User, null = True, blank = True,
35             on_delete = models.SET_NULL)
36
37     def __unicode__(self):
38         if self.name:
39             return u'%s <%s>' % (self.name, self.email)
40         else:
41             return self.email
42
43     def link_to_user(self, user):
44         self.name = user.get_profile().name()
45         self.user = user
46
47     class Meta:
48         verbose_name_plural = 'People'
49
50 class Project(models.Model):
51     linkname = models.CharField(max_length=255, unique=True)
52     name = models.CharField(max_length=255, unique=True)
53     listid = models.CharField(max_length=255, unique=True)
54     listemail = models.CharField(max_length=200)
55     web_url = models.CharField(max_length=2000, blank=True)
56     scm_url = models.CharField(max_length=2000, blank=True)
57     webscm_url = models.CharField(max_length=2000, blank=True)
58     send_notifications = models.BooleanField(default=False)
59
60     def __unicode__(self):
61         return self.name
62
63     def is_editable(self, user):
64         if not user.is_authenticated():
65             return False
66         return self in user.get_profile().maintainer_projects.all()
67
68 class UserProfile(models.Model):
69     user = models.ForeignKey(User, unique = True)
70     primary_project = models.ForeignKey(Project, null = True, blank = True)
71     maintainer_projects = models.ManyToManyField(Project,
72             related_name = 'maintainer_project')
73     send_email = models.BooleanField(default = False,
74             help_text = 'Selecting this option allows patchwork to send ' +
75                 'email on your behalf')
76     patches_per_page = models.PositiveIntegerField(default = 100,
77             null = False, blank = False,
78             help_text = 'Number of patches to display per page')
79
80     def name(self):
81         if self.user.first_name or self.user.last_name:
82             names = filter(bool, [self.user.first_name, self.user.last_name])
83             return u' '.join(names)
84         return self.user.username
85
86     def contributor_projects(self):
87         submitters = Person.objects.filter(user = self.user)
88         return Project.objects.filter(id__in =
89                                         Patch.objects.filter(
90                                             submitter__in = submitters)
91                                         .values('project_id').query)
92
93
94     def sync_person(self):
95         pass
96
97     def n_todo_patches(self):
98         return self.todo_patches().count()
99
100     def todo_patches(self, project = None):
101
102         # filter on project, if necessary
103         if project:
104             qs = Patch.objects.filter(project = project)
105         else:
106             qs = Patch.objects
107
108         qs = qs.filter(archived = False) \
109              .filter(delegate = self.user) \
110              .filter(state__in =
111                      State.objects.filter(action_required = True)
112                          .values('pk').query)
113         return qs
114
115     def __unicode__(self):
116         return self.name()
117
118 def _user_saved_callback(sender, created, instance, **kwargs):
119     try:
120         profile = instance.get_profile()
121     except UserProfile.DoesNotExist:
122         profile = UserProfile(user = instance)
123     profile.save()
124
125 models.signals.post_save.connect(_user_saved_callback, sender = User)
126
127 class State(models.Model):
128     name = models.CharField(max_length = 100)
129     ordering = models.IntegerField(unique = True)
130     action_required = models.BooleanField(default = True)
131
132     def __unicode__(self):
133         return self.name
134
135     class Meta:
136         ordering = ['ordering']
137
138 class HashField(models.CharField):
139     __metaclass__ = models.SubfieldBase
140
141     def __init__(self, algorithm = 'sha1', *args, **kwargs):
142         self.algorithm = algorithm
143         try:
144             import hashlib
145             def _construct(string = ''):
146                 return hashlib.new(self.algorithm, string)
147             self.construct = _construct
148             self.n_bytes = len(hashlib.new(self.algorithm).hexdigest())
149         except ImportError:
150             modules = { 'sha1': 'sha', 'md5': 'md5'}
151
152             if algorithm not in modules.keys():
153                 raise NameError("Unknown algorithm '%s'" % algorithm)
154
155             self.construct = __import__(modules[algorithm]).new
156
157         self.n_bytes = len(self.construct().hexdigest())
158
159         kwargs['max_length'] = self.n_bytes
160         super(HashField, self).__init__(*args, **kwargs)
161
162     def db_type(self, connection=None):
163         return 'char(%d)' % self.n_bytes
164
165 def get_default_initial_patch_state():
166     return State.objects.get(ordering=0)
167
168 class Patch(models.Model):
169     project = models.ForeignKey(Project)
170     msgid = models.CharField(max_length=255)
171     name = models.CharField(max_length=255)
172     date = models.DateTimeField(default=datetime.datetime.now)
173     submitter = models.ForeignKey(Person)
174     delegate = models.ForeignKey(User, blank = True, null = True)
175     state = models.ForeignKey(State, default=get_default_initial_patch_state)
176     archived = models.BooleanField(default = False)
177     headers = models.TextField(blank = True)
178     content = models.TextField(null = True, blank = True)
179     pull_url = models.CharField(max_length=255, null = True, blank = True)
180     commit_ref = models.CharField(max_length=255, null = True, blank = True)
181     hash = HashField(null = True, blank = True)
182
183     def __unicode__(self):
184         return self.name
185
186     def comments(self):
187         return Comment.objects.filter(patch = self)
188
189     def save(self):
190         try:
191             s = self.state
192         except:
193             self.state = State.objects.get(ordering =  0)
194
195         if self.hash is None and self.content is not None:
196             self.hash = hash_patch(self.content).hexdigest()
197
198         super(Patch, self).save()
199
200     def is_editable(self, user):
201         if not user.is_authenticated():
202             return False
203
204         if self.submitter.user == user or self.delegate == user:
205             return True
206
207         return self.project.is_editable(user)
208
209     def filename(self):
210         fname_re = re.compile('[^-_A-Za-z0-9\.]+')
211         str = fname_re.sub('-', self.name)
212         return str.strip('-') + '.patch'
213
214     @models.permalink
215     def get_absolute_url(self):
216         return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
217
218     class Meta:
219         verbose_name_plural = 'Patches'
220         ordering = ['date']
221         unique_together = [('msgid', 'project')]
222
223 class Comment(models.Model):
224     patch = models.ForeignKey(Patch)
225     msgid = models.CharField(max_length=255)
226     submitter = models.ForeignKey(Person)
227     date = models.DateTimeField(default = datetime.datetime.now)
228     headers = models.TextField(blank = True)
229     content = models.TextField()
230
231     response_re = re.compile( \
232             '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
233             re.M | re.I)
234
235     def patch_responses(self):
236         return ''.join([ match.group(0) + '\n' for match in
237                                 self.response_re.finditer(self.content)])
238
239     class Meta:
240         ordering = ['date']
241         unique_together = [('msgid', 'patch')]
242
243 class Bundle(models.Model):
244     owner = models.ForeignKey(User)
245     project = models.ForeignKey(Project)
246     name = models.CharField(max_length = 50, null = False, blank = False)
247     patches = models.ManyToManyField(Patch, through = 'BundlePatch')
248     public = models.BooleanField(default = False)
249
250     def n_patches(self):
251         return self.patches.all().count()
252
253     def ordered_patches(self):
254         return self.patches.order_by('bundlepatch__order')
255
256     def append_patch(self, patch):
257         # todo: use the aggregate queries in django 1.1
258         orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
259                  .values('order')
260
261         if len(orders) > 0:
262             max_order = orders[0]['order']
263         else:
264             max_order = 0
265
266         # see if the patch is already in this bundle
267         if BundlePatch.objects.filter(bundle = self, patch = patch).count():
268             raise Exception("patch is already in bundle")
269
270         bp = BundlePatch.objects.create(bundle = self, patch = patch,
271                 order = max_order + 1)
272         bp.save()
273
274     class Meta:
275         unique_together = [('owner', 'name')]
276
277     def public_url(self):
278         if not self.public:
279             return None
280         site = Site.objects.get_current()
281         return 'http://%s%s' % (site.domain,
282                 reverse('patchwork.views.bundle.bundle',
283                         kwargs = {
284                                 'username': self.owner.username,
285                                 'bundlename': self.name
286                         }))
287
288     @models.permalink
289     def get_absolute_url(self):
290         return ('patchwork.views.bundle.bundle', (), {
291                                 'username': self.owner.username,
292                                 'bundlename': self.name,
293                             })
294
295 class BundlePatch(models.Model):
296     patch = models.ForeignKey(Patch)
297     bundle = models.ForeignKey(Bundle)
298     order = models.IntegerField()
299
300     class Meta:
301         unique_together = [('bundle', 'patch')]
302         ordering = ['order']
303
304 class EmailConfirmation(models.Model):
305     validity = datetime.timedelta(days = settings.CONFIRMATION_VALIDITY_DAYS)
306     type = models.CharField(max_length = 20, choices = [
307                                 ('userperson', 'User-Person association'),
308                                 ('registration', 'Registration'),
309                                 ('optout', 'Email opt-out'),
310                             ])
311     email = models.CharField(max_length = 200)
312     user = models.ForeignKey(User, null = True)
313     key = HashField()
314     date = models.DateTimeField(default = datetime.datetime.now)
315     active = models.BooleanField(default = True)
316
317     def deactivate(self):
318         self.active = False
319         self.save()
320
321     def is_valid(self):
322         return self.date + self.validity > datetime.datetime.now()
323
324     def save(self):
325         max = 1 << 32
326         if self.key == '':
327             str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
328             self.key = self._meta.get_field('key').construct(str).hexdigest()
329         super(EmailConfirmation, self).save()
330
331 class EmailOptout(models.Model):
332     email = models.CharField(max_length = 200, primary_key = True)
333
334     def __unicode__(self):
335         return self.email
336
337     @classmethod
338     def is_optout(cls, email):
339         email = email.lower().strip()
340         return cls.objects.filter(email = email).count() > 0
341
342 class PatchChangeNotification(models.Model):
343     patch = models.ForeignKey(Patch, primary_key = True)
344     last_modified = models.DateTimeField(default = datetime.datetime.now)
345     orig_state = models.ForeignKey(State)
346
347 def _patch_change_callback(sender, instance, **kwargs):
348     # we only want notification of modified patches
349     if instance.pk is None:
350         return
351
352     if instance.project is None or not instance.project.send_notifications:
353         return
354
355     try:
356         orig_patch = Patch.objects.get(pk = instance.pk)
357     except Patch.DoesNotExist:
358         return
359
360     # If there's no interesting changes, abort without creating the
361     # notification
362     if orig_patch.state == instance.state:
363         return
364
365     notification = None
366     try:
367         notification = PatchChangeNotification.objects.get(patch = instance)
368     except PatchChangeNotification.DoesNotExist:
369         pass
370
371     if notification is None:
372         notification = PatchChangeNotification(patch = instance,
373                                                orig_state = orig_patch.state)
374
375     elif notification.orig_state == instance.state:
376         # If we're back at the original state, there is no need to notify
377         notification.delete()
378         return
379
380     notification.last_modified = datetime.datetime.now()
381     notification.save()
382
383 models.signals.pre_save.connect(_patch_change_callback, sender = Patch)