diff mbox series

[v5] models, templates: Add submission relations

Message ID 20200115131121.27216-1-dja@axtens.net
State Superseded
Headers show
Series [v5] models, templates: Add submission relations | expand

Commit Message

Daniel Axtens Jan. 15, 2020, 1:11 p.m. UTC
Introduces the ability to add relations between submissions. Relations
are displayed in the details page of a submission under 'Related'.
Related submissions 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>

---

I wanted to get this out asap, I'll start on the REST patch next.
---
 patchwork/admin.py                            |  8 ++++
 patchwork/api/event.py                        |  5 ++-
 .../migrations/0040_add_related_patches.py    | 42 +++++++++++++++++++
 patchwork/models.py                           | 32 +++++++++++++-
 patchwork/signals.py                          | 24 +++++++++++
 patchwork/templates/patchwork/submission.html | 37 ++++++++++++++++
 patchwork/views/patch.py                      | 13 ++++++
 7 files changed, 159 insertions(+), 2 deletions(-)
 create mode 100644 patchwork/migrations/0040_add_related_patches.py

Comments

Daniel Axtens Jan. 26, 2020, 12:49 p.m. UTC | #1
Daniel Axtens <dja@axtens.net> writes:

> Introduces the ability to add relations between submissions. Relations
> are displayed in the details page of a submission under 'Related'.
> Related submissions 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.

So it occurs to me that teeeeeeeechnically we should probably hide these
events completely in API < 1.2 because an API consumer doesn't know
about them... I would have no idea how to do this though, nor am I sure
if it's a worthwhile exercise... Thoughts?

> 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>
>
> ---
>
> I wanted to get this out asap, I'll start on the REST patch next.
> ---
>  patchwork/admin.py                            |  8 ++++
>  patchwork/api/event.py                        |  5 ++-
>  .../migrations/0040_add_related_patches.py    | 42 +++++++++++++++++++
>  patchwork/models.py                           | 32 +++++++++++++-
>  patchwork/signals.py                          | 24 +++++++++++
>  patchwork/templates/patchwork/submission.html | 37 ++++++++++++++++
>  patchwork/views/patch.py                      | 13 ++++++
>  7 files changed, 159 insertions(+), 2 deletions(-)
>  create mode 100644 patchwork/migrations/0040_add_related_patches.py
>
> diff --git a/patchwork/admin.py b/patchwork/admin.py
> index f9a94c6f5c07..c3d45240f1eb 100644
> --- a/patchwork/admin.py
> +++ b/patchwork/admin.py
> @@ -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)
> diff --git a/patchwork/api/event.py b/patchwork/api/event.py
> index a066faaec63b..d7685f4c138c 100644
> --- a/patchwork/api/event.py
> +++ b/patchwork/api/event.py
> @@ -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
> index 000000000000..f4d811fedddd
> --- /dev/null
> +++ b/patchwork/migrations/0040_add_related_patches.py
> @@ -0,0 +1,42 @@
> +# -*- coding: utf-8 -*-
> +# Generated by Django 1.11.27 on 2020-01-15 23:15
> +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'),
> +        ),
> +    ]
> diff --git a/patchwork/models.py b/patchwork/models.py
> index be71d4077892..4f7186d3ec10 100644
> --- a/patchwork/models.py
> +++ b/patchwork/models.py
> @@ -447,6 +447,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
> @@ -863,6 +869,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):
>  
> @@ -928,6 +947,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'
> @@ -937,6 +957,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'),
> @@ -952,7 +973,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.')
> @@ -1000,6 +1021,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(
> diff --git a/patchwork/signals.py b/patchwork/signals.py
> index 73ddfa5e35ee..3a2f0fbdd3a4 100644
> --- a/patchwork/signals.py
> +++ b/patchwork/signals.py
> @@ -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):
>  
> diff --git a/patchwork/templates/patchwork/submission.html b/patchwork/templates/patchwork/submission.html
> index 77a2711ab5b4..978559b8726b 100644
> --- a/patchwork/templates/patchwork/submission.html
> +++ b/patchwork/templates/patchwork/submission.html
> @@ -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">
> diff --git a/patchwork/views/patch.py b/patchwork/views/patch.py
> index f34053ce57da..e32ff0bff5f5 100644
> --- a/patchwork/views/patch.py
> +++ b/patchwork/views/patch.py
> @@ -110,12 +110,25 @@ 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)
>  
> -- 
> 2.20.1
Mete Polat Jan. 27, 2020, 8:56 a.m. UTC | #2
Hi Daniel,

On 26.01.20 13:49, Daniel Axtens wrote:
> Daniel Axtens <dja@axtens.net> writes:
> 
>> Introduces the ability to add relations between submissions. Relations
>> are displayed in the details page of a submission under 'Related'.
>> Related submissions 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.
> 
> So it occurs to me that teeeeeeeechnically we should probably hide these
> events completely in API < 1.2 because an API consumer doesn't know
> about them... I would have no idea how to do this though, nor am I sure
> if it's a worthwhile exercise... Thoughts?
> 

