Compare commits

..

419 Commits
0.1.1 ... main

Author SHA1 Message Date
Lukáš Kucharczyk 8acc4f9c5b
make table work better on small screens
Django CI/CD / test (push) Successful in 56s Details
Django CI/CD / build-and-push (push) Successful in 2m12s Details
2024-11-13 21:28:44 +01:00
Lukáš Kucharczyk 6b7a96dc06
make PopoverTruncated customizable 2024-11-13 21:28:17 +01:00
Lukáš Kucharczyk 5c5fd5f26a
truncate: strip trailing whitespace 2024-11-13 21:07:26 +01:00
Lukáš Kucharczyk 7181b6472c
fix mistakenly hardcoded value in truncate() 2024-11-13 21:06:52 +01:00
Lukáš Kucharczyk af06d07ee3
pre-commit hook: disable isort 2024-11-13 21:06:38 +01:00
Lukáš Kucharczyk 315e22a8ac
Add yaml to dependencies
Django CI/CD / test (push) Successful in 1m28s Details
Django CI/CD / build-and-push (push) Successful in 2m33s Details
2024-11-11 18:14:48 +01:00
Lukáš Kucharczyk 19676f8441 Implement converting prices (#79)
Django CI/CD / test (push) Successful in 1m17s Details
Django CI/CD / build-and-push (push) Successful in 2m10s Details
Reviewed-on: #79
2024-11-11 16:36:57 +00:00
Lukáš Kucharczyk f61cde180f Pass search_string to search_field.html
Django CI/CD / test (push) Successful in 1m10s Details
Django CI/CD / build-and-push (push) Successful in 2m1s Details
2024-11-10 00:05:33 +01:00
Lukáš Kucharczyk a53818257c Fix being unable to override c-vars from render_from_string 2024-11-10 00:05:11 +01:00
Lukáš Kucharczyk 2d3ea714c4 Extend session search 2024-11-09 23:52:09 +01:00
Lukáš Kucharczyk 832bb48983 Device: safe long type names directly in database 2024-11-09 23:51:28 +01:00
Lukáš Kucharczyk c6b1badf39 add session search
Django CI/CD / test (push) Successful in 1m10s Details
Django CI/CD / build-and-push (push) Successful in 2m16s Details
2024-11-09 21:34:01 +00:00
Lukáš Kucharczyk a3ed93c154 handle non-existent icons
Django CI/CD / test (push) Successful in 1m15s Details
Django CI/CD / build-and-push (push) Has been cancelled Details
2024-11-09 21:30:13 +00:00
Lukáš Kucharczyk cf503a7b7d improve devcontainer
Django CI/CD / test (push) Successful in 1m7s Details
Django CI/CD / build-and-push (push) Successful in 2m26s Details
2024-11-09 11:56:20 +01:00
Lukáš Kucharczyk d81df6452a add dev container
Django CI/CD / test (push) Successful in 1m15s Details
Django CI/CD / build-and-push (push) Successful in 2m26s Details
2024-11-09 11:22:21 +01:00
Lukáš Kucharczyk d9290373b0
also sort purchases by created_at
Django CI/CD / test (push) Successful in 59s Details
Django CI/CD / build-and-push (push) Successful in 2m24s Details
2024-10-18 09:50:10 +02:00
Lukáš Kucharczyk f8d621e710
fix stat dropdown
Django CI/CD / test (push) Successful in 1m16s Details
Django CI/CD / build-and-push (push) Successful in 2m7s Details
2024-10-16 18:31:12 +02:00
Lukáš Kucharczyk 9992d9c9bd
set edition platform to unspecified if none 2024-10-16 18:06:40 +02:00
Lukáš Kucharczyk 2ae81bb00f
update django-cotton to 1.2.1 2024-10-16 17:49:55 +02:00
Lukáš Kucharczyk 993abb4710
editorconfig: do not add newline to HTML 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk 23502eab85
do not throw error when no stats to calculate 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk c517d735c7
use unified dateformat more 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk 19056f846e
view_game: display timezone-aware time for end timestamp 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk 0759ad0804
make purchase price a float 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk 228fc2bf5f
avoid exception on game overview when sessions are 0 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk a5a7041920
rename icon 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk fbd829f70e
order platforms by name 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk 4873f25248
remove css cruft 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk 3578f1707f
add more icons 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk b74ccb6eaa
Remove extraneous statement 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk b0b1bb2d42
add icon field to platform, use everywhere 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk c40764a02f
fix bug in Component
A leftover from when there was only the A component function,
this bug was not found earlier because we used
templates instead of tags most of the time.
2024-09-14 10:40:03 +02:00
Lukáš Kucharczyk 649351efde
implement platform icons
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-09-14 06:42:34 +02:00
Lukáš Kucharczyk 698c8966c0
add purchase date to game view
Django CI/CD / test (push) Successful in 1m9s Details
Django CI/CD / build-and-push (push) Successful in 2m8s Details
2024-09-11 11:40:17 +02:00
Lukáš Kucharczyk 7f6584ecf7
finish purchase from list 2024-09-11 11:39:54 +02:00
Lukáš Kucharczyk 540f5ee42c
align last column to the right 2024-09-11 11:39:48 +02:00
Lukáš Kucharczyk 1c73268258
redirect to purchase list after modifying purchase 2024-09-10 14:50:49 +02:00
Lukáš Kucharczyk 3063a3d143
refund purchase from list 2024-09-10 14:50:02 +02:00
Lukáš Kucharczyk b589199ca6
drop purchase from list 2024-09-10 14:46:50 +02:00
Lukáš Kucharczyk 2fc661dade
re-add button titles 2024-09-10 14:46:10 +02:00
Lukáš Kucharczyk 1f535a6e84
formatting 2024-09-10 14:26:06 +02:00
Lukáš Kucharczyk a9c1135639
improve layout
Django CI/CD / test (push) Successful in 1m7s Details
Django CI/CD / build-and-push (push) Successful in 2m12s Details
2024-09-09 11:25:29 +02:00
Lukáš Kucharczyk 58cfaca1a9
add table header actions
Django CI/CD / test (push) Successful in 1m3s Details
Django CI/CD / build-and-push (push) Successful in 2m23s Details
2024-09-08 21:03:37 +02:00
Lukáš Kucharczyk c1b3493c80
Merge calculated and manual duration
Django CI/CD / test (push) Successful in 1m2s Details
Django CI/CD / build-and-push (push) Successful in 1m55s Details
2024-09-07 23:35:59 +02:00
Lukáš Kucharczyk a1df8720f5
Fix missing variable reference
Django CI/CD / test (push) Successful in 1m11s Details
Django CI/CD / build-and-push (push) Successful in 2m3s Details
2024-09-07 23:20:17 +02:00
Lukáš Kucharczyk 5a852bc2b9
tailwind: define accent and background colors
Django CI/CD / test (push) Successful in 55s Details
Django CI/CD / build-and-push (push) Successful in 2m6s Details
2024-09-04 21:59:29 +02:00
Lukáš Kucharczyk 8ab9bfeeeb
update deps 2024-09-04 21:59:06 +02:00
Lukáš Kucharczyk 5eee7176d4
add streak-releted basic functionality 2024-09-04 21:58:56 +02:00
Lukáš Kucharczyk 98c9c1faee
move time-related functionality out of views.general
Django CI/CD / test (push) Successful in 58s Details
Django CI/CD / build-and-push (push) Successful in 1m52s Details
2024-09-04 21:55:22 +02:00
Lukáš Kucharczyk 645ffa0dad
update styles
Django CI/CD / test (push) Successful in 1m3s Details
Django CI/CD / build-and-push (push) Successful in 2m4s Details
2024-09-03 22:39:25 +02:00
Lukáš Kucharczyk 4358708262
add links to add a new X to: game, edition, purchase, session, device, platform
Django CI/CD / test (push) Successful in 55s Details
Django CI/CD / build-and-push (push) Successful in 1m57s Details
2024-09-03 15:48:58 +02:00
Lukáš Kucharczyk c738245783
Properly display non-game type names
Django CI/CD / test (push) Successful in 1m8s Details
Django CI/CD / build-and-push (push) Successful in 1m55s Details
2024-09-02 23:52:28 +02:00
Lukáš Kucharczyk 57184ceea0
add one more breakpoint to better utilize smaller screens
Django CI/CD / test (push) Successful in 53s Details
Django CI/CD / build-and-push (push) Successful in 1m56s Details
2024-09-02 23:44:18 +02:00
Lukáš Kucharczyk c2b9409562
update styles
Django CI/CD / test (push) Successful in 53s Details
Django CI/CD / build-and-push (push) Successful in 1m52s Details
2024-09-02 20:14:52 +02:00
Lukáš Kucharczyk e067e65bce
linkify game, edition, purchase, session references
Django CI/CD / test (push) Successful in 1m0s Details
Django CI/CD / build-and-push (push) Has been cancelled Details
also add link styles for links in a table row
2024-09-02 20:04:21 +02:00
Lukáš Kucharczyk b8258e2937
replace slippers with django-cotton
Django CI/CD / test (push) Successful in 59s Details
Django CI/CD / build-and-push (push) Successful in 2m4s Details
main reason: slippers cannot pass request via context inside its
components, making it annoying to use template takes that take request.
more reasons: not actively worked on, no named slots, having to define
components in components.yaml + new components do not get registered
without restarting server
2024-09-02 17:43:41 +02:00
Lukáš Kucharczyk 9af4c79947
improve game view
Django CI/CD / test (push) Successful in 56s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-19 21:58:43 +02:00
Lukáš Kucharczyk d8b8182b91
fix table top rounding 2024-08-13 08:36:40 +02:00
Lukáš Kucharczyk 2fd44c1f53
separate views out 2/2
Django CI/CD / test (push) Successful in 57s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-12 21:52:26 +02:00
Lukáš Kucharczyk c3f99d124c
update base.css 2024-08-12 21:42:56 +02:00
Lukáš Kucharczyk 51f5b9fceb
update ruff path 2024-08-12 21:42:47 +02:00
Lukáš Kucharczyk 973f4416de
separate views out 1/2 2024-08-12 21:42:34 +02:00
Lukáš Kucharczyk a84209eb81
sort by timestamp
Django CI/CD / test (push) Successful in 51s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-11 21:39:14 +02:00
Lukáš Kucharczyk 498cd69328
improve display of game names, durations 2024-08-11 20:29:47 +02:00
Lukáš Kucharczyk b28c42d945
delete platform
Django CI/CD / test (push) Successful in 51s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-11 20:21:44 +02:00
Lukáš Kucharczyk 3099f02145
list editions 2024-08-11 20:21:27 +02:00
Lukáš Kucharczyk 74b9d0421c
list platforms, fix editing platform 2024-08-11 18:34:50 +02:00
Lukáš Kucharczyk c61adad180
list games 2024-08-11 18:21:11 +02:00
Lukáš Kucharczyk 298ecb4092
formatting 2024-08-11 17:58:35 +02:00
Lukáš Kucharczyk 020e12e20b
remove session recent filter 2024-08-11 17:58:08 +02:00
Lukáš Kucharczyk 6ef56bfed5
list, edit, and delete devices 2024-08-11 17:53:36 +02:00
Lukáš Kucharczyk fda4913c97
add ruff to shell.nix
Django CI/CD / test (push) Successful in 56s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-11 17:24:50 +02:00
Lukáš Kucharczyk e85b32e22f
update styles 2024-08-11 17:24:33 +02:00
Lukáš Kucharczyk 2d6d6d24a4
formatting 2024-08-11 17:24:26 +02:00
Lukáš Kucharczyk 00993a85db
remove black 2024-08-11 17:24:19 +02:00
Lukáš Kucharczyk 4f7e708255
vscode: replace black with ruff 2024-08-11 17:23:59 +02:00
Lukáš Kucharczyk 238e4839e0
formatting 2024-08-11 17:23:28 +02:00
Lukáš Kucharczyk b0ad806a93
fix version_date 2024-08-11 17:23:18 +02:00
Lukáš Kucharczyk 453b4fd922
add manage -> sessions 2024-08-11 17:22:58 +02:00
Lukáš Kucharczyk bb0d24809e
make sure titles are truncated 2024-08-11 17:13:31 +02:00
Lukáš Kucharczyk 3abd4c4af9
reuse existing variable 2024-08-09 13:59:14 +02:00
Lukáš Kucharczyk 2e5e77b4e5
replace navbar
Django CI/CD / test (push) Successful in 1m12s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-09 13:14:18 +02:00
Lukáš Kucharczyk e79cf5de7a
fix non-working views 2024-08-09 13:12:47 +02:00
Lukáš Kucharczyk c15eaca205
only overflow table, not paginator, improve styling
Django CI/CD / test (push) Successful in 1m5s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-09 12:42:54 +02:00
Lukáš Kucharczyk 496c99ccf1
formatting 2024-08-09 12:23:49 +02:00
Lukáš Kucharczyk 992622e8d1
make it possible to not use paginator when limit = 0 2024-08-09 12:23:40 +02:00
Lukáš Kucharczyk cabe36c822
add dark/light mode toggle 2024-08-09 12:22:26 +02:00
Lukáš Kucharczyk d84b67c460
improve pagination 2024-08-09 11:47:10 +02:00
Lukáš Kucharczyk 1c28950b53
add pagination
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-08 22:54:15 +02:00
Lukáš Kucharczyk b54bcdd9e9
remove cruft 2024-08-08 21:20:17 +02:00
Lukáš Kucharczyk 9ec6c958c8
remove unnecessary styles 2024-08-08 21:20:08 +02:00
Lukáš Kucharczyk 25deac6ea9
add more types 2024-08-08 21:19:43 +02:00
Lukáš Kucharczyk a5ac10b20d
use model variables for foreign keys where possible 2024-08-08 20:22:25 +02:00
Lukáš Kucharczyk 3de40ccad3
create purchase list without paging 2024-08-08 20:17:43 +02:00
Lukáš Kucharczyk 6a5dc9b62c
even more formatting 2024-08-08 15:08:50 +02:00
Lukáš Kucharczyk b6014a72e0
.gitignore: add .direnv 2024-08-08 14:49:09 +02:00
Lukáš Kucharczyk 245b47b8b3
improve shell.nix
do not let poetry manage venvs
no need to override python3
2024-08-08 14:48:58 +02:00
Lukáš Kucharczyk e33f23c18f
add .envrc 2024-08-08 14:48:20 +02:00
Lukáš Kucharczyk 33012bc328
vscode: add extensions and settings 2024-08-08 14:48:10 +02:00
Lukáš Kucharczyk 447bd4820c
reformat with djlint --reformat 2024-08-08 14:47:51 +02:00
Lukáš Kucharczyk 72e89dae77
remove cruft
Django CI/CD / test (push) Successful in 1m7s Details
Django CI/CD / build-and-push (push) Successful in 2m0s Details
2024-08-08 09:47:06 +02:00
Lukáš Kucharczyk 1cd0a8c0fb
add shell.nix 2024-08-08 09:27:51 +02:00
Lukáš Kucharczyk a9a430f856
change vscode settings 2024-08-08 09:27:36 +02:00
Lukáš Kucharczyk 0ee4c50a24
update dependencies
Django CI/CD / test (push) Successful in 1m5s Details
Django CI/CD / build-and-push (push) Successful in 2m1s Details
2024-08-08 09:17:09 +02:00
Lukáš Kucharczyk 714f0d97a9
Reformat
Django CI/CD / test (push) Successful in 1m0s Details
Django CI/CD / build-and-push (push) Successful in 2m10s Details
2024-08-04 22:40:43 +02:00
Lukáš Kucharczyk d622ddfbf3
Add all-time stats 2024-08-04 22:40:37 +02:00
Lukáš Kucharczyk 86fd40cc4a
Do not save non-durations as manual
Django CI/CD / test (push) Successful in 1m56s Details
Django CI/CD / build-and-push (push) Successful in 2m23s Details
2024-07-23 09:51:15 +02:00
Lukáš Kucharczyk e174850262
Update deps
Django CI/CD / test (push) Successful in 1m3s Details
Django CI/CD / build-and-push (push) Successful in 1m56s Details
2024-07-11 13:28:09 +02:00
Lukáš Kucharczyk 6328d835ee
Fix formatting 2024-07-09 23:04:14 +02:00
Lukáš Kucharczyk 34d42e2af5
Fix list session links
Django CI/CD / test (push) Successful in 1m2s Details
Django CI/CD / build-and-push (push) Successful in 2m1s Details
2024-07-09 23:03:52 +02:00
Lukáš Kucharczyk e19caf47bf
Make game overview more appealing
Django CI/CD / build-and-push (push) Blocked by required conditions Details
Django CI/CD / test (push) Has been cancelled Details
2024-07-09 23:03:03 +02:00
Lukáš Kucharczyk 72998ffc02
Fix incorrect font name 2024-07-09 20:38:03 +02:00
Lukáš Kucharczyk ba44814474
Improve game links
Django CI/CD / test (push) Successful in 1m6s Details
Django CI/CD / build-and-push (push) Successful in 1m56s Details
2024-07-09 19:40:47 +02:00
Lukáš Kucharczyk 86f8fde8fa
Avoid errors when displaying game overview with zero sessions
Django CI/CD / test (push) Successful in 1m4s Details
Django CI/CD / build-and-push (push) Successful in 2m10s Details
2024-07-09 07:32:49 +02:00
Lukáš Kucharczyk 811fec4b11
Ignore manual sessions when calculating session average
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Successful in 1m58s Details
2024-07-02 17:27:44 +02:00
Lukáš Kucharczyk fe6cf2758c
make dev does not ignore warnings
Django CI/CD / test (push) Successful in 1m9s Details
Django CI/CD / build-and-push (push) Successful in 1m55s Details
2024-06-26 18:35:05 +02:00
Lukáš Kucharczyk 1e1372ca56
Update Python deps 2024-06-26 18:34:38 +02:00
Lukáš Kucharczyk d91c0bc255
Update npm deps
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Successful in 1m57s Details
2024-06-26 17:39:53 +02:00
Lukáš Kucharczyk a14f5d3ae5
Add npm-check-updates 2024-06-26 17:39:39 +02:00
Lukáš Kucharczyk 4ac13053d5
Use new Poetry section for main deps 2024-06-26 17:31:43 +02:00
Lukáš Kucharczyk e9311225e7
Make setting up and developing easier 2024-06-26 17:18:58 +02:00
Lukáš Kucharczyk 44c70a5ee7
Formatting
Django CI/CD / test (push) Successful in 1m21s Details
Django CI/CD / build-and-push (push) Successful in 1m57s Details
2024-06-03 18:19:11 +02:00
Lukáš Kucharczyk cd804f2c77
Sort url paths 2024-06-03 18:18:58 +02:00
Lukáš Kucharczyk 15997bd5af
Re-enable delete session delete view 2024-06-03 18:07:10 +02:00
Lukáš Kucharczyk 880ea93424
Unify url path names 2024-06-03 18:05:34 +02:00
Lukáš Kucharczyk dc1a9d5c4f
Make sure attribute chains are evaluated safely 2024-05-30 14:26:38 +02:00
Lukáš Kucharczyk 51c25659a9
djhtml formatting
Django CI/CD / test (push) Successful in 1m5s Details
Django CI/CD / build-and-push (push) Successful in 1m57s Details
2024-04-30 12:04:16 +02:00
Lukáš Kucharczyk 973dda59d2
Improve game overview header 2024-04-30 12:03:52 +02:00
Lukáš Kucharczyk 64edca9ffa
Use display name in session list
Django CI/CD / test (push) Successful in 52s Details
Django CI/CD / build-and-push (push) Successful in 1m52s Details
2024-04-29 19:21:05 +02:00
Lukáš Kucharczyk 86e25b84ab
Allow deleting purchases
Django CI/CD / test (push) Successful in 53s Details
Django CI/CD / build-and-push (push) Successful in 1m47s Details
2024-04-29 16:35:54 +02:00
Lukáš Kucharczyk edc1d062bc
Update gunicorn to version 22.0.0
Django CI/CD / test (push) Successful in 1m46s Details
Django CI/CD / build-and-push (push) Successful in 2m28s Details
2024-04-17 12:28:10 +02:00
Lukáš Kucharczyk 12a517c9fa
Update sqlparse to version 0.5
Django CI/CD / test (push) Successful in 1m2s Details
Django CI/CD / build-and-push (push) Successful in 1m58s Details
2024-04-16 11:44:24 +02:00
Lukáš Kucharczyk c1882f66e3
Improve purchase name consistency on stats page
Django CI/CD / test (push) Successful in 1m0s Details
Django CI/CD / build-and-push (push) Successful in 2m0s Details
2024-04-15 13:55:17 +02:00
Lukáš Kucharczyk 1e87e67eb1
Reformat HTML with djhtml
Django CI/CD / test (push) Successful in 1m4s Details
Django CI/CD / build-and-push (push) Successful in 1m36s Details
2024-04-04 11:27:33 +02:00
Lukáš Kucharczyk 84552e088b
Update more dependencies 2024-04-04 11:27:14 +02:00
Lukáš Kucharczyk 79dc8ae25c
Update black
Django CI/CD / test (push) Failing after 44s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-04-04 11:09:09 +02:00
Lukáš Kucharczyk cee06e4f64
Update dependencies
Django CI/CD / test (push) Successful in 47s Details
Django CI/CD / build-and-push (push) Successful in 1m47s Details
2024-04-04 10:46:59 +02:00
Lukáš Kucharczyk d9b5f0eab2
stats: add monthly playtimes
Django CI/CD / test (push) Successful in 58s Details
Django CI/CD / build-and-push (push) Successful in 1m54s Details
2024-04-02 08:18:58 +02:00
Lukáš Kucharczyk ff28600710
Fix timestamp minutes on game page
Django CI/CD / test (push) Successful in 56s Details
Django CI/CD / build-and-push (push) Successful in 2m2s Details
Fixed #72
2024-03-27 14:38:00 +01:00
Lukáš Kucharczyk 7517bf5f37
Add stats for dropped purchases
Django CI/CD / test (push) Successful in 1m2s Details
Django CI/CD / build-and-push (push) Successful in 1m57s Details
2024-03-10 22:48:46 +01:00
Lukáš Kucharczyk 780a04d13f
Do not edit sort_name invisibly
Django CI/CD / test (push) Successful in 1m0s Details
Django CI/CD / build-and-push (push) Successful in 2m0s Details
Fixes #64
2024-03-04 16:50:37 +01:00
Lukáš Kucharczyk fd04e9fa77
Sort prefetch instead of the result
Django CI/CD / test (push) Successful in 57s Details
Django CI/CD / build-and-push (push) Successful in 1m52s Details
order_by on the final queryset results in duplicating editions, 1 for each purchase
to fix it we sort the thing we actually want to sort - non-game purchases - in a prefetch earlier in the code
2024-02-18 12:31:03 +01:00
Lukáš Kucharczyk 18902aedac
Reformat
Django CI/CD / test (push) Successful in 56s Details
Django CI/CD / build-and-push (push) Successful in 1m53s Details
2024-02-18 09:03:35 +01:00
Lukáš Kucharczyk f9e37e9b1e
Sort purchases also by date purchased
Django CI/CD / test (push) Successful in 1m4s Details
Django CI/CD / build-and-push (push) Has been cancelled Details
2024-02-18 09:02:08 +01:00
Lukáš Kucharczyk c747cd1fd8
Reformat
Django CI/CD / test (push) Successful in 55s Details
Django CI/CD / build-and-push (push) Successful in 1m33s Details
2024-02-10 09:50:53 +01:00
Lukáš Kucharczyk 6a5457191a
Add logout button 2024-02-10 09:48:09 +01:00
Lukáš Kucharczyk 76f6d0c377
Fix CSS bug 2024-02-10 09:03:16 +01:00
Lukáš Kucharczyk ae93703c08
Remove login_required from clone_session_by_id
Django CI/CD / test (push) Successful in 53s Details
Django CI/CD / build-and-push (push) Successful in 1m35s Details
2024-02-09 22:27:28 +01:00
Lukáš Kucharczyk c55176090c
Temporarily disable tests
Django CI/CD / test (push) Successful in 52s Details
Django CI/CD / build-and-push (push) Successful in 1m41s Details
2024-02-09 22:08:49 +01:00
Lukáš Kucharczyk 081b8a92de
Require login by default
Django CI/CD / test (push) Failing after 1m1s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-02-09 22:03:24 +01:00
Lukáš Kucharczyk d02a60675f
Render notes as Markdown
Django CI/CD / test (push) Failing after 1m5s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-02-09 21:37:39 +01:00
Lukáš Kucharczyk 4670568acb
Add .DS_Store to .gitignore
Django CI/CD / test (push) Successful in 1m4s Details
Django CI/CD / build-and-push (push) Successful in 1m34s Details
2024-01-15 22:09:29 +01:00
Lukáš Kucharczyk 4b75a1dea9
Increase session count on game overview when starting a new session
Django CI/CD / test (push) Successful in 50s Details
Django CI/CD / build-and-push (push) Successful in 1m32s Details
2024-01-15 21:41:25 +01:00
Lukáš Kucharczyk e2b7ff2e15
Remove cruft
Django CI/CD / test (push) Successful in 53s Details
Django CI/CD / build-and-push (push) Successful in 1m30s Details
2024-01-15 19:17:27 +01:00
Lukáš Kucharczyk b94aa49fc3
Fix title not being displayed on the Recent sessions page 2024-01-15 19:17:24 +01:00
Lukáš Kucharczyk 73a92e5636
Mark refunded purchases red
Django CI/CD / test (push) Successful in 1m3s Details
Django CI/CD / build-and-push (push) Successful in 1m36s Details
2024-01-15 11:19:18 +01:00
Lukáš Kucharczyk 42b28665e1
Version 1.5.2
Django CI/CD / test (push) Successful in 1m15s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-01-14 21:28:38 +01:00
Lukáš Kucharczyk 6ba187f8e4
Make it possible to end session from game overview 2024-01-14 21:27:18 +01:00
Lukáš Kucharczyk a765fd8d00
More game overview optimizations
Django CI/CD / test (push) Successful in 1m15s Details
Django CI/CD / build-and-push (push) Successful in 1m37s Details
2024-01-14 17:04:06 +01:00
Lukáš Kucharczyk 854e3cc54a
Do not copy notes when cloning session
Django CI/CD / test (push) Successful in 1m13s Details
Django CI/CD / build-and-push (push) Successful in 1m45s Details
2024-01-14 13:05:45 +01:00
Lukáš Kucharczyk 2d8eb32e90
Remove cruft
Django CI/CD / test (push) Successful in 1m3s Details
Django CI/CD / build-and-push (push) Successful in 1m29s Details
2024-01-10 17:13:59 +01:00
Lukáš Kucharczyk 1f1ed79ee5
Optimize session listing 2024-01-10 16:57:01 +01:00
Lukáš Kucharczyk 01fd7bad69
Remove cruft 2024-01-10 15:55:08 +01:00
Lukáš Kucharczyk 44f49e5974
Session list: speed up starting new sessions
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Successful in 1m33s Details
2024-01-10 15:54:09 +01:00
Lukáš Kucharczyk 0cf3411f63
Make ending session from session list faster
Django CI/CD / test (push) Successful in 1m16s Details
Django CI/CD / build-and-push (push) Successful in 1m40s Details
2024-01-10 15:12:45 +01:00
Lukáš Kucharczyk aa669710e1
Change update_session to template partial
Django CI/CD / test (push) Failing after 1m3s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-01-10 14:10:13 +01:00
Lukáš Kucharczyk 242833f886
Make it possible to drop purchases, or consider them infinite
Django CI/CD / build-and-push (push) Blocked by required conditions Details
Django CI/CD / test (push) Has been cancelled Details
2024-01-03 22:35:39 +01:00
Lukáš Kucharczyk 0cdfd3c298
Stats: optimize
Django CI/CD / test (push) Successful in 56s Details
Django CI/CD / build-and-push (push) Successful in 1m33s Details
2024-01-03 21:35:47 +01:00
Lukáš Kucharczyk a98b4839dd
Fix wrong unfinished purchases calculation
Django CI/CD / test (push) Successful in 1m10s Details
Django CI/CD / build-and-push (push) Successful in 1m42s Details
2024-01-02 20:03:59 +01:00
Lukáš Kucharczyk 1999f13cf2
stats: add first and last play
Django CI/CD / test (push) Successful in 1m12s Details
Django CI/CD / build-and-push (push) Successful in 1m36s Details
2024-01-01 18:42:14 +01:00
Lukáš Kucharczyk 8466f67c86
Fix errors caused by empty values
Django CI/CD / test (push) Successful in 1m10s Details
Django CI/CD / build-and-push (push) Successful in 1m41s Details
2024-01-01 18:21:50 +01:00
Lukáš Kucharczyk d9fbb4b896
Add title to stats page
Django CI/CD / build-and-push (push) Blocked by required conditions Details
Django CI/CD / test (push) Has been cancelled Details
2023-12-15 10:58:15 +01:00
Lukáš Kucharczyk 4ff3692606
Remove duplicate block
Django CI/CD / test (push) Successful in 1m34s Details
Django CI/CD / build-and-push (push) Successful in 1m29s Details
2023-11-30 18:05:52 +01:00
Lukáš Kucharczyk 8289c48896
Fix CI error
Django CI/CD / test (push) Successful in 1m14s Details
Django CI/CD / build-and-push (push) Successful in 1m24s Details
2023-11-30 17:44:31 +01:00
Lukáš Kucharczyk d1b9202337
Update poetry.lock
Django CI/CD / test (push) Failing after 1m15s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk fde93cb875
Organize better 2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk d1c3ac6079
Revert "Move GraphQL to separata app"
This reverts commit 6ac4209492.
2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk d921c2d8a6
Revert "Add UpdateGameMutation"
This reverts commit e9e61403a9.
2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk 52513e1ed8
Add UpdateGameMutation 2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk cb380814a7
Move GraphQL to separata app 2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk 5ef8c07f30
Initial working API 2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk 9573c3b8ff
Better formatting
Django CI/CD / test (push) Successful in 1m11s Details
Django CI/CD / build-and-push (push) Successful in 1m22s Details
2023-11-29 22:26:43 +01:00
Lukáš Kucharczyk c4354a1380
Update poetry.lock
Django CI/CD / test (push) Successful in 1m3s Details
Django CI/CD / build-and-push (push) Successful in 1m14s Details
2023-11-29 22:22:23 +01:00
Lukáš Kucharczyk a245b6ff0f
Fix longest session formatting
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Successful in 1m24s Details
Put space between hours and minutes
2023-11-29 09:08:10 +01:00
Lukáš Kucharczyk 6329d380b7
Editions are unique if name, platform OR year is different
Django CI/CD / test (push) Successful in 1m25s Details
Django CI/CD / build-and-push (push) Successful in 1m20s Details
2023-11-28 14:44:11 +01:00
Lukáš Kucharczyk 76fbc39fed
Disable hx-boost
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Successful in 1m17s Details
2023-11-28 14:29:56 +01:00
Lukáš Kucharczyk 4b6734c173
Add width, height, alt to images 2023-11-28 14:29:11 +01:00
Lukáš Kucharczyk b505b5b430
Stats: add highest session average
Django CI/CD / test (push) Successful in 1m2s Details
Django CI/CD / build-and-push (push) Successful in 1m23s Details
2023-11-21 21:57:17 +01:00
Lukáš Kucharczyk 87553ebdc5
Add djlint pre-commit hook
Django CI/CD / test (push) Successful in 1m13s Details
Django CI/CD / build-and-push (push) Successful in 1m15s Details
2023-11-21 18:19:25 +01:00
Lukáš Kucharczyk ba4fc0cac5
Do not trigger hx-boost for non-submit buttons
Django CI/CD / test (push) Successful in 1m7s Details
Django CI/CD / build-and-push (push) Successful in 1m20s Details
2023-11-21 18:12:58 +01:00
Lukáš Kucharczyk 8cb0276215
Use better way to find out if model record exists 2023-11-21 18:03:01 +01:00
Lukáš Kucharczyk f9a51ee83d
Remove experimental layout 2023-11-21 18:03:01 +01:00
Lukáš Kucharczyk c9deba7d65
Add stats for most sessions, longest session 2023-11-21 17:02:44 +01:00
Lukáš Kucharczyk c55fbe86b5
Support HTMX with Django Debug Toolbar 2023-11-21 16:59:21 +01:00
Lukáš Kucharczyk 0e93993498
Add django-debug-toolbar 2023-11-21 14:42:37 +01:00
Lukáš Kucharczyk 9fccdfbff0
Make links colorful 2023-11-20 23:07:11 +01:00
Lukáš Kucharczyk d78139a5b3
Display finished DLCs in stats better
Django CI/CD / test (push) Successful in 1m6s Details
Django CI/CD / build-and-push (push) Successful in 1m26s Details
2023-11-20 21:56:16 +01:00
Lukáš Kucharczyk 7dc43fbf77
Fix wrong export name 2023-11-20 21:54:51 +01:00
Lukáš Kucharczyk 5442926457
Allow DLC to have date_finished set
Django CI/CD / test (push) Successful in 1m6s Details
Django CI/CD / build-and-push (push) Successful in 1m35s Details
2023-11-20 21:42:23 +01:00
Lukáš Kucharczyk db4c635260
Remote JavaScript files 2023-11-20 21:25:21 +01:00
Lukáš Kucharczyk 4a1d08d4df
Fix CI, re-add test step
Django CI/CD / test (push) Successful in 1m0s Details
Django CI/CD / build-and-push (push) Successful in 1m45s Details
2023-11-18 11:10:33 +01:00
Lukáš Kucharczyk c35b539c42 Merge sessions and notes
Django CI/CD / build-and-push (push) Successful in 1m9s Details
2023-11-17 21:20:33 +01:00
Lukáš Kucharczyk bbe5e072b2 Don't display prices if zero 2023-11-17 21:10:56 +01:00
Lukáš Kucharczyk 6fc2f623dc Apply djlint 2023-11-17 21:06:57 +01:00
Lukáš Kucharczyk 9481bd5fef Add pre-commit
Django CI/CD / build-and-push (push) Successful in 1m33s Details
2023-11-17 09:34:51 +01:00
Lukáš Kucharczyk 4083165123 Use the black profile for isort 2023-11-17 09:15:18 +01:00
Lukáš Kucharczyk 45bb2681c7 Use isort on migrations 2023-11-17 09:15:06 +01:00
Lukáš Kucharczyk dbb8ec3f9a Handle empty edition_id 2023-11-17 09:14:25 +01:00
Lukáš Kucharczyk 206b5f6d46 Prevent HTMX from messing up the initial state
Django CI/CD / build-and-push (push) Successful in 1m15s Details
2023-11-16 20:33:56 +01:00
Lukáš Kucharczyk b7e14ecc83 Account for no sessions
Django CI/CD / build-and-push (push) Successful in 1m21s Details
2023-11-16 20:29:08 +01:00
Lukáš Kucharczyk 912e010729 Enable hx-boost everywhere
Django CI/CD / build-and-push (push) Successful in 1m18s Details
2023-11-16 19:56:08 +01:00
Lukáš Kucharczyk a485237456 Fix form not syncing due to HTMX
Django CI/CD / build-and-push (push) Successful in 2m38s Details
2023-11-16 19:03:16 +01:00
Lukáš Kucharczyk f5faf92ee0 Fix error
Django CI/CD / build-and-push (push) Successful in 1m57s Details
2023-11-16 16:53:59 +01:00
Lukáš Kucharczyk 07452d8c43 Re-instance gitea actions
Django CI/CD / test (push) Failing after 34s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2023-11-16 16:51:52 +01:00
Lukáš Kucharczyk 229a79d266 Update .drone.yml testing
continuous-integration/drone/push Build is failing Details
2023-11-16 16:30:17 +01:00
Lukáš Kucharczyk c6ed577fe3 Formatting
continuous-integration/drone/push Build is failing Details
2023-11-16 16:27:41 +01:00
Lukáš Kucharczyk 171e4779a3 Move static files in prod 2023-11-16 16:27:41 +01:00
Lukáš Kucharczyk 79f94e5984 Fix docker-compose.yml 2023-11-16 16:27:41 +01:00
Lukáš Kucharczyk ccebcb89c6 Improve Dockerfile
Major inspiration (aka direct theft) from https://github.com/wemake-services/wemake-django-template
2023-11-16 16:27:41 +01:00
Lukáš Kucharczyk fe0a6b39e3 Fix .dockerignore 2023-11-16 16:27:41 +01:00
Lukáš Kucharczyk 6a495f951f Remove Django admin 2023-11-16 16:27:41 +01:00
Lukáš Kucharczyk c8646d0a0c Update dependencies 2023-11-16 16:27:41 +01:00
Lukáš Kucharczyk f2bb15e669 Fix naive date 2023-11-16 16:27:41 +01:00
Lukáš Kucharczyk c49177d63c isort 2023-11-16 16:27:41 +01:00
Lukáš Kucharczyk bd8d30eac1 Improve time-related stuff
Add created_at to all models
Add modified_at to Session
Get rid of custom now() function
Make sure aware datetime is used everywhere
2023-11-16 16:27:41 +01:00
Lukáš Kucharczyk c44d8bf427 Improve time-related stuff
continuous-integration/drone/push Build is passing Details
Add created_at to all models
Add modified_at to Session
Get rid of custom now() function
Make sure aware datetime is used everywhere
2023-11-15 19:14:09 +01:00
Lukáš Kucharczyk 3f037b4c7c Only allow choosing purchases of selected edition
continuous-integration/drone/push Build is passing Details
2023-11-15 14:25:42 +01:00
Lukáš Kucharczyk 8783d1fc8e Name and related_purchase validation for non-games 2023-11-15 13:04:47 +01:00
Lukáš Kucharczyk 9a1d24dbfd Sort imports, remove cruft 2023-11-15 12:19:31 +01:00
Lukáš Kucharczyk 4720660cff Fix wrong playrange ordering 2023-11-15 10:40:52 +01:00
Lukáš Kucharczyk e158bc0623 Improve how editions and purchases are displayed
continuous-integration/drone/push Build is passing Details
2023-11-15 10:37:24 +01:00
Lukáš Kucharczyk 8982fc5086 Game View: order editions by year 2023-11-14 21:19:36 +01:00
Lukáš Kucharczyk 729e1d939b Version 1.5.1
continuous-integration/drone/push Build encountered an error Details
2023-11-14 21:10:42 +01:00
Lukáš Kucharczyk 2b4683e489 Improve and cleanup ConditionalElementHandler 2023-11-14 21:09:43 +01:00
Lukáš Kucharczyk cce810e8cf Improve purchase __str__ 2023-11-14 19:55:56 +01:00
Lukáš Kucharczyk 62cd17f702 Disallow choosing non-game purchase as related purchase 2023-11-14 19:55:19 +01:00
Lukáš Kucharczyk f31280c682 Version 1.5.0
continuous-integration/drone/push Build is passing Details
2023-11-14 19:31:17 +01:00
Lukáš Kucharczyk a745d16ec3 Order purchases by date on game view 2023-11-14 19:30:19 +01:00
Lukáš Kucharczyk ae079e36ec Add purchase types 2023-11-14 19:27:00 +01:00
Lukáš Kucharczyk c8a3212b77 CI: run migrations before tests
continuous-integration/drone/push Build was killed Details
2023-11-12 08:11:22 +01:00
Lukáš Kucharczyk d211326c3f Make sure empty stats are 0
continuous-integration/drone/push Build is failing Details
2023-11-12 08:01:12 +01:00
Lukáš Kucharczyk 270a291f05 Change stats years to 2000 up to current year 2023-11-12 07:50:12 +01:00
Lukáš Kucharczyk 13b750ca92 Add stat for finished this year's games 2023-11-12 07:40:29 +01:00
Lukáš Kucharczyk 015b6db2f7 Fix detecting manual durations
continuous-integration/drone/push Build is failing Details
2023-11-11 15:02:28 +01:00
Lukáš Kucharczyk 667b161fff Remove deprecated USE_L10N
continuous-integration/drone/push Build is failing Details
2023-11-10 21:37:13 +01:00
Lukáš Kucharczyk 5958cbf4a6 Add more tests 2023-11-10 21:34:36 +01:00
Lukáš Kucharczyk 3b37f2c3f0 Fix edge case in format_duration
continuous-integration/drone/push Build is passing Details
Fixes #65

```python
def test_specific_precise_if_unncessary(self):
        delta = timedelta(hours=2, minutes=40)
        result = format_duration(delta, "%02.0H:%02.0m")
        self.assertEqual(result, "02:40")
```
This test fails by returning "03:40" instead. The problem is in the way `format_duration` handles fractional hours.
To fix it, we need to switch between using hours and fractional hours
depending on if minutes are present in the formatted string.
2023-11-10 20:07:41 +01:00
Lukáš Kucharczyk 4517ff2b5a Fix ordering
continuous-integration/drone/push Build is passing Details
2023-11-09 21:43:17 +01:00
Lukáš Kucharczyk 884ce13e26 Also prefill year between game and edition
continuous-integration/drone/push Build is passing Details
2023-11-09 21:20:12 +01:00
Lukáš Kucharczyk dd219bae9d Version 1.4.0
continuous-integration/drone/push Build is passing Details
2023-11-09 21:11:43 +01:00
Lukáš Kucharczyk 60d29090a1 Adding new games is easier 2023-11-09 21:11:28 +01:00
Lukáš Kucharczyk 1bc3ca057b Refactor, remove cruft 2023-11-09 19:35:57 +01:00
Lukáš Kucharczyk c2c0886451 Add backlog decrease count 2023-11-09 19:15:49 +01:00
Lukáš Kucharczyk b0be7b5887 Fix sort names getting mangled
continuous-integration/drone/push Build is passing Details
2023-11-09 15:41:46 +01:00
Lukáš Kucharczyk 099d989f16 Pre-fill year when adding edition 2023-11-09 15:20:30 +01:00
Lukáš Kucharczyk a879360ebd UX improvements
continuous-integration/drone/push Build is passing Details
* ignore English articles when sorting names
  * added a new sort_name field that gets automatically created
* automatically fill certain values in forms:
  * new game: name and sort name after typing
  * new edition: name and sort name when selecting game
  * new purchase: platform when selecting edition
2023-11-09 14:49:00 +01:00
Lukáš Kucharczyk 866f2526e6 Fix hardcoded year 2023-11-09 10:10:44 +01:00
Lukáš Kucharczyk ce3c4b55f0 Order devices alphabetically on new session form
continuous-integration/drone/push Build is passing Details
2023-11-09 10:09:32 +01:00
Lukáš Kucharczyk c52cd822ae Use safe_division in more places 2023-11-09 10:06:14 +01:00
Lukáš Kucharczyk cdc6ca1324 Fix potential division by zero
continuous-integration/drone/push Build is passing Details
2023-11-09 09:18:49 +01:00
Lukáš Kucharczyk e7ed349356 Add more stats
continuous-integration/drone/push Build is passing Details
* Finished (count)
* Unfinished (count)
* Refunded (count)
2023-11-08 18:13:48 +01:00
Lukáš Kucharczyk 5052ca7dbf Add more stats
continuous-integration/drone/push Build is passing Details
* All finished games
* All finished 2023 games
* All finished games that were purchased this year
* Total sessions
* Days played
2023-11-08 16:24:22 +01:00
Lukáš Kucharczyk f408bfd927 stats: change overall stats table layout 2023-11-08 15:48:06 +01:00
Lukáš Kucharczyk 666dee33ba Model changes
continuous-integration/drone/push Build is passing Details
* More fields are now optional. This is to make it easier to add new items in bulk.
  * Game: Wikidata ID
  * Edition: Platform, Year
  * Purchase: Platform
  * Platform: Group
  * Session: Device
* New fields:
  * Game: Year Released
    * To record original year of release
    * Upon migration, this will be set to a year of any of the game's edition that has it set
  * Purchase: Date Finished
* Editions are now unique combination of name and platform
2023-11-06 19:48:12 +01:00
Lukáš Kucharczyk e0b09e051a simplify playtime range display 2023-11-06 12:05:39 +01:00
Lukáš Kucharczyk 4552cf7616 Version 1.3.0
continuous-integration/drone/push Build is passing Details
2023-11-05 15:10:56 +01:00
Lukáš Kucharczyk a614b51d29 Make some pages redirect back instead to session list 2023-11-05 15:09:51 +01:00
Lukáš Kucharczyk e67aa3fda1 Add more stats
continuous-integration/drone/push Build is passing Details
2023-11-02 20:12:32 +01:00
Lukáš Kucharczyk 8423fd02b4 Extend stats range to 2018
continuous-integration/drone/push Build is passing Details
2023-11-02 15:32:57 +01:00
Lukáš Kucharczyk 2bd07e5f2d Remove cruft
continuous-integration/drone/push Build is passing Details
2023-11-02 15:14:57 +01:00
Lukáš Kucharczyk 058b83522c Group by game instead of purchase 2023-11-02 15:14:50 +01:00
Lukáš Kucharczyk f13ed8a078 Reorder imports in views.py
continuous-integration/drone/push Build is passing Details
2023-11-02 09:53:28 +01:00
Lukáš Kucharczyk 02d5adcb3c Remove hardcoded year 2023-11-02 09:52:59 +01:00
Lukáš Kucharczyk d6fb16bb74 Make navigation more compact 2023-11-02 09:52:42 +01:00
Lukáš Kucharczyk 71b90b8202 Add stats link, year selector 2023-11-02 09:20:09 +01:00
Lukáš Kucharczyk 3ee36932c3 Limit stats of single year correctly 2023-11-02 09:17:08 +01:00
Lukáš Kucharczyk 391fcc79a8 Version 1.2.0
continuous-integration/drone/push Build is passing Details
2023-11-01 20:35:58 +01:00
Lukáš Kucharczyk 57d4fd7212 Add yearly stats page
Fixes #15
2023-11-01 20:35:52 +01:00
Lukáš Kucharczyk a5b2854bf6 Add a button to start session from game overview
continuous-integration/drone/push Build is passing Details
Fix #62
2023-10-13 19:22:43 +02:00
Lukáš Kucharczyk 518c0ecd56 Add more time tests for fractional numbers
continuous-integration/drone/push Build is passing Details
2023-10-13 17:01:33 +02:00
Lukáš Kucharczyk a6cd7a3430 Do not format as float if no precision specified
continuous-integration/drone/push Build is passing Details
2023-10-13 16:58:12 +02:00
Lukáš Kucharczyk dba8414fd9 Version 1.1.2
continuous-integration/drone/push Build is failing Details
2023-10-13 16:33:55 +02:00
Lukáš Kucharczyk 0e2113eefd Display durations in a consistent manner
Fixes #61
2023-10-13 16:32:12 +02:00
Lukáš Kucharczyk c4b0347f3b Version 1.1.1
continuous-integration/drone/push Build is passing Details
2023-10-09 20:56:23 +02:00
Lukáš Kucharczyk c6ed21167c Remove debugging cruft from container 2023-10-09 20:56:13 +02:00
Lukáš Kucharczyk 4ce15c44fc Add edit buttons to game overview, notes 2023-10-09 20:55:31 +02:00
Lukáš Kucharczyk c814b4c2cb Version 1.1.0
continuous-integration/drone/push Build is passing Details
2023-10-09 00:04:46 +02:00
Lukáš Kucharczyk 11b9c602de Improve game overview
- add counts for each section
- add average hours per session
2023-10-09 00:00:45 +02:00
Lukáš Kucharczyk 9a332593f4 Fix playtime range not working with manual session
continuous-integration/drone/push Build is passing Details
Fixes #55
2023-10-08 21:13:32 +02:00
Lukáš Kucharczyk 22935721ca Remove unused chart functionality
continuous-integration/drone/push Build is passing Details
2023-10-08 21:08:03 +02:00
Lukáš Kucharczyk a2ecdcf44a ci: automatically redeploy container
continuous-integration/drone/push Build was killed Details
2023-10-04 20:21:00 +02:00
Lukáš Kucharczyk 3c958c4a13 Organize changelog better
continuous-integration/drone/push Build is passing Details
2023-10-04 18:14:21 +02:00
Lukáš Kucharczyk 3db1724e22 Fix session being wrongly considered in progress
continuous-integration/drone/push Build is passing Details
Fixes #58
2023-10-04 18:12:13 +02:00
Lukáš Kucharczyk d2a9630b04 Fix date range on game overview
continuous-integration/drone/push Build is passing Details
2023-10-01 21:51:32 +02:00
Lukáš Kucharczyk e3ee832d3f Adjust game edit URL
continuous-integration/drone/push Build is passing Details
2023-10-01 21:28:20 +02:00
Lukáš Kucharczyk 7467e2732d Add game overview page
Fixes #8
2023-10-01 21:28:02 +02:00
Lukáš Kucharczyk 787ee8640f Try to shutdown container gracefully and faster
continuous-integration/drone/push Build is passing Details
2023-10-01 19:57:15 +02:00
Lukáš Kucharczyk ab41222f3c Switch fonts to WOFF2 2023-10-01 19:53:07 +02:00
Lukáš Kucharczyk 29bf3b1946 Further improve session list
continuous-integration/drone/push Build is passing Details
2023-09-30 19:44:35 +02:00
Lukáš Kucharczyk 3f7ccea2e2 fix tailwind config to only scan relevant files 2023-09-30 18:13:50 +02:00
Lukáš Kucharczyk b5ffb3586b Update base.css 2023-09-30 16:25:19 +02:00
Lukáš Kucharczyk 26d57a238e Update .vscode 2023-09-30 16:25:05 +02:00
Lukáš Kucharczyk 2d5ad3182c Update CSS 2023-09-30 16:24:54 +02:00
Lukáš Kucharczyk 49cc3ea0cc Improve session list
continuous-integration/drone/push Build is passing Details
Fixes #53
2023-09-30 15:55:38 +02:00
Lukáš Kucharczyk 440e1cfb71 Focus important fields on forms
continuous-integration/drone/push Build is passing Details
2023-09-30 12:40:02 +02:00
Lukáš Kucharczyk 1cbd8c5c55 Update generated CSS
continuous-integration/drone/push Build is passing Details
2023-09-20 19:06:04 +02:00
Lukáš Kucharczyk bc81a0ee8e Add hacky way to not reload page when starting/ending session
continuous-integration/drone/push Build is passing Details
Partially fixes #52
2023-09-20 18:54:54 +02:00
Lukáš Kucharczyk c5653977ff Add time copy button, improve session editing
continuous-integration/drone/push Build is passing Details
2023-09-18 20:21:05 +02:00
Lukáš Kucharczyk f151730ab6 Fix bug when filtering only manual sessions (#51)
continuous-integration/drone/push Build is passing Details
2023-09-18 19:37:54 +02:00
Lukáš Kucharczyk f469a67d94 add robots.txt
continuous-integration/drone/push Build is passing Details
2023-09-17 17:58:22 +02:00
Lukáš Kucharczyk 104ffc9d03 disable deleting sessions in code
continuous-integration/drone/push Build is passing Details
2023-09-17 17:17:22 +02:00
Lukáš Kucharczyk a4b13eb247 allow admin in prod 2/2
continuous-integration/drone/push Build is passing Details
2023-09-17 14:12:23 +02:00
Lukáš Kucharczyk 2307fac83a allow admin in prod
continuous-integration/drone/push Build is passing Details
2023-09-17 14:03:46 +02:00
Lukáš Kucharczyk 6b52c0d4c4 disable deleting sessions
continuous-integration/drone/push Build is passing Details
2023-09-17 13:28:15 +02:00
Lukáš Kucharczyk ff5d8c215d install dev dependecies
continuous-integration/drone/push Build is passing Details
2023-09-16 18:24:10 +02:00
Lukáš Kucharczyk cdb3b89b08 do not filter out refunded games when adding session
continuous-integration/drone/push Build is passing Details
2023-09-16 18:18:03 +02:00
Lukáš Kucharczyk ffa8198540 allow django admit 2023-09-16 18:17:36 +02:00
Lukáš Kucharczyk 0b7da3550c Change recent session view to current year
continuous-integration/drone/push Build is passing Details
2023-09-14 18:49:16 +02:00
Lukáš Kucharczyk e1655d6cfa input datetime-local needs day to also be two digits
continuous-integration/drone/push Build is passing Details
2023-03-02 21:33:07 +01:00
Lukáš Kucharczyk 29c41865d0 Exclude refunded purchases from start session form
continuous-integration/drone/push Build is passing Details
2023-03-01 22:07:49 +01:00
Lukáš Kucharczyk d21b461726 Update styles
continuous-integration/drone/push Build is passing Details
2023-02-21 23:51:07 +01:00
Lukáš Kucharczyk 95489cfb78 Add .editorconfig 2023-02-21 23:50:09 +01:00
Lukáš Kucharczyk fa4f1c4810 Improve forms, add helper buttons on add session form 2023-02-21 23:49:57 +01:00
Lukáš Kucharczyk 366c25a1ff Register Edition, Device in the admin UI 2023-02-20 22:01:20 +01:00
Lukáš Kucharczyk a3042caa20 Use date and datetime inputs
continuous-integration/drone/push Build is passing Details
Properly implements 4d91a76513
2023-02-20 21:33:15 +01:00
Lukáš Kucharczyk 7997f9bbb2 Sort games by name on edition form 2023-02-20 20:17:42 +01:00
Lukáš Kucharczyk b78c4ba9c5
Sync game name and edition name fields for QOL
continuous-integration/drone/push Build is passing Details
2023-02-20 18:21:48 +01:00
Lukáš Kucharczyk 1df889c45d
Fix gitignoring root static folder 2023-02-20 18:20:49 +01:00
Lukáš Kucharczyk 468d05a9e2
Sort games alphabetically on edition form 2023-02-20 17:37:14 +01:00
Lukáš Kucharczyk 2640a49734
Version 1.0.3
continuous-integration/drone/push Build is passing Details
2023-02-20 17:18:26 +01:00
Lukáš Kucharczyk 65c175afb2
Allow editing filtered entities from session list 2023-02-20 17:16:19 +01:00
Lukáš Kucharczyk 0814071a26
Improve display of editions on purchase form 2023-02-20 17:15:21 +01:00
Lukáš Kucharczyk 5f845f866e
Sort platforms alphabetically on edition form 2023-02-20 17:14:43 +01:00
Lukáš Kucharczyk c3d4697470
Add wikidata ID and year for editions 2023-02-20 17:13:35 +01:00
Lukáš Kucharczyk 77293f03e9 Add icons
continuous-integration/drone/push Build is passing Details
2023-02-19 17:20:57 +01:00
Lukáš Kucharczyk 1fa364e2ec Change timetracker icon 2023-02-19 16:35:04 +01:00
Lukáš Kucharczyk 4a6f4a2f9a Add icons for session filters
continuous-integration/drone/push Build is passing Details
2023-02-19 16:18:14 +01:00
Lukáš Kucharczyk 9590988b6a Fix error when generating charts with less than 2 entries
continuous-integration/drone/push Build is passing Details
2023-02-19 14:55:06 +01:00
Lukáš Kucharczyk 938c82a395 Allow filtering by game, edition, purchase from the session list 2023-02-19 14:36:12 +01:00
Lukáš Kucharczyk 33939f631c Fix form styling
continuous-integration/drone/push Build is passing Details
2023-02-18 22:36:26 +01:00
Lukáš Kucharczyk ac8cd6534a Version 1.0.2
continuous-integration/drone/push Build is passing Details
2023-02-18 21:48:55 +01:00
Lukáš Kucharczyk 51d8e953c0 Allow editing editions 2023-02-18 21:47:25 +01:00
Lukáš Kucharczyk 2eec677f41 Allow editing purchases 2023-02-18 21:44:19 +01:00
Lukáš Kucharczyk f2eb14d3ef Fix session starting 2023-02-18 21:43:51 +01:00
Lukáš Kucharczyk c337d2200f Fix ownership display
continuous-integration/drone/push Build is passing Details
2023-02-18 21:12:44 +01:00
Lukáš Kucharczyk 8a8b05b0bd Add support for device info
Closes #49
2023-02-18 21:12:18 +01:00
Lukáš Kucharczyk 9446065271 Add support for purchase ownership information
Closes #48
2023-02-18 20:57:03 +01:00
Lukáš Kucharczyk 755093845d Add support for prices on purchases 2023-02-18 20:56:23 +01:00
Lukáš Kucharczyk d4ab0596da Re-introduce index view
continuous-integration/drone/push Build is passing Details
2023-02-18 20:50:36 +01:00
Lukáš Kucharczyk 8dcbe2f0ad Fix displaying of "filterying by..." text 2023-02-18 20:50:19 +01:00
Lukáš Kucharczyk 25bc74eff1 Add support for game editions (#28) 2023-02-18 20:49:46 +01:00
Lukáš Kucharczyk 8a7d083fb2 Fix make css 2023-02-18 20:35:59 +01:00
Lukáš Kucharczyk 8296ebcf31
Fix redirect to index
continuous-integration/drone/push Build is passing Details
2023-02-06 17:23:44 +01:00
Lukáš Kucharczyk 04a4f2e0be Order homepage sessions from newest
continuous-integration/drone/push Build is passing Details
2023-01-31 16:37:44 +01:00
Lukáš Kucharczyk 4070b4e46e Version 1.0.1
continuous-integration/drone/push Build is passing Details
2023-01-30 22:17:47 +01:00
Lukáš Kucharczyk 4892218c83 Show only last 30 days on homepage
Fixes #47
2023-01-30 22:16:28 +01:00
Lukáš Kucharczyk 6b00a950ce Show markers on smaller graphs 2023-01-30 22:01:27 +01:00
Lukáš Kucharczyk feee9d6dac Make it possible to edit sessions
continuous-integration/drone/push Build is passing Details
Fixes #46
2023-01-30 17:38:44 +01:00
Lukáš Kucharczyk 9654fb017d
Separate Docker container by branch
continuous-integration/drone/push Build is passing Details
2023-01-25 11:36:34 +01:00
Lukáš Kucharczyk 1741397ee7 Prepare changelog
continuous-integration/drone/push Build is passing Details
2023-01-25 00:50:44 +01:00
Lukáš Kucharczyk da0c8d710b Ignore dist directory
continuous-integration/drone/push Build is passing Details
2023-01-22 11:02:26 +01:00
Lukáš Kucharczyk 215374167b Version 1.0.0
continuous-integration/drone/push Build is passing Details
2023-01-20 19:58:40 +01:00
Lukáš Kucharczyk 77268ae92f Add make loadall 2023-01-20 19:58:31 +01:00
Lukáš Kucharczyk c42687a072 Change ENTRYPOINT to CMD 2023-01-20 19:58:09 +01:00
Lukáš Kucharczyk ca16345374 Fix start session button starting different game than it says
Fixes #44
2023-01-20 19:57:45 +01:00
Lukáš Kucharczyk 3a3045be91 Sort form fields alphabetically
continuous-integration/drone/push Build is passing Details
Fixes #39
Fixes #40
2023-01-20 18:27:30 +01:00
Lukáš Kucharczyk d40612af72
Remove Caddy
continuous-integration/drone/push Build is passing Details
2023-01-20 17:15:53 +01:00
Lukáš Kucharczyk 18e8f93261
Additional fixes
continuous-integration/drone/push Build is passing Details
2023-01-20 15:06:42 +01:00
Lukáš Kucharczyk 56e5dfaa03 Rename project, part 2 (#42)
continuous-integration/drone/push Build is failing Details
Reviewed-on: #42
2023-01-20 13:37:46 +00:00
Lukáš Kucharczyk 2f00be455d Rename project (#41)
continuous-integration/drone/push Build is passing Details
The old naming scheme was causing confusion and probably errors.

Reviewed-on: #41
2023-01-19 19:35:25 +00:00
Lukáš Kucharczyk c3c9ae0632 Don't take time from .git
continuous-integration/drone/push Build is passing Details
.git is not included int the Dockerfile thus it cannot be used to get
time of last edit
2023-01-18 17:13:11 +01:00
Lukáš Kucharczyk 55c2693f32
Bump version to 0.2.5
continuous-integration/drone/push Build is passing Details
2023-01-18 17:01:37 +01:00
Lukáš Kucharczyk 972ff67050
Update CSS 2023-01-18 16:58:55 +01:00
Lukáš Kucharczyk 8ae99faa8e
Fix button taking up 100% width
Fixes #37
2023-01-18 16:58:25 +01:00
Lukáš Kucharczyk 8e4086ce83
Remove reduntant property last
Fixes #38
2023-01-18 16:57:32 +01:00
Lukáš Kucharczyk 2760068cde
Add more to .gitignore
continuous-integration/drone/push Build is passing Details
2023-01-18 16:33:38 +01:00
Lukáš Kucharczyk cef797c333 Revert "Add date and time pickers to forms"
continuous-integration/drone/push Build is passing Details
This reverts commit 4d91a76513.
2023-01-16 23:23:26 +01:00
Lukáš Kucharczyk 4d91a76513 Add date and time pickers to forms
continuous-integration/drone/push Build is passing Details
2023-01-16 22:07:43 +01:00
Lukáš Kucharczyk e51d586255 Automatically select purchase when adding session 2023-01-16 21:19:20 +01:00
Lukáš Kucharczyk 2553d6f9e6 Definitively disable pre-commit hook
continuous-integration/drone/push Build is passing Details
2023-01-16 19:49:41 +01:00
Lukáš Kucharczyk 8cf6270d8f Bump version 2023-01-16 19:47:32 +01:00
Lukáš Kucharczyk 0b1089b0f4 Disable pre-commit hook 2023-01-16 19:46:15 +01:00
Lukáš Kucharczyk 9534492f17 Exclude manual times from graphs
Fixes #35
2023-01-16 19:39:24 +01:00
Lukáš Kucharczyk 8b7ed90b49 Improve playtime graph date formatting 2023-01-16 19:27:52 +01:00
Lukáš Kucharczyk 2ce4dd3a0e Fix graph timeline being backwards 2023-01-16 17:26:10 +01:00
Lukáš Kucharczyk a851b5329a Correctly display game that is used as filter 2023-01-16 17:24:34 +01:00
Lukáš Kucharczyk 6fa049e1b1 Sort and clean up imports 2023-01-15 23:39:52 +01:00
Lukáš Kucharczyk 6b7ed0dbb5 Order by timestamp_start from the newest
continuous-integration/drone/push Build is passing Details
2023-01-15 23:20:43 +01:00
Lukáš Kucharczyk dd50d6dd40 Allow filtering by platform and game
continuous-integration/drone/push Build is passing Details
Fixes #32
2023-01-15 23:14:28 +01:00
Lukáš Kucharczyk 162f4f3dbf Fix Dockerfile
continuous-integration/drone/push Build is passing Details
2023-01-15 19:16:34 +01:00
Lukáš Kucharczyk e8e6d5bcae Display playtime graph on session list
continuous-integration/drone/push Build is failing Details
Fixes #29
2023-01-15 18:03:59 +01:00
Lukáš Kucharczyk c5b451a258 Fix error when showing session list with no sessions
Fixes #31
2023-01-15 18:02:48 +01:00
Lukáš Kucharczyk 163211ab0b Hide button if no last session 2023-01-15 13:03:30 +01:00
Lukáš Kucharczyk 64f5668dde Do not specify button width and height
continuous-integration/drone/push Build is passing Details
2023-01-13 22:11:12 +01:00
Lukáš Kucharczyk 465d958d9b Start sessions of last purchase from list
continuous-integration/drone/push Build is passing Details
Fixes #19
2023-01-13 16:54:24 +01:00
Lukáš Kucharczyk d8ece979a8 Revert make dev to plain runserver
The runserver_plus has problems with cache not being invalidated
2023-01-13 16:52:05 +01:00
Lukáš Kucharczyk 2defdd4657 List number of sessions when filtering on session list
continuous-integration/drone/push Build is passing Details
2023-01-10 20:47:33 +01:00
Lukáš Kucharczyk 078f87687f Make format_duration more robust
continuous-integration/drone/push Build is passing Details
2023-01-09 22:48:09 +01:00
Lukáš Kucharczyk 49723831e9 Fix displaying finish button
continuous-integration/drone/push Build is passing Details
2023-01-09 22:05:12 +01:00
Lukáš Kucharczyk 025ea0dd4e Fix migration
continuous-integration/drone/push Build is passing Details
2023-01-09 19:09:31 +01:00
Lukáš Kucharczyk 97467c7a52 Also set duration_manual to zero
continuous-integration/drone/push Build is passing Details
2023-01-09 19:05:47 +01:00
Lukáš Kucharczyk 7842d6f45d Remove debugging statement 2023-01-09 19:00:03 +01:00
Lukáš Kucharczyk b77089f7ad Show playtime total on session list
continuous-integration/drone/push Build is passing Details
Fixes #6
Fixes #25
2023-01-09 18:57:22 +01:00
Lukáš Kucharczyk 24f4459318 Avoid raising exception on format_duration(None)
Fixes #25
2023-01-09 16:14:01 +01:00
Lukáš Kucharczyk 751182df52 Emit gunicorn logs to stdin and stderr 2023-01-08 15:48:53 +01:00
Lukáš Kucharczyk 33e136a810 Add .dockerignore 2023-01-08 15:48:31 +01:00
Lukáš Kucharczyk 362732c22a Run make date via poetry 2023-01-08 15:48:12 +01:00
Lukáš Kucharczyk 8e1c670ffd Fix collectstaticfiles causing error
continuous-integration/drone/push Build is passing Details
Fixes #23
2023-01-08 15:46:09 +01:00
Lukáš Kucharczyk e5a9b9aa50 Fix CSRF error (#22)
continuous-integration/drone/push Build is passing Details
Fixes #21

Reviewed-on: #22
2023-01-08 14:35:28 +00:00
Lukáš Kucharczyk c9b2d5bd8d
Update changelog
continuous-integration/drone/push Build is passing Details
2023-01-07 22:08:57 +01:00
Lukáš Kucharczyk 0d20b543b0
Do not load the admin interface in prod
continuous-integration/drone/push Build is passing Details
2023-01-07 21:59:54 +01:00
Lukáš Kucharczyk f7b69f7704
Add more utilities to Makefile 2023-01-07 21:59:34 +01:00
Lukáš Kucharczyk 1ccfdc321a
Start caddy in the background 2023-01-07 21:59:17 +01:00
Lukáš Kucharczyk 25a58c2732
Be more explicit in docker-compose.yml 2023-01-07 21:59:09 +01:00
Lukáš Kucharczyk 270d9f7296
Ignore static folder when building container 2023-01-07 21:58:22 +01:00
Lukáš Kucharczyk 2939b4a515
Change to gunicorn
continuous-integration/drone/push Build is passing Details
2023-01-07 21:09:47 +01:00
Lukáš Kucharczyk d029fda896 Collect static files in entrypoint.sh
continuous-integration/drone/push Build is passing Details
2023-01-07 18:10:36 +01:00
Lukáš Kucharczyk 9dead362c1 Set STATIC_ROOT 2023-01-07 18:09:22 +01:00
Lukáš Kucharczyk d81dba727b Add docker-compose.yml 2023-01-07 18:09:10 +01:00
222 changed files with 13109 additions and 2271 deletions

View File

@ -0,0 +1,25 @@
{
"name": "Django Time Tracker",
"dockerFile": "../devcontainer.Dockerfile",
"customizations": {
"vscode": {
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.defaultInterpreterPath": "/usr/local/bin/python",
"terminal.integrated.defaultProfile.linux": "bash"
},
"extensions": [
"ms-python.python",
"ms-python.debugpy",
"ms-python.vscode-pylance",
"ms-azuretools.vscode-docker",
"batisteo.vscode-django",
"charliermarsh.ruff",
"bradlc.vscode-tailwindcss",
"EditorConfig.EditorConfig"
]
}
},
"forwardPorts": [8000],
"postCreateCommand": "poetry install && poetry run python manage.py migrate && npm install && make dev",
}

17
.dockerignore Normal file
View File

@ -0,0 +1,17 @@
.git
.githooks
.mypy_cache
.pytest_cache
.venv
.vscode
node_modules
static
.drone.yml
.editorconfig
.gitignore
Caddyfile
CHANGELOG.md
db.sqlite3
docker-compose*
Dockerfile
Makefile

View File

@ -5,21 +5,52 @@ name: default
steps:
- name: test
image: python:3.10
image: python:3.12
commands:
- python -m pip install poetry
- poetry install
- poetry env info
- poetry run python manage.py migrate
- poetry run pytest
- name: build container
- name: build-prod
image: plugins/docker
settings:
repo: registry.kucharczyk.xyz/timetracker
tags:
- latest
- 1.1.0
depends_on:
- "test"
when:
branch:
- main
- name: build-non-prod
image: plugins/docker
settings:
repo: registry.kucharczyk.xyz/timetracker
tags:
- ${DRONE_COMMIT_REF}
- ${DRONE_COMMIT_BRANCH}
when:
branch:
exclude:
- main
depends_on:
- "test"
- name: redeploy on portainer
image: plugins/webhook
settings:
urls:
from_secret: PORTAINER_TIMETRACKER_WEBHOOK_URL
depends_on:
- "build-prod"
trigger:
event:
- push
- cron
exclude:
- pull_request

20
.editorconfig Normal file
View File

@ -0,0 +1,20 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{js,py}]
charset = utf-8
# 4 space indentation
[*.py]
indent_style = space
indent_size = 4
[**/*.js]
indent_style = space
indent_size = 2
[*.html]
insert_final_newline = false

1
.envrc Normal file
View File

@ -0,0 +1 @@
use nix

0
.githooks/pre-commit → .githooks/pre-commit.disabled Executable file → Normal file
View File

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

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

9
.gitignore vendored
View File

@ -1,7 +1,12 @@
__pycache__
.mypy_cache
.pytest_cache
.venv
.venv/
node_modules
package-lock.json
db.sqlite3
db.sqlite3
/static/
dist/
.DS_Store
.python-version
.direnv

20
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,20 @@
repos:
# disable due to incomaptible formatting between
# black and ruff
# TODO: replace with ruff when it works on NixOS
# - repo: https://github.com/psf/black
# rev: 24.8.0
# hooks:
# - id: black
# - repo: https://github.com/pycqa/isort
# rev: 5.13.2
# hooks:
# - id: isort
# name: isort (python)
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.34.0
hooks:
- id: djlint-reformat-django
args: ["--ignore", "H011"]
- id: djlint-django
args: ["--ignore", "H011"]

11
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"recommendations": [
"charliermarsh.ruff",
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.debugpy",
"batisteo.vscode-django",
"bradlc.vscode-tailwindcss",
"EditorConfig.EditorConfig"
]
}

29
.vscode/settings.json vendored
View File

@ -4,5 +4,30 @@
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.analysis.typeCheckingMode": "basic"
}
"python.analysis.typeCheckingMode": "strict",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
},
"ruff.path": ["/nix/store/jaibb3v0rrnlw5ib54qqq3452yhp1xcb-ruff-0.5.7/bin/ruff"],
"tailwind-fold.supportedLanguages": [
"html",
"typescriptreact",
"javascriptreact",
"typescript",
"javascript",
"vue-html",
"vue",
"php",
"markdown",
"coffeescript",
"svelte",
"astro",
"erb",
"django-html"
]
}

View File

@ -1,3 +1,235 @@
## Unreleased
## New
* Render notes as Markdown
* Require login by default
* Add stats for dropped purchases, monthly playtimes
* Allow deleting purchases
* Add all-time stats
* Manage purchases
* Automatically convert purchase prices
## Improved
* mark refunded purchases red on game overview
* increase session count on game overview when starting a new session
* game overview:
* sort purchases also by date purchased (on top of date released)
* improve header format, make it more appealing
* ignore manual sessions when calculating session average
* stats: improve purchase name consistency
* session list: use display name instead of sort name
* unify the appearance of game links, and make them expand to full size on hover
## Fixed
* Fix title not being displayed on the Recent sessions page
* Avoid errors when displaying game overview with zero sessions
## 1.5.2 / 2024-01-14 21:27+01:00
## Improved
* game overview:
* improve how editions and purchases are displayed
* make it possible to end session from overview
* add purchase: only allow choosing purchases of selected edition
* session list:
* starting and ending sessions is much faster/doest not reload the page
* listing sessions is much faster
## 1.5.1 / 2023-11-14 21:10+01:00
## Improved
* Disallow choosing non-game purchase as related purchase
* Improve display of purchases
## 1.5.0 / 2023-11-14 19:27+01:00
## New
* Add stat for finished this year's games
* Add purchase types:
* Game (previously all of them were this type)
* DLC
* Season Pass
* Battle Pass
## Fixed
* Order purchases by date on game view
## 1.4.0 / 2023-11-09 21:01+01:00
### New
* More fields are now optional. This is to make it easier to add new items in bulk.
* Game: Wikidata ID
* Edition: Platform, Year
* Purchase: Platform
* Platform: Group
* Session: Device
* New fields:
* Game: Year Released
* To record original year of release
* Upon migration, this will be set to a year of any of the game's edition that has it set
* Purchase: Date Finished
* Editions are now unique combination of name and platform
* Add more stats:
* All finished games
* All finished 2023 games
* All finished games that were purchased this year
* Sessions (count)
* Days played
* Finished (count)
* Unfinished (count)
* Refunded (count)
* Backlog Decrease (count)
* New workflow:
* Adding Game, Edition, Purchase, and Session in a row is now much faster
### Improved
* game overview: simplify playtime range display
* new session: order devices alphabetically
* ignore English articles when sorting names
* added a new sort_name field that gets automatically created
* automatically fill certain values in forms:
* new game: name and sort name after typing
* new edition: name, sort name, and year when selecting game
* new purchase: platform when selecting edition
## 1.3.0 / 2023-11-05 15:09+01:00
### New
* Add Stats to the main navigation
* Allow selecting year on the Stats page
### Improved
* Make some pages redirect back instead to session list
### Improved
* Make navigation more compact
### Fixed
* Correctly limit sessions to a single year for stats
## 1.2.0 / 2023-11-01 20:18+01:00
### New
* Add yearly stats page (https://git.kucharczyk.xyz/lukas/timetracker/issues/15)
### Enhancements
* Add a button to start session from game overview
## 1.1.2 / 2023-10-13 16:30+02:00
### Enhancements
* Durations are formatted in a consisent manner across all pages
### Fixes
* Game Overview: display duration when >1 hour instead of displaying 0
## 1.1.1 / 2023-10-09 20:52+02:00
### New
* Add notes section to game overview
### Enhancements
* Make it possible to add any data on the game overview page
## 1.1.0 / 2023-10-09 00:01+02:00
### New
* Add game overview page (https://git.kucharczyk.xyz/lukas/timetracker/issues/8)
* Add helper buttons next to datime fields
* Add copy button on Add session page to copy times between fields
* Change fonts to IBM Plex
### Enhancements
* Improve form appearance
* Focus important fields on forms
* Use the same form when editing a session as when adding a session
* Change recent session view to current year instead of last 30 days
* Add a hacky way not to reload a page when starting or ending a session (https://git.kucharczyk.xyz/lukas/timetracker/issues/52)
* Improve session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/53)
### Fixes
* Fix session being wrongly considered in progress if it had a certain amount of manual hours (https://git.kucharczyk.xyz/lukas/timetracker/issues/58)
* Fix bug when filtering only manual sessions (https://git.kucharczyk.xyz/lukas/timetracker/issues/51)
## 1.0.3 / 2023-02-20 17:16+01:00
* Add wikidata ID and year for editions
* 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
* Breaking
* Due to major re-arranging and re-naming of the folder structure, tables also had to be renamed.
* Fixed
* Sort form fields alphabetically (https://git.kucharczyk.xyz/lukas/timetracker/issues/39, https://git.kucharczyk.xyz/lukas/timetracker/issues/40)
* Start session button starts different game than it says (#44)
## 0.2.5 / 2023-01-18 17:01+01:00
* New
* When adding session, pre-select game with the last session
* Fixed
* Start session now button would take up 100% width, leading to accidental clicks (https://git.kucharczyk.xyz/lukas/timetracker/issues/37)
* Removed
* Session model property `last` is already implemented by Django method `last()`, thus it was removed (https://git.kucharczyk.xyz/lukas/timetracker/issues/38)
## 0.2.4 / 2023-01-16 19:39+01:00
* Fixed
* When filtering by game, the "Filtering by (...)" text would erroneously list an unrelated platform
* Playtime graph would display timeline backwards
* Playtime graph with many dates would overlap (https://git.kucharczyk.xyz/lukas/timetracker/issues/34)
* Manually added times (= without end timestamp) would make graphs look ugly and noisy (https://git.kucharczyk.xyz/lukas/timetracker/issues/35)
## 0.2.3 / 2023-01-15 23:13+01:00
* Allow filtering by platform and game on session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/32)
* Order session by newest as preparation for https://git.kucharczyk.xyz/lukas/timetracker/issues/33
## 0.2.2 / 2023-01-15 17:59+01:00
* Display playtime graph on session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/29)
* Fix error when showing session list with no sessions (https://git.kucharczyk.xyz/lukas/timetracker/issues/31)
## 0.2.1 / 2023-01-13 16:53+01:00
* List number of sessions when filtering on session list
* Start sessions of last purchase from list (https://git.kucharczyk.xyz/lukas/timetracker/issues/19)
## 0.2.0 / 2023-01-09 22:42+01:00
* Show playtime total on session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/6)
* Make formatting durations more robust, change default duration display to "X hours" (https://git.kucharczyk.xyz/lukas/timetracker/issues/26)
## 0.1.4 / 2023-01-08 15:45+01:00
* Fix collectstaticfiles causing error when restarting container (https://git.kucharczyk.xyz/lukas/timetracker/issues/23)
## 0.1.3 / 2023-01-08 15:23+01:00
* Fix CSRF error (https://git.kucharczyk.xyz/lukas/timetracker/pulls/22)
## 0.1.2 / 2023-01-07 22:05+01:00
* Switch to Uvicorn/Gunicorn + Caddy (https://git.kucharczyk.xyz/lukas/timetracker/pulls/4)
## 0.1.1 / 2023-01-05 23:26+01:00
* Order by timestamp_start by default
* Add pre-commit hook to update version
@ -19,4 +251,4 @@
* Make it possible to add a new platform
* Save calculated duration to database if both timestamps are set
* Improve session listing
* Set version in the footer to fixed, fix main container height
* Set version in the footer to fixed, fix main container height

14
Caddyfile Normal file
View File

@ -0,0 +1,14 @@
{
auto_https off
admin off
}
:8000 {
handle_path /static/* {
root * /usr/share/caddy
file_server
}
handle {
reverse_proxy backend:8001
}
}

View File

@ -1,26 +1,45 @@
FROM node as css
WORKDIR /app
COPY . /app
RUN npm install && \
npx tailwindcss -i ./src/input.css -o ./src/web/tracker/static/base.css --minify
FROM python:3.12.0-slim-bullseye
FROM python:3.10-slim-bullseye
ENV VERSION_NUMBER=1.5.2 \
PROD=1 \
PYTHONUNBUFFERED=1 \
PYTHONFAULTHANDLER=1 \
PYTHONHASHSEED=random \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_DEFAULT_TIMEOUT=100 \
PIP_ROOT_USER_ACTION=ignore \
POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=false \
POETRY_CACHE_DIR='/var/cache/pypoetry' \
POETRY_HOME='/usr/local'
ENV VERSION_NUMBER 0.1.0-48-gdb5de81
ENV PROD 1
RUN apt-get update && apt-get upgrade -y \
&& apt-get install --no-install-recommends -y \
bash \
curl \
&& curl -sSL 'https://install.python-poetry.org' | python - \
&& poetry --version \
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
&& apt-get clean -y && rm -rf /var/lib/apt/lists/*
RUN useradd --create-home --uid 1000 timetracker
RUN useradd -m --uid 1000 timetracker \
&& mkdir -p '/var/www/django/static' \
&& chown timetracker:timetracker '/var/www/django/static'
WORKDIR /home/timetracker/app
COPY . /home/timetracker/app/
RUN chown -R timetracker:timetracker /home/timetracker/app
COPY --from=css /app/src/web/tracker/static/base.css /home/timetracker/app/src/web/tracker/static/base.css
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
RUN --mount=type=cache,target="$POETRY_CACHE_DIR" \
echo "$PROD" \
&& poetry version \
&& poetry run pip install -U pip \
&& poetry install --only main --no-interaction --no-ansi --sync
USER timetracker
ENV PATH="$PATH:/home/timetracker/.local/bin"
RUN pip install --no-cache-dir poetry
RUN poetry install --without dev
EXPOSE 8000
ENTRYPOINT [ "/entrypoint.sh" ]
CMD [ "/entrypoint.sh" ]

View File

@ -1,43 +1,63 @@
.PHONY: createsuperuser shell
all: css migrate
initialize: npm css migrate sethookdir loadplatforms
HTMLFILES := $(shell find src/web/tracker/templates -type f)
HTMLFILES := $(shell find games/templates -type f)
PYTHON_VERSION = 3.12
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
css: common/input.css
npx tailwindcss -i ./common/input.css -o ./games/static/base.css
makemigrations:
poetry run python src/web/manage.py makemigrations
poetry run python manage.py makemigrations
migrate: makemigrations
poetry run python src/web/manage.py migrate
poetry run python manage.py migrate
dev: migrate sethookdir
poetry run python src/web/manage.py runserver_plus
init:
pyenv install -s $(PYTHON_VERSION)
pyenv local $(PYTHON_VERSION)
pip install poetry
poetry install
npm install
dumptracker:
poetry run python src/web/manage.py dumpdata --format yaml tracker --output tracker_fixture.yaml
dev:
@npx concurrently \
--names "Django,Tailwind" \
--prefix-colors "blue,green" \
"poetry run python -Wa manage.py runserver" \
"npx tailwindcss -i ./common/input.css -o ./games/static/base.css --watch"
caddy:
caddy run --watch
dev-prod: migrate collectstatic
PROD=1 poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker
dumpgames:
poetry run python manage.py dumpdata --format yaml games --output tracker_fixture.yaml
loadplatforms:
poetry run python src/web/manage.py loaddata platforms.yaml
poetry run python manage.py loaddata platforms.yaml
loadall:
poetry run python manage.py loaddata data.yaml
loadsample:
poetry run python src/web/manage.py loaddata sample.yaml
poetry run python manage.py loaddata sample.yaml
createsuperuser:
poetry run python src/web/manage.py createsuperuser
poetry run python manage.py createsuperuser
shell:
poetry run python src/web/manage.py shell
poetry run python manage.py shell
collectstatic:
poetry run python manage.py collectstatic --clear --no-input
poetry.lock: pyproject.toml
poetry install
@ -45,8 +65,10 @@ poetry.lock: pyproject.toml
test: poetry.lock
poetry run pytest
sethookdir:
git config core.hooksPath .githooks
date:
poetry run python -c 'import datetime; from zoneinfo import ZoneInfo; print(datetime.datetime.isoformat(datetime.datetime.now(ZoneInfo("Europe/Prague")), timespec="minutes", sep=" "))'
make date:
python3 -c 'import datetime; from zoneinfo import ZoneInfo; print(datetime.datetime.isoformat(datetime.datetime.now(ZoneInfo("Europe/Prague")), timespec="minutes", sep=" "))'
cleanstatic:
rm -r static/*
clean: cleanstatic

View File

@ -1,3 +1,15 @@
# Timetracker
A simple game catalogue and play session tracker.
A simple game catalogue and play session tracker.
# Development
The project uses `pyenv` to manage installed Python versions.
If you have `pyenv` installed, you can simply run:
```
make init
```
This will make sure the correct Python version is installed, and it will install all dependencies using `poetry`.
Afterwards, you can start the development server using `make dev`.

195
common/components.py Normal file
View File

@ -0,0 +1,195 @@
from random import choices as random_choices
from string import ascii_lowercase
from typing import Any, Callable
from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string
from django.urls import NoReverseMatch, reverse
from django.utils.safestring import SafeText, mark_safe
from common.utils import truncate
HTMLAttribute = tuple[str, str | int | bool]
HTMLTag = str
def Component(
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
template: str = "",
tag_name: str = "",
) -> HTMLTag:
if not tag_name and not template:
raise ValueError("One of template or tag_name is required.")
if isinstance(children, str):
children = [children]
childrenBlob = "\n".join(children)
if len(attributes) == 0:
attributesBlob = ""
else:
attributesList = [f'{name}="{value}"' for name, value in attributes]
# make attribute list into a string
# and insert space between tag and attribute list
attributesBlob = f" {" ".join(attributesList)}"
tag: str = ""
if tag_name != "":
tag = f"<{tag_name}{attributesBlob}>{childrenBlob}</{tag_name}>"
elif template != "":
tag = render_to_string(
template,
{name: value for name, value in attributes}
| {"slot": mark_safe("\n".join(children))},
)
return mark_safe(tag)
def randomid(seed: str = "", length: int = 10) -> str:
return seed + "".join(random_choices(ascii_lowercase, k=length))
def Popover(
popover_content: str,
wrapped_content: str = "",
children: list[HTMLTag] = [],
attributes: list[HTMLAttribute] = [],
) -> str:
if not wrapped_content and not children:
raise ValueError("One of wrapped_content or children is required.")
id = randomid()
return Component(
attributes=attributes
+ [
("id", id),
("wrapped_content", wrapped_content),
("popover_content", popover_content),
],
children=children,
template="cotton/popover.html",
)
def PopoverTruncated(input_string: str, length: int = 30, ellipsis: str = "") -> str:
if (truncated := truncate(input_string, length, ellipsis)) != input_string:
return Popover(wrapped_content=truncated, popover_content=input_string)
else:
return input_string
def A(
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
url: str | Callable[..., Any] = "",
):
"""
Returns the HTML tag "a".
"url" can either be:
- URL (string)
- path name passed to reverse() (string)
- function
"""
additional_attributes = []
if url:
if type(url) is str:
try:
url_result = reverse(url)
except NoReverseMatch:
url_result = url
elif callable(url):
url_result = url()
else:
raise TypeError("'url' is neither str nor function.")
additional_attributes = [("href", url_result)]
return Component(
tag_name="a", attributes=attributes + additional_attributes, children=children
)
def Button(
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
size: str = "base",
icon: bool = False,
color: str = "blue",
):
return Component(
template="cotton/button.html",
attributes=attributes + [("size", size), ("icon", icon), ("color", color)],
children=children,
)
def Div(
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
):
return Component(tag_name="div", attributes=attributes, children=children)
def Input(
type: str = "text",
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
):
return Component(
tag_name="input", attributes=attributes + [("type", type)], children=children
)
def Form(
action="",
method="get",
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
):
return Component(
tag_name="form",
attributes=attributes + [("action", action), ("method", method)],
children=children,
)
def Icon(
name: str,
attributes: list[HTMLAttribute] = [],
):
try:
result = Component(template=f"cotton/icon/{name}.html", attributes=attributes)
except TemplateDoesNotExist:
result = Icon(name="unspecified", attributes=attributes)
return result
def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeText:
link = reverse("view_game", args=[int(game_id)])
a_content = Div(
[("class", "inline-flex gap-2 items-center")],
[
Icon(
platform.icon,
[("title", platform.name)],
),
PopoverTruncated(name),
],
)
return mark_safe(
A(
url=link,
children=[a_content],
),
)
def NameWithPlatformIcon(name: str, platform: str) -> SafeText:
content = Div(
[("class", "inline-flex gap-2 items-center")],
[
Icon(
platform.icon,
[("title", platform.name)],
),
PopoverTruncated(name),
],
)
return mark_safe(content)

30
common/import_data.py Normal file
View File

@ -0,0 +1,30 @@
import csv
from typing import TypeAlias
from games.models import Game
DataList: TypeAlias = list[dict[str, str]] | None
def read_csv(filename: str) -> DataList:
with open(filename, "r") as csvfile:
writer = csv.DictReader(csvfile)
return writer
def import_data(data: DataList):
matching_names = {}
for line in data:
name = line["name"]
if name not in matching_names:
# try exact match first
try:
game_id = Game.objects.get(name__iexact=name)
except:
pass
matching_names[name] = game_id
print(f"Exact matched {len(matching_names)} games.")
def import_from_file(filename: str):
import_data(read_csv(filename))

171
common/input.css Normal file
View File

@ -0,0 +1,171 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: "IBM Plex Mono";
src: url("fonts/IBMPlexMono-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: "IBM Plex Sans";
src: url("fonts/IBMPlexSans-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: "IBM Plex Serif";
src: url("fonts/IBMPlexSerif-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: "IBM Plex Serif";
src: url("fonts/IBMPlexSerif-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: "IBM Plex Sans Condensed";
src: url("fonts/IBMPlexSansCondensed-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
}
/* a:hover {
text-decoration-color: #ff4400;
color: rgb(254, 185, 160);
transition: all 0.2s ease-out;
} */
form label {
@apply dark:text-slate-400;
}
.responsive-table {
@apply dark:text-white mx-auto table-fixed;
}
.responsive-table tr:nth-child(even) {
@apply bg-slate-800
}
.responsive-table tbody tr:nth-child(odd) {
@apply bg-slate-900
}
.responsive-table thead th {
@apply text-left border-b-2 border-b-slate-500 text-xl;
}
.responsive-table thead th:not(:first-child),
.responsive-table td:not(:first-child) {
@apply border-l border-l-slate-500;
}
@layer utilities {
.min-w-20char {
min-width: 20ch;
}
.max-w-20char {
max-width: 20ch;
}
.min-w-30char {
min-width: 30ch;
}
.max-w-30char {
max-width: 30ch;
}
.max-w-35char {
max-width: 35ch;
}
.max-w-40char {
max-width: 40ch;
}
}
form input,
select,
textarea {
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
}
form input:disabled,
select:disabled,
textarea:disabled {
@apply dark:bg-slate-700 dark:text-slate-400;
}
.errorlist {
@apply mt-4 mb-1 pl-3 py-2 bg-red-600 text-slate-200 w-[300px];
}
@media screen and (min-width: 768px) {
form input,
select,
textarea {
width: 300px;
}
}
@media screen and (max-width: 768px) {
form input,
select,
textarea {
width: 150px;
}
}
#button-container button {
@apply mx-1;
}
.basic-button-container {
@apply flex space-x-2 justify-center;
}
.basic-button {
@apply inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out;
}
.markdown-content ul {
list-style-type: disc;
list-style-position: inside;
padding-left: 1em;
}
.markdown-content ol {
list-style-type: decimal;
list-style-position: inside;
padding-left: 1em;
}
.markdown-content ul,
.markdown-content ol {
list-style-position: outside;
padding-left: 1em;
}
.markdown-content ul ul,
.markdown-content ul ol,
.markdown-content ol ul,
.markdown-content ol ol {
list-style-type: circle;
margin-top: 0.5em;
margin-bottom: 0.5em;
padding-left: 1em;
}
/* .truncate-container {
@apply inline-block relative;
a {
@apply inline-block truncate max-w-20char transition-all group-hover:absolute group-hover:max-w-none group-hover:-top-8 group-hover:-left-6 group-hover:min-w-60 group-hover:px-6 group-hover:py-3.5 group-hover:bg-purple-600 group-hover:rounded-sm group-hover:outline-dashed group-hover:outline-purple-400 group-hover:outline-4;
}
} */

169
common/time.py Normal file
View File

@ -0,0 +1,169 @@
import re
from datetime import date, datetime, timedelta
from django.utils import timezone
from common.utils import generate_split_ranges
dateformat: str = "%d/%m/%Y"
datetimeformat: str = "%d/%m/%Y %H:%M"
timeformat: str = "%H:%M"
durationformat: str = "%2.1H hours"
durationformat_manual: str = "%H hours"
def _safe_timedelta(duration: timedelta | int | None):
if duration == None:
return timedelta(0)
elif isinstance(duration, int):
return timedelta(seconds=duration)
elif isinstance(duration, timedelta):
return duration
def format_duration(
duration: timedelta | int | float | None, format_string: str = "%H hours"
) -> str:
"""
Format timedelta into the specified format_string.
Valid format variables:
- %H hours
- %m minutes
- %s seconds
- %r total seconds
Values don't change into higher units if those units are missing
from the formatting string. For example:
- 61 seconds as "%s" = 61 seconds
- 61 seconds as "%m %s" = 1 minutes 1 seconds"
Format specifiers can include width and precision options:
- %5.2H: hours formatted with width 5 and 2 decimal places (padded with zeros)
"""
minute_seconds = 60
hour_seconds = 60 * minute_seconds
day_seconds = 24 * hour_seconds
safe_duration = _safe_timedelta(duration)
# we don't need float
seconds_total = int(safe_duration.total_seconds())
# timestamps where end is before start
if seconds_total < 0:
seconds_total = 0
days = hours = hours_float = minutes = seconds = 0
remainder = seconds = seconds_total
if "%d" in format_string:
days, remainder = divmod(seconds_total, day_seconds)
if re.search(r"%\d*\.?\d*H", format_string):
hours_float, remainder = divmod(remainder, hour_seconds)
hours = float(hours_float) + remainder / hour_seconds
if re.search(r"%\d*\.?\d*m", format_string):
minutes, seconds = divmod(remainder, minute_seconds)
literals = {
"d": str(days),
"H": str(hours) if "m" not in format_string else str(hours_float),
"m": str(minutes),
"s": str(seconds),
"r": str(seconds_total),
}
formatted_string = format_string
for pattern, replacement in literals.items():
# Match format specifiers with optional width and precision
match = re.search(rf"%(\d*\.?\d*){pattern}", formatted_string)
if match:
format_spec = match.group(1)
if "." in format_spec:
# Format the number as float if precision is specified
replacement = f"{float(replacement):{format_spec}f}"
else:
# Format the number as integer if no precision is specified
replacement = f"{int(float(replacement)):>{format_spec}}"
# Replace the format specifier with the formatted number
formatted_string = re.sub(
rf"%\d*\.?\d*{pattern}", replacement, formatted_string
)
return formatted_string
def local_strftime(datetime: datetime, format: str = datetimeformat) -> str:
return timezone.localtime(datetime).strftime(format)
def daterange(start: date, end: date, end_inclusive: bool = False) -> list[date]:
time_between: timedelta = end - start
if (days_between := time_between.days) < 1:
raise ValueError("start and end have to be at least 1 day apart.")
if end_inclusive:
print(f"{end_inclusive=}")
print(f"{days_between=}")
days_between += 1
print(f"{days_between=}")
return [start + timedelta(x) for x in range(days_between)]
def streak(datelist: list[date]) -> dict[str, int | tuple[date, date]]:
if len(datelist) == 1:
return {"days": 1, "dates": (datelist[0], datelist[0])}
else:
print(f"Processing {len(datelist)} dates.")
missing = sorted(
set(
datelist[0] + timedelta(x)
for x in range((datelist[-1] - datelist[0]).days)
)
- set(datelist)
)
print(f"{len(missing)} days missing.")
datelist_with_missing = sorted(datelist + missing)
ranges = list(generate_split_ranges(datelist_with_missing, missing))
print(f"{len(ranges)} ranges calculated.")
longest_consecutive_days = timedelta(0)
longest_range: tuple[date, date] = (date(1970, 1, 1), date(1970, 1, 1))
for start, end in ranges:
if (current_streak := end - start) > longest_consecutive_days:
longest_consecutive_days = current_streak
longest_range = (start, end)
return {"days": longest_consecutive_days.days + 1, "dates": longest_range}
def streak_bruteforce(datelist: list[date]) -> dict[str, int | tuple[date, date]]:
if (datelist_length := len(datelist)) == 0:
raise ValueError("Number of dates in the list is 0.")
datelist.sort()
current_streak = 1
current_start = datelist[0]
current_end = datelist[0]
current_date = datelist[0]
highest_streak = 1
highest_streak_daterange = (current_start, current_end)
def update_highest_streak():
nonlocal highest_streak, highest_streak_daterange
if current_streak > highest_streak:
highest_streak = current_streak
highest_streak_daterange = (current_start, current_end)
def reset_streak():
nonlocal current_start, current_end, current_streak
current_start = current_end = current_date
current_streak = 1
def increment_streak():
nonlocal current_end, current_streak
current_end = current_date
current_streak += 1
for i, datelist_item in enumerate(datelist, start=1):
current_date = datelist_item
if current_date == current_start or current_date == current_end:
continue
if current_date - timedelta(1) != current_end and i != datelist_length:
update_highest_streak()
reset_streak()
elif current_date - timedelta(1) == current_end and i == datelist_length:
increment_streak()
update_highest_streak()
else:
increment_streak()
return {"days": highest_streak, "dates": highest_streak_daterange}
def available_stats_year_range():
return range(datetime.now().year, 1999, -1)

66
common/utils.py Normal file
View File

@ -0,0 +1,66 @@
from datetime import date
from typing import Any, Generator, TypeVar
def safe_division(numerator: int | float, denominator: int | float) -> int | float:
"""
Divides without triggering division by zero exception.
Returns 0 if denominator is 0.
"""
try:
return numerator / denominator
except ZeroDivisionError:
return 0
def safe_getattr(obj: object, attr_chain: str, default: Any | None = None) -> object:
"""
Safely get the nested attribute from an object.
Parameters:
obj (object): The object from which to retrieve the attribute.
attr_chain (str): The chain of attributes, separated by dots.
default: The default value to return if any attribute in the chain does not exist.
Returns:
The value of the nested attribute if it exists, otherwise the default value.
"""
attrs = attr_chain.split(".")
for attr in attrs:
try:
obj = getattr(obj, attr)
except AttributeError:
return default
return obj
def truncate(input_string: str, length: int = 30, ellipsis: str = "") -> str:
return (
(f"{input_string[:length-len(ellipsis)].rstrip()}{ellipsis}")
if len(input_string) > length
else input_string
)
T = TypeVar("T", str, int, date)
def generate_split_ranges(
value_list: list[T], split_points: list[T]
) -> Generator[tuple[T, T], None, None]:
for x in range(0, len(split_points) + 1):
if x == 0:
start = 0
elif x >= len(split_points):
start = value_list.index(split_points[x - 1]) + 1
else:
start = value_list.index(split_points[x - 1]) + 1
try:
end = value_list.index(split_points[x])
except IndexError:
end = len(value_list)
yield (value_list[start], value_list[end - 1])
def format_float_or_int(number: int | float):
return int(number) if float(number).is_integer() else f"{number:03.2f}"

24
devcontainer.Dockerfile Normal file
View File

@ -0,0 +1,24 @@
FROM python:3.13-slim
# Set up environment
ENV PYTHONUNBUFFERED=1
WORKDIR /workspace
# Install Poetry
RUN apt-get update && apt-get install -y \
curl \
make \
npm \
&& rm -rf /var/lib/apt/lists/*
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="/root/.local/bin:$PATH"
# Copy pyproject.toml and poetry.lock for dependency installation
COPY pyproject.toml poetry.lock* ./
RUN poetry install --no-root
# Copy the rest of the application code
COPY . .
# Set up Django development server
EXPOSE 8000

View File

@ -0,0 +1,17 @@
---
services:
timetracker:
image: registry.kucharczyk.xyz/timetracker
build:
context: .
dockerfile: Dockerfile
container_name: timetracker
environment:
- TZ=Europe/Prague
- CSRF_TRUSTED_ORIGINS="https://tracker.kucharczyk.xyz"
user: "1000"
# volumes:
# - "db:/home/timetracker/app/src/timetracker/db.sqlite3"
ports:
- "8000:8000"
restart: unless-stopped

30
docker-compose.yml Normal file
View File

@ -0,0 +1,30 @@
---
services:
backend:
image: registry.kucharczyk.xyz/timetracker
build:
context: .
dockerfile: Dockerfile
environment:
- TZ=Europe/Prague
- CSRF_TRUSTED_ORIGINS="https://tracker.kucharczyk.xyz"
user: "1000"
volumes:
- "static-files:/var/www/django/static"
- "$PWD/db.sqlite3:/home/timetracker/app/db.sqlite3"
restart: unless-stopped
frontend:
image: caddy
volumes:
- "static-files:/usr/share/caddy:ro"
- "$PWD/Caddyfile:/etc/caddy/Caddyfile"
ports:
- "8000:8000"
depends_on:
- backend
volumes:
static-files:

View File

@ -1,8 +1,23 @@
#!/bin/bash
# Apply database migrations
set -euo pipefail
echo "Apply database migrations"
poetry run python src/web/manage.py migrate
poetry run python manage.py migrate
# Start server
echo "Starting server"
poetry run python src/web/manage.py runserver 0.0.0.0:8000
echo "Collect static files"
poetry run python manage.py collectstatic --clear --no-input
_term() {
echo "Caught SIGTERM signal!"
kill -SIGTERM "$gunicorn_pid"
kill -SIGTERM "$django_q_pid"
}
trap _term SIGTERM
echo "Starting Django-Q cluster"
poetry run python manage.py qcluster & django_q_pid=$!
echo "Starting app"
poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile - & gunicorn_pid=$!
wait "$gunicorn_pid" "$django_q_pid"

20
games/admin.py Normal file
View File

@ -0,0 +1,20 @@
from django.contrib import admin
from games.models import (
Device,
Edition,
ExchangeRate,
Game,
Platform,
Purchase,
Session,
)
# Register your models here.
admin.site.register(Game)
admin.site.register(Purchase)
admin.site.register(Platform)
admin.site.register(Session)
admin.site.register(Edition)
admin.site.register(Device)
admin.site.register(ExchangeRate)

33
games/apps.py Normal file
View File

@ -0,0 +1,33 @@
from datetime import timedelta
from django.apps import AppConfig
from django.core.management import call_command
from django.db.models.signals import post_migrate
from django.utils.timezone import now
class GamesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "games"
def ready(self):
post_migrate.connect(schedule_tasks, sender=self)
def schedule_tasks(sender, **kwargs):
from django_q.models import Schedule
from django_q.tasks import schedule
if not Schedule.objects.filter(name="Update converted prices").exists():
schedule(
"games.tasks.convert_prices",
name="Update converted prices",
schedule_type=Schedule.MINUTES,
next_run=now() + timedelta(seconds=30),
)
from games.models import ExchangeRate
if not ExchangeRate.objects.exists():
print("ExchangeRate table is empty. Loading fixture...")
call_command("loaddata", "exchangerates.yaml")

1091
games/fixtures/data.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,112 @@
- model: games.exchangerate
pk: 1
fields:
currency_from: USD
currency_to: CZK
year: 2024
rate: 23.4
- model: games.exchangerate
pk: 2
fields:
currency_from: CNY
currency_to: CZK
year: 2024
rate: 3.267
- model: games.exchangerate
pk: 3
fields:
currency_from: USD
currency_to: CZK
year: 2019
rate: 22.466
- model: games.exchangerate
pk: 4
fields:
currency_from: USD
currency_to: CZK
year: 2023
rate: 22.63
- model: games.exchangerate
pk: 5
fields:
currency_from: USD
currency_to: CZK
year: 2017
rate: 25.819
- model: games.exchangerate
pk: 6
fields:
currency_from: USD
currency_to: CZK
year: 2013
rate: 19.023
- model: games.exchangerate
pk: 7
fields:
currency_from: CNY
currency_to: CZK
year: 2019
rate: 3.295
- model: games.exchangerate
pk: 8
fields:
currency_from: CNY
currency_to: CZK
year: 2016
rate: 3.795
- model: games.exchangerate
pk: 9
fields:
currency_from: CNY
currency_to: CZK
year: 2015
rate: 3.707
- model: games.exchangerate
pk: 10
fields:
currency_from: CNY
currency_to: CZK
year: 2020
rate: 3.26
- model: games.exchangerate
pk: 11
fields:
currency_from: EUR
currency_to: CZK
year: 2012
rate: 25.51
- model: games.exchangerate
pk: 12
fields:
currency_from: EUR
currency_to: CZK
year: 2010
rate: 26.465
- model: games.exchangerate
pk: 13
fields:
currency_from: EUR
currency_to: CZK
year: 2014
rate: 27.52
- model: games.exchangerate
pk: 14
fields:
currency_from: EUR
currency_to: CZK
year: 2024
rate: 25.21
- model: games.exchangerate
pk: 15
fields:
currency_from: EUR
currency_to: CZK
year: 2022
rate: 24.325
- model: games.exchangerate
pk: 16
fields:
currency_from: CNY
currency_to: CZK
year: 2018
rate: 3.268

View File

@ -1,28 +1,28 @@
- model: tracker.Platform
- model: games.Platform
fields:
name: Steam
group: PC
- model: tracker.Platform
- model: games.Platform
fields:
name: Xbox Gamepass
group: PC
- model: tracker.Platform
- model: games.Platform
fields:
name: Epic Games Store
group: PC
- model: tracker.Platform
- model: games.Platform
fields:
name: Playstation 5
group: Playstation
- model: tracker.Platform
- model: games.Platform
fields:
name: Playstation 4
group: Playstation
- model: tracker.Platform
- model: games.Platform
fields:
name: Nintendo Switch
group: Nintendo
- model: tracker.Platform
- model: games.Platform
fields:
name: Nintendo 3DS
group: Nintendo

View File

@ -0,0 +1,225 @@
name,platform,start,end
Nioh 2,PS5,2022-12-17 19:34,2022-12-17 22:53
Nioh 2,PS5,2022-12-15 02:25,2022-12-15 03:57
Nioh 2,PS5,2022-12-13 02:41,2022-12-13 04:34
Nioh 2,PS5,2022-12-11 21:01,2022-12-11 23:21
VALKYRIE ELYSIUM,PS5,2022-12-11 06:07,2022-12-11 06:20
Metal: Hellsinger,PS5,2022-12-11 05:50,2022-12-11 06:07
Nioh 2,PS5,2022-12-11 04:31,2022-12-11 05:50
Nioh 2,PS5,2022-12-11 04:11,2022-12-11 04:26
Forspoken,PS5,2022-12-10 22:29,2022-12-10 23:10
Nioh 2,PS5,2022-12-10 19:44,2022-12-10 22:29
Nioh 2,PS5,2022-12-09 02:14,2022-12-09 04:16
Nioh 2,PS5,2022-12-08 01:03,2022-12-08 01:57
Nioh 2,PS5,2022-12-07 00:43,2022-12-07 04:16
Nioh 2,PS5,2022-12-04 20:48,2022-12-04 23:31
Nioh 2,PS5,2022-12-04 04:26,2022-12-04 07:01
Nioh 2,PS5,2022-12-04 04:20,2022-12-04 04:22
Nioh 2,PS5,2022-11-26 19:18,2022-11-26 21:28
Nioh 2,PS5,2022-11-26 19:16,2022-11-26 19:18
Nioh 2,PS5,2022-11-26 02:46,2022-11-26 03:56
Nioh 2,PS5,2022-11-26 02:01,2022-11-26 02:43
God of War Ragnarök,PS5,2022-11-24 23:03,2022-11-25 01:32
God of War Ragnarök,PS5,2022-11-23 00:41,2022-11-23 07:52
God of War Ragnarök,PS5,2022-11-21 22:52,2022-11-22 04:51
God of War Ragnarök,PS5,2022-11-21 02:11,2022-11-21 05:13
God of War Ragnarök,PS5,2022-11-20 21:34,2022-11-20 22:50
God of War Ragnarök,PS5,2022-11-20 03:46,2022-11-20 05:52
God of War Ragnarök,PS5,2022-11-19 14:30,2022-11-19 16:14
God of War Ragnarök,PS5,2022-11-18 23:15,2022-11-19 04:16
God of War Ragnarök,PS5,2022-11-18 19:58,2022-11-18 20:40
God of War Ragnarök,PS5,2022-11-18 03:50,2022-11-18 06:25
God of War Ragnarök,PS5,2022-11-17 19:36,2022-11-18 00:26
God of War Ragnarök,PS5,2022-11-17 13:16,2022-11-17 16:13
God of War Ragnarök,PS5,2022-11-16 21:45,2022-11-16 22:38
God of War Ragnarök,PS5,2022-11-16 00:14,2022-11-16 04:28
God of War Ragnarök,PS5,2022-11-15 01:33,2022-11-15 05:06
God of War Ragnarök,PS5,2022-11-14 00:37,2022-11-14 04:43
God of War Ragnarök,PS5,2022-11-12 23:32,2022-11-13 03:45
God of War Ragnarök,PS5,2022-11-12 03:17,2022-11-12 05:00
Grand Theft Auto V (PlayStation®5),PS5,2022-10-03 02:01,2022-10-03 02:23
Grand Theft Auto V (PlayStation®5),PS5,2022-10-02 13:59,2022-10-02 15:54
Grand Theft Auto V (PlayStation®5),PS5,2022-09-30 22:40,2022-10-01 02:50
Grand Theft Auto V (PlayStation®5),PS5,2022-09-27 22:38,2022-09-28 00:16
Grand Theft Auto V (PlayStation®5),PS5,2022-09-27 19:27,2022-09-27 21:09
Grand Theft Auto V (PlayStation®5),PS5,2022-09-26 20:58,2022-09-26 23:38
Grand Theft Auto V (PlayStation®5),PS5,2022-09-25 23:56,2022-09-26 02:36
Grand Theft Auto V (PlayStation®5),PS5,2022-09-25 14:57,2022-09-25 16:38
Grand Theft Auto V (PlayStation®5),PS5,2022-09-25 02:04,2022-09-25 02:12
Grand Theft Auto V (PlayStation®5),PS5,2022-09-23 20:33,2022-09-23 23:38
Wo Long: Fallen Dynasty,PS5,2022-09-18 15:26,2022-09-18 16:58
Grand Theft Auto V (PlayStation®5),PS5,2022-08-31 00:42,2022-08-31 01:15
Grand Theft Auto V (PlayStation®5),PS5,2022-08-18 13:43,2022-08-18 15:12
Grand Theft Auto V (PlayStation®5),PS5,2022-08-18 00:42,2022-08-18 01:58
Tony Hawk's™ Pro Skater™ 1 + 2,PS5,2022-08-18 00:40,2022-08-18 00:42
Tony Hawk's™ Pro Skater™ 1 + 2,PS5,2022-08-14 15:46,2022-08-14 16:10
FINAL FANTASY VII,PS5,2022-07-26 18:56,2022-07-26 20:22
FINAL FANTASY VII REMAKE,PS5,2022-07-26 17:39,2022-07-26 18:53
FINAL FANTASY VII REMAKE,PS5,2022-07-25 22:12,2022-07-26 04:37
FINAL FANTASY VII REMAKE,PS5,2022-07-24 00:09,2022-07-24 05:33
FINAL FANTASY VII REMAKE,PS5,2022-07-23 23:34,2022-07-23 23:48
FINAL FANTASY VII REMAKE,PS5,2022-07-23 18:05,2022-07-23 19:44
FINAL FANTASY VII REMAKE,PS5,2022-07-22 17:07,2022-07-23 01:48
FINAL FANTASY VII REMAKE,PS5,2022-07-22 15:26,2022-07-22 15:59
FINAL FANTASY VII REMAKE,PS5,2022-07-21 21:27,2022-07-21 22:43
FINAL FANTASY VII REMAKE,PS5,2022-07-21 20:48,2022-07-21 20:58
FINAL FANTASY VII REMAKE,PS5,2022-07-21 18:35,2022-07-21 18:36
Stray,PS5,2022-07-21 01:24,2022-07-21 02:34
Stray,PS5,2022-07-19 23:49,2022-07-20 03:24
Atelier Ayesha ~The Alchemist of Dusk~,PS3,2022-07-04 03:26,2022-07-04 03:34
Red Dead Redemption,PS3,2022-07-04 02:36,2022-07-04 03:14
Ghost of Tsushima,PS5,2022-07-04 01:38,2022-07-04 02:34
Dark Cloud™,PS4,2022-07-01 23:48,2022-07-02 00:04
Atelier Ayesha ~The Alchemist of Dusk~,PS3,2022-07-01 23:20,2022-07-01 23:46
Resident Evil Directors Cut,PS5,2022-07-01 23:14,2022-07-01 23:20
ELEX II,PS5,2022-07-01 22:48,2022-07-01 23:13
OlliOlli World,PS5,2022-07-01 21:30,2022-07-01 22:30
Deep Rock Galactic,PS5,2022-06-16 05:30,2022-06-16 06:14
Curse of the Dead Gods,PS5,2022-06-16 05:00,2022-06-16 05:22
Persona 5: Dancing in Starlight,PS5,2022-04-29 20:14,2022-04-29 20:15
Persona 5: Dancing in Starlight,PS5,2022-04-29 00:18,2022-04-29 00:44
Dying Light 2,PS5,2022-04-14 01:26,2022-04-14 01:27
Grand Theft Auto V (PlayStation®5),PS5,2022-03-24 16:26,2022-03-24 16:27
Grand Theft Auto V (PlayStation®5),PS5,2022-03-21 15:52,2022-03-21 15:59
Horizon Forbidden West,PS5,2022-02-23 19:37,2022-02-24 00:24
Horizon Forbidden West,PS5,2022-02-23 13:57,2022-02-23 17:44
Horizon Forbidden West,PS5,2022-02-22 18:05,2022-02-23 05:26
Horizon Forbidden West,PS5,2022-02-22 15:39,2022-02-22 17:02
Horizon Forbidden West,PS5,2022-02-22 00:05,2022-02-22 04:08
Horizon Forbidden West,PS5,2022-02-20 15:39,2022-02-20 23:08
Horizon Forbidden West,PS5,2022-02-20 14:54,2022-02-20 15:09
Horizon Forbidden West,PS5,2022-02-19 23:37,2022-02-20 04:45
Horizon Forbidden West,PS5,2022-02-18 23:15,2022-02-19 03:27
Assassin's Creed® Origins,PS5,2022-02-18 21:49,2022-02-18 23:15
Assassin's Creed® Origins,PS5,2022-01-17 02:38,2022-01-17 02:50
Deep Rock Galactic,PS5,2022-01-17 00:57,2022-01-17 02:35
HITMAN 3,PS5,2021-11-17 00:35,2021-11-17 01:17
HITMAN 3,PS5,2021-11-08 01:59,2021-11-08 06:17
HITMAN 3,PS5,2021-11-07 03:10,2021-11-07 05:23
HITMAN 3,PS5,2021-11-06 04:23,2021-11-06 08:49
HITMAN 3,PS5,2021-11-06 02:17,2021-11-06 03:31
HITMAN 3,PS5,2021-11-05 21:33,2021-11-05 23:24
HITMAN 3,PS5,2021-11-05 03:09,2021-11-05 03:34
HITMAN 3,PS5,2021-11-05 00:47,2021-11-05 02:26
HITMAN 3,PS5,2021-11-04 20:27,2021-11-04 23:32
HITMAN 3,PS5,2021-11-04 01:34,2021-11-04 05:33
RESIDENT EVIL 3,PS5,2021-11-03 23:14,2021-11-03 23:56
RESIDENT EVIL 3,PS5,2021-11-02 23:56,2021-11-03 05:10
RESIDENT EVIL 3,PS5,2021-11-02 21:22,2021-11-02 23:23
RESIDENT EVIL 3,PS5,2021-11-02 05:36,2021-11-02 06:56
HITMAN 3,PS5,2021-11-02 03:00,2021-11-02 05:36
HITMAN 3,PS5,2021-11-02 01:19,2021-11-02 01:25
HITMAN™ 2,PS5,2021-11-02 01:09,2021-11-02 01:19
HITMAN 3,PS5,2021-11-01 23:45,2021-11-02 01:09
RESIDENT EVIL 3,PS5,2021-11-01 19:32,2021-11-01 19:47
Marvel's Spider-Man: Miles Morales,PS5,2021-10-17 01:06,2021-10-17 03:27
Marvel's Spider-Man: Miles Morales,PS5,2021-10-16 20:58,2021-10-16 22:00
Marvel's Spider-Man: Miles Morales,PS5,2021-10-05 02:30,2021-10-05 03:27
Marvel's Spider-Man: Miles Morales,PS5,2021-10-03 23:12,2021-10-04 01:21
Marvel's Spider-Man: Miles Morales,PS5,2021-10-03 03:02,2021-10-03 04:42
Marvel's Spider-Man: Miles Morales,PS5,2021-10-02 20:12,2021-10-02 21:10
Marvel's Spider-Man: Miles Morales,PS5,2021-10-02 01:40,2021-10-02 03:36
Marvel's Spider-Man: Miles Morales,PS5,2021-10-01 04:34,2021-10-01 05:30
DEATHLOOP,PS5,2021-10-01 01:12,2021-10-01 04:27
DEATHLOOP,PS5,2021-09-30 03:04,2021-09-30 06:30
DEATHLOOP,PS5,2021-09-29 00:28,2021-09-29 05:08
Persona 5 Royal,PS5,2021-09-28 00:36,2021-09-28 03:08
Persona 5 Royal,PS5,2021-09-27 02:16,2021-09-27 05:56
Persona 5 Royal,PS5,2021-09-26 14:54,2021-09-26 16:32
Persona 5 Royal,PS5,2021-09-25 18:43,2021-09-25 23:26
Persona 5 Royal,PS5,2021-09-24 21:41,2021-09-25 03:40
Persona 5 Royal,PS5,2021-09-23 00:18,2021-09-23 06:26
Persona 5 Royal,PS5,2021-09-21 20:27,2021-09-22 05:43
Persona 5 Royal,PS5,2021-09-21 01:07,2021-09-21 06:06
Borderlands: The Handsome Collection,PS5,2021-09-20 23:59,2021-09-21 01:07
Persona 5 Royal,PS5,2021-09-20 23:53,2021-09-20 23:59
DEATHLOOP,PS5,2021-09-20 02:03,2021-09-20 06:29
DEATHLOOP,PS5,2021-09-19 19:49,2021-09-20 01:16
Borderlands: The Handsome Collection,PS5,2021-09-19 00:51,2021-09-19 03:41
Borderlands: The Handsome Collection,PS5,2021-09-17 23:45,2021-09-18 01:48
Borderlands: The Handsome Collection,PS5,2021-09-17 23:40,2021-09-17 23:41
DEATHLOOP,PS5,2021-09-17 16:48,2021-09-17 18:56
DEATHLOOP,PS5,2021-09-17 03:02,2021-09-17 04:39
DEATHLOOP,PS5,2021-09-17 00:03,2021-09-17 02:53
DEATHLOOP,PS5,2021-09-16 18:39,2021-09-16 21:12
Persona 5 Royal,PS5,2021-09-16 18:29,2021-09-16 18:30
Persona 5 Royal,PS5,2021-09-16 02:26,2021-09-16 06:13
Persona 5 Royal,PS5,2021-09-16 02:20,2021-09-16 02:21
Persona 5 Royal,PS5,2021-09-15 01:48,2021-09-15 06:07
Persona 5 Royal,PS5,2021-09-14 22:21,2021-09-15 01:22
Persona 5 Royal,PS5,2021-09-14 02:01,2021-09-14 05:48
Persona 5 Royal,PS5,2021-09-14 00:24,2021-09-14 01:46
Persona 5 Royal,PS5,2021-08-12 05:04,2021-08-12 07:05
Persona 5 Royal,PS5,2021-08-11 05:02,2021-08-11 06:48
Persona 5 Royal,PS5,2021-08-09 00:37,2021-08-09 06:15
Persona 5 Royal,PS5,2021-08-08 00:31,2021-08-08 08:01
Persona 5 Royal,PS5,2021-08-07 19:51,2021-08-07 22:50
Persona 5 Royal,PS5,2021-08-06 23:51,2021-08-07 01:35
Persona 5 Royal,PS5,2021-08-06 19:26,2021-08-06 22:26
Persona 5 Royal,PS5,2021-08-06 02:42,2021-08-06 06:51
Persona 5 Royal,PS5,2021-08-06 00:37,2021-08-06 01:54
Far Cry® 5,PS5,2021-08-01 23:27,2021-08-02 02:09
Far Cry® 5,PS5,2021-08-01 18:10,2021-08-01 19:40
STAR WARS™: Squadrons,PS5,2021-08-01 18:02,2021-08-01 18:10
STAR WARS™: Squadrons,PS5,2021-08-01 00:24,2021-08-01 00:30
STEEP,PS5,2021-08-01 00:15,2021-08-01 00:24
Red Dead Redemption 2,PS5,2021-07-31 23:48,2021-08-01 00:13
Persona 5 Royal,PS5,2021-07-30 19:09,2021-07-30 19:10
Persona 5 Royal,PS5,2021-07-29 03:41,2021-07-29 04:59
Persona 5 Royal,PS5,2021-07-28 02:32,2021-07-28 03:07
Demon's Souls,PS5,2021-07-28 00:12,2021-07-28 02:32
Red Dead Redemption 2,PS5,2021-07-27 23:20,2021-07-27 23:23
Red Dead Redemption 2,PS5,2021-07-26 00:42,2021-07-26 01:13
Ghost of Tsushima,PS5,2021-07-25 19:03,2021-07-25 22:12
Ghost of Tsushima,PS5,2021-07-25 18:52,2021-07-25 18:55
Rez Infinite,PS5,2021-07-25 18:32,2021-07-25 18:52
Returnal,PS5,2021-07-25 05:24,2021-07-25 05:26
Tom Clancy's The Division® 2,PS5,2021-07-25 02:12,2021-07-25 05:24
Returnal,PS5,2021-07-25 00:00,2021-07-25 02:12
Returnal,PS5,2021-07-24 16:13,2021-07-24 17:39
Returnal,PS5,2021-07-24 03:02,2021-07-24 07:02
Returnal,PS5,2021-07-23 18:08,2021-07-23 20:39
Returnal,PS5,2021-07-23 14:36,2021-07-23 15:38
Titanfall™ 2,PS5,2021-07-22 02:42,2021-07-22 03:20
Returnal,PS5,2021-07-20 02:12,2021-07-20 05:40
Returnal,PS5,2021-07-19 03:37,2021-07-19 05:24
Concrete Genie,PS5,2021-07-19 03:35,2021-07-19 03:37
Concrete Genie,PS5,2021-07-18 05:04,2021-07-18 05:30
Stranded Deep,PS5,2021-07-18 04:32,2021-07-18 04:58
Sniper Elite 4,PS5,2021-07-18 04:16,2021-07-18 04:32
Oddworld: Soulstorm,PS5,2021-07-18 04:00,2021-07-18 04:14
Zombie Army 4: Dead War,PS5,2021-07-18 03:48,2021-07-18 03:58
Sekiro™: Shadows Die Twice,PS5,2021-07-18 03:00,2021-07-18 03:48
Returnal,PS5,2021-07-17 17:41,2021-07-17 23:32
Returnal,PS5,2021-07-17 01:35,2021-07-17 06:07
Returnal,PS5,2021-07-17 00:23,2021-07-17 01:21
Another World - 20th Anniversary Edition,PS5,2021-07-15 22:09,2021-07-15 23:33
Sekiro™: Shadows Die Twice,PS5,2021-07-15 21:52,2021-07-15 22:09
Sekiro™: Shadows Die Twice,PS5,2021-07-15 19:07,2021-07-15 20:13
Sekiro™: Shadows Die Twice,PS5,2021-07-15 01:50,2021-07-15 03:31
Sekiro™: Shadows Die Twice,PS5,2021-07-15 00:12,2021-07-15 01:14
Sekiro™: Shadows Die Twice,PS5,2021-07-14 03:19,2021-07-14 05:13
Persona 5,PS5,2021-07-13 21:56,2021-07-13 21:58
Persona 5,PS5,2021-07-13 01:32,2021-07-13 02:59
Maquette,PS5,2021-07-13 01:30,2021-07-13 01:32
Maquette,PS5,2021-07-12 23:59,2021-07-13 00:34
ASTRO's PLAYROOM,PS5,2021-07-12 23:06,2021-07-12 23:59
Crash Bandicoot N. Sane Trilogy,PS5,2021-07-12 23:01,2021-07-12 23:06
Virtua Fighter 5 Ultimate Showdown,PS4,2021-07-02 19:46,2021-07-02 20:57
Bloodborne™,PS4,2021-04-03 19:50,2021-04-03 23:52
Tom Clancy's The Division® 2,PS4,2021-04-03 02:10,2021-04-03 05:22
Bloodborne™,PS4,2021-04-02 21:34,2021-04-03 02:10
Bloodborne™,PS4,2021-04-02 06:01,2021-04-02 08:11
Tom Clancy's The Division® 2,PS4,2021-04-02 04:24,2021-04-02 06:01
Tom Clancy's The Division® 2,PS4,2021-03-31 02:55,2021-03-31 05:50
Tom Clancy's The Division® 2,PS4,2021-03-29 02:00,2021-03-29 02:02
Tom Clancy's The Division® 2,PS4,2021-03-27 03:31,2021-03-27 06:29
Tom Clancy's The Division® 2,PS4,2021-03-26 05:10,2021-03-26 06:03
Remnant: From the Ashes,PS4,2021-03-23 03:20,2021-03-23 05:58
DARK SOULS™ II: Scholar of the First Sin,PS4,2021-03-20 01:31,2021-03-20 01:34
Remnant: From the Ashes,PS4,2021-03-12 01:29,2021-03-12 01:30
Remnant: From the Ashes,PS4,2021-03-08 02:41,2021-03-08 05:38
Remnant: From the Ashes,PS4,2021-03-07 03:21,2021-03-07 06:49
13 Sentinels: Aegis Rim,PS4,2021-03-07 03:20,2021-03-07 03:21
DARK SOULS™ II: Scholar of the First Sin,PS4,2020-10-24 23:43,2020-10-25 01:18
Ghost of Tsushima,PS4,2020-10-24 23:14,2020-10-24 23:22
1 name platform start end
2 Nioh 2 PS5 2022-12-17 19:34 2022-12-17 22:53
3 Nioh 2 PS5 2022-12-15 02:25 2022-12-15 03:57
4 Nioh 2 PS5 2022-12-13 02:41 2022-12-13 04:34
5 Nioh 2 PS5 2022-12-11 21:01 2022-12-11 23:21
6 VALKYRIE ELYSIUM PS5 2022-12-11 06:07 2022-12-11 06:20
7 Metal: Hellsinger PS5 2022-12-11 05:50 2022-12-11 06:07
8 Nioh 2 PS5 2022-12-11 04:31 2022-12-11 05:50
9 Nioh 2 PS5 2022-12-11 04:11 2022-12-11 04:26
10 Forspoken PS5 2022-12-10 22:29 2022-12-10 23:10
11 Nioh 2 PS5 2022-12-10 19:44 2022-12-10 22:29
12 Nioh 2 PS5 2022-12-09 02:14 2022-12-09 04:16
13 Nioh 2 PS5 2022-12-08 01:03 2022-12-08 01:57
14 Nioh 2 PS5 2022-12-07 00:43 2022-12-07 04:16
15 Nioh 2 PS5 2022-12-04 20:48 2022-12-04 23:31
16 Nioh 2 PS5 2022-12-04 04:26 2022-12-04 07:01
17 Nioh 2 PS5 2022-12-04 04:20 2022-12-04 04:22
18 Nioh 2 PS5 2022-11-26 19:18 2022-11-26 21:28
19 Nioh 2 PS5 2022-11-26 19:16 2022-11-26 19:18
20 Nioh 2 PS5 2022-11-26 02:46 2022-11-26 03:56
21 Nioh 2 PS5 2022-11-26 02:01 2022-11-26 02:43
22 God of War Ragnarök PS5 2022-11-24 23:03 2022-11-25 01:32
23 God of War Ragnarök PS5 2022-11-23 00:41 2022-11-23 07:52
24 God of War Ragnarök PS5 2022-11-21 22:52 2022-11-22 04:51
25 God of War Ragnarök PS5 2022-11-21 02:11 2022-11-21 05:13
26 God of War Ragnarök PS5 2022-11-20 21:34 2022-11-20 22:50
27 God of War Ragnarök PS5 2022-11-20 03:46 2022-11-20 05:52
28 God of War Ragnarök PS5 2022-11-19 14:30 2022-11-19 16:14
29 God of War Ragnarök PS5 2022-11-18 23:15 2022-11-19 04:16
30 God of War Ragnarök PS5 2022-11-18 19:58 2022-11-18 20:40
31 God of War Ragnarök PS5 2022-11-18 03:50 2022-11-18 06:25
32 God of War Ragnarök PS5 2022-11-17 19:36 2022-11-18 00:26
33 God of War Ragnarök PS5 2022-11-17 13:16 2022-11-17 16:13
34 God of War Ragnarök PS5 2022-11-16 21:45 2022-11-16 22:38
35 God of War Ragnarök PS5 2022-11-16 00:14 2022-11-16 04:28
36 God of War Ragnarök PS5 2022-11-15 01:33 2022-11-15 05:06
37 God of War Ragnarök PS5 2022-11-14 00:37 2022-11-14 04:43
38 God of War Ragnarök PS5 2022-11-12 23:32 2022-11-13 03:45
39 God of War Ragnarök PS5 2022-11-12 03:17 2022-11-12 05:00
40 Grand Theft Auto V (PlayStation®5) PS5 2022-10-03 02:01 2022-10-03 02:23
41 Grand Theft Auto V (PlayStation®5) PS5 2022-10-02 13:59 2022-10-02 15:54
42 Grand Theft Auto V (PlayStation®5) PS5 2022-09-30 22:40 2022-10-01 02:50
43 Grand Theft Auto V (PlayStation®5) PS5 2022-09-27 22:38 2022-09-28 00:16
44 Grand Theft Auto V (PlayStation®5) PS5 2022-09-27 19:27 2022-09-27 21:09
45 Grand Theft Auto V (PlayStation®5) PS5 2022-09-26 20:58 2022-09-26 23:38
46 Grand Theft Auto V (PlayStation®5) PS5 2022-09-25 23:56 2022-09-26 02:36
47 Grand Theft Auto V (PlayStation®5) PS5 2022-09-25 14:57 2022-09-25 16:38
48 Grand Theft Auto V (PlayStation®5) PS5 2022-09-25 02:04 2022-09-25 02:12
49 Grand Theft Auto V (PlayStation®5) PS5 2022-09-23 20:33 2022-09-23 23:38
50 Wo Long: Fallen Dynasty PS5 2022-09-18 15:26 2022-09-18 16:58
51 Grand Theft Auto V (PlayStation®5) PS5 2022-08-31 00:42 2022-08-31 01:15
52 Grand Theft Auto V (PlayStation®5) PS5 2022-08-18 13:43 2022-08-18 15:12
53 Grand Theft Auto V (PlayStation®5) PS5 2022-08-18 00:42 2022-08-18 01:58
54 Tony Hawk's™ Pro Skater™ 1 + 2 PS5 2022-08-18 00:40 2022-08-18 00:42
55 Tony Hawk's™ Pro Skater™ 1 + 2 PS5 2022-08-14 15:46 2022-08-14 16:10
56 FINAL FANTASY VII PS5 2022-07-26 18:56 2022-07-26 20:22
57 FINAL FANTASY VII REMAKE PS5 2022-07-26 17:39 2022-07-26 18:53
58 FINAL FANTASY VII REMAKE PS5 2022-07-25 22:12 2022-07-26 04:37
59 FINAL FANTASY VII REMAKE PS5 2022-07-24 00:09 2022-07-24 05:33
60 FINAL FANTASY VII REMAKE PS5 2022-07-23 23:34 2022-07-23 23:48
61 FINAL FANTASY VII REMAKE PS5 2022-07-23 18:05 2022-07-23 19:44
62 FINAL FANTASY VII REMAKE PS5 2022-07-22 17:07 2022-07-23 01:48
63 FINAL FANTASY VII REMAKE PS5 2022-07-22 15:26 2022-07-22 15:59
64 FINAL FANTASY VII REMAKE PS5 2022-07-21 21:27 2022-07-21 22:43
65 FINAL FANTASY VII REMAKE PS5 2022-07-21 20:48 2022-07-21 20:58
66 FINAL FANTASY VII REMAKE PS5 2022-07-21 18:35 2022-07-21 18:36
67 Stray PS5 2022-07-21 01:24 2022-07-21 02:34
68 Stray PS5 2022-07-19 23:49 2022-07-20 03:24
69 Atelier Ayesha ~The Alchemist of Dusk~ PS3 2022-07-04 03:26 2022-07-04 03:34
70 Red Dead Redemption PS3 2022-07-04 02:36 2022-07-04 03:14
71 Ghost of Tsushima PS5 2022-07-04 01:38 2022-07-04 02:34
72 Dark Cloud™ PS4 2022-07-01 23:48 2022-07-02 00:04
73 Atelier Ayesha ~The Alchemist of Dusk~ PS3 2022-07-01 23:20 2022-07-01 23:46
74 Resident Evil Director’s Cut PS5 2022-07-01 23:14 2022-07-01 23:20
75 ELEX II PS5 2022-07-01 22:48 2022-07-01 23:13
76 OlliOlli World PS5 2022-07-01 21:30 2022-07-01 22:30
77 Deep Rock Galactic PS5 2022-06-16 05:30 2022-06-16 06:14
78 Curse of the Dead Gods PS5 2022-06-16 05:00 2022-06-16 05:22
79 Persona 5: Dancing in Starlight PS5 2022-04-29 20:14 2022-04-29 20:15
80 Persona 5: Dancing in Starlight PS5 2022-04-29 00:18 2022-04-29 00:44
81 Dying Light 2 PS5 2022-04-14 01:26 2022-04-14 01:27
82 Grand Theft Auto V (PlayStation®5) PS5 2022-03-24 16:26 2022-03-24 16:27
83 Grand Theft Auto V (PlayStation®5) PS5 2022-03-21 15:52 2022-03-21 15:59
84 Horizon Forbidden West PS5 2022-02-23 19:37 2022-02-24 00:24
85 Horizon Forbidden West PS5 2022-02-23 13:57 2022-02-23 17:44
86 Horizon Forbidden West PS5 2022-02-22 18:05 2022-02-23 05:26
87 Horizon Forbidden West PS5 2022-02-22 15:39 2022-02-22 17:02
88 Horizon Forbidden West PS5 2022-02-22 00:05 2022-02-22 04:08
89 Horizon Forbidden West PS5 2022-02-20 15:39 2022-02-20 23:08
90 Horizon Forbidden West PS5 2022-02-20 14:54 2022-02-20 15:09
91 Horizon Forbidden West PS5 2022-02-19 23:37 2022-02-20 04:45
92 Horizon Forbidden West PS5 2022-02-18 23:15 2022-02-19 03:27
93 Assassin's Creed® Origins PS5 2022-02-18 21:49 2022-02-18 23:15
94 Assassin's Creed® Origins PS5 2022-01-17 02:38 2022-01-17 02:50
95 Deep Rock Galactic PS5 2022-01-17 00:57 2022-01-17 02:35
96 HITMAN 3 PS5 2021-11-17 00:35 2021-11-17 01:17
97 HITMAN 3 PS5 2021-11-08 01:59 2021-11-08 06:17
98 HITMAN 3 PS5 2021-11-07 03:10 2021-11-07 05:23
99 HITMAN 3 PS5 2021-11-06 04:23 2021-11-06 08:49
100 HITMAN 3 PS5 2021-11-06 02:17 2021-11-06 03:31
101 HITMAN 3 PS5 2021-11-05 21:33 2021-11-05 23:24
102 HITMAN 3 PS5 2021-11-05 03:09 2021-11-05 03:34
103 HITMAN 3 PS5 2021-11-05 00:47 2021-11-05 02:26
104 HITMAN 3 PS5 2021-11-04 20:27 2021-11-04 23:32
105 HITMAN 3 PS5 2021-11-04 01:34 2021-11-04 05:33
106 RESIDENT EVIL 3 PS5 2021-11-03 23:14 2021-11-03 23:56
107 RESIDENT EVIL 3 PS5 2021-11-02 23:56 2021-11-03 05:10
108 RESIDENT EVIL 3 PS5 2021-11-02 21:22 2021-11-02 23:23
109 RESIDENT EVIL 3 PS5 2021-11-02 05:36 2021-11-02 06:56
110 HITMAN 3 PS5 2021-11-02 03:00 2021-11-02 05:36
111 HITMAN 3 PS5 2021-11-02 01:19 2021-11-02 01:25
112 HITMAN™ 2 PS5 2021-11-02 01:09 2021-11-02 01:19
113 HITMAN 3 PS5 2021-11-01 23:45 2021-11-02 01:09
114 RESIDENT EVIL 3 PS5 2021-11-01 19:32 2021-11-01 19:47
115 Marvel's Spider-Man: Miles Morales PS5 2021-10-17 01:06 2021-10-17 03:27
116 Marvel's Spider-Man: Miles Morales PS5 2021-10-16 20:58 2021-10-16 22:00
117 Marvel's Spider-Man: Miles Morales PS5 2021-10-05 02:30 2021-10-05 03:27
118 Marvel's Spider-Man: Miles Morales PS5 2021-10-03 23:12 2021-10-04 01:21
119 Marvel's Spider-Man: Miles Morales PS5 2021-10-03 03:02 2021-10-03 04:42
120 Marvel's Spider-Man: Miles Morales PS5 2021-10-02 20:12 2021-10-02 21:10
121 Marvel's Spider-Man: Miles Morales PS5 2021-10-02 01:40 2021-10-02 03:36
122 Marvel's Spider-Man: Miles Morales PS5 2021-10-01 04:34 2021-10-01 05:30
123 DEATHLOOP PS5 2021-10-01 01:12 2021-10-01 04:27
124 DEATHLOOP PS5 2021-09-30 03:04 2021-09-30 06:30
125 DEATHLOOP PS5 2021-09-29 00:28 2021-09-29 05:08
126 Persona 5 Royal PS5 2021-09-28 00:36 2021-09-28 03:08
127 Persona 5 Royal PS5 2021-09-27 02:16 2021-09-27 05:56
128 Persona 5 Royal PS5 2021-09-26 14:54 2021-09-26 16:32
129 Persona 5 Royal PS5 2021-09-25 18:43 2021-09-25 23:26
130 Persona 5 Royal PS5 2021-09-24 21:41 2021-09-25 03:40
131 Persona 5 Royal PS5 2021-09-23 00:18 2021-09-23 06:26
132 Persona 5 Royal PS5 2021-09-21 20:27 2021-09-22 05:43
133 Persona 5 Royal PS5 2021-09-21 01:07 2021-09-21 06:06
134 Borderlands: The Handsome Collection PS5 2021-09-20 23:59 2021-09-21 01:07
135 Persona 5 Royal PS5 2021-09-20 23:53 2021-09-20 23:59
136 DEATHLOOP PS5 2021-09-20 02:03 2021-09-20 06:29
137 DEATHLOOP PS5 2021-09-19 19:49 2021-09-20 01:16
138 Borderlands: The Handsome Collection PS5 2021-09-19 00:51 2021-09-19 03:41
139 Borderlands: The Handsome Collection PS5 2021-09-17 23:45 2021-09-18 01:48
140 Borderlands: The Handsome Collection PS5 2021-09-17 23:40 2021-09-17 23:41
141 DEATHLOOP PS5 2021-09-17 16:48 2021-09-17 18:56
142 DEATHLOOP PS5 2021-09-17 03:02 2021-09-17 04:39
143 DEATHLOOP PS5 2021-09-17 00:03 2021-09-17 02:53
144 DEATHLOOP PS5 2021-09-16 18:39 2021-09-16 21:12
145 Persona 5 Royal PS5 2021-09-16 18:29 2021-09-16 18:30
146 Persona 5 Royal PS5 2021-09-16 02:26 2021-09-16 06:13
147 Persona 5 Royal PS5 2021-09-16 02:20 2021-09-16 02:21
148 Persona 5 Royal PS5 2021-09-15 01:48 2021-09-15 06:07
149 Persona 5 Royal PS5 2021-09-14 22:21 2021-09-15 01:22
150 Persona 5 Royal PS5 2021-09-14 02:01 2021-09-14 05:48
151 Persona 5 Royal PS5 2021-09-14 00:24 2021-09-14 01:46
152 Persona 5 Royal PS5 2021-08-12 05:04 2021-08-12 07:05
153 Persona 5 Royal PS5 2021-08-11 05:02 2021-08-11 06:48
154 Persona 5 Royal PS5 2021-08-09 00:37 2021-08-09 06:15
155 Persona 5 Royal PS5 2021-08-08 00:31 2021-08-08 08:01
156 Persona 5 Royal PS5 2021-08-07 19:51 2021-08-07 22:50
157 Persona 5 Royal PS5 2021-08-06 23:51 2021-08-07 01:35
158 Persona 5 Royal PS5 2021-08-06 19:26 2021-08-06 22:26
159 Persona 5 Royal PS5 2021-08-06 02:42 2021-08-06 06:51
160 Persona 5 Royal PS5 2021-08-06 00:37 2021-08-06 01:54
161 Far Cry® 5 PS5 2021-08-01 23:27 2021-08-02 02:09
162 Far Cry® 5 PS5 2021-08-01 18:10 2021-08-01 19:40
163 STAR WARS™: Squadrons PS5 2021-08-01 18:02 2021-08-01 18:10
164 STAR WARS™: Squadrons PS5 2021-08-01 00:24 2021-08-01 00:30
165 STEEP PS5 2021-08-01 00:15 2021-08-01 00:24
166 Red Dead Redemption 2 PS5 2021-07-31 23:48 2021-08-01 00:13
167 Persona 5 Royal PS5 2021-07-30 19:09 2021-07-30 19:10
168 Persona 5 Royal PS5 2021-07-29 03:41 2021-07-29 04:59
169 Persona 5 Royal PS5 2021-07-28 02:32 2021-07-28 03:07
170 Demon's Souls PS5 2021-07-28 00:12 2021-07-28 02:32
171 Red Dead Redemption 2 PS5 2021-07-27 23:20 2021-07-27 23:23
172 Red Dead Redemption 2 PS5 2021-07-26 00:42 2021-07-26 01:13
173 Ghost of Tsushima PS5 2021-07-25 19:03 2021-07-25 22:12
174 Ghost of Tsushima PS5 2021-07-25 18:52 2021-07-25 18:55
175 Rez Infinite PS5 2021-07-25 18:32 2021-07-25 18:52
176 Returnal PS5 2021-07-25 05:24 2021-07-25 05:26
177 Tom Clancy's The Division® 2 PS5 2021-07-25 02:12 2021-07-25 05:24
178 Returnal PS5 2021-07-25 00:00 2021-07-25 02:12
179 Returnal PS5 2021-07-24 16:13 2021-07-24 17:39
180 Returnal PS5 2021-07-24 03:02 2021-07-24 07:02
181 Returnal PS5 2021-07-23 18:08 2021-07-23 20:39
182 Returnal PS5 2021-07-23 14:36 2021-07-23 15:38
183 Titanfall™ 2 PS5 2021-07-22 02:42 2021-07-22 03:20
184 Returnal PS5 2021-07-20 02:12 2021-07-20 05:40
185 Returnal PS5 2021-07-19 03:37 2021-07-19 05:24
186 Concrete Genie PS5 2021-07-19 03:35 2021-07-19 03:37
187 Concrete Genie PS5 2021-07-18 05:04 2021-07-18 05:30
188 Stranded Deep PS5 2021-07-18 04:32 2021-07-18 04:58
189 Sniper Elite 4 PS5 2021-07-18 04:16 2021-07-18 04:32
190 Oddworld: Soulstorm PS5 2021-07-18 04:00 2021-07-18 04:14
191 Zombie Army 4: Dead War PS5 2021-07-18 03:48 2021-07-18 03:58
192 Sekiro™: Shadows Die Twice PS5 2021-07-18 03:00 2021-07-18 03:48
193 Returnal PS5 2021-07-17 17:41 2021-07-17 23:32
194 Returnal PS5 2021-07-17 01:35 2021-07-17 06:07
195 Returnal PS5 2021-07-17 00:23 2021-07-17 01:21
196 Another World - 20th Anniversary Edition PS5 2021-07-15 22:09 2021-07-15 23:33
197 Sekiro™: Shadows Die Twice PS5 2021-07-15 21:52 2021-07-15 22:09
198 Sekiro™: Shadows Die Twice PS5 2021-07-15 19:07 2021-07-15 20:13
199 Sekiro™: Shadows Die Twice PS5 2021-07-15 01:50 2021-07-15 03:31
200 Sekiro™: Shadows Die Twice PS5 2021-07-15 00:12 2021-07-15 01:14
201 Sekiro™: Shadows Die Twice PS5 2021-07-14 03:19 2021-07-14 05:13
202 Persona 5 PS5 2021-07-13 21:56 2021-07-13 21:58
203 Persona 5 PS5 2021-07-13 01:32 2021-07-13 02:59
204 Maquette PS5 2021-07-13 01:30 2021-07-13 01:32
205 Maquette PS5 2021-07-12 23:59 2021-07-13 00:34
206 ASTRO's PLAYROOM PS5 2021-07-12 23:06 2021-07-12 23:59
207 Crash Bandicoot N. Sane Trilogy PS5 2021-07-12 23:01 2021-07-12 23:06
208 Virtua Fighter 5 Ultimate Showdown PS4 2021-07-02 19:46 2021-07-02 20:57
209 Bloodborne™ PS4 2021-04-03 19:50 2021-04-03 23:52
210 Tom Clancy's The Division® 2 PS4 2021-04-03 02:10 2021-04-03 05:22
211 Bloodborne™ PS4 2021-04-02 21:34 2021-04-03 02:10
212 Bloodborne™ PS4 2021-04-02 06:01 2021-04-02 08:11
213 Tom Clancy's The Division® 2 PS4 2021-04-02 04:24 2021-04-02 06:01
214 Tom Clancy's The Division® 2 PS4 2021-03-31 02:55 2021-03-31 05:50
215 Tom Clancy's The Division® 2 PS4 2021-03-29 02:00 2021-03-29 02:02
216 Tom Clancy's The Division® 2 PS4 2021-03-27 03:31 2021-03-27 06:29
217 Tom Clancy's The Division® 2 PS4 2021-03-26 05:10 2021-03-26 06:03
218 Remnant: From the Ashes PS4 2021-03-23 03:20 2021-03-23 05:58
219 DARK SOULS™ II: Scholar of the First Sin PS4 2021-03-20 01:31 2021-03-20 01:34
220 Remnant: From the Ashes PS4 2021-03-12 01:29 2021-03-12 01:30
221 Remnant: From the Ashes PS4 2021-03-08 02:41 2021-03-08 05:38
222 Remnant: From the Ashes PS4 2021-03-07 03:21 2021-03-07 06:49
223 13 Sentinels: Aegis Rim PS4 2021-03-07 03:20 2021-03-07 03:21
224 DARK SOULS™ II: Scholar of the First Sin PS4 2020-10-24 23:43 2020-10-25 01:18
225 Ghost of Tsushima PS4 2020-10-24 23:14 2020-10-24 23:22

View File

@ -0,0 +1,71 @@
- model: games.game
pk: 1
fields:
name: Nioh 2
wikidata: Q67482292
- model: games.game
pk: 2
fields:
name: Elden Ring
wikidata: Q64826862
- model: games.game
pk: 3
fields:
name: Cyberpunk 2077
wikidata: Q3182559
- model: games.purchase
pk: 1
fields:
game: 1
platform: 1
date_purchased: 2021-02-13
date_refunded: null
- model: games.purchase
pk: 2
fields:
game: 2
platform: 1
date_purchased: 2022-02-24
date_refunded: null
- model: games.purchase
pk: 3
fields:
game: 3
platform: 1
date_purchased: 2020-12-07
date_refunded: null
- model: games.platform
pk: 1
fields:
name: Steam
group: PC
- model: games.platform
pk: 3
fields:
name: Xbox Gamepass
group: PC
- model: games.platform
pk: 4
fields:
name: Epic Games Store
group: PC
- model: games.platform
pk: 5
fields:
name: Playstation 5
group: Playstation
- model: games.platform
pk: 6
fields:
name: Playstation 4
group: Playstation
- model: games.platform
pk: 7
fields:
name: Nintendo Switch
group: Nintendo
- model: games.platform
pk: 8
fields:
name: Nintendo 3DS
group: Nintendo

179
games/forms.py Normal file
View File

@ -0,0 +1,179 @@
from django import forms
from django.urls import reverse
from common.utils import safe_getattr
from games.models import Device, Edition, Game, Platform, Purchase, Session
custom_date_widget = forms.DateInput(attrs={"type": "date"})
custom_datetime_widget = forms.DateTimeInput(
attrs={"type": "datetime-local"}, format="%Y-%m-%d %H:%M"
)
autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"})
class SessionForm(forms.ModelForm):
# purchase = forms.ModelChoiceField(
# queryset=Purchase.objects.filter(date_refunded=None).order_by("edition__name")
# )
purchase = forms.ModelChoiceField(
queryset=Purchase.objects.order_by("edition__sort_name"),
widget=forms.Select(attrs={"autofocus": "autofocus"}),
)
device = forms.ModelChoiceField(queryset=Device.objects.order_by("name"))
class Meta:
widgets = {
"timestamp_start": custom_datetime_widget,
"timestamp_end": custom_datetime_widget,
}
model = Session
fields = [
"purchase",
"timestamp_start",
"timestamp_end",
"duration_manual",
"device",
"note",
]
class EditionChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj) -> str:
return f"{obj.sort_name} ({obj.platform}, {obj.year_released})"
class IncludePlatformSelect(forms.Select):
def create_option(self, name, value, *args, **kwargs):
option = super().create_option(name, value, *args, **kwargs)
if platform_id := safe_getattr(value, "instance.platform.id"):
option["attrs"]["data-platform"] = platform_id
return option
class PurchaseForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Automatically update related_purchase <select/>
# to only include purchases of the selected edition.
related_purchase_by_edition_url = reverse("related_purchase_by_edition")
self.fields["edition"].widget.attrs.update(
{
"hx-trigger": "load, click",
"hx-get": related_purchase_by_edition_url,
"hx-target": "#id_related_purchase",
"hx-swap": "outerHTML",
}
)
edition = EditionChoiceField(
queryset=Edition.objects.order_by("sort_name"),
widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}),
)
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
related_purchase = forms.ModelChoiceField(
queryset=Purchase.objects.filter(type=Purchase.GAME).order_by(
"edition__sort_name"
),
required=False,
)
class Meta:
widgets = {
"date_purchased": custom_date_widget,
"date_refunded": custom_date_widget,
"date_finished": custom_date_widget,
"date_dropped": custom_date_widget,
}
model = Purchase
fields = [
"edition",
"platform",
"date_purchased",
"date_refunded",
"date_finished",
"date_dropped",
"infinite",
"price",
"price_currency",
"ownership_type",
"type",
"related_purchase",
"name",
]
def clean(self):
cleaned_data = super().clean()
purchase_type = cleaned_data.get("type")
related_purchase = cleaned_data.get("related_purchase")
name = cleaned_data.get("name")
# Set the type on the instance to use get_type_display()
# This is safe because we're not saving the instance.
self.instance.type = purchase_type
if purchase_type != Purchase.GAME:
type_display = self.instance.get_type_display()
if not related_purchase:
self.add_error(
"related_purchase",
f"{type_display} must have a related purchase.",
)
if not name:
self.add_error("name", f"{type_display} must have a name.")
return cleaned_data
class IncludeNameSelect(forms.Select):
def create_option(self, name, value, *args, **kwargs):
option = super().create_option(name, value, *args, **kwargs)
if value:
option["attrs"]["data-name"] = value.instance.name
option["attrs"]["data-year"] = value.instance.year_released
return option
class GameModelChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
# Use sort_name as the label for the option
return obj.sort_name
class EditionForm(forms.ModelForm):
game = GameModelChoiceField(
queryset=Game.objects.order_by("sort_name"),
widget=IncludeNameSelect(attrs={"autofocus": "autofocus"}),
)
platform = forms.ModelChoiceField(
queryset=Platform.objects.order_by("name"), required=False
)
class Meta:
model = Edition
fields = ["game", "name", "sort_name", "platform", "year_released", "wikidata"]
class GameForm(forms.ModelForm):
class Meta:
model = Game
fields = ["name", "sort_name", "year_released", "wikidata"]
widgets = {"name": autofocus_input_widget}
class PlatformForm(forms.ModelForm):
class Meta:
model = Platform
fields = [
"name",
"icon",
"group",
]
widgets = {"name": autofocus_input_widget}
class DeviceForm(forms.ModelForm):
class Meta:
model = Device
fields = ["name", "type"]
widgets = {"name": autofocus_input_widget}

View File

@ -0,0 +1 @@
from .game import Mutation as GameMutation

View File

@ -0,0 +1,29 @@
import graphene
from games.graphql.types import Game
from games.models import Game as GameModel
class UpdateGameMutation(graphene.Mutation):
class Arguments:
id = graphene.ID(required=True)
name = graphene.String()
year_released = graphene.Int()
wikidata = graphene.String()
game = graphene.Field(Game)
def mutate(self, info, id, name=None, year_released=None, wikidata=None):
game_instance = GameModel.objects.get(pk=id)
if name is not None:
game_instance.name = name
if year_released is not None:
game_instance.year_released = year_released
if wikidata is not None:
game_instance.wikidata = wikidata
game_instance.save()
return UpdateGameMutation(game=game_instance)
class Mutation(graphene.ObjectType):
update_game = UpdateGameMutation.Field()

View File

@ -0,0 +1,6 @@
from .device import Query as DeviceQuery
from .edition import Query as EditionQuery
from .game import Query as GameQuery
from .platform import Query as PlatformQuery
from .purchase import Query as PurchaseQuery
from .session import Query as SessionQuery

View File

@ -0,0 +1,11 @@
import graphene
from games.graphql.types import Device
from games.models import Device as DeviceModel
class Query(graphene.ObjectType):
devices = graphene.List(Device)
def resolve_devices(self, info, **kwargs):
return DeviceModel.objects.all()

View File

@ -0,0 +1,11 @@
import graphene
from games.graphql.types import Edition
from games.models import Game as EditionModel
class Query(graphene.ObjectType):
editions = graphene.List(Edition)
def resolve_editions(self, info, **kwargs):
return EditionModel.objects.all()

View File

@ -0,0 +1,18 @@
import graphene
from games.graphql.types import Game
from games.models import Game as GameModel
class Query(graphene.ObjectType):
games = graphene.List(Game)
game_by_name = graphene.Field(Game, name=graphene.String(required=True))
def resolve_games(self, info, **kwargs):
return GameModel.objects.all()
def resolve_game_by_name(self, info, name):
try:
return GameModel.objects.get(name=name)
except GameModel.DoesNotExist:
return None

View File

@ -0,0 +1,11 @@
import graphene
from games.graphql.types import Platform
from games.models import Platform as PlatformModel
class Query(graphene.ObjectType):
platforms = graphene.List(Platform)
def resolve_platforms(self, info, **kwargs):
return PlatformModel.objects.all()

View File

@ -0,0 +1,11 @@
import graphene
from games.graphql.types import Purchase
from games.models import Purchase as PurchaseModel
class Query(graphene.ObjectType):
purchases = graphene.List(Purchase)
def resolve_purchases(self, info, **kwargs):
return PurchaseModel.objects.all()

View File

@ -0,0 +1,11 @@
import graphene
from games.graphql.types import Session
from games.models import Session as SessionModel
class Query(graphene.ObjectType):
sessions = graphene.List(Session)
def resolve_sessions(self, info, **kwargs):
return SessionModel.objects.all()

44
games/graphql/types.py Normal file
View File

@ -0,0 +1,44 @@
from graphene_django import DjangoObjectType
from games.models import Device as DeviceModel
from games.models import Edition as EditionModel
from games.models import Game as GameModel
from games.models import Platform as PlatformModel
from games.models import Purchase as PurchaseModel
from games.models import Session as SessionModel
class Game(DjangoObjectType):
class Meta:
model = GameModel
fields = "__all__"
class Edition(DjangoObjectType):
class Meta:
model = EditionModel
fields = "__all__"
class Purchase(DjangoObjectType):
class Meta:
model = PurchaseModel
fields = "__all__"
class Session(DjangoObjectType):
class Meta:
model = SessionModel
fields = "__all__"
class Platform(DjangoObjectType):
class Meta:
model = PlatformModel
fields = "__all__"
class Device(DjangoObjectType):
class Meta:
model = DeviceModel
fields = "__all__"

View File

@ -0,0 +1,24 @@
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.utils.timezone import now
from django_q.models import Schedule
from django_q.tasks import schedule
class Command(BaseCommand):
help = "Manually schedule the next update_converted_prices task"
def handle(self, *args, **kwargs):
if not Schedule.objects.filter(name="Update converted prices").exists():
schedule(
"games.tasks.convert_prices",
name="Update converted prices",
schedule_type=Schedule.MINUTES,
next_run=now() + timedelta(seconds=30),
)
self.stdout.write(
self.style.SUCCESS("Scheduled the update_converted_prices task.")
)
else:
self.stdout.write(self.style.WARNING("Task is already scheduled."))

View File

@ -1,7 +1,7 @@
# Generated by Django 4.1.4 on 2023-01-02 18:27
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
@ -60,14 +60,14 @@ class Migration(migrations.Migration):
(
"game",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="tracker.game"
on_delete=django.db.models.deletion.CASCADE, to="games.game"
),
),
(
"platform",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="tracker.platform",
to="games.platform",
),
),
],
@ -93,7 +93,7 @@ class Migration(migrations.Migration):
"purchase",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="tracker.purchase",
to="games.purchase",
),
),
],

View File

@ -1,13 +1,14 @@
# 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"),
("games", "0001_initial"),
]
operations = [

View File

@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("tracker", "0002_alter_session_duration_manual"),
("games", "0002_alter_session_duration_manual"),
]
operations = [

View File

@ -0,0 +1,22 @@
# Generated by Django 4.1.5 on 2023-01-09 14:49
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0003_alter_session_duration_manual_and_more"),
]
operations = [
migrations.AlterField(
model_name="session",
name="duration_manual",
field=models.DurationField(
blank=True, default=datetime.timedelta(0), null=True
),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 4.1.5 on 2023-01-09 17:43
from datetime import timedelta
from django.db import migrations
def set_duration_calculated_none_to_zero(apps, schema_editor):
Session = apps.get_model("games", "Session")
for session in Session.objects.all():
if session.duration_calculated == None:
session.duration_calculated = timedelta(0)
session.save()
def revert_set_duration_calculated_none_to_zero(apps, schema_editor):
Session = apps.get_model("games", "Session")
for session in Session.objects.all():
if session.duration_calculated == timedelta(0):
session.duration_calculated = None
session.save()
class Migration(migrations.Migration):
dependencies = [
("games", "0004_alter_session_duration_manual"),
]
operations = [
migrations.RunPython(
set_duration_calculated_none_to_zero,
revert_set_duration_calculated_none_to_zero,
)
]

View File

@ -0,0 +1,35 @@
# Generated by Django 4.1.5 on 2023-01-09 18:04
from datetime import timedelta
from django.db import migrations
def set_duration_manual_none_to_zero(apps, schema_editor):
Session = apps.get_model("games", "Session")
for session in Session.objects.all():
if session.duration_manual == None:
session.duration_manual = timedelta(0)
session.save()
def revert_set_duration_manual_none_to_zero(apps, schema_editor):
Session = apps.get_model("games", "Session")
for session in Session.objects.all():
if session.duration_manual == timedelta(0):
session.duration_manual = None
session.save()
class Migration(migrations.Migration):
dependencies = [
("games", "0005_auto_20230109_1843"),
]
operations = [
migrations.RunPython(
set_duration_manual_none_to_zero,
revert_set_duration_manual_none_to_zero,
)
]

View File

@ -0,0 +1,35 @@
# Generated by Django 4.1.5 on 2023-01-19 18:30
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0006_auto_20230109_1904"),
]
operations = [
migrations.AlterField(
model_name="purchase",
name="game",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="games.game"
),
),
migrations.AlterField(
model_name="purchase",
name="platform",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="games.platform"
),
),
migrations.AlterField(
model_name="session",
name="purchase",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="games.purchase"
),
),
]

View File

@ -0,0 +1,41 @@
# Generated by Django 4.1.5 on 2023-02-18 16:29
import django.db.models.deletion
from django.db import migrations, models
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"
),
),
],
),
]

View File

@ -0,0 +1,34 @@
# 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)]

View File

@ -0,0 +1,21 @@
# Generated by Django 4.1.5 on 2023-02-18 19:06
import django.db.models.deletion
from django.db import migrations, models
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"
),
),
]

View File

@ -0,0 +1,18 @@
# 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",
),
]

View File

@ -0,0 +1,23 @@
# 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),
),
]

View File

@ -0,0 +1,31 @@
# 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,
),
),
]

View File

@ -0,0 +1,52 @@
# Generated by Django 4.1.5 on 2023-02-18 19:59
import django.db.models.deletion
from django.db import migrations, models
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",
),
),
]

View File

@ -0,0 +1,23 @@
# 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),
),
]

View File

@ -0,0 +1,51 @@
# Generated by Django 4.1.5 on 2023-11-06 11:10
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0015_edition_wikidata_edition_year_released"),
]
operations = [
migrations.AlterField(
model_name="edition",
name="platform",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="games.platform",
),
),
migrations.AlterField(
model_name="edition",
name="year_released",
field=models.IntegerField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name="game",
name="wikidata",
field=models.CharField(blank=True, default=None, max_length=50, null=True),
),
migrations.AlterField(
model_name="platform",
name="group",
field=models.CharField(blank=True, default=None, max_length=255, null=True),
),
migrations.AlterField(
model_name="session",
name="device",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="games.device",
),
),
]

View File

@ -0,0 +1,141 @@
# Generated by Django 4.1.5 on 2023-11-06 18:14
import django.db.models.deletion
from django.db import migrations, models
def rename_duplicates(apps, schema_editor):
Edition = apps.get_model("games", "Edition")
duplicates = (
Edition.objects.values("name", "platform")
.annotate(name_count=models.Count("id"))
.filter(name_count__gt=1)
)
for duplicate in duplicates:
counter = 1
duplicate_editions = Edition.objects.filter(
name=duplicate["name"], platform_id=duplicate["platform"]
).order_by("id")
for edition in duplicate_editions[1:]: # Skip the first one
edition.name = f"{edition.name} {counter}"
edition.save()
counter += 1
def update_game_year(apps, schema_editor):
Game = apps.get_model("games", "Game")
Edition = apps.get_model("games", "Edition")
for game in Game.objects.filter(year__isnull=True):
# Try to get the first related edition with a non-null year_released
edition = Edition.objects.filter(game=game, year_released__isnull=False).first()
if edition:
# If an edition is found, update the game's year
game.year = edition.year_released
game.save()
class Migration(migrations.Migration):
replaces = [
("games", "0016_alter_edition_platform_alter_edition_year_released_and_more"),
("games", "0017_alter_device_type_alter_purchase_platform"),
("games", "0018_auto_20231106_1825"),
("games", "0019_alter_edition_unique_together"),
("games", "0020_game_year"),
("games", "0021_auto_20231106_1909"),
("games", "0022_rename_year_game_year_released"),
]
dependencies = [
("games", "0015_edition_wikidata_edition_year_released"),
]
operations = [
migrations.AlterField(
model_name="edition",
name="platform",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="games.platform",
),
),
migrations.AlterField(
model_name="edition",
name="year_released",
field=models.IntegerField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name="game",
name="wikidata",
field=models.CharField(blank=True, default=None, max_length=50, null=True),
),
migrations.AlterField(
model_name="platform",
name="group",
field=models.CharField(blank=True, default=None, max_length=255, null=True),
),
migrations.AlterField(
model_name="session",
name="device",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="games.device",
),
),
migrations.AlterField(
model_name="device",
name="type",
field=models.CharField(
choices=[
("pc", "PC"),
("co", "Console"),
("ha", "Handheld"),
("mo", "Mobile"),
("sbc", "Single-board computer"),
("un", "Unknown"),
],
default="un",
max_length=3,
),
),
migrations.AlterField(
model_name="purchase",
name="platform",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="games.platform",
),
),
migrations.RunPython(
code=rename_duplicates,
),
migrations.AlterUniqueTogether(
name="edition",
unique_together={("name", "platform")},
),
migrations.AddField(
model_name="game",
name="year",
field=models.IntegerField(blank=True, default=None, null=True),
),
migrations.RunPython(
code=update_game_year,
),
migrations.RenameField(
model_name="game",
old_name="year",
new_name="year_released",
),
]

View File

@ -0,0 +1,41 @@
# Generated by Django 4.1.5 on 2023-11-06 16:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0016_alter_edition_platform_alter_edition_year_released_and_more"),
]
operations = [
migrations.AlterField(
model_name="device",
name="type",
field=models.CharField(
choices=[
("pc", "PC"),
("co", "Console"),
("ha", "Handheld"),
("mo", "Mobile"),
("sbc", "Single-board computer"),
("un", "Unknown"),
],
default="un",
max_length=3,
),
),
migrations.AlterField(
model_name="purchase",
name="platform",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="games.platform",
),
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 4.1.5 on 2023-11-06 17:25
from django.db import migrations, models
def rename_duplicates(apps, schema_editor):
Edition = apps.get_model("games", "Edition")
duplicates = (
Edition.objects.values("name", "platform")
.annotate(name_count=models.Count("id"))
.filter(name_count__gt=1)
)
for duplicate in duplicates:
counter = 1
duplicate_editions = Edition.objects.filter(
name=duplicate["name"], platform_id=duplicate["platform"]
).order_by("id")
for edition in duplicate_editions[1:]: # Skip the first one
edition.name = f"{edition.name} {counter}"
edition.save()
counter += 1
class Migration(migrations.Migration):
dependencies = [
("games", "0017_alter_device_type_alter_purchase_platform"),
]
operations = [
migrations.RunPython(rename_duplicates),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.1.5 on 2023-11-06 17:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("games", "0018_auto_20231106_1825"),
]
operations = [
migrations.AlterUniqueTogether(
name="edition",
unique_together={("name", "platform")},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.5 on 2023-11-06 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0019_alter_edition_unique_together"),
]
operations = [
migrations.AddField(
model_name="game",
name="year",
field=models.IntegerField(blank=True, default=None, null=True),
),
]

View File

@ -0,0 +1,24 @@
from django.db import migrations
def update_game_year(apps, schema_editor):
Game = apps.get_model("games", "Game")
Edition = apps.get_model("games", "Edition")
for game in Game.objects.filter(year__isnull=True):
# Try to get the first related edition with a non-null year_released
edition = Edition.objects.filter(game=game, year_released__isnull=False).first()
if edition:
# If an edition is found, update the game's year
game.year = edition.year_released
game.save()
class Migration(migrations.Migration):
dependencies = [
("games", "0020_game_year"),
]
operations = [
migrations.RunPython(update_game_year),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.5 on 2023-11-06 18:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("games", "0021_auto_20231106_1909"),
]
operations = [
migrations.RenameField(
model_name="game",
old_name="year",
new_name="year_released",
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 4.1.5 on 2023-11-06 18:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"games",
"0016_alter_edition_platform_alter_edition_year_released_and_more_squashed_0022_rename_year_game_year_released",
),
]
operations = [
migrations.AddField(
model_name="purchase",
name="date_finished",
field=models.DateField(blank=True, null=True),
),
]

View File

@ -0,0 +1,39 @@
# Generated by Django 4.1.5 on 2023-11-09 09:32
from django.db import migrations, models
def create_sort_name(apps, schema_editor):
Edition = apps.get_model(
"games", "Edition"
) # Replace 'your_app_name' with the actual name of your app
for edition in Edition.objects.all():
name = edition.name
# Check for articles at the beginning of the name and move them to the end
if name.lower().startswith("the "):
sort_name = f"{name[4:]}, The"
elif name.lower().startswith("a "):
sort_name = f"{name[2:]}, A"
elif name.lower().startswith("an "):
sort_name = f"{name[3:]}, An"
else:
sort_name = name
# Save the sort_name back to the database
edition.sort_name = sort_name
edition.save()
class Migration(migrations.Migration):
dependencies = [
("games", "0023_purchase_date_finished"),
]
operations = [
migrations.AddField(
model_name="edition",
name="sort_name",
field=models.CharField(blank=True, default=None, max_length=255, null=True),
),
migrations.RunPython(create_sort_name),
]

View File

@ -0,0 +1,39 @@
# Generated by Django 4.1.5 on 2023-11-09 09:32
from django.db import migrations, models
def create_sort_name(apps, schema_editor):
Game = apps.get_model(
"games", "Game"
) # Replace 'your_app_name' with the actual name of your app
for game in Game.objects.all():
name = game.name
# Check for articles at the beginning of the name and move them to the end
if name.lower().startswith("the "):
sort_name = f"{name[4:]}, The"
elif name.lower().startswith("a "):
sort_name = f"{name[2:]}, A"
elif name.lower().startswith("an "):
sort_name = f"{name[3:]}, An"
else:
sort_name = name
# Save the sort_name back to the database
game.sort_name = sort_name
game.save()
class Migration(migrations.Migration):
dependencies = [
("games", "0024_edition_sort_name"),
]
operations = [
migrations.AddField(
model_name="game",
name="sort_name",
field=models.CharField(blank=True, default=None, max_length=255, null=True),
),
migrations.RunPython(create_sort_name),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 4.1.5 on 2023-11-14 08:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0025_game_sort_name"),
]
operations = [
migrations.AddField(
model_name="purchase",
name="type",
field=models.CharField(
choices=[
("game", "Game"),
("dlc", "DLC"),
("season_pass", "Season Pass"),
("battle_pass", "Battle Pass"),
],
default="game",
max_length=255,
),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 4.1.5 on 2023-11-14 08:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0026_purchase_type"),
]
operations = [
migrations.AddField(
model_name="purchase",
name="related_purchase",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="games.purchase",
),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 4.1.5 on 2023-11-14 11:05
from django.db import migrations, models
from games.models import Purchase
def null_game_name(apps, schema_editor):
Purchase.objects.filter(type=Purchase.GAME).update(name=None)
class Migration(migrations.Migration):
dependencies = [
("games", "0027_purchase_related_purchase"),
]
operations = [
migrations.AddField(
model_name="purchase",
name="name",
field=models.CharField(
blank=True, default="Unknown Name", max_length=255, null=True
),
),
migrations.RunPython(null_game_name),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 4.1.5 on 2023-11-14 21:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0028_purchase_name"),
]
operations = [
migrations.AlterField(
model_name="purchase",
name="related_purchase",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="related_purchases",
to="games.purchase",
),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.5 on 2023-11-15 12:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0029_alter_purchase_related_purchase"),
]
operations = [
migrations.AlterField(
model_name="purchase",
name="name",
field=models.CharField(blank=True, default="", max_length=255, null=True),
),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 4.1.5 on 2023-11-15 13:51
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0030_alter_purchase_name"),
]
operations = [
migrations.AddField(
model_name="device",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name="edition",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name="game",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name="platform",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name="purchase",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name="session",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
]

View File

@ -0,0 +1,52 @@
# Generated by Django 4.1.5 on 2023-11-15 18:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0031_device_created_at_edition_created_at_game_created_at_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="session",
options={"get_latest_by": "timestamp_start"},
),
migrations.AddField(
model_name="session",
name="modified_at",
field=models.DateTimeField(auto_now=True),
),
migrations.AlterField(
model_name="device",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name="edition",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name="game",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name="platform",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name="purchase",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name="session",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2023-11-28 13:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("games", "0032_alter_session_options_session_modified_at_and_more"),
]
operations = [
migrations.AlterUniqueTogether(
name="edition",
unique_together={("name", "platform", "year_released")},
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.7 on 2024-01-03 21:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0033_alter_edition_unique_together"),
]
operations = [
migrations.AddField(
model_name="purchase",
name="date_dropped",
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name="purchase",
name="infinite",
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 5.1 on 2024-08-11 15:50
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0034_purchase_date_dropped_purchase_infinite"),
]
operations = [
migrations.AlterField(
model_name="session",
name="device",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="games.device",
),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.1 on 2024-08-11 16:48
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0035_alter_session_device'),
]
operations = [
migrations.AlterField(
model_name='edition',
name='platform',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='games.platform'),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 5.1.1 on 2024-09-14 07:05
from django.db import migrations, models
from django.utils.text import slugify
def update_empty_icons(apps, schema_editor):
Platform = apps.get_model("games", "Platform")
for platform in Platform.objects.filter(icon=""):
platform.icon = slugify(platform.name)
platform.save()
class Migration(migrations.Migration):
dependencies = [
("games", "0036_alter_edition_platform"),
]
operations = [
migrations.AddField(
model_name="platform",
name="icon",
field=models.SlugField(blank=True),
),
migrations.RunPython(update_empty_icons),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.1 on 2024-10-04 09:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0037_platform_icon'),
]
operations = [
migrations.AlterField(
model_name='purchase',
name='price',
field=models.FloatField(default=0),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-11-09 22:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0038_alter_purchase_price'),
]
operations = [
migrations.AlterField(
model_name='device',
name='type',
field=models.CharField(choices=[('PC', 'PC'), ('Console', 'Console'), ('Handheld', 'Handheld'), ('Mobile', 'Mobile'), ('Single-board computer', 'Single-board computer'), ('Unknown', 'Unknown')], default='Unknown', max_length=255),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 5.1.2 on 2024-11-09 22:39
from django.db import migrations
def update_device_types(apps, schema_editor):
Device = apps.get_model("games", "Device")
# Mapping of short names to long names
type_map = {
"pc": "PC",
"co": "Console",
"ha": "Handheld",
"mo": "Mobile",
"sbc": "Single-board computer",
"un": "Unknown",
}
# Loop through all devices and update the type field
for device in Device.objects.all():
if device.type in type_map:
device.type = type_map[device.type]
device.save()
class Migration(migrations.Migration):
dependencies = [
("games", "0039_alter_device_type"),
]
operations = [
migrations.RunPython(update_device_types),
]

View File

@ -0,0 +1,36 @@
# Generated by Django 5.1.3 on 2024-11-10 15:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0040_migrate_device_types'),
]
operations = [
migrations.AddField(
model_name='purchase',
name='converted_currency',
field=models.CharField(max_length=3, null=True),
),
migrations.AddField(
model_name='purchase',
name='converted_price',
field=models.FloatField(null=True),
),
migrations.CreateModel(
name='ExchangeRate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('currency_from', models.CharField(max_length=255)),
('currency_to', models.CharField(max_length=255)),
('year', models.PositiveIntegerField()),
('rate', models.FloatField()),
],
options={
'unique_together': {('currency_from', 'currency_to', 'year')},
},
),
]

306
games/models.py Normal file
View File

@ -0,0 +1,306 @@
from datetime import timedelta
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F, Sum
from django.template.defaultfilters import slugify
from django.utils import timezone
from common.time import format_duration
class Game(models.Model):
name = models.CharField(max_length=255)
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
year_released = models.IntegerField(null=True, blank=True, default=None)
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
created_at = models.DateTimeField(auto_now_add=True)
session_average: float | int | timedelta | None
session_count: int | None
def __str__(self):
return self.name
class Platform(models.Model):
name = models.CharField(max_length=255)
group = models.CharField(max_length=255, null=True, blank=True, default=None)
icon = models.SlugField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.icon:
self.icon = slugify(self.name)
super().save(*args, **kwargs)
def get_sentinel_platform():
return Platform.objects.get_or_create(
name="Unspecified", icon="unspecified", group="Unspecified"
)[0]
class Edition(models.Model):
class Meta:
unique_together = [["name", "platform", "year_released"]]
game = models.ForeignKey(Game, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
platform = models.ForeignKey(
Platform, on_delete=models.SET_DEFAULT, null=True, blank=True, default=None
)
year_released = models.IntegerField(null=True, blank=True, default=None)
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.sort_name
def save(self, *args, **kwargs):
if self.platform is None:
self.platform = get_sentinel_platform()
super().save(*args, **kwargs)
class PurchaseQueryset(models.QuerySet):
def refunded(self):
return self.filter(date_refunded__isnull=False)
def not_refunded(self):
return self.filter(date_refunded__isnull=True)
def finished(self):
return self.filter(date_finished__isnull=False)
def games_only(self):
return self.filter(type=Purchase.GAME)
class Purchase(models.Model):
PHYSICAL = "ph"
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"),
]
GAME = "game"
DLC = "dlc"
SEASONPASS = "season_pass"
BATTLEPASS = "battle_pass"
TYPES = [
(GAME, "Game"),
(DLC, "DLC"),
(SEASONPASS, "Season Pass"),
(BATTLEPASS, "Battle Pass"),
]
objects = PurchaseQueryset().as_manager()
edition = models.ForeignKey(Edition, on_delete=models.CASCADE)
platform = models.ForeignKey(
Platform, on_delete=models.CASCADE, default=None, null=True, blank=True
)
date_purchased = models.DateField()
date_refunded = models.DateField(blank=True, null=True)
date_finished = models.DateField(blank=True, null=True)
date_dropped = models.DateField(blank=True, null=True)
infinite = models.BooleanField(default=False)
price = models.FloatField(default=0)
price_currency = models.CharField(max_length=3, default="USD")
converted_price = models.FloatField(null=True)
converted_currency = models.CharField(max_length=3, null=True)
ownership_type = models.CharField(
max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL
)
type = models.CharField(max_length=255, choices=TYPES, default=GAME)
name = models.CharField(max_length=255, default="", null=True, blank=True)
related_purchase = models.ForeignKey(
"self",
on_delete=models.SET_NULL,
default=None,
null=True,
blank=True,
related_name="related_purchases",
)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
additional_info = [
self.get_type_display() if self.type != Purchase.GAME else "",
(
f"{self.edition.platform} version on {self.platform}"
if self.platform != self.edition.platform
else self.platform
),
self.edition.year_released,
self.get_ownership_type_display(),
]
return f"{self.edition} ({', '.join(filter(None, map(str, additional_info)))})"
def is_game(self):
return self.type == self.GAME
def save(self, *args, **kwargs):
if self.type == Purchase.GAME:
self.name = ""
elif self.type != Purchase.GAME and not self.related_purchase:
raise ValidationError(
f"{self.get_type_display()} must have a related purchase."
)
if self.pk is not None:
# Retrieve the existing instance from the database
existing_purchase = Purchase.objects.get(pk=self.pk)
# If price has changed, reset converted fields
if (
existing_purchase.price != self.price
or existing_purchase.price_currency != self.price_currency
):
self.converted_price = None
self.converted_currency = None
super().save(*args, **kwargs)
class SessionQuerySet(models.QuerySet):
def total_duration_formatted(self):
return format_duration(self.total_duration_unformatted())
def total_duration_unformatted(self):
result = self.aggregate(
duration=Sum(F("duration_calculated") + F("duration_manual"))
)
return result["duration"]
def calculated_duration_formatted(self):
return format_duration(self.calculated_duration_unformatted())
def calculated_duration_unformatted(self):
result = self.aggregate(duration=Sum(F("duration_calculated")))
return result["duration"]
def without_manual(self):
return self.exclude(duration_calculated__iexact=0)
def only_manual(self):
return self.filter(duration_calculated__iexact=0)
class Session(models.Model):
class Meta:
get_latest_by = "timestamp_start"
purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE)
timestamp_start = models.DateTimeField()
timestamp_end = models.DateTimeField(blank=True, null=True)
duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0))
duration_calculated = models.DurationField(blank=True, null=True)
device = models.ForeignKey(
"Device",
on_delete=models.SET_DEFAULT,
null=True,
blank=True,
default=None,
)
note = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
objects = SessionQuerySet.as_manager()
def __str__(self):
mark = ", manual" if self.is_manual() else ""
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})"
def finish_now(self):
self.timestamp_end = timezone.now()
def start_now():
self.timestamp_start = timezone.now()
def duration_seconds(self) -> timedelta:
manual = timedelta(0)
calculated = timedelta(0)
if self.is_manual() and isinstance(self.duration_manual, timedelta):
manual = self.duration_manual
if self.timestamp_end != None and self.timestamp_start != None:
calculated = self.timestamp_end - self.timestamp_start
return timedelta(seconds=(manual + calculated).total_seconds())
def duration_formatted(self) -> str:
result = format_duration(self.duration_seconds(), "%02.0H:%02.0m")
return result
def is_manual(self) -> bool:
return not self.duration_manual == timedelta(0)
@property
def duration_sum(self) -> str:
return Session.objects.all().total_duration_formatted()
def save(self, *args, **kwargs) -> None:
if self.timestamp_start != None and self.timestamp_end != None:
self.duration_calculated = self.timestamp_end - self.timestamp_start
else:
self.duration_calculated = timedelta(0)
if not isinstance(self.duration_manual, timedelta):
self.duration_manual = timedelta(0)
if not self.device:
default_device, _ = Device.objects.get_or_create(
type=Device.UNKNOWN, defaults={"name": "Unknown"}
)
self.device = default_device
super(Session, self).save(*args, **kwargs)
class Device(models.Model):
PC = "PC"
CONSOLE = "Console"
HANDHELD = "Handheld"
MOBILE = "Mobile"
SBC = "Single-board computer"
UNKNOWN = "Unknown"
DEVICE_TYPES = [
(PC, "PC"),
(CONSOLE, "Console"),
(HANDHELD, "Handheld"),
(MOBILE, "Mobile"),
(SBC, "Single-board computer"),
(UNKNOWN, "Unknown"),
]
name = models.CharField(max_length=255)
type = models.CharField(max_length=255, choices=DEVICE_TYPES, default=UNKNOWN)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.name} ({self.type})"
class ExchangeRate(models.Model):
currency_from = models.CharField(max_length=255)
currency_to = models.CharField(max_length=255)
year = models.PositiveIntegerField()
rate = models.FloatField()
class Meta:
unique_together = ("currency_from", "currency_to", "year")
def __str__(self):
return f"{self.currency_from}/{self.currency_to} - {self.rate} ({self.year})"

30
games/schema.py Normal file
View File

@ -0,0 +1,30 @@
import graphene
from games.graphql.mutations import GameMutation
from games.graphql.queries import (
DeviceQuery,
EditionQuery,
GameQuery,
PlatformQuery,
PurchaseQuery,
SessionQuery,
)
class Query(
GameQuery,
EditionQuery,
DeviceQuery,
PlatformQuery,
PurchaseQuery,
SessionQuery,
graphene.ObjectType,
):
pass
class Mutation(GameMutation, graphene.ObjectType):
pass
schema = graphene.Schema(query=Query, mutation=Mutation)

3276
games/static/base.css Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Some files were not shown because too many files have changed in this diff Show More