diff --git a/alembic/versions/128e7228f182_add_ability_to_opt_out_of_free_shirts.py b/alembic/versions/128e7228f182_add_ability_to_opt_out_of_free_shirts.py new file mode 100644 index 000000000..de3f60de6 --- /dev/null +++ b/alembic/versions/128e7228f182_add_ability_to_opt_out_of_free_shirts.py @@ -0,0 +1,59 @@ +"""Add ability to opt out of free shirts + +Revision ID: 128e7228f182 +Revises: 2e4feb6c2d18 +Create Date: 2024-10-15 19:55:59.416212 + +""" + + +# revision identifiers, used by Alembic. +revision = '128e7228f182' +down_revision = '2e4feb6c2d18' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + + +try: + is_sqlite = op.get_context().dialect.name == 'sqlite' +except Exception: + is_sqlite = False + +if is_sqlite: + op.get_context().connection.execute('PRAGMA foreign_keys=ON;') + utcnow_server_default = "(datetime('now', 'utc'))" +else: + utcnow_server_default = "timezone('utc', current_timestamp)" + +def sqlite_column_reflect_listener(inspector, table, column_info): + """Adds parenthesis around SQLite datetime defaults for utcnow.""" + if column_info['default'] == "datetime('now', 'utc')": + column_info['default'] = utcnow_server_default + +sqlite_reflect_kwargs = { + 'listeners': [('column_reflect', sqlite_column_reflect_listener)] +} + +# =========================================================================== +# HOWTO: Handle alter statements in SQLite +# +# def upgrade(): +# if is_sqlite: +# with op.batch_alter_table('table_name', reflect_kwargs=sqlite_reflect_kwargs) as batch_op: +# batch_op.alter_column('column_name', type_=sa.Unicode(), server_default='', nullable=False) +# else: +# op.alter_column('table_name', 'column_name', type_=sa.Unicode(), server_default='', nullable=False) +# +# =========================================================================== + + +def upgrade(): + op.add_column('attendee', sa.Column('shirt_opt_out', sa.Integer(), server_default='227291107', nullable=False)) + + +def downgrade(): + op.drop_column('attendee', 'shirt_opt_out') diff --git a/uber/configspec.ini b/uber/configspec.ini index bf3045460..f35e90827 100644 --- a/uber/configspec.ini +++ b/uber/configspec.ini @@ -1562,6 +1562,12 @@ rated_great = string(default="Staffer went above and beyond") default_wristband = string(default="red") __many__ = string +[[shirt_opt_out]] +opt_in = string(default="Opt out of receiving a shirt?") +staff_opt_out = string(default="I would like to opt out of receiving my staff shirt.") +event_opt_out = string(default="I would like to opt out of receiving my free shirt for volunteering.") +all_opt_out = string(default="I would like to opt out of ANY free shirts.") + [[tracking]] created = string(default="created") updated = string(default="updated") @@ -1582,20 +1588,6 @@ full = string(default="All Info") [[food_restriction]] vegan = string(default="Vegan") -[[hotel_room_type]] -room_double = string(default="Double Room") -room_king = string(default="King Room") - -[[suite_room_type]] -suite_big = string(default="Big Suite") -suite_medium = string(default="Medium Suite") -suite_tiny = string(default="Tiny Suite") - -[[hotel_priorities]] -hotel_room_type = string(default="Hotel Room Type") -hotel_selection = string(default="Hotel Selection") -hotel_dates = string(default="Check-In and Check-Out Dates") - [[sandwich]] [[dealer_status]] diff --git a/uber/models/attendee.py b/uber/models/attendee.py index 377c92569..4d3f9c407 100644 --- a/uber/models/attendee.py +++ b/uber/models/attendee.py @@ -230,6 +230,7 @@ class Attendee(MagModel, TakesPaymentMixin): shirt = Column(Choice(c.SHIRT_OPTS), default=c.NO_SHIRT) staff_shirt = Column(Choice(c.STAFF_SHIRT_OPTS), default=c.NO_SHIRT) num_event_shirts = Column(Choice(c.STAFF_EVENT_SHIRT_OPTS, allow_unspecified=True), default=-1) + shirt_opt_out = Column(Choice(c.SHIRT_OPT_OUT_OPTS), default=c.OPT_IN) can_spam = Column(Boolean, default=False) regdesk_info = Column(UnicodeText, admin_only=True) extra_merch = Column(UnicodeText, admin_only=True) @@ -1225,7 +1226,8 @@ def has_extras(self): @property def shirt_size_marked(self): if c.STAFF_SHIRT_OPTS == c.SHIRT_OPTS: - return self.shirt not in [c.NO_SHIRT, c.SIZE_UNKNOWN] + return self.shirt not in [c.NO_SHIRT, c.SIZE_UNKNOWN] or ( + not self.gets_staff_shirt and not self.num_event_shirts_owed) else: return (not self.num_event_shirts_owed or self.shirt not in [c.NO_SHIRT, c.SIZE_UNKNOWN]) and ( not self.gets_staff_shirt or self.staff_shirt not in [c.NO_SHIRT, c.SIZE_UNKNOWN]) @@ -1236,6 +1238,7 @@ def shirt_info_marked(self): return (self.num_event_shirts != -1 or not c.STAFF_EVENT_SHIRT_OPTS) and self.shirt_size_marked elif self.volunteer_event_shirt_eligible: return self.shirt_size_marked + return self.shirt_opt_out != c.OPT_IN @property def is_group_leader(self): @@ -1401,9 +1404,9 @@ def gets_swadge(self): @property def paid_for_a_shirt(self): return self.amount_extra >= c.SHIRT_LEVEL - + @property - def num_free_event_shirts(self): + def num_potential_free_event_shirts(self): """ If someone is staff-shirt-eligible, we use the number of event shirts they have selected (if any). Volunteers also get a free event shirt. Staff get an event shirt if staff shirts are turned off for the event. @@ -1412,6 +1415,12 @@ def num_free_event_shirts(self): return max(0, self.num_event_shirts) if self.gets_staff_shirt else bool( self.volunteer_event_shirt_eligible or (self.badge_type == c.STAFF_BADGE and c.HOURS_FOR_SHIRT)) + @property + def num_free_event_shirts(self): + if self.shirt_opt_out in [c.EVENT_OPT_OUT, c.ALL_OPT_OUT]: + return 0 + return self.num_potential_free_event_shirts + @property def volunteer_event_shirt_eligible(self): return bool(c.VOLUNTEER_RIBBON in self.ribbon_ints and c.HOURS_FOR_SHIRT) @@ -1426,10 +1435,16 @@ def num_event_shirts_owed(self): int(self.paid_for_a_shirt), self.num_free_event_shirts ]) + + @property + def could_get_staff_shirt(self): + return bool(self.badge_type == c.STAFF_BADGE and c.SHIRTS_PER_STAFFER > 0) @property def gets_staff_shirt(self): - return bool(self.badge_type == c.STAFF_BADGE and c.SHIRTS_PER_STAFFER > 0) + if self.shirt_opt_out in [c.STAFF_OPT_OUT, c.ALL_OPT_OUT]: + return False + return self.could_get_staff_shirt @property def num_staff_shirts_owed(self): @@ -1438,6 +1453,23 @@ def num_staff_shirts_owed(self): @property def gets_any_kind_of_shirt(self): return self.gets_staff_shirt or self.num_event_shirts_owed > 0 + + @property + def shirt_opt_out_opts(self): + opt_list = [] + for key, val in c.SHIRT_OPT_OUT_OPTS: + if key == c.STAFF_OPT_OUT and not self.could_get_staff_shirt: + continue + elif key == c.EVENT_OPT_OUT and self.num_potential_free_event_shirts == 0: + continue + elif key == c.ALL_OPT_OUT and (not self.could_get_staff_shirt or not c.STAFF_EVENT_SHIRT_OPTS): + continue + else: + if self.shirt_opt_out != c.OPT_IN and key == c.OPT_IN: + # Changes the text to make the form a little clearer + val = f"I would like to receive my {'staff' if self.could_get_staff_shirt else 'free'} shirt!" + opt_list.append((key, val)) + return opt_list @property def has_personalized_badge(self): diff --git a/uber/site_sections/staffing.py b/uber/site_sections/staffing.py index 42b582518..79e1af76e 100644 --- a/uber/site_sections/staffing.py +++ b/uber/site_sections/staffing.py @@ -1,11 +1,13 @@ import cherrypy from datetime import datetime, timedelta +from pockets.autolog import log import ics from uber.config import c from uber.custom_tags import safe_string from uber.decorators import ajax, ajax_gettable, all_renderable, check_shutdown, csrf_protected, render, public from uber.errors import HTTPRedirect +from uber.models import Attendee from uber.utils import check_csrf, create_valid_user_supplied_redirect_url, ensure_csrf_token_exists, localized_now @@ -42,21 +44,23 @@ def food_restrictions(self, session, message='', **params): } @check_shutdown - def shirt_size(self, session, message='', shirt=None, staff_shirt=None, num_event_shirts=None, csrf_token=None): + def shirt_size(self, session, message='', **params): attendee = session.logged_in_volunteer() - if shirt is not None or staff_shirt is not None: - check_csrf(csrf_token) - if (shirt and not int(shirt)) or (attendee.gets_staff_shirt and - c.STAFF_SHIRT_OPTS != c.SHIRT_OPTS and not int(staff_shirt)): - message = 'You must select a shirt size' - else: - if shirt: - attendee.shirt = int(shirt) - if staff_shirt: - attendee.staff_shirt = int(staff_shirt) - if c.STAFF_EVENT_SHIRT_OPTS and c.BEFORE_VOLUNTEER_SHIRT_DEADLINE and num_event_shirts: - attendee.num_event_shirts = int(num_event_shirts) - raise HTTPRedirect('index?message={}', 'Shirt info uploaded') + if cherrypy.request.method == "POST": + check_csrf(params.get('csrf_token')) + test_attendee = Attendee(**attendee.to_dict()) + test_attendee.apply(params) + + if c.STAFF_EVENT_SHIRT_OPTS and test_attendee.gets_staff_shirt and test_attendee.num_event_shirts == -1: + message = "Please indicate your preference for shirt type." + elif not test_attendee.shirt_size_marked: + message = "Please select a shirt size." + + if not message: + for attr in ['shirt', 'staff_shirt', 'num_event_shirts', 'shirt_opt_out']: + if params.get(attr): + setattr(attendee, attr, int(params.get(attr))) + raise HTTPRedirect('index?message={}', 'Shirt info uploaded.') return { 'message': message, diff --git a/uber/templates/staffing/shirt_item.html b/uber/templates/staffing/shirt_item.html index cb75db8bb..978f15ed6 100644 --- a/uber/templates/staffing/shirt_item.html +++ b/uber/templates/staffing/shirt_item.html @@ -1,5 +1,5 @@ {% import 'macros.html' as macros %} -{% if (c.HOURS_FOR_SHIRT and attendee.gets_any_kind_of_shirt or attendee.gets_staff_shirt) and c.PRE_CON %} +{% if c.PRE_CON and (c.HOURS_FOR_SHIRT and attendee.num_potential_free_event_shirts) or attendee.could_get_staff_shirt %}
One of the perks of volunteering is a t-shirt for anyone {% if c.HOURS_FOR_SHIRT and attendee.volunteer_event_shirt_eligible %}who takes at least {{ c.HOURS_FOR_SHIRT }} weighted hours worth of shifts{% else %} on staff{% endif %}. -{% if attendee.gets_staff_shirt %} +{% if attendee.could_get_staff_shirt %}
Event staff are given a themed staff t-shirt that serves as a uniform for the event, and is theirs to keep forever. @@ -50,23 +50,31 @@