Inital commit
[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
149     def create_user(self):
150         user = User.objects.create_user(self.username,
151                 self.email, self.password)
152         user.first_name = self.first_name
153         user.last_name = self.last_name
154         user.save()
155         profile = UserProfile(user = user)
156         profile.save()
157         self.delete()
158
159         # link a person to this user. if none exists, create.
160         person = None
161         try:
162             person = Person.objects.get(email = user.email)
163         except Exception:
164             pass
165         if not person:
166             person = Person(email = user.email)
167
168         person.link_to_user(user)
169         person.save()
170
171         return user
172
173     class Admin:
174         pass
175
176 class UserPersonConfirmation(models.Model):
177     user = models.ForeignKey(User)
178     email = models.CharField(max_length = 200)
179     date = models.DateTimeField(default=datetime.datetime.now)
180     key = models.CharField(max_length = 32, default = _confirm_key)
181
182     def confirm(self):
183         person = None
184         try:
185             person = Person.objects.get(email = self.email)
186         except Exception:
187             pass
188         if not person:
189             person = Person(email = self.email)
190
191         person.link_to_user(self.user)
192         person.save()
193
194
195     class Admin:
196         pass
197
198
199 class State(models.Model):
200     name = models.CharField(max_length = 100)
201     ordering = models.IntegerField(unique = True)
202     action_required = models.BooleanField(default = True)
203
204     def __str__(self):
205         return self.name
206
207     class Meta:
208         ordering = ['ordering']
209
210     class Admin:
211         pass
212
213 class HashField(models.Field):
214     __metaclass__ = models.SubfieldBase
215
216     def __init__(self, algorithm = 'sha1', *args, **kwargs):
217         self.algorithm = algorithm
218         super(HashField, self).__init__(*args, **kwargs)
219
220     def db_type(self):
221         n_bytes = len(hashlib.new(self.algorithm).digest())
222         if settings.DATABASE_ENGINE == 'postgresql':
223             return 'bytea'
224         elif settings.DATABASE_ENGINE == 'mysql':
225             return 'binary(%d)' % n_bytes
226
227     def to_python(self, value):
228         return value
229
230     def get_db_prep_save(self, value):
231         return ''.join(map(lambda x: '\\%03o' % ord(x), value))
232
233     def get_manipulator_field_objs(self):
234         return [oldforms.TextField]
235
236 class Patch(models.Model):
237     project = models.ForeignKey(Project)
238     msgid = models.CharField(max_length=255, unique = True)
239     name = models.CharField(max_length=255)
240     date = models.DateTimeField(default=datetime.datetime.now)
241     submitter = models.ForeignKey(Person)
242     delegate = models.ForeignKey(User, blank = True, null = True)
243     state = models.ForeignKey(State)
244     archived = models.BooleanField(default = False)
245     headers = models.TextField(blank = True)
246     content = models.TextField()
247     commit_ref = models.CharField(max_length=255, null = True, blank = True)
248     hash = HashField()
249
250     def __str__(self):
251         return self.name
252
253     def comments(self):
254         return Comment.objects.filter(patch = self)
255
256     def save(self):
257         try:
258             s = self.state
259         except:
260             self.state = State.objects.get(ordering =  0)
261         if hash is None:
262             print "no hash"
263         super(Patch, self).save()
264
265     def is_editable(self, user):
266         if not user.is_authenticated():
267             return False
268
269         if self.submitter.user == user or self.delegate == user:
270             return True
271
272         profile = user.get_profile()
273         return self.project in user.get_profile().maintainer_projects.all()
274
275     def form(self):
276         f = PatchForm(instance = self, prefix = self.id)
277         return f
278
279     def filename(self):
280         fname_re = re.compile('[^-_A-Za-z0-9\.]+')
281         str = fname_re.sub('-', self.name)
282         return str.strip('-') + '.patch'
283
284     def mbox(self):
285         comment = None
286         try:
287             comment = Comment.objects.get(msgid = self.msgid)
288         except Exception:
289             pass
290
291         body = ''
292         if comment:
293             body = comment.content.strip() + "\n\n"
294         body += self.content
295
296         mail = MIMEText(body)
297         mail['Subject'] = self.name
298         mail['Date'] = email.utils.formatdate(
299                         time.mktime(self.date.utctimetuple()))
300         mail['From'] = str(self.submitter)
301         mail['X-Patchwork-Id'] = str(self.id)
302         mail.set_unixfrom('From patchwork ' + self.date.ctime())
303
304         return mail
305
306
307     @models.permalink
308     def get_absolute_url(self):
309         return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
310
311     class Meta:
312         verbose_name_plural = 'Patches'
313         ordering = ['date']
314
315     class Admin:
316         pass
317
318 class Comment(models.Model):
319     patch = models.ForeignKey(Patch)
320     msgid = models.CharField(max_length=255, unique = True)
321     submitter = models.ForeignKey(Person)
322     date = models.DateTimeField(default = datetime.datetime.now)
323     headers = models.TextField(blank = True)
324     content = models.TextField()
325
326     class Admin:
327         pass
328
329     class Meta:
330         ordering = ['date']
331
332 class Bundle(models.Model):
333     owner = models.ForeignKey(User)
334     project = models.ForeignKey(Project)
335     name = models.CharField(max_length = 50, null = False, blank = False)
336     patches = models.ManyToManyField(Patch)
337     public = models.BooleanField(default = False)
338
339     def n_patches(self):
340         return self.patches.all().count()
341
342     class Meta:
343         unique_together = [('owner', 'name')]
344
345     class Admin:
346         pass
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.patches.all()])
362