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