[Patchew-devel] [PATCH 04/10] models: create Result model

Paolo Bonzini pbonzini at redhat.com
Wed Jun 13 10:37:38 UTC 2018


The Result model has more or less a 1:1 correspondence with the fields in
the REST results detail view, and thus with ResultSerializerFull.
Until all plugins are converted, everything will still go through
namedtuple results, so the namedtuple is kept, renamed to ResultTuple.
The duplicated code will disappear at the end of this series.

There is an extra field tracking the time of the last update, which will
be used by the testing module; for simplicity it is not yet presented
by the serializers, though it will be added when ResultTuple is dropped.

The external interface of Result and ResultTuple is more or less the same,
except that there is no more renderer.  The renderer is now discovered by
convention: it is always a PatchewModule, and the name of the result up
to the first period (if any) identifies it.

As for obj, the parent object, the Result object has no clue whether it
comes from a project or a message, but the subclasses (ProjectResult
and MessageResult) do, so no change is needed.

Compared to properties, logs are always stored in the database, independent
of the size.  However, they are always stored in xz format compared to
the JSON string format used by properties.  Using xz will complicate
migrations a little, since the log property won't be available there,
but not as much as handling blobs.

Signed-off-by: Paolo Bonzini <pbonzini at redhat.com>
---
 api/migrations/0027_auto_20180521_0152.py |  61 +++++++++
 api/models.py                             | 150 ++++++++++++++++++++--
 mods/git.py                               |   4 +-
 mods/testing.py                           |   6 +-
 4 files changed, 204 insertions(+), 17 deletions(-)
 create mode 100644 api/migrations/0027_auto_20180521_0152.py

diff --git a/api/migrations/0027_auto_20180521_0152.py b/api/migrations/0027_auto_20180521_0152.py
new file mode 100644
index 0000000..d6833ef
--- /dev/null
+++ b/api/migrations/0027_auto_20180521_0152.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-05-31 05:44
+from __future__ import unicode_literals
+
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import jsonfield.encoder
+import jsonfield.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('api', '0026_auto_20180426_0829'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='LogEntry',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('data_xz', models.BinaryField()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Result',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=256)),
+                ('last_update', models.DateTimeField()),
+                ('status', models.CharField(max_length=7, validators=[django.core.validators.RegexValidator(code='invalid', message='status must be one of pending, success, failure, running', regex='pending|success|failure|running')])),
+                ('data', jsonfield.fields.JSONField(dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={})),
+            ],
+        ),
+        migrations.CreateModel(
+            name='MessageResult',
+            fields=[
+                ('result_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='api.Result')),
+                ('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.Message')),
+            ],
+            bases=('api.result',),
+        ),
+        migrations.CreateModel(
+            name='ProjectResult',
+            fields=[
+                ('result_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='api.Result')),
+                ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.Project')),
+            ],
+            bases=('api.result',),
+        ),
+        migrations.AddField(
+            model_name='result',
+            name='log_entry',
+            field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.LogEntry'),
+        ),
+        migrations.AlterIndexTogether(
+            name='result',
+            index_together=set([('status', 'name')]),
+        ),
+    ]
diff --git a/api/models.py b/api/models.py
index 0545b80..6288d8d 100644
--- a/api/models.py
+++ b/api/models.py
@@ -14,7 +14,9 @@ import json
 import datetime
 import re
 
+from django.core import validators
 from django.db import models
+from django.db.models import Q
 from django.contrib.auth.models import User
 from django.urls import reverse
 import jsonfield
@@ -25,6 +27,115 @@ from event import emit_event, declare_event
 from .blobs import save_blob, load_blob, load_blob_json
 import mod
 
