First stab at tracking build results
authorMagnus Hagander <magnus@hagander.net>
Tue, 19 Jun 2018 12:17:12 +0000 (14:17 +0200)
committerMagnus Hagander <magnus@hagander.net>
Tue, 19 Jun 2018 12:17:57 +0000 (14:17 +0200)
pgcommitfest/commitfest/admin.py
pgcommitfest/commitfest/api.py
pgcommitfest/commitfest/migrations/0003_patch_build_status.py [new file with mode: 0644]
pgcommitfest/commitfest/models.py
pgcommitfest/commitfest/templates/commitfest.html
pgcommitfest/commitfest/templates/patch.html
pgcommitfest/commitfest/templatetags/commitfest.py
pgcommitfest/commitfest/views.py
pgcommitfest/urls.py

index 05fe3eda8b3758ca7f7c798007913c2fd110b382..909ace3f83f2b37fa6d4a46d342f0037e60da2bf 100644 (file)
@@ -22,6 +22,8 @@ admin.site.register(CommitFest)
 admin.site.register(Topic)
 admin.site.register(Patch, PatchAdmin)
 admin.site.register(PatchHistory)
+admin.site.register(BuildProvider)
+admin.site.register(PatchBuildStatus)
 
 admin.site.register(MailThread)
 admin.site.register(MailThreadAttachment, MailThreadAttachmentAdmin)
index 1c97ef126fd172d13edb45ca7f307ebbaee7f0fa..fec46eab482603af0b2d3f0eb379fc0545b5be14 100644 (file)
@@ -6,9 +6,10 @@ from functools import wraps
 from django.utils.decorators import available_attrs
 from django.db import connection
 
+import datetime
 import json
 
-from models import CommitFest
+from models import CommitFest, Patch, PatchBuildStatus, BuildProvider
 
 def api_authenticate(view_func):
        def _wrapped_view(request, *args, **kwargs):
