From 9127be0c42fb4db861a806e502d7415d467381f3 Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Tue, 14 Jul 2026 00:53:12 +0200 Subject: [PATCH] Give every model created/modified timestamps TimeStampedModel goes on UUIDModel, so all 28 domain models get row birthdays in one place. Without them the dashboard can only ever describe the present: "42 members" is knowable, "members joined this month" is not, and no metric can show direction. Order dropped its own created/modified -- redeclaring a field from an abstract base is an error, and its column survives as a plain AlterField (verbose_name only), so no order data moves. Note for reading early charts: auto_now_add backfills existing rows with a single migration timestamp, so everything that predates this commit shares one birthday and will show up as a spike at that instant rather than as real history. Co-Authored-By: Claude Opus 4.8 --- authentication/tests.py | 2 +- ...odified_clubmembership_created_and_more.py | 58 ++++++++++ controlpanel/admin.py | 1 - controlpanel/models.py | 1 - controlpanel/templates/controlpanel/base.html | 8 +- .../templates/controlpanel/dashboard.html | 5 +- ...endance_modified_event_created_and_more.py | 69 +++++++++++ ..._answer_modified_field_created_and_more.py | 58 ++++++++++ ...family_modified_member_created_and_more.py | 36 ++++++ rosterchief/base.py | 15 ++- ...eated_applieddiscount_modified_and_more.py | 107 ++++++++++++++++++ shop/models.py | 4 +- static/css/app.css | 12 ++ ...tion_created_position_modified_and_more.py | 58 ++++++++++ templates/_base.html | 2 +- 15 files changed, 423 insertions(+), 13 deletions(-) create mode 100644 club/migrations/0013_club_created_club_modified_clubmembership_created_and_more.py delete mode 100644 controlpanel/admin.py delete mode 100644 controlpanel/models.py create mode 100644 events/migrations/0009_attendance_created_attendance_modified_event_created_and_more.py create mode 100644 formbuilder/migrations/0005_answer_created_answer_modified_field_created_and_more.py create mode 100644 members/migrations/0003_family_created_family_modified_member_created_and_more.py create mode 100644 shop/migrations/0010_applieddiscount_created_applieddiscount_modified_and_more.py create mode 100644 teams/migrations/0004_position_created_position_modified_and_more.py diff --git a/authentication/tests.py b/authentication/tests.py index 799a18a..d44dfe5 100644 --- a/authentication/tests.py +++ b/authentication/tests.py @@ -412,7 +412,7 @@ class ChangePasswordPageTests(TestCase): def test_forgot_password_is_an_accent_button_and_both_actions_have_icons(self): html = self.response.content.decode() - forgot = html[html.index('class="btn btn-accent gap-2"') :] + forgot = html[html.index("btn-accent") :] submit = html[html.index('class="btn btn-primary gap-2"') :] self.assertIn("")]) diff --git a/club/migrations/0013_club_created_club_modified_clubmembership_created_and_more.py b/club/migrations/0013_club_created_club_modified_clubmembership_created_and_more.py new file mode 100644 index 0000000..fcc113a --- /dev/null +++ b/club/migrations/0013_club_created_club_modified_clubmembership_created_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 6.0.6 on 2026-07-13 22:51 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('club', '0012_club_logo_club_primary_color'), + ] + + operations = [ + migrations.AddField( + model_name='club', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 565739, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='club', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='clubmembership', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 565911, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='clubmembership', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='clubrole', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 565939, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='clubrole', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='season', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 565961, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='season', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + ] diff --git a/controlpanel/admin.py b/controlpanel/admin.py deleted file mode 100644 index 846f6b4..0000000 --- a/controlpanel/admin.py +++ /dev/null @@ -1 +0,0 @@ -# Register your models here. diff --git a/controlpanel/models.py b/controlpanel/models.py deleted file mode 100644 index 6b20219..0000000 --- a/controlpanel/models.py +++ /dev/null @@ -1 +0,0 @@ -# Create your models here. diff --git a/controlpanel/templates/controlpanel/base.html b/controlpanel/templates/controlpanel/base.html index 5595f43..a1f346d 100644 --- a/controlpanel/templates/controlpanel/base.html +++ b/controlpanel/templates/controlpanel/base.html @@ -7,16 +7,18 @@ {% block main %}
-
-

+
+

{% block heading %}Control panel{% endblock heading %}

- {% block subheading %}{% endblock subheading %} + {% block subheading %}{% endblock subheading %}
+
{% block actions %}{% endblock actions %}

+
{% lucide "layout-dashboard" size=16 %} Dashboard {% lucide "building-2" size=16 %} Clubs diff --git a/controlpanel/templates/controlpanel/dashboard.html b/controlpanel/templates/controlpanel/dashboard.html index ed95e7c..a5c5832 100644 --- a/controlpanel/templates/controlpanel/dashboard.html +++ b/controlpanel/templates/controlpanel/dashboard.html @@ -1,10 +1,11 @@ {% extends "controlpanel/base.html" %} {% load lucide %} -{% block heading %}Platform overview{% endblock heading %} +{% block heading %}RosterChief Platform Dashboard{% endblock heading %} +{% block subheading %}Welcome back {{ user.member.first_name }}!{% endblock subheading %} {% block actions %} - {% lucide "plus" size=16 %} New club + {% lucide "plus" size=16 %} Create new club {% endblock actions %} {% block panel %} diff --git a/events/migrations/0009_attendance_created_attendance_modified_event_created_and_more.py b/events/migrations/0009_attendance_created_attendance_modified_event_created_and_more.py new file mode 100644 index 0000000..4a3a4d3 --- /dev/null +++ b/events/migrations/0009_attendance_created_attendance_modified_event_created_and_more.py @@ -0,0 +1,69 @@ +# Generated by Django 6.0.6 on 2026-07-13 22:51 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0008_event_created_by'), + ] + + operations = [ + migrations.AddField( + model_name='attendance', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 565980, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='attendance', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='event', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 565998, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='event', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='eventseries', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566017, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='eventseries', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='location', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566033, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='location', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='opponent', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566050, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='opponent', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + ] diff --git a/formbuilder/migrations/0005_answer_created_answer_modified_field_created_and_more.py b/formbuilder/migrations/0005_answer_created_answer_modified_field_created_and_more.py new file mode 100644 index 0000000..c068112 --- /dev/null +++ b/formbuilder/migrations/0005_answer_created_answer_modified_field_created_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 6.0.6 on 2026-07-13 22:51 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('formbuilder', '0004_alter_field_key_alter_form_slug'), + ] + + operations = [ + migrations.AddField( + model_name='answer', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566066, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='answer', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='field', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566082, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='field', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='form', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566098, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='form', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='submission', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566113, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='submission', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + ] diff --git a/members/migrations/0003_family_created_family_modified_member_created_and_more.py b/members/migrations/0003_family_created_family_modified_member_created_and_more.py new file mode 100644 index 0000000..e878d25 --- /dev/null +++ b/members/migrations/0003_family_created_family_modified_member_created_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 6.0.6 on 2026-07-13 22:51 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0002_alter_familymembership_unique_together_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='family', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566131, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='family', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='member', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566186, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='member', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + ] diff --git a/rosterchief/base.py b/rosterchief/base.py index 173b25a..827012d 100644 --- a/rosterchief/base.py +++ b/rosterchief/base.py @@ -68,8 +68,19 @@ class TenantQuerySet(models.QuerySet): return self.filter(club=require_current_club()) -class UUIDModel(models.Model): - """Abstract base class giving every model a UUID primary key""" +class TimeStampedModel(models.Model): + """Row birthdays. Without these, every metric can only describe the present — + "42 members" is knowable, "members joined this month" is not.""" + + created = models.DateTimeField(_("created"), auto_now_add=True) + modified = models.DateTimeField(_("modified"), auto_now=True) + + class Meta: + abstract = True + + +class UUIDModel(TimeStampedModel): + """Abstract base class giving every model a UUID primary key and timestamps.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) diff --git a/shop/migrations/0010_applieddiscount_created_applieddiscount_modified_and_more.py b/shop/migrations/0010_applieddiscount_created_applieddiscount_modified_and_more.py new file mode 100644 index 0000000..52e69ff --- /dev/null +++ b/shop/migrations/0010_applieddiscount_created_applieddiscount_modified_and_more.py @@ -0,0 +1,107 @@ +# Generated by Django 6.0.6 on 2026-07-13 22:51 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0009_applieddiscount_unique_discount_per_order'), + ] + + operations = [ + migrations.AddField( + model_name='applieddiscount', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566202, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='applieddiscount', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='cart', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566218, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='cart', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='cartitem', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566233, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='cartitem', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='discount', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566250, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='discount', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='invoice', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566266, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='invoice', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='orderline', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566282, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='orderline', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='payment', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566299, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='payment', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='product', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566314, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='product', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AlterField( + model_name='order', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + ] diff --git a/shop/models.py b/shop/models.py index 9e4a30f..7331d28 100644 --- a/shop/models.py +++ b/shop/models.py @@ -142,8 +142,8 @@ class Order(ClubScopedModel): status = models.CharField(_("status"), max_length=255, choices=OrderStatus.choices, default=OrderStatus.PENDING) total = models.DecimalField(_("total"), max_digits=10, decimal_places=2) - created = models.DateTimeField(_("created at"), auto_now_add=True) - modified = models.DateTimeField(_("modified"), auto_now=True) + # `created`/`modified` come from TimeStampedModel — declaring them here as well + # would clash with the abstract base. class Meta: verbose_name = _("order") diff --git a/static/css/app.css b/static/css/app.css index 0e46d71..bda83c8 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -23,6 +23,8 @@ --text-xl--line-height: calc(1.75 / 1.25); --text-2xl: 1.5rem; --text-2xl--line-height: calc(2 / 1.5); + --text-3xl: 1.875rem; + --text-3xl--line-height: calc(2.25 / 1.875); --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; @@ -3174,9 +3176,15 @@ .px-6 { padding-inline: calc(var(--spacing) * 6); } + .px-8 { + padding-inline: calc(var(--spacing) * 8); + } .py-2 { padding-block: calc(var(--spacing) * 2); } + .py-4 { + padding-block: calc(var(--spacing) * 4); + } .pt-2 { padding-top: calc(var(--spacing) * 2); } @@ -3202,6 +3210,10 @@ font-size: var(--text-2xl); line-height: var(--tw-leading, var(--text-2xl--line-height)); } + .text-3xl { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } .text-base { font-size: var(--text-base); line-height: var(--tw-leading, var(--text-base--line-height)); diff --git a/teams/migrations/0004_position_created_position_modified_and_more.py b/teams/migrations/0004_position_created_position_modified_and_more.py new file mode 100644 index 0000000..0b4cf51 --- /dev/null +++ b/teams/migrations/0004_position_created_position_modified_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 6.0.6 on 2026-07-13 22:51 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0003_position_management_position_implies_staff_position'), + ] + + operations = [ + migrations.AddField( + model_name='position', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566331, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='position', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='staffassignment', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566347, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='staffassignment', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='team', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566410, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='team', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddField( + model_name='teammembership', + name='created', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2026, 7, 13, 22, 51, 35, 566427, tzinfo=datetime.timezone.utc), verbose_name='created'), + preserve_default=False, + ), + migrations.AddField( + model_name='teammembership', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + ] diff --git a/templates/_base.html b/templates/_base.html index e55b53f..7bc4ab4 100644 --- a/templates/_base.html +++ b/templates/_base.html @@ -81,7 +81,7 @@
{% endif %} -
+
{% block main %}{% endblock main %}