+class LogEntry(models.Model):
+    data_xz = models.BinaryField()
+
+    @property
+    def data(self):
+        if not hasattr(self, "_data"):
+            self._data = lzma.decompress(self.data_xz).decode("utf-8")
+        return self._data
+
+    @data.setter
+    def data(self, value):
+        self._data = value
+        self.data_xz = lzma.compress(value.encode("utf-8"))
+
+class Result(models.Model):
+    PENDING = 'pending'
+    SUCCESS = 'success'
+    FAILURE = 'failure'
+    RUNNING = 'running'
+    VALID_STATUSES = (PENDING, SUCCESS, FAILURE, RUNNING)
+    VALID_STATUSES_RE = '|'.join(VALID_STATUSES)
+
+    name = models.CharField(max_length=256)
+    last_update = models.DateTimeField()
+    status = models.CharField(max_length=7, validators=[
+        validators.RegexValidator(regex=VALID_STATUSES_RE,
+                                  message='status must be one of ' + ', '.join(VALID_STATUSES),
+                                  code='invalid')])
+    log_entry = models.OneToOneField(LogEntry, on_delete=models.CASCADE,
+                                     null=True)
+    data = jsonfield.JSONField()
+
+    class Meta:
+        index_together = [('status', 'name')]
+
+    def is_success(self):
+        return self.status == self.SUCCESS
+
+    def is_failure(self):
+        return self.status == self.FAILURE
+
+    def is_completed(self):
+        return self.is_success() or self.is_failure()
+
+    def is_pending(self):
+        return self.status == self.PENDING
+
+    def is_running(self):
+        return self.status == self.RUNNING
+
+    def save(self):
+        self.last_update = datetime.datetime.utcnow()
+        return super(Result, self).save()
+
+    @property
+    def renderer(self):
+        found = re.match("^[^.]*", self.name)
+        return mod.get_module(found.group(0)) if found else None
+
+    @property
+    def obj(self):
+        return None
+
+    def render(self):
+        if self.renderer is None:
+            return None
+        return self.renderer.render_result(self)
+
+    @property
+    def log(self):
+        if self.log_entry is None:
+            return None
+        else:
+            return self.log_entry.data
+
+    @log.setter
+    def log(self, value):
+        entry = self.log_entry
+        if value is None:
+            if entry is not None:
+                self.log_entry = None
+                entry.delete()
+        else:
+            if entry is None:
+                entry = LogEntry()
+            entry.data = value
+            entry.save()
+            if self.log_entry is None:
+                self.log_entry = entry
+
+    def get_log_url(self, request=None):
+        if not self.is_completed() or self.renderer is None:
+            return None
+        log_url = self.renderer.get_result_log_url(self)
+        if log_url is not None and request is not None:
+            log_url = request.build_absolute_uri(log_url)
+        return log_url
+
+    @staticmethod
+    def get_result_tuples(obj, module, results):
+        name_filter = Q(name=module) | Q(name__startswith=module + '.')
+        renderer = mod.get_module(module)
+        for r in obj.results.filter(name_filter):
+            results.append(ResultTuple(name=r.name, obj=obj, status=r.status,
+                                       log=r.log, data=r.data, renderer=renderer))
+
+    def __str__(self):
+        return '%s (%s)' % (self.name, self.status)
+
 class Project(models.Model):
     name = models.CharField(max_length=1024, db_index=True, unique=True,
                             help_text="""The name of the project""")
@@ -183,6 +294,16 @@ class Project(models.Model):
                 series.save()
         return len(updated_series)
 
+    def create_result(self, **kwargs):
+        return ProjectResult(project=self, **kwargs)
+
+class ProjectResult(Result):
+    project = models.ForeignKey(Project, related_name='results')
+
+    @property
+    def obj(self):
+        return self.project
+
 class ProjectProperty(models.Model):
     project = models.ForeignKey('Project', on_delete=models.CASCADE)
     name = models.CharField(max_length=1024, db_index=True)
@@ -592,12 +713,22 @@ class Message(models.Model):
         self.save()
         emit_event("SeriesComplete", project=self.project, series=self)
 
+    def create_result(self, **kwargs):
+        return MessageResult(message=self, **kwargs)
+
     def __str__(self):
         return self.subject
 
     class Meta:
         unique_together = ('project', 'message_id',)
 
