]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/models.py
models: Fix HeaderParser import on python 2.4
[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     from email.parser import HeaderParser
36     import email.utils
37 except ImportError:
38     # Python 2.4 compatibility
39     from email.MIMENonMultipart import MIMENonMultipart
40     from email.Encoders import encode_7or8bit
41     from email.Parser import HeaderParser
42     import email.Utils
43     email.utils = email.Utils
44
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)
49
50     def __str__(self):
51         if self.name:
52             return '%s <%s>' % (self.name, self.email)
53         else:
54             return self.email
55
56     def link_to_user(self, user):
57         self.name = user.get_profile().name()
58         self.user = user
59
60     class Meta:
61         verbose_name_plural = 'People'
62
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)
68
69     def __str__(self):
70         return self.name
71
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')
83
84     def name(self):
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 ' '.join(names)
88         return self.user.username
89
90     def contributor_projects(self):
91         submitters = Person.objects.filter(user = self.user)
92         return Project.objects \
93             .filter(id__in = \
94                     Patch.objects.filter(
95                         submitter__in = submitters) \
96                     .values('project_id').query)
97
98
99     def sync_person(self):
100         pass
101
102     def n_todo_patches(self):
103         return self.todo_patches().count()
104
105     def todo_patches(self, project = None):
106
107         # filter on project, if necessary
108         if project:
109             qs = Patch.objects.filter(project = project)
110         else:
111             qs = Patch.objects
112
113         qs = qs.filter(archived = False) \
114              .filter(delegate = self.user) \
115              .filter(state__in = \
116                      State.objects.filter(action_required = True) \
117                          .values('pk').query)
118         return qs
119
120     def save(self):
121         super(UserProfile, self).save()
122         people = Person.objects.filter(email = self.user.email)
123         if not people:
124             person = Person(email = self.user.email,
125                     name = self.name(), user = self.user)
126             person.save()
127         else:
128             for person in people:
129                  person.link_to_user(self.user)
130                  person.save()
131
132     def __str__(self):
133         return self.name()
134
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)
139
140     def __str__(self):
141         return self.name
142
143     class Meta:
144         ordering = ['ordering']
145
146 class HashField(models.CharField):
147     __metaclass__ = models.SubfieldBase
148
149     def __init__(self, algorithm = 'sha1', *args, **kwargs):
150         self.algorithm = algorithm
151         try:
152             import hashlib
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())
157         except ImportError:
158             modules = { 'sha1': 'sha', 'md5': 'md5'}
159
160             if algorithm not in modules.keys():
161                 raise NameError("Unknown algorithm '%s'" % algorithm)
162
163             self.construct = __import__(modules[algorithm]).new
164
165         self.n_bytes = len(self.construct().hexdigest())
166
167         kwargs['max_length'] = self.n_bytes
168         super(HashField, self).__init__(*args, **kwargs)
169
170     def db_type(self):
171         return 'char(%d)' % self.n_bytes
172
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))
179         encode_7or8bit(self)
180
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)
195
196     def __str__(self):
197         return self.name
198
199     def comments(self):
200         return Comment.objects.filter(patch = self)
201
202     def save(self):
203         try:
204             s = self.state
205         except:
206             self.state = State.objects.get(ordering =  0)
207
208         if self.hash is None and self.content is not None:
209             self.hash = hash_patch(self.content).hexdigest()
210
211         super(Patch, self).save()
212
213     def is_editable(self, user):
214         if not user.is_authenticated():
215             return False
216
217         if self.submitter.user == user or self.delegate == user:
218             return True
219
220         profile = user.get_profile()
221         return self.project in user.get_profile().maintainer_projects.all()
222
223     def form(self):
224         f = PatchForm(instance = self, prefix = self.id)
225         return f
226
227     def filename(self):
228         fname_re = re.compile('[^-_A-Za-z0-9\.]+')
229         str = fname_re.sub('-', self.name)
230         return str.strip('-') + '.patch'
231
232     def mbox(self):
233         postscript_re = re.compile('\n-{2,3} ?\n')
234
235         comment = None
236         try:
237             comment = Comment.objects.get(patch = self, msgid = self.msgid)
238         except Exception:
239             pass
240
241         body = ''
242         if comment:
243             body = comment.content.strip() + "\n"
244
245         parts = postscript_re.split(body, 1)
246         if len(parts) == 2:
247             (body, postscript) = parts
248             body = body.strip() + "\n"
249             postscript = postscript.strip() + "\n"
250         else:
251             postscript = ''
252
253         responses = False
254         for comment in Comment.objects.filter(patch = self) \
255                 .exclude(msgid = self.msgid):
256             body += comment.patch_responses()
257
258         if body:
259             body += '\n'
260
261         if postscript:
262             body += '---\n' + postscript.strip() + '\n'
263
264         if self.content:
265             body += '\n' + self.content
266
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())
275
276
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]
282
283         return mail
284
285     @models.permalink
286     def get_absolute_url(self):
287         return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
288
289     class Meta:
290         verbose_name_plural = 'Patches'
291         ordering = ['date']
292         unique_together = [('msgid', 'project')]
293
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()
301
302     response_re = re.compile( \
303             '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
304             re.M | re.I)
305
306     def patch_responses(self):
307         return ''.join([ match.group(0) + '\n' for match in \
308                                 self.response_re.finditer(self.content)])
309
310     class Meta:
311         ordering = ['date']
312         unique_together = [('msgid', 'patch')]
313
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)
320
321     def n_patches(self):
322         return self.patches.all().count()
323
324     def ordered_patches(self):
325         return self.patches.order_by('bundlepatch__order');
326
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') \
330                  .values('order')
331
332         if len(orders) > 0:
333             max_order = orders[0]['order']
334         else:
335             max_order = 0
336
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")
340
341         bp = BundlePatch.objects.create(bundle = self, patch = patch,
342                 order = max_order + 1)
343         bp.save()
344
345     class Meta:
346         unique_together = [('owner', 'name')]
347
348     def public_url(self):
349         if not self.public:
350             return None
351         site = Site.objects.get_current()
352         return 'http://%s%s' % (site.domain,
353                 reverse('patchwork.views.bundle.public',
354                         kwargs = {
355                                 'username': self.owner.username,
356                                 'bundlename': self.name
357                         }))
358
359     def mbox(self):
360         return '\n'.join([p.mbox().as_string(True) \
361                         for p in self.ordered_patches()])
362
363 class BundlePatch(models.Model):
364     patch = models.ForeignKey(Patch)
365     bundle = models.ForeignKey(Bundle)
366     order = models.IntegerField()
367
368     class Meta:
369         unique_together = [('bundle', 'patch')]
370         ordering = ['order']
371
372 class UserPersonConfirmation(models.Model):
373     user = models.ForeignKey(User)
374     email = models.CharField(max_length = 200)
375     key = HashField()
376     date = models.DateTimeField(default=datetime.datetime.now)
377     active = models.BooleanField(default = True)
378
379     def confirm(self):
380         if not self.active:
381             return
382         person = None
383         try:
384             person = Person.objects.get(email__iexact = self.email)
385         except Exception:
386             pass
387         if not person:
388             person = Person(email = self.email)
389
390         person.link_to_user(self.user)
391         person.save()
392         self.active = False
393         self.save()
394
395     def save(self):
396         max = 1 << 32
397         if self.key == '':
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()
401
402