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,