Files
old-saburly-wagtail-web/env/lib/python3.10/site-packages/wagtail/contrib/search_promotions/views.py
2024-08-27 20:33:44 +02:00

312 lines
11 KiB
Python

from django.core.paginator import InvalidPage, Paginator
from django.db import transaction
from django.db.models import Sum, functions
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from wagtail.admin import messages
from wagtail.admin.auth import permission_required
from wagtail.admin.forms.search import SearchForm
from wagtail.admin.modal_workflow import render_modal_workflow
from wagtail.admin.ui.tables import Column, RelatedObjectsColumn, TitleColumn
from wagtail.admin.views import generic
from wagtail.contrib.search_promotions import forms, models
from wagtail.contrib.search_promotions.models import Query, SearchPromotion
from wagtail.log_actions import log
from wagtail.permission_policies.base import ModelPermissionPolicy
from wagtail.search.utils import normalise_query_string
class SearchPromotionColumn(RelatedObjectsColumn):
cell_template_name = "wagtailsearchpromotions/search_promotion_column.html"
class IndexView(generic.IndexView):
model = Query
template_name = "wagtailsearchpromotions/index.html"
results_template_name = "wagtailsearchpromotions/index_results.html"
context_object_name = "queries"
page_title = gettext_lazy("Search Terms")
header_icon = "pick"
paginate_by = 20
permission_policy = ModelPermissionPolicy(SearchPromotion)
index_url_name = "wagtailsearchpromotions:index"
index_results_url_name = "wagtailsearchpromotions:index_results"
_show_breadcrumbs = True
search_fields = ["query_string"]
default_ordering = "query_string"
add_url_name = "wagtailsearchpromotions:add"
add_item_label = gettext_lazy("Add new promoted result")
columns = [
TitleColumn(
"query_string",
label=gettext_lazy("Search term(s)"),
width="40%",
url_name="wagtailsearchpromotions:edit",
sort_key="query_string",
),
SearchPromotionColumn(
"editors_picks",
label=gettext_lazy("Promoted results"),
width="40%",
),
Column(
"views",
label=gettext_lazy("Views (past week)"),
width="20%",
sort_key="views",
),
]
def get_base_queryset(self):
# Use a subquery to filter out the Query objects that do not have a
# SearchPromotion instead of using .filter(editors_picks__isnull=False).
# The latter would use a JOIN which would result in duplicate rows before
# the sum is calculated, causing the sum to be incorrect.
has_promotions = SearchPromotion.objects.values_list("query_id", flat=True)
queryset = self.model.objects.filter(pk__in=has_promotions)
# Prevent N+1 queries by annotating the sum instead of using the
# Query.hits property and prefetching the related editors_picks.
queryset = queryset.annotate(
views=functions.Coalesce(Sum("daily_hits__hits"), 0)
).prefetch_related("editors_picks", "editors_picks__page")
return queryset
def get_breadcrumbs_items(self):
breadcrumbs = super().get_breadcrumbs_items()
breadcrumbs[-1]["label"] = _("Promoted search results")
return breadcrumbs
def save_searchpicks(query, new_query, searchpicks_formset):
# Save
if searchpicks_formset.is_valid():
# Set sort_order
for i, form in enumerate(searchpicks_formset.ordered_forms):
form.instance.sort_order = i
# Make sure the form is marked as changed so it gets saved with the new order
form.has_changed = lambda: True
# log deleted items before saving, otherwise we lose their IDs
items_for_deletion = [
form.instance
for form in searchpicks_formset.deleted_forms
if form.instance.pk
]
with transaction.atomic():
for search_pick in items_for_deletion:
log(search_pick, "wagtail.delete")
searchpicks_formset.save()
for search_pick in searchpicks_formset.new_objects:
log(search_pick, "wagtail.create")
# If query was changed, move all search picks to the new query
if query != new_query:
searchpicks_formset.get_queryset().update(query=new_query)
# log all items in the formset as having changed
for search_pick, changed_fields in searchpicks_formset.changed_objects:
log(search_pick, "wagtail.edit")
else:
# only log objects with actual changes
for search_pick, changed_fields in searchpicks_formset.changed_objects:
if changed_fields:
log(search_pick, "wagtail.edit")
return True
else:
return False
@permission_required("wagtailsearchpromotions.add_searchpromotion")
def add(request):
if request.method == "POST":
# Get query
query_form = forms.QueryForm(request.POST)
if query_form.is_valid():
query = Query.get(query_form["query_string"].value())
# Save search picks
searchpicks_formset = forms.SearchPromotionsFormSet(
request.POST, instance=query
)
if save_searchpicks(query, query, searchpicks_formset):
for search_pick in searchpicks_formset.new_objects:
log(search_pick, "wagtail.create")
messages.success(
request,
_("Editor's picks for '%(query)s' created.") % {"query": query},
buttons=[
messages.button(
reverse("wagtailsearchpromotions:edit", args=(query.id,)),
_("Edit"),
)
],
)
return redirect("wagtailsearchpromotions:index")
else:
if len(searchpicks_formset.non_form_errors()):
# formset level error (e.g. no forms submitted)
messages.error(
request,
" ".join(
error for error in searchpicks_formset.non_form_errors()
),
)
else:
# specific errors will be displayed within form fields
messages.error(
request,
_("Recommendations have not been created due to errors"),
)
else:
searchpicks_formset = forms.SearchPromotionsFormSet()
else:
query_form = forms.QueryForm()
searchpicks_formset = forms.SearchPromotionsFormSet()
return TemplateResponse(
request,
"wagtailsearchpromotions/add.html",
{
"query_form": query_form,
"searchpicks_formset": searchpicks_formset,
"form_media": query_form.media + searchpicks_formset.media,
},
)
@permission_required("wagtailsearchpromotions.change_searchpromotion")
def edit(request, query_id):
query = get_object_or_404(Query, id=query_id)
if request.method == "POST":
# Get query
query_form = forms.QueryForm(request.POST)
# and the recommendations
searchpicks_formset = forms.SearchPromotionsFormSet(
request.POST, instance=query
)
if query_form.is_valid():
new_query = Query.get(query_form["query_string"].value())
# Save search picks
if save_searchpicks(query, new_query, searchpicks_formset):
messages.success(
request,
_("Editor's picks for '%(query)s' updated.") % {"query": new_query},
buttons=[
messages.button(
reverse("wagtailsearchpromotions:edit", args=(query.id,)),
_("Edit"),
)
],
)
return redirect("wagtailsearchpromotions:index")
else:
if len(searchpicks_formset.non_form_errors()):
messages.error(
request,
" ".join(
error for error in searchpicks_formset.non_form_errors()
),
)
# formset level error (e.g. no forms submitted)
else:
messages.error(
request, _("Recommendations have not been saved due to errors")
)
# specific errors will be displayed within form fields
else:
query_form = forms.QueryForm(initial={"query_string": query.query_string})
searchpicks_formset = forms.SearchPromotionsFormSet(instance=query)
return TemplateResponse(
request,
"wagtailsearchpromotions/edit.html",
{
"query_form": query_form,
"searchpicks_formset": searchpicks_formset,
"query": query,
"form_media": query_form.media + searchpicks_formset.media,
},
)
@permission_required("wagtailsearchpromotions.delete_searchpromotion")
def delete(request, query_id):
query = get_object_or_404(Query, id=query_id)
if request.method == "POST":
editors_picks = query.editors_picks.all()
with transaction.atomic():
for search_pick in editors_picks:
log(search_pick, "wagtail.delete")
editors_picks.delete()
messages.success(request, _("Editor's picks deleted."))
return redirect("wagtailsearchpromotions:index")
return TemplateResponse(
request,
"wagtailsearchpromotions/confirm_delete.html",
{
"query": query,
},
)
def chooser(request, get_results=False):
# Get most popular queries
queries = models.Query.get_most_popular()
# If searching, filter results by query string
if "q" in request.GET:
searchform = SearchForm(request.GET)
if searchform.is_valid():
query_string = searchform.cleaned_data["q"]
queries = queries.filter(
query_string__icontains=normalise_query_string(query_string)
)
else:
searchform = SearchForm()
paginator = Paginator(queries, per_page=10)
try:
queries = paginator.page(request.GET.get("p", 1))
except InvalidPage:
raise Http404
# Render
if get_results:
return TemplateResponse(
request,
"wagtailsearchpromotions/queries/chooser/results.html",
{
"queries": queries,
},
)
else:
return render_modal_workflow(
request,
"wagtailsearchpromotions/queries/chooser/chooser.html",
None,
{
"queries": queries,
"searchform": searchform,
},
json_data={"step": "chooser"},
)
def chooserresults(request):
return chooser(request, get_results=True)