11 Commits

Author SHA1 Message Date
f9b53da19c Use better way to find out if model record exists
All checks were successful
Django CI/CD / test (push) Successful in 1m1s
Django CI/CD / build-and-push (push) Successful in 1m22s
2023-11-21 17:42:57 +01:00
9a0e505aae Remove experimental layout 2023-11-21 17:42:41 +01:00
c9deba7d65 Add stats for most sessions, longest session 2023-11-21 17:02:44 +01:00
c55fbe86b5 Support HTMX with Django Debug Toolbar 2023-11-21 16:59:21 +01:00
0e93993498 Add django-debug-toolbar 2023-11-21 14:42:37 +01:00
9fccdfbff0 Make links colorful 2023-11-20 23:07:11 +01:00
d78139a5b3 Display finished DLCs in stats better
All checks were successful
Django CI/CD / test (push) Successful in 1m6s
Django CI/CD / build-and-push (push) Successful in 1m26s
2023-11-20 21:56:16 +01:00
7dc43fbf77 Fix wrong export name 2023-11-20 21:54:51 +01:00
5442926457 Allow DLC to have date_finished set
All checks were successful
Django CI/CD / test (push) Successful in 1m6s
Django CI/CD / build-and-push (push) Successful in 1m35s
2023-11-20 21:42:23 +01:00
db4c635260 Remote JavaScript files 2023-11-20 21:25:21 +01:00
4a1d08d4df Fix CI, re-add test step
All checks were successful
Django CI/CD / test (push) Successful in 1m0s
Django CI/CD / build-and-push (push) Successful in 1m45s
2023-11-18 11:10:33 +01:00
16 changed files with 257 additions and 94 deletions

View File

@ -1,25 +0,0 @@
name: Django CI/CD
on:
push:
branches: [ main ]
paths-ignore: [ 'README.md' ]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
registry.kucharczyk.xyz/timetracker:latest
registry.kucharczyk.xyz/timetracker:${{ env.VERSION_NUMBER }}
env:
VERSION_NUMBER: 1.5.1

36
.github/workflows/build-docker.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Django CI/CD
on:
push:
paths-ignore: [ 'README.md' ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: 3.12
- run: |
python -m pip install poetry
poetry install
poetry env info
poetry run python manage.py migrate
poetry run pytest
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
registry.kucharczyk.xyz/timetracker:latest
registry.kucharczyk.xyz/timetracker:${{ env.VERSION_NUMBER }}
env:
VERSION_NUMBER: 1.5.1

View File

@ -23,6 +23,12 @@
font-style: normal;
}
a:hover {
text-decoration-color: #ff4400;
color: rgb(254, 185, 160);
transition: all 0.2s ease-out;
}
form label {
@apply dark:text-slate-400;
}

View File

@ -1173,6 +1173,12 @@ select {
font-style: normal;
}
a:hover {
text-decoration-color: #ff4400;
color: rgb(254, 185, 160);
transition: all 0.2s ease-out;
}
:is(.dark form label) {
--tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity));
@ -1477,10 +1483,6 @@ th label {
display: block;
}
.md\:w-1\/2 {
width: 50%;
}
.md\:w-auto {
width: auto;
}

View File

@ -1,24 +1,24 @@
import { syncSelectInputUntilChanged } from './utils.js';
import { syncSelectInputUntilChanged } from "./utils.js";
let syncData = [
{
"source": "#id_game",
"source_value": "dataset.name",
"target": "#id_name",
"target_value": "value"
source: "#id_game",
source_value: "dataset.name",
target: "#id_name",
target_value: "value",
},
{
"source": "#id_game",
"source_value": "textContent",
"target": "#id_sort_name",
"target_value": "value"
source: "#id_game",
source_value: "textContent",
target: "#id_sort_name",
target_value: "value",
},
{
"source": "#id_game",
"source_value": "dataset.year",
"target": "#id_year_released",
"target_value": "value"
},
]
source: "#id_game",
source_value: "dataset.year",
target: "#id_year_released",
target_value: "value",
},
];
syncSelectInputUntilChanged(syncData, "form");

View File

@ -1,12 +1,12 @@
import { syncSelectInputUntilChanged } from './utils.js'
import { syncSelectInputUntilChanged } from "./utils.js";
let syncData = [
{
"source": "#id_name",
"source_value": "value",
"target": "#id_sort_name",
"target_value": "value"
}
]
source: "#id_name",
source_value: "value",
target: "#id_sort_name",
target_value: "value",
},
];
syncSelectInputUntilChanged(syncData, "form")
syncSelectInputUntilChanged(syncData, "form");

