Skip to content

Commit 75f469d

Browse files
daviddavisweb-flow
andcommitted
Add configurable week start day preference
- Add WeekStartDayChoices model and week_start_day field to User - Add week start day dropdown to preferences page with new week SVG icon - Update statistics activity grid to respect week start day setting - Update events calendar to respect week start day setting - Dynamically render weekday labels in both statistics and calendar views Co-authored-by: GitHub Copilot <noreply@github.com>
1 parent 4e84280 commit 75f469d

9 files changed

Lines changed: 163 additions & 51 deletions

File tree

src/app/statistics.py

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@
88
from dateutil.relativedelta import relativedelta
99
from django.apps import apps
1010
from django.db import models
11-
from django.db.models import (
12-
Prefetch,
13-
Q,
14-
)
11+
from django.db.models import Prefetch, Q
1512
from django.utils import timezone
1613

1714
from app import config
@@ -388,12 +385,47 @@ def time_line_sort_key(media):
388385
return timezone.localdate(media.start_date)
389386

390387

388+
def _build_month_labels(date_range, week_start_weekday):
389+
"""Build month labels and their corresponding week counts for the activity grid."""
390+
months = []
391+
weeks_per_month = []
392+
current_month = date_range[0].strftime("%b")
393+
week_count = 0
394+
395+
for current_date in date_range:
396+
if current_date.weekday() == week_start_weekday:
397+
month = current_date.strftime("%b")
398+
399+
if current_month != month:
400+
if current_month is not None:
401+
if week_count > 1:
402+
months.append(current_month)
403+
weeks_per_month.append(week_count)
404+
else:
405+
months.append("")
406+
weeks_per_month.append(week_count)
407+
current_month = month
408+
week_count = 0
409+
410+
week_count += 1
411+
# For the last month
412+
if week_count > 1:
413+
months.append(current_month)
414+
weeks_per_month.append(week_count)
415+
416+
return months, weeks_per_month
417+
418+
391419
def get_activity_data(user, start_date, end_date):
392420
"""Get daily activity counts for the last year."""
393421
if end_date is None:
394422
end_date = timezone.localtime()
395423

396-
start_date_aligned = get_aligned_monday(start_date)
424+
week_start_sunday = getattr(user, "week_start_day", "monday") == "sunday"
425+
start_date_aligned = get_aligned_week_start(
426+
start_date,
427+
week_start_sunday=week_start_sunday,
428+
)
397429

398430
combined_data = get_filtered_historical_data(start_date_aligned, end_date, user)
399431

@@ -404,7 +436,10 @@ def get_activity_data(user, start_date, end_date):
404436
min(dates) if dates else timezone.localdate(),
405437
datetime.time.min,
406438
)
407-
start_date_aligned = get_aligned_monday(start_date)
439+
start_date_aligned = get_aligned_week_start(
440+
start_date,
441+
week_start_sunday=week_start_sunday,
442+
)
408443

409444
# Aggregate counts by date
410445
date_counts = {}
@@ -440,36 +475,19 @@ def get_activity_data(user, start_date, end_date):
440475
# Format data into calendar weeks
441476
calendar_weeks = [activity_data[i : i + 7] for i in range(0, len(activity_data), 7)]
442477

443-
# Generate months list with their Monday counts
444-
months = []
445-
mondays_per_month = []
446-
current_month = date_range[0].strftime("%b")
447-
monday_count = 0
478+
# Generate months list with their week-start-day counts
479+
# The first day of each week column corresponds to the user's chosen week start day
480+
week_start_weekday = 6 if week_start_sunday else 0 # 0=Monday, 6=Sunday
481+
months, weeks_per_month = _build_month_labels(date_range, week_start_weekday)
448482

449-
for current_date in date_range:
450-
if current_date.weekday() == 0: # Monday
451-
month = current_date.strftime("%b")
452-
453-
if current_month != month:
454-
if current_month is not None:
455-
if monday_count > 1:
456-
months.append(current_month)
457-
mondays_per_month.append(monday_count)
458-
else:
459-
months.append("")
460-
mondays_per_month.append(monday_count)
461-
current_month = month
462-
monday_count = 0
463-
464-
monday_count += 1
465-
# For the last month
466-
if monday_count > 1:
467-
months.append(current_month)
468-
mondays_per_month.append(monday_count)
483+
# Weekday labels depend on week start day
484+
days = list(calendar.day_abbr)
485+
weekday_labels = [days[6], *days[0:6]] if week_start_sunday else days
469486

