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.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.profile.maintainer_projects.all()
69 ordering = ['linkname']
72 class UserProfile(models.Model):
73 user = models.OneToOneField(User, unique = True, related_name='profile')
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)
97 def sync_person(self):
100 def n_todo_patches(self):
101 return self.todo_patches().count()
103 def todo_patches(self, project = None):
105 # filter on project, if necessary
107 qs = Patch.objects.filter(project = project)
111 qs = qs.filter(archived = False) \
112 .filter(delegate = self.user) \
114 State.objects.filter(action_required = True)
118 def __unicode__(self):
121 def _user_saved_callback(sender, created, instance, **kwargs):
123 profile = instance.profile
124 except UserProfile.DoesNotExist:
125 profile = UserProfile(user = instance)
128 models.signals.post_save.connect(_user_saved_callback, sender = User)
130 class State(models.Model):
131 name = models.CharField(max_length = 100)
132 ordering = models.IntegerField(unique = True)
133 action_required = models.BooleanField(default = True)
135 def __unicode__(self):
139 ordering = ['ordering']
141 class HashField(models.CharField):
142 __metaclass__ = models.SubfieldBase
144 def __init__(self, algorithm = 'sha1', *args, **kwargs):
145 self.algorithm = algorithm
148 def _construct(string = ''):
149 return hashlib.new(self.algorithm, string)
150 self.construct = _construct
151 self.n_bytes = len(hashlib.new(self.algorithm).hexdigest())
153 modules = { 'sha1': 'sha', 'md5': 'md5'}
155 if algorithm not in modules.keys():
156 raise NameError("Unknown algorithm '%s'" % algorithm)
158 self.construct = __import__(modules[algorithm]).new
160 self.n_bytes = len(self.construct().hexdigest())
162 kwargs['max_length'] = self.n_bytes
163 super(HashField, self).__init__(*args, **kwargs)
165 def db_type(self, connection=None):
166 return 'char(%d)' % self.n_bytes
168 def get_default_initial_patch_state():
169 return State.objects.get(ordering=0)
171 class Patch(models.Model):
172 project = models.ForeignKey(Project)
173 msgid = models.CharField(max_length=255)
174 name = models.CharField(max_length=255)
175 date = models.DateTimeField(default=datetime.datetime.now)
176 submitter = models.ForeignKey(Person)
177 delegate = models.ForeignKey(User, blank = True, null = True)
178 state = models.ForeignKey(State, default=get_default_initial_patch_state)
179 archived = models.BooleanField(default = False)
180 headers = models.TextField(blank = True)
181 content = models.TextField(null = True, blank = True)
182 pull_url = models.CharField(max_length=255, null = True, blank = True)
183 commit_ref = models.CharField(max_length=255, null = True, blank = True)
184 hash = HashField(null = True, blank = True)
186 def __unicode__(self):
190 return Comment.objects.filter(patch = self)
196 self.state = State.objects.get(ordering = 0)
198 if self.hash is None and self.content is not None:
199 self.hash = hash_patch(self.content).hexdigest()
201 super(Patch, self).save()
203 def is_editable(self, user):
204 if not user.is_authenticated():
207 if self.submitter.user == user or self.delegate == user:
210 return self.project.is_editable(user)
213 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
214 str = fname_re.sub('-', self.name)
215 return str.strip('-') + '.patch'
218 def get_absolute_url(self):
219 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
222 verbose_name_plural = 'Patches'
224 unique_together = [('msgid', 'project')]
226 class Comment(models.Model):
227 patch = models.ForeignKey(Patch)
228 msgid = models.CharField(max_length=255)
229 submitter = models.ForeignKey(Person)
230 date = models.DateTimeField(default = datetime.datetime.now)
231 headers = models.TextField(blank = True)
232 content = models.TextField()
234 response_re = re.compile( \
235 '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
238 def patch_responses(self):
239 return ''.join([ match.group(0) + '\n' for match in
240 self.response_re.finditer(self.content)])
244 unique_together = [('msgid', 'patch')]
246 class Bundle(models.Model):
247 owner = models.ForeignKey(User)
248 project = models.ForeignKey(Project)
249 name = models.CharField(max_length = 50, null = False, blank = False)
250 patches = models.ManyToManyField(Patch, through = 'BundlePatch')
251 public = models.BooleanField(default = False)
254 return self.patches.all().count()
256 def ordered_patches(self):
257 return self.patches.order_by('bundlepatch__order')
259 def append_patch(self, patch):
260 # todo: use the aggregate queries in django 1.1
261 orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
265 max_order = orders[0]['order']
269 # see if the patch is already in this bundle
270 if BundlePatch.objects.filter(bundle = self, patch = patch).count():
271 raise Exception("patch is already in bundle")
273 bp = BundlePatch.objects.create(bundle = self, patch = patch,
274 order = max_order + 1)
278 unique_together = [('owner', 'name')]
280 def public_url(self):
283 site = Site.objects.get_current()
284 return 'http://%s%s' % (site.domain,
285 reverse('patchwork.views.bundle.bundle',
287 'username': self.owner.username,
288 'bundlename': self.name
292 def get_absolute_url(self):
293 return ('patchwork.views.bundle.bundle', (), {
294 'username': self.owner.username,
295 'bundlename': self.name,
298 class BundlePatch(models.Model):
299 patch = models.ForeignKey(Patch)
300 bundle = models.ForeignKey(Bundle)
301 order = models.IntegerField()
304 unique_together = [('bundle', 'patch')]
307 class EmailConfirmation(models.Model):
308 validity = datetime.timedelta(days = settings.CONFIRMATION_VALIDITY_DAYS)
309 type = models.CharField(max_length = 20, choices = [
310 ('userperson', 'User-Person association'),
311 ('registration', 'Registration'),
312 ('optout', 'Email opt-out'),
314 email = models.CharField(max_length = 200)
315 user = models.ForeignKey(User, null = True)
317 date = models.DateTimeField(default = datetime.datetime.now)
318 active = models.BooleanField(default = True)
320 def deactivate(self):
325 return self.date + self.validity > datetime.datetime.now()
330 str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
331 self.key = self._meta.get_field('key').construct(str).hexdigest()
332 super(EmailConfirmation, self).save()
334 class EmailOptout(models.Model):
335 email = models.CharField(max_length = 200, primary_key = True)
337 def __unicode__(self):
341 def is_optout(cls, email):
342 email = email.lower().strip()
343 return cls.objects.filter(email = email).count() > 0
345 class PatchChangeNotification(models.Model):
346 patch = models.ForeignKey(Patch, primary_key = True)
347 last_modified = models.DateTimeField(default = datetime.datetime.now)
348 orig_state = models.ForeignKey(State)
350 def _patch_change_callback(sender, instance, **kwargs):
351 # we only want notification of modified patches
352 if instance.pk is None:
355 if instance.project is None or not instance.project.send_notifications:
359 orig_patch = Patch.objects.get(pk = instance.pk)
360 except Patch.DoesNotExist:
363 # If there's no interesting changes, abort without creating the
365 if orig_patch.state == instance.state:
370 notification = PatchChangeNotification.objects.get(patch = instance)
371 except PatchChangeNotification.DoesNotExist:
374 if notification is None:
375 notification = PatchChangeNotification(patch = instance,
376 orig_state = orig_patch.state)
378 elif notification.orig_state == instance.state:
379 # If we're back at the original state, there is no need to notify
380 notification.delete()
383 notification.last_modified = datetime.datetime.now()
386 models.signals.pre_save.connect(_patch_change_callback, sender = Patch)