diff --git a/hosts/tables.py b/hosts/tables.py
index b34b7d61..296b8df3 100644
--- a/hosts/tables.py
+++ b/hosts/tables.py
@@ -22,12 +22,14 @@
HOSTNAME_TEMPLATE = '{{ record.hostname }}'
SEC_UPDATES_TEMPLATE = (
'{% with count=record.get_num_security_updates %}'
- '{% if count != 0 %}{{ count }}{% else %}{% endif %}'
+ '{% if count != 0 %}'
+ '{{ count }}{% endif %}'
'{% endwith %}'
)
BUG_UPDATES_TEMPLATE = (
'{% with count=record.get_num_bugfix_updates %}'
- '{% if count != 0 %}{{ count }}{% else %}{% endif %}'
+ '{% if count != 0 %}'
+ '{{ count }}{% endif %}'
'{% endwith %}'
)
AFFECTED_ERRATA_TEMPLATE = (
diff --git a/hosts/views.py b/hosts/views.py
index f940c3a4..b4e002dd 100644
--- a/hosts/views.py
+++ b/hosts/views.py
@@ -90,6 +90,9 @@ def host_list(request):
if 'package' in request.GET:
hosts = hosts.filter(packages__name__name=request.GET['package'])
+ if 'update_id' in request.GET:
+ hosts = hosts.filter(updates=request.GET['update_id'])
+
if 'repo_id' in request.GET:
hosts = hosts.filter(repos=request.GET['repo_id'])
diff --git a/packages/tables.py b/packages/tables.py
index 633c79a2..0004c79a 100644
--- a/packages/tables.py
+++ b/packages/tables.py
@@ -14,7 +14,7 @@
import django_tables2 as tables
-from packages.models import Package, PackageName
+from packages.models import Package, PackageName, PackageUpdate
from util.tables import BaseTable
PACKAGE_NAME_TEMPLATE = '{{ record }}'
@@ -118,3 +118,74 @@ class PackageNameTable(BaseTable):
class Meta(BaseTable.Meta):
model = PackageName
fields = ('packagename_name', 'versions')
+
+
+UPDATE_OLD_TEMPLATE = (
+ ''
+ '{{ record.oldpackage }}'
+)
+UPDATE_NEW_TEMPLATE = (
+ ''
+ '{{ record.newpackage }}'
+)
+UPDATE_HOSTS_TEMPLATE = (
+ ''
+ '{{ record.host_count }}'
+)
+UPDATE_AFFECTED_TEMPLATE = (
+ ''
+ '{{ record.affected_count }}'
+)
+UPDATE_FIXED_TEMPLATE = (
+ ''
+ '{{ record.fixed_count }}'
+)
+
+
+UPDATE_TYPE_TEMPLATE = (
+ '{% if record.security %}'
+ 'Security'
+ '{% else %}'
+ 'Bugfix'
+ '{% endif %}'
+)
+
+
+class PackageUpdateTable(BaseTable):
+ oldpackage = tables.TemplateColumn(
+ UPDATE_OLD_TEMPLATE,
+ verbose_name='Installed',
+ attrs={'th': {'class': 'col-sm-3'}, 'td': {'class': 'col-sm-3'}},
+ )
+ newpackage = tables.TemplateColumn(
+ UPDATE_NEW_TEMPLATE,
+ verbose_name='Available',
+ attrs={'th': {'class': 'col-sm-3'}, 'td': {'class': 'col-sm-3'}},
+ )
+ security = tables.TemplateColumn(
+ UPDATE_TYPE_TEMPLATE,
+ verbose_name='Type',
+ attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
+ )
+ hosts = tables.TemplateColumn(
+ UPDATE_HOSTS_TEMPLATE,
+ verbose_name='Hosts',
+ order_by='host_count',
+ attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
+ )
+ affected = tables.TemplateColumn(
+ UPDATE_AFFECTED_TEMPLATE,
+ verbose_name='Affected by Errata',
+ order_by='affected_count',
+ attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
+ )
+ fixed = tables.TemplateColumn(
+ UPDATE_FIXED_TEMPLATE,
+ verbose_name='Fixed in Errata',
+ order_by='fixed_count',
+ attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
+ )
+
+ class Meta(BaseTable.Meta):
+ model = PackageUpdate
+ fields = ('oldpackage', 'newpackage', 'security', 'hosts', 'affected', 'fixed')
diff --git a/packages/templates/packages/package_update_list.html b/packages/templates/packages/package_update_list.html
new file mode 100644
index 00000000..62c2557d
--- /dev/null
+++ b/packages/templates/packages/package_update_list.html
@@ -0,0 +1,7 @@
+{% extends "objectlist.html" %}
+
+{% block page_title %}Package Updates{% endblock %}
+
+{% block breadcrumbs %} {{ block.super }}
Package Updates{% endblock %}
+
+{% block content_title %} Package Updates {% endblock %}
diff --git a/packages/tests/test_views.py b/packages/tests/test_views.py
new file mode 100644
index 00000000..9fc86abd
--- /dev/null
+++ b/packages/tests/test_views.py
@@ -0,0 +1,72 @@
+from django.contrib.auth.models import User
+from django.test import TestCase, override_settings
+from django.urls import reverse
+
+from arch.models import PackageArchitecture
+from packages.models import Package, PackageName, PackageUpdate
+
+
+@override_settings(
+ CELERY_TASK_ALWAYS_EAGER=True,
+ CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}}
+)
+class PackageUpdateViewTests(TestCase):
+
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser', password='testpass'
+ )
+ self.client.login(username='testuser', password='testpass')
+ self.arch = PackageArchitecture.objects.create(name='x86_64')
+ self.name = PackageName.objects.create(name='openssl')
+ self.old = Package.objects.create(
+ name=self.name, arch=self.arch, epoch='',
+ version='1.1.1', release='1', packagetype='R',
+ )
+ self.new = Package.objects.create(
+ name=self.name, arch=self.arch, epoch='',
+ version='1.1.2', release='1', packagetype='R',
+ )
+ self.sec_update = PackageUpdate.objects.create(
+ oldpackage=self.old, newpackage=self.new, security=True,
+ )
+ self.bug_update = PackageUpdate.objects.create(
+ oldpackage=self.old, newpackage=self.new, security=False,
+ )
+
+ def test_update_list(self):
+ resp = self.client.get(reverse('packages:package_update_list'))
+ self.assertEqual(resp.status_code, 200)
+ self.assertContains(resp, 'openssl')
+
+ def test_update_list_filter_security(self):
+ resp = self.client.get(
+ reverse('packages:package_update_list'), {'security': 'true'}
+ )
+ self.assertEqual(resp.status_code, 200)
+ self.assertContains(resp, 'Security')
+
+ def test_update_list_filter_bugfix(self):
+ resp = self.client.get(
+ reverse('packages:package_update_list'), {'security': 'false'}
+ )
+ self.assertEqual(resp.status_code, 200)
+ self.assertContains(resp, 'Bugfix')
+
+ def test_update_list_search(self):
+ resp = self.client.get(
+ reverse('packages:package_update_list'), {'search': 'openssl'}
+ )
+ self.assertEqual(resp.status_code, 200)
+ self.assertContains(resp, 'openssl')
+
+ def test_update_list_search_no_results(self):
+ resp = self.client.get(
+ reverse('packages:package_update_list'), {'search': 'nonexistent'}
+ )
+ self.assertEqual(resp.status_code, 200)
+
+ def test_update_list_requires_login(self):
+ self.client.logout()
+ resp = self.client.get(reverse('packages:package_update_list'))
+ self.assertEqual(resp.status_code, 302)
diff --git a/packages/urls.py b/packages/urls.py
index bc027807..75273450 100644
--- a/packages/urls.py
+++ b/packages/urls.py
@@ -27,4 +27,5 @@
path('name//', views.package_name_detail, name='package_name_detail'),
path('id/', views.package_list, name='package_list'),
path('id//', views.package_detail, name='package_detail'),
+ path('updates/', views.package_update_list, name='package_update_list'),
]
diff --git a/packages/views.py b/packages/views.py
index 9f75b415..6afa4f4d 100644
--- a/packages/views.py
+++ b/packages/views.py
@@ -16,7 +16,7 @@
# along with Patchman. If not, see
from django.contrib.auth.decorators import login_required
-from django.db.models import Q
+from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, render
from django_tables2 import RequestConfig
from rest_framework import viewsets
@@ -26,7 +26,7 @@
from packages.serializers import (
PackageNameSerializer, PackageSerializer, PackageUpdateSerializer,
)
-from packages.tables import PackageNameTable, PackageTable
+from packages.tables import PackageNameTable, PackageTable, PackageUpdateTable
from util.filterspecs import Filter, FilterBar
@@ -172,6 +172,48 @@ def package_name_detail(request, packagename):
'allversions': allversions})
+@login_required
+def package_update_list(request):
+ updates = PackageUpdate.objects.select_related(
+ 'oldpackage__name', 'oldpackage__arch',
+ 'newpackage__name', 'newpackage__arch',
+ ).annotate(
+ host_count=Count('host', distinct=True),
+ affected_count=Count('oldpackage__affected_by_erratum', distinct=True),
+ fixed_count=Count('newpackage__provides_fix_in_erratum', distinct=True),
+ )
+
+ if 'security' in request.GET:
+ security = request.GET['security'] == 'true'
+ updates = updates.filter(security=security)
+ if 'host_id' in request.GET:
+ updates = updates.filter(host=request.GET['host_id'])
+ if 'search' in request.GET:
+ terms = request.GET['search'].lower()
+ query = Q()
+ for term in terms.split(' '):
+ q = (Q(oldpackage__name__name__icontains=term) |
+ Q(newpackage__name__name__icontains=term))
+ query = query & q
+ updates = updates.filter(query)
+ else:
+ terms = ''
+
+ filter_list = []
+ filter_list.append(Filter(request, 'Type', 'security',
+ {'true': 'Security', 'false': 'Bugfix'}))
+ filter_bar = FilterBar(request, filter_list)
+
+ table = PackageUpdateTable(updates.distinct())
+ RequestConfig(request, paginate={'per_page': 50}).configure(table)
+
+ return render(request,
+ 'packages/package_update_list.html',
+ {'table': table,
+ 'filter_bar': filter_bar,
+ 'terms': terms})
+
+
class PackageNameViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows package names to be viewed or edited.
diff --git a/util/templates/navbar.html b/util/templates/navbar.html
index 263be6a4..d07c40d5 100644
--- a/util/templates/navbar.html
+++ b/util/templates/navbar.html
@@ -12,7 +12,13 @@
Mirrors
- Packages
+
Errata