10 Commits

Author SHA1 Message Date
3314c9b42b use unified dateformat more
All checks were successful
Django CI/CD / test (push) Successful in 56s
Django CI/CD / build-and-push (push) Successful in 2m13s
2024-10-14 14:26:48 +02:00
62a1fab15f view_game: display timezone-aware time for end timestamp
All checks were successful
Django CI/CD / test (push) Successful in 1m8s
Django CI/CD / build-and-push (push) Successful in 2m16s
2024-10-14 13:02:18 +02:00
7ec622a38a make purchase price a float
All checks were successful
Django CI/CD / test (push) Successful in 1m7s
Django CI/CD / build-and-push (push) Successful in 2m8s
2024-10-04 11:36:46 +02:00
7f5a1889f3 avoid exception on game overview when sessions are 0
All checks were successful
Django CI/CD / test (push) Successful in 1m7s
Django CI/CD / build-and-push (push) Successful in 2m7s
2024-10-04 10:17:38 +02:00
10e96bbc88 rename icon
All checks were successful
Django CI/CD / test (push) Successful in 1m20s
Django CI/CD / build-and-push (push) Successful in 2m17s
2024-10-04 10:11:33 +02:00
60d3ba6569 order platforms by name
All checks were successful
Django CI/CD / test (push) Successful in 1m4s
Django CI/CD / build-and-push (push) Successful in 1m51s
2024-09-14 20:19:30 +02:00
bcb845adac remove css cruft 2024-09-14 20:19:20 +02:00
bd222f253e add more icons 2024-09-14 20:19:14 +02:00
45e3cfed00 Remove extraneous statement
All checks were successful
Django CI/CD / test (push) Successful in 54s
Django CI/CD / build-and-push (push) Successful in 1m58s
2024-09-14 11:10:28 +02:00
36dd5635b2 add icon field to platform, use everywhere
All checks were successful
Django CI/CD / test (push) Successful in 52s
Django CI/CD / build-and-push (push) Has been skipped
2024-09-14 11:07:38 +02:00
52 changed files with 1033 additions and 2058 deletions

View File

@ -1,25 +0,0 @@
{
"name": "Django Time Tracker",
"dockerFile": "../devcontainer.Dockerfile",
"customizations": {
"vscode": {
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.defaultInterpreterPath": "/usr/local/bin/python",
"terminal.integrated.defaultProfile.linux": "bash"
},
"extensions": [
"ms-python.python",
"ms-python.debugpy",
"ms-python.vscode-pylance",
"ms-azuretools.vscode-docker",
"batisteo.vscode-django",
"charliermarsh.ruff",
"bradlc.vscode-tailwindcss",
"EditorConfig.EditorConfig"
]
}
},
"forwardPorts": [8000],
"postCreateCommand": "poetry install && poetry run python manage.py migrate && npm install && make dev",
}

View File

@ -15,6 +15,3 @@ indent_size = 4
[**/*.js] [**/*.js]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[*.html]
insert_final_newline = false

20
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,20 @@
repos:
# disable due to incomaptible formatting between
# black and ruff
# TODO: replace with ruff when it works on NixOS
# - repo: https://github.com/psf/black
# rev: 24.8.0
# hooks:
# - id: black
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
name: isort (python)
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.34.0
hooks:
- id: djlint-reformat-django
args: ["--ignore", "H011"]
- id: djlint-django
args: ["--ignore", "H011"]

View File

@ -7,7 +7,6 @@
* Allow deleting purchases * Allow deleting purchases
* Add all-time stats * Add all-time stats
* Manage purchases * Manage purchases
* Automatically convert purchase prices
## Improved ## Improved
* mark refunded purchases red on game overview * mark refunded purchases red on game overview

View File

