Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search trace browser #1917

Merged
merged 9 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions peachjam_search/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ class SearchTrace(models.Model):
# this is the name of the search configuration, for tracking changes across versions
config_version = models.CharField(max_length=50, null=False)
request_id = models.CharField(max_length=1024, null=True, editable=False)
previous_search = models.ForeignKey("self", on_delete=models.CASCADE, null=True)
previous_search = models.ForeignKey(
"self", on_delete=models.CASCADE, null=True, related_name="next_searches"
)

user = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
ip_address = models.CharField(max_length=1024, null=True)
Expand Down Expand Up @@ -45,7 +47,9 @@ class Meta:
class SearchClick(models.Model):
"""A click on a search result."""

search_trace = models.ForeignKey(SearchTrace, on_delete=models.CASCADE)
search_trace = models.ForeignKey(
SearchTrace, on_delete=models.CASCADE, related_name="clicks"
)
frbr_uri = models.CharField(max_length=2048)
portion = models.CharField(max_length=2048, null=True)
position = models.IntegerField()
Expand Down
91 changes: 91 additions & 0 deletions peachjam_search/templates/peachjam_search/_searchtrace_card.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{% load i18n humanize %}
<div class="card" id="{{ trace.pk }}">
<div class="card-header d-flex">
<h5 class="flex-grow-1 m-0">
{{ n }}.
{{ trace.search|default:trace.field_searches }}
<small class="text-muted">- <a href="{% url "search:search_trace" pk=trace.pk %}">{{ trace.pk }}</a></small>
</h5>
<div>
{% if trace.previous_search %}
<span title="{{ trace.created_at }}">+ {{ trace.previous_search.created_at|timesince:trace.created_at }}</span>
{% else %}
{{ trace.created_at }}
{% endif %}
</div>
</div>
<table class="table table-condensed mb-3">
<tr>
<th>{% trans "Search" %}</th>
<td>{{ trace.search }}</td>
<th>{% trans "Advanced search" %}</th>
<td>{{ trace.field_searches }}</td>
</tr>
<tr>
<th>{% trans "Filters" %}</th>
<td>{{ trace.filters }}</td>
<th>{% trans "Ordering" %}</th>
<td>{{ trace.ordering }}</td>
</tr>
<tr>
<th>{% trans "Page" %}</th>
<td>{{ trace.page }}</td>
<th>{% trans "Number of results" %}</th>
<td>{{ trace.n_results }}</td>
</tr>
<tr>
<th>{% trans "User" %}</th>
<td colspan="3">
{{ trace.ip_address|default_if_none:'?' }}
<br/>
{{ trace.user_agent }}
{% if trace.user %}
<br/>
{{ trace.user }}
{% if trace.staff %}
({% trans "staff" %})
{% endif %}
{% endif %}
</td>
</tr>
<tr>
<th>{% trans "Config version" %}</th>
<td>{{ trace.config_version }}</td>
<th>{% trans "Request ID" %}</th>
<td>{{ trace.request_id }}</td>
</tr>
</table>
{% with trace.clicks.all as clicks %}
{% if clicks %}
<div class="card-body">
<h5>{% trans "Clicks" %}</h5>
<table class="table table-condensed">
{% for click in clicks %}
<tr>
<td>#{{ click.position }}</td>
<td>
<a href="{{ click.frbr_uri }}" target="_blank">{{ click.frbr_uri }}</a>
</td>
<td>{{ click.portion|default_if_none:"" }}</td>
<td>
+ <span title="{{ click.created_at }}">{{ trace.created_at|timesince:click.created_at }}</span>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% endwith %}
</div>
{% with trace.next_searches.all as next_searches %}
{% if next_searches %}
<div class="row">
{% for next_trace in trace.next_searches.all %}
<div class="col">
<div class="my-3 text-center h1">↓</div>
{% include 'peachjam_search/_searchtrace_card.html' with trace=next_trace n=n|add:1 %}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
10 changes: 10 additions & 0 deletions peachjam_search/templates/peachjam_search/_searchtrace_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% load i18n %}
<form action="{% url 'search:search_traces' %}" method="get">
<div class="input-group mb-3">
<input type="text"
class="form-control"
name="id"
placeholder="{% trans "Trace ID" %}"/>
<button type="submit" class="btn btn-primary">{% trans "Go" %}</button>
</div>
</form>
19 changes: 19 additions & 0 deletions peachjam_search/templates/peachjam_search/searchtrace_detail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends 'peachjam/layouts/main.html' %}
{% load i18n %}
{% block title %}
{% trans "Search trace" %} {{ trace.pk }}
{% endblock %}
{% block page-content %}
<div class="container py-3">
<div class="breadcrumbs mb-3">
<a href="{% url 'search:search_traces' %}">{% trans "Search traces" %}</a>
</div>
<div class="row mb-3">
<div class="col-md-9">
<h1>{% trans "Search trace" %}: {{ trace.id }}</h1>
</div>
<div class="col">{% include 'peachjam_search/_searchtrace_form.html' %}</div>
</div>
{% include 'peachjam_search/_searchtrace_card.html' with n=1 %}
</div>
{% endblock %}
58 changes: 58 additions & 0 deletions peachjam_search/templates/peachjam_search/searchtrace_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{% extends 'peachjam/layouts/main.html' %}
{% load i18n %}
{% block title %}
{% trans "Search traces" %}
{% endblock %}
{% block page-content %}
<div class="container py-3">
{% if messages %}
<div class="container mt-3">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show"
role="alert">
{{ message }}
<button type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
<div class="row mb-3">
<div class="col-md-9">
<h1>{% trans "Search traces" %}</h1>
</div>
<div class="col">{% include 'peachjam_search/_searchtrace_form.html' %}</div>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "Search" %}</th>
<th>{% trans "Number of Results" %}</th>
<th>{% trans "Page" %}</th>
<th>{% trans "Created At" %}</th>
</tr>
</thead>
<tbody>
{% for trace in traces %}
<tr>
<td>
<a href="{% url 'search:search_trace' pk=trace.id %}">{{ trace.id }}</a>
</td>
<td>{{ trace.search|default:trace.field_searches }}</td>
<td>{{ trace.n_results }}</td>
<td>{{ trace.page }}</td>
<td>{{ trace.created_at }}</td>
</tr>
{% empty %}
<tr>
<td colspan="5">{% trans "No search traces found." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include 'peachjam/_pagination.html' %}
</div>
{% endblock %}
4 changes: 4 additions & 0 deletions peachjam_search/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@
urlpatterns = [
path("api/", include(router.urls)),
path("", views.SearchView.as_view(), name="search"),
path("traces", views.SearchTraceListView.as_view(), name="search_traces"),
path(
"traces/<uuid:pk>", views.SearchTraceDetailView.as_view(), name="search_trace"
),
]
51 changes: 50 additions & 1 deletion peachjam_search/views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import copy

