Add filters
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""Views for managing saved filter presets (FilterPreset model)."""
|
||||
|
||||
import json
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import SafeText, mark_safe
|
||||
|
||||
from games.models import FilterPreset
|
||||
|
||||
|
||||
@login_required
|
||||
def list_presets(request: HttpRequest) -> HttpResponse:
|
||||
"""Return a preset dropdown as an HTML fragment."""
|
||||
mode = request.GET.get("mode", "games")
|
||||
presets = FilterPreset.objects.filter(mode=mode).order_by("name")
|
||||
|
||||
items: list[str] = []
|
||||
for preset in presets:
|
||||
filter_json = (
|
||||
json.dumps(preset.object_filter) if preset.object_filter else ""
|
||||
)
|
||||
list_url = reverse(f"games:list_{mode}")
|
||||
delete_url = reverse("games:delete_preset", args=[preset.id])
|
||||
|
||||
items.append(
|
||||
f"<li>"
|
||||
f'<a href="{list_url}?filter={quote(filter_json)}" '
|
||||
f'class="flex justify-between items-center px-4 py-2 text-sm '
|
||||
f'text-heading hover:bg-neutral-secondary-medium">'
|
||||
f"<span>{preset.name}</span>"
|
||||
f'<span class="text-red-500 hover:text-red-700 cursor-pointer ml-4" '
|
||||
f'data-delete-preset="{preset.id}" '
|
||||
f'href="{delete_url}">x</span>'
|
||||
f"</a></li>"
|
||||
)
|
||||
|
||||
if not items:
|
||||
items = [
|
||||
'<li class="px-4 py-2 text-sm text-body italic">'
|
||||
"No saved presets</li>"
|
||||
]
|
||||
|
||||
return HttpResponse(
|
||||
mark_safe(f'<ul class="py-1">{"".join(items)}</ul>')
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def save_preset(request: HttpRequest) -> HttpResponse:
|
||||
"""Save the current filter as a new preset."""
|
||||
if request.method != "POST":
|
||||
return HttpResponse(status=405)
|
||||
|
||||
name = request.POST.get("name", "").strip()
|
||||
mode = request.POST.get("mode", "games")
|
||||
filter_json_str = request.POST.get("filter", "")
|
||||
|
||||
if not name:
|
||||
messages.error(request, "Preset name is required.")
|
||||
return HttpResponse(status=400)
|
||||
|
||||
object_filter: dict = {}
|
||||
if filter_json_str:
|
||||
try:
|
||||
object_filter = json.loads(filter_json_str)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
FilterPreset.objects.create(
|
||||
name=name,
|
||||
mode=mode,
|
||||
object_filter=object_filter,
|
||||
)
|
||||
messages.success(request, f'Filter preset "{name}" saved.')
|
||||
return HttpResponse(status=201)
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_preset(request: HttpRequest, preset_id: int) -> HttpResponse:
|
||||
"""Delete a saved filter preset."""
|
||||
preset = get_object_or_404(FilterPreset, id=preset_id)
|
||||
name = preset.name
|
||||
preset.delete()
|
||||
messages.success(request, f'Preset "{name}" deleted.')
|
||||
return HttpResponse(status=200)
|
||||
|
||||
|
||||
@login_required
|
||||
def load_preset(request: HttpRequest, preset_id: int) -> HttpResponse:
|
||||
"""Load a preset and redirect to the appropriate list view."""
|
||||
preset = get_object_or_404(FilterPreset, id=preset_id)
|
||||
filter_json = json.dumps(preset.object_filter) if preset.object_filter else ""
|
||||
return redirect(
|
||||
f"{reverse(f'games:list_{preset.mode}')}?filter={quote(filter_json)}"
|
||||
)
|
||||
+44
-21
@@ -18,6 +18,7 @@ from common.components import (
|
||||
Component,
|
||||
CsrfInput,
|
||||
Div,
|
||||
FilterBar,
|
||||
GameStatus,
|
||||
GameStatusSelector,
|
||||
H1,
|
||||
@@ -42,6 +43,7 @@ from common.time import (
|
||||
timeformat,
|
||||
)
|
||||
from common.utils import build_dynamic_filter, paginate, safe_division, truncate
|
||||
from games.filters import parse_game_filter
|
||||
from games.forms import GameForm
|
||||
from games.models import Game
|
||||
from games.views.general import use_custom_redirect
|
||||
@@ -51,26 +53,35 @@ from games.views.playevent import create_playevent_tabledata
|
||||
@login_required
|
||||
def list_games(request: HttpRequest, search_string: str = "") -> HttpResponse:
|
||||
games = Game.objects.order_by("-created_at")
|
||||
search_string = request.GET.get("search_string", search_string)
|
||||
if search_string != "":
|
||||
filters = [
|
||||
Q(name__icontains=search_string),
|
||||
Q(sort_name__icontains=search_string),
|
||||
Q(platform__name__icontains=search_string),
|
||||
]
|
||||
try:
|
||||
year_value = int(search_string)
|
||||
except ValueError:
|
||||
year_value = None
|
||||
if year_value:
|
||||
filters.append(Q(year_released=year_value))
|
||||
search_string_parts = search_string.split()
|
||||
# only search for status if it exactly matches and is the only word
|
||||
if len(search_string_parts) == 1:
|
||||
if search_string.title() in Game.Status.labels:
|
||||
search_status = Game.Status[search_string.upper()]
|
||||
filters.append(Q(status=search_status))
|
||||
games = games.filter(build_dynamic_filter(filters, "|"))
|
||||
|
||||
# ── Structured filter (Stash-style JSON) ──
|
||||
filter_json = request.GET.get("filter", "")
|
||||
if filter_json:
|
||||
game_filter = parse_game_filter(filter_json)
|
||||
if game_filter is not None:
|
||||
games = games.filter(game_filter.to_q())
|
||||
else:
|
||||
# ── Legacy free-text search ──
|
||||
search_string = request.GET.get("search_string", search_string)
|
||||
if search_string != "":
|
||||
filters = [
|
||||
Q(name__icontains=search_string),
|
||||
Q(sort_name__icontains=search_string),
|
||||
Q(platform__name__icontains=search_string),
|
||||
]
|
||||
try:
|
||||
year_value = int(search_string)
|
||||
except ValueError:
|
||||
year_value = None
|
||||
if year_value:
|
||||
filters.append(Q(year_released=year_value))
|
||||
search_string_parts = search_string.split()
|
||||
if len(search_string_parts) == 1:
|
||||
if search_string.title() in Game.Status.labels:
|
||||
search_status = Game.Status[search_string.upper()]
|
||||
filters.append(Q(status=search_status))
|
||||
games = games.filter(build_dynamic_filter(filters, "|"))
|
||||
|
||||
games, page_obj, elided_page_range = paginate(request, games)
|
||||
|
||||
data = {
|
||||
@@ -126,7 +137,19 @@ def list_games(request: HttpRequest, search_string: str = "") -> HttpResponse:
|
||||
elided_page_range=elided_page_range,
|
||||
request=request,
|
||||
)
|
||||
return render_page(request, content, title="Manage games")
|
||||
# Prepend the filter bar above the table
|
||||
filter_bar = FilterBar(
|
||||
filter_json=filter_json,
|
||||
preset_list_url=reverse("games:list_presets"),
|
||||
preset_save_url=reverse("games:save_preset"),
|
||||
)
|
||||
content = mark_safe(str(filter_bar) + str(content))
|
||||
return render_page(
|
||||
request,
|
||||
content,
|
||||
title="Manage games",
|
||||
scripts=ModuleScript("range_slider.js") + ModuleScript("selectable_filter.js") + ModuleScript("filter_bar.js"),
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
+24
-5
@@ -12,7 +12,7 @@ from django.views.decorators.http import require_POST
|
||||
|
||||
from django.template.defaultfilters import date as date_filter
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.utils.safestring import SafeText
|
||||
from django.utils.safestring import SafeText, mark_safe
|
||||
|
||||
from common.components import (
|
||||
A,
|
||||
@@ -95,9 +95,16 @@ def _render_purchase_row(purchase):
|
||||
|
||||
@login_required
|
||||
def list_purchases(request: HttpRequest) -> HttpResponse:
|
||||
purchases, page_obj, elided_page_range = paginate(
|
||||
request, Purchase.objects.order_by("-date_purchased", "-created_at")
|
||||
)
|
||||
purchases = Purchase.objects.order_by("-date_purchased", "-created_at")
|
||||
|
||||
filter_json = request.GET.get("filter", "")
|
||||
if filter_json:
|
||||
from games.filters import parse_purchase_filter
|
||||
pf = parse_purchase_filter(filter_json)
|
||||
if pf is not None:
|
||||
purchases = purchases.filter(pf.to_q())
|
||||
|
||||
purchases, page_obj, elided_page_range = paginate(request, purchases)
|
||||
|
||||
data = {
|
||||
"header_action": A(
|
||||
@@ -121,7 +128,19 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
|
||||
elided_page_range=elided_page_range,
|
||||
request=request,
|
||||
)
|
||||
return render_page(request, content, title="Manage purchases")
|
||||
from common.components import PurchaseFilterBar, ModuleScript
|
||||
filter_bar = PurchaseFilterBar(
|
||||
filter_json=filter_json,
|
||||
preset_list_url=reverse("games:list_presets"),
|
||||
preset_save_url=reverse("games:save_preset"),
|
||||
)
|
||||
content = mark_safe(str(filter_bar) + str(content))
|
||||
return render_page(
|
||||
request,
|
||||
content,
|
||||
title="Manage purchases",
|
||||
scripts=ModuleScript("range_slider.js") + ModuleScript("selectable_filter.js") + ModuleScript("filter_bar.js"),
|
||||
)
|
||||
|
||||
|
||||
def _purchase_additional_row() -> SafeText:
|
||||
|
||||
+33
-10
@@ -40,15 +40,25 @@ from games.models import Device, Game, Session
|
||||
def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse:
|
||||
sessions = Session.objects.order_by("-timestamp_start", "created_at")
|
||||
device_list = Device.objects.order_by("name")
|
||||
search_string = request.GET.get("search_string", search_string)
|
||||
if search_string != "":
|
||||
sessions = sessions.filter(
|
||||
Q(game__name__icontains=search_string)
|
||||
| Q(game__name__icontains=search_string)
|
||||
| Q(game__platform__name__icontains=search_string)
|
||||
| Q(device__name__icontains=search_string)
|
||||
| Q(device__type__icontains=search_string)
|
||||
)
|
||||
|
||||
# ── Structured filter (JSON) ──
|
||||
filter_json = request.GET.get("filter", "")
|
||||
if filter_json:
|
||||
from games.filters import parse_session_filter
|
||||
session_filter = parse_session_filter(filter_json)
|
||||
if session_filter is not None:
|
||||
sessions = sessions.filter(session_filter.to_q())
|
||||
else:
|
||||
# ── Legacy free-text search ──
|
||||
search_string = request.GET.get("search_string", search_string)
|
||||
if search_string != "":
|
||||
sessions = sessions.filter(
|
||||
Q(game__name__icontains=search_string)
|
||||
| Q(game__name__icontains=search_string)
|
||||
| Q(game__platform__name__icontains=search_string)
|
||||
| Q(device__name__icontains=search_string)
|
||||
| Q(device__type__icontains=search_string)
|
||||
)
|
||||
try:
|
||||
last_session = sessions.latest()
|
||||
except Session.DoesNotExist:
|
||||
@@ -157,7 +167,20 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
|
||||
elided_page_range=elided_page_range,
|
||||
request=request,
|
||||
)
|
||||
return render_page(request, content, title="Manage sessions")
|
||||
from common.components import SessionFilterBar
|
||||
filter_json = request.GET.get("filter", "")
|
||||
filter_bar = SessionFilterBar(
|
||||
filter_json=filter_json,
|
||||
preset_list_url=reverse("games:list_presets"),
|
||||
preset_save_url=reverse("games:save_preset"),
|
||||
)
|
||||
content = mark_safe(str(filter_bar) + str(content))
|
||||
return render_page(
|
||||
request,
|
||||
content,
|
||||
title="Manage sessions",
|
||||
scripts=ModuleScript("range_slider.js") + ModuleScript("selectable_filter.js") + ModuleScript("filter_bar.js"),
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
Reference in New Issue
Block a user