Navigation: fix for missing events

Now that we have eviction, events may disappear. Deal with it:

* event-specific 404 that still allows for navigation
* first/last buttons
* navigation to prev/next when prev/next is not just 1 step away
* don't use HttpRedirect for "lookup based" views
    in principle, the thing you were looking for might go missing in-between
    drawback: these URLs are not "stable"
This commit is contained in:
Klaas van Schelven
2024-07-19 11:03:08 +02:00
parent aec78f6318
commit b76e474ef1
6 changed files with 180 additions and 34 deletions

View File

@@ -4,6 +4,7 @@ import uuid
from django.db import models from django.db import models
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.db.models import Min, Max
from projects.models import Project from projects.models import Project
from compat.timestamp import parse_timestamp from compat.timestamp import parse_timestamp
@@ -230,3 +231,15 @@ class Event(models.Model):
assert re.match( assert re.match(
r".*unique constraint failed.*events_event.*project_id.*events_event.*event_id", str(e).lower()) r".*unique constraint failed.*events_event.*project_id.*events_event.*event_id", str(e).lower())
return None, False return None, False
def get_digest_order_bounds(self):
if not hasattr(self, "_digest_order_bounds"):
d = Event.objects.filter(issue_id=self.issue.id).aggregate(lo=Min("digest_order"), hi=Max("digest_order"))
self._digest_order_bounds = d["lo"], d["hi"]
return self._digest_order_bounds
def has_prev(self):
return self.digest_order > self.get_digest_order_bounds()[0]
def has_next(self):
return self.digest_order < self.get_digest_order_bounds()[1]

View File

