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)
116 super(UserProfile, self).save()
117 people = Person.objects.filter(email = self.user.email)
119 person = Person(email = self.user.email,
120 name = self.name(), user = self.user)
123 for person in people:
124 person.link_to_user(self.user)
127 def __unicode__(self):
130 def _user_saved_callback(sender, created, instance, **kwargs):
132 profile = instance.get_profile()
133 except UserProfile.DoesNotExist:
134 profile = UserProfile(user = instance)
137 models.signals.post_save.connect(_user_saved_callback, sender = User)
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)
144 def __unicode__(self):
148 ordering = ['ordering']
150 class HashField(models.CharField):
151 __metaclass__ = models.SubfieldBase
153 def __init__(self, algorithm = 'sha1', *args, **kwargs):
154 self.algorithm = algorithm
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())
162 modules = { 'sha1': 'sha', 'md5': 'md5'}
164 if algorithm not in modules.keys():
165 raise NameError("Unknown algorithm '%s'" % algorithm)
167 self.construct = __import__(modules[algorithm]).new
169 self.n_bytes = len(self.construct().hexdigest())
171 kwargs['max_length'] = self.n_bytes
172 super(HashField, self).__init__(*args, **kwargs)
174 def db_type(self, connection=None):
175 return 'char(%d)' % self.n_bytes
177 def get_default_initial_patch_state():
178 return State.objects.get(ordering=0)
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)
195 def __unicode__(self):
199 return Comment.objects.filter(patch = self)
205 self.state = State.objects.get(ordering = 0)
207 if self.hash is None and self.content is not None:
208 self.hash = hash_patch(self.content).hexdigest()
210 super(Patch, self).save()
212 def is_editable(self, user):
213 if not user.is_authenticated():
216 if self.submitter.user == user or self.delegate == user:
219 return self.project.is_editable(user)
222 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
223 str = fname_re.sub('-', self.name)
224 return str.strip('-') + '.patch'
227 def get_absolute_url(self):
228 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
231 verbose_name_plural = 'Patches'
233 unique_together = [('msgid', 'project')]
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()
243 response_re = re.compile( \
244 '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
247 def patch_responses(self):
248 return ''.join([ match.group(0) + '\n' for match in
249 self.response_re.finditer(self.content)])
253 unique_together = [('msgid', 'patch')]
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)
263 return self.patches.all().count()
265 def ordered_patches(self):
266 return self.patches.order_by('bundlepatch__order')
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') \
274 max_order = orders[0]['order']
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")
282 bp = BundlePatch.objects.create(bundle = self, patch = patch,
283 order = max_order + 1)
287 unique_together = [('owner', 'name')]
289 def public_url(self):
292 site = Site.objects.get_current()
293 return 'http://%s%s' % (site.domain,
294 reverse('patchwork.views.bundle.bundle',
296 'username': self.owner.username,
297 'bundlename': self.name
301 def get_absolute_url(self):
302 return ('patchwork.views.bundle.bundle', (), {
303 'username': self.owner.username,
304 'bundlename': self.name,
307 class BundlePatch(models.Model):
308 patch = models.ForeignKey(Patch)
309 bundle = models.ForeignKey(Bundle)
310 order = models.IntegerField()
313 unique_together = [('bundle', 'patch')]
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'),
323 email = models.CharField(max_length = 200)
324 user = models.ForeignKey(User, null = True)
326 date = models.DateTimeField(default = datetime.datetime.now)
327 active = models.BooleanField(default = True)
329 def deactivate(self):
334 return self.date + self.validity > datetime.datetime.now()
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()
343 class EmailOptout(models.Model):
344 email = models.CharField(max_length = 200, primary_key = True)
346 def __unicode__(self):
350 def is_optout(cls, email):
351 email = email.lower().strip()
352 return cls.objects.filter(email = email).count() > 0
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)
359 def _patch_change_callback(sender, instance, **kwargs):
360 # we only want notification of modified patches
361 if instance.pk is None:
364 if instance.project is None or not instance.project.send_notifications:
368 orig_patch = Patch.objects.get(pk = instance.pk)
369 except Patch.DoesNotExist:
372 # If there's no interesting changes, abort without creating the
374 if orig_patch.state == instance.state:
379 notification = PatchChangeNotification.objects.get(patch = instance)
380 except PatchChangeNotification.DoesNotExist:
383 if notification is None:
384 notification = PatchChangeNotification(patch = instance,
385 orig_state = orig_patch.state)
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()
392 notification.last_modified = datetime.datetime.now()
395 models.signals.pre_save.connect(_patch_change_callback, sender = Patch)