]> git.ipfire.org Git - thirdparty/patchwork.git/commitdiff
models, templates: Add patch relations
authorMete Polat <metepolat2000@gmail.com>
Thu, 27 Feb 2020 23:29:31 +0000 (23:29 +0000)
committerDaniel Axtens <dja@axtens.net>
Sun, 15 Mar 2020 23:05:51 +0000 (10:05 +1100)
Introduces the ability to add relations between patches. Relations are
displayed in the details page of a patch under 'Related'. Related
patches located in another projects can be viewed as well.

Changes to relations are tracked in events. Currently the display of
this is very bare in the API but that will be fixed in a subsequent patch:
this is the minimum required to avoid throwing errors when you view the
events feed.

Signed-off-by: Mete Polat <metepolat2000@gmail.com>
[dja: address some review comments from Stephen, add an admin view,
      move to using Events, misc tidy-ups.]
Signed-off-by: Daniel Axtens <dja@axtens.net>
patchwork/admin.py
patchwork/api/event.py
patchwork/migrations/0040_add_related_patches.py [new file with mode: 0644]
patchwork/models.py
patchwork/signals.py
patchwork/templates/patchwork/submission.html
patchwork/views/patch.py

index f9a94c6f5c07b024570eeb39c18e5bfbf158ed40..c3d45240f1eb1b8794aa4aade07a81f799451b36 100644 (file)
@@ -14,6 +14,7 @@ from patchwork.models import Comment
 from patchwork.models import CoverLetter
 from patchwork.models import DelegationRule
 from patchwork.models import Patch
+from patchwork.models import PatchRelation
 from patchwork.models import Person
 from patchwork.models import Project
 from patchwork.models import Series
@@ -174,3 +175,10 @@ class TagAdmin(admin.ModelAdmin):
 
 
 admin.site.register(Tag, TagAdmin)
+
+
+class PatchRelationAdmin(admin.ModelAdmin):
+    model = PatchRelation
+
+
+admin.site.register(PatchRelation, PatchRelationAdmin)
index fdff6a4f2fa60fecfc9fe0203736bb4b2187908a..44c345208e6e3482f5e1c566c42a7bda6f4aba20 100644 (file)
@@ -42,6 +42,8 @@ class EventSerializer(ModelSerializer):
                                              'current_state'],
         Event.CATEGORY_PATCH_DELEGATED: ['patch', 'previous_delegate',
                                          'current_delegate'],
+        Event.CATEGORY_PATCH_RELATION_CHANGED: ['patch', 'previous_relation',
+                                                'current_relation'],
         Event.CATEGORY_CHECK_CREATED: ['patch', 'created_check'],
         Event.CATEGORY_SERIES_CREATED: ['series'],
         Event.CATEGORY_SERIES_COMPLETED: ['series'],
@@ -68,7 +70,8 @@ class EventSerializer(ModelSerializer):
         model = Event
         fields = ('id', 'category', 'project', 'date', 'actor', 'patch',
                   'series', 'cover', 'previous_state', 'current_state',
-                  'previous_delegate', 'current_delegate', 'created_check')
+                  'previous_delegate', 'current_delegate', 'created_check',
+                  'previous_relation', 'current_relation',)
         read_only_fields = fields
         versioned_fields = {
             '1.2': ('actor', ),
diff --git a/patchwork/migrations/0040_add_related_patches.py b/patchwork/migrations/0040_add_related_patches.py
new file mode 100644 (file)
index 0000000..fc2994d
--- /dev/null
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('patchwork', '0039_unique_series_references'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='PatchRelation',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+            ],
+        ),
+        migrations.AlterField(
+            model_name='event',
+            name='category',
+            field=models.CharField(choices=[(b'cover-created', b'Cover Letter Created'), (b'patch-created', b'Patch Created'), (b'patch-completed', b'Patch Completed'), (b'patch-state-changed', b'Patch State Changed'), (b'patch-delegated', b'Patch Delegate Changed'), (b'patch-relation-changed', b'Patch Relation Changed'), (b'check-created', b'Check Created'), (b'series-created', b'Series Created'), (b'series-completed', b'Series Completed')], db_index=True, help_text=b'The category of the event.', max_length=25),
+        ),
+        migrations.AddField(
+            model_name='event',
+            name='current_relation',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='patchwork.PatchRelation'),
+        ),
+        migrations.AddField(
+            model_name='event',
+            name='previous_relation',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='patchwork.PatchRelation'),
+        ),
+        migrations.AddField(
+            model_name='patch',
+            name='related',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patches', related_query_name='patch', to='patchwork.PatchRelation'),
+        ),
+    ]
index 08ee341eb6c12426c95eae6f100635aff2ad65d5..e295e17368f07dc19d593f55fb116d55bfb4426d 100644 (file)
@@ -449,6 +449,12 @@ class Patch(Submission):
         default=None, null=True,
         help_text='The number assigned to this patch in the series')
 