470487
return {
471488
"calendar_weeks": calendar_weeks,
472-
"months": list(zip(months, mondays_per_month, strict=False)),
489+
"months": list(zip(months, weeks_per_month, strict=False)),
490+
"weekday_labels": weekday_labels,
473491
"stats": {
474492
"most_active_day": most_active_day,
475493
"most_active_day_percentage": day_percentage,
@@ -479,12 +497,16 @@ def get_activity_data(user, start_date, end_date):
479497
}
480498

481499

482-
def get_aligned_monday(datetime_obj):
483-
"""Get the Monday of the week containing the given date."""
500+
def get_aligned_week_start(datetime_obj, *, week_start_sunday=False):
501+
"""Get the first day of the week containing the given date."""
484502
if datetime_obj is None:
485503
return None
486504

487-
days_to_subtract = datetime_obj.weekday() # 0=Monday, 6=Sunday
505+
if week_start_sunday:
506+
# Sunday=weekday 6; if Sunday, subtract 0; else subtract (weekday+1)
507+
days_to_subtract = (datetime_obj.weekday() + 1) % 7
508+
else:
509+
days_to_subtract = datetime_obj.weekday() # 0=Monday, 6=Sunday
488510
return datetime_obj - datetime.timedelta(days=days_to_subtract)
489511

490512

src/events/views.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,18 @@ def calendar(request):
6060
) - timedelta(days=1)
6161

6262
# Get calendar data
63-
calendar_format = cal.monthcalendar(year, month)
63+
first_weekday = 6 if request.user.week_start_day == "sunday" else 0
64+
c = cal.Calendar(firstweekday=first_weekday)
65+
calendar_format = c.monthdayscalendar(year, month)
6466
month_name = cal.month_name[month]
6567

68+
# Build weekday headers based on user preference
69+
days = list(cal.day_abbr)
70+
sunday = 6
71+
weekday_headers = (
72+
[days[sunday], *days[0:sunday]] if first_weekday == sunday else days
73+
)
74+
6675
# Get events and organize by day
6776
releases = Event.objects.get_user_events(request.user, first_day, last_day)
6877

@@ -80,6 +89,7 @@ def calendar(request):
8089

