Improve year picker on stats page
This commit is contained in:
@@ -20,6 +20,7 @@ from common.components.primitives import (
|
|||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
CsrfInput,
|
CsrfInput,
|
||||||
Div,
|
Div,
|
||||||
|
ExternalScript,
|
||||||
H1,
|
H1,
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
@@ -35,6 +36,7 @@ from common.components.primitives import (
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableTd,
|
TableTd,
|
||||||
|
YearPicker,
|
||||||
paginated_table_content,
|
paginated_table_content,
|
||||||
)
|
)
|
||||||
from common.components.search_select import (
|
from common.components.search_select import (
|
||||||
@@ -73,6 +75,7 @@ __all__ = [
|
|||||||
"ButtonGroup",
|
"ButtonGroup",
|
||||||
"CsrfInput",
|
"CsrfInput",
|
||||||
"Div",
|
"Div",
|
||||||
|
"ExternalScript",
|
||||||
"H1",
|
"H1",
|
||||||
"Icon",
|
"Icon",
|
||||||
"Input",
|
"Input",
|
||||||
@@ -91,6 +94,7 @@ __all__ = [
|
|||||||
"TableHeader",
|
"TableHeader",
|
||||||
"TableRow",
|
"TableRow",
|
||||||
"TableTd",
|
"TableTd",
|
||||||
|
"YearPicker",
|
||||||
"paginated_table_content",
|
"paginated_table_content",
|
||||||
"GameLink",
|
"GameLink",
|
||||||
"GameStatus",
|
"GameStatus",
|
||||||
|
|||||||
@@ -433,6 +433,92 @@ def ModuleScript(filename: str) -> SafeText:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ExternalScript(url: str) -> SafeText:
|
||||||
|
"""A plain `<script src=...>` tag for an external/CDN script."""
|
||||||
|
return mark_safe(f'<script src="{url}"></script>')
|
||||||
|
|
||||||
|
|
||||||
|
def YearPicker(
|
||||||
|
year: int | None = None,
|
||||||
|
available_years: tuple[int, ...] = (),
|
||||||
|
url_template: str = "",
|
||||||
|
) -> SafeText:
|
||||||
|
"""A Flowbite-datepicker year picker.
|
||||||
|
|
||||||
|
`year` is the selected year, or ``None`` for the all-time view (the empty
|
||||||
|
state). `available_years` are the years to enable in the popup grid.
|
||||||
|
`url_template` is a navigation URL containing the literal ``__year__``
|
||||||
|
placeholder, substituted with the chosen year in JS (keeps this component
|
||||||
|
decoupled from the project's URL names).
|
||||||
|
|
||||||
|
The Flowbite-datepicker UMD bundle is *not* loaded here — the view hoists it
|
||||||
|
via ``render_page(scripts=...)``.
|
||||||
|
"""
|
||||||
|
label = str(year) if year is not None else "Choose a year"
|
||||||
|
selected = str(year) if year is not None else ""
|
||||||
|
classes = (
|
||||||
|
"bg-brand text-white border-transparent hover:bg-brand-strong"
|
||||||
|
if year is not None
|
||||||
|
else "bg-neutral-secondary-medium text-heading border border-default-medium "
|
||||||
|
"hover:bg-neutral-tertiary-medium focus:ring-4 focus:ring-brand-medium"
|
||||||
|
)
|
||||||
|
years_csv = ",".join(str(y) for y in available_years)
|
||||||
|
return mark_safe(f"""<div class="relative inline-block" x-data="{{ pickerOpen: false }}"
|
||||||
|
@keydown.escape.window="pickerOpen = false">
|
||||||
|
<button type="button"
|
||||||
|
x-on:click="pickerOpen = !pickerOpen; $refs.pickerInput._pickerInstance && ($refs.pickerInput._pickerInstance.active ? $refs.pickerInput._pickerInstance.hide() : $refs.pickerInput._pickerInstance.show())"
|
||||||
|
class="inline-flex items-center rounded-base px-4 py-2 text-sm font-medium {classes}">
|
||||||
|
{label}
|
||||||
|
<svg class="w-4 h-4 ms-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<input type="text" x-ref="pickerInput" id="year-picker-input"
|
||||||
|
class="absolute opacity-0 pointer-events-none"
|
||||||
|
style="width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0;"
|
||||||
|
data-available-years="{years_csv}"
|
||||||
|
data-selected-year="{selected}"
|
||||||
|
data-url-template="{url_template}">
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {{
|
||||||
|
const pickerEl = document.getElementById('year-picker-input');
|
||||||
|
if (!pickerEl || pickerEl._pickerInstance) return;
|
||||||
|
|
||||||
|
const selectedYear = pickerEl.dataset.selectedYear;
|
||||||
|
const urlTemplate = pickerEl.dataset.urlTemplate;
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const availableYears = new Set(pickerEl.dataset.availableYears
|
||||||
|
.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n)));
|
||||||
|
|
||||||
|
const picker = new Datepicker(pickerEl, {{
|
||||||
|
pickLevel: 2,
|
||||||
|
format: 'yyyy',
|
||||||
|
minDate: new Date(1999, 0, 1),
|
||||||
|
maxDate: new Date(currentYear, 11, 31),
|
||||||
|
autohide: false,
|
||||||
|
orientation: 'bottom end',
|
||||||
|
showOnClick: false,
|
||||||
|
showOnFocus: false,
|
||||||
|
beforeShowYear: (date) => ({{ enabled: availableYears.has(date.getFullYear()) }})
|
||||||
|
}});
|
||||||
|
pickerEl._pickerInstance = picker;
|
||||||
|
|
||||||
|
picker.element.addEventListener('changeDate', (e) => {{
|
||||||
|
const year = e.detail.date?.getFullYear();
|
||||||
|
if (year && urlTemplate) {{
|
||||||
|
window.location.href = urlTemplate.replace('__year__', year);
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
if (selectedYear) {{
|
||||||
|
picker.dates = [new Date(parseInt(selectedYear), 0, 1)];
|
||||||
|
picker.update();
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
</script>""")
|
||||||
|
|
||||||
|
|
||||||
def AddForm(
|
def AddForm(
|
||||||
form,
|
form,
|
||||||
*,
|
*,
|
||||||
|
|||||||
+56
-7
@@ -898,9 +898,6 @@
|
|||||||
max-width: 96rem;
|
max-width: 96rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.mx-2 {
|
|
||||||
margin-inline: calc(var(--spacing) * 2);
|
|
||||||
}
|
|
||||||
.mx-auto {
|
.mx-auto {
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
@@ -940,6 +937,9 @@
|
|||||||
.mt-5 {
|
.mt-5 {
|
||||||
margin-top: calc(var(--spacing) * 5);
|
margin-top: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
|
.mt-6 {
|
||||||
|
margin-top: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
.apexcharts-canvas {
|
.apexcharts-canvas {
|
||||||
& .apexcharts-tooltip {
|
& .apexcharts-tooltip {
|
||||||
background-color: primary !important;
|
background-color: primary !important;
|
||||||
@@ -1199,6 +1199,9 @@
|
|||||||
min-width: 4rem;
|
min-width: 4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.mr-3 {
|
||||||
|
margin-right: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
.mr-4 {
|
.mr-4 {
|
||||||
margin-right: calc(var(--spacing) * 4);
|
margin-right: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
@@ -1245,6 +1248,9 @@
|
|||||||
.mb-10 {
|
.mb-10 {
|
||||||
margin-bottom: calc(var(--spacing) * 10);
|
margin-bottom: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
|
.mb-12 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 12);
|
||||||
|
}
|
||||||
.apexcharts-xaxistooltip {
|
.apexcharts-xaxistooltip {
|
||||||
.apexcharts-canvas & {
|
.apexcharts-canvas & {
|
||||||
color: var(--color-body) !important;
|
color: var(--color-body) !important;
|
||||||
@@ -1577,6 +1583,9 @@
|
|||||||
.w-72 {
|
.w-72 {
|
||||||
width: calc(var(--spacing) * 72);
|
width: calc(var(--spacing) * 72);
|
||||||
}
|
}
|
||||||
|
.w-\[300px\] {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
.w-auto {
|
.w-auto {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
@@ -1595,6 +1604,9 @@
|
|||||||
.max-w-120 {
|
.max-w-120 {
|
||||||
max-width: calc(var(--spacing) * 120);
|
max-width: calc(var(--spacing) * 120);
|
||||||
}
|
}
|
||||||
|
.max-w-lg {
|
||||||
|
max-width: var(--container-lg);
|
||||||
|
}
|
||||||
.max-w-md {
|
.max-w-md {
|
||||||
max-width: var(--container-md);
|
max-width: var(--container-md);
|
||||||
}
|
}
|
||||||
@@ -2134,6 +2146,12 @@
|
|||||||
.bg-neutral-primary-soft {
|
.bg-neutral-primary-soft {
|
||||||
background-color: var(--color-neutral-primary-soft);
|
background-color: var(--color-neutral-primary-soft);
|
||||||
}
|
}
|
||||||
|
.bg-neutral-primary-soft\/30 {
|
||||||
|
background-color: color-mix(in srgb, #fff 30%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--color-neutral-primary-soft) 30%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
.bg-neutral-quaternary {
|
.bg-neutral-quaternary {
|
||||||
background-color: var(--color-neutral-quaternary);
|
background-color: var(--color-neutral-quaternary);
|
||||||
}
|
}
|
||||||
@@ -2161,6 +2179,9 @@
|
|||||||
.bg-red-500 {
|
.bg-red-500 {
|
||||||
background-color: var(--color-red-500);
|
background-color: var(--color-red-500);
|
||||||
}
|
}
|
||||||
|
.bg-red-600 {
|
||||||
|
background-color: var(--color-red-600);
|
||||||
|
}
|
||||||
.bg-red-700 {
|
.bg-red-700 {
|
||||||
background-color: var(--color-red-700);
|
background-color: var(--color-red-700);
|
||||||
}
|
}
|
||||||
@@ -2346,6 +2367,9 @@
|
|||||||
.pb-16 {
|
.pb-16 {
|
||||||
padding-bottom: calc(var(--spacing) * 16);
|
padding-bottom: calc(var(--spacing) * 16);
|
||||||
}
|
}
|
||||||
|
.pl-3 {
|
||||||
|
padding-left: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
.datatable-empty {
|
.datatable-empty {
|
||||||
.datatable-wrapper .datatable-table & {
|
.datatable-wrapper .datatable-table & {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -2384,10 +2408,6 @@
|
|||||||
font-size: var(--text-4xl);
|
font-size: var(--text-4xl);
|
||||||
line-height: var(--tw-leading, var(--text-4xl--line-height));
|
line-height: var(--tw-leading, var(--text-4xl--line-height));
|
||||||
}
|
}
|
||||||
.text-5xl {
|
|
||||||
font-size: var(--text-5xl);
|
|
||||||
line-height: var(--tw-leading, var(--text-5xl--line-height));
|
|
||||||
}
|
|
||||||
.text-base {
|
.text-base {
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
line-height: var(--tw-leading, var(--text-base--line-height));
|
line-height: var(--tw-leading, var(--text-base--line-height));
|
||||||
@@ -2614,6 +2634,9 @@
|
|||||||
.text-red-800 {
|
.text-red-800 {
|
||||||
color: var(--color-red-800);
|
color: var(--color-red-800);
|
||||||
}
|
}
|
||||||
|
.text-slate-200 {
|
||||||
|
color: var(--color-slate-200);
|
||||||
|
}
|
||||||
.text-slate-300 {
|
.text-slate-300 {
|
||||||
color: var(--color-slate-300);
|
color: var(--color-slate-300);
|
||||||
}
|
}
|
||||||
@@ -2862,6 +2885,17 @@
|
|||||||
background-color: var(--color-gray-50);
|
background-color: var(--color-gray-50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.first-of-type\:border-t-0 {
|
||||||
|
&:first-of-type {
|
||||||
|
border-top-style: var(--tw-border-style);
|
||||||
|
border-top-width: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.first-of-type\:pt-0 {
|
||||||
|
&:first-of-type {
|
||||||
|
padding-top: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
.hover\:scale-110 {
|
.hover\:scale-110 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@@ -3209,6 +3243,11 @@
|
|||||||
border-bottom-left-radius: var(--radius-lg);
|
border-bottom-left-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sm\:p-5 {
|
||||||
|
@media (width >= 40rem) {
|
||||||
|
padding: calc(var(--spacing) * 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
.sm\:px-4 {
|
.sm\:px-4 {
|
||||||
@media (width >= 40rem) {
|
@media (width >= 40rem) {
|
||||||
padding-inline: calc(var(--spacing) * 4);
|
padding-inline: calc(var(--spacing) * 4);
|
||||||
@@ -3926,6 +3965,16 @@
|
|||||||
border-end-end-radius: var(--radius-lg);
|
border-end-end-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.\[\&\>div\]\:w-\[calc\(33\.333\%-0\.67rem\)\] {
|
||||||
|
&>div {
|
||||||
|
width: calc(33.333% - 0.67rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>div\]\:w-\[calc\(50\%-0\.5rem\)\] {
|
||||||
|
&>div {
|
||||||
|
width: calc(50% - 0.5rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
--color-body: var(--color-gray-400);
|
--color-body: var(--color-gray-400);
|
||||||
|
|||||||
+12
-2
@@ -13,12 +13,18 @@ from django.urls import reverse
|
|||||||
from django.utils.timezone import localtime
|
from django.utils.timezone import localtime
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
|
|
||||||
|
from common.components import ExternalScript
|
||||||
from common.layout import render_page
|
from common.layout import render_page
|
||||||
from common.time import format_duration
|
from common.time import format_duration
|
||||||
from games.models import Game, Platform, Purchase, Session
|
from games.models import Game, Platform, Purchase, Session
|
||||||
from games.views.stats_content import stats_content
|
from games.views.stats_content import stats_content
|
||||||
from games.views.stats_data import compute_stats
|
from games.views.stats_data import compute_stats
|
||||||
|
|
||||||
|
# Flowbite-datepicker UMD bundle, hoisted into the stats pages for YearPicker.
|
||||||
|
_STATS_SCRIPTS = ExternalScript(
|
||||||
|
"https://cdn.jsdelivr.net/npm/flowbite-datepicker@2.0.0/dist/Datepicker.umd.min.js"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def model_counts(request: HttpRequest) -> dict[str, bool]:
|
def model_counts(request: HttpRequest) -> dict[str, bool]:
|
||||||
now = timezone_now()
|
now = timezone_now()
|
||||||
@@ -71,7 +77,9 @@ def use_custom_redirect(
|
|||||||
def stats_alltime(request: HttpRequest) -> HttpResponse:
|
def stats_alltime(request: HttpRequest) -> HttpResponse:
|
||||||
request.session["return_path"] = request.path
|
request.session["return_path"] = request.path
|
||||||
data = compute_stats(None)
|
data = compute_stats(None)
|
||||||
return render_page(request, stats_content(data), title=data["title"])
|
return render_page(
|
||||||
|
request, stats_content(data), title=data["title"], scripts=_STATS_SCRIPTS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -85,7 +93,9 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
return HttpResponseRedirect(reverse("games:stats_alltime"))
|
return HttpResponseRedirect(reverse("games:stats_alltime"))
|
||||||
request.session["return_path"] = request.path
|
request.session["return_path"] = request.path
|
||||||
data = compute_stats(year)
|
data = compute_stats(year)
|
||||||
return render_page(request, stats_content(data), title=data["title"])
|
return render_page(
|
||||||
|
request, stats_content(data), title=data["title"], scripts=_STATS_SCRIPTS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ like the old `{% if key %}` blocks: a missing or empty value hides the section.
|
|||||||
|
|
||||||
from django.template.defaultfilters import date as date_filter
|
from django.template.defaultfilters import date as date_filter
|
||||||
from django.template.defaultfilters import floatformat
|
from django.template.defaultfilters import floatformat
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.html import conditional_escape
|
from django.utils.html import conditional_escape
|
||||||
from django.utils.safestring import SafeText, mark_safe
|
from django.utils.safestring import SafeText, mark_safe
|
||||||
|
|
||||||
from common.components import Component, Div, GameLink
|
from common.components import A, Component, Div, GameLink, YearPicker
|
||||||
from common.time import durationformat, format_duration
|
from common.time import durationformat, format_duration
|
||||||
|
|
||||||
_CELL = "px-2 sm:px-4 md:px-6 md:py-2"
|
_CELL = "px-2 sm:px-4 md:px-6 md:py-2"
|
||||||
@@ -41,7 +42,7 @@ def _kv(label, value) -> SafeText:
|
|||||||
def _h1(title: str) -> SafeText:
|
def _h1(title: str) -> SafeText:
|
||||||
return Component(
|
return Component(
|
||||||
tag_name="h1",
|
tag_name="h1",
|
||||||
attributes=[("class", "text-5xl text-center my-6")],
|
attributes=[("class", "text-3xl text-heading text-center my-6")],
|
||||||
children=[title],
|
children=[title],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,39 +76,35 @@ def _purchase_name(purchase) -> SafeText:
|
|||||||
return GameLink(first_game.id, name)
|
return GameLink(first_game.id, name)
|
||||||
|
|
||||||
|
|
||||||
def _year_dropdown(year, year_range) -> SafeText:
|
def _year_nav(year, year_range, url_template) -> SafeText:
|
||||||
options = []
|
# `year` is an int for a specific year, or "Alltime" (from compute_stats)
|
||||||
for year_item in year_range or []:
|
# for the all-time view. Normalize to int-or-None so nothing downstream has
|
||||||
attrs = [("value", str(year_item))]
|
# to know about the "Alltime" sentinel.
|
||||||
if year == year_item:
|
year_int = year if isinstance(year, int) else None
|
||||||
attrs.append(("selected", True))
|
is_alltime = year_int is None
|
||||||
options.append(
|
|
||||||
Component(tag_name="option", attributes=attrs, children=[str(year_item)])
|
alltime_classes = (
|
||||||
|
"inline-flex items-center rounded-base px-4 py-2 mr-3 text-sm font-medium "
|
||||||
)
|
)
|
||||||
select = Component(
|
alltime_classes += (
|
||||||
tag_name="select",
|
"bg-brand text-white hover:bg-brand-strong"
|
||||||
attributes=[
|
if is_alltime
|
||||||
("name", "year"),
|
else "text-body hover:text-heading underline decoration-dotted"
|
||||||
("id", "yearSelect"),
|
|
||||||
("onchange", "this.form.submit();"),
|
|
||||||
("class", "mx-2"),
|
|
||||||
],
|
|
||||||
children=options,
|
|
||||||
)
|
)
|
||||||
label = Component(
|
alltime_btn = A(
|
||||||
tag_name="label",
|
url_name="games:stats_alltime",
|
||||||
attributes=[
|
attributes=[("class", alltime_classes)],
|
||||||
("class", "text-5xl text-center inline-block mb-10"),
|
children=["All-time stats"],
|
||||||
("for", "yearSelect"),
|
|
||||||
],
|
|
||||||
children=["Stats for:"],
|
|
||||||
)
|
)
|
||||||
form = Component(
|
picker = YearPicker(
|
||||||
tag_name="form",
|
year=year_int,
|
||||||
attributes=[("method", "get"), ("class", "text-center")],
|
available_years=tuple(year_range or []),
|
||||||
children=[label, select],
|
url_template=url_template,
|
||||||
|
)
|
||||||
|
return Div(
|
||||||
|
[("class", "flex justify-center items-center mb-12")],
|
||||||
|
[alltime_btn, picker],
|
||||||
)
|
)
|
||||||
return Div([("class", "flex justify-center items-center")], [form])
|
|
||||||
|
|
||||||
|
|
||||||
def _playtime_table(ctx) -> SafeText:
|
def _playtime_table(ctx) -> SafeText:
|
||||||
@@ -260,8 +257,14 @@ def _priced_table(purchases, currency) -> SafeText:
|
|||||||
def stats_content(ctx: dict) -> SafeText:
|
def stats_content(ctx: dict) -> SafeText:
|
||||||
year = ctx.get("year")
|
year = ctx.get("year")
|
||||||
currency = ctx.get("total_spent_currency")
|
currency = ctx.get("total_spent_currency")
|
||||||
|
# Build a navigation URL with an `__year__` placeholder the picker's JS
|
||||||
|
# substitutes. Reverse a sentinel year, then swap it for the placeholder
|
||||||
|
# (anchored on `stats/0` so the match is unambiguous).
|
||||||
|
url_template = reverse("games:stats_by_year", args=[0]).replace(
|
||||||
|
"stats/0", "stats/__year__"
|
||||||
|
)
|
||||||
sections: list = [
|
sections: list = [
|
||||||
_year_dropdown(year, ctx.get("stats_dropdown_year_range")),
|
_year_nav(year, ctx.get("stats_dropdown_year_range"), url_template),
|
||||||
_h1("Playtime"),
|
_h1("Playtime"),
|
||||||
_playtime_table(ctx),
|
_playtime_table(ctx),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -253,7 +253,8 @@ class RenderedPagesTest(TestCase):
|
|||||||
def test_stats_alltime(self):
|
def test_stats_alltime(self):
|
||||||
html = self.get("games:stats_alltime").content.decode()
|
html = self.get("games:stats_alltime").content.decode()
|
||||||
for marker in [
|
for marker in [
|
||||||
'id="yearSelect"',
|
'id="year-picker-input"',
|
||||||
|
"All-time stats",
|
||||||
"responsive-table",
|
"responsive-table",
|
||||||
"Playtime",
|
"Playtime",
|
||||||
"Purchases",
|
"Purchases",
|
||||||
|
|||||||
Reference in New Issue
Block a user