@@ -74,3 +75,63 @@ GROUP BY p.id, poc.id""".format(wherestring), params)
        }
        return HttpResponse(json.dumps(res),
                                                content_type='application/json')
+
+
+@csrf_exempt
+@api_authenticate
+def build_result(request, cfid, patchid):
+       if request.method != 'POST':
+                       return HttpResponse('Invalid method', status=405)
+       if request.META['CONTENT_TYPE'] != 'application/json':
+               return HttpResponse("Only JSON accepted", status=415)
+       try:
+               obj = json.loads(request.body)
+       except ValueError:
+               return HttpResponse("Invalid data format", status=415)
+
+       commitfest = get_object_or_404(CommitFest, pk=cfid)
+       patch = get_object_or_404(Patch, pk=patchid)
+
+       # Mandatory fields
+       try:
+               provider = BuildProvider.objects.get(urlname=obj['provider'])
+               messageid = obj['messageid']
+               status = obj['status']
+               statustime = obj['timestamp']
+       except BuildProvider.DoesNotExist:
+               return HttpResponse("Invalid build provider", status=422)
+       except KeyError, e:
+               return HttpResponse("Mandatory parameter {0} missing".format(e.args[0]), status=400)
+
+       if not status in PatchBuildStatus._STATUS_MAP:
+               return HttpResponse("Invalid build status {0}".format(status), status=422)
+       try:
+               statustime = datetime.datetime.strptime(statustime, '%Y-%m-%dT%H:%M:%S.%fZ')
+       except ValueError:
+               return HttpResponse("Invalid timestamp {0}".format(statustime), status=422)
+
+       # Optional parameters
+       url = obj.get('url', '')
+       commitid = obj.get('commit', '')
+
+       (buildstatus, created) = PatchBuildStatus.objects.get_or_create(commitfest=commitfest,
+                                                                                                                                       patch=patch,
+                                                                                                                                       buildprovider=provider,
+                                                                                                                                       status_timestamp=statustime,
+                                                                                                                                       defaults={
+                                                                                                                                               'buildmessageid': messageid,
+                                                                                                                                               'buildstatus': PatchBuildStatus._STATUS_MAP[status],
+                                                                                                                                               'status_url': url,
+                                                                                                                                               'master_commit_id': commitid,
+                                                                                                                                       },
+       )
+       if not created:
+               if buildstatus.buildmessageid == messageid and \
+                  buildstatus.buildstatus == PatchBuildStatus._STATUS_MAP[status] and \
+                  buildstatus.status_url == url and \
+                  buildstatus.master_commit_id == commitid:
+                       return HttpResponse("Build status already stored", status=200)
+               return HttpResponse("Conflicting build status already stored", status=409)
+
+       # That's it!
+       return HttpResponse("Stored", status=201)
diff --git a/pgcommitfest/commitfest/migrations/0003_patch_build_status.py b/pgcommitfest/commitfest/migrations/0003_patch_build_status.py
new file mode 100644 (file)
index 0000000..6b8a70d
--- /dev/null
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('commitfest', '0002_notifications'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='BuildProvider',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('urlname', models.CharField(unique=True, max_length=16)),
+                ('name', models.CharField(max_length=100)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='PatchBuildStatus',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('buildmessageid', models.CharField(max_length=1000)),
+                ('buildstatus', models.IntegerField(default=0, choices=[(0, b'Pending'), (1, b'Success'), (2, b'Fail')])),
+                ('status_timestamp', models.DateTimeField()),
+                ('status_url', models.URLField(blank=True)),
+                ('master_commit_id', models.CharField(max_length=40, blank=True)),
+                ('buildprovider', models.ForeignKey(to='commitfest.BuildProvider')),
+                ('commitfest', models.ForeignKey(to='commitfest.CommitFest')),
+                ('patch', models.ForeignKey(to='commitfest.Patch')),
+            ],
+        ),
+        migrations.AlterUniqueTogether(
+            name='patchbuildstatus',
+            unique_together=set([('patch', 'commitfest', 'buildprovider', 'status_timestamp')]),
+        ),
+    ]
index 8e5a442926ee1646759d46687e0304d1d5285fc1..b1361175d15b989deb9ee74e46ef7ce84b5f82c0 100644 (file)
@@ -250,6 +250,52 @@ class PatchHistory(models.Model):
                        if u != self.by: # Don't notify for changes we make ourselves
                                PendingNotification(history=self, user=u).save()
 
+class BuildProvider(models.Model):
+       urlname = models.CharField(max_length=16, null=False, blank=False, unique=True)
+       name = models.CharField(max_length=100, null=False, blank=False)
+
+       def __unicode__(self):
+               return self.name
+
+class PatchBuildStatus(models.Model):
+       STATUS_PENDING=0
+       STATUS_SUCCESS=1
+       STATUS_FAIL=2
+       _STATUS_CHOICES=(
+               (STATUS_PENDING, "Pending"),
+               (STATUS_SUCCESS, "Success"),
+               (STATUS_FAIL, "Fail",)
+       )
+       _STATUS_CHOICE_MAP=dict(_STATUS_CHOICES)
+       _STATUS_MAP={
+               'pending': STATUS_PENDING,
+               'success': STATUS_SUCCESS,
+               'fail': STATUS_FAIL,
+       }
+       _STATUS_REVERSE_MAP={v:k for k,v in _STATUS_MAP.items()}
+
+       commitfest = models.ForeignKey(CommitFest, null=False, blank=False)
+       patch = models.ForeignKey(Patch, null=False, blank=False)
+       buildprovider = models.ForeignKey(BuildProvider, null=False, blank=False)
+       buildmessageid = models.CharField(max_length=1000, null=False, blank=False)
+       buildstatus = models.IntegerField(null=False, blank=False, default=0, choices=_STATUS_CHOICES)
+       status_timestamp = models.DateTimeField(null=False, blank=False)
+       status_url = models.URLField(null=False, blank=True)
+       master_commit_id = models.CharField(max_length=40, null=False, blank=True)
+
+       class Meta:
+               unique_together = (
+                       ('patch', 'commitfest', 'buildprovider', 'status_timestamp'),
+               )
+
+       @property
+       def textstatus(self):
+               return self._STATUS_CHOICE_MAP[self.buildstatus]
+
+       @property
+       def urlstatus(self):
+               return self._STATUS_REVERSE_MAP[self.buildstatus]
+
 class MailThread(models.Model):
        # This class tracks mail threads from the main postgresql.org
        # mailinglist archives. For each thread, we store *one* messageid.
index c9d1e91e96c279b09b1a4fe65b7f1b97b2574aaf..b68db0971cbdcdced44b49fe9b9f2b9ff12dcad7 100644 (file)
@@ -68,6 +68,7 @@
    <th>{%if p.is_open%}<a href="#" style="color:#333333;" onclick="return sortpatches(3);">Num cfs</a>{%if sortkey == 3%}<div style="float:right;"><i class="glyphicon glyphicon-arrow-down"></i></div>{%endif%}{%else%}Num cfs{%endif%}</th>
    <th>{%if p.is_open%}<a href="#" style="color:#333333;" onclick="return sortpatches(1);">Latest activity</a>{%if sortkey == 1%}<div style="float:right;"><i class="glyphicon glyphicon-arrow-down"></i></div>{%endif%}{%else%}Latest activity{%endif%}</th>
    <th>{%if p.is_open%}<a href="#" style="color:#333333;" onclick="return sortpatches(2);">Latest mail</a>{%if sortkey == 2%}<div style="float:right;"><i class="glyphicon glyphicon-arrow-down"></i></div>{%endif%}{%else%}Latest mail{%endif%}</th>
+   <th>Build status</th>
 {%if user.is_staff%}
    <th>Select</th>
 {%endif%}
@@ -78,7 +79,7 @@
 
 {%if grouping%}
 {%ifchanged p.topic%}
-  <tr><th colspan="{%if user.is_staff%}9{%else%}8{%endif%}">{{p.topic}}</th></tr>
+  <tr><th colspan="{%if user.is_staff%}10{%else%}9{%endif%}">{{p.topic}}</th></tr>
 {%endifchanged%} 
 {%endif%}
   <tr>
    <td>{{p.num_cfs}}</td>
    <td style="white-space: nowrap;">{{p.modified|date:"Y-m-d"}}<br/>{{p.modified|date:"H:i"}}</td>
    <td style="white-space: nowrap;">{{p.lastmail|date:"Y-m-d"}}<br/>{{p.lastmail|date:"H:i"}}</td>
+   <td>
+{%for bs in p.buildstatus%}
+{%if bs.url%}<a href="{{bs.url}}">{%endif%}<span class="glyphicon glyphicon-{{bs.status|glyphbuildstatus}}" title="{{bs.provider}} {{bs.status|buildstatus}}"></span>{%if bs.url%}</a>{%endif%}
+{%endfor%}
+</td>
 {%if user.is_staff%}
    <td style="white-space: nowrap;"><input type="checkbox" class="sender_checkbox" id="send_authors_{{p.id}}">Author<br/><input type="checkbox" class="sender_checkbox" id="send_reviewers_{{p.id}}">Reviewer</td>
 {%endif%}
index 3ed775e36198f5c854e390662dd3e50e4652a9dc..4194ed2db45f8c6331ff2faa18bbf43abdf08117 100644 (file)
        </dl>
       </td>
     </tr>
+    <tr>
+      <th>Build status</th>
+      <td>
+{%for s in buildstatuses|dictsort:"status_timestamp"%}
+       <div><span class="glyphicon glyphicon-{{s.buildstatus|glyphbuildstatus}}" title="{{s.textstatus}}"></span> from {{s.buildprovider}} at {{s.status_timestamp}} (tested patch in <a href="https://www.postgresql.org/message-id/{{s.buildmessageid}}/">{{s.buildmessageid}}</a>{%if s.master_commit_id%} against git commit <a href="https://git.postgresql.org/pg/commitdiff/{{s.master_commit_id}}">{{s.master_commit_id}}</a>{%endif%})</div>
+{%endfor%}
+</td>
+    </tr>
     <tr>
       <th>History</th>
       <td>
index b8f68e469445f9b7350b1502a423e5a9d435ac4f..acb49be309e47bc8bcb7bd1910a507ee19b1d777 100644 (file)
@@ -1,7 +1,7 @@
 from django.template.defaultfilters import stringfilter
 from django import template
 
-from pgcommitfest.commitfest.models import PatchOnCommitFest
+from pgcommitfest.commitfest.models import PatchOnCommitFest, PatchBuildStatus
 
 register = template.Library()
 
@@ -41,3 +41,18 @@ def alertmap(value):
 @stringfilter
 def hidemail(value):
        return value.replace('@', ' at ')
+
+@register.filter(name='buildstatus')
+@stringfilter
+def buildstatus(value):
+       return PatchBuildStatus._STATUS_CHOICE_MAP[int(value)]
+
+@register.filter(name='glyphbuildstatus')
+@stringfilter
+def glyphbuildstatus(value):
+       v = int(value)
+       if v == PatchBuildStatus.STATUS_PENDING:
+               return 'question-sign'
+       elif v == PatchBuildStatus.STATUS_SUCCESS:
+               return 'ok-sign'
+       return 'minus-sign'
index 3ee60e3c8969d9169b0428e308ae0d5511a8d252..3a0411516c79e19a130db0968b96b2ed64f532bd 100644 (file)
@@ -20,7 +20,7 @@ from pgcommitfest.mailqueue.util import send_mail, send_simple_mail
 from pgcommitfest.userprofile.util import UserWrapper
 
 from models import CommitFest, Patch, PatchOnCommitFest, PatchHistory, Committer
-from models import MailThread
+from models import MailThread, PatchBuildStatus
 from forms import PatchForm, NewPatchForm, CommentForm, CommitFestFilterForm
 from forms import BulkEmailForm
 from ajax import doAttachThread, refresh_single_thread
@@ -194,7 +194,8 @@ def commitfest(request, cfid):
 (poc.status=ANY(%(openstatuses)s)) AS is_open,
 (SELECT string_agg(first_name || ' ' || last_name || ' (' || username || ')', ', ') FROM auth_user INNER JOIN commitfest_patch_authors cpa ON cpa.user_id=auth_user.id WHERE cpa.patch_id=p.id) AS author_names,
 (SELECT string_agg(first_name || ' ' || last_name || ' (' || username || ')', ', ') FROM auth_user INNER JOIN commitfest_patch_reviewers cpr ON cpr.user_id=auth_user.id WHERE cpr.patch_id=p.id) AS reviewer_names,
-(SELECT count(1) FROM commitfest_patchoncommitfest pcf WHERE pcf.patch_id=p.id) AS num_cfs
+(SELECT count(1) FROM commitfest_patchoncommitfest pcf WHERE pcf.patch_id=p.id) AS num_cfs,
+(SELECT json_agg(json_build_object('provider', providername, 'status', buildstatus, 'timestamp', status_timestamp, 'url', status_url)) FROM (SELECT DISTINCT ON (buildprovider_id) *, bp.name AS providername FROM commitfest_patchbuildstatus pbs INNER JOIN commitfest_buildprovider bp ON bp.id=buildprovider_id WHERE pbs.commitfest_id=%(cid)s AND pbs.patch_id=p.id ORDER BY buildprovider_id, status_timestamp DESC) x) AS buildstatus
 FROM commitfest_patch p
 INNER JOIN commitfest_patchoncommitfest poc ON poc.patch_id=p.id
 INNER JOIN commitfest_topic t ON t.id=p.topic_id
@@ -247,6 +248,7 @@ def patch(request, cfid, patchid):
        patch = get_object_or_404(Patch.objects.select_related(), pk=patchid, commitfests=cf)
        patch_commitfests = PatchOnCommitFest.objects.select_related('commitfest').filter(patch=patch).order_by('-commitfest__startdate')
        committers = Committer.objects.filter(active=True).order_by('user__last_name', 'user__first_name')
+       buildstatuses = PatchBuildStatus.objects.select_related('buildprovider').filter(commitfest=cf, patch=patch).order_by('buildprovider', '-status_timestamp').distinct('buildprovider')
 
        #XXX: this creates a session, so find a smarter way. Probably handle
        #it in the callback and just ask the user then?
@@ -270,6 +272,7 @@ def patch(request, cfid, patchid):
                'cf': cf,
                'patch': patch,
                'patch_commitfests': patch_commitfests,
+               'buildstatuses': buildstatuses,
                'is_committer': is_committer,
                'is_this_committer': is_this_committer,
                'is_reviewer': is_reviewer,
index 105dd5d3b7d4705b55843b2ae666e95c355c7871..dc5deb5e85a0d9317aa01b8c134964dcdf51d2d3 100644 (file)
@@ -35,6 +35,7 @@ urlpatterns = [
     url(r'^thread_notify/$', views.thread_notify),
     url(r'^api/active_commitfests/$', api.active_commitfests),
     url(r'^api/commitfest/(\d+)/$', api.commitfest),
+    url(r'^api/commitfest/(\d+)/(\d+)/$', api.build_result),
 
     url(r'^selectable/', include('selectable.urls')),