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