diff --git a/errata/apps.py b/errata/apps.py index 95360240..9512378e 100644 --- a/errata/apps.py +++ b/errata/apps.py @@ -26,6 +26,8 @@ def ready(self): from django.db.models.signals import post_save from django.utils import timezone + import errata.signals # noqa: F401 + def set_initial_last_run(sender, instance, created, **kwargs): if created and instance.name == 'update_errata_cves_cwes_every_12_hours': instance.last_run_at = timezone.now() - timedelta(days=1) diff --git a/errata/migrations/0008_add_cached_count_fields.py b/errata/migrations/0008_add_cached_count_fields.py new file mode 100644 index 00000000..ff9d3bc9 --- /dev/null +++ b/errata/migrations/0008_add_cached_count_fields.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.28 on 2026-02-13 06:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('errata', '0007_alter_erratum_fixed_packages'), + ] + + operations = [ + migrations.AddField( + model_name='erratum', + name='affected_packages_count', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='erratum', + name='cves_count', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='erratum', + name='fixed_packages_count', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='erratum', + name='osreleases_count', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='erratum', + name='references_count', + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/errata/migrations/0009_backfill_cached_counts.py b/errata/migrations/0009_backfill_cached_counts.py new file mode 100644 index 00000000..4d084fa8 --- /dev/null +++ b/errata/migrations/0009_backfill_cached_counts.py @@ -0,0 +1,45 @@ +# Copyright 2026 Marcus Furlong +# +# This file is part of Patchman. +# +# Patchman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 only. +# +# Patchman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchman. If not, see + +from django.db import migrations + + +def backfill_counts(apps, schema_editor): + Erratum = apps.get_model('errata', 'Erratum') + for erratum in Erratum.objects.all().iterator(): + erratum.affected_packages_count = erratum.affected_packages.count() + erratum.fixed_packages_count = erratum.fixed_packages.count() + erratum.osreleases_count = erratum.osreleases.count() + erratum.cves_count = erratum.cves.count() + erratum.references_count = erratum.references.count() + erratum.save(update_fields=[ + 'affected_packages_count', + 'fixed_packages_count', + 'osreleases_count', + 'cves_count', + 'references_count', + ]) + + +class Migration(migrations.Migration): + + dependencies = [ + ('errata', '0008_add_cached_count_fields'), + ] + + operations = [ + migrations.RunPython(backfill_counts, migrations.RunPython.noop), + ] diff --git a/errata/models.py b/errata/models.py index 8c21bcfa..49a0ef5b 100644 --- a/errata/models.py +++ b/errata/models.py @@ -40,6 +40,11 @@ class Erratum(models.Model): osreleases = models.ManyToManyField(OSRelease, blank=True) cves = models.ManyToManyField(CVE, blank=True) references = models.ManyToManyField(Reference, blank=True) + affected_packages_count = models.PositiveIntegerField(default=0) + fixed_packages_count = models.PositiveIntegerField(default=0) + osreleases_count = models.PositiveIntegerField(default=0) + cves_count = models.PositiveIntegerField(default=0) + references_count = models.PositiveIntegerField(default=0) objects = ErratumManager() @@ -49,9 +54,9 @@ class Meta: ordering = ['-issue_date', 'name'] def __str__(self): - text = f'{self.name} ({self.e_type}), {self.cves.count()} related CVEs, ' - text += f'affecting {self.osreleases.count()} OS Releases, ' - text += f'providing {self.fixed_packages.count()} fixed Packages' + text = f'{self.name} ({self.e_type}), {self.cves_count} related CVEs, ' + text += f'affecting {self.osreleases_count} OS Releases, ' + text += f'providing {self.fixed_packages_count} fixed Packages' return text def get_absolute_url(self): diff --git a/errata/signals.py b/errata/signals.py new file mode 100644 index 00000000..50e101d5 --- /dev/null +++ b/errata/signals.py @@ -0,0 +1,60 @@ +# Copyright 2026 Marcus Furlong +# +# This file is part of Patchman. +# +# Patchman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 only. +# +# Patchman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchman. If not, see + +from django.db.models.signals import m2m_changed +from django.dispatch import receiver + +from errata.models import Erratum + + +@receiver(m2m_changed, sender=Erratum.affected_packages.through) +def update_affected_packages_count(sender, instance, action, **kwargs): + """Update affected_packages_count when Erratum.affected_packages M2M changes.""" + if action in ('post_add', 'post_remove', 'post_clear'): + instance.affected_packages_count = instance.affected_packages.count() + instance.save(update_fields=['affected_packages_count']) + + +@receiver(m2m_changed, sender=Erratum.fixed_packages.through) +def update_fixed_packages_count(sender, instance, action, **kwargs): + """Update fixed_packages_count when Erratum.fixed_packages M2M changes.""" + if action in ('post_add', 'post_remove', 'post_clear'): + instance.fixed_packages_count = instance.fixed_packages.count() + instance.save(update_fields=['fixed_packages_count']) + + +@receiver(m2m_changed, sender=Erratum.osreleases.through) +def update_osreleases_count(sender, instance, action, **kwargs): + """Update osreleases_count when Erratum.osreleases M2M changes.""" + if action in ('post_add', 'post_remove', 'post_clear'): + instance.osreleases_count = instance.osreleases.count() + instance.save(update_fields=['osreleases_count']) + + +@receiver(m2m_changed, sender=Erratum.cves.through) +def update_cves_count(sender, instance, action, **kwargs): + """Update cves_count when Erratum.cves M2M changes.""" + if action in ('post_add', 'post_remove', 'post_clear'): + instance.cves_count = instance.cves.count() + instance.save(update_fields=['cves_count']) + + +@receiver(m2m_changed, sender=Erratum.references.through) +def update_references_count(sender, instance, action, **kwargs): + """Update references_count when Erratum.references M2M changes.""" + if action in ('post_add', 'post_remove', 'post_clear'): + instance.references_count = instance.references.count() + instance.save(update_fields=['references_count']) diff --git a/errata/tables.py b/errata/tables.py index d70be2cd..57be40b4 100644 --- a/errata/tables.py +++ b/errata/tables.py @@ -19,31 +19,31 @@ ERRATUM_NAME_TEMPLATE = '{{ record.name }}' PACKAGES_AFFECTED_TEMPLATE = ( - '{% with count=record.affected_packages.count %}' + '{% with count=record.affected_packages_count %}' '{% if count != 0 %}' '{{ count }}' '{% else %}{% endif %}{% endwith %}' ) PACKAGES_FIXED_TEMPLATE = ( - '{% with count=record.fixed_packages.count %}' + '{% with count=record.fixed_packages_count %}' '{% if count != 0 %}' '{{ count }}' '{% else %}{% endif %}{% endwith %}' ) OSRELEASES_TEMPLATE = ( - '{% with count=record.osreleases.count %}' + '{% with count=record.osreleases_count %}' '{% if count != 0 %}' '{{ count }}' '{% else %}{% endif %}{% endwith %}' ) ERRATUM_CVES_TEMPLATE = ( - '{% with count=record.cves.count %}' + '{% with count=record.cves_count %}' '{% if count != 0 %}' '{{ count }}' '{% else %}{% endif %}{% endwith %}' ) REFERENCES_TEMPLATE = ( - '{% with count=record.references.count %}' + '{% with count=record.references_count %}' '{% if count != 0 %}' '{{ count }}' '{% else %}{% endif %}{% endwith %}' diff --git a/errata/templates/errata/erratum_detail.html b/errata/templates/errata/erratum_detail.html index 3e622401..ff446265 100644 --- a/errata/templates/errata/erratum_detail.html +++ b/errata/templates/errata/erratum_detail.html @@ -8,7 +8,7 @@ {% block content %} -{% with affected_count=erratum.affected_packages.count fixed_count=erratum.fixed_packages.count %} +{% with affected_count=erratum.affected_packages_count fixed_count=erratum.fixed_packages_count %}