X-Git-Url: https://git.ozlabs.org/?p=patchwork;a=blobdiff_plain;f=apps%2Fpatchwork%2Fmodels.py;h=f21d07322c544d3860a3128f65f76a7faa12b880;hp=e3fc9c7e1f7b068e0bb176c8d83ac35fb8d1873a;hb=41f19b6643b44768dc06561c992c04ed6148477d;hpb=ac876f29bdead6fc9e649bb35d7a7a1dd314a182 diff --git a/apps/patchwork/models.py b/apps/patchwork/models.py index e3fc9c7..f21d073 100644 --- a/apps/patchwork/models.py +++ b/apps/patchwork/models.py @@ -22,24 +22,33 @@ from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.contrib.sites.models import Site from django.conf import settings -import django.oldforms as oldforms +from patchwork.parser import hash_patch import re import datetime, time -import string import random -import hashlib -from email.mime.text import MIMEText -import email.utils + +try: + from email.mime.nonmultipart import MIMENonMultipart + from email.encoders import encode_7or8bit + from email.parser import HeaderParser + import email.utils +except ImportError: + # Python 2.4 compatibility + from email.MIMENonMultipart import MIMENonMultipart + from email.Encoders import encode_7or8bit + from email.Parser import HeaderParser + import email.Utils + email.utils = email.Utils class Person(models.Model): email = models.CharField(max_length=255, unique = True) - name = models.CharField(max_length=255, null = True) - user = models.ForeignKey(User, null = True) + name = models.CharField(max_length=255, null = True, blank = True) + user = models.ForeignKey(User, null = True, blank = True) - def __str__(self): + def __unicode__(self): if self.name: - return '%s <%s>' % (self.name, self.email) + return u'%s <%s>' % (self.name, self.email) else: return self.email @@ -56,12 +65,17 @@ class Project(models.Model): listid = models.CharField(max_length=255, unique=True) listemail = models.CharField(max_length=200) - def __str__(self): + def __unicode__(self): return self.name + def is_editable(self, user): + if not user.is_authenticated(): + return False + return self in user.get_profile().maintainer_projects.all() + class UserProfile(models.Model): user = models.ForeignKey(User, unique = True) - primary_project = models.ForeignKey(Project, null = True) + primary_project = models.ForeignKey(Project, null = True, blank = True) maintainer_projects = models.ManyToManyField(Project, related_name = 'maintainer_project') send_email = models.BooleanField(default = False, @@ -73,17 +87,16 @@ class UserProfile(models.Model): def name(self): if self.user.first_name or self.user.last_name: - names = filter(bool, [self.user.first_name, self.user.last_name]) - return ' '.join(names) + names = filter(bool, [self.user.first_name, self.user.last_name]) + return u' '.join(names) return self.user.username def contributor_projects(self): submitters = Person.objects.filter(user = self.user) - return Project.objects \ - .filter(id__in = \ - Patch.objects.filter( - submitter__in = submitters) \ - .values('project_id').query) + return Project.objects.filter(id__in = + Patch.objects.filter( + submitter__in = submitters) + .values('project_id').query) def sync_person(self): @@ -102,129 +115,83 @@ class UserProfile(models.Model): qs = qs.filter(archived = False) \ .filter(delegate = self.user) \ - .filter(state__in = \ - State.objects.filter(action_required = True) \ + .filter(state__in = + State.objects.filter(action_required = True) .values('pk').query) return qs def save(self): - super(UserProfile, self).save() - people = Person.objects.filter(email = self.user.email) - if not people: - person = Person(email = self.user.email, - name = self.name(), user = self.user) + super(UserProfile, self).save() + people = Person.objects.filter(email = self.user.email) + if not people: + person = Person(email = self.user.email, + name = self.name(), user = self.user) person.save() - else: - for person in people: - person.user = self.user - person.save() + else: + for person in people: + person.link_to_user(self.user) + person.save() - def __str__(self): + def __unicode__(self): return self.name() -def _confirm_key(): - allowedchars = string.ascii_lowercase + string.digits - str = '' - for i in range(1, 32): - str += random.choice(allowedchars) - return str; - -class RegistrationRequest(models.Model): - username = models.CharField(max_length = 30, unique = True) - first_name = models.CharField(max_length = 50) - last_name = models.CharField(max_length = 50) - email = models.CharField(max_length = 200, unique = True) - password = models.CharField(max_length = 200) - key = models.CharField(max_length = 32, default = _confirm_key) - date = models.DateTimeField(default=datetime.datetime.now) - active = models.BooleanField(default = True) - - def create_user(self): - if not self.active: - return - user = User.objects.create_user(self.username, - self.email, self.password) - user.first_name = self.first_name - user.last_name = self.last_name - user.save() - profile = UserProfile(user = user) - profile.save() - self.active = False - self.save() +def _user_created_callback(sender, created, instance, **kwargs): + if not created: + return + profile = UserProfile(user = instance) + profile.save() - # link a person to this user. if none exists, create. - person = None - try: - person = Person.objects.get(email = user.email) - except Exception: - pass - if not person: - person = Person(email = user.email) - - person.link_to_user(user) - person.save() - - return user - -class UserPersonConfirmation(models.Model): - user = models.ForeignKey(User) - email = models.CharField(max_length = 200) - key = models.CharField(max_length = 32, default = _confirm_key) - date = models.DateTimeField(default=datetime.datetime.now) - active = models.BooleanField(default = True) - - def confirm(self): - if not self.active: - return - person = None - try: - person = Person.objects.get(email = self.email) - except Exception: - pass - if not person: - person = Person(email = self.email) - - person.link_to_user(self.user) - person.save() - self.active = False +models.signals.post_save.connect(_user_created_callback, sender = User) class State(models.Model): name = models.CharField(max_length = 100) ordering = models.IntegerField(unique = True) action_required = models.BooleanField(default = True) - def __str__(self): + def __unicode__(self): return self.name class Meta: ordering = ['ordering'] -class HashField(models.Field): +class HashField(models.CharField): __metaclass__ = models.SubfieldBase def __init__(self, algorithm = 'sha1', *args, **kwargs): self.algorithm = algorithm - super(HashField, self).__init__(*args, **kwargs) + try: + import hashlib + def _construct(string = ''): + return hashlib.new(self.algorithm, string) + self.construct = _construct + self.n_bytes = len(hashlib.new(self.algorithm).hexdigest()) + except ImportError: + modules = { 'sha1': 'sha', 'md5': 'md5'} - def db_type(self): - n_bytes = len(hashlib.new(self.algorithm).digest()) - if settings.DATABASE_ENGINE == 'postgresql': - return 'bytea' - elif settings.DATABASE_ENGINE == 'mysql': - return 'binary(%d)' % n_bytes + if algorithm not in modules.keys(): + raise NameError("Unknown algorithm '%s'" % algorithm) + + self.construct = __import__(modules[algorithm]).new + + self.n_bytes = len(self.construct().hexdigest()) - def to_python(self, value): - return value + kwargs['max_length'] = self.n_bytes + super(HashField, self).__init__(*args, **kwargs) - def get_db_prep_save(self, value): - return ''.join(map(lambda x: '\\%03o' % ord(x), value)) + def db_type(self): + return 'char(%d)' % self.n_bytes - def get_manipulator_field_objs(self): - return [oldforms.TextField] +class PatchMbox(MIMENonMultipart): + patch_charset = 'utf-8' + def __init__(self, _text): + MIMENonMultipart.__init__(self, 'text', 'plain', + **{'charset': self.patch_charset}) + self.set_payload(_text.encode(self.patch_charset)) + encode_7or8bit(self) class Patch(models.Model): project = models.ForeignKey(Project) - msgid = models.CharField(max_length=255, unique = True) + msgid = models.CharField(max_length=255) name = models.CharField(max_length=255) date = models.DateTimeField(default=datetime.datetime.now) submitter = models.ForeignKey(Person) @@ -232,23 +199,26 @@ class Patch(models.Model): state = models.ForeignKey(State) archived = models.BooleanField(default = False) headers = models.TextField(blank = True) - content = models.TextField() + content = models.TextField(null = True, blank = True) + pull_url = models.CharField(max_length=255, null = True, blank = True) commit_ref = models.CharField(max_length=255, null = True, blank = True) - hash = HashField() + hash = HashField(null = True, blank = True) - def __str__(self): + def __unicode__(self): return self.name def comments(self): - return Comment.objects.filter(patch = self) + return Comment.objects.filter(patch = self) def save(self): - try: + try: s = self.state except: self.state = State.objects.get(ordering = 0) - if hash is None: - print "no hash" + + if self.hash is None and self.content is not None: + self.hash = hash_patch(self.content).hexdigest() + super(Patch, self).save() def is_editable(self, user): @@ -258,12 +228,7 @@ class Patch(models.Model): if self.submitter.user == user or self.delegate == user: return True - profile = user.get_profile() - return self.project in user.get_profile().maintainer_projects.all() - - def form(self): - f = PatchForm(instance = self, prefix = self.id) - return f + return self.project.is_editable(user) def filename(self): fname_re = re.compile('[^-_A-Za-z0-9\.]+') @@ -271,27 +236,56 @@ class Patch(models.Model): return str.strip('-') + '.patch' def mbox(self): + postscript_re = re.compile('\n-{2,3} ?\n') + comment = None try: - comment = Comment.objects.get(msgid = self.msgid) + comment = Comment.objects.get(patch = self, msgid = self.msgid) except Exception: pass body = '' if comment: - body = comment.content.strip() + "\n\n" - body += self.content + body = comment.content.strip() + "\n" + + parts = postscript_re.split(body, 1) + if len(parts) == 2: + (body, postscript) = parts + body = body.strip() + "\n" + postscript = postscript.strip() + "\n" + else: + postscript = '' + + for comment in Comment.objects.filter(patch = self) \ + .exclude(msgid = self.msgid): + body += comment.patch_responses() + + if body: + body += '\n' + + if postscript: + body += '---\n' + postscript.strip() + '\n' - mail = MIMEText(body) + if self.content: + body += '\n' + self.content + + mail = PatchMbox(body) mail['Subject'] = self.name mail['Date'] = email.utils.formatdate( time.mktime(self.date.utctimetuple())) - mail['From'] = str(self.submitter) + mail['From'] = unicode(self.submitter) mail['X-Patchwork-Id'] = str(self.id) + mail['Message-Id'] = self.msgid mail.set_unixfrom('From patchwork ' + self.date.ctime()) - return mail + copied_headers = ['To', 'Cc'] + orig_headers = HeaderParser().parsestr(str(self.headers)) + for header in copied_headers: + if header in orig_headers: + mail[header] = orig_headers[header] + + return mail @models.permalink def get_absolute_url(self): @@ -300,28 +294,59 @@ class Patch(models.Model): class Meta: verbose_name_plural = 'Patches' ordering = ['date'] + unique_together = [('msgid', 'project')] class Comment(models.Model): patch = models.ForeignKey(Patch) - msgid = models.CharField(max_length=255, unique = True) + msgid = models.CharField(max_length=255) submitter = models.ForeignKey(Person) date = models.DateTimeField(default = datetime.datetime.now) headers = models.TextField(blank = True) content = models.TextField() + response_re = re.compile( \ + '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$', + re.M | re.I) + + def patch_responses(self): + return ''.join([ match.group(0) + '\n' for match in + self.response_re.finditer(self.content)]) + class Meta: ordering = ['date'] + unique_together = [('msgid', 'patch')] class Bundle(models.Model): owner = models.ForeignKey(User) project = models.ForeignKey(Project) name = models.CharField(max_length = 50, null = False, blank = False) - patches = models.ManyToManyField(Patch) + patches = models.ManyToManyField(Patch, through = 'BundlePatch') public = models.BooleanField(default = False) def n_patches(self): return self.patches.all().count() + def ordered_patches(self): + return self.patches.order_by('bundlepatch__order') + + def append_patch(self, patch): + # todo: use the aggregate queries in django 1.1 + orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \ + .values('order') + + if len(orders) > 0: + max_order = orders[0]['order'] + else: + max_order = 0 + + # see if the patch is already in this bundle + if BundlePatch.objects.filter(bundle = self, patch = patch).count(): + raise Exception("patch is already in bundle") + + bp = BundlePatch.objects.create(bundle = self, patch = patch, + order = max_order + 1) + bp.save() + class Meta: unique_together = [('owner', 'name')] @@ -337,6 +362,47 @@ class Bundle(models.Model): })) def mbox(self): - return '\n'.join([p.mbox().as_string(True) \ - for p in self.patches.all()]) + return '\n'.join([p.mbox().as_string(True) + for p in self.ordered_patches()]) + +class BundlePatch(models.Model): + patch = models.ForeignKey(Patch) + bundle = models.ForeignKey(Bundle) + order = models.IntegerField() + + class Meta: + unique_together = [('bundle', 'patch')] + ordering = ['order'] + +class EmailConfirmation(models.Model): + validity = datetime.timedelta(days = settings.CONFIRMATION_VALIDITY_DAYS) + type = models.CharField(max_length = 20, choices = [ + ('userperson', 'User-Person association'), + ('registration', 'Registration'), + ('optout', 'Email opt-out'), + ]) + email = models.CharField(max_length = 200) + user = models.ForeignKey(User, null = True) + key = HashField() + date = models.DateTimeField(default = datetime.datetime.now) + active = models.BooleanField(default = True) + + def deactivate(self): + self.active = False + self.save() + + def is_valid(self): + return self.date + self.validity > datetime.datetime.now() + + def save(self): + max = 1 << 32 + if self.key == '': + str = '%s%s%d' % (self.user, self.email, random.randint(0, max)) + self.key = self._meta.get_field('key').construct(str).hexdigest() + super(EmailConfirmation, self).save() + +class EmailOptout(models.Model): + email = models.CharField(max_length = 200, primary_key = True) + def __unicode__(self): + return self.email