Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow opt-out of free shirts #4415

Merged
merged 1 commit into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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')
20 changes: 6 additions & 14 deletions uber/configspec.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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]]
Expand Down
40 changes: 36 additions & 4 deletions uber/models/attendee.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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])
Expand All @@ -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):
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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):
Expand All @@ -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):
Expand Down
32 changes: 18 additions & 14 deletions uber/site_sections/staffing.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions uber/templates/staffing/shirt_item.html
Original file line number Diff line number Diff line change
@@ -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 %}
<li>
{{ macros.checklist_image(attendee.shirt_info_marked) }}
{% if not attendee.placeholder %}
Expand All @@ -8,7 +8,7 @@
Let us know
{% endif %}
your shirt size
{% if c.STAFF_EVENT_SHIRT_OPTS and attendee.gets_staff_shirt %}
{% if c.STAFF_EVENT_SHIRT_OPTS and attendee.could_get_staff_shirt %}
and your preference of staff or event shirt
{% endif %}
{% if c.VOLUNTEER_SHIRT_DEADLINE %}
Expand Down
45 changes: 34 additions & 11 deletions uber/templates/staffing/shirt_size.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ <h2> Tell Us Your Shirt Size </h2>

<p>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 %}
<h3>Staff Shirt Policy</h3>
<p>
Event staff are given a themed staff t-shirt that serves as a uniform for the event, and is theirs to keep forever.
Expand Down Expand Up @@ -50,23 +50,31 @@ <h3>Staff Shirt Policy</h3>
<form method="post" action="shirt_size" id="shirt_size" class="form-inline">
<div class="row g-sm-3 row-cols-auto">
{{ csrf_token() }}
{% if c.STAFF_EVENT_SHIRT_OPTS or attendee.num_event_shirts_owed %}
<div class="col mb-3">
<label class="form-text">
Shirt Opt In/Out
</label>
<select class="form-select" name="shirt_opt_out" id="shirt_opt_out_select">
{{ options(attendee.shirt_opt_out_opts, attendee.shirt_opt_out) }}
</select>
</div>
{% if c.STAFF_EVENT_SHIRT_OPTS or attendee.num_potential_free_event_shirts %}
<div class="col mb-3" id="event_shirt">
<label class="form-text">
{% if attendee.gets_staff_shirt and c.STAFF_SHIRT_OPTS != c.SHIRT_OPTS %}Event {% endif %}Shirt Size
{% if attendee.could_get_staff_shirt and c.STAFF_SHIRT_OPTS != c.SHIRT_OPTS %}Event {% endif %}Shirt Size
</label>
<select class="form-select" name="shirt">
<select class="form-select" name="shirt" id="shirt_select">
<option value="{{ c.NO_SHIRT }}">Select a size</option>
{{ options(c.SHIRT_OPTS[1:], attendee.shirt) }}
</select>
</div>
{% endif %}
{% if attendee.gets_staff_shirt and c.STAFF_SHIRT_OPTS != c.SHIRT_OPTS %}
{% if attendee.could_get_staff_shirt and c.STAFF_SHIRT_OPTS != c.SHIRT_OPTS %}
<div class="col mb-3" id="staff_shirt">
<label class="form-text">
Staff Shirt Size
</label>
<select class="form-select" name="staff_shirt">
<select class="form-select" name="staff_shirt" id="staff_shirt_select">
<option value="{{ c.NO_SHIRT }}">Select a size</option>
{{ options(c.STAFF_SHIRT_OPTS[1:], attendee.staff_shirt) }}
</select>
Expand All @@ -78,15 +86,30 @@ <h3>Staff Shirt Policy</h3>
</div>
</div>
<script type="text/javascript">
if($.field('num_event_shirts')) {
showOrHideSizeOpts = function() {
var showOrHideSizeOpts = function() {
if($('#staff_shirt').length) {
$('#staff_shirt').toggle($('input[name="num_event_shirts"]:checked').val() < {{ c.SHIRTS_PER_STAFFER }});
}
$('#event_shirt').toggle($('input[name="num_event_shirts"]:checked').val() > 0 || {{ (c.STAFF_SHIRT_OPTS == c.SHIRT_OPTS)|lower }});
}
showOrHideSizeOpts();
$('input[name="num_event_shirts"]').bind('change',showOrHideSizeOpts);
}

var disableSizeOpts = function() {
let opt_out = $('#shirt_opt_out_select').val();
if($('#staff_shirt').length) {
$('#staff_shirt_select').prop('disabled', ['{{ c.STAFF_OPT_OUT }}', '{{ c.ALL_OPT_OUT }}'].includes(opt_out));
$('#shirt_select').prop('disabled', ['{{ c.EVENT_OPT_OUT }}', '{{ c.ALL_OPT_OUT }}'].includes(opt_out));
} else {
$('#shirt_select').prop('disabled', opt_out != '{{ c.OPT_IN }}');
}
}

$().ready(function () {
disableSizeOpts();
$('#shirt_opt_out_select').on('change', disableSizeOpts);
if($.field('num_event_shirts')) {
showOrHideSizeOpts();
$('input[name="num_event_shirts"]').bind('change',showOrHideSizeOpts);
}
})
</script>
{% endblock %}
Loading