]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/models.py
[models] Create patch mbox with utf-8 encoding
[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 import django.oldforms as oldforms
27
28 import re
29 import datetime, time
30 import string
31 import random
32
33 try:
34     from email.mime.text import MIMEText
35     import email.utils
36 except ImportError:
37     # Python 2.4 compatibility
38     from email.MIMEText import MIMEText
39     import email.Utils
40     email.utils = email.Utils
41
42 class Person(models.Model):
43     email = models.CharField(max_length=255, unique = True)
44     name = models.CharField(max_length=255, null = True)
45     user = models.ForeignKey(User, null = True)
46
47     def __str__(self):
48         if self.name:
49             return '%s <%s>' % (self.name, self.email)
50         else:
51             return self.email
52
53     def link_to_user(self, user):
54         self.name = user.get_profile().name()
55         self.user = user
56
57     class Meta:
58         verbose_name_plural = 'People'
59
60 class Project(models.Model):
61     linkname = models.CharField(max_length=255, unique=True)
62     name = models.CharField(max_length=255, unique=True)
63     listid = models.CharField(max_length=255, unique=True)
64     listemail = models.CharField(max_length=200)
65
66     def __str__(self):
67         return self.name
68
69 class UserProfile(models.Model):
70     user = models.ForeignKey(User, unique = True)
71     primary_project = models.ForeignKey(Project, null = True)
72     maintainer_projects = models.ManyToManyField(Project,
73             related_name = 'maintainer_project')
74     send_email = models.BooleanField(default = False,
75             help_text = 'Selecting this option allows patchwork to send ' +
76                 'email on your behalf')
77     patches_per_page = models.PositiveIntegerField(default = 100,
78             null = False, blank = False,
79             help_text = 'Number of patches to display per page')
80
81     def name(self):
82         if self.user.first_name or self.user.last_name:
83             names = filter(bool, [self.user.first_name, self.user.last_name])
84             return ' '.join(names)
85         return self.user.username
86
87     def contributor_projects(self):
88         submitters = Person.objects.filter(user = self.user)
89         return Project.objects \
90             .filter(id__in = \
91                     Patch.objects.filter(
92                         submitter__in = submitters) \
93                     .values('project_id').query)
94
95
96     def sync_person(self):
97         pass
98
99     def n_todo_patches(self):
100         return self.todo_patches().count()
101
102     def todo_patches(self, project = None):
103
104         # filter on project, if necessary
105         if project:
106             qs = Patch.objects.filter(project = project)
107         else:
108             qs = Patch.objects
109
110         qs = qs.filter(archived = False) \
111              .filter(delegate = self.user) \
112              .filter(state__in = \
113                      State.objects.filter(action_required = True) \
114                          .values('pk').query)
115         return qs
116
117     def save(self):
118         super(UserProfile, self).save()
119         people = Person.objects.filter(email = self.user.email)
120         if not people:
121             person = Person(email = self.user.email,
122                     name = self.name(), user = self.user)
123             person.save()
124         else:
125             for person in people:
126                  person.link_to_user(self.user)
127                  person.save()
128
129     def __str__(self):
130         return self.name()
131
132 class State(models.Model):
133     name = models.CharField(max_length = 100)
134     ordering = models.IntegerField(unique = True)
135     action_required = models.BooleanField(default = True)
136
137     def __str__(self):
138         return self.name
139
140     class Meta:
141         ordering = ['ordering']
142
143 class HashField(models.CharField):
144     __metaclass__ = models.SubfieldBase
145
146     def __init__(self, algorithm = 'sha1', *args, **kwargs):
147         self.algorithm = algorithm
148         try:
149             import hashlib
150             def _construct(string = ''):
151                 return hashlib.new(self.algorithm, string)
152             self.construct = _construct
153             self.n_bytes = len(hashlib.new(self.algorithm).hexdigest())
154         except ImportError:
155             modules = { 'sha1': 'sha', 'md5': 'md5'}
156
157             if algorithm not in modules.keys():
158                 raise NameError("Unknown algorithm '%s'" % algorithm)
159
160             self.construct = __import__(modules[algorithm]).new
161
162         self.n_bytes = len(self.construct().hexdigest())
163
164         kwargs['max_length'] = self.n_bytes
165         super(HashField, self).__init__(*args, **kwargs)
166
167     def db_type(self):
168         return 'char(%d)' % self.n_bytes
169
170 class Patch(models.Model):
171     project = models.ForeignKey(Project)
172     msgid = models.CharField(max_length=255, unique = True)
173     name = models.CharField(max_length=255)
174     date = models.DateTimeField(default=datetime.datetime.now)
175     submitter = models.ForeignKey(Person)
176     delegate = models.ForeignKey(User, blank = True, null = True)
177     state = models.ForeignKey(State)
178     archived = models.BooleanField(default = False)
179     headers = models.TextField(blank = True)
180     content = models.TextField()
181     commit_ref = models.CharField(max_length=255, null = True, blank = True)
182     hash = HashField(null = True, db_index = True)
183
184     def __str__(self):
185         return self.name
186
187     def comments(self):
188         return Comment.objects.filter(patch = self)
189
190     def save(self):
191         try:
192             s = self.state
193         except:
194             self.state = State.objects.get(ordering =  0)
195
196         if self.hash is None:
197             self.hash = hash_patch(self.content).hexdigest()
198
199         super(Patch, self).save()
200
201     def is_editable(self, user):
202         if not user.is_authenticated():
203             return False
204
205         if self.submitter.user == user or self.delegate == user:
206             return True
207
208         profile = user.get_profile()
209         return self.project in user.get_profile().maintainer_projects.all()
210
211     def form(self):
212         f = PatchForm(instance = self, prefix = self.id)
213         return f
214
215     def filename(self):
216         fname_re = re.compile('[^-_A-Za-z0-9\.]+')
217         str = fname_re.sub('-', self.name)
218         return str.strip('-') + '.patch'
219
220     def mbox(self):
221         comment = None
222         try:
223             comment = Comment.objects.get(patch = self, msgid = self.msgid)
224         except Exception:
225             pass
226
227         body = ''
228         if comment:
229             body = comment.content.strip() + "\n"
230
231         responses = False
232         for comment in Comment.objects.filter(patch = self) \
233                 .exclude(msgid = self.msgid):
234             body += comment.patch_responses()
235
236         if body:
237             body += '\n'
238
239         body += self.content
240
241         mail = MIMEText(body, _charset = 'utf-8')
242         mail['Subject'] = self.name
243         mail['Date'] = email.utils.formatdate(
244                         time.mktime(self.date.utctimetuple()))
245         mail['From'] = str(self.submitter)
246         mail['X-Patchwork-Id'] = str(self.id)
247         mail.set_unixfrom('From patchwork ' + self.date.ctime())
248
249         return mail
250
251
252     @models.permalink
253     def get_absolute_url(self):
254         return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
255
256     class Meta:
257         verbose_name_plural = 'Patches'
258         ordering = ['date']
259
260 class Comment(models.Model):
261     patch = models.ForeignKey(Patch)
262     msgid = models.CharField(max_length=255, unique = True)
263     submitter = models.ForeignKey(Person)
264     date = models.DateTimeField(default = datetime.datetime.now)
265     headers = models.TextField(blank = True)
266     content = models.TextField()
267
268     response_re = re.compile('^(Acked|Signed-off|Nacked)-by: .*$', re.M)
269
270     def patch_responses(self):
271         return ''.join([ match.group(0) + '\n' for match in \
272                                 self.response_re.finditer(self.content)])
273
274     class Meta:
275         ordering = ['date']
276
277 class Bundle(models.Model):
278     owner = models.ForeignKey(User)
279     project = models.ForeignKey(Project)
280     name = models.CharField(max_length = 50, null = False, blank = False)
281     patches = models.ManyToManyField(Patch)
282     public = models.BooleanField(default = False)
283
284     def n_patches(self):
285         return self.patches.all().count()
286
287     class Meta:
288         unique_together = [('owner', 'name')]
289
290     def public_url(self):
291         if not self.public:
292             return None
293         site = Site.objects.get_current()
294         return 'http://%s%s' % (site.domain,
295                 reverse('patchwork.views.bundle.public',
296                         kwargs = {
297                                 'username': self.owner.username,
298                                 'bundlename': self.name
299                         }))
300
301     def mbox(self):
302         return '\n'.join([p.mbox().as_string(True) \
303                         for p in self.patches.all()])
304
305 class UserPersonConfirmation(models.Model):
306     user = models.ForeignKey(User)
307     email = models.CharField(max_length = 200)
308     key = HashField()
309     date = models.DateTimeField(default=datetime.datetime.now)
310     active = models.BooleanField(default = True)
311
312     def confirm(self):
313         if not self.active:
314             return
315         person = None
316         try:
317             person = Person.objects.get(email = self.email)
318         except Exception:
319             pass
320         if not person:
321             person = Person(email = self.email)
322
323         person.link_to_user(self.user)
324         person.save()
325         self.active = False
326         self.save()
327
328     def save(self):
329         max = 1 << 32
330         if self.key == '':
331             str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
332             self.key = self._meta.get_field('key').construct(str).hexdigest()
333         super(UserPersonConfirmation, self).save()
334
335