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
35 from email.parser import HeaderParser
38 # Python 2.4 compatibility
39 from email.MIMENonMultipart import MIMENonMultipart
40 from email.Encoders import encode_7or8bit
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)
47 user = models.ForeignKey(User, null = True)
51 return '%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)
71 class UserProfile(models.Model):
72 user = models.ForeignKey(User, unique = True)
73 primary_project = models.ForeignKey(Project, null = True)
74 maintainer_projects = models.ManyToManyField(Project,
75 related_name = 'maintainer_project')
76 send_email = models.BooleanField(default = False,
77 help_text = 'Selecting this option allows patchwork to send ' +
78 'email on your behalf')
79 patches_per_page = models.PositiveIntegerField(default = 100,
80 null = False, blank = False,
81 help_text = 'Number of patches to display per page')
84 if self.user.first_name or self.user.last_name:
85 names = filter(bool, [self.user.first_name, self.user.last_name])
86 return ' '.join(names)
87 return self.user.username
89 def contributor_projects(self):
90 submitters = Person.objects.filter(user = self.user)
91 return Project.objects \
94 submitter__in = submitters) \
95 .values('project_id').query)
98 def sync_person(self):
101 def n_todo_patches(self):
102 return self.todo_patches().count()
104 def todo_patches(self, project = None):
106 # filter on project, if necessary
108 qs = Patch.objects.filter(project = project)
112 qs = qs.filter(archived = False) \
113 .filter(delegate = self.user) \
114 .filter(state__in = \
115 State.objects.filter(action_required = True) \
120 super(UserProfile, self).save()
121 people = Person.objects.filter(email = self.user.email)
123 person = Person(email = self.user.email,
124 name = self.name(), user = self.user)
127 for person in people:
128 person.link_to_user(self.user)
134 class State(models.Model):
135 name = models.CharField(max_length = 100)
136 ordering = models.IntegerField(unique = True)
137 action_required = models.BooleanField(default = True)
143 ordering = ['ordering']
145 class HashField(models.CharField):
146 __metaclass__ = models.SubfieldBase
148 def __init__(self, algorithm = 'sha1', *args, **kwargs):
149 self.algorithm = algorithm
152 def _construct(string = ''):
153 return hashlib.new(self.algorithm, string)
154 self.construct = _construct
155 self.n_bytes = len(hashlib.new(self.algorithm).hexdigest())
157 modules = { 'sha1': 'sha', 'md5': 'md5'}
159 if algorithm not in modules.keys():
160 raise NameError("Unknown algorithm '%s'" % algorithm)
162 self.construct = __import__(modules[algorithm]).new
164 self.n_bytes = len(self.construct().hexdigest())
166 kwargs['max_length'] = self.n_bytes
167 super(HashField, self).__init__(*args, **kwargs)
170 return 'char(%d)' % self.n_bytes
172 class PatchMbox(MIMENonMultipart):
173 patch_charset = 'utf-8'
174 def __init__(self, _text):
175 MIMENonMultipart.__init__(self, 'text', 'plain',
176 **{'charset': self.patch_charset})
177 self.set_payload(_text.encode(self.patch_charset))
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)
188 archived = models.BooleanField(default = False)
189 headers = models.TextField(blank = True)
190 content = models.TextField()
191 commit_ref = models.CharField(max_length=255, null = True, blank = True)
192 hash = HashField(null = True, db_index = True)
198 return Comment.objects.filter(patch = self)
204 self.state = State.objects.get(ordering = 0)
206 if self.hash is None:
207 self.hash = hash_patch(self.content).hexdigest()
209 super(Patch, self).save()
211 def is_editable(self, user):
212 if not user.is_authenticated():
215 if self.submitter.user == user or self.delegate == user:
218 profile = user.get_profile()
219 return self.project in user.get_profile().maintainer_projects.all()
222 f = PatchForm(instance = self, prefix = self.id)
226 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
227 str = fname_re.sub('-', self.name)
228 return str.strip('-') + '.patch'
231 postscript_re = re.compile('\n-{2,3} ?\n')
235 comment = Comment.objects.get(patch = self, msgid = self.msgid)
241 body = comment.content.strip() + "\n"
243 parts = postscript_re.split(body, 1)
245 (body, postscript) = parts
246 body = body.strip() + "\n"
247 postscript = postscript.strip() + "\n"
252 for comment in Comment.objects.filter(patch = self) \
253 .exclude(msgid = self.msgid):
254 body += comment.patch_responses()
260 body += '---\n' + postscript.strip() + '\n'
262 body += '\n' + self.content
264 mail = PatchMbox(body)
265 mail['Subject'] = self.name
266 mail['Date'] = email.utils.formatdate(
267 time.mktime(self.date.utctimetuple()))
268 mail['From'] = unicode(self.submitter)
269 mail['X-Patchwork-Id'] = str(self.id)
270 mail['Message-Id'] = self.msgid
271 mail.set_unixfrom('From patchwork ' + self.date.ctime())
274 copied_headers = ['To', 'Cc']
275 orig_headers = HeaderParser().parsestr(str(self.headers))
276 for header in copied_headers:
277 if header in orig_headers:
278 mail[header] = orig_headers[header]
283 def get_absolute_url(self):
284 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
287 verbose_name_plural = 'Patches'
289 unique_together = [('msgid', 'project')]
291 class Comment(models.Model):
292 patch = models.ForeignKey(Patch)
293 msgid = models.CharField(max_length=255)
294 submitter = models.ForeignKey(Person)
295 date = models.DateTimeField(default = datetime.datetime.now)
296 headers = models.TextField(blank = True)
297 content = models.TextField()
299 response_re = re.compile( \
300 '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
303 def patch_responses(self):
304 return ''.join([ match.group(0) + '\n' for match in \
305 self.response_re.finditer(self.content)])
309 unique_together = [('msgid', 'patch')]
311 class Bundle(models.Model):
312 owner = models.ForeignKey(User)
313 project = models.ForeignKey(Project)
314 name = models.CharField(max_length = 50, null = False, blank = False)
315 patches = models.ManyToManyField(Patch, through = 'BundlePatch')
316 public = models.BooleanField(default = False)
319 return self.patches.all().count()
321 def ordered_patches(self):
322 return self.patches.order_by('bundlepatch__order');
324 def append_patch(self, patch):
325 # todo: use the aggregate queries in django 1.1
326 orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
330 max_order = orders[0]['order']
334 # see if the patch is already in this bundle
335 if BundlePatch.objects.filter(bundle = self, patch = patch).count():
336 raise Exception("patch is already in bundle")
338 bp = BundlePatch.objects.create(bundle = self, patch = patch,
339 order = max_order + 1)
343 unique_together = [('owner', 'name')]
345 def public_url(self):
348 site = Site.objects.get_current()
349 return 'http://%s%s' % (site.domain,
350 reverse('patchwork.views.bundle.public',
352 'username': self.owner.username,
353 'bundlename': self.name
357 return '\n'.join([p.mbox().as_string(True) \
358 for p in self.ordered_patches()])
360 class BundlePatch(models.Model):
361 patch = models.ForeignKey(Patch)
362 bundle = models.ForeignKey(Bundle)
363 order = models.IntegerField()
366 unique_together = [('bundle', 'patch')]
369 class UserPersonConfirmation(models.Model):
370 user = models.ForeignKey(User)
371 email = models.CharField(max_length = 200)
373 date = models.DateTimeField(default=datetime.datetime.now)
374 active = models.BooleanField(default = True)
381 person = Person.objects.get(email__iexact = self.email)
385 person = Person(email = self.email)
387 person.link_to_user(self.user)
395 str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
396 self.key = self._meta.get_field('key').construct(str).hexdigest()
397 super(UserPersonConfirmation, self).save()