Compare commits
3 Commits
filters
...
98c9c1faee
Author | SHA1 | Date | |
---|---|---|---|
98c9c1faee
|
|||
645ffa0dad
|
|||
4358708262
|
@ -1,5 +1,13 @@
|
||||
import re
|
||||
from datetime import timedelta
|
||||
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"
|
||||
|
||||
|
||||
def _safe_timedelta(duration: timedelta | int | None):
|
||||
@ -70,3 +78,9 @@ 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,8 +1,9 @@
|
||||
from random import choices
|
||||
from string import ascii_lowercase
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
@ -31,16 +32,68 @@ HTMLAttribute = tuple[str, str]
|
||||
HTMLTag = str
|
||||
|
||||
|
||||
def A(attributes: list[HTMLAttribute], children: list[HTMLTag] | HTMLTag) -> HTMLTag:
|
||||
def Component(
|
||||
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):
|
||||
children = [children]
|
||||
childrenBlob = "\n".join(children)
|
||||
attributesList = [f'{name} = "{value}"' for name, value in attributes]
|
||||
attributesBlob = " ".join(attributesList)
|
||||
tag: str = f"<a {attributesBlob}>{childrenBlob}</a>"
|
||||
tag: str = ""
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
Divides without triggering division by zero exception.
|
||||
|
@ -1782,10 +1782,6 @@ 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));
|
||||
@ -1966,6 +1962,10 @@ input:checked + .toggle-bg {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-top {
|
||||
vertical-align: top;
|
||||
}
|
||||
@ -2693,6 +2693,12 @@ 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));
|
||||
@ -2926,6 +2932,16 @@ 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;
|
||||
@ -3075,6 +3091,10 @@ textarea:disabled:is(.dark *) {
|
||||
--tw-space-x-reverse: 1;
|
||||
}
|
||||
|
||||
.rtl\:text-left:where([dir="rtl"], [dir="rtl"] *) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.rtl\:text-right:where([dir="rtl"], [dir="rtl"] *) {
|
||||
text-align: right;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
<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">
|
||||
{{ text }}
|
||||
{{ slot }}
|
||||
</button>
|
||||
|
@ -1,19 +1,24 @@
|
||||
{% load param_utils %}
|
||||
<div class="shadow-md sm:rounded-lg" hx-boost="false">
|
||||
<div class="relative overflow-x-auto sm:rounded-lg">
|
||||
<div class="shadow-md" hx-boost="false">
|
||||
<div class="relative overflow-x-auto sm:rounded-t-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>
|
||||
{{ 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">
|
||||
<tr>
|
||||
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody class="dark:divide-y">
|
||||
{% 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"
|
||||
<nav class="flex items-center flex-column md:flex-row justify-between px-6 py-4 dark:bg-gray-900 sm:rounded-b-lg"
|
||||
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">
|
||||
|
3
games/templates/cotton/table_header.html
Normal file
3
games/templates/cotton/table_header.html
Normal file
@ -0,0 +1,3 @@
|
||||
<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>
|
@ -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 border-b [&_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 [&_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 />
|
||||
<c-simple-table :columns=data.columns :rows=data.rows :page_obj=page_obj :elided_page_range=elided_page_range :header_action=data.header_action />
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
@ -7,9 +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
|
||||
@ -35,6 +36,7 @@ def list_devices(request: HttpRequest) -> HttpResponse:
|
||||
else None
|
||||
),
|
||||
"data": {
|
||||
"header_action": A([], Button([], "Add device"), url="add_device"),
|
||||
"columns": [
|
||||
"Name",
|
||||
"Type",
|
||||
@ -45,7 +47,7 @@ def list_devices(request: HttpRequest) -> HttpResponse:
|
||||
[
|
||||
device.name,
|
||||
device.get_type_display(),
|
||||
device.created_at.strftime(dateformat),
|
||||
local_strftime(device.created_at, 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.utils import A, truncate_with_popover
|
||||
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
|
||||
@ -36,6 +36,7 @@ def list_editions(request: HttpRequest) -> HttpResponse:
|
||||
else None
|
||||
),
|
||||
"data": {
|
||||
"header_action": A([], Button([], "Add edition"), url="add_edition"),
|
||||
"columns": [
|
||||
"Game",
|
||||
"Name",
|
||||
@ -74,7 +75,7 @@ def list_editions(request: HttpRequest) -> HttpResponse:
|
||||
truncate_with_popover(str(edition.platform)),
|
||||
edition.year_released,
|
||||
edition.wikidata,
|
||||
edition.created_at.strftime(dateformat),
|
||||
local_strftime(edition.created_at, 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 format_duration
|
||||
from common.utils import A, safe_division, truncate_with_popover
|
||||
from games.forms import GameForm
|
||||
from games.models import Edition, Game, Purchase, Session
|
||||
from games.views.general import (
|
||||
from common.time import (
|
||||
dateformat,
|
||||
datetimeformat,
|
||||
durationformat,
|
||||
durationformat_manual,
|
||||
format_duration,
|
||||
local_strftime,
|
||||
timeformat,
|
||||
use_custom_redirect,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@login_required
|
||||
@ -45,6 +45,7 @@ def list_games(request: HttpRequest) -> HttpResponse:
|
||||
else None
|
||||
),
|
||||
"data": {
|
||||
"header_action": A([], Button([], "Add game"), url="add_game"),
|
||||
"columns": [
|
||||
"Name",
|
||||
"Sort Name",
|
||||
@ -74,7 +75,7 @@ def list_games(request: HttpRequest) -> HttpResponse:
|
||||
),
|
||||
game.year_released,
|
||||
game.wikidata,
|
||||
game.created_at.strftime(dateformat),
|
||||
local_strftime(game.created_at, dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
{
|
||||
@ -174,9 +175,9 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
||||
)
|
||||
|
||||
if sessions:
|
||||
playrange_start = sessions.earliest().timestamp_start.strftime("%b %Y")
|
||||
playrange_start = local_strftime(sessions.earliest().timestamp_start, "%b %Y")
|
||||
latest_session = sessions.latest()
|
||||
playrange_end = latest_session.timestamp_start.strftime("%b %Y")
|
||||
playrange_end = local_strftime(latest_session.timestamp_start, "%b %Y")
|
||||
|
||||
playrange = (
|
||||
playrange_start
|
||||
@ -268,7 +269,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
||||
"columns": ["Date", "Duration", "Duration (manual)", "Actions"],
|
||||
"rows": [
|
||||
[
|
||||
f"{session.timestamp_start.strftime(datetimeformat)}{f" — {session.timestamp_end.strftime(timeformat)}" if session.timestamp_end else ""}",
|
||||
f"{local_strftime(session.timestamp_start)}{f" — {session.timestamp_end.strftime(timeformat)}" if session.timestamp_end else ""}",
|
||||
(
|
||||
format_duration(session.duration_calculated, durationformat)
|
||||
if session.duration_calculated
|
||||
|
@ -12,12 +12,6 @@ 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,9 +7,11 @@ 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 dateformat, use_custom_redirect
|
||||
from games.views.general import use_custom_redirect
|
||||
|
||||
|
||||
@login_required
|
||||
@ -35,6 +37,7 @@ def list_platforms(request: HttpRequest) -> HttpResponse:
|
||||
else None
|
||||
),
|
||||
"data": {
|
||||
"header_action": A([], Button([], "Add platform"), url="add_platform"),
|
||||
"columns": [
|
||||
"Name",
|
||||
"Group",
|
||||
@ -45,7 +48,7 @@ def list_platforms(request: HttpRequest) -> HttpResponse:
|
||||
[
|
||||
platform.name,
|
||||
platform.group,
|
||||
platform.created_at.strftime(dateformat),
|
||||
local_strftime(platform.created_at, dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
{
|
||||
|
@ -13,10 +13,11 @@ from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from common.utils import A, truncate_with_popover
|
||||
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 dateformat, use_custom_redirect
|
||||
from games.views.general import use_custom_redirect
|
||||
|
||||
|
||||
@login_required
|
||||
@ -42,6 +43,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
|
||||
else None
|
||||
),
|
||||
"data": {
|
||||
"header_action": A([], Button([], "Add purchase"), url="add_purchase"),
|
||||
"columns": [
|
||||
"Name",
|
||||
"Type",
|
||||
@ -79,23 +81,23 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
|
||||
purchase.price,
|
||||
purchase.price_currency,
|
||||
purchase.infinite,
|
||||
purchase.date_purchased.strftime(dateformat),
|
||||
local_strftime(purchase.date_purchased, dateformat),
|
||||
(
|
||||
purchase.date_refunded.strftime(dateformat)
|
||||
local_strftime(purchase.date_refunded, dateformat)
|
||||
if purchase.date_refunded
|
||||
else "-"
|
||||
),
|
||||
(
|
||||
purchase.date_finished.strftime(dateformat)
|
||||
local_strftime(purchase.date_finished, dateformat)
|
||||
if purchase.date_finished
|
||||
else "-"
|
||||
),
|
||||
(
|
||||
purchase.date_dropped.strftime(dateformat)
|
||||
local_strftime(purchase.date_dropped, dateformat)
|
||||
if purchase.date_dropped
|
||||
else "-"
|
||||
),
|
||||
purchase.created_at.strftime(dateformat),
|
||||
local_strftime(purchase.created_at, dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
{
|
||||
|
@ -8,18 +8,18 @@ from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from common.time import format_duration
|
||||
from common.utils import A, truncate_with_popover
|
||||
from games.forms import SessionForm
|
||||
from games.models import Purchase, Session
|
||||
from games.views.general import (
|
||||
from common.time import (
|
||||
dateformat,
|
||||
datetimeformat,
|
||||
durationformat,
|
||||
durationformat_manual,
|
||||
format_duration,
|
||||
local_strftime,
|
||||
timeformat,
|
||||
use_custom_redirect,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@login_required
|
||||
@ -45,6 +45,7 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
|
||||
else None
|
||||
),
|
||||
"data": {
|
||||
"header_action": A([], Button([], "Add session"), url="add_session"),
|
||||
"columns": [
|
||||
"Name",
|
||||
"Date",
|
||||
@ -57,18 +58,13 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
|
||||
"rows": [
|
||||
[
|
||||
A(
|
||||
[
|
||||
(
|
||||
"href",
|
||||
reverse(
|
||||
"view_game",
|
||||
args=[session.purchase.edition.game.pk],
|
||||
),
|
||||
)
|
||||
],
|
||||
truncate_with_popover(session.purchase.edition.name),
|
||||
children=truncate_with_popover(session.purchase.edition.name),
|
||||
url=reverse(
|
||||
"view_game",
|
||||
args=[session.purchase.edition.game.pk],
|
||||
),
|
||||
),
|
||||
f"{session.timestamp_start.strftime(datetimeformat)}{f" — {session.timestamp_end.strftime(timeformat)}" if session.timestamp_end else ""}",
|
||||
f"{local_strftime(session.timestamp_start)}{f" — {local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}",
|
||||
(
|
||||
format_duration(session.duration_calculated, durationformat)
|
||||
if session.duration_calculated
|
||||
|
Reference in New Issue
Block a user