from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ValidationError
from django.http.response import JsonResponse
from django.shortcuts import redirect, reverse
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.translation import get_language_from_request
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie
from django.views.generic import TemplateView
from django.views.generic import DetailView, ListView, TemplateView
from django_elasticsearch_dsl_drf.filter_backends import (
DefaultOrderingFilterBackend,
FacetedFilterSearchFilterBackend,
Expand Down Expand Up @@ -602,3 +605,49 @@ def dispatch(self, request, *args, **kwargs):
class SearchClickViewSet(CreateModelMixin, GenericViewSet):
permission_classes = (AllowAny,)
serializer_class = SearchClickSerializer


class SearchTraceListView(PermissionRequiredMixin, ListView):
model = SearchTrace
paginate_by = 50
context_object_name = "traces"

def get(self, request, *args, **kwargs):
if request.GET.get("id"):
# try to find this trace and redirect to the detail view if it exists,
# otherwise show the list
try:
trace = SearchTrace.objects.filter(pk=request.GET["id"]).first()
if trace:
return redirect("search:search_trace", pk=trace.pk)
except ValidationError:
pass
messages.warning(request, _("Search trace not found"))
return redirect("search:search_traces")
return super().get(request, *args, **kwargs)

def has_permission(self):
return self.request.user.is_authenticated and self.request.user.is_staff


class SearchTraceDetailView(PermissionRequiredMixin, DetailView):
model = SearchTrace
queryset = SearchTrace.objects.prefetch_related("previous_search", "next_searches")
context_object_name = "trace"

def get(self, request, *args, **kwargs):
trace = self.get_object()
# walk the previous searches chain to find the first one
if trace.previous_search:
original_trace = trace
while trace.previous_search:
trace = trace.previous_search
url = (
reverse("search:search_trace", kwargs={"pk": trace.pk})
+ f"#{original_trace.pk}"
)
return redirect(url, pk=trace.pk)
return super().get(request, *args, **kwargs)

def has_permission(self):
return self.request.user.is_authenticated and self.request.user.is_staff
Loading