Allow filtering by game, edition, purchase from the session list

This commit is contained in:
Lukáš Kucharczyk 2023-02-19 14:30:26 +01:00
parent dc6c295ee7
commit b773d9df58
Signed by: lukas
SSH Key Fingerprint: SHA256:vMuSwvwAvcT6htVAioMP7rzzwMQNi3roESyhv+nAxeg
5 changed files with 94 additions and 199 deletions

View File

@ -1,133 +1,6 @@
## 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
* Add stat for finished this year's games
* Add purchase types:
* Game (previously all of them were this type)
* DLC
* Season Pass
* Battle Pass
## Fixed
* Order purchases by date on game view
## 1.4.0 / 2023-11-09 21:01+01:00
### New
* More fields are now optional. This is to make it easier to add new items in bulk.
* Game: Wikidata ID
* Edition: Platform, Year
* Purchase: Platform
* Platform: Group
* Session: Device
* New fields:
* Game: Year Released
* To record original year of release
* Upon migration, this will be set to a year of any of the game's edition that has it set
* Purchase: Date Finished
* Editions are now unique combination of name and platform
* Add more stats:
* All finished games
* All finished 2023 games
* All finished games that were purchased this year
* Sessions (count)
* Days played
* Finished (count)
* Unfinished (count)
* Refunded (count)
* Backlog Decrease (count)
* New workflow:
* Adding Game, Edition, Purchase, and Session in a row is now much faster
### Improved
* game overview: simplify playtime range display
* new session: order devices alphabetically
* ignore English articles when sorting names
* added a new sort_name field that gets automatically created
* automatically fill certain values in forms:
* new game: name and sort name after typing
* new edition: name, sort name, and year when selecting game
* new purchase: platform when selecting edition
## 1.3.0 / 2023-11-05 15:09+01:00
### New
* Add Stats to the main navigation
* Allow selecting year on the Stats page
### Improved
* Make some pages redirect back instead to session list
### Improved
* Make navigation more compact
### Fixed
* Correctly limit sessions to a single year for stats
## 1.2.0 / 2023-11-01 20:18+01:00
### New
* Add yearly stats page (https://git.kucharczyk.xyz/lukas/timetracker/issues/15)
### Enhancements
* Add a button to start session from game overview
## 1.1.2 / 2023-10-13 16:30+02:00
### Enhancements
* Durations are formatted in a consisent manner across all pages
### Fixes
* Game Overview: display duration when >1 hour instead of displaying 0
## 1.1.1 / 2023-10-09 20:52+02:00
### New
* Add notes section to game overview
### Enhancements
* Make it possible to add any data on the game overview page
## 1.1.0 / 2023-10-09 00:01+02:00
### New
* Add game overview page (https://git.kucharczyk.xyz/lukas/timetracker/issues/8)
* Add helper buttons next to datime fields
* Add copy button on Add session page to copy times between fields
* Change fonts to IBM Plex
### Enhancements
* Improve form appearance
* Focus important fields on forms
* Use the same form when editing a session as when adding a session
* Change recent session view to current year instead of last 30 days
* Add a hacky way not to reload a page when starting or ending a session (https://git.kucharczyk.xyz/lukas/timetracker/issues/52)
* Improve session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/53)
### Fixes
* Fix session being wrongly considered in progress if it had a certain amount of manual hours (https://git.kucharczyk.xyz/lukas/timetracker/issues/58)
* Fix bug when filtering only manual sessions (https://git.kucharczyk.xyz/lukas/timetracker/issues/51)
## 1.0.3 / 2023-02-20 17:16+01:00
* Add wikidata ID and year for editions
* Add icons for game, edition, purchase filters
* Allow filtering by game, edition, purchase from the session list * Allow filtering by game, edition, purchase from the session list
* Allow editing filtered entities from session list
## 1.0.2 / 2023-02-18 21:48+01:00 ## 1.0.2 / 2023-02-18 21:48+01:00

View File

@ -14,7 +14,11 @@ textarea {
#session-table { #session-table {
display: grid; display: grid;
grid-template-columns: 3fr 1fr repeat(2, 2fr) 0.5fr 1fr; grid-template-columns: 3fr repeat(3, 1fr) 0.5fr 1fr;
}
.purchase-name:hover > span:nth-child(2) {
@apply dark:text-slate-300
} }
#button-container button { #button-container button {

View File

@ -3,65 +3,92 @@
{% block title %}{{ title }}{% endblock title %} {% block title %}{{ title }}{% endblock title %}
{% block content %} {% block content %}
{% if dataset.count >= 1 %} <div class="text-center text-xl mb-4 dark:text-slate-400">
<div class="mx-auto text-center my-4"> {% if dataset.count >= 2 %}
<a id="last-session-start" <img src="data:image/svg+xml;base64,{{ chart|safe }}" class="mx-auto mb-3" />
href="{% url 'start_session_same_as_last' last.id %}" {% endif %}
hx-get="{% url 'start_session_same_as_last' last.id %}" {% if dataset.count >= 1 %}
hx-swap="afterbegin" <div class="mb-4">Total playtime: {{ total_duration }} over {{ dataset.count }} sessions.</div>
hx-target=".responsive-table tbody" {% endif %}
hx-select=".responsive-table tbody tr:first-child" {% if purchase or platform or edition or game or ownership_type %}
onClick="document.querySelector('#last-session-start').classList.add('invisible')" <span class="block">
class="{% if last.timestamp_end == null %}invisible{% endif %}"> <a class="text-red-400 inline" href="{% url 'list_sessions' %}">
{% include 'components/button_start.html' with text=last.purchase title="Start session of last played game" only %} <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 inline">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</a>Filtering by "{% firstof purchase platform game edition ownership_type %}"
</span>
{% if purchase %}<a class="dark:text-white hover:underline block" href="{% url 'list_sessions_by_edition' purchase.edition.id %}">See all platforms</a>{% endif %}
{% endif %}
{% if dataset.count >= 1 %}
<a href="{% url 'start_session' last.id %}">
<button type="button" title="Track last tracked" class="mt-10 py-1 px-2 bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg ">
<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>
{{ last.purchase }}
</button>
</a> </a>
</div> </div>
{% endif %} <div id="session-table" class="gap-4 shadow rounded-xl max-w-screen-2xl mx-auto dark:bg-slate-700 p-2 justify-center">
{% if dataset.count != 0 %} <div class="dark:border-white dark:text-slate-300 text-lg">Purchase</div>
<table class="responsive-table"> <div class="dark:border-white dark:text-slate-300 text-lg">Platform</div>
<thead> <div class="dark:border-white dark:text-slate-300 text-lg text-center">Start</div>
<tr> <div class="dark:border-white dark:text-slate-300 text-lg text-center">End</div>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Name</th> <div class="dark:border-white dark:text-slate-300 text-lg">Duration</div>
<th class="hidden sm:table-cell px-2 sm:px-4 md:px-6 md:py-2">Start</th> <div class="dark:border-white dark:text-slate-300 text-lg text-right">Manage</div>
<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> <div class="purchase-name">
</tr> <span class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">{{ data.purchase.edition }} <span class="dark:text-slate-400">({{ data.purchase.get_ownership_type_display }})</span></span>
</thead>
<tbody> <span class="dark:text-slate-700 transition-colors duration-300">
{% for data in dataset %} (<a class="hover:underline" href="{% url 'list_sessions_by_game' data.purchase.edition.game.id %}">G</a>,
<tr> <a class="hover:underline" href="{% url 'list_sessions_by_edition' data.purchase.edition.id %}">E</a>,
<td class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char md:max-w-40char"> <a class="hover:underline" href="{% url 'list_sessions_by_purchase' data.purchase.id %}">P</a>,
<a class="underline decoration-slate-500 sm:decoration-2" <a class="hover:underline" href="{% url 'list_sessions_by_ownership_type' data.purchase.ownership_type %}">O</a>)
href="{% url 'view_game' data.purchase.edition.game.id %}"> </span>
{{ data.purchase.edition }} </div>
</a> <div class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap"><a class="hover:underline" href="{% url 'list_sessions_by_platform' data.purchase.platform.id %}">{{ data.purchase.platform }}</a></div>
</td> <div class="dark:text-slate-400 text-center">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden sm:table-cell"> <div class="dark:text-slate-400 text-center">
{{ data.timestamp_start | date:"d/m/Y H:i" }} {% if data.unfinished %}
</td> <span class="text-red-400">Not finished yet.</span>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell"> {% elif data.duration_manual %}
{% if data.unfinished %} --
<a href="{% url 'update_session' data.id %}" {% else %}
hx-get="{% url 'update_session' data.id %}" {{ data.timestamp_end | date:"d/m/Y H:i" }}
hx-swap="outerHTML" {% endif %}
hx-target=".responsive-table tbody tr:first-child" </div>
hx-select=".responsive-table tbody tr:first-child" <div class="dark:text-slate-400 flex">{{ data.duration_formatted }}{% if data.duration_manual %} <svg title="Added manually" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5 ml-1 self-center">
hx-indicator="#indicator" <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
onClick="document.querySelector('#last-session-start').classList.remove('invisible')"> </svg>
<span class="text-yellow-300">Finish now?</span> {% endif %}</div>
</a> <div id="button-container" class="flex justify-end">
{% elif data.duration_manual %} {% if data.unfinished %}
-- <a href="{% url 'update_session' data.id %}">
{% else %} <button type="button" title="Set to finished" class="py-1 px-2 flex justify-center items-center 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 w-7 h-4 rounded-lg ">
{{ data.timestamp_end | date:"d/m/Y H:i" }} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
{% endif %} <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />
</td> </svg>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ data.duration_formatted }}</td> </button>
</tr> </a>
{% endfor %} {% endif %}
</tbody> <a href="{% url 'edit_session' data.id %}">
</table> <button type="button" title="Edit" class="py-1 px-2 flex justify-center items-center bg-blue-600 hover:bg-blue-700 focus:ring-blue-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 w-7 h-4 rounded-lg ">
{% else %} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
<div class="mx-auto text-center text-slate-300 text-xl">No sessions found.</div> <path d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z" />
{% endif %} <path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z" />
</svg>
</button>
</a>
<a href="{% url 'delete_session' data.id %}">
<button type="button" edit="Delete" class="py-1 px-2 flex justify-center items-center bg-red-600 hover:bg-red-700 focus:ring-red-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 w-7 h-4 rounded-lg ">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
<path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z" clip-rule="evenodd" />
</svg>
</button>
</a>
</div>
{% endfor %}
</div>
{% endblock content %} {% endblock content %}

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",
),
] ]

View File

@ -253,12 +253,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="",
@ -283,6 +277,9 @@ def list_sessions(
elif filter == "game": elif filter == "game":
dataset = Session.objects.filter(purchase__edition__game=game_id) dataset = Session.objects.filter(purchase__edition__game=game_id)
context["game"] = Game.objects.get(id=game_id) context["game"] = Game.objects.get(id=game_id)
elif filter == "ownership_type":
dataset = Session.objects.filter(purchase__ownership_type=ownership_type)
context["ownership_type"] = dict(Purchase.OWNERSHIP_TYPES)[ownership_type]
elif filter == "recent": elif filter == "recent":
dataset = Session.objects.filter( dataset = Session.objects.filter(
timestamp_start__gte=datetime.now() - timedelta(days=30) timestamp_start__gte=datetime.now() - timedelta(days=30)