]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/models.py
54b86566d7bd9a07b0dabb18c978b3d9b44f01f1
[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.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.profile.maintainer_projects.all()
67
68     class Meta:
69         ordering = ['linkname']
70
71
72 class UserProfile(models.Model):
73     user = models.OneToOneField(User, unique = True, related_name='profile')
74     primary_project = models.ForeignKey(Project, null = True, blank = True)
75     maintainer_projects = models.ManyToManyField(Project,
76             related_name = 'maintainer_project')
77     send_email = models.BooleanField(default = False,
78             help_text = 'Selecting this option allows patchwork to send ' +
79                 'email on your behalf')
80     patches_per_page = models.PositiveIntegerField(default = 100,
81             null = False, blank = False,
82             help_text = 'Number of patches to display per page')
83
84     def name(self):
85         if self.user.first_name or self.user.last_name:
86             names = filter(bool, [self.user.first_name, self.user.last_name])
87             return u' '.join(names)
88         return self.user.username
89
90     def contributor_projects(self):
91         submitters = Person.objects.filter(user = self.user)
92         return Project.objects.filter(id__in =
93                                         Patch.objects.filter(
94                                             submitter__in = submitters)
95                                         .values('project_id').query)
96
97     def sync_person(self):
98         pass
99
100     def n_todo_patches(self):
101         return self.todo_patches().count()
102
103     def todo_patches(self, project = None):
104
105         # filter on project, if necessary
106         if project:
107             qs = Patch.objects.filter(project = project)
108         else:
109             qs = Patch.objects
110
111         qs = qs.filter(archived = False) \
112              .filter(delegate = self.user) \
113              .filter(state__in =
114                      State.objects.filter(action_required = True)
115                          .values('pk').query)
116         return qs
117
118     def __unicode__(self):
119         return self.name()
120
121 def _user_saved_callback(sender, created, instance, **kwargs):
122     try:
123         profile = instance.profile
124     except UserProfile.DoesNotExist:
125         profile = UserProfile(user = instance)
126     profile.save()
127
128 models.signals.post_save.connect(_user_saved_callback, sender = User)
129
130 class State(models.Model):
131     name = models.CharField(max_length = 100)
132     ordering = models.IntegerField(unique = True)
133     action_required = models.BooleanField(default = True)
134
135     def __unicode__(self):
136         return self.name
137
138     class Meta:
139         ordering = ['ordering']
140
141 class HashField(models.CharField):
142     __metaclass__ = models.SubfieldBase
143
144     def __init__(self, algorithm = 'sha1', *args, **kwargs):
145         self.algorithm = algorithm
146         try:
147             import hashlib
148             def _construct(string = ''):
149                 return hashlib.new(self.algorithm, string)
150             self.construct = _construct
151             self.n_bytes = len(hashlib.new(self.algorithm).hexdigest())
152         except ImportError:
153             modules = { 'sha1': 'sha', 'md5': 'md5'}
154
155             if algorithm not in modules.keys():
156                 raise NameError("Unknown algorithm '%s'" % algorithm)
157
158             self.construct = __import__(modules[algorithm]).new
159
160         self.n_bytes = len(self.construct().hexdigest())
161
162         kwargs['max_length'] = self.n_bytes
163         super(HashField, self).__init__(*args, **kwargs)
164
165     def db_type(self, connection=None):
166         return 'char(%d)' % self.n_bytes
167
168 def get_default_initial_patch_state():
169     return State.objects.get(ordering=0)
170
171 class Patch(models.Model):
172     project = models.ForeignKey(Project)
173     msgid = models.CharField(max_length=255)
174     name = models.CharField(max_length=255)
175     date = models.DateTimeField(default=datetime.datetime.now)
176     submitter = models.ForeignKey(Person)
177     delegate = models.ForeignKey(User, blank = True, null = True)
178     state = models.ForeignKey(State, default=get_default_initial_patch_state)
179     archived = models.BooleanField(default = False)
180     headers = models.TextField(blank = True)
181     content = models.TextField(null = True, blank = True)
182     pull_url = models.CharField(max_length=255, null = True, blank = True)
183     commit_ref = models.CharField(max_length=255, null = True, blank = True)
184     hash = HashField(null = True, blank = True)
185
186     def __unicode__(self):
187         return self.name
188
189     def comments(self):
190         return Comment.objects.filter(patch = self)
191
192     def save(self):
193         try:
194             s = self.state
195         except:
196             self.state = State.objects.get(ordering =  0)
197
198         if self.hash is None and self.content is not None:
199             self.hash = hash_patch(self.content).hexdigest()
200
201         super(Patch, self).save()
202
203     def is_editable(self, user):
204         if not user.is_authenticated():
205             return False
206
207         if self.submitter.user == user or self.delegate == user:
208             return True
209
210         return self.project.is_editable(user)
211
212     def filename(self):
213         fname_re = re.compile('[^-_A-Za-z0-9\.]+')
214         str = fname_re.sub('-', self.name)
215         return str.strip('-') + '.patch'
216
217     @models.permalink
218     def get_absolute_url(self):
219         return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
220
221     class Meta:
222         verbose_name_plural = 'Patches'
223         ordering = ['date']
224         unique_together = [('msgid', 'project')]
225
226 class Comment(models.Model):
227     patch = models.ForeignKey(Patch)
228     msgid = models.CharField(max_length=255)
229     submitter = models.ForeignKey(Person)
230     date = models.DateTimeField(default = datetime.datetime.now)
231     headers = models.TextField(blank = True)
232     content = models.TextField()
233
234     response_re = re.compile( \
235             '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
236             re.M | re.I)
237
238     def patch_responses(self):
239         return ''.join([ match.group(0) + '\n' for match in
240                                 self.response_re.finditer(self.content)])
241
242     class Meta:
243         ordering = ['date']
244         unique_together = [('msgid', 'patch')]
245
246 class Bundle(models.Model):
247     owner = models.ForeignKey(User)
248     project = models.ForeignKey(Project)
249     name = models.CharField(max_length = 50, null = False, blank = False)
250     patches = models.ManyToManyField(Patch, through = 'BundlePatch')
251     public = models.BooleanField(default = False)
252
253     def n_patches(self):
254         return self.patches.all().count()
255
256     def ordered_patches(self):
257         return self.patches.order_by('bundlepatch__order')
258
259     def append_patch(self, patch):
260         # todo: use the aggregate queries in django 1.1
261         orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
262                  .values('order')
263
264         if len(orders) > 0:
265             max_order = orders[0]['order']
266         else:
267             max_order = 0
268
269         # see if the patch is already in this bundle
270         if BundlePatch.objects.filter(bundle = self, patch = patch).count():
271             raise Exception("patch is already in bundle")
272
273         bp = BundlePatch.objects.create(bundle = self, patch = patch,
274                 order = max_order + 1)
275         bp.save()
276
277     class Meta:
278         unique_together = [('owner', 'name')]
279
280     def public_url(self):
281         if not self.public:
282             return None
283         site = Site.objects.get_current()
284         return 'http://%s%s' % (site.domain,
285                 reverse('patchwork.views.bundle.bundle',
286                         kwargs = {
287                                 'username': self.owner.username,
288                                 'bundlename': self.name
289                         }))
290
291     @models.permalink
292     def get_absolute_url(self):
293         return ('patchwork.views.bundle.bundle', (), {
294                                 'username': self.owner.username,
295                                 'bundlename': self.name,
296                             })
297
298 class BundlePatch(models.Model):
299     patch = models.ForeignKey(Patch)
300     bundle = models.ForeignKey(Bundle)
301     order = models.IntegerField()
302
303     class Meta:
304         unique_together = [('bundle', 'patch')]
305         ordering = ['order']
306
307 class EmailConfirmation(models.Model):
308     validity = datetime.timedelta(days = settings.CONFIRMATION_VALIDITY_DAYS)
309     type = models.CharField(max_length = 20, choices = [
310                                 ('userperson', 'User-Person association'),
311                                 ('registration', 'Registration'),
312                                 ('optout', 'Email opt-out'),
313                             ])
314     email = models.CharField(max_length = 200)
315     user = models.ForeignKey(User, null = True)
316     key = HashField()
317     date = models.DateTimeField(default = datetime.datetime.now)
318     active = models.BooleanField(default = True)
319
320     def deactivate(self):
321         self.active = False
322         self.save()
323
324     def is_valid(self):
325         return self.date + self.validity > datetime.datetime.now()
326
327     def save(self):
328         max = 1 << 32
329         if self.key == '':
330             str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
331             self.key = self._meta.get_field('key').construct(str).hexdigest()
332         super(EmailConfirmation, self).save()
333
334 class EmailOptout(models.Model):
335     email = models.CharField(max_length = 200, primary_key = True)
336
337     def __unicode__(self):
338         return self.email
339
340     @classmethod
341     def is_optout(cls, email):
342         email = email.lower().strip()
343         return cls.objects.filter(email = email).count() > 0
344
345 class PatchChangeNotification(models.Model):
346     patch = models.ForeignKey(Patch, primary_key = True)
347     last_modified = models.DateTimeField(default = datetime.datetime.now)
348     orig_state = models.ForeignKey(State)
349
350 def _patch_change_callback(sender, instance, **kwargs):
351     # we only want notification of modified patches
352     if instance.pk is None:
353         return
354
355     if instance.project is None or not instance.project.send_notifications:
356         return
357
358     try:
359         orig_patch = Patch.objects.get(pk = instance.pk)
360     except Patch.DoesNotExist:
361         return
362
363     # If there's no interesting changes, abort without creating the
364     # notification
365     if orig_patch.state == instance.state:
366         return
367
368     notification = None
369     try:
370         notification = PatchChangeNotification.objects.get(patch = instance)
371     except PatchChangeNotification.DoesNotExist:
372         pass
373
374     if notification is None:
375         notification = PatchChangeNotification(patch = instance,
376                                                orig_state = orig_patch.state)
377
378     elif notification.orig_state == instance.state:
379         # If we're back at the original state, there is no need to notify
380         notification.delete()
381         return
382
383     notification.last_modified = datetime.datetime.now()
384     notification.save()
385
386 models.signals.pre_save.connect(_patch_change_callback, sender = Patch)