Compare commits

..

176 Commits

Author SHA1 Message Date
Lukáš Kucharczyk d6f77c0c19 Add django-cors-headers
Django CI/CD / build-and-push (pull_request) Has been cancelled Details
2023-11-18 09:23:54 +01:00
Lukáš Kucharczyk 52881a88c6 Remove top-level package.json 2023-11-18 09:23:54 +01:00
Lukáš Kucharczyk 76b09fea39 Integrate TailwindCSS 2023-11-18 09:23:54 +01:00
Lukáš Kucharczyk 89f6959fc1 Change API url 2023-11-18 09:23:10 +01:00
Lukáš Kucharczyk 77d6ad2618 Add django-rest-framework 2023-11-18 09:23:09 +01:00
Lukáš Kucharczyk 125d17da8a Merge sessions and notes 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 38d416c4f3 Don't display prices if zero 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 63bb8e5fa6 Apply djlint 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 71ede4e4dd Add pre-commit 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 12159e00c1 Use the black profile for isort 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk f4bdb0a3e2 Use isort on migrations 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 583efe1754 Handle empty edition_id 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 0f5067d66d Prevent HTMX from messing up the initial state 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk a32d6c38ab Account for no sessions 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 4f67bd85c7 Enable hx-boost everywhere 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk cabcaebead Fix form not syncing due to HTMX 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 047f67c4cb Fix error 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 34118e1319 Re-instance gitea actions 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk bd07f19939 Update .drone.yml testing 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 78a50739a8 Formatting 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk a579ef7d7d Move static files in prod 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 8ab2dcc950 Fix docker-compose.yml 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk ee4efae594 Improve Dockerfile
Major inspiration (aka direct theft) from https://github.com/wemake-services/wemake-django-template
2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk b3eed84e1b Fix .dockerignore 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk eb53a5983c Remove Django admin 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 023736d544 Update dependencies 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 4a990bb6c6 Fix naive date 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk af85607ada isort 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 978714125b 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-18 09:22:26 +01:00
Lukáš Kucharczyk 51ba5cfa20 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-18 09:22:26 +01:00
Lukáš Kucharczyk a7dd0c5556 Only allow choosing purchases of selected edition 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 7b2dba7483 Name and related_purchase validation for non-games 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 250fa0ee9d Sort imports, remove cruft 2023-11-18 09:22:26 +01:00
Lukáš Kucharczyk 952961a3ad Fix wrong playrange ordering 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 1f9b3d5682 Improve how editions and purchases are displayed 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk c8e8ba8c8e Game View: order editions by year 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk ab9c1d61dd Version 1.5.1 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk d64ad125ff Improve and cleanup ConditionalElementHandler 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 410f20acea Improve purchase __str__ 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk d280fa3798 Disallow choosing non-game purchase as related purchase 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 29d6cb8a4a Version 1.5.0 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk c4fdefadb0 Order purchases by date on game view 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 7ba212e718 Add purchase types 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 7b459a2735 CI: run migrations before tests 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk f0822ff221 Make sure empty stats are 0 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 200819ceb0 Change stats years to 2000 up to current year 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 8a433616c4 Add stat for finished this year's games 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 809e8e2d7c Fix detecting manual durations 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 528b7d78a1 Remove deprecated USE_L10N 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 5270525fc2 Add more tests 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 09faa8e6fd Fix edge case in format_duration
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-18 09:22:25 +01:00
Lukáš Kucharczyk 87f50cedd2 Fix ordering 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk cbf872d21f Also prefill year between game and edition 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 881e5df3d1 Version 1.4.0 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk fa6ac21b42 Adding new games is easier 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 01ce582b74 Refactor, remove cruft 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 827279fb3d Add backlog decrease count 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk fa2b9b5c88 Fix sort names getting mangled 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 55ff3cd1a9 Pre-fill year when adding edition 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 68d1bfc2b9 UX improvements
* 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-18 09:22:25 +01:00
Lukáš Kucharczyk 4112d593f6 Fix hardcoded year 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 553030c08e Order devices alphabetically on new session form 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 06a4319623 Use safe_division in more places 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk cb99d8b656 Fix potential division by zero 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk b8df870cca Add more stats
* Finished (count)
* Unfinished (count)
* Refunded (count)
2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 7781efd849 Add more stats
* All finished games
* All finished 2023 games
* All finished games that were purchased this year
* Total sessions
* Days played
2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 9fa95758bf stats: change overall stats table layout 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk b81989a352 Model changes
* 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-18 09:22:25 +01:00
Lukáš Kucharczyk 5e6a5a4024 simplify playtime range display 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 288bf65afb Version 1.3.0 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 1d0bafd72d Make some pages redirect back instead to session list 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk db9ac2edeb Add more stats 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 2076fe20d1 Extend stats range to 2018 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk ff30962ce9 Remove cruft 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk d06e284426 Group by game instead of purchase 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 1f36a32b3d Reorder imports in views.py 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk e87f985cb3 Remove hardcoded year 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 8ca62f32e3 Make navigation more compact 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 5bc04c463a Add stats link, year selector 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 0130ab0059 Limit stats of single year correctly 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk df5e605582 Version 1.2.0 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk ade8a47c86 Add yearly stats page
Fixes #15
2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 4dd6f13160 Add a button to start session from game overview
Fix #62
2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 50f0571144 Add more time tests for fractional numbers 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 3fae309ff2 Do not format as float if no precision specified 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk e0a4aae6f6 Version 1.1.2 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk e1848caee9 Display durations in a consistent manner
Fixes #61
2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 4767c17004 Version 1.1.1 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 2cbe10a983 Remove debugging cruft from container 2023-11-18 09:22:25 +01:00
Lukáš Kucharczyk 693e1b1cd3 Add edit buttons to game overview, notes 2023-11-18 09:22:24 +01:00
Lukáš Kucharczyk 8eeeb9d3c9 Version 1.1.0 2023-11-18 09:22:24 +01:00
Lukáš Kucharczyk caa2ae06e1 Improve game overview
- add counts for each section
- add average hours per session
2023-11-18 09:22:24 +01:00
Lukáš Kucharczyk b9869cf232 Fix playtime range not working with manual session
Fixes #55
2023-11-18 09:22:24 +01:00
Lukáš Kucharczyk 36f80b7f01 Remove unused chart functionality 2023-11-18 09:22:24 +01:00
Lukáš Kucharczyk 9df959d989 ci: automatically redeploy container 2023-11-18 09:21:40 +01:00
Lukáš Kucharczyk db7b60bdf2 Organize changelog better 2023-11-18 09:21:40 +01:00
Lukáš Kucharczyk 470426004e Fix session being wrongly considered in progress
Fixes #58
2023-11-18 09:21:40 +01:00
Lukáš Kucharczyk 3e04ef2b83 Fix date range on game overview 2023-11-18 09:21:40 +01:00
Lukáš Kucharczyk 760f12e6b1 Adjust game edit URL 2023-11-18 09:21:40 +01:00
Lukáš Kucharczyk ac1f86cac3 Add game overview page
Fixes #8
2023-11-18 09:21:40 +01:00
Lukáš Kucharczyk 775ec6424b Try to shutdown container gracefully and faster 2023-11-18 09:21:40 +01:00
Lukáš Kucharczyk 6000384c72 Switch fonts to WOFF2 2023-11-18 09:21:40 +01:00
Lukáš Kucharczyk 8786a4518e Further improve session list 2023-11-18 09:21:34 +01:00
Lukáš Kucharczyk a2ababaebc Update base.css 2023-11-18 09:21:24 +01:00
Lukáš Kucharczyk aae09a913c Update .vscode 2023-11-18 09:21:24 +01:00
Lukáš Kucharczyk c07f966cf1 Update CSS 2023-11-18 09:21:24 +01:00
Lukáš Kucharczyk d653f83a33 Improve session list
Fixes #53
2023-11-18 09:21:24 +01:00
Lukáš Kucharczyk 552d4bee9e Focus important fields on forms 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 8293737058 Update generated CSS 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 5234e19705 Add hacky way to not reload page when starting/ending session
Partially fixes #52
2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 077d8d1e3c Add time copy button, improve session editing 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 8ba5344a4d Fix bug when filtering only manual sessions (#51) 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 9d2a0ace33 add robots.txt 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 9b1f4e95e0 disable deleting sessions in code 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk d1f843a267 allow admin in prod 2/2 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 65a110b37c allow admin in prod 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk bbb5d0bfd5 disable deleting sessions 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 61c3b02472 install dev dependecies 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 4355953e42 do not filter out refunded games when adding session 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 1610b7d8a4 allow django admit 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk a9c3e93954 Change recent session view to current year 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk fb8fea4724 input datetime-local needs day to also be two digits 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 11b19cbace Exclude refunded purchases from start session form 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk fd9d531baf Update styles 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 3c02e3beaa Add .editorconfig 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 2b9f321edf Improve forms, add helper buttons on add session form 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk ee44c52324 Register Edition, Device in the admin UI 2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk b30679f6e7 Use date and datetime inputs
Properly implements 4d91a76513
2023-11-18 09:20:54 +01:00
Lukáš Kucharczyk 42d002a9d9 Sort games by name on edition form 2023-11-18 09:20:24 +01:00
Lukáš Kucharczyk 56ffde8402 Sync game name and edition name fields for QOL 2023-11-18 09:20:24 +01:00
Lukáš Kucharczyk 94706595c5 Fix gitignoring root static folder 2023-11-18 09:20:24 +01:00
Lukáš Kucharczyk 352f6133d8 Sort games alphabetically on edition form 2023-11-18 09:20:24 +01:00
Lukáš Kucharczyk 8f1bbab895 Version 1.0.3 2023-11-18 09:20:24 +01:00
Lukáš Kucharczyk 38b25ff27e Allow editing filtered entities from session list 2023-11-18 09:20:24 +01:00
Lukáš Kucharczyk 4086c078bd Improve display of editions on purchase form 2023-11-18 09:20:23 +01:00
Lukáš Kucharczyk c1023bdcbc Sort platforms alphabetically on edition form 2023-11-18 09:20:23 +01:00
Lukáš Kucharczyk 115a2a0844 Add wikidata ID and year for editions 2023-11-18 09:20:23 +01:00
Lukáš Kucharczyk 326e4aac60 Add icons 2023-11-18 09:20:23 +01:00
Lukáš Kucharczyk fa4f256cb8 Change timetracker icon 2023-11-18 09:20:23 +01:00
Lukáš Kucharczyk 739a41c4aa Add icons for session filters 2023-11-18 09:20:23 +01:00
Lukáš Kucharczyk 376beea02f Fix error when generating charts with less than 2 entries 2023-11-18 09:20:23 +01:00
Lukáš Kucharczyk 5e54871374 Allow filtering by game, edition, purchase from the session list 2023-11-18 09:20:23 +01:00
Lukáš Kucharczyk 42e9ba15c5 Fix form styling 2023-11-18 09:20:23 +01:00
Lukáš Kucharczyk 2680476fe8 Version 1.0.2 2023-11-18 09:20:11 +01:00
Lukáš Kucharczyk a05e0e0fa3 Allow editing editions 2023-11-18 09:20:11 +01:00
Lukáš Kucharczyk d2455ec82a Allow editing purchases 2023-11-18 09:20:11 +01:00
Lukáš Kucharczyk f0e307dcfb Fix session starting 2023-11-18 09:20:11 +01:00
Lukáš Kucharczyk 1a6e5dc8fc Fix ownership display 2023-11-18 09:20:11 +01:00
Lukáš Kucharczyk f8844ce091 Add support for device info
Closes #49
2023-11-18 09:20:11 +01:00
Lukáš Kucharczyk 5efb054c30 Add support for purchase ownership information
Closes #48
2023-11-18 09:20:11 +01:00
Lukáš Kucharczyk 622c52bb20 Add support for prices on purchases 2023-11-18 09:20:11 +01:00
Lukáš Kucharczyk d447302b46 Re-introduce index view 2023-11-18 09:20:11 +01:00
Lukáš Kucharczyk b0b3567bf3 Fix displaying of "filterying by..." text 2023-11-18 09:20:11 +01:00
Lukáš Kucharczyk 812f74d79f Add support for game editions (#28) 2023-11-18 09:20:11 +01:00
Lukáš Kucharczyk 7974b74cd4 Fix make css 2023-11-18 09:20:11 +01:00
Lukáš Kucharczyk c61891cff4 Fix redirect to index 2023-11-18 09:19:59 +01:00
Lukáš Kucharczyk 6790c434f7 Order homepage sessions from newest 2023-11-18 09:19:59 +01:00
Lukáš Kucharczyk b45889fd85 Version 1.0.1 2023-11-18 09:19:59 +01:00
Lukáš Kucharczyk 791203c2dc Show only last 30 days on homepage
Fixes #47
2023-11-18 09:19:59 +01:00
Lukáš Kucharczyk 8918df1dfd Show markers on smaller graphs 2023-11-18 09:19:42 +01:00
Lukáš Kucharczyk c1b1cda38e Make it possible to edit sessions
Fixes #46
2023-11-18 09:19:42 +01:00
Lukáš Kucharczyk 345315db38
Start converting to react-router
continuous-integration/drone/push Build is passing Details
2023-01-25 17:50:15 +01:00
Lukáš Kucharczyk d9076e4367
Add apiService 2023-01-25 17:49:02 +01:00
Lukáš Kucharczyk 65e556656f
Fix Vite proxy config 2023-01-25 17:48:51 +01:00
Lukáš Kucharczyk 22fd1c356a
Add tailwind typography and forms 2023-01-25 17:48:33 +01:00
Lukáš Kucharczyk 9080425e1e
Add django-cors-headers 2023-01-25 17:48:17 +01:00
Lukáš Kucharczyk c64c68791f
Add eslint and prettier 2023-01-25 13:31:05 +01:00
Lukáš Kucharczyk f6edc8688a
Remove cruft 2023-01-25 13:19:50 +01:00
Lukáš Kucharczyk 1c73bba301
Remove top-level package.json 2023-01-25 13:07:45 +01:00
Lukáš Kucharczyk da5783c08f
Add Nav component 2023-01-25 12:56:32 +01:00
Lukáš Kucharczyk 04e6b01c2a
Integrate TailwindCSS 2023-01-25 12:47:39 +01:00
Lukáš Kucharczyk 3a14ea98a6
Change API url 2023-01-25 12:17:56 +01:00
Lukáš Kucharczyk ba27511b97
Add django-rest-framework 2023-01-25 12:05:37 +01:00
Lukáš Kucharczyk ae95015f55
CI: autotag
continuous-integration/drone/push Build is passing Details
2023-01-25 11:49:22 +01:00
Lukáš Kucharczyk c217fd30ef
Add vite proxy config 2023-01-25 11:48:17 +01:00
Lukáš Kucharczyk ec16d40361
Add vite react
continuous-integration/drone/push Build is failing Details
2023-01-25 11:44:04 +01:00
22 changed files with 664 additions and 377 deletions

View File

@ -3,10 +3,12 @@ name: Django CI/CD
on: on:
push: push:
branches: [ main ] branches: [ main ]
paths-ignore: [ 'README.md' ] pull_request:
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

View File

@ -1,5 +1,17 @@
## 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:
@ -8,6 +20,9 @@
* 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
@ -95,22 +110,24 @@
### Enhancements ### Enhancements
* Improve form appearance * Improve form appearance
* Add helper buttons next to datime fields
* Change recent session view to current year instead of last 30 days
* 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 * 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) * Improve session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/53)
* Change fonts to IBM Plex
* Only use local WOFF2 font files ### 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 ## 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
* Add icons for the above * Allow editing filtered entities from session list
## 1.0.2 / 2023-02-18 21:48+01:00 ## 1.0.2 / 2023-02-18 21:48+01:00

