Compare commits
26 Commits
6def13d2b6
...
0.1.0
Author | SHA1 | Date | |
---|---|---|---|
250f841e00 | |||
89adf479f6 | |||
5f9ca5781f | |||
1a2f0b974d | |||
c6bb60bbbb | |||
126e758172 | |||
6e4db38ee4 | |||
aae05f23e7 | |||
cd35af471a | |||
d896a37779 | |||
eec8f1b9f5
|
|||
b695a35fb1 | |||
76bd923a00 | |||
811e7771cc | |||
7d31f6f416 | |||
c03b9fe8e1 | |||
ec4a095425 | |||
f2076a6cd0 | |||
4d52fd21f8 | |||
ab9d8d1ae3 | |||
9d142126b1 | |||
0f2f0d281e | |||
a115adae28 | |||
63504a5768 | |||
fe7aec1b87 | |||
3cdfb94fe2 |
@ -8,7 +8,11 @@ steps:
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: registry.kucharczyk.xyz/timetracker
|
||||
tags: latest
|
||||
environment:
|
||||
VERSION_NUMBER: $(git describe --tags --abbrev=0)
|
||||
tags:
|
||||
- latest
|
||||
- $VERSION_NUMBER
|
||||
trigger:
|
||||
event:
|
||||
- push
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
__pycache__
|
||||
.venv
|
||||
node_modules
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
db.sqlite3
|
16
Dockerfile
16
Dockerfile
@ -1,17 +1,15 @@
|
||||
FROM python:3.10-slim-bullseye
|
||||
RUN apt update \
|
||||
&& apt install --no-install-recommends --yes \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
RUN python3 -m venv pip $VIRTUAL_ENV
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
RUN pip install --no-cache-dir poetry
|
||||
RUN useradd --create-home --uid 1000 timetracker
|
||||
RUN git clone https://git.kucharczyk.xyz/lukas/timetracker.git /home/timetracker/app
|
||||
WORKDIR /home/timetracker/app
|
||||
RUN chown -R timetracker /home/timetracker/app
|
||||
RUN poetry install
|
||||
EXPOSE 8000
|
||||
COPY . /home/timetracker/app/
|
||||
RUN chown -R timetracker:timetracker /home/timetracker/app
|
||||
RUN poetry install --without dev
|
||||
COPY entrypoint.sh /
|
||||
RUN chmod +x /entrypoint.sh
|
||||
USER timetracker
|
||||
CMD [ "python3", "src/web/manage.py", "runserver", "0.0.0.0:8000" ]
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
37
Makefile
Normal file
37
Makefile
Normal file
@ -0,0 +1,37 @@
|
||||
.PHONY: createsuperuser
|
||||
|
||||
all: css migrate
|
||||
|
||||
initialize: npm css migrate loadplatforms
|
||||
|
||||
HTMLFILES := $(shell find src/web/tracker/templates -type f)
|
||||
|
||||
npm:
|
||||
npm install
|
||||
|
||||
css: src/input.css
|
||||
npx tailwindcss -i ./src/input.css -o ./src/web/tracker/static/base.css
|
||||
|
||||
css-dev: css
|
||||
npx tailwindcss -i ./src/input.css -o ./src/web/tracker/static/base.css --watch
|
||||
|
||||
makemigrations:
|
||||
python src/web/manage.py makemigrations
|
||||
|
||||
migrate: makemigrations
|
||||
python src/web/manage.py migrate
|
||||
|
||||
dev: migrate
|
||||
python src/web/manage.py runserver
|
||||
|
||||
dumptracker:
|
||||
python src/web/manage.py dumpdata --format yaml tracker --output tracker_fixture.yaml
|
||||
|
||||
loadplatforms:
|
||||
python src/web/manage.py loaddata platforms.yaml
|
||||
|
||||
loadsample:
|
||||
python src/web/manage.py loaddata sample.yaml
|
||||
|
||||
createsuperuser:
|
||||
python src/web/manage.py createsuperuser
|
8
entrypoint.sh
Normal file
8
entrypoint.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# Apply database migrations
|
||||
echo "Apply database migrations"
|
||||
python src/web/manage.py migrate
|
||||
|
||||
# Start server
|
||||
echo "Starting server"
|
||||
python src/web/manage.py runserver 0.0.0.0:8000
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "timelogger"
|
||||
version = "0.1.0"
|
||||
name = "timetracker"
|
||||
version = "0.0.0"
|
||||
description = "A simple time tracker."
|
||||
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"]
|
||||
license = "GPL"
|
||||
|
@ -1,3 +1,15 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind utilities;
|
||||
|
||||
form label {
|
||||
@apply dark:text-slate-400;
|
||||
}
|
||||
|
||||
form input,select,textarea {
|
||||
@apply dark:bg-slate-500 dark:border dark:border-slate-900 dark:text-slate-100;
|
||||
}
|
||||
|
||||
form input[type=submit] {
|
||||
@apply p-2 bg-purple-900;
|
||||
}
|
Binary file not shown.
@ -1,3 +1,8 @@
|
||||
from django.contrib import admin
|
||||
from .models import Game, Purchase, Platform, Session
|
||||
|
||||
# Register your models here.
|
||||
admin.site.register(Game)
|
||||
admin.site.register(Purchase)
|
||||
admin.site.register(Platform)
|
||||
admin.site.register(Session)
|
||||
|
98
src/web/tracker/fixtures/sample.yaml
Normal file
98
src/web/tracker/fixtures/sample.yaml
Normal file
@ -0,0 +1,98 @@
|
||||
- model: tracker.game
|
||||
pk: 1
|
||||
fields:
|
||||
name: Nioh 2
|
||||
wikidata: Q67482292
|
||||
- model: tracker.game
|
||||
pk: 2
|
||||
fields:
|
||||
name: Elden Ring
|
||||
wikidata: Q64826862
|
||||
- model: tracker.game
|
||||
pk: 3
|
||||
fields:
|
||||
name: Cyberpunk 2077
|
||||
wikidata: Q3182559
|
||||
- model: tracker.purchase
|
||||
pk: 1
|
||||
fields:
|
||||
game: 1
|
||||
platform: 1
|
||||
date_purchased: 2021-02-13
|
||||
date_refunded: null
|
||||
- model: tracker.purchase
|
||||
pk: 2
|
||||
fields:
|
||||
game: 2
|
||||
platform: 1
|
||||
date_purchased: 2022-02-24
|
||||
date_refunded: null
|
||||
- model: tracker.purchase
|
||||
pk: 3
|
||||
fields:
|
||||
game: 3
|
||||
platform: 1
|
||||
date_purchased: 2020-12-07
|
||||
date_refunded: null
|
||||
- model: tracker.platform
|
||||
pk: 1
|
||||
fields:
|
||||
name: Steam
|
||||
group: PC
|
||||
- model: tracker.platform
|
||||
pk: 3
|
||||
fields:
|
||||
name: Xbox Gamepass
|
||||
group: PC
|
||||
- model: tracker.platform
|
||||
pk: 4
|
||||
fields:
|
||||
name: Epic Games Store
|
||||
group: PC
|
||||
- model: tracker.platform
|
||||
pk: 5
|
||||
fields:
|
||||
name: Playstation 5
|
||||
group: Playstation
|
||||
- model: tracker.platform
|
||||
pk: 6
|
||||
fields:
|
||||
name: Playstation 4
|
||||
group: Playstation
|
||||
- model: tracker.platform
|
||||
pk: 7
|
||||
fields:
|
||||
name: Nintendo Switch
|
||||
group: Nintendo
|
||||
- model: tracker.platform
|
||||
pk: 8
|
||||
fields:
|
||||
name: Nintendo 3DS
|
||||
group: Nintendo
|
||||
- model: tracker.session
|
||||
pk: 1
|
||||
fields:
|
||||
purchase: 2
|
||||
timestamp_start: 2022-12-31 14:25:58+00:00
|
||||
timestamp_end: 2022-12-31 16:25:22+00:00
|
||||
duration_manual: null
|
||||
duration_calculated: null
|
||||
note: ''
|
||||
- model: tracker.session
|
||||
pk: 3
|
||||
fields:
|
||||
purchase: 3
|
||||
timestamp_start: 2023-01-01 22:00:23+00:00
|
||||
timestamp_end: 2023-01-01 23:28:23+00:00
|
||||
duration_manual: null
|
||||
duration_calculated: null
|
||||
note: ''
|
||||
- model: tracker.session
|
||||
pk: 4
|
||||
fields:
|
||||
purchase: 3
|
||||
timestamp_start: 2020-01-01 23:29:17+00:00
|
||||
timestamp_end: 2020-01-01 23:29:17+00:00
|
||||
duration_manual: '12:00:00'
|
||||
duration_calculated: null
|
||||
note: ''
|
@ -1,6 +1,7 @@
|
||||
# Generated by Django 4.1.4 on 2022-12-30 22:02
|
||||
# Generated by Django 4.1.4 on 2023-01-02 18:27
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -11,7 +12,68 @@ class Migration(migrations.Migration):
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="TrackerModel",
|
||||
name="Game",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("wikidata", models.CharField(max_length=50)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Platform",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("group", models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Purchase",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("date_purchased", models.DateField()),
|
||||
("date_refunded", models.DateField(blank=True, null=True)),
|
||||
(
|
||||
"game",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="tracker.game"
|
||||
),
|
||||
),
|
||||
(
|
||||
"platform",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="tracker.platform",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Session",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
@ -22,10 +84,18 @@ class Migration(migrations.Migration):
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=200)),
|
||||
("timestamp_start", models.DateTimeField()),
|
||||
("timestamp_end", models.DateTimeField()),
|
||||
("note", models.TextField()),
|
||||
("duration_manual", models.DurationField(blank=True, null=True)),
|
||||
("duration_calculated", models.DurationField(blank=True, null=True)),
|
||||
("note", models.TextField(blank=True, null=True)),
|
||||
(
|
||||
"purchase",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="tracker.purchase",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.1.4 on 2023-01-02 18:55
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("tracker", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="duration_manual",
|
||||
field=models.DurationField(
|
||||
blank=True, default=datetime.timedelta(0), null=True
|
||||
),
|
||||
),
|
||||
]
|
@ -1,104 +0,0 @@
|
||||
# Generated by Django 4.1.4 on 2022-12-31 12:35
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("tracker", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Game",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("wikidata", models.CharField(max_length=50)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Platform",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("group", models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Purchase",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("date_purchased", models.DateTimeField()),
|
||||
("date_refunded", models.DateTimeField()),
|
||||
(
|
||||
"game",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="tracker.game"
|
||||
),
|
||||
),
|
||||
(
|
||||
"platform",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="tracker.platform",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Session",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("timestamp_start", models.DateTimeField()),
|
||||
("timestamp_end", models.DateTimeField()),
|
||||
("duration_manual", models.DurationField()),
|
||||
("duration_calculated", models.DurationField()),
|
||||
("note", models.TextField()),
|
||||
(
|
||||
"purchase",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="tracker.purchase",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="TrackerModel",
|
||||
),
|
||||
]
|
@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.1.4 on 2022-12-31 13:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("tracker", "0002_game_platform_purchase_session_delete_trackermodel"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="date_purchased",
|
||||
field=models.DateField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="date_refunded",
|
||||
field=models.DateField(),
|
||||
),
|
||||
]
|
@ -1,4 +1,4 @@
|
||||
# Generated by Django 4.1.4 on 2022-12-31 13:26
|
||||
# Generated by Django 4.1.4 on 2023-01-02 23:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
@ -6,15 +6,10 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("tracker", "0005_alter_purchase_date_refunded"),
|
||||
("tracker", "0002_alter_session_duration_manual"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="duration_calculated",
|
||||
field=models.DurationField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="duration_manual",
|
||||
@ -22,7 +17,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="note",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
name="timestamp_end",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.4 on 2022-12-31 13:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("tracker", "0003_alter_purchase_date_purchased_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="date_refunded",
|
||||
field=models.DateField(blank=True),
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.4 on 2022-12-31 13:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("tracker", "0004_alter_purchase_date_refunded"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="date_refunded",
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
]
|
@ -1,4 +1,5 @@
|
||||
from django.db import models
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class Game(models.Model):
|
||||
@ -30,13 +31,24 @@ class Platform(models.Model):
|
||||
class Session(models.Model):
|
||||
purchase = models.ForeignKey("Purchase", on_delete=models.CASCADE)
|
||||
timestamp_start = models.DateTimeField()
|
||||
timestamp_end = models.DateTimeField()
|
||||
timestamp_end = models.DateTimeField(blank=True, null=True)
|
||||
duration_manual = models.DurationField(blank=True, null=True)
|
||||
duration_calculated = models.DurationField(blank=True, null=True)
|
||||
note = models.TextField(blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.purchase
|
||||
mark = ", manual" if self.duration_manual != None else ""
|
||||
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.total_duration()}{mark})"
|
||||
|
||||
def calculated_duration(self):
|
||||
return self.timestamp_end - self.timestamp_start
|
||||
if self.timestamp_end == None or self.timestamp_start == None:
|
||||
return 0
|
||||
else:
|
||||
return self.timestamp_end - self.timestamp_start
|
||||
|
||||
def total_duration(self):
|
||||
return (
|
||||
self.calculated_duration()
|
||||
if self.duration_manual == None
|
||||
else self.duration_manual + self.calculated_duration()
|
||||
)
|
||||
|
@ -717,6 +717,26 @@ select {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.left-0 {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.bottom-0 {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.left-1 {
|
||||
left: 0.25rem;
|
||||
}
|
||||
|
||||
.bottom-1 {
|
||||
bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@ -726,10 +746,6 @@ select {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mr-3 {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
@ -746,8 +762,8 @@ select {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.h-6 {
|
||||
height: 1.5rem;
|
||||
.h-screen {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
@ -798,38 +814,20 @@ select {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(229 231 235 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-gray-100 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(243 244 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
@ -851,6 +849,15 @@ select {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
@ -861,81 +868,129 @@ select {
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-slate-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(30 41 59 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-slate-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(51 65 85 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-slate-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(71 85 105 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-slate-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(100 116 139 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-slate-400 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(148 163 184 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-slate-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(203 213 225 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.shadow {
|
||||
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.dark form label {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(148 163 184 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark form input,.dark select,.dark textarea {
|
||||
border-width: 1px;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(15 23 42 / var(--tw-border-opacity));
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(100 116 139 / var(--tw-bg-opacity));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(241 245 249 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
form input[type=submit] {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(88 28 135 / var(--tw-bg-opacity));
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.hover\:underline:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:border-gray-700 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(55 65 81 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-white {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(255 255 255 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-slate-800 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 41 59 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-900 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-800 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-slate-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(51 65 85 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-gray-900 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-slate-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(203 213 225 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-slate-400 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(148 163 184 / var(--tw-text-opacity));
|
||||
}
|
||||
.dark .dark\:border-white {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(255 255 255 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:h-9 {
|
||||
height: 2.25rem;
|
||||
}
|
||||
.dark .dark\:bg-gray-800 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:bg-gray-900 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:bg-slate-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(51 65 85 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:bg-slate-400 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(148 163 184 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:text-slate-400 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(148 163 184 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:text-slate-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(203 213 225 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark .dark\:text-slate-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(71 85 105 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
@ -6,30 +6,34 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}No Title{% endblock title %}</title>
|
||||
<title>Timetracker - {% block title %}Untitled{% endblock title %}</title>
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||
<link rel="stylesheet" href="{% static 'base.css' %}" />
|
||||
</head>
|
||||
|
||||
<body class="bg-white dark:bg-slate-800 dark:text-white">
|
||||
<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">
|
||||
<a href="#" class="flex items-center">
|
||||
<span class="text-4xl">⌚</span>
|
||||
<span class="self-center text-xl font-semibold whitespace-nowrap">Timetracker</span>
|
||||
</a>
|
||||
<div class="w-full md:block md:w-auto">
|
||||
<ul
|
||||
class="flex flex-col md:flex-row p-4 mt-4">
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_game' %}">New Game</a></li>
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_purchase' %}">New Purchase</a></li>
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_session' %}">New Session</a></li>
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'list_sessions' %}">All Sessions</a></li>
|
||||
</ul>
|
||||
<body class="dark">
|
||||
<div class="dark:bg-gray-800 h-screen">
|
||||
<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">
|
||||
<a href="#" class="flex items-center">
|
||||
<span class="text-4xl">⌚</span>
|
||||
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
|
||||
</a>
|
||||
<div class="w-full md:block md:w-auto">
|
||||
<ul
|
||||
class="flex flex-col md:flex-row p-4 mt-4 dark:text-white">
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_game' %}">New Game</a></li>
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_purchase' %}">New Purchase</a></li>
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_session' %}">New Session</a></li>
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'list_sessions' %}">All Sessions</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% block content %}No content here.{% endblock %}
|
||||
</nav>
|
||||
{% block content %}No content here.{% endblock %}
|
||||
</div>
|
||||
{% load version %}
|
||||
<span id="version-info" class="absolute left-1 bottom-1 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,17 +1,23 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Tracker Entry List{% endblock title %}
|
||||
{% block title %}Sessions{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
{% if purchase %}
|
||||
<div class="text-center text-xl mb-4 dark:text-slate-400">
|
||||
<h1>Listing sessions only for purchase "{{ purchase }}"</h1>
|
||||
<a class="dark:text-white hover:underline" href="{% url 'list_sessions' %}">View all sessions</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="grid grid-cols-4 gap-4 shadow rounded-xl max-w-screen-lg mx-auto dark:bg-slate-700 p-2 justify-center">
|
||||
<div class="dark:border-white dark:text-slate-300 text-lg">Name</div>
|
||||
<div class="dark:border-white dark:text-slate-300 text-lg">Start</div>
|
||||
<div class="dark:border-white dark:text-slate-300 text-lg">End</div>
|
||||
<div class="dark:border-white dark:text-slate-300 text-lg">Duration</div>
|
||||
{% for data in dataset %}
|
||||
<div class="dark:text-slate-400">{{ data.purchase }}</div>
|
||||
<div class="dark:text-slate-400">{{ data.timestamp_start }}</div>
|
||||
<div class="dark:text-slate-400">{{ data.timestamp_end }}</div>
|
||||
<div class=""><a class="dark:text-white hover:underline" href="{% url 'list_sessions' data.purchase.id %}">{{ data.purchase }}</a></div>
|
||||
<div class="dark:text-slate-400">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div>
|
||||
<div class="dark:text-slate-400">{{ data.timestamp_end | date:"d/m/Y H:i" }}</div>
|
||||
<div class="dark:text-slate-400">{{ data.time_delta }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
18
src/web/tracker/templatetags/version.py
Normal file
18
src/web/tracker/templatetags/version.py
Normal file
@ -0,0 +1,18 @@
|
||||
from django import template
|
||||
import time
|
||||
import os
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def version_date():
|
||||
return time.strftime(
|
||||
"%d-%b-%Y %H:%m",
|
||||
time.gmtime(os.path.getmtime(os.path.abspath(os.path.join(".git")))),
|
||||
)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def version():
|
||||
return os.environ.get("VERSION_NUMBER", "UNKNOWN VERSION")
|
@ -7,4 +7,9 @@ urlpatterns = [
|
||||
path("add-session/", views.add_session, name="add_session"),
|
||||
path("add-purchase/", views.add_purchase, name="add_purchase"),
|
||||
path("list-sessions/", views.list_sessions, name="list_sessions"),
|
||||
path(
|
||||
"list-sessions/by-purchase/<int:purchase_id>",
|
||||
views.list_sessions,
|
||||
name="list_sessions",
|
||||
),
|
||||
]
|
||||
|
@ -4,6 +4,7 @@ from .models import Game, Platform, Purchase, Session
|
||||
from .forms import SessionForm, PurchaseForm, GameForm
|
||||
from datetime import datetime
|
||||
from django.db.models import ExpressionWrapper, F, DurationField
|
||||
import logging
|
||||
|
||||
|
||||
def add_session(request):
|
||||
@ -18,9 +19,16 @@ def add_session(request):
|
||||
return render(request, "add_session.html", context)
|
||||
|
||||
|
||||
def list_sessions(request):
|
||||
def list_sessions(request, purchase_id=None):
|
||||
context = {}
|
||||
dataset = Session.objects.annotate(
|
||||
|
||||
if purchase_id != None:
|
||||
dataset = Session.objects.filter(purchase=purchase_id)
|
||||
context["purchase"] = Purchase.objects.get(id=purchase_id)
|
||||
else:
|
||||
dataset = Session.objects.all()
|
||||
|
||||
dataset = dataset.annotate(
|
||||
time_delta=ExpressionWrapper(
|
||||
F("timestamp_end") - F("timestamp_start"), output_field=DurationField()
|
||||
)
|
||||
|
@ -11,6 +11,8 @@ https://docs.djangoproject.com/en/4.1/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
@ -106,7 +108,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
TIME_ZONE = os.environ.get("TZ", "UTC")
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@ -122,3 +124,13 @@ STATIC_URL = "static/"
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# https://docs.djangoproject.com/en/4.1/topics/logging/
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {"console": {"class": "logging.StreamHandler"}},
|
||||
"root": {"handlers": ["console"], "level": "WARNING"},
|
||||
}
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = ["https://tracker.kucharczyk.xyz"]
|
||||
|
@ -19,6 +19,7 @@ from django.views.generic import RedirectView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("", RedirectView.as_view(url="/tracker/list-sessions")),
|
||||
path("tracker/", include("tracker.urls")),
|
||||
]
|
||||
|
Reference in New Issue
Block a user