class Project(models.Model):
    linkname = models.CharField(max_length=255, unique=True)
    name = models.CharField(max_length=255, unique=True)
    description = models.TextField(blank=True, null=True)
    listid = models.CharField(max_length=255)
    listemail = models.CharField(max_length=200)
    web_url = models.CharField(max_length=2000, blank=True)
    scm_url = models.CharField(max_length=2000, blank=True)
    webscm_url = models.CharField(max_length=2000, blank=True)
    send_notifications = models.BooleanField(default=False)
    use_tags = models.BooleanField(default=True)
    git_send_email_only = models.BooleanField(default=False)
    subject_prefix_tags = models.CharField(max_length=255, blank=True,
               help_text='Comma separated list of tags')

    @cached_property
    def tags(self):
        if not self.use_tags:
            return []
        return list(Tag.objects.all())

    def get_subject_prefix_tags(self):
        return get_comma_separated_field(self.subject_prefix_tags)

    def get_listemail_tag(self):
        return self.listemail.split("@")[0]

    def __str__(self):
        return self.name

    class Meta:
        ordering = ['linkname'] class Person(models.Model):
    email = models.CharField(max_length=255, unique=True)
    name = models.CharField(max_length=255, null=True, blank=True)
    user = models.ForeignKey(User, null=True, blank=True,
                             on_delete=models.SET_NULL)

    def display_name(self):
        if self.name:
            return self.name
        else:
            return self.email

    def email_name(self):
        if (self.name):
            return "\"%s\" <%s>" % (self.name, self.email)
        else:
            return self.email

    def link_to_user(self, user):
        self.name = user.profile.name()
        self.user = user

    def __str__(self):
        return self.display_name()

    class Meta:
        verbose_name_plural = 'People' class UserProfile(models.Model):
    user = models.OneToOneField(User, unique=True, related_name='profile',
                                on_delete=models.CASCADE)
    primary_project = models.ForeignKey(Project, null=True, blank=True,
                                        on_delete=models.CASCADE)
    maintainer_projects = models.ManyToManyField(Project,
             related_name='maintainer_project', blank=True)
    send_email = models.BooleanField(default=False,
             help_text='Selecting this option allows patchwork to send '
             'email on your behalf')
    patches_per_page = models.PositiveIntegerField(
            default=100, null=False, blank=False,
            help_text='Number of patches to display per page')

    def name(self):
        return user_name(self.user)

    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)

    def sync_person(self):
        pass

    def n_todo(self):
        return self.todo_patches().count() + self.todo_series().count()

    def todo_patches(self, project=None):

        # filter on project, if necessary
        if project:
            qs = Patch.objects.filter(project=project)
        else:
            qs = Patch.objects

        qs = qs.filter(archived=False) \
            .filter(delegate=self.user) \
            .filter(state__in=State.objects.filter(action_required=True)
                    .values('pk').query)
        return qs

    def todo_series(self, project=None):
        # filter on project, if necessary
        if project:
            qs = Series.objects.filter(project=project)
        else:
            qs = Series.objects

        qs = qs.filter(Q(reviewer=self.user),
                       ~Q(last_revision__state=RevisionState.DONE))
        return qs

    def __str__(self):
        return self.name() class State(models.Model):
    name = models.CharField(max_length=100)
    ordering = models.IntegerField(unique=True)
    action_required = models.BooleanField(default=True)

    @classmethod
    def from_string(cls, name):
        return State.objects.get(name__iexact=name)

    def __str__(self):
        return self.name

    class Meta:
        ordering = ['ordering'] class Patch(models.Model):
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    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, on_delete=models.CASCADE)
    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)
    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 series(self):
        try:
            rev = SeriesRevisionPatch.objects.filter(patch=self)[0].revision
            return rev.series
        except Exception:
            return None

    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 filename(self):
        return filename(self.name, '.patch')

    def human_name(self):
        return self.name

    @models.permalink
    def get_absolute_url(self):
        return ('patch', (), {'patch_id': self.id})

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = 'Patches'
        ordering = ['date']
        unique_together = [('msgid', 'project')] class Comment(models.Model):
    patch = models.ForeignKey(Patch, on_delete=models.CASCADE)
    msgid = models.CharField(max_length=255)
    submitter = models.ForeignKey(Person, on_delete=models.CASCADE)
    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|Fixes): .*$',
        re.M | re.I)

    def patch_responses(self):
        return ''.join([match.group(0) + '\n' for match in
                        self.response_re.finditer(self.content)])

    def save(self, *args, **kwargs):
        super(Comment, self).save(*args, **kwargs)
        self.patch.refresh_tag_counts()

    def delete(self, *args, **kwargs):
        super(Comment, self).delete(*args, **kwargs)
        self.patch.refresh_tag_counts()

    class Meta:
        ordering = ['date']
        unique_together = [('msgid', 'patch')] class Bundle(models.Model):
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    name = models.CharField(max_length=50, null=False, blank=False)
    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()

    def public_url(self):
        if not self.public:
            return None
        site = Site.objects.get_current()
        return 'http://%s%s' % (site.domain,
                                reverse('bundle',
                                        kwargs={
                                            'username': self.owner.username,
                                            'bundlename': self.name
                                        }))

    @models.permalink
    def get_absolute_url(self):
        return ('bundle', (), {
            'username': self.owner.username,
            'bundlename': self.name,
        })

    class Meta:
        unique_together = [('owner', 'name')] class Tag(models.Model):
    name = models.CharField(max_length=20)
    pattern = models.CharField(max_length=50,
            help_text='A simple regex to match the tag in the content of '
                      'a message. Will be used with MULTILINE and IGNORECASE '
                      'flags. eg. ^Acked-by:')
    abbrev = models.CharField(max_length=2, unique=True,
            help_text='Short (one-or-two letter) abbreviation for the tag, '
                       'used in table column headers')

    @property
    def attr_name(self):
        return 'tag_%d_count' % self.id

    def __str__(self):
        return self.name

    class Meta:
        ordering = ['abbrev'] class EventLog(models.Model):
    event = models.ForeignKey(Event, on_delete=models.CASCADE)
    event_time = models.DateTimeField(auto_now=True)
    series = models.ForeignKey(Series, null=True, on_delete=models.CASCADE)
    user = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
    parameters = jsonfield.JSONField(null=True)
    patch = models.ForeignKey(Patch, null=True, on_delete=models.CASCADE)

    class Meta:
        ordering = ['-event_time'] class Test(models.Model):
    # no mail, default so test systems/scripts can have a grace period to
    # settle down and give useful results
    RECIPIENT_NONE = 0
    # send mail only to submitter
    RECIPIENT_SUBMITTER = 1
    # send mail to submitter and mailing-list in Cc
    RECIPIENT_MAILING_LIST = 2
    # send mail to the addresses listed in the mail_to_list field only
    RECIPIENT_TO_LIST = 3
    RECIPIENT_CHOICES = (
        (RECIPIENT_NONE, 'none'),
        (RECIPIENT_SUBMITTER, 'submitter'),
        (RECIPIENT_MAILING_LIST, 'mailing list'),
        (RECIPIENT_TO_LIST, 'recipient list'),
    )

    # send result mail on any state (but pending)
    CONDITION_ALWAYS = 0
    # send result mail on warning or failure
    CONDITION_ON_WARNING = 1
    # send result mail on error
    CONDITION_ON_FAILURE = 2
    CONDITION_CHOICES = (
        (CONDITION_ALWAYS, 'always'),
        (CONDITION_ON_WARNING, 'on warning/failure'),
        (CONDITION_ON_FAILURE, 'on failure'),
    )

    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    name = models.CharField(max_length=255)
    mail_recipient = models.SmallIntegerField(choices=RECIPIENT_CHOICES,
                                              default=RECIPIENT_NONE)
    # email addresses in these lists are always added to the To: and Cc:fields,
    # unless we don't want to send any email at all.
    mail_to_list = models.CharField(max_length=255, blank=True, null=True,
                                    help_text='Comma separated list of emails')
    mail_cc_list = models.CharField(max_length=255, blank=True, null=True,
                                    help_text='Comma separated list of emails')
    mail_condition = models.SmallIntegerField(choices=CONDITION_CHOICES,
                                              default=CONDITION_ALWAYS)

    class Meta:
        unique_together = [('project', 'name')]

    def get_to_list(self):
        return get_comma_separated_field(self.mail_to_list)

    def get_cc_list(self):
        return get_comma_separated_field(self.mail_cc_list)

    def __str__(self):
        return self.name class TestResult(models.Model):

    test = models.ForeignKey(Test, on_delete=models.CASCADE)
    revision = models.ForeignKey(SeriesRevision, blank=True, null=True,
                                 on_delete=models.CASCADE)
    patch = models.ForeignKey(Patch, blank=True, null=True,
                              on_delete=models.CASCADE)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    date = models.DateTimeField(auto_now=True)
    state = models.SmallIntegerField(choices=TestState.STATE_CHOICES)
    url = models.URLField(blank=True, null=True)
    summary = models.TextField(blank=True, null=True)

    def __str__(self):
        return self.get_state_display()

    class Meta:
        unique_together = [('test', 'revision'), ('test', 'patch')] class DelegationRule(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    path = models.CharField(max_length=255)
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    priority = models.IntegerField(default=0)

    def __str__(self):
        return self.path

    class Meta:
        ordering = ['-priority', 'path']
        unique_together = (('path', 'project')) class Series(models.Model):
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    name = models.CharField(max_length=200, default=SERIES_DEFAULT_NAME)
    submitter = models.ForeignKey(Person, related_name='submitters',
                                  on_delete=models.CASCADE)
    reviewer = models.ForeignKey(User, related_name='reviewers', null=True,
                                 blank=True, on_delete=models.CASCADE)
    submitted = models.DateTimeField(default=datetime.datetime.now)
    last_updated = models.DateTimeField(auto_now=True)
    # direct access to the latest revision so we can get the latest revision
    # information with a JOIN
    last_revision = models.OneToOneField('SeriesRevision', null=True,
                                         related_name='+',
                                         on_delete=models.CASCADE)

    def revisions(self):
        return SeriesRevision.objects.filter(series=self)

    def latest_revision(self):
        return self.revisions().reverse()[0]

    def get_absolute_url(self):
        return reverse('series', kwargs={'series': self.pk})

    def dump(self):
        print('')
        print('===')
        print('Series: %s' % self)
        print('    version: %d' % self.version)
        print('    n_patches: %d' % self.n_patches)
        for rev in self.revisions():
            print('    rev %d:' % rev.version)
            i = 1
            for patch in rev.ordered_patches():
                print('        patch %d:' % i)
                print('            subject: %s' % patch.name)
                print('            msgid  : %s' % patch.msgid)
                i += 1

    def filename(self):
        return filename(self.name, '.mbox')

    def human_name(self):
        if self.name == SERIES_DEFAULT_NAME:
            if self.last_revision:
                ordered_patches = self.last_revision.ordered_patches()
                if ordered_patches:
                    return "series starting with " + ordered_patches[0].name
            return "Incomplete Series"
        else:
            return self.name

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = 'Series'
        ordering = ["-id"] class SeriesRevision(models.Model):
    series = models.ForeignKey(Series, on_delete=models.CASCADE)
    version = models.IntegerField(default=1)
    root_msgid = models.CharField(max_length=255)
    cover_letter = models.TextField(null=True, blank=True)
    n_patches = models.IntegerField(default=0)
    patches = models.ManyToManyField(Patch, through='SeriesRevisionPatch')
    state = models.SmallIntegerField(choices=RevisionState.CHOICES,
                                     default=RevisionState.INCOMPLETE)
    state_summary = jsonfield.JSONField(null=True)
    test_state = models.SmallIntegerField(choices=TestState.STATE_CHOICES,
                                          null=True, blank=True)
    is_rerun = models.BooleanField(default=False)

    class Meta:
        unique_together = [('series', 'version')]
        ordering = ['version']

    def ordered_patches(self):
        return self.patches.order_by('seriesrevisionpatch__order')

    def add_patch(self, patch, order):
        # see if the patch is already in this revision
        if SeriesRevisionPatch.objects.filter(revision=self,
                                              patch=patch).count():
            raise Exception("patch is already in revision")

        sp = SeriesRevisionPatch.objects.create(revision=self, patch=patch,
                                                order=order)
        sp.save()

        if self.patches_count == self.n_patches:
            series_revision_complete.send(sender=self.__class__, revision=self)

    @property
    def patches_count(self):
        return self.patches.count()

    @property
    def is_complete(self):
        return self.patches_count >= self.n_patches

    @property
    def is_strange(self):
        if self.patches_count > self.n_patches:
            return True

        names = [patch.name for patch in self.ordered_patches()]

        # one patch "series", not much to break
        if len(names) == 1:
            return False

        for index, name in enumerate(names):
            match = re.search(r"\[[^]]*(\d+)/(\d+).*\]", name)
            if not match:  # we have a patch without proper numbering
                return True
            if (index + 1) != int(match.group(1)):  # numbering is off
                return True

        return False

    def duplicate_meta(self):
        new = SeriesRevision.objects.get(pk=self.pk)
        new.pk = None
        new.cover_letter = None
        new.version = self.version + 1
        new.test_state = None
        new.save()

        return new

    def duplicate(self, exclude_patches=()):
        """Create a new revision based on 'self', incrementing the version
           and populating the new revision with all 'self' patches.
           exclude_patch (a list of 'order's) can be used to exclude
           patches from the operation"""
        new = self.duplicate_meta()
        order = 0
        for p in self.ordered_patches():
            order += 1
            if order in exclude_patches:
                continue
            new.add_patch(p, order)
        return new

    def refresh_test_state(self):
        results = TestResult.objects.filter(revision=self)
        if results.count() > 0:
            self.test_state = max([r.state for r in results])
        else:
            self.test_state = None
        self.save()
        self.series.save()

    def human_name(self):
        name = self.series.name
        if name == SERIES_DEFAULT_NAME:
            name = "series starting with " + self.ordered_patches()[0].name
        if self.version > 1:
            name += " (rev%d)" % self.version
        return name

    def __str__(self):
        return "Revision " + str(self.version)
from django import forms
from django.contrib import admin
from patchwork.models import (Project, Person, UserProfile, State, Patch,
                              Comment, Bundle, Tag, EventLog, Test, TestResult,
                              DelegationRule, Series, SeriesRevision)
# Patchwork - automated patch tracking system
# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
#
# This file is part of the Patchwork package.
#
# Patchwork is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Patchwork is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Patchwork; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

from __future__ import absolute_import




class DelegationRuleInline(admin.TabularInline):
    model = DelegationRule
    fields = ('path', 'user', 'priority')


class ProjectAdmin(admin.ModelAdmin):
    list_display = ('name', 'linkname', 'listid', 'listemail')
    inlines = [
        DelegationRuleInline,
    ]


admin.site.register(Project, ProjectAdmin)


class PersonAdmin(admin.ModelAdmin):
    list_display = ('__str__', 'has_account')
    search_fields = ('name', 'email')

    def has_account(self, person):
        return bool(person.user)

    has_account.boolean = True
    has_account.admin_order_field = 'user'
    has_account.short_description = 'Account'


admin.site.register(Person, PersonAdmin)


class UserProfileAdmin(admin.ModelAdmin):
    search_fields = ('user__username', 'user__first_name', 'user__last_name')


admin.site.register(UserProfile, UserProfileAdmin)


class StateAdmin(admin.ModelAdmin):
    list_display = ('name', 'action_required')


admin.site.register(State, StateAdmin)


class SeriesForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(SeriesForm, self).__init__(*args, **kwargs)

        self.fields['last_revision'].queryset = SeriesRevision.objects.filter(