Compare commits

..

199 Commits

Author SHA1 Message Date
lukas fb1f6d2a33 Add django-cors-headers 2023-11-18 10:23:44 +01:00
lukas b219e3f6bc Remove top-level package.json 2023-11-18 10:23:44 +01:00
lukas eff598f475 Integrate TailwindCSS 2023-11-18 10:23:44 +01:00
lukas a3be509893 Change API url 2023-11-18 10:23:43 +01:00
lukas 6af754afa6 Add django-rest-framework 2023-11-18 10:23:43 +01:00
lukas c99743701e Remove Django admin 2023-11-18 10:23:34 +01:00
lukas da0a04e0c6 Name and related_purchase validation for non-games 2023-11-18 10:23:33 +01:00
lukas e4c6e9e414 Add purchase types 2023-11-18 10:23:27 +01:00
lukas 2eaccc57b0 Remove unused chart functionality 2023-11-18 10:22:31 +01:00
lukas 865ecd1ee0 Switch fonts to WOFF2 2023-11-18 10:22:19 +01:00
lukas fed1bfa053 Further improve session list 2023-11-18 10:22:19 +01:00
lukas dd92148db5 Update CSS 2023-11-18 10:21:55 +01:00
lukas 8bf2c32eb5 Improve session list
Fixes #53
2023-11-18 10:21:55 +01:00
lukas d303039b1c Add hacky way to not reload page when starting/ending session
Partially fixes #52
2023-11-18 10:21:27 +01:00
lukas 67b9cbb048 allow django admit 2023-11-18 10:21:09 +01:00
lukas 5d36ad386e Improve forms, add helper buttons on add session form 2023-11-18 10:21:09 +01:00
lukas 42bc391e57 Use date and datetime inputs
Properly implements 4d91a76513
2023-11-18 10:20:32 +01:00
lukas 850ca382ad Add wikidata ID and year for editions 2023-11-18 10:20:22 +01:00
lukas d2e0bcfb12 Add icons for session filters 2023-11-18 10:20:03 +01:00
lukas b773d9df58 Allow filtering by game, edition, purchase from the session list 2023-11-18 10:20:03 +01:00
lukas dc6c295ee7 Fix form styling 2023-11-18 10:19:05 +01:00
lukas d272915ef6 Fix make css 2023-11-18 10:18:58 +01:00
lukas cbc8062d92 Show only last 30 days on homepage
Fixes #47
2023-11-18 10:18:58 +01:00
lukas 02c04badac Start converting to react-router 2023-11-18 10:16:41 +01:00
lukas 5756b736d8 Add apiService 2023-11-18 10:16:41 +01:00
lukas d4c0d47712 Fix Vite proxy config 2023-11-18 10:16:41 +01:00
lukas 6f62b2026b Add tailwind typography and forms 2023-11-18 10:16:41 +01:00
lukas e6640a4083 Add django-cors-headers 2023-11-18 10:16:41 +01:00
lukas 81fbcc9281 Add eslint and prettier 2023-11-18 10:16:41 +01:00
lukas 2e891fc166 Remove cruft 2023-11-18 10:16:41 +01:00
lukas b95d5dfb98 Remove top-level package.json 2023-11-18 10:16:40 +01:00
lukas 0c564ef146 Add Nav component 2023-11-18 10:16:35 +01:00
lukas 048401b20a Integrate TailwindCSS 2023-11-18 10:16:35 +01:00
lukas aecf0d6a6e Change API url 2023-11-18 10:16:13 +01:00
lukas a4ec5d0dbc Add django-rest-framework 2023-11-18 10:16:13 +01:00
lukas bec4e3716a CI: autotag 2023-11-18 10:15:21 +01:00
lukas 03142bc3c3 Add vite proxy config 2023-11-18 10:15:21 +01:00
lukas b0ef975c2b Add vite react 2023-11-18 10:15:20 +01:00
lukas 555608d8c6 Fix syntax
Django CI/CD / build-and-push (push) Successful in 1m14s
2023-11-18 09:52:17 +01:00
lukas a7293c659d CI: Ignore README.md 2023-11-18 09:33:31 +01:00
lukas f36e692361 Do not run for pull requests 2023-11-18 09:33:10 +01:00
lukas fe97f540a0 Fix CI being blocked 2023-11-18 09:32:41 +01:00
lukas c35b539c42 Merge sessions and notes
Django CI/CD / build-and-push (push) Successful in 1m9s
2023-11-17 21:20:33 +01:00
lukas bbe5e072b2 Don't display prices if zero 2023-11-17 21:10:56 +01:00
lukas 6fc2f623dc Apply djlint 2023-11-17 21:06:57 +01:00
lukas 9481bd5fef Add pre-commit
Django CI/CD / build-and-push (push) Successful in 1m33s
2023-11-17 09:34:51 +01:00
lukas 4083165123 Use the black profile for isort 2023-11-17 09:15:18 +01:00
lukas 45bb2681c7 Use isort on migrations 2023-11-17 09:15:06 +01:00
lukas dbb8ec3f9a Handle empty edition_id 2023-11-17 09:14:25 +01:00
lukas 206b5f6d46 Prevent HTMX from messing up the initial state
Django CI/CD / build-and-push (push) Successful in 1m15s
2023-11-16 20:33:56 +01:00
lukas b7e14ecc83 Account for no sessions
Django CI/CD / build-and-push (push) Successful in 1m21s
2023-11-16 20:29:08 +01:00
lukas 912e010729 Enable hx-boost everywhere
Django CI/CD / build-and-push (push) Successful in 1m18s
2023-11-16 19:56:08 +01:00
lukas a485237456 Fix form not syncing due to HTMX
Django CI/CD / build-and-push (push) Successful in 2m38s
2023-11-16 19:03:16 +01:00
lukas f5faf92ee0 Fix error
Django CI/CD / build-and-push (push) Successful in 1m57s
2023-11-16 16:53:59 +01:00
lukas 07452d8c43 Re-instance gitea actions
Django CI/CD / test (push) Failing after 34s
Django CI/CD / build-and-push (push) Has been skipped
2023-11-16 16:51:52 +01:00
lukas 229a79d266 Update .drone.yml testing
continuous-integration/drone/push Build is failing
2023-11-16 16:30:17 +01:00
lukas c6ed577fe3 Formatting
continuous-integration/drone/push Build is failing
2023-11-16 16:27:41 +01:00
lukas 171e4779a3 Move static files in prod 2023-11-16 16:27:41 +01:00
lukas 79f94e5984 Fix docker-compose.yml 2023-11-16 16:27:41 +01:00
lukas 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
lukas fe0a6b39e3 Fix .dockerignore 2023-11-16 16:27:41 +01:00
lukas 6a495f951f Remove Django admin 2023-11-16 16:27:41 +01:00
lukas c8646d0a0c Update dependencies 2023-11-16 16:27:41 +01:00
lukas f2bb15e669 Fix naive date 2023-11-16 16:27:41 +01:00
lukas c49177d63c isort 2023-11-16 16:27:41 +01:00
lukas 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
lukas c44d8bf427 Improve time-related stuff
continuous-integration/drone/push Build is passing
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
lukas 3f037b4c7c Only allow choosing purchases of selected edition
continuous-integration/drone/push Build is passing
2023-11-15 14:25:42 +01:00
lukas 8783d1fc8e Name and related_purchase validation for non-games 2023-11-15 13:04:47 +01:00
lukas 9a1d24dbfd Sort imports, remove cruft 2023-11-15 12:19:31 +01:00
lukas 4720660cff Fix wrong playrange ordering 2023-11-15 10:40:52 +01:00
lukas e158bc0623 Improve how editions and purchases are displayed
continuous-integration/drone/push Build is passing
2023-11-15 10:37:24 +01:00
lukas 8982fc5086 Game View: order editions by year 2023-11-14 21:19:36 +01:00
lukas 729e1d939b Version 1.5.1
continuous-integration/drone/push Build encountered an error
2023-11-14 21:10:42 +01:00
lukas 2b4683e489 Improve and cleanup ConditionalElementHandler 2023-11-14 21:09:43 +01:00
lukas cce810e8cf Improve purchase __str__ 2023-11-14 19:55:56 +01:00
lukas 62cd17f702 Disallow choosing non-game purchase as related purchase 2023-11-14 19:55:19 +01:00
lukas f31280c682 Version 1.5.0
continuous-integration/drone/push Build is passing
2023-11-14 19:31:17 +01:00
lukas a745d16ec3 Order purchases by date on game view 2023-11-14 19:30:19 +01:00
lukas ae079e36ec Add purchase types 2023-11-14 19:27:00 +01:00
lukas c8a3212b77 CI: run migrations before tests
continuous-integration/drone/push Build was killed
2023-11-12 08:11:22 +01:00
lukas d211326c3f Make sure empty stats are 0
continuous-integration/drone/push Build is failing
2023-11-12 08:01:12 +01:00
lukas 270a291f05 Change stats years to 2000 up to current year 2023-11-12 07:50:12 +01:00
lukas 13b750ca92 Add stat for finished this year's games 2023-11-12 07:40:29 +01:00
lukas 015b6db2f7 Fix detecting manual durations
continuous-integration/drone/push Build is failing
2023-11-11 15:02:28 +01:00
lukas 667b161fff Remove deprecated USE_L10N
continuous-integration/drone/push Build is failing
2023-11-10 21:37:13 +01:00
lukas 5958cbf4a6 Add more tests 2023-11-10 21:34:36 +01:00
lukas 3b37f2c3f0 Fix edge case in format_duration
continuous-integration/drone/push Build is passing
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
lukas 4517ff2b5a Fix ordering
continuous-integration/drone/push Build is passing
2023-11-09 21:43:17 +01:00
lukas 884ce13e26 Also prefill year between game and edition
continuous-integration/drone/push Build is passing
2023-11-09 21:20:12 +01:00
lukas dd219bae9d Version 1.4.0
continuous-integration/drone/push Build is passing
2023-11-09 21:11:43 +01:00
lukas 60d29090a1 Adding new games is easier 2023-11-09 21:11:28 +01:00
lukas 1bc3ca057b Refactor, remove cruft 2023-11-09 19:35:57 +01:00
lukas c2c0886451 Add backlog decrease count 2023-11-09 19:15:49 +01:00
lukas b0be7b5887 Fix sort names getting mangled
continuous-integration/drone/push Build is passing
2023-11-09 15:41:46 +01:00
lukas 099d989f16 Pre-fill year when adding edition 2023-11-09 15:20:30 +01:00
lukas a879360ebd UX improvements
continuous-integration/drone/push Build is passing
* 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
lukas 866f2526e6 Fix hardcoded year 2023-11-09 10:10:44 +01:00
lukas ce3c4b55f0 Order devices alphabetically on new session form
continuous-integration/drone/push Build is passing
2023-11-09 10:09:32 +01:00
lukas c52cd822ae Use safe_division in more places 2023-11-09 10:06:14 +01:00
lukas cdc6ca1324 Fix potential division by zero
continuous-integration/drone/push Build is passing
2023-11-09 09:18:49 +01:00
lukas e7ed349356 Add more stats
continuous-integration/drone/push Build is passing
* Finished (count)
* Unfinished (count)
* Refunded (count)
2023-11-08 18:13:48 +01:00
lukas 5052ca7dbf Add more stats
continuous-integration/drone/push Build is passing
* 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
lukas f408bfd927 stats: change overall stats table layout 2023-11-08 15:48:06 +01:00
lukas 666dee33ba Model changes
continuous-integration/drone/push Build is passing
* 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
lukas e0b09e051a simplify playtime range display 2023-11-06 12:05:39 +01:00
lukas 4552cf7616 Version 1.3.0
continuous-integration/drone/push Build is passing
2023-11-05 15:10:56 +01:00
lukas a614b51d29 Make some pages redirect back instead to session list 2023-11-05 15:09:51 +01:00
lukas e67aa3fda1 Add more stats
continuous-integration/drone/push Build is passing
2023-11-02 20:12:32 +01:00
lukas 8423fd02b4 Extend stats range to 2018
continuous-integration/drone/push Build is passing
2023-11-02 15:32:57 +01:00
lukas 2bd07e5f2d Remove cruft
continuous-integration/drone/push Build is passing
2023-11-02 15:14:57 +01:00
lukas 058b83522c Group by game instead of purchase 2023-11-02 15:14:50 +01:00
lukas f13ed8a078 Reorder imports in views.py
continuous-integration/drone/push Build is passing
2023-11-02 09:53:28 +01:00
lukas 02d5adcb3c Remove hardcoded year 2023-11-02 09:52:59 +01:00
lukas d6fb16bb74 Make navigation more compact 2023-11-02 09:52:42 +01:00
lukas 71b90b8202 Add stats link, year selector 2023-11-02 09:20:09 +01:00
lukas 3ee36932c3 Limit stats of single year correctly 2023-11-02 09:17:08 +01:00
lukas 391fcc79a8 Version 1.2.0
continuous-integration/drone/push Build is passing
2023-11-01 20:35:58 +01:00
lukas 57d4fd7212 Add yearly stats page
Fixes #15
2023-11-01 20:35:52 +01:00
lukas a5b2854bf6 Add a button to start session from game overview
continuous-integration/drone/push Build is passing
Fix #62
2023-10-13 19:22:43 +02:00
lukas 518c0ecd56 Add more time tests for fractional numbers
continuous-integration/drone/push Build is passing
2023-10-13 17:01:33 +02:00
lukas a6cd7a3430 Do not format as float if no precision specified
continuous-integration/drone/push Build is passing
2023-10-13 16:58:12 +02:00
lukas dba8414fd9 Version 1.1.2
continuous-integration/drone/push Build is failing
2023-10-13 16:33:55 +02:00
lukas 0e2113eefd Display durations in a consistent manner
Fixes #61
2023-10-13 16:32:12 +02:00
lukas c4b0347f3b Version 1.1.1
continuous-integration/drone/push Build is passing
2023-10-09 20:56:23 +02:00
lukas c6ed21167c Remove debugging cruft from container 2023-10-09 20:56:13 +02:00
lukas 4ce15c44fc Add edit buttons to game overview, notes 2023-10-09 20:55:31 +02:00
lukas c814b4c2cb Version 1.1.0
continuous-integration/drone/push Build is passing
2023-10-09 00:04:46 +02:00
lukas 11b9c602de Improve game overview
- add counts for each section
- add average hours per session
2023-10-09 00:00:45 +02:00
lukas 9a332593f4 Fix playtime range not working with manual session
continuous-integration/drone/push Build is passing
Fixes #55
2023-10-08 21:13:32 +02:00
lukas 22935721ca Remove unused chart functionality
continuous-integration/drone/push Build is passing
2023-10-08 21:08:03 +02:00
lukas a2ecdcf44a ci: automatically redeploy container
continuous-integration/drone/push Build was killed
2023-10-04 20:21:00 +02:00
lukas 3c958c4a13 Organize changelog better
continuous-integration/drone/push Build is passing
2023-10-04 18:14:21 +02:00
lukas 3db1724e22 Fix session being wrongly considered in progress
continuous-integration/drone/push Build is passing
Fixes #58
2023-10-04 18:12:13 +02:00
lukas d2a9630b04 Fix date range on game overview
continuous-integration/drone/push Build is passing
2023-10-01 21:51:32 +02:00
lukas e3ee832d3f Adjust game edit URL
continuous-integration/drone/push Build is passing
2023-10-01 21:28:20 +02:00
lukas 7467e2732d Add game overview page
Fixes #8
2023-10-01 21:28:02 +02:00
lukas 787ee8640f Try to shutdown container gracefully and faster
continuous-integration/drone/push Build is passing
2023-10-01 19:57:15 +02:00
lukas ab41222f3c Switch fonts to WOFF2 2023-10-01 19:53:07 +02:00
lukas 29bf3b1946 Further improve session list
continuous-integration/drone/push Build is passing
2023-09-30 19:44:35 +02:00
lukas 3f7ccea2e2 fix tailwind config to only scan relevant files 2023-09-30 18:13:50 +02:00
lukas b5ffb3586b Update base.css 2023-09-30 16:25:19 +02:00
lukas 26d57a238e Update .vscode 2023-09-30 16:25:05 +02:00
lukas 2d5ad3182c Update CSS 2023-09-30 16:24:54 +02:00
lukas 49cc3ea0cc Improve session list
continuous-integration/drone/push Build is passing
Fixes #53
2023-09-30 15:55:38 +02:00
lukas 440e1cfb71 Focus important fields on forms
continuous-integration/drone/push Build is passing
2023-09-30 12:40:02 +02:00
lukas 1cbd8c5c55 Update generated CSS
continuous-integration/drone/push Build is passing
2023-09-20 19:06:04 +02:00
lukas bc81a0ee8e Add hacky way to not reload page when starting/ending session
continuous-integration/drone/push Build is passing
Partially fixes #52
2023-09-20 18:54:54 +02:00
lukas c5653977ff Add time copy button, improve session editing
continuous-integration/drone/push Build is passing
2023-09-18 20:21:05 +02:00
lukas f151730ab6 Fix bug when filtering only manual sessions (#51)
continuous-integration/drone/push Build is passing
2023-09-18 19:37:54 +02:00
lukas f469a67d94 add robots.txt
continuous-integration/drone/push Build is passing
2023-09-17 17:58:22 +02:00
lukas 104ffc9d03 disable deleting sessions in code
continuous-integration/drone/push Build is passing
2023-09-17 17:17:22 +02:00
lukas a4b13eb247 allow admin in prod 2/2
continuous-integration/drone/push Build is passing
2023-09-17 14:12:23 +02:00
lukas 2307fac83a allow admin in prod
continuous-integration/drone/push Build is passing
2023-09-17 14:03:46 +02:00
lukas 6b52c0d4c4 disable deleting sessions
continuous-integration/drone/push Build is passing
2023-09-17 13:28:15 +02:00
lukas ff5d8c215d install dev dependecies
continuous-integration/drone/push Build is passing
2023-09-16 18:24:10 +02:00
lukas cdb3b89b08 do not filter out refunded games when adding session
continuous-integration/drone/push Build is passing
2023-09-16 18:18:03 +02:00
lukas ffa8198540 allow django admit 2023-09-16 18:17:36 +02:00
lukas 0b7da3550c Change recent session view to current year
continuous-integration/drone/push Build is passing
2023-09-14 18:49:16 +02:00
lukas e1655d6cfa input datetime-local needs day to also be two digits
continuous-integration/drone/push Build is passing
2023-03-02 21:33:07 +01:00
lukas 29c41865d0 Exclude refunded purchases from start session form
continuous-integration/drone/push Build is passing
2023-03-01 22:07:49 +01:00
lukas d21b461726 Update styles
continuous-integration/drone/push Build is passing
2023-02-21 23:51:07 +01:00
lukas 95489cfb78 Add .editorconfig 2023-02-21 23:50:09 +01:00
lukas fa4f1c4810 Improve forms, add helper buttons on add session form 2023-02-21 23:49:57 +01:00
lukas 366c25a1ff Register Edition, Device in the admin UI 2023-02-20 22:01:20 +01:00
lukas a3042caa20 Use date and datetime inputs
continuous-integration/drone/push Build is passing
Properly implements 4d91a76513
2023-02-20 21:33:15 +01:00
lukas 7997f9bbb2 Sort games by name on edition form 2023-02-20 20:17:42 +01:00
lukas b78c4ba9c5 Sync game name and edition name fields for QOL
continuous-integration/drone/push Build is passing
2023-02-20 18:21:48 +01:00
lukas 1df889c45d Fix gitignoring root static folder 2023-02-20 18:20:49 +01:00
lukas 468d05a9e2 Sort games alphabetically on edition form 2023-02-20 17:37:14 +01:00
lukas 2640a49734 Version 1.0.3
continuous-integration/drone/push Build is passing
2023-02-20 17:18:26 +01:00
lukas 65c175afb2 Allow editing filtered entities from session list 2023-02-20 17:16:19 +01:00
lukas 0814071a26 Improve display of editions on purchase form 2023-02-20 17:15:21 +01:00
lukas 5f845f866e Sort platforms alphabetically on edition form 2023-02-20 17:14:43 +01:00
lukas c3d4697470 Add wikidata ID and year for editions 2023-02-20 17:13:35 +01:00
lukas 77293f03e9 Add icons
continuous-integration/drone/push Build is passing
2023-02-19 17:20:57 +01:00
lukas 1fa364e2ec Change timetracker icon 2023-02-19 16:35:04 +01:00
lukas 4a6f4a2f9a Add icons for session filters
continuous-integration/drone/push Build is passing
2023-02-19 16:18:14 +01:00
lukas 9590988b6a Fix error when generating charts with less than 2 entries
continuous-integration/drone/push Build is passing
2023-02-19 14:55:06 +01:00
lukas 938c82a395 Allow filtering by game, edition, purchase from the session list 2023-02-19 14:36:12 +01:00
lukas 33939f631c Fix form styling
continuous-integration/drone/push Build is passing
2023-02-18 22:36:26 +01:00
lukas ac8cd6534a Version 1.0.2
continuous-integration/drone/push Build is passing
2023-02-18 21:48:55 +01:00
lukas 51d8e953c0 Allow editing editions 2023-02-18 21:47:25 +01:00
lukas 2eec677f41 Allow editing purchases 2023-02-18 21:44:19 +01:00
lukas f2eb14d3ef Fix session starting 2023-02-18 21:43:51 +01:00
lukas c337d2200f Fix ownership display
continuous-integration/drone/push Build is passing
2023-02-18 21:12:44 +01:00
lukas 8a8b05b0bd Add support for device info
Closes #49
2023-02-18 21:12:18 +01:00
lukas 9446065271 Add support for purchase ownership information
Closes #48
2023-02-18 20:57:03 +01:00
lukas 755093845d Add support for prices on purchases 2023-02-18 20:56:23 +01:00
lukas d4ab0596da Re-introduce index view
continuous-integration/drone/push Build is passing
2023-02-18 20:50:36 +01:00
lukas 8dcbe2f0ad Fix displaying of "filterying by..." text 2023-02-18 20:50:19 +01:00
lukas 25bc74eff1 Add support for game editions (#28) 2023-02-18 20:49:46 +01:00
lukas 8a7d083fb2 Fix make css 2023-02-18 20:35:59 +01:00
lukas 8296ebcf31 Fix redirect to index
continuous-integration/drone/push Build is passing
2023-02-06 17:23:44 +01:00
lukas 04a4f2e0be Order homepage sessions from newest
continuous-integration/drone/push Build is passing
2023-01-31 16:37:44 +01:00
lukas 4070b4e46e Version 1.0.1
continuous-integration/drone/push Build is passing
2023-01-30 22:17:47 +01:00
lukas 4892218c83 Show only last 30 days on homepage
Fixes #47
2023-01-30 22:16:28 +01:00
lukas 6b00a950ce Show markers on smaller graphs 2023-01-30 22:01:27 +01:00
lukas feee9d6dac Make it possible to edit sessions
continuous-integration/drone/push Build is passing
Fixes #46
2023-01-30 17:38:44 +01:00
22 changed files with 376 additions and 663 deletions
+1 -3
View File
@@ -3,12 +3,10 @@ name: Django CI/CD
on: on:
push: push:
branches: [ main ] branches: [ main ]
pull_request: paths-ignore: [ 'README.md' ]
branches: [ main ]
jobs: jobs:
build-and-push: build-and-push:
needs: test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
+9 -26
View File
@@ -1,17 +1,5 @@
## Unreleased ## Unreleased
## Improved
* game overview: improve how editions and purchases are displayed
* add purchase: only allow choosing purchases of selected edition
## 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 ## New
* Add stat for finished this year's games * Add stat for finished this year's games
* Add purchase types: * Add purchase types:
@@ -20,9 +8,6 @@
* Season Pass * Season Pass
* Battle Pass * Battle Pass
## Fixed
* Order purchases by date on game view
## 1.4.0 / 2023-11-09 21:01+01:00 ## 1.4.0 / 2023-11-09 21:01+01:00
### New ### New
@@ -110,24 +95,22 @@
### Enhancements ### Enhancements
* Improve form appearance * Improve form appearance
* Focus important fields on forms * Add helper buttons next to datime fields
* 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 * 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) * Fix bug when filtering only manual sessions (https://git.kucharczyk.xyz/lukas/timetracker/issues/51)
* Add copy button on Add session page to copy times between fields
* Use the same form when editing a session as when adding a session
* Add a hacky way not to reload a page when starting or ending a session (https://git.kucharczyk.xyz/lukas/timetracker/issues/52)
* Focus important fields on forms
* Improve session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/53)
* Change fonts to IBM Plex
* Only use local WOFF2 font files
## 1.0.3 / 2023-02-20 17:16+01:00 ## 1.0.3 / 2023-02-20 17:16+01:00
* Add wikidata ID and year for editions * 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 filtering by game, edition, purchase from the session list
* Allow editing filtered entities from session list * Add icons for the above
## 1.0.2 / 2023-02-18 21:48+01:00 ## 1.0.2 / 2023-02-18 21:48+01:00
+6 -2
View File
@@ -9,6 +9,11 @@ custom_datetime_widget = forms.DateTimeInput(
) )
autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"}) autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"})
custom_date_widget = forms.DateInput(attrs={"type": "date"})
custom_datetime_widget = forms.DateTimeInput(
attrs={"type": "datetime-local"}, format="%Y-%m-%d %H:%M"
)
class SessionForm(forms.ModelForm): class SessionForm(forms.ModelForm):
# purchase = forms.ModelChoiceField( # purchase = forms.ModelChoiceField(
@@ -82,7 +87,6 @@ class PurchaseForm(forms.ModelForm):
widgets = { widgets = {
"date_purchased": custom_date_widget, "date_purchased": custom_date_widget,
"date_refunded": custom_date_widget, "date_refunded": custom_date_widget,
"date_finished": custom_date_widget,
} }
model = Purchase model = Purchase
fields = [ fields = [
@@ -147,7 +151,7 @@ class EditionForm(forms.ModelForm):
class Meta: class Meta:
model = Edition model = Edition
fields = ["game", "name", "sort_name", "platform", "year_released", "wikidata"] fields = ["game", "name", "platform", "year_released", "wikidata"]
class GameForm(forms.ModelForm): class GameForm(forms.ModelForm):
@@ -1,11 +1,10 @@
# Generated by Django 4.1.5 on 2023-11-14 08:41 # Generated by Django 4.1.5 on 2023-11-14 08:41
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("games", "0026_purchase_type"), ("games", "0026_purchase_type"),
] ]
-1
View File
@@ -1,7 +1,6 @@
# Generated by Django 4.1.5 on 2023-11-14 11:05 # Generated by Django 4.1.5 on 2023-11-14 11:05
from django.db import migrations, models from django.db import migrations, models
from games.models import Purchase from games.models import Purchase
+8 -22
View File
@@ -2,6 +2,7 @@ from datetime import timedelta
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.core.exceptions import ValidationError
from django.db.models import F, Manager, Sum from django.db.models import F, Manager, Sum
from django.utils import timezone from django.utils import timezone
@@ -38,13 +39,9 @@ class Edition(models.Model):
game = models.ForeignKey("Game", on_delete=models.CASCADE) game = models.ForeignKey("Game", on_delete=models.CASCADE)
name = models.CharField(max_length=255) 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.CASCADE)
platform = models.ForeignKey( year_released = models.IntegerField(default=datetime.today().year)
"Platform", on_delete=models.CASCADE, 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) wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return self.sort_name return self.sort_name
@@ -124,25 +121,14 @@ class Purchase(models.Model):
type = models.CharField(max_length=255, choices=TYPES, default=GAME) type = models.CharField(max_length=255, choices=TYPES, default=GAME)
name = models.CharField(max_length=255, default="", null=True, blank=True) name = models.CharField(max_length=255, default="", null=True, blank=True)
related_purchase = models.ForeignKey( related_purchase = models.ForeignKey(
"Purchase", "Purchase", on_delete=models.SET_NULL, default=None, null=True, blank=True
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): def __str__(self):
additional_info = [ platform_info = self.platform
self.get_type_display() if self.type != Purchase.GAME else "", if self.platform != self.edition.platform:
f"{self.edition.platform} version on {self.platform}" platform_info = f"{self.edition.platform} version on {self.platform}"
if self.platform != self.edition.platform return f"{self.edition} ({platform_info}, {self.edition.year_released}, {self.get_ownership_type_display()})"
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): def is_game(self):
return self.type == self.GAME return self.type == self.GAME
+52 -179
View File
@@ -755,10 +755,6 @@ select {
position: absolute; position: absolute;
} }
.relative {
position: relative;
}
.bottom-2 { .bottom-2 {
bottom: 0.5rem; bottom: 0.5rem;
} }
@@ -775,55 +771,20 @@ select {
top: 0.75rem; top: 0.75rem;
} }
.mx-2 {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.mx-auto { .mx-auto {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
.my-2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.my-4 { .my-4 {
margin-top: 1rem; margin-top: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.my-6 {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.mb-10 {
margin-bottom: 2.5rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-4 { .mb-4 {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.ml-1 {
margin-left: 0.25rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.mr-4 { .mr-4 {
margin-right: 1rem; margin-right: 1rem;
} }
@@ -836,10 +797,6 @@ select {
display: block; display: block;
} }
.inline-block {
display: inline-block;
}
.inline { .inline {
display: inline; display: inline;
} }
@@ -856,14 +813,6 @@ select {
display: none; display: none;
} }
.h-4 {
height: 1rem;
}
.h-5 {
height: 1.25rem;
}
.h-6 { .h-6 {
height: 1.5rem; height: 1.5rem;
} }
@@ -872,22 +821,10 @@ select {
min-height: 100vh; min-height: 100vh;
} }
.w-5 {
width: 1.25rem;
}
.w-6 { .w-6 {
width: 1.5rem; width: 1.5rem;
} }
.w-7 {
width: 1.75rem;
}
.w-auto {
width: auto;
}
.w-full { .w-full {
width: 100%; width: 100%;
} }
@@ -896,10 +833,6 @@ select {
max-width: 1024px; max-width: 1024px;
} }
.max-w-sm {
max-width: 24rem;
}
.max-w-xs { .max-w-xs {
max-width: 20rem; max-width: 20rem;
} }
@@ -926,18 +859,10 @@ select {
align-items: center; align-items: center;
} }
.justify-center {
justify-content: center;
}
.justify-between { .justify-between {
justify-content: space-between; justify-content: space-between;
} }
.gap-2 {
gap: 0.5rem;
}
.self-center { .self-center {
align-self: center; align-self: center;
} }
@@ -960,35 +885,16 @@ select {
border-radius: 0.5rem; border-radius: 0.5rem;
} }
.rounded-sm {
border-radius: 0.125rem;
}
.border-gray-200 { .border-gray-200 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity)); border-color: rgb(229 231 235 / var(--tw-border-opacity));
} }
.border-slate-500 {
--tw-border-opacity: 1;
border-color: rgb(100 116 139 / var(--tw-border-opacity));
}
.bg-gray-200 {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}
.bg-green-600 { .bg-green-600 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity)); background-color: rgb(22 163 74 / var(--tw-bg-opacity));
} }
.bg-violet-600 {
--tw-bg-opacity: 1;
background-color: rgb(124 58 237 / var(--tw-bg-opacity));
}
.bg-white { .bg-white {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@@ -1003,11 +909,6 @@ select {
padding-right: 0.5rem; padding-right: 0.5rem;
} }
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-1 { .py-1 {
padding-top: 0.25rem; padding-top: 0.25rem;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
@@ -1026,10 +927,6 @@ select {
padding-right: 1rem; padding-right: 1rem;
} }
.pt-1 {
padding-top: 0.25rem;
}
.text-center { .text-center {
text-align: center; text-align: center;
} }
@@ -1038,9 +935,12 @@ select {
font-family: IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
} }
.text-3xl { .font-serif {
font-size: 1.875rem; font-family: IBM Plex Serif, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
line-height: 2.25rem; }
.font-sans {
font-family: IBM Plex Sans, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
} }
.text-4xl { .text-4xl {
@@ -1048,21 +948,11 @@ select {
line-height: 2.5rem; line-height: 2.5rem;
} }
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.text-base { .text-base {
font-size: 1rem; font-size: 1rem;
line-height: 1.5rem; line-height: 1.5rem;
} }
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-xl { .text-xl {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.75rem; line-height: 1.75rem;
@@ -1073,17 +963,55 @@ select {
line-height: 1rem; line-height: 1rem;
} }
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-9xl {
font-size: 8rem;
line-height: 1;
}
.text-8xl {
font-size: 6rem;
line-height: 1;
}
.text-7xl {
font-size: 4.5rem;
line-height: 1;
}
.text-6xl {
font-size: 3.75rem;
line-height: 1;
}
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.font-semibold { .font-semibold {
font-weight: 600; font-weight: 600;
} }
.italic { .font-bold {
font-style: italic; font-weight: 700;
} }
.text-gray-700 { .capitalize {
--tw-text-opacity: 1; text-transform: capitalize;
color: rgb(55 65 81 / var(--tw-text-opacity)); }
.italic {
font-style: italic;
} }
.text-slate-300 { .text-slate-300 {
@@ -1101,14 +1029,6 @@ select {
color: rgb(253 224 71 / var(--tw-text-opacity)); color: rgb(253 224 71 / var(--tw-text-opacity));
} }
.underline {
text-decoration-line: underline;
}
.decoration-slate-500 {
text-decoration-color: #64748b;
}
.shadow-md { .shadow-md {
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
@@ -1208,10 +1128,12 @@ select {
} }
.responsive-table thead th:not(:first-child), .responsive-table thead th:not(:first-child),
.responsive-table td:not(:first-child) { td:not(:first-child) {
border-left-width: 1px; border-left-width: 1px;
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-left-color: rgb(100 116 139 / var(--tw-border-opacity)); border-left-color: rgb(100 116 139 / var(--tw-border-opacity));
padding-left: 1rem;
padding-right: 1rem;
} }
:is(.dark form input),:is(.dark :is(.dark form input),:is(.dark
@@ -1343,21 +1265,11 @@ th label {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
} }
.hover\:bg-gray-400:hover {
--tw-bg-opacity: 1;
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
}
.hover\:bg-green-700:hover { .hover\:bg-green-700:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(21 128 61 / var(--tw-bg-opacity)); background-color: rgb(21 128 61 / var(--tw-bg-opacity));
} }
.hover\:bg-violet-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(109 40 217 / var(--tw-bg-opacity));
}
.hover\:underline:hover { .hover\:underline:hover {
text-decoration-line: underline; text-decoration-line: underline;
} }
@@ -1378,11 +1290,6 @@ th label {
--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity)); --tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity));
} }
.focus\:ring-violet-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(139 92 246 / var(--tw-ring-opacity));
}
.focus\:ring-offset-2:focus { .focus\:ring-offset-2:focus {
--tw-ring-offset-width: 2px; --tw-ring-offset-width: 2px;
} }
@@ -1391,14 +1298,6 @@ th label {
--tw-ring-offset-color: #bfdbfe; --tw-ring-offset-color: #bfdbfe;
} }
.focus\:ring-offset-violet-200:focus {
--tw-ring-offset-color: #ddd6fe;
}
.group:hover .group-hover\:block {
display: block;
}
:is(.dark .dark\:bg-gray-800) { :is(.dark .dark\:bg-gray-800) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity)); background-color: rgb(31 41 55 / var(--tw-bg-opacity));
@@ -1409,16 +1308,6 @@ th label {
background-color: rgb(17 24 39 / var(--tw-bg-opacity)); background-color: rgb(17 24 39 / var(--tw-bg-opacity));
} }
:is(.dark .dark\:text-slate-400) {
--tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity));
}
:is(.dark .dark\:text-slate-500) {
--tw-text-opacity: 1;
color: rgb(100 116 139 / var(--tw-text-opacity));
}
:is(.dark .dark\:text-slate-600) { :is(.dark .dark\:text-slate-600) {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(71 85 105 / var(--tw-text-opacity)); color: rgb(71 85 105 / var(--tw-text-opacity));
@@ -1430,10 +1319,6 @@ th label {
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:inline {
display: inline;
}
.sm\:table-cell { .sm\:table-cell {
display: table-cell; display: table-cell;
} }
@@ -1442,19 +1327,11 @@ th label {
max-width: 28rem; max-width: 28rem;
} }
.sm\:max-w-xl {
max-width: 36rem;
}
.sm\:px-4 { .sm\:px-4 {
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;
} }
.sm\:pl-12 {
padding-left: 3rem;
}
.sm\:pl-2 { .sm\:pl-2 {
padding-left: 0.5rem; padding-left: 0.5rem;
} }
@@ -1509,10 +1386,6 @@ th label {
display: table-cell; display: table-cell;
} }
.lg\:max-w-3xl {
max-width: 48rem;
}
.lg\:max-w-lg { .lg\:max-w-lg {
max-width: 32rem; max-width: 32rem;
} }
+27 -22
View File
@@ -1,24 +1,29 @@
import { syncSelectInputUntilChanged } from './utils.js'; /**
* @description Sync select field with input field until user focuses it.
* @param {HTMLSelectElement} sourceElementSelector
* @param {HTMLInputElement} targetElementSelector
*/
function syncSelectInputUntilChanged(
sourceElementSelector,
targetElementSelector
) {
const sourceElement = document.querySelector(sourceElementSelector);
const targetElement = document.querySelector(targetElementSelector);
function sourceElementHandler(event) {
let selected = event.target.value;
let selectedValue = document.querySelector(
`#id_game option[value='${selected}']`
).textContent;
targetElement.value = selectedValue;
}
function targetElementHandler(event) {
sourceElement.removeEventListener("change", sourceElementHandler);
}
let syncData = [ sourceElement.addEventListener("change", sourceElementHandler);
{ targetElement.addEventListener("focus", targetElementHandler);
"source": "#id_game", }
"source_value": "dataset.name",
"target": "#id_name",
"target_value": "value"
},
{
"source": "#id_game",
"source_value": "textContent",
"target": "#id_sort_name",
"target_value": "value"
},
{
"source": "#id_game",
"source_value": "dataset.year",
"target": "#id_year_released",
"target_value": "value"
},
]
syncSelectInputUntilChanged(syncData, "form"); window.addEventListener("load", () => {
syncSelectInputUntilChanged("#id_game", "#id_name");
});
+17 -29
View File
@@ -1,9 +1,4 @@
import { import { syncSelectInputUntilChanged, getEl, conditionalElementHandler } from "./utils.js";
syncSelectInputUntilChanged,
getEl,
disableElementsWhenTrue,
disableElementsWhenFalse,
} from "./utils.js";
let syncData = [ let syncData = [
{ {
@@ -16,28 +11,21 @@ let syncData = [
syncSelectInputUntilChanged(syncData, "form"); syncSelectInputUntilChanged(syncData, "form");
function setupElementHandlers() {
disableElementsWhenTrue("#id_type", "game", [
"#id_name",
"#id_related_purchase",
]);
disableElementsWhenFalse("#id_type", "game", ["#id_date_finished"]);
}
document.addEventListener("DOMContentLoaded", setupElementHandlers); let myConfig = [
document.addEventListener("htmx:afterSwap", setupElementHandlers); () => {
getEl("#id_type").onchange = () => { return getEl("#id_type").value == "game";
setupElementHandlers(); },
}; ["#id_name", "#id_related_purchase"],
(el) => {
document.body.addEventListener('htmx:beforeRequest', function(event) { el.disabled = "disabled";
// Assuming 'Purchase1' is the element that triggers the HTMX request },
if (event.target.id === 'id_edition') { (el) => {
var idEditionValue = document.getElementById('id_edition').value; el.disabled = "";
// Condition to check - replace this with your actual logic
if (idEditionValue != '') {
event.preventDefault(); // This cancels the HTMX request
}
} }
}); ]
document.DOMContentLoaded = conditionalElementHandler(...myConfig)
getEl("#id_type").onchange = () => {
conditionalElementHandler(...myConfig)
}
-3
View File
@@ -8,9 +8,6 @@ for (let button of document.querySelectorAll("[data-target]")) {
event.preventDefault(); event.preventDefault();
if (type == "now") { if (type == "now") {
targetElement.value = toISOUTCString(new Date); targetElement.value = toISOUTCString(new Date);
} else if (type == "copy") {
const oppositeName = targetElement.name == "timestamp_start" ? "timestamp_end" : "timestamp_start";
document.querySelector(`[name='${oppositeName}']`).value = targetElement.value;
} else if (type == "toggle") { } else if (type == "toggle") {
if (targetElement.type == "datetime-local") targetElement.type = "text"; if (targetElement.type == "datetime-local") targetElement.type = "text";
else targetElement.type = "datetime-local"; else targetElement.type = "datetime-local";
+30 -72
View File
@@ -3,16 +3,9 @@
* @param {Date} date * @param {Date} date
* @returns {string} * @returns {string}
*/ */
function toISOUTCString(date) { export function toISOUTCString(date) {
function stringAndPad(number) { let month = (date.getMonth() + 1).toString().padStart(2, 0);
return number.toString().padStart(2, 0); return `${date.getFullYear()}-${month}-${date.getDate()}T${date.getHours()}:${date.getMinutes()}`;
}
const year = date.getFullYear();
const month = stringAndPad(date.getMonth() + 1);
const day = stringAndPad(date.getDate());
const hours = stringAndPad(date.getHours());
const minutes = stringAndPad(date.getMinutes());
return `${year}-${month}-${day}T${hours}:${minutes}`;
} }
/** /**
@@ -99,72 +92,37 @@ function getEl(selector) {
return document.getElementsByClassName(selector) return document.getElementsByClassName(selector)
} }
else { else {
return document.getElementsByTagName(selector) return document.getElementsByName(selector)
} }
} }
/** /**
* @description Applies different behaviors to elements based on multiple conditional configurations. * @description Does something to elements when something happens.
* Each configuration is an array containing a condition function, an array of target element selectors, * @param {() => boolean} condition The condition that is being tested.
* and two callback functions for handling matched and unmatched conditions. * @param {string[]} targetElements
* @param {...Array} configs Each configuration is an array of the form: * @param {(elementName: HTMLElement) => void} callbackfn1 Called when the condition matches.
* - 0: {function(): boolean} condition - Function that returns true or false based on a condition. * @param {(elementName: HTMLElement) => void} callbackfn2 Called when the condition doesn't match.
* - 1: {string[]} targetElements - Array of CSS selectors for target elements.
* - 2: {function(HTMLElement): void} callbackfn1 - Function to execute when condition is true.
* - 3: {function(HTMLElement): void} callbackfn2 - Function to execute when condition is false.
*/ */
function conditionalElementHandler(...configs) { function conditionalElementHandler(condition, targetElements, callbackfn1, callbackfn2) {
configs.forEach(([condition, targetElements, callbackfn1, callbackfn2]) => { if (condition()) {
if (condition()) { targetElements.forEach((elementName) => {
targetElements.forEach(elementName => { let el = getEl(elementName);
let el = getEl(elementName); if (el === null) {
if (el === null) { console.error("Element ${elementName} doesn't exist.");
console.error(`Element ${elementName} doesn't exist.`); } else {
} else { callbackfn1(el);
callbackfn1(el); }
} });
}); } else {
} else { targetElements.forEach((elementName) => {
targetElements.forEach(elementName => { let el = getEl(elementName);
let el = getEl(elementName); if (el === null) {
if (el === null) { console.error("Element ${elementName} doesn't exist.");
console.error(`Element ${elementName} doesn't exist.`); } else {
} else { callbackfn2(el);
callbackfn2(el); }
} });
}); }
}
});
} }
function disableElementsWhenFalse(targetSelect, targetValue, elementList) { export { toISOUTCString, syncSelectInputUntilChanged, getEl, conditionalElementHandler };
return conditionalElementHandler([
() => {
return getEl(targetSelect).value != targetValue;
},
elementList,
(el) => {
el.disabled = "disabled";
},
(el) => {
el.disabled = "";
},
]);
}
function disableElementsWhenTrue(targetSelect, targetValue, elementList) {
return conditionalElementHandler([
() => {
return getEl(targetSelect).value == targetValue;
},
elementList,
(el) => {
el.disabled = "disabled";
},
(el) => {
el.disabled = "";
},
]);
}
export { toISOUTCString, syncSelectInputUntilChanged, getEl, conditionalElementHandler, disableElementsWhenFalse, disableElementsWhenTrue, getValueFromProperty };
+7 -13
View File
@@ -6,19 +6,13 @@
{% block content %} {% block content %}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<table class="mx-auto"> <table class="mx-auto">
{% csrf_token %} {% csrf_token %}
{{ form.as_table }}
<tr> {{ form.as_table }}
<td></td> <tr>
<td> <td></td>
<input type="submit" value="Submit" /> <td><input type="submit" value="Submit"/></td>
</td> </tr>
</tr>
</table> </table>
</form> </form>
{% endblock content %} {% endblock content %}
{% block scripts %}
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
{% endblock scripts %}
+9 -19
View File
@@ -6,27 +6,17 @@
{% block content %} {% block content %}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<table class="mx-auto"> <table class="mx-auto">
{% csrf_token %} {% csrf_token %}
{{ form.as_table }}
<tr> {{ form.as_table }}
<td></td> <tr>
<td> <td></td>
<input type="submit" name="submit" value="Submit" /> <td><input type="submit" value="Submit"/></td>
</td> </tr>
</tr>
<tr>
<td></td>
<td>
<input type="submit"
name="submit_and_redirect"
value="Submit & Create Purchase" />
</td>
</tr>
</table> </table>
</form> </form>
{% endblock content %} {% endblock content %}
{% block scripts %} {% block scripts %}
{% if script_name %} {% load static %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script> <script type="module" src="{% static 'js/add_edition.js' %}"></script>
{% endif %}
{% endblock scripts %} {% endblock scripts %}
+27 -31
View File
@@ -1,38 +1,34 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}
{{ title }} {% block title %}{{ title }}{% endblock title %}
{% endblock title %}
{% block content %} {% block content %}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<table class="mx-auto"> <table class="mx-auto">
{% csrf_token %} {% csrf_token %}
{% for field in form %}
<tr> {% for field in form %}
<th>{{ field.label_tag }}</th> <tr>
{% if field.name == "note" %} <th>{{ field.label_tag }}</th>
<td>{{ field }}</td> {% if field.name == "note" %}
{% else %} <td>{{ field }}</td>
<td>{{ field }}</td> {% else %}
{% endif %} <td>{{ field }}</td>
{% if field.name == "timestamp_start" or field.name == "timestamp_end" %} {% endif %}
<td> {% if field.name == "timestamp_start" or field.name == "timestamp_end" %}
<div class="basic-button-container"> <td>
<button class="basic-button" data-target="{{ field.name }}" data-type="now">Set to now</button> <div class="basic-button-container">
<button class="basic-button" <button class="basic-button" data-target="{{field.name}}" data-type="now">Set to now</button>
data-target="{{ field.name }}" <button class="basic-button" data-target="{{field.name}}" data-type="toggle">Toggle text</button>
data-type="toggle">Toggle text</button> </div>
<button class="basic-button" data-target="{{ field.name }}" data-type="copy">Copy</button> </td>
</div> {% endif %}
</td> </tr>
{% endif %} {% endfor %}
</tr> <tr>
{% endfor %} <td></td>
<tr> <td><input type="submit" value="Submit"/></td>
<td></td> </tr>
<td>
<input type="submit" value="Submit" />
</td>
</tr>
</table> </table>
</form> </form>
{% load static %} {% load static %}
+10 -16
View File
@@ -2,29 +2,22 @@
<html lang="en"> <html lang="en">
{% load static %} {% load static %}
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8"/>
<meta name="description" content="Self-hosted time-tracker." /> <meta name="description" content="Self-hosted time-tracker."/>
<meta name="keywords" content="time, tracking, video games, self-hosted" /> <meta name="keywords" content="time, tracking, video games, self-hosted"/>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Timetracker - <title>Timetracker - {% block title %}Untitled{% endblock title %}</title>
{% block title %}
Untitled
{% endblock title %}
</title>
<script src="{% static 'js/htmx.min.js' %}"></script> <script src="{% static 'js/htmx.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'base.css' %}" /> <link rel="stylesheet" href="{% static 'base.css' %}" />
</head> </head>
<body class="dark" hx-indicator="#indicator" hx-boost="true">
<img id="indicator" <body class="dark">
src="{% static 'icons/loading.png' %}" <img id="indicator" src="{% static 'icons/loading.png' %}" class="absolute right-3 top-3 animate-spin htmx-indicator" />
class="absolute right-3 top-3 animate-spin htmx-indicator" />
<div class="dark:bg-gray-800 min-h-screen"> <div class="dark:bg-gray-800 min-h-screen">
<nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded"> <nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
<div class="container flex flex-wrap items-center justify-between mx-auto"> <div class="container flex flex-wrap items-center justify-between mx-auto">
<a href="{% url 'list_sessions_recent' %}" class="flex items-center"> <a href="{% url 'list_sessions_recent' %}" class="flex items-center">
<span class="text-4xl"> <span class="text-4xl"></span>
<img src="{% static 'icons/schedule.png' %}" width="48" class="mr-4" />
</span>
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span> <span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
</a> </a>
<div class="w-full md:block md:w-auto"> <div class="w-full md:block md:w-auto">
@@ -98,4 +91,5 @@
{% block scripts %} {% block scripts %}
{% endblock scripts %} {% endblock scripts %}
</body> </body>
</html> </html>
+22 -13
View File
@@ -1,13 +1,22 @@
{% comment %} <button
title type="button"
text title="{{ title }}"
{% endcomment %} autofocus
<a href="{{ link }}" class="truncate max-w-xs sm:max-w-md lg:max-w-lg py-1 px-2 bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg"
title="{{ title }}" >
class="truncate max-w-xs py-1 px-2 text-xs bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-sm"> <svg
{% comment %} <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="self-center w-6 h-6 inline"> xmlns="http://www.w3.org/2000/svg"
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" /> fill="none"
</svg> viewBox="0 0 24 24"
{% endcomment %} stroke-width="1.5"
{{ text }} stroke="currentColor"
</a> class="self-center w-6 h-6 inline"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z"
/>
</svg>
{{ text }}
</button>
+69 -64
View File
@@ -1,68 +1,73 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% block title %}
{{ title }} {% block title %}{{ title }}{% endblock title %}
{% endblock title %}
{% block content %} {% block content %}
{% if dataset.count >= 1 %}
<div class="mx-auto text-center my-4"> {% if dataset.count >= 1 %}
<a id="last-session-start" <div class="mx-auto text-center my-4">
href="{% url 'start_session_same_as_last' last.id %}" <a
hx-get="{% url 'start_session_same_as_last' last.id %}" id="last-session-start"
hx-swap="afterbegin" href="{% url 'start_session' last.id %}"
hx-target=".responsive-table tbody" hx-get="{% url 'start_session' last.id %}"
hx-select=".responsive-table tbody tr:first-child" hx-indicator="#indicator"
onClick="document.querySelector('#last-session-start').classList.add('invisible')" hx-swap="afterbegin"
class="{% if last.timestamp_end == null %}invisible{% endif %}"> hx-target=".responsive-table tbody"
{% include 'components/button_start.html' with text=last.purchase title="Start session of last played game" only %} hx-select=".responsive-table tbody tr:first-child"
</a> onClick="document.querySelector('#last-session-start').classList.add('invisible')"
</div> class="{% if last.timestamp_end == null %}invisible{% endif %}"
{% endif %} >
{% if dataset.count != 0 %} {% include 'components/button.html' with text=last.purchase title="Start session of last played game" only %}
<table class="responsive-table"> </a>
<thead> </div>
<tr> {% endif %}
<th class="px-2 sm:px-4 md:px-6 md:py-2">Name</th>
<th class="hidden sm:table-cell px-2 sm:px-4 md:px-6 md:py-2">Start</th> <table class="responsive-table">
<th class="hidden lg:table-cell px-2 sm:px-4 md:px-6 md:py-2">End</th> <thead>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Duration</th> <tr>
</tr> <th class="px-2 sm:px-4 md:px-6 md:py-2">Name</th>
</thead> <th class="hidden sm:table-cell px-2 sm:px-4 md:px-6 md:py-2">Start</th>
<tbody> <th class="hidden lg:table-cell px-2 sm:px-4 md:px-6 md:py-2">End</th>
{% for data in dataset %} <th class="px-2 sm:px-4 md:px-6 md:py-2">Duration</th>
<tr> </tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char md:max-w-40char"> </thead>
<a class="underline decoration-slate-500 sm:decoration-2" <tbody>
href="{% url 'view_game' data.purchase.edition.game.id %}"> {% for data in dataset %}
{{ data.purchase.edition }} <tr>
</a> <td
</td> class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char md:max-w-40char"
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden sm:table-cell"> >
{{ data.timestamp_start | date:"d/m/Y H:i" }} {{ data.purchase.edition }}
</td> </td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden sm:table-cell">
{% if data.unfinished %} {{ data.timestamp_start | date:"d/m/Y H:i" }}
<a href="{% url 'update_session' data.id %}" </td>
hx-get="{% url 'update_session' data.id %}" <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell">
hx-swap="outerHTML" {% if data.unfinished %}
hx-target=".responsive-table tbody tr:first-child" <a
hx-select=".responsive-table tbody tr:first-child" href="{% url 'update_session' data.id %}"
hx-indicator="#indicator" hx-get="{% url 'update_session' data.id %}"
onClick="document.querySelector('#last-session-start').classList.remove('invisible')"> hx-swap="outerHTML"
<span class="text-yellow-300">Finish now?</span> hx-target=".responsive-table tbody tr:first-child"
</a> hx-select=".responsive-table tbody tr:first-child"
{% elif data.duration_manual %} hx-indicator="#indicator"
-- onClick="document.querySelector('#last-session-start').classList.remove('invisible')"
{% else %} >
{{ data.timestamp_end | date:"d/m/Y H:i" }} <span class="text-yellow-300">Finish now?</span>
{% endif %} </a>
</td> {% elif data.duration_manual %}
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ data.duration_formatted }}</td> --
</tr> {% else %}
{% endfor %} {{ data.timestamp_end | date:"d/m/Y H:i" }}
</tbody> {% endif %}
</table> </td>
{% else %} <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
<div class="mx-auto text-center text-slate-300 text-xl">No sessions found.</div> {{ data.duration_formatted }}
{% endif %} </td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock content %} {% endblock content %}
+12 -11
View File
@@ -194,17 +194,18 @@
</thead> </thead>
<tbody> <tbody>
{% for purchase in all_purchased_this_year %} {% for purchase in all_purchased_this_year %}
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
<a class="underline decoration-slate-500 sm:decoration-2" <a class="underline decoration-slate-500 sm:decoration-2" href="{% url 'edit_purchase' purchase.id %}">
href="{% url 'edit_purchase' purchase.id %}"> {{ purchase.edition.name }}
{{ purchase.edition.name }} {% if purchase.type != "game" %}
{% if purchase.type != "game" %}({{ purchase.name }}, {{ purchase.get_type_display }}){% endif %} ({{ purchase.name }}, {{ purchase.get_type_display }})
</a> {% endif %}
</td> </a>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td> </td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
</tr> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
+30 -25
View File
@@ -22,37 +22,42 @@
</h1> </h1>
<ul> <ul>
{% for edition in editions %} {% for edition in editions %}
<li class="sm:pl-2 flex items-center"> <li class="sm:pl-2 flex items-center">
{{ edition.name }} ({{ edition.platform }}, {{ edition.year_released }}) {{ edition.name }} ({{ edition.platform }}, {{ edition.year_released }})
{% if edition.wikidata %} {% if edition.wikidata %}
<span class="hidden sm:inline"> <span class="hidden sm:inline">
<a href="https://www.wikidata.org/wiki/{{ edition.wikidata }}"> <a href="https://www.wikidata.org/wiki/{{ edition.wikidata }}">
<img class="inline mx-2 w-6" src="{% static 'icons/wikidata.png' %}" /> <img class="inline mx-2 w-6" src="{% static 'icons/wikidata.png' %}"/>
</a> </a>
</span> </span>
{% endif %} {% endif %}
{% url 'edit_edition' edition.id as edit_url %} {% url 'edit_edition' edition.id as edit_url %}
{% include 'components/edit_button.html' with edit_url=edit_url %} {% include 'components/edit_button.html' with edit_url=edit_url %}
</li> </li>
<ul> {% endfor %}
{% for purchase in edition.game_purchases %} </ul>
<li class="sm:pl-6 flex items-center"> <h1 class="text-3xl mt-4 mb-1">Purchases <span class="dark:text-slate-500">({{ purchases.count }})</span></h1>
{{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }} <ul>
{% if purchase.price != 0 %}({{ purchase.price }} {{ purchase.price_currency }}){% endif %} {% for purchase in purchases %}
{% url 'edit_purchase' purchase.id as edit_url %} <li class="sm:pl-2 flex items-center">
{% include 'components/edit_button.html' with edit_url=edit_url %} {{ purchase.platform }}
</li> ({{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}, {{ purchase.price }} {{ purchase.price_currency}})
{% url 'edit_purchase' purchase.id as edit_url %}
{% include 'components/edit_button.html' with edit_url=edit_url %}
{% if purchase.related_purchases %}
<li>
<ul> <ul>
{% for related_purchase in purchase.nongame_related_purchases %} {% for related_purchase in purchase.related_purchases %}
<li class="sm:pl-12 flex items-center"> <li class="sm:pl-6 flex items-center">
{{ related_purchase.name }} ({{ related_purchase.get_type_display }}, {{ purchase.platform }}, {{ related_purchase.date_purchased | date:"Y" }}, {{ related_purchase.price }} {{ related_purchase.price_currency }}) {{ related_purchase.name}} ({{ related_purchase.get_type_display }}, {{ related_purchase.date_purchased | date:"Y" }}, {{ related_purchase.price }} {{ related_purchase.price_currency}})
{% url 'edit_purchase' related_purchase.id as edit_url %} {% url 'edit_purchase' related_purchase.id as edit_url %}
{% include 'components/edit_button.html' with edit_url=edit_url %} {% include 'components/edit_button.html' with edit_url=edit_url %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endfor %} </li>
</ul> {% endif %}
</li>
{% endfor %} {% endfor %}
</ul> </ul>
<h1 class="text-3xl mt-4 mb-1 flex gap-2 items-center"> <h1 class="text-3xl mt-4 mb-1 flex gap-2 items-center">
-6
View File
@@ -93,10 +93,4 @@ urlpatterns = [
{"filter": "ownership_type"}, {"filter": "ownership_type"},
name="list_sessions_by_ownership_type", name="list_sessions_by_ownership_type",
), ),
path("stats/", views.stats, name="stats_current_year"),
path(
"stats/<int:year>",
views.stats,
name="stats_by_year",
),
] ]
+25 -45
View File
@@ -1,15 +1,8 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Callable from zoneinfo import ZoneInfo
from django.core.exceptions import ObjectDoesNotExist from common.time import now as now_with_tz
from django.db.models import Count, F, Prefetch, Sum from django.conf import settings
from django.db.models.functions import TruncDate
from django.http import (
HttpRequest,
HttpResponse,
HttpResponseBadRequest,
HttpResponseRedirect,
)
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@@ -140,10 +133,23 @@ def edit_game(request, game_id=None):
def view_game(request, game_id=None): def view_game(request, game_id=None):
game = Game.objects.get(id=game_id) game = Game.objects.get(id=game_id)
nongame_related_purchases_prefetch = Prefetch( context["title"] = "View Game"
"related_purchases", context["game"] = game
queryset=Purchase.objects.exclude(type=Purchase.GAME), context["editions"] = Edition.objects.filter(game_id=game_id)
to_attr="nongame_related_purchases", game_purchases = Purchase.objects.filter(edition__game_id=game_id).filter(
type=Purchase.GAME
)
for purchase in game_purchases:
purchase.related_purchases = Purchase.objects.exclude(
type=Purchase.GAME
).filter(related_purchase=purchase.id)
context["purchases"] = game_purchases
context["sessions"] = Session.objects.filter(
purchase__edition__game_id=game_id
).order_by("-timestamp_start")
context["total_hours"] = float(
format_duration(context["sessions"].total_duration_unformatted(), "%2.1H")
) )
game_purchases_prefetch = Prefetch( game_purchases_prefetch = Prefetch(
"purchase_set", "purchase_set",
@@ -253,12 +259,6 @@ def start_session_same_as_last(request, last_session_id: int):
return redirect("list_sessions") return redirect("list_sessions")
# def delete_session(request, session_id=None):
# session = Session.objects.get(id=session_id)
# session.delete()
# return redirect("list_sessions")
def list_sessions( def list_sessions(
request, request,
filter="", filter="",
@@ -287,12 +287,10 @@ def list_sessions(
dataset = Session.objects.filter(purchase__ownership_type=ownership_type) dataset = Session.objects.filter(purchase__ownership_type=ownership_type)
context["ownership_type"] = dict(Purchase.OWNERSHIP_TYPES)[ownership_type] context["ownership_type"] = dict(Purchase.OWNERSHIP_TYPES)[ownership_type]
elif filter == "recent": elif filter == "recent":
current_year = timezone.now().year
first_day_of_year = timezone.make_aware(datetime(current_year, 1, 1))
dataset = Session.objects.filter( dataset = Session.objects.filter(
timestamp_start__gte=first_day_of_year timestamp_start__gte=datetime.now() - timedelta(days=30)
).order_by("-timestamp_start") )
context["title"] = "This year" context["title"] = "Last 30 days"
else: else:
# by default, sort from newest to oldest # by default, sort from newest to oldest
dataset = Session.objects.order_by("-timestamp_start") dataset = Session.objects.order_by("-timestamp_start")
@@ -306,10 +304,8 @@ def list_sessions(
context["total_duration"] = dataset.total_duration_formatted() context["total_duration"] = dataset.total_duration_formatted()
context["dataset"] = dataset context["dataset"] = dataset
try: # cannot use dataset[0] here because that might be only partial QuerySet
context["last"] = Session.objects.latest() context["last"] = Session.objects.all().order_by("timestamp_start").last()
except ObjectDoesNotExist:
context["last"] = None
return render(request, "list_sessions.html", context) return render(request, "list_sessions.html", context)
@@ -547,19 +543,3 @@ def add_platform(request):
context["form"] = form context["form"] = form
context["title"] = "Add New Platform" context["title"] = "Add New Platform"
return render(request, "add.html", context) return render(request, "add.html", context)
def add_device(request):
context = {}
form = DeviceForm(request.POST or None)
if form.is_valid():
form.save()
return redirect("index")
context["form"] = form
context["title"] = "Add New Device"
return render(request, "add.html", context)
def index(request):
return redirect("list_sessions_recent")
Generated
+14 -59
View File
@@ -98,17 +98,6 @@ editorconfig = ">=0.12.2"
jsbeautifier = "*" jsbeautifier = "*"
six = ">=1.13.0" six = ">=1.13.0"
[[package]]
name = "distlib"
version = "0.3.7"
description = "Distribution utilities"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"},
{file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
]
[[package]] [[package]]
name = "django" name = "django"
version = "4.2.7" version = "4.2.7"
@@ -248,9 +237,7 @@ files = [
] ]
[package.extras] [package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] test = ["pytest (>=6)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
typing = ["typing-extensions (>=4.8)"]
[[package]] [[package]]
name = "gunicorn" name = "gunicorn"
@@ -361,20 +348,6 @@ files = [
editorconfig = ">=0.12.2" editorconfig = ">=0.12.2"
six = ">=1.13.0" six = ">=1.13.0"
[[package]]
name = "json5"
version = "0.9.14"
description = "A Python implementation of the JSON5 data format."
optional = false
python-versions = "*"
files = [
{file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"},
{file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"},
]
[package.extras]
dev = ["hypothesis"]
[[package]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "2.1.3" version = "2.1.3"
@@ -505,23 +478,22 @@ files = [
] ]
[[package]] [[package]]
name = "nodeenv" name = "packaging"
version = "1.8.0" version = "22.0"
description = "Node.js virtual environment builder" description = "Core utilities for Python packages"
category = "dev"
optional = false optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" python-versions = ">=3.7"
files = [ files = [
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"},
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"},
] ]
[package.dependencies]
setuptools = "*"
[[package]] [[package]]
name = "packaging" name = "pathspec"
version = "23.2" version = "0.10.3"
description = "Core utilities for Python packages" description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@@ -657,24 +629,6 @@ files = [
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"] testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
version = "3.5.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.8"
files = [
{file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"},
{file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"},
]
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "7.4.3" version = "7.4.3"
@@ -693,7 +647,7 @@ packaging = "*"
pluggy = ">=0.12,<2.0" pluggy = ">=0.12,<2.0"
[package.extras] [package.extras]
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
@@ -871,6 +825,7 @@ testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jar
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
description = "Python 2 and 3 compatibility utilities" description = "Python 2 and 3 compatibility utilities"
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [ files = [