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 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:53:12 +02:00
parent 1dd2e5099f
commit 9127be0c42
15 changed files with 423 additions and 13 deletions

View File

@@ -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("<svg", forgot[: forgot.index("</a>")])

View File

@@ -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'),
),
]

View File

@@ -1 +0,0 @@
# Register your models here.

View File

@@ -1 +0,0 @@
# Create your models here.

View File

@@ -7,16 +7,18 @@
{% block main %}
<div class="mb-6 flex flex-wrap items-center justify-between gap-3">
<div>
<h1 class="text-2xl font-bold">
<div class="flex flex-col gap-2">
<h1 class="text-3xl font-bold">
{% block heading %}Control panel{% endblock heading %}
</h1>
{% block subheading %}{% endblock subheading %}
<span class="text-sm text-base-content/50">{% block subheading %}{% endblock subheading %}</span>
</div>
<div class="flex gap-2">
{% block actions %}{% endblock actions %}
</div>
</div>
<div role="tablist" class="tabs-boxed tabs mb-6 w-fit">
<a role="tab" href="{% url 'controlpanel:dashboard' %}" class="tab gap-2 {% if nav == 'dashboard' %}tab-active{% endif %}">{% lucide "layout-dashboard" size=16 %} Dashboard</a>
<a role="tab" href="{% url 'controlpanel:club_list' %}" class="tab gap-2 {% if nav == 'clubs' %}tab-active{% endif %}">{% lucide "building-2" size=16 %} Clubs</a>

View File

@@ -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 %}
<a class="btn btn-primary gap-2" href="{% url 'controlpanel:club_create' %}">{% lucide "plus" size=16 %} New club</a>
<a class="btn btn-primary gap-2" href="{% url 'controlpanel:club_create' %}">{% lucide "plus" size=16 %} Create new club</a>
{% endblock actions %}
{% block panel %}

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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)

View File

@@ -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'),
),
]

View File

@@ -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")

View File

@@ -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));

View File

@@ -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'),
),
]

View File

@@ -81,7 +81,7 @@
</div>
{% endif %}
<main class="mx-auto w-full p-4">
<main class="mx-auto w-full py-4 px-8">
{% block main %}{% endblock main %}
</main>