8190
context = {
8291
"calendar": calendar_format,
92+
"weekday_headers": weekday_headers,
8393
"month": month,
8494
"month_name": month_name,
8595
"year": year,

src/templates/app/icons/week.svg

Lines changed: 15 additions & 0 deletions
Loading

src/templates/app/statistics.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,13 @@ <h2 class="text-xl font-semibold mb-4 text-center">Activity History</h2>
183183
<div class="flex">
184184
<!-- Weekday labels -->
185185
<div class="flex flex-col justify-between pr-2 text-sm text-gray-400">
186-
<div class="h-4 flex items-center">Mon</div>
187-
<div class="h-4"></div>
188-
<div class="h-4 flex items-center">Wed</div>
189-
<div class="h-4"></div>
190-
<div class="h-4 flex items-center">Fri</div>
191-
<div class="h-4"></div>
192-
<div class="h-4 flex items-center">Sun</div>
186+
{% for label in activity_data.weekday_labels %}
187+
{% if forloop.counter|divisibleby:2 %}
188+
<div class="h-4"></div>
189+
{% else %}
190+
<div class="h-4 flex items-center">{{ label }}</div>
191+
{% endif %}
192+
{% endfor %}
193193
</div>
194194

195195
<!-- Weeks grid -->

src/templates/events/components/calendar_grid.html

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,9 @@
22
{% load events_tags %}
33

44
<div class="grid grid-cols-7 gap-1 mb-2">
5-
<div class="text-center text-sm font-medium text-gray-400 py-2">Mon</div>
6-
<div class="text-center text-sm font-medium text-gray-400 py-2">Tue</div>
7-
<div class="text-center text-sm font-medium text-gray-400 py-2">Wed</div>
8-
<div class="text-center text-sm font-medium text-gray-400 py-2">Thu</div>
9-
<div class="text-center text-sm font-medium text-gray-400 py-2">Fri</div>
10-
<div class="text-center text-sm font-medium text-gray-400 py-2">Sat</div>
11-
<div class="text-center text-sm font-medium text-gray-400 py-2">Sun</div>
5+
{% for header in weekday_headers %}
6+
<div class="text-center text-sm font-medium text-gray-400 py-2">{{ header }}</div>
7+
{% endfor %}
128
</div>
139

1410
<div class="grid grid-cols-7 gap-1">

src/templates/users/preferences.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,26 @@ <h2 class="text-xl font-semibold">Preferences</h2>
169169
</div>
170170
</div>
171171

172+
{# Week Start Day #}
173+
<div class="mb-5">
174+
<div class="flex items-center justify-between p-3 bg-[#39404b] rounded-md">
175+
<div class="flex-1">
176+
<div class="flex items-center text-gray-200 mb-1">
177+
{% include "app/icons/week.svg" with classes="w-5 h-5 mr-2" %}
178+
<span class="text-sm font-medium">Week Start Day</span>
179+
</div>
180+
<p class="text-xs text-gray-400 ml-7">Choose which day the week starts on for calendars and statistics.</p>
181+
</div>
182+
<select name="week_start_day"
183+
class="ml-4 p-2 bg-[#39404b] rounded-md text-white text-sm border border-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-400">
184+
{% for choice in week_start_day_choices %}
185+
<option value="{{ choice.0 }}"
186+
{% if user.week_start_day == choice.0 %}selected{% endif %}>{{ choice.1 }}</option>
187+
{% endfor %}
188+
</select>
189+
</div>
190+
</div>
191+
172192
{# Watch Provider Region #}
173193
<div class="mb-5">
174194
<div class="flex items-center justify-between p-3 bg-[#39404b] rounded-md">
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 5.2.13 on 2026-04-19 16:31
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('users', '0050_user_watch_provider_region'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='user',
15+
name='week_start_day',
16+
field=models.CharField(choices=[('monday', 'Monday'), ('sunday', 'Sunday')], default='monday', help_text='First day of the week', max_length=10),
17+
),
18+
migrations.AddConstraint(
19+
model_name='user',
20+
constraint=models.CheckConstraint(condition=models.Q(('week_start_day__in', ['monday', 'sunday'])), name='week_start_day_valid'),
21+
),
22+
]

src/users/models.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ class TimeFormatChoices(models.TextChoices):
106106
HOUR_12 = "g:i A", "2:30 PM (12-hour)"
107107

108108

109+
class WeekStartDayChoices(models.TextChoices):
110+
"""Choices for week start day."""
111+
112+
MONDAY = "monday", "Monday"
113+
SUNDAY = "sunday", "Sunday"
114+
115+
109116
class User(AbstractUser):
110117
"""Custom user model."""
111118

@@ -312,6 +319,13 @@ class User(AbstractUser):
312319
help_text="Preferred time display format",
313320
)
314321

322+
week_start_day = models.CharField(
323+
max_length=10,
324+
default=WeekStartDayChoices.MONDAY,
325+
choices=WeekStartDayChoices,
326+
help_text="First day of the week",
327+
)
328+
315329
# Progress bar
316330
progress_bar = models.BooleanField(
317331
default=True,
@@ -510,6 +524,10 @@ class Meta:
510524
name="quick_watch_date_valid",
511525
condition=models.Q(quick_watch_date__in=QuickWatchDateChoices.values),
512526
),
527+
models.CheckConstraint(
528+
name="week_start_day_valid",
529+
condition=models.Q(week_start_day__in=WeekStartDayChoices.values),
530+
),
513531
]
514532

515533
def update_preference(self, field_name, new_value):

src/users/views.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
from app.models import Item, MediaTypes
1616
from app.providers import tmdb
1717
from users.forms import NotificationSettingsForm, PasswordChangeForm, UserUpdateForm
18-
from users.models import DateFormatChoices, QuickWatchDateChoices, TimeFormatChoices
18+
from users.models import (
19+
DateFormatChoices,
20+
QuickWatchDateChoices,
21+
TimeFormatChoices,
22+
WeekStartDayChoices,
23+
)
1924

2025
logger = logging.getLogger(__name__)
2126

@@ -225,6 +230,7 @@ def preferences(request):
225230
"quick_watch_date_choices": QuickWatchDateChoices.choices,
226231
"date_format_choices": DateFormatChoices.choices,
227232
"time_format_choices": TimeFormatChoices.choices,
233+
"week_start_day_choices": WeekStartDayChoices.choices,
228234
"watch_provider_choices": watch_provider_regions,
229235
},
230236
)
@@ -253,6 +259,9 @@ def preferences(request):
253259
"time_format",
254260
TimeFormatChoices.HOUR_24,
255261
)
262+
week_start_day = request.POST.get("week_start_day")
263+
if week_start_day in WeekStartDayChoices.values:
264+
request.user.week_start_day = week_start_day
256265
media_types_checked = request.POST.getlist("media_types_checkboxes")
257266

258267
provider_region = request.POST.get("watch_provider_region", "")

0 commit comments

Comments
 (0)