+    # related patches metadata
+
+    related = models.ForeignKey(
+        'PatchRelation', null=True, blank=True, on_delete=models.SET_NULL,
+        related_name='patches', related_query_name='patch')
+
     objects = PatchManager()
 
     @staticmethod
@@ -865,6 +871,19 @@ class BundlePatch(models.Model):
         ordering = ['order']
 
 
+@python_2_unicode_compatible
+class PatchRelation(models.Model):
+
+    def __str__(self):
+        patches = self.patches.all()
+        if not patches:
+            return '<Empty>'
+        name = ', '.join(patch.name for patch in patches[:10])
+        if len(name) > 60:
+            name = name[:60] + '...'
+        return name
+
+
 @python_2_unicode_compatible
 class Check(models.Model):
 
@@ -930,6 +949,7 @@ class Event(models.Model):
     CATEGORY_PATCH_COMPLETED = 'patch-completed'
     CATEGORY_PATCH_STATE_CHANGED = 'patch-state-changed'
     CATEGORY_PATCH_DELEGATED = 'patch-delegated'
+    CATEGORY_PATCH_RELATION_CHANGED = 'patch-relation-changed'
     CATEGORY_CHECK_CREATED = 'check-created'
     CATEGORY_SERIES_CREATED = 'series-created'
     CATEGORY_SERIES_COMPLETED = 'series-completed'
@@ -939,6 +959,7 @@ class Event(models.Model):
         (CATEGORY_PATCH_COMPLETED, 'Patch Completed'),
         (CATEGORY_PATCH_STATE_CHANGED, 'Patch State Changed'),
         (CATEGORY_PATCH_DELEGATED, 'Patch Delegate Changed'),
+        (CATEGORY_PATCH_RELATION_CHANGED, 'Patch Relation Changed'),
         (CATEGORY_CHECK_CREATED, 'Check Created'),
         (CATEGORY_SERIES_CREATED, 'Series Created'),
         (CATEGORY_SERIES_COMPLETED, 'Series Completed'),
@@ -954,7 +975,7 @@ class Event(models.Model):
     # event metadata
 
     category = models.CharField(
-        max_length=20,
+        max_length=25,
         choices=CATEGORY_CHOICES,
         db_index=True,
         help_text='The category of the event.')
@@ -1002,6 +1023,15 @@ class Event(models.Model):
         User, related_name='+', null=True, blank=True,
         on_delete=models.CASCADE)
 
