Skip to content

Commit a0384a0

Browse files
committed
add package updates list view with table, filters, and nav entry
- add PackageUpdateTable with installed/available package links and security badges - add package_update_list view with security type and search filters - add /packages/updates/ url route - add packages submenu in navbar (packages + updates) - add 6 view tests
1 parent b96e4f7 commit a0384a0

File tree

8 files changed

+210
-6
lines changed

8 files changed

+210
-6
lines changed

hosts/tables.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
HOSTNAME_TEMPLATE = '<a href="{{ record.get_absolute_url }}">{{ record.hostname }}</a>'
2323
SEC_UPDATES_TEMPLATE = (
2424
'{% with count=record.get_num_security_updates %}'
25-
'{% if count != 0 %}<span style="color:red">{{ count }}</span>{% else %}{% endif %}'
25+
'{% if count != 0 %}<a href="{% url \'packages:package_update_list\' %}?host_id={{ record.id }}&security=true">'
26+
'<span style="color:red">{{ count }}</span></a>{% endif %}'
2627
'{% endwith %}'
2728
)
2829
BUG_UPDATES_TEMPLATE = (
2930
'{% with count=record.get_num_bugfix_updates %}'
30-
'{% if count != 0 %}<span style="color:orange">{{ count }}</span>{% else %}{% endif %}'
31+
'{% if count != 0 %}<a href="{% url \'packages:package_update_list\' %}?host_id={{ record.id }}&security=false">'
32+
'<span style="color:orange">{{ count }}</span></a>{% endif %}'
3133
'{% endwith %}'
3234
)
3335
AFFECTED_ERRATA_TEMPLATE = (

hosts/views.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ def host_list(request):
9090
if 'package' in request.GET:
9191
hosts = hosts.filter(packages__name__name=request.GET['package'])
9292

93+
if 'update_id' in request.GET:
94+
hosts = hosts.filter(updates=request.GET['update_id'])
95+
9396
if 'repo_id' in request.GET:
9497
hosts = hosts.filter(repos=request.GET['repo_id'])
9598

packages/tables.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import django_tables2 as tables
1616

17-
from packages.models import Package, PackageName
17+
from packages.models import Package, PackageName, PackageUpdate
1818
from util.tables import BaseTable
1919

2020
PACKAGE_NAME_TEMPLATE = '<a href="{{ record.get_absolute_url }}">{{ record }}</a>'
@@ -118,3 +118,74 @@ class PackageNameTable(BaseTable):
118118
class Meta(BaseTable.Meta):
119119
model = PackageName
120120
fields = ('packagename_name', 'versions')
121+
122+
123+
UPDATE_OLD_TEMPLATE = (
124+
'<a href="{% url \'packages:package_detail\' record.oldpackage.id %}">'
125+
'{{ record.oldpackage }}</a>'
126+
)
127+
UPDATE_NEW_TEMPLATE = (
128+
'<a href="{% url \'packages:package_detail\' record.newpackage.id %}">'
129+
'{{ record.newpackage }}</a>'
130+
)
131+
UPDATE_HOSTS_TEMPLATE = (
132+
'<a href="{% url \'hosts:host_list\' %}?update_id={{ record.id }}">'
133+
'{{ record.host_count }}</a>'
134+
)
135+
UPDATE_AFFECTED_TEMPLATE = (
136+
'<a href="{% url \'errata:erratum_list\' %}?package_id={{ record.oldpackage.id }}&type=affected">'
137+
'{{ record.affected_count }}</a>'
138+
)
139+
UPDATE_FIXED_TEMPLATE = (
140+
'<a href="{% url \'errata:erratum_list\' %}?package_id={{ record.newpackage.id }}&type=fixed">'
141+
'{{ record.fixed_count }}</a>'
142+
)
143+
144+
145+
UPDATE_TYPE_TEMPLATE = (
146+
'{% if record.security %}'
147+
'<span class="label label-danger">Security</span>'
148+
'{% else %}'
149+
'<span class="label label-info">Bugfix</span>'
150+
'{% endif %}'
151+
)
152+
153+
154+
class PackageUpdateTable(BaseTable):
155+
oldpackage = tables.TemplateColumn(
156+
UPDATE_OLD_TEMPLATE,
157+
verbose_name='Installed',
158+
attrs={'th': {'class': 'col-sm-3'}, 'td': {'class': 'col-sm-3'}},
159+
)
160+
newpackage = tables.TemplateColumn(
161+
UPDATE_NEW_TEMPLATE,
162+
verbose_name='Available',
163+
attrs={'th': {'class': 'col-sm-3'}, 'td': {'class': 'col-sm-3'}},
164+
)
165+
security = tables.TemplateColumn(
166+
UPDATE_TYPE_TEMPLATE,
167+
verbose_name='Type',
168+
attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
169+
)
170+
hosts = tables.TemplateColumn(
171+
UPDATE_HOSTS_TEMPLATE,
172+
verbose_name='Hosts',
173+
order_by='host_count',
174+
attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
175+
)
176+
affected = tables.TemplateColumn(
177+
UPDATE_AFFECTED_TEMPLATE,
178+
verbose_name='Affected by Errata',
179+
order_by='affected_count',
180+
attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
181+
)
182+
fixed = tables.TemplateColumn(
183+
UPDATE_FIXED_TEMPLATE,
184+
verbose_name='Fixed in Errata',
185+
order_by='fixed_count',
186+
attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
187+
)
188+
189+
class Meta(BaseTable.Meta):
190+
model = PackageUpdate
191+
fields = ('oldpackage', 'newpackage', 'security', 'hosts', 'affected', 'fixed')
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{% extends "objectlist.html" %}
2+
3+
{% block page_title %}Package Updates{% endblock %}
4+
5+
{% block breadcrumbs %} {{ block.super }} <li class="active"><a href="{% url 'packages:package_update_list' %}">Package Updates</a></li>{% endblock %}
6+
7+
{% block content_title %} Package Updates {% endblock %}

packages/tests/test_views.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from django.contrib.auth.models import User
2+
from django.test import TestCase, override_settings
3+
from django.urls import reverse
4+
5+
from arch.models import PackageArchitecture
6+
from packages.models import Package, PackageName, PackageUpdate
7+
8+
9+
@override_settings(
10+
CELERY_TASK_ALWAYS_EAGER=True,
11+
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}}
12+
)
13+
class PackageUpdateViewTests(TestCase):
14+
15+
def setUp(self):
16+
self.user = User.objects.create_user(
17+
username='testuser', password='testpass'
18+
)
19+
self.client.login(username='testuser', password='testpass')
20+
self.arch = PackageArchitecture.objects.create(name='x86_64')
21+
self.name = PackageName.objects.create(name='openssl')
22+
self.old = Package.objects.create(
23+
name=self.name, arch=self.arch, epoch='',
24+
version='1.1.1', release='1', packagetype='R',
25+
)
26+
self.new = Package.objects.create(
27+
name=self.name, arch=self.arch, epoch='',
28+
version='1.1.2', release='1', packagetype='R',
29+
)
30+
self.sec_update = PackageUpdate.objects.create(
31+
oldpackage=self.old, newpackage=self.new, security=True,
32+
)
33+
self.bug_update = PackageUpdate.objects.create(
34+
oldpackage=self.old, newpackage=self.new, security=False,
35+
)
36+
37+
def test_update_list(self):
38+
resp = self.client.get(reverse('packages:package_update_list'))
39+
self.assertEqual(resp.status_code, 200)
40+
self.assertContains(resp, 'openssl')
41+
42+
def test_update_list_filter_security(self):
43+
resp = self.client.get(
44+
reverse('packages:package_update_list'), {'security': 'true'}
45+
)
46+
self.assertEqual(resp.status_code, 200)
47+
self.assertContains(resp, 'Security')
48+
49+
def test_update_list_filter_bugfix(self):
50+
resp = self.client.get(
51+
reverse('packages:package_update_list'), {'security': 'false'}
52+
)
53+
self.assertEqual(resp.status_code, 200)
54+
self.assertContains(resp, 'Bugfix')
55+
56+
def test_update_list_search(self):
57+
resp = self.client.get(
58+
reverse('packages:package_update_list'), {'search': 'openssl'}
59+
)
60+
self.assertEqual(resp.status_code, 200)
61+
self.assertContains(resp, 'openssl')
62+
63+
def test_update_list_search_no_results(self):
64+
resp = self.client.get(
65+
reverse('packages:package_update_list'), {'search': 'nonexistent'}
66+
)
67+
self.assertEqual(resp.status_code, 200)
68+
69+
def test_update_list_requires_login(self):
70+
self.client.logout()
71+
resp = self.client.get(reverse('packages:package_update_list'))
72+
self.assertEqual(resp.status_code, 302)

