Skip to content
12 changes: 7 additions & 5 deletions src/camps/migrations/0039_camp_kickoff.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
# Generated by Django 4.2.21 on 2025-05-28 22:50
from __future__ import annotations

import django.contrib.postgres.fields.ranges
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('camps', '0038_alter_permission_options'),
("camps", "0038_alter_permission_options"),
]

operations = [
migrations.AddField(
model_name='camp',
name='kickoff',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(blank=True, help_text='The camp kickoff period.', null=True, verbose_name='Camp Kickoff'),
model_name="camp",
name="kickoff",
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
blank=True, help_text="The camp kickoff period.", null=True, verbose_name="Camp Kickoff"
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Generated by Django 4.2.21 on 2025-07-01 13:04
from __future__ import annotations

import django.db.models.deletion
from django.db import migrations
from django.db import models


class Migration(migrations.Migration):
dependencies = [
("tickets", "0028_alter_prizeticket_comment"),
("camps", "0039_camp_kickoff"),
]

operations = [
migrations.AddField(
model_name="camp",
name="ticket_type_full_week_adult",
field=models.ForeignKey(
blank=True,
help_text="The ticket type for 'Adult Full Week' for this camp",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="full_week_adult_camps",
to="tickets.tickettype",
),
),
migrations.AddField(
model_name="camp",
name="ticket_type_full_week_child",
field=models.ForeignKey(
blank=True,
help_text="The ticket type for 'Child Full Week' for this camp",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="full_week_child_camps",
to="tickets.tickettype",
),
),
migrations.AddField(
model_name="camp",
name="ticket_type_one_day_adult",
field=models.ForeignKey(
blank=True,
help_text="The ticket type for 'Adult One Day' for this camp",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="one_day_adult_camps",
to="tickets.tickettype",
),
),
migrations.AddField(
model_name="camp",
name="ticket_type_one_day_child",
field=models.ForeignKey(
blank=True,
help_text="The ticket type for 'Child One Day' for this camp",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="one_day_child_camps",
to="tickets.tickettype",
),
),
]
184 changes: 178 additions & 6 deletions src/camps/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@

import logging
from datetime import timedelta
from django.conf import settings

from django.apps import apps
from django.contrib.postgres.fields import DateTimeRangeField
from django.conf import settings
from django.contrib.auth.models import Permission as DjangoPermission
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import DateTimeRangeField
from django.core.exceptions import ValidationError
from django.contrib.auth.models import Permission as DjangoPermission
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django_prometheus.models import ExportModelOperationsMixin
from psycopg2.extras import DateTimeTZRange

from tickets.models import PrizeTicket
from tickets.models import ShopTicket
from tickets.models import SponsorTicket
from utils.models import CreatedUpdatedModel
from utils.models import UUIDModel

Expand Down Expand Up @@ -62,7 +65,12 @@ class Meta:
help_text="Abbreviated version of the slug. Used in IRC channel names and other places with restricted name length.",
)

kickoff = DateTimeRangeField(null=True, blank=True, verbose_name="Camp Kickoff", help_text="The camp kickoff period.")
kickoff = DateTimeRangeField(
null=True,
blank=True,
verbose_name="Camp Kickoff",
help_text="The camp kickoff period.",
)

buildup = DateTimeRangeField(
verbose_name="Buildup Period",
Expand Down Expand Up @@ -128,6 +136,42 @@ class Meta:
related_name="+",
)

ticket_type_full_week_adult = models.ForeignKey(
"tickets.TicketType",
on_delete=models.SET_NULL,
help_text="The ticket type for 'Adult Full Week' for this camp",
null=True,
blank=True,
related_name="full_week_adult_camps",
)

ticket_type_one_day_adult = models.ForeignKey(
"tickets.TicketType",
on_delete=models.SET_NULL,
help_text="The ticket type for 'Adult One Day' for this camp",
null=True,
blank=True,
related_name="one_day_adult_camps",
)

ticket_type_full_week_child = models.ForeignKey(
"tickets.TicketType",
on_delete=models.SET_NULL,
help_text="The ticket type for 'Child Full Week' for this camp",
null=True,
blank=True,
related_name="full_week_child_camps",
)

ticket_type_one_day_child = models.ForeignKey(
"tickets.TicketType",
on_delete=models.SET_NULL,
help_text="The ticket type for 'Child One Day' for this camp",
null=True,
blank=True,
related_name="one_day_child_camps",
)

def get_absolute_url(self):
return reverse("camp_detail", kwargs={"camp_slug": self.slug})

Expand All @@ -152,7 +196,6 @@ def clean(self) -> None:
def __str__(self) -> str:
return f"{self.title} - {self.tagline}"


def activate_team_permissions(self) -> None:
"""Add permissions to this camps teams."""
permission_content_type = ContentType.objects.get_for_model(Permission)
Expand Down Expand Up @@ -181,7 +224,6 @@ def deactivate_team_permissions(self) -> None:
group.permissions.remove(permission)
logger.debug(f"Removed permission {permission} from group {group}")


@property
def logo_small(self) -> str:
return f"img/{self.slug}/logo/{self.slug}-logo-s.png"
Expand Down Expand Up @@ -303,3 +345,133 @@ def event_sessions(self):
def event_slots(self):
EventSlot = apps.get_model("program", "EventSlot")
return EventSlot.objects.filter(event_session__in=self.event_sessions.all())

@property
def checked_in_full_week_adults(self) -> int:
"""Return the count of full week adult tickets checked in"""
shop_tickets = (
ShopTicket.objects.filter(
ticket_type=self.ticket_type_full_week_adult,
).exclude(used_at=None)
).count()

sponsor_tickets = (
SponsorTicket.objects.filter(
ticket_type=self.ticket_type_full_week_adult,
).exclude(used_at=None)
).count()

prize_tickets = (
PrizeTicket.objects.filter(
ticket_type=self.ticket_type_full_week_adult,
).exclude(used_at=None)
).count()

return shop_tickets + sponsor_tickets + prize_tickets

@property
def checked_in_full_week_children(self) -> int:
"""Return the count of full week children tickets checked in"""
shop_tickets = (
ShopTicket.objects.filter(
ticket_type=self.ticket_type_full_week_child,
).exclude(used_at=None)
).count()

sponsor_tickets = (
SponsorTicket.objects.filter(
ticket_type=self.ticket_type_full_week_child,
).exclude(used_at=None)
).count()

prize_tickets = (
PrizeTicket.objects.filter(
ticket_type=self.ticket_type_full_week_child,
).exclude(used_at=None)
).count()

return shop_tickets + sponsor_tickets + prize_tickets

@property
def checked_in_one_day_adults(self) -> int:
"""Return the count of todays one day adult tickets checked in.

Count tickets with a checked in timestamp from 0600-0600 next day.
Reason being early arriving participants might get checked in before 10.
"""
now = timezone.localtime()
today_06_hour = now.replace(hour=6, minute=0, second=0)
if now < today_06_hour:
start = today_06_hour - timezone.timedelta(days=1)
end = today_06_hour
else:
start = today_06_hour
end = today_06_hour + timezone.timedelta(days=1)

shop_tickets = (
ShopTicket.objects.filter(
ticket_type=self.ticket_type_one_day_adult,
).filter(used_at__gte=start, used_at__lt=end)
).count()

sponsor_tickets = (
SponsorTicket.objects.filter(
ticket_type=self.ticket_type_one_day_adult,
).filter(used_at__gte=start, used_at__lt=end)
).count()

prize_tickets = (
PrizeTicket.objects.filter(
ticket_type=self.ticket_type_one_day_adult,
).filter(used_at__gte=start, used_at__lt=end)
).count()

return shop_tickets + sponsor_tickets + prize_tickets

@property
def checked_in_one_day_children(self) -> int:
"""Return the count of todays one day children tickets checked in.

Count tickets with a checked in timestamp from 0600-0600 next day.
Reason being early arriving participants might get checked in before 10.
"""
now = timezone.localtime()
today_06_hour = now.replace(hour=6, minute=0, second=0)
if now < today_06_hour:
start = today_06_hour - timezone.timedelta(days=1)
end = today_06_hour
else:
start = today_06_hour
end = today_06_hour + timezone.timedelta(days=1)

shop_tickets = (
ShopTicket.objects.filter(
ticket_type=self.ticket_type_one_day_child,
).filter(used_at__gte=start, used_at__lt=end)
).count()

sponsor_tickets = (
SponsorTicket.objects.filter(
ticket_type=self.ticket_type_one_day_child,
).filter(used_at__gte=start, used_at__lt=end)
).count()

prize_tickets = (
PrizeTicket.objects.filter(
ticket_type=self.ticket_type_one_day_child,
).filter(used_at__gte=start, used_at__lt=end)
).count()

return shop_tickets + sponsor_tickets + prize_tickets

@property
def participant_count(self) -> int:
"""Retrieve the participant count for all used 'full week' tickets
and todays used 'one day' tickets.
"""
return (
self.checked_in_full_week_adults
+ self.checked_in_full_week_children
+ self.checked_in_one_day_adults
+ self.checked_in_one_day_children
)
Loading
Loading