timetracker/games/views/general.py

507 lines
20 KiB
Python
Raw Normal View History

2025-02-08 13:46:56 +01:00
from datetime import datetime, timedelta
2024-08-11 17:23:28 +02:00
from typing import Any, Callable
2024-02-09 22:03:18 +01:00
from django.contrib.auth.decorators import login_required
from django.db.models import Avg, Count, ExpressionWrapper, F, Prefetch, Q, Sum, fields
2024-08-08 09:47:06 +02:00
from django.db.models.functions import TruncDate, TruncMonth
2024-08-08 21:19:43 +02:00
from django.db.models.manager import BaseManager
2024-08-12 21:42:34 +02:00
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, render
2023-11-02 09:20:09 +01:00
from django.urls import reverse
2025-02-08 13:46:56 +01:00
from django.utils.timezone import now as timezone_now
2023-01-15 23:39:52 +01:00
2024-10-16 18:31:12 +02:00
from common.time import available_stats_year_range, dateformat, format_duration
2024-08-12 21:42:34 +02:00
from common.utils import safe_division
2025-01-29 22:05:06 +01:00
from games.models import Game, Platform, Purchase, Session
2023-01-04 17:27:54 +01:00
2024-08-08 21:19:43 +02:00
def model_counts(request: HttpRequest) -> dict[str, bool]:
2025-02-08 13:46:56 +01:00
today_played = Session.objects.filter(
timestamp_start__year=2025, timestamp_start__day=8, timestamp_start__month=2
).aggregate(time=Sum(F("duration_calculated")))["time"]
last_7_played = Session.objects.filter(
timestamp_start__gte=(timezone_now() - timedelta(days=7))
).aggregate(time=Sum(F("duration_calculated")))["time"]
return {
"game_available": Game.objects.exists(),
"platform_available": Platform.objects.exists(),
"purchase_available": Purchase.objects.exists(),
"session_count": Session.objects.exists(),
2025-02-08 13:46:56 +01:00
"today_played": format_duration(today_played, "%H h %m m"),
"last_7_played": format_duration(last_7_played, "%H h %m m"),
}
2022-12-31 14:18:27 +01:00
2024-10-16 18:31:12 +02:00
def global_current_year(request: HttpRequest) -> dict[str, int]:
return {"global_current_year": datetime.now().year}
def use_custom_redirect(
2024-08-11 17:23:28 +02:00
func: Callable[..., HttpResponse],
) -> Callable[..., HttpResponse]:
"""
Will redirect to "return_path" session variable if set.
"""
def wrapper(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
response = func(request, *args, **kwargs)
if isinstance(response, HttpResponseRedirect) and (
next_url := request.session.get("return_path")
):
return HttpResponseRedirect(next_url)
return response
return wrapper
2024-08-04 22:40:37 +02:00
@login_required
2024-08-08 21:19:43 +02:00
def stats_alltime(request: HttpRequest) -> HttpResponse:
2024-08-04 22:40:37 +02:00
year = "Alltime"
2025-01-29 22:05:06 +01:00
this_year_sessions = Session.objects.all().prefetch_related(Prefetch("game"))
2024-08-04 22:40:37 +02:00
this_year_sessions_with_durations = this_year_sessions.annotate(
duration=ExpressionWrapper(
F("timestamp_end") - F("timestamp_start"),
output_field=fields.DurationField(),
)
)
longest_session = this_year_sessions_with_durations.order_by("-duration").first()
2025-01-29 22:05:06 +01:00
this_year_games = Game.objects.filter(sessions__in=this_year_sessions).distinct()
2024-08-04 22:40:37 +02:00
this_year_games_with_session_counts = this_year_games.annotate(
2025-01-29 22:05:06 +01:00
session_count=Count("sessions"),
2024-08-04 22:40:37 +02:00
)
game_highest_session_count = this_year_games_with_session_counts.order_by(
"-session_count"
).first()
selected_currency = "CZK"
unique_days = (
this_year_sessions.annotate(date=TruncDate("timestamp_start"))
.values("date")
.distinct()
.aggregate(dates=Count("date"))
)
this_year_played_purchases = Purchase.objects.filter(
2025-01-29 22:05:06 +01:00
games__sessions__in=this_year_sessions
2024-08-04 22:40:37 +02:00
).distinct()
this_year_purchases = Purchase.objects.all()
2025-01-29 22:05:06 +01:00
this_year_purchases_with_currency = this_year_purchases.select_related("games")
2024-08-04 22:40:37 +02:00
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
date_refunded=None
)
this_year_purchases_refunded = this_year_purchases_with_currency.refunded()
this_year_purchases_unfinished_dropped_nondropped = (
this_year_purchases_without_refunded.filter(date_finished__isnull=True)
.filter(infinite=False)
.filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC))
) # do not count battle passes etc.
this_year_purchases_unfinished = (
this_year_purchases_unfinished_dropped_nondropped.filter(
date_dropped__isnull=True
)
)
this_year_purchases_dropped = (
this_year_purchases_unfinished_dropped_nondropped.filter(
date_dropped__isnull=False
)
)
this_year_purchases_without_refunded_count = (
this_year_purchases_without_refunded.count()
)
this_year_purchases_unfinished_count = this_year_purchases_unfinished.count()
this_year_purchases_unfinished_percent = int(
safe_division(
this_year_purchases_unfinished_count,
this_year_purchases_without_refunded_count,
)
* 100
)
2024-08-08 21:19:43 +02:00
purchases_finished_this_year: BaseManager[Purchase] = Purchase.objects.finished()
2024-08-04 22:40:37 +02:00
purchases_finished_this_year_released_this_year = (
purchases_finished_this_year.all().order_by("date_finished")
)
purchased_this_year_finished_this_year = (
this_year_purchases_without_refunded.all()
).order_by("date_finished")
this_year_spendings = this_year_purchases_without_refunded.aggregate(
total_spent=Sum(F("converted_price"))
2024-08-04 22:40:37 +02:00
)
total_spent = this_year_spendings["total_spent"] or 0
games_with_playtime = (
2025-01-29 22:05:06 +01:00
Game.objects.filter(sessions__in=this_year_sessions)
2024-08-04 22:40:37 +02:00
.annotate(
total_playtime=Sum(
2025-01-29 22:05:06 +01:00
F("sessions__duration_calculated") + F("sessions__duration_manual")
2024-08-04 22:40:37 +02:00
)
)
.values("id", "name", "total_playtime")
)
month_playtimes = (
this_year_sessions.annotate(month=TruncMonth("timestamp_start"))
.values("month")
.annotate(playtime=Sum("duration_calculated"))
.order_by("month")
)
for month in month_playtimes:
month["playtime"] = format_duration(month["playtime"], "%2.0H")
highest_session_average_game = (
2025-01-29 22:05:06 +01:00
Game.objects.filter(sessions__in=this_year_sessions)
.annotate(session_average=Avg("sessions__duration_calculated"))
2024-08-04 22:40:37 +02:00
.order_by("-session_average")
.first()
)
top_10_games_by_playtime = games_with_playtime.order_by("-total_playtime")[:10]
for game in top_10_games_by_playtime:
game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H")
total_playtime_per_platform = (
2025-01-29 22:05:06 +01:00
this_year_sessions.values("game__platform__name")
2024-08-04 22:40:37 +02:00
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
2025-01-29 22:05:06 +01:00
.annotate(platform_name=F("game__platform__name"))
2024-08-04 22:40:37 +02:00
.values("platform_name", "total_playtime")
.order_by("-total_playtime")
)
for item in total_playtime_per_platform:
item["formatted_playtime"] = format_duration(item["total_playtime"], "%2.0H")
backlog_decrease_count = (
Purchase.objects.all().intersection(purchases_finished_this_year).count()
)
first_play_date = "N/A"
last_play_date = "N/A"
if this_year_sessions:
first_session = this_year_sessions.earliest()
2025-01-29 22:05:06 +01:00
first_play_game = first_session.game
2024-10-14 14:26:48 +02:00
first_play_date = first_session.timestamp_start.strftime(dateformat)
2024-08-04 22:40:37 +02:00
last_session = this_year_sessions.latest()
2025-01-29 22:05:06 +01:00
last_play_game = last_session.game
2024-10-14 14:26:48 +02:00
last_play_date = last_session.timestamp_start.strftime(dateformat)
2024-08-04 22:40:37 +02:00
all_purchased_this_year_count = this_year_purchases_with_currency.count()
2024-08-08 21:19:43 +02:00
all_purchased_refunded_this_year_count: int = this_year_purchases_refunded.count()
2024-08-04 22:40:37 +02:00
this_year_purchases_dropped_count = this_year_purchases_dropped.count()
this_year_purchases_dropped_percentage = int(
safe_division(this_year_purchases_dropped_count, all_purchased_this_year_count)
* 100
)
context = {
"total_hours": format_duration(
this_year_sessions.total_duration_unformatted(), "%2.0H"
),
"total_year_games": this_year_played_purchases.all().count(),
2024-08-04 22:40:37 +02:00
"top_10_games_by_playtime": top_10_games_by_playtime,
"year": year,
"total_playtime_per_platform": total_playtime_per_platform,
"total_spent": total_spent,
"total_spent_currency": selected_currency,
"spent_per_game": int(
safe_division(total_spent, this_year_purchases_without_refunded_count)
),
"this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(),
"total_sessions": this_year_sessions.count(),
"unique_days": unique_days["dates"],
"unique_days_percent": int(unique_days["dates"] / 365 * 100),
"purchased_unfinished_count": this_year_purchases_unfinished_count,
"unfinished_purchases_percent": this_year_purchases_unfinished_percent,
"dropped_count": this_year_purchases_dropped_count,
"dropped_percentage": this_year_purchases_dropped_percentage,
"refunded_percent": int(
safe_division(
all_purchased_refunded_this_year_count,
all_purchased_this_year_count,
)
* 100
),
"all_purchased_refunded_this_year": this_year_purchases_refunded,
"all_purchased_refunded_this_year_count": all_purchased_refunded_this_year_count,
"all_purchased_this_year_count": all_purchased_this_year_count,
"backlog_decrease_count": backlog_decrease_count,
"longest_session_time": (
format_duration(longest_session.duration, "%2.0Hh %2.0mm")
if longest_session
else 0
),
2025-01-29 22:05:06 +01:00
"longest_session_game": (longest_session.game if longest_session else None),
2024-08-04 22:40:37 +02:00
"highest_session_count": (
game_highest_session_count.session_count
if game_highest_session_count
else 0
),
"highest_session_count_game": (
game_highest_session_count if game_highest_session_count else None
),
"highest_session_average": (
format_duration(
highest_session_average_game.session_average, "%2.0Hh %2.0mm"
)
if highest_session_average_game
else 0
),
"highest_session_average_game": highest_session_average_game,
"first_play_game": first_play_game,
"first_play_date": first_play_date,
"last_play_game": last_play_game,
"last_play_date": last_play_date,
"title": f"{year} Stats",
2024-10-16 18:31:12 +02:00
"stats_dropdown_year_range": available_stats_year_range(),
2024-08-04 22:40:37 +02:00
}
request.session["return_path"] = request.path
return render(request, "stats.html", context)
2024-02-09 22:03:18 +01:00
@login_required
2024-08-08 21:19:43 +02:00
def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
2023-11-02 09:20:09 +01:00
selected_year = request.GET.get("year")
if selected_year:
return HttpResponseRedirect(reverse("stats_by_year", args=[selected_year]))
if year == 0:
2024-08-04 22:40:37 +02:00
return HttpResponseRedirect(reverse("stats_alltime"))
2024-01-03 21:35:47 +01:00
this_year_sessions = Session.objects.filter(
timestamp_start__year=year
2025-01-29 22:05:06 +01:00
).prefetch_related("game")
this_year_sessions_with_durations = this_year_sessions.annotate(
duration=ExpressionWrapper(
F("timestamp_end") - F("timestamp_start"),
output_field=fields.DurationField(),
)
)
longest_session = this_year_sessions_with_durations.order_by("-duration").first()
2025-01-29 22:05:06 +01:00
this_year_games = Game.objects.filter(sessions__in=this_year_sessions).distinct()
2024-01-01 18:21:50 +01:00
this_year_games_with_session_counts = this_year_games.annotate(
session_count=Count(
2025-01-29 22:05:06 +01:00
"sessions",
filter=Q(sessions__timestamp_start__year=year),
)
)
game_highest_session_count = this_year_games_with_session_counts.order_by(
"-session_count"
).first()
2023-11-09 19:35:57 +01:00
selected_currency = "CZK"
unique_days = (
2023-11-09 19:35:57 +01:00
this_year_sessions.annotate(date=TruncDate("timestamp_start"))
.values("date")
.distinct()
.aggregate(dates=Count("date"))
)
2023-11-09 19:35:57 +01:00
this_year_played_purchases = Purchase.objects.filter(
2025-01-29 22:05:06 +01:00
games__sessions__in=this_year_sessions
2023-11-02 20:12:32 +01:00
).distinct()
this_year_played_games = Game.objects.filter(
sessions__in=this_year_sessions
).distinct()
2023-11-09 19:35:57 +01:00
this_year_purchases = Purchase.objects.filter(date_purchased__year=year)
2025-01-29 22:05:06 +01:00
this_year_purchases_with_currency = this_year_purchases.prefetch_related("games")
2023-11-09 19:35:57 +01:00
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
date_refunded=None
).exclude(ownership_type=Purchase.DEMO)
2023-11-09 19:35:57 +01:00
this_year_purchases_refunded = this_year_purchases_with_currency.refunded()
2024-03-10 22:48:46 +01:00
this_year_purchases_unfinished_dropped_nondropped = (
this_year_purchases_without_refunded.filter(date_finished__isnull=True)
.filter(infinite=False)
.filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC))
2023-11-20 21:56:16 +01:00
) # do not count battle passes etc.
2023-11-09 10:06:14 +01:00
2024-03-10 22:48:46 +01:00
this_year_purchases_unfinished = (
this_year_purchases_unfinished_dropped_nondropped.filter(
date_dropped__isnull=True
)
)
this_year_purchases_dropped = (
this_year_purchases_unfinished_dropped_nondropped.filter(
date_dropped__isnull=False
)
)
2024-01-03 21:35:47 +01:00
this_year_purchases_without_refunded_count = (
this_year_purchases_without_refunded.count()
)
this_year_purchases_unfinished_count = this_year_purchases_unfinished.count()
2023-11-09 19:35:57 +01:00
this_year_purchases_unfinished_percent = int(
2023-11-09 10:06:14 +01:00
safe_division(
2024-01-03 21:35:47 +01:00
this_year_purchases_unfinished_count,
this_year_purchases_without_refunded_count,
2023-11-09 09:18:49 +01:00
)
2023-11-09 10:06:14 +01:00
* 100
)
2023-11-02 20:12:32 +01:00
2023-11-09 19:35:57 +01:00
purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year)
purchases_finished_this_year_released_this_year = (
2025-01-29 22:05:06 +01:00
purchases_finished_this_year.filter(games__year_released=year).order_by(
2023-11-09 19:35:57 +01:00
"date_finished"
)
)
purchased_this_year_finished_this_year = (
2024-01-03 21:35:47 +01:00
this_year_purchases_without_refunded.filter(date_finished__year=year)
).order_by("date_finished")
2023-11-09 19:35:57 +01:00
this_year_spendings = this_year_purchases_without_refunded.aggregate(
total_spent=Sum(F("converted_price"))
)
2023-11-12 08:01:12 +01:00
total_spent = this_year_spendings["total_spent"] or 0
2023-11-02 15:08:11 +01:00
games_with_playtime = (
2025-01-29 22:05:06 +01:00
Game.objects.filter(sessions__in=this_year_sessions)
2023-11-02 15:08:11 +01:00
.annotate(
total_playtime=Sum(
2025-01-29 22:05:06 +01:00
F("sessions__duration_calculated") + F("sessions__duration_manual")
2023-11-02 15:08:11 +01:00
)
2023-11-01 20:18:39 +01:00
)
2023-11-02 15:08:11 +01:00
.values("id", "name", "total_playtime")
2023-11-01 20:18:39 +01:00
)
2024-04-02 08:18:58 +02:00
month_playtimes = (
this_year_sessions.annotate(month=TruncMonth("timestamp_start"))
.values("month")
.annotate(playtime=Sum("duration_calculated"))
.order_by("month")
)
for month in month_playtimes:
month["playtime"] = format_duration(month["playtime"], "%2.0H")
2023-11-21 21:57:17 +01:00
highest_session_average_game = (
2025-01-29 22:05:06 +01:00
Game.objects.filter(sessions__in=this_year_sessions)
.annotate(session_average=Avg("sessions__duration_calculated"))
2023-11-21 21:57:17 +01:00
.order_by("-session_average")
.first()
)
top_10_games_by_playtime = games_with_playtime.order_by("-total_playtime")
2023-11-02 15:08:11 +01:00
for game in top_10_games_by_playtime:
game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H")
2023-11-01 20:18:39 +01:00
total_playtime_per_platform = (
2025-01-29 22:05:06 +01:00
this_year_sessions.values("game__platform__name")
2023-11-02 15:09:31 +01:00
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
2025-01-29 22:05:06 +01:00
.annotate(platform_name=F("game__platform__name"))
2023-11-02 15:09:31 +01:00
.values("platform_name", "total_playtime")
.order_by("-total_playtime")
2023-11-01 20:18:39 +01:00
)
for item in total_playtime_per_platform:
item["formatted_playtime"] = format_duration(item["total_playtime"], "%2.0H")
2023-11-09 19:15:49 +01:00
backlog_decrease_count = (
Purchase.objects.filter(date_purchased__year__lt=year)
2023-11-09 19:35:57 +01:00
.intersection(purchases_finished_this_year)
2023-11-09 19:15:49 +01:00
.count()
)
2024-01-01 18:42:14 +01:00
first_play_date = "N/A"
last_play_date = "N/A"
2024-08-08 21:19:43 +02:00
first_play_game = None
last_play_game = None
2024-01-01 18:42:14 +01:00
if this_year_sessions:
first_session = this_year_sessions.earliest()
2025-01-29 22:05:06 +01:00
first_play_game = first_session.game
2024-10-14 14:26:48 +02:00
first_play_date = first_session.timestamp_start.strftime(dateformat)
2024-01-01 18:42:14 +01:00
last_session = this_year_sessions.latest()
2025-01-29 22:05:06 +01:00
last_play_game = last_session.game
2024-10-14 14:26:48 +02:00
last_play_date = last_session.timestamp_start.strftime(dateformat)
2024-01-01 18:42:14 +01:00
2024-01-03 21:35:47 +01:00
all_purchased_this_year_count = this_year_purchases_with_currency.count()
all_purchased_refunded_this_year_count = this_year_purchases_refunded.count()
2024-03-10 22:48:46 +01:00
this_year_purchases_dropped_count = this_year_purchases_dropped.count()
this_year_purchases_dropped_percentage = int(
safe_division(this_year_purchases_dropped_count, all_purchased_this_year_count)
* 100
)
2023-11-01 20:18:39 +01:00
context = {
"total_hours": format_duration(
2023-11-09 19:35:57 +01:00
this_year_sessions.total_duration_unformatted(), "%2.0H"
2023-11-01 20:18:39 +01:00
),
"total_games": this_year_played_games.count(),
"total_year_games": this_year_played_purchases.filter(
2025-01-29 22:05:06 +01:00
games__year_released=year
2023-11-02 20:12:32 +01:00
).count(),
2023-11-02 15:08:11 +01:00
"top_10_games_by_playtime": top_10_games_by_playtime,
2023-11-01 20:18:39 +01:00
"year": year,
"total_playtime_per_platform": total_playtime_per_platform,
2023-11-02 20:12:32 +01:00
"total_spent": total_spent,
"total_spent_currency": selected_currency,
2023-11-09 19:35:57 +01:00
"all_purchased_this_year": this_year_purchases_without_refunded,
"spent_per_game": int(
2024-01-03 21:35:47 +01:00
safe_division(total_spent, this_year_purchases_without_refunded_count)
2023-11-09 21:43:17 +01:00
),
"all_finished_this_year": purchases_finished_this_year.prefetch_related(
2025-01-29 22:05:06 +01:00
"games"
2024-01-03 21:35:47 +01:00
).order_by("date_finished"),
"all_finished_this_year_count": purchases_finished_this_year.count(),
"this_year_finished_this_year": purchases_finished_this_year_released_this_year.prefetch_related(
2025-01-29 22:05:06 +01:00
"games"
2024-08-11 18:34:50 +02:00
).order_by("date_finished"),
2024-01-03 21:35:47 +01:00
"this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(),
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.prefetch_related(
2025-01-29 22:05:06 +01:00
"games"
2024-08-11 18:34:50 +02:00
).order_by("date_finished"),
2023-11-09 19:35:57 +01:00
"total_sessions": this_year_sessions.count(),
"unique_days": unique_days["dates"],
"unique_days_percent": int(unique_days["dates"] / 365 * 100),
2023-11-09 19:35:57 +01:00
"purchased_unfinished": this_year_purchases_unfinished,
2024-01-03 21:35:47 +01:00
"purchased_unfinished_count": this_year_purchases_unfinished_count,
2023-11-09 19:35:57 +01:00
"unfinished_purchases_percent": this_year_purchases_unfinished_percent,
2024-03-10 22:48:46 +01:00
"dropped_count": this_year_purchases_dropped_count,
"dropped_percentage": this_year_purchases_dropped_percentage,
"refunded_percent": int(
2023-11-09 10:06:14 +01:00
safe_division(
2024-01-03 21:35:47 +01:00
all_purchased_refunded_this_year_count,
all_purchased_this_year_count,
2023-11-09 10:06:14 +01:00
)
* 100
),
2023-11-09 19:35:57 +01:00
"all_purchased_refunded_this_year": this_year_purchases_refunded,
2024-01-03 21:35:47 +01:00
"all_purchased_refunded_this_year_count": all_purchased_refunded_this_year_count,
2023-11-09 19:35:57 +01:00
"all_purchased_this_year": this_year_purchases_with_currency.order_by(
"date_purchased"
),
2024-01-03 21:35:47 +01:00
"all_purchased_this_year_count": all_purchased_this_year_count,
2023-11-09 19:15:49 +01:00
"backlog_decrease_count": backlog_decrease_count,
2024-02-18 09:03:35 +01:00
"longest_session_time": (
format_duration(longest_session.duration, "%2.0Hh %2.0mm")
if longest_session
else 0
),
2025-01-29 22:05:06 +01:00
"longest_session_game": (longest_session.game if longest_session else None),
2024-02-18 09:03:35 +01:00
"highest_session_count": (
game_highest_session_count.session_count
if game_highest_session_count
else 0
),
"highest_session_count_game": (
2024-07-09 19:40:47 +02:00
game_highest_session_count if game_highest_session_count else None
2024-02-18 09:03:35 +01:00
),
"highest_session_average": (
format_duration(
highest_session_average_game.session_average, "%2.0Hh %2.0mm"
)
if highest_session_average_game
else 0
),
2023-11-21 21:57:17 +01:00
"highest_session_average_game": highest_session_average_game,
2024-07-09 19:40:47 +02:00
"first_play_game": first_play_game,
2024-01-01 18:42:14 +01:00
"first_play_date": first_play_date,
2024-07-09 19:40:47 +02:00
"last_play_game": last_play_game,
2024-01-01 18:42:14 +01:00
"last_play_date": last_play_date,
2023-12-15 10:58:15 +01:00
"title": f"{year} Stats",
2024-04-02 08:18:58 +02:00
"month_playtimes": month_playtimes,
2024-10-16 18:31:12 +02:00
"stats_dropdown_year_range": available_stats_year_range(),
2023-11-01 20:18:39 +01:00
}
request.session["return_path"] = request.path
2023-11-01 20:18:39 +01:00
return render(request, "stats.html", context)
2024-06-03 18:19:11 +02:00
2024-02-09 22:03:18 +01:00
@login_required
2024-08-08 21:19:43 +02:00
def index(request: HttpRequest) -> HttpResponse:
2024-08-11 17:58:08 +02:00
return redirect("list_sessions")