Compare commits
2 Commits
98c9c1faee
...
filters
Author | SHA1 | Date | |
---|---|---|---|
c2f1d8fe0a
|
|||
cd3e400297
|
@ -1,13 +1,5 @@
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
dateformat: str = "%d/%m/%Y"
|
||||
datetimeformat: str = "%d/%m/%Y %H:%M"
|
||||
timeformat: str = "%H:%M"
|
||||
durationformat: str = "%2.1H hours"
|
||||
durationformat_manual: str = "%H hours"
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
def _safe_timedelta(duration: timedelta | int | None):
|
||||
@ -78,9 +70,3 @@ def format_duration(
|
||||
rf"%\d*\.?\d*{pattern}", replacement, formatted_string
|
||||
)
|
||||
return formatted_string
|
||||
|
||||
|
||||
def local_strftime(datetime: datetime, format: str = datetimeformat) -> str:
|
||||
return timezone.localtime(datetime).strftime(format)
|
||||
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com
|
||||
! tailwindcss v3.4.7 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
@ -1782,6 +1782,10 @@ input:checked + .toggle-bg {
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
.border-b {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.border-blue-600 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(28 100 242 / var(--tw-border-opacity));
|
||||
@ -1812,11 +1816,6 @@ input:checked + .toggle-bg {
|
||||
border-color: rgb(220 215 254 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.\!bg-gray-50 {
|
||||
--tw-bg-opacity: 1 !important;
|
||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity)) !important;
|
||||
}
|
||||
|
||||
.bg-blue-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(225 239 254 / var(--tw-bg-opacity));
|
||||
@ -2693,12 +2692,6 @@ textarea:disabled:is(.dark *) {
|
||||
outline-color: #AC94FA;
|
||||
}
|
||||
|
||||
.dark\:divide-y:is(.dark *) > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-divide-y-reverse: 0;
|
||||
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
||||
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
|
||||
}
|
||||
|
||||
.dark\:divide-gray-600:is(.dark *) > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-divide-opacity: 1;
|
||||
border-color: rgb(75 85 99 / var(--tw-divide-opacity));
|
||||
@ -2728,11 +2721,6 @@ textarea:disabled:is(.dark *) {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.dark\:\!bg-gray-700:is(.dark *) {
|
||||
--tw-bg-opacity: 1 !important;
|
||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity)) !important;
|
||||
}
|
||||
|
||||
.dark\:bg-blue-200:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(195 221 253 / var(--tw-bg-opacity));
|
||||
@ -2932,16 +2920,6 @@ textarea:disabled:is(.dark *) {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.sm\:rounded-b-lg {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.sm\:rounded-t-lg {
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.sm\:px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% load param_utils %}
|
||||
<div class="shadow-md" hx-boost="false">
|
||||
<div class="relative overflow-x-auto sm:rounded-t-lg">
|
||||
<div class="shadow-md sm:rounded-lg" hx-boost="false">
|
||||
<div class="relative overflow-x-auto sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||
{% if header_action %}
|
||||
<c-table-header>
|
||||
@ -12,13 +12,13 @@
|
||||
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="dark:divide-y">
|
||||
<tbody>
|
||||
{% for row in rows %}<c-table-row :data=row />{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if page_obj and elided_page_range %}
|
||||
<nav class="flex items-center flex-column md:flex-row justify-between px-6 py-4 dark:bg-gray-900 sm:rounded-b-lg"
|
||||
<nav class="flex items-center flex-column md:flex-row justify-between px-6 py-4"
|
||||
aria-label="Table navigation">
|
||||
<span class="text-sm font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0 block w-full md:inline md:w-auto">Showing <span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.start_index }}</span>—<span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.end_index }}</span> of <span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.paginator.count }}</span></span>
|
||||
<ul class="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
|
||||
|
@ -1,4 +1,4 @@
|
||||
<tr class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 [&_a]:underline [&_a]:underline-offset-4 [&_a]:decoration-2">
|
||||
<tr class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 border-b [&_a]:underline [&_a]:underline-offset-4 [&_a]:decoration-2">
|
||||
{% if slot %}
|
||||
{{ slot }}
|
||||
{% else %}
|
||||
|
@ -5,6 +5,6 @@
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<div class="2xl:max-w-screen-2xl xl:max-w-screen-xl md:max-w-screen-md sm:max-w-screen-sm self-center">
|
||||
<c-simple-table :columns=data.columns :rows=data.rows :page_obj=page_obj :elided_page_range=elided_page_range :header_action=data.header_action />
|
||||
<c-simple-table :columns=data.columns :rows=data.rows :page_obj=page_obj :elided_page_range=elided_page_range />
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
@ -7,10 +7,10 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
|
||||
from common.time import dateformat, local_strftime
|
||||
from common.utils import A, Button
|
||||
from games.forms import DeviceForm
|
||||
from games.models import Device
|
||||
from games.views.general import dateformat
|
||||
|
||||
|
||||
@login_required
|
||||
@ -47,7 +47,7 @@ def list_devices(request: HttpRequest) -> HttpResponse:
|
||||
[
|
||||
device.name,
|
||||
device.get_type_display(),
|
||||
local_strftime(device.created_at, dateformat),
|
||||
device.created_at.strftime(dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
{
|
||||
|
@ -7,10 +7,10 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
|
||||
from common.time import dateformat, local_strftime
|
||||
from common.utils import A, Button, truncate_with_popover
|
||||
from games.forms import EditionForm
|
||||
from games.models import Edition, Game
|
||||
from games.views.general import dateformat
|
||||
|
||||
|
||||
@login_required
|
||||
@ -75,7 +75,7 @@ def list_editions(request: HttpRequest) -> HttpResponse:
|
||||
truncate_with_popover(str(edition.platform)),
|
||||
edition.year_released,
|
||||
edition.wikidata,
|
||||
local_strftime(edition.created_at, dateformat),
|
||||
edition.created_at.strftime(dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
{
|
||||
|
@ -8,18 +8,18 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
|
||||
from common.time import (
|
||||
dateformat,
|
||||
durationformat,
|
||||
durationformat_manual,
|
||||
format_duration,
|
||||
local_strftime,
|
||||
timeformat,
|
||||
)
|
||||
from common.time import format_duration
|
||||
from common.utils import A, Button, safe_division, truncate_with_popover
|
||||
from games.forms import GameForm
|
||||
from games.models import Edition, Game, Purchase, Session
|
||||
from games.views.general import use_custom_redirect
|
||||
from games.views.general import (
|
||||
dateformat,
|
||||
datetimeformat,
|
||||
durationformat,
|
||||
durationformat_manual,
|
||||
timeformat,
|
||||
use_custom_redirect,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@ -75,7 +75,7 @@ def list_games(request: HttpRequest) -> HttpResponse:
|
||||
),
|
||||
game.year_released,
|
||||
game.wikidata,
|
||||
local_strftime(game.created_at, dateformat),
|
||||
game.created_at.strftime(dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
{
|
||||
@ -175,9 +175,9 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
||||
)
|
||||
|
||||
if sessions:
|
||||
playrange_start = local_strftime(sessions.earliest().timestamp_start, "%b %Y")
|
||||
playrange_start = sessions.earliest().timestamp_start.strftime("%b %Y")
|
||||
latest_session = sessions.latest()
|
||||
playrange_end = local_strftime(latest_session.timestamp_start, "%b %Y")
|
||||
playrange_end = latest_session.timestamp_start.strftime("%b %Y")
|
||||
|
||||
playrange = (
|
||||
playrange_start
|
||||
@ -269,7 +269,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
||||
"columns": ["Date", "Duration", "Duration (manual)", "Actions"],
|
||||
"rows": [
|
||||
[
|
||||
f"{local_strftime(session.timestamp_start)}{f" — {session.timestamp_end.strftime(timeformat)}" if session.timestamp_end else ""}",
|
||||
f"{session.timestamp_start.strftime(datetimeformat)}{f" — {session.timestamp_end.strftime(timeformat)}" if session.timestamp_end else ""}",
|
||||
(
|
||||
format_duration(session.duration_calculated, durationformat)
|
||||
if session.duration_calculated
|
||||
|
@ -12,6 +12,12 @@ from common.time import format_duration
|
||||
from common.utils import safe_division
|
||||
from games.models import Edition, Game, Platform, Purchase, Session
|
||||
|
||||
dateformat: str = "%d/%m/%Y"
|
||||
datetimeformat: str = "%d/%m/%Y %H:%M"
|
||||
timeformat: str = "%H:%M"
|
||||
durationformat: str = "%2.1H hours"
|
||||
durationformat_manual: str = "%H hours"
|
||||
|
||||
|
||||
def model_counts(request: HttpRequest) -> dict[str, bool]:
|
||||
return {
|
||||
|
@ -7,11 +7,10 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
|
||||
from common.time import dateformat, local_strftime
|
||||
from common.utils import A, Button
|
||||
from games.forms import PlatformForm
|
||||
from games.models import Platform
|
||||
from games.views.general import use_custom_redirect
|
||||
from games.views.general import dateformat, use_custom_redirect
|
||||
|
||||
|
||||
@login_required
|
||||
@ -48,7 +47,7 @@ def list_platforms(request: HttpRequest) -> HttpResponse:
|
||||
[
|
||||
platform.name,
|
||||
platform.group,
|
||||
local_strftime(platform.created_at, dateformat),
|
||||
platform.created_at.strftime(dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
{
|
||||
|
@ -13,11 +13,10 @@ from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from common.time import dateformat, local_strftime
|
||||
from common.utils import A, Button, truncate_with_popover
|
||||
from games.forms import PurchaseForm
|
||||
from games.models import Edition, Purchase
|
||||
from games.views.general import use_custom_redirect
|
||||
from games.views.general import dateformat, use_custom_redirect
|
||||
|
||||
|
||||
@login_required
|
||||
@ -81,23 +80,23 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
|
||||
purchase.price,
|
||||
purchase.price_currency,
|
||||
purchase.infinite,
|
||||
local_strftime(purchase.date_purchased, dateformat),
|
||||
purchase.date_purchased.strftime(dateformat),
|
||||
(
|
||||
local_strftime(purchase.date_refunded, dateformat)
|
||||
purchase.date_refunded.strftime(dateformat)
|
||||
if purchase.date_refunded
|
||||
else "-"
|
||||
),
|
||||
(
|
||||
local_strftime(purchase.date_finished, dateformat)
|
||||
purchase.date_finished.strftime(dateformat)
|
||||
if purchase.date_finished
|
||||
else "-"
|
||||
),
|
||||
(
|
||||
local_strftime(purchase.date_dropped, dateformat)
|
||||
purchase.date_dropped.strftime(dateformat)
|
||||
if purchase.date_dropped
|
||||
else "-"
|
||||
),
|
||||
local_strftime(purchase.created_at, dateformat),
|
||||
purchase.created_at.strftime(dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
{
|
||||
|
@ -1,25 +1,299 @@
|
||||
from typing import Any
|
||||
import operator
|
||||
from functools import reduce
|
||||
from json import dumps as json_dumps
|
||||
from json import loads as json_loads
|
||||
from typing import Any, NotRequired, TypeAlias, TypedDict, TypeGuard
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.models.query_utils import Q
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
from common.time import (
|
||||
dateformat,
|
||||
durationformat,
|
||||
durationformat_manual,
|
||||
format_duration,
|
||||
local_strftime,
|
||||
timeformat,
|
||||
)
|
||||
from common.time import format_duration
|
||||
from common.utils import A, Button, truncate_with_popover
|
||||
from games.forms import SessionForm
|
||||
from games.models import Purchase, Session
|
||||
from games.views.general import use_custom_redirect
|
||||
from games.views.general import (
|
||||
dateformat,
|
||||
datetimeformat,
|
||||
durationformat,
|
||||
durationformat_manual,
|
||||
timeformat,
|
||||
use_custom_redirect,
|
||||
)
|
||||
|
||||
|
||||
class Filter(TypedDict):
|
||||
filter_id: str
|
||||
filter_display: str
|
||||
filter_string: str
|
||||
|
||||
|
||||
def is_filter(obj: dict[Any, Any]) -> TypeGuard[Filter]:
|
||||
return (
|
||||
isinstance(obj, dict)
|
||||
and "filter_id" in obj
|
||||
and isinstance(obj["filter_id"], str)
|
||||
and "filter_display" in obj
|
||||
and isinstance(obj["filter_display"], str)
|
||||
and "filter_string" in obj
|
||||
and isinstance(obj["filter_string"], str)
|
||||
)
|
||||
|
||||
|
||||
FilterList: TypeAlias = list[Filter]
|
||||
|
||||
|
||||
def is_filterlist(obj: list[Any]) -> TypeGuard[FilterList]:
|
||||
return isinstance(obj, list) and all([is_filter(item) for item in obj])
|
||||
|
||||
|
||||
ModelFilterSet: TypeAlias = list[dict[str, FilterList]]
|
||||
|
||||
|
||||
class FieldFilter(TypedDict):
|
||||
filtered_field: str
|
||||
filtered_value: str
|
||||
negated: NotRequired[bool]
|
||||
filter: Filter
|
||||
|
||||
|
||||
def is_fieldfilter(obj: dict) -> TypeGuard[FieldFilter]:
|
||||
return (
|
||||
isinstance(obj, dict)
|
||||
and "filtered_field" in obj
|
||||
and isinstance(obj["filtered_field"], str)
|
||||
and "filtered_value" in obj
|
||||
and isinstance(obj["filtered_value"], str)
|
||||
and "filter" in obj
|
||||
and is_filter(obj["filter"])
|
||||
)
|
||||
|
||||
|
||||
FilterSet: TypeAlias = list[FieldFilter]
|
||||
|
||||
|
||||
def is_filterset(obj: list) -> TypeGuard[FilterSet]:
|
||||
return isinstance(obj, list) and all([is_fieldfilter(item) for item in obj])
|
||||
|
||||
|
||||
iexact_filter: Filter = {
|
||||
"filter_id": "IEXACT",
|
||||
"filter_display": "Equals (case-insensitive)",
|
||||
"filter_string": "__iexact",
|
||||
}
|
||||
exact_filter: Filter = {
|
||||
"filter_id": "EXACT",
|
||||
"filter_display": "Equals (case-sensitive)",
|
||||
"filter_string": "__exact",
|
||||
}
|
||||
isnull_filter: Filter = {
|
||||
"filter_id": "ISNULL",
|
||||
"filter_display": "Is null",
|
||||
"filter_string": "__isnull",
|
||||
}
|
||||
contains_filter: Filter = {
|
||||
"filter_id": "CONTAINS",
|
||||
"filter_display": "Contains",
|
||||
"filter_string": "__contains",
|
||||
}
|
||||
startswith_filter: Filter = {
|
||||
"filter_id": "STARTSWITH",
|
||||
"filter_display": "Starts with",
|
||||
"filter_string": "__startswith",
|
||||
}
|
||||
endswith_filter: Filter = {
|
||||
"filter_id": "ENDSWITH",
|
||||
"filter_display": "Ends with",
|
||||
"filter_string": "__endswith",
|
||||
}
|
||||
gt_filter: Filter = {
|
||||
"filter_id": "GT",
|
||||
"filter_display": "Greater than",
|
||||
"filter_string": "__gt",
|
||||
}
|
||||
lt_filter: Filter = {
|
||||
"filter_id": "LT",
|
||||
"filter_display": "Lesser than",
|
||||
"filter_string": "__lt",
|
||||
}
|
||||
year_gt_filter: Filter = {
|
||||
"filter_id": "YEARGT",
|
||||
"filter_display": "Greater than",
|
||||
"filter_string": "__year__gt",
|
||||
}
|
||||
year_lt_filter: Filter = {
|
||||
"filter_id": "YEARLT",
|
||||
"filter_display": "Lesser than",
|
||||
"filter_string": "__year__lt",
|
||||
}
|
||||
year_exact_filter: Filter = {
|
||||
"filter_id": "YEAREXACT",
|
||||
"filter_display": "Equals (case-sensitive)",
|
||||
"filter_string": "__year__exact",
|
||||
}
|
||||
|
||||
defined_filters = [
|
||||
iexact_filter,
|
||||
exact_filter,
|
||||
isnull_filter,
|
||||
contains_filter,
|
||||
startswith_filter,
|
||||
endswith_filter,
|
||||
gt_filter,
|
||||
lt_filter,
|
||||
year_gt_filter,
|
||||
year_lt_filter,
|
||||
year_exact_filter,
|
||||
]
|
||||
|
||||
defined_filters_list = {list["filter_id"]: list for list in defined_filters}
|
||||
|
||||
char_filter: FilterList = [
|
||||
iexact_filter,
|
||||
isnull_filter,
|
||||
contains_filter,
|
||||
startswith_filter,
|
||||
endswith_filter,
|
||||
]
|
||||
text_filter: FilterList = [
|
||||
isnull_filter,
|
||||
contains_filter,
|
||||
]
|
||||
num_filter: FilterList = [exact_filter, gt_filter, lt_filter]
|
||||
date_filter: FilterList = [
|
||||
year_exact_filter,
|
||||
isnull_filter,
|
||||
year_gt_filter,
|
||||
year_lt_filter,
|
||||
]
|
||||
|
||||
conditions = ["and", "or"]
|
||||
session_filters: ModelFilterSet = [
|
||||
{"name": char_filter},
|
||||
{"timestamp_start": date_filter},
|
||||
{"timestamp_end": date_filter},
|
||||
{"duration_manual": num_filter},
|
||||
{"duration_calculated": num_filter},
|
||||
{"note": text_filter},
|
||||
{"device": char_filter},
|
||||
{"created_at": date_filter},
|
||||
{"modified_at": date_filter},
|
||||
]
|
||||
name_contains_age: FieldFilter = {
|
||||
"filtered_field": "name",
|
||||
"filtered_value": "age",
|
||||
"filter": contains_filter,
|
||||
}
|
||||
simple_example_filter: FilterSet = [name_contains_age]
|
||||
timestamp_start_year_2024: FieldFilter = {
|
||||
"filtered_field": "timestamp_start",
|
||||
"filtered_value": "2024",
|
||||
"filter": year_exact_filter,
|
||||
}
|
||||
physical_only: FieldFilter = {
|
||||
"filtered_field": "purchase__ownership_type",
|
||||
"filtered_value": "ph",
|
||||
"filter": exact_filter,
|
||||
}
|
||||
|
||||
|
||||
def negate_filter(filter: FieldFilter) -> FieldFilter:
|
||||
return {**filter, "negated": True}
|
||||
|
||||
|
||||
without_physical: FieldFilter = negate_filter(physical_only)
|
||||
combined_example_filter: FilterSet = [name_contains_age, timestamp_start_year_2024]
|
||||
combined_with_negated_example_filter = [timestamp_start_year_2024, without_physical]
|
||||
|
||||
|
||||
def string_to_dict(s: str) -> dict[str, str]:
|
||||
key, value = s.split("=")
|
||||
return {key: value}
|
||||
|
||||
|
||||
def create_django_filter_dict(
|
||||
filter: Filter, field: str, filtered_value: str
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Creates a dict that can be used with the Django
|
||||
filter function by unpacking it:
|
||||
Model.objects.filter(**return_value)
|
||||
"""
|
||||
if not is_filter(filter):
|
||||
raise ValueError("filter is not of type Filter")
|
||||
return {f"{field}{filter["filter_string"]}": filtered_value}
|
||||
|
||||
|
||||
def join_filter_with_condition(filters: FilterSet, condition: str):
|
||||
if not is_filterset(filters):
|
||||
raise ValueError("filters is not FilterSet")
|
||||
conditions = {"AND": operator.and_, "OR": operator.or_, "XOR": operator.xor}
|
||||
condition = condition.upper()
|
||||
if condition not in conditions:
|
||||
raise ValueError(f"Condition '{condition}' not one of '{conditions.keys()}'.")
|
||||
q_objects: list[Q] = []
|
||||
for filter_item in filters:
|
||||
q = Q(
|
||||
**create_django_filter_dict(
|
||||
filter_item["filter"],
|
||||
filter_item["filtered_field"],
|
||||
filter_item["filtered_value"],
|
||||
)
|
||||
)
|
||||
if filter_item.get("negated", False):
|
||||
q = ~q
|
||||
q_objects.append(q)
|
||||
return reduce(conditions[condition], q_objects)
|
||||
|
||||
|
||||
def apply_filters(
|
||||
filters: FilterSet,
|
||||
queryset: QuerySet[Any],
|
||||
) -> QuerySet[Any] | None:
|
||||
if len(filters) == 0:
|
||||
return queryset
|
||||
if type(filters) is not list:
|
||||
raise ValueError("filters argument not of type list")
|
||||
# TODO: modify FilterSet so it includes the condition to use
|
||||
# so we can remove the hard-coding of "AND" here
|
||||
return queryset.filter(join_filter_with_condition(filters, "AND"))
|
||||
|
||||
|
||||
def filters_to_string(filters: FilterSet) -> str:
|
||||
constructed_filters: list[dict[str, str | bool]] = []
|
||||
for filter in filters:
|
||||
constructed_filters.append(
|
||||
{
|
||||
"id": filter["filter"]["filter_id"],
|
||||
"field": filter["filtered_field"],
|
||||
"value": filter["filtered_value"],
|
||||
"negated": filter.get("negated", False),
|
||||
}
|
||||
)
|
||||
return json_dumps(constructed_filters)
|
||||
|
||||
|
||||
def string_to_filters(filter_string: str) -> FilterSet:
|
||||
obj = json_loads(filter_string)
|
||||
filters = [
|
||||
{
|
||||
"filter": defined_filters_list[item["id"]],
|
||||
"filtered_field": item["field"],
|
||||
"filtered_value": item["value"],
|
||||
"negated": item.get("negated", False),
|
||||
}
|
||||
for item in obj
|
||||
]
|
||||
if not is_filterset(filters):
|
||||
raise ValueError("filters is not of type FilterSet")
|
||||
return filters
|
||||
|
||||
|
||||
@login_required
|
||||
@ -27,7 +301,11 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
|
||||
context: dict[Any, Any] = {}
|
||||
page_number = request.GET.get("page", 1)
|
||||
limit = request.GET.get("limit", 10)
|
||||
filters = request.GET.get("filters", "")
|
||||
sessions = Session.objects.order_by("-timestamp_start")
|
||||
if filters != "":
|
||||
filter_obj = string_to_filters(filters)
|
||||
sessions = apply_filters(filter_obj, queryset=sessions)
|
||||
page_obj = None
|
||||
if int(limit) != 0:
|
||||
paginator = Paginator(sessions, limit)
|
||||
@ -64,7 +342,7 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
|
||||
args=[session.purchase.edition.game.pk],
|
||||
),
|
||||
),
|
||||
f"{local_strftime(session.timestamp_start)}{f" — {local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}",
|
||||
f"{session.timestamp_start.strftime(datetimeformat)}{f" — {session.timestamp_end.strftime(timeformat)}" if session.timestamp_end else ""}",
|
||||
(
|
||||
format_duration(session.duration_calculated, durationformat)
|
||||
if session.duration_calculated
|
||||
|
Reference in New Issue
Block a user