Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
views: add series-list view
This view is meant to give a quickoverview on a project since some of
them can have hundreds of patches actives.
This view also allows the user to apply changes on all of the series
patches at once

Signed-off-by: andrepapoti <andrepapoti@gmail.com>
Signed-off-by: Victor Accarini <victor.accarini@profusion.mobi>
  • Loading branch information
andrepapoti authored and victor-accarini committed May 8, 2025
commit 9e708d2648d301e08a56ddb399fceb945641b6bb
6 changes: 6 additions & 0 deletions htdocs/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,12 @@ span.p_mod { color: #a020f0; }
font-weight: bold;
}

/* series */
a.series-list-header {
color: inherit; /* Inherit color from parent element */
text-decoration: none; /* Optional: removes underline */
}

/* bundles */
table.bundlelist {
margin-top: 2em;
Expand Down
25 changes: 25 additions & 0 deletions patchwork/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import validate_unicode_slug
from django.db.models import Count
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
Expand Down Expand Up @@ -1015,6 +1016,30 @@ def add_dependencies(self, dependencies):
)
self.save()

@property
def interest_count(self):
count = self.patches.aggregate(Count('attention_set', distinct=True))
return count['attention_set__count']

@property
def check_count(self):
"""Generate a list of unique checks for all patchs in the series.

Compile a list of checks associated with this series patches for each
type of check. Only "unique" checks are considered, identified by their
'context' field. This means, given n checks with the same 'context', the
newest check is the only one counted regardless of its value. The end
result will be a association of types to number of unique checks for
said type.
"""
counts = {key: 0 for key, _ in Check.STATE_CHOICES}

for p in self.patches.all():
for check in p.checks:
counts[check.state] += 1

return counts

