1 # Patchwork - automated patch tracking system
2 # Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
4 # This file is part of the Patchwork package.
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.
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.
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
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
34 from email.mime.text import MIMEText
37 # Python 2.4 compatibility
38 from email.MIMEText import MIMEText
40 email.utils = email.Utils
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)
49 return '%s <%s>' % (self.name, self.email)
53 def link_to_user(self, user):
54 self.name = user.get_profile().name()
58 verbose_name_plural = 'People'
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)
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')
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
87 def contributor_projects(self):
88 submitters = Person.objects.filter(user = self.user)
89 return Project.objects \
92 submitter__in = submitters) \
93 .values('project_id').query)
96 def sync_person(self):
99 def n_todo_patches(self):
100 return self.todo_patches().count()
102 def todo_patches(self, project = None):
104 # filter on project, if necessary
106 qs = Patch.objects.filter(project = project)
110 qs = qs.filter(archived = False) \
111 .filter(delegate = self.user) \
112 .filter(state__in = \
113 State.objects.filter(action_required = True) \
118 super(UserProfile, self).save()
119 people = Person.objects.filter(email = self.user.email)
121 person = Person(email = self.user.email,
122 name = self.name(), user = self.user)
125 for person in people:
126 person.link_to_user(self.user)
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)
141 ordering = ['ordering']
143 class HashField(models.CharField):
144 __metaclass__ = models.SubfieldBase
146 def __init__(self, algorithm = 'sha1', *args, **kwargs):
147 self.algorithm = algorithm
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())
155 modules = { 'sha1': 'sha', 'md5': 'md5'}
157 if algorithm not in modules.keys():
158 raise NameError("Unknown algorithm '%s'" % algorithm)
160 self.construct = __import__(modules[algorithm]).new
162 self.n_bytes = len(self.construct().hexdigest())
164 kwargs['max_length'] = self.n_bytes
165 super(HashField, self).__init__(*args, **kwargs)
168 return 'char(%d)' % self.n_bytes
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)
188 return Comment.objects.filter(patch = self)
194 self.state = State.objects.get(ordering = 0)
196 if self.hash is None:
197 self.hash = hash_patch(self.content).hexdigest()
199 super(Patch, self).save()
201 def is_editable(self, user):
202 if not user.is_authenticated():
205 if self.submitter.user == user or self.delegate == user:
208 profile = user.get_profile()
209 return self.project in user.get_profile().maintainer_projects.all()
212 f = PatchForm(instance = self, prefix = self.id)
216 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
217 str = fname_re.sub('-', self.name)
218 return str.strip('-') + '.patch'
223 comment = Comment.objects.get(patch = self, msgid = self.msgid)
229 body = comment.content.strip() + "\n"
232 for comment in Comment.objects.filter(patch = self) \
233 .exclude(msgid = self.msgid):
234 body += comment.patch_responses()
241 mail = MIMEText(body)
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())
253 def get_absolute_url(self):
254 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
257 verbose_name_plural = 'Patches'
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()
268 response_re = re.compile('^(Acked|Signed-off|Nacked)-by: .*$', re.M)
270 def patch_responses(self):
271 return ''.join([ match.group(0) + '\n' for match in \
272 self.response_re.finditer(self.content)])
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)
285 return self.patches.all().count()
288 unique_together = [('owner', 'name')]
290 def public_url(self):
293 site = Site.objects.get_current()
294 return 'http://%s%s' % (site.domain,
295 reverse('patchwork.views.bundle.public',
297 'username': self.owner.username,
298 'bundlename': self.name
302 return '\n'.join([p.mbox().as_string(True) \
303 for p in self.patches.all()])
305 class UserPersonConfirmation(models.Model):
306 user = models.ForeignKey(User)
307 email = models.CharField(max_length = 200)
309 date = models.DateTimeField(default=datetime.datetime.now)
310 active = models.BooleanField(default = True)
317 person = Person.objects.get(email = self.email)
321 person = Person(email = self.email)
323 person.link_to_user(self.user)
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()