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
32 from email.mime.nonmultipart import MIMENonMultipart
33 from email.encoders import encode_7or8bit
34 from email.parser import HeaderParser
37 # Python 2.4 compatibility
38 from email.MIMENonMultipart import MIMENonMultipart
39 from email.Encoders import encode_7or8bit
40 from email.Parser import HeaderParser
42 email.utils = email.Utils
44 class Person(models.Model):
45 email = models.CharField(max_length=255, unique = True)
46 name = models.CharField(max_length=255, null = True, blank = True)
47 user = models.ForeignKey(User, null = True, blank = True)
49 def __unicode__(self):
51 return u'%s <%s>' % (self.name, self.email)
55 def link_to_user(self, user):
56 self.name = user.get_profile().name()
60 verbose_name_plural = 'People'
62 class Project(models.Model):
63 linkname = models.CharField(max_length=255, unique=True)
64 name = models.CharField(max_length=255, unique=True)
65 listid = models.CharField(max_length=255, unique=True)
66 listemail = models.CharField(max_length=200)
68 def __unicode__(self):
71 def is_editable(self, user):
72 if not user.is_authenticated():
74 return self in user.get_profile().maintainer_projects.all()
76 class UserProfile(models.Model):
77 user = models.ForeignKey(User, unique = True)
78 primary_project = models.ForeignKey(Project, null = True, blank = True)
79 maintainer_projects = models.ManyToManyField(Project,
80 related_name = 'maintainer_project')
81 send_email = models.BooleanField(default = False,
82 help_text = 'Selecting this option allows patchwork to send ' +
83 'email on your behalf')
84 patches_per_page = models.PositiveIntegerField(default = 100,
85 null = False, blank = False,
86 help_text = 'Number of patches to display per page')
89 if self.user.first_name or self.user.last_name:
90 names = filter(bool, [self.user.first_name, self.user.last_name])
91 return u' '.join(names)
92 return self.user.username
94 def contributor_projects(self):
95 submitters = Person.objects.filter(user = self.user)
96 return Project.objects.filter(id__in =
98 submitter__in = submitters)
99 .values('project_id').query)
102 def sync_person(self):
105 def n_todo_patches(self):
106 return self.todo_patches().count()
108 def todo_patches(self, project = None):
110 # filter on project, if necessary
112 qs = Patch.objects.filter(project = project)
116 qs = qs.filter(archived = False) \
117 .filter(delegate = self.user) \
119 State.objects.filter(action_required = True)
124 super(UserProfile, self).save()
125 people = Person.objects.filter(email = self.user.email)
127 person = Person(email = self.user.email,
128 name = self.name(), user = self.user)
131 for person in people:
132 person.link_to_user(self.user)
135 def __unicode__(self):
138 def _user_created_callback(sender, created, instance, **kwargs):
141 profile = UserProfile(user = instance)
144 models.signals.post_save.connect(_user_created_callback, sender = User)
146 class State(models.Model):
147 name = models.CharField(max_length = 100)
148 ordering = models.IntegerField(unique = True)
149 action_required = models.BooleanField(default = True)
151 def __unicode__(self):
155 ordering = ['ordering']
157 class HashField(models.CharField):
158 __metaclass__ = models.SubfieldBase
160 def __init__(self, algorithm = 'sha1', *args, **kwargs):
161 self.algorithm = algorithm
164 def _construct(string = ''):
165 return hashlib.new(self.algorithm, string)
166 self.construct = _construct
167 self.n_bytes = len(hashlib.new(self.algorithm).hexdigest())
169 modules = { 'sha1': 'sha', 'md5': 'md5'}
171 if algorithm not in modules.keys():
172 raise NameError("Unknown algorithm '%s'" % algorithm)
174 self.construct = __import__(modules[algorithm]).new
176 self.n_bytes = len(self.construct().hexdigest())
178 kwargs['max_length'] = self.n_bytes
179 super(HashField, self).__init__(*args, **kwargs)
182 return 'char(%d)' % self.n_bytes
184 class PatchMbox(MIMENonMultipart):
185 patch_charset = 'utf-8'
186 def __init__(self, _text):
187 MIMENonMultipart.__init__(self, 'text', 'plain',
188 **{'charset': self.patch_charset})
189 self.set_payload(_text.encode(self.patch_charset))
192 class Patch(models.Model):
193 project = models.ForeignKey(Project)
194 msgid = models.CharField(max_length=255)
195 name = models.CharField(max_length=255)
196 date = models.DateTimeField(default=datetime.datetime.now)
197 submitter = models.ForeignKey(Person)
198 delegate = models.ForeignKey(User, blank = True, null = True)
199 state = models.ForeignKey(State)
200 archived = models.BooleanField(default = False)
201 headers = models.TextField(blank = True)
202 content = models.TextField(null = True, blank = True)
203 pull_url = models.CharField(max_length=255, null = True, blank = True)
204 commit_ref = models.CharField(max_length=255, null = True, blank = True)
205 hash = HashField(null = True, blank = True)
207 def __unicode__(self):
211 return Comment.objects.filter(patch = self)
217 self.state = State.objects.get(ordering = 0)
219 if self.hash is None and self.content is not None:
220 self.hash = hash_patch(self.content).hexdigest()
222 super(Patch, self).save()
224 def is_editable(self, user):
225 if not user.is_authenticated():
228 if self.submitter.user == user or self.delegate == user:
231 return self.project.is_editable(user)
234 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
235 str = fname_re.sub('-', self.name)
236 return str.strip('-') + '.patch'
239 postscript_re = re.compile('\n-{2,3} ?\n')
243 comment = Comment.objects.get(patch = self, msgid = self.msgid)
249 body = comment.content.strip() + "\n"
251 parts = postscript_re.split(body, 1)
253 (body, postscript) = parts
254 body = body.strip() + "\n"
255 postscript = postscript.strip() + "\n"
259 for comment in Comment.objects.filter(patch = self) \
260 .exclude(msgid = self.msgid):
261 body += comment.patch_responses()
267 body += '---\n' + postscript.strip() + '\n'
270 body += '\n' + self.content
272 mail = PatchMbox(body)
273 mail['Subject'] = self.name
274 mail['Date'] = email.utils.formatdate(
275 time.mktime(self.date.utctimetuple()))
276 mail['From'] = unicode(self.submitter)
277 mail['X-Patchwork-Id'] = str(self.id)
278 mail['Message-Id'] = self.msgid
279 mail.set_unixfrom('From patchwork ' + self.date.ctime())
282 copied_headers = ['To', 'Cc']
283 orig_headers = HeaderParser().parsestr(str(self.headers))
284 for header in copied_headers:
285 if header in orig_headers:
286 mail[header] = orig_headers[header]
291 def get_absolute_url(self):
292 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
295 verbose_name_plural = 'Patches'
297 unique_together = [('msgid', 'project')]
299 class Comment(models.Model):
300 patch = models.ForeignKey(Patch)
301 msgid = models.CharField(max_length=255)
302 submitter = models.ForeignKey(Person)
303 date = models.DateTimeField(default = datetime.datetime.now)
304 headers = models.TextField(blank = True)
305 content = models.TextField()
307 response_re = re.compile( \
308 '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
311 def patch_responses(self):
312 return ''.join([ match.group(0) + '\n' for match in
313 self.response_re.finditer(self.content)])
317 unique_together = [('msgid', 'patch')]
319 class Bundle(models.Model):
320 owner = models.ForeignKey(User)
321 project = models.ForeignKey(Project)
322 name = models.CharField(max_length = 50, null = False, blank = False)
323 patches = models.ManyToManyField(Patch, through = 'BundlePatch')
324 public = models.BooleanField(default = False)
327 return self.patches.all().count()
329 def ordered_patches(self):
330 return self.patches.order_by('bundlepatch__order')
332 def append_patch(self, patch):
333 # todo: use the aggregate queries in django 1.1
334 orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
338 max_order = orders[0]['order']
342 # see if the patch is already in this bundle
343 if BundlePatch.objects.filter(bundle = self, patch = patch).count():
344 raise Exception("patch is already in bundle")
346 bp = BundlePatch.objects.create(bundle = self, patch = patch,
347 order = max_order + 1)
351 unique_together = [('owner', 'name')]
353 def public_url(self):
356 site = Site.objects.get_current()
357 return 'http://%s%s' % (site.domain,
358 reverse('patchwork.views.bundle.public',
360 'username': self.owner.username,
361 'bundlename': self.name
365 return '\n'.join([p.mbox().as_string(True)
366 for p in self.ordered_patches()])
368 class BundlePatch(models.Model):
369 patch = models.ForeignKey(Patch)
370 bundle = models.ForeignKey(Bundle)
371 order = models.IntegerField()
374 unique_together = [('bundle', 'patch')]
377 class EmailConfirmation(models.Model):
378 validity = datetime.timedelta(days = settings.CONFIRMATION_VALIDITY_DAYS)
379 type = models.CharField(max_length = 20, choices = [
380 ('userperson', 'User-Person association'),
381 ('registration', 'Registration'),
382 ('optout', 'Email opt-out'),
384 email = models.CharField(max_length = 200)
385 user = models.ForeignKey(User, null = True)
387 date = models.DateTimeField(default = datetime.datetime.now)
388 active = models.BooleanField(default = True)
390 def deactivate(self):
395 return self.date + self.validity > datetime.datetime.now()
400 str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
401 self.key = self._meta.get_field('key').construct(str).hexdigest()
402 super(EmailConfirmation, self).save()
404 class EmailOptout(models.Model):
405 email = models.CharField(max_length = 200, primary_key = True)
407 def __unicode__(self):