diff --git a/kompassi/events/ropecon2026/management/commands/setup_ropecon2026.py b/kompassi/events/ropecon2026/management/commands/setup_ropecon2026.py index a1e714516..f7a6b525b 100644 --- a/kompassi/events/ropecon2026/management/commands/setup_ropecon2026.py +++ b/kompassi/events/ropecon2026/management/commands/setup_ropecon2026.py @@ -276,7 +276,7 @@ def setup_program_v2(self): "state": ["active"], "v1-personnel-class": ["ohjelma"], }, - job_title="Ohjelmanjärjestäjä", + job_title="Ohjelma", job_title_mode=JobTitleMode.OVERRIDE, priority=self.get_ordering_number(), ), diff --git a/kompassi/involvement/emperkelators/ropecon2026.py b/kompassi/involvement/emperkelators/ropecon2026.py index 108f0e009..a8bb30866 100644 --- a/kompassi/involvement/emperkelators/ropecon2026.py +++ b/kompassi/involvement/emperkelators/ropecon2026.py @@ -102,11 +102,96 @@ def __lt__(self, other: BadgeType) -> bool: BADGE_TYPES = list(BadgeType) +class Rule(Enum): + """ + Explains to the user why they have the perks they have. + This is not used for any logic, only for display purposes. + """ + + title_fi: str + title_en: str + + NONE = "none", "Ei sääntöä", "No rule" + V1_CONCOM = "v1-concom", "Työvoima V1: Conitea", "Labour V1: Concom" + V1_OVERSEER = "v1-overseer", "Työvoima V1: Ylivänkäri", "Labour V1: Overseer" + V1_VOLUNTEER = "v1-volunteer", "Työvoima V1: Vapaaehtoinen", "Labour V1: Volunteer" + V1_NO_MATCH = "v1-no-match", "Työvoima V1: Mikään sääntö ei sovellu", "Labour V1: No rule matches" + PROGRAM_HOST_2_OR_MORE = ( + "program-host-2-or-more", + "Päävastuullinen väh. 2 aikataulumerkinnässä", + "Main program host in two or more schedule items", + ) + PROGRAM_HOST_1_OVER_6H = ( + "program-host-1-over-6h", + "Päävastuullinen 1 aikataulumerkinnässä, joka kestää väh. 6h", + "Main program host in one schedule item of 6 hours or more", + ) + PROGRAM_HOST_1_AND_HELPER_1 = ( + "program-host-1-and-helper-1", + "Päävastuullinen 1 aikataulumerkinnässä ja ohjelma-apuri toisessa", + "Main program host in one schedule item and helper in another", + ) + PROGRAM_HOST_1 = ( + "program-host-1", + "Päävastuullinen 1 aikataulumerkinnässä", + "Main program host in one schedule item", + ) + PROGRAM_HELPER_2_OR_MORE = ( + "program-helper-2-or-more", + "Ohjelma-apuri väh. 2 aikataulumerkinnässä", + "Program helper in two or more schedule items", + ) + PROGRAM_HELPER_1 = ( + "program-helper-1", + "Ohjelma-apuri 1 aikataulumerkinnässä", + "Program helper in one schedule item", + ) + PROGRAM_NO_MATCH = ( + "program-no-match", + "Ohjelmanjärjestäjä, johon mikään tarkempi sääntö ei sovellu", + "Program host that does not match any more specific rule", + ) + + @classmethod + def as_dimension_dto(cls) -> DimensionDTO: + return DimensionDTO( + slug="perks-rule", + title=dict( + en="Perks rule", + fi="Etusääntö", + ), + # description=dict( + # en="Identifies the rule that was used to determine the perks for this person", + # fi="Kertoo säännön, jota käytettiin tämän henkilön etujen määrittämiseen", + # ), + choices=[ + DimensionValueDTO( + slug=tt.value, + title=dict( + en=tt.title_en, + fi=tt.title_fi, + ), + ) + for tt in cls + if tt != cls.NONE + ], + value_ordering=ValueOrdering.MANUAL, + ) + + def __new__(cls, value: str, title_fi: str, title_en: str): + obj = object.__new__(cls) + obj._value_ = value + obj.title_fi = title_fi + obj.title_en = title_en + return obj + + class Perks(pydantic.BaseModel): badge_type: BadgeType = BadgeType.NONE ticket_type: TicketType = TicketType.NONE meals: int = 0 override_formatted_perks: str = "" + rule: Rule = Rule.NONE @property def formatted_perks(self) -> str: @@ -129,7 +214,7 @@ def formatted_perks(self) -> str: return ", ".join(parts) @classmethod - def for_legacy_signup(cls, involvement: Involvement) -> Perks: + def for_v1_signup(cls, involvement: Involvement) -> Perks: if involvement.type != InvolvementType.LEGACY_SIGNUP: raise ValueError(f"Expected involvement type {InvolvementType.LEGACY_SIGNUP}, got {involvement.type}") if involvement.signup is None: @@ -140,78 +225,115 @@ def for_legacy_signup(cls, involvement: Involvement) -> Perks: ticket_type = TicketType.WEEKEND_TICKET badge_type = BadgeType.ORGANIZER_BADGE meals = 2 + rule = Rule.V1_CONCOM elif "tyovoima" in personnel_classes or "ylivankari" in personnel_classes or "ylityovoima" in personnel_classes: ticket_type = TicketType.WEEKEND_TICKET badge_type = BadgeType.VOLUNTEER_BADGE meals = 2 + rule = Rule.V1_VOLUNTEER else: ticket_type = TicketType.NONE badge_type = BadgeType.NONE meals = 0 + rule = Rule.V1_NO_MATCH return Perks( ticket_type=ticket_type, badge_type=badge_type, meals=meals, override_formatted_perks=involvement.signup.override_formatted_perks, + rule=rule, ) @classmethod - def for_program_host(cls, ticket_type: TicketType, meals: int) -> Perks: + def for_program_host(cls, ticket_type: TicketType, meals: int, rule: Rule) -> Perks: return Perks( badge_type=BadgeType.PROGRAM_BADGE, ticket_type=ticket_type, meals=meals, + rule=rule, ) +def extract_schedule_items_from_involvements(involvements: list[Involvement]) -> list: + schedule_items = [] + for involvement in involvements: + if involvement.program: + schedule_items.extend(involvement.program.schedule_items.all()) + return schedule_items + + class RopeconEmperkelator(BaseEmperkelator): @cached_property def perks(self) -> Perks: if ( v1_signup := next((i for i in self.involvements if i.type == InvolvementType.LEGACY_SIGNUP), None) ) is not None: - return Perks.for_legacy_signup(v1_signup) + return Perks.for_v1_signup(v1_signup) program_involvements = [i for i in self.involvements if i.type == InvolvementType.PROGRAM_HOST] main_involvements = [ i for i in program_involvements if i.response and i.response.survey.slug != "offer-program-hosts" ] + main_schedule_items = extract_schedule_items_from_involvements(main_involvements) + six_hours = timedelta(hours=6) main_program_host_involvements_over6h = [ i for i in main_involvements if i.program and i.program.schedule_items.filter(duration__gte=six_hours).exists() ] + main_program_host_schedule_items_over6h = extract_schedule_items_from_involvements( + main_program_host_involvements_over6h + ) helper_involvements = [ i for i in program_involvements if i.response and i.response.survey.slug == "offer-program-hosts" ] + helper_schedule_items = extract_schedule_items_from_involvements(helper_involvements) # päävastuullinen vähintään 2 aikataulumerkinnässä TAI # päävastuullinen yhdessä aikataulumerkinnässä, joka kestää 6 h tai enemmän TAI # päävastuullinen yhdessä aikataulumerkinnässä ja ohjelma-apuri jossain muussa ohjelmassa -> viikonloppulippu + 1 ruokalippu if ( - len(main_involvements) >= 2 - or len(main_program_host_involvements_over6h) > 0 - or (len(main_involvements) == 1 and len(helper_involvements) > 0) + len(main_schedule_items) >= 2 + or len(main_program_host_schedule_items_over6h) > 0 + or (len(main_schedule_items) == 1 and len(helper_schedule_items) > 0) ): - return Perks.for_program_host(ticket_type=TicketType.WEEKEND_TICKET, meals=1) + return Perks.for_program_host( + ticket_type=TicketType.WEEKEND_TICKET, + meals=1, + rule=Rule.PROGRAM_HOST_2_OR_MORE, + ) # päävastuullinen yhdessä aikataulumerkinnässä -> päivälippu + 1 ruokalippu - if len(main_involvements) == 1: - return Perks.for_program_host(ticket_type=TicketType.DAY_TICKET, meals=1) + if len(main_schedule_items) == 1: + return Perks.for_program_host( + ticket_type=TicketType.DAY_TICKET, + meals=1, + rule=Rule.PROGRAM_HOST_1, + ) # ohjelma-apuri vähintään 2 aikataulumerkinnässä -> viikonloppulippu (ei ruokaa) - if len(helper_involvements) >= 2: - return Perks.for_program_host(ticket_type=TicketType.WEEKEND_TICKET, meals=0) + if len(helper_schedule_items) >= 2: + return Perks.for_program_host( + ticket_type=TicketType.WEEKEND_TICKET, + meals=0, + rule=Rule.PROGRAM_HELPER_2_OR_MORE, + ) # ohjelma-apuri yhdessä aikataulumerkinnässä -> päivälippu (ei ruokaa) - if len(helper_involvements) == 1: - return Perks.for_program_host(ticket_type=TicketType.DAY_TICKET, meals=0) + if len(helper_schedule_items) == 1: + return Perks.for_program_host( + ticket_type=TicketType.DAY_TICKET, + meals=0, + rule=Rule.PROGRAM_HELPER_1, + ) if program_involvements: - raise ValueError( - f"Unexpected program involvements that do not match any perk rules for person {self.person.id}: {program_involvements}" + return Perks.for_program_host( + ticket_type=TicketType.DAY_TICKET, + meals=0, + rule=Rule.PROGRAM_NO_MATCH, ) return Perks() @@ -221,6 +343,7 @@ def get_dimension_dtos(cls, event: Event) -> list[DimensionDTO]: return [ *super().get_dimension_dtos(event), TicketType.as_dimension_dto(), + Rule.as_dimension_dto(), ] @classmethod @@ -250,6 +373,7 @@ def get_dimension_values(self) -> CachedDimensions: if self.perks.badge_type.v1_personnel_class_slug else [], "ticket-type": [self.perks.ticket_type.value] if self.perks.ticket_type != TicketType.NONE else [], + "perks-rule": [self.perks.rule.value] if self.perks.rule != Rule.NONE else [], } def get_annotation_values(self) -> dict[str, str | int | float | bool]: