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)
133 allowedchars = string.ascii_lowercase + string.digits
135 for i in range(1, 32):
136 str += random.choice(allowedchars)
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)
151 person = Person.objects.get(email = self.email)
155 person = Person(email = self.email)
157 person.link_to_user(self.user)
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)
170 ordering = ['ordering']
172 class HashField(models.Field):
173 __metaclass__ = models.SubfieldBase
175 def __init__(self, algorithm = 'sha1', *args, **kwargs):
176 self.algorithm = algorithm
182 if algorithm == 'sha1':
184 self.hash_constructor = sha.new
185 elif algorithm == 'md5':
187 self.hash_constructor = md5.new
189 raise NameError("Unknown algorithm '%s'" % algorithm)
191 super(HashField, self).__init__(*args, **kwargs)
195 n_bytes = len(hashlib.new(self.algorithm).digest())
197 n_bytes = len(self.hash_constructor().digest())
198 if settings.DATABASE_ENGINE.startswith('postgresql'):
200 elif settings.DATABASE_ENGINE == 'mysql':
201 return 'binary(%d)' % n_bytes
203 raise Exception("Unknown database engine '%s'" % \
204 settings.DATABASE_ENGINE)
206 def to_python(self, value):
209 def get_db_prep_save(self, value):
210 return ''.join(map(lambda x: '\\%03o' % ord(x), value))
212 def get_manipulator_field_objs(self):
213 return [oldforms.TextField]
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)
233 return Comment.objects.filter(patch = self)
239 self.state = State.objects.get(ordering = 0)
241 if self.hash is None:
242 self.hash = hash_patch(self.content).digest()
244 super(Patch, self).save()
246 def is_editable(self, user):
247 if not user.is_authenticated():
250 if self.submitter.user == user or self.delegate == user:
253 profile = user.get_profile()
254 return self.project in user.get_profile().maintainer_projects.all()
257 f = PatchForm(instance = self, prefix = self.id)
261 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
262 str = fname_re.sub('-', self.name)
263 return str.strip('-') + '.patch'
268 comment = Comment.objects.get(msgid = self.msgid)
274 body = comment.content.strip() + "\n\n"
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())
289 def get_absolute_url(self):
290 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
293 verbose_name_plural = 'Patches'
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()
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)
315 return self.patches.all().count()
318 unique_together = [('owner', 'name')]
320 def public_url(self):
323 site = Site.objects.get_current()
324 return 'http://%s%s' % (site.domain,
325 reverse('patchwork.views.bundle.public',
327 'username': self.owner.username,
328 'bundlename': self.name
332 return '\n'.join([p.mbox().as_string(True) \
333 for p in self.patches.all()])