From f056e313f683a4fb42aec81f04c7363a2b8e8283 Mon Sep 17 00:00:00 2001
From: Victoria Earl
Date: Fri, 11 Oct 2024 15:14:43 -0400
Subject: [PATCH] Update Rock Island for Super 2025 Fixes
https://magfest.atlassian.net/browse/MAGDEV-1313.
---
...4feb6c2d18_update_rock_island_checklist.py | 85 ++++++++++++++++
uber/automated_emails.py | 40 +++++---
uber/configspec.ini | 13 ++-
uber/model_checks.py | 20 ++++
uber/models/guests.py | 24 ++++-
uber/models/tracking.py | 3 +-
uber/site_sections/guest_reports.py | 71 ++++++++++++-
uber/site_sections/guests.py | 29 +++++-
uber/tasks/groups.py | 24 ++++-
.../daily_checks/ri_inventory_updates.html | 15 +++
.../emails/guests/rock_island_intro.txt | 8 ++
.../guests/rock_island_inventory_reminder.txt | 8 ++
.../band_autograph_deadline.html | 2 +-
.../guest_checklist/merch_deadline.html | 84 ++++++++++++++--
uber/templates/guest_reports/rock_island.html | 63 +++++++++---
uber/templates/guests_macros.html | 8 +-
uber/templates/macros.html | 99 +++++++++----------
17 files changed, 496 insertions(+), 100 deletions(-)
create mode 100644 alembic/versions/2e4feb6c2d18_update_rock_island_checklist.py
create mode 100644 uber/templates/emails/daily_checks/ri_inventory_updates.html
create mode 100644 uber/templates/emails/guests/rock_island_intro.txt
create mode 100644 uber/templates/emails/guests/rock_island_inventory_reminder.txt
diff --git a/alembic/versions/2e4feb6c2d18_update_rock_island_checklist.py b/alembic/versions/2e4feb6c2d18_update_rock_island_checklist.py
new file mode 100644
index 000000000..bef01a955
--- /dev/null
+++ b/alembic/versions/2e4feb6c2d18_update_rock_island_checklist.py
@@ -0,0 +1,85 @@
+"""Update rock island checklist
+
+Revision ID: 2e4feb6c2d18
+Revises: 9b657ae4c4ac
+Create Date: 2024-10-11 11:40:31.124938
+
+"""
+
+
+# revision identifiers, used by Alembic.
+revision = '2e4feb6c2d18'
+down_revision = '9b657ae4c4ac'
+branch_labels = None
+depends_on = None
+
+from alembic import op
+import sqlalchemy as sa
+import residue
+
+
+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('guest_merch', sa.Column('inventory_updated', residue.UTCDateTime(), nullable=True))
+ op.add_column('guest_merch', sa.Column('delivery_method', sa.Integer(), nullable=True))
+ op.add_column('guest_merch', sa.Column('payout_method', sa.Integer(), nullable=True))
+ op.add_column('guest_merch', sa.Column('paypal_email', sa.Unicode(), server_default='', nullable=False))
+ op.add_column('guest_merch', sa.Column('check_payable', sa.Unicode(), server_default='', nullable=False))
+ op.add_column('guest_merch', sa.Column('check_zip_code', sa.Unicode(), server_default='', nullable=False))
+ op.add_column('guest_merch', sa.Column('check_address1', sa.Unicode(), server_default='', nullable=False))
+ op.add_column('guest_merch', sa.Column('check_address2', sa.Unicode(), server_default='', nullable=False))
+ op.add_column('guest_merch', sa.Column('check_city', sa.Unicode(), server_default='', nullable=False))
+ op.add_column('guest_merch', sa.Column('check_region', sa.Unicode(), server_default='', nullable=False))
+ op.add_column('guest_merch', sa.Column('check_country', sa.Unicode(), server_default='', nullable=False))
+ op.add_column('guest_merch', sa.Column('arrival_plans', sa.Unicode(), server_default='', nullable=False))
+ op.add_column('guest_merch', sa.Column('merch_events', sa.Unicode(), server_default='', nullable=False))
+ op.drop_column('guest_merch', 'bringing_boxes')
+
+
+def downgrade():
+ op.add_column('guest_merch', sa.Column('bringing_boxes', sa.VARCHAR(), server_default=sa.text("''::character varying"), autoincrement=False, nullable=False))
+ op.drop_column('guest_merch', 'merch_events')
+ op.drop_column('guest_merch', 'arrival_plans')
+ op.drop_column('guest_merch', 'check_country')
+ op.drop_column('guest_merch', 'check_region')
+ op.drop_column('guest_merch', 'check_city')
+ op.drop_column('guest_merch', 'check_address2')
+ op.drop_column('guest_merch', 'check_address1')
+ op.drop_column('guest_merch', 'check_zip_code')
+ op.drop_column('guest_merch', 'check_payable')
+ op.drop_column('guest_merch', 'paypal_email')
+ op.drop_column('guest_merch', 'payout_method')
+ op.drop_column('guest_merch', 'delivery_method')
+ op.drop_column('guest_merch', 'inventory_updated')
diff --git a/uber/automated_emails.py b/uber/automated_emails.py
index b5768b05b..ce37ab896 100644
--- a/uber/automated_emails.py
+++ b/uber/automated_emails.py
@@ -577,7 +577,7 @@ def generic_placeholder(a): return a.placeholder and (not deferred_attendee_plac
Attendee,
'Claim your deferred badge for {EVENT_NAME} {EVENT_YEAR}!',
'placeholders/deferred.html',
- deferred_attendee_placeholder,
+ deferred_attendee_placeholder,
when=after(c.PREREG_OPEN),
ident='claim_deferred_badge')
@@ -864,13 +864,14 @@ def __init__(self, conf):
when=days_before(7, c.FINAL_EMAIL_DEADLINE),
ident='hotel_requirements_reminder_last_chance')
- AutomatedEmailFixture(
- Room,
- '{EVENT_NAME} Hotel Room Assignment',
- 'hotel/room_assignment.txt',
- lambda r: r.locked_in,
- sender=c.ROOM_EMAIL_SENDER,
- ident='hotel_room_assignment')
+ if not c.HOTEL_REQUESTS_URL:
+ AutomatedEmailFixture(
+ Room,
+ '{EVENT_NAME} Hotel Room Assignment',
+ 'hotel/room_assignment.txt',
+ lambda r: r.locked_in,
+ sender=c.ROOM_EMAIL_SENDER,
+ ident='hotel_room_assignment')
# =============================
@@ -1264,7 +1265,6 @@ def __init__(self, subject, template, filter, ident, **kwargs):
if c.PANELS_ENABLED:
-
PanelAppEmailFixture(
'Your {EVENT_NAME} Panel Application Has Been Received: {{ app.name }}',
'panels/application.html',
@@ -1344,11 +1344,10 @@ def __init__(self, subject, template, filter, ident, **kwargs):
**kwargs)
-AutomatedEmailFixture(
- GuestGroup,
+BandEmailFixture(
'{EVENT_NAME} Performer Checklist',
'guests/band_notification.txt',
- lambda b: b.group_type == c.BAND, sender=c.BAND_EMAIL,
+ lambda b: True,
ident='band_checklist_inquiry')
BandEmailFixture(
@@ -1468,3 +1467,20 @@ def __init__(self, subject, template, filter, ident, **kwargs):
lambda g: not g.checklist_completed,
when=days_after(7, c.GUEST_INFO_DEADLINE),
ident='guest_reminder_2')
+
+AutomatedEmailFixture(
+ GuestGroup,
+ f'Sign up to sell merch at {c.EVENT_NAME} Rock Island',
+ 'guests/rock_island_intro.txt',
+ lambda g: g.group_type in c.ROCK_ISLAND_GROUPS and g.deadline_from_model('merch') and not g.group_type == c.BAND,
+ ident='rock_island_intro',
+ sender=c.ROCK_ISLAND_EMAIL)
+
+AutomatedEmailFixture(
+ GuestGroup,
+ f'Last chance to finalize your {c.EVENT_NAME} Rock Island Inventory',
+ 'guests/rock_island_inventory_reminder.txt',
+ lambda g: g.group_type in c.ROCK_ISLAND_GROUPS and g.merch and g.merch.selling_merch == c.ROCK_ISLAND,
+ ident='ri_inventory_reminder',
+ when=days_before(7, c.ROCK_ISLAND_DEADLINE),
+ sender=c.ROCK_ISLAND_EMAIL)
\ No newline at end of file
diff --git a/uber/configspec.ini b/uber/configspec.ini
index 505d5b1cb..bf3045460 100644
--- a/uber/configspec.ini
+++ b/uber/configspec.ini
@@ -1899,13 +1899,24 @@ no = string(default="No, we definitely will not need rehearsal space and we are
maybe = string(default="We might need rehearsal space; please contact us about our needs.")
yes = string(default="We definitely need rehearsal space; please contact us about our needs.")
+[[guest_merch_delivery]]
+shipping = string(default="Shipping to the warehouse")
+in_person = string(default="Bringing in person")
+
+[[guest_merch_payout_method]]
+paypal = string(default="PayPal")
+check = string(default="Check")
+other = string(default="Other")
+
[[merch_types]]
-cd = string(default="Album")
+cd = string(default="Music")
tshirt = string(default="T-Shirt")
apparel = string(default="Other Apparel")
pin = string(default="Pin")
sticker = string(default="Sticker")
poster = string(default="Poster")
+button = string(default="Button")
+patch = string(default="Patch")
miscellaneous = string(default="Miscellaneous")
[[album_media]]
diff --git a/uber/model_checks.py b/uber/model_checks.py
index 33a585771..a28f20239 100644
--- a/uber/model_checks.py
+++ b/uber/model_checks.py
@@ -769,6 +769,26 @@ def is_merch_checklist_complete(guest_merch):
and guest_merch.poc_region
and guest_merch.poc_country):
return 'You must tell us your complete mailing address'
+
+ elif not guest_merch.delivery_method:
+ return 'Please tell us how you will bring us your inventory'
+ elif not guest_merch.payout_method:
+ return 'Please tell us how you would like to be paid for your merch'
+ elif guest_merch.payout_method == c.PAYPAL and not guest_merch.paypal_email:
+ return 'We need your PayPal email address to pay you via PayPal'
+ elif guest_merch.payout_method == c.CHECK:
+ if not guest_merch.check_payable:
+ return 'Please include the name that should go on your check'
+ if not (
+ guest_merch.check_zip_code
+ and guest_merch.check_address1
+ and guest_merch.check_city
+ and guest_merch.check_region
+ and guest_merch.check_country
+ ):
+ return 'Please include the mailing address to send a check to.'
+ elif not guest_merch.arrival_plans:
+ return 'Please tell us your estimated arrival to Rock Island to check in your inventory'
@validation.GuestTravelPlans
diff --git a/uber/models/guests.py b/uber/models/guests.py
index a80c5a378..bbb07702d 100644
--- a/uber/models/guests.py
+++ b/uber/models/guests.py
@@ -3,7 +3,7 @@
import shutil
import uuid
from collections import defaultdict
-from datetime import timedelta
+from datetime import datetime, timedelta
from pockets import uniquify, classproperty
from residue import JSON, CoerceUTF8 as UnicodeText, UTCDateTime, UUID
@@ -343,8 +343,21 @@ class GuestMerch(MagModel):
guest_id = Column(UUID, ForeignKey('guest_group.id'), unique=True)
selling_merch = Column(Choice(c.GUEST_MERCH_OPTS), nullable=True)
+ delivery_method = Column(Choice(c.GUEST_MERCH_DELIVERY_OPTS), nullable=True)
+ payout_method = Column(Choice(c.GUEST_MERCH_PAYOUT_METHOD_OPTS), nullable=True)
+ paypal_email = Column(UnicodeText)
+ check_payable = Column(UnicodeText)
+ check_zip_code = Column(UnicodeText)
+ check_address1 = Column(UnicodeText)
+ check_address2 = Column(UnicodeText)
+ check_city = Column(UnicodeText)
+ check_region = Column(UnicodeText)
+ check_country = Column(UnicodeText)
+
+ arrival_plans = Column(UnicodeText)
+ merch_events = Column(UnicodeText)
inventory = Column(JSON, default={}, server_default='{}')
- bringing_boxes = Column(UnicodeText)
+ inventory_updated = Column(UTCDateTime, nullable=True)
extra_info = Column(UnicodeText)
tax_phone = Column(UnicodeText)
@@ -402,6 +415,10 @@ def rock_island_url(self):
@property
def rock_island_csv_url(self):
return '../guest_reports/rock_island_csv?id={}'.format(self.guest_id)
+
+ @property
+ def rock_island_square_export_url(self):
+ return f'../guest_reports/rock_island_square_xlsx?id={self.guest_id}'
@property
def status(self):
@@ -592,6 +609,7 @@ def remove_inventory_item(self, item_id, *, persist_files=True):
if persist_files:
self._prune_inventory_file(item, inventory, prune_missing=True)
self.inventory = inventory
+ self.inventory_updated = datetime.now()
return item
def set_inventory(self, inventory, *, persist_files=True):
@@ -599,12 +617,14 @@ def set_inventory(self, inventory, *, persist_files=True):
self._save_inventory_files(inventory)
self._prune_inventory_files(inventory, prune_missing=True)
self.inventory = inventory
+ self.inventory_updated = datetime.now()
def update_inventory(self, inventory, *, persist_files=True):
if persist_files:
self._save_inventory_files(inventory)
self._prune_inventory_files(inventory, prune_missing=False)
self.inventory = dict(self.inventory, **inventory)
+ self.inventory_updated = datetime.now()
class GuestCharity(MagModel):
diff --git a/uber/models/tracking.py b/uber/models/tracking.py
index 8c5bc29f7..e94273f2f 100644
--- a/uber/models/tracking.py
+++ b/uber/models/tracking.py
@@ -129,7 +129,8 @@ def repr(cls, column, value):
def differences(cls, instance):
diff = {}
for attr, column in instance.__table__.columns.items():
- if attr in ['currently_sending', 'last_send_time', 'unapproved_count', 'last_updated', 'last_synced']:
+ if attr in ['currently_sending', 'last_send_time',
+ 'unapproved_count', 'last_updated', 'last_synced', 'inventory_updated']:
continue
new_val = getattr(instance, attr)
diff --git a/uber/site_sections/guest_reports.py b/uber/site_sections/guest_reports.py
index 6f090df06..fa8f2cc5a 100644
--- a/uber/site_sections/guest_reports.py
+++ b/uber/site_sections/guest_reports.py
@@ -3,7 +3,7 @@
from uber.config import c
from uber.custom_tags import time_day_local
-from uber.decorators import all_renderable, csv_file, site_mappable
+from uber.decorators import all_renderable, csv_file, site_mappable, xlsx_file
from uber.errors import HTTPRedirect
from uber.models import Group, GuestAutograph, GuestGroup, GuestMerch, GuestTravelPlans
from uber.utils import convert_to_absolute_url
@@ -101,6 +101,73 @@ def rock_island(self, session, message='', only_empty=None, id=None, **params):
'guest_groups': [guest for guest in guest_groups if session.admin_can_see_guest_group(guest)],
'only_empty': only_empty
}
+
+ @site_mappable(download=True)
+ @xlsx_file
+ def rock_island_square_xlsx(self, out, session, id=None, **params):
+ header_row = [
+ 'Token', 'Item Name', 'Variation Name', 'Unit and Precision', 'SKU', 'Description', 'Category',
+ 'SEO Title', 'SEO Description', 'Permalink', 'Square Online Item Visibility', 'Weight (lb)', 'Shipping Enabled',
+ 'Self-serve Ordering Enabled', 'Delivery Enabled', 'Pickup Enabled', 'Price', 'Sellable', 'Stockable',
+ 'Skip Detail Screen in POS', 'Option Name 1', 'Option Value 1', 'Current Quantity MAGFest Rock Island',
+ 'New Quantity MAGFest Rock Island'
+ ]
+
+ query = session.query(GuestGroup).options(
+ subqueryload(GuestGroup.group)).options(
+ subqueryload(GuestGroup.merch))
+
+ if id:
+ guest_groups = [query.get(id)]
+ else:
+ guest_groups = query.filter(
+ GuestGroup.id == GuestMerch.guest_id,
+ GuestMerch.selling_merch == c.ROCK_ISLAND,
+ GuestGroup.group_id == Group.id).order_by(
+ Group.name).all()
+
+ rows = []
+ item_type_square_name = {
+ c.CD: "MUSIC",
+ c.TSHIRT: "APPAREL",
+ c.APPAREL: "APPAREL",
+ c.PIN: "PIN",
+ c.STICKER: "STICKER",
+ c.POSTER: "POSTER",
+ c.BUTTON: "BUTTON",
+ c.PATCH: "PATCH",
+ c.MISCELLANEOUS: "MISC",
+ }
+
+ def _inventory_sort_key(item):
+ return ' '.join([
+ c.MERCH_TYPES[int(item['type'])],
+ item['name']
+ ])
+
+ def _generate_row(item, guest, variation_name='Regular'):
+ item_type = int(item['type'])
+ item_name = f'{item_type_square_name[item_type]} {guest.group.name} {item['name']}'
+ if item_type == c.CD:
+ item_name = f'{item_name} {c.ALBUM_MEDIAS[int(item['media'])]}'
+ elif item_type == c.TSHIRT:
+ item_name = f'{item_name} T-shirt'
+
+ return [
+ '', item_name, variation_name, '', '', '', guest.group.name, '', '', '',
+ 'hidden', '', 'N', '', 'N', 'N', '{:.2f}'.format(float(item['price'])),
+ '', '', 'N', '', '', '', ''
+ ]
+
+ for guest in guest_groups:
+ for item in sorted(guest.merch.inventory.values(), key=_inventory_sort_key):
+ merch_type = int(item['type'])
+ if merch_type in (c.TSHIRT, c.APPAREL):
+ for line_item in guest.merch.line_items(item):
+ rows.append(_generate_row(item, guest, guest.merch.line_item_to_string(item, line_item)))
+ else:
+ rows.append(_generate_row(item, guest))
+ out.writerows(header_row, rows)
@site_mappable(download=True)
@csv_file
@@ -127,7 +194,7 @@ def _inventory_sort_key(item):
item['price']
])
- for guest in [guest for guest in guest_groups if session.admin_can_see_guest_group(guest)]:
+ for guest in guest_groups:
for item in sorted(guest.merch.inventory.values(), key=_inventory_sort_key):
merch_type = int(item['type'])
if merch_type in (c.TSHIRT, c.APPAREL):
diff --git a/uber/site_sections/guests.py b/uber/site_sections/guests.py
index 13471da1f..faf798606 100644
--- a/uber/site_sections/guests.py
+++ b/uber/site_sections/guests.py
@@ -189,11 +189,22 @@ def merch(self, session, guest_id, message='', coverage=False, warning=False, **
guest = session.guest_group(guest_id)
guest_merch = session.guest_merch(params, checkgroups=GuestMerch.all_checkgroups, bools=GuestMerch.all_bools)
guest_merch.handlers = guest_merch.extract_handlers(params)
+
+ autograph_params = params.copy()
+ autograph_params.pop('id', None)
+ if guest.autograph:
+ autograph_params['id'] = guest.autograph.id
+ guest_autograph = session.guest_autograph(autograph_params)
+
group_params = dict()
+
if cherrypy.request.method == 'POST':
message = check(guest_merch)
+
if not message:
- if c.REQUIRE_DEDICATED_GUEST_TABLE_PRESENCE \
+ if not guest.deadline_from_model('autograph') and params.get('rock_island_autographs', '') == '':
+ message = 'Please select whether you would like to have a Meet N Greet at Rock Island.'
+ elif c.REQUIRE_DEDICATED_GUEST_TABLE_PRESENCE \
and guest_merch.selling_merch == c.OWN_TABLE \
and guest.group_type == c.BAND \
and not all([coverage, warning]):
@@ -214,6 +225,17 @@ def merch(self, session, guest_id, message='', coverage=False, warning=False, **
else:
guest.group.apply(group_params, restricted=True)
if not message:
+ if not guest.deadline_from_model('autograph'):
+ guest.autograph = guest_autograph
+ session.add(guest_autograph)
+ if (guest_autograph.is_new and guest_autograph.rock_island_autographs) or \
+ guest_autograph.orig_value_of('rock_island_autographs') != guest_autograph.rock_island_autographs:
+ send_email.delay(
+ c.ROCK_ISLAND_EMAIL,
+ c.ROCK_ISLAND_EMAIL,
+ '{} Meet & Greet Notification'.format(guest.group.name),
+ render('emails/guests/meetgreet_notification.txt', {'guest': guest}, encoding=None),
+ model=guest.to_dict('id'))
guest.merch = guest_merch
session.add(guest_merch)
raise HTTPRedirect('index?id={}&message={}', guest.id, 'Your merchandise preferences have been saved')
@@ -223,9 +245,10 @@ def merch(self, session, guest_id, message='', coverage=False, warning=False, **
return {
'guest': guest,
'guest_merch': guest_merch,
+ 'guest_autograph': guest.autograph or guest_autograph,
'group': group_params or guest.group,
'message': message,
- 'agreed_to_ri_faq': guest.group_type == c.BAND and guest_merch and
+ 'agreed_to_ri_faq': guest.group_type in c.ROCK_ISLAND_GROUPS and guest_merch and
guest_merch.orig_value_of('selling_merch') != c.NO_MERCH and guest_merch.poc_address1,
}
@@ -303,7 +326,7 @@ def autograph(self, session, guest_id, message='', **params):
guest_autograph.length = 60 * int(params.get('length'), 0)
guest_autograph.rock_island_length = 60 * int(params.get('rock_island_length', 0))
- if guest_autograph.rock_island_autographs or \
+ if (guest_autograph.is_new and guest_autograph.rock_island_autographs) or \
guest_autograph.orig_value_of('rock_island_autographs') != guest_autograph.rock_island_autographs:
send_email.delay(
c.ROCK_ISLAND_EMAIL,
diff --git a/uber/tasks/groups.py b/uber/tasks/groups.py
index 2c413dad5..301cdc400 100644
--- a/uber/tasks/groups.py
+++ b/uber/tasks/groups.py
@@ -1,12 +1,16 @@
-from datetime import datetime
+import pytz
+
+from datetime import datetime, timedelta
from celery.schedules import crontab
from pockets.autolog import log
from sqlalchemy.orm.exc import NoResultFound
from uber.config import c
-from uber.models import Group, Session
+from uber.decorators import render
+from uber.models import Group, GuestGroup, GuestMerch, Session
from uber.tasks import celery
-from uber.utils import SignNowRequest
+from uber.tasks.email import send_email
+from uber.utils import SignNowRequest, localized_now
__all__ = ['check_document_signed', 'convert_declined_groups']
@@ -46,3 +50,17 @@ def convert_declined_groups():
for group in declined_groups:
result = decline_and_convert_dealer_group(session, group, delete_group=c.DELETE_DECLINED_GROUPS)
log.debug(f"{group.name} converted: {result}")
+
+
+@celery.schedule(crontab(minute=0, hour=0))
+def rock_island_updates():
+ with Session() as session:
+ updated_ri_inventories = session.query(GuestGroup).join(
+ GuestMerch, GuestGroup.merch).filter(
+ GuestMerch.inventory_updated > datetime.now(pytz.UTC) - timedelta(hours=24))
+ if updated_ri_inventories.count():
+ subject = '{} Rock Island Inventory Updates for {}'.format(c.EVENT_NAME, localized_now().strftime('%Y-%m-%d'))
+ body = render('emails/daily_checks/ri_inventory_updates.html', {
+ 'updated_ri_inventories': updated_ri_inventories,
+ }, encoding=None)
+ send_email(c.REPORTS_EMAIL, c.ROCK_ISLAND_EMAIL, subject, body, format='html', model='n/a', session=session)
\ No newline at end of file
diff --git a/uber/templates/emails/daily_checks/ri_inventory_updates.html b/uber/templates/emails/daily_checks/ri_inventory_updates.html
new file mode 100644
index 000000000..e04ae2a8b
--- /dev/null
+++ b/uber/templates/emails/daily_checks/ri_inventory_updates.html
@@ -0,0 +1,15 @@
+
+
+
+
{{ updated_ri_inventories.count() }} groups updated their Rock Island inventory
+
The following guests have updated their Rock Island inventories in the last 24 hours:
+ {% for guest in updated_ri_inventories %}
+
+
+ {{ guest.group.name }}: Last Updated {{ guest.merch.inventory_updated|datetime_local }} (View or Download Square Export)
+
+
+ {% endfor %}
+
+
+
diff --git a/uber/templates/emails/guests/rock_island_intro.txt b/uber/templates/emails/guests/rock_island_intro.txt
new file mode 100644
index 000000000..d19032efd
--- /dev/null
+++ b/uber/templates/emails/guests/rock_island_intro.txt
@@ -0,0 +1,8 @@
+{{ guest.group.name }},
+
+{{ c.EVENT_NAME }} encourages performers to sell their merchandise through "Rock Island," where our volunteers sell your merchandise at a booth which is staffed throughout the event. We need you to tell us whether you intend to sell merch at Rock Island by filling out the form at {{ c.URL_BASE }}/guests/merch?guest_id={{ guest.id }}
+
+You must tell us whether you intend to sell merch at Rock Island by {{ g.deadline_from_model('merch')|datetime_local }}. If you do intend to sell merch, you must also input your inventory by {{ c.ROCK_ISLAND_DEADLINE|datetime_local }}. Failure to submit this information on time may result in forfeiture of the opportunity to sell merchandise at Rock Island.
+
+- Rock Island
+rockisland@magfest.org
\ No newline at end of file
diff --git a/uber/templates/emails/guests/rock_island_inventory_reminder.txt b/uber/templates/emails/guests/rock_island_inventory_reminder.txt
new file mode 100644
index 000000000..8c5f5b6f4
--- /dev/null
+++ b/uber/templates/emails/guests/rock_island_inventory_reminder.txt
@@ -0,0 +1,8 @@
+{{ guest.group.name }},
+
+Thanks for opting into selling merch at Rock Island! {% if not guest.merch.inventory %}You have not uploaded any inventory yet! Y{% else %}This is just a friendly reminder that y{% endif %}ou have until {{ c.ROCK_ISLAND_DEADLINE|datetime_local }} to finalize your Rock Island inventory, which you can do here: {{ c.URL_BASE }}/guests/merch?guest_id={{ guest.id }}
+
+{% if not guest.merch.inventory %}Failure to submit this information on time may result in forfeiture of the opportunity to sell merchandise at Rock Island.
+
+{% endif %}- Rock Island
+rockisland@magfest.org
\ No newline at end of file
diff --git a/uber/templates/guest_checklist/band_autograph_deadline.html b/uber/templates/guest_checklist/band_autograph_deadline.html
index e80af4ce4..ba86569c0 100644
--- a/uber/templates/guest_checklist/band_autograph_deadline.html
+++ b/uber/templates/guest_checklist/band_autograph_deadline.html
@@ -3,7 +3,7 @@
{% block deadline_text %}
{% if guest.autograph_status %}
You have already indicated
- {% if not guest.autograph.num %}
+ {% if not guest.autograph.num and not guest.autograph.rock_island_autographs %}
that you do not wish to hold any autograph sessions,
{% else %}
that you would like
diff --git a/uber/templates/guest_checklist/merch_deadline.html b/uber/templates/guest_checklist/merch_deadline.html
index 69ef60457..77b21ddcc 100644
--- a/uber/templates/guest_checklist/merch_deadline.html
+++ b/uber/templates/guest_checklist/merch_deadline.html
@@ -185,7 +185,7 @@