]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/models.py
Add support for git-pull requests
[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(null = True)
191     pull_url = models.CharField(max_length=255, null = True)
192     commit_ref = models.CharField(max_length=255, null = True, blank = True)
193     hash = HashField(null = True, db_index = True)
194
195     def __str__(self):
196         return self.name
197
198     def comments(self):
199         return Comment.objects.filter(patch = self)
200
201     def save(self):
202         try:
203             s = self.state
204         except:
205             self.state = State.objects.get(ordering =  0)
206
207         if self.hash is None and self.content is not None:
208             self.hash = hash_patch(self.content).hexdigest()
209
210         super(Patch, self).save()
211
212     def is_editable(self, user):
213         if not user.is_authenticated():
214             return False
215
216         if self.submitter.user == user or self.delegate == user:
217             return True
218
219         profile = user.get_profile()
220         return self.project in user.get_profile().maintainer_projects.all()
221
222     def form(self):
223         f = PatchForm(instance = self, prefix = self.id)
224         return f
225
226     def filename(self):
227         fname_re = re.compile('[^-_A-Za-z0-9\.]+')
228         str = fname_re.sub('-', self.name)
229         return str.strip('-') + '.patch'
230
231     def mbox(self):
232         postscript_re = re.compile('\n-{2,3} ?\n')
233
234         comment = None
235         try:
236             comment = Comment.objects.get(patch = self, msgid = self.msgid)
237         except Exception:
238             pass
239
240         body = ''
241         if comment:
242             body = comment.content.strip() + "\n"
243
244         parts = postscript_re.split(body, 1)
245         if len(parts) == 2:
246             (body, postscript) = parts
247             body = body.strip() + "\n"
248             postscript = postscript.strip() + "\n"
249         else:
250             postscript = ''
251
252         responses = False
253         for comment in Comment.objects.filter(patch = self) \
254                 .exclude(msgid = self.msgid):
255             body += comment.patch_responses()
256
257         if body:
258             body += '\n'
259
260         if postscript:
261             body += '---\n' + postscript.strip() + '\n'
262
263         if self.content:
264             body += '\n' + self.content
265
266         mail = PatchMbox(body)
267         mail['Subject'] = self.name
268         mail['Date'] = email.utils.formatdate(
269                         time.mktime(self.date.utctimetuple()))
270         mail['From'] = unicode(self.submitter)
271         mail['X-Patchwork-Id'] = str(self.id)
272         mail['Message-Id'] = self.msgid
273         mail.set_unixfrom('From patchwork ' + self.date.ctime())
274
275
276         copied_headers = ['To', 'Cc']
277         orig_headers = HeaderParser().parsestr(str(self.headers))
278         for header in copied_headers:
279             if header in orig_headers:
280                 mail[header] = orig_headers[header]
281
282         return mail
283
284     @models.permalink
285     def get_absolute_url(self):
286         return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
287
288     class Meta:
289         verbose_name_plural = 'Patches'
290         ordering = ['date']
291         unique_together = [('msgid', 'project')]
292
293 class Comment(models.Model):
294     patch = models.ForeignKey(Patch)
295     msgid = models.CharField(max_length=255)
296     submitter = models.ForeignKey(Person)
297     date = models.DateTimeField(default = datetime.datetime.now)
298     headers = models.TextField(blank = True)
299     content = models.TextField()
300
301     response_re = re.compile( \
302             '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
303             re.M | re.I)
304
305     def patch_responses(self):
306         return ''.join([ match.group(0) + '\n' for match in \
307                                 self.response_re.finditer(self.content)])
308
309     class Meta:
310         ordering = ['date']
311         unique_together = [('msgid', 'patch')]
312
313 class Bundle(models.Model):
314     owner = models.ForeignKey(User)
315     project = models.ForeignKey(Project)
316     name = models.CharField(max_length = 50, null = False, blank = False)
317     patches = models.ManyToManyField(Patch, through = 'BundlePatch')
318     public = models.BooleanField(default = False)
319
320     def n_patches(self):
321         return self.patches.all().count()
322
323     def ordered_patches(self):
324         return self.patches.order_by('bundlepatch__order');
325
326     def append_patch(self, patch):
327         # todo: use the aggregate queries in django 1.1
328         orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
329                  .values('order')
330
331         if len(orders) > 0:
332             max_order = orders[0]['order']
333         else:
334             max_order = 0
335
336         # see if the patch is already in this bundle
337         if BundlePatch.objects.filter(bundle = self, patch = patch).count():
338             raise Exception("patch is already in bundle")
339
340         bp = BundlePatch.objects.create(bundle = self, patch = patch,
341                 order = max_order + 1)
342         bp.save()
343
344     class Meta:
345         unique_together = [('owner', 'name')]
346
347     def public_url(self):
348         if not self.public:
349             return None
350         site = Site.objects.get_current()
351         return 'http://%s%s' % (site.domain,
352                 reverse('patchwork.views.bundle.public',
353                         kwargs = {
354                                 'username': self.owner.username,
355                                 'bundlename': self.name
356                         }))
357
358     def mbox(self):
359         return '\n'.join([p.mbox().as_string(True) \
360                         for p in self.ordered_patches()])
361
362 class BundlePatch(models.Model):
363     patch = models.ForeignKey(Patch)
364     bundle = models.ForeignKey(Bundle)
365     order = models.IntegerField()
366
367     class Meta:
368         unique_together = [('bundle', 'patch')]
369         ordering = ['order']
370
371 class UserPersonConfirmation(models.Model):
372     user = models.ForeignKey(User)
373     email = models.CharField(max_length = 200)
374     key = HashField()
375     date = models.DateTimeField(default=datetime.datetime.now)
376     active = models.BooleanField(default = True)
377
378     def confirm(self):
379         if not self.active:
380             return
381         person = None
382         try:
383             person = Person.objects.get(email__iexact = self.email)
384         except Exception:
385             pass
386         if not person:
387             person = Person(email = self.email)
388
389         person.link_to_user(self.user)
390         person.save()
391         self.active = False
392         self.save()
393
394     def save(self):
395         max = 1 << 32
396         if self.key == '':
397             str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
398             self.key = self._meta.get_field('key').construct(str).hexdigest()
399         super(UserPersonConfirmation, self).save()
400
401