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
41 from email.Parser import HeaderParser
43 email.utils = email.Utils
45 class Person(models.Model):
46 email = models.CharField(max_length=255, unique = True)
47 name = models.CharField(max_length=255, null = True)
48 user = models.ForeignKey(User, null = True)
50 def __unicode__(self):
52 return u'%s <%s>' % (self.name, self.email)
56 def link_to_user(self, user):
57 self.name = user.get_profile().name()
61 verbose_name_plural = 'People'
63 class Project(models.Model):
64 linkname = models.CharField(max_length=255, unique=True)
65 name = models.CharField(max_length=255, unique=True)
66 listid = models.CharField(max_length=255, unique=True)
67 listemail = models.CharField(max_length=200)
69 def __unicode__(self):
72 class UserProfile(models.Model):
73 user = models.ForeignKey(User, unique = True)
74 primary_project = models.ForeignKey(Project, null = True)
75 maintainer_projects = models.ManyToManyField(Project,
76 related_name = 'maintainer_project')
77 send_email = models.BooleanField(default = False,
78 help_text = 'Selecting this option allows patchwork to send ' +
79 'email on your behalf')
80 patches_per_page = models.PositiveIntegerField(default = 100,
81 null = False, blank = False,
82 help_text = 'Number of patches to display per page')
85 if self.user.first_name or self.user.last_name:
86 names = filter(bool, [self.user.first_name, self.user.last_name])
87 return u' '.join(names)
88 return self.user.username
90 def contributor_projects(self):
91 submitters = Person.objects.filter(user = self.user)
92 return Project.objects \
95 submitter__in = submitters) \
96 .values('project_id').query)
99 def sync_person(self):
102 def n_todo_patches(self):
103 return self.todo_patches().count()
105 def todo_patches(self, project = None):
107 # filter on project, if necessary
109 qs = Patch.objects.filter(project = project)
113 qs = qs.filter(archived = False) \
114 .filter(delegate = self.user) \
115 .filter(state__in = \
116 State.objects.filter(action_required = True) \
121 super(UserProfile, self).save()
122 people = Person.objects.filter(email = self.user.email)
124 person = Person(email = self.user.email,
125 name = self.name(), user = self.user)
128 for person in people:
129 person.link_to_user(self.user)
132 def __unicode__(self):
135 class State(models.Model):
136 name = models.CharField(max_length = 100)
137 ordering = models.IntegerField(unique = True)
138 action_required = models.BooleanField(default = True)
140 def __unicode__(self):
144 ordering = ['ordering']
146 class HashField(models.CharField):
147 __metaclass__ = models.SubfieldBase
149 def __init__(self, algorithm = 'sha1', *args, **kwargs):
150 self.algorithm = algorithm
153 def _construct(string = ''):
154 return hashlib.new(self.algorithm, string)
155 self.construct = _construct
156 self.n_bytes = len(hashlib.new(self.algorithm).hexdigest())
158 modules = { 'sha1': 'sha', 'md5': 'md5'}
160 if algorithm not in modules.keys():
161 raise NameError("Unknown algorithm '%s'" % algorithm)
163 self.construct = __import__(modules[algorithm]).new
165 self.n_bytes = len(self.construct().hexdigest())
167 kwargs['max_length'] = self.n_bytes
168 super(HashField, self).__init__(*args, **kwargs)
171 return 'char(%d)' % self.n_bytes
173 class PatchMbox(MIMENonMultipart):
174 patch_charset = 'utf-8'
175 def __init__(self, _text):
176 MIMENonMultipart.__init__(self, 'text', 'plain',
177 **{'charset': self.patch_charset})
178 self.set_payload(_text.encode(self.patch_charset))
181 class Patch(models.Model):
182 project = models.ForeignKey(Project)
183 msgid = models.CharField(max_length=255)
184 name = models.CharField(max_length=255)
185 date = models.DateTimeField(default=datetime.datetime.now)
186 submitter = models.ForeignKey(Person)
187 delegate = models.ForeignKey(User, blank = True, null = True)
188 state = models.ForeignKey(State)
189 archived = models.BooleanField(default = False)
190 headers = models.TextField(blank = True)
191 content = models.TextField(null = True)
192 pull_url = models.CharField(max_length=255, null = True)
193 commit_ref = models.CharField(max_length=255, null = True, blank = True)
194 hash = HashField(null = True, db_index = True)
196 def __unicode__(self):
200 return Comment.objects.filter(patch = self)
206 self.state = State.objects.get(ordering = 0)
208 if self.hash is None and self.content is not None:
209 self.hash = hash_patch(self.content).hexdigest()
211 super(Patch, self).save()
213 def is_editable(self, user):
214 if not user.is_authenticated():
217 if self.submitter.user == user or self.delegate == user:
220 profile = user.get_profile()
221 return self.project in user.get_profile().maintainer_projects.all()
224 f = PatchForm(instance = self, prefix = self.id)
228 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
229 str = fname_re.sub('-', self.name)
230 return str.strip('-') + '.patch'
233 postscript_re = re.compile('\n-{2,3} ?\n')
237 comment = Comment.objects.get(patch = self, msgid = self.msgid)
243 body = comment.content.strip() + "\n"
245 parts = postscript_re.split(body, 1)
247 (body, postscript) = parts
248 body = body.strip() + "\n"
249 postscript = postscript.strip() + "\n"
254 for comment in Comment.objects.filter(patch = self) \
255 .exclude(msgid = self.msgid):
256 body += comment.patch_responses()
262 body += '---\n' + postscript.strip() + '\n'
265 body += '\n' + self.content
267 mail = PatchMbox(body)
268 mail['Subject'] = self.name
269 mail['Date'] = email.utils.formatdate(
270 time.mktime(self.date.utctimetuple()))
271 mail['From'] = unicode(self.submitter)
272 mail['X-Patchwork-Id'] = str(self.id)
273 mail['Message-Id'] = self.msgid
274 mail.set_unixfrom('From patchwork ' + self.date.ctime())
277 copied_headers = ['To', 'Cc']
278 orig_headers = HeaderParser().parsestr(str(self.headers))
279 for header in copied_headers:
280 if header in orig_headers:
281 mail[header] = orig_headers[header]
286 def get_absolute_url(self):
287 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
290 verbose_name_plural = 'Patches'
292 unique_together = [('msgid', 'project')]
294 class Comment(models.Model):
295 patch = models.ForeignKey(Patch)
296 msgid = models.CharField(max_length=255)
297 submitter = models.ForeignKey(Person)
298 date = models.DateTimeField(default = datetime.datetime.now)
299 headers = models.TextField(blank = True)
300 content = models.TextField()
302 response_re = re.compile( \
303 '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
306 def patch_responses(self):
307 return ''.join([ match.group(0) + '\n' for match in \
308 self.response_re.finditer(self.content)])
312 unique_together = [('msgid', 'patch')]
314 class Bundle(models.Model):
315 owner = models.ForeignKey(User)
316 project = models.ForeignKey(Project)
317 name = models.CharField(max_length = 50, null = False, blank = False)
318 patches = models.ManyToManyField(Patch, through = 'BundlePatch')
319 public = models.BooleanField(default = False)
322 return self.patches.all().count()
324 def ordered_patches(self):
325 return self.patches.order_by('bundlepatch__order');
327 def append_patch(self, patch):
328 # todo: use the aggregate queries in django 1.1
329 orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
333 max_order = orders[0]['order']
337 # see if the patch is already in this bundle
338 if BundlePatch.objects.filter(bundle = self, patch = patch).count():
339 raise Exception("patch is already in bundle")
341 bp = BundlePatch.objects.create(bundle = self, patch = patch,
342 order = max_order + 1)
346 unique_together = [('owner', 'name')]
348 def public_url(self):
351 site = Site.objects.get_current()
352 return 'http://%s%s' % (site.domain,
353 reverse('patchwork.views.bundle.public',
355 'username': self.owner.username,
356 'bundlename': self.name
360 return '\n'.join([p.mbox().as_string(True) \
361 for p in self.ordered_patches()])
363 class BundlePatch(models.Model):
364 patch = models.ForeignKey(Patch)
365 bundle = models.ForeignKey(Bundle)
366 order = models.IntegerField()
369 unique_together = [('bundle', 'patch')]
372 class UserPersonConfirmation(models.Model):
373 user = models.ForeignKey(User)
374 email = models.CharField(max_length = 200)
376 date = models.DateTimeField(default=datetime.datetime.now)
377 active = models.BooleanField(default = True)
384 person = Person.objects.get(email__iexact = self.email)
388 person = Person(email = self.email)
390 person.link_to_user(self.user)
398 str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
399 self.key = self._meta.get_field('key').construct(str).hexdigest()
400 super(UserPersonConfirmation, self).save()