View File

@ -2,7 +2,7 @@ import {
syncSelectInputUntilChanged,
getEl,
disableElementsWhenTrue,
disableElementsWhenFalse,
disableElementsWhenValueNotEqual,
} from "./utils.js";
let syncData = [
@ -21,7 +21,11 @@ function setupElementHandlers() {
"#id_name",
"#id_related_purchase",
]);
disableElementsWhenFalse("#id_type", "game", ["#id_date_finished"]);
disableElementsWhenValueNotEqual(
"#id_type",
["game", "dlc"],
["#id_date_finished"]
);
}
document.addEventListener("DOMContentLoaded", setupElementHandlers);
@ -30,14 +34,14 @@ getEl("#id_type").onchange = () => {
setupElementHandlers();
};
document.body.addEventListener('htmx:beforeRequest', function(event) {
document.body.addEventListener("htmx:beforeRequest", function (event) {
// Assuming 'Purchase1' is the element that triggers the HTMX request
if (event.target.id === 'id_edition') {
var idEditionValue = document.getElementById('id_edition').value;
if (event.target.id === "id_edition") {
var idEditionValue = document.getElementById("id_edition").value;
// Condition to check - replace this with your actual logic
if (idEditionValue != '') {
event.preventDefault(); // This cancels the HTMX request
}
// Condition to check - replace this with your actual logic
if (idEditionValue != "") {
event.preventDefault(); // This cancels the HTMX request
}
}
});

View File

@ -7,10 +7,14 @@ for (let button of document.querySelectorAll("[data-target]")) {
button.addEventListener("click", (event) => {
event.preventDefault();
if (type == "now") {
targetElement.value = toISOUTCString(new Date);
targetElement.value = toISOUTCString(new Date());
} else if (type == "copy") {
const oppositeName = targetElement.name == "timestamp_start" ? "timestamp_end" : "timestamp_start";
document.querySelector(`[name='${oppositeName}']`).value = targetElement.value;
const oppositeName =
targetElement.name == "timestamp_start"
? "timestamp_end"
: "timestamp_start";
document.querySelector(`[name='${oppositeName}']`).value =
targetElement.value;
} else if (type == "toggle") {
if (targetElement.type == "datetime-local") targetElement.type = "text";
else targetElement.type = "datetime-local";

View File

@ -75,7 +75,10 @@ function syncSelectInputUntilChanged(syncData, parentSelector = document) {
* @param {string} property - The property to retrieve the value from.
*/
function getValueFromProperty(sourceElement, property) {
let source = (sourceElement instanceof HTMLSelectElement) ? sourceElement.selectedOptions[0] : sourceElement
let source =
sourceElement instanceof HTMLSelectElement
? sourceElement.selectedOptions[0]
: sourceElement;
if (property.startsWith("dataset.")) {
let datasetKey = property.slice(8); // Remove 'dataset.' part
return source.dataset[datasetKey];
@ -93,13 +96,11 @@ function getValueFromProperty(sourceElement, property) {
*/
function getEl(selector) {
if (selector.startsWith("#")) {
return document.getElementById(selector.slice(1))
}
else if (selector.startsWith(".")) {
return document.getElementsByClassName(selector)
}
else {
return document.getElementsByTagName(selector)
return document.getElementById(selector.slice(1));
} else if (selector.startsWith(".")) {
return document.getElementsByClassName(selector);
} else {
return document.getElementsByTagName(selector);
}
}
@ -116,7 +117,7 @@ function getEl(selector) {
function conditionalElementHandler(...configs) {
configs.forEach(([condition, targetElements, callbackfn1, callbackfn2]) => {
if (condition()) {
targetElements.forEach(elementName => {
targetElements.forEach((elementName) => {
let el = getEl(elementName);
if (el === null) {
console.error(`Element ${elementName} doesn't exist.`);
@ -125,7 +126,7 @@ function conditionalElementHandler(...configs) {
}
});
} else {
targetElements.forEach(elementName => {
targetElements.forEach((elementName) => {
let el = getEl(elementName);
if (el === null) {
console.error(`Element ${elementName} doesn't exist.`);
@ -137,16 +138,44 @@ function conditionalElementHandler(...configs) {
});
}
function disableElementsWhenFalse(targetSelect, targetValue, elementList) {
function disableElementsWhenValueNotEqual(
targetSelect,
targetValue,
elementList
) {
return conditionalElementHandler([
() => {
return getEl(targetSelect).value != targetValue;
let target = getEl(targetSelect);
console.debug(
`${disableElementsWhenTrue.name}: triggered on ${target.id}`
);
console.debug(`
${disableElementsWhenTrue.name}: matching against value(s): ${targetValue}`);
if (targetValue instanceof Array) {
if (targetValue.every((value) => target.value != value)) {
console.debug(
`${disableElementsWhenTrue.name}: none of the values is equal to ${target.value}, returning true.`
);
return true;
}
} else {
console.debug(
`${disableElementsWhenTrue.name}: none of the values is equal to ${target.value}, returning true.`
);
return target.value != targetValue;
}
},
elementList,
(el) => {
console.debug(
`${disableElementsWhenTrue.name}: evaluated true, disabling ${el.id}.`
);
el.disabled = "disabled";
},
(el) => {
console.debug(
`${disableElementsWhenTrue.name}: evaluated false, NOT disabling ${el.id}.`
);
el.disabled = "";
},
]);
@ -167,4 +196,12 @@ function disableElementsWhenTrue(targetSelect, targetValue, elementList) {
]);
}
export { toISOUTCString, syncSelectInputUntilChanged, getEl, conditionalElementHandler, disableElementsWhenFalse, disableElementsWhenTrue, getValueFromProperty };
export {
toISOUTCString,
syncSelectInputUntilChanged,
getEl,
conditionalElementHandler,
disableElementsWhenValueNotEqual,
disableElementsWhenTrue,
getValueFromProperty,
};

View File

@ -18,8 +18,6 @@
</select>
</form>
</div>
<div class="flex flex-column flex-wrap justify-center">
<div class="md:w-1/2">
<h1 class="text-5xl text-center my-6">Playtime</h1>
<table class="responsive-table">
<tbody>
@ -51,10 +49,16 @@
<td class="px-2 sm:px-4 md:px-6 md:py-2">Finished ({{ year }})</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ this_year_finished_this_year.count }}</td>
</tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Longest session</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ longest_session_time }} ({{ longest_session_game }})</td>
</tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Most sessions</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ highest_session_count }} ({{ highest_session_count_game }})</td>
</tr>
</tbody>
</table>
</div>
<div class="md:w-1/2">
<h1 class="text-5xl text-center my-6">Purchases</h1>
<table class="responsive-table">
<tbody>
@ -136,7 +140,13 @@
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
<a class="underline decoration-slate-500 sm:decoration-2"
href="{% url 'edit_purchase' purchase.id %}">{{ purchase.edition.name }}</a>
href="{% url 'edit_purchase' purchase.id %}">
{% if purchase.type == 'dlc' %}
{{ purchase.name }} ({{ purchase.edition.name }} DLC)
{% else %}
{{ purchase.edition.name }}
{% endif %}
</a>
</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
</tr>

View File

@ -61,7 +61,6 @@
{% url 'start_game_session' game.id as add_session_link %}
{% include 'components/button.html' with title="Start new session" text="New" link=add_session_link %}
and Notes <span class="dark:text-slate-500">({{ sessions_with_notes_count }})</span>
</h1>
<ul>
{% for session in sessions %}

View File

@ -2,8 +2,8 @@ from datetime import datetime, timedelta
from typing import Any, Callable
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count, F, Prefetch, Sum
from django.db.models.functions import TruncDate
from django.db.models import Count, ExpressionWrapper, F, Prefetch, Q, Sum, fields
from django.db.models.functions import Extract, TruncDate
from django.http import (
HttpRequest,
HttpResponse,
@ -30,11 +30,11 @@ from .models import Edition, Game, Platform, Purchase, Session
def model_counts(request):
return {
"game_available": Game.objects.count() != 0,
"edition_available": Edition.objects.count() != 0,
"platform_available": Platform.objects.count() != 0,
"purchase_available": Purchase.objects.count() != 0,
"session_count": Session.objects.count(),
"game_available": Game.objects.exists(),
"edition_available": Edition.objects.exists(),
"platform_available": Platform.objects.exists(),
"purchase_available": Purchase.objects.exists(),
"session_count": Session.objects.exists(),
}
@ -321,6 +321,22 @@ def stats(request, year: int = 0):
if year == 0:
year = timezone.now().year
this_year_sessions = Session.objects.filter(timestamp_start__year=year)
this_year_sessions_with_durations = this_year_sessions.annotate(
duration=ExpressionWrapper(
F("timestamp_end") - F("timestamp_start"),
output_field=fields.DurationField(),
)
)
longest_session = this_year_sessions_with_durations.order_by("-duration").first()
this_year_games_with_session_counts = Game.objects.annotate(
session_count=Count(
"edition__purchase__session",
filter=Q(edition__purchase__session__timestamp_start__year=year),
)
)
game_highest_session_count = this_year_games_with_session_counts.order_by(
"-session_count"
).first()
selected_currency = "CZK"
unique_days = (
this_year_sessions.annotate(date=TruncDate("timestamp_start"))
@ -344,8 +360,8 @@ def stats(request, year: int = 0):
this_year_purchases_unfinished = this_year_purchases_without_refunded.filter(
date_finished__isnull=True
).filter(
type=Purchase.GAME
) # do not count DLC etc.
Q(type=Purchase.GAME) | Q(type=Purchase.DLC)
) # do not count battle passes etc.
this_year_purchases_unfinished_percent = int(
safe_division(
@ -444,6 +460,13 @@ def stats(request, year: int = 0):
"date_purchased"
),
"backlog_decrease_count": backlog_decrease_count,
"longest_session_time": format_duration(
longest_session.duration if longest_session else timedelta(0),
"%2.0Hh%2.0mm",
),
"longest_session_game": longest_session.purchase.edition.name,
"highest_session_count": game_highest_session_count.session_count,
"highest_session_count_game": game_highest_session_count.name,
}
request.session["return_path"] = request.path

63
poetry.lock generated
View File

@ -1,9 +1,10 @@
# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
# This file is automatically @generated by Poetry and should not be changed by hand.
[[package]]
name = "asgiref"
version = "3.7.2"
description = "ASGI specs, helper code, and adapters"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -18,6 +19,7 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
name = "black"
version = "22.12.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -51,6 +53,7 @@ uvloop = ["uvloop (>=0.15.2)"]
name = "cfgv"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -62,6 +65,7 @@ files = [
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -76,6 +80,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
@ -87,6 +92,7 @@ files = [
name = "cssbeautifier"
version = "1.14.11"
description = "CSS unobfuscator and beautifier."
category = "dev"
optional = false
python-versions = "*"
files = [
@ -102,6 +108,7 @@ six = ">=1.13.0"
name = "distlib"
version = "0.3.7"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -113,6 +120,7 @@ files = [
name = "django"
version = "4.2.7"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -129,10 +137,27 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"]
[[package]]
name = "django-debug-toolbar"
version = "4.2.0"
description = "A configurable set of panels that display various debug information about the current request/response."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
{file = "django_debug_toolbar-4.2.0-py3-none-any.whl", hash = "sha256:af99128c06e8e794479e65ab62cc6c7d1e74e1c19beb44dcbf9bad7a9c017327"},
{file = "django_debug_toolbar-4.2.0.tar.gz", hash = "sha256:bc7fdaafafcdedefcc67a4a5ad9dac96efd6e41db15bc74d402a54a2ba4854dc"},
]
[package.dependencies]
django = ">=3.2.4"
sqlparse = ">=0.2"
[[package]]
name = "django-extensions"
version = "3.2.3"
description = "Extensions for Django"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -147,6 +172,7 @@ Django = ">=3.2"
name = "djhtml"
version = "1.5.2"
description = "Django/Jinja template indenter"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -160,6 +186,7 @@ dev = ["black", "flake8", "isort", "nox", "pre-commit"]
name = "djlint"
version = "1.34.0"
description = "HTML Template Linter and Formatter"
category = "dev"
optional = false
python-versions = ">=3.8.0,<4.0.0"
files = [
@ -184,6 +211,7 @@ tqdm = ">=4.62.2,<5.0.0"
name = "editorconfig"
version = "0.12.3"
description = "EditorConfig File Locator and Interpreter for Python"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -195,6 +223,7 @@ files = [
name = "filelock"
version = "3.13.1"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -211,6 +240,7 @@ typing = ["typing-extensions (>=4.8)"]
name = "gunicorn"
version = "20.1.0"
description = "WSGI HTTP Server for UNIX"
category = "main"
optional = false
python-versions = ">=3.5"
files = [
@ -231,6 +261,7 @@ tornado = ["tornado (>=0.2)"]
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 = [
@ -242,6 +273,7 @@ files = [
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 = [
@ -253,6 +285,7 @@ files = [
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 = [
@ -264,6 +297,7 @@ files = [
name = "identify"
version = "2.5.31"
description = "File identification library for Python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -278,6 +312,7 @@ license = ["ukkonen"]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -289,6 +324,7 @@ files = [
name = "isort"
version = "5.12.0"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
python-versions = ">=3.8.0"
files = [
@ -306,6 +342,7 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"]
name = "jsbeautifier"
version = "1.14.11"
description = "JavaScript unobfuscator and beautifier."
category = "dev"
optional = false
python-versions = "*"
files = [
@ -320,6 +357,7 @@ six = ">=1.13.0"
name = "json5"
version = "0.9.14"
description = "A Python implementation of the JSON5 data format."
category = "dev"
optional = false
python-versions = "*"
files = [
@ -334,6 +372,7 @@ dev = ["hypothesis"]
name = "markupsafe"
version = "2.1.3"
description = "Safely add untrusted strings to HTML/XML markup."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -403,6 +442,7 @@ files = [
name = "mypy"
version = "0.991"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -452,6 +492,7 @@ reports = ["lxml"]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@ -463,6 +504,7 @@ files = [
name = "nodeenv"
version = "1.8.0"
description = "Node.js virtual environment builder"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [
@ -477,6 +519,7 @@ setuptools = "*"
name = "packaging"
version = "23.2"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -488,6 +531,7 @@ files = [
name = "pathspec"
version = "0.11.2"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -499,6 +543,7 @@ files = [
name = "platformdirs"
version = "3.11.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -514,6 +559,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co
name = "pluggy"
version = "1.3.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -529,6 +575,7 @@ testing = ["pytest", "pytest-benchmark"]
name = "pre-commit"
version = "3.5.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -547,6 +594,7 @@ virtualenv = ">=20.10.0"
name = "pytest"
version = "7.4.3"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -567,6 +615,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -626,6 +675,7 @@ files = [
name = "regex"
version = "2023.10.3"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -723,6 +773,7 @@ files = [
name = "setuptools"
version = "68.2.2"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -739,6 +790,7 @@ testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jar
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 = [
@ -750,6 +802,7 @@ files = [
name = "sqlparse"
version = "0.4.4"
description = "A non-validating SQL parser."
category = "main"
optional = false
python-versions = ">=3.5"
files = [
@ -766,6 +819,7 @@ test = ["pytest", "pytest-cov"]
name = "tqdm"
version = "4.66.1"
description = "Fast, Extensible Progress Meter"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -786,6 +840,7 @@ telegram = ["requests"]
name = "typing-extensions"
version = "4.8.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -797,6 +852,7 @@ files = [
name = "tzdata"
version = "2023.3"
description = "Provider of IANA time zone data"
category = "main"
optional = false
python-versions = ">=2"
files = [
@ -808,6 +864,7 @@ files = [
name = "uvicorn"
version = "0.20.0"
description = "The lightning-fast ASGI server."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -826,6 +883,7 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
name = "virtualenv"
version = "20.24.6"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -846,6 +904,7 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
name = "werkzeug"
version = "2.3.8"
description = "The comprehensive WSGI web application library."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -862,4 +921,4 @@ watchdog = ["watchdog (>=2.3)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "32e7c40e7148530effb10ebd5d67a4f1c8fe30794a4d3b5d213d4f30048c79ea"
content-hash = "498b3358998a9f3bbfb74fae7d6a90de7b55b9bdc76845bce52f65785afd0c1e"

View File

@ -24,6 +24,7 @@ djhtml = "^1.5.2"
djlint = "^1.19.11"
isort = "^5.11.4"
pre-commit = "^3.5.0"
django-debug-toolbar = "^4.2.0"
[tool.isort]
profile = "black"

View File

@ -43,6 +43,7 @@ INSTALLED_APPS = [
if DEBUG:
INSTALLED_APPS.append("django_extensions")
INSTALLED_APPS.append("django.contrib.admin")
INSTALLED_APPS.append("debug_toolbar")
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
@ -54,6 +55,11 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
if DEBUG:
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
INTERNAL_IPS = ["127.0.0.1"]
DEBUG_TOOLBAR_CONFIG = {"ROOT_TAG_EXTRA_ATTRS": "hx-preserve"}
ROOT_URLCONF = "timetracker.urls"
TEMPLATES = [

View File

@ -25,3 +25,4 @@ urlpatterns = [
if settings.DEBUG:
urlpatterns.append(path("admin/", admin.site.urls))
urlpatterns.append(path("__debug__/", include("debug_toolbar.urls")))