Compare commits
	
		
			43 Commits
		
	
	
		
			4b45127335
			...
			fix-csrf
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 615d4e59c6 | |||
| 83f075e49d | |||
| d3682368b4 | |||
| c9b2d5bd8d | |||
| 0d20b543b0 | |||
| f7b69f7704 | |||
| 1ccfdc321a | |||
| 25a58c2732 | |||
| 270d9f7296 | |||
| 2939b4a515 | |||
| d029fda896 | |||
| 9dead362c1 | |||
| d81dba727b | |||
| f550978e4a | |||
| db5de81c09 | |||
| 15ed6504b1 | |||
| fd9bf8c026 | |||
| 5172c38c16 | |||
| 9c56ed4ce8 | |||
| d00bb1cd06 | |||
| bedfbb7f31 | |||
| f2b08cd1cd | |||
| 5ad0e52787 | |||
| f7ec07994f | |||
| 03e89a92c7 | |||
| 76bf03b482 | |||
| e6b5804e37 | |||
| 2807c5e00e | |||
| 8efce77062 | |||
| 89be0c031b | |||
| 4e67735de8 | |||
| 869e0e0fe0 | |||
| 85f52fc735 | |||
| 34ce1e9b05 | |||
| 67f5090bf8 | |||
| 51d5306f91 | |||
| 66a49ff911 | |||
| 3e32261d4a | |||
| 9b07758198 | |||
| c57f969a00 | |||
| fd7fc7c710 | |||
| 32f10e183e | |||
| fdb9aa8e84 | 
							
								
								
									
										1
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | src/web/static/* | ||||||
| @ -1,9 +1,16 @@ | |||||||
| --- | --- | ||||||
| kind: pipeline | kind: pipeline | ||||||
| type: docker | type: docker | ||||||
| name: build image | name: default | ||||||
|  |  | ||||||
| steps: | steps: | ||||||
|  | - name: test | ||||||
|  |   image: python:3.10 | ||||||
|  |   commands: | ||||||
|  |     - python -m pip install poetry | ||||||
|  |     - poetry install | ||||||
|  |     - poetry env info | ||||||
|  |     - poetry run pytest | ||||||
| - name: build container | - name: build container | ||||||
|   image: plugins/docker |   image: plugins/docker | ||||||
|   settings: |   settings: | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								.githooks/pre-commit
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										14
									
								
								.githooks/pre-commit
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | set -euo pipefail | ||||||
|  | echo "----------------" | ||||||
|  | echo "Pre-commit hooks" | ||||||
|  | echo "================" | ||||||
|  | BASE_VERSION_NUMBER=$(git describe --tags --abbrev=0) | ||||||
|  | FULL_VERSION_NUMBER=$(git describe --tags) | ||||||
|  | echo "Updating "VERSION_NUMBER" in Dockerfile to $FULL_VERSION_NUMBER" | ||||||
|  | sed -i "s/^ENV VERSION_NUMBER.*$/ENV VERSION_NUMBER ${FULL_VERSION_NUMBER}/" Dockerfile | ||||||
|  | echo "Updating "version" in pyproject.toml to $BASE_VERSION_NUMBER" | ||||||
|  | sed -i "s/^version = \".*\"$/version = \"${BASE_VERSION_NUMBER}\"/" pyproject.toml | ||||||
|  | git add Dockerfile | ||||||
|  | git add pyproject.toml | ||||||
|  | echo "----------------" | ||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,5 +1,8 @@ | |||||||
| __pycache__ | __pycache__ | ||||||
|  | .mypy_cache | ||||||
|  | .pytest_cache | ||||||
| .venv | .venv | ||||||
| node_modules | node_modules | ||||||
| package-lock.json | package-lock.json | ||||||
| db.sqlite3 | db.sqlite3 | ||||||
|  | src/web/static | ||||||
							
								
								
									
										8
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | { | ||||||
|  |     "python.testing.pytestArgs": [ | ||||||
|  |         "tests" | ||||||
|  |     ], | ||||||
|  |     "python.testing.unittestEnabled": false, | ||||||
|  |     "python.testing.pytestEnabled": true, | ||||||
|  |     "python.analysis.typeCheckingMode": "basic" | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @ -1,4 +1,25 @@ | |||||||
| ## Unreleased | ## Unreleased | ||||||
|  |  | ||||||
|  | * Fix collectstaticfiles causing error when restarting container (https://git.kucharczyk.xyz/lukas/timetracker/issues/23) | ||||||
|  |  | ||||||
|  | ## 0.1.3 / 2023-01-08 15:23+01:00 | ||||||
|  |  | ||||||
|  | * Fix CSRF error (https://git.kucharczyk.xyz/lukas/timetracker/pulls/22) | ||||||
|  |  | ||||||
|  | ## 0.1.2 / 2023-01-07 22:05+01:00 | ||||||
|  |  | ||||||
|  | * Switch to Uvicorn/Gunicorn + Caddy (https://git.kucharczyk.xyz/lukas/timetracker/pulls/4) | ||||||
|  |  | ||||||
|  | ## 0.1.1 / 2023-01-05 23:26+01:00 | ||||||
|  | * Order by timestamp_start by default | ||||||
|  | * Add pre-commit hook to update version | ||||||
|  | * Improve the newcomer experience by guiding through each step | ||||||
|  | * Fix errors with empty database | ||||||
|  | * Fix negative playtimes being considered positive | ||||||
|  | * Add %d for days to common.util.time.format_duration | ||||||
|  | * Set up tests, add tests for common.util.time | ||||||
|  | * Display total hours played on homepage | ||||||
|  | * Add format_duration to common.util.time | ||||||
| * Allow deleting sessions | * Allow deleting sessions | ||||||
| * Redirect after adding game/platform/purchase/session | * Redirect after adding game/platform/purchase/session | ||||||
| * Fix display of duration_manual | * Fix display of duration_manual | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								Caddyfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Caddyfile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | { | ||||||
|  |     auto_https off | ||||||
|  |     admin off | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :8000 { | ||||||
|  |     handle_path /static/* { | ||||||
|  |         root * src/web/static/ | ||||||
|  |         file_server | ||||||
|  |     } | ||||||
|  |     handle { | ||||||
|  |         reverse_proxy :8001 | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										31
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								Dockerfile
									
									
									
									
									
								
							| @ -1,16 +1,31 @@ | |||||||
| FROM python:3.10-slim-bullseye | FROM node as css | ||||||
| ENV VIRTUAL_ENV=/opt/venv | WORKDIR /app | ||||||
| RUN python3 -m venv pip $VIRTUAL_ENV | COPY . /app | ||||||
| ENV PATH="$VIRTUAL_ENV/bin:$PATH" | RUN npm install && \ | ||||||
| RUN pip install --no-cache-dir poetry |     npx tailwindcss -i ./src/input.css -o ./src/web/tracker/static/base.css --minify | ||||||
| RUN useradd --create-home --uid 1000 timetracker |  | ||||||
|  | FROM python:3.10.9-alpine | ||||||
|  |  | ||||||
|  | ENV VERSION_NUMBER 0.1.2-3-g83f075e | ||||||
|  | ENV PROD 1 | ||||||
|  |  | ||||||
|  | RUN apk add \ | ||||||
|  |     bash \ | ||||||
|  |     vim \ | ||||||
|  |     curl \ | ||||||
|  |     caddy | ||||||
|  | RUN adduser -D -u 1000 timetracker | ||||||
| WORKDIR /home/timetracker/app | WORKDIR /home/timetracker/app | ||||||
| COPY . /home/timetracker/app/ | COPY . /home/timetracker/app/ | ||||||
| RUN chown -R timetracker:timetracker /home/timetracker/app | RUN chown -R timetracker:timetracker /home/timetracker/app | ||||||
| RUN poetry install --without dev | COPY --from=css /app/src/web/tracker/static/base.css /home/timetracker/app/src/web/tracker/static/base.css | ||||||
| COPY entrypoint.sh / | COPY entrypoint.sh / | ||||||
| RUN chmod +x /entrypoint.sh | RUN chmod +x /entrypoint.sh | ||||||
|  |  | ||||||
| USER timetracker | USER timetracker | ||||||
|  | ENV PATH="$PATH:/home/timetracker/.local/bin" | ||||||
|  | RUN pip install --no-cache-dir poetry | ||||||
|  | RUN poetry install --without dev | ||||||
|  |  | ||||||
| EXPOSE 8000 | EXPOSE 8000 | ||||||
| ENV VERSION_NUMBER 0.1.0-18-gb8a15e4 |  | ||||||
| ENTRYPOINT [ "/entrypoint.sh" ] | ENTRYPOINT [ "/entrypoint.sh" ] | ||||||
							
								
								
									
										48
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								Makefile
									
									
									
									
									
								
							| @ -1,8 +1,6 @@ | |||||||
| .PHONY: createsuperuser shell |  | ||||||
|  |  | ||||||
| all: css migrate | all: css migrate | ||||||
|  |  | ||||||
| initialize: npm css migrate loadplatforms | initialize: npm css migrate sethookdir loadplatforms | ||||||
|  |  | ||||||
| HTMLFILES := $(shell find src/web/tracker/templates -type f) | HTMLFILES := $(shell find src/web/tracker/templates -type f) | ||||||
|  |  | ||||||
| @ -16,25 +14,51 @@ css-dev: css | |||||||
| 	npx tailwindcss -i ./src/input.css -o  ./src/web/tracker/static/base.css --watch | 	npx tailwindcss -i ./src/input.css -o  ./src/web/tracker/static/base.css --watch | ||||||
|  |  | ||||||
| makemigrations: | makemigrations: | ||||||
| 	python src/web/manage.py makemigrations | 	poetry run python src/web/manage.py makemigrations | ||||||
|  |  | ||||||
| migrate: makemigrations | migrate: makemigrations | ||||||
| 	python src/web/manage.py migrate | 	poetry run python src/web/manage.py migrate | ||||||
|  |  | ||||||
| dev: migrate | dev: migrate sethookdir | ||||||
| 	python src/web/manage.py runserver | 	poetry run python src/web/manage.py runserver_plus | ||||||
|  |  | ||||||
|  | caddy: | ||||||
|  | 	caddy run --watch | ||||||
|  |  | ||||||
|  | dev-prod: migrate collectstatic sethookdir | ||||||
|  | 	cd src/web/; PROD=1 poetry run python -m gunicorn --bind 0.0.0.0:8001 web.asgi:application -k uvicorn.workers.UvicornWorker | ||||||
|  |  | ||||||
| dumptracker: | dumptracker: | ||||||
| 	python src/web/manage.py dumpdata --format yaml tracker --output tracker_fixture.yaml | 	poetry run python src/web/manage.py dumpdata --format yaml tracker --output tracker_fixture.yaml | ||||||
|  |  | ||||||
| loadplatforms: | loadplatforms: | ||||||
| 	python src/web/manage.py loaddata platforms.yaml | 	poetry run python src/web/manage.py loaddata platforms.yaml | ||||||
|  |  | ||||||
| loadsample: | loadsample: | ||||||
| 	python src/web/manage.py loaddata sample.yaml | 	poetry run python src/web/manage.py loaddata sample.yaml | ||||||
|  |  | ||||||
| createsuperuser: | createsuperuser: | ||||||
| 	python src/web/manage.py createsuperuser | 	poetry run python src/web/manage.py createsuperuser | ||||||
|  |  | ||||||
| shell: | shell: | ||||||
| 	python src/web/manage.py shell | 	poetry run python src/web/manage.py shell | ||||||
|  |  | ||||||
|  | collectstatic: | ||||||
|  | 	poetry run python src/web/manage.py collectstatic -c --no-input | ||||||
|  |  | ||||||
|  | poetry.lock: pyproject.toml | ||||||
|  | 	poetry install | ||||||
|  |  | ||||||
|  | test: poetry.lock | ||||||
|  | 	poetry run pytest | ||||||
|  |  | ||||||
|  | sethookdir: | ||||||
|  | 	git config core.hooksPath .githooks | ||||||
|  |  | ||||||
|  | date: | ||||||
|  | 	python3 -c 'import datetime; from zoneinfo import ZoneInfo; print(datetime.datetime.isoformat(datetime.datetime.now(ZoneInfo("Europe/Prague")), timespec="minutes", sep=" "))' | ||||||
|  |  | ||||||
|  | cleanstatic: | ||||||
|  | 	rm -r src/web/static/* | ||||||
|  |  | ||||||
|  | clean: cleanstatic | ||||||
|  | |||||||
							
								
								
									
										17
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | --- | ||||||
|  | services: | ||||||
|  |   timetracker: | ||||||
|  |     image: registry.kucharczyk.xyz/timetracker | ||||||
|  |     build: | ||||||
|  |       context: . | ||||||
|  |       dockerfile: Dockerfile | ||||||
|  |     container_name: timetracker | ||||||
|  |     environment: | ||||||
|  |       - TZ=Europe/Prague | ||||||
|  |       - CSRF_TRUSTED_ORIGINS="https://tracker.kucharczyk.xyz" | ||||||
|  |     user: "1000" | ||||||
|  |     # volumes: | ||||||
|  |     #   - "db:/home/timetracker/app/src/web/db.sqlite3" | ||||||
|  |     ports: | ||||||
|  |       - "8000:8000" | ||||||
|  |     restart: unless-stopped | ||||||
| @ -1,8 +1,13 @@ | |||||||
| #!/bin/bash | #!/bin/bash | ||||||
| # Apply database migrations | # Apply database migrations | ||||||
|  | set -euo pipefail | ||||||
| echo "Apply database migrations" | echo "Apply database migrations" | ||||||
| python src/web/manage.py migrate | poetry run python src/web/manage.py migrate | ||||||
|  |  | ||||||
|  | echo "Collect static files" | ||||||
|  | poetry run python src/web/manage.py collectstatic --clear --no-input | ||||||
|  |  | ||||||
| # Start server |  | ||||||
| echo "Starting server" | echo "Starting server" | ||||||
| python src/web/manage.py runserver 0.0.0.0:8000 | caddy start | ||||||
|  | cd src/web || exit | ||||||
|  | poetry run python -m gunicorn --bind 0.0.0.0:8001 web.asgi:application -k uvicorn.workers.UvicornWorker | ||||||
|  | |||||||
							
								
								
									
										536
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										536
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @ -15,6 +15,25 @@ files = [ | |||||||
| [package.extras] | [package.extras] | ||||||
| tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "attrs" | ||||||
|  | version = "22.2.0" | ||||||
|  | description = "Classes Without Boilerplate" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.6" | ||||||
|  | files = [ | ||||||
|  |     {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, | ||||||
|  |     {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] | ||||||
|  | dev = ["attrs[docs,tests]"] | ||||||
|  | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] | ||||||
|  | tests = ["attrs[tests-no-zope]", "zope.interface"] | ||||||
|  | tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "black" | name = "black" | ||||||
| version = "22.12.0" | version = "22.12.0" | ||||||
| @ -54,7 +73,7 @@ uvloop = ["uvloop (>=0.15.2)"] | |||||||
| name = "click" | name = "click" | ||||||
| version = "8.1.3" | version = "8.1.3" | ||||||
| description = "Composable command line interface toolkit" | description = "Composable command line interface toolkit" | ||||||
| category = "dev" | category = "main" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7" | python-versions = ">=3.7" | ||||||
| files = [ | files = [ | ||||||
| @ -69,7 +88,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} | |||||||
| name = "colorama" | name = "colorama" | ||||||
| version = "0.4.6" | version = "0.4.6" | ||||||
| description = "Cross-platform colored terminal text." | description = "Cross-platform colored terminal text." | ||||||
| category = "dev" | category = "main" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" | ||||||
| files = [ | files = [ | ||||||
| @ -77,16 +96,32 @@ files = [ | |||||||
|     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, |     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "cssbeautifier" | ||||||
|  | version = "1.14.7" | ||||||
|  | description = "CSS unobfuscator and beautifier." | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = "*" | ||||||
|  | files = [ | ||||||
|  |     {file = "cssbeautifier-1.14.7.tar.gz", hash = "sha256:be7f1ea7a7b009f0172c2c0d0bebb2d136346e786f7182185ea944affb52135a"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.dependencies] | ||||||
|  | editorconfig = ">=0.12.2" | ||||||
|  | jsbeautifier = "*" | ||||||
|  | six = ">=1.13.0" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "django" | name = "django" | ||||||
| version = "4.1.4" | version = "4.1.5" | ||||||
| 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." | ||||||
| category = "main" | category = "main" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "Django-4.1.4-py3-none-any.whl", hash = "sha256:0b223bfa55511f950ff741983d408d78d772351284c75e9f77d2b830b6b4d148"}, |     {file = "Django-4.1.5-py3-none-any.whl", hash = "sha256:4b214a05fe4c99476e99e2445c8b978c8369c18d4dea8e22ec412862715ad763"}, | ||||||
|     {file = "Django-4.1.4.tar.gz", hash = "sha256:d38a4e108d2386cb9637da66a82dc8d0733caede4c83c4afdbda78af4214211b"}, |     {file = "Django-4.1.5.tar.gz", hash = "sha256:ff56ebd7ead0fd5dbe06fe157b0024a7aaea2e0593bb3785fb594cf94dad58ef"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -98,6 +133,242 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} | |||||||
| argon2 = ["argon2-cffi (>=19.1.0)"] | argon2 = ["argon2-cffi (>=19.1.0)"] | ||||||
| bcrypt = ["bcrypt"] | bcrypt = ["bcrypt"] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "django-extensions" | ||||||
|  | version = "3.2.1" | ||||||
|  | description = "Extensions for Django" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.6" | ||||||
|  | files = [ | ||||||
|  |     {file = "django-extensions-3.2.1.tar.gz", hash = "sha256:2a4f4d757be2563cd1ff7cfdf2e57468f5f931cc88b23cf82ca75717aae504a4"}, | ||||||
|  |     {file = "django_extensions-3.2.1-py3-none-any.whl", hash = "sha256:421464be390289513f86cb5e18eb43e5dc1de8b4c27ba9faa3b91261b0d67e09"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.dependencies] | ||||||
|  | Django = ">=3.2" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "djhtml" | ||||||
|  | version = "1.5.2" | ||||||
|  | description = "Django/Jinja template indenter" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = "*" | ||||||
|  | files = [ | ||||||
|  |     {file = "djhtml-1.5.2.tar.gz", hash = "sha256:b54c4ab6effaf3dbe87d616ba30304f1dba22f07127a563df4130a71acc290ea"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | dev = ["black", "flake8", "isort", "nox", "pre-commit"] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "djlint" | ||||||
|  | version = "1.19.11" | ||||||
|  | description = "HTML Template Linter and Formatter" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7.2,<4.0.0" | ||||||
|  | files = [ | ||||||
|  |     {file = "djlint-1.19.11-py3-none-any.whl", hash = "sha256:c19d732c79b660f7d406517254bfd3e98f6c3c2de0cb300766b5555077d4ae08"}, | ||||||
|  |     {file = "djlint-1.19.11.tar.gz", hash = "sha256:98bbe094a6f176258a578b16c492cbbb9384e3a3b447c9241ee4a6427d703402"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.dependencies] | ||||||
|  | click = ">=8.0.1,<9.0.0" | ||||||
|  | colorama = ">=0.4.4,<0.5.0" | ||||||
|  | cssbeautifier = ">=1.14.4,<2.0.0" | ||||||
|  | html-tag-names = ">=0.1.2,<0.2.0" | ||||||
|  | html-void-elements = ">=0.1.0,<0.2.0" | ||||||
|  | importlib-metadata = ">=6.0.0,<7.0.0" | ||||||
|  | jsbeautifier = ">=1.14.4,<2.0.0" | ||||||
|  | pathspec = ">=0.10.0,<0.11.0" | ||||||
|  | PyYAML = ">=6.0,<7.0" | ||||||
|  | regex = ">=2022.1.18,<2023.0.0" | ||||||
|  | tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} | ||||||
|  | tqdm = ">=4.62.2,<5.0.0" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "editorconfig" | ||||||
|  | version = "0.12.3" | ||||||
|  | description = "EditorConfig File Locator and Interpreter for Python" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = "*" | ||||||
|  | files = [ | ||||||
|  |     {file = "EditorConfig-0.12.3-py3-none-any.whl", hash = "sha256:6b0851425aa875b08b16789ee0eeadbd4ab59666e9ebe728e526314c4a2e52c1"}, | ||||||
|  |     {file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "exceptiongroup" | ||||||
|  | version = "1.1.0" | ||||||
|  | description = "Backport of PEP 654 (exception groups)" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, | ||||||
|  |     {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | test = ["pytest (>=6)"] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "gunicorn" | ||||||
|  | version = "20.1.0" | ||||||
|  | description = "WSGI HTTP Server for UNIX" | ||||||
|  | category = "main" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.5" | ||||||
|  | files = [ | ||||||
|  |     {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, | ||||||
|  |     {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.dependencies] | ||||||
|  | setuptools = ">=3.0" | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | eventlet = ["eventlet (>=0.24.1)"] | ||||||
|  | gevent = ["gevent (>=1.4.0)"] | ||||||
|  | setproctitle = ["setproctitle"] | ||||||
|  | tornado = ["tornado (>=0.2)"] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "h11" | ||||||
|  | version = "0.14.0" | ||||||
|  | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" | ||||||
|  | category = "main" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, | ||||||
|  |     {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" | ||||||
|  | category = "dev" | ||||||
|  | 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." | ||||||
|  | category = "dev" | ||||||
|  | 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]] | ||||||
|  | name = "importlib-metadata" | ||||||
|  | version = "6.0.0" | ||||||
|  | description = "Read metadata from Python packages" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, | ||||||
|  |     {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.dependencies] | ||||||
|  | zipp = ">=0.5" | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] | ||||||
|  | perf = ["ipython"] | ||||||
|  | testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "iniconfig" | ||||||
|  | version = "1.1.1" | ||||||
|  | description = "iniconfig: brain-dead simple config-ini parsing" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = "*" | ||||||
|  | files = [ | ||||||
|  |     {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, | ||||||
|  |     {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "jsbeautifier" | ||||||
|  | version = "1.14.7" | ||||||
|  | description = "JavaScript unobfuscator and beautifier." | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = "*" | ||||||
|  | files = [ | ||||||
|  |     {file = "jsbeautifier-1.14.7.tar.gz", hash = "sha256:77993254db1ff6f84eb6e1d75e3b6b72cba2ef20813a585b2d81e8e5e3c713c6"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.dependencies] | ||||||
|  | editorconfig = ">=0.12.2" | ||||||
|  | six = ">=1.13.0" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "markupsafe" | ||||||
|  | version = "2.1.1" | ||||||
|  | description = "Safely add untrusted strings to HTML/XML markup." | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, | ||||||
|  |     {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "mypy" | name = "mypy" | ||||||
| version = "0.991" | version = "0.991" | ||||||
| @ -161,6 +432,18 @@ files = [ | |||||||
|     {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, |     {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "packaging" | ||||||
|  | version = "22.0" | ||||||
|  | description = "Core utilities for Python packages" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, | ||||||
|  |     {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pathspec" | name = "pathspec" | ||||||
| version = "0.10.3" | version = "0.10.3" | ||||||
| @ -189,6 +472,46 @@ files = [ | |||||||
| docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] | ||||||
| test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] | test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "pluggy" | ||||||
|  | version = "1.0.0" | ||||||
|  | description = "plugin and hook calling mechanisms for python" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.6" | ||||||
|  | files = [ | ||||||
|  |     {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, | ||||||
|  |     {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | dev = ["pre-commit", "tox"] | ||||||
|  | testing = ["pytest", "pytest-benchmark"] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "pytest" | ||||||
|  | version = "7.2.0" | ||||||
|  | description = "pytest: simple powerful testing with Python" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, | ||||||
|  |     {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.dependencies] | ||||||
|  | attrs = ">=19.2.0" | ||||||
|  | colorama = {version = "*", markers = "sys_platform == \"win32\""} | ||||||
|  | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} | ||||||
|  | iniconfig = "*" | ||||||
|  | packaging = "*" | ||||||
|  | pluggy = ">=0.12,<2.0" | ||||||
|  | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pyyaml" | name = "pyyaml" | ||||||
| version = "6.0" | version = "6.0" | ||||||
| @ -239,6 +562,133 @@ files = [ | |||||||
|     {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, |     {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "regex" | ||||||
|  | version = "2022.10.31" | ||||||
|  | description = "Alternative regular expression module, to replace re." | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.6" | ||||||
|  | files = [ | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"}, | ||||||
|  |     {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"}, | ||||||
|  |     {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"}, | ||||||
|  |     {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"}, | ||||||
|  |     {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"}, | ||||||
|  |     {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"}, | ||||||
|  |     {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"}, | ||||||
|  |     {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "setuptools" | ||||||
|  | version = "65.6.3" | ||||||
|  | description = "Easily download, build, install, upgrade, and uninstall Python packages" | ||||||
|  | category = "main" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, | ||||||
|  |     {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] | ||||||
|  | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] | ||||||
|  | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "six" | ||||||
|  | version = "1.16.0" | ||||||
|  | description = "Python 2 and 3 compatibility utilities" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" | ||||||
|  | files = [ | ||||||
|  |     {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, | ||||||
|  |     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "sqlparse" | name = "sqlparse" | ||||||
| version = "0.4.3" | version = "0.4.3" | ||||||
| @ -263,6 +713,27 @@ files = [ | |||||||
|     {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, |     {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "tqdm" | ||||||
|  | version = "4.64.1" | ||||||
|  | description = "Fast, Extensible Progress Meter" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"}, | ||||||
|  |     {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.dependencies] | ||||||
|  | colorama = {version = "*", markers = "platform_system == \"Windows\""} | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | dev = ["py-make (>=0.1.0)", "twine", "wheel"] | ||||||
|  | notebook = ["ipywidgets (>=6)"] | ||||||
|  | slack = ["slack-sdk"] | ||||||
|  | telegram = ["requests"] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "typing-extensions" | name = "typing-extensions" | ||||||
| version = "4.4.0" | version = "4.4.0" | ||||||
| @ -287,7 +758,60 @@ files = [ | |||||||
|     {file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"}, |     {file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "uvicorn" | ||||||
|  | version = "0.20.0" | ||||||
|  | description = "The lightning-fast ASGI server." | ||||||
|  | category = "main" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "uvicorn-0.20.0-py3-none-any.whl", hash = "sha256:c3ed1598a5668208723f2bb49336f4509424ad198d6ab2615b7783db58d919fd"}, | ||||||
|  |     {file = "uvicorn-0.20.0.tar.gz", hash = "sha256:a4e12017b940247f836bc90b72e725d7dfd0c8ed1c51eb365f5ba30d9f5127d8"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.dependencies] | ||||||
|  | click = ">=7.0" | ||||||
|  | h11 = ">=0.8" | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "werkzeug" | ||||||
|  | version = "2.2.2" | ||||||
|  | description = "The comprehensive WSGI web application library." | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"}, | ||||||
|  |     {file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.dependencies] | ||||||
|  | MarkupSafe = ">=2.1.1" | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | watchdog = ["watchdog"] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "zipp" | ||||||
|  | version = "3.11.0" | ||||||
|  | description = "Backport of pathlib-compatible object wrapper for zip files" | ||||||
|  | category = "dev" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, | ||||||
|  |     {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] | ||||||
|  | testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] | ||||||
|  |  | ||||||
| [metadata] | [metadata] | ||||||
| lock-version = "2.0" | lock-version = "2.0" | ||||||
| python-versions = "^3.10" | python-versions = "^3.10" | ||||||
| content-hash = "0c8d59942dd82c7e89746cfdce544794bf1ce317cac4cdb7b2b5a9137001131d" | content-hash = "fd85e51c8fb99824a433b451c9712b7418c13688b9eb0e8ca6c51768f544e48f" | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| [tool.poetry] | [tool.poetry] | ||||||
| name = "timetracker" | name = "timetracker" | ||||||
| version = "0.0.0" | version = "0.1.2" | ||||||
| description = "A simple time tracker." | description = "A simple time tracker." | ||||||
| authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"] | authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"] | ||||||
| license = "GPL" | license = "GPL" | ||||||
| @ -9,12 +9,24 @@ readme = "README.md" | |||||||
| [tool.poetry.dependencies] | [tool.poetry.dependencies] | ||||||
| python = "^3.10" | python = "^3.10" | ||||||
| django = "^4.1.4" | django = "^4.1.4" | ||||||
|  | gunicorn = "^20.1.0" | ||||||
|  | uvicorn = "^0.20.0" | ||||||
|  |  | ||||||
| [tool.poetry.group.dev.dependencies] | [tool.poetry.group.dev.dependencies] | ||||||
| black = "^22.12.0" | black = "^22.12.0" | ||||||
| mypy = "^0.991" | mypy = "^0.991" | ||||||
| pyyaml = "^6.0" | pyyaml = "^6.0" | ||||||
|  | pytest = "^7.2.0" | ||||||
|  | django-extensions = "^3.2.1" | ||||||
|  | werkzeug = "^2.2.2" | ||||||
|  | djhtml = "^1.5.2" | ||||||
|  | djlint = "^1.19.11" | ||||||
|  |  | ||||||
| [build-system] | [build-system] | ||||||
| requires = ["poetry-core"] | requires = ["poetry-core"] | ||||||
| build-backend = "poetry.core.masonry.api" | build-backend = "poetry.core.masonry.api" | ||||||
|  |  | ||||||
|  | [tool.pytest.ini_options] | ||||||
|  | pythonpath = [ | ||||||
|  |   "src" | ||||||
|  | ] | ||||||
|  | |||||||
| @ -6,10 +6,17 @@ form label { | |||||||
|   @apply dark:text-slate-400; |   @apply dark:text-slate-400; | ||||||
| } | } | ||||||
|  |  | ||||||
| form input,select,textarea { | form input, | ||||||
|     @apply dark:bg-slate-500 dark:border dark:border-slate-900 dark:text-slate-100; | select, | ||||||
|  | textarea { | ||||||
|  |   @apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100; | ||||||
| } | } | ||||||
|  |  | ||||||
| form input[type=submit] { | #session-table { | ||||||
|     @apply p-2 bg-purple-900; |   display: grid; | ||||||
|  |   grid-template-columns: repeat(3, 2fr) 0.5fr 1fr; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #button-container button { | ||||||
|  |   @apply mx-1; | ||||||
| } | } | ||||||
| @ -1,7 +1,44 @@ | |||||||
| from datetime import datetime | from datetime import datetime, timedelta | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from zoneinfo import ZoneInfo | from zoneinfo import ZoneInfo | ||||||
|  | import re | ||||||
|  |  | ||||||
|  |  | ||||||
| def now(): | def now() -> datetime: | ||||||
|     return datetime.now(ZoneInfo(settings.TIME_ZONE)) |     return datetime.now(ZoneInfo(settings.TIME_ZONE)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def format_duration( | ||||||
|  |     duration: timedelta, format_string: str = "%H hours %m minutes" | ||||||
|  | ) -> str: | ||||||
|  |     """ | ||||||
|  |     Format timedelta into the specified format_string. | ||||||
|  |     Valid format variables: | ||||||
|  |     - %H hours | ||||||
|  |     - %m minutes | ||||||
|  |     - %s seconds | ||||||
|  |     - %r total seconds | ||||||
|  |     """ | ||||||
|  |     minute_seconds = 60 | ||||||
|  |     hour_seconds = 60 * minute_seconds | ||||||
|  |     day_seconds = 24 * hour_seconds | ||||||
|  |     if not isinstance(duration, timedelta): | ||||||
|  |         duration = timedelta(seconds=duration) | ||||||
|  |     seconds_total = int(duration.total_seconds()) | ||||||
|  |     # timestamps where end is before start | ||||||
|  |     if seconds_total < 0: | ||||||
|  |         seconds_total = 0 | ||||||
|  |     days, remainder = divmod(seconds_total, day_seconds) | ||||||
|  |     hours, remainder = divmod(remainder, hour_seconds) | ||||||
|  |     minutes, seconds = divmod(remainder, minute_seconds) | ||||||
|  |     literals = { | ||||||
|  |         "%d": str(days), | ||||||
|  |         "%H": str(hours), | ||||||
|  |         "%m": str(minutes), | ||||||
|  |         "%s": str(seconds), | ||||||
|  |         "%r": str(seconds_total), | ||||||
|  |     } | ||||||
|  |     formatted_string = format_string | ||||||
|  |     for pattern, replacement in literals.items(): | ||||||
|  |         formatted_string = re.sub(pattern, replacement, formatted_string) | ||||||
|  |     return formatted_string | ||||||
|  | |||||||
| @ -1,7 +1,9 @@ | |||||||
| from django.db import models | from django.db import models | ||||||
| from datetime import datetime | from datetime import datetime, timedelta | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from zoneinfo import ZoneInfo | from zoneinfo import ZoneInfo | ||||||
|  | from common.util.time import format_duration | ||||||
|  | from django.db.models import Sum | ||||||
|  |  | ||||||
|  |  | ||||||
| class Game(models.Model): | class Game(models.Model): | ||||||
| @ -45,28 +47,19 @@ class Session(models.Model): | |||||||
|     def finish_now(self): |     def finish_now(self): | ||||||
|         self.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE)) |         self.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE)) | ||||||
|  |  | ||||||
|     def duration_seconds(self): |     def duration_seconds(self) -> timedelta: | ||||||
|         if self.duration_manual == None: |         if self.duration_manual == None: | ||||||
|             if self.timestamp_end == None or self.timestamp_start == None: |             if self.timestamp_end == None or self.timestamp_start == None: | ||||||
|                 return 0 |                 return timedelta(0) | ||||||
|             else: |             else: | ||||||
|                 value = self.timestamp_end - self.timestamp_start |                 value = self.timestamp_end - self.timestamp_start | ||||||
|         else: |         else: | ||||||
|             value = self.duration_manual |             value = self.duration_manual | ||||||
|         return value.total_seconds() |         return timedelta(seconds=value.total_seconds()) | ||||||
|  |  | ||||||
|     def duration_formatted(self): |     def duration_formatted(self) -> str: | ||||||
|         seconds = self.duration_seconds() |         result = format_duration(self.duration_seconds(), "%H:%m") | ||||||
|         if seconds == 0: |         return result | ||||||
|             return seconds |  | ||||||
|         hours, remainder = divmod(seconds, 3600) |  | ||||||
|         minutes = remainder // 60 |  | ||||||
|         if hours == 0 and minutes == 0: |  | ||||||
|             return "less than a minute" |  | ||||||
|         else: |  | ||||||
|             hour_string = f"{int(hours)}h" if hours != 0 else "" |  | ||||||
|             minute_string = f"{int(minutes)}m" if minutes != 0 else "" |  | ||||||
|             return f"{hour_string}{minute_string}" |  | ||||||
|  |  | ||||||
|     def duration_any(self): |     def duration_any(self): | ||||||
|         return ( |         return ( | ||||||
| @ -75,6 +68,32 @@ class Session(models.Model): | |||||||
|             else self.duration_manual |             else self.duration_manual | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def calculated_sum() -> timedelta: | ||||||
|  |         calculated_sum_query = Session.objects.all().aggregate( | ||||||
|  |             Sum("duration_calculated") | ||||||
|  |         ) | ||||||
|  |         calculated_sum = ( | ||||||
|  |             timedelta(0) | ||||||
|  |             if calculated_sum_query["duration_calculated__sum"] == None | ||||||
|  |             else calculated_sum_query["duration_calculated__sum"] | ||||||
|  |         ) | ||||||
|  |         return calculated_sum | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def manual_sum() -> timedelta: | ||||||
|  |         manual_sum_query = Session.objects.all().aggregate(Sum("duration_manual")) | ||||||
|  |         manual_sum = ( | ||||||
|  |             timedelta(0) | ||||||
|  |             if manual_sum_query["duration_manual__sum"] == None | ||||||
|  |             else manual_sum_query["duration_manual__sum"] | ||||||
|  |         ) | ||||||
|  |         return manual_sum | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def total_sum() -> timedelta: | ||||||
|  |         return Session.manual_sum() + Session.calculated_sum() | ||||||
|  |  | ||||||
|     def save(self, *args, **kwargs): |     def save(self, *args, **kwargs): | ||||||
|         if self.timestamp_start != None and self.timestamp_end != None: |         if self.timestamp_start != None and self.timestamp_end != None: | ||||||
|             self.duration_calculated = self.timestamp_end - self.timestamp_start |             self.duration_calculated = self.timestamp_end - self.timestamp_start | ||||||
|  | |||||||
| @ -742,6 +742,10 @@ select { | |||||||
|   margin-top: 1rem; |   margin-top: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .ml-1 { | ||||||
|  |   margin-left: 0.25rem; | ||||||
|  | } | ||||||
|  |  | ||||||
| .block { | .block { | ||||||
|   display: block; |   display: block; | ||||||
| } | } | ||||||
| @ -750,8 +754,12 @@ select { | |||||||
|   display: flex; |   display: flex; | ||||||
| } | } | ||||||
|  |  | ||||||
| .grid { | .h-5 { | ||||||
|   display: grid; |   height: 1.25rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .h-4 { | ||||||
|  |   height: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .min-h-screen { | .min-h-screen { | ||||||
| @ -762,12 +770,20 @@ select { | |||||||
|   width: 100%; |   width: 100%; | ||||||
| } | } | ||||||
|  |  | ||||||
| .max-w-screen-lg { | .w-5 { | ||||||
|   max-width: 1024px; |   width: 1.25rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .grid-cols-5 { | .w-7 { | ||||||
|   grid-template-columns: repeat(5, minmax(0, 1fr)); |   width: 1.75rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .w-4 { | ||||||
|  |   width: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .max-w-screen-lg { | ||||||
|  |   max-width: 1024px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .flex-col { | .flex-col { | ||||||
| @ -782,6 +798,10 @@ select { | |||||||
|   align-items: center; |   align-items: center; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .justify-end { | ||||||
|  |   justify-content: flex-end; | ||||||
|  | } | ||||||
|  |  | ||||||
| .justify-center { | .justify-center { | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
| } | } | ||||||
| @ -810,8 +830,8 @@ select { | |||||||
|   border-radius: 0.75rem; |   border-radius: 0.75rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .border { | .rounded-lg { | ||||||
|   border-width: 1px; |   border-radius: 0.5rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .border-gray-200 { | .border-gray-200 { | ||||||
| @ -819,19 +839,24 @@ select { | |||||||
|   border-color: rgb(229 231 235 / var(--tw-border-opacity)); |   border-color: rgb(229 231 235 / var(--tw-border-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .border-red-900 { |  | ||||||
|   --tw-border-opacity: 1; |  | ||||||
|   border-color: rgb(127 29 29 / var(--tw-border-opacity)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .bg-white { | .bg-white { | ||||||
|   --tw-bg-opacity: 1; |   --tw-bg-opacity: 1; | ||||||
|   background-color: rgb(255 255 255 / var(--tw-bg-opacity)); |   background-color: rgb(255 255 255 / var(--tw-bg-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .bg-red-700 { | .bg-green-600 { | ||||||
|   --tw-bg-opacity: 1; |   --tw-bg-opacity: 1; | ||||||
|   background-color: rgb(185 28 28 / var(--tw-bg-opacity)); |   background-color: rgb(22 163 74 / var(--tw-bg-opacity)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .bg-blue-600 { | ||||||
|  |   --tw-bg-opacity: 1; | ||||||
|  |   background-color: rgb(37 99 235 / var(--tw-bg-opacity)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .bg-red-600 { | ||||||
|  |   --tw-bg-opacity: 1; | ||||||
|  |   background-color: rgb(220 38 38 / var(--tw-bg-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .p-4 { | .p-4 { | ||||||
| @ -842,15 +867,21 @@ select { | |||||||
|   padding: 0.5rem; |   padding: 0.5rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .p-1 { |  | ||||||
|   padding: 0.25rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .py-2 { | .py-2 { | ||||||
|   padding-top: 0.5rem; |   padding-top: 0.5rem; | ||||||
|   padding-bottom: 0.5rem; |   padding-bottom: 0.5rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .py-1 { | ||||||
|  |   padding-top: 0.25rem; | ||||||
|  |   padding-bottom: 0.25rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .px-2 { | ||||||
|  |   padding-left: 0.5rem; | ||||||
|  |   padding-right: 0.5rem; | ||||||
|  | } | ||||||
|  |  | ||||||
| .pl-3 { | .pl-3 { | ||||||
|   padding-left: 0.75rem; |   padding-left: 0.75rem; | ||||||
| } | } | ||||||
| @ -863,6 +894,10 @@ select { | |||||||
|   text-align: center; |   text-align: center; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .text-right { | ||||||
|  |   text-align: right; | ||||||
|  | } | ||||||
|  |  | ||||||
| .text-4xl { | .text-4xl { | ||||||
|   font-size: 2.25rem; |   font-size: 2.25rem; | ||||||
|   line-height: 2.5rem; |   line-height: 2.5rem; | ||||||
| @ -883,9 +918,9 @@ select { | |||||||
|   line-height: 1.75rem; |   line-height: 1.75rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .text-sm { | .text-base { | ||||||
|   font-size: 0.875rem; |   font-size: 1rem; | ||||||
|   line-height: 1.25rem; |   line-height: 1.5rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .font-semibold { | .font-semibold { | ||||||
| @ -902,18 +937,47 @@ select { | |||||||
|   color: rgb(203 213 225 / var(--tw-text-opacity)); |   color: rgb(203 213 225 / var(--tw-text-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .text-red-400 { | ||||||
|  |   --tw-text-opacity: 1; | ||||||
|  |   color: rgb(248 113 113 / var(--tw-text-opacity)); | ||||||
|  | } | ||||||
|  |  | ||||||
| .shadow { | .shadow { | ||||||
|   --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); |   --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); | ||||||
|   --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); |   --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); | ||||||
|   box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); |   box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .shadow-md { | ||||||
|  |   --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); | ||||||
|  |   --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); | ||||||
|  |   box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .transition { | ||||||
|  |   transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; | ||||||
|  |   transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; | ||||||
|  |   transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; | ||||||
|  |   transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | ||||||
|  |   transition-duration: 150ms; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .duration-200 { | ||||||
|  |   transition-duration: 200ms; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .ease-in { | ||||||
|  |   transition-timing-function: cubic-bezier(0.4, 0, 1, 1); | ||||||
|  | } | ||||||
|  |  | ||||||
| .dark form label { | .dark form label { | ||||||
|   --tw-text-opacity: 1; |   --tw-text-opacity: 1; | ||||||
|   color: rgb(148 163 184 / var(--tw-text-opacity)); |   color: rgb(148 163 184 / var(--tw-text-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .dark form input,.dark select,.dark textarea { | .dark form input,.dark  | ||||||
|  | select,.dark  | ||||||
|  | textarea { | ||||||
|   border-width: 1px; |   border-width: 1px; | ||||||
|   --tw-border-opacity: 1; |   --tw-border-opacity: 1; | ||||||
|   border-color: rgb(15 23 42 / var(--tw-border-opacity)); |   border-color: rgb(15 23 42 / var(--tw-border-opacity)); | ||||||
| @ -923,30 +987,69 @@ select { | |||||||
|   color: rgb(241 245 249 / var(--tw-text-opacity)); |   color: rgb(241 245 249 / var(--tw-text-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| form input[type=submit] { | #session-table { | ||||||
|  |   display: grid; | ||||||
|  |   grid-template-columns: repeat(3, 2fr) 0.5fr 1fr; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #button-container button { | ||||||
|  |   margin-left: 0.25rem; | ||||||
|  |   margin-right: 0.25rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .hover\:bg-green-700:hover { | ||||||
|   --tw-bg-opacity: 1; |   --tw-bg-opacity: 1; | ||||||
|   background-color: rgb(88 28 135 / var(--tw-bg-opacity)); |   background-color: rgb(21 128 61 / var(--tw-bg-opacity)); | ||||||
|   padding: 0.5rem; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| .hover\:border-dotted:hover { | .hover\:bg-blue-700:hover { | ||||||
|   border-style: dotted; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hover\:border-white:hover { |  | ||||||
|   --tw-border-opacity: 1; |  | ||||||
|   border-color: rgb(255 255 255 / var(--tw-border-opacity)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hover\:bg-orange-700:hover { |  | ||||||
|   --tw-bg-opacity: 1; |   --tw-bg-opacity: 1; | ||||||
|   background-color: rgb(194 65 12 / var(--tw-bg-opacity)); |   background-color: rgb(29 78 216 / var(--tw-bg-opacity)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .hover\:bg-red-700:hover { | ||||||
|  |   --tw-bg-opacity: 1; | ||||||
|  |   background-color: rgb(185 28 28 / var(--tw-bg-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .hover\:underline:hover { | .hover\:underline:hover { | ||||||
|   text-decoration-line: underline; |   text-decoration-line: underline; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .focus\:outline-none:focus { | ||||||
|  |   outline: 2px solid transparent; | ||||||
|  |   outline-offset: 2px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .focus\:ring-2:focus { | ||||||
|  |   --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); | ||||||
|  |   --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); | ||||||
|  |   box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .focus\:ring-green-500:focus { | ||||||
|  |   --tw-ring-opacity: 1; | ||||||
|  |   --tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .focus\:ring-blue-500:focus { | ||||||
|  |   --tw-ring-opacity: 1; | ||||||
|  |   --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .focus\:ring-red-500:focus { | ||||||
|  |   --tw-ring-opacity: 1; | ||||||
|  |   --tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .focus\:ring-offset-2:focus { | ||||||
|  |   --tw-ring-offset-width: 2px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .focus\:ring-offset-blue-200:focus { | ||||||
|  |   --tw-ring-offset-color: #bfdbfe; | ||||||
|  | } | ||||||
|  |  | ||||||
| .dark .dark\:border-white { | .dark .dark\:border-white { | ||||||
|   --tw-border-opacity: 1; |   --tw-border-opacity: 1; | ||||||
|   border-color: rgb(255 255 255 / var(--tw-border-opacity)); |   border-color: rgb(255 255 255 / var(--tw-border-opacity)); | ||||||
|  | |||||||
| @ -1,13 +1,13 @@ | |||||||
| {% extends 'base.html' %} | {% extends "base.html" %} | ||||||
|  |  | ||||||
| {% block title %}{{ title }}{% endblock title %} | {% block title %}{{ title }}{% endblock title %} | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
| <form method="POST" enctype="multipart/form-data" class="mx-auto"> |     <form method="post" enctype="multipart/form-data" class="mx-auto"> | ||||||
|         {% csrf_token %} |         {% csrf_token %} | ||||||
|  |  | ||||||
|         {{ form.as_p }} |         {{ form.as_p }} | ||||||
|  |  | ||||||
|     <input type="submit" value="Submit"> |         <input type="submit" value="Submit"/> | ||||||
|     </form> |     </form> | ||||||
| {% endblock content %} | {% endblock content %} | ||||||
| @ -1,13 +0,0 @@ | |||||||
| {% extends 'base.html' %} |  | ||||||
|  |  | ||||||
| {% block title %}Add New Purchase{% endblock title %} |  | ||||||
|  |  | ||||||
| {% block content %} |  | ||||||
| <form method="POST" enctype="multipart/form-data" class="mx-auto"> |  | ||||||
|     {% csrf_token %} |  | ||||||
|  |  | ||||||
|     {{ form.as_p }} |  | ||||||
|  |  | ||||||
|     <input type="submit" value="Submit"> |  | ||||||
| </form> |  | ||||||
| {% endblock content %} |  | ||||||
| @ -1,13 +0,0 @@ | |||||||
| {% extends 'base.html' %} |  | ||||||
|  |  | ||||||
| {% block title %}Add New Session{% endblock title %} |  | ||||||
|  |  | ||||||
| {% block content %} |  | ||||||
| <form method="POST" enctype="multipart/form-data" class="mx-auto"> |  | ||||||
|     {% csrf_token %} |  | ||||||
|  |  | ||||||
|     {{ form.as_p }} |  | ||||||
|  |  | ||||||
|     <input type="submit" value="Submit"> |  | ||||||
| </form> |  | ||||||
| {% endblock content %} |  | ||||||
| @ -4,10 +4,12 @@ | |||||||
|     {% load static %} |     {% load static %} | ||||||
|  |  | ||||||
|     <head> |     <head> | ||||||
|     <meta charset="utf-8"> |         <meta charset="utf-8"/> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> |         <meta name="description" content="Self-hosted time-tracker."/> | ||||||
|  |         <meta name="keywords" content="time, tracking, video games, self-hosted"/> | ||||||
|  |         <meta name="viewport" content="width=device-width, initial-scale=1"/> | ||||||
|         <title>Timetracker - {% block title %}Untitled{% endblock title %}</title> |         <title>Timetracker - {% block title %}Untitled{% endblock title %}</title> | ||||||
|     <link rel="stylesheet" href="https://rsms.me/inter/inter.css"> |         <link rel="stylesheet" href="https://rsms.me/inter/inter.css"/> | ||||||
|         <link rel="stylesheet" href="{% static 'base.css' %}" /> |         <link rel="stylesheet" href="{% static 'base.css' %}" /> | ||||||
|     </head> |     </head> | ||||||
|  |  | ||||||
| @ -37,7 +39,7 @@ | |||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|             </nav> |             </nav> | ||||||
|         {% block content %}No content here.{% endblock %} |             {% block content %}No content here.{% endblock content %} | ||||||
|         </div> |         </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> | ||||||
|  | |||||||
| @ -1,13 +1,17 @@ | |||||||
| {% extends 'base.html' %} | {% extends "base.html" %} | ||||||
|  |  | ||||||
| {% block title %}{{ title }}{% endblock title %} | {% block title %}{{ title }}{% endblock title %} | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
|     <div class="text-slate-300 mx-auto max-w-screen-lg text-center"> |     <div class="text-slate-300 mx-auto max-w-screen-lg text-center"> | ||||||
|         {% if session_count > 0 %} |         {% if session_count > 0 %} | ||||||
| You have played a total of {{ session_count }} sessions. |             You have played a total of {{ session_count }} sessions for a total of {{ total_duration }}. | ||||||
|  |         {% elif not game_available or not platform_available %} | ||||||
|  |             There are no games in the database. Start by clicking "New Game" and "New Platform". | ||||||
|  |         {% elif not purchase_available %} | ||||||
|  |             There are no owned games. Click "New Purchase" at the top. | ||||||
|         {% else %} |         {% else %} | ||||||
| Start by clicking the links at the top. To track playtime, you need to have at least 1 owned game. |             You haven't played any games yet. Click "New Session" to add one now. | ||||||
|         {% endif %} |         {% endif %} | ||||||
|     </div> |     </div> | ||||||
| {% endblock content %} | {% endblock content %} | ||||||
| @ -9,27 +9,51 @@ | |||||||
|             <a class="dark:text-white hover:underline" href="{% url 'list_sessions' %}">View all sessions</a> |             <a class="dark:text-white hover:underline" href="{% url 'list_sessions' %}">View all sessions</a> | ||||||
|         </div> |         </div> | ||||||
|     {% endif %} |     {% endif %} | ||||||
| <div class="grid grid-cols-5 gap-4 shadow rounded-xl max-w-screen-lg mx-auto dark:bg-slate-700 p-2 justify-center"> |     <div id="session-table" class="gap-4 shadow rounded-xl max-w-screen-lg mx-auto dark:bg-slate-700 p-2 justify-center"> | ||||||
|         <div class="dark:border-white dark:text-slate-300 text-lg">Name</div> |         <div class="dark:border-white dark:text-slate-300 text-lg">Name</div> | ||||||
|     <div class="dark:border-white dark:text-slate-300 text-lg">Start</div> |         <div class="dark:border-white dark:text-slate-300 text-lg text-center">Start</div> | ||||||
|     <div class="dark:border-white dark:text-slate-300 text-lg">End</div> |         <div class="dark:border-white dark:text-slate-300 text-lg text-center">End</div> | ||||||
|         <div class="dark:border-white dark:text-slate-300 text-lg">Duration</div> |         <div class="dark:border-white dark:text-slate-300 text-lg">Duration</div> | ||||||
|     <div></div> |         <div class="dark:border-white dark:text-slate-300 text-lg text-right">Manage</div> | ||||||
|         {% for data in dataset %} |         {% for data in dataset %} | ||||||
|     <div class=""><a class="dark:text-white hover:underline" href="{% url 'list_sessions' data.purchase.id %}">{{ data.purchase }}</a></div> |             <div><a class="dark:text-white hover:underline" href="{% url 'list_sessions' data.purchase.id %}">{{ data.purchase }}</a></div> | ||||||
|     <div class="dark:text-slate-400">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div> |             <div class="dark:text-slate-400 text-center">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div> | ||||||
|     <div class="dark:text-slate-400"> |             <div class="dark:text-slate-400 text-center"> | ||||||
|                 {% if data.unfinished %} |                 {% if data.unfinished %} | ||||||
|         Not finished yet. <a href="{% url 'update_session' data.id %}"><button class="bg-red-700 hover:bg-orange-700 border border-red-900 hover:border-dotted hover:border-white rounded p-1 text-white text-sm">Finish now?</button></a> |                     <span class="text-red-400">Not finished yet.</span> | ||||||
|                 {% elif data.duration_manual %} |                 {% elif data.duration_manual %} | ||||||
|         MANUAL |                     -- | ||||||
|                 {% else %} |                 {% else %} | ||||||
|                     {{ data.timestamp_end | date:"d/m/Y H:i" }} |                     {{ data.timestamp_end | date:"d/m/Y H:i" }} | ||||||
|                 {% endif %} |                 {% endif %} | ||||||
|             </div> |             </div> | ||||||
|     <div class="dark:text-slate-400">{{ data.duration_formatted }}{% if data.duration_manual %} (M){% endif %}</div> |             <div class="dark:text-slate-400 flex">{{ data.duration_formatted }}{% if data.duration_manual %} <svg title="Added manually" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5 ml-1 self-center"> | ||||||
|     <div> |                 <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" /> | ||||||
|         <a href="{% url 'delete_session' data.id %}"><button>❌</button></a> |               </svg> | ||||||
|  |               {% endif %}</div> | ||||||
|  |             <div id="button-container" class="flex justify-end"> | ||||||
|  |                 {% if data.unfinished %} | ||||||
|  |                     <a href="{% url 'update_session' data.id %}"> | ||||||
|  |                         <button type="button" title="Set to finished" class="py-1 px-2 flex justify-center items-center  bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 w-7 h-4 rounded-lg "> | ||||||
|  |                             <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4"> | ||||||
|  |                                 <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" /> | ||||||
|  |                             </svg> | ||||||
|  |                         </button> | ||||||
|  |                     </a> | ||||||
|  |                 {% endif %} | ||||||
|  |                 <button type="button" title="Edit" class="py-1 px-2 flex justify-center items-center  bg-blue-600 hover:bg-blue-700 focus:ring-blue-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 w-7 h-4 rounded-lg "> | ||||||
|  |                     <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> | ||||||
|  |                         <path d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z" /> | ||||||
|  |                         <path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z" /> | ||||||
|  |                     </svg> | ||||||
|  |                 </button> | ||||||
|  |                 <a href="{% url 'delete_session' data.id %}"> | ||||||
|  |                     <button type="button" edit="Delete" class="py-1 px-2 flex justify-center items-center  bg-red-600 hover:bg-red-700 focus:ring-red-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 w-7 h-4 rounded-lg "> | ||||||
|  |                         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> | ||||||
|  |                             <path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z" clip-rule="evenodd" /> | ||||||
|  |                         </svg> | ||||||
|  |                     </button> | ||||||
|  |                 </a> | ||||||
|             </div> |             </div> | ||||||
|         {% endfor %} |         {% endfor %} | ||||||
|     </div> |     </div> | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| from django import template | from django import template | ||||||
|  | from django.conf import settings | ||||||
| import time | import time | ||||||
| import os | import os | ||||||
|  |  | ||||||
| @ -9,7 +10,11 @@ register = template.Library() | |||||||
| def version_date(): | def version_date(): | ||||||
|     return time.strftime( |     return time.strftime( | ||||||
|         "%d-%b-%Y %H:%m", |         "%d-%b-%Y %H:%m", | ||||||
|         time.gmtime(os.path.getmtime(os.path.abspath(os.path.join(".git")))), |         time.gmtime( | ||||||
|  |             os.path.getmtime( | ||||||
|  |                 os.path.abspath(os.path.join(settings.BASE_DIR, "..", "..", ".git")) | ||||||
|  |             ) | ||||||
|  |         ), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -2,10 +2,12 @@ from django.shortcuts import render, redirect | |||||||
|  |  | ||||||
| from .models import Game, Platform, Purchase, Session | from .models import Game, Platform, Purchase, Session | ||||||
| from .forms import SessionForm, PurchaseForm, GameForm, PlatformForm | from .forms import SessionForm, PurchaseForm, GameForm, PlatformForm | ||||||
| from datetime import datetime | from datetime import datetime, timedelta | ||||||
| from zoneinfo import ZoneInfo | from zoneinfo import ZoneInfo | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from common.util.time import now as now_with_tz | from common.util.time import now as now_with_tz, format_duration | ||||||
|  | from django.db.models import Sum | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  |  | ||||||
| def model_counts(request): | def model_counts(request): | ||||||
| @ -26,8 +28,9 @@ def add_session(request): | |||||||
|         form.save() |         form.save() | ||||||
|         return redirect("list_sessions") |         return redirect("list_sessions") | ||||||
|  |  | ||||||
|  |     context["title"] = "Add New Session" | ||||||
|     context["form"] = form |     context["form"] = form | ||||||
|     return render(request, "add_session.html", context) |     return render(request, "add.html", context) | ||||||
|  |  | ||||||
|  |  | ||||||
| def update_session(request, session_id=None): | def update_session(request, session_id=None): | ||||||
| @ -50,7 +53,7 @@ def list_sessions(request, purchase_id=None): | |||||||
|         dataset = Session.objects.filter(purchase=purchase_id) |         dataset = Session.objects.filter(purchase=purchase_id) | ||||||
|         context["purchase"] = Purchase.objects.get(id=purchase_id) |         context["purchase"] = Purchase.objects.get(id=purchase_id) | ||||||
|     else: |     else: | ||||||
|         dataset = Session.objects.all() |         dataset = Session.objects.all().order_by("timestamp_start") | ||||||
|  |  | ||||||
|     for session in dataset: |     for session in dataset: | ||||||
|         if session.timestamp_end == None and session.duration_manual == None: |         if session.timestamp_end == None and session.duration_manual == None: | ||||||
| @ -102,4 +105,12 @@ def add_platform(request): | |||||||
|  |  | ||||||
| def index(request): | def index(request): | ||||||
|     context = {} |     context = {} | ||||||
|  |     if Session.objects.count() == 0: | ||||||
|  |         duration: str = "" | ||||||
|  |     else: | ||||||
|  |         context["total_duration"] = format_duration( | ||||||
|  |             Session.total_sum(), | ||||||
|  |             "%H hours %m minutes", | ||||||
|  |         ) | ||||||
|  |     context["title"] = "Index" | ||||||
|     return render(request, "index.html", context) |     return render(request, "index.html", context) | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent | |||||||
| SECRET_KEY = "django-insecure-x0_t$gei=_o_p(%%!-db$jezka@y+d67$a8tvw13nl^8$l*t@=" | SECRET_KEY = "django-insecure-x0_t$gei=_o_p(%%!-db$jezka@y+d67$a8tvw13nl^8$l*t@=" | ||||||
|  |  | ||||||
| # SECURITY WARNING: don't run with debug turned on in production! | # SECURITY WARNING: don't run with debug turned on in production! | ||||||
| DEBUG = True | DEBUG = False if os.environ.get("PROD") else True | ||||||
|  |  | ||||||
| ALLOWED_HOSTS = ["*"] | ALLOWED_HOSTS = ["*"] | ||||||
|  |  | ||||||
| @ -34,7 +34,6 @@ ALLOWED_HOSTS = ["*"] | |||||||
|  |  | ||||||
| INSTALLED_APPS = [ | INSTALLED_APPS = [ | ||||||
|     "tracker.apps.TrackerConfig", |     "tracker.apps.TrackerConfig", | ||||||
|     "django.contrib.admin", |  | ||||||
|     "django.contrib.auth", |     "django.contrib.auth", | ||||||
|     "django.contrib.contenttypes", |     "django.contrib.contenttypes", | ||||||
|     "django.contrib.sessions", |     "django.contrib.sessions", | ||||||
| @ -42,6 +41,10 @@ INSTALLED_APPS = [ | |||||||
|     "django.contrib.staticfiles", |     "django.contrib.staticfiles", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | if DEBUG: | ||||||
|  |     INSTALLED_APPS.append("django_extensions") | ||||||
|  |     INSTALLED_APPS.append("django.contrib.admin") | ||||||
|  |  | ||||||
| MIDDLEWARE = [ | MIDDLEWARE = [ | ||||||
|     "django.middleware.security.SecurityMiddleware", |     "django.middleware.security.SecurityMiddleware", | ||||||
|     "django.contrib.sessions.middleware.SessionMiddleware", |     "django.contrib.sessions.middleware.SessionMiddleware", | ||||||
| @ -109,7 +112,7 @@ AUTH_PASSWORD_VALIDATORS = [ | |||||||
|  |  | ||||||
| LANGUAGE_CODE = "en-us" | LANGUAGE_CODE = "en-us" | ||||||
|  |  | ||||||
| TIME_ZONE = os.environ.get("TZ", "UTC") | TIME_ZONE = "Europe/Prague" if DEBUG else os.environ.get("TZ", "UTC") | ||||||
|  |  | ||||||
| USE_I18N = True | USE_I18N = True | ||||||
|  |  | ||||||
| @ -120,6 +123,7 @@ USE_TZ = True | |||||||
| # https://docs.djangoproject.com/en/4.1/howto/static-files/ | # https://docs.djangoproject.com/en/4.1/howto/static-files/ | ||||||
|  |  | ||||||
| STATIC_URL = "static/" | STATIC_URL = "static/" | ||||||
|  | STATIC_ROOT = BASE_DIR / "static" | ||||||
|  |  | ||||||
| # Default primary key field type | # Default primary key field type | ||||||
| # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field | # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field | ||||||
| @ -130,8 +134,19 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" | |||||||
| LOGGING = { | LOGGING = { | ||||||
|     "version": 1, |     "version": 1, | ||||||
|     "disable_existing_loggers": False, |     "disable_existing_loggers": False, | ||||||
|     "handlers": {"console": {"class": "logging.StreamHandler"}}, |     "handlers": { | ||||||
|     "root": {"handlers": ["console"], "level": "WARNING"}, |         "console": {"class": "logging.StreamHandler"}, | ||||||
|  |     }, | ||||||
|  |     "loggers": { | ||||||
|  |         "django": { | ||||||
|  |             "handlers": ["console"], | ||||||
|  |             "level": "INFO", | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
| } | } | ||||||
|  |  | ||||||
| CSRF_TRUSTED_ORIGINS = ["https://tracker.kucharczyk.xyz"] | _csrf_trusted_origins = os.environ.get("CSRF_TRUSTED_ORIGINS") | ||||||
|  | if _csrf_trusted_origins: | ||||||
|  |     CSRF_TRUSTED_ORIGINS = _csrf_trusted_origins.split(",") | ||||||
|  | else: | ||||||
|  |     CSRF_TRUSTED_ORIGINS = [] | ||||||
|  | |||||||
| @ -16,10 +16,13 @@ Including another URLconf | |||||||
| from django.contrib import admin | from django.contrib import admin | ||||||
| from django.urls import include, path | from django.urls import include, path | ||||||
| from django.views.generic import RedirectView | from django.views.generic import RedirectView | ||||||
|  | from django.conf import settings | ||||||
|  |  | ||||||
|  |  | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path("admin/", admin.site.urls), |  | ||||||
|     path("", RedirectView.as_view(url="/tracker/list-sessions")), |     path("", RedirectView.as_view(url="/tracker/list-sessions")), | ||||||
|     path("tracker/", include("tracker.urls")), |     path("tracker/", include("tracker.urls")), | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | if settings.DEBUG: | ||||||
|  |     urlpatterns.append(path("admin/", admin.site.urls)) | ||||||
|  | |||||||
							
								
								
									
										58
									
								
								tests/test_time.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								tests/test_time.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | |||||||
|  | import unittest | ||||||
|  | from web.common.util.time import format_duration | ||||||
|  | from datetime import timedelta | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FormatDurationTest(unittest.TestCase): | ||||||
|  |     def setUp(self) -> None: | ||||||
|  |  | ||||||
|  |         return super().setUp() | ||||||
|  |  | ||||||
|  |     def test_only_days(self): | ||||||
|  |         delta = timedelta(days=3) | ||||||
|  |         result = format_duration(delta, "%d days") | ||||||
|  |         self.assertEqual(result, "3 days") | ||||||
|  |  | ||||||
|  |     def test_only_hours(self): | ||||||
|  |         delta = timedelta(hours=1) | ||||||
|  |         result = format_duration(delta, "%H hours") | ||||||
|  |         self.assertEqual(result, "1 hours") | ||||||
|  |  | ||||||
|  |     def test_only_minutes(self): | ||||||
|  |         delta = timedelta(minutes=34) | ||||||
|  |         result = format_duration(delta, "%m minutes") | ||||||
|  |         self.assertEqual(result, "34 minutes") | ||||||
|  |  | ||||||
|  |     def test_only_overflow_seconds(self): | ||||||
|  |         delta = timedelta(seconds=61) | ||||||
|  |         result = format_duration(delta, "%s seconds") | ||||||
|  |         self.assertEqual(result, "1 seconds") | ||||||
|  |  | ||||||
|  |     def test_only_rawseconds(self): | ||||||
|  |         delta = timedelta(seconds=5690) | ||||||
|  |         result = format_duration(delta, "%r total seconds") | ||||||
|  |         self.assertEqual(result, "5690 total seconds") | ||||||
|  |  | ||||||
|  |     def test_empty(self): | ||||||
|  |         delta = timedelta() | ||||||
|  |         result = format_duration(delta, "") | ||||||
|  |         self.assertEqual(result, "") | ||||||
|  |  | ||||||
|  |     def test_zero(self): | ||||||
|  |         delta = timedelta() | ||||||
|  |         result = format_duration(delta, "%r seconds") | ||||||
|  |         self.assertEqual(result, "0 seconds") | ||||||
|  |  | ||||||
|  |     def test_all_at_once(self): | ||||||
|  |         delta = timedelta(days=50, hours=10, minutes=34, seconds=24) | ||||||
|  |         result = format_duration( | ||||||
|  |             delta, "%d days, %H hours, %m minutes, %s seconds, %r total seconds" | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             result, "50 days, 10 hours, 34 minutes, 24 seconds, 4358064 total seconds" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_negative(self): | ||||||
|  |         delta = timedelta(hours=-2) | ||||||
|  |         result = format_duration(delta, "%H hours") | ||||||
|  |         self.assertEqual(result, "0 hours") | ||||||
		Reference in New Issue
	
	Block a user