Compare commits
15 Commits
1.0.3
...
345315db38
Author | SHA1 | Date | |
---|---|---|---|
345315db38
|
|||
d9076e4367
|
|||
65e556656f
|
|||
22fd1c356a
|
|||
9080425e1e
|
|||
c64c68791f
|
|||
f6edc8688a
|
|||
1c73bba301
|
|||
da5783c08f
|
|||
04e6b01c2a
|
|||
3a14ea98a6
|
|||
ba27511b97
|
|||
ae95015f55
|
|||
c217fd30ef
|
|||
ec16d40361
|
@ -25,9 +25,7 @@ steps:
|
|||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
settings:
|
settings:
|
||||||
repo: registry.kucharczyk.xyz/timetracker
|
repo: registry.kucharczyk.xyz/timetracker
|
||||||
tags:
|
auto_tag: true
|
||||||
- ${DRONE_COMMIT_REF}
|
|
||||||
- ${DRONE_COMMIT_BRANCH}
|
|
||||||
when:
|
when:
|
||||||
branch:
|
branch:
|
||||||
exclude:
|
exclude:
|
||||||
|
3
.gitignore
vendored
@ -5,6 +5,5 @@ __pycache__
|
|||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
static/admin/
|
static
|
||||||
static/django_extensions/
|
|
||||||
dist/
|
dist/
|
20
CHANGELOG.md
@ -1,22 +1,6 @@
|
|||||||
## 1.0.3 / 2023-02-20 17:16+01:00
|
## Unrelease
|
||||||
|
|
||||||
* Add wikidata ID and year for editions
|
* Add React
|
||||||
* Add icons for game, edition, purchase filters
|
|
||||||
* Allow filtering by game, edition, purchase from the session list
|
|
||||||
* Allow editing filtered entities from session list
|
|
||||||
|
|
||||||
## 1.0.2 / 2023-02-18 21:48+01:00
|
|
||||||
|
|
||||||
* Add support for device info (https://git.kucharczyk.xyz/lukas/timetracker/issues/49)
|
|
||||||
* Add support for purchase ownership information (https://git.kucharczyk.xyz/lukas/timetracker/issues/48)
|
|
||||||
* Add support for purchase prices
|
|
||||||
* Add support for game editions (https://git.kucharczyk.xyz/lukas/timetracker/issues/28)
|
|
||||||
|
|
||||||
## 1.0.1 / 2023-01-30 22:17+01:00
|
|
||||||
|
|
||||||
* Make it possible to edit sessions (https://git.kucharczyk.xyz/lukas/timetracker/issues/46)
|
|
||||||
* Show markers on smaller graphs to make it clearer which dates the session belong to
|
|
||||||
* Show only last 30 days on the homepage (https://git.kucharczyk.xyz/lukas/timetracker/issues/47)
|
|
||||||
|
|
||||||
## 1.0.0 / 2023-01-20 19:54+01:00
|
## 1.0.0 / 2023-01-20 19:54+01:00
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ RUN npm install && \
|
|||||||
|
|
||||||
FROM python:3.10.9-slim-bullseye
|
FROM python:3.10.9-slim-bullseye
|
||||||
|
|
||||||
ENV VERSION_NUMBER 1.0.3
|
ENV VERSION_NUMBER 1.0.0
|
||||||
ENV PROD 1
|
ENV PROD 1
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
10
Makefile
@ -1,18 +1,12 @@
|
|||||||
all: css migrate
|
all: migrate
|
||||||
|
|
||||||
initialize: npm css migrate sethookdir loadplatforms
|
initialize: npm migrate sethookdir loadplatforms
|
||||||
|
|
||||||
HTMLFILES := $(shell find games/templates -type f)
|
HTMLFILES := $(shell find games/templates -type f)
|
||||||
|
|
||||||
npm:
|
npm:
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
css: common/input.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
|
||||||
|
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
form label {
|
|
||||||
@apply dark:text-slate-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
form input,
|
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
#session-table {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 3fr 2fr repeat(2, 1fr) 0.5fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase-name > span:nth-child(2) {
|
|
||||||
@apply ml-4
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase-name > span:nth-child(2) > a > img {
|
|
||||||
@apply opacity-0 transition-opacity duration-500
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase-name:hover > span:nth-child(2) > a > img {
|
|
||||||
@apply opacity-50
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase-name > span:nth-child(2) > a > img:hover {
|
|
||||||
@apply opacity-100
|
|
||||||
}
|
|
||||||
|
|
||||||
#button-container button {
|
|
||||||
@apply mx-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
@apply text-left;
|
|
||||||
}
|
|
||||||
|
|
||||||
th label {
|
|
||||||
@apply mr-4;
|
|
||||||
}
|
|
@ -64,7 +64,7 @@ def get_chart(data, title="", xlabel="", ylabel=""):
|
|||||||
plt.switch_backend("SVG")
|
plt.switch_backend("SVG")
|
||||||
fig, ax = plt.subplots()
|
fig, ax = plt.subplots()
|
||||||
fig.set_size_inches(10, 4)
|
fig.set_size_inches(10, 4)
|
||||||
lines = ax.plot(x, y, "-o")
|
ax.plot(x, y)
|
||||||
first = x[0]
|
first = x[0]
|
||||||
last = x[-1]
|
last = x[-1]
|
||||||
difference = last - first
|
difference = last - first
|
||||||
@ -76,11 +76,7 @@ def get_chart(data, title="", xlabel="", ylabel=""):
|
|||||||
elif difference.days < 720:
|
elif difference.days < 720:
|
||||||
ax.xaxis.set_major_locator(mdates.MonthLocator())
|
ax.xaxis.set_major_locator(mdates.MonthLocator())
|
||||||
ax.xaxis.set_minor_locator(mdates.WeekdayLocator())
|
ax.xaxis.set_minor_locator(mdates.WeekdayLocator())
|
||||||
for line in lines:
|
|
||||||
line.set_marker("")
|
|
||||||
else:
|
else:
|
||||||
for line in lines:
|
|
||||||
line.set_marker("")
|
|
||||||
ax.xaxis.set_major_locator(mdates.YearLocator())
|
ax.xaxis.set_major_locator(mdates.YearLocator())
|
||||||
ax.xaxis.set_minor_locator(mdates.MonthLocator())
|
ax.xaxis.set_minor_locator(mdates.MonthLocator())
|
||||||
|
|
||||||
|
17
frontend/.eslintrc.cjs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true
|
||||||
|
},
|
||||||
|
extends: ["eslint/recommended", "plugin:react/recommended", "plugin:prettier/recommended"],
|
||||||
|
overrides: [],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module"
|
||||||
|
},
|
||||||
|
plugins: ["react"],
|
||||||
|
rules: {},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: { jsx: true }
|
||||||
|
}
|
||||||
|
};
|
24
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
9
frontend/.prettierrc
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"singleAttributePerLine": true
|
||||||
|
}
|
17
frontend/index.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="description" content="Self-hosted time-tracker."/>
|
||||||
|
<meta name="keywords" content="time, tracking, video games, self-hosted"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<!-- TODO: replace with own icon -->
|
||||||
|
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||||
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"/>
|
||||||
|
<title>Timetracker</title>
|
||||||
|
</head>
|
||||||
|
<body class="dark">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
33
frontend/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"autoprefixer": "^10.4.13",
|
||||||
|
"eslint-config-prettier": "^8.6.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"postcss": "^8.4.21",
|
||||||
|
"prettier": "^2.8.3",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.7.0",
|
||||||
|
"tailwindcss": "^3.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.26",
|
||||||
|
"@types/react-dom": "^18.0.9",
|
||||||
|
"@vitejs/plugin-react": "^3.0.0",
|
||||||
|
"eslint": "^8.32.0",
|
||||||
|
"eslint-plugin-import": "^2.27.5",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
|
"eslint-plugin-react": "^7.32.1",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"vite": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
6
frontend/postcss.config.cjs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
0
frontend/src/App.css
Normal file
20
frontend/src/App.jsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import "./App.css";
|
||||||
|
import Nav from "./components/Nav";
|
||||||
|
import { Outlet, useLoaderData } from "react-router-dom";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="dark:bg-gray-800 min-h-screen">
|
||||||
|
<Nav />
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
{/* {% load version %} */}
|
||||||
|
{/* <span className="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span> */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
71
frontend/src/components/Nav.jsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
function Nav() {
|
||||||
|
return (
|
||||||
|
<nav className="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
|
||||||
|
<div className="container flex flex-wrap items-center justify-between mx-auto">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
<span className="text-4xl">⌚</span>
|
||||||
|
<span className="self-center text-xl font-semibold whitespace-nowrap text-white">
|
||||||
|
Timetracker
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<div className="w-full md:block md:w-auto">
|
||||||
|
<ul className="flex flex-col md:flex-row p-4 mt-4 dark:text-white">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="block py-2 pl-3 pr-4 hover:underline"
|
||||||
|
href="{% url 'add_game' %}"
|
||||||
|
>
|
||||||
|
New Game
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="block py-2 pl-3 pr-4 hover:underline"
|
||||||
|
href="{% url 'add_platform' %}"
|
||||||
|
>
|
||||||
|
New Platform
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/* {% if game_available and platform_available %} */}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="block py-2 pl-3 pr-4 hover:underline"
|
||||||
|
href="{% url 'add_purchase' %}"
|
||||||
|
>
|
||||||
|
New Purchase
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/* {% endif %} */}
|
||||||
|
{/* {% if purchase_available %} */}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="block py-2 pl-3 pr-4 hover:underline"
|
||||||
|
href="{% url 'add_session' %}"
|
||||||
|
>
|
||||||
|
New Session
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/* {% endif %} */}
|
||||||
|
{/* {% if session_count > 0 %} */}
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
className="block py-2 pl-3 pr-4 hover:underline"
|
||||||
|
to="/sessions"
|
||||||
|
>
|
||||||
|
All Sessions
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
{/* {% endif %} */}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Nav;
|
162
frontend/src/components/SessionList.jsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
export default function SessionList() {
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8000/api/sessions/25/",
|
||||||
|
"timestamp_start": "2020-01-01T00:00:00+01:00",
|
||||||
|
"timestamp_end": null,
|
||||||
|
"duration_manual": "12:00:00",
|
||||||
|
"duration_calculated": "00:00:00",
|
||||||
|
"note": "",
|
||||||
|
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8000/api/sessions/26/",
|
||||||
|
"timestamp_start": "2022-12-31T15:25:00+01:00",
|
||||||
|
"timestamp_end": "2022-12-31T17:25:00+01:00",
|
||||||
|
"duration_manual": "00:00:00",
|
||||||
|
"duration_calculated": "02:00:00",
|
||||||
|
"note": "",
|
||||||
|
"purchase": "http://localhost:8000/api/purchases/2/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8000/api/sessions/27/",
|
||||||
|
"timestamp_start": "2023-01-01T23:00:00+01:00",
|
||||||
|
"timestamp_end": "2023-01-02T00:28:00+01:00",
|
||||||
|
"duration_manual": "00:00:00",
|
||||||
|
"duration_calculated": "01:28:00",
|
||||||
|
"note": "",
|
||||||
|
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8000/api/sessions/28/",
|
||||||
|
"timestamp_start": "2023-01-02T22:08:00+01:00",
|
||||||
|
"timestamp_end": "2023-01-03T01:08:00+01:00",
|
||||||
|
"duration_manual": "00:00:00",
|
||||||
|
"duration_calculated": "03:00:00",
|
||||||
|
"note": "",
|
||||||
|
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8000/api/sessions/29/",
|
||||||
|
"timestamp_start": "2023-01-03T22:36:00+01:00",
|
||||||
|
"timestamp_end": "2023-01-04T00:12:00+01:00",
|
||||||
|
"duration_manual": "00:00:00",
|
||||||
|
"duration_calculated": "01:36:00",
|
||||||
|
"note": "",
|
||||||
|
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8000/api/sessions/30/",
|
||||||
|
"timestamp_start": "2023-01-04T20:35:00+01:00",
|
||||||
|
"timestamp_end": "2023-01-04T22:36:00+01:00",
|
||||||
|
"duration_manual": "00:00:00",
|
||||||
|
"duration_calculated": "02:01:00",
|
||||||
|
"note": "",
|
||||||
|
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8000/api/sessions/31/",
|
||||||
|
"timestamp_start": "2023-01-06T18:48:00+01:00",
|
||||||
|
"timestamp_end": "2023-01-06T23:39:00+01:00",
|
||||||
|
"duration_manual": "00:00:00",
|
||||||
|
"duration_calculated": "04:51:00",
|
||||||
|
"note": "",
|
||||||
|
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8000/api/sessions/32/",
|
||||||
|
"timestamp_start": "2023-01-07T23:49:00+01:00",
|
||||||
|
"timestamp_end": "2023-01-08T01:43:00+01:00",
|
||||||
|
"duration_manual": "00:00:00",
|
||||||
|
"duration_calculated": "01:54:00",
|
||||||
|
"note": "",
|
||||||
|
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8000/api/sessions/33/",
|
||||||
|
"timestamp_start": "2023-01-08T16:21:00+01:00",
|
||||||
|
"timestamp_end": "2023-01-08T18:27:00+01:00",
|
||||||
|
"duration_manual": "00:00:00",
|
||||||
|
"duration_calculated": "02:06:00",
|
||||||
|
"note": "",
|
||||||
|
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8000/api/sessions/34/",
|
||||||
|
"timestamp_start": "2023-01-08T19:04:00+01:00",
|
||||||
|
"timestamp_end": "2023-01-08T22:03:00+01:00",
|
||||||
|
"duration_manual": "00:00:00",
|
||||||
|
"duration_calculated": "02:59:00",
|
||||||
|
"note": "",
|
||||||
|
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8000/api/sessions/35/",
|
||||||
|
"timestamp_start": "2023-01-09T19:35:48+01:00",
|
||||||
|
"timestamp_end": "2023-01-09T22:13:20.519058+01:00",
|
||||||
|
"duration_manual": "00:00:00",
|
||||||
|
"duration_calculated": "02:37:32.519058",
|
||||||
|
"note": "",
|
||||||
|
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8000/api/sessions/36/",
|
||||||
|
"timestamp_start": "2023-01-10T15:50:12+01:00",
|
||||||
|
"timestamp_end": "2023-01-10T17:03:45.424429+01:00",
|
||||||
|
"duration_manual": "00:00:00",
|
||||||
|
"duration_calculated": "01:13:33.424429",
|
||||||
|
"note": "",
|
||||||
|
"purchase": "http://localhost:8000/api/purchases/4/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const header = ["url", "timestamp_start", "timestamp_end", "duration_manual", "duration_calculated", "note", "purchase"]
|
||||||
|
// const header = ["Name", "Platform", "Start", "End", "Duration", "Manage"]
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div id="session-table" className="gap-4 shadow rounded-xl max-w-screen-lg mx-auto dark:bg-slate-700 p-2 justify-center">
|
||||||
|
{header.map(column => {
|
||||||
|
<div className="dark:border-white dark:text-slate-300 text-lg">{column}</div>
|
||||||
|
})}
|
||||||
|
{data.map(session => {
|
||||||
|
<>
|
||||||
|
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
<a className="hover:underline" href="">
|
||||||
|
{ session.url }
|
||||||
|
</a>
|
||||||
|
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
<a className="hover:underline" href="">
|
||||||
|
{ session.timestamp_start }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
<a className="hover:underline" href="">
|
||||||
|
{ session.timestamp_end }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
<a className="hover:underline" href="">
|
||||||
|
{ session.duration_manual }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
<a className="hover:underline" href="">
|
||||||
|
{ session.duration_calculated }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
<a className="hover:underline" href="">
|
||||||
|
{ session.note }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
<a className="hover:underline" href="">
|
||||||
|
{ session.purchase }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
16
frontend/src/error-page.jsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { useRouteError } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function ErrorPage() {
|
||||||
|
const error = useRouteError()
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container text-center">
|
||||||
|
<h1 className="text-3xl">Oops!</h1>
|
||||||
|
<p>Sorry, an unexpected error has occurred.</p>
|
||||||
|
<p>
|
||||||
|
<i>{error.statusText || error.message}</i>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
22
frontend/src/index.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
form label {
|
||||||
|
@apply dark:text-slate-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
#session-table {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3fr 1fr repeat(2, 2fr) 0.5fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button-container button {
|
||||||
|
@apply mx-1;
|
||||||
|
}
|
34
frontend/src/main.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import './index.css'
|
||||||
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
||||||
|
// import { loader as sessionLoader } from './routes/sessions'
|
||||||
|
import ErrorPage from "./error-page"
|
||||||
|
import SessionList from './components/SessionList'
|
||||||
|
// import Session from './routes/sessions'
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <App />,
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
// loader: sessionLoader,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "sessions/",
|
||||||
|
element: <SessionList />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// path: "sessions",
|
||||||
|
// element: <SessionList />
|
||||||
|
// }
|
||||||
|
])
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
17
frontend/src/services/ApiService.jsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export async function api(url) {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (response.ok) {
|
||||||
|
const jsonValue = await response.json();
|
||||||
|
return Promise.resolve(jsonValue);
|
||||||
|
} else {
|
||||||
|
return Promise.reject('Response was not OK.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSession(sessionId) {
|
||||||
|
return await api(`/api/sessions/${sessionId}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSessionList() {
|
||||||
|
return await api(`/api/sessions/`);
|
||||||
|
}
|
12
frontend/tailwind.config.cjs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: "class",
|
||||||
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
theme: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["Inter", "sans-serif"],
|
||||||
|
},
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
|
||||||
|
};
|
15
frontend/vite.config.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:8000',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -1,12 +1,10 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from games.models import Game, Platform, Purchase, Session, Edition, Device
|
from games.models import Game, Platform, Purchase, Session
|
||||||
|
|
||||||
|
|
||||||
class SessionForm(forms.ModelForm):
|
class SessionForm(forms.ModelForm):
|
||||||
purchase = forms.ModelChoiceField(
|
purchase = forms.ModelChoiceField(queryset=Purchase.objects.order_by("game__name"))
|
||||||
queryset=Purchase.objects.order_by("edition__name")
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Session
|
model = Session
|
||||||
@ -15,39 +13,17 @@ class SessionForm(forms.ModelForm):
|
|||||||
"timestamp_start",
|
"timestamp_start",
|
||||||
"timestamp_end",
|
"timestamp_end",
|
||||||
"duration_manual",
|
"duration_manual",
|
||||||
"device",
|
|
||||||
"note",
|
"note",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class EditionChoiceField(forms.ModelChoiceField):
|
|
||||||
def label_from_instance(self, obj) -> str:
|
|
||||||
return f"{obj.name} ({obj.platform}, {obj.year_released})"
|
|
||||||
|
|
||||||
|
|
||||||
class PurchaseForm(forms.ModelForm):
|
class PurchaseForm(forms.ModelForm):
|
||||||
edition = EditionChoiceField(queryset=Edition.objects.order_by("name"))
|
game = forms.ModelChoiceField(queryset=Game.objects.order_by("name"))
|
||||||
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Purchase
|
model = Purchase
|
||||||
fields = [
|
fields = ["game", "platform", "date_purchased", "date_refunded"]
|
||||||
"edition",
|
|
||||||
"platform",
|
|
||||||
"date_purchased",
|
|
||||||
"date_refunded",
|
|
||||||
"price",
|
|
||||||
"price_currency",
|
|
||||||
"ownership_type",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class EditionForm(forms.ModelForm):
|
|
||||||
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Edition
|
|
||||||
fields = ["game", "name", "platform", "year_released", "wikidata"]
|
|
||||||
|
|
||||||
|
|
||||||
class GameForm(forms.ModelForm):
|
class GameForm(forms.ModelForm):
|
||||||
@ -60,9 +36,3 @@ class PlatformForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = ["name", "group"]
|
fields = ["name", "group"]
|
||||||
|
|
||||||
|
|
||||||
class DeviceForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = Device
|
|
||||||
fields = ["name", "type"]
|
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
# Generated by Django 4.1.5 on 2023-02-18 16:29
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("games", "0007_alter_purchase_game_alter_purchase_platform_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Edition",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.CharField(max_length=255)),
|
|
||||||
(
|
|
||||||
"game",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE, to="games.game"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"platform",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE, to="games.platform"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,34 +0,0 @@
|
|||||||
# Generated by Django 4.1.5 on 2023-02-18 18:51
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def create_edition_of_game(apps, schema_editor):
|
|
||||||
Game = apps.get_model("games", "Game")
|
|
||||||
Edition = apps.get_model("games", "Edition")
|
|
||||||
Platform = apps.get_model("games", "Platform")
|
|
||||||
first_platform = Platform.objects.first()
|
|
||||||
all_games = Game.objects.all()
|
|
||||||
all_editions = Edition.objects.all()
|
|
||||||
for game in all_games:
|
|
||||||
existing_edition = None
|
|
||||||
try:
|
|
||||||
existing_edition = all_editions.objects.get(game=game.id)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if existing_edition == None:
|
|
||||||
edition = Edition()
|
|
||||||
edition.id = game.id
|
|
||||||
edition.game = game
|
|
||||||
edition.name = game.name
|
|
||||||
edition.platform = first_platform
|
|
||||||
edition.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("games", "0008_edition"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [migrations.RunPython(create_edition_of_game)]
|
|
@ -1,21 +0,0 @@
|
|||||||
# Generated by Django 4.1.5 on 2023-02-18 19:06
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("games", "0009_create_editions"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="purchase",
|
|
||||||
name="game",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE, to="games.edition"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 4.1.5 on 2023-02-18 19:18
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("games", "0010_alter_purchase_game"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name="purchase",
|
|
||||||
old_name="game",
|
|
||||||
new_name="edition",
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 4.1.5 on 2023-02-18 19:53
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("games", "0011_rename_game_purchase_edition"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="purchase",
|
|
||||||
name="price",
|
|
||||||
field=models.IntegerField(default=0),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="purchase",
|
|
||||||
name="price_currency",
|
|
||||||
field=models.CharField(default="USD", max_length=3),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,31 +0,0 @@
|
|||||||
# Generated by Django 4.1.5 on 2023-02-18 19:54
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("games", "0012_purchase_price_purchase_price_currency"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="purchase",
|
|
||||||
name="ownership_type",
|
|
||||||
field=models.CharField(
|
|
||||||
choices=[
|
|
||||||
("ph", "Physical"),
|
|
||||||
("di", "Digital"),
|
|
||||||
("du", "Digital Upgrade"),
|
|
||||||
("re", "Rented"),
|
|
||||||
("bo", "Borrowed"),
|
|
||||||
("tr", "Trial"),
|
|
||||||
("de", "Demo"),
|
|
||||||
("pi", "Pirated"),
|
|
||||||
],
|
|
||||||
default="di",
|
|
||||||
max_length=2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,52 +0,0 @@
|
|||||||
# Generated by Django 4.1.5 on 2023-02-18 19:59
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("games", "0013_purchase_ownership_type"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Device",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.CharField(max_length=255)),
|
|
||||||
(
|
|
||||||
"type",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("pc", "PC"),
|
|
||||||
("co", "Console"),
|
|
||||||
("ha", "Handheld"),
|
|
||||||
("mo", "Mobile"),
|
|
||||||
("sbc", "Single-board computer"),
|
|
||||||
],
|
|
||||||
default="pc",
|
|
||||||
max_length=3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="session",
|
|
||||||
name="device",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="games.device",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 4.1.5 on 2023-02-20 14:55
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("games", "0014_device_session_device"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="edition",
|
|
||||||
name="wikidata",
|
|
||||||
field=models.CharField(blank=True, default=None, max_length=50, null=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="edition",
|
|
||||||
name="year_released",
|
|
||||||
field=models.IntegerField(default=2023),
|
|
||||||
),
|
|
||||||
]
|
|
@ -16,52 +16,14 @@ class Game(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Edition(models.Model):
|
|
||||||
game = models.ForeignKey("Game", on_delete=models.CASCADE)
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
platform = models.ForeignKey("Platform", on_delete=models.CASCADE)
|
|
||||||
year_released = models.IntegerField(default=datetime.today().year)
|
|
||||||
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class Purchase(models.Model):
|
class Purchase(models.Model):
|
||||||
PHYSICAL = "ph"
|
game = models.ForeignKey("Game", on_delete=models.CASCADE)
|
||||||
DIGITAL = "di"
|
|
||||||
DIGITALUPGRADE = "du"
|
|
||||||
RENTED = "re"
|
|
||||||
BORROWED = "bo"
|
|
||||||
TRIAL = "tr"
|
|
||||||
DEMO = "de"
|
|
||||||
PIRATED = "pi"
|
|
||||||
OWNERSHIP_TYPES = [
|
|
||||||
(PHYSICAL, "Physical"),
|
|
||||||
(DIGITAL, "Digital"),
|
|
||||||
(DIGITALUPGRADE, "Digital Upgrade"),
|
|
||||||
(RENTED, "Rented"),
|
|
||||||
(BORROWED, "Borrowed"),
|
|
||||||
(TRIAL, "Trial"),
|
|
||||||
(DEMO, "Demo"),
|
|
||||||
(PIRATED, "Pirated"),
|
|
||||||
]
|
|
||||||
|
|
||||||
edition = models.ForeignKey("Edition", on_delete=models.CASCADE)
|
|
||||||
platform = models.ForeignKey("Platform", on_delete=models.CASCADE)
|
platform = models.ForeignKey("Platform", on_delete=models.CASCADE)
|
||||||
date_purchased = models.DateField()
|
date_purchased = models.DateField()
|
||||||
date_refunded = models.DateField(blank=True, null=True)
|
date_refunded = models.DateField(blank=True, null=True)
|
||||||
price = models.IntegerField(default=0)
|
|
||||||
price_currency = models.CharField(max_length=3, default="USD")
|
|
||||||
ownership_type = models.CharField(
|
|
||||||
max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
platform_info = self.platform
|
return f"{self.game} ({self.platform})"
|
||||||
if self.platform != self.edition.platform:
|
|
||||||
platform_info = f"{self.edition.platform} version on {self.platform}"
|
|
||||||
return f"{self.edition} ({platform_info}, {self.edition.year_released}, {self.get_ownership_type_display()})"
|
|
||||||
|
|
||||||
|
|
||||||
class Platform(models.Model):
|
class Platform(models.Model):
|
||||||
@ -86,7 +48,6 @@ class Session(models.Model):
|
|||||||
timestamp_end = models.DateTimeField(blank=True, null=True)
|
timestamp_end = models.DateTimeField(blank=True, null=True)
|
||||||
duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0))
|
duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0))
|
||||||
duration_calculated = models.DurationField(blank=True, null=True)
|
duration_calculated = models.DurationField(blank=True, null=True)
|
||||||
device = models.ForeignKey("Device", on_delete=models.CASCADE, null=True)
|
|
||||||
note = models.TextField(blank=True, null=True)
|
note = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
objects = SessionQuerySet.as_manager()
|
objects = SessionQuerySet.as_manager()
|
||||||
@ -124,23 +85,3 @@ class Session(models.Model):
|
|||||||
else:
|
else:
|
||||||
self.duration_calculated = timedelta(0)
|
self.duration_calculated = timedelta(0)
|
||||||
super(Session, self).save(*args, **kwargs)
|
super(Session, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Device(models.Model):
|
|
||||||
PC = "pc"
|
|
||||||
CONSOLE = "co"
|
|
||||||
HANDHELD = "ha"
|
|
||||||
MOBILE = "mo"
|
|
||||||
SBC = "sbc"
|
|
||||||
DEVICE_TYPES = [
|
|
||||||
(PC, "PC"),
|
|
||||||
(CONSOLE, "Console"),
|
|
||||||
(HANDHELD, "Handheld"),
|
|
||||||
(MOBILE, "Mobile"),
|
|
||||||
(SBC, "Single-board computer"),
|
|
||||||
]
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
type = models.CharField(max_length=3, choices=DEVICE_TYPES, default=PC)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name} ({self.get_type_display()})"
|
|
||||||
|
0
games/serializers.py
Normal file
Before Width: | Height: | Size: 292 KiB |
Before Width: | Height: | Size: 321 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 306 KiB |
Before Width: | Height: | Size: 312 KiB |
Before Width: | Height: | Size: 364 KiB |
Before Width: | Height: | Size: 42 KiB |
@ -3,14 +3,11 @@
|
|||||||
{% block title %}{{ title }}{% endblock title %}
|
{% block title %}{{ title }}{% endblock title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data" class="mx-auto">
|
||||||
<table class="mx-auto">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{{ form.as_table }}
|
{{ form.as_p }}
|
||||||
<tr>
|
|
||||||
<td><input type="submit" value="Submit"/></td>
|
<input type="submit" value="Submit"/>
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
@ -17,8 +17,8 @@
|
|||||||
<div class="dark:bg-gray-800 min-h-screen">
|
<div class="dark:bg-gray-800 min-h-screen">
|
||||||
<nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
|
<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 'index' %}" class="flex items-center">
|
||||||
<span class="text-4xl"><img src="{% static 'icons/schedule.png' %}" width="48" class="mr-4" /></span>
|
<span class="text-4xl">⌚</span>
|
||||||
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
|
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="w-full md:block md:w-auto">
|
<div class="w-full md:block md:w-auto">
|
||||||
@ -27,14 +27,10 @@
|
|||||||
<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_game' %}">New Game</a></li>
|
||||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_platform' %}">New Platform</a></li>
|
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_platform' %}">New Platform</a></li>
|
||||||
{% if game_available and platform_available %}
|
{% if game_available and platform_available %}
|
||||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_edition' %}">New Edition</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if edition_available %}
|
|
||||||
<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_purchase' %}">New Purchase</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if purchase_available %}
|
{% if purchase_available %}
|
||||||
<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 'add_session' %}">New Session</a></li>
|
||||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_device' %}">New Device</a></li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if session_count > 0 %}
|
{% if session_count > 0 %}
|
||||||
<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 'list_sessions' %}">All Sessions</a></li>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% load static %}
|
{% block title %}Sessions{% endblock title %}
|
||||||
|
|
||||||
{% block title %}{{ title }}{% endblock title %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="text-center text-xl mb-4 dark:text-slate-400">
|
<div class="text-center text-xl mb-4 dark:text-slate-400">
|
||||||
@ -12,33 +10,15 @@
|
|||||||
{% if dataset.count >= 1 %}
|
{% if dataset.count >= 1 %}
|
||||||
<div class="mb-4">Total playtime: {{ total_duration }} over {{ dataset.count }} sessions.</div>
|
<div class="mb-4">Total playtime: {{ total_duration }} over {{ dataset.count }} sessions.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if purchase or platform or edition or game or ownership_type %}
|
{% if purchase or platform or game %}
|
||||||
<span class="block">
|
<a class="text-red-400 inline" href="{% url 'list_sessions' %}"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 inline">
|
||||||
<a class="text-red-400 inline" href="{% url 'list_sessions' %}">
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 inline">
|
</svg>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
</a><span>Filtering by "{% firstof purchase platform game %}"</span>
|
||||||
</svg>
|
{% if purchase %}<a class="dark:text-white hover:underline block" href="{% url 'list_sessions_by_game' purchase.game.id %}">See all platforms</a>{% endif %}
|
||||||
</a>
|
|
||||||
{% if purchase %}
|
|
||||||
Filtering by purchase "{{ purchase }}"
|
|
||||||
(<a href="{% url 'edit_purchase' purchase.id %}" class="hover:underline dark:text-white">Edit</a>)
|
|
||||||
{% elif platform %}
|
|
||||||
Filtering by purchase "{{ platform }}"
|
|
||||||
(<a href="{% url 'edit_platform' platform.id %}" class="hover:underline dark:text-white">Edit</a>)
|
|
||||||
{% elif game %}
|
|
||||||
Filtering by purchase "{{ game }}"
|
|
||||||
(<a href="{% url 'edit_game' game.id %}" class="hover:underline dark:text-white">Edit</a>)
|
|
||||||
{% elif edition %}
|
|
||||||
Filtering by purchase "{{ edition }}"
|
|
||||||
(<a href="{% url 'edit_edition' edition.id %}" class="hover:underline dark:text-white">Edit</a>)
|
|
||||||
{% elif ownership_type %}
|
|
||||||
Filtering by ownership type "{{ ownership_type }}"
|
|
||||||
{% endif%}
|
|
||||||
</span>
|
|
||||||
{% if purchase %}<a class="dark:text-white hover:underline block" href="{% url 'list_sessions_by_edition' purchase.edition.id %}">See all platforms</a>{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if dataset.count >= 1 %}
|
{% if dataset.count >= 1 %}
|
||||||
<a href="{% url 'start_session' last.id %}">
|
<a class="clear-both" href="{% url 'start_session' last.purchase.id %}">
|
||||||
<button type="button" title="Track last tracked" class="mt-10 py-1 px-2 bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg ">
|
<button type="button" title="Track last tracked" class="mt-10 py-1 px-2 bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg ">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="self-center w-6 h-6 inline">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="self-center w-6 h-6 inline">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
|
||||||
@ -48,38 +28,16 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div id="session-table" class="gap-4 shadow rounded-xl max-w-screen-2xl mx-auto dark:bg-slate-700 p-2 justify-center">
|
<div id="session-table" class="gap-4 shadow rounded-xl max-w-screen-lg mx-auto dark:bg-slate-700 p-2 justify-center">
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg">Purchase</div>
|
<div class="dark:border-white dark:text-slate-300 text-lg">Name</div>
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg">Platform</div>
|
<div class="dark:border-white dark:text-slate-300 text-lg">Platform</div>
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg text-center">Start</div>
|
<div class="dark:border-white dark:text-slate-300 text-lg text-center">Start</div>
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg text-center">End</div>
|
<div class="dark:border-white dark:text-slate-300 text-lg text-center">End</div>
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg">Duration</div>
|
<div class="dark:border-white dark:text-slate-300 text-lg">Duration</div>
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg text-right">Manage</div>
|
<div class="dark:border-white dark:text-slate-300 text-lg text-right">Manage</div>
|
||||||
{% for data in dataset %}
|
{% for data in dataset %}
|
||||||
<div class="purchase-name">
|
<div class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap"><a class="hover:underline" href="{% url 'list_sessions_by_purchase' data.purchase.id %}">{{ data.purchase.game }}</a></div>
|
||||||
<span class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">{{ data.purchase.edition }} <span class="dark:text-slate-400">(<a class="hover:underline" href="{% url 'list_sessions_by_ownership_type' data.purchase.ownership_type %}">{{ data.purchase.get_ownership_type_display }}</a>)</span></span>
|
<div class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap"><a class="hover:underline" href="{% url 'list_sessions_by_platform' data.purchase.platform.id %}">{{ data.purchase.platform }}</a></div>
|
||||||
|
|
||||||
<span>
|
|
||||||
<a href="{% url 'list_sessions_by_game' data.purchase.edition.game.id %}">
|
|
||||||
<img src="{% static 'icons/game_white.png' %}" width="32" class="inline" alt="Filter by this game" title="Filter by this game" />
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'list_sessions_by_edition' data.purchase.edition.id %}">
|
|
||||||
<img src="{% static 'icons/edition_white.png' %}" width="32" class="inline" alt="Filter by this edition" title="Filter by this edition" />
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'list_sessions_by_purchase' data.purchase.id %}">
|
|
||||||
<img src="{% static 'icons/purchase_white.png' %}" width="32" class="inline" alt="Filter by this purchase" title="Filter by this purchase" />
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
<a class="hover:underline" href="{% url 'list_sessions_by_platform' data.purchase.platform.id %}">
|
|
||||||
{% if data.purchase.platform != data.purchase.edition.platform %}
|
|
||||||
{{data.purchase.edition.platform}} on {{ data.purchase.platform }}
|
|
||||||
{% else %}
|
|
||||||
{{ data.purchase.platform }}
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="dark:text-slate-400 text-center">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div>
|
<div class="dark:text-slate-400 text-center">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div>
|
||||||
<div class="dark:text-slate-400 text-center">
|
<div class="dark:text-slate-400 text-center">
|
||||||
{% if data.unfinished %}
|
{% if data.unfinished %}
|
||||||
@ -104,14 +62,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'edit_session' data.id %}">
|
<button type="button" title="Edit" class="py-1 px-2 flex justify-center items-center bg-blue-600 hover:bg-blue-700 focus:ring-blue-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 w-7 h-4 rounded-lg ">
|
||||||
<button type="button" title="Edit" class="py-1 px-2 flex justify-center items-center bg-blue-600 hover:bg-blue-700 focus:ring-blue-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 w-7 h-4 rounded-lg ">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
<path d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z" />
|
||||||
<path d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z" />
|
<path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z" />
|
||||||
<path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z" />
|
</svg>
|
||||||
</svg>
|
</button>
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'delete_session' data.id %}">
|
<a href="{% url 'delete_session' data.id %}">
|
||||||
<button type="button" edit="Delete" class="py-1 px-2 flex justify-center items-center bg-red-600 hover:bg-red-700 focus:ring-red-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 w-7 h-4 rounded-lg ">
|
<button type="button" edit="Delete" class="py-1 px-2 flex justify-center items-center bg-red-600 hover:bg-red-700 focus:ring-red-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 w-7 h-4 rounded-lg ">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||||
|
@ -4,12 +4,6 @@ from games import views
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.index, name="index"),
|
path("", views.index, name="index"),
|
||||||
path(
|
|
||||||
"list-sessions/recent",
|
|
||||||
views.list_sessions,
|
|
||||||
{"filter": "recent"},
|
|
||||||
name="list_sessions_recent",
|
|
||||||
),
|
|
||||||
path("add-game/", views.add_game, name="add_game"),
|
path("add-game/", views.add_game, name="add_game"),
|
||||||
path("add-platform/", views.add_platform, name="add_platform"),
|
path("add-platform/", views.add_platform, name="add_platform"),
|
||||||
path("add-session/", views.add_session, name="add_session"),
|
path("add-session/", views.add_session, name="add_session"),
|
||||||
@ -19,7 +13,7 @@ urlpatterns = [
|
|||||||
name="update_session",
|
name="update_session",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"start-session/<int:last_session_id>",
|
"start-session/<int:purchase_id>",
|
||||||
views.start_session,
|
views.start_session,
|
||||||
name="start_session",
|
name="start_session",
|
||||||
),
|
),
|
||||||
@ -29,13 +23,6 @@ urlpatterns = [
|
|||||||
name="delete_session",
|
name="delete_session",
|
||||||
),
|
),
|
||||||
path("add-purchase/", views.add_purchase, name="add_purchase"),
|
path("add-purchase/", views.add_purchase, name="add_purchase"),
|
||||||
path("add-edition/", views.add_edition, name="add_edition"),
|
|
||||||
path("edit-edition/<int:edition_id>", views.edit_edition, name="edit_edition"),
|
|
||||||
path("edit-game/<int:game_id>", 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/", views.list_sessions, name="list_sessions"),
|
||||||
path(
|
path(
|
||||||
"list-sessions/by-purchase/<int:purchase_id>",
|
"list-sessions/by-purchase/<int:purchase_id>",
|
||||||
@ -55,16 +42,4 @@ urlpatterns = [
|
|||||||
{"filter": "game"},
|
{"filter": "game"},
|
||||||
name="list_sessions_by_game",
|
name="list_sessions_by_game",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"list-sessions/by-edition/<int:edition_id>",
|
|
||||||
views.list_sessions,
|
|
||||||
{"filter": "edition"},
|
|
||||||
name="list_sessions_by_edition",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"list-sessions/by-ownership/<str:ownership_type>",
|
|
||||||
views.list_sessions,
|
|
||||||
{"filter": "ownership_type"},
|
|
||||||
name="list_sessions_by_ownership_type",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
140
games/views.py
@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from common.plots import playtime_over_time_chart
|
from common.plots import playtime_over_time_chart
|
||||||
@ -6,21 +6,13 @@ from common.time import now as now_with_tz
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
|
|
||||||
from .forms import (
|
from .forms import GameForm, PlatformForm, PurchaseForm, SessionForm
|
||||||
GameForm,
|
from .models import Game, Platform, Purchase, Session
|
||||||
PlatformForm,
|
|
||||||
PurchaseForm,
|
|
||||||
SessionForm,
|
|
||||||
EditionForm,
|
|
||||||
DeviceForm,
|
|
||||||
)
|
|
||||||
from .models import Game, Platform, Purchase, Session, Edition
|
|
||||||
|
|
||||||
|
|
||||||
def model_counts(request):
|
def model_counts(request):
|
||||||
return {
|
return {
|
||||||
"game_available": Game.objects.count() != 0,
|
"game_available": Game.objects.count() != 0,
|
||||||
"edition_available": Edition.objects.count() != 0,
|
|
||||||
"platform_available": Platform.objects.count() != 0,
|
"platform_available": Platform.objects.count() != 0,
|
||||||
"purchase_available": Purchase.objects.count() != 0,
|
"purchase_available": Purchase.objects.count() != 0,
|
||||||
"session_count": Session.objects.count(),
|
"session_count": Session.objects.count(),
|
||||||
@ -55,75 +47,8 @@ def update_session(request, session_id=None):
|
|||||||
return redirect("list_sessions")
|
return redirect("list_sessions")
|
||||||
|
|
||||||
|
|
||||||
def edit_session(request, session_id=None):
|
def start_session(request, purchase_id=None):
|
||||||
context = {}
|
session = SessionForm({"purchase": purchase_id, "timestamp_start": now_with_tz()})
|
||||||
session = Session.objects.get(id=session_id)
|
|
||||||
form = SessionForm(request.POST or None, instance=session)
|
|
||||||
if form.is_valid():
|
|
||||||
form.save()
|
|
||||||
return redirect("list_sessions")
|
|
||||||
context["title"] = "Edit Session"
|
|
||||||
context["form"] = form
|
|
||||||
return render(request, "add.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
def edit_purchase(request, purchase_id=None):
|
|
||||||
context = {}
|
|
||||||
purchase = Purchase.objects.get(id=purchase_id)
|
|
||||||
form = PurchaseForm(request.POST or None, instance=purchase)
|
|
||||||
if form.is_valid():
|
|
||||||
form.save()
|
|
||||||
return redirect("list_sessions")
|
|
||||||
context["title"] = "Edit Purchase"
|
|
||||||
context["form"] = form
|
|
||||||
return render(request, "add.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
def edit_game(request, game_id=None):
|
|
||||||
context = {}
|
|
||||||
purchase = Game.objects.get(id=game_id)
|
|
||||||
form = GameForm(request.POST or None, instance=purchase)
|
|
||||||
if form.is_valid():
|
|
||||||
form.save()
|
|
||||||
return redirect("list_sessions")
|
|
||||||
context["title"] = "Edit Game"
|
|
||||||
context["form"] = form
|
|
||||||
return render(request, "add.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
def edit_platform(request, platform_id=None):
|
|
||||||
context = {}
|
|
||||||
purchase = Platform.objects.get(id=platform_id)
|
|
||||||
form = PlatformForm(request.POST or None, instance=purchase)
|
|
||||||
if form.is_valid():
|
|
||||||
form.save()
|
|
||||||
return redirect("list_sessions")
|
|
||||||
context["title"] = "Edit Platform"
|
|
||||||
context["form"] = form
|
|
||||||
return render(request, "add.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
def edit_edition(request, edition_id=None):
|
|
||||||
context = {}
|
|
||||||
edition = Edition.objects.get(id=edition_id)
|
|
||||||
form = EditionForm(request.POST or None, instance=edition)
|
|
||||||
if form.is_valid():
|
|
||||||
form.save()
|
|
||||||
return redirect("list_sessions")
|
|
||||||
context["title"] = "Edit Edition"
|
|
||||||
context["form"] = form
|
|
||||||
return render(request, "add.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
def start_session(request, last_session_id: int):
|
|
||||||
last_session = Session.objects.get(id=last_session_id)
|
|
||||||
session = SessionForm(
|
|
||||||
{
|
|
||||||
"purchase": last_session.purchase.id,
|
|
||||||
"timestamp_start": now_with_tz(),
|
|
||||||
"device": last_session.device,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
session.save()
|
session.save()
|
||||||
return redirect("list_sessions")
|
return redirect("list_sessions")
|
||||||
|
|
||||||
@ -134,17 +59,8 @@ def delete_session(request, session_id=None):
|
|||||||
return redirect("list_sessions")
|
return redirect("list_sessions")
|
||||||
|
|
||||||
|
|
||||||
def list_sessions(
|
def list_sessions(request, filter="", purchase_id="", platform_id="", game_id=""):
|
||||||
request,
|
|
||||||
filter="",
|
|
||||||
purchase_id="",
|
|
||||||
platform_id="",
|
|
||||||
game_id="",
|
|
||||||
edition_id="",
|
|
||||||
ownership_type: str = "",
|
|
||||||
):
|
|
||||||
context = {}
|
context = {}
|
||||||
context["title"] = "Sessions"
|
|
||||||
|
|
||||||
if filter == "purchase":
|
if filter == "purchase":
|
||||||
dataset = Session.objects.filter(purchase=purchase_id)
|
dataset = Session.objects.filter(purchase=purchase_id)
|
||||||
@ -152,20 +68,9 @@ def list_sessions(
|
|||||||
elif filter == "platform":
|
elif filter == "platform":
|
||||||
dataset = Session.objects.filter(purchase__platform=platform_id)
|
dataset = Session.objects.filter(purchase__platform=platform_id)
|
||||||
context["platform"] = Platform.objects.get(id=platform_id)
|
context["platform"] = Platform.objects.get(id=platform_id)
|
||||||
elif filter == "edition":
|
|
||||||
dataset = Session.objects.filter(purchase__edition=edition_id)
|
|
||||||
context["edition"] = Edition.objects.get(id=edition_id)
|
|
||||||
elif filter == "game":
|
elif filter == "game":
|
||||||
dataset = Session.objects.filter(purchase__edition__game=game_id)
|
dataset = Session.objects.filter(purchase__game=game_id)
|
||||||
context["game"] = Game.objects.get(id=game_id)
|
context["game"] = Game.objects.get(id=game_id)
|
||||||
elif filter == "ownership_type":
|
|
||||||
dataset = Session.objects.filter(purchase__ownership_type=ownership_type)
|
|
||||||
context["ownership_type"] = dict(Purchase.OWNERSHIP_TYPES)[ownership_type]
|
|
||||||
elif filter == "recent":
|
|
||||||
dataset = Session.objects.filter(
|
|
||||||
timestamp_start__gte=datetime.now() - timedelta(days=30)
|
|
||||||
).order_by("-timestamp_start")
|
|
||||||
context["title"] = "Last 30 days"
|
|
||||||
else:
|
else:
|
||||||
# by default, sort from newest to oldest
|
# by default, sort from newest to oldest
|
||||||
dataset = Session.objects.all().order_by("-timestamp_start")
|
dataset = Session.objects.all().order_by("-timestamp_start")
|
||||||
@ -180,7 +85,7 @@ def list_sessions(
|
|||||||
# cannot use dataset[0] here because that might be only partial QuerySet
|
# cannot use dataset[0] here because that might be only partial QuerySet
|
||||||
context["last"] = Session.objects.all().order_by("timestamp_start").last()
|
context["last"] = Session.objects.all().order_by("timestamp_start").last()
|
||||||
# charts are always oldest->newest
|
# charts are always oldest->newest
|
||||||
if dataset.count() >= 2:
|
if Session.objects.count() >= 2:
|
||||||
context["chart"] = playtime_over_time_chart(dataset.order_by("timestamp_start"))
|
context["chart"] = playtime_over_time_chart(dataset.order_by("timestamp_start"))
|
||||||
|
|
||||||
return render(request, "list_sessions.html", context)
|
return render(request, "list_sessions.html", context)
|
||||||
@ -212,18 +117,6 @@ def add_game(request):
|
|||||||
return render(request, "add.html", context)
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
def add_edition(request):
|
|
||||||
context = {}
|
|
||||||
form = EditionForm(request.POST or None)
|
|
||||||
if form.is_valid():
|
|
||||||
form.save()
|
|
||||||
return redirect("index")
|
|
||||||
|
|
||||||
context["form"] = form
|
|
||||||
context["title"] = "Add New Edition"
|
|
||||||
return render(request, "add.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
def add_platform(request):
|
def add_platform(request):
|
||||||
context = {}
|
context = {}
|
||||||
form = PlatformForm(request.POST or None)
|
form = PlatformForm(request.POST or None)
|
||||||
@ -236,17 +129,8 @@ def add_platform(request):
|
|||||||
return render(request, "add.html", context)
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
def add_device(request):
|
|
||||||
context = {}
|
|
||||||
form = DeviceForm(request.POST or None)
|
|
||||||
if form.is_valid():
|
|
||||||
form.save()
|
|
||||||
return redirect("index")
|
|
||||||
|
|
||||||
context["form"] = form
|
|
||||||
context["title"] = "Add New Device"
|
|
||||||
return render(request, "add.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
return redirect("list_sessions_recent")
|
context = {}
|
||||||
|
context["total_duration"] = Session().duration_sum
|
||||||
|
context["title"] = "Index"
|
||||||
|
return render(request, "index.html", context)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"devDependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@tailwindcss/typography": "^0.5.8",
|
"@tailwindcss/typography": "^0.5.9"
|
||||||
"tailwindcss": "^3.2.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
54
poetry.lock
generated
@ -220,6 +220,21 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
|||||||
argon2 = ["argon2-cffi (>=19.1.0)"]
|
argon2 = ["argon2-cffi (>=19.1.0)"]
|
||||||
bcrypt = ["bcrypt"]
|
bcrypt = ["bcrypt"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-cors-headers"
|
||||||
|
version = "3.13.0"
|
||||||
|
description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "django-cors-headers-3.13.0.tar.gz", hash = "sha256:f9dc6b4e3f611c3199700b3e5f3398c28757dcd559c2f82932687f3d0443cfdf"},
|
||||||
|
{file = "django_cors_headers-3.13.0-py3-none-any.whl", hash = "sha256:37e42883b5f1f2295df6b4bba96eb2417a14a03270cb24b2a07f021cd4487cf4"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Django = ">=3.2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-extensions"
|
name = "django-extensions"
|
||||||
version = "3.2.1"
|
version = "3.2.1"
|
||||||
@ -235,6 +250,36 @@ files = [
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
Django = ">=3.2"
|
Django = ">=3.2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-rest-framework"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "alias."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "django-rest-framework-0.1.0.tar.gz", hash = "sha256:47a8f496fa69e3b6bd79f68dd7a1527d907d6b77f009e9db7cf9bb21cc565e4a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
djangorestframework = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "djangorestframework"
|
||||||
|
version = "3.14.0"
|
||||||
|
description = "Web APIs for Django, made easy."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"},
|
||||||
|
{file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
django = ">=3.0"
|
||||||
|
pytz = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "djhtml"
|
name = "djhtml"
|
||||||
version = "1.5.2"
|
version = "1.5.2"
|
||||||
@ -828,6 +873,13 @@ files = [
|
|||||||
{file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"},
|
{file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"},
|
||||||
{file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"},
|
{file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"},
|
||||||
{file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"},
|
{file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"},
|
||||||
|
{file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"},
|
||||||
|
{file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"},
|
||||||
|
{file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"},
|
||||||
|
{file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"},
|
||||||
|
{file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"},
|
||||||
|
{file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"},
|
||||||
|
{file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"},
|
||||||
{file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"},
|
{file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"},
|
||||||
{file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"},
|
{file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"},
|
||||||
{file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"},
|
{file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"},
|
||||||
@ -1297,4 +1349,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "bf02e951b8c14fbe6f2709b6df7f9664f99ad5fbf7195d8e7b18c8574d00e683"
|
content-hash = "7a7ba3831802cf91b722b956817a0606b7b8f97724b1a23e5e581657fb34ea19"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "timetracker"
|
name = "timetracker"
|
||||||
version = "1.0.3"
|
version = "1.0.0"
|
||||||
description = "A simple time tracker."
|
description = "A simple time tracker."
|
||||||
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"]
|
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"]
|
||||||
license = "GPL"
|
license = "GPL"
|
||||||
@ -14,6 +14,8 @@ gunicorn = "^20.1.0"
|
|||||||
uvicorn = "^0.20.0"
|
uvicorn = "^0.20.0"
|
||||||
pandas = "^1.5.2"
|
pandas = "^1.5.2"
|
||||||
matplotlib = "^3.6.3"
|
matplotlib = "^3.6.3"
|
||||||
|
django-rest-framework = "^0.1.0"
|
||||||
|
django-cors-headers = "^3.13.0"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^22.12.0"
|
black = "^22.12.0"
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
darkMode: 'class',
|
|
||||||
content: ["./**/*.{html,js}"],
|
|
||||||
theme: {
|
|
||||||
fontFamily: {
|
|
||||||
sans: ['Inter', 'sans-serif'],
|
|
||||||
},
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
require('@tailwindcss/typography'),
|
|
||||||
require('@tailwindcss/forms')
|
|
||||||
],
|
|
||||||
}
|
|
@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/4.1/ref/settings/
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from corsheaders.defaults import default_headers, default_methods
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
@ -38,6 +39,8 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
|
"rest_framework",
|
||||||
|
"corsheaders",
|
||||||
]
|
]
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
@ -45,6 +48,7 @@ if DEBUG:
|
|||||||
INSTALLED_APPS.append("django.contrib.admin")
|
INSTALLED_APPS.append("django.contrib.admin")
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
@ -149,3 +153,24 @@ if _csrf_trusted_origins:
|
|||||||
CSRF_TRUSTED_ORIGINS = _csrf_trusted_origins.split(",")
|
CSRF_TRUSTED_ORIGINS = _csrf_trusted_origins.split(",")
|
||||||
else:
|
else:
|
||||||
CSRF_TRUSTED_ORIGINS = []
|
CSRF_TRUSTED_ORIGINS = []
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
"DEFAULT_PERMISSION_CLASSES": [
|
||||||
|
"rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
FRONTEND_ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "frontend", "dist"))
|
||||||
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
|
||||||
|
CORS_ALLOW_HEADERS = list(default_headers) + [
|
||||||
|
"Accept-Language",
|
||||||
|
"Connection",
|
||||||
|
"Host",
|
||||||
|
"Origin",
|
||||||
|
"Referer",
|
||||||
|
"Sec-Fetch-Dest",
|
||||||
|
"Sec-Fetch-Mode",
|
||||||
|
"Sec-Fetch-Site",
|
||||||
|
]
|
||||||
|
@ -17,10 +17,64 @@ from django.conf import settings
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
from rest_framework import routers, serializers, viewsets
|
||||||
|
from games.models import Game, Purchase, Platform, Session
|
||||||
|
|
||||||
|
|
||||||
|
class GameSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Game
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Platform
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Purchase
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class SessionSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Session
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class GameViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Game.objects.all()
|
||||||
|
serializer_class = GameSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Platform.objects.all()
|
||||||
|
serializer_class = PlatformSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Purchase.objects.all()
|
||||||
|
serializer_class = PurchaseSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class SessionViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Session.objects.all()
|
||||||
|
serializer_class = SessionSerializer
|
||||||
|
|
||||||
|
|
||||||
|
router = routers.DefaultRouter()
|
||||||
|
router.register(r"games", GameViewSet)
|
||||||
|
router.register(r"platforms", PlatformViewSet)
|
||||||
|
router.register(r"purchases", PurchaseViewSet)
|
||||||
|
router.register(r"sessions", SessionViewSet)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", RedirectView.as_view(url="/tracker")),
|
path("api/", include(router.urls)),
|
||||||
path("tracker/", include("games.urls")),
|
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|