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.nonmultipart import MIMENonMultipart
35 from email.encoders import encode_7or8bit
38 # Python 2.4 compatibility
39 from email.MIMENonMultipart import MIMENonMultipart
40 from email.Encoders import encode_7or8bit
42 email.utils = email.Utils
44 class Person(models.Model):
45 email = models.CharField(max_length=255, unique = True)
46 name = models.CharField(max_length=255, null = True)
47 user = models.ForeignKey(User, null = True)
51 return '%s <%s>' % (self.name, self.email)
55 def link_to_user(self, user):
56 self.name = user.get_profile().name()
60 verbose_name_plural = 'People'
62 class Project(models.Model):
63 linkname = models.CharField(max_length=255, unique=True)
64 name = models.CharField(max_length=255, unique=True)
65 listid = models.CharField(max_length=255, unique=True)
66 listemail = models.CharField(max_length=200)
71 class UserProfile(models.Model):
72 user = models.ForeignKey(User, unique = True)
73 primary_project = models.ForeignKey(Project, null = True)
74 maintainer_projects = models.ManyToManyField(Project,
75 related_name = 'maintainer_project')
76 send_email = models.BooleanField(default = False,
77 help_text = 'Selecting this option allows patchwork to send ' +
78 'email on your behalf')
79 patches_per_page = models.PositiveIntegerField(default = 100,
80 null = False, blank = False,
81 help_text = 'Number of patches to display per page')
84 if self.user.first_name or self.user.last_name:
85 names = filter(bool, [self.user.first_name, self.user.last_name])
86 return ' '.join(names)
87 return self.user.username
89 def contributor_projects(self):
90 submitters = Person.objects.filter(user = self.user)
91 return Project.objects \
94 submitter__in = submitters) \
95 .values('project_id').query)
98 def sync_person(self):
101 def n_todo_patches(self):
102 return self.todo_patches().count()
104 def todo_patches(self, project = None):
106 # filter on project, if necessary
108 qs = Patch.objects.filter(project = project)
112 qs = qs.filter(archived = False) \
113 .filter(delegate = self.user) \
114 .filter(state__in = \
115 State.objects.filter(action_required = True) \
120 super(UserProfile, self).save()
121 people = Person.objects.filter(email = self.user.email)
123 person = Person(email = self.user.email,
124 name = self.name(), user = self.user)
127 for person in people:
128 person.link_to_user(self.user)
134 class State(models.Model):
135 name = models.CharField(max_length = 100)
136 ordering = models.IntegerField(unique = True)
137 action_required = models.BooleanField(default = True)
143 ordering = ['ordering']
145 class HashField(models.CharField):
146 __metaclass__ = models.SubfieldBase
148 def __init__(self, algorithm = 'sha1', *args, **kwargs):
149 self.algorithm = algorithm
152 def _construct(string = ''):
153 return hashlib.new(self.algorithm, string)
154 self.construct = _construct
155 self.n_bytes = len(hashlib.new(self.algorithm).hexdigest())
157 modules = { 'sha1': 'sha', 'md5': 'md5'}
159 if algorithm not in modules.keys():
160 raise NameError("Unknown algorithm '%s'" % algorithm)
162 self.construct = __import__(modules[algorithm]).new
164 self.n_bytes = len(self.construct().hexdigest())
166 kwargs['max_length'] = self.n_bytes
167 super(HashField, self).__init__(*args, **kwargs)
170 return 'char(%d)' % self.n_bytes
172 class PatchMbox(MIMENonMultipart):
173 patch_charset = 'utf-8'
174 def __init__(self, _text):
175 MIMENonMultipart.__init__(self, 'text', 'plain',
176 **{'charset': self.patch_charset})
177 self.set_payload(_text.encode(self.patch_charset))
180 class Patch(models.Model):
181 project = models.ForeignKey(Project)
182 msgid = models.CharField(max_length=255, unique = True)
183 name = models.CharField(max_length=255)
184 date = models.DateTimeField(default=datetime.datetime.now)
185 submitter = models.ForeignKey(Person)
186 delegate = models.ForeignKey(User, blank = True, null = True)
187 state = models.ForeignKey(State)
188 archived = models.BooleanField(default = False)
189 headers = models.TextField(blank = True)
190 content = models.TextField()
191 commit_ref = models.CharField(max_length=255, null = True, blank = True)
192 hash = HashField(null = True, db_index = True)
198 return Comment.objects.filter(patch = self)
204 self.state = State.objects.get(ordering = 0)
206 if self.hash is None:
207 self.hash = hash_patch(self.content).hexdigest()
209 super(Patch, self).save()
211 def is_editable(self, user):
212 if not user.is_authenticated():
215 if self.submitter.user == user or self.delegate == user:
218 profile = user.get_profile()
219 return self.project in user.get_profile().maintainer_projects.all()
222 f = PatchForm(instance = self, prefix = self.id)
226 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
227 str = fname_re.sub('-', self.name)
228 return str.strip('-') + '.patch'
233 comment = Comment.objects.get(patch = self, msgid = self.msgid)
239 body = comment.content.strip() + "\n"
242 for comment in Comment.objects.filter(patch = self) \
243 .exclude(msgid = self.msgid):
244 body += comment.patch_responses()
251 mail = PatchMbox(body)
252 mail['Subject'] = self.name
253 mail['Date'] = email.utils.formatdate(
254 time.mktime(self.date.utctimetuple()))
255 mail['From'] = unicode(self.submitter)
256 mail['X-Patchwork-Id'] = str(self.id)
257 mail.set_unixfrom('From patchwork ' + self.date.ctime())
263 def get_absolute_url(self):
264 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
267 verbose_name_plural = 'Patches'
270 class Comment(models.Model):
271 patch = models.ForeignKey(Patch)
272 msgid = models.CharField(max_length=255, unique = True)
273 submitter = models.ForeignKey(Person)
274 date = models.DateTimeField(default = datetime.datetime.now)
275 headers = models.TextField(blank = True)
276 content = models.TextField()
278 response_re = re.compile('^(Acked|Signed-off|Nacked)-by: .*$', re.M)
280 def patch_responses(self):
281 return ''.join([ match.group(0) + '\n' for match in \
282 self.response_re.finditer(self.content)])
287 class Bundle(models.Model):
288 owner = models.ForeignKey(User)
289 project = models.ForeignKey(Project)
290 name = models.CharField(max_length = 50, null = False, blank = False)
291 patches = models.ManyToManyField(Patch)
292 public = models.BooleanField(default = False)
295 return self.patches.all().count()
298 unique_together = [('owner', 'name')]
300 def public_url(self):
303 site = Site.objects.get_current()
304 return 'http://%s%s' % (site.domain,
305 reverse('patchwork.views.bundle.public',
307 'username': self.owner.username,
308 'bundlename': self.name
312 return '\n'.join([p.mbox().as_string(True) \
313 for p in self.patches.all()])
315 class UserPersonConfirmation(models.Model):
316 user = models.ForeignKey(User)
317 email = models.CharField(max_length = 200)
319 date = models.DateTimeField(default=datetime.datetime.now)
320 active = models.BooleanField(default = True)
327 person = Person.objects.get(email = self.email)
331 person = Person(email = self.email)
333 person.link_to_user(self.user)
341 str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
342 self.key = self._meta.get_field('key').construct(str).hexdigest()
343 super(UserPersonConfirmation, self).save()