View File

@ -9,11 +9,6 @@ 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(
@ -87,6 +82,7 @@ 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 = [
@ -151,7 +147,7 @@ class EditionForm(forms.ModelForm):
class Meta: class Meta:
model = Edition model = Edition
fields = ["game", "name", "platform", "year_released", "wikidata"] fields = ["game", "name", "sort_name", "platform", "year_released", "wikidata"]
class GameForm(forms.ModelForm): class GameForm(forms.ModelForm):

View File

@ -1,10 +1,11 @@
# Generated by Django 4.1.5 on 2023-11-14 08:41 # Generated by Django 4.1.5 on 2023-11-14 08:41
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("games", "0026_purchase_type"), ("games", "0026_purchase_type"),
] ]

View File

@ -1,6 +1,7 @@
# 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

View File

@ -2,7 +2,6 @@ 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
@ -39,9 +38,13 @@ 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)
platform = models.ForeignKey("Platform", on_delete=models.CASCADE) sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
year_released = models.IntegerField(default=datetime.today().year) platform = models.ForeignKey(
"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
@ -121,14 +124,25 @@ 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", on_delete=models.SET_NULL, default=None, null=True, blank=True "Purchase",
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):
platform_info = self.platform additional_info = [
if self.platform != self.edition.platform: self.get_type_display() if self.type != Purchase.GAME else "",
platform_info = f"{self.edition.platform} version on {self.platform}" f"{self.edition.platform} version on {self.platform}"
return f"{self.edition} ({platform_info}, {self.edition.year_released}, {self.get_ownership_type_display()})" 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): def is_game(self):
return self.type == self.GAME return self.type == self.GAME