+class MessageResult(Result):
+    message = models.ForeignKey(Message, related_name='results')
+
+    @property
+    def obj(self):
+        return self.message
+
 class MessageProperty(models.Model):
     message = models.ForeignKey('Message', on_delete=models.CASCADE,
                                 related_name='properties')
@@ -626,34 +757,29 @@ class Module(models.Model):
     def __str__(self):
         return self.name
 
-class Result(namedtuple("Result", "name status log obj data renderer")):
+class ResultTuple(namedtuple("ResultTuple", "name status log obj data renderer")):
     __slots__ = ()
-    PENDING = 'pending'
-    SUCCESS = 'success'
-    FAILURE = 'failure'
-    RUNNING = 'running'
-    VALID_STATUSES = (PENDING, SUCCESS, FAILURE, RUNNING)
 
     def __new__(cls, name, status, obj, log=None, data=None, renderer=None):
-        if status not in cls.VALID_STATUSES:
+        if status not in Result.VALID_STATUSES:
             raise ValueError("invalid value '%s' for status field" % status)
-        return super(cls, Result).__new__(cls, status=status, log=log,
+        return super(cls, ResultTuple).__new__(cls, status=status, log=log,
                                           obj=obj, data=data, name=name, renderer=renderer)
 
     def is_success(self):
-        return self.status == self.SUCCESS
+        return self.status == Result.SUCCESS
 
     def is_failure(self):
-        return self.status == self.FAILURE
+        return self.status == Result.FAILURE
 
     def is_completed(self):
         return self.is_success() or self.is_failure()
 
     def is_pending(self):
-        return self.status == self.PENDING
+        return self.status == Result.PENDING
 
     def is_running(self):
-        return self.status == self.RUNNING
+        return self.status == Result.RUNNING
 
     def render(self):
         if self.renderer is None:
diff --git a/mods/git.py b/mods/git.py
index 4bfb5c6..fadce4c 100644
--- a/mods/git.py
+++ b/mods/git.py
@@ -18,7 +18,7 @@ from django.core.exceptions import PermissionDenied
 from django.utils.html import format_html
 from mod import PatchewModule
 from event import declare_event, register_handler, emit_event
-from api.models import Message, MessageProperty, Result
+from api.models import Message, MessageProperty, Result, ResultTuple
 from api.rest import PluginMethodField
 from api.views import APILoginRequiredView, prepare_series
 from patchew.logviewer import LogView
@@ -150,7 +150,7 @@ class GitModule(PatchewModule):
                 status = Result.SUCCESS
         else:
             status = Result.PENDING
-        results.append(Result(name='git', obj=obj, status=status,
+        results.append(ResultTuple(name='git', obj=obj, status=status,
                               log=log, data=data, renderer=self))
 
     def prepare_message_hook(self, request, message, detailed):
diff --git a/mods/testing.py b/mods/testing.py
index efa2d82..ca3d60d 100644
--- a/mods/testing.py
+++ b/mods/testing.py
@@ -17,7 +17,7 @@ from mod import PatchewModule
 import time
 import math
 from api.views import APILoginRequiredView
-from api.models import Message, MessageProperty, Project, Result
+from api.models import Message, MessageProperty, Project, Result, ResultTuple
 from api.search import SearchEngine
 from event import emit_event, declare_event, register_handler
 from patchew.logviewer import LogView
@@ -270,12 +270,12 @@ class TestingModule(PatchewModule):
 
             data = p.copy()
             del data['passed']
-            results.append(Result(name='testing.' + tn, obj=obj, status=passed_str,
+            results.append(ResultTuple(name='testing.' + tn, obj=obj, status=passed_str,
                                   log=log, data=data, renderer=self))
 
         if obj.get_property("testing.ready"):
             for tn in all_tests:
-                results.append(Result(name='testing.' + tn, obj=obj, status='pending'))
+                results.append(ResultTuple(name='testing.' + tn, obj=obj, status='pending'))
 
     def prepare_message_hook(self, request, message, detailed):
         if not message.is_series_head:
-- 
2.17.0





More information about the Patchew-devel mailing list