Compare commits
1 Commits
fe6cf2758c
...
css_experi
Author | SHA1 | Date | |
---|---|---|---|
241aa9dc13
|
2
.github/workflows/build-docker.yml
vendored
2
.github/workflows/build-docker.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
poetry install
|
poetry install
|
||||||
poetry env info
|
poetry env info
|
||||||
poetry run python manage.py migrate
|
poetry run python manage.py migrate
|
||||||
# PROD=1 poetry run pytest
|
PROD=1 poetry run pytest
|
||||||
build-and-push:
|
build-and-push:
|
||||||
needs: test
|
needs: test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,4 +8,3 @@ db.sqlite3
|
|||||||
/static/
|
/static/
|
||||||
dist/
|
dist/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.python-version
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 24.3.0
|
rev: 22.12.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
|
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,19 +1,8 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
## New
|
|
||||||
* Render notes as Markdown
|
|
||||||
* Require login by default
|
|
||||||
* Add stats for dropped purchases, monthly playtimes
|
|
||||||
* Allow deleting purchases
|
|
||||||
|
|
||||||
## Improved
|
## Improved
|
||||||
* mark refunded purchases red on game overview
|
* mark refunded purchases red on game overview
|
||||||
* increase session count on game overview when starting a new session
|
* increase session count on game overview when starting a new session
|
||||||
* game overview:
|
|
||||||
* sort purchases also by date purchased (on top of date released)
|
|
||||||
* improve header format
|
|
||||||
* stats: improve purchase name consistency
|
|
||||||
* session list: use display name instead of sort name
|
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
* Fix title not being displayed on the Recent sessions page
|
* Fix title not being displayed on the Recent sessions page
|
||||||
|
20
Makefile
20
Makefile
@ -3,7 +3,6 @@ all: css migrate
|
|||||||
initialize: npm css migrate sethookdir loadplatforms
|
initialize: npm css migrate sethookdir loadplatforms
|
||||||
|
|
||||||
HTMLFILES := $(shell find games/templates -type f)
|
HTMLFILES := $(shell find games/templates -type f)
|
||||||
PYTHON_VERSION = 3.12
|
|
||||||
|
|
||||||
npm:
|
npm:
|
||||||
npm install
|
npm install
|
||||||
@ -11,26 +10,17 @@ npm:
|
|||||||
css: common/input.css
|
css: common/input.css
|
||||||
npx tailwindcss -i ./common/input.css -o ./games/static/base.css
|
npx tailwindcss -i ./common/input.css -o ./games/static/base.css
|
||||||
|
|
||||||
|
css-dev: css
|
||||||
|
npx tailwindcss -i ./common/input.css -o ./games/static/base.css --watch
|
||||||
|
|
||||||
makemigrations:
|
makemigrations:
|
||||||
poetry run python manage.py makemigrations
|
poetry run python manage.py makemigrations
|
||||||
|
|
||||||
migrate: makemigrations
|
migrate: makemigrations
|
||||||
poetry run python manage.py migrate
|
poetry run python manage.py migrate
|
||||||
|
|
||||||
init:
|
dev: migrate
|
||||||
pyenv install -s $(PYTHON_VERSION)
|
poetry run python manage.py runserver
|
||||||
pyenv local $(PYTHON_VERSION)
|
|
||||||
pip install poetry
|
|
||||||
poetry install
|
|
||||||
npm install
|
|
||||||
|
|
||||||
dev:
|
|
||||||
@npx concurrently \
|
|
||||||
--names "Django,Tailwind" \
|
|
||||||
--prefix-colors "blue,green" \
|
|
||||||
"poetry run python -Wa manage.py runserver" \
|
|
||||||
"npx tailwindcss -i ./common/input.css -o ./games/static/base.css --watch"
|
|
||||||
|
|
||||||
|
|
||||||
caddy:
|
caddy:
|
||||||
caddy run --watch
|
caddy run --watch
|
||||||
|
12
README.md
12
README.md
@ -1,15 +1,3 @@
|
|||||||
# Timetracker
|
# Timetracker
|
||||||
|
|
||||||
A simple game catalogue and play session tracker.
|
A simple game catalogue and play session tracker.
|
||||||
|
|
||||||
# Development
|
|
||||||
|
|
||||||
The project uses `pyenv` to manage installed Python versions.
|
|
||||||
If you have `pyenv` installed, you can simply run:
|
|
||||||
|
|
||||||
```
|
|
||||||
make init
|
|
||||||
```
|
|
||||||
|
|
||||||
This will make sure the correct Python version is installed, and it will install all dependencies using `poetry`.
|
|
||||||
Afterwards, you can start the development server using `make dev`.
|
|
@ -117,31 +117,3 @@ th label {
|
|||||||
.basic-button {
|
.basic-button {
|
||||||
@apply inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out;
|
@apply inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content ul {
|
|
||||||
list-style-type: disc;
|
|
||||||
list-style-position: inside;
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ol {
|
|
||||||
list-style-type: decimal;
|
|
||||||
list-style-position: inside;
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ul,
|
|
||||||
.markdown-content ol {
|
|
||||||
list-style-position: outside;
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ul ul,
|
|
||||||
.markdown-content ul ol,
|
|
||||||
.markdown-content ol ul,
|
|
||||||
.markdown-content ol ol {
|
|
||||||
list-style-type: circle;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
|
@ -7,24 +7,3 @@ def safe_division(numerator: int | float, denominator: int | float) -> int | flo
|
|||||||
return numerator / denominator
|
return numerator / denominator
|
||||||
except ZeroDivisionError:
|
except ZeroDivisionError:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def safe_getattr(obj, attr_chain, default=None):
|
|
||||||
"""
|
|
||||||
Safely get the nested attribute from an object.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
obj (object): The object from which to retrieve the attribute.
|
|
||||||
attr_chain (str): The chain of attributes, separated by dots.
|
|
||||||
default: The default value to return if any attribute in the chain does not exist.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The value of the nested attribute if it exists, otherwise the default value.
|
|
||||||
"""
|
|
||||||
attrs = attr_chain.split(".")
|
|
||||||
for attr in attrs:
|
|
||||||
try:
|
|
||||||
obj = getattr(obj, attr)
|
|
||||||
except AttributeError:
|
|
||||||
return default
|
|
||||||
return obj
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from common.utils import safe_getattr
|
|
||||||
|
|
||||||
from games.models import Device, Edition, Game, Platform, Purchase, Session
|
from games.models import Device, Edition, Game, Platform, Purchase, Session
|
||||||
|
|
||||||
@ -46,8 +45,8 @@ class EditionChoiceField(forms.ModelChoiceField):
|
|||||||
class IncludePlatformSelect(forms.Select):
|
class IncludePlatformSelect(forms.Select):
|
||||||
def create_option(self, name, value, *args, **kwargs):
|
def create_option(self, name, value, *args, **kwargs):
|
||||||
option = super().create_option(name, value, *args, **kwargs)
|
option = super().create_option(name, value, *args, **kwargs)
|
||||||
if platform_id := safe_getattr(value, "instance.platform.id"):
|
if value:
|
||||||
option["attrs"]["data-platform"] = platform_id
|
option["attrs"]["data-platform"] = value.instance.platform.id
|
||||||
return option
|
return option
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,6 +18,19 @@ class Game(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
def get_sort_name(name):
|
||||||
|
articles = ["a", "an", "the"]
|
||||||
|
name_parts = name.split()
|
||||||
|
first_word = name_parts[0].lower()
|
||||||
|
if first_word in articles:
|
||||||
|
return f"{' '.join(name_parts[1:])}, {name_parts[0]}"
|
||||||
|
else:
|
||||||
|
return name
|
||||||
|
|
||||||
|
self.sort_name = get_sort_name(self.name)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Edition(models.Model):
|
class Edition(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -36,6 +49,19 @@ class Edition(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.sort_name
|
return self.sort_name
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
def get_sort_name(name):
|
||||||
|
articles = ["a", "an", "the"]
|
||||||
|
name_parts = name.split()
|
||||||
|
first_word = name_parts[0].lower()
|
||||||
|
if first_word in articles:
|
||||||
|
return f"{' '.join(name_parts[1:])}, {name_parts[0]}"
|
||||||
|
else:
|
||||||
|
return name
|
||||||
|
|
||||||
|
self.sort_name = get_sort_name(self.name)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class PurchaseQueryset(models.QuerySet):
|
class PurchaseQueryset(models.QuerySet):
|
||||||
def refunded(self):
|
def refunded(self):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
|
! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -211,8 +211,6 @@ textarea {
|
|||||||
/* 1 */
|
/* 1 */
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
letter-spacing: inherit;
|
|
||||||
/* 1 */
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -236,9 +234,9 @@ select {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input:where([type='button']),
|
[type='button'],
|
||||||
input:where([type='reset']),
|
[type='reset'],
|
||||||
input:where([type='submit']) {
|
[type='submit'] {
|
||||||
-webkit-appearance: button;
|
-webkit-appearance: button;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@ -689,10 +687,6 @@ select {
|
|||||||
--tw-backdrop-opacity: ;
|
--tw-backdrop-opacity: ;
|
||||||
--tw-backdrop-saturate: ;
|
--tw-backdrop-saturate: ;
|
||||||
--tw-backdrop-sepia: ;
|
--tw-backdrop-sepia: ;
|
||||||
--tw-contain-size: ;
|
|
||||||
--tw-contain-layout: ;
|
|
||||||
--tw-contain-paint: ;
|
|
||||||
--tw-contain-style: ;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::backdrop {
|
::backdrop {
|
||||||
@ -743,10 +737,6 @@ select {
|
|||||||
--tw-backdrop-opacity: ;
|
--tw-backdrop-opacity: ;
|
||||||
--tw-backdrop-saturate: ;
|
--tw-backdrop-saturate: ;
|
||||||
--tw-backdrop-sepia: ;
|
--tw-backdrop-sepia: ;
|
||||||
--tw-contain-size: ;
|
|
||||||
--tw-contain-layout: ;
|
|
||||||
--tw-contain-paint: ;
|
|
||||||
--tw-contain-style: ;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@ -856,8 +846,8 @@ select {
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mb-8 {
|
.mb-4 {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ml-1 {
|
.ml-1 {
|
||||||
@ -900,10 +890,6 @@ select {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-24 {
|
|
||||||
height: 6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-3 {
|
.h-3 {
|
||||||
height: 0.75rem;
|
height: 0.75rem;
|
||||||
}
|
}
|
||||||
@ -956,10 +942,6 @@ select {
|
|||||||
max-width: 20rem;
|
max-width: 20rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-1 {
|
|
||||||
flex: 1 1 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
@ -1014,6 +996,10 @@ select {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rounded {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.rounded-full {
|
.rounded-full {
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
}
|
}
|
||||||
@ -1051,6 +1037,11 @@ select {
|
|||||||
background-color: rgb(124 58 237 / var(--tw-bg-opacity));
|
background-color: rgb(124 58 237 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-white {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.p-4 {
|
.p-4 {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
@ -1075,10 +1066,6 @@ select {
|
|||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pb-16 {
|
|
||||||
padding-bottom: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-3 {
|
.pl-3 {
|
||||||
padding-left: 0.75rem;
|
padding-left: 0.75rem;
|
||||||
}
|
}
|
||||||
@ -1091,10 +1078,6 @@ select {
|
|||||||
padding-top: 0.25rem;
|
padding-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pt-8 {
|
|
||||||
padding-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@ -1142,6 +1125,10 @@ select {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.text-gray-700 {
|
.text-gray-700 {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||||
@ -1157,11 +1144,6 @@ select {
|
|||||||
color: rgb(203 213 225 / var(--tw-text-opacity));
|
color: rgb(203 213 225 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-slate-500 {
|
|
||||||
--tw-text-opacity: 1;
|
|
||||||
color: rgb(100 116 139 / var(--tw-text-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-white {
|
.text-white {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||||
@ -1250,7 +1232,7 @@ a:hover {
|
|||||||
transition: all 0.2s ease-out;
|
transition: all 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
form label:is(.dark *) {
|
:is(.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));
|
||||||
}
|
}
|
||||||
@ -1260,7 +1242,7 @@ form label:is(.dark *) {
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.responsive-table:is(.dark *) {
|
:is(.dark .responsive-table) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
@ -1291,9 +1273,9 @@ form label:is(.dark *) {
|
|||||||
border-left-color: rgb(100 116 139 / var(--tw-border-opacity));
|
border-left-color: rgb(100 116 139 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
form input:is(.dark *),
|
:is(.dark form input),:is(.dark
|
||||||
select:is(.dark *),
|
select),:is(.dark
|
||||||
textarea:is(.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));
|
||||||
@ -1303,9 +1285,9 @@ textarea:is(.dark *) {
|
|||||||
color: rgb(241 245 249 / var(--tw-text-opacity));
|
color: rgb(241 245 249 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
form input:disabled:is(.dark *),
|
:is(.dark form input:disabled),:is(.dark
|
||||||
select:disabled:is(.dark *),
|
select:disabled),:is(.dark
|
||||||
textarea:disabled:is(.dark *) {
|
textarea:disabled) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(51 65 85 / var(--tw-bg-opacity));
|
background-color: rgb(51 65 85 / var(--tw-bg-opacity));
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
@ -1420,34 +1402,6 @@ th label {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content ul {
|
|
||||||
list-style-type: disc;
|
|
||||||
list-style-position: inside;
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ol {
|
|
||||||
list-style-type: decimal;
|
|
||||||
list-style-position: inside;
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ul,
|
|
||||||
.markdown-content ol {
|
|
||||||
list-style-position: outside;
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ul ul,
|
|
||||||
.markdown-content ul ol,
|
|
||||||
.markdown-content ol ul,
|
|
||||||
.markdown-content ol ol {
|
|
||||||
list-style-type: circle;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover\:bg-gray-400:hover {
|
.hover\:bg-gray-400:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
|
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
|
||||||
@ -1504,32 +1458,32 @@ th label {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-gray-800:is(.dark *) {
|
:is(.dark .dark\:bg-gray-800) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-gray-900:is(.dark *) {
|
:is(.dark .dark\:bg-gray-900) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-slate-400:is(.dark *) {
|
:is(.dark .dark\:text-slate-400) {
|
||||||
--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\:text-slate-500:is(.dark *) {
|
:is(.dark .dark\:text-slate-500) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(100 116 139 / var(--tw-text-opacity));
|
color: rgb(100 116 139 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-slate-600:is(.dark *) {
|
:is(.dark .dark\:text-slate-600) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(71 85 105 / var(--tw-text-opacity));
|
color: rgb(71 85 105 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-white:is(.dark *) {
|
:is(.dark .dark\:text-white) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
@ -22,14 +22,6 @@
|
|||||||
value="Submit & Create Session" />
|
value="Submit & Create Session" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if purchase_id %}
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'delete_purchase' purchase_id %}" class="text-red-600" onclick="return confirm('Are you sure you want to delete this purchase?');">Delete</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -23,8 +23,8 @@
|
|||||||
height="24"
|
height="24"
|
||||||
width="24"
|
width="24"
|
||||||
alt="loading indicator" />
|
alt="loading indicator" />
|
||||||
<div class="flex flex-col min-h-screen">
|
<div class="dark:bg-gray-800 min-h-screen">
|
||||||
<nav class="dark:bg-gray-900 border-gray-200 h-24 flex items-center">
|
<nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
|
||||||
<div class="container flex flex-wrap items-center justify-between mx-auto">
|
<div class="container flex flex-wrap items-center justify-between mx-auto">
|
||||||
<a href="{% url 'list_sessions_recent' %}" class="flex items-center">
|
<a href="{% url 'list_sessions_recent' %}" class="flex items-center">
|
||||||
<span class="text-4xl">
|
<span class="text-4xl">
|
||||||
@ -39,80 +39,72 @@
|
|||||||
<div class="w-full md:block md:w-auto">
|
<div class="w-full md:block md:w-auto">
|
||||||
<ul class="flex flex-col md:flex-row p-4 mt-4 dark:text-white">
|
<ul class="flex flex-col md:flex-row p-4 mt-4 dark:text-white">
|
||||||
<li class="relative group">
|
<li class="relative group">
|
||||||
{% if user.is_authenticated %}
|
<a class="block py-2 pl-3 pr-4 hover:underline"
|
||||||
<a class="block py-2 pl-3 pr-4 hover:underline"
|
href="{% url 'add_game' %}">New</a>
|
||||||
href="{% url 'add_game' %}">New</a>
|
<ul class="absolute hidden text-gray-700 pt-1 group-hover:block w-auto whitespace-nowrap">
|
||||||
<ul class="absolute hidden text-gray-700 pt-1 group-hover:block w-auto whitespace-nowrap">
|
{% if purchase_available %}
|
||||||
{% if purchase_available %}
|
|
||||||
<li>
|
|
||||||
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
|
||||||
href="{% url 'add_device' %}">Device</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
<li>
|
<li>
|
||||||
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
||||||
href="{% url 'add_game' %}">Game</a>
|
href="{% url 'add_device' %}">Device</a>
|
||||||
</li>
|
|
||||||
{% if game_available and platform_available %}
|
|
||||||
<li>
|
|
||||||
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
|
||||||
href="{% url 'add_edition' %}">Edition</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
<li>
|
|
||||||
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
|
||||||
href="{% url 'add_platform' %}">Platform</a>
|
|
||||||
</li>
|
|
||||||
{% if edition_available %}
|
|
||||||
<li>
|
|
||||||
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
|
||||||
href="{% url 'add_purchase' %}">Purchase</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if purchase_available %}
|
|
||||||
<li>
|
|
||||||
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
|
||||||
href="{% url 'add_session' %}">Session</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{% if session_count > 0 %}
|
|
||||||
<li class="relative group">
|
|
||||||
<a class="block py-2 pl-3 pr-4 hover:underline"
|
|
||||||
href="{% url 'stats_current_year' %}">Stats</a>
|
|
||||||
<ul class="absolute hidden text-gray-700 pt-1 group-hover:block">
|
|
||||||
{% for year in stats_dropdown_year_range %}
|
|
||||||
<li>
|
|
||||||
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
|
||||||
href="{% url 'stats_by_year' year %}">{{ year }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="block py-2 pl-3 pr-4 hover:underline"
|
|
||||||
href="{% url 'list_sessions' %}">All Sessions</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="block py-2 pl-3 pr-4 hover:underline"
|
|
||||||
href="{% url 'logout' %}">Log Out</a>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
<li>
|
||||||
</ul>
|
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
||||||
</div>
|
href="{% url 'add_game' %}">Game</a>
|
||||||
|
</li>
|
||||||
|
{% if game_available and platform_available %}
|
||||||
|
<li>
|
||||||
|
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
||||||
|
href="{% url 'add_edition' %}">Edition</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
||||||
|
href="{% url 'add_platform' %}">Platform</a>
|
||||||
|
</li>
|
||||||
|
{% if edition_available %}
|
||||||
|
<li>
|
||||||
|
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
||||||
|
href="{% url 'add_purchase' %}">Purchase</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if purchase_available %}
|
||||||
|
<li>
|
||||||
|
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
||||||
|
href="{% url 'add_session' %}">Session</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% if session_count > 0 %}
|
||||||
|
<li class="relative group">
|
||||||
|
<a class="block py-2 pl-3 pr-4 hover:underline"
|
||||||
|
href="{% url 'stats_current_year' %}">Stats</a>
|
||||||
|
<ul class="absolute hidden text-gray-700 pt-1 group-hover:block">
|
||||||
|
{% for year in stats_dropdown_year_range %}
|
||||||
|
<li>
|
||||||
|
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
||||||
|
href="{% url 'stats_by_year' year %}">{{ year }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="block py-2 pl-3 pr-4 hover:underline"
|
||||||
|
href="{% url 'list_sessions' %}">All Sessions</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
|
||||||
<div class="flex flex-1 dark:bg-gray-800 justify-center pt-8 pb-16">
|
|
||||||
{% block content %}
|
|
||||||
No content here.
|
|
||||||
{% endblock content %}
|
|
||||||
</div>
|
</div>
|
||||||
{% load version %}
|
</nav>
|
||||||
<span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
|
{% block content %}
|
||||||
</div>
|
No content here.
|
||||||
{% block scripts %}
|
{% endblock content %}
|
||||||
{% endblock scripts %}
|
</div>
|
||||||
</body>
|
{% load version %}
|
||||||
</html>
|
<span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
|
||||||
|
{% block scripts %}
|
||||||
|
{% endblock scripts %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
@ -4,39 +4,38 @@
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="flex-col">
|
{% if dataset_count >= 1 %}
|
||||||
{% if dataset_count >= 1 %}
|
{% url 'list_sessions_start_session_from_session' last.id as start_session_url %}
|
||||||
{% url 'list_sessions_start_session_from_session' last.id as start_session_url %}
|
<div class="mx-auto text-center my-4">
|
||||||
<div class="mx-auto text-center my-4">
|
<a id="last-session-start"
|
||||||
<a id="last-session-start"
|
href="{{ start_session_url }}"
|
||||||
href="{{ start_session_url }}"
|
hx-get="{{ start_session_url }}"
|
||||||
hx-get="{{ start_session_url }}"
|
hx-swap="afterbegin"
|
||||||
hx-swap="afterbegin"
|
hx-target=".responsive-table tbody"
|
||||||
hx-target=".responsive-table tbody"
|
onClick="document.querySelector('#last-session-start').classList.add('invisible')"
|
||||||
onClick="document.querySelector('#last-session-start').classList.add('invisible')"
|
class="{% if last.timestamp_end == null %}invisible{% endif %}">
|
||||||
class="{% if last.timestamp_end == null %}invisible{% endif %}">
|
{% include 'components/button_start.html' with text=last.purchase title="Start session of last played game" only %}
|
||||||
{% include 'components/button_start.html' with text=last.purchase title="Start session of last played game" only %}
|
</a>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
{% if dataset_count != 0 %}
|
||||||
{% if dataset_count != 0 %}
|
<table class="responsive-table">
|
||||||
<table class="responsive-table">
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Name</th>
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Name</th>
|
<th class="hidden sm:table-cell px-2 sm:px-4 md:px-6 md:py-2">Start</th>
|
||||||
<th class="hidden sm:table-cell px-2 sm:px-4 md:px-6 md:py-2">Start</th>
|
<th class="hidden lg:table-cell px-2 sm:px-4 md:px-6 md:py-2">End</th>
|
||||||
<th class="hidden lg:table-cell px-2 sm:px-4 md:px-6 md:py-2">End</th>
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Duration</th>
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Duration</th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody>
|
||||||
<tbody>
|
{% for session in dataset %}
|
||||||
{% for session in dataset %}
|
|
||||||
{% partialdef session-row inline=True %}
|
{% partialdef session-row inline=True %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char md:max-w-40char">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char md:max-w-40char">
|
||||||
<a class="underline decoration-slate-500 sm:decoration-2"
|
<a class="underline decoration-slate-500 sm:decoration-2"
|
||||||
href="{% url 'view_game' session.purchase.edition.game.id %}">
|
href="{% url 'view_game' session.purchase.edition.game.id %}">
|
||||||
{{ session.purchase.edition.name }}
|
{{ session.purchase.edition }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden sm:table-cell">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden sm:table-cell">
|
||||||
@ -44,13 +43,13 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell">
|
||||||
{% if not session.timestamp_end %}
|
{% if not session.timestamp_end %}
|
||||||
{% url 'list_sessions_end_session' session.id as end_session_url %}
|
{% url 'list_sessions_end_session' session.id as end_session_url %}
|
||||||
<a href="{{ end_session_url }}"
|
<a href="{{ end_session_url }}"
|
||||||
hx-get="{{ end_session_url }}"
|
hx-get="{{ end_session_url }}"
|
||||||
hx-target="closest tr"
|
hx-target="closest tr"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
hx-indicator="#indicator"
|
hx-indicator="#indicator"
|
||||||
onClick="document.querySelector('#last-session-start').classList.remove('invisible')">
|
onClick="document.querySelector('#last-session-start').classList.remove('invisible')">
|
||||||
<span class="text-yellow-300">Finish now?</span>
|
<span class="text-yellow-300">Finish now?</span>
|
||||||
</a>
|
</a>
|
||||||
{% elif session.duration_manual %}
|
{% elif session.duration_manual %}
|
||||||
@ -62,11 +61,10 @@
|
|||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ session.duration_formatted }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ session.duration_formatted }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endpartialdef %}
|
{% endpartialdef %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="mx-auto text-center text-slate-300 text-xl">No sessions found.</div>
|
<div class="mx-auto text-center text-slate-300 text-xl">No sessions found.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
36
games/templates/manage.html
Normal file
36
games/templates/manage.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<table class="table table-sm table-zebra">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left">
|
||||||
|
<th></th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Year</th>
|
||||||
|
<th>Wikidata ID</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Manage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for game in games %}
|
||||||
|
<tr>
|
||||||
|
<th>{{ game.pk }}</th>
|
||||||
|
<td>{{ game.name }}</td>
|
||||||
|
<td>{{ game.year_released }}</td>
|
||||||
|
<td>{{ game.wikidata }}</td>
|
||||||
|
<td>{{ game.created_at }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="join">
|
||||||
|
<button class="btn btn-primary btn-sm join-item">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning btn-sm join-item">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock content %}
|
@ -1,23 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
Login
|
|
||||||
{% endblock title %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="flex items-center flex-col">
|
|
||||||
<h2 class="text-3xl text-white mb-8">Please log in to continue</h2>
|
|
||||||
<form method="post">
|
|
||||||
<table>
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form.as_table }}
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<input type="submit" value="Login" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</form>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
@ -3,15 +3,6 @@
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% partialdef purchase-name %}
|
|
||||||
{% if purchase.type != 'game' %}
|
|
||||||
{{ purchase.name }} ({{ purchase.edition.name }} {{ purchase.get_type_display }})
|
|
||||||
{% else %}
|
|
||||||
{{ purchase.edition.name }}
|
|
||||||
{% endif %}
|
|
||||||
{% endpartialdef %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
|
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
|
||||||
<div class="flex justify-center items-center">
|
<div class="flex justify-center items-center">
|
||||||
@ -82,19 +73,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h1 class="text-5xl text-center my-6">Playtime per month</h1>
|
|
||||||
<table class="responsive-table">
|
|
||||||
<tbody>
|
|
||||||
{% for month in month_playtimes %}
|
|
||||||
<tr>
|
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2">{{ month.month | date:"F" }}</td>
|
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ month.playtime }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h1 class="text-5xl text-center my-6">Purchases</h1>
|
<h1 class="text-5xl text-center my-6">Purchases</h1>
|
||||||
<table class="responsive-table">
|
<table class="responsive-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -108,12 +86,6 @@
|
|||||||
{{ all_purchased_refunded_this_year_count }} ({{ refunded_percent }}%)
|
{{ all_purchased_refunded_this_year_count }} ({{ refunded_percent }}%)
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2">Dropped</td>
|
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
|
||||||
{{ dropped_count }} ({{ dropped_percentage }}%)
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2">Unfinished</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2">Unfinished</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
@ -181,7 +153,11 @@
|
|||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
<a class="underline decoration-slate-500 sm:decoration-2"
|
<a class="underline decoration-slate-500 sm:decoration-2"
|
||||||
href="{% url 'edit_purchase' purchase.id %}">
|
href="{% url 'edit_purchase' purchase.id %}">
|
||||||
{% partial purchase-name %}
|
{% if purchase.type == 'dlc' %}
|
||||||
|
{{ purchase.name }} ({{ purchase.edition.name }} DLC)
|
||||||
|
{% else %}
|
||||||
|
{{ purchase.edition.name }}
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
|
||||||
@ -245,7 +221,8 @@
|
|||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
<a class="underline decoration-slate-500 sm:decoration-2"
|
<a class="underline decoration-slate-500 sm:decoration-2"
|
||||||
href="{% url 'edit_purchase' purchase.id %}">
|
href="{% url 'edit_purchase' purchase.id %}">
|
||||||
{% partial purchase-name %}
|
{{ purchase.edition.name }}
|
||||||
|
{% if purchase.type == "dlc" %}({{ purchase.name }}, {{ purchase.get_type_display }}){% endif %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
|
||||||
@ -270,7 +247,8 @@
|
|||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
<a class="underline decoration-slate-500 sm:decoration-2"
|
<a class="underline decoration-slate-500 sm:decoration-2"
|
||||||
href="{% url 'edit_purchase' purchase.id %}">
|
href="{% url 'edit_purchase' purchase.id %}">
|
||||||
{% partial purchase-name %}
|
{{ purchase.edition.name }}
|
||||||
|
{% if purchase.type != "game" %}({{ purchase.name }}, {{ purchase.get_type_display }}){% endif %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
|
||||||
|
@ -3,22 +3,18 @@
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load markdown_extras %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
|
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
|
||||||
<h1 class="text-5xl flex items-center">
|
<h1 class="text-4xl flex items-center">
|
||||||
{{ game.name }}
|
{{ game.name }}
|
||||||
|
<span class="dark:text-slate-500">(#{{ game.pk }})</span>
|
||||||
{% url 'edit_game' game.id as edit_url %}
|
{% url 'edit_game' game.id as edit_url %}
|
||||||
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
||||||
</h1>
|
</h1>
|
||||||
<h2 class="text-lg my-2 ml-2 dark:text-slate-500">First Released: <span class="text-white">{{ game.year_released }}</span></h2>
|
|
||||||
<h2 class="text-lg my-2 ml-2">
|
<h2 class="text-lg my-2 ml-2">
|
||||||
<span class="dark:text-slate-500">Playtime: </span>
|
{{ hours_sum }} <span class="dark:text-slate-500">total</span>
|
||||||
{{ hours_sum }} <span class="dark:text-slate-500">hours over</span> {{ session_count }} <span class="dark:text-slate-500">sessions (</span>{{ session_average }}<span class="dark:text-slate-500">/session)</span>
|
{{ session_average }} <span class="dark:text-slate-500">avg</span>
|
||||||
</h2>
|
({{ playrange }})
|
||||||
<h2 class="text-lg my-2 ml-2">
|
|
||||||
<span class="dark:text-slate-500">Played in: </span>
|
|
||||||
{{ playrange }}
|
|
||||||
</h2>
|
</h2>
|
||||||
<hr class="border-slate-500">
|
<hr class="border-slate-500">
|
||||||
<h1 class="text-3xl mt-4 mb-1">
|
<h1 class="text-3xl mt-4 mb-1">
|
||||||
@ -78,29 +74,29 @@
|
|||||||
{% for session in sessions %}
|
{% for session in sessions %}
|
||||||
{% partialdef session-info inline=True %}
|
{% partialdef session-info inline=True %}
|
||||||
<li class="sm:pl-2 mt-4 mb-2 dark:text-slate-400 flex items-center space-x-1">
|
<li class="sm:pl-2 mt-4 mb-2 dark:text-slate-400 flex items-center space-x-1">
|
||||||
{{ session.timestamp_start | date:"d/m/Y H:i" }}{% if session.timestamp_end %}-{{ session.timestamp_end | date:"H:i" }}{% endif %}
|
{{ session.timestamp_start | date:"d/m/Y H:m" }}
|
||||||
({{ session.device.get_type_display | default:"Unknown" }}, {{ session.duration_formatted }})
|
({{ session.device.get_type_display | default:"Unknown" }}, {{ session.duration_formatted }})
|
||||||
{% url 'edit_session' session.id as edit_url %}
|
{% url 'edit_session' session.id as edit_url %}
|
||||||
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
||||||
{% if not session.timestamp_end %}
|
{% if not session.timestamp_end %}
|
||||||
{% url 'view_game_end_session' session.id as end_session_url %}
|
{% url 'view_game_end_session' session.id as end_session_url %}
|
||||||
<a
|
<a
|
||||||
class="flex bg-green-600 rounded-full px-2 w-7 h-4 text-white justify-center items-center"
|
class="flex bg-green-600 rounded-full px-2 w-7 h-4 text-white justify-center items-center"
|
||||||
href="{{ end_session_url }}"
|
href="{{ end_session_url }}"
|
||||||
hx-get="{{ end_session_url }}"
|
hx-get="{{ end_session_url }}"
|
||||||
hx-target="closest li"
|
hx-target="closest li"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
hx-vals="js:{session_count:getSessionCount()}"
|
hx-vals="js:{session_count:getSessionCount()}"
|
||||||
hx-indicator="#indicator"
|
hx-indicator="#indicator"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" class="h-3" x="0px" y="0px" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" class="h-3" x="0px" y="0px" viewBox="0 0 24 24">
|
||||||
<path d="M 12 2 C 6.486 2 2 6.486 2 12 C 2 17.514 6.486 22 12 22 C 17.514 22 22 17.514 22 12 C 22 10.874 21.803984 9.7942031 21.458984 8.7832031 L 19.839844 10.402344 C 19.944844 10.918344 20 11.453 20 12 C 20 16.411 16.411 20 12 20 C 7.589 20 4 16.411 4 12 C 4 7.589 7.589 4 12 4 C 13.633 4 15.151922 4.4938906 16.419922 5.3378906 L 17.851562 3.90625 C 16.203562 2.71225 14.185 2 12 2 z M 21.292969 3.2929688 L 11 13.585938 L 7.7070312 10.292969 L 6.2929688 11.707031 L 11 16.414062 L 22.707031 4.7070312 L 21.292969 3.2929688 z"></path>
|
<path d="M 12 2 C 6.486 2 2 6.486 2 12 C 2 17.514 6.486 22 12 22 C 17.514 22 22 17.514 22 12 C 22 10.874 21.803984 9.7942031 21.458984 8.7832031 L 19.839844 10.402344 C 19.944844 10.918344 20 11.453 20 12 C 20 16.411 16.411 20 12 20 C 7.589 20 4 16.411 4 12 C 4 7.589 7.589 4 12 4 C 13.633 4 15.151922 4.4938906 16.419922 5.3378906 L 17.851562 3.90625 C 16.203562 2.71225 14.185 2 12 2 z M 21.292969 3.2929688 L 11 13.585938 L 7.7070312 10.292969 L 6.2929688 11.707031 L 11 16.414062 L 22.707031 4.7070312 L 21.292969 3.2929688 z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
<li class="sm:pl-4 markdown-content">{{ session.note|markdown }}</li>
|
<li class="sm:pl-4 italic">{{ session.note|linebreaks }}</li>
|
||||||
<div class="hidden" hx-swap-oob="innerHTML:#session-count">
|
<div class="hidden" hx-swap-oob="innerHTML:#session-count">
|
||||||
({{ session_count }})
|
({{ session_count }})
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
from django import template
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
import markdown
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="markdown")
|
|
||||||
def markdown_format(text):
|
|
||||||
return mark_safe(markdown.markdown(text))
|
|
@ -4,60 +4,32 @@ from games import views
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.index, name="index"),
|
path("", views.index, name="index"),
|
||||||
path("device/add", views.add_device, name="add_device"),
|
|
||||||
path("edition/add", views.add_edition, name="add_edition"),
|
|
||||||
path(
|
path(
|
||||||
"edition/add/for-game/<int:game_id>",
|
"list-sessions/recent",
|
||||||
views.add_edition,
|
views.list_sessions,
|
||||||
name="add_edition_for_game",
|
{"filter": "recent"},
|
||||||
|
name="list_sessions_recent",
|
||||||
),
|
),
|
||||||
path("edition/<int:edition_id>/edit", views.edit_edition, name="edit_edition"),
|
path("add-game/", views.add_game, name="add_game"),
|
||||||
path("game/add", views.add_game, name="add_game"),
|
path("add-platform/", views.add_platform, name="add_platform"),
|
||||||
path("game/<int:game_id>/edit", views.edit_game, name="edit_game"),
|
path("add-session/", views.add_session, name="add_session"),
|
||||||
path("game/<int:game_id>/view", views.view_game, name="view_game"),
|
|
||||||
path("platform/add", views.add_platform, name="add_platform"),
|
|
||||||
path("platform/<int:platform_id>/edit", views.edit_platform, name="edit_platform"),
|
|
||||||
path("purchase/add", views.add_purchase, name="add_purchase"),
|
|
||||||
path("purchase/<int:purchase_id>/edit", views.edit_purchase, name="edit_purchase"),
|
|
||||||
path(
|
path(
|
||||||
"purchase/<int:purchase_id>/delete",
|
"add-session-for-purchase/<int:purchase_id>",
|
||||||
views.delete_purchase,
|
|
||||||
name="delete_purchase",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"purchase/related-purchase-by-edition",
|
|
||||||
views.related_purchase_by_edition,
|
|
||||||
name="related_purchase_by_edition",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"purchase/add/for-edition/<int:edition_id>",
|
|
||||||
views.add_purchase,
|
|
||||||
name="add_purchase_for_edition",
|
|
||||||
),
|
|
||||||
path("session/add", views.add_session, name="add_session"),
|
|
||||||
path(
|
|
||||||
"session/add/for-purchase/<int:purchase_id>",
|
|
||||||
views.add_session,
|
views.add_session,
|
||||||
name="add_session_for_purchase",
|
name="add_session_for_purchase",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"session/add/from-game/<int:session_id>",
|
"session/clone/from-game/<int:session_id>",
|
||||||
views.new_session_from_existing_session,
|
views.new_session_from_existing_session,
|
||||||
{"template": "view_game.html#session-info"},
|
{"template": "view_game.html#session-info"},
|
||||||
name="view_game_start_session_from_session",
|
name="view_game_start_session_from_session",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"session/add/from-list/<int:session_id>",
|
"session/clone/from-list/<int:session_id>",
|
||||||
views.new_session_from_existing_session,
|
views.new_session_from_existing_session,
|
||||||
{"template": "list_sessions.html#session-row"},
|
{"template": "list_sessions.html#session-row"},
|
||||||
name="list_sessions_start_session_from_session",
|
name="list_sessions_start_session_from_session",
|
||||||
),
|
),
|
||||||
path("session/<int:session_id>/edit", views.edit_session, name="edit_session"),
|
|
||||||
path(
|
|
||||||
"session/<int:session_id>/delete",
|
|
||||||
views.delete_session,
|
|
||||||
name="delete_session",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"session/end/from-game/<int:session_id>",
|
"session/end/from-game/<int:session_id>",
|
||||||
views.end_session,
|
views.end_session,
|
||||||
@ -70,39 +42,62 @@ urlpatterns = [
|
|||||||
{"template": "list_sessions.html#session-row"},
|
{"template": "list_sessions.html#session-row"},
|
||||||
name="list_sessions_end_session",
|
name="list_sessions_end_session",
|
||||||
),
|
),
|
||||||
path("session/list", views.list_sessions, name="list_sessions"),
|
# path(
|
||||||
|
# "delete_session/by-id/<int:session_id>",
|
||||||
|
# views.delete_session,
|
||||||
|
# name="delete_session",
|
||||||
|
# ),
|
||||||
|
path("add-purchase/", views.add_purchase, name="add_purchase"),
|
||||||
path(
|
path(
|
||||||
"session/list/recent",
|
"add-purchase-for-edition/<int:edition_id>",
|
||||||
views.list_sessions,
|
views.add_purchase,
|
||||||
{"filter": "recent"},
|
name="add_purchase_for_edition",
|
||||||
name="list_sessions_recent",
|
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"session/list/by-purchase/<int:purchase_id>",
|
"related-purchase-by-edition",
|
||||||
|
views.related_purchase_by_edition,
|
||||||
|
name="related_purchase_by_edition",
|
||||||
|
),
|
||||||
|
path("add-edition/", views.add_edition, name="add_edition"),
|
||||||
|
path(
|
||||||
|
"add-edition-for-game/<int:game_id>",
|
||||||
|
views.add_edition,
|
||||||
|
name="add_edition_for_game",
|
||||||
|
),
|
||||||
|
path("edit-edition/<int:edition_id>", views.edit_edition, name="edit_edition"),
|
||||||
|
path("game/<int:game_id>/view", views.view_game, name="view_game"),
|
||||||
|
path("game/<int:game_id>/edit", views.edit_game, name="edit_game"),
|
||||||
|
path("edit-platform/<int:platform_id>", views.edit_platform, name="edit_platform"),
|
||||||
|
path("add-device/", views.add_device, name="add_device"),
|
||||||
|
path("edit-session/<int:session_id>", views.edit_session, name="edit_session"),
|
||||||
|
path("edit-purchase/<int:purchase_id>", views.edit_purchase, name="edit_purchase"),
|
||||||
|
path("list-sessions/", views.list_sessions, name="list_sessions"),
|
||||||
|
path(
|
||||||
|
"list-sessions/by-purchase/<int:purchase_id>",
|
||||||
views.list_sessions,
|
views.list_sessions,
|
||||||
{"filter": "purchase"},
|
{"filter": "purchase"},
|
||||||
name="list_sessions_by_purchase",
|
name="list_sessions_by_purchase",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"session/list/by-platform/<int:platform_id>",
|
"list-sessions/by-platform/<int:platform_id>",
|
||||||
views.list_sessions,
|
views.list_sessions,
|
||||||
{"filter": "platform"},
|
{"filter": "platform"},
|
||||||
name="list_sessions_by_platform",
|
name="list_sessions_by_platform",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"session/list/by-game/<int:game_id>",
|
"list-sessions/by-game/<int:game_id>",
|
||||||
views.list_sessions,
|
views.list_sessions,
|
||||||
{"filter": "game"},
|
{"filter": "game"},
|
||||||
name="list_sessions_by_game",
|
name="list_sessions_by_game",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"session/list/by-edition/<int:edition_id>",
|
"list-sessions/by-edition/<int:edition_id>",
|
||||||
views.list_sessions,
|
views.list_sessions,
|
||||||
{"filter": "edition"},
|
{"filter": "edition"},
|
||||||
name="list_sessions_by_edition",
|
name="list_sessions_by_edition",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"session/list/by-ownership/<str:ownership_type>",
|
"list-sessions/by-ownership/<str:ownership_type>",
|
||||||
views.list_sessions,
|
views.list_sessions,
|
||||||
{"filter": "ownership_type"},
|
{"filter": "ownership_type"},
|
||||||
name="list_sessions_by_ownership_type",
|
name="list_sessions_by_ownership_type",
|
||||||
|
118
games/views.py
118
games/views.py
@ -1,7 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
import re
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
|
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Avg,
|
Avg,
|
||||||
@ -12,9 +11,8 @@ from django.db.models import (
|
|||||||
Q,
|
Q,
|
||||||
Sum,
|
Sum,
|
||||||
fields,
|
fields,
|
||||||
IntegerField,
|
|
||||||
)
|
)
|
||||||
from django.db.models.functions import TruncDate, ExtractMonth, TruncMonth
|
from django.db.models.functions import TruncDate
|
||||||
from django.http import (
|
from django.http import (
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
@ -55,7 +53,6 @@ def stats_dropdown_year_range(request):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def add_session(request, purchase_id=None):
|
def add_session(request, purchase_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
initial = {"timestamp_start": timezone.now()}
|
initial = {"timestamp_start": timezone.now()}
|
||||||
@ -104,7 +101,6 @@ def use_custom_redirect(
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@use_custom_redirect
|
@use_custom_redirect
|
||||||
def edit_session(request, session_id=None):
|
def edit_session(request, session_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
@ -118,7 +114,6 @@ def edit_session(request, session_id=None):
|
|||||||
return render(request, "add_session.html", context)
|
return render(request, "add_session.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@use_custom_redirect
|
@use_custom_redirect
|
||||||
def edit_purchase(request, purchase_id=None):
|
def edit_purchase(request, purchase_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
@ -129,12 +124,10 @@ def edit_purchase(request, purchase_id=None):
|
|||||||
return redirect("list_sessions")
|
return redirect("list_sessions")
|
||||||
context["title"] = "Edit Purchase"
|
context["title"] = "Edit Purchase"
|
||||||
context["form"] = form
|
context["form"] = form
|
||||||
context["purchase_id"] = purchase_id
|
|
||||||
context["script_name"] = "add_purchase.js"
|
context["script_name"] = "add_purchase.js"
|
||||||
return render(request, "add_purchase.html", context)
|
return render(request, "add_purchase.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@use_custom_redirect
|
@use_custom_redirect
|
||||||
def edit_game(request, game_id=None):
|
def edit_game(request, game_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
@ -148,14 +141,11 @@ def edit_game(request, game_id=None):
|
|||||||
return render(request, "add.html", context)
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def view_game(request, game_id=None):
|
def view_game(request, game_id=None):
|
||||||
game = Game.objects.get(id=game_id)
|
game = Game.objects.get(id=game_id)
|
||||||
nongame_related_purchases_prefetch = Prefetch(
|
nongame_related_purchases_prefetch = Prefetch(
|
||||||
"related_purchases",
|
"related_purchases",
|
||||||
queryset=Purchase.objects.exclude(type=Purchase.GAME).order_by(
|
queryset=Purchase.objects.exclude(type=Purchase.GAME),
|
||||||
"date_purchased"
|
|
||||||
),
|
|
||||||
to_attr="nongame_related_purchases",
|
to_attr="nongame_related_purchases",
|
||||||
)
|
)
|
||||||
game_purchases_prefetch = Prefetch(
|
game_purchases_prefetch = Prefetch(
|
||||||
@ -206,7 +196,6 @@ def view_game(request, game_id=None):
|
|||||||
return render(request, "view_game.html", context)
|
return render(request, "view_game.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@use_custom_redirect
|
@use_custom_redirect
|
||||||
def edit_platform(request, platform_id=None):
|
def edit_platform(request, platform_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
@ -220,7 +209,6 @@ def edit_platform(request, platform_id=None):
|
|||||||
return render(request, "add.html", context)
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@use_custom_redirect
|
@use_custom_redirect
|
||||||
def edit_edition(request, edition_id=None):
|
def edit_edition(request, edition_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
@ -256,7 +244,6 @@ def clone_session_by_id(session_id: int) -> Session:
|
|||||||
return clone
|
return clone
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@use_custom_redirect
|
@use_custom_redirect
|
||||||
def new_session_from_existing_session(request, session_id: int, template: str = ""):
|
def new_session_from_existing_session(request, session_id: int, template: str = ""):
|
||||||
session = clone_session_by_id(session_id)
|
session = clone_session_by_id(session_id)
|
||||||
@ -269,7 +256,6 @@ def new_session_from_existing_session(request, session_id: int, template: str =
|
|||||||
return redirect("list_sessions")
|
return redirect("list_sessions")
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@use_custom_redirect
|
@use_custom_redirect
|
||||||
def end_session(request, session_id: int, template: str = ""):
|
def end_session(request, session_id: int, template: str = ""):
|
||||||
session = get_object_or_404(Session, id=session_id)
|
session = get_object_or_404(Session, id=session_id)
|
||||||
@ -284,14 +270,12 @@ def end_session(request, session_id: int, template: str = ""):
|
|||||||
return redirect("list_sessions")
|
return redirect("list_sessions")
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
# def delete_session(request, session_id=None):
|
||||||
def delete_session(request, session_id=None):
|
# session = Session.objects.get(id=session_id)
|
||||||
session = get_object_or_404(Session, id=session_id)
|
# session.delete()
|
||||||
session.delete()
|
# return redirect("list_sessions")
|
||||||
return redirect("list_sessions")
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def list_sessions(
|
def list_sessions(
|
||||||
request,
|
request,
|
||||||
filter="",
|
filter="",
|
||||||
@ -343,7 +327,6 @@ def list_sessions(
|
|||||||
return render(request, "list_sessions.html", context)
|
return render(request, "list_sessions.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def stats(request, year: int = 0):
|
def stats(request, year: int = 0):
|
||||||
selected_year = request.GET.get("year")
|
selected_year = request.GET.get("year")
|
||||||
if selected_year:
|
if selected_year:
|
||||||
@ -392,23 +375,13 @@ def stats(request, year: int = 0):
|
|||||||
)
|
)
|
||||||
this_year_purchases_refunded = this_year_purchases_with_currency.refunded()
|
this_year_purchases_refunded = this_year_purchases_with_currency.refunded()
|
||||||
|
|
||||||
this_year_purchases_unfinished_dropped_nondropped = (
|
this_year_purchases_unfinished = (
|
||||||
this_year_purchases_without_refunded.filter(date_finished__isnull=True)
|
this_year_purchases_without_refunded.filter(date_finished__isnull=True)
|
||||||
|
.filter(date_dropped__isnull=True)
|
||||||
.filter(infinite=False)
|
.filter(infinite=False)
|
||||||
.filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC))
|
.filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC))
|
||||||
) # do not count battle passes etc.
|
) # do not count battle passes etc.
|
||||||
|
|
||||||
this_year_purchases_unfinished = (
|
|
||||||
this_year_purchases_unfinished_dropped_nondropped.filter(
|
|
||||||
date_dropped__isnull=True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
this_year_purchases_dropped = (
|
|
||||||
this_year_purchases_unfinished_dropped_nondropped.filter(
|
|
||||||
date_dropped__isnull=False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
this_year_purchases_without_refunded_count = (
|
this_year_purchases_without_refunded_count = (
|
||||||
this_year_purchases_without_refunded.count()
|
this_year_purchases_without_refunded.count()
|
||||||
)
|
)
|
||||||
@ -446,15 +419,6 @@ def stats(request, year: int = 0):
|
|||||||
)
|
)
|
||||||
.values("id", "name", "total_playtime")
|
.values("id", "name", "total_playtime")
|
||||||
)
|
)
|
||||||
month_playtimes = (
|
|
||||||
this_year_sessions.annotate(month=TruncMonth("timestamp_start"))
|
|
||||||
.values("month")
|
|
||||||
.annotate(playtime=Sum("duration_calculated"))
|
|
||||||
.order_by("month")
|
|
||||||
)
|
|
||||||
for month in month_playtimes:
|
|
||||||
month["playtime"] = format_duration(month["playtime"], "%2.0H")
|
|
||||||
|
|
||||||
highest_session_average_game = (
|
highest_session_average_game = (
|
||||||
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
|
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
|
||||||
.annotate(
|
.annotate(
|
||||||
@ -497,12 +461,6 @@ def stats(request, year: int = 0):
|
|||||||
|
|
||||||
all_purchased_this_year_count = this_year_purchases_with_currency.count()
|
all_purchased_this_year_count = this_year_purchases_with_currency.count()
|
||||||
all_purchased_refunded_this_year_count = this_year_purchases_refunded.count()
|
all_purchased_refunded_this_year_count = this_year_purchases_refunded.count()
|
||||||
|
|
||||||
this_year_purchases_dropped_count = this_year_purchases_dropped.count()
|
|
||||||
this_year_purchases_dropped_percentage = int(
|
|
||||||
safe_division(this_year_purchases_dropped_count, all_purchased_this_year_count)
|
|
||||||
* 100
|
|
||||||
)
|
|
||||||
context = {
|
context = {
|
||||||
"total_hours": format_duration(
|
"total_hours": format_duration(
|
||||||
this_year_sessions.total_duration_unformatted(), "%2.0H"
|
this_year_sessions.total_duration_unformatted(), "%2.0H"
|
||||||
@ -541,8 +499,6 @@ def stats(request, year: int = 0):
|
|||||||
"purchased_unfinished": this_year_purchases_unfinished,
|
"purchased_unfinished": this_year_purchases_unfinished,
|
||||||
"purchased_unfinished_count": this_year_purchases_unfinished_count,
|
"purchased_unfinished_count": this_year_purchases_unfinished_count,
|
||||||
"unfinished_purchases_percent": this_year_purchases_unfinished_percent,
|
"unfinished_purchases_percent": this_year_purchases_unfinished_percent,
|
||||||
"dropped_count": this_year_purchases_dropped_count,
|
|
||||||
"dropped_percentage": this_year_purchases_dropped_percentage,
|
|
||||||
"refunded_percent": int(
|
"refunded_percent": int(
|
||||||
safe_division(
|
safe_division(
|
||||||
all_purchased_refunded_this_year_count,
|
all_purchased_refunded_this_year_count,
|
||||||
@ -557,50 +513,37 @@ def stats(request, year: int = 0):
|
|||||||
),
|
),
|
||||||
"all_purchased_this_year_count": all_purchased_this_year_count,
|
"all_purchased_this_year_count": all_purchased_this_year_count,
|
||||||
"backlog_decrease_count": backlog_decrease_count,
|
"backlog_decrease_count": backlog_decrease_count,
|
||||||
"longest_session_time": (
|
"longest_session_time": format_duration(
|
||||||
format_duration(longest_session.duration, "%2.0Hh %2.0mm")
|
longest_session.duration, "%2.0Hh %2.0mm"
|
||||||
if longest_session
|
)
|
||||||
else 0
|
if longest_session
|
||||||
),
|
else 0,
|
||||||
"longest_session_game": (
|
"longest_session_game": longest_session.purchase.edition.name
|
||||||
longest_session.purchase.edition.name if longest_session else "N/A"
|
if longest_session
|
||||||
),
|
else "N/A",
|
||||||
"highest_session_count": (
|
"highest_session_count": game_highest_session_count.session_count
|
||||||
game_highest_session_count.session_count
|
if game_highest_session_count
|
||||||
if game_highest_session_count
|
else 0,
|
||||||
else 0
|
"highest_session_count_game": game_highest_session_count.name
|
||||||
),
|
if game_highest_session_count
|
||||||
"highest_session_count_game": (
|
else "N/A",
|
||||||
game_highest_session_count.name if game_highest_session_count else "N/A"
|
"highest_session_average": format_duration(
|
||||||
),
|
highest_session_average_game.session_average, "%2.0Hh %2.0mm"
|
||||||
"highest_session_average": (
|
)
|
||||||
format_duration(
|
if highest_session_average_game
|
||||||
highest_session_average_game.session_average, "%2.0Hh %2.0mm"
|
else 0,
|
||||||
)
|
|
||||||
if highest_session_average_game
|
|
||||||
else 0
|
|
||||||
),
|
|
||||||
"highest_session_average_game": highest_session_average_game,
|
"highest_session_average_game": highest_session_average_game,
|
||||||
"first_play_name": first_play_name,
|
"first_play_name": first_play_name,
|
||||||
"first_play_date": first_play_date,
|
"first_play_date": first_play_date,
|
||||||
"last_play_name": last_play_name,
|
"last_play_name": last_play_name,
|
||||||
"last_play_date": last_play_date,
|
"last_play_date": last_play_date,
|
||||||
"title": f"{year} Stats",
|
"title": f"{year} Stats",
|
||||||
"month_playtimes": month_playtimes,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request.session["return_path"] = request.path
|
request.session["return_path"] = request.path
|
||||||
return render(request, "stats.html", context)
|
return render(request, "stats.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def delete_purchase(request, purchase_id=None):
|
|
||||||
purchase = get_object_or_404(Purchase, id=purchase_id)
|
|
||||||
purchase.delete()
|
|
||||||
return redirect("list_sessions")
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def add_purchase(request, edition_id=None):
|
def add_purchase(request, edition_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
initial = {"date_purchased": timezone.now()}
|
initial = {"date_purchased": timezone.now()}
|
||||||
@ -636,7 +579,6 @@ def add_purchase(request, edition_id=None):
|
|||||||
return render(request, "add_purchase.html", context)
|
return render(request, "add_purchase.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def add_game(request):
|
def add_game(request):
|
||||||
context = {}
|
context = {}
|
||||||
form = GameForm(request.POST or None)
|
form = GameForm(request.POST or None)
|
||||||
@ -655,7 +597,6 @@ def add_game(request):
|
|||||||
return render(request, "add_game.html", context)
|
return render(request, "add_game.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def add_edition(request, game_id=None):
|
def add_edition(request, game_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@ -690,7 +631,6 @@ def add_edition(request, game_id=None):
|
|||||||
return render(request, "add_edition.html", context)
|
return render(request, "add_edition.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def add_platform(request):
|
def add_platform(request):
|
||||||
context = {}
|
context = {}
|
||||||
form = PlatformForm(request.POST or None)
|
form = PlatformForm(request.POST or None)
|
||||||
@ -703,7 +643,6 @@ def add_platform(request):
|
|||||||
return render(request, "add.html", context)
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def add_device(request):
|
def add_device(request):
|
||||||
context = {}
|
context = {}
|
||||||
form = DeviceForm(request.POST or None)
|
form = DeviceForm(request.POST or None)
|
||||||
@ -716,6 +655,5 @@ def add_device(request):
|
|||||||
return render(request, "add.html", context)
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def index(request):
|
def index(request):
|
||||||
return redirect("list_sessions_recent")
|
return redirect("list_sessions_recent")
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
{
|
{
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.6",
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"concurrently": "^8.2.2",
|
"tailwindcss": "^3.3.3"
|
||||||
"npm-check-updates": "^16.14.20",
|
|
||||||
"tailwindcss": "^3.4.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
654
poetry.lock
generated
654
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -7,28 +7,27 @@ license = "GPL"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
packages = [{include = "timetracker"}]
|
packages = [{include = "timetracker"}]
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.main.dependencies]
|
||||||
black = "^24.4.2"
|
|
||||||
mypy = "^1.10.1"
|
|
||||||
pyyaml = "^6.0.1"
|
|
||||||
pytest = "^8.2.2"
|
|
||||||
django-extensions = "^3.2.3"
|
|
||||||
djhtml = "^3.0.6"
|
|
||||||
djlint = "^1.34.1"
|
|
||||||
isort = "^5.13.2"
|
|
||||||
pre-commit = "^3.7.1"
|
|
||||||
django-debug-toolbar = "^4.4.2"
|
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "^3.11"
|
python = "^3.11"
|
||||||
django = "^5.0.6"
|
django = "^4.2.0"
|
||||||
gunicorn = "^22.0.0"
|
gunicorn = "^20.1.0"
|
||||||
uvicorn = "^0.30.1"
|
uvicorn = "^0.20.0"
|
||||||
graphene-django = "^3.2.2"
|
graphene-django = "^3.1.5"
|
||||||
django-htmx = "^1.18.0"
|
django-htmx = "^1.17.2"
|
||||||
django-template-partials = "^24.2"
|
django-template-partials = "^23.4"
|
||||||
markdown = "^3.6"
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
black = "^22.12.0"
|
||||||
|
mypy = "^0.991"
|
||||||
|
pyyaml = "^6.0"
|
||||||
|
pytest = "^7.2.0"
|
||||||
|
django-extensions = "^3.2.1"
|
||||||
|
werkzeug = "^2.2.2"
|
||||||
|
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]
|
[tool.isort]
|
||||||
|
@ -67,9 +67,6 @@ if DEBUG:
|
|||||||
DEBUG_TOOLBAR_CONFIG = {"ROOT_TAG_EXTRA_ATTRS": "hx-preserve"}
|
DEBUG_TOOLBAR_CONFIG = {"ROOT_TAG_EXTRA_ATTRS": "hx-preserve"}
|
||||||
|
|
||||||
ROOT_URLCONF = "timetracker.urls"
|
ROOT_URLCONF = "timetracker.urls"
|
||||||
LOGIN_URL = "/login/"
|
|
||||||
LOGIN_REDIRECT_URL = "/"
|
|
||||||
LOGOUT_REDIRECT_URL = "/login/"
|
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
|
@ -15,7 +15,6 @@ Including another URLconf
|
|||||||
"""
|
"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth import views as auth_views
|
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
@ -23,10 +22,8 @@ from graphene_django.views import GraphQLView
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", RedirectView.as_view(url="/tracker")),
|
path("", RedirectView.as_view(url="/tracker")),
|
||||||
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))),
|
|
||||||
path("login/", auth_views.LoginView.as_view(), name="login"),
|
|
||||||
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
|
|
||||||
path("tracker/", include("games.urls")),
|
path("tracker/", include("games.urls")),
|
||||||
|
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
Reference in New Issue
Block a user