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