@ -2,14 +2,11 @@ from random import choices as random_choices
from string import ascii_lowercase from string import ascii_lowercase
from typing import Any, Callable from typing import Any, Callable
from django.template import TemplateDoesNotExist
from django.template.defaultfilters import floatformat
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import NoReverseMatch, reverse from django.urls import NoReverseMatch, reverse
from django.utils.safestring import SafeText, mark_safe from django.utils.safestring import SafeText, mark_safe
from common.utils import truncate from common.utils import truncate
from games.models import Purchase
HTMLAttribute = tuple[str, str | int | bool] HTMLAttribute = tuple[str, str | int | bool]
HTMLTag = str HTMLTag = str
@ -52,7 +49,6 @@ def randomid(seed: str = "", length: int = 10) -> str:
def Popover( def Popover(
popover_content: str, popover_content: str,
wrapped_content: str = "", wrapped_content: str = "",
wrapped_classes: str = "",
children: list[HTMLTag] = [], children: list[HTMLTag] = [],
attributes: list[HTMLAttribute] = [], attributes: list[HTMLAttribute] = [],
) -> str: ) -> str:
@ -65,43 +61,17 @@ def Popover(
("id", id), ("id", id),
("wrapped_content", wrapped_content), ("wrapped_content", wrapped_content),
("popover_content", popover_content), ("popover_content", popover_content),
("wrapped_classes", wrapped_classes),
], ],
children=children, children=children,
template="cotton/popover.html", template="cotton/popover.html",
) )
def PopoverTruncated( def PopoverTruncated(input_string: str) -> str:
input_string: str, if (truncated := truncate(input_string)) != input_string:
popover_content: str = "", return Popover(wrapped_content=truncated, popover_content=input_string)
popover_if_not_truncated: bool = False,
length: int = 30,
ellipsis: str = "",
endpart: str = "",
) -> str:
"""
Returns `input_string` truncated after `length` of characters
and displays the untruncated text in a popover HTML element.
The truncated text ends in `ellipsis`, and optionally
an always-visible `endpart` can be specified.
`popover_content` can be specified if:
1. It needs to be always displayed regardless if text is truncated.
2. It needs to differ from `input_string`.
"""
if (truncated := truncate(input_string, length, ellipsis, endpart)) != input_string:
return Popover(
wrapped_content=truncated,
popover_content=popover_content if popover_content else input_string,
)
else: else:
if popover_content and popover_if_not_truncated: return input_string
return Popover(
wrapped_content=input_string,
popover_content=popover_content if popover_content else "",
)
else:
return input_string
def A( def A(
@ -154,38 +124,11 @@ def Div(
return Component(tag_name="div", attributes=attributes, children=children) return Component(tag_name="div", attributes=attributes, children=children)
def Input(
type: str = "text",
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
):
return Component(
tag_name="input", attributes=attributes + [("type", type)], children=children
)
def Form(
action="",
method="get",
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
):
return Component(
tag_name="form",
attributes=attributes + [("action", action), ("method", method)],
children=children,
)
def Icon( def Icon(
name: str, name: str,
attributes: list[HTMLAttribute] = [], attributes: list[HTMLAttribute] = [],
): ):
try: return Component(template=f"cotton/icon/{name}.html", attributes=attributes)
result = Component(template=f"cotton/icon/{name}.html", attributes=attributes)
except TemplateDoesNotExist:
result = Icon(name="unspecified", attributes=attributes)
return result
def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeText: def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeText:
@ -209,47 +152,6 @@ def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeTe
) )
def LinkedPurchase(purchase: Purchase) -> SafeText:
link = reverse("view_purchase", args=[int(purchase.id)])
link_content = ""
popover_content = ""
edition_count = purchase.editions.count()
popover_if_not_truncated = False
if edition_count == 1:
link_content += purchase.editions.first().name
popover_content = link_content
if edition_count > 1:
if purchase.name:
link_content += f"{purchase.name}"
popover_content += f"<h1>{purchase.name}</h1><br>"
else:
link_content += f"{edition_count} games"
popover_if_not_truncated = True
popover_content += f"""
<ul class="list-disc list-inside">
{"".join(f"<li>{edition.name}</li>" for edition in purchase.editions.all())}
</ul>
"""
icon = purchase.platform.icon if edition_count == 1 else "unspecified"
if link_content == "":
raise ValueError("link_content is empty!!")
a_content = Div(
[("class", "inline-flex gap-2 items-center")],
[
Icon(
icon,
[("title", "Multiple")],
),
PopoverTruncated(
input_string=link_content,
popover_content=mark_safe(popover_content),
popover_if_not_truncated=popover_if_not_truncated,
),
],
)
return mark_safe(A(url=link, children=[a_content]))
def NameWithPlatformIcon(name: str, platform: str) -> SafeText: def NameWithPlatformIcon(name: str, platform: str) -> SafeText:
content = Div( content = Div(
[("class", "inline-flex gap-2 items-center")], [("class", "inline-flex gap-2 items-center")],
@ -263,11 +165,3 @@ def NameWithPlatformIcon(name: str, platform: str) -> SafeText:
) )
return mark_safe(content) return mark_safe(content)
def PurchasePrice(purchase) -> str:
return Popover(
popover_content=f"{floatformat(purchase.price)} {purchase.price_currency}",
wrapped_content=f"{floatformat(purchase.converted_price)} {purchase.converted_currency}",
wrapped_classes="underline decoration-dotted",
)

View File

@ -163,7 +163,3 @@ def streak_bruteforce(datelist: list[date]) -> dict[str, int | tuple[date, date]
else: else:
increment_streak() increment_streak()
return {"days": highest_streak, "dates": highest_streak_daterange} return {"days": highest_streak, "dates": highest_streak_daterange}
def available_stats_year_range():
return range(datetime.now().year, 1999, -1)

View File

@ -34,31 +34,14 @@ def safe_getattr(obj: object, attr_chain: str, default: Any | None = None) -> ob
return obj return obj
def truncate_(input_string: str, length: int = 30, ellipsis: str = "") -> str: def truncate(input_string: str, length: int = 30, ellipsis: str = "") -> str:
return ( return (
(f"{input_string[:length-len(ellipsis)].rstrip()}{ellipsis}") (f"{input_string[:length-len(ellipsis)]}{ellipsis}")
if len(input_string) > length if len(input_string) > 30
else input_string else input_string
) )
def truncate(
input_string: str, length: int = 30, ellipsis: str = "", endpart: str = ""
) -> str:
max_content_length = length - len(endpart)
if max_content_length < 0:
raise ValueError("Length cannot be shorter than the length of endpart.")
if len(input_string) > max_content_length:
return f"{input_string[:max_content_length - len(ellipsis)].rstrip()}{ellipsis}{endpart}"
return (
f"{input_string}{endpart}"
if len(input_string) + len(endpart) <= length
else f"{input_string[:length - len(ellipsis) - len(endpart)].rstrip()}{ellipsis}{endpart}"
)
T = TypeVar("T", str, int, date) T = TypeVar("T", str, int, date)

View File

@ -1,24 +0,0 @@
FROM python:3.13-slim
# Set up environment
ENV PYTHONUNBUFFERED=1
WORKDIR /workspace
# Install Poetry
RUN apt-get update && apt-get install -y \
curl \
make \
npm \
&& rm -rf /var/lib/apt/lists/*
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="/root/.local/bin:$PATH"
# Copy pyproject.toml and poetry.lock for dependency installation
COPY pyproject.toml poetry.lock* ./
RUN poetry install --no-root
# Copy the rest of the application code
COPY . .
# Set up Django development server
EXPOSE 8000

View File

@ -10,14 +10,10 @@ poetry run python manage.py collectstatic --clear --no-input
_term() { _term() {
echo "Caught SIGTERM signal!" echo "Caught SIGTERM signal!"
kill -SIGTERM "$gunicorn_pid" kill -SIGTERM "$gunicorn_pid"
kill -SIGTERM "$django_q_pid"
} }
trap _term SIGTERM trap _term SIGTERM
echo "Starting Django-Q cluster"
poetry run python manage.py qcluster & django_q_pid=$!
echo "Starting app" echo "Starting app"
poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile - & gunicorn_pid=$! poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile - & gunicorn_pid=$!
wait "$gunicorn_pid" "$django_q_pid" wait "$gunicorn_pid"

View File

@ -1,14 +1,6 @@
from django.contrib import admin from django.contrib import admin
from games.models import ( from games.models import Device, Edition, Game, Platform, Purchase, Session
Device,
Edition,
ExchangeRate,
Game,
Platform,
Purchase,
Session,
)
# Register your models here. # Register your models here.
admin.site.register(Game) admin.site.register(Game)
@ -17,4 +9,3 @@ admin.site.register(Platform)
admin.site.register(Session) admin.site.register(Session)
admin.site.register(Edition) admin.site.register(Edition)
admin.site.register(Device) admin.site.register(Device)
admin.site.register(ExchangeRate)

View File

@ -1,33 +1,6 @@
from datetime import timedelta
from django.apps import AppConfig from django.apps import AppConfig
from django.core.management import call_command
from django.db.models.signals import post_migrate
from django.utils.timezone import now
class GamesConfig(AppConfig): class GamesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "games" name = "games"
def ready(self):
post_migrate.connect(schedule_tasks, sender=self)
def schedule_tasks(sender, **kwargs):
from django_q.models import Schedule
from django_q.tasks import schedule
if not Schedule.objects.filter(name="Update converted prices").exists():
schedule(
"games.tasks.convert_prices",
name="Update converted prices",
schedule_type=Schedule.MINUTES,
next_run=now() + timedelta(seconds=30),
)
from games.models import ExchangeRate
if not ExchangeRate.objects.exists():
print("ExchangeRate table is empty. Loading fixture...")
call_command("loaddata", "exchangerates.yaml")

View File

@ -1,112 +0,0 @@
- model: games.exchangerate
pk: 1
fields:
currency_from: USD
currency_to: CZK
year: 2024
rate: 23.4
- model: games.exchangerate
pk: 2
fields:
currency_from: CNY
currency_to: CZK
year: 2024
rate: 3.267
- model: games.exchangerate
pk: 3
fields:
currency_from: USD
currency_to: CZK
year: 2019
rate: 22.466
- model: games.exchangerate
pk: 4
fields:
currency_from: USD
currency_to: CZK
year: 2023
rate: 22.63
- model: games.exchangerate
pk: 5
fields:
currency_from: USD
currency_to: CZK
year: 2017
rate: 25.819
- model: games.exchangerate
pk: 6
fields:
currency_from: USD
currency_to: CZK
year: 2013
rate: 19.023
- model: games.exchangerate
pk: 7
fields:
currency_from: CNY
currency_to: CZK
year: 2019
rate: 3.295
- model: games.exchangerate
pk: 8
fields:
currency_from: CNY
currency_to: CZK
year: 2016
rate: 3.795
- model: games.exchangerate
pk: 9
fields:
currency_from: CNY
currency_to: CZK
year: 2015
rate: 3.707
- model: games.exchangerate
pk: 10
fields:
currency_from: CNY
currency_to: CZK
year: 2020
rate: 3.26
- model: games.exchangerate
pk: 11
fields:
currency_from: EUR
currency_to: CZK
year: 2012
rate: 25.51
- model: games.exchangerate
pk: 12
fields:
currency_from: EUR
currency_to: CZK
year: 2010
rate: 26.465
- model: games.exchangerate
pk: 13
fields:
currency_from: EUR
currency_to: CZK
year: 2014
rate: 27.52
- model: games.exchangerate
pk: 14
fields:
currency_from: EUR
currency_to: CZK
year: 2024
rate: 25.21
- model: games.exchangerate
pk: 15
fields:
currency_from: EUR
currency_to: CZK
year: 2022
rate: 24.325
- model: games.exchangerate
pk: 16
fields:
currency_from: CNY
currency_to: CZK
year: 2018
rate: 3.268

View File

@ -16,7 +16,7 @@ class SessionForm(forms.ModelForm):
# queryset=Purchase.objects.filter(date_refunded=None).order_by("edition__name") # queryset=Purchase.objects.filter(date_refunded=None).order_by("edition__name")
# ) # )
purchase = forms.ModelChoiceField( purchase = forms.ModelChoiceField(
queryset=Purchase.objects.all(), queryset=Purchase.objects.order_by("edition__sort_name"),
widget=forms.Select(attrs={"autofocus": "autofocus"}), widget=forms.Select(attrs={"autofocus": "autofocus"}),
) )
@ -38,12 +38,12 @@ class SessionForm(forms.ModelForm):
] ]
class EditionChoiceField(forms.ModelMultipleChoiceField): class EditionChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj) -> str: def label_from_instance(self, obj) -> str:
return f"{obj.sort_name} ({obj.platform}, {obj.year_released})" return f"{obj.sort_name} ({obj.platform}, {obj.year_released})"
class IncludePlatformSelect(forms.SelectMultiple): class IncludePlatformSelect(forms.Select):
def create_option(self, name, value, *args, **kwargs): def create_option(self, name, value, *args, **kwargs):
option = super().create_option(name, value, *args, **kwargs) option = super().create_option(name, value, *args, **kwargs)
if platform_id := safe_getattr(value, "instance.platform.id"): if platform_id := safe_getattr(value, "instance.platform.id"):
@ -58,7 +58,7 @@ class PurchaseForm(forms.ModelForm):
# Automatically update related_purchase <select/> # Automatically update related_purchase <select/>
# to only include purchases of the selected edition. # to only include purchases of the selected edition.
related_purchase_by_edition_url = reverse("related_purchase_by_edition") related_purchase_by_edition_url = reverse("related_purchase_by_edition")
self.fields["editions"].widget.attrs.update( self.fields["edition"].widget.attrs.update(
{ {
"hx-trigger": "load, click", "hx-trigger": "load, click",
"hx-get": related_purchase_by_edition_url, "hx-get": related_purchase_by_edition_url,
@ -67,13 +67,15 @@ class PurchaseForm(forms.ModelForm):
} }
) )
editions = EditionChoiceField( edition = EditionChoiceField(
queryset=Edition.objects.order_by("sort_name"), queryset=Edition.objects.order_by("sort_name"),
widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}), widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}),
) )
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name")) platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
related_purchase = forms.ModelChoiceField( related_purchase = forms.ModelChoiceField(
queryset=Purchase.objects.filter(type=Purchase.GAME), queryset=Purchase.objects.filter(type=Purchase.GAME).order_by(
"edition__sort_name"
),
required=False, required=False,
) )
@ -86,7 +88,7 @@ class PurchaseForm(forms.ModelForm):
} }
model = Purchase model = Purchase
fields = [ fields = [
"editions", "edition",
"platform", "platform",
"date_purchased", "date_purchased",
"date_refunded", "date_refunded",

View File

@ -1,24 +0,0 @@
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.utils.timezone import now
from django_q.models import Schedule
from django_q.tasks import schedule
class Command(BaseCommand):
help = "Manually schedule the next update_converted_prices task"
def handle(self, *args, **kwargs):
if not Schedule.objects.filter(name="Update converted prices").exists():
schedule(
"games.tasks.convert_prices",
name="Update converted prices",
schedule_type=Schedule.MINUTES,
next_run=now() + timedelta(seconds=30),
)
self.stdout.write(
self.style.SUCCESS("Scheduled the update_converted_prices task.")
)
else:
self.stdout.write(self.style.WARNING("Task is already scheduled."))

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.2 on 2024-11-09 22:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0038_alter_purchase_price'),
]
operations = [
migrations.AlterField(
model_name='device',
name='type',
field=models.CharField(choices=[('PC', 'PC'), ('Console', 'Console'), ('Handheld', 'Handheld'), ('Mobile', 'Mobile'), ('Single-board computer', 'Single-board computer'), ('Unknown', 'Unknown')], default='Unknown', max_length=255),
),
]

View File

@ -1,33 +0,0 @@
# Generated by Django 5.1.2 on 2024-11-09 22:39
from django.db import migrations
def update_device_types(apps, schema_editor):
Device = apps.get_model("games", "Device")
# Mapping of short names to long names
type_map = {
"pc": "PC",
"co": "Console",
"ha": "Handheld",
"mo": "Mobile",
"sbc": "Single-board computer",
"un": "Unknown",
}
# Loop through all devices and update the type field
for device in Device.objects.all():
if device.type in type_map:
device.type = type_map[device.type]
device.save()
class Migration(migrations.Migration):
dependencies = [
("games", "0039_alter_device_type"),
]
operations = [
migrations.RunPython(update_device_types),
]

View File

@ -1,36 +0,0 @@
# Generated by Django 5.1.3 on 2024-11-10 15:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0040_migrate_device_types'),
]
operations = [
migrations.AddField(
model_name='purchase',
name='converted_currency',
field=models.CharField(max_length=3, null=True),
),
migrations.AddField(
model_name='purchase',
name='converted_price',
field=models.FloatField(null=True),
),
migrations.CreateModel(
name='ExchangeRate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('currency_from', models.CharField(max_length=255)),
('currency_to', models.CharField(max_length=255)),
('year', models.PositiveIntegerField()),
('rate', models.FloatField()),
],
options={
'unique_together': {('currency_from', 'currency_to', 'year')},
},
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.3 on 2025-01-07 20:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0041_purchase_converted_currency_purchase_converted_price_and_more'),
]
operations = [
migrations.AddField(
model_name='purchase',
name='editions_temp',
field=models.ManyToManyField(blank=True, related_name='temp_purchases', to='games.edition'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 5.1.3 on 2025-01-07 20:17
from django.db import migrations
def migrate_edition_to_editions_temp(apps, schema_editor):
Purchase = apps.get_model("games", "Purchase")
for purchase in Purchase.objects.all():
if purchase.edition:
purchase.editions_temp.add(purchase.edition)
purchase.save()
else:
print(f"No edition found for Purchase {purchase.id}")
class Migration(migrations.Migration):
dependencies = [
("games", "0042_purchase_editions_temp"),
]
operations = [
migrations.RunPython(migrate_edition_to_editions_temp),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.3 on 2025-01-07 20:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("games", "0043_auto_20250107_2117"),
]
operations = [
migrations.RemoveField(model_name="purchase", name="edition"),
migrations.RenameField(
model_name="purchase",
old_name="editions_temp",
new_name="editions",
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.3 on 2025-01-07 20:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0044_auto_20250107_2132'),
]
operations = [
migrations.AlterField(
model_name='purchase',
name='editions',
field=models.ManyToManyField(blank=True, related_name='purchases', to='games.edition'),
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 5.1.3 on 2025-01-08 20:06
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0045_alter_purchase_editions"),
]
operations = [
migrations.AddField(
model_name="game",
name="platform",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="games.platform",
),
),
migrations.AlterUniqueTogether(
name="game",
unique_together={("name", "platform", "year_released")},
),
]

View File

@ -1,61 +0,0 @@
# Generated by Django 5.1.3 on 2025-01-19 20:29
from django.db import connection, migrations, models
def recreate_games(apps, schema_editor):
Edition = apps.get_model("games", "Edition")
Game = apps.get_model("games", "Game")
Purchase = apps.get_model("games", "Purchase")
with connection.cursor() as cursor:
print("Create table games_gametemp")
cursor.execute(
"CREATE TABLE games_gametemp AS SELECT * FROM games_game WHERE 1=0;"
)
for edition in Edition.objects.all():
print(f"Re-create edition with ID {edition.id}")
cursor.execute(
"""
INSERT INTO games_gametemp (
id, name, sort_name, year_released, platform_id, wikidata, created_at
)
VALUES (%s, %s, %s, %s, %s, %s, %s)
""",
[
edition.id, # Reuse the Edition ID
edition.name,
edition.sort_name,
edition.year_released,
edition.platform_id,
Game.objects.get(id=edition.game.id).wikidata,
edition.created_at,
],
)
print("Turn foreign keys off")
cursor.execute("PRAGMA foreign_keys = OFF;")
print("Drop table games_game")
cursor.execute("DROP TABLE games_game;")
print("Drop table games_edition")
cursor.execute("DROP TABLE games_edition;")
print("Rename table games_gametemp to games_game")
# cursor.execute("ALTER TABLE games_gametemp RENAME TO games_game;")
cursor.execute("CREATE TABLE games_game AS SELECT * FROM games_gametemp;")
class Migration(migrations.Migration):
dependencies = [
("games", "0046_game_platform_alter_game_unique_together"),
]
operations = [
migrations.RunPython(recreate_games),
migrations.AlterField(
model_name="purchase",
name="editions",
field=models.ManyToManyField(
blank=True, related_name="purchases", to="games.game"
),
),
]

View File

@ -9,6 +9,20 @@ from django.utils import timezone
from common.time import format_duration from common.time import format_duration
class Game(models.Model):
name = models.CharField(max_length=255)
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
year_released = models.IntegerField(null=True, blank=True, default=None)
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
created_at = models.DateTimeField(auto_now_add=True)
session_average: float | int | timedelta | None
session_count: int | None
def __str__(self):
return self.name
class Platform(models.Model): class Platform(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
group = models.CharField(max_length=255, null=True, blank=True, default=None) group = models.CharField(max_length=255, null=True, blank=True, default=None)
@ -24,32 +38,6 @@ class Platform(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_sentinel_platform():
return Platform.objects.get_or_create(
name="Unspecified", icon="unspecified", group="Unspecified"
)[0]
class Game(models.Model):
class Meta:
unique_together = [["name", "platform", "year_released"]]
name = models.CharField(max_length=255)
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
year_released = models.IntegerField(null=True, blank=True, default=None)
platform = models.ForeignKey(
Platform, on_delete=models.SET_DEFAULT, null=True, blank=True, default=None
)
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
created_at = models.DateTimeField(auto_now_add=True)
session_average: float | int | timedelta | None
session_count: int | None
def __str__(self):
return self.name
class Edition(models.Model): class Edition(models.Model):
class Meta: class Meta:
unique_together = [["name", "platform", "year_released"]] unique_together = [["name", "platform", "year_released"]]
@ -67,11 +55,6 @@ class Edition(models.Model):
def __str__(self): def __str__(self):
return self.sort_name return self.sort_name
def save(self, *args, **kwargs):
if self.platform is None:
self.platform = get_sentinel_platform()
super().save(*args, **kwargs)
class PurchaseQueryset(models.QuerySet): class PurchaseQueryset(models.QuerySet):
def refunded(self): def refunded(self):
@ -119,7 +102,7 @@ class Purchase(models.Model):
objects = PurchaseQueryset().as_manager() objects = PurchaseQueryset().as_manager()
editions = models.ManyToManyField(Game, related_name="purchases", blank=True) edition = models.ForeignKey(Edition, on_delete=models.CASCADE)
platform = models.ForeignKey( platform = models.ForeignKey(
Platform, on_delete=models.CASCADE, default=None, null=True, blank=True Platform, on_delete=models.CASCADE, default=None, null=True, blank=True
) )
@ -130,8 +113,6 @@ class Purchase(models.Model):
infinite = models.BooleanField(default=False) infinite = models.BooleanField(default=False)
price = models.FloatField(default=0) price = models.FloatField(default=0)
price_currency = models.CharField(max_length=3, default="USD") price_currency = models.CharField(max_length=3, default="USD")
converted_price = models.FloatField(null=True)
converted_currency = models.CharField(max_length=3, null=True)
ownership_type = models.CharField( ownership_type = models.CharField(
max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL
) )
@ -147,41 +128,29 @@ class Purchase(models.Model):
) )
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
@property
def first_edition(self):
return self.editions.first()
def __str__(self): def __str__(self):
additional_info = [ additional_info = [
self.get_type_display() if self.type != Purchase.GAME else "", self.get_type_display() if self.type != Purchase.GAME else "",
( (
f"{self.first_edition.platform} version on {self.platform}" f"{self.edition.platform} version on {self.platform}"
if self.platform != self.first_edition.platform if self.platform != self.edition.platform
else self.platform else self.platform
), ),
self.first_edition.year_released, self.edition.year_released,
self.get_ownership_type_display(), self.get_ownership_type_display(),
] ]
return f"{self.first_edition} ({', '.join(filter(None, map(str, additional_info)))})" return f"{self.edition} ({', '.join(filter(None, map(str, additional_info)))})"
def is_game(self): def is_game(self):
return self.type == self.GAME return self.type == self.GAME
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.type != Purchase.GAME and not self.related_purchase: if self.type == Purchase.GAME:
self.name = ""
elif self.type != Purchase.GAME and not self.related_purchase:
raise ValidationError( raise ValidationError(
f"{self.get_type_display()} must have a related purchase." f"{self.get_type_display()} must have a related purchase."
) )
if self.pk is not None:
# Retrieve the existing instance from the database
existing_purchase = Purchase.objects.get(pk=self.pk)
# If price has changed, reset converted fields
if (
existing_purchase.price != self.price
or existing_purchase.price_currency != self.price_currency
):
self.converted_price = None
self.converted_currency = None
super().save(*args, **kwargs) super().save(*args, **kwargs)
@ -279,12 +248,12 @@ class Session(models.Model):
class Device(models.Model): class Device(models.Model):
PC = "PC" PC = "pc"
CONSOLE = "Console" CONSOLE = "co"
HANDHELD = "Handheld" HANDHELD = "ha"
MOBILE = "Mobile" MOBILE = "mo"
SBC = "Single-board computer" SBC = "sbc"
UNKNOWN = "Unknown" UNKNOWN = "un"
DEVICE_TYPES = [ DEVICE_TYPES = [
(PC, "PC"), (PC, "PC"),
(CONSOLE, "Console"), (CONSOLE, "Console"),
@ -294,21 +263,8 @@ class Device(models.Model):
(UNKNOWN, "Unknown"), (UNKNOWN, "Unknown"),
] ]
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
type = models.CharField(max_length=255, choices=DEVICE_TYPES, default=UNKNOWN) type = models.CharField(max_length=3, choices=DEVICE_TYPES, default=UNKNOWN)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return f"{self.name} ({self.type})" return f"{self.name} ({self.get_type_display()})"
class ExchangeRate(models.Model):
currency_from = models.CharField(max_length=255)
currency_to = models.CharField(max_length=255)
year = models.PositiveIntegerField()
rate = models.FloatField()
class Meta:
unique_together = ("currency_from", "currency_to", "year")
def __str__(self):
return f"{self.currency_from}/{self.currency_to} - {self.rate} ({self.year})"

View File

@ -1,113 +1,5 @@
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(63 131 248 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(63 131 248 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
/* /*
! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com ! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com
*/ */
/* /*
@ -550,7 +442,7 @@ video {
/* Make elements with the HTML hidden attribute stay hidden by default */ /* Make elements with the HTML hidden attribute stay hidden by default */
[hidden]:where(:not([hidden="until-found"])) { [hidden] {
display: none; display: none;
} }
@ -1212,6 +1104,114 @@ input:checked + .toggle-bg {
border-color: #1C64F2; border-color: #1C64F2;
} }
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(63 131 248 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(63 131 248 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
.container { .container {
width: 100%; width: 100%;
} }
@ -1258,10 +1258,6 @@ input:checked + .toggle-bg {
border-width: 0; border-width: 0;
} }
.pointer-events-none {
pointer-events: none;
}
.visible { .visible {
visibility: visible; visibility: visible;
} }
@ -1294,11 +1290,6 @@ input:checked + .toggle-bg {
inset: 0px; inset: 0px;
} }
.inset-y-0 {
top: 0px;
bottom: 0px;
}
.bottom-0 { .bottom-0 {
bottom: 0px; bottom: 0px;
} }
@ -1327,10 +1318,6 @@ input:checked + .toggle-bg {
right: 0.75rem; right: 0.75rem;
} }
.start-0 {
inset-inline-start: 0px;
}
.top-0 { .top-0 {
top: 0px; top: 0px;
} }
@ -1431,10 +1418,6 @@ input:checked + .toggle-bg {
margin-inline-start: 0.625rem; margin-inline-start: 0.625rem;
} }
.mt-1 {
margin-top: 0.25rem;
}
.mt-2 { .mt-2 {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
@ -1552,10 +1535,6 @@ input:checked + .toggle-bg {
width: 16rem; width: 16rem;
} }
.w-80 {
width: 20rem;
}
.w-full { .w-full {
width: 100%; width: 100%;
} }
@ -1652,14 +1631,6 @@ input:checked + .toggle-bg {
resize: both; resize: both;
} }
.list-inside {
list-style-position: inside;
}
.list-disc {
list-style-type: disc;
}
.grid-cols-4 { .grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
} }
@ -1994,18 +1965,6 @@ input:checked + .toggle-bg {
padding-bottom: 1rem; padding-bottom: 1rem;
} }
.pb-4 {
padding-bottom: 1rem;
}
.ps-10 {
padding-inline-start: 2.5rem;
}
.ps-3 {
padding-inline-start: 0.75rem;
}
.pt-2 { .pt-2 {
padding-top: 0.5rem; padding-top: 0.5rem;
} }
@ -2172,6 +2131,11 @@ input:checked + .toggle-bg {
color: rgb(17 24 39 / var(--tw-text-opacity)); color: rgb(17 24 39 / var(--tw-text-opacity));
} }
.text-red-600 {
--tw-text-opacity: 1;
color: rgb(224 36 36 / var(--tw-text-opacity));
}
.text-slate-300 { .text-slate-300 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity)); color: rgb(203 213 225 / var(--tw-text-opacity));
@ -2200,10 +2164,6 @@ input:checked + .toggle-bg {
text-decoration-color: #64748b; text-decoration-color: #64748b;
} }
.decoration-dotted {
text-decoration-style: dotted;
}
.opacity-0 { .opacity-0 {
opacity: 0; opacity: 0;
} }
@ -2652,11 +2612,6 @@ textarea:disabled:is(.dark *) {
z-index: 10; z-index: 10;
} }
.focus\:border-blue-500:focus {
--tw-border-opacity: 1;
border-color: rgb(63 131 248 / var(--tw-border-opacity));
}
.focus\:text-blue-700:focus { .focus\:text-blue-700:focus {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(26 86 219 / var(--tw-text-opacity)); color: rgb(26 86 219 / var(--tw-text-opacity));
@ -2684,11 +2639,6 @@ textarea:disabled:is(.dark *) {
--tw-ring-color: rgb(164 202 254 / var(--tw-ring-opacity)); --tw-ring-color: rgb(164 202 254 / var(--tw-ring-opacity));
} }
.focus\:ring-blue-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(63 131 248 / var(--tw-ring-opacity));
}
.focus\:ring-blue-700:focus { .focus\:ring-blue-700:focus {
--tw-ring-opacity: 1; --tw-ring-opacity: 1;
--tw-ring-color: rgb(26 86 219 / var(--tw-ring-opacity)); --tw-ring-color: rgb(26 86 219 / var(--tw-ring-opacity));
@ -2930,16 +2880,6 @@ textarea:disabled:is(.dark *) {
color: rgb(255 255 255 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder {
--tw-placeholder-opacity: 1;
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
}
.dark\:placeholder-gray-400:is(.dark *)::placeholder {
--tw-placeholder-opacity: 1;
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
}
.odd\:dark\:bg-gray-900:is(.dark *):nth-child(odd) { .odd\:dark\:bg-gray-900:is(.dark *):nth-child(odd) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity)); background-color: rgb(17 24 39 / var(--tw-bg-opacity));
@ -3010,11 +2950,6 @@ textarea:disabled:is(.dark *) {
color: rgb(255 255 255 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.dark\:focus\:border-blue-500:focus:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(63 131 248 / var(--tw-border-opacity));
}
.dark\:focus\:text-white:focus:is(.dark *) { .dark\:focus\:text-white:focus:is(.dark *) {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
@ -3129,10 +3064,6 @@ textarea:disabled:is(.dark *) {
flex-direction: row; flex-direction: row;
} }
.md\:justify-between {
justify-content: space-between;
}
.md\:space-x-8 > :not([hidden]) ~ :not([hidden]) { .md\:space-x-8 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0; --tw-space-x-reverse: 0;
margin-right: calc(2rem * var(--tw-space-x-reverse)); margin-right: calc(2rem * var(--tw-space-x-reverse));
@ -3276,13 +3207,3 @@ textarea:disabled:is(.dark *) {
.\[\&_td\:last-child\]\:text-right td:last-child { .\[\&_td\:last-child\]\:text-right td:last-child {
text-align: right; text-align: right;
} }
@media not all and (min-width: 640px) {
.\[\&_td\:not\(\:first-child\)\:not\(\:last-child\)\]\:max-sm\:hidden td:not(:first-child):not(:last-child) {
display: none;
}
.\[\&_th\:not\(\:first-child\)\:not\(\:last-child\)\]\:max-sm\:hidden th:not(:first-child):not(:last-child) {
display: none;
}
}

View File

@ -1,57 +0,0 @@
import requests
from games.models import ExchangeRate, Purchase
# fixme: save preferred currency in user model
currency_to = "CZK"
currency_to = currency_to.upper()
def save_converted_info(purchase, converted_price, converted_currency):
print(
f"Changing converted price of {purchase} to {converted_price} {converted_currency} "
)
purchase.converted_price = converted_price
purchase.converted_currency = converted_currency
purchase.save()
def convert_prices():
purchases = Purchase.objects.filter(
converted_price__isnull=True, converted_currency__isnull=True
)
for purchase in purchases:
if purchase.price_currency.upper() == currency_to or purchase.price == 0:
save_converted_info(purchase, purchase.price, currency_to)
continue
year = purchase.date_purchased.year
currency_from = purchase.price_currency.upper()
exchange_rate = ExchangeRate.objects.filter(
currency_from=currency_from, currency_to=currency_to, year=year
).first()
if not exchange_rate:
try:
response = requests.get(
f"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@{year}-01-01/v1/currencies/{currency_from}.json"
)
response.raise_for_status()
data = response.json()
rate = data[currency_from].get(currency_to)
if rate:
exchange_rate = ExchangeRate.objects.create(
currency_from=currency_from,
currency_to=currency_to,
year=year,
rate=rate,
)
except requests.RequestException as e:
print(
f"Failed to fetch exchange rate for {currency_from}->{currency_to} in {year}: {e}"
)
if exchange_rate:
save_converted_info(
purchase, purchase.price * exchange_rate.rate, currency_to
)

View File

@ -1,2 +1,24 @@
<c-layouts.add> {% extends "base.html" %}
</c-layouts.add> {% load static %}
{% block title %}
{{ title }}
{% endblock title %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<table class="mx-auto">
{% csrf_token %}
{{ form.as_table }}
<tr>
<td></td>
<td>
<input type="submit" value="Submit" />
</td>
</tr>
</table>
</form>
{% endblock content %}
{% block scripts %}
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
{% endblock scripts %}

View File

@ -1,12 +1,32 @@
<c-layouts.add> {% extends "base.html" %}
<c-slot name="additional_row"> {% load static %}
<tr> {% block title %}
<td></td> {{ title }}
<td> {% endblock title %}
<input type="submit" {% block content %}
name="submit_and_redirect" <form method="post" enctype="multipart/form-data">
value="Submit & Create Purchase" /> <table class="mx-auto">
</td> {% csrf_token %}
</tr> {{ form.as_table }}
</c-slot> <tr>
</c-layouts.add> <td></td>
<td>
<input type="submit" name="submit" value="Submit" />
</td>
</tr>
<tr>
<td></td>
<td>
<input type="submit"
name="submit_and_redirect"
value="Submit & Create Purchase" />
</td>
</tr>
</table>
</form>
{% endblock content %}
{% block scripts %}
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
{% endblock scripts %}

View File

@ -1,12 +1,32 @@
<c-layouts.add> {% extends "base.html" %}
<c-slot name="additional_row"> {% load static %}
<tr> {% block title %}
<td></td> {{ title }}
<td> {% endblock title %}
<input type="submit" {% block content %}
name="submit_and_redirect" <form method="post" enctype="multipart/form-data">
value="Submit & Create Edition" /> <table class="mx-auto">
</td> {% csrf_token %}
</tr> {{ form.as_table }}
</c-slot> <tr>
</c-layouts.add> <td></td>
<td>
<input type="submit" name="submit" value="Submit" />
</td>
</tr>
<tr>
<td></td>
<td>
<input type="submit"
name="submit_and_redirect"
value="Submit & Create Edition" />
</td>
</tr>
</table>
</form>
{% endblock content %}
{% block scripts %}
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
{% endblock scripts %}

View File

@ -1,12 +1,42 @@
<c-layouts.add> {% extends "base.html" %}
<c-slot name="additional_row"> {% load static %}
<tr> {% block title %}
<td></td> {{ title }}
<td> {% endblock title %}
<input type="submit" {% block content %}
name="submit_and_redirect" <form method="post" enctype="multipart/form-data">
value="Submit & Create Session" /> <table class="mx-auto">
</td> {% csrf_token %}
</tr> {{ form.as_table }}
</c-slot> <tr>
</c-layouts.add> <td></td>
<td>
<input type="submit" name="submit" value="Submit" />
</td>
</tr>
<tr>
<td></td>
<td>
<input type="submit"
name="submit_and_redirect"
value="Submit & Create Session" />
</td>
</tr>
{% if purchase_id %}
<tr>
<td></td>
<td>
<a href="{% url 'delete_purchase' purchase_id %}"
class="text-red-600"
onclick="return confirm('Are you sure you want to delete this purchase?');">Delete</a>
</td>
</tr>
{% endif %}
</table>
</form>
{% endblock content %}
{% block scripts %}
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
{% endblock scripts %}

View File

@ -1,36 +1,40 @@
<c-layouts.add> {% extends "base.html" %}
<c-slot name="form_content"> {% block title %}
<form method="post" enctype="multipart/form-data"> {{ title }}
<table class="mx-auto"> {% endblock title %}
{% csrf_token %} {% block content %}
{% for field in form %} <form method="post" enctype="multipart/form-data">
<table class="mx-auto">
{% csrf_token %}
{% for field in form %}
<tr>
<th>{{ field.label_tag }}</th>
{% if field.name == "note" %}
<td>{{ field }}</td>
{% else %}
<td>{{ field }}</td>
{% endif %}
{% if field.name == "timestamp_start" or field.name == "timestamp_end" %}
<td>
<div class="basic-button-container" hx-boost="false">
<button class="basic-button" data-target="{{ field.name }}" data-type="now">Set to now</button>
<button class="basic-button"
data-target="{{ field.name }}"
data-type="toggle">Toggle text</button>
<button class="basic-button" data-target="{{ field.name }}" data-type="copy">Copy</button>
</div>
</td>
{% endif %}
</tr>
{% endfor %}
<tr> <tr>
<th>{{ field.label_tag }}</th> <td></td>
{% if field.name == "note" %} <td>
<td>{{ field }}</td> <input type="submit" value="Submit" />
{% else %} </td>
<td>{{ field }}</td>
{% endif %}
{% if field.name == "timestamp_start" or field.name == "timestamp_end" %}
<td>
<div class="basic-button-container" hx-boost="false">
<button class="basic-button" data-target="{{ field.name }}" data-type="now">Set to now</button>
<button class="basic-button"
data-target="{{ field.name }}"
data-type="toggle">Toggle text</button>
<button class="basic-button" data-target="{{ field.name }}" data-type="copy">Copy</button>
</div>
</td>
{% endif %}
</tr> </tr>
{% endfor %} </table>
<tr> </form>
<td></td> {% load static %}
<td> <script type="module" src="{% static 'js/add_session.js' %}"></script>
<input type="submit" value="Submit" /> {% endblock content %}
</td>
</tr>
</table>
</form>
</c-slot>
</c-layouts.add>

View File

@ -7,7 +7,11 @@
<meta name="description" content="Self-hosted time-tracker." /> <meta name="description" content="Self-hosted time-tracker." />
<meta name="keywords" content="time, tracking, video games, self-hosted" /> <meta name="keywords" content="time, tracking, video games, self-hosted" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Timetracker - {{ title }}</title> <title>Timetracker -
{% block title %}
Untitled
{% endblock title %}
</title>
<script src="{% static 'js/htmx.min.js' %}"></script> <script src="{% static 'js/htmx.min.js' %}"></script>
{% django_htmx_script %} {% django_htmx_script %}
<link rel="stylesheet" href="{% static 'base.css' %}" /> <link rel="stylesheet" href="{% static 'base.css' %}" />
@ -30,11 +34,16 @@
alt="loading indicator" /> alt="loading indicator" />
<div class="flex flex-col min-h-screen"> <div class="flex flex-col min-h-screen">
{% include "navbar.html" %} {% include "navbar.html" %}
<div class="flex flex-1 flex-col dark:bg-gray-800 pt-8">{{ slot }}</div> <div class="flex flex-1 flex-col dark:bg-gray-800 pt-8">
{% block content %}
No content here.
{% endblock content %}
</div>
{% load version %} {% load version %}
<span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span> <span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
</div> </div>
{{ scripts }} {% block scripts %}
{% endblock scripts %}
<script> <script>
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon'); var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon'); var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');

View File

@ -1,25 +0,0 @@
<c-layouts.base>
{% load static %}
{% if form_content %}
{{ form_content }}
{% else %}
<form method="post" enctype="multipart/form-data">
<table class="mx-auto">
{% csrf_token %}
{{ form.as_table }}
<tr>
<td></td>
<td>
<input type="submit" value="Submit" />
</td>
</tr>
{{ additional_row }}
</table>
</form>
{% endif %}
<c-slot name="scripts">
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
</c-slot>
</c-layouts.base>

View File

@ -1,10 +1,8 @@
<span data-popover-target={{ id }} class="{{ wrapped_classes }}">{{ wrapped_content|default:slot }}</span> <span data-popover-target={{ id }} class="{{ class }}">{{ wrapped_content|default:slot }}</span>
<div data-popover <div data-popover
id="{{ id }}" id="{{ id }}"
role="tooltip" 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"> 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">{{ popover_content }}</div>
<div data-popper-arrow></div> <div data-popper-arrow></div>
<!-- for Tailwind CSS to generate decoration-dotted CSS from Python component -->
<span class="hidden decoration-dotted"></span>
</div> </div>

View File

@ -1,12 +0,0 @@
<c-vars :name="id" />
<div class="pb-4 bg-white dark:bg-gray-900">
<label for="table-search" class="sr-only">Search</label>
<div class="relative mt-1">
<div class="absolute inset-y-0 rtl:inset-r-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
</svg>
</div>
<input type="text" id="{{ id }}" name="{{ name }}" value="{{ search_string }}" class="block pt-2 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg w-80 bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{% if placeholder %}{{ placeholder }}{% else %}Search{% endif %}">
</div>
</div>

View File

@ -7,20 +7,20 @@
{{ header_action }} {{ header_action }}
</c-table-header> </c-table-header>
{% endif %} {% endif %}
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 [&_th:not(:first-child):not(:last-child)]:max-sm:hidden"> <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr> <tr>
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %} {% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
</tr> </tr>
</thead> </thead>
<tbody class="dark:divide-y [&_td:not(:first-child):not(:last-child)]:max-sm:hidden"> <tbody class="dark:divide-y">
{% for row in rows %}<c-table-row :data=row />{% endfor %} {% for row in rows %}<c-table-row :data=row />{% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% if page_obj and elided_page_range %} {% if page_obj and elided_page_range %}
<nav class="flex items-center flex-col md:flex-row md: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 dark:bg-gray-900 sm:rounded-b-lg"
aria-label="Table navigation"> aria-label="Table navigation">
<span class="text-sm text-center font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0 block w-full md:inline md:w-auto"><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> <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"> <ul class="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
<li> <li>
{% if page_obj.has_previous %} {% if page_obj.has_previous %}

View File

@ -1,6 +1,10 @@
<c-layouts.base> {% extends "base.html" %}
{% load static %} {% load static %}
<div class="2xl:max-w-screen-2xl xl:max-w-screen-xl md:max-w-screen-md sm:max-w-screen-sm self-center"> {% block title %}
<c-simple-table :columns=data.columns :rows=data.rows :page_obj=page_obj :elided_page_range=elided_page_range :header_action=data.header_action /> {{ title }}
</div> {% endblock title %}
</c-layouts.base> {% 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 />
</div>
{% endblock content %}

View File

@ -122,7 +122,7 @@
</div> </div>
</li> </li>
<li> <li>
<a href="{% url 'stats_by_year' global_current_year %}" <a href="{% url 'stats_by_year' 0 %}"
class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Stats</a> class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Stats</a>
</li> </li>
<li> <li>

View File

@ -1,18 +1,22 @@
<c-layouts.base title="Login"> {% extends "base.html" %}
{% load static %} {% load static %}
<div class="flex items-center flex-col"> {% block title %}
<h2 class="text-3xl text-white mb-8">Please log in to continue</h2> Login
<form method="post"> {% endblock title %}
<table> {% block content %}
{% csrf_token %} <div class="flex items-center flex-col">
{{ form.as_table }} <h2 class="text-3xl text-white mb-8">Please log in to continue</h2>
<tr> <form method="post">
<td></td> <table>
<td> {% csrf_token %}
<input type="submit" value="Login" /> {{ form.as_table }}
</td> <tr>
</tr> <td></td>
</form> <td>
</table> <input type="submit" value="Login" />
</div> </td>
</c-layouts.base> </tr>
</form>
</table>
</div>
{% endblock content %}

View File

@ -1,287 +1,279 @@
<c-layouts.base> {% extends "base.html" %}
{% block title %}
{{ title }}
{% endblock title %}
{% load static %} {% load static %}
{% partialdef purchase-name %} {% partialdef purchase-name %}
{% if purchase.type != 'game' %} {% if purchase.type != 'game' %}
<c-gamelink :game_id=purchase.first_edition.game.id> <c-gamelink :game_id=purchase.edition.game.id>
{{ purchase.name }} ({{ purchase.first_edition.edition.name }} {{ purchase.get_type_display }}) {{ purchase.name }} ({{ purchase.edition.name }} {{ purchase.get_type_display }})
</c-gamelink> </c-gamelink>
{% else %} {% else %}
<c-gamelink :game_id=purchase.first_edition.game.id :name=purchase.first_edition.name /> <c-gamelink :game_id=purchase.edition.game.id :name=purchase.edition.name />
{% endif %} {% endif %}
{% endpartialdef %} {% endpartialdef %}
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto"> {% block content %}
<div class="flex justify-center items-center"> <div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
<form method="get" class="text-center"> <div class="flex justify-center items-center">
<label class="text-5xl text-center inline-block mb-10" for="yearSelect">Stats for:</label> <form method="get" class="text-center">
<select name="year" <label class="text-5xl text-center inline-block mb-10" for="yearSelect">Stats for:</label>
id="yearSelect" <select name="year"
onchange="this.form.submit();" id="yearSelect"
class="mx-2"> onchange="this.form.submit();"
{% for year_item in stats_dropdown_year_range %} class="mx-2">
<option value="{{ year_item }}" {% if year == year_item %}selected{% endif %}>{{ year_item }}</option> {% for year_item in stats_dropdown_year_range %}
{% endfor %} <option value="{{ year_item }}" {% if year == year_item %}selected{% endif %}>{{ year_item }}</option>
</select> {% endfor %}
</form> </select>
</div> </form>
<h1 class="text-5xl text-center my-6">Playtime</h1> </div>
<table class="responsive-table"> <h1 class="text-5xl text-center my-6">Playtime</h1>
<tbody> <table class="responsive-table">
<tr> <tbody>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Hours</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_hours }}</td>
</tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Sessions</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_sessions }}</td>
</tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Days</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ unique_days }} ({{ unique_days_percent }}%)</td>
</tr>
{% if total_games %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Games</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Hours</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_games }}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_hours }}</td>
</tr> </tr>
{% endif %}
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Games ({{ year }})</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_2023_games }}</td>
</tr>
{% if all_finished_this_year_count %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Finished</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Sessions</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_finished_this_year_count }}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_sessions }}</td>
</tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Days</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ unique_days }} ({{ unique_days_percent }}%)</td>
</tr>
{% if total_games %}
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Games</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_games }}</td>
</tr>
{% endif %}
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Games ({{ year }})</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_2023_games }}</td>
</tr>
{% if all_finished_this_year_count %}
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Finished</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_finished_this_year_count }}</td>
</tr>
{% endif %}
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Finished ({{ year }})</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ this_year_finished_this_year_count }}</td>
</tr> </tr>
{% endif %}
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Finished ({{ year }})</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ this_year_finished_this_year_count }}</td>
</tr>
{% if longest_session_game.id %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Longest session</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Longest session</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
{{ longest_session_time }} (<c-gamelink :game_id=longest_session_game.id :name=longest_session_game.name />) {{ longest_session_time }} (<c-gamelink :game_id=longest_session_game.id :name=longest_session_game.name />)
</td> </td>
</tr> </tr>
{% endif %}
{% if highest_session_count_game.id %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Most sessions</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Most sessions</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
{{ highest_session_count }} (<c-gamelink :game_id=highest_session_count_game.id :name=highest_session_count_game.name />) {{ highest_session_count }} (<c-gamelink :game_id=highest_session_count_game.id :name=highest_session_count_game.name />)
</td> </td>
</tr> </tr>
{% endif %}
{% if highest_session_average_game.id %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Highest session average</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Highest session average</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
{{ highest_session_average }} (<c-gamelink :game_id=highest_session_average_game.id :name=highest_session_average_game.name />) {{ highest_session_average }} (<c-gamelink :game_id=highest_session_average_game.id :name=highest_session_average_game.name />)
</td> </td>
</tr> </tr>
{% endif %}
{% if first_play_game.id %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">First play</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">First play</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
<c-gamelink :game_id=first_play_game.id :name=first_play_game.name /> ({{ first_play_date }}) <c-gamelink :game_id=first_play_game.id :name=first_play_game.name /> ({{ first_play_date }})
</td> </td>
</tr> </tr>
{% endif %}
{% if last_play_game.id %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Last play</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Last play</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
<c-gamelink :game_id=last_play_game.id :name=last_play_game.name /> ({{ last_play_date }}) <c-gamelink :game_id=last_play_game.id :name=last_play_game.name /> ({{ last_play_date }})
</td> </td>
</tr> </tr>
{% endif %}
</tbody>
</table>
{% if month_playtimes %}
<h1 class="text-5xl text-center my-6">Playtime per month</h1>
<table class="responsive-table">
<tbody>
{% for month in month_playtimes %}
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">{{ month.month | date:"F" }}</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ month.playtime }}</td>
</tr>
{% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} {% if month_playtime %}
<h1 class="text-5xl text-center my-6">Purchases</h1> <h1 class="text-5xl text-center my-6">Playtime per month</h1>
<table class="responsive-table"> <table class="responsive-table">
<tbody> <tbody>
<tr> {% for month in month_playtimes %}
<td class="px-2 sm:px-4 md:px-6 md:py-2">Total</td> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_purchased_this_year_count }}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">{{ month.month | date:"F" }}</td>
</tr> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ month.playtime }}</td>
<tr> </tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Refunded</td> {% endfor %}
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"> </tbody>
{{ all_purchased_refunded_this_year_count }} ({{ refunded_percent }}%) </table>
</td> {% endif %}
</tr> <h1 class="text-5xl text-center my-6">Purchases</h1>
<tr> <table class="responsive-table">
<td class="px-2 sm:px-4 md:px-6 md:py-2">Dropped</td> <tbody>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ dropped_count }} ({{ dropped_percentage }}%)</td>
</tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Unfinished</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
{{ purchased_unfinished_count }} ({{ unfinished_purchases_percent }}%)
</td>
</tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Backlog Decrease</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ backlog_decrease_count }}</td>
</tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Spendings ({{ total_spent_currency }})</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
{{ total_spent | floatformat }} ({{ spent_per_game | floatformat }}/game)
</td>
</tr>
</tbody>
</table>
<h1 class="text-5xl text-center my-6">Top games by playtime</h1>
<table class="responsive-table">
<thead>
<tr>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Name</th>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Playtime (hours)</th>
</tr>
</thead>
<tbody>
{% for game in top_10_games_by_playtime %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Total</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_purchased_this_year_count }}</td>
</tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Refunded</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
<c-gamelink :game_id=game.id :name=game.name /> {{ all_purchased_refunded_this_year_count }} ({{ refunded_percent }}%)
</td> </td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ game.formatted_playtime }}</td>
</tr> </tr>
{% endfor %}
</tbody>
</table>
<h1 class="text-5xl text-center my-6">Platforms by playtime</h1>
<table class="responsive-table">
<thead>
<tr>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Platform</th>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Playtime (hours)</th>
</tr>
</thead>
<tbody>
{% for item in total_playtime_per_platform %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ item.platform_name }}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Dropped</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ item.formatted_playtime }}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ dropped_count }} ({{ dropped_percentage }}%)</td>
</tr> </tr>
{% endfor %} <tr>
</tbody> <td class="px-2 sm:px-4 md:px-6 md:py-2">Unfinished</td>
</table> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
{% if all_finished_this_year %} {{ purchased_unfinished_count }} ({{ unfinished_purchases_percent }}%)
<h1 class="text-5xl text-center my-6">Finished</h1> </td>
</tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Backlog Decrease</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ backlog_decrease_count }}</td>
</tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Spendings ({{ total_spent_currency }})</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_spent }} ({{ spent_per_game }}/game)</td>
</tr>
</tbody>
</table>
<h1 class="text-5xl text-center my-6">Top games by playtime</h1>
<table class="responsive-table"> <table class="responsive-table">
<thead> <thead>
<tr> <tr>
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th> <th class="px-2 sm:px-4 md:px-6 md:py-2">Name</th>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th> <th class="px-2 sm:px-4 md:px-6 md:py-2">Playtime (hours)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for purchase in all_finished_this_year %} {% for game in top_10_games_by_playtime %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td> <c-gamelink :game_id=game.id :name=game.name />
</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ game.formatted_playtime }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} <h1 class="text-5xl text-center my-6">Platforms by playtime</h1>
{% if this_year_finished_this_year %}
<h1 class="text-5xl text-center my-6">Finished ({{ year }} games)</h1>
<table class="responsive-table"> <table class="responsive-table">
<thead> <thead>
<tr> <tr>
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th> <th class="px-2 sm:px-4 md:px-6 md:py-2">Platform</th>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th> <th class="px-2 sm:px-4 md:px-6 md:py-2">Playtime (hours)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for purchase in this_year_finished_this_year %} {% for item in total_playtime_per_platform %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ item.platform_name }}</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ item.formatted_playtime }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} {% if all_finished_this_year %}
{% if purchased_this_year_finished_this_year %} <h1 class="text-5xl text-center my-6">Finished</h1>
<h1 class="text-5xl text-center my-6">Bought and Finished ({{ year }})</h1> <table class="responsive-table">
<table class="responsive-table"> <thead>
<thead>
<tr>
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
</tr>
</thead>
<tbody>
{% for purchase in purchased_this_year_finished_this_year %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td> <th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td> <th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for purchase in all_finished_this_year %}
{% endif %} <tr>
{% if purchased_unfinished %} <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td>
<h1 class="text-5xl text-center my-6">Unfinished Purchases</h1> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
<table class="responsive-table"> </tr>
<thead> {% endfor %}
<tr> </tbody>
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th> </table>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Price ({{ total_spent_currency }})</th> {% endif %}
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th> {% if this_year_finished_this_year %}
</tr> <h1 class="text-5xl text-center my-6">Finished ({{ year }} games)</h1>
</thead> <table class="responsive-table">
<tbody> <thead>
{% for purchase in purchased_unfinished %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td> <th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.converted_price | floatformat }}</td> <th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for purchase in this_year_finished_this_year %}
{% endif %} <tr>
{% if all_purchased_this_year %} <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td>
<h1 class="text-5xl text-center my-6">All Purchases</h1> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
<table class="responsive-table"> </tr>
<thead> {% endfor %}
<tr> </tbody>
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th> </table>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Price ({{ total_spent_currency }})</th> {% endif %}
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th> {% if purchased_this_year_finished_this_year %}
</tr> <h1 class="text-5xl text-center my-6">Bought and Finished ({{ year }})</h1>
</thead> <table class="responsive-table">
<tbody> <thead>
{% for purchase in all_purchased_this_year %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td> <th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.converted_price | floatformat }}</td> <th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for purchase in purchased_this_year_finished_this_year %}
{% endif %} <tr>
</div> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td>
</c-layouts.base> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if purchased_unfinished %}
<h1 class="text-5xl text-center my-6">Unfinished Purchases</h1>
<table class="responsive-table">
<thead>
<tr>
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Price ({{ total_spent_currency }})</th>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
</tr>
</thead>
<tbody>
{% for purchase in purchased_unfinished %}
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if all_purchased_this_year %}
<h1 class="text-5xl text-center my-6">All Purchases</h1>
<table class="responsive-table">
<thead>
<tr>
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Price ({{ total_spent_currency }})</th>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
</tr>
</thead>
<tbody>
{% for purchase in all_purchased_this_year %}
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
{% endblock content %}