@@ -1,5 +1,16 @@
{% if event.digest_order > 1 %} {% if event.has_prev %} {# no need for 'is_first': if you can go to the left, you can go all the way to the left too #}
<a href="{% url this_view issue_pk=issue.pk digest_order=event.digest_order|add:"-1" %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center"> <a href="{% url this_view issue_pk=issue.pk nav="first" %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M3.22 7.595a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 0 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06l-3.25 3.25Zm8.25-3.25-3.25 3.25a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 1 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06Z" clip-rule="evenodd" />
</svg>
</a>
{% else %}
<div class="font-bold text-slate-300 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md inline-flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M3.22 7.595a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 0 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06l-3.25 3.25Zm8.25-3.25-3.25 3.25a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 1 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06Z" clip-rule="evenodd" />
</div>
{% endif %}
{% if event.has_prev %}
<a href="{% url this_view issue_pk=issue.pk digest_order=event.digest_order nav="prev" %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M9.78 4.22a.75.75 0 0 1 0 1.06L7.06 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L5.47 8.53a.75.75 0 0 1 0-1.06l3.25-3.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" /></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M9.78 4.22a.75.75 0 0 1 0 1.06L7.06 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L5.47 8.53a.75.75 0 0 1 0-1.06l3.25-3.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" /></svg>
</a> </a>
{% else %} {% else %}
@@ -8,8 +19,8 @@
</div> </div>
{% endif %} {% endif %}
{% if event.digest_order < issue.digested_event_count %} {% if event.has_next %}
<a href="{% url this_view issue_pk=issue.pk digest_order=event.digest_order|add:"1" %}"" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center"> <a href="{% url this_view issue_pk=issue.pk digest_order=event.digest_order nav="next" %}"" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
</a> </a>
{% else %} {% else %}
@@ -17,3 +28,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
</div> </div>
{% endif %} {% endif %}
{% if event.has_next %}
<a href="{% url this_view issue_pk=issue.pk nav="last" %}"" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M12.78 7.595a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06l3.25 3.25Zm-8.25-3.25 3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %}
<div class="font-bold text-slate-300 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md inline-flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M12.78 7.595a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06l3.25 3.25Zm-8.25-3.25 3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06Z" clip-rule="evenodd" /></svg>
</div>
{% endif %}

View File

@@ -0,0 +1,47 @@
{% extends "issues/base.html" %}
{% load static %}
{% load stricter_templates %}
{% block tab_content %}
{# this is here to fool tailwind (because we're foolish enough to put html in python) <span class="text-xs"></span> #}
<div class="flex">
<div class="overflow-hidden">
<div class="italic">xxxx xx xx xx:xx (Event xxx of {{ issue.digested_event_count }})</div>
</div>
<div class="ml-auto flex-none">
<div class="flex place-content-end">
{# copy/paste of _event_nav, but not based on any event (we have none), prev/next are meaningless also #}
{# so we have first/last enabled, and the middle ones disabled #}
<a href="{% url this_view issue_pk=issue.pk nav="first" %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M3.22 7.595a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 0 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06l-3.25 3.25Zm8.25-3.25-3.25 3.25a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 1 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06Z" clip-rule="evenodd" />
</svg>
</a>
<div class="font-bold text-slate-300 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md inline-flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M9.78 4.22a.75.75 0 0 1 0 1.06L7.06 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L5.47 8.53a.75.75 0 0 1 0-1.06l3.25-3.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" /></svg>
</div>
<div class="font-bold text-slate-300 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md inline-flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
</div>
<a href="{% url this_view issue_pk=issue.pk nav="last" %}"" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M12.78 7.595a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06l3.25 3.25Zm-8.25-3.25 3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06Z" clip-rule="evenodd" /></svg>
</a>
</div>
</div>
</div>
<h1 class="text-2xl font-bold mt-4">404: Event missing from Bugsink</h1>
<div class="mb-6">
This event cannot be found. It could have been removed manually or as part of the eviction process.
</div>
{% endblock %}

View File

@@ -407,11 +407,6 @@ class ViewTests(TransactionTestCase):
response = self.client.get(f"/issues/issue/{self.issue.id}/history/") response = self.client.get(f"/issues/issue/{self.issue.id}/history/")
self.assertContains(response, self.issue.title()) self.assertContains(response, self.issue.title())
def test_issue_last_event(self):
response = self.client.get(f"/issues/issue/{self.issue.id}/event/last/")
self.assertEquals(302, response.status_code)
self.assertTrue(str(self.event.id) in response.url)
def test_issue_event_list(self): def test_issue_event_list(self):
response = self.client.get(f"/issues/issue/{self.issue.id}/events/") response = self.client.get(f"/issues/issue/{self.issue.id}/events/")
self.assertContains(response, self.issue.title()) self.assertContains(response, self.issue.title())

View File

@@ -1,9 +1,27 @@
from django.urls import path from django.urls import path, register_converter
from .views import ( from .views import (
issue_list, issue_event_stacktrace, issue_event_details, issue_last_event, issue_event_list, issue_history, issue_list, issue_event_stacktrace, issue_event_details, issue_event_list, issue_history, issue_grouping,
issue_grouping, issue_event_breadcrumbs, event_by_internal_id, history_comment_new, history_comment_edit, issue_event_breadcrumbs, event_by_internal_id, history_comment_new, history_comment_edit, history_comment_delete)
history_comment_delete)
def regex_converter(passed_regex):
class RegexConverter:
regex = passed_regex
def to_python(self, value):
return value
def to_url(self, value):
return value
return RegexConverter
register_converter(regex_converter("(first|last)"), "first-last")
register_converter(regex_converter("(prev|next)"), "prev-next")
urlpatterns = [ urlpatterns = [
path('<int:project_pk>/', issue_list, {"state_filter": "open"}, name="issue_list_open"), path('<int:project_pk>/', issue_list, {"state_filter": "open"}, name="issue_list_open"),
@@ -13,7 +31,6 @@ urlpatterns = [
path('<int:project_pk>/all/', issue_list, {"state_filter": "all"}, name="issue_list_all"), path('<int:project_pk>/all/', issue_list, {"state_filter": "all"}, name="issue_list_all"),
path('issue/<uuid:issue_pk>/event/<uuid:event_pk>/', issue_event_stacktrace, name="event_stacktrace"), path('issue/<uuid:issue_pk>/event/<uuid:event_pk>/', issue_event_stacktrace, name="event_stacktrace"),
path('issue/<uuid:issue_pk>/event/<uuid:event_pk>/details/', issue_event_details, name="event_details"), path('issue/<uuid:issue_pk>/event/<uuid:event_pk>/details/', issue_event_details, name="event_details"),
path('issue/<uuid:issue_pk>/event/<uuid:event_pk>/breadcrumbs/', issue_event_breadcrumbs, name="event_breadcrumbs"), path('issue/<uuid:issue_pk>/event/<uuid:event_pk>/breadcrumbs/', issue_event_breadcrumbs, name="event_breadcrumbs"),
@@ -22,9 +39,24 @@ urlpatterns = [
path('issue/<uuid:issue_pk>/event/<int:digest_order>/breadcrumbs/', issue_event_breadcrumbs, path('issue/<uuid:issue_pk>/event/<int:digest_order>/breadcrumbs/', issue_event_breadcrumbs,
name="event_breadcrumbs"), name="event_breadcrumbs"),
path('issue/<uuid:issue_pk>/event/<int:digest_order>/', issue_event_stacktrace, name="event_stacktrace"),
path('issue/<uuid:issue_pk>/event/<int:digest_order>/details/', issue_event_details, name="event_details"),
path('issue/<uuid:issue_pk>/event/<int:digest_order>/breadcrumbs/', issue_event_breadcrumbs,
name="event_breadcrumbs"),
path('issue/<uuid:issue_pk>/event/<int:digest_order>/<prev-next:nav>/', issue_event_stacktrace,
name="event_stacktrace"),
path('issue/<uuid:issue_pk>/event/<int:digest_order>/<prev-next:nav>/details/', issue_event_details,
name="event_details"),
path('issue/<uuid:issue_pk>/event/<int:digest_order>/<prev-next:nav>/breadcrumbs/', issue_event_breadcrumbs,
name="event_breadcrumbs"),
path('issue/<uuid:issue_pk>/event/<first-last:nav>/', issue_event_stacktrace, name="event_stacktrace"),
path('issue/<uuid:issue_pk>/event/<first-last:nav>/details/', issue_event_details, name="event_details"),
path('issue/<uuid:issue_pk>/event/<first-last:nav>/breadcrumbs/', issue_event_details, name="event_breadcrumbs"),
path('issue/<uuid:issue_pk>/history/', issue_history), path('issue/<uuid:issue_pk>/history/', issue_history),
path('issue/<uuid:issue_pk>/grouping/', issue_grouping), path('issue/<uuid:issue_pk>/grouping/', issue_grouping),
path('issue/<uuid:issue_pk>/event/last/', issue_last_event),
path('issue/<uuid:issue_pk>/events/', issue_event_list), path('issue/<uuid:issue_pk>/events/', issue_event_list),
path('event/<uuid:event_pk>/', event_by_internal_id, name="event_by_internal_id"), path('event/<uuid:event_pk>/', event_by_internal_id, name="event_by_internal_id"),

View File

@@ -9,6 +9,7 @@ from django.utils.safestring import mark_safe
from django.template.defaultfilters import date from django.template.defaultfilters import date
from django.urls import reverse from django.urls import reverse
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404
from bugsink.decorators import project_membership_required, issue_membership_required, atomic_for_request_method from bugsink.decorators import project_membership_required, issue_membership_required, atomic_for_request_method
from bugsink.transaction import durable_atomic from bugsink.transaction import durable_atomic
@@ -235,14 +236,6 @@ def event_by_internal_id(request, event_pk):
return redirect(issue_event_stacktrace, issue_pk=issue.pk, event_pk=event.pk) return redirect(issue_event_stacktrace, issue_pk=issue.pk, event_pk=event.pk)
@atomic_for_request_method
@issue_membership_required
def issue_last_event(request, issue):
last_event = issue.event_set.order_by("timestamp").last()
return redirect(issue_event_stacktrace, issue_pk=issue.pk, event_pk=last_event.pk)
def _handle_post(request, issue): def _handle_post(request, issue):
if _is_valid_action(request.POST["action"], issue): if _is_valid_action(request.POST["action"], issue):
_apply_action(IssueStateManager, issue, request.POST["action"], request.user) _apply_action(IssueStateManager, issue, request.POST["action"], request.user)
@@ -257,27 +250,49 @@ def _handle_post(request, issue):
return HttpResponseRedirect(request.path_info) return HttpResponseRedirect(request.path_info)
def _get_event(issue, event_pk, digest_order): def _get_event(issue, event_pk, digest_order, nav):
if nav is not None:
if nav == "first":
return Event.objects.filter(issue=issue).order_by("digest_order").first()
if nav == "last":
return Event.objects.filter(issue=issue).order_by("digest_order").last()
if nav in ["prev", "next"]:
if nav == "prev":
result = Event.objects.filter(
issue=issue, digest_order__lt=digest_order).order_by("-digest_order").first()
elif nav == "next":
result = Event.objects.filter(
issue=issue, digest_order__gt=digest_order).order_by("digest_order").first()
if result is None:
raise Event.DoesNotExist
return result
raise Http404("Cannot look up with '%s'" % nav)
if event_pk is not None: if event_pk is not None:
# we match on both internal and external id, trying internal first # we match on both internal and external id, trying internal first
try: try:
return Event.objects.get(pk=event_pk) return Event.objects.get(pk=event_pk)
except Event.DoesNotExist: except Event.DoesNotExist:
return get_object_or_404(Event, issue=issue, event_id=event_pk) return Event.objects.get(issue=issue, event_id=event_pk)
elif digest_order is not None: elif digest_order is not None:
return get_object_or_404(Event, issue=issue, digest_order=digest_order) return Event.objects.get(issue=issue, digest_order=digest_order)
else: else:
raise ValueError("either event_pk or digest_order must be provided") raise ValueError("either event_pk or digest_order must be provided")
@atomic_for_request_method @atomic_for_request_method
@issue_membership_required @issue_membership_required
def issue_event_stacktrace(request, issue, event_pk=None, digest_order=None): def issue_event_stacktrace(request, issue, event_pk=None, digest_order=None, nav=None):
if request.method == "POST": if request.method == "POST":
return _handle_post(request, issue) return _handle_post(request, issue)
event = _get_event(issue, event_pk, digest_order) try:
event = _get_event(issue, event_pk, digest_order, nav)
except Event.DoesNotExist:
return issue_event_404(request, issue, "stacktrace", "event_stacktrace")
parsed_data = json.loads(event.data) parsed_data = json.loads(event.data)
@@ -318,13 +333,31 @@ def issue_event_stacktrace(request, issue, event_pk=None, digest_order=None):
}) })
def issue_event_404(request, issue, tab, this_view):
"""If the Event is 404, but the issue is not, we can still show the issue page; we show a message for the event"""
last_event = issue.event_set.order_by("timestamp").last() # the template needs this for the tabs, we pick the last
return render(request, "issues/event_404.html", {
"tab": tab,
"this_view": this_view,
"project": issue.project,
"issue": issue,
"event": last_event,
"is_event_page": False, # this variable is used to denote "we have event-related info", which we don't
"mute_options": GLOBAL_MUTE_OPTIONS,
})
@atomic_for_request_method @atomic_for_request_method
@issue_membership_required @issue_membership_required
def issue_event_breadcrumbs(request, issue, event_pk=None, digest_order=None): def issue_event_breadcrumbs(request, issue, event_pk=None, digest_order=None, nav=None):
if request.method == "POST": if request.method == "POST":
return _handle_post(request, issue) return _handle_post(request, issue)
event = _get_event(issue, event_pk, digest_order) try:
event = _get_event(issue, event_pk, digest_order, nav)
except Event.DoesNotExist:
return issue_event_404(request, issue, "breadcrumbs", "event_breadcrumbs")
parsed_data = json.loads(event.data) parsed_data = json.loads(event.data)
@@ -348,11 +381,14 @@ def _date_with_milis_html(timestamp):
@atomic_for_request_method @atomic_for_request_method
@issue_membership_required @issue_membership_required
def issue_event_details(request, issue, event_pk=None, digest_order=None): def issue_event_details(request, issue, event_pk=None, digest_order=None, nav=None):
if request.method == "POST": if request.method == "POST":
return _handle_post(request, issue) return _handle_post(request, issue)
event = _get_event(issue, event_pk, digest_order) try:
event = _get_event(issue, event_pk, digest_order, nav)
except Event.DoesNotExist:
return issue_event_404(request, issue, "event-details", "event_details")
parsed_data = json.loads(event.data) parsed_data = json.loads(event.data)
key_info = [ key_info = [
@@ -361,8 +397,9 @@ def issue_event_details(request, issue, event_pk=None, digest_order=None):
("bugsink_internal_id", event.id), ("bugsink_internal_id", event.id),
("issue_id", issue.id), ("issue_id", issue.id),
("timestamp", _date_with_milis_html(event.timestamp)), ("timestamp", _date_with_milis_html(event.timestamp)),
("ingested_at", _date_with_milis_html(event.ingested_at)), ("ingested at", _date_with_milis_html(event.ingested_at)),
("digested_at", _date_with_milis_html(event.digested_at)), ("digested at", _date_with_milis_html(event.digested_at)),
("digest order", event.digest_order),
] ]
if parsed_data.get("logger"): if parsed_data.get("logger"):
key_info.append(("logger", parsed_data["logger"])) key_info.append(("logger", parsed_data["logger"]))