]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/models.py
Hook-up hashing infrastructure
[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             n_bytes = len(hashlib.new(self.algorithm).digest())
196         else:
197             n_bytes = len(self.hash_constructor().digest())
198         if settings.DATABASE_ENGINE.startswith('postgresql'):
199             return 'bytea'
200         elif settings.DATABASE_ENGINE == 'mysql':
201             return 'binary(%d)' % n_bytes
202         else:
203             raise Exception("Unknown database engine '%s'" % \
204                             settings.DATABASE_ENGINE)
205
206     def to_python(self, value):
207         return value
208
209     def get_db_prep_save(self, value):
210         return ''.join(map(lambda x: '\\%03o' % ord(x), value))
211
212     def get_manipulator_field_objs(self):
213         return [oldforms.TextField]
214
215 class Patch(models.Model):
216     project = models.ForeignKey(Project)
217     msgid = models.CharField(max_length=255, unique = True)
218     name = models.CharField(max_length=255)
219     date = models.DateTimeField(default=datetime.datetime.now)
220     submitter = models.ForeignKey(Person)
221     delegate = models.ForeignKey(User, blank = True, null = True)
222     state = models.ForeignKey(State)
223     archived = models.BooleanField(default = False)
224     headers = models.TextField(blank = True)
225     content = models.TextField()
226     commit_ref = models.CharField(max_length=255, null = True, blank = True)
227     hash = HashField(null = True)
228
229     def __str__(self):
230         return self.name
231
232     def comments(self):
233         return Comment.objects.filter(patch = self)
234
235     def save(self):
236         try:
237             s = self.state
238         except:
239             self.state = State.objects.get(ordering =  0)
240
241         if self.hash is None:
242             self.hash = hash_patch(self.content).digest()
243
244         super(Patch, self).save()
245
246     def is_editable(self, user):
247         if not user.is_authenticated():
248             return False
249
250         if self.submitter.user == user or self.delegate == user:
251             return True
252
253         profile = user.get_profile()
254         return self.project in user.get_profile().maintainer_projects.all()
255
256     def form(self):
257         f = PatchForm(instance = self, prefix = self.id)
258         return f
259
260     def filename(self):
261         fname_re = re.compile('[^-_A-Za-z0-9\.]+')
262         str = fname_re.sub('-', self.name)
263         return str.strip('-') + '.patch'
264
265     def mbox(self):
266         comment = None
267         try:
268             comment = Comment.objects.get(msgid = self.msgid)
269         except Exception:
270             pass
271
272         body = ''
273         if comment:
274             body = comment.content.strip() + "\n\n"
275         body += self.content
276
277         mail = MIMEText(body)
278         mail['Subject'] = self.name
279         mail['Date'] = email.utils.formatdate(
280                         time.mktime(self.date.utctimetuple()))
281         mail['From'] = str(self.submitter)
282         mail['X-Patchwork-Id'] = str(self.id)
283         mail.set_unixfrom('From patchwork ' + self.date.ctime())
284
285         return mail
286
287
288     @models.permalink
289     def get_absolute_url(self):
290         return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
291
292     class Meta:
293         verbose_name_plural = 'Patches'
294         ordering = ['date']
295
296 class Comment(models.Model):
297     patch = models.ForeignKey(Patch)
298     msgid = models.CharField(max_length=255, unique = True)
299     submitter = models.ForeignKey(Person)
300     date = models.DateTimeField(default = datetime.datetime.now)
301     headers = models.TextField(blank = True)
302     content = models.TextField()
303
304     class Meta:
305         ordering = ['date']
306
307 class Bundle(models.Model):
308     owner = models.ForeignKey(User)
309     project = models.ForeignKey(Project)
310     name = models.CharField(max_length = 50, null = False, blank = False)
311     patches = models.ManyToManyField(Patch)
312     public = models.BooleanField(default = False)
313
314     def n_patches(self):
315         return self.patches.all().count()
316
317     class Meta:
318         unique_together = [('owner', 'name')]
319
320     def public_url(self):
321         if not self.public:
322             return None
323         site = Site.objects.get_current()
324         return 'http://%s%s' % (site.domain,
325                 reverse('patchwork.views.bundle.public',
326                         kwargs = {
327                                 'username': self.owner.username,
328                                 'bundlename': self.name
329                         }))
330
331     def mbox(self):
332         return '\n'.join([p.mbox().as_string(True) \
333                         for p in self.patches.all()])
334