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('^(Tested|Reviewed|Acked|Signed-off|Nacked)-by: .*$', re.M | re.I)
301 def patch_responses(self):
302 return ''.join([ match.group(0) + '\n' for match in \
303 self.response_re.finditer(self.content)])
307 unique_together = [('msgid', 'patch')]
309 class Bundle(models.Model):
310 owner = models.ForeignKey(User)
311 project = models.ForeignKey(Project)
312 name = models.CharField(max_length = 50, null = False, blank = False)
313 patches = models.ManyToManyField(Patch, through = 'BundlePatch')
314 public = models.BooleanField(default = False)
317 return self.patches.all().count()
319 def ordered_patches(self):
320 return self.patches.order_by('bundlepatch__order');
322 def append_patch(self, patch):
323 # todo: use the aggregate queries in django 1.1
324 orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
328 max_order = orders[0]['order']
332 # see if the patch is already in this bundle
333 if BundlePatch.objects.filter(bundle = self, patch = patch).count():
334 raise Exception("patch is already in bundle")
336 bp = BundlePatch.objects.create(bundle = self, patch = patch,
337 order = max_order + 1)
341 unique_together = [('owner', 'name')]
343 def public_url(self):
346 site = Site.objects.get_current()
347 return 'http://%s%s' % (site.domain,
348 reverse('patchwork.views.bundle.public',
350 'username': self.owner.username,
351 'bundlename': self.name
355 return '\n'.join([p.mbox().as_string(True) \
356 for p in self.ordered_patches()])
358 class BundlePatch(models.Model):
359 patch = models.ForeignKey(Patch)
360 bundle = models.ForeignKey(Bundle)
361 order = models.IntegerField()
364 unique_together = [('bundle', 'patch')]
367 class UserPersonConfirmation(models.Model):
368 user = models.ForeignKey(User)
369 email = models.CharField(max_length = 200)
371 date = models.DateTimeField(default=datetime.datetime.now)
372 active = models.BooleanField(default = True)
379 person = Person.objects.get(email__iexact = self.email)
383 person = Person(email = self.email)
385 person.link_to_user(self.user)
393 str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
394 self.key = self._meta.get_field('key').construct(str).hexdigest()
395 super(UserPersonConfirmation, self).save()