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(null = True)
191 pull_url = models.CharField(max_length=255, null = True)
192 commit_ref = models.CharField(max_length=255, null = True, blank = True)
193 hash = HashField(null = True, db_index = True)
199 return Comment.objects.filter(patch = self)
205 self.state = State.objects.get(ordering = 0)
207 if self.hash is None and self.content is not None:
208 self.hash = hash_patch(self.content).hexdigest()
210 super(Patch, self).save()
212 def is_editable(self, user):
213 if not user.is_authenticated():
216 if self.submitter.user == user or self.delegate == user:
219 profile = user.get_profile()
220 return self.project in user.get_profile().maintainer_projects.all()
223 f = PatchForm(instance = self, prefix = self.id)
227 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
228 str = fname_re.sub('-', self.name)
229 return str.strip('-') + '.patch'
232 postscript_re = re.compile('\n-{2,3} ?\n')
236 comment = Comment.objects.get(patch = self, msgid = self.msgid)
242 body = comment.content.strip() + "\n"
244 parts = postscript_re.split(body, 1)
246 (body, postscript) = parts
247 body = body.strip() + "\n"
248 postscript = postscript.strip() + "\n"
253 for comment in Comment.objects.filter(patch = self) \
254 .exclude(msgid = self.msgid):
255 body += comment.patch_responses()
261 body += '---\n' + postscript.strip() + '\n'
264 body += '\n' + self.content
266 mail = PatchMbox(body)
267 mail['Subject'] = self.name
268 mail['Date'] = email.utils.formatdate(
269 time.mktime(self.date.utctimetuple()))
270 mail['From'] = unicode(self.submitter)
271 mail['X-Patchwork-Id'] = str(self.id)
272 mail['Message-Id'] = self.msgid
273 mail.set_unixfrom('From patchwork ' + self.date.ctime())
276 copied_headers = ['To', 'Cc']
277 orig_headers = HeaderParser().parsestr(str(self.headers))
278 for header in copied_headers:
279 if header in orig_headers:
280 mail[header] = orig_headers[header]
285 def get_absolute_url(self):
286 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
289 verbose_name_plural = 'Patches'
291 unique_together = [('msgid', 'project')]
293 class Comment(models.Model):
294 patch = models.ForeignKey(Patch)
295 msgid = models.CharField(max_length=255)
296 submitter = models.ForeignKey(Person)
297 date = models.DateTimeField(default = datetime.datetime.now)
298 headers = models.TextField(blank = True)
299 content = models.TextField()
301 response_re = re.compile( \
302 '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
305 def patch_responses(self):
306 return ''.join([ match.group(0) + '\n' for match in \
307 self.response_re.finditer(self.content)])
311 unique_together = [('msgid', 'patch')]
313 class Bundle(models.Model):
314 owner = models.ForeignKey(User)
315 project = models.ForeignKey(Project)
316 name = models.CharField(max_length = 50, null = False, blank = False)
317 patches = models.ManyToManyField(Patch, through = 'BundlePatch')
318 public = models.BooleanField(default = False)
321 return self.patches.all().count()
323 def ordered_patches(self):
324 return self.patches.order_by('bundlepatch__order');
326 def append_patch(self, patch):
327 # todo: use the aggregate queries in django 1.1
328 orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
332 max_order = orders[0]['order']
336 # see if the patch is already in this bundle
337 if BundlePatch.objects.filter(bundle = self, patch = patch).count():
338 raise Exception("patch is already in bundle")
340 bp = BundlePatch.objects.create(bundle = self, patch = patch,
341 order = max_order + 1)
345 unique_together = [('owner', 'name')]
347 def public_url(self):
350 site = Site.objects.get_current()
351 return 'http://%s%s' % (site.domain,
352 reverse('patchwork.views.bundle.public',
354 'username': self.owner.username,
355 'bundlename': self.name
359 return '\n'.join([p.mbox().as_string(True) \
360 for p in self.ordered_patches()])
362 class BundlePatch(models.Model):
363 patch = models.ForeignKey(Patch)
364 bundle = models.ForeignKey(Bundle)
365 order = models.IntegerField()
368 unique_together = [('bundle', 'patch')]
371 class UserPersonConfirmation(models.Model):
372 user = models.ForeignKey(User)
373 email = models.CharField(max_length = 200)
375 date = models.DateTimeField(default=datetime.datetime.now)
376 active = models.BooleanField(default = True)
383 person = Person.objects.get(email__iexact = self.email)
387 person = Person(email = self.email)
389 person.link_to_user(self.user)
397 str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
398 self.key = self._meta.get_field('key').construct(str).hexdigest()
399 super(UserPersonConfirmation, self).save()