diff --git a/hosts/admin.py b/hosts/admin.py
index 43bf31da..46e6bc85 100644
--- a/hosts/admin.py
+++ b/hosts/admin.py
@@ -24,5 +24,12 @@ class HostAdmin(admin.ModelAdmin):
readonly_fields = ('packages', 'updates')
+class HostRepoAdmin(admin.ModelAdmin):
+ def get_queryset(self, request):
+ qs = super().get_queryset(request) \
+ .select_related('host', 'repo')
+ return qs
+
+
admin.site.register(Host, HostAdmin)
-admin.site.register(HostRepo)
+admin.site.register(HostRepo, HostRepoAdmin)
diff --git a/hosts/managers.py b/hosts/managers.py
index 73301b58..433b23d9 100644
--- a/hosts/managers.py
+++ b/hosts/managers.py
@@ -20,4 +20,4 @@
class HostManager(models.Manager):
def get_queryset(self):
- return super().get_queryset().select_related('osvariant', 'arch', 'domain')
+ return super().get_queryset().select_related('osvariant', 'arch', 'domain', 'osvariant__arch')
diff --git a/hosts/views.py b/hosts/views.py
index b4e002dd..cc49a763 100644
--- a/hosts/views.py
+++ b/hosts/views.py
@@ -44,7 +44,7 @@ def _get_filtered_hosts(filter_params):
"""Helper to reconstruct filtered queryset from filter params."""
params = parse_qs(filter_params)
- hosts = Host.objects.select_related('osvariant', 'arch', 'domain')
+ hosts = Host.objects.all()
if 'domain_id' in params:
hosts = hosts.filter(domain=params['domain_id'][0])
@@ -79,7 +79,7 @@ def _get_filtered_hosts(filter_params):
@login_required
def host_list(request):
# Use cached count fields instead of expensive annotations
- hosts = Host.objects.select_related('osvariant', 'arch', 'domain')
+ hosts = Host.objects.all()
if 'domain_id' in request.GET:
hosts = hosts.filter(domain=request.GET['domain_id'])
@@ -130,7 +130,8 @@ def host_list(request):
filter_list.append(Filter(request, 'Domain', 'domain_id', Domain.objects.all()))
filter_list.append(Filter(request, 'OS Release', 'osrelease_id',
OSRelease.objects.filter(osvariant__host__in=hosts)))
- filter_list.append(Filter(request, 'OS Variant', 'osvariant_id', OSVariant.objects.filter(host__in=hosts)))
+ filter_list.append(Filter(request, 'OS Variant', 'osvariant_id',
+ OSVariant.objects.filter(host__in=hosts).select_related('arch')))
filter_list.append(Filter(request, 'Architecture', 'arch_id', MachineArchitecture.objects.filter(host__in=hosts)))
filter_list.append(Filter(request, 'Reboot Required', 'reboot_required', {'true': 'Yes', 'false': 'No'}))
filter_bar = FilterBar(request, filter_list)
@@ -158,12 +159,15 @@ def host_list(request):
def host_detail(request, hostname):
host = get_object_or_404(Host, hostname=hostname)
reports = Report.objects.filter(host=hostname).order_by('-created')[:3]
- hostrepos = HostRepo.objects.filter(host=host)
+ hostrepos = HostRepo.objects.filter(host=host).select_related('repo')
# Build packages list with update info
- updates_by_package = {u.oldpackage_id: u for u in host.updates.select_related('oldpackage', 'newpackage')}
+ updates_by_package = {u.oldpackage_id: u for u in host.updates.select_related('oldpackage',
+ 'newpackage',
+ 'newpackage__name',
+ 'newpackage__arch')}
packages_with_updates = []
- for package in host.packages.select_related('name', 'arch').order_by('name__name'):
+ for package in host.packages.order_by('name__name'):
package.update = updates_by_package.get(package.id)
packages_with_updates.append(package)
@@ -297,7 +301,7 @@ class HostViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows hosts to be viewed or edited.
"""
- queryset = Host.objects.select_related('osvariant', 'arch', 'domain').all()
+ queryset = Host.objects.all()
serializer_class = HostSerializer
filterset_class = HostFilter
diff --git a/operatingsystems/tables.py b/operatingsystems/tables.py
index 14437094..1738a226 100644
--- a/operatingsystems/tables.py
+++ b/operatingsystems/tables.py
@@ -24,11 +24,11 @@
OSRELEASE_NAME_TEMPLATE = '{{ record.name }}'
OSRELEASE_REPOS_TEMPLATE = (
''
- '{{ record.repos.count }}'
+ '{{ record.repos_count }}'
)
OSVARIANTS_TEMPLATE = (
''
- '{{ record.osvariant_set.count }}'
+ '{{ record.osvariant_count }}'
)
OSRELEASE_HOSTS_TEMPLATE = (
'{% load common %}'
@@ -36,7 +36,7 @@
)
OSRELEASE_ERRATA_TEMPLATE = (
''
- '{{ record.erratum_set.count }}'
+ '{{ record.erratum_count }}'
)
# OSVariantTable templates
@@ -47,7 +47,7 @@
)
OSVARIANT_HOSTS_TEMPLATE = (
''
- '{{ record.host_set.count }}'
+ '{{ record.hosts_count }}'
)
OSVARIANT_OSRELEASE_TEMPLATE = (
'{% if record.osrelease %}'
@@ -55,7 +55,7 @@
'{% endif %}'
)
REPOS_OSRELEASE_TEMPLATE = (
- '{% if record.osrelease.repos.count != None %}{{ record.osrelease.repos.count }}{% else %}0{% endif %}'
+ '{% if record.osrelease.repos.count != None %}{{ record.repos_count }}{% else %}0{% endif %}'
)
diff --git a/operatingsystems/templates/operatingsystems/osrelease_detail.html b/operatingsystems/templates/operatingsystems/osrelease_detail.html
index be94f43d..7cc81ea8 100644
--- a/operatingsystems/templates/operatingsystems/osrelease_detail.html
+++ b/operatingsystems/templates/operatingsystems/osrelease_detail.html
@@ -44,7 +44,7 @@
{% if osvariant_count == 0 %}
{{ osrelease }} has no Variants
{% else %}
- {% gen_table osrelease.osvariant_set.select_related %}
+ {% gen_table osrelease.osvariant_set.all %}
{% endif %}
@@ -54,7 +54,7 @@
{% if repos_count == 0 %}
{{ osrelease }} has no Repositories
{% else %}
- {% gen_table osrelease.repos.select_related %}
+ {% gen_table osrelease.repos.all %}
{% endif %}
{% if user.is_authenticated and perms.is_admin %}
diff --git a/operatingsystems/views.py b/operatingsystems/views.py
index 40824d0d..07015766 100644
--- a/operatingsystems/views.py
+++ b/operatingsystems/views.py
@@ -19,7 +19,7 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
-from django.db.models import Count, Q
+from django.db.models import Count, Prefetch, Q
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django_tables2 import RequestConfig
@@ -34,6 +34,7 @@
OSReleaseSerializer, OSVariantSerializer,
)
from operatingsystems.tables import OSReleaseTable, OSVariantTable
+from repos.models import Repository
from util import sanitize_filter_params
@@ -182,7 +183,11 @@ def delete_nohost_osvariants(request):
@login_required
def osrelease_list(request):
- osreleases = OSRelease.objects.all()
+ osreleases = OSRelease.objects.all().order_by('name').annotate(
+ repos_count=Count('repos', distinct=True),
+ osvariant_count=Count('osvariant', distinct=True),
+ erratum_count=Count('erratum', distinct=True),
+ )
if 'erratum_id' in request.GET:
osreleases = osreleases.filter(erratum=request.GET['erratum_id'])
@@ -216,7 +221,10 @@ def osrelease_list(request):
@login_required
def osrelease_detail(request, osrelease_id):
- osrelease = get_object_or_404(OSRelease, id=osrelease_id)
+ repos = Prefetch('repos', Repository.objects.prefetch_related('mirror_set'))
+ osvariant_set = Prefetch('osvariant_set', OSVariant.objects.prefetch_related('host_set'))
+ osrelease = get_object_or_404(OSRelease.objects.prefetch_related(repos, osvariant_set),
+ id=osrelease_id)
if request.method == 'POST':
repos_form = AddReposToOSReleaseForm(request.POST, instance=osrelease)
diff --git a/packages/admin.py b/packages/admin.py
index bc4b1aaa..c348bbe7 100644
--- a/packages/admin.py
+++ b/packages/admin.py
@@ -27,6 +27,12 @@ class PackageAdmin(admin.ModelAdmin):
class PackageUpdateAdmin(admin.ModelAdmin):
readonly_fields = ('oldpackage', 'newpackage')
+ def get_queryset(self, request):
+ qs = super().get_queryset(request) \
+ .select_related('oldpackage__name', 'oldpackage__arch',
+ 'newpackage__name', 'newpackage__arch')
+ return qs
+
admin.site.register(Package, PackageAdmin)
admin.site.register(PackageName)
diff --git a/packages/tables.py b/packages/tables.py
index 6b41ac03..b53819f9 100644
--- a/packages/tables.py
+++ b/packages/tables.py
@@ -115,7 +115,7 @@ class PackageNameTable(BaseTable):
attrs={'th': {'class': 'col-sm-5'}, 'td': {'class': 'col-sm-5'}},
)
versions = tables.TemplateColumn(
- '{{ record.package_set.count }}',
+ '{{ record.package_count }}',
orderable=False,
verbose_name='Versions',
attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
diff --git a/packages/views.py b/packages/views.py
index 0f2aaecb..3fea2c4f 100644
--- a/packages/views.py
+++ b/packages/views.py
@@ -32,13 +32,13 @@
@login_required
def package_list(request):
- packages = Package.objects.select_related('name', 'arch')
+ packages = Package.objects.all()
if 'arch_id' in request.GET:
- packages = packages.filter(arch=request.GET['arch_id']).distinct()
+ packages = packages.filter(arch=request.GET['arch_id'])
if 'packagetype' in request.GET:
- packages = packages.filter(packagetype=request.GET['packagetype']).distinct()
+ packages = packages.filter(packagetype=request.GET['packagetype'])
if 'erratum_id' in request.GET:
if request.GET['type'] == 'affected':
@@ -109,12 +109,14 @@ def package_list(request):
filter_list.append(Filter(request, 'Architecture', 'arch_id', PackageArchitecture.objects.all()))
filter_bar = FilterBar(request, filter_list)
+ count = packages.count() # faster without joining tables from annotate below
packages = packages.annotate(
host_count=Count('host', distinct=True),
repo_count=Count('mirror__repo', distinct=True),
affected_count=Count('affected_by_erratum', distinct=True),
fixed_count=Count('provides_fix_in_erratum', distinct=True),
)
+ packages.count = lambda: count
table = PackageTable(packages)
RequestConfig(request, paginate={'per_page': 50}).configure(table)
@@ -151,7 +153,12 @@ def package_name_list(request):
filter_list.append(Filter(request, 'Architecture', 'arch_id', PackageArchitecture.objects.all()))
filter_bar = FilterBar(request, filter_list)
- packages = packages.annotate(host_count=Count('package__host', distinct=True))
+ count = packages.count() # faster without joining tables from annotate below
+ packages = packages.annotate(
+ host_count=Count('package__host', distinct=True),
+ package_count=Count('package', distinct=True),
+ )
+ packages.count = lambda: count
table = PackageNameTable(packages)
RequestConfig(request, paginate={'per_page': 50}).configure(table)
@@ -245,7 +252,7 @@ class PackageViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows packages to be viewed or edited.
"""
- queryset = Package.objects.select_related('name', 'arch').all()
+ queryset = Package.objects.all()
serializer_class = PackageSerializer
filterset_fields = [
'name',
diff --git a/reports/migrations/0006_alter_report_options_alter_report_created.py b/reports/migrations/0006_alter_report_options_alter_report_created.py
new file mode 100644
index 00000000..aff6bc91
--- /dev/null
+++ b/reports/migrations/0006_alter_report_options_alter_report_created.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.29 on 2026-04-20 15:06
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('reports', '0005_alter_report_options'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='report',
+ options={'ordering': ['-created'], 'verbose_name': 'Report', 'verbose_name_plural': 'Reports'},
+ ),
+ migrations.AlterField(
+ model_name='report',
+ name='created',
+ field=models.DateTimeField(auto_now_add=True, db_index=True),
+ ),
+ ]
diff --git a/reports/models.py b/reports/models.py
index 578e2c46..7f9884ed 100644
--- a/reports/models.py
+++ b/reports/models.py
@@ -27,7 +27,7 @@
class Report(models.Model):
- created = models.DateTimeField(auto_now_add=True)
+ created = models.DateTimeField(auto_now_add=True, db_index=True)
host = models.CharField(max_length=255, null=True)
domain = models.CharField(max_length=255, null=True)
tags = models.CharField(max_length=255, null=True, default='')
diff --git a/repos/tables.py b/repos/tables.py
index a0e3aca0..74e7b3e3 100644
--- a/repos/tables.py
+++ b/repos/tables.py
@@ -23,7 +23,7 @@
REPO_NAME_TEMPLATE = '
{{ record }}'
MIRRORS_TEMPLATE = (
'
'
- '{{ record.mirror_set.count }}'
+ '{{ record.mirror_count }}'
)
REPO_ENABLED_TEMPLATE = '{% load common %}{% yes_no_img record.enabled %}'
SECURITY_TEMPLATE = '{% load common %}{% yes_no_img record.security %}'
diff --git a/repos/views.py b/repos/views.py
index 7a804a19..b0c9da77 100644
--- a/repos/views.py
+++ b/repos/views.py
@@ -20,7 +20,7 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db import IntegrityError
-from django.db.models import Q
+from django.db.models import Count, Q
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@@ -46,7 +46,7 @@
@login_required
def repo_list(request):
- repos = Repository.objects.select_related('arch').order_by('name')
+ repos = Repository.objects.order_by('name').annotate(mirror_count=Count('mirror', distinct=True))
if 'repotype' in request.GET:
repos = repos.filter(repotype=request.GET['repotype'])
@@ -417,7 +417,7 @@ def _get_filtered_repos(filter_params):
"""Helper to reconstruct filtered queryset from filter params."""
params = parse_qs(filter_params)
- repos = Repository.objects.select_related('arch').order_by('name')
+ repos = Repository.objects.order_by('name')
if 'repotype' in params:
repos = repos.filter(repotype=params['repotype'][0])
@@ -588,7 +588,7 @@ class RepositoryViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows repositories to be viewed or edited.
"""
- queryset = Repository.objects.select_related('arch').all()
+ queryset = Repository.objects.all()
serializer_class = RepositorySerializer
diff --git a/security/tables.py b/security/tables.py
index ba209829..142bf09b 100644
--- a/security/tables.py
+++ b/security/tables.py
@@ -40,7 +40,7 @@
CWE_DESCRIPTION_TEMPLATE = '
{{ record.description }}'
CWE_CVES_TEMPLATE = (
'
'
- '{{ record.cve_set.count }}'
+ '{{ record.cve_count }}'
)
# ReferenceTable templates
diff --git a/security/views.py b/security/views.py
index 7ae5d851..c7e068e0 100644
--- a/security/views.py
+++ b/security/views.py
@@ -15,7 +15,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
@@ -32,7 +32,7 @@
@login_required
def cwe_list(request):
- cwes = CWE.objects.all()
+ cwes = CWE.objects.all().annotate(cve_count=Count('cve', distinct=True)).order_by('cwe_id')
if 'search' in request.GET:
terms = request.GET['search'].lower()
@@ -65,7 +65,9 @@ def cwe_detail(request, cwe_id):
@login_required
def cve_list(request):
- cves = CVE.objects.all()
+ cves = CVE.objects.all() \
+ .prefetch_related('cvss_scores', 'cwes', 'erratum_set') \
+ .order_by('-cve_id')
if 'erratum_id' in request.GET:
cves = cves.filter(erratum=request.GET['erratum_id'])
@@ -117,10 +119,10 @@ def cve_detail(request, cve_id):
@login_required
def reference_list(request):
- refs = Reference.objects.all().order_by('ref_type')
+ refs = Reference.objects.all().prefetch_related('erratum_set').order_by('ref_type')
if 'ref_type' in request.GET:
- refs = refs.filter(ref_type=request.GET['ref_type']).distinct()
+ refs = refs.filter(ref_type=request.GET['ref_type'])
if 'erratum_id' in request.GET:
refs = refs.filter(erratum__id=request.GET['erratum_id'])
@@ -137,7 +139,7 @@ def reference_list(request):
filter_list = []
filter_list.append(Filter(request, 'Reference Type', 'ref_type',
- Reference.objects.values_list('ref_type', flat=True).distinct()))
+ Reference.objects.values_list('ref_type', flat=True)))
filter_bar = FilterBar(request, filter_list)
table = ReferenceTable(refs)
diff --git a/util/context_processors.py b/util/context_processors.py
index 97bbcf2c..56bfcca9 100644
--- a/util/context_processors.py
+++ b/util/context_processors.py
@@ -20,6 +20,7 @@
from pathlib import Path
from django.db.models import F
+from django.urls import reverse
from django.utils import timezone
from hosts.models import Host
@@ -87,6 +88,10 @@ def issues_count(request):
"""Context processor to provide issues count for navbar."""
if not request.user.is_authenticated:
return {'issues_count': 0}
+ if request.path.startswith(reverse("admin:index")):
+ return {'issues_count': 0}
+ if hasattr(request, 'issues_count'):
+ return {'issues_count': request.issues_count}
hosts = Host.objects.all()
osvariants = OSVariant.objects.all()
@@ -159,5 +164,6 @@ def issues_count(request):
(1 if nohost_repos.exists() else 0) +
(1 if unprocessed_reports.exists() else 0)
)
+ request.issues_count = count # cache result while django-tables2 renders templates
return {'issues_count': count}
diff --git a/util/templates/dashboard.html b/util/templates/dashboard.html
index 7afc18fb..de28a4fc 100644
--- a/util/templates/dashboard.html
+++ b/util/templates/dashboard.html
@@ -219,7 +219,7 @@
{% endwith %}
{% with count=orphaned_packages.count %}
- {% if count < 0 %}
+ {% if count > 0 %}
@@ -234,7 +234,7 @@
{% endwith %}
{% with count=unprocessed_reports.count %}
- {% if count < 0 %}
+ {% if count > 0 %}
@@ -244,7 +244,7 @@
{% endif %}
{% endwith %}
-{% if not has_issues %}
+{% if issues_count == 0 %}
No issues found!
{% endif %}
diff --git a/util/views.py b/util/views.py
index 6222546b..fc3ddc88 100644
--- a/util/views.py
+++ b/util/views.py
@@ -19,13 +19,13 @@
from django.contrib.auth.decorators import login_required
from django.contrib.sites.models import Site
-from django.db.models import F
+from django.db.models import Exists, F, OuterRef
from django.shortcuts import render
from django.utils import timezone
from hosts.models import Host
from operatingsystems.models import OSRelease, OSVariant
-from packages.models import Package
+from packages.models import Package, PackageUpdate
from reports.models import Report
from repos.models import Mirror, Repository
from util import get_setting_of_type
@@ -65,7 +65,7 @@ def dashboard(request):
nohost_osvariants = osvariants.filter(host__isnull=True)
# os release issues
- norepo_osreleases = None
+ norepo_osreleases = OSRelease.objects.none()
if hosts.filter(host_repos_only=False).exists():
norepo_osreleases = osreleases.filter(repos__isnull=True)
@@ -81,8 +81,14 @@ def dashboard(request):
nohost_repos = repos.filter(host__isnull=True)
# package issues
- norepo_packages = packages.filter(mirror__isnull=True, oldpackage__isnull=True, host__isnull=False).distinct() # noqa
- orphaned_packages = packages.filter(mirror__isnull=True, host__isnull=True).distinct() # noqa
+ nomirror_packages = Mirror.packages.through.objects.filter(package=OuterRef('pk'))
+ nohost_packages = Host.packages.through.objects.filter(package=OuterRef('pk'))
+ nooldpackage_packages = PackageUpdate.objects.filter(oldpackage=OuterRef('pk'))
+ norepo_packages = packages.filter(Exists(nohost_packages),
+ ~Exists(nomirror_packages),
+ ~Exists(nooldpackage_packages))
+ orphaned_packages = packages.filter(~Exists(nohost_packages),
+ ~Exists(nomirror_packages))
# report issues
unprocessed_reports = Report.objects.filter(processed=False)
@@ -91,15 +97,15 @@ def dashboard(request):
possible_mirrors = {}
# Use cached packages_count to avoid N+1 queries
- for csvalue in Mirror.objects.filter(packages_count__gt=0).values('packages_checksum').distinct():
- checksum = csvalue['packages_checksum']
- if checksum is not None and checksum != 'yast':
- mirrors = list(Mirror.objects.filter(
- packages_checksum=checksum,
- packages_count__gt=0
- ).select_related('repo'))
- if mirrors:
- checksums[checksum] = mirrors
+ mirrors = Mirror.objects.filter(packages_count__gt=0, packages_checksum__isnull=False) \
+ .exclude(packages_checksum='yast') \
+ .select_related('repo')
+
+ for mirror in mirrors:
+ checksum = mirror.packages_checksum
+ if checksum not in checksums:
+ checksums[checksum] = []
+ checksums[checksum].append(mirror)
for checksum in checksums:
first_mirror = checksums[checksum][0]
@@ -110,32 +116,10 @@ def dashboard(request):
possible_mirrors[checksum] = checksums[checksum]
continue
- has_issues = (
- noosrelease_osvariants.exists() or
- nohost_osvariants.exists() or
- (norepo_osreleases is not None and norepo_osreleases.exists()) or
- stale_hosts.exists() or
- reboot_hosts.exists() or
- secupdate_hosts.exists() or
- bugupdate_hosts.exists() or
- norepo_hosts.exists() or
- diff_rdns_hosts.exists() or
- failed_mirrors.exists() or
- disabled_mirrors.exists() or
- norefresh_mirrors.exists() or
- failed_repos.exists() or
- unused_repos.exists() or
- nomirror_repos.exists() or
- nohost_repos.exists() or
- bool(possible_mirrors) or
- norepo_packages.exists()
- )
-
return render(
request,
'dashboard.html',
{'site': site,
- 'has_issues': has_issues,
'noosrelease_osvariants': noosrelease_osvariants,
'norepo_hosts': norepo_hosts,
'nohost_osvariants': nohost_osvariants,