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