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)
196 n_bytes = len(hashlib.new(self.algorithm).digest())
198 n_bytes = len(self.hash_constructor().digest())
199 if settings.DATABASE_ENGINE.startswith('postgresql'):
201 elif settings.DATABASE_ENGINE == 'mysql':
202 return 'binary(%d)' % n_bytes
204 raise Exception("Unknown database engine '%s'" % \
205 settings.DATABASE_ENGINE)
207 def to_python(self, value):
210 def get_db_prep_save(self, value):
211 return ''.join(map(lambda x: '\\%03o' % ord(x), value))
213 def get_manipulator_field_objs(self):
214 return [oldforms.TextField]
216 class Patch(models.Model):
217 project = models.ForeignKey(Project)
218 msgid = models.CharField(max_length=255, unique = True)
219 name = models.CharField(max_length=255)
220 date = models.DateTimeField(default=datetime.datetime.now)
221 submitter = models.ForeignKey(Person)
222 delegate = models.ForeignKey(User, blank = True, null = True)
223 state = models.ForeignKey(State)
224 archived = models.BooleanField(default = False)
225 headers = models.TextField(blank = True)
226 content = models.TextField()
227 commit_ref = models.CharField(max_length=255, null = True, blank = True)
228 hash = HashField(null = True)
234 return Comment.objects.filter(patch = self)
240 self.state = State.objects.get(ordering = 0)
242 if self.hash is None:
243 self.hash = hash_patch(self.content).digest()
245 super(Patch, self).save()
247 def is_editable(self, user):
248 if not user.is_authenticated():
251 if self.submitter.user == user or self.delegate == user:
254 profile = user.get_profile()
255 return self.project in user.get_profile().maintainer_projects.all()
258 f = PatchForm(instance = self, prefix = self.id)
262 fname_re = re.compile('[^-_A-Za-z0-9\.]+')
263 str = fname_re.sub('-', self.name)
264 return str.strip('-') + '.patch'
269 comment = Comment.objects.get(msgid = self.msgid)
275 body = comment.content.strip() + "\n\n"
278 mail = MIMEText(body)
279 mail['Subject'] = self.name
280 mail['Date'] = email.utils.formatdate(
281 time.mktime(self.date.utctimetuple()))
282 mail['From'] = str(self.submitter)
283 mail['X-Patchwork-Id'] = str(self.id)
284 mail.set_unixfrom('From patchwork ' + self.date.ctime())
290 def get_absolute_url(self):
291 return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
294 verbose_name_plural = 'Patches'
297 class Comment(models.Model):
298 patch = models.ForeignKey(Patch)
299 msgid = models.CharField(max_length=255, unique = True)
300 submitter = models.ForeignKey(Person)
301 date = models.DateTimeField(default = datetime.datetime.now)
302 headers = models.TextField(blank = True)
303 content = models.TextField()
308 class Bundle(models.Model):
309 owner = models.ForeignKey(User)
310 project = models.ForeignKey(Project)
311 name = models.CharField(max_length = 50, null = False, blank = False)
312 patches = models.ManyToManyField(Patch)
313 public = models.BooleanField(default = False)
316 return self.patches.all().count()
319 unique_together = [('owner', 'name')]
321 def public_url(self):
324 site = Site.objects.get_current()
325 return 'http://%s%s' % (site.domain,
326 reverse('patchwork.views.bundle.public',
328 'username': self.owner.username,
329 'bundlename': self.name
333 return '\n'.join([p.mbox().as_string(True) \
334 for p in self.patches.all()])