diff --git a/events/models.py b/events/models.py index d7e89d3..a80323c 100644 --- a/events/models.py +++ b/events/models.py @@ -4,7 +4,6 @@ import uuid from django.db import models from django.db.utils import IntegrityError -from django.db.models import Min, Max from django.utils.functional import cached_property from projects.models import Project @@ -276,18 +275,6 @@ class Event(models.Model): 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] - @cached_property def get_tags(self): return list( diff --git a/issues/templates/issues/_event_nav.html b/issues/templates/issues/_event_nav.html index e3500ca..92e78d8 100644 --- a/issues/templates/issues/_event_nav.html +++ b/issues/templates/issues/_event_nav.html @@ -1,5 +1,11 @@ - {% 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 #} - +{% load add_to_qs %} + +
{# nav="last": when doing a new search on an event-page, you want the most recent matching event to show up #} + +
+ + {% if has_prev %} {# no need for 'is_first': if you can go to the left, you can go all the way to the left too #} +
{% else %} @@ -8,8 +14,8 @@ {% endif %} - {% if event.has_prev %} - + {% if has_prev %} + {% else %} @@ -18,8 +24,8 @@ {% endif %} - {% if event.has_next %} - + {% if has_next %} + {% else %} @@ -28,8 +34,8 @@ {% endif %} - {% if event.has_next %} - + {% if has_next %} + {% else %} diff --git a/issues/templates/issues/base.html b/issues/templates/issues/base.html index 21a2508..d3e503c 100644 --- a/issues/templates/issues/base.html +++ b/issues/templates/issues/base.html @@ -3,6 +3,7 @@ {% load issues %} {% load humanize %} {% load stricter_templates %} +{% load add_to_qs %} {% block title %}{{ issue.title }} · {{ block.super }}{% endblock %} {% block content %} @@ -95,7 +96,7 @@

{{ issue.calculated_type }}

{{ issue.calculated_value }}
- {% if parsed_data.request %}
{{ parsed_data.request.method }} {{ parsed_data.request.url }}
{% endif %} + {% if request_repr %}
{{ request_repr }}
{% endif %}
{% if issue.last_frame_module %}{{ issue.last_frame_module}}{% else %}{{ issue.last_frame_filename }}{% endif %}{% if issue.last_frame_function %} in {{ issue.last_frame_function }}{% endif %}
{# top, LHS (various texts) #} @@ -106,13 +107,13 @@ {# overflow-x-auto is needed at the level of the flex item such that it works at the level where we need it (the code listings)#}
{# 96rem is 1536px, which matches the 2xl class; this is no "must" but eyeballing revealed: good result #}
-
Stacktrace
-
Event Details
-
Breadcrumbs
-
Tags
-
Event List
-
Grouping
-
History
+
Stacktrace
+
Event Details
+
Breadcrumbs
+
Event List
+
Tags
+
Grouping
+
History
diff --git a/issues/templates/issues/breadcrumbs.html b/issues/templates/issues/breadcrumbs.html index 204b1bf..ce82219 100644 --- a/issues/templates/issues/breadcrumbs.html +++ b/issues/templates/issues/breadcrumbs.html @@ -8,7 +8,7 @@
-
{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }})
+
{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }} total{% if q %} — {{ event_qs.count }} found by search{% endif %})
diff --git a/issues/templates/issues/event_404.html b/issues/templates/issues/event_404.html index 5a147e6..d5c257c 100644 --- a/issues/templates/issues/event_404.html +++ b/issues/templates/issues/event_404.html @@ -1,24 +1,34 @@ {% extends "issues/base.html" %} {% load static %} {% load stricter_templates %} +{% load add_to_qs %} +{% load humanize %} {% block tab_content %}
-
xxxx xx xx xx:xx (Event xxx of {{ issue.digested_event_count }})
+
{{ issue.digested_event_count|intcomma }} events in total{% if q %} — {{ event_qs_count }} found by search{% endif %}.
- {# 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 #} + {# copy/paste of _event_nav, but not based on any event (we have none), prev/next are meaningless also; first/last only when we have an event_qs to navigate through #} +
{# nav="last": when doing a new search on an event-page, you want the most recent matching event to show up #} + +
- + {% if event_qs_count %} + + {% else %} +
+ +
+ {% endif %}
@@ -28,19 +38,35 @@
- + {% if event_qs_count %} + + {% else %} +
+ + +
+ {% endif %}
-

404: Event missing from Bugsink

+

{% if event_qs_count %}404: Event missing from Bugsink{% else %}No Events{% endif %}

+ {# We apply the heuristic (textually) that if you have some events in your event_qs, but no current event, you're in a "404-like" ("This event not found") state #} + {# and if there's really no events in the qs, that that fact is what you should focus on. #} + {# This works well in practice (better than trying to match these texts to "where in _get_event() did we go wrong?" #} + {% if event_qs_count %} This event cannot be found. It could have been removed manually or as part of the eviction process. + {% elif q %} + No events found for this search. + {% else %} + No events found. They could have been removed manually or as part of the eviction process. + {% endif %}
{% endblock %} diff --git a/issues/templates/issues/event_details.html b/issues/templates/issues/event_details.html index 9be82fa..2683cf3 100644 --- a/issues/templates/issues/event_details.html +++ b/issues/templates/issues/event_details.html @@ -7,7 +7,7 @@
-
{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }})
+
{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }} total{% if q %} — {{ event_qs.count }} found by search{% endif %})
diff --git a/issues/templates/issues/event_list.html b/issues/templates/issues/event_list.html index 1ec55a8..9cf6f42 100644 --- a/issues/templates/issues/event_list.html +++ b/issues/templates/issues/event_list.html @@ -1,17 +1,23 @@ {% extends "issues/base.html" %} {% load issues %} +{% load add_to_qs %} {% load humanize %} +{% load add_to_qs %} {% block tab_content %}
- Showing {{ page_obj.start_index|intcomma }} - {{ page_obj.end_index|intcomma }} of {{ page_obj.paginator.count|intcomma }} - {% if issue.digested_event_count != issue.stored_event_count %} - available events ({{ issue.digested_event_count|intcomma }} total observed). - {% else %} - total events. + Showing {{ page_obj.start_index|intcomma }} - {{ page_obj.end_index|intcomma }} of + {% if page_obj.paginator.count == issue.stored_event_count and issue.stored_event_count == issue.digested_event_count %} {# all equal #} + {{ page_obj.paginator.count|intcomma }} total events. + {% elif page_obj.paginator.count == issue.stored_event_count and issue.stored_event_count != issue.digested_event_count %} {# evictions applied #} + {{ page_obj.paginator.count|intcomma }} available events ({{ issue.digested_event_count|intcomma }} total observed). + {% elif page_obj.paginator.count != issue.stored_event_count and issue.stored_event_count == issue.digested_event_count %} {# search filters #} + {{ page_obj.paginator.count|intcomma }} events found ({{ issue.digested_event_count|intcomma }} total observed). + {% else %} {# everything unequal #} + {{ page_obj.paginator.count|intcomma }} events found ({{ issue.digested_event_count|intcomma }} available, {{ issue.digested_event_count|intcomma }} total observed). {% endif %}
@@ -20,8 +26,12 @@ {# UI / UX question: is it a good idea to reuse-with-different-meaning (pages, not events) for this? #} {# adapted copy/pasta from _event_nav #}
+
+ +
+ {% if page_obj.has_previous %} {# no need for 'is_first': if you can go to the left, you can go all the way to the left too #} - + {% else %}
@@ -30,7 +40,7 @@ {% endif %} {% if page_obj.has_previous %} - + {% else %} @@ -40,7 +50,7 @@ {% endif %} {% if page_obj.has_next %} - + {% else %} @@ -50,7 +60,7 @@ {% endif %} {% if page_obj.has_next %} - + {% else %} @@ -109,11 +119,11 @@ TODO - {{ event.digest_order }} + {{ event.digest_order }} {# how useful is this really? #} - {{ event.id|truncatechars:9 }} + {{ event.id|truncatechars:9 }} @@ -140,6 +150,12 @@ TODO +{% empty %} + + + No events found{% if q %} for "{{ q }}"{% endif %}. + + {% endfor %} {# note: no "empty" case; event-less issues are not something I expect to really support (for some definition of "really") #} diff --git a/issues/templates/issues/stacktrace.html b/issues/templates/issues/stacktrace.html index c4bb7c6..f218b10 100644 --- a/issues/templates/issues/stacktrace.html +++ b/issues/templates/issues/stacktrace.html @@ -10,7 +10,7 @@ {# event-nav only #}
-
{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }})
+
{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }} total{% if q %} — {{ event_qs.count }} found by search{% endif %})
@@ -31,7 +31,7 @@
{% if forloop.counter0 == 0 %} -
{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }})
+
{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }} total{% if q %} — {{ event_qs.count }} found by search{% endif %})
{% endif %}

