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