2 Commits

Author SHA1 Message Date
c2f1d8fe0a add backend functionality
All checks were successful
Django CI/CD / test (push) Successful in 50s
Django CI/CD / build-and-push (push) Has been skipped
2024-09-03 15:30:51 +02:00
cd3e400297 add links to add a new X to: game, edition, purchase, session, device, platform
All checks were successful
Django CI/CD / test (push) Successful in 52s
Django CI/CD / build-and-push (push) Successful in 2m5s
2024-09-03 15:27:04 +02:00
38 changed files with 754 additions and 1173 deletions

View File

@ -1,117 +0,0 @@
from random import choices as random_choices
from string import ascii_lowercase
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
HTMLAttribute = tuple[str, str | int | bool]
HTMLTag = str
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 = ""
if tag_name != "":
tag = f"<a {attributesBlob}>{childrenBlob}</a>"
elif template != "":
tag = render_to_string(
template,
{name: value for name, value in attributes}
| {"slot": mark_safe("\n".join(children))},
)
return mark_safe(tag)
def randomid(seed: str = "", length: int = 10) -> str:
return seed + "".join(random_choices(ascii_lowercase, k=length))
def Popover(
popover_content: str,
wrapped_content: str = "",
children: list[HTMLTag] = [],
attributes: list[HTMLAttribute] = [],
) -> str:
if not wrapped_content and not children:
raise ValueError("One of wrapped_content or children is required.")
id = randomid()
return Component(
attributes=attributes
+ [
("id", id),
("wrapped_content", wrapped_content),
("popover_content", popover_content),
],
children=children,
template="cotton/popover.html",
)
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 = [],
size: str = "base",
icon: bool = False,
color: str = "blue",
):
return Component(
template="cotton/button.html",
attributes=attributes + [("size", size), ("icon", icon), ("color", color)],
children=children,
)
def Div(
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
):
return Component(tag_name="div", attributes=attributes, children=children)
def Icon(
name: str,
attributes: list[HTMLAttribute] = [],
):
return Component(template=f"cotton/icon/{name}.html", attributes=attributes)

View File

@ -1,15 +1,5 @@
import re
from datetime import date, datetime, timedelta
from django.utils import timezone
from common.utils import generate_split_ranges
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):
@ -80,86 +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)
def daterange(start: date, end: date, end_inclusive: bool = False) -> list[date]:
time_between: timedelta = end - start
if (days_between := time_between.days) < 1:
raise ValueError("start and end have to be at least 1 day apart.")
if end_inclusive:
print(f"{end_inclusive=}")
print(f"{days_between=}")
days_between += 1
print(f"{days_between=}")
return [start + timedelta(x) for x in range(days_between)]
def streak(datelist: list[date]) -> dict[str, int | tuple[date, date]]:
if len(datelist) == 1:
return {"days": 1, "dates": (datelist[0], datelist[0])}
else:
print(f"Processing {len(datelist)} dates.")
missing = sorted(
set(
datelist[0] + timedelta(x)
for x in range((datelist[-1] - datelist[0]).days)
)
- set(datelist)
)
print(f"{len(missing)} days missing.")
datelist_with_missing = sorted(datelist + missing)
ranges = list(generate_split_ranges(datelist_with_missing, missing))
print(f"{len(ranges)} ranges calculated.")
longest_consecutive_days = timedelta(0)
longest_range: tuple[date, date] = (date(1970, 1, 1), date(1970, 1, 1))
for start, end in ranges:
if (current_streak := end - start) > longest_consecutive_days:
longest_consecutive_days = current_streak
longest_range = (start, end)
return {"days": longest_consecutive_days.days + 1, "dates": longest_range}
def streak_bruteforce(datelist: list[date]) -> dict[str, int | tuple[date, date]]:
if (datelist_length := len(datelist)) == 0:
raise ValueError("Number of dates in the list is 0.")
datelist.sort()
current_streak = 1
current_start = datelist[0]
current_end = datelist[0]
current_date = datelist[0]
highest_streak = 1
highest_streak_daterange = (current_start, current_end)
def update_highest_streak():
nonlocal highest_streak, highest_streak_daterange
if current_streak > highest_streak:
highest_streak = current_streak
highest_streak_daterange = (current_start, current_end)
def reset_streak():
nonlocal current_start, current_end, current_streak
current_start = current_end = current_date
current_streak = 1
def increment_streak():
nonlocal current_end, current_streak
current_end = current_date
current_streak += 1
for i, datelist_item in enumerate(datelist, start=1):
current_date = datelist_item
if current_date == current_start or current_date == current_end:
continue
if current_date - timedelta(1) != current_end and i != datelist_length:
update_highest_streak()
reset_streak()
elif current_date - timedelta(1) == current_end and i == datelist_length:
increment_streak()
update_highest_streak()
else:
increment_streak()
return {"days": highest_streak, "dates": highest_streak_daterange}

View File

@ -1,7 +1,97 @@
from datetime import date
from typing import Any, Generator, TypeVar
from random import choices
from string import ascii_lowercase
from typing import Any, Callable
from common.components import Popover
from django.template.loader import render_to_string
from django.urls import NoReverseMatch, reverse
from django.utils.safestring import mark_safe
def Popover(
wrapped_content: str,
popover_content: str = "",
) -> str:
id = randomid()
if popover_content == "":
popover_content = wrapped_content
content = f"<span data-popover-target={id}>{wrapped_content}</span>"
result = mark_safe(
str(content)
+ render_to_string(
"cotton/popover.html",
{
"id": id,
"slot": popover_content,
},
)
)
return result
HTMLAttribute = tuple[str, str]
HTMLTag = str
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 = ""
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:
@ -46,26 +136,12 @@ def truncate(input_string: str, length: int = 30, ellipsis: str = "…") -> str:
def truncate_with_popover(input_string: str) -> str:
if (truncated := truncate(input_string)) != input_string:
print(f"Not the same after: {truncated=}")
return Popover(wrapped_content=truncated, popover_content=input_string)
else:
print("Strings are the same!")
return input_string
T = TypeVar("T", str, int, date)
def generate_split_ranges(
value_list: list[T], split_points: list[T]
) -> Generator[tuple[T, T], None, None]:
for x in range(0, len(split_points) + 1):
if x == 0:
start = 0
elif x >= len(split_points):
start = value_list.index(split_points[x - 1]) + 1
else:
start = value_list.index(split_points[x - 1]) + 1
try:
end = value_list.index(split_points[x])
except IndexError:
end = len(value_list)
yield (value_list[start], value_list[end - 1])
def randomid(seed: str = "", length: int = 10) -> str:
return seed + "".join(choices(ascii_lowercase, k=length))

View File