View File

@ -1,88 +1,94 @@
<c-layouts.base> {% extends "base.html" %}
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto"> {% block title %}
<div id="game-info" class="mb-10"> {{ title }}
<div class="flex gap-5 mb-3"> {% endblock title %}
<span class="text-balance max-w-[30rem] text-4xl"> {% load static %}
<span class="font-bold font-serif">{{ game.name }}</span>{% if game.year_released %}&nbsp;<c-popover id="popover-year" popover_content="Original release year" class="text-slate-500 text-2xl">{{ game.year_released }}</c-popover>{% endif %} {% load markdown_extras %}
</span> {% block content %}
<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">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6">
<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>
<c-popover id="popover-sessions" popover_content="Number of sessions" class="flex gap-2 items-center">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6">
<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>
<c-popover id="popover-average" popover_content="Average playtime per session" class="flex gap-2 items-center">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6">
<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>
<c-popover id="popover-playrange" popover_content="Earliest and latest dates played" class="flex gap-2 items-center">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6">
<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>
</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>
</div> </div>
<div class="flex gap-4 dark:text-slate-400 mb-3"> <c-h1 :badge=edition_count>Editions</c-h1>
<c-popover id="popover-hours" popover_content="Total hours played" class="flex gap-2 items-center"> <div class="mb-6">
<svg xmlns="http://www.w3.org/2000/svg" <c-simple-table :rows=edition_data.rows :columns=edition_data.columns />
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6">
<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>
<c-popover id="popover-sessions" popover_content="Number of sessions" class="flex gap-2 items-center">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6">
<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>
<c-popover id="popover-average" popover_content="Average playtime per session" class="flex gap-2 items-center">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6">
<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>
<c-popover id="popover-playrange" popover_content="Earliest and latest dates played" class="flex gap-2 items-center">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6">
<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>
</div> </div>
<div class="inline-flex rounded-md shadow-sm mb-3" role="group"> <div class="mb-6">
<a href="{% url 'edit_game' game.id %}"> <c-h1 :badge=purchase_count>Purchases</c-h1>
<button type="button" <c-simple-table :rows=purchase_data.rows :columns=purchase_data.columns />
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"> </div>
Edit <div class="mb-6">
</button> <c-h1 :badge=session_count>Sessions</c-h1>
</a> <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 />
<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>
</div> </div>
<c-h1 :badge="edition_count">Editions</c-h1> <script>
<div class="mb-6"> function getSessionCount() {
<c-simple-table :rows=edition_data.rows :columns=edition_data.columns /> return document.getElementById('session-count').textContent.match("[0-9]+");
</div> }
<div class="mb-6"> </script>
<c-h1 :badge="purchase_count">Purchases</c-h1> {% endblock content %}
<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>
</div>
<script>
function getSessionCount() {
return document.getElementById('session-count').textContent.match("[0-9]+");
}
</script>
</c-layouts.base>

