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