Compare commits
No commits in common. "d98758f7515d1f5657a345b1cc8851a88b2127ef" and "c2b940956270c23d8cc8bf344f33b9c25b17653c" have entirely different histories.
d98758f751
...
c2b9409562
|
@ -1,9 +1,8 @@
|
||||||
from random import choices
|
from random import choices
|
||||||
from string import ascii_lowercase
|
from string import ascii_lowercase
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
|
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import NoReverseMatch, reverse
|
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,68 +31,16 @@ HTMLAttribute = tuple[str, str]
|
||||||
HTMLTag = str
|
HTMLTag = str
|
||||||
|
|
||||||
|
|
||||||
def Component(
|
def A(attributes: list[HTMLAttribute], children: list[HTMLTag] | HTMLTag) -> HTMLTag:
|
||||||
attributes: list[HTMLAttribute] = [],
|
|
||||||
children: list[HTMLTag] | HTMLTag = [],
|
|
||||||
template: str = "",
|
|
||||||
tag_name: str = "",
|
|
||||||
) -> HTMLTag:
|
|
||||||
if not tag_name and not template:
|
|
||||||
raise ValueError("One of template or tag_name is required.")
|
|
||||||
if isinstance(children, str):
|
if isinstance(children, str):
|
||||||
children = [children]
|
children = [children]
|
||||||
childrenBlob = "\n".join(children)
|
childrenBlob = "\n".join(children)
|
||||||
attributesList = [f'{name} = "{value}"' for name, value in attributes]
|
attributesList = [f'{name} = "{value}"' for name, value in attributes]
|
||||||
attributesBlob = " ".join(attributesList)
|
attributesBlob = " ".join(attributesList)
|
||||||
tag: str = ""
|
tag: str = f"<a {attributesBlob}>{childrenBlob}</a>"
|
||||||
if tag_name != "":
|
|
||||||
tag = f"<a {attributesBlob}>{childrenBlob}</a>"
|
|
||||||
elif template != "":
|
|
||||||
tag = render_to_string(
|
|
||||||
template,
|
|
||||||
{name: value for name, value in attributes} | {"slot": "\n".join(children)},
|
|
||||||
)
|
|
||||||
return mark_safe(tag)
|
return mark_safe(tag)
|
||||||
|
|
||||||
|
|
||||||
def A(
|
|
||||||
attributes: list[HTMLAttribute] = [],
|
|
||||||
children: list[HTMLTag] | HTMLTag = [],
|
|
||||||
url: str | Callable[..., Any] = "",
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Returns the HTML tag "a".
|
|
||||||
"url" can either be:
|
|
||||||
- URL (string)
|
|
||||||
- path name passed to reverse() (string)
|
|
||||||
- function
|
|
||||||
"""
|
|
||||||
additional_attributes = []
|
|
||||||
if url:
|
|
||||||
if type(url) is str:
|
|
||||||
try:
|
|
||||||
url_result = reverse(url)
|
|
||||||
except NoReverseMatch:
|
|
||||||
url_result = url
|
|
||||||
elif callable(url):
|
|
||||||
url_result = url()
|
|
||||||
else:
|
|
||||||
raise TypeError("'url' is neither str nor function.")
|
|
||||||
additional_attributes = [("href", url_result)]
|
|
||||||
return Component(
|
|
||||||
tag_name="a", attributes=attributes + additional_attributes, children=children
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def Button(
|
|
||||||
attributes: list[HTMLAttribute] = [],
|
|
||||||
children: list[HTMLTag] | HTMLTag = [],
|
|
||||||
):
|
|
||||||
return Component(
|
|
||||||
template="cotton/button.html", attributes=attributes, children=children
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def safe_division(numerator: int | float, denominator: int | float) -> int | float:
|
def safe_division(numerator: int | float, denominator: int | float) -> int | float:
|
||||||
"""
|
"""
|
||||||
Divides without triggering division by zero exception.
|
Divides without triggering division by zero exception.
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 mt-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 mt-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
{{ text }}
|
{{ text }}
|
||||||
{{ slot }}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -2,11 +2,6 @@
|
||||||
<div class="shadow-md sm:rounded-lg" hx-boost="false">
|
<div class="shadow-md sm:rounded-lg" hx-boost="false">
|
||||||
<div class="relative overflow-x-auto sm:rounded-lg">
|
<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">
|
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||||
{% if header_action %}
|
|
||||||
<c-table-header>
|
|
||||||
{{ header_action }}
|
|
||||||
</c-table-header>
|
|
||||||
{% endif %}
|
|
||||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
|
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<caption class="p-2 text-lg font-semibold rtl:text-left text-right text-gray-900 bg-white dark:text-white dark:bg-gray-900">
|
|
||||||
{{ slot }}
|
|
||||||
</caption>
|
|
|
@ -5,6 +5,6 @@
|
||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="2xl:max-w-screen-2xl md:max-w-screen-md sm:max-w-screen-sm self-center">
|
<div class="2xl:max-w-screen-2xl 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>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
@ -7,7 +7,6 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from common.utils import A, Button
|
|
||||||
from games.forms import DeviceForm
|
from games.forms import DeviceForm
|
||||||
from games.models import Device
|
from games.models import Device
|
||||||
from games.views.general import dateformat
|
from games.views.general import dateformat
|
||||||
|
@ -36,7 +35,6 @@ def list_devices(request: HttpRequest) -> HttpResponse:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"data": {
|
"data": {
|
||||||
"header_action": A([], Button([], "Add device"), url="add_device"),
|
|
||||||
"columns": [
|
"columns": [
|
||||||
"Name",
|
"Name",
|
||||||
"Type",
|
"Type",
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from common.utils import A, Button, truncate_with_popover
|
from common.utils import A, truncate_with_popover
|
||||||
from games.forms import EditionForm
|
from games.forms import EditionForm
|
||||||
from games.models import Edition, Game
|
from games.models import Edition, Game
|
||||||
from games.views.general import dateformat
|
from games.views.general import dateformat
|
||||||
|
@ -36,7 +36,6 @@ def list_editions(request: HttpRequest) -> HttpResponse:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"data": {
|
"data": {
|
||||||
"header_action": A([], Button([], "Add edition"), url="add_edition"),
|
|
||||||
"columns": [
|
"columns": [
|
||||||
"Game",
|
"Game",
|
||||||
"Name",
|
"Name",
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from common.time import format_duration
|
from common.time import format_duration
|
||||||
from common.utils import A, Button, safe_division, truncate_with_popover
|
from common.utils import A, safe_division, truncate_with_popover
|
||||||
from games.forms import GameForm
|
from games.forms import GameForm
|
||||||
from games.models import Edition, Game, Purchase, Session
|
from games.models import Edition, Game, Purchase, Session
|
||||||
from games.views.general import (
|
from games.views.general import (
|
||||||
|
@ -45,7 +45,6 @@ def list_games(request: HttpRequest) -> HttpResponse:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"data": {
|
"data": {
|
||||||
"header_action": A([], Button([], "Add game"), url="add_game"),
|
|
||||||
"columns": [
|
"columns": [
|
||||||
"Name",
|
"Name",
|
||||||
"Sort Name",
|
"Sort Name",
|
||||||
|
|
|
@ -7,7 +7,6 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from common.utils import A, Button
|
|
||||||
from games.forms import PlatformForm
|
from games.forms import PlatformForm
|
||||||
from games.models import Platform
|
from games.models import Platform
|
||||||
from games.views.general import dateformat, use_custom_redirect
|
from games.views.general import dateformat, use_custom_redirect
|
||||||
|
@ -36,7 +35,6 @@ def list_platforms(request: HttpRequest) -> HttpResponse:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"data": {
|
"data": {
|
||||||
"header_action": A([], Button([], "Add platform"), url="add_platform"),
|
|
||||||
"columns": [
|
"columns": [
|
||||||
"Name",
|
"Name",
|
||||||
"Group",
|
"Group",
|
||||||
|
|
|
@ -13,7 +13,7 @@ from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from common.utils import A, Button, truncate_with_popover
|
from common.utils import A, truncate_with_popover
|
||||||
from games.forms import PurchaseForm
|
from games.forms import PurchaseForm
|
||||||
from games.models import Edition, Purchase
|
from games.models import Edition, Purchase
|
||||||
from games.views.general import dateformat, use_custom_redirect
|
from games.views.general import dateformat, use_custom_redirect
|
||||||
|
@ -42,7 +42,6 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"data": {
|
"data": {
|
||||||
"header_action": A([], Button([], "Add purchase"), url="add_purchase"),
|
|
||||||
"columns": [
|
"columns": [
|
||||||
"Name",
|
"Name",
|
||||||
"Platform",
|
"Platform",
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
import operator
|
from typing import Any
|
||||||
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.contrib.auth.decorators import login_required
|
||||||
from django.core.paginator import Paginator
|
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.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from typing_extensions import TypeGuard
|
|
||||||
|
|
||||||
from common.time import format_duration
|
from common.time import format_duration
|
||||||
from common.utils import A, Button, truncate_with_popover
|
from common.utils import A, truncate_with_popover
|
||||||
from games.forms import SessionForm
|
from games.forms import SessionForm
|
||||||
from games.models import Purchase, Session
|
from games.models import Purchase, Session
|
||||||
from games.views.general import (
|
from games.views.general import (
|
||||||
|
@ -29,283 +22,12 @@ from games.views.general import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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
|
@login_required
|
||||||
def list_sessions(request: HttpRequest) -> HttpResponse:
|
def list_sessions(request: HttpRequest) -> HttpResponse:
|
||||||
context: dict[Any, Any] = {}
|
context: dict[Any, Any] = {}
|
||||||
page_number = request.GET.get("page", 1)
|
page_number = request.GET.get("page", 1)
|
||||||
limit = request.GET.get("limit", 10)
|
limit = request.GET.get("limit", 10)
|
||||||
filters = request.GET.get("filters", "")
|
|
||||||
sessions = Session.objects.order_by("-timestamp_start")
|
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
|
page_obj = None
|
||||||
if int(limit) != 0:
|
if int(limit) != 0:
|
||||||
paginator = Paginator(sessions, limit)
|
paginator = Paginator(sessions, limit)
|
||||||
|
@ -323,7 +45,6 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"data": {
|
"data": {
|
||||||
"header_action": A([], Button([], "Add session"), url="add_session"),
|
|
||||||
"columns": [
|
"columns": [
|
||||||
"Name",
|
"Name",
|
||||||
"Date",
|
"Date",
|
||||||
|
@ -336,11 +57,16 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
|
||||||
"rows": [
|
"rows": [
|
||||||
[
|
[
|
||||||
A(
|
A(
|
||||||
children=truncate_with_popover(session.purchase.edition.name),
|
[
|
||||||
url=reverse(
|
(
|
||||||
"view_game",
|
"href",
|
||||||
args=[session.purchase.edition.game.pk],
|
reverse(
|
||||||
),
|
"view_game",
|
||||||
|
args=[session.purchase.edition.game.pk],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
truncate_with_popover(session.purchase.edition.name),
|
||||||
),
|
),
|
||||||
f"{session.timestamp_start.strftime(datetimeformat)}{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 ""}",
|
||||||
(
|
(
|
||||||
|
|
Loading…
Reference in New Issue