1 # Patchwork - automated patch tracking system
2 # Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
4 # This file is part of the Patchwork package.
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.
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.
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
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
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)
37 def __unicode__(self):
39 return u'%s <%s>' % (self.name, self.email)
43 def link_to_user(self, user):
44 self.name = user.get_profile().name()
48 verbose_name_plural = 'People'
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)
60 def __unicode__(self):
63 def is_editable(self, user):
64 if not user.is_authenticated():
66 return self in user.get_profile().maintainer_projects.all()
69 ordering = ['linkname']
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')
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
90 def contributor_projects(self):
91 submitters = Person.objects.filter(user = self.user)
92 return Project.objects.filter(id__in =
94 submitter__in = submitters)
95 .values('project_id').query)
98 def sync_person(self):
101 def n_todo_patches(self):
102 return self.todo_patches().count()
104 def todo_patches(self, project = None):
106 # filter on project, if necessary
108 qs = Patch.objects.filter(project = project)
112 qs = qs.filter(archived = False) \
113 .filter(delegate = self.user) \
115 State.objects.filter(action_required = True)
119 def __unicode__(self):
122 def _user_saved_callback(sender, created, instance, **kwargs):
124 profile = instance.get_profile()
125 except UserProfile.DoesNotExist:
126 profile = UserProfile(user = instance)
129 models.signals.post_save.connect(_user_saved_callback, sender = User)
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)
136 def __unicode__(self):
140 ordering = ['ordering']
142 class HashField(models.CharField):
143 __metaclass__ = models.SubfieldBase
145 def __init__(self, algorithm = 'sha1', *args, **kwargs):
146 self.algorithm = algorithm
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())
154 modules = { 'sha1': 'sha', 'md5': 'md5'}
156 if algorithm not in modules.keys():
157 raise NameError("Unknown algorithm '%s'" % algorithm)
159 self.construct = __import__(modules[algorithm]).new
161 self.n_bytes = len(self.construct().hexdigest())
163 kwargs['max_length'] = self.n_bytes
164 super(HashField, self).__init__(*args, **kwargs)
166 def db_type(self, connection=None):
167 return 'char(%d)' % self.n_bytes
169 def get_default_initial_patch_state():
170 return State.objects.get(ordering=0)
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)
187 def __unicode__(self):
191 return Comment.objects.filter(patch = self)
197 self.state = State.objects.get(ordering = 0)
199 if self.hash is None and self.content is not None:
200 self.hash = hash_patch(self.content).hexdigest()
202 super(Patch, self).save()
204 def is_editable(self, user):
205 if not user.is_authenticated():
208 if self.submitter.user == user or self.delegate == user:
211 return self.project.is_editable(user)
214 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
215 str = fname_re.sub('-', self.name)
216 return str.strip('-') + '.patch'
219 def get_absolute_url(self):
220 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
223 verbose_name_plural = 'Patches'
225 unique_together = [('msgid', 'project')]
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()
235 response_re = re.compile( \
236 '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
239 def patch_responses(self):
240 return ''.join([ match.group(0) + '\n' for match in
241 self.response_re.finditer(self.content)])
245 unique_together = [('msgid', 'patch')]
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)
255 return self.patches.all().count()
257 def ordered_patches(self):
258 return self.patches.order_by('bundlepatch__order')
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') \
266 max_order = orders[0]['order']
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")
274 bp = BundlePatch.objects.create(bundle = self, patch = patch,
275 order = max_order + 1)
279 unique_together = [('owner', 'name')]
281 def public_url(self):
284 site = Site.objects.get_current()
285 return 'http://%s%s' % (site.domain,
286 reverse('patchwork.views.bundle.bundle',
288 'username': self.owner.username,
289 'bundlename': self.name
293 def get_absolute_url(self):
294 return ('patchwork.views.bundle.bundle', (), {
295 'username': self.owner.username,
296 'bundlename': self.name,
299 class BundlePatch(models.Model):
300 patch = models.ForeignKey(Patch)
301 bundle = models.ForeignKey(Bundle)
302 order = models.IntegerField()
305 unique_together = [('bundle', 'patch')]
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'),
315 email = models.CharField(max_length = 200)
316 user = models.ForeignKey(User, null = True)
318 date = models.DateTimeField(default = datetime.datetime.now)
319 active = models.BooleanField(default = True)
321 def deactivate(self):
326 return self.date + self.validity > datetime.datetime.now()
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()
335 class EmailOptout(models.Model):
336 email = models.CharField(max_length = 200, primary_key = True)
338 def __unicode__(self):
342 def is_optout(cls, email):
343 email = email.lower().strip()
344 return cls.objects.filter(email = email).count() > 0
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)
351 def _patch_change_callback(sender, instance, **kwargs):
352 # we only want notification of modified patches
353 if instance.pk is None:
356 if instance.project is None or not instance.project.send_notifications:
360 orig_patch = Patch.objects.get(pk = instance.pk)
361 except Patch.DoesNotExist:
364 # If there's no interesting changes, abort without creating the
366 if orig_patch.state == instance.state:
371 notification = PatchChangeNotification.objects.get(patch = instance)
372 except PatchChangeNotification.DoesNotExist:
375 if notification is None:
376 notification = PatchChangeNotification(patch = instance,
377 orig_state = orig_patch.state)
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()
384 notification.last_modified = datetime.datetime.now()
387 models.signals.pre_save.connect(_patch_change_callback, sender = Patch)