Skip to content

Commit 9e708d2

Browse files
andrepapotivictor-accarini
authored andcommitted
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>
1 parent 748355a commit 9e708d2

File tree

7 files changed

+278
-0
lines changed

7 files changed

+278
-0
lines changed

htdocs/css/style.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,12 @@ span.p_mod { color: #a020f0; }
431431
font-weight: bold;
432432
}
433433

434+
/* series */
435+
a.series-list-header {
436+
color: inherit; /* Inherit color from parent element */
437+
text-decoration: none; /* Optional: removes underline */
438+
}
439+
434440
/* bundles */
435441
table.bundlelist {
436442
margin-top: 2em;

patchwork/models.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from django.contrib.auth.models import User
1515
from django.core.exceptions import ValidationError
1616
from django.core.validators import validate_unicode_slug
17+
from django.db.models import Count
1718
from django.db import models
1819
from django.urls import reverse
1920
from django.utils.functional import cached_property
@@ -1015,6 +1016,30 @@ def add_dependencies(self, dependencies):
10151016
)
10161017
self.save()
10171018

1019+
@property
1020+
def interest_count(self):
1021+
count = self.patches.aggregate(Count('attention_set', distinct=True))
1022+
return count['attention_set__count']
1023+
1024+
@property
1025+
def check_count(self):
1026+
"""Generate a list of unique checks for all patchs in the series.
1027+
1028+
Compile a list of checks associated with this series patches for each
1029+
type of check. Only "unique" checks are considered, identified by their
1030+
'context' field. This means, given n checks with the same 'context', the
1031+
newest check is the only one counted regardless of its value. The end
1032+
result will be a association of types to number of unique checks for
1033+
said type.
1034+
"""
1035+
counts = {key: 0 for key, _ in Check.STATE_CHOICES}
1036+
1037+
for p in self.patches.all():
1038+
for check in p.checks:
1039+
counts[check.state] += 1
1040+
1041+
return counts
1042+
10181043
def add_cover_letter(self, cover):
10191044
"""Add a cover letter to the series.
10201045
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
{% extends "base.html" %}
2+
3+
{% load person %}
4+
{% load static %}
5+
6+
{% block title %}{{project.name}}{% endblock %}
7+
{% block series_active %}active{% endblock %}
8+
9+
{% block body %}
10+
11+
{% load person %}
12+
{% load listurl %}
13+
{% load patch %}
14+
{% load series %}
15+
{% load project %}
16+
{% load static %}
17+
18+
{% include "patchwork/partials/pagination.html" %}
19+
20+
<input type="hidden" name="form" value="serieslistform"/>
21+
<input type="hidden" name="project" value="{{project.id}}"/>
22+
23+
<table id="serieslist" class="table table-hover table-extra-condensed table-striped pw-list" data-toggle="checkboxes" data-range="true">
24+
<thead>
25+
<tr>
26+
{% if user.is_authenticated and user.profile.show_ids %}
27+
<th>
28+
ID
29+
</th>
30+
{% endif %}
31+
32+
<th>
33+
<span class="series-list-header">Series</span>
34+
</th>
35+
36+
<th>
37+
Version
38+
</th>
39+
40+
<th>
41+
{% project_tags %}
42+
</th>
43+
44+
<th>
45+
<span class="series-list-header" title="Success / Warning / Fail">S/W/F</span>
46+
</th>
47+
48+
<th>
49+
<span class="series-list-header" title="Declared review interest">Review Interest</span>
50+
</th>
51+
52+
<th>
53+
<span class="series-list-header">Patches</span>
54+
</th>
55+
56+
<th>
57+
{% if 'date.asc' == order %}
58+
<a href="{% listurl order='date.desc' %}" class="colactive">
59+
<span class="glyphicon glyphicon-chevron-up"></span>
60+
{% elif 'date.desc' == order %}
61+
<a href="{% listurl order='date.asc' %}" class="colactive">
62+
<span class="glyphicon glyphicon-chevron-down"></span>
63+
{% endif %}
64+
<span class="series-list-header">Date</span>
65+
{% if 'date.asc' == order or 'date.desc' == order%}
66+
</a>
67+
{% endif %}
68+
</th>
69+
70+
<th>
71+
<span class="series-list-header">Submitter</span>
72+
</th>
73+
</tr>
74+
</thead>
75+
76+
<tbody>
77+
{% for series in page %}
78+
<tr id="series_row:{{series.id}}">
79+
{% if user.is_authenticated and user.profile.show_ids %}
80+
<td>
81+
<button type="button" class="btn btn-xs btn-copy" data-clipboard-text="{{ series.id }}" title="Copy to Clipboard">
82+
{{ series.id }}
83+
</button>
84+
</td>
85+
{% endif %}
86+
<td>
87+
{{ series.name|default:"[no subject]"|truncatechars:100 }}
88+
</td>
89+
<td>
90+
{{ series.version|default:"-"}}
91+
</td>
92+
93+
<td id="series-tags:{{series.id}}" class="text-nowrap">{{ series|series_tags }}</td>
94+
<td id="series-checks:{{series.id}}" class="text-nowrap">{{ series|series_checks }}</td>
95+
<td id="series-interest:{{series.id}}" class="text-nowrap">{{ series|series_interest }}</td>
96+
<td>{{ series.received_total}}</td>
97+
<td class="text-nowrap">{{ series.date|date:"Y-m-d" }}</td>
98+
<td>{{ series.submitter|personify:project }}</td>
99+
</tr>
100+
{% empty %}
101+
<tr>
102+
<td colspan="8">No series to display</td>
103+
</tr>
104+
{% endfor %}
105+
</tbody>
106+
</table>
107+
108+
{% if page.paginator.count %}
109+
{% include "patchwork/partials/pagination.html" %}
110+
{% endif %}
111+
{% endblock %}

patchwork/templatetags/series.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Patchwork - automated patch tracking system
2+
# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
3+
# Copyright (C) 2015 Intel Corporation
4+
#
5+
# SPDX-License-Identifier: GPL-2.0-or-later
6+
7+
from django import template
8+
from django.utils.safestring import mark_safe
9+
10+
from patchwork.models import Check
11+
12+
13+
register = template.Library()
14+
15+
16+
@register.filter(name='series_tags')
17+
def series_tags(series):
18+
counts = []
19+
titles = []
20+
21+
for tag in [t for t in series.project.tags if t.show_column]:
22+
count = 0
23+
for patch in series.patches.with_tag_counts(series.project).all():
24+
count += getattr(patch, tag.attr_name)
25+
26+
titles.append('%d %s' % (count, tag.name))
27+
if count == 0:
28+
counts.append('-')
29+
else:
30+
counts.append(str(count))
31+
32+
return mark_safe(
33+
'<span title="%s">%s</span>' % (' / '.join(titles), ' '.join(counts))
34+
)
35+
36+
37+
@register.filter(name='series_checks')
38+
def series_checks(series):
39+
required = [Check.STATE_SUCCESS, Check.STATE_WARNING, Check.STATE_FAIL]
40+
titles = ['Success', 'Warning', 'Fail']
41+
counts = series.check_count
42+
43+
check_elements = []
44+
for state in required[::-1]:
45+
if counts[state]:
46+
color = dict(Check.STATE_CHOICES).get(state)
47+
count = str(counts[state])
48+
else:
49+
color = ''
50+
count = '-'
51+
52+
check_elements.append(
53+
f'<span class="patchlistchecks {color}">{count}</span>'
54+
)
55+
56+
check_elements.reverse()
57+
58+
return mark_safe(
59+
'<span title="%s">%s</span>'
60+
% (' / '.join(titles), ''.join(check_elements))
61+
)
62+
63+
64+
@register.filter(name='series_interest')
65+
def series_interest(series):
66+
reviews = series.interest_count
67+
review_title = (
68+
f'has {reviews} interested reviewers'
69+
if reviews > 0
70+
else 'no interested reviewers'
71+
)
72+
review_class = 'exists' if reviews > 0 else ''
73+
return mark_safe(
74+
'<span class="patchinterest %s" title="%s">%s</span>'
75+
% (review_class, review_title, reviews if reviews > 0 else '-')
76+
)

patchwork/urls.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,19 @@
3333
path('', project_views.project_list, name='project-list'),
3434
path(
3535
'project/<project_id>/list/',
36+
patch_views.patch_list_redirect,
37+
name='patch-list-redirect',
38+
),
39+
path(
40+
'project/<project_id>/patches/',
3641
patch_views.patch_list,
3742
name='patch-list',
3843
),
44+
path(
45+
'project/<project_id>/series/',
46+
series_views.series_list,
47+
name='series-list',
48+
),
3949
path(
4050
'project/<project_id>/bundles/',
4151
bundle_views.bundle_list,

patchwork/views/patch.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from django.http import HttpResponseRedirect
1212
from django.shortcuts import get_object_or_404
1313
from django.shortcuts import render
14+
from django.shortcuts import redirect
1415
from django.urls import reverse
1516

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

4243

44+
def patch_list_redirect(request, project_id):
45+
new_url = reverse('patch-list', kwargs={'project_id': project_id})
46+
return redirect(f'{new_url}?{request.GET.urlencode()}')
47+
48+
4349
def patch_detail(request, project_id, msgid):
4450
project = get_object_or_404(Project, linkname=project_id)
4551
db_msgid = Patch.decode_msgid(msgid)

patchwork/views/series.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
# Copyright (C) 2017 Stephen Finucane <stephen@that.guru>
33
#
44
# SPDX-License-Identifier: GPL-2.0-or-later
5+
import collections
56

67
from django.http import HttpResponse
78
from django.shortcuts import get_object_or_404
9+
from django.shortcuts import render
810

911
from patchwork.models import Series
12+
from patchwork.models import Project
1013
from patchwork.views.utils import series_to_mbox
14+
from patchwork.paginator import Paginator
1115

1216

1317
def series_mbox(request, series_id):
@@ -20,3 +24,43 @@ def series_mbox(request, series_id):
2024
)
2125

2226
return response
27+
28+
29+
def series_list(request, project_id):
30+
project = get_object_or_404(Project, linkname=project_id)
31+
sort = request.GET.get('order', 'date.desc')
32+
sort_field, sort_dir = sort.split('.')
33+
sort_order = f"{'-' if sort_dir == 'desc' else ''}{sort_field}"
34+
context = {}
35+
series_list = (
36+
Series.objects.filter(project=project)
37+
.only(
38+
'submitter',
39+
'project',
40+
'version',
41+
'name',
42+
'date',
43+
'id',
44+
'cover_letter',
45+
)
46+
.select_related('project', 'submitter', 'cover_letter')
47+
.order_by(sort_order)
48+
)
49+
50+
paginator = Paginator(request, series_list)
51+
context.update(
52+
{
53+
'project': project,
54+
'projects': Project.objects.all(),
55+
'series_list': series_list,
56+
'page': paginator.current_page,
57+
'order': sort,
58+
'list_view': {
59+
'view': 'series-list',
60+
'view_params': {'project_id': project.linkname},
61+
'params': collections.OrderedDict(),
62+
},
63+
}
64+
)
65+
66+
return render(request, 'patchwork/series-list.html', context)

0 commit comments

Comments
 (0)