Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
htdocs/js/*.min.js binary
htdocs/css/selectize.bootstrap3.css binary
14 changes: 14 additions & 0 deletions docs/api/schemas/latest/patchwork.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,15 @@ paths:
schema:
title: ''
type: string
- in: query
name: labels
description: |
List of labels assigned to queried patches, separated by ','.
If a label name begins with a '-', patches with that label won't
be included.
schema:
title: ''
type: string
responses:
'200':
description: 'List of patches'
Expand Down Expand Up @@ -2312,6 +2321,11 @@ components:
type: array
items:
$ref: '#/components/schemas/PatchEmbedded'
labels:
title: Labels
type: array
items:
type: string
PatchDetail:
type: object
title: Patches
Expand Down
18 changes: 18 additions & 0 deletions docs/api/schemas/patchwork.j2
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,17 @@ paths:
schema:
title: ''
type: string
{% endif %}
{% if version >= (1, 4) %}
- in: query
name: labels
description: |
List of labels assigned to queried patches, separated by ','.
If a label name begins with a '-', patches with that label won't
be included.
schema:
title: ''
type: string
{% endif %}
responses:
'200':
Expand Down Expand Up @@ -2397,6 +2408,13 @@ components:
type: array
items:
$ref: '#/components/schemas/PatchEmbedded'
{% endif %}
{% if version >= (1, 4) %}
labels:
title: Labels
type: array
items:
type: string
{% endif %}
PatchDetail:
type: object
Expand Down
14 changes: 14 additions & 0 deletions docs/api/schemas/v1.4/patchwork.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,15 @@ paths:
schema:
title: ''
type: string
- in: query
name: labels
description: |
List of labels assigned to queried patches, separated by ','.
If a label name begins with a '-', patches with that label won't
be included.
schema:
title: ''
type: string
responses:
'200':
description: 'List of patches'
Expand Down Expand Up @@ -2312,6 +2321,11 @@ components:
type: array
items:
$ref: '#/components/schemas/PatchEmbedded'
labels:
title: Labels
type: array
items:
type: string
PatchDetail:
type: object
title: Patches
Expand Down
27 changes: 27 additions & 0 deletions docs/deployment/management.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,30 @@ patches for these new tags.
.. option:: patch_id

a patch ID number. If not supplied, all patches will be updated.

.. _command-relabel:

relabel
~~~~~~~

.. program:: manage.py relabel

Relabel patches based on the subject line.

.. code-block:: shell

./manage.py relabel [PROJECT [PROJECT...]]

Patchwork extracts labels from square brackets at the beginning of the subject
line, eg. ``[label1, label2] Patch title``.

Only labels created in the admin interface will be recognized.
The script prioritizes project-specific labels if both project-specific and
general labels with the same name exist.

The script will not remove labels manually added to patches, but will re-add
labels manually removed from patches.

.. option:: PROJECT

list of IDs of projects to update. Relabels all projects if none specified.
23 changes: 23 additions & 0 deletions docs/usage/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,29 @@ or a cover letter or the web URL to a patch or a series:

__ https://www.kernel.org/doc/html/latest/process/submitting-patches.html

Labels
~~~~~~

Labels are a way to organize and prioritize submissions. You can apply labels
to patches and cover letters to signify priority, category, or any other marker
you wish.

Labels can be either global or tied to the project they are created in. Labels
can have an optional description attached, which will provide a little insight
into the purpose of the label. Labels are completely customizable and the
labels available will vary by instance.

Labels are parsed out of the square brackets of the subject line.
For example, subject line ``[RFC, main] Add tests`` may cause the patch to be
labeled with ``RFC`` and ``main``, if such labels were created for the project
or instance. Labels can be created in the admin panel.

.. note::

In order to refresh the list of labels for existing patches, for example
after updating Patchwork or adding a new label in the admin panel,
use the :ref:`command-relabel` management command.

Checks
~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion htdocs/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,4 @@ js

:Website: https://selectize.github.io/selectize.js/
:GitHub: https://github.com/selectize/selectize.js
:Version: 0.13.5
:Version: 0.15.2
26 changes: 20 additions & 6 deletions htdocs/css/selectize.bootstrap3.css

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions htdocs/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,14 @@ select[class^=change-property-], .archive-patch-select, .add-bundle {
margin: 0px 4px 0px 4px;
}

.labels-field {
padding: 4px;
margin-right: 8px;
box-sizing: border-box;
border-radius: 4px;
min-width: 200px;
}

.patch-form-submit {
font-weight: bold;
padding: 4px;
Expand Down
34 changes: 32 additions & 2 deletions htdocs/js/selectize.min.js

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions patchwork/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from patchwork.models import Cover
from patchwork.models import CoverComment
from patchwork.models import DelegationRule
from patchwork.models import Label
from patchwork.models import Patch
from patchwork.models import PatchComment
from patchwork.models import PatchRelation
Expand Down Expand Up @@ -44,11 +45,18 @@ class DelegationRuleInline(admin.TabularInline):
fields = ('path', 'user', 'priority')


class LabelInline(admin.TabularInline):
model = Label
fields = ('name', 'description', 'color')
extra = 0


@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
list_display = ('name', 'linkname', 'listid', 'listemail')
inlines = [
DelegationRuleInline,
LabelInline,
]


Expand Down Expand Up @@ -91,6 +99,7 @@ class PatchAdmin(admin.ModelAdmin):
'is_pull_request',
)
list_filter = ('project', 'submitter', 'state', 'archived')
readonly_fields = ('labels',)
list_select_related = ('submitter', 'project', 'state')
search_fields = ('name', 'submitter__name', 'submitter__email')
date_hierarchy = 'date'
Expand Down Expand Up @@ -194,3 +203,12 @@ class TagAdmin(admin.ModelAdmin):
@admin.register(PatchRelation)
class PatchRelationAdmin(admin.ModelAdmin):
model = PatchRelation


@admin.register(Label)
class LabelAdmin(admin.ModelAdmin):
list_display = ('name', 'color')

def get_queryset(self, request):
qs = super(LabelAdmin, self).get_queryset(request)
return qs.filter(project=None)
9 changes: 8 additions & 1 deletion patchwork/api/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from rest_framework.generics import RetrieveAPIView
from rest_framework.reverse import reverse
from rest_framework.serializers import SerializerMethodField
from rest_framework.serializers import StringRelatedField

from patchwork.api.base import BaseHyperlinkedModelSerializer
from patchwork.api.filters import CoverFilterSet
Expand All @@ -25,6 +26,7 @@ class CoverListSerializer(BaseHyperlinkedModelSerializer):
mbox = SerializerMethodField()
series = SeriesSerializer(read_only=True)
comments = SerializerMethodField()
labels = StringRelatedField(many=True)

def get_web_url(self, instance):
request = self.context.get('request')
Expand Down Expand Up @@ -62,11 +64,13 @@ class Meta:
'mbox',
'series',
'comments',
'labels',
)
read_only_fields = fields
versioned_fields = {
'1.1': ('web_url', 'mbox', 'comments'),
'1.2': ('list_archive_url',),
'1.4': ('labels',),
}
extra_kwargs = {
'url': {'view_name': 'api-cover-detail'},
Expand Down Expand Up @@ -110,7 +114,10 @@ class CoverList(ListAPIView):
def get_queryset(self):
return (
Cover.objects.all()
.prefetch_related('series__project')
.prefetch_related(
'series__project',
'labels',
)
.select_related('project', 'submitter', 'series')
.defer('content', 'headers')
)
Expand Down
20 changes: 20 additions & 0 deletions patchwork/api/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
from patchwork.models import Series
from patchwork.models import State

from patchwork.models import exclude_submissions_by_labels
from patchwork.models import filter_submissions_by_labels


# custom backend

Expand Down Expand Up @@ -175,6 +178,21 @@ def msgid_filter(queryset, name, value):
return queryset.filter(**{name: '<' + value + '>'})


def labels_filter(queryset, _, value):
label_names = value.split(',')

labels_pos, labels_neg = [], []
for label in label_names:
if not label.startswith('-'):
labels_pos.append(label)
else:
labels_neg.append(label[1:])

queryset = exclude_submissions_by_labels(queryset, labels_neg)
queryset = filter_submissions_by_labels(queryset, labels_pos)
return queryset


class CoverFilterSet(TimestampMixin, BaseFilterSet):
project = ProjectFilter(queryset=Project.objects.all(), distinct=False)
# NOTE(stephenfin): We disable the select-based HTML widgets for these
Expand Down Expand Up @@ -206,6 +224,7 @@ class PatchFilterSet(TimestampMixin, BaseFilterSet):
state = StateFilter(queryset=State.objects.all(), distinct=False)
hash = CharFilter(lookup_expr='iexact')
msgid = CharFilter(method=msgid_filter)
labels = CharFilter(method=labels_filter)

class Meta:
model = Patch
Expand All @@ -225,6 +244,7 @@ class Meta:
)
versioned_fields = {
'1.2': ('hash', 'msgid'),
'1.4': ('labels'),
}


Expand Down
10 changes: 9 additions & 1 deletion patchwork/api/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from rest_framework.reverse import reverse
from rest_framework.serializers import SerializerMethodField
from rest_framework import status
from rest_framework.serializers import StringRelatedField

from patchwork.api.base import BaseHyperlinkedModelSerializer
from patchwork.api.base import PatchworkPermission
Expand Down Expand Up @@ -94,6 +95,7 @@ class PatchListSerializer(BaseHyperlinkedModelSerializer):
default=[],
style={'base_template': 'input.html'},
)
labels = StringRelatedField(many=True)

def get_web_url(self, instance):
request = self.context.get('request')
Expand Down Expand Up @@ -177,6 +179,7 @@ class Meta:
'checks',
'tags',
'related',
'labels',
)
read_only_fields = (
'url',
Expand All @@ -194,13 +197,15 @@ class Meta:
'check',
'checks',
'tags',
'labels',
)
versioned_fields = {
'1.1': ('comments', 'web_url'),
'1.2': (
'list_archive_url',
'related',
),
'1.4': ('labels',),
}
extra_kwargs = {
'url': {'view_name': 'api-patch-detail'},
Expand Down Expand Up @@ -367,6 +372,7 @@ def get_queryset(self):
'project',
'series__project',
'related__patches__project',
'labels',
)
.select_related('state', 'submitter', 'series')
.defer('content', 'diff', 'headers')
Expand All @@ -391,7 +397,9 @@ class PatchDetail(RetrieveUpdateAPIView):
def get_queryset(self):
return (
Patch.objects.all()
.prefetch_related('check_set', 'related__patches__project')
.prefetch_related(
'check_set', 'related__patches__project', 'labels'
)
.select_related(
'project', 'state', 'submitter', 'delegate', 'series'
)
Expand Down
24 changes: 24 additions & 0 deletions patchwork/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,27 @@ def from_db_value(self, value, *args, **kwargs):

def db_type(self, connection=None):
return 'char(%d)' % self.n_bytes


class ColorField(models.Field):
description = 'Hex color code'

def get_internal_type(self):
return 'PositiveIntegerField'

def to_python(self, value):
if isinstance(value, str) or value is None:
return value
return '#%06x' % value

def from_db_value(self, value, *args, **kwargs):
return self.to_python(value)

def get_prep_value(self, value):
return int(value.lstrip('#'), 16)

def formfield(self, *args, **kwargs):
from patchwork import forms # noqa

kwargs['form_class'] = forms.ColorField
return super(ColorField, self).formfield(*args, **kwargs)
Loading