Ejemplo n.º 1
0
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()
Ejemplo n.º 2
0
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'),
        ]
Ejemplo n.º 3
0
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'
Ejemplo n.º 4
0
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')]