import datetime from django.conf import settings from django.contrib.auth.models import User from django.db import connection, models from django.db.models import Count from patchwork.models import ( Patch, State, Person, Project, ) def x_months_ago(x): '''Find the starting date of X months ago. if x=3 and its May 6, then this would treat may as short month, and return March 1st. ''' assert x > 0 # Get first day of this month. month = datetime.datetime.now().replace( day=1, hour=0, minute=0, second=0, microsecond=0) for x in range(x - 1): month = month.replace(day=1) - datetime.timedelta(days=1) return month.replace(day=1) class Team(models.Model): name = models.CharField(max_length=1024, unique=True) display_name = models.CharField(max_length=1024, null=False) active = models.BooleanField(default=True) @property def total_submitted(self): return TeamCredit.objects.filter(team=self).count() @property def total_accepted(self): s = State.objects.get(name='Accepted') return TeamCredit.objects.filter(team=self, patch__state=s).count() def __unicode__(self): if self.display_name: return self.display_name return self.name class CommitTagCredit(models.Model): tag = models.CharField(max_length=1024) person = models.ForeignKey(Person) commit = models.CharField(max_length=1024) def __unicode__(self): return '%s %s: %s' % (self.person, self.tag, self.commit) class ProjectTagCredit(models.Model): commit_tag = models.ForeignKey(CommitTagCredit) project = models.ForeignKey(Project) class Meta: unique_together = ('commit_tag', 'project') def __unicode__(self): return '%s %s: %s' % ( self.project, self.commit_tag.tag, self.commit_tag.commit ) class TeamTagCredit(models.Model): commit_tag = models.ForeignKey(CommitTagCredit) team = models.ForeignKey(Team) class Meta: unique_together = ('commit_tag', 'team') def __unicode__(self): return '%s %s: %s' % ( self.team, self.commit_tag.tag, self.commit_tag.commit ) class TeamMembership(models.Model): team = models.ForeignKey(Team) user = models.ForeignKey(User) class Meta: unique_together = ('team', 'user') def __unicode__(self): return '%s: %s' % (self.team.name, self.user.username) class TeamCredit(models.Model): '''gives credit to a team for a patch''' patch = models.ForeignKey(Patch) team = models.ForeignKey(Team) last_state_change = models.DateTimeField( null=True, help_text='Used to support time-to-acceptance metrics') class Meta: unique_together = ('patch', 'team') def __unicode__(self): return self.patch.name + ' for ' + self.team.name @staticmethod def patch_count_by_month(num_months, state=None, values=None, **filter_args): # Allow us to group patches by month. # http://stackoverflow.com/questions/8746014/ # Basically date_trunc_sql adds some raw SQL to our queryset that makes # the timestamp of each patch only be year and month, so that they # can be annotated by month. start = x_months_ago(num_months) truncate_date = connection.ops.date_trunc_sql('month', 'date') credits = TeamCredit.objects.filter( patch__date__gte=start, **filter_args ).exclude( patch__state=State.objects.get(name='Superseded')) if state: credits = credits.filter(patch__state=state) if not values: values = ('month',) credits = credits.extra( {'month': truncate_date} ).values(*values).annotate( Count('patch__pk', distinct=True)).order_by('month') return credits @classmethod def get_month_metrics(cls, num_months=6, **filter_args): s = State.objects.get(name='Accepted') submit = cls.patch_count_by_month(num_months, **filter_args) accept = cls.patch_count_by_month(num_months, s, **filter_args) metrics = [] for x in reversed(range(num_months)): metrics.append([x_months_ago(x + 1), 0, 0]) for metric in metrics: # About: if str(metric[0]).startswith(str(x['month'])) # In order to keep unit testing simple we use sqlite. Production # uses postgres. The truncate_date operation in sqlite returns a # string and in postgres we get a datetime. So the easiest way to # compare is to just convert to strings. for x in submit: if str(metric[0]).startswith(str(x['month'])): metric[1] += x['patch__pk__count'] for x in accept: if str(metric[0]).startswith(str(x['month'])): metric[2] += x['patch__pk__count'] return [(x[0].strftime('%Y-%m'), x[1], x[2]) for x in metrics] @staticmethod def time_to_acceptance(**filter_args): s = State.objects.get(name='Accepted') sql = '''SELECT COUNT(tc.weeks), tc.weeks FROM (SELECT DISTINCT ON (linaro_metrics_teamcredit.patch_id) TRUNC(DATE_PART('day', linaro_metrics_teamcredit.last_state_change - patchwork_submission.date)/7) AS weeks FROM linaro_metrics_teamcredit INNER JOIN patchwork_patch ON patchwork_patch.submission_ptr_id = linaro_metrics_teamcredit.patch_id AND patchwork_patch.state_id = %d INNER JOIN patchwork_submission ON patchwork_submission.id = linaro_metrics_teamcredit.patch_id) AS tc GROUP BY tc.weeks ORDER BY tc.weeks;''' % s.id cursor = connection.cursor() cursor.execute(sql) buckets = ([7, 0], [14, 0], [21, 0], [28, 0], [35, 0], [42, 0]) for row in cursor: if row[1] >= 5: buckets[-1][1] += row[0] else: buckets[int(row[1])][1] += row[0] return buckets @staticmethod def age_distribution_data(increment_by, num_increments): cursor = connection.cursor() sql = ''' SELECT COUNT(increment), increment FROM ( SELECT DISTINCT ON (patchwork_submission.id) ROUND( DATE_PART('day', NOW() - patchwork_submission.date) / %d) AS increment FROM linaro_metrics_teamcredit INNER JOIN patchwork_submission ON patchwork_submission.id = linaro_metrics_teamcredit.patch_id INNER JOIN patchwork_patch ON patchwork_patch.submission_ptr_id = patchwork_submission.id WHERE patchwork_patch.state_id = %d ORDER BY patchwork_submission.id) AS increments GROUP BY increment ORDER BY increment; ''' % (increment_by, State.objects.get(name='New').id) cursor.execute(sql) increments = [] for row in cursor: if len(increments) == num_increments: increments[-1][1] += row[0] else: increments.append([int(row[1] * increment_by), row[0]]) return increments def _patch_author(patch): # determine if the patch was authored by a linaro engineer # due to historical reasons, not all "user" accounts will be @linaro.org # (ie from the days of launchpad accounts). Additionally, some "Persons" # just haven't been assigned to a team known to patches, so the user sync # script won't know to create a "user" for the person. We just have to do # our best to see if the person.email or user.email is linaro is_linaro = False user = None person = getattr(patch, 'linaro_author', patch.submitter) if not person: return False, user if person.email.endswith('@linaro.org') or \ (person.user and person.user.email.endswith('@linaro.org')): is_linaro = True user = person.user return is_linaro, user def _patch_saved_callback(sender, created, instance, **kwargs): credits = [] if created: is_linaro, author = _patch_author(instance) memberships = None if author: memberships = TeamMembership.objects.filter(user=author) for m in memberships: credits.append(TeamCredit(patch=instance, team=m.team)) # we have some historical cases where an author wasn't "linaro" but is # the member of a team. So check for either "linaro" or a membership if (not memberships or memberships.count() == 0) and is_linaro: # author.user was None (never been a part of team known to us), or # author.user isn't currently a member of any teams we know about team, _ = Team.objects.get_or_create( name=settings.DEFAULT_TEAM, defaults={'display_name': 'No Team Found'}) credits.append(TeamCredit(patch=instance, team=team)) else: for tc in TeamCredit.objects.filter(patch=instance): credits.append(tc) for tc in credits: tc.last_state_change = datetime.datetime.now() tc.save() models.signals.post_save.connect(_patch_saved_callback, sender=Patch)