mbox series
Message IDe1be2e22d2c9ec79330672c5f03a435b04a0ef44.1780583783.git.pw@patches.jarry.cc
StateNew
Delegate
ArchivedNo
Headers
show
Message-ID: 
 <e1be2e22d2c9ec79330672c5f03a435b04a0ef44.1780583783.git.pw@patches.jarry.cc>
In-Reply-To: <cover.1780583783.git.pw@patches.jarry.cc>
References: <cover.1780583783.git.pw@patches.jarry.cc>
From: Robin Jarry <robin@jarry.cc>
Date: Fri, 29 May 2026 16:43:55 +0200
Subject: [PATCH patchwork v4 04/15] series: add metadata key-value store
Sender: pw@patches.jarry.cc
Reply-To: pw@patches.jarry.cc
List-ID: <pw.jarry.cc>
X-Patchwork-Hint: ignore
To: pw@patches.jarry.cc
Cc: Robin Jarry <rjarry@redhat.com>,
    Robin Jarry <robin@jarry.cc>
Series
Forge ml sync
github_branchforge
github_prhttps://github.com/rjarry/patchwork/pull/3

Commit Message

Robin JarryMay. 29, 2026, 16:43. UTC
[v4,04/15] series: add metadata key-value store

Add a SeriesMetadata model for attaching arbitrary key-value pairs to
patch series. This will be used by forge backends to store references
such as PR numbers and branch names (e.g. github_pr=42,
github_branch=pwforge/1a2b/fix).

The key field is indexed for fast lookups by metadata type. The value
field is also indexed to support reverse lookups (e.g. finding which
series is linked to a given PR number).

Signed-off-by: Robin Jarry <robin@jarry.cc>
---

Notes:
    https://github.com/rjarry/patchwork/pull/3/commits/e1be2e22d2c9ec79330672c5f03a435b04a0ef44

 patchwork/admin.py                            |  9 ++++
 patchwork/migrations/0050_series_metadata.py  | 41 +++++++++++++++++++
 patchwork/models.py                           | 17 ++++++++
 patchwork/templates/patchwork/submission.html | 10 +++++
 patchwork/templatetags/utils.py               |  9 ++++
 patchwork/tests/test_series.py                | 39 ++++++++++++++++++
 6 files changed, 125 insertions(+)
 create mode 100644 patchwork/migrations/0050_series_metadata.py

Patch

mbox series
diff --git a/patchwork/admin.py b/patchwork/admin.py
index d1c389a..a264878 100644
--- a/patchwork/admin.py
+++ b/patchwork/admin.py
@@ -19,6 +19,7 @@ from patchwork.models import PatchRelation
 from patchwork.models import Person
 from patchwork.models import Project
 from patchwork.models import Series
+from patchwork.models import SeriesMetadata
 from patchwork.models import SeriesReference
 from patchwork.models import State
 from patchwork.models import Tag
@@ -181,6 +182,14 @@ class SeriesReferenceAdmin(admin.ModelAdmin):
 admin.site.register(SeriesReference, SeriesReferenceAdmin)
 
 
+class SeriesMetadataAdmin(admin.ModelAdmin):
+    model = SeriesMetadata
+    list_display = ('series', 'key', 'value')
+
+
+admin.site.register(SeriesMetadata, SeriesMetadataAdmin)
+
+
 class CheckAdmin(admin.ModelAdmin):
     list_display = (
         'patch',
diff --git a/patchwork/migrations/0050_series_metadata.py b/patchwork/migrations/0050_series_metadata.py
new file mode 100644
index 0000000..5f6b292
--- /dev/null
+++ b/patchwork/migrations/0050_series_metadata.py
@@ -0,0 +1,41 @@
+# Generated by Django 5.1.15 on 2026-05-30 00:43
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('patchwork', '0049_series_respin_tracking'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SeriesMetadata',
+            fields=[
+                (
+                    'id',
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                ('key', models.CharField(db_index=True, max_length=255)),
+                ('value', models.TextField(db_index=True, max_length=255)),
+                (
+                    'series',
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name='metadata',
+                        to='patchwork.series',
+                    ),
+                ),
+            ],
+            options={
+                'ordering': ['key'],
+                'unique_together': {('series', 'key')},
+            },
+        ),
+    ]
diff --git a/patchwork/models.py b/patchwork/models.py
index 387490a..0b23b3b 100644
--- a/patchwork/models.py
+++ b/patchwork/models.py
@@ -1042,6 +1042,23 @@ class SeriesReference(models.Model):
         unique_together = [('project', 'msgid')]
 
 