@ -1,5 +1,5 @@
/*
! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com
! tailwindcss v3.4.7 | MIT License | https://tailwindcss.com
*/
/*
@ -1346,10 +1346,6 @@ input:checked + .toggle-bg {
z-index: 50;
}
.m-4 {
margin: 1rem;
}
.mx-2 {
margin-left: 0.5rem;
margin-right: 0.5rem;
@ -1539,8 +1535,8 @@ input:checked + .toggle-bg {
width: 100%;
}
.max-w-\[30rem\] {
max-width: 30rem;
.max-w-80 {
max-width: 20rem;
}
.max-w-screen-lg {
@ -1738,8 +1734,8 @@ input:checked + .toggle-bg {
white-space: nowrap;
}
.text-balance {
text-wrap: balance;
.text-wrap {
text-wrap: wrap;
}
.rounded {
@ -1786,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));
@ -1816,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));
@ -1865,16 +1860,6 @@ input:checked + .toggle-bg {
background-color: rgb(5 122 85 / var(--tw-bg-opacity));
}
.bg-green-700 {
--tw-bg-opacity: 1;
background-color: rgb(4 108 78 / var(--tw-bg-opacity));
}
.bg-red-700 {
--tw-bg-opacity: 1;
background-color: rgb(200 30 30 / var(--tw-bg-opacity));
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@ -1955,11 +1940,6 @@ input:checked + .toggle-bg {
padding-bottom: 0.75rem;
}
.py-3\.5 {
padding-top: 0.875rem;
padding-bottom: 0.875rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
@ -2086,11 +2066,6 @@ input:checked + .toggle-bg {
letter-spacing: -0.025em;
}
.text-black {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
}
.text-blue-600 {
--tw-text-opacity: 1;
color: rgb(28 100 242 / var(--tw-text-opacity));
@ -2553,11 +2528,6 @@ textarea:disabled:is(.dark *) {
background-color: rgb(4 108 78 / var(--tw-bg-opacity));
}
.hover\:bg-green-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(3 84 63 / var(--tw-bg-opacity));
}
.hover\:bg-red-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(253 232 232 / var(--tw-bg-opacity));
@ -2568,11 +2538,6 @@ textarea:disabled:is(.dark *) {
background-color: rgb(240 82 82 / var(--tw-bg-opacity));
}
.hover\:bg-red-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(155 28 28 / var(--tw-bg-opacity));
}
.hover\:bg-white:hover {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@ -2644,21 +2609,11 @@ textarea:disabled:is(.dark *) {
--tw-ring-color: rgb(26 86 219 / var(--tw-ring-opacity));
}
.focus\:ring-gray-100:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(243 244 246 / var(--tw-ring-opacity));
}
.focus\:ring-gray-200:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(229 231 235 / var(--tw-ring-opacity));
}
.focus\:ring-green-300:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(132 225 188 / var(--tw-ring-opacity));
}
.focus\:ring-green-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(14 159 110 / var(--tw-ring-opacity));
@ -2669,11 +2624,6 @@ textarea:disabled:is(.dark *) {
--tw-ring-color: rgb(4 108 78 / var(--tw-ring-opacity));
}
.focus\:ring-red-300:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(248 180 180 / var(--tw-ring-opacity));
}
.focus\:ring-offset-2:focus {
--tw-ring-offset-width: 2px;
}
@ -2742,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));
@ -2777,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));
@ -2820,21 +2759,11 @@ textarea:disabled:is(.dark *) {
background-color: rgb(17 24 39 / 0.8);
}
.dark\:bg-green-600:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(5 122 85 / var(--tw-bg-opacity));
}
.dark\:bg-purple-800:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(85 33 181 / var(--tw-bg-opacity));
}
.dark\:bg-red-600:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(224 36 36 / var(--tw-bg-opacity));
}
.dark\:text-blue-500:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(63 131 248 / var(--tw-text-opacity));
@ -2925,11 +2854,6 @@ textarea:disabled:is(.dark *) {
background-color: rgb(5 122 85 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-green-700:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(4 108 78 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-red-700:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(200 30 30 / var(--tw-bg-opacity));
@ -2970,26 +2894,11 @@ textarea:disabled:is(.dark *) {
--tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-gray-700:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(55 65 81 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-green-500:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(14 159 110 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-green-800:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(3 84 63 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-red-900:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(119 29 29 / var(--tw-ring-opacity));
}
@media (min-width: 640px) {
.sm\:table-cell {
display: table-cell;
@ -3011,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;
@ -3188,10 +3087,6 @@ textarea:disabled:is(.dark *) {
border-end-end-radius: 0.5rem;
}
.\[\&_\:last-child\]\:text-right :last-child {
text-align: right;
}
.\[\&_a\]\:underline a {
text-decoration-line: underline;
}
@ -3203,7 +3098,3 @@ textarea:disabled:is(.dark *) {
.\[\&_a\]\:underline-offset-4 a {
text-underline-offset: 4px;
}
.\[\&_h1\]\:mb-2 h1 {
margin-bottom: 0.5rem;
}

View File

@ -18,11 +18,11 @@
<script src="https://cdn.jsdelivr.net/npm/flowbite@2.4.1/dist/flowbite.min.js"></script>
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark')
}
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark')
}
</script>
</head>
<body hx-indicator="#indicator">
@ -45,46 +45,46 @@
{% block scripts %}
{% endblock scripts %}
<script>
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
// Change the icons inside the button based on previous settings
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
var themeToggleBtn = document.getElementById('theme-toggle');
var themeToggleBtn = document.getElementById('theme-toggle');
themeToggleBtn.addEventListener('click', function () {
themeToggleBtn.addEventListener('click', function () {
// toggle icons inside button
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
// if set via local storage previously
if (localStorage.getItem('color-theme')) {
if (localStorage.getItem('color-theme') === 'light') {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
}
// if NOT set via local storage previously
if (localStorage.getItem('color-theme')) {
if (localStorage.getItem('color-theme') === 'light') {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
} else {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
}
});
// if NOT set via local storage previously
} else {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
});
</script>
</body>
</html>

View File

@ -1,6 +1,5 @@
<c-vars color="blue" size="base" />
<button type="button"
title="{{ title }}"
class=" {% if color == "blue" %} bg-blue-700 dark:bg-blue-600 dark:focus:ring-blue-800 dark:hover:bg-blue-700 focus:ring-blue-300 hover:bg-blue-800 text-white {% elif color == "red" %} bg-red-700 dark:bg-red-600 dark:focus:ring-red-900 dark:hover:bg-red-700 focus:ring-red-300 hover:bg-red-800 text-white {% elif color == "gray" %} bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-600 dark:focus:ring-gray-700 dark:hover:bg-gray-700 dark:hover:text-white dark:text-gray-400 focus:ring-gray-100 hover:bg-gray-100 hover:text-blue-700 text-gray-900 border {% elif color == "green" %} bg-green-700 dark:bg-green-600 dark:focus:ring-green-800 dark:hover:bg-green-700 focus:ring-green-300 hover:bg-green-800 text-white {% endif %} focus:outline-none focus:ring-4 font-medium mb-2 me-2 rounded-lg {% if size == "xs" %} px-3 py-2 text-xs {% elif size == "sm" %} px-3 py-2 text-sm {% elif size == "base" %} px-5 py-2.5 text-sm {% elif size == "lg" %} px-5 py-3 text-base {% elif size == "xl" %} px-6 py-3.5 text-base {% endif %} {% if icon %} inline-flex text-center items-center gap-2 {% else %} {% endif %} ">
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>

View File

@ -1,8 +0,0 @@
<div class="inline-flex rounded-md shadow-sm" role="group">
{% if slot %}{{ slot }}{% endif %}
{% for button in buttons %}
{% if button.slot %}
<c-button-group-button-sm :href=button.href :slot=button.slot :color=button.color :hover=button.hover :title=button.title />
{% endif %}
{% endfor %}
</div>

View File

@ -3,21 +3,18 @@
class="[&:first-of-type_button]:rounded-s-lg [&:last-of-type_button]:rounded-e-lg">
{% if color == "gray" %}
<button type="button"
title="{{ title }}"
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white">
{{ slot }}
{{ text }}
</button>
{% elif color == "red" %}
<button type="button"
title="{{ title }}"
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-red-500 hover:text-white focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:border-red-700 dark:hover:bg-red-700 dark:focus:ring-blue-500 dark:focus:text-white">
{{ slot }}
{{ text }}
</button>
{% elif color == "green" %}
<button type="button"
title="{{ title }}"
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-green-500 hover:border-green-600 hover:text-white focus:z-10 focus:ring-2 focus:ring-green-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:border-green-700 dark:hover:bg-green-600 dark:focus:ring-green-500 dark:focus:text-white">
{{ slot }}
{{ text }}
</button>
{% endif %}
</a>

View File

@ -0,0 +1,6 @@
<c-vars color="gray" />
<div class="inline-flex rounded-md shadow-sm" role="group">
{% for button in buttons %}
<c-button-group-button-sm :href=button.href :text=button.text :color=button.color />
{% endfor %}
</div>

View File

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 43.470703 8.9863281 A 1.50015 1.50015 0 0 0 42.439453 9.4394531 L 16.5 35.378906 L 5.5605469 24.439453 A 1.50015 1.50015 0 1 0 3.4394531 26.560547 L 15.439453 38.560547 A 1.50015 1.50015 0 0 0 17.560547 38.560547 L 44.560547 11.560547 A 1.50015 1.50015 0 0 0 43.470703 8.9863281 z">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 477 B

View File

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 24 4 C 20.491685 4 17.570396 6.6214322 17.080078 10 L 10.238281 10 A 1.50015 1.50015 0 0 0 9.9804688 9.9785156 A 1.50015 1.50015 0 0 0 9.7578125 10 L 6.5 10 A 1.50015 1.50015 0 1 0 6.5 13 L 8.6386719 13 L 11.15625 39.029297 C 11.427329 41.835926 13.811782 44 16.630859 44 L 31.367188 44 C 34.186411 44 36.570826 41.836168 36.841797 39.029297 L 39.361328 13 L 41.5 13 A 1.50015 1.50015 0 1 0 41.5 10 L 38.244141 10 A 1.50015 1.50015 0 0 0 37.763672 10 L 30.919922 10 C 30.429604 6.6214322 27.508315 4 24 4 z M 24 7 C 25.879156 7 27.420767 8.2681608 27.861328 10 L 20.138672 10 C 20.579233 8.2681608 22.120844 7 24 7 z M 11.650391 13 L 36.347656 13 L 33.855469 38.740234 C 33.730439 40.035363 32.667963 41 31.367188 41 L 16.630859 41 C 15.331937 41 14.267499 40.033606 14.142578 38.740234 L 11.650391 13 z M 20.476562 17.978516 A 1.50015 1.50015 0 0 0 19 19.5 L 19 34.5 A 1.50015 1.50015 0 1 0 22 34.5 L 22 19.5 A 1.50015 1.50015 0 0 0 20.476562 17.978516 z M 27.476562 17.978516 A 1.50015 1.50015 0 0 0 26 19.5 L 26 34.5 A 1.50015 1.50015 0 1 0 29 34.5 L 29 19.5 A 1.50015 1.50015 0 0 0 27.476562 17.978516 z">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 40.5 6 C 40.11625 6 39.732453 6.1464531 39.439453 6.4394531 L 21.462891 24.417969 L 20 28 L 23.582031 26.537109 L 41.560547 8.5605469 C 42.145547 7.9745469 42.145547 7.0254531 41.560547 6.4394531 C 41.267547 6.1464531 40.88375 6 40.5 6 z M 12.5 7 C 9.4802259 7 7 9.4802259 7 12.5 L 7 35.5 C 7 38.519774 9.4802259 41 12.5 41 L 35.5 41 C 38.519774 41 41 38.519774 41 35.5 L 41 18.5 A 1.50015 1.50015 0 1 0 38 18.5 L 38 35.5 C 38 36.898226 36.898226 38 35.5 38 L 12.5 38 C 11.101774 38 10 36.898226 10 35.5 L 10 12.5 C 10 11.101774 11.101774 10 12.5 10 L 29.5 10 A 1.50015 1.50015 0 1 0 29.5 7 L 12.5 7 z">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 798 B

View File

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 24 5.0507812 C 22.945045 5.0507812 21.890232 5.4877258 21.160156 6.3613281 L 7.0566406 23.257812 C 5.2226244 25.45627 6.8812774 29 9.7441406 29 L 38.255859 29 C 41.119312 29 42.778426 25.453888 40.943359 23.255859 L 26.839844 6.3613281 C 26.109768 5.4877258 25.054955 5.0507812 24 5.0507812 z M 24 8.015625 C 24.194045 8.015625 24.387185 8.105759 24.537109 8.2851562 L 38.638672 25.179688 C 38.977605 25.585659 38.784407 26 38.255859 26 L 9.7441406 26 C 9.2150038 26 9.0213443 25.58723 9.3613281 25.179688 L 23.462891 8.2851562 C 23.612815 8.1057586 23.805955 8.015625 24 8.015625 z M 10.5 33 C 8.0324991 33 6 35.032499 6 37.5 L 6 38.5 C 6 40.967501 8.0324991 43 10.5 43 L 37.5 43 C 39.967501 43 42 40.967501 42 38.5 L 42 37.5 C 42 35.032499 39.967501 33 37.5 33 L 10.5 33 z M 10.5 36 L 37.5 36 C 38.346499 36 39 36.653501 39 37.5 L 39 38.5 C 39 39.346499 38.346499 40 37.5 40 L 10.5 40 C 9.6535009 40 9 39.346499 9 38.5 L 9 37.5 C 9 36.653501 9.6535009 36 10.5 36 z">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 35.5 6 C 33.585045 6 32 7.5850452 32 9.5 L 32 19.365234 L 11.339844 6.6074219 C 9.0734225 5.2081236 6 6.9228749 6 9.5859375 L 6 38.414062 C 6 41.077126 9.0734225 42.791876 11.339844 41.392578 L 32 28.634766 L 32 38.5 C 32 40.414955 33.585045 42 35.5 42 L 38.5 42 C 40.414955 42 42 40.414955 42 38.5 L 42 9.5 C 42 7.5850452 40.414955 6 38.5 6 L 35.5 6 z M 35.5 9 L 38.5 9 C 38.795045 9 39 9.2049548 39 9.5 L 39 38.5 C 39 38.795045 38.795045 39 38.5 39 L 35.5 39 C 35.204955 39 35 38.795045 35 38.5 L 35 9.5 C 35 9.2049548 35.204955 9 35.5 9 z M 9.4765625 9.0566406 C 9.5668015 9.0647233 9.6637771 9.0984812 9.7636719 9.1601562 L 32 22.892578 L 32 25.107422 L 9.7636719 38.839844 C 9.364093 39.086546 9 38.883001 9 38.414062 L 9 9.5859375 C 9 9.3514688 9.091623 9.1841053 9.2324219 9.1054688 C 9.3028213 9.0661502 9.3863235 9.048558 9.4765625 9.0566406 z">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 7.4765625 4.9785156 A 1.50015 1.50015 0 0 0 6 6.5 L 6 31.5 L 6 43.5 A 1.50015 1.50015 0 1 0 9 43.5 L 9 33 L 40.5 33 C 41.329 33 42 32.328 42 31.5 L 42 6.5 C 42 5.672 41.329 5 40.5 5 L 7.7460938 5 A 1.50015 1.50015 0 0 0 7.4765625 4.9785156 z M 16 8 L 24 8 L 24 15 L 32 15 L 32 8 L 39 8 L 39 15 L 32 15 L 32 23 L 39 23 L 39 30 L 32 30 L 32 23 L 24 23 L 24 30 L 16 30 L 16 23 L 9 23 L 9 15 L 16 15 L 16 8 z M 16 15 L 16 23 L 24 23 L 24 15 L 16 15 z">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 643 B

View File

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 11.396484 4.1113281 C 9.1042001 4.2020187 7 6.0721788 7 8.5917969 L 7 39.408203 C 7 42.767694 10.742758 44.971891 13.681641 43.34375 L 41.490234 27.935547 C 44.513674 26.260259 44.513674 21.739741 41.490234 20.064453 L 13.681641 4.65625 C 12.94692 4.2492148 12.160579 4.0810979 11.396484 4.1113281 z M 11.431641 7.0664062 C 11.690234 7.0652962 11.961284 7.1323321 12.226562 7.2792969 L 40.037109 22.6875 C 41.13567 23.296212 41.13567 24.703788 40.037109 25.3125 L 12.226562 40.720703 C 11.165446 41.308562 10 40.620712 10 39.408203 L 10 8.5917969 C 10 7.9855423 10.290709 7.5116121 10.714844 7.2617188 C 10.926911 7.136772 11.173048 7.0675163 11.431641 7.0664062 z">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 861 B

View File

@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="50"
height="50"
viewBox="0 0 48 48"
class="w-4 h-4">
<path fill="currentColor" d="M 23.976562 4.9785156 A 1.50015 1.50015 0 0 0 22.5 6.5 L 22.5 22.5 L 6.5 22.5 A 1.50015 1.50015 0 1 0 6.5 25.5 L 22.5 25.5 L 22.5 41.5 A 1.50015 1.50015 0 1 0 25.5 41.5 L 25.5 25.5 L 41.5 25.5 A 1.50015 1.50015 0 1 0 41.5 22.5 L 25.5 22.5 L 25.5 6.5 A 1.50015 1.50015 0 0 0 23.976562 4.9785156 z">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 496 B

View File

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 26 3 C 20.494917 3 16 7.494921 16 13 C 16 18.505079 20.494917 23 26 23 C 31.505083 23 36 18.505079 36 13 C 36 7.494921 31.505083 3 26 3 z M 26 6 C 29.883764 6 33 9.1162385 33 13 C 33 16.883762 29.883764 20 26 20 C 22.116236 20 19 16.883762 19 13 C 19 9.1162385 22.116236 6 26 6 z M 24.75 9 C 24.273 9 23.862531 9.3366875 23.769531 9.8046875 L 23.269531 12.304688 C 23.210531 12.598688 23.286562 12.903766 23.476562 13.134766 C 23.666562 13.366766 23.95 13.5 24.25 13.5 L 26.25 13.5 C 26.664 13.5 27 13.836 27 14.25 C 27 14.765 26.481 15 26 15 C 25.115 15 24.583922 14.685156 24.544922 14.660156 C 24.085922 14.363156 23.472969 14.489313 23.167969 14.945312 C 22.861969 15.405313 22.986313 16.026031 23.445312 16.332031 C 23.548313 16.400031 24.491 17 26 17 C 27.71 17 29 15.818 29 14.25 C 29 12.733 27.767 11.5 26.25 11.5 L 25.470703 11.5 L 25.570312 11 L 27.5 11 C 28.052 11 28.5 10.552 28.5 10 C 28.5 9.448 28.052 9 27.5 9 L 24.75 9 z M 41.613281 22.019531 C 40.493082 22.029231 39.429184 22.473279 38.484375 23.175781 C 37.470126 23.929243 34.418425 26.208042 31.777344 28.179688 C 31.204622 26.351508 29.505924 25 27.5 25 L 23.107422 25 C 20.296203 25 18.985532 24.772226 17.859375 24.533203 C 16.733218 24.29418 15.646783 24 13.826172 24 C 9.9413941 24 7.0123317 26.492986 5.09375 28.791016 C 3.1751683 31.089045 2.1347656 33.384766 2.1347656 33.384766 A 1.5002787 1.5002787 0 1 0 4.8652344 34.628906 C 4.8652344 34.628906 5.7643161 32.669814 7.3964844 30.714844 C 9.0286527 28.759873 11.25995 27 13.826172 27 C 15.347561 27 16.006735 27.20582 17.236328 27.466797 C 18.465921 27.727774 20.123641 28 23.107422 28 L 27.5 28 C 28.346499 28 29 28.653501 29 29.5 C 29 29.969499 28.794195 30.374296 28.470703 30.646484 C 28.470416 30.646699 28.429688 30.677734 28.429688 30.677734 A 1.5001988 1.5001988 0 0 0 28.345703 30.748047 A 1.5001988 1.5001988 0 0 0 28.34375 30.75 C 28.105295 30.908613 27.816466 31 27.5 31 L 20.5 31 A 1.50015 1.50015 0 1 0 20.5 34 L 27.5 34 C 28.440637 34 29.315307 33.701451 30.041016 33.199219 C 30.042186 33.198409 30.043753 33.198076 30.044922 33.197266 A 1.5001988 1.5001988 0 0 0 30.224609 33.082031 C 30.224609 33.082031 38.775959 26.696426 40.273438 25.583984 A 1.50015 1.50015 0 0 0 40.273438 25.582031 C 40.837627 25.162534 41.309824 25.022381 41.640625 25.019531 C 41.971426 25.016631 42.218287 25.096901 42.560547 25.439453 C 43.150922 26.029324 43.147391 26.935102 42.572266 27.533203 C 37.217133 32.036197 33.848465 35.036886 31.623047 36.794922 C 29.369881 38.574924 28.424996 39 27.5 39 C 23.847885 39 19.80067 38 15.5 38 C 13 38 11.242781 39.343609 10.300781 40.599609 C 9.3587815 41.855609 9.0449219 43.136719 9.0449219 43.136719 A 1.50015 1.50015 0 1 0 11.955078 43.863281 C 11.955078 43.863281 12.141219 43.144391 12.699219 42.400391 C 13.257219 41.656391 14 41 15.5 41 C 19.30733 41 23.336115 42 27.5 42 C 29.402004 42 31.084837 41.044435 33.482422 39.150391 C 35.849764 37.280238 39.175413 34.30991 44.498047 29.833984 A 1.50015 1.50015 0 0 0 44.681641 29.681641 C 44.688541 29.674741 44.690436 29.665143 44.697266 29.658203 L 44.701172 29.662109 L 44.753906 29.607422 A 1.50015 1.50015 0 0 0 45.083984 29.074219 C 46.330485 27.321792 46.248887 24.884269 44.681641 23.318359 C 43.8529 22.488911 42.73348 22.009881 41.613281 22.019531 z">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,8 +1,7 @@
<span data-popover-target={{ id }} class="{{ class }}">{{ wrapped_content|default:slot }}</span>
<div data-popover
id="{{ id }}"
role="tooltip"
class="absolute z-10 invisible inline-block text-sm text-white transition-opacity duration-300 bg-white border border-purple-200 rounded-lg shadow-sm opacity-0 dark:text-white dark:border-purple-600 dark:bg-purple-800">
<div class="px-3 py-2">{{ popover_content }}</div>
<div class="px-3 py-2">{{ slot }}</div>
<div data-popper-arrow></div>
</div>

View File

@ -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">

View File

@ -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 [&_:last-child]:text-right">
<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 %}

View File

@ -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 %}

View File

@ -1,155 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block title %}
{{ title }}
{% endblock title %}
{% block content %}
<div class="flex self-center m-4 flex-col gap-4 [&_h1]:mb-2">
<div>
<h1 class="text-white text-lg">No size</h1>
<c-button>
No attributes
</c-button>
<c-button color="blue">
No attributes, blue
</c-button>
<c-button color="red">
No attributes, red
</c-button>
<c-button color="green">
No attributes, green
</c-button>
<c-button color="gray">
No attributes, gray
</c-button>
</div>
<div>
<h1 class="text-white text-lg">No size, icons</h1>
<c-button>
<c-icon.edit />
</c-button>
<c-button>
<c-icon.finish />
</c-button>
<c-button>
<c-icon.end />
</c-button>
<c-button>
<c-icon.delete />
</c-button>
<c-button>
<c-icon.play />
</c-button>
</div>
<div>
<h1 class="text-white text-lg">Extra Small, icons</h1>
<c-button icon="true" size="xs">
<c-icon.edit /> Edit
</c-button>
<c-button icon="true" size="xs">
<c-icon.finish />
</c-button>
<c-button icon="true" size="xs">
<c-icon.end />
</c-button>
<c-button icon="true" size="xs">
<c-icon.delete />
</c-button>
<c-button icon="true" size="xs">
<c-icon.play />
</c-button>
</div>
<div>
<h1 class="text-white text-lg">Small, icons</h1>
<c-button icon="true" size="sm">
<c-icon.edit /> Edit
</c-button>
<c-button icon="true" size="sm">
<c-icon.finish />
</c-button>
<c-button icon="true" size="sm">
<c-icon.end />
</c-button>
<c-button icon="true" size="sm">
<c-icon.delete />
</c-button>
<c-button icon="true" size="sm">
<c-icon.play />
</c-button>
</div>
<div>
<h1 class="text-white text-lg">Base, icons</h1>
<c-button icon="true" size="base">
<c-icon.edit /> Edit
</c-button>
<c-button icon="true" size="base">
<c-icon.finish />
</c-button>
<c-button icon="true" size="base">
<c-icon.end />
</c-button>
<c-button icon="true" size="base">
<c-icon.delete />
</c-button>
<c-button icon="true" size="base">
<c-icon.play />
</c-button>
</div>
<div>
<h1 class="text-white text-lg">Large, icons</h1>
<c-button icon="true" size="lg">
<c-icon.edit /> Edit
</c-button>
<c-button icon="true" size="lg">
<c-icon.finish />
</c-button>
<c-button icon="true" size="lg">
<c-icon.end />
</c-button>
<c-button icon="true" size="lg">
<c-icon.delete />
</c-button>
<c-button icon="true" size="lg">
<c-icon.play />
</c-button>
</div>
<div>
<h1 class="text-white text-lg">Extra Large, icons</h1>
<c-button icon="true" size="xl">
<c-icon.edit /> Edit
</c-button>
<c-button icon="true" size="xl">
<c-icon.finish />
</c-button>
<c-button icon="true" size="xl">
<c-icon.end />
</c-button>
<c-button icon="true" size="xl">
<c-icon.delete />
</c-button>
<c-button icon="true" size="xl">
<c-icon.play />
</c-button>
</div>
<div>
<h1 class="text-white text-lg">Group (sm)</h1>
<c-button-group>
<c-button-group-button-sm>
No attributes
</c-button-group-button-sm>
<c-button-group-button-sm color="blue">
No attributes, blue
</c-button-group-button-sm>
<c-button-group-button-sm color="red">
No attributes, red
</c-button-group-button-sm>
<c-button-group-button-sm color="green">
No attributes, green
</c-button-group-button-sm>
<c-button-group-button-sm color="gray">
No attributes, gray
</c-button-group-button-sm>
</c-button-group>
</div>
</div>
{% endblock content %}

View File

@ -9,7 +9,7 @@
{{ purchase.name }} ({{ purchase.edition.name }} {{ purchase.get_type_display }})
</c-gamelink>
{% else %}
<c-gamelink :game_id=purchase.edition.game.id :name=purchase.edition.name />
<c-gamelink :game_id=purchase.edition.game.id name=purchase.edition.name />
{% endif %}
{% endpartialdef %}
{% block content %}

View File

@ -8,12 +8,15 @@
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
<div id="game-info" class="mb-10">
<div class="flex gap-5 mb-3">
<span class="text-balance max-w-[30rem] text-4xl">
<span class="font-bold font-serif">{{ game.name }}</span>&nbsp;<c-popover id="popover-year" popover_content="Original release year" class="text-slate-500 text-2xl">{{ game.year_released }}</c-popover>
</span>
</div>
<div class="flex gap-4 dark:text-slate-400 mb-3">
<c-popover id="popover-hours" popover_content="Total hours played" class="flex gap-2 items-center">
<span class="text-wrap max-w-80 text-4xl">
<span class="font-bold font-serif">{{ game.name }}</span>&nbsp;<span data-popover-target="popover-year" class="text-slate-500 text-2xl">{{ game.year_released }}</span>
<c-popover id="popover-year">
Original release year
</c-popover>
</span>
</div>
<div class="flex gap-4 dark:text-slate-400 mb-3">
<span data-popover-target="popover-hours" class="flex gap-2 items-center">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@ -23,8 +26,12 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
{{ hours_sum }}
<c-popover id="popover-hours">
Total hours played
</c-popover>
<c-popover id="popover-sessions" popover_content="Number of sessions" class="flex gap-2 items-center">
</span>
<span data-popover-target="popover-sessions"
class="flex gap-2 items-center">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@ -34,8 +41,11 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5-3.9 19.5m-2.1-19.5-3.9 19.5" />
</svg>
{{ session_count }}
<c-popover id="popover-sessions">
Number of sessions
</c-popover>
<c-popover id="popover-average" popover_content="Average playtime per session" class="flex gap-2 items-center">
</span>
<span data-popover-target="popover-average" class="flex gap-2 items-center">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@ -45,8 +55,12 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 14.25v2.25m3-4.5v4.5m3-6.75v6.75m3-9v9M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z" />
</svg>
{{ session_average_without_manual }}
<c-popover id="popover-average">
Average playtime per session
</c-popover>
<c-popover id="popover-playrange" popover_content="Earliest and latest dates played" class="flex gap-2 items-center">
</span>
<span data-popover-target="popover-playrange"
class="flex gap-2 items-center">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@ -56,39 +70,42 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z" />
</svg>
{{ playrange }}
<c-popover id="popover-playrange">
Earliest and latest dates played
</c-popover>
</div>
<div class="inline-flex rounded-md shadow-sm mb-3" role="group">
<a href="{% url 'edit_game' game.id %}">
<button type="button"
class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white">
Edit
</button>
</a>
<a href="{% url 'delete_game' game.id %}">
<button type="button"
class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-e-lg hover:bg-red-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-red-700 dark:focus:ring-blue-500 dark:focus:text-white">
Delete
</button>
</a>
</div>
</span>
</div>
<c-h1 :badge=edition_count>Editions</c-h1>
<div class="mb-6">
<c-simple-table :rows=edition_data.rows :columns=edition_data.columns />
</div>
<div class="mb-6">
<c-h1 :badge=purchase_count>Purchases</c-h1>
<c-simple-table :rows=purchase_data.rows :columns=purchase_data.columns />
</div>
<div class="mb-6">
<c-h1 :badge=session_count>Sessions</c-h1>
<c-simple-table :rows=session_data.rows :columns=session_data.columns :header_action=session_data.header_action :page_obj=session_page_obj :elided_page_range=session_elided_page_range />
<div class="inline-flex rounded-md shadow-sm mb-3" role="group">
<a href="{% url 'edit_game' game.id %}">
<button type="button"
class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white">
Edit
</button>
</a>
<a href="{% url 'delete_game' game.id %}">
<button type="button"
class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-e-lg hover:bg-red-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-red-700 dark:focus:ring-blue-500 dark:focus:text-white">
Delete
</button>
</a>
</div>
</div>
<script>
<c-h1 :badge=edition_count>Editions</c-h1>
<div class="mb-6">
<c-simple-table :rows=edition_data.rows :columns=edition_data.columns />
</div>
<div class="mb-6">
<c-h1 :badge=purchase_count>Purchases</c-h1>
<c-simple-table :rows=purchase_data.rows :columns=purchase_data.columns />
</div>
<div class="mb-6">
<c-h1 :badge=session_count>Sessions</c-h1>
<c-simple-table :rows=session_data.rows :columns=session_data.columns :page_obj=session_page_obj :elided_page_range=session_elided_page_range />
</div>
</div>
<script>
function getSessionCount() {
return document.getElementById('session-count').textContent.match("[0-9]+");
}
</script>
{% endblock content %}
</script>
{% endblock content %}

View File

@ -44,31 +44,16 @@ urlpatterns = [
purchase.edit_purchase,
name="edit_purchase",
),
path(
"purchase/<int:purchase_id>/drop",
purchase.drop_purchase,
name="drop_purchase",
),
path(
"purchase/<int:purchase_id>/delete",
purchase.delete_purchase,
name="delete_purchase",
),
path(
"purchase/<int:purchase_id>/finish",
purchase.finish_purchase,
name="finish_purchase",
),
path(
"purchase/list",
purchase.list_purchases,
name="list_purchases",
),
path(
"purchase/<int:purchase_id>/refund",
purchase.refund_purchase,
name="refund_purchase",
),
path(
"purchase/related-purchase-by-edition",
purchase.related_purchase_by_edition,

View File

@ -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.components import A, Button, Icon
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,19 +47,19 @@ 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.html",
"cotton/button_group_sm.html",
{
"buttons": [
{
"href": reverse("edit_device", args=[device.pk]),
"slot": Icon("edit"),
"text": "Edit",
"color": "gray",
},
{
"href": reverse("delete_device", args=[device.pk]),
"slot": Icon("delete"),
"text": "Delete",
"color": "red",
},
]

View File

@ -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.components import A, Button, Icon
from common.time import dateformat, local_strftime
from common.utils import truncate_with_popover
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
@ -76,21 +75,21 @@ 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.html",
"cotton/button_group_sm.html",
{
"buttons": [
{
"href": reverse("edit_edition", args=[edition.pk]),
"slot": Icon("edit"),
"text": "Edit",
"color": "gray",
},
{
"href": reverse(
"delete_edition", args=[edition.pk]
),
"slot": Icon("delete"),
"text": "Delete",
"color": "red",
},
]

View File

@ -8,19 +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.components import A, Button, Div, Icon, Popover
from common.time import (
dateformat,
durationformat,
durationformat_manual,
format_duration,
local_strftime,
timeformat,
)
from common.utils import safe_division, truncate, truncate_with_popover
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
@ -76,19 +75,19 @@ 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.html",
"cotton/button_group_sm.html",
{
"buttons": [
{
"href": reverse("edit_game", args=[game.pk]),
"slot": Icon("edit"),
"text": "Edit",
"color": "gray",
},
{
"href": reverse("delete_game", args=[game.pk]),
"slot": Icon("delete"),
"text": "Delete",
"color": "red",
},
]
@ -176,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
@ -207,17 +206,17 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
edition.platform,
edition.year_released,
render_to_string(
"cotton/button_group.html",
"cotton/button_group_sm.html",
{
"buttons": [
{
"href": reverse("edit_edition", args=[edition.pk]),
"slot": Icon("edit"),
"text": "Edit",
"color": "gray",
},
{
"href": reverse("delete_edition", args=[edition.pk]),
"slot": Icon("delete"),
"text": "Delete",
"color": "red",
},
]
@ -229,25 +228,24 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
}
purchase_data: dict[str, Any] = {
"columns": ["Name", "Type", "Date", "Price", "Actions"],
"columns": ["Name", "Type", "Price", "Actions"],
"rows": [
[
purchase.name if purchase.name else purchase.edition.name,
purchase.get_type_display(),
purchase.date_purchased.strftime(dateformat),
f"{purchase.price} {purchase.price_currency}",
render_to_string(
"cotton/button_group.html",
"cotton/button_group_sm.html",
{
"buttons": [
{
"href": reverse("edit_purchase", args=[purchase.pk]),
"slot": Icon("edit"),
"text": "Edit",
"color": "gray",
},
{
"href": reverse("delete_purchase", args=[purchase.pk]),
"slot": Icon("delete"),
"text": "Delete",
"color": "red",
},
]
@ -261,7 +259,6 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
sessions_all = Session.objects.filter(purchase__edition__game=game).order_by(
"-timestamp_start"
)
last_session = sessions_all.latest()
session_count = sessions_all.count()
session_paginator = Paginator(sessions_all, 5)
page_number = request.GET.get("page", 1)
@ -269,74 +266,32 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
sessions = session_page_obj.object_list
session_data: dict[str, Any] = {
"header_action": Div(
children=[
A(
url="add_session",
children=Button(
icon=True,
size="xs",
children=[Icon("play"), "LOG"],
),
),
A(
url=reverse(
"list_sessions_start_session_from_session",
args=[last_session.pk],
),
children=Popover(
popover_content=last_session.purchase.edition.name,
children=[
Button(
icon=True,
color="gray",
size="xs",
children=[
Icon("play"),
truncate(f"{last_session.purchase.edition.name}"),
],
)
],
),
),
],
),
"columns": ["Date", "Duration", "Actions"],
"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
else f"{format_duration(session.duration_manual, durationformat_manual)}*"
else "-"
),
(
format_duration(session.duration_manual, durationformat_manual)
if session.duration_manual
else "-"
),
render_to_string(
"cotton/button_group.html",
"cotton/button_group_sm.html",
{
"buttons": [
{
"href": reverse(
"list_sessions_end_session", args=[session.pk]
),
"slot": Icon("end"),
"title": "Finish session now",
"color": "green",
"hover": "green",
}
if session.timestamp_end is None
# this only works without leaving an empty
# a element and wrong rounding of button edges
# because we check if button.href is not None
# in the button group component
else {},
{
"href": reverse("edit_session", args=[session.pk]),
"slot": Icon("edit"),
"text": "Edit",
"color": "gray",
},
{
"href": reverse("delete_session", args=[session.pk]),
"slot": Icon("delete"),
"text": "Delete",
"color": "red",
},
]

View File

@ -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 {

View File

@ -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.components import A, Button, Icon
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,23 +47,23 @@ 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.html",
"cotton/button_group_sm.html",
{
"buttons": [
{
"href": reverse(
"edit_platform", args=[platform.pk]
),
"slot": Icon("edit"),
"text": "Edit",
"color": "gray",
},
{
"href": reverse(
"delete_platform", args=[platform.pk]
),
"slot": Icon("delete"),
"text": "Delete",
"color": "red",
},
]

View File

@ -13,12 +13,10 @@ from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from common.components import A, Button, Icon
from common.time import dateformat
from common.utils import truncate_with_popover
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
@ -100,50 +98,21 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
),
purchase.created_at.strftime(dateformat),
render_to_string(
"cotton/button_group.html",
"cotton/button_group_sm.html",
{
"buttons": [
{
"href": reverse(
"finish_purchase", args=[purchase.pk]
),
"slot": Icon("checkmark"),
"title": "Mark as finished",
}
if not purchase.date_finished
else {},
{
"href": reverse(
"drop_purchase", args=[purchase.pk]
),
"slot": Icon("eject"),
"title": "Mark as dropped",
}
if not purchase.date_dropped
else {},
{
"href": reverse(
"refund_purchase", args=[purchase.pk]
),
"slot": Icon("refund"),
"title": "Mark as refunded",
}
if not purchase.date_refunded
else {},
{
"href": reverse(
"edit_purchase", args=[purchase.pk]
),
"slot": Icon("edit"),
"title": "Edit",
"text": "Edit",
"color": "gray",
},
{
"href": reverse(
"delete_purchase", args=[purchase.pk]
),
"slot": Icon("delete"),
"title": "Delete",
"text": "Delete",
"color": "red",
},
]
@ -213,31 +182,7 @@ def edit_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
def delete_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
purchase = get_object_or_404(Purchase, id=purchase_id)
purchase.delete()
return redirect("list_purchases")
@login_required
def drop_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
purchase = get_object_or_404(Purchase, id=purchase_id)
purchase.date_dropped = timezone.now()
purchase.save()
return redirect("list_purchases")
@login_required
def refund_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
purchase = get_object_or_404(Purchase, id=purchase_id)
purchase.date_refunded = timezone.now()
purchase.save()
return redirect("list_purchases")
@login_required
def finish_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
purchase = get_object_or_404(Purchase, id=purchase_id)
purchase.date_finished = timezone.now()
purchase.save()
return redirect("list_purchases")
return redirect("list_sessions")
def related_purchase_by_edition(request: HttpRequest) -> HttpResponse:

View File

@ -1,26 +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.components import A, Button, Div, Icon, Popover
from common.time import (
dateformat,
durationformat,
durationformat_manual,
format_duration,
local_strftime,
timeformat,
)
from common.utils import truncate, truncate_with_popover
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
@ -28,8 +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")
last_session = sessions.latest()
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)
@ -47,44 +323,12 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
else None
),
"data": {
"header_action": Div(
children=[
A(
url="add_session",
children=Button(
icon=True,
size="xs",
children=[Icon("play"), "LOG"],
),
),
A(
url=reverse(
"list_sessions_start_session_from_session",
args=[last_session.pk],
),
children=Popover(
popover_content=last_session.purchase.edition.name,
children=[
Button(
icon=True,
color="gray",
size="xs",
children=[
Icon("play"),
truncate(
f"{last_session.purchase.edition.name}"
),
],
)
],
),
),
],
),
"header_action": A([], Button([], "Add session"), url="add_session"),
"columns": [
"Name",
"Date",
"Duration",
"Duration (manual)",
"Device",
"Created",
"Actions",
@ -98,48 +342,34 @@ 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
else f"{format_duration(session.duration_manual, durationformat_manual)}*"
else "-"
),
(
format_duration(session.duration_manual, durationformat_manual)
if session.duration_manual
else "-"
),
session.device,
session.created_at.strftime(dateformat),
render_to_string(
"cotton/button_group.html",
"cotton/button_group_sm.html",
{
"buttons": [
{
"href": reverse(
"list_sessions_end_session", args=[session.pk]
),
"slot": Icon("end"),
"title": "Finish session now",
"color": "green",
"hover": "green",
}
if session.timestamp_end is None
# this only works without leaving an empty
# a element and wrong rounding of button edges
# because we check if button.href is not None
# in the button group component
else {},
{
"href": reverse("edit_session", args=[session.pk]),
"slot": Icon("edit"),
"title": "Edit",
# "color": "gray",
"hover": "green",
"text": "Edit",
"color": "gray",
},
{
"href": reverse(
"delete_session", args=[session.pk]
),
"slot": Icon("delete"),
"title": "Delete",
"text": "Delete",
"color": "red",
"hover": "red",
},
]
},
@ -209,6 +439,7 @@ def clone_session_by_id(session_id: int) -> Session:
@login_required
@use_custom_redirect
def new_session_from_existing_session(
request: HttpRequest, session_id: int, template: str = ""
) -> HttpResponse:
@ -223,6 +454,7 @@ def new_session_from_existing_session(
@login_required
@use_custom_redirect
def end_session(
request: HttpRequest, session_id: int, template: str = ""
) -> HttpResponse:

326
poetry.lock generated
View File

@ -113,13 +113,13 @@ files = [
[[package]]
name = "django"
version = "5.1.1"
version = "5.1"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.10"
files = [
{file = "Django-5.1.1-py3-none-any.whl", hash = "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f"},
{file = "Django-5.1.1.tar.gz", hash = "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2"},
{file = "Django-5.1-py3-none-any.whl", hash = "sha256:d3b811bf5371a26def053d7ee42a9df1267ef7622323fe70a601936725aa4557"},
{file = "Django-5.1.tar.gz", hash = "sha256:848a5980e8efb76eea70872fb0e4bc5e371619c70fffbe48e3e1b50b2c09455d"},
]
[package.dependencies]
@ -133,13 +133,13 @@ bcrypt = ["bcrypt"]
[[package]]
name = "django-cotton"
version = "0.9.37"
version = "0.9.34"
description = "Bringing component based design to Django templates."
optional = false
python-versions = "<4,>=3.8"
files = [
{file = "django_cotton-0.9.37-py3-none-any.whl", hash = "sha256:b917102bee417eb749df25eae75a985db0749fb8ac9711ab57fb5f78de03d8e8"},
{file = "django_cotton-0.9.37.tar.gz", hash = "sha256:0ccfc76d6edfc8dfaf0bb5d960c6695fff78cbcc1a3c52e263a0484b8dbbb9a5"},
{file = "django_cotton-0.9.34-py3-none-any.whl", hash = "sha256:9721dd79066d5a28eefb84527bea7f2daa8f1db6111ffdecbc5dd64fe2e300c9"},
{file = "django_cotton-0.9.34.tar.gz", hash = "sha256:3f2d950a9ad0985955ca0fb2d5fbb42be1f07f55239864fe5a1d0e873303f0bd"},
]
[package.dependencies]
@ -191,13 +191,13 @@ django = ">=3.2"
[[package]]
name = "django-template-partials"
version = "24.4"
version = "24.2"
description = "django-template-partials"
optional = false
python-versions = "*"
files = [
{file = "django_template_partials-24.4-py2.py3-none-any.whl", hash = "sha256:ee59d3839385d7f648907c3fa8d5923fcd66cd8090f141fe2a1c338b917984e2"},
{file = "django_template_partials-24.4.tar.gz", hash = "sha256:25b67301470fc274ecc419e5e5fd4686a5020b1c038fd241a70eb087809034b6"},
{file = "django-template-partials-24.2.tar.gz", hash = "sha256:e594063faec6d012df3a4170edf3c5dcc07c82b1c311aa42a1a5838493fd3a72"},
{file = "django_template_partials-24.2-py2.py3-none-any.whl", hash = "sha256:b859072e6b3cd780743399bf5e9cee8be1c56c88844425a4e669a65c136205b2"},
]
[package.dependencies]
@ -222,27 +222,27 @@ dev = ["nox", "pre-commit"]
[[package]]
name = "djlint"
version = "1.35.2"
version = "1.34.1"
description = "HTML Template Linter and Formatter"
optional = false
python-versions = "<4.0,>=3.8"
python-versions = ">=3.8.0,<4.0.0"
files = [
{file = "djlint-1.35.2-py3-none-any.whl", hash = "sha256:4ba995bad378f2afa77c8ea56ba1c14429d9ff26a18e8ae23bc71eedb9152243"},
{file = "djlint-1.35.2.tar.gz", hash = "sha256:318de9d4b9b0061a111f8f5164ecbacd8215f449dd4bd5a76d2a691c815ee103"},
{file = "djlint-1.34.1-py3-none-any.whl", hash = "sha256:96ff1c464fb6f061130ebc88663a2ea524d7ec51f4b56221a2b3f0320a3cfce8"},
{file = "djlint-1.34.1.tar.gz", hash = "sha256:db93fa008d19eaadb0454edf1704931d14469d48508daba2df9941111f408346"},
]
[package.dependencies]
click = ">=8.0.1"
colorama = ">=0.4.4"
cssbeautifier = ">=1.14.4"
html-tag-names = ">=0.1.2"
html-void-elements = ">=0.1.0"
jsbeautifier = ">=1.14.4"
json5 = ">=0.9.11"
pathspec = ">=0.12.0"
PyYAML = ">=6.0"
regex = ">=2023"
tqdm = ">=4.62.2"
click = ">=8.0.1,<9.0.0"
colorama = ">=0.4.4,<0.5.0"
cssbeautifier = ">=1.14.4,<2.0.0"
html-tag-names = ">=0.1.2,<0.2.0"
html-void-elements = ">=0.1.0,<0.2.0"
jsbeautifier = ">=1.14.4,<2.0.0"
json5 = ">=0.9.11,<0.10.0"
pathspec = ">=0.12.0,<0.13.0"
PyYAML = ">=6.0,<7.0"
regex = ">=2023.0.0,<2024.0.0"
tqdm = ">=4.62.2,<5.0.0"
[[package]]
name = "editorconfig"
@ -256,19 +256,19 @@ files = [
[[package]]
name = "filelock"
version = "3.16.0"
version = "3.15.4"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
files = [
{file = "filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"},
{file = "filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec"},
{file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"},
{file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"},
]
[package.extras]
docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.3)"]
typing = ["typing-extensions (>=4.12.2)"]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"]
typing = ["typing-extensions (>=4.8)"]
[[package]]
name = "graphene"
@ -316,13 +316,13 @@ test = ["coveralls", "django-filter (>=22.1)", "djangorestframework (>=3.6.3)",
[[package]]
name = "graphql-core"
version = "3.2.4"
version = "3.2.3"
description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL."
optional = false
python-versions = "<4,>=3.6"
python-versions = ">=3.6,<4"
files = [
{file = "graphql-core-3.2.4.tar.gz", hash = "sha256:acbe2e800980d0e39b4685dd058c2f4042660b89ebca38af83020fd872ff1264"},
{file = "graphql_core-3.2.4-py3-none-any.whl", hash = "sha256:1604f2042edc5f3114f49cac9d77e25863be51b23a54a61a23245cf32f6476f0"},
{file = "graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676"},
{file = "graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3"},
]
[[package]]
@ -459,13 +459,13 @@ files = [
[[package]]
name = "markdown"
version = "3.7"
version = "3.6"
description = "Python implementation of John Gruber's Markdown."
optional = false
python-versions = ">=3.8"
files = [
{file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"},
{file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"},
{file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"},
{file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"},
]
[package.extras]
@ -474,38 +474,38 @@ testing = ["coverage", "pyyaml"]
[[package]]
name = "mypy"
version = "1.11.2"
version = "1.11.1"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"},
{file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"},
{file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"},
{file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"},
{file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"},
{file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"},
{file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"},
{file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"},
{file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"},
{file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"},
{file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"},
{file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"},
{file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"},
{file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"},
{file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"},
{file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"},
{file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"},
{file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"},
{file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"},
{file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"},
{file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"},
{file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"},
{file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"},
{file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"},
{file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"},
{file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"},
{file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"},
{file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"},
{file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"},
{file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"},
{file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"},
{file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"},
{file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"},
{file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"},
{file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"},
{file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"},
{file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"},
{file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"},
{file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"},
{file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"},
{file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"},
{file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"},
{file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"},
{file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"},
{file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"},
{file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"},
{file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"},
{file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"},
{file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"},
{file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"},
{file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"},
{file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"},
{file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"},
{file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"},
]
[package.dependencies]
@ -564,13 +564,13 @@ files = [
[[package]]
name = "platformdirs"
version = "4.3.1"
version = "4.2.2"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.3.1-py3-none-any.whl", hash = "sha256:facaa5a3c57aa1e053e3da7b49e0cc31fe0113ca42a4659d5c2e98e545624afe"},
{file = "platformdirs-4.3.1.tar.gz", hash = "sha256:63b79589009fa8159973601dd4563143396b35c5f93a58b36f9049ff046949b1"},
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
]
[package.extras]
@ -711,90 +711,104 @@ files = [
[[package]]
name = "regex"
version = "2024.7.24"
version = "2023.12.25"
description = "Alternative regular expression module, to replace re."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.7"
files = [
{file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"},
{file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"},
{file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"},
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"},
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"},
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"},
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"},
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"},
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"},
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"},
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"},
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"},
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"},
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"},
{file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"},
{file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"},
{file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"},
{file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"},
{file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"},
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"},
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"},
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"},
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"},
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"},
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"},
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"},
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"},
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"},
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"},
{file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"},
{file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"},
{file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"},
{file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"},
{file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"},
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"},
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"},
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"},
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"},
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"},
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"},
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"},
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"},
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"},
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"},
{file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"},
{file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"},
{file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"},
{file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"},
{file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"},
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"},
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"},
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"},
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"},
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"},
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"},
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"},
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"},
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"},
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"},
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"},
{file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"},
{file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"},
{file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"},
{file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"},
{file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"},
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"},
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"},
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"},
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"},
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"},
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"},
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"},
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"},
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"},
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"},
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"},
{file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"},
{file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"},
{file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"},
{file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"},
{file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"},
{file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"},
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"},
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"},
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"},
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"},
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"},
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"},
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"},
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"},
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"},
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"},
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"},
{file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"},
{file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"},
{file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"},
{file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"},
{file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"},
{file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"},
{file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"},
{file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"},
{file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"},
{file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"},
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"},
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"},
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"},
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"},
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"},
{file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"},
{file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"},
{file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"},
{file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"},
{file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"},
{file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"},
{file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"},
{file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"},
{file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"},
{file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"},
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"},
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"},
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"},
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"},
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"},
{file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"},
{file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"},
{file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"},
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"},
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"},
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"},
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"},
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"},
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"},
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"},
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"},
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"},
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"},
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"},
{file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"},
{file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"},
{file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"},
{file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"},
{file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"},
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"},
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"},
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"},
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"},
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"},
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"},
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"},
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"},
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"},
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"},
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"},
{file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"},
{file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"},
{file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"},
{file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"},
{file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"},
{file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"},
{file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"},
{file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"},
{file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"},
{file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"},
{file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"},
{file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"},
{file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"},
{file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"},
{file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"},
{file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"},
{file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"},
{file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"},
{file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"},
]
[[package]]
@ -889,13 +903,13 @@ files = [
[[package]]
name = "uvicorn"
version = "0.30.6"
version = "0.30.5"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
files = [
{file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"},
{file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"},
{file = "uvicorn-0.30.5-py3-none-any.whl", hash = "sha256:b2d86de274726e9878188fa07576c9ceeff90a839e2b6e25c917fe05f5a6c835"},
{file = "uvicorn-0.30.5.tar.gz", hash = "sha256:ac6fdbd4425c5fd17a9fe39daf4d4d075da6fdc80f653e5894cdc2fd98752bee"},
]
[package.dependencies]
@ -907,13 +921,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
[[package]]
name = "virtualenv"
version = "20.26.4"
version = "20.26.3"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"},
{file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"},
{file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"},
{file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"},
]
[package.dependencies]
@ -928,4 +942,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "508565632b7642265ad85a540a98a9ff3fa1e936eb26c0b7efae0707429b4c17"
content-hash = "f96f55c381f25a4a473be8d53ef83d13c70bb7c731b5132f4ece2af1b3d3afed"

View File

@ -24,7 +24,7 @@ python = "^3.11"
django = "^5.0.6"
gunicorn = "^22.0.0"
uvicorn = "^0.30.1"
graphene-django = "^3.2.0"
graphene-django = "^3.2.2"
django-htmx = "^1.18.0"
django-template-partials = "^24.2"
markdown = "^3.6"

View File

@ -1,27 +0,0 @@
import os
import time
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
django.setup()
from common.time import streak_bruteforce
from games.models import Session
all = Session.objects.filter(timestamp_start__gt="1970-01-01")
data = []
for session in all:
current = session.timestamp_start
data.append(current.date())
start = time.time_ns()
start_cpu = time.process_time_ns()
print(streak_bruteforce(data))
end = time.time_ns()
end_cpu = time.process_time_ns()
print(
f"Processed {all.count()} items in {((end - start)/ 1_000_000_000):.10f} seconds and {((end_cpu - start_cpu)/ 1_000_000_000):.10f} seconds of process time."
)

View File

@ -1,5 +1,4 @@
const defaultTheme = require('tailwindcss/defaultTheme')
const colors = require('tailwindcss/colors');
module.exports = {
darkMode: 'class',
@ -11,10 +10,6 @@ module.exports = {
'mono': ['IBM Plex Mono', ...defaultTheme.fontFamily.mono],
'serif': ['IBM Plex Serif', ...defaultTheme.fontFamily.serif],
'condensed': ['IBM Plex Sans Condensed', ...defaultTheme.fontFamily.sans],
},
colors: {
'accent': colors.violet[600],
'background': colors.gray[800],
}
},
},

View File

@ -1,60 +0,0 @@
import unittest
from datetime import date
from common.time import daterange, streak_bruteforce
class StreakTest(unittest.TestCase):
streak = streak_bruteforce
def test_daterange_exclusive(self):
d = daterange(date(2024, 8, 1), date(2024, 8, 3))
self.assertEqual(
d,
[date(2024, 8, 1), date(2024, 8, 2)],
)
def test_daterange_inclusive(self):
d = daterange(date(2024, 8, 1), date(2024, 8, 3), end_inclusive=True)
self.assertEqual(
d,
[date(2024, 8, 1), date(2024, 8, 2), date(2024, 8, 3)],
)
def test_1day_streak(self):
self.assertEqual(streak([date(2024, 8, 1)])["days"], 1)
def test_2day_streak(self):
self.assertEqual(streak([date(2024, 8, 1), date(2024, 8, 2)])["days"], 2)
def test_31day_streak(self):
self.assertEqual(
streak(daterange(date(2024, 8, 1), date(2024, 8, 31), end_inclusive=True))[
"days"
],
31,
)
def test_5day_streak_in_10_days(self):
d = daterange(
date(2024, 8, 1), date(2024, 8, 5), end_inclusive=True
) + daterange(date(2024, 8, 7), date(2024, 8, 10), end_inclusive=True)
self.assertEqual(streak(d)["days"], 5)
def test_10day_streak_in_31_days(self):
d = daterange(date(2024, 8, 1), date(2024, 8, 31), end_inclusive=True)
d.remove(date(2024, 8, 8))
d.remove(date(2024, 8, 15))
d.remove(date(2024, 8, 21))
self.assertEqual(streak(d)["days"], 10)
def test_10day_streak_in_31_days_with_consecutive_missing(self):
d = daterange(date(2024, 8, 1), date(2024, 8, 31), end_inclusive=True)
d.remove(date(2024, 8, 4))
d.remove(date(2024, 8, 5))
d.remove(date(2024, 8, 6))
d.remove(date(2024, 8, 7))
d.remove(date(2024, 8, 8))
d.remove(date(2024, 8, 15))
d.remove(date(2024, 8, 21))
self.assertEqual(streak(d)["days"], 10)