We could possibly use request.path to extract the current API version
and tell our queryset to filter out our unwanted events. Or am I missing
something here?

Best regards,

Mete

>> 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>
>>
>> ---
>>
>> I wanted to get this out asap, I'll start on the REST patch next.
>> ---
>>  patchwork/admin.py                            |  8 ++++
>>  patchwork/api/event.py                        |  5 ++-
>>  .../migrations/0040_add_related_patches.py    | 42 +++++++++++++++++++
>>  patchwork/models.py                           | 32 +++++++++++++-
>>  patchwork/signals.py                          | 24 +++++++++++
>>  patchwork/templates/patchwork/submission.html | 37 ++++++++++++++++
>>  patchwork/views/patch.py                      | 13 ++++++
>>  7 files changed, 159 insertions(+), 2 deletions(-)
>>  create mode 100644 patchwork/migrations/0040_add_related_patches.py
>>
>> diff --git a/patchwork/admin.py b/patchwork/admin.py
>> index f9a94c6f5c07..c3d45240f1eb 100644
>> --- a/patchwork/admin.py
>> +++ b/patchwork/admin.py
>> @@ -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)
>> diff --git a/patchwork/api/event.py b/patchwork/api/event.py
>> index a066faaec63b..d7685f4c138c 100644
>> --- a/patchwork/api/event.py
>> +++ b/patchwork/api/event.py
>> @@ -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
>> index 000000000000..f4d811fedddd
>> --- /dev/null
>> +++ b/patchwork/migrations/0040_add_related_patches.py
>> @@ -0,0 +1,42 @@
>> +# -*- coding: utf-8 -*-
>> +# Generated by Django 1.11.27 on 2020-01-15 23:15
>> +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'),
>> +        ),
>> +    ]
>> diff --git a/patchwork/models.py b/patchwork/models.py
>> index be71d4077892..4f7186d3ec10 100644
>> --- a/patchwork/models.py
>> +++ b/patchwork/models.py
>> @@ -447,6 +447,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
>> @@ -863,6 +869,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):
>>  
>> @@ -928,6 +947,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'
>> @@ -937,6 +957,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'),
>> @@ -952,7 +973,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.')
>> @@ -1000,6 +1021,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(
>> diff --git a/patchwork/signals.py b/patchwork/signals.py
>> index 73ddfa5e35ee..3a2f0fbdd3a4 100644
>> --- a/patchwork/signals.py
>> +++ b/patchwork/signals.py
>> @@ -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):
>>  
>> diff --git a/patchwork/templates/patchwork/submission.html b/patchwork/templates/patchwork/submission.html
>> index 77a2711ab5b4..978559b8726b 100644
>> --- a/patchwork/templates/patchwork/submission.html
>> +++ b/patchwork/templates/patchwork/submission.html
>> @@ -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">
>> diff --git a/patchwork/views/patch.py b/patchwork/views/patch.py
>> index f34053ce57da..e32ff0bff5f5 100644
>> --- a/patchwork/views/patch.py
>> +++ b/patchwork/views/patch.py
>> @@ -110,12 +110,25 @@ 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)
>>  
>> -- 
>> 2.20.1
diff mbox series

Patch

diff --git a/patchwork/admin.py b/patchwork/admin.py
index f9a94c6f5c07..c3d45240f1eb 100644
--- a/patchwork/admin.py
+++ b/patchwork/admin.py
@@ -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)
diff --git a/patchwork/api/event.py b/patchwork/api/event.py
index a066faaec63b..d7685f4c138c 100644
--- a/patchwork/api/event.py
+++ b/patchwork/api/event.py
@@ -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
index 000000000000..f4d811fedddd
--- /dev/null
+++ b/patchwork/migrations/0040_add_related_patches.py
@@ -0,0 +1,42 @@ 
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.27 on 2020-01-15 23:15
+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'),
+        ),
+    ]
diff --git a/patchwork/models.py b/patchwork/models.py
index be71d4077892..4f7186d3ec10 100644
--- a/patchwork/models.py
+++ b/patchwork/models.py
@@ -447,6 +447,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
@@ -863,6 +869,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):
 
@@ -928,6 +947,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'
@@ -937,6 +957,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'),
@@ -952,7 +973,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.')
@@ -1000,6 +1021,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(
diff --git a/patchwork/signals.py b/patchwork/signals.py
index 73ddfa5e35ee..3a2f0fbdd3a4 100644
--- a/patchwork/signals.py
+++ b/patchwork/signals.py
@@ -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):
 
diff --git a/patchwork/templates/patchwork/submission.html b/patchwork/templates/patchwork/submission.html
index 77a2711ab5b4..978559b8726b 100644
--- a/patchwork/templates/patchwork/submission.html
+++ b/patchwork/templates/patchwork/submission.html
@@ -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">
diff --git a/patchwork/views/patch.py b/patchwork/views/patch.py
index f34053ce57da..e32ff0bff5f5 100644
--- a/patchwork/views/patch.py
+++ b/patchwork/views/patch.py
@@ -110,12 +110,25 @@  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)