From f50d73f5c144e6d7a7b26eb1461312dbb2e9e5c8 Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Thu, 4 Jan 2024 00:54:38 +0530 Subject: [PATCH] Define an enum for Rsvp, but keep the old LabeledEnum pending StateManager support --- funnel/models/helpers.py | 10 ---------- funnel/models/project.py | 6 +++--- funnel/models/rsvp.py | 26 ++++++++++++++++++++++---- funnel/views/project.py | 10 +++++----- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/funnel/models/helpers.py b/funnel/models/helpers.py index d8c3a458d..549f25a51 100644 --- a/funnel/models/helpers.py +++ b/funnel/models/helpers.py @@ -34,7 +34,6 @@ 'PASSWORD_MIN_LENGTH', 'PASSWORD_MAX_LENGTH', 'IntTitle', - 'IntNameTitle', 'check_password_strength', 'profanity', 'add_to_class', @@ -144,15 +143,6 @@ class IntTitle(DataclassFromType, int): title: str = '' -@dataclass(frozen=True) -class IntNameTitle(DataclassFromType, int): - """Integer value with a name and title (for enums).""" - - # The empty default is required for Mypy's enum plugin's `Enum.__call__` analysis - name: str = '' - title: str = '' - - @dataclass class PasswordCheckType: """ diff --git a/funnel/models/project.py b/funnel/models/project.py index a5008db7e..5067b2694 100644 --- a/funnel/models/project.py +++ b/funnel/models/project.py @@ -920,10 +920,10 @@ def rsvp_for( def rsvp_for(self, account: Account | None, create=False) -> Rsvp | None: return Rsvp.get_for(self, account, create) - def rsvps_with(self, status: str): + def rsvps_with(self, state: RsvpStateEnum) -> Query[Rsvp]: return self.rsvps.join(Account).filter( Account.state.ACTIVE, - Rsvp._state == status, # pylint: disable=protected-access + Rsvp._state == state, # pylint: disable=protected-access ) def rsvp_counts(self) -> dict[str, int]: @@ -1561,7 +1561,7 @@ def __repr__(self) -> str: from .label import Label from .project_membership import ProjectMembership from .proposal import Proposal -from .rsvp import Rsvp +from .rsvp import Rsvp, RsvpStateEnum from .session import Session from .sponsor_membership import ProjectSponsorMembership from .update import Update diff --git a/funnel/models/rsvp.py b/funnel/models/rsvp.py index 00bbc11f7..e8a9394e9 100644 --- a/funnel/models/rsvp.py +++ b/funnel/models/rsvp.py @@ -2,13 +2,15 @@ from __future__ import annotations +from dataclasses import dataclass +from enum import ReprEnum from typing import TYPE_CHECKING, Any, Literal, Self, overload from flask import current_app from baseframe import __ from coaster.sqlalchemy import StateManager, with_roles -from coaster.utils import LabeledEnum +from coaster.utils import DataclassFromType, LabeledEnum from . import ( Mapped, @@ -26,7 +28,7 @@ from .project import Project from .project_membership import project_child_role_map -__all__ = ['Rsvp', 'RSVP_STATUS'] +__all__ = ['RSVP_STATUS', 'RsvpStateEnum', 'Rsvp'] class RSVP_STATUS(LabeledEnum): # noqa: N801 @@ -38,6 +40,22 @@ class RSVP_STATUS(LabeledEnum): # noqa: N801 AWAITING = ('A', 'awaiting', __("Awaiting")) +@dataclass(frozen=True) +class _RsvpOptions(DataclassFromType, str): + """RSVP options.""" + + # The empty default is required for Mypy's enum plugin's `Enum.__call__` analysis + response: str = '' + label: str = '' + + +class RsvpStateEnum(_RsvpOptions, ReprEnum): + YES = 'Y', __("Yes"), __("Going") + NO = 'N', __("No"), __("Not going") + MAYBE = 'M', __("Maybe"), __("Maybe") + AWAITING = 'A', __("Invite"), __("Awaiting") + + class Rsvp(UuidMixin, NoIdMixin, Model): __tablename__ = 'rsvp' project_id: Mapped[int] = sa_orm.mapped_column( @@ -68,8 +86,8 @@ class Rsvp(UuidMixin, NoIdMixin, Model): _state: Mapped[str] = sa_orm.mapped_column( 'state', sa.CHAR(1), - StateManager.check_constraint('state', RSVP_STATUS, sa.CHAR(1)), - default=RSVP_STATUS.AWAITING, + StateManager.check_constraint('state', RsvpStateEnum, sa.CHAR(1)), + default=RsvpStateEnum.AWAITING, nullable=False, ) state = with_roles( diff --git a/funnel/views/project.py b/funnel/views/project.py index 05d77af6c..5a41ec5c2 100644 --- a/funnel/views/project.py +++ b/funnel/views/project.py @@ -30,13 +30,13 @@ ProjectTransitionForm, ) from ..models import ( - RSVP_STATUS, Account, Project, ProjectRsvpStateEnum, RegistrationCancellationNotification, RegistrationConfirmationNotification, Rsvp, + RsvpStateEnum, SavedProject, db, sa, @@ -744,7 +744,7 @@ def rsvp_list(self) -> ReturnRenderWith: 'project': self.obj.current_access(datasets=('primary', 'related')), 'going_rsvps': [ _r.current_access(datasets=('without_parent', 'related', 'related')) - for _r in self.obj.rsvps_with(RSVP_STATUS.YES) + for _r in self.obj.rsvps_with(RsvpStateEnum.YES) ], 'rsvp_form_fields': [ field.get('name', '') @@ -755,7 +755,7 @@ def rsvp_list(self) -> ReturnRenderWith: else None, } - def get_rsvp_state_csv(self, state): + def get_rsvp_state_csv(self, state: RsvpStateEnum) -> Response: """Export participant list as a CSV.""" outfile = io.StringIO(newline='') out = csv.writer(outfile) @@ -789,14 +789,14 @@ def get_rsvp_state_csv(self, state): @requires_roles({'promoter'}) def rsvp_list_yes_csv(self) -> ReturnView: """Return a CSV of RSVP participants who answered Yes.""" - return self.get_rsvp_state_csv(state=RSVP_STATUS.YES) + return self.get_rsvp_state_csv(RsvpStateEnum.YES) @route('rsvp_list/maybe.csv') @requires_login @requires_roles({'promoter'}) def rsvp_list_maybe_csv(self) -> ReturnView: """Return a CSV of RSVP participants who answered Maybe.""" - return self.get_rsvp_state_csv(state=RSVP_STATUS.MAYBE) + return self.get_rsvp_state_csv(RsvpStateEnum.MAYBE) @route('save', methods=['POST']) @requires_login