View File

@ -1,34 +0,0 @@
<c-layouts.base>
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
<div class="flex flex-col gap-5 mb-3">
<span class="text-balance max-w-[30rem] text-4xl">
<span class="font-bold font-serif">{% if purchase.name %}{{ purchase.name }}{% else %}Unnamed purchase{% endif %}</span> <span class="text-slate-500 text-2xl">({{ purchase.editions.count }} games)</span>
</span>
<div class="inline-flex rounded-md shadow-sm mb-3" role="group">
<a href="{% url 'edit_purchase' purchase.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_purchase' purchase.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>Price: {{ purchase.converted_price | floatformat }} {{ purchase.converted_currency }} ({{ purchase.price | floatformat }} {{ purchase.price_currency }})</div>
<div>
<h2 class="text-base">Items:</h2>
<ul class="list-disc list-inside">
{% for edition in purchase.editions.all %}
<li><c-gamelink :game_id=edition.game.id :name=edition.name /></li>
{% endfor %}
</ul>
</div>
</div>
</div>
</c-layouts.base>

View File

@ -54,11 +54,6 @@ urlpatterns = [
purchase.delete_purchase, purchase.delete_purchase,
name="delete_purchase", name="delete_purchase",
), ),
path(
"purchase/<int:purchase_id>/view",
purchase.view_purchase,
name="view_purchase",
),
path( path(
"purchase/<int:purchase_id>/finish", "purchase/<int:purchase_id>/finish",
purchase.finish_purchase, purchase.finish_purchase,
@ -121,7 +116,6 @@ urlpatterns = [
name="list_sessions_end_session", name="list_sessions_end_session",
), ),
path("session/list", session.list_sessions, name="list_sessions"), path("session/list", session.list_sessions, name="list_sessions"),
path("session/search", session.search_sessions, name="search_sessions"),
path("stats/", general.stats_alltime, name="stats_alltime"), path("stats/", general.stats_alltime, name="stats_alltime"),
path( path(
"stats/<int:year>", "stats/<int:year>",

View File

@ -47,6 +47,7 @@ def list_editions(request: HttpRequest) -> HttpResponse:
"Game", "Game",
"Name", "Name",
"Sort Name", "Sort Name",
"Platform",
"Year", "Year",
"Wikidata", "Wikidata",
"Created", "Created",

View File

@ -13,11 +13,9 @@ from common.components import (
Button, Button,
Div, Div,
Icon, Icon,
LinkedPurchase,
NameWithPlatformIcon, NameWithPlatformIcon,
Popover, Popover,
PopoverTruncated, PopoverTruncated,
PurchasePrice,
) )
from common.time import ( from common.time import (
dateformat, dateformat,
@ -27,7 +25,7 @@ from common.time import (
local_strftime, local_strftime,
timeformat, timeformat,
) )
from common.utils import safe_division, truncate from common.utils import format_float_or_int, safe_division, truncate
from games.forms import GameForm from games.forms import GameForm
from games.models import Edition, Game, Purchase, Session from games.models import Edition, Game, Purchase, Session
from games.views.general import use_custom_redirect from games.views.general import use_custom_redirect
@ -163,7 +161,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
to_attr="nongame_related_purchases", to_attr="nongame_related_purchases",
) )
game_purchases_prefetch: Prefetch[Purchase] = Prefetch( game_purchases_prefetch: Prefetch[Purchase] = Prefetch(
"purchases", "purchase_set",
queryset=Purchase.objects.filter(type=Purchase.GAME).prefetch_related( queryset=Purchase.objects.filter(type=Purchase.GAME).prefetch_related(
nongame_related_purchases_prefetch nongame_related_purchases_prefetch
), ),
@ -175,14 +173,14 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
.order_by("year_released") .order_by("year_released")
) )
purchases = Purchase.objects.filter(editions__game=game).order_by("date_purchased") purchases = Purchase.objects.filter(edition__game=game).order_by("date_purchased")
sessions = Session.objects.prefetch_related("device").filter( sessions = Session.objects.prefetch_related("device").filter(
purchase__editions__game=game purchase__edition__game=game
) )
session_count = sessions.count() session_count = sessions.count()
session_count_without_manual = ( session_count_without_manual = (
Session.objects.without_manual().filter(purchase__editions__game=game).count() Session.objects.without_manual().filter(purchase__edition__game=game).count()
) )
if sessions: if sessions:
@ -243,10 +241,13 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
"columns": ["Name", "Type", "Date", "Price", "Actions"], "columns": ["Name", "Type", "Date", "Price", "Actions"],
"rows": [ "rows": [
[ [
LinkedPurchase(purchase), NameWithPlatformIcon(
name=purchase.name if purchase.name else purchase.edition.name,
platform=purchase.platform,
),
purchase.get_type_display(), purchase.get_type_display(),
purchase.date_purchased.strftime(dateformat), purchase.date_purchased.strftime(dateformat),
PurchasePrice(purchase), f"{format_float_or_int(purchase.price)} {purchase.price_currency}",
render_to_string( render_to_string(
"cotton/button_group.html", "cotton/button_group.html",
{ {
@ -269,7 +270,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
], ],
} }
sessions_all = Session.objects.filter(purchase__editions__game=game).order_by( sessions_all = Session.objects.filter(purchase__edition__game=game).order_by(
"-timestamp_start" "-timestamp_start"
) )
last_session = None last_session = None
@ -298,7 +299,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
args=[last_session.pk], args=[last_session.pk],
), ),
children=Popover( children=Popover(
popover_content=last_session.purchase.first_edition.name, popover_content=last_session.purchase.edition.name,
children=[ children=[
Button( Button(
icon=True, icon=True,
@ -306,9 +307,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
size="xs", size="xs",
children=[ children=[
Icon("play"), Icon("play"),
truncate( truncate(f"{last_session.purchase.edition.name}"),
f"{last_session.purchase.first_edition.name}"
),
], ],
) )
], ],
@ -324,7 +323,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
NameWithPlatformIcon( NameWithPlatformIcon(
name=session.purchase.name name=session.purchase.name
if session.purchase.name if session.purchase.name
else session.purchase.first_edition.name, else session.purchase.edition.name,
platform=session.purchase.platform, platform=session.purchase.platform,
), ),
f"{local_strftime(session.timestamp_start)}{f"{local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}", f"{local_strftime(session.timestamp_start)}{f"{local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}",
@ -375,7 +374,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
"editions": editions, "editions": editions,
"game": game, "game": game,
"playrange": playrange, "playrange": playrange,
"purchase_count": Purchase.objects.filter(editions__game=game).count(), "purchase_count": Purchase.objects.filter(edition__game=game).count(),
"session_average_without_manual": round( "session_average_without_manual": round(
safe_division( safe_division(
total_hours_without_manual, int(session_count_without_manual) total_hours_without_manual, int(session_count_without_manual)

View File

@ -1,15 +1,14 @@
from datetime import datetime
from typing import Any, Callable from typing import Any, Callable
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Avg, Count, ExpressionWrapper, F, Prefetch, Q, Sum, fields from django.db.models import Avg, Count, ExpressionWrapper, F, Q, Sum, fields
from django.db.models.functions import TruncDate, TruncMonth from django.db.models.functions import TruncDate, TruncMonth
from django.db.models.manager import BaseManager from django.db.models.manager import BaseManager
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from common.time import available_stats_year_range, dateformat, format_duration from common.time import dateformat, format_duration
from common.utils import safe_division from common.utils import safe_division
from games.models import Edition, Game, Platform, Purchase, Session from games.models import Edition, Game, Platform, Purchase, Session
@ -24,10 +23,6 @@ def model_counts(request: HttpRequest) -> dict[str, bool]:
} }
def global_current_year(request: HttpRequest) -> dict[str, int]:
return {"global_current_year": datetime.now().year}
def use_custom_redirect( def use_custom_redirect(
func: Callable[..., HttpResponse], func: Callable[..., HttpResponse],
) -> Callable[..., HttpResponse]: ) -> Callable[..., HttpResponse]:
@ -49,9 +44,7 @@ def use_custom_redirect(
@login_required @login_required
def stats_alltime(request: HttpRequest) -> HttpResponse: def stats_alltime(request: HttpRequest) -> HttpResponse:
year = "Alltime" year = "Alltime"
this_year_sessions = Session.objects.all().prefetch_related( this_year_sessions = Session.objects.all().select_related("purchase__edition")
Prefetch("purchase__editions")
)
this_year_sessions_with_durations = this_year_sessions.annotate( this_year_sessions_with_durations = this_year_sessions.annotate(
duration=ExpressionWrapper( duration=ExpressionWrapper(
F("timestamp_end") - F("timestamp_start"), F("timestamp_end") - F("timestamp_start"),
@ -60,10 +53,10 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
) )
longest_session = this_year_sessions_with_durations.order_by("-duration").first() longest_session = this_year_sessions_with_durations.order_by("-duration").first()
this_year_games = Game.objects.filter( this_year_games = Game.objects.filter(
editions__purchase__session__in=this_year_sessions edition__purchase__session__in=this_year_sessions
).distinct() ).distinct()
this_year_games_with_session_counts = this_year_games.annotate( this_year_games_with_session_counts = this_year_games.annotate(
session_count=Count("editions__purchase__session"), session_count=Count("edition__purchase__session"),
) )
game_highest_session_count = this_year_games_with_session_counts.order_by( game_highest_session_count = this_year_games_with_session_counts.order_by(
"-session_count" "-session_count"
@ -80,7 +73,9 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
).distinct() ).distinct()
this_year_purchases = Purchase.objects.all() this_year_purchases = Purchase.objects.all()
this_year_purchases_with_currency = this_year_purchases.select_related("editions") this_year_purchases_with_currency = this_year_purchases.select_related(
"edition"
).filter(price_currency__exact=selected_currency)
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter( this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
date_refunded=None date_refunded=None
) )
@ -124,16 +119,16 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
).order_by("date_finished") ).order_by("date_finished")
this_year_spendings = this_year_purchases_without_refunded.aggregate( this_year_spendings = this_year_purchases_without_refunded.aggregate(
total_spent=Sum(F("converted_price")) total_spent=Sum(F("price"))
) )
total_spent = this_year_spendings["total_spent"] or 0 total_spent = this_year_spendings["total_spent"] or 0
games_with_playtime = ( games_with_playtime = (
Game.objects.filter(editions__purchase__session__in=this_year_sessions) Game.objects.filter(edition__purchase__session__in=this_year_sessions)
.annotate( .annotate(
total_playtime=Sum( total_playtime=Sum(
F("editions__purchase__session__duration_calculated") F("edition__purchase__session__duration_calculated")
+ F("editions__purchase__session__duration_manual") + F("edition__purchase__session__duration_manual")
) )
) )
.values("id", "name", "total_playtime") .values("id", "name", "total_playtime")
@ -148,9 +143,9 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
month["playtime"] = format_duration(month["playtime"], "%2.0H") month["playtime"] = format_duration(month["playtime"], "%2.0H")
highest_session_average_game = ( highest_session_average_game = (
Game.objects.filter(editions__purchase__session__in=this_year_sessions) Game.objects.filter(edition__purchase__session__in=this_year_sessions)
.annotate( .annotate(
session_average=Avg("editions__purchase__session__duration_calculated") session_average=Avg("edition__purchase__session__duration_calculated")
) )
.order_by("-session_average") .order_by("-session_average")
.first() .first()
@ -177,10 +172,10 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
last_play_date = "N/A" last_play_date = "N/A"
if this_year_sessions: if this_year_sessions:
first_session = this_year_sessions.earliest() first_session = this_year_sessions.earliest()
first_play_game = first_session.purchase.first_edition.game first_play_game = first_session.purchase.edition.game
first_play_date = first_session.timestamp_start.strftime(dateformat) first_play_date = first_session.timestamp_start.strftime(dateformat)
last_session = this_year_sessions.latest() last_session = this_year_sessions.latest()
last_play_game = last_session.purchase.first_edition.game last_play_game = last_session.purchase.edition.game
last_play_date = last_session.timestamp_start.strftime(dateformat) last_play_date = last_session.timestamp_start.strftime(dateformat)
all_purchased_this_year_count = this_year_purchases_with_currency.count() all_purchased_this_year_count = this_year_purchases_with_currency.count()
@ -229,7 +224,7 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
else 0 else 0
), ),
"longest_session_game": ( "longest_session_game": (
longest_session.purchase.first_edition.game if longest_session else None longest_session.purchase.edition.game if longest_session else None
), ),
"highest_session_count": ( "highest_session_count": (
game_highest_session_count.session_count game_highest_session_count.session_count
@ -252,7 +247,6 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
"last_play_game": last_play_game, "last_play_game": last_play_game,
"last_play_date": last_play_date, "last_play_date": last_play_date,
"title": f"{year} Stats", "title": f"{year} Stats",
"stats_dropdown_year_range": available_stats_year_range(),
} }
request.session["return_path"] = request.path request.session["return_path"] = request.path
@ -268,7 +262,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
return HttpResponseRedirect(reverse("stats_alltime")) return HttpResponseRedirect(reverse("stats_alltime"))
this_year_sessions = Session.objects.filter( this_year_sessions = Session.objects.filter(
timestamp_start__year=year timestamp_start__year=year
).prefetch_related("purchase__editions") ).select_related("purchase__edition")
this_year_sessions_with_durations = this_year_sessions.annotate( this_year_sessions_with_durations = this_year_sessions.annotate(
duration=ExpressionWrapper( duration=ExpressionWrapper(
F("timestamp_end") - F("timestamp_start"), F("timestamp_end") - F("timestamp_start"),
@ -277,12 +271,12 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
) )
longest_session = this_year_sessions_with_durations.order_by("-duration").first() longest_session = this_year_sessions_with_durations.order_by("-duration").first()
this_year_games = Game.objects.filter( this_year_games = Game.objects.filter(
edition__purchases__session__in=this_year_sessions edition__purchase__session__in=this_year_sessions
).distinct() ).distinct()
this_year_games_with_session_counts = this_year_games.annotate( this_year_games_with_session_counts = this_year_games.annotate(
session_count=Count( session_count=Count(
"edition__purchases__session", "edition__purchase__session",
filter=Q(edition__purchases__session__timestamp_start__year=year), filter=Q(edition__purchase__session__timestamp_start__year=year),
) )
) )
game_highest_session_count = this_year_games_with_session_counts.order_by( game_highest_session_count = this_year_games_with_session_counts.order_by(
@ -300,10 +294,12 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
).distinct() ).distinct()
this_year_purchases = Purchase.objects.filter(date_purchased__year=year) this_year_purchases = Purchase.objects.filter(date_purchased__year=year)
this_year_purchases_with_currency = this_year_purchases.prefetch_related("editions") this_year_purchases_with_currency = this_year_purchases.select_related(
"edition"
).filter(price_currency__exact=selected_currency)
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter( this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
date_refunded=None date_refunded=None
).exclude(ownership_type=Purchase.DEMO) )
this_year_purchases_refunded = this_year_purchases_with_currency.refunded() this_year_purchases_refunded = this_year_purchases_with_currency.refunded()
this_year_purchases_unfinished_dropped_nondropped = ( this_year_purchases_unfinished_dropped_nondropped = (
@ -337,7 +333,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year) purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year)
purchases_finished_this_year_released_this_year = ( purchases_finished_this_year_released_this_year = (
purchases_finished_this_year.filter(editions__year_released=year).order_by( purchases_finished_this_year.filter(edition__year_released=year).order_by(
"date_finished" "date_finished"
) )
) )
@ -346,16 +342,16 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
).order_by("date_finished") ).order_by("date_finished")
this_year_spendings = this_year_purchases_without_refunded.aggregate( this_year_spendings = this_year_purchases_without_refunded.aggregate(
total_spent=Sum(F("converted_price")) total_spent=Sum(F("price"))
) )
total_spent = this_year_spendings["total_spent"] or 0 total_spent = this_year_spendings["total_spent"] or 0
games_with_playtime = ( games_with_playtime = (
Game.objects.filter(edition__purchases__session__in=this_year_sessions) Game.objects.filter(edition__purchase__session__in=this_year_sessions)
.annotate( .annotate(
total_playtime=Sum( total_playtime=Sum(
F("edition__purchases__session__duration_calculated") F("edition__purchase__session__duration_calculated")
+ F("edition__purchases__session__duration_manual") + F("edition__purchase__session__duration_manual")
) )
) )
.values("id", "name", "total_playtime") .values("id", "name", "total_playtime")
@ -370,9 +366,9 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
month["playtime"] = format_duration(month["playtime"], "%2.0H") month["playtime"] = format_duration(month["playtime"], "%2.0H")
highest_session_average_game = ( highest_session_average_game = (
Game.objects.filter(edition__purchases__session__in=this_year_sessions) Game.objects.filter(edition__purchase__session__in=this_year_sessions)
.annotate( .annotate(
session_average=Avg("edition__purchases__session__duration_calculated") session_average=Avg("edition__purchase__session__duration_calculated")
) )
.order_by("-session_average") .order_by("-session_average")
.first() .first()
@ -403,10 +399,10 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
last_play_game = None last_play_game = None
if this_year_sessions: if this_year_sessions:
first_session = this_year_sessions.earliest() first_session = this_year_sessions.earliest()
first_play_game = first_session.purchase.first_edition.game first_play_game = first_session.purchase.edition.game
first_play_date = first_session.timestamp_start.strftime(dateformat) first_play_date = first_session.timestamp_start.strftime(dateformat)
last_session = this_year_sessions.latest() last_session = this_year_sessions.latest()
last_play_game = last_session.purchase.first_edition.game last_play_game = last_session.purchase.edition.game
last_play_date = last_session.timestamp_start.strftime(dateformat) last_play_date = last_session.timestamp_start.strftime(dateformat)
all_purchased_this_year_count = this_year_purchases_with_currency.count() all_purchased_this_year_count = this_year_purchases_with_currency.count()
@ -423,7 +419,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
), ),
"total_games": this_year_played_purchases.count(), "total_games": this_year_played_purchases.count(),
"total_2023_games": this_year_played_purchases.filter( "total_2023_games": this_year_played_purchases.filter(
editions__year_released=year edition__year_released=year
).count(), ).count(),
"top_10_games_by_playtime": top_10_games_by_playtime, "top_10_games_by_playtime": top_10_games_by_playtime,
"year": year, "year": year,
@ -434,16 +430,16 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
"spent_per_game": int( "spent_per_game": int(
safe_division(total_spent, this_year_purchases_without_refunded_count) safe_division(total_spent, this_year_purchases_without_refunded_count)
), ),
"all_finished_this_year": purchases_finished_this_year.prefetch_related( "all_finished_this_year": purchases_finished_this_year.select_related(
"editions" "edition"
).order_by("date_finished"), ).order_by("date_finished"),
"all_finished_this_year_count": purchases_finished_this_year.count(), "all_finished_this_year_count": purchases_finished_this_year.count(),
"this_year_finished_this_year": purchases_finished_this_year_released_this_year.prefetch_related( "this_year_finished_this_year": purchases_finished_this_year_released_this_year.select_related(
"editions" "edition"
).order_by("date_finished"), ).order_by("date_finished"),
"this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(), "this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(),
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.prefetch_related( "purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.select_related(
"editions" "edition"
).order_by("date_finished"), ).order_by("date_finished"),
"total_sessions": this_year_sessions.count(), "total_sessions": this_year_sessions.count(),
"unique_days": unique_days["dates"], "unique_days": unique_days["dates"],
@ -473,7 +469,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
else 0 else 0
), ),
"longest_session_game": ( "longest_session_game": (
longest_session.purchase.first_edition.game if longest_session else None longest_session.purchase.edition.game if longest_session else None
), ),
"highest_session_count": ( "highest_session_count": (
game_highest_session_count.session_count game_highest_session_count.session_count
@ -497,7 +493,6 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
"last_play_date": last_play_date, "last_play_date": last_play_date,
"title": f"{year} Stats", "title": f"{year} Stats",
"month_playtimes": month_playtimes, "month_playtimes": month_playtimes,
"stats_dropdown_year_range": available_stats_year_range(),
} }
request.session["return_path"] = request.path request.session["return_path"] = request.path

View File

@ -13,8 +13,9 @@ from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from common.components import A, Button, Icon, LinkedPurchase, PurchasePrice from common.components import A, Button, Icon, LinkedNameWithPlatformIcon
from common.time import dateformat from common.time import dateformat
from common.utils import format_float_or_int
from games.forms import PurchaseForm from games.forms import PurchaseForm
from games.models import Edition, Purchase from games.models import Edition, Purchase
from games.views.general import use_custom_redirect from games.views.general import use_custom_redirect
@ -25,7 +26,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
context: dict[Any, Any] = {} context: dict[Any, Any] = {}
page_number = request.GET.get("page", 1) page_number = request.GET.get("page", 1)
limit = request.GET.get("limit", 10) limit = request.GET.get("limit", 10)
purchases = Purchase.objects.order_by("-date_purchased", "-created_at") purchases = Purchase.objects.order_by("-date_purchased")
page_obj = None page_obj = None
if int(limit) != 0: if int(limit) != 0:
paginator = Paginator(purchases, limit) paginator = Paginator(purchases, limit)
@ -48,6 +49,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
"Name", "Name",
"Type", "Type",
"Price", "Price",
"Currency",
"Infinite", "Infinite",
"Purchased", "Purchased",
"Refunded", "Refunded",
@ -58,9 +60,14 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
], ],
"rows": [ "rows": [
[ [
LinkedPurchase(purchase), LinkedNameWithPlatformIcon(
name=purchase.edition.name,
game_id=purchase.edition.game.pk,
platform=purchase.platform,
),
purchase.get_type_display(), purchase.get_type_display(),
PurchasePrice(purchase), format_float_or_int(purchase.price),
purchase.price_currency,
purchase.infinite, purchase.infinite,
purchase.date_purchased.strftime(dateformat), purchase.date_purchased.strftime(dateformat),
( (
@ -169,7 +176,7 @@ def add_purchase(request: HttpRequest, edition_id: int = 0) -> HttpResponse:
context["form"] = form context["form"] = form
context["title"] = "Add New Purchase" context["title"] = "Add New Purchase"
# context["script_name"] = "add_purchase.js" context["script_name"] = "add_purchase.js"
return render(request, "add_purchase.html", context) return render(request, "add_purchase.html", context)
@ -185,7 +192,7 @@ def edit_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
context["title"] = "Edit Purchase" context["title"] = "Edit Purchase"
context["form"] = form context["form"] = form
context["purchase_id"] = str(purchase_id) context["purchase_id"] = str(purchase_id)
# context["script_name"] = "add_purchase.js" context["script_name"] = "add_purchase.js"
return render(request, "add_purchase.html", context) return render(request, "add_purchase.html", context)
@ -196,12 +203,6 @@ def delete_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
return redirect("list_purchases") return redirect("list_purchases")
@login_required
def view_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
purchase = get_object_or_404(Purchase, id=purchase_id)
return render(request, "view_purchase.html", {"purchase": purchase})
@login_required @login_required
def drop_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse: def drop_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
purchase = get_object_or_404(Purchase, id=purchase_id) purchase = get_object_or_404(Purchase, id=purchase_id)

View File

@ -2,22 +2,13 @@ from typing import Any
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Q
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from common.components import ( from common.components import A, Button, Div, Icon, LinkedNameWithPlatformIcon, Popover
A,
Button,
Div,
Form,
Icon,
LinkedNameWithPlatformIcon,
Popover,
)
from common.time import ( from common.time import (
dateformat, dateformat,
durationformat, durationformat,
@ -33,24 +24,12 @@ from games.views.general import use_custom_redirect
@login_required @login_required
def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse: def list_sessions(request: HttpRequest) -> HttpResponse:
context: dict[Any, Any] = {} context: dict[Any, Any] = {}
page_number = request.GET.get("page", 1) page_number = request.GET.get("page", 1)
limit = request.GET.get("limit", 10) limit = request.GET.get("limit", 10)
sessions = Session.objects.order_by("-timestamp_start") sessions = Session.objects.order_by("-timestamp_start")
search_string = request.GET.get("search_string", search_string) last_session = sessions.latest()
if search_string != "":
sessions = sessions.filter(
Q(purchase__edition__name__icontains=search_string)
| Q(purchase__edition__game__name__icontains=search_string)
| Q(purchase__platform__name__icontains=search_string)
| Q(device__name__icontains=search_string)
| Q(device__type__icontains=search_string)
)
try:
last_session = sessions.latest()
except Session.DoesNotExist:
last_session = None
page_obj = None page_obj = None
if int(limit) != 0: if int(limit) != 0:
paginator = Paginator(sessions, limit) paginator = Paginator(sessions, limit)
@ -70,55 +49,37 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
"data": { "data": {
"header_action": Div( "header_action": Div(
children=[ children=[
Form( A(
children=[ url="add_session",
render_to_string( children=Button(
"cotton/search_field.html", icon=True,
{ size="xs",
"id": "search_string", children=[Icon("play"), "LOG"],
"search_string": search_string, ),
},
)
]
), ),
Div( A(
children=[ url=reverse(
A( "list_sessions_start_session_from_session",
url="add_session", args=[last_session.pk],
children=Button( ),
children=Popover(
popover_content=last_session.purchase.edition.name,
children=[
Button(
icon=True, icon=True,
color="gray",
size="xs", 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.first_edition.name,
children=[ children=[
Button( Icon("play"),
icon=True, truncate(
color="gray", f"{last_session.purchase.edition.name}"
size="xs", ),
children=[
Icon("play"),
truncate(
f"{last_session.purchase.first_edition.name}"
),
],
)
], ],
), )
) ],
if last_session ),
else "",
]
), ),
], ],
attributes=[("class", "flex justify-between")],
), ),
"columns": [ "columns": [
"Name", "Name",
@ -131,8 +92,8 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
"rows": [ "rows": [
[ [
LinkedNameWithPlatformIcon( LinkedNameWithPlatformIcon(
name=session.purchase.first_edition.name, name=session.purchase.edition.name,
game_id=session.purchase.first_edition.game.pk, game_id=session.purchase.edition.game.pk,
platform=session.purchase.platform, platform=session.purchase.platform,
), ),
f"{local_strftime(session.timestamp_start)}{f"{local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}", f"{local_strftime(session.timestamp_start)}{f"{local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}",
@ -189,11 +150,6 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
return render(request, "list_purchases.html", context) return render(request, "list_purchases.html", context)
@login_required
def search_sessions(request: HttpRequest) -> HttpResponse:
return list_sessions(request, search_string=request.GET.get("search_string", ""))
@login_required @login_required
def add_session(request: HttpRequest, purchase_id: int = 0) -> HttpResponse: def add_session(request: HttpRequest, purchase_id: int = 0) -> HttpResponse:
context = {} context = {}
@ -221,7 +177,6 @@ def add_session(request: HttpRequest, purchase_id: int = 0) -> HttpResponse:
form = SessionForm(initial=initial) form = SessionForm(initial=initial)
context["title"] = "Add New Session" context["title"] = "Add New Session"
context["script_name"] = "add_session.js"
context["form"] = form context["form"] = form
return render(request, "add_session.html", context) return render(request, "add_session.html", context)

View File

@ -4,7 +4,7 @@
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"npm-check-updates": "^16.14.20", "npm-check-updates": "^16.14.20",
"tailwindcss": "^3.4.14" "tailwindcss": "^3.4.4"
}, },
"dependencies": { "dependencies": {
"flowbite": "^2.4.1" "flowbite": "^2.4.1"

703
poetry.lock generated
View File

@ -1,4 +1,18 @@
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "aniso8601"
version = "9.0.1"
description = "A library for parsing ISO 8601 strings."
optional = false
python-versions = "*"
files = [
{file = "aniso8601-9.0.1-py2.py3-none-any.whl", hash = "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f"},
{file = "aniso8601-9.0.1.tar.gz", hash = "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"},
]
[package.extras]
dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"]
[[package]] [[package]]
name = "asgiref" name = "asgiref"
@ -15,16 +29,26 @@ files = [
tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
[[package]] [[package]]
name = "certifi" name = "beautifulsoup4"
version = "2024.8.30" version = "4.12.3"
description = "Python package for providing Mozilla's CA Bundle." description = "Screen-scraping library"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6.0"
files = [ files = [
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"},
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"},
] ]
[package.dependencies]
soupsieve = ">1.2"
[package.extras]
cchardet = ["cchardet"]
chardet = ["chardet"]
charset-normalizer = ["charset-normalizer"]
html5lib = ["html5lib"]
lxml = ["lxml"]
[[package]] [[package]]
name = "cfgv" name = "cfgv"
version = "3.4.0" version = "3.4.0"
@ -36,120 +60,6 @@ files = [
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
] ]
[[package]]
name = "charset-normalizer"
version = "3.4.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
files = [
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"},
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"},
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"},
{file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"},
{file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"},
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"},
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"},
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"},
{file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"},
{file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"},
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"},
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"},
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"},
{file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"},
{file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"},
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"},
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"},
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"},
{file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"},
{file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"},
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"},
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"},
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"},
{file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"},
{file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"},
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"},
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"},
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"},
{file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"},
{file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"},
{file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"},
{file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"},
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.7" version = "8.1.7"
@ -175,21 +85,6 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
[[package]]
name = "croniter"
version = "5.0.1"
description = "croniter provides iteration for datetime object with cron like format"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6"
files = [
{file = "croniter-5.0.1-py2.py3-none-any.whl", hash = "sha256:eb28439742291f6c10b181df1a5ecf421208b1fc62ef44501daec1780a0b09e9"},
{file = "croniter-5.0.1.tar.gz", hash = "sha256:7d9b1ef25b10eece48fdf29d8ac52f9b6252abff983ac614ade4f3276294019e"},
]
[package.dependencies]
python-dateutil = "*"
pytz = ">2021.1"
[[package]] [[package]]
name = "cssbeautifier" name = "cssbeautifier"
version = "1.15.1" version = "1.15.1"
@ -207,24 +102,24 @@ six = ">=1.13.0"
[[package]] [[package]]
name = "distlib" name = "distlib"
version = "0.3.9" version = "0.3.8"
description = "Distribution utilities" description = "Distribution utilities"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
{file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
] ]
[[package]] [[package]]
name = "django" name = "django"
version = "5.1.3" version = "5.1.1"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
files = [ files = [
{file = "Django-5.1.3-py3-none-any.whl", hash = "sha256:8b38a9a12da3ae00cb0ba72da985ec4b14de6345046b1e174b1fd7254398f818"}, {file = "Django-5.1.1-py3-none-any.whl", hash = "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f"},
{file = "Django-5.1.3.tar.gz", hash = "sha256:c0fa0e619c39325a169208caef234f90baa925227032ad3f44842ba14d75234a"}, {file = "Django-5.1.1.tar.gz", hash = "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2"},
] ]
[package.dependencies] [package.dependencies]
@ -238,17 +133,17 @@ bcrypt = ["bcrypt"]
[[package]] [[package]]
name = "django-cotton" name = "django-cotton"
version = "1.3.0" version = "0.9.37"
description = "Bringing component based design to Django templates." description = "Bringing component based design to Django templates."
optional = false optional = false
python-versions = "<4,>=3.8" python-versions = "<4,>=3.8"
files = [ files = [
{file = "django_cotton-1.3.0-py3-none-any.whl", hash = "sha256:a23f29b759c43423e2f901352c0810388839cc412f6985614153c6ccfcfc2595"}, {file = "django_cotton-0.9.37-py3-none-any.whl", hash = "sha256:b917102bee417eb749df25eae75a985db0749fb8ac9711ab57fb5f78de03d8e8"},
{file = "django_cotton-1.3.0.tar.gz", hash = "sha256:8f4a15dd55c8ee9182cf7234c228ea45d9fcdec1de125221bce8d05af035730a"}, {file = "django_cotton-0.9.37.tar.gz", hash = "sha256:0ccfc76d6edfc8dfaf0bb5d960c6695fff78cbcc1a3c52e263a0484b8dbbb9a5"},
] ]
[package.dependencies] [package.dependencies]
django = ">=4.2,<5.2" beautifulsoup4 = ">=4.12.2,<4.13.0"
[[package]] [[package]]
name = "django-debug-toolbar" name = "django-debug-toolbar"
@ -281,55 +176,18 @@ Django = ">=3.2"
[[package]] [[package]]
name = "django-htmx" name = "django-htmx"
version = "1.21.0" version = "1.19.0"
description = "Extensions for using Django with htmx." description = "Extensions for using Django with htmx."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.8"
files = [ files = [
{file = "django_htmx-1.21.0-py3-none-any.whl", hash = "sha256:64bc31463017a80552b767bc216ee5700248fa72e7ccd2963495e69afbdb6abe"}, {file = "django_htmx-1.19.0-py3-none-any.whl", hash = "sha256:875a642814e52278c1728842436beda2001847a493ab79fd82da3fb46ead140f"},
{file = "django_htmx-1.21.0.tar.gz", hash = "sha256:6ed3b42effd5980f22e68f36cd14ee4311bff3b6cb8435a89e27f45995691572"}, {file = "django_htmx-1.19.0.tar.gz", hash = "sha256:e7e17304e78e07f96eca0affc3ce1806edfdf3538bb7cb1912452b101f3e627d"},
] ]
[package.dependencies] [package.dependencies]
asgiref = ">=3.6" asgiref = ">=3.6"
django = ">=4.2" django = ">=3.2"
[[package]]
name = "django-picklefield"
version = "3.2"
description = "Pickled object field for Django"
optional = false
python-versions = ">=3"
files = [
{file = "django-picklefield-3.2.tar.gz", hash = "sha256:aa463f5d79d497dbe789f14b45180f00a51d0d670067d0729f352a3941cdfa4d"},
{file = "django_picklefield-3.2-py3-none-any.whl", hash = "sha256:e9a73539d110f69825d9320db18bcb82e5189ff48dbed41821c026a20497764c"},
]
[package.dependencies]
Django = ">=3.2"
[package.extras]
tests = ["tox"]
[[package]]
name = "django-q2"
version = "1.7.4"
description = "A multiprocessing distributed task queue for Django"
optional = false
python-versions = "<4,>=3.8"
files = [
{file = "django_q2-1.7.4-py3-none-any.whl", hash = "sha256:6eda6d56505822ee5ebc6c4eac1dde726f5dbf20ee9ea7080575535852e2671f"},
{file = "django_q2-1.7.4.tar.gz", hash = "sha256:56a3781cc480474fa9c04bbde62445b0a9b4195adc409bd963b8f593b0598c43"},
]
[package.dependencies]
django = ">=4.2,<6"
django-picklefield = ">=3.1,<4.0"
[package.extras]
rollbar = ["django-q-rollbar (>=0.1)"]
sentry = ["django-q-sentry (>=0.1)"]
testing = ["blessed (>=1.19.1,<2.0.0)", "boto3 (>=1.24.92,<2.0.0)", "croniter (>=2.0.1,<3.0.0)", "django-redis (>=5.2.0,<6.0.0)", "hiredis (>=2.0.0,<3.0.0)", "iron-mq (>=0.9,<0.10)", "psutil (>=5.9.2,<6.0.0)", "pymongo (>=4.2.0,<5.0.0)", "redis (>=4.3.4,<5.0.0)", "setproctitle (>=1.3.2,<2.0.0)"]
[[package]] [[package]]
name = "django-template-partials" name = "django-template-partials"
@ -351,12 +209,12 @@ tests = ["coverage", "django_coverage_plugin"]
[[package]] [[package]]
name = "djhtml" name = "djhtml"
version = "3.0.7" version = "3.0.6"
description = "Django/Jinja template indenter" description = "Django/Jinja template indenter"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "djhtml-3.0.7.tar.gz", hash = "sha256:558c905b092a0c8afcbed27dea2f50aa6eb853a658b309e4e0f2bb378bdf6178"}, {file = "djhtml-3.0.6.tar.gz", hash = "sha256:abfc4d7b4730432ca6a98322fbdf8ae9d6ba254ea57ba3759a10ecb293bc57de"},
] ]
[package.extras] [package.extras]
@ -364,43 +222,25 @@ dev = ["nox", "pre-commit"]
[[package]] [[package]]
name = "djlint" name = "djlint"
version = "1.36.1" version = "1.35.2"
description = "HTML Template Linter and Formatter" description = "HTML Template Linter and Formatter"
optional = false optional = false
python-versions = ">=3.9" python-versions = "<4.0,>=3.8"
files = [ files = [
{file = "djlint-1.36.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef40527fd6cd82cdd18f65a6bf5b486b767d2386f6c21f2ebd60e5d88f487fe8"}, {file = "djlint-1.35.2-py3-none-any.whl", hash = "sha256:4ba995bad378f2afa77c8ea56ba1c14429d9ff26a18e8ae23bc71eedb9152243"},
{file = "djlint-1.36.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4712de3dea172000a098da6a0cd709d158909b4964ba0f68bee584cef18b4878"}, {file = "djlint-1.35.2.tar.gz", hash = "sha256:318de9d4b9b0061a111f8f5164ecbacd8215f449dd4bd5a76d2a691c815ee103"},
{file = "djlint-1.36.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d01c1425170b7059d68a3b01709e1c31d2cd6520a1eb0166e6670fd250518a"},
{file = "djlint-1.36.1-cp310-cp310-win_amd64.whl", hash = "sha256:65585a97d3a37760b4c1fbd089a3573506ad0ab2885119322a66231f911d113f"},
{file = "djlint-1.36.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:607437a0a230462916858c269bc5dfd15ff71b27d15dfd1ad6e96b3da9cbd8f6"},
{file = "djlint-1.36.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ddc9ae6b83b288465f6685b24797adbde79952d6e1a5276026e5ef479bac76f"},
{file = "djlint-1.36.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:001e5124b0ebab60a2044134abd11ff11dee772e7c3caaa2c8d12eb5d3b1f1dc"},
{file = "djlint-1.36.1-cp311-cp311-win_amd64.whl", hash = "sha256:095d62f3cabbac08683c51c1d9dacab522b54437a2a317df9e134599360f7b89"},
{file = "djlint-1.36.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:210f319c2d22489aebc0e9c1acd5015ca3892b66fa35647344511b3c03fcbe82"},
{file = "djlint-1.36.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7aa3db13d7702c35f4e408325061d9d4e84d006c99bb3e55fddf2b2543736923"},
{file = "djlint-1.36.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f907e97f4d67f4423dc71671592891cfd9cd311aeef14db25633f292dbf7048"},
{file = "djlint-1.36.1-cp312-cp312-win_amd64.whl", hash = "sha256:abadf6b61dc53d81710f230542f57f2d470b7503cd3108ad8a0113271c0514dd"},
{file = "djlint-1.36.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f31646435385eec1d4b03dad7bebb5e4078d9893c60d490a685535bd6303c83"},
{file = "djlint-1.36.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4399477ac51f9c8147eedbef70aa8465eccba6759d875d1feec6782744aa168a"},
{file = "djlint-1.36.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c217b17d3ae3c0e3b5fff57fb708029cceda6e232f5a54ff1b3aeb43a7540"},
{file = "djlint-1.36.1-cp313-cp313-win_amd64.whl", hash = "sha256:1577490802ca4697af3488ed13066c9214ef0f625a96aa20d4f297e37aa19303"},
{file = "djlint-1.36.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ae356faf8180c7629ca705b7b9d8c9269b2c53273a1887a438a21b8fa263588"},
{file = "djlint-1.36.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2237ac5cecd2524960e1684f64ce358624b0d769b7404e5aad415750ad00edc9"},
{file = "djlint-1.36.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02c22352a49c053ff6260428ed571afb783011d20afc98b44bbe1dd2fa2d5418"},
{file = "djlint-1.36.1-cp39-cp39-win_amd64.whl", hash = "sha256:99a2debeea2e931b68360306fdbf13861e3d6f96037a9d882f3d4d5e44fdc319"},
{file = "djlint-1.36.1-py3-none-any.whl", hash = "sha256:950782b396dd82b74622c09d7e4c52328e56a3b03c8ac790c319708e5caa0686"},
{file = "djlint-1.36.1.tar.gz", hash = "sha256:f7260637ed72c270fa6dd4a87628e1a21c49b24a46df52e4e26f44d4934fb97c"},
] ]
[package.dependencies] [package.dependencies]
click = ">=8.0.1" click = ">=8.0.1"
colorama = ">=0.4.4" colorama = ">=0.4.4"
cssbeautifier = ">=1.14.4" cssbeautifier = ">=1.14.4"
html-tag-names = ">=0.1.2"
html-void-elements = ">=0.1.0"
jsbeautifier = ">=1.14.4" jsbeautifier = ">=1.14.4"
json5 = ">=0.9.11" json5 = ">=0.9.11"
pathspec = ">=0.12" pathspec = ">=0.12.0"
pyyaml = ">=6" PyYAML = ">=6.0"
regex = ">=2023" regex = ">=2023"
tqdm = ">=4.62.2" tqdm = ">=4.62.2"
@ -416,40 +256,39 @@ files = [
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "3.16.1" version = "3.16.0"
description = "A platform independent file lock." description = "A platform independent file lock."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"},
{file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, {file = "filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec"},
] ]
[package.extras] [package.extras]
docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] 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.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.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)"] typing = ["typing-extensions (>=4.12.2)"]
[[package]] [[package]]
name = "graphene" name = "graphene"
version = "3.4.3" version = "3.3"
description = "GraphQL Framework for Python" description = "GraphQL Framework for Python"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "graphene-3.4.3-py2.py3-none-any.whl", hash = "sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71"}, {file = "graphene-3.3-py2.py3-none-any.whl", hash = "sha256:bb3810be33b54cb3e6969506671eb72319e8d7ba0d5ca9c8066472f75bf35a38"},
{file = "graphene-3.4.3.tar.gz", hash = "sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa"}, {file = "graphene-3.3.tar.gz", hash = "sha256:529bf40c2a698954217d3713c6041d69d3f719ad0080857d7ee31327112446b0"},
] ]
[package.dependencies] [package.dependencies]
aniso8601 = ">=8,<10"
graphql-core = ">=3.1,<3.3" graphql-core = ">=3.1,<3.3"
graphql-relay = ">=3.1,<3.3" graphql-relay = ">=3.1,<3.3"
python-dateutil = ">=2.7.0,<3"
typing-extensions = ">=4.7.1,<5"
[package.extras] [package.extras]
dev = ["coveralls (>=3.3,<5)", "mypy (>=1.10,<2)", "pytest (>=8,<9)", "pytest-asyncio (>=0.16,<2)", "pytest-benchmark (>=4,<5)", "pytest-cov (>=5,<6)", "pytest-mock (>=3,<4)", "ruff (==0.5.0)", "types-python-dateutil (>=2.8.1,<3)"] dev = ["black (==22.3.0)", "coveralls (>=3.3,<4)", "flake8 (>=4,<5)", "iso8601 (>=1,<2)", "mock (>=4,<5)", "pytest (>=6,<7)", "pytest-asyncio (>=0.16,<2)", "pytest-benchmark (>=3.4,<4)", "pytest-cov (>=3,<4)", "pytest-mock (>=3,<4)", "pytz (==2022.1)", "snapshottest (>=0.6,<1)"]
test = ["coveralls (>=3.3,<5)", "pytest (>=8,<9)", "pytest-asyncio (>=0.16,<2)", "pytest-benchmark (>=4,<5)", "pytest-cov (>=5,<6)", "pytest-mock (>=3,<4)"] test = ["coveralls (>=3.3,<4)", "iso8601 (>=1,<2)", "mock (>=4,<5)", "pytest (>=6,<7)", "pytest-asyncio (>=0.16,<2)", "pytest-benchmark (>=3.4,<4)", "pytest-cov (>=3,<4)", "pytest-mock (>=3,<4)", "pytz (==2022.1)", "snapshottest (>=0.6,<1)"]
[[package]] [[package]]
name = "graphene-django" name = "graphene-django"
@ -477,13 +316,13 @@ test = ["coveralls", "django-filter (>=22.1)", "djangorestframework (>=3.6.3)",
[[package]] [[package]]
name = "graphql-core" name = "graphql-core"
version = "3.2.5" version = "3.2.4"
description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL."
optional = false optional = false
python-versions = "<4,>=3.6" python-versions = "<4,>=3.6"
files = [ files = [
{file = "graphql_core-3.2.5-py3-none-any.whl", hash = "sha256:2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a"}, {file = "graphql-core-3.2.4.tar.gz", hash = "sha256:acbe2e800980d0e39b4685dd058c2f4042660b89ebca38af83020fd872ff1264"},
{file = "graphql_core-3.2.5.tar.gz", hash = "sha256:e671b90ed653c808715645e3998b7ab67d382d55467b7e2978549111bbabf8d5"}, {file = "graphql_core-3.2.4-py3-none-any.whl", hash = "sha256:1604f2042edc5f3114f49cac9d77e25863be51b23a54a61a23245cf32f6476f0"},
] ]
[[package]] [[package]]
@ -532,34 +371,42 @@ files = [
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
] ]
[[package]]
name = "html-tag-names"
version = "0.1.2"
description = "List of known HTML tag names"
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "html-tag-names-0.1.2.tar.gz", hash = "sha256:04924aca48770f36b5a41c27e4d917062507be05118acb0ba869c97389084297"},
{file = "html_tag_names-0.1.2-py3-none-any.whl", hash = "sha256:eeb69ef21078486b615241f0393a72b41352c5219ee648e7c61f5632d26f0420"},
]
[[package]]
name = "html-void-elements"
version = "0.1.0"
description = "List of HTML void tag names."
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "html-void-elements-0.1.0.tar.gz", hash = "sha256:931b88f84cd606fee0b582c28fcd00e41d7149421fb673e1e1abd2f0c4f231f0"},
{file = "html_void_elements-0.1.0-py3-none-any.whl", hash = "sha256:784cf39db03cdeb017320d9301009f8f3480f9d7b254d0974272e80e0cb5e0d2"},
]
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.6.2" version = "2.6.0"
description = "File identification library for Python" description = "File identification library for Python"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.8"
files = [ files = [
{file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"}, {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"},
{file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"}, {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"},
] ]
[package.extras] [package.extras]
license = ["ukkonen"] license = ["ukkonen"]
[[package]]
name = "idna"
version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
files = [
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
]
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.0.0" version = "2.0.0"
@ -627,43 +474,38 @@ testing = ["coverage", "pyyaml"]
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "1.13.0" version = "1.11.2"
description = "Optional static typing for Python" description = "Optional static typing for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"},
{file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"},
{file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, {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.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"},
{file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"},
{file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"},
{file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"},
{file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, {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.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"},
{file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"},
{file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"},
{file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"},
{file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, {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.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"},
{file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"},
{file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"},
{file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"},
{file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, {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.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"},
{file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"},
{file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"},
{file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"},
{file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, {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.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"},
{file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"},
{file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"},
{file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"},
{file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"},
{file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"},
{file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"},
{file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"},
{file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"},
] ]
[package.dependencies] [package.dependencies]
@ -672,7 +514,6 @@ typing-extensions = ">=4.6.0"
[package.extras] [package.extras]
dmypy = ["psutil (>=4.0)"] dmypy = ["psutil (>=4.0)"]
faster-cache = ["orjson"]
install-types = ["pip"] install-types = ["pip"]
mypyc = ["setuptools (>=50)"] mypyc = ["setuptools (>=50)"]
reports = ["lxml"] reports = ["lxml"]
@ -701,13 +542,13 @@ files = [
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "24.2" version = "24.1"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
] ]
[[package]] [[package]]
@ -723,19 +564,19 @@ files = [
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.3.6" version = "4.3.1"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.1-py3-none-any.whl", hash = "sha256:facaa5a3c57aa1e053e3da7b49e0cc31fe0113ca42a4659d5c2e98e545624afe"},
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, {file = "platformdirs-4.3.1.tar.gz", hash = "sha256:63b79589009fa8159973601dd4563143396b35c5f93a58b36f9049ff046949b1"},
] ]
[package.extras] [package.extras]
docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
type = ["mypy (>=1.11.2)"] type = ["mypy (>=1.8)"]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
@ -788,13 +629,13 @@ test = ["coveralls", "futures", "mock", "pytest (>=2.7.3)", "pytest-benchmark",
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.3.3" version = "8.3.2"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
] ]
[package.dependencies] [package.dependencies]
@ -806,31 +647,6 @@ pluggy = ">=1.5,<2"
[package.extras] [package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
]
[package.dependencies]
six = ">=1.5"
[[package]]
name = "pytz"
version = "2024.2"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
files = [
{file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"},
{file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"},
]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.2" version = "6.0.2"
@ -895,128 +711,92 @@ files = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "2024.11.6" version = "2024.7.24"
description = "Alternative regular expression module, to replace re." description = "Alternative regular expression module, to replace re."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"},
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"},
{file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, {file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, {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.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, {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.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"},
{file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, {file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"},
{file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, {file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"},
{file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"},
{file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"},
{file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, {file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, {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.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"},
{file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, {file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"},
{file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, {file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"},
{file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"},
{file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"},
{file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, {file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, {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.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"},
{file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, {file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"},
{file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, {file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"},
{file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"},
{file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"},
{file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, {file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, {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.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, {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.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"},
{file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"},
{file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, {file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"},
{file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, {file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"},
{file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"},
{file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, {file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, {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.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, {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.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"},
{file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"},
{file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, {file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"},
{file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, {file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"},
{file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, {file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"},
{file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"},
{file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"},
{file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"},
{file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"},
] ]
[[package]]
name = "requests"
version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
@ -1028,6 +808,17 @@ files = [
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
] ]
[[package]]
name = "soupsieve"
version = "2.6"
description = "A modern CSS selector implementation for Beautiful Soup."
optional = false
python-versions = ">=3.8"
files = [
{file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"},
{file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"},
]
[[package]] [[package]]
name = "sqlparse" name = "sqlparse"
version = "0.5.1" version = "0.5.1"
@ -1056,13 +847,13 @@ files = [
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "4.67.0" version = "4.66.5"
description = "Fast, Extensible Progress Meter" description = "Fast, Extensible Progress Meter"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "tqdm-4.67.0-py3-none-any.whl", hash = "sha256:0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be"}, {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"},
{file = "tqdm-4.67.0.tar.gz", hash = "sha256:fe5a6f95e6fe0b9755e9469b77b9c3cf850048224ecaa8293d7d2d31f97d869a"}, {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"},
] ]
[package.dependencies] [package.dependencies]
@ -1070,7 +861,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras] [package.extras]
dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"]
discord = ["requests"]
notebook = ["ipywidgets (>=6)"] notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"] slack = ["slack-sdk"]
telegram = ["requests"] telegram = ["requests"]
@ -1088,32 +878,15 @@ files = [
[[package]] [[package]]
name = "tzdata" name = "tzdata"
version = "2024.2" version = "2024.1"
description = "Provider of IANA time zone data" description = "Provider of IANA time zone data"
optional = false optional = false
python-versions = ">=2" python-versions = ">=2"
files = [ files = [
{file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
{file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
] ]
[[package]]
name = "urllib3"
version = "2.2.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
files = [
{file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
{file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.30.6" version = "0.30.6"
@ -1134,13 +907,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.27.1" version = "20.26.4"
description = "Virtual Python Environment builder" description = "Virtual Python Environment builder"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.7"
files = [ files = [
{file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, {file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"},
{file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, {file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"},
] ]
[package.dependencies] [package.dependencies]
@ -1155,4 +928,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "b5bb46a6591964aec145637cd9a412a681f2cc5e7e4fdd6fd9ecb0fe8724b8e3" content-hash = "508565632b7642265ad85a540a98a9ff3fa1e936eb26c0b7efae0707429b4c17"

View File

@ -28,12 +28,9 @@ graphene-django = "^3.2.0"
django-htmx = "^1.18.0" django-htmx = "^1.18.0"
django-template-partials = "^24.2" django-template-partials = "^24.2"
markdown = "^3.6" markdown = "^3.6"
django-cotton = "^1.2.1"
django-q2 = "^1.7.4"
croniter = "^5.0.1" django-cotton = "^0.9.34"
requests = "^2.32.3"
pyyaml = "^6.0.2"
[tool.isort] [tool.isort]
profile = "black" profile = "black"

View File

@ -42,18 +42,8 @@ INSTALLED_APPS = [
"graphene_django", "graphene_django",
"django_htmx", "django_htmx",
"django_cotton", "django_cotton",
"django_q",
] ]
Q_CLUSTER = {
"name": "DjangoQ",
"workers": 4,
"recycle": 500,
"timeout": 60,
"retry": 120,
"orm": "default",
}
GRAPHENE = {"SCHEMA": "games.schema.schema"} GRAPHENE = {"SCHEMA": "games.schema.schema"}
if DEBUG: if DEBUG:
@ -94,7 +84,6 @@ TEMPLATES = [
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
"games.views.general.model_counts", "games.views.general.model_counts",
"games.views.general.global_current_year",
], ],
"builtins": [ "builtins": [
"template_partials.templatetags.partials", "template_partials.templatetags.partials",