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)
36 def __unicode__(self):
38 return u'%s <%s>' % (self.name, self.email)
42 def link_to_user(self, user):
43 self.name = user.get_profile().name()
47 verbose_name_plural = 'People'
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()
59 def __unicode__(self):
62 def is_editable(self, user):
63 if not user.is_authenticated():
65 return self in user.get_profile().maintainer_projects.all()
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')
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
85 def contributor_projects(self):
86 submitters = Person.objects.filter(user = self.user)
87 return Project.objects.filter(id__in =
89 submitter__in = submitters)
90 .values('project_id').query)
93 def sync_person(self):
96 def n_todo_patches(self):
97 return self.todo_patches().count()
99 def todo_patches(self, project = None):
101 # filter on project, if necessary
103 qs = Patch.objects.filter(project = project)
107 qs = qs.filter(archived = False) \
108 .filter(delegate = self.user) \
110 State.objects.filter(action_required = True)
115 super(UserProfile, self).save()
116 people = Person.objects.filter(email = self.user.email)
118 person = Person(email = self.user.email,
119 name = self.name(), user = self.user)
122 for person in people:
123 person.link_to_user(self.user)
126 def __unicode__(self):
129 def _user_saved_callback(sender, created, instance, **kwargs):
131 profile = instance.get_profile()
132 except UserProfile.DoesNotExist:
133 profile = UserProfile(user = instance)
136 models.signals.post_save.connect(_user_saved_callback, sender = User)
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)
143 def __unicode__(self):
147 ordering = ['ordering']
149 class HashField(models.CharField):
150 __metaclass__ = models.SubfieldBase
152 def __init__(self, algorithm = 'sha1', *args, **kwargs):
153 self.algorithm = algorithm
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())
161 modules = { 'sha1': 'sha', 'md5': 'md5'}
163 if algorithm not in modules.keys():
164 raise NameError("Unknown algorithm '%s'" % algorithm)
166 self.construct = __import__(modules[algorithm]).new
168 self.n_bytes = len(self.construct().hexdigest())
170 kwargs['max_length'] = self.n_bytes
171 super(HashField, self).__init__(*args, **kwargs)
173 def db_type(self, connection=None):
174 return 'char(%d)' % self.n_bytes
176 def get_default_initial_patch_state():
177 return State.objects.get(ordering=0)
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)
194 def __unicode__(self):
198 return Comment.objects.filter(patch = self)
204 self.state = State.objects.get(ordering = 0)
206 if self.hash is None and self.content is not None:
207 self.hash = hash_patch(self.content).hexdigest()
209 super(Patch, self).save()
211 def is_editable(self, user):
212 if not user.is_authenticated():
215 if self.submitter.user == user or self.delegate == user:
218 return self.project.is_editable(user)
221 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
222 str = fname_re.sub('-', self.name)
223 return str.strip('-') + '.patch'
226 def get_absolute_url(self):
227 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
230 verbose_name_plural = 'Patches'
232 unique_together = [('msgid', 'project')]
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()
242 response_re = re.compile( \
243 '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
246 def patch_responses(self):
247 return ''.join([ match.group(0) + '\n' for match in
248 self.response_re.finditer(self.content)])
252 unique_together = [('msgid', 'patch')]
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)
262 return self.patches.all().count()
264 def ordered_patches(self):
265 return self.patches.order_by('bundlepatch__order')
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') \
273 max_order = orders[0]['order']
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")
281 bp = BundlePatch.objects.create(bundle = self, patch = patch,
282 order = max_order + 1)
286 unique_together = [('owner', 'name')]
288 def public_url(self):
291 site = Site.objects.get_current()
292 return 'http://%s%s' % (site.domain,
293 reverse('patchwork.views.bundle.bundle',
295 'username': self.owner.username,
296 'bundlename': self.name
300 def get_absolute_url(self):
301 return ('patchwork.views.bundle.bundle', (), {
302 'username': self.owner.username,
303 'bundlename': self.name,
306 class BundlePatch(models.Model):
307 patch = models.ForeignKey(Patch)
308 bundle = models.ForeignKey(Bundle)
309 order = models.IntegerField()
312 unique_together = [('bundle', 'patch')]
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'),
322 email = models.CharField(max_length = 200)
323 user = models.ForeignKey(User, null = True)
325 date = models.DateTimeField(default = datetime.datetime.now)
326 active = models.BooleanField(default = True)
328 def deactivate(self):
333 return self.date + self.validity > datetime.datetime.now()
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()
342 class EmailOptout(models.Model):
343 email = models.CharField(max_length = 200, primary_key = True)
345 def __unicode__(self):
349 def is_optout(cls, email):
350 email = email.lower().strip()
351 return cls.objects.filter(email = email).count() > 0
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)
358 def _patch_change_callback(sender, instance, **kwargs):
359 # we only want notification of modified patches
360 if instance.pk is None:
363 if instance.project is None or not instance.project.send_notifications:
367 orig_patch = Patch.objects.get(pk = instance.pk)
368 except Patch.DoesNotExist:
371 # If there's no interesting changes, abort without creating the
373 if orig_patch.state == instance.state:
378 notification = PatchChangeNotification.objects.get(patch = instance)
379 except PatchChangeNotification.DoesNotExist:
382 if notification is None:
383 notification = PatchChangeNotification(patch = instance,
384 orig_state = orig_patch.state)
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()
391 notification.last_modified = datetime.datetime.now()
394 models.signals.pre_save.connect(_patch_change_callback, sender = Patch)