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, on_delete=models.CASCADE) key = HashField() date = models.DateTimeField(default=datetime.datetime.utcnow) active = models.BooleanField(default=True) def deactivate(self): self.active = False self.save() def is_valid(self): return self.date + self.validity > datetime.datetime.utcnow() def save(self, *args, **kwargs): limit = 1 << 32 if not self.key: key = '%s%s%d' % (self.user, self.email, random.randint(0, limit)) self.key = self._meta.get_field('key').construct(key).hexdigest() super(EmailConfirmation, self).save()
class Patch(Submission): # patch metadata diff = models.TextField(null=True, blank=True) commit_ref = models.CharField(max_length=255, null=True, blank=True) pull_url = models.CharField(max_length=255, null=True, blank=True) tags = models.ManyToManyField(Tag, through=PatchTag) # patchwork metadata delegate = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE) state = models.ForeignKey(State, null=True, on_delete=models.CASCADE) archived = models.BooleanField(default=False) hash = HashField(null=True, blank=True) # duplicate project from submission in subclass so we can count the # patches in a project without needing to do a JOIN. patch_project = models.ForeignKey(Project, on_delete=models.CASCADE) # series metadata series = models.ForeignKey( 'Series', null=True, blank=True, on_delete=models.CASCADE, related_name='patches', related_query_name='patch') number = models.PositiveSmallIntegerField( default=None, null=True, help_text='The number assigned to this patch in the series') objects = PatchManager() @staticmethod def extract_tags(content, tags): counts = Counter() for tag in tags: regex = re.compile(tag.pattern, re.MULTILINE | re.IGNORECASE) counts[tag] = len(regex.findall(content)) return counts def _set_tag(self, tag, count): if count == 0: self.patchtag_set.filter(tag=tag).delete() return patchtag, _ = PatchTag.objects.get_or_create(patch=self, tag=tag) if patchtag.count != count: patchtag.count = count patchtag.save() def refresh_tag_counts(self): tags = self.project.tags counter = Counter() if self.content: counter += self.extract_tags(self.content, tags) for comment in self.comments.all(): counter = counter + self.extract_tags(comment.content, tags) for tag in tags: self._set_tag(tag, counter[tag]) def save(self, *args, **kwargs): if not hasattr(self, 'state') or not self.state: self.state = get_default_initial_patch_state() if self.hash is None and self.diff is not None: self.hash = hash_diff(self.diff) super(Patch, self).save(**kwargs) self.refresh_tag_counts() def is_editable(self, user): if not user.is_authenticated: return False if user in [self.submitter.user, self.delegate]: self._edited_by = user return True if self.project.is_editable(user): self._edited_by = user return True return False @property def combined_check_state(self): """Return the combined state for all checks. Generate the combined check's state for this patch. This check is one of the following, based on the value of each unique check: * failure, if any context's latest check reports as failure * warning, if any context's latest check reports as warning * pending, if there are no checks, or a context's latest Check reports as pending * success, if latest checks for all contexts reports as success """ state_names = dict(Check.STATE_CHOICES) states = [check.state for check in self.checks] if not states: return state_names[Check.STATE_PENDING] for state in [Check.STATE_FAIL, Check.STATE_WARNING, Check.STATE_PENDING]: # order sensitive if state in states: return state_names[state] return state_names[Check.STATE_SUCCESS] @property def checks(self): """Return the list of unique checks. Generate a list of checks associated with this patch for each type of Check. Only "unique" checks are considered, identified by their 'context' field. This means, given n checks with the same 'context', the newest check is the only one counted regardless of its value. The end result will be a association of types to number of unique checks for said type. """ unique = {} duplicates = [] for check in self.check_set.all(): ctx = check.context user = check.user_id if user in unique and ctx in unique[user]: # recheck condition - ignore the older result if unique[user][ctx].date > check.date: duplicates.append(check.id) continue duplicates.append(unique[user][ctx].id) if user not in unique: unique[user] = {} unique[user][ctx] = check # filter out the "duplicates" or older, now-invalid results # Why don't we use filter or exclude here? Surprisingly, it's # an optimisation in the common case. Where we're looking at # checks in anything that uses a generic_list() in the view, # we do a prefetch_related('check_set'). But, if we then do a # .filter or a .exclude, that throws out the existing, cached # information, and does another query. (See the Django docs on # prefetch_related.) So, do it 'by hand' in Python. We can # also be confident that this won't be worse, seeing as we've # just iterated over self.check_set.all() *anyway*. return [c for c in self.check_set.all() if c.id not in duplicates] @property def check_count(self): """Generate a list of unique checks for each patch. Compile a list of checks associated with this patch for each type of check. Only "unique" checks are considered, identified by their 'context' field. This means, given n checks with the same 'context', the newest check is the only one counted regardless of its value. The end result will be a association of types to number of unique checks for said type. """ counts = {key: 0 for key, _ in Check.STATE_CHOICES} for check in self.checks: counts[check.state] += 1 return counts def get_absolute_url(self): return reverse('patch-detail', kwargs={'project_id': self.project.linkname, 'msgid': self.url_msgid}) def get_mbox_url(self): return reverse('patch-mbox', kwargs={'project_id': self.project.linkname, 'msgid': self.url_msgid}) def __str__(self): return self.name class Meta: verbose_name_plural = 'Patches' base_manager_name = 'objects' unique_together = [('series', 'number')] indexes = [ # This is a covering index for the /list/ query models.Index(fields=['archived', 'patch_project', 'state', 'delegate'], name='patch_list_covering_idx'), ]
class Patch(SeriesMixin, Submission): # patch metadata diff = models.TextField(null=True, blank=True) commit_ref = models.CharField(max_length=255, null=True, blank=True) pull_url = models.CharField(max_length=255, null=True, blank=True) tags = models.ManyToManyField(Tag, through=PatchTag) # patchwork metadata delegate = models.ForeignKey(User, blank=True, null=True) state = models.ForeignKey(State, null=True) archived = models.BooleanField(default=False) hash = HashField(null=True, blank=True) objects = PatchManager() @staticmethod def extract_tags(content, tags): counts = Counter() for tag in tags: regex = re.compile(tag.pattern, re.MULTILINE | re.IGNORECASE) counts[tag] = len(regex.findall(content)) return counts @staticmethod def hash_diff(diff): """Generate a hash from a diff.""" hunk_re = re.compile(r'^\@\@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? \@\@') filename_re = re.compile(r'^(---|\+\+\+) (\S+)') # normalise spaces diff = diff.replace('\r', '') diff = diff.strip() + '\n' prefixes = ['-', '+', ' '] hash = hashlib.sha1() for line in diff.split('\n'): if len(line) <= 0: continue hunk_match = hunk_re.match(line) filename_match = filename_re.match(line) if filename_match: # normalise -p1 top-directories if filename_match.group(1) == '---': filename = 'a/' else: filename = 'b/' filename += '/'.join(filename_match.group(2).split('/')[1:]) line = filename_match.group(1) + ' ' + filename elif hunk_match: # remove line numbers, but leave line counts def fn(x): if not x: return 1 return int(x) line_nos = list(map(fn, hunk_match.groups())) line = '@@ -%d +%d @@' % tuple(line_nos) elif line[0] in prefixes: # if we have a +, - or context line, leave as-is pass else: # other lines are ignored continue hash.update((line + '\n').encode('utf-8')) return hash def _set_tag(self, tag, count): if count == 0: self.patchtag_set.filter(tag=tag).delete() return patchtag, _ = PatchTag.objects.get_or_create(patch=self, tag=tag) if patchtag.count != count: patchtag.count = count patchtag.save() def refresh_tag_counts(self): tags = self.project.tags counter = Counter() if self.content: counter += self.extract_tags(self.content, tags) for comment in self.comments.all(): counter = counter + self.extract_tags(comment.content, tags) for tag in tags: self._set_tag(tag, counter[tag]) def save(self, *args, **kwargs): if not hasattr(self, 'state') or not self.state: self.state = get_default_initial_patch_state() if self.hash is None and self.diff is not None: self.hash = self.hash_diff(self.diff).hexdigest() super(Patch, self).save(**kwargs) self.refresh_tag_counts() def is_editable(self, user): if not user.is_authenticated(): return False if user in [self.submitter.user, self.delegate]: return True return self.project.is_editable(user) @property def filename(self): fname_re = re.compile(r'[^-_A-Za-z0-9\.]+') str = fname_re.sub('-', self.name) return str.strip('-') + '.patch' @property def combined_check_state(self): """Return the combined state for all checks. Generate the combined check's state for this patch. This check is one of the following, based on the value of each unique check: * failure, if any context's latest check reports as failure * warning, if any context's latest check reports as warning * pending, if there are no checks, or a context's latest Check reports as pending * success, if latest checks for all contexts reports as success """ state_names = dict(Check.STATE_CHOICES) states = [check.state for check in self.checks] if not states: return state_names[Check.STATE_PENDING] for state in [Check.STATE_FAIL, Check.STATE_WARNING, Check.STATE_PENDING]: # order sensitive if state in states: return state_names[state] return state_names[Check.STATE_SUCCESS] @property def checks(self): """Return the list of unique checks. Generate a list of checks associated with this patch for each type of Check. Only "unique" checks are considered, identified by their 'context' field. This means, given n checks with the same 'context', the newest check is the only one counted regardless of its value. The end result will be a association of types to number of unique checks for said type. """ unique = {} duplicates = [] for check in self.check_set.all(): ctx = check.context user = check.user if user in unique and ctx in unique[user]: # recheck condition - ignore the older result if unique[user][ctx].date > check.date: duplicates.append(check.id) continue duplicates.append(unique[user][ctx].id) if user not in unique: unique[user] = {} unique[user][ctx] = check # filter out the "duplicates" or older, now-invalid results # Why don't we use filter or exclude here? Surprisingly, it's # an optimisation in the common case. Where we're looking at # checks in anything that uses a generic_list() in the view, # we do a prefetch_related('check_set'). But, if we then do a # .filter or a .exclude, that throws out the existing, cached # information, and does another query. (See the Django docs on # prefetch_related.) So, do it 'by hand' in Python. We can # also be confident that this won't be worse, seeing as we've # just iterated over self.check_set.all() *anyway*. return [c for c in self.check_set.all() if c.id not in duplicates] @property def check_count(self): """Generate a list of unique checks for each patch. Compile a list of checks associated with this patch for each type of check. Only "unique" checks are considered, identified by their 'context' field. This means, given n checks with the same 'context', the newest check is the only one counted regardless of its value. The end result will be a association of types to number of unique checks for said type. """ counts = {key: 0 for key, _ in Check.STATE_CHOICES} for check in self.checks: counts[check.state] += 1 return counts @models.permalink def get_absolute_url(self): return ('patch-detail', (), {'patch_id': self.id}) @models.permalink def get_mbox_url(self): return ('patch-mbox', (), {'patch_id': self.id}) def __str__(self): return self.name class Meta: verbose_name_plural = 'Patches'
class Patch(models.Model): project = models.ForeignKey(Project) msgid = models.CharField(max_length=255) name = models.CharField(max_length=255) date = models.DateTimeField(default=datetime.datetime.now) last_updated = models.DateTimeField(auto_now=True) submitter = models.ForeignKey(Person) delegate = models.ForeignKey(User, blank=True, null=True) state = models.ForeignKey(State, null=True) archived = models.BooleanField(default=False) headers = models.TextField(blank=True) 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(null=True, blank=True) tags = models.ManyToManyField(Tag, through=PatchTag) objects = PatchManager() def commit_message(self): """Retrieves the commit message""" return Comment.objects.filter(patch=self, msgid=self.msgid) def answers(self): """Retrieves the answers (ie all comments but the commit message)""" return Comment.objects.filter(Q(patch=self) & ~Q(msgid=self.msgid)) def comments(self): """Retrieves all comments of this patch ie. the commit message and the answers""" return Comment.objects.filter(patch=self) def _set_tag(self, tag, count): if count == 0: self.patchtag_set.filter(tag=tag).delete() return (patchtag, _) = PatchTag.objects.get_or_create(patch=self, tag=tag) if patchtag.count != count: patchtag.count = count patchtag.save() def refresh_tag_counts(self): tags = self.project.tags counter = Counter() for comment in self.comment_set.all(): counter = counter + extract_tags(comment.content, tags) for tag in tags: self._set_tag(tag, counter[tag]) def save(self): if not hasattr(self, 'state') or not self.state: self.state = get_default_initial_patch_state() 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): if not user.is_authenticated(): return False if self.submitter.user == user or self.delegate == user: return True return self.project.is_editable(user) def filename(self): return filename(self.name, '.patch') def human_name(self): return self.name @models.permalink def get_absolute_url(self): return ('patchwork.views.patch.patch', (), {'patch_id': self.id}) def __str__(self): return self.name class Meta: verbose_name_plural = 'Patches' ordering = ['date'] unique_together = [('msgid', 'project')]