+class SeriesMetadata(models.Model):
+    series = models.ForeignKey(
+        Series,
+        related_name='metadata',
+        on_delete=models.CASCADE,
+    )
+    key = models.CharField(max_length=255, db_index=True)
+    value = models.TextField(max_length=255, db_index=True)
+
+    def __str__(self):
+        return '%s=%s' % (self.key, self.value)
+
+    class Meta:
+        unique_together = [('series', 'key')]
+        ordering = ['key']
+
+
 class Bundle(models.Model):
     owner = models.ForeignKey(
         User,
diff --git a/patchwork/templates/patchwork/submission.html b/patchwork/templates/patchwork/submission.html
index b02555e..4cec979 100644
--- a/patchwork/templates/patchwork/submission.html
+++ b/patchwork/templates/patchwork/submission.html
@@ -112,6 +112,16 @@
     </td>
   </tr>
 {% endif %}
+{% if submission.series.metadata.all %}
+  <tr>
+    <th>Metadata</th>
+    <td>
+{% for entry in submission.series.metadata.all %}
+      <strong>{{ entry.key }}:</strong> {{ entry.value|metadata_value }}{% if not forloop.last %} | {% endif %}
+{% endfor %}
+    </td>
+  </tr>
+{% endif %}
 {% if submission.related %}
   <tr>
     <th>Related</th>
diff --git a/patchwork/templatetags/utils.py b/patchwork/templatetags/utils.py
index 78c0aac..198fa04 100644
--- a/patchwork/templatetags/utils.py
+++ b/patchwork/templatetags/utils.py
@@ -4,6 +4,7 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 
 from django import template
+from django.utils.html import format_html
 
 register = template.Library()
 
@@ -16,3 +17,11 @@ def verbose_name_plural(obj):
 @register.simple_tag
 def is_editable(obj, user):
     return obj.is_editable(user)
+
+
+@register.filter
+def metadata_value(value):
+    s = str(value)
+    if s.startswith(('http://', 'https://')):
+        return format_html('<a href="{}">{}</a>', s, s)
+    return format_html('<code>{}</code>', s)
diff --git a/patchwork/tests/test_series.py b/patchwork/tests/test_series.py
index e5f60e3..dadf34e 100644
--- a/patchwork/tests/test_series.py
+++ b/patchwork/tests/test_series.py
@@ -1081,3 +1081,42 @@ class SeriesDependencyTestCase(SeriesDependencyBase):
         self.assertEqual(series2.dependencies.count(), 1)
         self.assertEqual(series3.dependencies.count(), 2)
         self.assertEqual(series3.dependents.count(), 0)
+
+
+class SeriesMetadataTest(TestCase):
+    def test_create_metadata(self):
+        series = utils.create_series()
+        meta = models.SeriesMetadata.objects.create(
+            series=series, key='github_pr', value='42'
+        )
+        self.assertEqual(str(meta), 'github_pr=42')
+
+    def test_unique_key_per_series(self):
+        from django.db import IntegrityError
+
+        series = utils.create_series()
+        models.SeriesMetadata.objects.create(
+            series=series, key='github_pr', value='42'
+        )
+        with self.assertRaises(IntegrityError):
+            models.SeriesMetadata.objects.create(
+                series=series, key='github_pr', value='99'
+            )
+
+    def test_lookup_by_key_and_value(self):
+        series = utils.create_series()
+        models.SeriesMetadata.objects.create(
+            series=series, key='github_pr', value='42'
+        )
+        found = models.SeriesMetadata.objects.get(key='github_pr', value='42')
+        self.assertEqual(found.series, series)
+
+    def test_multiple_keys(self):
+        series = utils.create_series()
+        models.SeriesMetadata.objects.create(
+            series=series, key='github_pr', value='42'
+        )
+        models.SeriesMetadata.objects.create(
+            series=series, key='github_branch', value='pwforge/1a2b/fix'
+        )
+        self.assertEqual(series.metadata.count(), 2)