From 20361ce75a1e8d1e6a95bf97bd8a075e1bfe02cb Mon Sep 17 00:00:00 2001 From: Klaas van Schelven Date: Tue, 20 Feb 2024 17:49:48 +0100 Subject: [PATCH] Date/issue_count correct in list_view --- bugsink/tests.py | 2 ++ ingest/views.py | 19 +++++++++--- issues/factories.py | 10 +++++++ ..._count_issue_first_seen_issue_last_seen.py | 30 +++++++++++++++++++ ...issues_issu_first_s_9fb0f9_idx_and_more.py | 19 ++++++++++++ issues/models.py | 12 ++++++++ issues/templates/issues/issue_list.html | 12 ++++---- issues/tests.py | 14 +++++---- issues/views.py | 2 +- 9 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 issues/migrations/0008_issue_event_count_issue_first_seen_issue_last_seen.py create mode 100644 issues/migrations/0009_issue_issues_issu_first_s_9fb0f9_idx_and_more.py diff --git a/bugsink/tests.py b/bugsink/tests.py index 5f725c0..de21b07 100644 --- a/bugsink/tests.py +++ b/bugsink/tests.py @@ -5,6 +5,7 @@ from django.test import TestCase as DjangoTestCase from projects.models import Project from issues.models import Issue +from issues.factories import denormalized_issue_fields from events.models import Event from events.factories import create_event @@ -180,6 +181,7 @@ class PCRegistryTestCase(DjangoTestCase): project=project, is_muted=True, unmute_on_volume_based_conditions='[{"period": "day", "nr_of_periods": 1, "volume": 100}]', + **denormalized_issue_fields(), ) create_event(project, issue) diff --git a/ingest/views.py b/ingest/views.py index dd01788..d91675e 100644 --- a/ingest/views.py +++ b/ingest/views.py @@ -106,6 +106,11 @@ class BaseIngestAPIView(APIView): issue, issue_created = Issue.objects.get_or_create( project=ingested_event.project, hash=hash_, + defaults={ + "first_seen": ingested_event.timestamp, + "last_seen": ingested_event.timestamp, + "event_count": 1, + }, ) event, event_created = Event.from_ingested(ingested_event, issue, event_data) @@ -121,11 +126,17 @@ class BaseIngestAPIView(APIView): if ingested_event.project.alert_on_new_issue: send_new_issue_alert.delay(issue.id) - elif issue_is_regression(issue, event.release): # new issues cannot be regressions by definition, hence 'else' - if ingested_event.project.alert_on_regression: - send_regression_alert.delay(issue.id) + else: + # new issues cannot be regressions by definition, hence this is in the 'else' branch + if issue_is_regression(issue, event.release): + if ingested_event.project.alert_on_regression: + send_regression_alert.delay(issue.id) - IssueStateManager.reopen(issue) + IssueStateManager.reopen(issue) + + # update the denormalized fields + issue.last_seen = ingested_event.timestamp + issue.event_count += 1 if issue.id not in get_pc_registry().by_issue: pc_registry.by_issue[issue.id] = PeriodCounter() diff --git a/issues/factories.py b/issues/factories.py index b6bc014..7fa93a2 100644 --- a/issues/factories.py +++ b/issues/factories.py @@ -19,6 +19,7 @@ def get_or_create_issue(project=None, event_data=None): issue, issue_created = Issue.objects.get_or_create( project=project, hash=hash_, + defaults=denormalized_issue_fields(), ) return issue, issue_created @@ -31,3 +32,12 @@ def create_event_data(): "timestamp": timezone.now().isoformat(), "platform": "python", } + + +def denormalized_issue_fields(): + """return a dict of fields that are expected to be denormalized on Issue; for testing purposes""" + return { + "first_seen": timezone.now(), + "last_seen": timezone.now(), + "event_count": 1, + } diff --git a/issues/migrations/0008_issue_event_count_issue_first_seen_issue_last_seen.py b/issues/migrations/0008_issue_event_count_issue_first_seen_issue_last_seen.py new file mode 100644 index 0000000..614c26e --- /dev/null +++ b/issues/migrations/0008_issue_event_count_issue_first_seen_issue_last_seen.py @@ -0,0 +1,30 @@ +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('issues', '0007_remove_issue_events'), + ] + + operations = [ + migrations.AddField( + model_name='issue', + name='event_count', + field=models.IntegerField(default=1), + preserve_default=False, + ), + migrations.AddField( + model_name='issue', + name='first_seen', + field=models.DateTimeField(default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='issue', + name='last_seen', + field=models.DateTimeField(default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/issues/migrations/0009_issue_issues_issu_first_s_9fb0f9_idx_and_more.py b/issues/migrations/0009_issue_issues_issu_first_s_9fb0f9_idx_and_more.py new file mode 100644 index 0000000..7456847 --- /dev/null +++ b/issues/migrations/0009_issue_issues_issu_first_s_9fb0f9_idx_and_more.py @@ -0,0 +1,19 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('issues', '0008_issue_event_count_issue_first_seen_issue_last_seen'), + ] + + operations = [ + migrations.AddIndex( + model_name='issue', + index=models.Index(fields=['first_seen'], name='issues_issu_first_s_9fb0f9_idx'), + ), + migrations.AddIndex( + model_name='issue', + index=models.Index(fields=['last_seen'], name='issues_issu_last_se_400a05_idx'), + ), + ] diff --git a/issues/models.py b/issues/models.py index 10db238..7b1045b 100644 --- a/issues/models.py +++ b/issues/models.py @@ -15,6 +15,11 @@ class Issue(models.Model): "projects.Project", blank=False, null=True, on_delete=models.SET_NULL) # SET_NULL: cleanup 'later' hash = models.CharField(max_length=32, blank=False, null=False) + # denormalized fields: + last_seen = models.DateTimeField(blank=False, null=False) + first_seen = models.DateTimeField(blank=False, null=False) + event_count = models.IntegerField(blank=False, null=False) + # fields related to resolution: # what does this mean for the release-based use cases? it means what you filter on. # it also simply means: it was "marked as resolved" after the last regression (if any) @@ -23,6 +28,7 @@ class Issue(models.Model): fixed_at = models.TextField(blank=False, null=False, default='[]') events_at = models.TextField(blank=False, null=False, default='[]') + # fields related to muting: is_muted = models.BooleanField(default=False) unmute_on_volume_based_conditions = models.TextField(blank=False, null=False, default="[]") # json string @@ -74,6 +80,12 @@ class Issue(models.Model): def occurs_in_last_release(self): return False # TODO actually implement (and then: implement in a performant manner) + class Meta: + indexes = [ + models.Index(fields=["first_seen"]), + models.Index(fields=["last_seen"]), + ] + class IssueStateManager(object): """basically: a namespace; with static methods that combine field-setting in a single place""" diff --git a/issues/templates/issues/issue_list.html b/issues/templates/issues/issue_list.html index fea13c5..7616431 100644 --- a/issues/templates/issues/issue_list.html +++ b/issues/templates/issues/issue_list.html @@ -50,11 +50,11 @@ https://flowbite.com/docs/forms/floating-label/ - FOO + {# FOO #} - BAR + {# BAR #} @@ -69,24 +69,22 @@ https://flowbite.com/docs/forms/floating-label/
{{ issue.title|truncatechars:100 }}
-
from 8 Nov 12:33 | last 8 Nov 12:39 | with 3 events +
from {{ issue.first_seen|date:"d M G:i" }} | last {{ issue.last_seen|date:"d M G:i" }} | with {{ issue.event_count }} events {% if issue.parsed_data.transaction %}| {{ issue.parsed_data.transaction }} {% endif %}
{% comment %} [ ] first release (or commit), last release (or commit) - [ ] first seen - [ ] last seen. [ ] assigned to [ ] n.r. of events? {% endcomment %} - ABC + {# ABC #} - 123 + {# 123 #} diff --git a/issues/tests.py b/issues/tests.py index beea408..f878654 100644 --- a/issues/tests.py +++ b/issues/tests.py @@ -10,6 +10,7 @@ from bugsink.period_counter import PeriodCounter, TL_DAY from .models import Issue, IssueStateManager from .regressions import is_regression, is_regression_2, issue_is_regression +from .factories import denormalized_issue_fields def fresh(obj): @@ -164,7 +165,7 @@ class RegressionIssueTestCase(DjangoTestCase): project = Project.objects.create() # new issue is not a regression - issue = Issue.objects.create(project=project) + issue = Issue.objects.create(project=project, **denormalized_issue_fields()) self.assertFalse(issue_is_regression(fresh(issue), "anything")) # resolve the issue, a reoccurrence is a regression @@ -185,7 +186,7 @@ class RegressionIssueTestCase(DjangoTestCase): create_release_if_needed(project, "2.0.0") # new issue is not a regression - issue = Issue.objects.create(project=project) + issue = Issue.objects.create(project=project, **denormalized_issue_fields()) self.assertFalse(issue_is_regression(fresh(issue), "anything")) # resolve the by latest, reoccurrences of older releases are not regressions but occurrences by latest are @@ -212,7 +213,7 @@ class RegressionIssueTestCase(DjangoTestCase): create_release_if_needed(project, "2.0.0") # new issue is not a regression - issue = Issue.objects.create(project=project) + issue = Issue.objects.create(project=project, **denormalized_issue_fields()) self.assertFalse(issue_is_regression(fresh(issue), "anything")) # resolve the by next, reoccurrences of any existing releases are not regressions @@ -296,7 +297,7 @@ class MuteUnmuteTestCase(TestCase): def test_mute_no_vbc_for_unmute(self): project = Project.objects.create() - issue = Issue.objects.create(project=project) + issue = Issue.objects.create(project=project, **denormalized_issue_fields()) IssueStateManager.mute(issue, "[]") issue.save() @@ -306,7 +307,7 @@ class MuteUnmuteTestCase(TestCase): def test_mute_simple_case(self): project = Project.objects.create() - issue = Issue.objects.create(project=project) + issue = Issue.objects.create(project=project, **denormalized_issue_fields()) IssueStateManager.mute(issue, "[{\"period\": \"day\", \"nr_of_periods\": 1, \"volume\": 1}]") issue.save() @@ -321,6 +322,7 @@ class MuteUnmuteTestCase(TestCase): project=project, unmute_on_volume_based_conditions='[{"period": "day", "nr_of_periods": 1, "volume": 1}]', is_muted=True, + **denormalized_issue_fields(), ) pc = registry.by_issue[issue.id] = PeriodCounter() @@ -337,6 +339,7 @@ class MuteUnmuteTestCase(TestCase): project=project, unmute_on_volume_based_conditions='[{"period": "day", "nr_of_periods": 1, "volume": 1}]', is_muted=True, + **denormalized_issue_fields(), ) # because we create our objects before getting the lazy registry, event-listeners will be correctly set by @@ -362,6 +365,7 @@ class MuteUnmuteTestCase(TestCase): {"period": "month", "nr_of_periods": 1, "volume": 1} ]''', is_muted=True, + **denormalized_issue_fields(), ) # because we create our objects before getting the lazy registry, event-listeners will be correctly set by diff --git a/issues/views.py b/issues/views.py index 435792a..788b527 100644 --- a/issues/views.py +++ b/issues/views.py @@ -10,7 +10,7 @@ from .models import Issue, IssueStateManager def issue_list(request, project_id): - issue_list = Issue.objects.filter(project_id=project_id) + issue_list = Issue.objects.filter(project_id=project_id).order_by("-last_seen") project = get_object_or_404(Project, pk=project_id) return render(request, "issues/issue_list.html", {