{{ exception.type }}

{{ exception.value }}
diff --git a/issues/urls.py b/issues/urls.py index 2d8a93f..773b247 100644 --- a/issues/urls.py +++ b/issues/urls.py @@ -22,6 +22,7 @@ def regex_converter(passed_regex): register_converter(regex_converter("(first|last)"), "first-last") register_converter(regex_converter("(prev|next)"), "prev-next") +register_converter(regex_converter("(none)"), "str-none") urlpatterns = [ @@ -35,6 +36,11 @@ urlpatterns = [ path('issue//event//details/', issue_event_details, name="event_details"), path('issue//event//breadcrumbs/', issue_event_breadcrumbs, name="event_breadcrumbs"), + path('issue//event//', issue_event_stacktrace, name="event_stacktrace"), + path('issue//event//details/', issue_event_details, name="event_details"), + path('issue//event//breadcrumbs/', + issue_event_breadcrumbs, name="event_breadcrumbs"), + path('issue//event//', issue_event_stacktrace, name="event_stacktrace"), path('issue//event//details/', issue_event_details, name="event_details"), path('issue//event//breadcrumbs/', issue_event_breadcrumbs, diff --git a/issues/views.py b/issues/views.py index 7f69b2b..af1944b 100644 --- a/issues/views.py +++ b/issues/views.py @@ -3,6 +3,7 @@ import json import sentry_sdk import re +from django.db.models import Min, Max from django.utils import timezone from django.shortcuts import render, get_object_or_404, redirect from django.db.models import Q, Subquery @@ -26,7 +27,7 @@ from events.ua_stuff import get_contexts_enriched_with_ua from compat.timestamp import format_timestamp from projects.models import ProjectMembership -from tags.models import TagValue, IssueTag +from tags.models import TagValue, IssueTag, EventTag from .models import Issue, IssueQuerysetStateManager, IssueStateManager, TurningPoint, TurningPointKind from .forms import CommentForm @@ -62,6 +63,13 @@ class KnownCountPaginator(Paginator): return self._count +def _request_repr(parsed_data): + if "request" not in parsed_data: + return "" + + return parsed_data["request"].get("method", "") + " " + parsed_data["request"].get("url", "") + + def _is_valid_action(action, issue): """We take the 'strict' approach of complaining even when the action is simply a no-op, because you're already in the desired state.""" @@ -240,6 +248,9 @@ def _and_join(q_objects): def _search(project, issue_list, q): + if not q: + return issue_list + # The simplest possible query-language that could have any value: key:value is recognized as such; the rest is "free # text"; no support for quoting of spaces. slices_to_remove = [] @@ -274,6 +285,44 @@ def _search(project, issue_list, q): return issue_list +def _search_events(project, event_list, q): + if not q: + return event_list + + # The simplest possible query-language that could have any value: key:value is recognized as such; the rest is "free + # text"; no support for quoting of spaces. + slices_to_remove = [] + clauses = [] + for match in re.finditer(r"(\S+):(\S+)", q): + slices_to_remove.append(match.span()) + key, value = match.groups() + try: + tag_value_obj = TagValue.objects.get(project=project, key__key=key, value=value) + except TagValue.DoesNotExist: + # if the tag doesn't exist, we can't have any events with it; the below short-circuit is fine, I think (I + # mean: we _could_ say "tag x is to blame" but that's not what one does generally in search, is it? + return event_list.none() + + # TODO: Extensive performance testing of various choices here is necessary; in particular the choice of Subquery + # vs. joins; and the choice of a separate query to get TagValue v.s. doing everything in a single big query will + # have different trade-offs _in practice_. + clauses.append( + Q(id__in=Subquery(EventTag.objects.filter(value=tag_value_obj).values_list("event_id", flat=True)))) + + # this is really TSTTCPW (or more like a "fake it till you make it" thing); but I'd rather "have something" and then + # have really-good-search than to have either nothing at all, or half-baked search. Note that we didn't even bother + # to set indexes on the fields we search on (nor create a single searchable field for the whole of 'title'). + plain_text_q = remove_slices(q, slices_to_remove).strip() + if plain_text_q: + clauses.append(Q(Q(calculated_type__icontains=plain_text_q) | Q(calculated_value__icontains=plain_text_q))) + + # if we reach this point, there's always either a plain_text_q or some key/value pair (this is a condition for + # _and_join) + event_list = event_list.filter(_and_join(clauses)) + + return event_list + + def _issue_list_pt_2(request, project, state_filter, unapplied_issue_ids): d_state_filter = { "open": lambda qs: qs.filter(is_resolved=False, is_muted=False), @@ -336,38 +385,42 @@ def _handle_post(request, issue): return HttpResponseRedirect(request.path_info) -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() +def _get_event(qs, event_pk, digest_order, nav): + """Returns the event using the "url lookup".""" - if nav in ["prev", "next"]: + if nav is not None: + if nav not in ["first", "last", "prev", "next"]: + raise Http404("Cannot look up with '%s'" % nav) + + if nav == "first": + result = qs.order_by("digest_order").first() + elif nav == "last": + result = qs.order_by("digest_order").last() + elif nav in ["prev", "next"]: if digest_order is None: raise Http404("Cannot look up with '%s' without digest_order" % nav) if nav == "prev": - result = Event.objects.filter( - issue=issue, digest_order__lt=digest_order).order_by("-digest_order").first() + result = qs.filter(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 + result = qs.filter(digest_order__gt=digest_order).order_by("digest_order").first() - raise Http404("Cannot look up with '%s'" % nav) + if result is None: + raise Event.DoesNotExist + return result elif event_pk is not None: # we match on both internal and external id, trying internal first + if event_pk == "none": + raise Event.DoesNotExist() + try: return Event.objects.get(pk=event_pk) except Event.DoesNotExist: - return Event.objects.get(issue=issue, event_id=event_pk) + return qs.get(event_id=event_pk) elif digest_order is not None: - return Event.objects.get(issue=issue, digest_order=digest_order) + return qs.get(digest_order=digest_order) else: raise Http404("Either event_pk, nav, or digest_order must be provided") @@ -378,10 +431,14 @@ def issue_event_stacktrace(request, issue, event_pk=None, digest_order=None, nav if request.method == "POST": return _handle_post(request, issue) + event_qs = Event.objects.filter(issue=issue) + if request.GET.get("q"): + event_qs = _search_events(issue.project, event_qs, request.GET["q"]) + try: - event = _get_event(issue, event_pk, digest_order, nav) + event = _get_event(event_qs, event_pk, digest_order, nav) except Event.DoesNotExist: - return issue_event_404(request, issue, "stacktrace", "event_stacktrace") + return issue_event_404(request, issue, event_qs, "stacktrace", "event_stacktrace") parsed_data = event.get_parsed_data() @@ -425,16 +482,20 @@ def issue_event_stacktrace(request, issue, event_pk=None, digest_order=None, nav "event": event, "is_event_page": True, "parsed_data": parsed_data, + "request_repr": _request_repr(parsed_data), "exceptions": exceptions, "stack_of_plates": stack_of_plates, "mute_options": GLOBAL_MUTE_OPTIONS, + "q": request.GET.get("q", ""), + "event_qs": event_qs, + **_has_next_prev(event, event_qs), }) -def issue_event_404(request, issue, tab, this_view): +def issue_event_404(request, issue, event_qs, 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("digest_order").last() # the template needs this for the tabs + last_event = event_qs.last() # used for switching to an event-page (using tabs) return render(request, "issues/event_404.html", { "tab": tab, "this_view": this_view, @@ -443,6 +504,9 @@ def issue_event_404(request, issue, tab, this_view): "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, + "event_qs": event_qs, + "q": request.GET.get("q", ""), + "event_qs_count": event_qs.count(), # avoids repeating the count() query }) @@ -452,10 +516,14 @@ def issue_event_breadcrumbs(request, issue, event_pk=None, digest_order=None, na if request.method == "POST": return _handle_post(request, issue) + event_qs = Event.objects.filter(issue=issue) + if request.GET.get("q"): + event_qs = _search_events(issue.project, event_qs, request.GET["q"]) + try: - event = _get_event(issue, event_pk, digest_order, nav) + event = _get_event(event_qs, event_pk, digest_order, nav) except Event.DoesNotExist: - return issue_event_404(request, issue, "breadcrumbs", "event_breadcrumbs") + return issue_event_404(request, issue, event_qs, "breadcrumbs", "event_breadcrumbs") parsed_data = event.get_parsed_data() @@ -466,9 +534,12 @@ def issue_event_breadcrumbs(request, issue, event_pk=None, digest_order=None, na "issue": issue, "event": event, "is_event_page": True, - "parsed_data": parsed_data, + "request_repr": _request_repr(parsed_data), "breadcrumbs": get_values(parsed_data.get("breadcrumbs")), "mute_options": GLOBAL_MUTE_OPTIONS, + "q": request.GET.get("q", ""), + "event_qs": event_qs, + **_has_next_prev(event, event_qs), }) @@ -478,16 +549,28 @@ def _date_with_milis_html(timestamp): '' + date(timestamp, "u")[:3] + '') +def _has_next_prev(event, event_qs): + d = event_qs.aggregate(lo=Min("digest_order"), hi=Max("digest_order")) + return { + "has_prev": event.digest_order > d["lo"] if d.get("lo") is not None else False, + "has_next": event.digest_order < d["hi"] if d.get("hi") is not None else False, + } + + @atomic_for_request_method @issue_membership_required def issue_event_details(request, issue, event_pk=None, digest_order=None, nav=None): if request.method == "POST": return _handle_post(request, issue) + event_qs = Event.objects.filter(issue=issue) + if request.GET.get("q"): + event_qs = _search_events(issue.project, event_qs, request.GET["q"]) + try: - event = _get_event(issue, event_pk, digest_order, nav) + event = _get_event(event_qs, event_pk, digest_order, nav) except Event.DoesNotExist: - return issue_event_404(request, issue, "event-details", "event_details") + return issue_event_404(request, issue, event_qs, "event-details", "event_details") parsed_data = event.get_parsed_data() key_info = [ @@ -557,11 +640,15 @@ def issue_event_details(request, issue, event_pk=None, digest_order=None, nav=No "event": event, "is_event_page": True, "parsed_data": parsed_data, + "request_repr": _request_repr(parsed_data), "key_info": key_info, "logentry_info": logentry_info, "deployment_info": deployment_info, "contexts": contexts, "mute_options": GLOBAL_MUTE_OPTIONS, + "q": request.GET.get("q", ""), + "event_qs": event_qs, + **_has_next_prev(event, event_qs), }) @@ -571,14 +658,15 @@ def issue_history(request, issue): if request.method == "POST": return _handle_post(request, issue) - last_event = issue.event_set.order_by("digest_order").last() # the template needs this for the tabs + event_qs = _search_events(issue.project, issue.event_set.order_by("digest_order"), request.GET.get("q", "")) + last_event = event_qs.last() # used for switching to an event-page (using tabs) return render(request, "issues/history.html", { "tab": "history", "project": issue.project, "issue": issue, "event": last_event, "is_event_page": False, - "parsed_data": last_event.get_parsed_data(), + "request_repr": _request_repr(last_event.get_parsed_data()) if last_event is not None else "", "mute_options": GLOBAL_MUTE_OPTIONS, }) @@ -589,14 +677,15 @@ def issue_tags(request, issue): if request.method == "POST": return _handle_post(request, issue) - last_event = issue.event_set.order_by("digest_order").last() # the template needs this for the tabs + event_qs = _search_events(issue.project, issue.event_set.order_by("digest_order"), request.GET.get("q", "")) + last_event = event_qs.last() # used for switching to an event-page (using tabs) return render(request, "issues/tags.html", { "tab": "tags", "project": issue.project, "issue": issue, "event": last_event, "is_event_page": False, - "parsed_data": last_event.get_parsed_data(), + "request_repr": _request_repr(last_event.get_parsed_data()) if last_event is not None else "", "mute_options": GLOBAL_MUTE_OPTIONS, }) @@ -607,14 +696,15 @@ def issue_grouping(request, issue): if request.method == "POST": return _handle_post(request, issue) - last_event = issue.event_set.order_by("digest_order").last() # the template needs this for the tabs + event_qs = _search_events(issue.project, issue.event_set.order_by("digest_order"), request.GET.get("q", "")) + last_event = event_qs.last() # used for switching to an event-page (using tabs) return render(request, "issues/grouping.html", { "tab": "grouping", "project": issue.project, "issue": issue, "event": last_event, "is_event_page": False, - "parsed_data": last_event.get_parsed_data(), + "request_repr": _request_repr(last_event.get_parsed_data()) if last_event is not None else "", "mute_options": GLOBAL_MUTE_OPTIONS, }) @@ -627,13 +717,18 @@ def issue_event_list(request, issue): event_list = issue.event_set.order_by("digest_order") - # re 250: in general "big is good" because it allows a lot "at a glance". - paginator = KnownCountPaginator(event_list, 250, count=issue.stored_event_count) + if "q" in request.GET: + event_list = _search_events(issue.project, event_list, request.GET["q"]) + paginator = Paginator(event_list, 250) # might as well use Paginator; the cost of .count() is incurred anyway + else: + # re 250: in general "big is good" because it allows a lot "at a glance". + paginator = KnownCountPaginator(event_list, 250, count=issue.stored_event_count) page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) - last_event = issue.event_set.order_by("digest_order").last() # the template needs this for the tabs + last_event = event_list.last() # used for switching to an event-page (using tabs) + return render(request, "issues/event_list.html", { "tab": "event-list", "project": issue.project, @@ -641,8 +736,9 @@ def issue_event_list(request, issue): "event": last_event, "event_list": event_list, "is_event_page": False, - "parsed_data": last_event.get_parsed_data(), + "request_repr": _request_repr(last_event.get_parsed_data()) if last_event is not None else "", "mute_options": GLOBAL_MUTE_OPTIONS, + "q": request.GET.get("q", ""), "page_obj": page_obj, }) diff --git a/theme/templatetags/add_to_qs.py b/theme/templatetags/add_to_qs.py index 7907e84..a1c62f9 100644 --- a/theme/templatetags/add_to_qs.py +++ b/theme/templatetags/add_to_qs.py @@ -7,6 +7,8 @@ register = template.Library() @register.simple_tag(takes_context=True) def add_to_qs(context, **kwargs): + """add kwargs to query string""" + if 'request' not in context: # "should not happen", because this tag is only assumed to be used in RequestContext templates, but it's not # something I want to break for. Also: we have an answer that "mostly works" for that case, so let's do that. @@ -15,3 +17,16 @@ def add_to_qs(context, **kwargs): query = copy(context['request'].GET.dict()) query.update(kwargs) return urlencode(query) + + +@register.simple_tag(takes_context=True) +def current_qs(context): + if 'request' not in context: + # "should not happen", because this tag is only assumed to be used in RequestContext templates, but it's not + # something I want to break for. Also: we have an answer that "mostly works" for that case, so let's do that. + return "" + + query = copy(context['request'].GET.dict()) + if query: + return '?' + urlencode(query) + return ""