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
33 from email.mime.nonmultipart import MIMENonMultipart
34 from email.encoders import encode_7or8bit
37 # Python 2.4 compatibility
38 from email.MIMENonMultipart import MIMENonMultipart
39 from email.Encoders import encode_7or8bit
41 email.utils = email.Utils
43 class Person(models.Model):
44 email = models.CharField(max_length=255, unique = True)
45 name = models.CharField(max_length=255, null = True)
46 user = models.ForeignKey(User, null = True)
50 return '%s <%s>' % (self.name, self.email)
54 def link_to_user(self, user):
55 self.name = user.get_profile().name()
59 verbose_name_plural = 'People'
61 class Project(models.Model):
62 linkname = models.CharField(max_length=255, unique=True)
63 name = models.CharField(max_length=255, unique=True)
64 listid = models.CharField(max_length=255, unique=True)
65 listemail = models.CharField(max_length=200)
70 class UserProfile(models.Model):
71 user = models.ForeignKey(User, unique = True)
72 primary_project = models.ForeignKey(Project, null = True)
73 maintainer_projects = models.ManyToManyField(Project,
74 related_name = 'maintainer_project')
75 send_email = models.BooleanField(default = False,
76 help_text = 'Selecting this option allows patchwork to send ' +
77 'email on your behalf')
78 patches_per_page = models.PositiveIntegerField(default = 100,
79 null = False, blank = False,
80 help_text = 'Number of patches to display per page')
83 if self.user.first_name or self.user.last_name:
84 names = filter(bool, [self.user.first_name, self.user.last_name])
85 return ' '.join(names)
86 return self.user.username
88 def contributor_projects(self):
89 submitters = Person.objects.filter(user = self.user)
90 return Project.objects \
93 submitter__in = submitters) \
94 .values('project_id').query)
97 def sync_person(self):
100 def n_todo_patches(self):
101 return self.todo_patches().count()
103 def todo_patches(self, project = None):
105 # filter on project, if necessary
107 qs = Patch.objects.filter(project = project)
111 qs = qs.filter(archived = False) \
112 .filter(delegate = self.user) \
113 .filter(state__in = \
114 State.objects.filter(action_required = True) \
119 super(UserProfile, self).save()
120 people = Person.objects.filter(email = self.user.email)
122 person = Person(email = self.user.email,
123 name = self.name(), user = self.user)
126 for person in people:
127 person.link_to_user(self.user)
133 class State(models.Model):
134 name = models.CharField(max_length = 100)
135 ordering = models.IntegerField(unique = True)
136 action_required = models.BooleanField(default = True)
142 ordering = ['ordering']
144 class HashField(models.CharField):
145 __metaclass__ = models.SubfieldBase
147 def __init__(self, algorithm = 'sha1', *args, **kwargs):
148 self.algorithm = algorithm
151 def _construct(string = ''):
152 return hashlib.new(self.algorithm, string)
153 self.construct = _construct
154 self.n_bytes = len(hashlib.new(self.algorithm).hexdigest())
156 modules = { 'sha1': 'sha', 'md5': 'md5'}
158 if algorithm not in modules.keys():
159 raise NameError("Unknown algorithm '%s'" % algorithm)
161 self.construct = __import__(modules[algorithm]).new
163 self.n_bytes = len(self.construct().hexdigest())
165 kwargs['max_length'] = self.n_bytes
166 super(HashField, self).__init__(*args, **kwargs)
169 return 'char(%d)' % self.n_bytes
171 class PatchMbox(MIMENonMultipart):
172 patch_charset = 'utf-8'
173 def __init__(self, _text):
174 MIMENonMultipart.__init__(self, 'text', 'plain',
175 **{'charset': self.patch_charset})
176 self.set_payload(_text.encode(self.patch_charset))
179 class Patch(models.Model):
180 project = models.ForeignKey(Project)
181 msgid = models.CharField(max_length=255)
182 name = models.CharField(max_length=255)
183 date = models.DateTimeField(default=datetime.datetime.now)
184 submitter = models.ForeignKey(Person)
185 delegate = models.ForeignKey(User, blank = True, null = True)
186 state = models.ForeignKey(State)
187 archived = models.BooleanField(default = False)
188 headers = models.TextField(blank = True)
189 content = models.TextField()
190 commit_ref = models.CharField(max_length=255, null = True, blank = True)
191 hash = HashField(null = True, db_index = True)
197 return Comment.objects.filter(patch = self)
203 self.state = State.objects.get(ordering = 0)
205 if self.hash is None:
206 self.hash = hash_patch(self.content).hexdigest()
208 super(Patch, self).save()
210 def is_editable(self, user):
211 if not user.is_authenticated():
214 if self.submitter.user == user or self.delegate == user:
217 profile = user.get_profile()
218 return self.project in user.get_profile().maintainer_projects.all()
221 f = PatchForm(instance = self, prefix = self.id)
225 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
226 str = fname_re.sub('-', self.name)
227 return str.strip('-') + '.patch'
232 comment = Comment.objects.get(patch = self, msgid = self.msgid)
238 body = comment.content.strip() + "\n"
241 for comment in Comment.objects.filter(patch = self) \
242 .exclude(msgid = self.msgid):
243 body += comment.patch_responses()
250 mail = PatchMbox(body)
251 mail['Subject'] = self.name
252 mail['Date'] = email.utils.formatdate(
253 time.mktime(self.date.utctimetuple()))
254 mail['From'] = unicode(self.submitter)
255 mail['X-Patchwork-Id'] = str(self.id)
256 mail.set_unixfrom('From patchwork ' + self.date.ctime())
262 def get_absolute_url(self):
263 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
266 verbose_name_plural = 'Patches'
268 unique_together = [('msgid', 'project')]
270 class Comment(models.Model):
271 patch = models.ForeignKey(Patch)
272 msgid = models.CharField(max_length=255)
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)])
286 unique_together = [('msgid', 'patch')]
288 class Bundle(models.Model):
289 owner = models.ForeignKey(User)
290 project = models.ForeignKey(Project)
291 name = models.CharField(max_length = 50, null = False, blank = False)
292 patches = models.ManyToManyField(Patch)
293 public = models.BooleanField(default = False)
296 return self.patches.all().count()
299 unique_together = [('owner', 'name')]
301 def public_url(self):
304 site = Site.objects.get_current()
305 return 'http://%s%s' % (site.domain,
306 reverse('patchwork.views.bundle.public',
308 'username': self.owner.username,
309 'bundlename': self.name
313 return '\n'.join([p.mbox().as_string(True) \
314 for p in self.patches.all()])
316 class UserPersonConfirmation(models.Model):
317 user = models.ForeignKey(User)
318 email = models.CharField(max_length = 200)
320 date = models.DateTimeField(default=datetime.datetime.now)
321 active = models.BooleanField(default = True)
328 person = Person.objects.get(email__iexact = self.email)
332 person = Person(email = self.email)
334 person.link_to_user(self.user)
342 str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
343 self.key = self._meta.get_field('key').construct(str).hexdigest()
344 super(UserPersonConfirmation, self).save()