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, unique = True)
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'
269 class Comment(models.Model):
270 patch = models.ForeignKey(Patch)
271 msgid = models.CharField(max_length=255, unique = True)
272 submitter = models.ForeignKey(Person)
273 date = models.DateTimeField(default = datetime.datetime.now)
274 headers = models.TextField(blank = True)
275 content = models.TextField()
277 response_re = re.compile('^(Acked|Signed-off|Nacked)-by: .*$', re.M)
279 def patch_responses(self):
280 return ''.join([ match.group(0) + '\n' for match in \
281 self.response_re.finditer(self.content)])
286 class Bundle(models.Model):
287 owner = models.ForeignKey(User)
288 project = models.ForeignKey(Project)
289 name = models.CharField(max_length = 50, null = False, blank = False)
290 patches = models.ManyToManyField(Patch)
291 public = models.BooleanField(default = False)
294 return self.patches.all().count()
297 unique_together = [('owner', 'name')]
299 def public_url(self):
302 site = Site.objects.get_current()
303 return 'http://%s%s' % (site.domain,
304 reverse('patchwork.views.bundle.public',
306 'username': self.owner.username,
307 'bundlename': self.name
311 return '\n'.join([p.mbox().as_string(True) \
312 for p in self.patches.all()])
314 class UserPersonConfirmation(models.Model):
315 user = models.ForeignKey(User)
316 email = models.CharField(max_length = 200)
318 date = models.DateTimeField(default=datetime.datetime.now)
319 active = models.BooleanField(default = True)
326 person = Person.objects.get(email = self.email)
330 person = Person(email = self.email)
332 person.link_to_user(self.user)
340 str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
341 self.key = self._meta.get_field('key').construct(str).hexdigest()
342 super(UserPersonConfirmation, self).save()