packages/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@
2727
path('name/<str:packagename>/', views.package_name_detail, name='package_name_detail'),
2828
path('id/', views.package_list, name='package_list'),
2929
path('id/<int:package_id>/', views.package_detail, name='package_detail'),
30+
path('updates/', views.package_update_list, name='package_update_list'),
3031
]

packages/views.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# along with Patchman. If not, see <http://www.gnu.org/licenses/>
1717

1818
from django.contrib.auth.decorators import login_required
19-
from django.db.models import Q
19+
from django.db.models import Count, Q
2020
from django.shortcuts import get_object_or_404, render
2121
from django_tables2 import RequestConfig
2222
from rest_framework import viewsets
@@ -26,7 +26,7 @@
2626
from packages.serializers import (
2727
PackageNameSerializer, PackageSerializer, PackageUpdateSerializer,
2828
)
29-
from packages.tables import PackageNameTable, PackageTable
29+
from packages.tables import PackageNameTable, PackageTable, PackageUpdateTable
3030
from util.filterspecs import Filter, FilterBar
3131

3232

@@ -172,6 +172,48 @@ def package_name_detail(request, packagename):
172172
'allversions': allversions})
173173

174174

175+
@login_required
176+
def package_update_list(request):
177+
updates = PackageUpdate.objects.select_related(
178+
'oldpackage__name', 'oldpackage__arch',
179+
'newpackage__name', 'newpackage__arch',
180+
).annotate(
181+
host_count=Count('host', distinct=True),
182+
affected_count=Count('oldpackage__affected_by_erratum', distinct=True),
183+
fixed_count=Count('newpackage__provides_fix_in_erratum', distinct=True),
184+
)
185+
186+
if 'security' in request.GET:
187+
security = request.GET['security'] == 'true'
188+
updates = updates.filter(security=security)
189+
if 'host_id' in request.GET:
190+
updates = updates.filter(host=request.GET['host_id'])
191+
if 'search' in request.GET:
192+
terms = request.GET['search'].lower()
193+
query = Q()
194+
for term in terms.split(' '):
195+
q = (Q(oldpackage__name__name__icontains=term) |
196+
Q(newpackage__name__name__icontains=term))
197+
query = query & q
198+
updates = updates.filter(query)
199+
else:
200+
terms = ''
201+
202+
filter_list = []
203+
filter_list.append(Filter(request, 'Type', 'security',
204+
{'true': 'Security', 'false': 'Bugfix'}))
205+
filter_bar = FilterBar(request, filter_list)
206+
207+
table = PackageUpdateTable(updates.distinct())
208+
RequestConfig(request, paginate={'per_page': 50}).configure(table)
209+
210+
return render(request,
211+
'packages/package_update_list.html',
212+
{'table': table,
213+
'filter_bar': filter_bar,
214+
'terms': terms})
215+
216+
175217
class PackageNameViewSet(viewsets.ModelViewSet):
176218
"""
177219
API endpoint that allows package names to be viewed or edited.

util/templates/navbar.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212
<li><a href="{% url 'repos:mirror_list' %}">Mirrors</a></li>
1313
</ul>
1414
</li>
15-
<li class="{% if '/packages' in request.path %}active{% endif %}"><a href="{% url 'packages:package_name_list' %}">Packages</a></li>
15+
<li class="has-submenu open {% if '/packages' in request.path %}active{% endif %}">
16+
<a href="#">Packages <span class="caret"></span></a>
17+
<ul class="submenu">
18+
<li><a href="{% url 'packages:package_name_list' %}">Packages</a></li>
19+
<li><a href="{% url 'packages:package_update_list' %}">Updates</a></li>
20+
</ul>
21+
</li>
1622
<li class="{% if '/errata' in request.path %}active{% endif %}"><a href="{% url 'errata:erratum_list' %}">Errata</a></li>
1723
<li class="has-submenu open {% if '/cves' in request.path or '/cwes' in request.path or '/references' in request.path or '/security' in request.path %}active{% endif %}">
1824
<a href="#">Security <span class="caret"></span></a>

0 commit comments

Comments
 (0)