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
33 from email.mime.nonmultipart import MIMENonMultipart
34 from email.encoders import encode_7or8bit
37 # Python 2.4 compatibility
38 from email.MIMENonMultipart import MIMENonMultipart
39 from email.Encoders import encode_7or8bit
41 email.utils = email.Utils
43 class Person(models.Model):
44 email = models.CharField(max_length=255, unique = True)
45 name = models.CharField(max_length=255, null = True)
46 user = models.ForeignKey(User, null = True)
50 return '%s <%s>' % (self.name, self.email)
54 def link_to_user(self, user):
55 self.name = user.get_profile().name()
59 verbose_name_plural = 'People'
61 class Project(models.Model):
62 linkname = models.CharField(max_length=255, unique=True)
63 name = models.CharField(max_length=255, unique=True)
64 listid = models.CharField(max_length=255, unique=True)
65 listemail = models.CharField(max_length=200)
70 class UserProfile(models.Model):
71 user = models.ForeignKey(User, unique = True)
72 primary_project = models.ForeignKey(Project, null = True)
73 maintainer_projects = models.ManyToManyField(Project,
74 related_name = 'maintainer_project')
75 send_email = models.BooleanField(default = False,
76 help_text = 'Selecting this option allows patchwork to send ' +
77 'email on your behalf')
78 patches_per_page = models.PositiveIntegerField(default = 100,
79 null = False, blank = False,
80 help_text = 'Number of patches to display per page')
83 if self.user.first_name or self.user.last_name:
84 names = filter(bool, [self.user.first_name, self.user.last_name])
85 return ' '.join(names)
86 return self.user.username
88 def contributor_projects(self):
89 submitters = Person.objects.filter(user = self.user)
90 return Project.objects \
93 submitter__in = submitters) \
94 .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) \
113 .filter(state__in = \
114 State.objects.filter(action_required = True) \
119 super(UserProfile, self).save()
120 people = Person.objects.filter(email = self.user.email)
122 person = Person(email = self.user.email,
123 name = self.name(), user = self.user)
126 for person in people:
127 person.link_to_user(self.user)
133 class State(models.Model):
134 name = models.CharField(max_length = 100)
135 ordering = models.IntegerField(unique = True)
136 action_required = models.BooleanField(default = True)
142 ordering = ['ordering']
144 class HashField(models.CharField):
145 __metaclass__ = models.SubfieldBase
147 def __init__(self, algorithm = 'sha1', *args, **kwargs):
148 self.algorithm = algorithm
151 def _construct(string = ''):
152 return hashlib.new(self.algorithm, string)
153 self.construct = _construct
154 self.n_bytes = len(hashlib.new(self.algorithm).hexdigest())
156 modules = { 'sha1': 'sha', 'md5': 'md5'}
158 if algorithm not in modules.keys():
159 raise NameError("Unknown algorithm '%s'" % algorithm)
161 self.construct = __import__(modules[algorithm]).new
163 self.n_bytes = len(self.construct().hexdigest())
165 kwargs['max_length'] = self.n_bytes
166 super(HashField, self).__init__(*args, **kwargs)
169 return 'char(%d)' % self.n_bytes
171 class PatchMbox(MIMENonMultipart):
172 patch_charset = 'utf-8'
173 def __init__(self, _text):
174 MIMENonMultipart.__init__(self, 'text', 'plain',
175 **{'charset': self.patch_charset})
176 self.set_payload(_text.encode(self.patch_charset))
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)
187 archived = models.BooleanField(default = False)
188 headers = models.TextField(blank = True)
189 content = models.TextField()
190 commit_ref = models.CharField(max_length=255, null = True, blank = True)
191 hash = HashField(null = True, db_index = True)
197 return Comment.objects.filter(patch = self)
203 self.state = State.objects.get(ordering = 0)
205 if self.hash is None:
206 self.hash = hash_patch(self.content).hexdigest()
208 super(Patch, self).save()
210 def is_editable(self, user):
211 if not user.is_authenticated():
214 if self.submitter.user == user or self.delegate == user:
217 profile = user.get_profile()
218 return self.project in user.get_profile().maintainer_projects.all()
221 f = PatchForm(instance = self, prefix = self.id)
225 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
226 str = fname_re.sub('-', self.name)
227 return str.strip('-') + '.patch'
230 postscript_re = re.compile('\n-{2,3} ?\n')
234 comment = Comment.objects.get(patch = self, msgid = self.msgid)
240 body = comment.content.strip() + "\n"
242 parts = postscript_re.split(body, 1)
244 (body, postscript) = parts
245 body = body.strip() + "\n"
246 postscript = postscript.strip() + "\n"
251 for comment in Comment.objects.filter(patch = self) \
252 .exclude(msgid = self.msgid):
253 body += comment.patch_responses()
259 body += '---\n' + postscript.strip() + '\n'
261 body += '\n' + self.content
263 mail = PatchMbox(body)
264 mail['Subject'] = self.name
265 mail['Date'] = email.utils.formatdate(
266 time.mktime(self.date.utctimetuple()))
267 mail['From'] = unicode(self.submitter)
268 mail['X-Patchwork-Id'] = str(self.id)
269 mail['Message-Id'] = self.msgid
270 mail.set_unixfrom('From patchwork ' + self.date.ctime())
276 def get_absolute_url(self):
277 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
280 verbose_name_plural = 'Patches'
282 unique_together = [('msgid', 'project')]
284 class Comment(models.Model):
285 patch = models.ForeignKey(Patch)
286 msgid = models.CharField(max_length=255)
287 submitter = models.ForeignKey(Person)
288 date = models.DateTimeField(default = datetime.datetime.now)
289 headers = models.TextField(blank = True)
290 content = models.TextField()
292 response_re = re.compile('^(Tested|Reviewed|Acked|Signed-off|Nacked)-by: .*$', re.M | re.I)
294 def patch_responses(self):
295 return ''.join([ match.group(0) + '\n' for match in \
296 self.response_re.finditer(self.content)])
300 unique_together = [('msgid', 'patch')]
302 class Bundle(models.Model):
303 owner = models.ForeignKey(User)
304 project = models.ForeignKey(Project)
305 name = models.CharField(max_length = 50, null = False, blank = False)
306 patches = models.ManyToManyField(Patch, through = 'BundlePatch')
307 public = models.BooleanField(default = False)
310 return self.patches.all().count()
312 def ordered_patches(self):
313 return self.patches.order_by('bundlepatch__order');
315 def append_patch(self, patch):
316 # todo: use the aggregate queries in django 1.1
317 orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
321 max_order = orders[0]['order']
325 # see if the patch is already in this bundle
326 if BundlePatch.objects.filter(bundle = self, patch = patch).count():
327 raise Exception("patch is already in bundle")
329 bp = BundlePatch.objects.create(bundle = self, patch = patch,
330 order = max_order + 1)
334 unique_together = [('owner', 'name')]
336 def public_url(self):
339 site = Site.objects.get_current()
340 return 'http://%s%s' % (site.domain,
341 reverse('patchwork.views.bundle.public',
343 'username': self.owner.username,
344 'bundlename': self.name
348 return '\n'.join([p.mbox().as_string(True) \
349 for p in self.ordered_patches()])
351 class BundlePatch(models.Model):
352 patch = models.ForeignKey(Patch)
353 bundle = models.ForeignKey(Bundle)
354 order = models.IntegerField()
357 unique_together = [('bundle', 'patch')]
360 class UserPersonConfirmation(models.Model):
361 user = models.ForeignKey(User)
362 email = models.CharField(max_length = 200)
364 date = models.DateTimeField(default=datetime.datetime.now)
365 active = models.BooleanField(default = True)
372 person = Person.objects.get(email__iexact = self.email)
376 person = Person(email = self.email)
378 person.link_to_user(self.user)
386 str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
387 self.key = self._meta.get_field('key').construct(str).hexdigest()
388 super(UserPersonConfirmation, self).save()