def add_cover_letter(self, cover):
"""Add a cover letter to the series.

Expand Down
111 changes: 111 additions & 0 deletions patchwork/templates/patchwork/series-list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
{% extends "base.html" %}

{% load person %}
{% load static %}

{% block title %}{{project.name}}{% endblock %}
{% block series_active %}active{% endblock %}

{% block body %}

{% load person %}
{% load listurl %}
{% load patch %}
{% load series %}
{% load project %}
{% load static %}

{% include "patchwork/partials/pagination.html" %}

<input type="hidden" name="form" value="serieslistform"/>
<input type="hidden" name="project" value="{{project.id}}"/>

<table id="serieslist" class="table table-hover table-extra-condensed table-striped pw-list" data-toggle="checkboxes" data-range="true">
<thead>
<tr>
{% if user.is_authenticated and user.profile.show_ids %}
<th>
ID
</th>
{% endif %}

<th>
<span class="series-list-header">Series</span>
</th>

<th>
Version
</th>

<th>
{% project_tags %}
</th>

<th>
<span class="series-list-header" title="Success / Warning / Fail">S/W/F</span>
</th>

<th>
<span class="series-list-header" title="Declared review interest">Review Interest</span>
</th>

<th>
<span class="series-list-header">Patches</span>
</th>

<th>
{% if 'date.asc' == order %}
<a href="{% listurl order='date.desc' %}" class="colactive">
<span class="glyphicon glyphicon-chevron-up"></span>
{% elif 'date.desc' == order %}
<a href="{% listurl order='date.asc' %}" class="colactive">
<span class="glyphicon glyphicon-chevron-down"></span>
{% endif %}
<span class="series-list-header">Date</span>
{% if 'date.asc' == order or 'date.desc' == order%}
</a>
{% endif %}
</th>

<th>
<span class="series-list-header">Submitter</span>
</th>
</tr>
</thead>

<tbody>
{% for series in page %}
<tr id="series_row:{{series.id}}">
{% if user.is_authenticated and user.profile.show_ids %}
<td>
<button type="button" class="btn btn-xs btn-copy" data-clipboard-text="{{ series.id }}" title="Copy to Clipboard">
{{ series.id }}
</button>
</td>
{% endif %}
<td>
{{ series.name|default:"[no subject]"|truncatechars:100 }}
</td>
<td>
{{ series.version|default:"-"}}
</td>

<td id="series-tags:{{series.id}}" class="text-nowrap">{{ series|series_tags }}</td>
<td id="series-checks:{{series.id}}" class="text-nowrap">{{ series|series_checks }}</td>
<td id="series-interest:{{series.id}}" class="text-nowrap">{{ series|series_interest }}</td>
<td>{{ series.received_total}}</td>
<td class="text-nowrap">{{ series.date|date:"Y-m-d" }}</td>
<td>{{ series.submitter|personify:project }}</td>
</tr>
{% empty %}
<tr>
<td colspan="8">No series to display</td>
</tr>
{% endfor %}
</tbody>
</table>

{% if page.paginator.count %}
{% include "patchwork/partials/pagination.html" %}
{% endif %}
{% endblock %}
76 changes: 76 additions & 0 deletions patchwork/templatetags/series.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Patchwork - automated patch tracking system
# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
# Copyright (C) 2015 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-or-later

from django import template
from django.utils.safestring import mark_safe

from patchwork.models import Check


register = template.Library()


@register.filter(name='series_tags')
def series_tags(series):
counts = []
titles = []

for tag in [t for t in series.project.tags if t.show_column]:
count = 0
for patch in series.patches.with_tag_counts(series.project).all():
count += getattr(patch, tag.attr_name)

titles.append('%d %s' % (count, tag.name))
if count == 0:
counts.append('-')
else:
counts.append(str(count))

return mark_safe(
'<span title="%s">%s</span>' % (' / '.join(titles), ' '.join(counts))
)


@register.filter(name='series_checks')
def series_checks(series):
required = [Check.STATE_SUCCESS, Check.STATE_WARNING, Check.STATE_FAIL]
titles = ['Success', 'Warning', 'Fail']
counts = series.check_count

check_elements = []
for state in required[::-1]:
if counts[state]:
color = dict(Check.STATE_CHOICES).get(state)
count = str(counts[state])
else:
color = ''
count = '-'

check_elements.append(
f'<span class="patchlistchecks {color}">{count}</span>'
)

check_elements.reverse()

return mark_safe(
'<span title="%s">%s</span>'
% (' / '.join(titles), ''.join(check_elements))
)


@register.filter(name='series_interest')
def series_interest(series):
reviews = series.interest_count
review_title = (
f'has {reviews} interested reviewers'
if reviews > 0
else 'no interested reviewers'
)
review_class = 'exists' if reviews > 0 else ''
return mark_safe(
'<span class="patchinterest %s" title="%s">%s</span>'
% (review_class, review_title, reviews if reviews > 0 else '-')
)
10 changes: 10 additions & 0 deletions patchwork/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,19 @@
path('', project_views.project_list, name='project-list'),
path(
'project/<project_id>/list/',
patch_views.patch_list_redirect,
name='patch-list-redirect',
),
path(
'project/<project_id>/patches/',
patch_views.patch_list,
name='patch-list',
),
Comment on lines 34 to 43
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you rename this you need to provide a redirect. There are existing examples of redirects here. Don't forget to preserve query string arguments though since those matter here but didn't matter for fetching e.g. patch mboxes.

This should also be a separate commit, ideally placed before this one in the series.

path(
'project/<project_id>/series/',
series_views.series_list,
name='series-list',
),
path(
'project/<project_id>/bundles/',
bundle_views.bundle_list,
Expand Down
6 changes: 6 additions & 0 deletions patchwork/views/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.shortcuts import redirect
from django.urls import reverse

from patchwork.forms import CreateBundleForm
Expand Down Expand Up @@ -40,6 +41,11 @@ def patch_list(request, project_id):
return render(request, 'patchwork/list.html', context)


def patch_list_redirect(request, project_id):
new_url = reverse('patch-list', kwargs={'project_id': project_id})
return redirect(f'{new_url}?{request.GET.urlencode()}')


def patch_detail(request, project_id, msgid):
project = get_object_or_404(Project, linkname=project_id)
db_msgid = Patch.decode_msgid(msgid)
Expand Down
44 changes: 44 additions & 0 deletions patchwork/views/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
# Copyright (C) 2017 Stephen Finucane <stephen@that.guru>
#
# SPDX-License-Identifier: GPL-2.0-or-later
import collections

from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import render

from patchwork.models import Series
from patchwork.models import Project
from patchwork.views.utils import series_to_mbox
from patchwork.paginator import Paginator


def series_mbox(request, series_id):
Expand All @@ -20,3 +24,43 @@ def series_mbox(request, series_id):
)

return response


def series_list(request, project_id):
project = get_object_or_404(Project, linkname=project_id)
sort = request.GET.get('order', 'date.desc')
sort_field, sort_dir = sort.split('.')
sort_order = f"{'-' if sort_dir == 'desc' else ''}{sort_field}"
context = {}
series_list = (
Series.objects.filter(project=project)
.only(
'submitter',
'project',
'version',
'name',
'date',
'id',
'cover_letter',
)
.select_related('project', 'submitter', 'cover_letter')
.order_by(sort_order)
)

paginator = Paginator(request, series_list)
context.update(
{
'project': project,
'projects': Project.objects.all(),
'series_list': series_list,
'page': paginator.current_page,
'order': sort,
'list_view': {
'view': 'series-list',
'view_params': {'project_id': project.linkname},
'params': collections.OrderedDict(),
},
}
)

return render(request, 'patchwork/series-list.html', context)