]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/models.py
Add Message-Id to xmlrpc patch_get_mbox headers
[patchwork] / apps / patchwork / models.py
1 # Patchwork - automated patch tracking system
2 # Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
3 #
4 # This file is part of the Patchwork package.
5 #
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.
10 #
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.
15 #
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
19
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
26
27 import re
28 import datetime, time
29 import string
30 import random
31
32 try:
33     from email.mime.nonmultipart import MIMENonMultipart
34     from email.encoders import encode_7or8bit
35     import email.utils
36 except ImportError:
37     # Python 2.4 compatibility
38     from email.MIMENonMultipart import MIMENonMultipart
39     from email.Encoders import encode_7or8bit
40     import email.Utils
41     email.utils = email.Utils
42
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)
47
48     def __str__(self):
49         if self.name:
50             return '%s <%s>' % (self.name, self.email)
51         else:
52             return self.email
53
54     def link_to_user(self, user):
55         self.name = user.get_profile().name()
56         self.user = user
57
58     class Meta:
59         verbose_name_plural = 'People'
60
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)
66
67     def __str__(self):
68         return self.name
69
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')
81
82     def name(self):
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
87
88     def contributor_projects(self):
89         submitters = Person.objects.filter(user = self.user)
90         return Project.objects \
91             .filter(id__in = \
92                     Patch.objects.filter(
93                         submitter__in = submitters) \
94                     .values('project_id').query)
95
96
97     def sync_person(self):
98         pass
99
100     def n_todo_patches(self):
101         return self.todo_patches().count()
102
103     def todo_patches(self, project = None):
104
105         # filter on project, if necessary
106         if project:
107             qs = Patch.objects.filter(project = project)
108         else:
109             qs = Patch.objects
110
111         qs = qs.filter(archived = False) \
112              .filter(delegate = self.user) \
113              .filter(state__in = \
114                      State.objects.filter(action_required = True) \
115                          .values('pk').query)
116         return qs
117
118     def save(self):
119         super(UserProfile, self).save()
120         people = Person.objects.filter(email = self.user.email)
121         if not people:
122             person = Person(email = self.user.email,
123                     name = self.name(), user = self.user)
124             person.save()
125         else:
126             for person in people:
127                  person.link_to_user(self.user)
128                  person.save()
129
130     def __str__(self):
131         return self.name()
132
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)
137
138     def __str__(self):
139         return self.name
140
141     class Meta:
142         ordering = ['ordering']
143
144 class HashField(models.CharField):
145     __metaclass__ = models.SubfieldBase
146
147     def __init__(self, algorithm = 'sha1', *args, **kwargs):
148         self.algorithm = algorithm
149         try:
150             import hashlib
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())
155         except ImportError:
156             modules = { 'sha1': 'sha', 'md5': 'md5'}
157
158             if algorithm not in modules.keys():
159                 raise NameError("Unknown algorithm '%s'" % algorithm)
160
161             self.construct = __import__(modules[algorithm]).new
162
163         self.n_bytes = len(self.construct().hexdigest())
164
165         kwargs['max_length'] = self.n_bytes
166         super(HashField, self).__init__(*args, **kwargs)
167
168     def db_type(self):
169         return 'char(%d)' % self.n_bytes
170
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))
177         encode_7or8bit(self)
178
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)
192
193     def __str__(self):
194         return self.name
195
196     def comments(self):
197         return Comment.objects.filter(patch = self)
198
199     def save(self):
200         try:
201             s = self.state
202         except:
203             self.state = State.objects.get(ordering =  0)
204
205         if self.hash is None:
206             self.hash = hash_patch(self.content).hexdigest()
207
208         super(Patch, self).save()
209
210     def is_editable(self, user):
211         if not user.is_authenticated():
212             return False
213
214         if self.submitter.user == user or self.delegate == user:
215             return True
216
217         profile = user.get_profile()
218         return self.project in user.get_profile().maintainer_projects.all()
219
220     def form(self):
221         f = PatchForm(instance = self, prefix = self.id)
222         return f
223
224     def filename(self):
225         fname_re = re.compile('[^-_A-Za-z0-9\.]+')
226         str = fname_re.sub('-', self.name)
227         return str.strip('-') + '.patch'
228
229     def mbox(self):
230         postscript_re = re.compile('\n-{2,3} ?\n')
231
232         comment = None
233         try:
234             comment = Comment.objects.get(patch = self, msgid = self.msgid)
235         except Exception:
236             pass
237
238         body = ''
239         if comment:
240             body = comment.content.strip() + "\n"
241
242         parts = postscript_re.split(body, 1)
243         if len(parts) == 2:
244             (body, postscript) = parts
245             body = body.strip() + "\n"
246             postscript = postscript.strip() + "\n"
247         else:
248             postscript = ''
249
250         responses = False
251         for comment in Comment.objects.filter(patch = self) \
252                 .exclude(msgid = self.msgid):
253             body += comment.patch_responses()
254
255         if body:
256             body += '\n'
257
258         if postscript:
259             body += '---\n' + postscript.strip() + '\n'
260
261         body += '\n' + self.content
262
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['Message-Id'] = self.msgid
270         mail.set_unixfrom('From patchwork ' + self.date.ctime())
271
272         return mail
273
274
275     @models.permalink
276     def get_absolute_url(self):
277         return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
278
279     class Meta:
280         verbose_name_plural = 'Patches'
281         ordering = ['date']
282         unique_together = [('msgid', 'project')]
283
284 class Comment(models.Model):
285     patch = models.ForeignKey(Patch)
286     msgid = models.CharField(max_length=255)
287     submitter = models.ForeignKey(Person)
288     date = models.DateTimeField(default = datetime.datetime.now)
289     headers = models.TextField(blank = True)
290     content = models.TextField()
291
292     response_re = re.compile('^(Tested|Reviewed|Acked|Signed-off|Nacked)-by: .*$', re.M | re.I)
293
294     def patch_responses(self):
295         return ''.join([ match.group(0) + '\n' for match in \
296                                 self.response_re.finditer(self.content)])
297
298     class Meta:
299         ordering = ['date']
300         unique_together = [('msgid', 'patch')]
301
302 class Bundle(models.Model):
303     owner = models.ForeignKey(User)
304     project = models.ForeignKey(Project)
305     name = models.CharField(max_length = 50, null = False, blank = False)
306     patches = models.ManyToManyField(Patch, through = 'BundlePatch')
307     public = models.BooleanField(default = False)
308
309     def n_patches(self):
310         return self.patches.all().count()
311
312     def ordered_patches(self):
313         return self.patches.order_by('bundlepatch__order');
314
315     def append_patch(self, patch):
316         # todo: use the aggregate queries in django 1.1
317         orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
318                  .values('order')
319
320         if len(orders) > 0:
321             max_order = orders[0]['order']
322         else:
323             max_order = 0
324
325         # see if the patch is already in this bundle
326         if BundlePatch.objects.filter(bundle = self, patch = patch).count():
327             raise Exception("patch is already in bundle")
328
329         bp = BundlePatch.objects.create(bundle = self, patch = patch,
330                 order = max_order + 1)
331         bp.save()
332
333     class Meta:
334         unique_together = [('owner', 'name')]
335
336     def public_url(self):
337         if not self.public:
338             return None
339         site = Site.objects.get_current()
340         return 'http://%s%s' % (site.domain,
341                 reverse('patchwork.views.bundle.public',
342                         kwargs = {
343                                 'username': self.owner.username,
344                                 'bundlename': self.name
345                         }))
346
347     def mbox(self):
348         return '\n'.join([p.mbox().as_string(True) \
349                         for p in self.ordered_patches()])
350
351 class BundlePatch(models.Model):
352     patch = models.ForeignKey(Patch)
353     bundle = models.ForeignKey(Bundle)
354     order = models.IntegerField()
355
356     class Meta:
357         unique_together = [('bundle', 'patch')]
358         ordering = ['order']
359
360 class UserPersonConfirmation(models.Model):
361     user = models.ForeignKey(User)
362     email = models.CharField(max_length = 200)
363     key = HashField()
364     date = models.DateTimeField(default=datetime.datetime.now)
365     active = models.BooleanField(default = True)
366
367     def confirm(self):
368         if not self.active:
369             return
370         person = None
371         try:
372             person = Person.objects.get(email__iexact = self.email)
373         except Exception:
374             pass
375         if not person:
376             person = Person(email = self.email)
377
378         person.link_to_user(self.user)
379         person.save()
380         self.active = False
381         self.save()
382
383     def save(self):
384         max = 1 << 32
385         if self.key == '':
386             str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
387             self.key = self._meta.get_field('key').construct(str).hexdigest()
388         super(UserPersonConfirmation, self).save()
389
390