View File

@ -755,6 +755,10 @@ select {
position: absolute; position: absolute;
} }
.relative {
position: relative;
}
.bottom-2 { .bottom-2 {
bottom: 0.5rem; bottom: 0.5rem;
} }
@ -771,20 +775,55 @@ 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;
} }
@ -797,6 +836,10 @@ select {
display: block; display: block;
} }
.inline-block {
display: inline-block;
}
.inline { .inline {
display: inline; display: inline;
} }
@ -813,6 +856,14 @@ select {
display: none; display: none;
} }
.h-4 {
height: 1rem;
}
.h-5 {
height: 1.25rem;
}
.h-6 { .h-6 {
height: 1.5rem; height: 1.5rem;
} }
@ -821,10 +872,22 @@ 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%;
} }
@ -833,6 +896,10 @@ 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;
} }
@ -859,10 +926,18 @@ 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;
} }
@ -885,16 +960,35 @@ 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));
@ -909,6 +1003,11 @@ 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;
@ -927,6 +1026,10 @@ select {
padding-right: 1rem; padding-right: 1rem;
} }
.pt-1 {
padding-top: 0.25rem;
}
.text-center { .text-center {
text-align: center; text-align: center;
} }
@ -935,12 +1038,9 @@ 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;
} }
.font-serif { .text-3xl {
font-family: IBM Plex Serif, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; font-size: 1.875rem;
} 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 {
@ -948,11 +1048,21 @@ 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;
@ -963,57 +1073,19 @@ 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;
} }
.font-bold {
font-weight: 700;
}
.capitalize {
text-transform: capitalize;
}
.italic { .italic {
font-style: italic; font-style: italic;
} }
.text-gray-700 {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity));
}
.text-slate-300 { .text-slate-300 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity)); color: rgb(203 213 225 / var(--tw-text-opacity));
@ -1029,6 +1101,14 @@ 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);
@ -1128,12 +1208,10 @@ select {
} }
.responsive-table thead th:not(:first-child), .responsive-table thead th:not(:first-child),
td:not(:first-child) { .responsive-table 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
@ -1265,11 +1343,21 @@ 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;
} }
@ -1290,6 +1378,11 @@ 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;
} }
@ -1298,6 +1391,14 @@ 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));
@ -1308,6 +1409,16 @@ 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));
@ -1319,6 +1430,10 @@ 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;
} }
@ -1327,11 +1442,19 @@ 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;
} }
@ -1386,6 +1509,10 @@ 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;
} }

