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.set_unixfrom('From patchwork ' + self.date.ctime())
275 def get_absolute_url(self):
276 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
279 verbose_name_plural = 'Patches'
281 unique_together = [('msgid', 'project')]
283 class Comment(models.Model):
284 patch = models.ForeignKey(Patch)
285 msgid = models.CharField(max_length=255)
286 submitter = models.ForeignKey(Person)
287 date = models.DateTimeField(default = datetime.datetime.now)
288 headers = models.TextField(blank = True)
289 content = models.TextField()
291 response_re = re.compile('^(Tested|Reviewed|Acked|Signed-off|Nacked)-by: .*$', re.M | re.I)
293 def patch_responses(self):
294 return ''.join([ match.group(0) + '\n' for match in \
295 self.response_re.finditer(self.content)])
299 unique_together = [('msgid', 'patch')]
301 class Bundle(models.Model):
302 owner = models.ForeignKey(User)
303 project = models.ForeignKey(Project)
304 name = models.CharField(max_length = 50, null = False, blank = False)
305 patches = models.ManyToManyField(Patch, through = 'BundlePatch')
306 public = models.BooleanField(default = False)
309 return self.patches.all().count()
311 def ordered_patches(self):
312 return self.patches.order_by('bundlepatch__order');
314 def append_patch(self, patch):
315 # todo: use the aggregate queries in django 1.1
316 orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
320 max_order = orders[0]['order']
324 # see if the patch is already in this bundle
325 if BundlePatch.objects.filter(bundle = self, patch = patch).count():
326 raise Exception("patch is already in bundle")
328 bp = BundlePatch.objects.create(bundle = self, patch = patch,
329 order = max_order + 1)
333 unique_together = [('owner', 'name')]
335 def public_url(self):
338 site = Site.objects.get_current()
339 return 'http://%s%s' % (site.domain,
340 reverse('patchwork.views.bundle.public',
342 'username': self.owner.username,
343 'bundlename': self.name
347 return '\n'.join([p.mbox().as_string(True) \
348 for p in self.ordered_patches()])
350 class BundlePatch(models.Model):
351 patch = models.ForeignKey(Patch)
352 bundle = models.ForeignKey(Bundle)
353 order = models.IntegerField()
356 unique_together = [('bundle', 'patch')]
359 class UserPersonConfirmation(models.Model):
360 user = models.ForeignKey(User)
361 email = models.CharField(max_length = 200)
363 date = models.DateTimeField(default=datetime.datetime.now)
364 active = models.BooleanField(default = True)
371 person = Person.objects.get(email__iexact = self.email)
375 person = Person(email = self.email)
377 person.link_to_user(self.user)
385 str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
386 self.key = self._meta.get_field('key').construct(str).hexdigest()
387 super(UserPersonConfirmation, self).save()