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()
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()
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')
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
86 def contributor_projects(self):
87 submitters = Person.objects.filter(user = self.user)
88 return Project.objects.filter(id__in =
90 submitter__in = submitters)
91 .values('project_id').query)
94 def sync_person(self):
97 def n_todo_patches(self):
98 return self.todo_patches().count()
100 def todo_patches(self, project = None):
102 # filter on project, if necessary
104 qs = Patch.objects.filter(project = project)
108 qs = qs.filter(archived = False) \
109 .filter(delegate = self.user) \
111 State.objects.filter(action_required = True)
115 def __unicode__(self):
118 def _user_saved_callback(sender, created, instance, **kwargs):
120 profile = instance.get_profile()
121 except UserProfile.DoesNotExist:
122 profile = UserProfile(user = instance)
125 models.signals.post_save.connect(_user_saved_callback, sender = User)
127 class State(models.Model):
128 name = models.CharField(max_length = 100)
129 ordering = models.IntegerField(unique = True)
130 action_required = models.BooleanField(default = True)
132 def __unicode__(self):
136 ordering = ['ordering']
138 class HashField(models.CharField):
139 __metaclass__ = models.SubfieldBase
141 def __init__(self, algorithm = 'sha1', *args, **kwargs):
142 self.algorithm = algorithm
145 def _construct(string = ''):
146 return hashlib.new(self.algorithm, string)
147 self.construct = _construct
148 self.n_bytes = len(hashlib.new(self.algorithm).hexdigest())
150 modules = { 'sha1': 'sha', 'md5': 'md5'}
152 if algorithm not in modules.keys():
153 raise NameError("Unknown algorithm '%s'" % algorithm)
155 self.construct = __import__(modules[algorithm]).new
157 self.n_bytes = len(self.construct().hexdigest())
159 kwargs['max_length'] = self.n_bytes
160 super(HashField, self).__init__(*args, **kwargs)
162 def db_type(self, connection=None):
163 return 'char(%d)' % self.n_bytes
165 def get_default_initial_patch_state():
166 return State.objects.get(ordering=0)
168 class Patch(models.Model):
169 project = models.ForeignKey(Project)
170 msgid = models.CharField(max_length=255)
171 name = models.CharField(max_length=255)
172 date = models.DateTimeField(default=datetime.datetime.now)
173 submitter = models.ForeignKey(Person)
174 delegate = models.ForeignKey(User, blank = True, null = True)
175 state = models.ForeignKey(State, default=get_default_initial_patch_state)
176 archived = models.BooleanField(default = False)
177 headers = models.TextField(blank = True)
178 content = models.TextField(null = True, blank = True)
179 pull_url = models.CharField(max_length=255, null = True, blank = True)
180 commit_ref = models.CharField(max_length=255, null = True, blank = True)
181 hash = HashField(null = True, blank = True)
183 def __unicode__(self):
187 return Comment.objects.filter(patch = self)
193 self.state = State.objects.get(ordering = 0)
195 if self.hash is None and self.content is not None:
196 self.hash = hash_patch(self.content).hexdigest()
198 super(Patch, self).save()
200 def is_editable(self, user):
201 if not user.is_authenticated():
204 if self.submitter.user == user or self.delegate == user:
207 return self.project.is_editable(user)
210 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
211 str = fname_re.sub('-', self.name)
212 return str.strip('-') + '.patch'
215 def get_absolute_url(self):
216 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
219 verbose_name_plural = 'Patches'
221 unique_together = [('msgid', 'project')]
223 class Comment(models.Model):
224 patch = models.ForeignKey(Patch)
225 msgid = models.CharField(max_length=255)
226 submitter = models.ForeignKey(Person)
227 date = models.DateTimeField(default = datetime.datetime.now)
228 headers = models.TextField(blank = True)
229 content = models.TextField()
231 response_re = re.compile( \
232 '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
235 def patch_responses(self):
236 return ''.join([ match.group(0) + '\n' for match in
237 self.response_re.finditer(self.content)])
241 unique_together = [('msgid', 'patch')]
243 class Bundle(models.Model):
244 owner = models.ForeignKey(User)
245 project = models.ForeignKey(Project)
246 name = models.CharField(max_length = 50, null = False, blank = False)
247 patches = models.ManyToManyField(Patch, through = 'BundlePatch')
248 public = models.BooleanField(default = False)
251 return self.patches.all().count()
253 def ordered_patches(self):
254 return self.patches.order_by('bundlepatch__order')
256 def append_patch(self, patch):
257 # todo: use the aggregate queries in django 1.1
258 orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
262 max_order = orders[0]['order']
266 # see if the patch is already in this bundle
267 if BundlePatch.objects.filter(bundle = self, patch = patch).count():
268 raise Exception("patch is already in bundle")
270 bp = BundlePatch.objects.create(bundle = self, patch = patch,
271 order = max_order + 1)
275 unique_together = [('owner', 'name')]
277 def public_url(self):
280 site = Site.objects.get_current()
281 return 'http://%s%s' % (site.domain,
282 reverse('patchwork.views.bundle.bundle',
284 'username': self.owner.username,
285 'bundlename': self.name
289 def get_absolute_url(self):
290 return ('patchwork.views.bundle.bundle', (), {
291 'username': self.owner.username,
292 'bundlename': self.name,
295 class BundlePatch(models.Model):
296 patch = models.ForeignKey(Patch)
297 bundle = models.ForeignKey(Bundle)
298 order = models.IntegerField()
301 unique_together = [('bundle', 'patch')]
304 class EmailConfirmation(models.Model):
305 validity = datetime.timedelta(days = settings.CONFIRMATION_VALIDITY_DAYS)
306 type = models.CharField(max_length = 20, choices = [
307 ('userperson', 'User-Person association'),
308 ('registration', 'Registration'),
309 ('optout', 'Email opt-out'),
311 email = models.CharField(max_length = 200)
312 user = models.ForeignKey(User, null = True)
314 date = models.DateTimeField(default = datetime.datetime.now)
315 active = models.BooleanField(default = True)
317 def deactivate(self):
322 return self.date + self.validity > datetime.datetime.now()
327 str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
328 self.key = self._meta.get_field('key').construct(str).hexdigest()
329 super(EmailConfirmation, self).save()
331 class EmailOptout(models.Model):
332 email = models.CharField(max_length = 200, primary_key = True)
334 def __unicode__(self):
338 def is_optout(cls, email):
339 email = email.lower().strip()
340 return cls.objects.filter(email = email).count() > 0
342 class PatchChangeNotification(models.Model):
343 patch = models.ForeignKey(Patch, primary_key = True)
344 last_modified = models.DateTimeField(default = datetime.datetime.now)
345 orig_state = models.ForeignKey(State)
347 def _patch_change_callback(sender, instance, **kwargs):
348 # we only want notification of modified patches
349 if instance.pk is None:
352 if instance.project is None or not instance.project.send_notifications:
356 orig_patch = Patch.objects.get(pk = instance.pk)
357 except Patch.DoesNotExist:
360 # If there's no interesting changes, abort without creating the
362 if orig_patch.state == instance.state:
367 notification = PatchChangeNotification.objects.get(patch = instance)
368 except PatchChangeNotification.DoesNotExist:
371 if notification is None:
372 notification = PatchChangeNotification(patch = instance,
373 orig_state = orig_patch.state)
375 elif notification.orig_state == instance.state:
376 # If we're back at the original state, there is no need to notify
377 notification.delete()
380 notification.last_modified = datetime.datetime.now()
383 models.signals.pre_save.connect(_patch_change_callback, sender = Patch)