View File

@ -1,29 +1,24 @@
/** 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);
}
sourceElement.addEventListener("change", sourceElementHandler); let syncData = [
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"
},
]
window.addEventListener("load", () => { syncSelectInputUntilChanged(syncData, "form");
syncSelectInputUntilChanged("#id_game", "#id_name");
});

View File

@ -1,4 +1,9 @@
import { syncSelectInputUntilChanged, getEl, conditionalElementHandler } from "./utils.js"; import {
syncSelectInputUntilChanged,
getEl,
disableElementsWhenTrue,
disableElementsWhenFalse,
} from "./utils.js";
let syncData = [ let syncData = [
{ {
@ -11,21 +16,28 @@ let syncData = [
syncSelectInputUntilChanged(syncData, "form"); syncSelectInputUntilChanged(syncData, "form");
function setupElementHandlers() {
let myConfig = [ disableElementsWhenTrue("#id_type", "game", [
() => { "#id_name",
return getEl("#id_type").value == "game"; "#id_related_purchase",
}, ]);
["#id_name", "#id_related_purchase"], disableElementsWhenFalse("#id_type", "game", ["#id_date_finished"]);
(el) => {
el.disabled = "disabled";
},
(el) => {
el.disabled = "";
} }
]
document.DOMContentLoaded = conditionalElementHandler(...myConfig) document.addEventListener("DOMContentLoaded", setupElementHandlers);
document.addEventListener("htmx:afterSwap", setupElementHandlers);
getEl("#id_type").onchange = () => { getEl("#id_type").onchange = () => {
conditionalElementHandler(...myConfig) setupElementHandlers();
};
document.body.addEventListener('htmx:beforeRequest', function(event) {
// Assuming 'Purchase1' is the element that triggers the HTMX request
if (event.target.id === 'id_edition') {
var idEditionValue = document.getElementById('id_edition').value;
// Condition to check - replace this with your actual logic
if (idEditionValue != '') {
event.preventDefault(); // This cancels the HTMX request
} }
}
});

View File

@ -8,6 +8,9 @@ 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";

View File

@ -3,9 +3,16 @@
* @param {Date} date * @param {Date} date
* @returns {string} * @returns {string}
*/ */
export function toISOUTCString(date) { function toISOUTCString(date) {
let month = (date.getMonth() + 1).toString().padStart(2, 0); function stringAndPad(number) {
return `${date.getFullYear()}-${month}-${date.getDate()}T${date.getHours()}:${date.getMinutes()}`; return number.toString().padStart(2, 0);
}
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}`;
} }
/** /**
@ -92,37 +99,72 @@ function getEl(selector) {
return document.getElementsByClassName(selector) return document.getElementsByClassName(selector)
} }
else { else {
return document.getElementsByName(selector) return document.getElementsByTagName(selector)
} }
} }
/** /**
* @description Does something to elements when something happens. * @description Applies different behaviors to elements based on multiple conditional configurations.
* @param {() => boolean} condition The condition that is being tested. * Each configuration is an array containing a condition function, an array of target element selectors,
* @param {string[]} targetElements * and two callback functions for handling matched and unmatched conditions.
* @param {(elementName: HTMLElement) => void} callbackfn1 Called when the condition matches. * @param {...Array} configs Each configuration is an array of the form:
* @param {(elementName: HTMLElement) => void} callbackfn2 Called when the condition doesn't match. * - 0: {function(): boolean} condition - Function that returns true or false based on a condition.
* - 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(condition, targetElements, callbackfn1, callbackfn2) { function conditionalElementHandler(...configs) {
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);
} }
}); });
} }
});
} }
export { toISOUTCString, syncSelectInputUntilChanged, getEl, conditionalElementHandler }; function disableElementsWhenFalse(targetSelect, targetValue, elementList) {
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 };

View File

@ -7,12 +7,18 @@
<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 }} {{ form.as_table }}
<tr> <tr>
<td></td> <td></td>
<td><input type="submit" value="Submit"/></td> <td>
<input type="submit" value="Submit" />
</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 %}

View File

@ -7,16 +7,26 @@
<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 }} {{ form.as_table }}
<tr> <tr>
<td></td> <td></td>
<td><input type="submit" value="Submit"/></td> <td>
<input type="submit" name="submit" value="Submit" />
</td>
</tr>
<tr>
<td></td>
<td>
<input type="submit"
name="submit_and_redirect"
value="Submit & Create Purchase" />
</td>
</tr> </tr>
</table> </table>
</form> </form>
{% endblock content %} {% endblock content %}
{% block scripts %} {% block scripts %}
{% load static %} {% if script_name %}
<script type="module" src="{% static 'js/add_edition.js' %}"></script> <script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
{% endblock scripts %} {% endblock scripts %}

View File

@ -1,12 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}
{% block title %}{{ title }}{% endblock title %} {{ 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 %} {% for field in form %}
<tr> <tr>
<th>{{ field.label_tag }}</th> <th>{{ field.label_tag }}</th>
@ -19,7 +18,10 @@
<td> <td>
<div class="basic-button-container"> <div class="basic-button-container">
<button class="basic-button" data-target="{{ field.name }}" data-type="now">Set to now</button> <button class="basic-button" data-target="{{ field.name }}" data-type="now">Set to now</button>
<button class="basic-button" data-target="{{field.name}}" data-type="toggle">Toggle text</button> <button class="basic-button"
data-target="{{ field.name }}"
data-type="toggle">Toggle text</button>
<button class="basic-button" data-target="{{ field.name }}" data-type="copy">Copy</button>
</div> </div>
</td> </td>
{% endif %} {% endif %}
@ -27,7 +29,9 @@
{% endfor %} {% endfor %}
<tr> <tr>
<td></td> <td></td>
<td><input type="submit" value="Submit"/></td> <td>
<input type="submit" value="Submit" />
</td>
</tr> </tr>
</table> </table>
</form> </form>

View File

@ -6,18 +6,25 @@
<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 - {% block title %}Untitled{% endblock title %}</title> <title>Timetracker -
{% 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">
<body class="dark"> <img id="indicator"
<img id="indicator" src="{% static 'icons/loading.png' %}" class="absolute right-3 top-3 animate-spin htmx-indicator" /> src="{% static 'icons/loading.png' %}"
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> <span class="text-4xl">
<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">
@ -91,5 +98,4 @@
{% block scripts %} {% block scripts %}
{% endblock scripts %} {% endblock scripts %}
</body> </body>
</html> </html>

View File

@ -1,22 +1,13 @@
<button {% comment %}
type="button" title
text
{% endcomment %}
<a href="{{ link }}"
title="{{ title }}" title="{{ title }}"
autofocus 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">
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" {% 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">
> <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
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="self-center w-6 h-6 inline"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z"
/>
</svg> </svg>
{% endcomment %}
{{ text }} {{ text }}
</button> </a>

View File

@ -1,29 +1,24 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% block title %}
{% block title %}{{ title }}{% endblock title %} {{ title }}
{% endblock title %}
{% block content %} {% block content %}
{% if dataset.count >= 1 %} {% if dataset.count >= 1 %}
<div class="mx-auto text-center my-4"> <div class="mx-auto text-center my-4">
<a <a id="last-session-start"
id="last-session-start" href="{% url 'start_session_same_as_last' last.id %}"
href="{% url 'start_session' last.id %}" hx-get="{% url 'start_session_same_as_last' last.id %}"
hx-get="{% url 'start_session' last.id %}"
hx-indicator="#indicator"
hx-swap="afterbegin" hx-swap="afterbegin"
hx-target=".responsive-table tbody" hx-target=".responsive-table tbody"
hx-select=".responsive-table tbody tr:first-child" hx-select=".responsive-table tbody tr:first-child"
onClick="document.querySelector('#last-session-start').classList.add('invisible')" onClick="document.querySelector('#last-session-start').classList.add('invisible')"
class="{% if last.timestamp_end == null %}invisible{% endif %}" class="{% if last.timestamp_end == null %}invisible{% endif %}">
> {% include 'components/button_start.html' with text=last.purchase title="Start session of last played game" only %}
{% include 'components/button.html' with text=last.purchase title="Start session of last played game" only %}
</a> </a>
</div> </div>
{% endif %} {% endif %}
{% if dataset.count != 0 %}
<table class="responsive-table"> <table class="responsive-table">
<thead> <thead>
<tr> <tr>
@ -36,25 +31,24 @@
<tbody> <tbody>
{% for data in dataset %} {% for data in dataset %}
<tr> <tr>
<td <td class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char md:max-w-40char">
class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char md:max-w-40char" <a class="underline decoration-slate-500 sm:decoration-2"
> href="{% url 'view_game' data.purchase.edition.game.id %}">
{{ data.purchase.edition }} {{ data.purchase.edition }}
</a>
</td> </td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden sm:table-cell"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden sm:table-cell">
{{ data.timestamp_start | date:"d/m/Y H:i" }} {{ data.timestamp_start | date:"d/m/Y H:i" }}
</td> </td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell">
{% if data.unfinished %} {% if data.unfinished %}
<a <a href="{% url 'update_session' data.id %}"
href="{% url 'update_session' data.id %}"
hx-get="{% url 'update_session' data.id %}" hx-get="{% url 'update_session' data.id %}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-target=".responsive-table tbody tr:first-child" hx-target=".responsive-table tbody tr:first-child"
hx-select=".responsive-table tbody tr:first-child" hx-select=".responsive-table tbody tr:first-child"
hx-indicator="#indicator" hx-indicator="#indicator"
onClick="document.querySelector('#last-session-start').classList.remove('invisible')" onClick="document.querySelector('#last-session-start').classList.remove('invisible')">
>
<span class="text-yellow-300">Finish now?</span> <span class="text-yellow-300">Finish now?</span>
</a> </a>
{% elif data.duration_manual %} {% elif data.duration_manual %}
@ -63,11 +57,12 @@
{{ data.timestamp_end | date:"d/m/Y H:i" }} {{ data.timestamp_end | date:"d/m/Y H:i" }}
{% endif %} {% endif %}
</td> </td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ data.duration_formatted }}</td>
{{ data.duration_formatted }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% else %}
<div class="mx-auto text-center text-slate-300 text-xl">No sessions found.</div>
{% endif %}
{% endblock content %} {% endblock content %}

View File

@ -196,11 +196,10 @@
{% 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" href="{% url 'edit_purchase' purchase.id %}"> <a class="underline decoration-slate-500 sm:decoration-2"
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 }})
{% endif %}
</a> </a>
</td> </td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>

View File

@ -34,30 +34,25 @@
{% 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>
{% endfor %}
</ul>
<h1 class="text-3xl mt-4 mb-1">Purchases <span class="dark:text-slate-500">({{ purchases.count }})</span></h1>
<ul> <ul>
{% for purchase in purchases %} {% for purchase in edition.game_purchases %}
<li class="sm:pl-2 flex items-center"> <li class="sm:pl-6 flex items-center">
{{ purchase.platform }} {{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}
({{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}, {{ purchase.price }} {{ purchase.price_currency}}) {% if purchase.price != 0 %}({{ purchase.price }} {{ purchase.price_currency }}){% endif %}
{% url 'edit_purchase' purchase.id as edit_url %} {% url 'edit_purchase' 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 %}
{% if purchase.related_purchases %} </li>
<li>
<ul> <ul>
{% for related_purchase in purchase.related_purchases %} {% for related_purchase in purchase.nongame_related_purchases %}
<li class="sm:pl-6 flex items-center"> <li class="sm:pl-12 flex items-center">
{{ related_purchase.name}} ({{ related_purchase.get_type_display }}, {{ related_purchase.date_purchased | date:"Y" }}, {{ related_purchase.price }} {{ related_purchase.price_currency}}) {{ related_purchase.name }} ({{ related_purchase.get_type_display }}, {{ purchase.platform }}, {{ 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>
</li> {% endfor %}
{% endif %} </ul>
</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">

View File

@ -93,4 +93,10 @@ 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",
),
] ]

View File

@ -1,8 +1,15 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from zoneinfo import ZoneInfo from typing import Any, Callable
from common.time import now as now_with_tz from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings from django.db.models import Count, F, Prefetch, Sum
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
@ -133,23 +140,10 @@ 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)
context["title"] = "View Game" nongame_related_purchases_prefetch = Prefetch(
context["game"] = game "related_purchases",
context["editions"] = Edition.objects.filter(game_id=game_id) queryset=Purchase.objects.exclude(type=Purchase.GAME),
game_purchases = Purchase.objects.filter(edition__game_id=game_id).filter( to_attr="nongame_related_purchases",
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",
@ -259,6 +253,12 @@ 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,10 +287,12 @@ 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=datetime.now() - timedelta(days=30) timestamp_start__gte=first_day_of_year
) ).order_by("-timestamp_start")
context["title"] = "Last 30 days" context["title"] = "This year"
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")
@ -304,8 +306,10 @@ def list_sessions(
context["total_duration"] = dataset.total_duration_formatted() context["total_duration"] = dataset.total_duration_formatted()
context["dataset"] = dataset context["dataset"] = dataset
# cannot use dataset[0] here because that might be only partial QuerySet try:
context["last"] = Session.objects.all().order_by("timestamp_start").last() context["last"] = Session.objects.latest()
except ObjectDoesNotExist:
context["last"] = None
return render(request, "list_sessions.html", context) return render(request, "list_sessions.html", context)
@ -543,3 +547,19 @@ 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")

73
poetry.lock generated
View File

@ -98,6 +98,17 @@ 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"
@ -237,7 +248,9 @@ files = [
] ]
[package.extras] [package.extras]
test = ["pytest (>=6)"] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
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"
@ -348,6 +361,20 @@ 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"
@ -478,22 +505,23 @@ files = [
] ]
[[package]] [[package]]
name = "packaging" name = "nodeenv"
version = "22.0" version = "1.8.0"
description = "Core utilities for Python packages" description = "Node.js virtual environment builder"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [ files = [
{file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
{file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
] ]
[package.dependencies]
setuptools = "*"
[[package]] [[package]]
name = "pathspec" name = "packaging"
version = "0.10.3" version = "23.2"
description = "Utility library for gitignore style pattern matching of file paths." description = "Core utilities for Python packages"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -629,6 +657,24 @@ 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"
@ -647,7 +693,7 @@ packaging = "*"
pluggy = ">=0.12,<2.0" pluggy = ">=0.12,<2.0"
[package.extras] [package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
@ -825,7 +871,6 @@ 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 = [