+    # fields for 'patch-relation-changed-changed' events
+
+    previous_relation = models.ForeignKey(
+        PatchRelation, related_name='+', null=True, blank=True,
+        on_delete=models.CASCADE)
+    current_relation = models.ForeignKey(
+        PatchRelation, related_name='+', null=True, blank=True,
+        on_delete=models.CASCADE)
+
     # fields or 'patch-check-created' events
 
     created_check = models.ForeignKey(
index 73ddfa5e35eebe60e0963be7140028f0c3262261..3a2f0fbdd3a44a0a1884925f5fc4aa4c40cb4691 100644 (file)
@@ -134,6 +134,30 @@ def create_patch_delegated_event(sender, instance, raw, **kwargs):
     create_event(instance, orig_patch.delegate, instance.delegate)
 
 
+@receiver(pre_save, sender=Patch)
+def create_patch_relation_changed_event(sender, instance, raw, **kwargs):
+
+    def create_event(patch, before, after):
+        return Event.objects.create(
+            category=Event.CATEGORY_PATCH_RELATION_CHANGED,
+            project=patch.project,
+            actor=getattr(patch, '_edited_by', None),
+            patch=patch,
+            previous_relation=before,
+            current_relation=after)
+
+    # don't trigger for items loaded from fixtures or new items
+    if raw or not instance.pk:
+        return
+
+    orig_patch = Patch.objects.get(pk=instance.pk)
+
+    if orig_patch.related == instance.related:
+        return
+
+    create_event(instance, orig_patch.related, instance.related)
+
+
 @receiver(pre_save, sender=Patch)
 def create_patch_completed_event(sender, instance, raw, **kwargs):
 
index 77a2711ab5b4ea58060ae543729ce1e6bc72e248..978559b8726bb8492e49e4de01f95b8724468cdf 100644 (file)
@@ -110,6 +110,43 @@ function toggle_div(link_id, headers_id, label_show, label_hide)
   </td>
  </tr>
 {% endif %}
+{% if submission.related %}
+ <tr>
+  <th>Related</th>
+  <td>
+   <a id="togglerelated"
+      href="javascript:toggle_div('togglerelated', 'related')"
+   >show</a>
+   <div id="related" class="submissionlist" style="display:none;">
+    <ul>
+     {% for sibling in related_same_project %}
+      <li>
+       {% if sibling.id != submission.id %}
+        <a href="{% url 'patch-detail' project_id=project.linkname msgid=sibling.url_msgid %}">
+         {{ sibling.name|default:"[no subject]"|truncatechars:100 }}
+        </a>
+       {% endif %}
+      </li>
+     {% endfor %}
+     {% if related_different_project %}
+      <a id="togglerelatedoutside"
+         href="javascript:toggle_div('togglerelatedoutside', 'relatedoutside', 'show from other projects')"
+      >show from other projects</a>
+      <div id="relatedoutside" class="submissionlist" style="display:none;">
+       {% for sibling in related_outside %}
+        <li>
+         <a href="{% url 'patch-detail' project_id=sibling.project.linkname msgid=sibling.url_msgid %}">
+          {{ sibling.name|default:"[no subject]"|truncatechars:100 }}
+         </a> (in {{ sibling.project }})
+        </li>
+       {% endfor %}
+      </div>
+     {% endif %}
+    </ul>
+   </div>
+  </td>
+ </tr>
+{% endif %}
 </table>
 
 <div class="patchforms">
index b368cfa4beb815019807ef1d1dbb70bddf6cf09f..470ad19730fea3a4b438e211bae6d6d3ad74f71c 100644 (file)
@@ -110,12 +110,26 @@ def patch_detail(request, project_id, msgid):
     comments = comments.only('submitter', 'date', 'id', 'content',
                              'submission')
 
+    if patch.related:
+        related_same_project = patch.related.patches.only(
+            'name', 'msgid', 'project', 'related')
+        # avoid a second trip out to the db for info we already have
+        related_different_project = [
+            related_patch for related_patch in related_same_project
+            if related_patch.project_id != patch.project_id
+        ]
+    else:
+        related_same_project = []
+        related_different_project = []
+
     context['comments'] = comments
     context['checks'] = patch.check_set.all().select_related('user')
     context['submission'] = patch
     context['patchform'] = form
     context['createbundleform'] = createbundleform
     context['project'] = patch.project
+    context['related_same_project'] = related_same_project
+    context['related_different_project'] = related_different_project
 
     return render(request, 'patchwork/submission.html', context)