diff --git a/docker-compose.yml b/docker-compose.yml index de2296737..02442ef8a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,9 +16,11 @@ x-uber: &uber - uber_hostname=localhost - uber_plugins=["magprime"] - uber_dev_box=True + - UBER_CONFIG_FILES=uber.ini + - LOG_CONFIG=true volumes: - - $PWD:/app/ - - $PWD/../magprime:/app/plugins/magprime + - .:/app/ + - ./../magprime:/app/plugins/magprime services: diff --git a/test-defaults.ini b/test-defaults.ini index 6b4dead8b..0ed50520a 100644 --- a/test-defaults.ini +++ b/test-defaults.ini @@ -1,7 +1,7 @@ path = "/rams" hostname = "localhost" -url_root = "https://localhost" +url_root = "http://localhost" consent_form_url = "http://magfest.org/parentalconsentform" diff --git a/uber.ini b/uber.ini new file mode 100644 index 000000000..817baee2b --- /dev/null +++ b/uber.ini @@ -0,0 +1,52 @@ +[hotel_lottery] +[[hotels]] +[[[gaylord]]] +name = "Gaylord National Harbor" +description = "The drive can't be that bad, can it?" + +[[[roof]]] +name = "Rooftop Room" +description = "Camping out on the roof of the Donald E. Stephens Convention Center" + +[[[cardboard]]] +name = "Cardboard Box" +description = "Literally a big box. Do you fits?" + +[[[mark_center]]] +name = "Hilton Mark Center" +description = "The tall one" + +[[room_types]] +[[[king]]] +name = "King Room" +description = "One really big bed" + +[[[double]]] +name = "Double Room" +description = "Two beds" + +[[suite_room_types]] +[[[super]]] +name = "Super Suite" +description = "This is the one everyone wants" + +[[[meh]]] +name = "Meh Suite" +description = "I guess" + +[[[overpriced]]] +name = "Overpriced Suite" +description = "This one is just crazy expensive. Otherwise a normal room." + +[[hotel_priorities]] +[[[hotel]]] +name = "Hotel" +description = "Which hotel matters to me." + +[[[dates]]] +name = "Dates" +description = "Check-In and Check-Out dates matter to me." + +[[[room]]] +name = "Room Type" +description = "The type of room I get matters." \ No newline at end of file diff --git a/uber/config.py b/uber/config.py index af085be63..efe1cf6fd 100644 --- a/uber/config.py +++ b/uber/config.py @@ -1554,6 +1554,14 @@ def _unrepr(d): c.SAME_NUMBER_REPEATED = r'^(\d)\1+$' +c.HOTEL_LOTTERY = _config.get('hotel_lottery', {}) +for key in ["hotels", "room_types", "suite_room_types", "hotel_priorities"]: + opts = [] + for name, item in c.HOTEL_LOTTERY.get(key, {}).items(): + if isinstance(item, dict): + opts.append((name, item)) + setattr(c, f"HOTEL_LOTTERY_{key.upper()}_OPTS", opts) + # Allows 0-9, a-z, A-Z, and a handful of punctuation characters c.VALID_BADGE_PRINTED_CHARS = r'[a-zA-Z0-9!"#$%&\'()*+,\-\./:;<=>?@\[\\\]^_`\{|\}~ "]' c.EVENT_QR_ID = c.EVENT_QR_ID or c.EVENT_NAME_AND_YEAR.replace(' ', '_').lower() diff --git a/uber/configspec.ini b/uber/configspec.ini index 911841850..16aa3cd13 100644 --- a/uber/configspec.ini +++ b/uber/configspec.ini @@ -1152,6 +1152,29 @@ room_deadline = string(default='') # Date after which staffers cannot drop their own shifts drop_shifts_deadline = string(default='') +# Hotel lottery dates +# The first day you can request to check in +hotel_lottery_checkin_start = string(default='%(epoch)s') +# The last day you can request to check in +hotel_lottery_checkin_end = string(default='%(eschaton)s') +# The first day you can request to check out +hotel_lottery_checkout_start = string(default='%(epoch)s') +# The last day you can request to check out +hotel_lottery_checkout_end = string(default='%(eschaton)s') + +# Open and close dates of the lottery form +hotel_lottery_form_open = string(default="") +hotel_lottery_form_close = string(default="") + +[hotel_lottery] +[[__many__]] +name = string(default="Sample Hotel") +description = string(default="A very exemplar hotel you can't stay at") +[[[__many__]]] +name = string(default="Normal Room") +description = string(default="One or more beds, two or more walls.") +price = integer(default=1000) +capacity = integer(default=4) [badge_type_prices] # Add badge types here to make them attendee-facing badges. They will be displayed @@ -1458,6 +1481,20 @@ 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/decorators.py b/uber/decorators.py index 9a14cb422..cc82af23e 100644 --- a/uber/decorators.py +++ b/uber/decorators.py @@ -255,6 +255,7 @@ def returns_json(*args, **kwargs): assert cherrypy.request.method == 'POST', 'POST required, got {}'.format(cherrypy.request.method) check_csrf(kwargs.pop('csrf_token', None)) except Exception: + traceback.print_exc() message = "There was an issue submitting the form. Please refresh and try again." return json.dumps({'success': False, 'message': message, 'error': message}, cls=serializer).encode('utf-8') return json.dumps(func(*args, **kwargs), cls=serializer).encode('utf-8') diff --git a/uber/errors.py b/uber/errors.py index fe8b3a127..dbb4658a8 100644 --- a/uber/errors.py +++ b/uber/errors.py @@ -1,4 +1,5 @@ from urllib.parse import quote +from uber.config import c import cherrypy @@ -49,6 +50,8 @@ def __init__(self, page, *args, **kwargs): query += '{sep}original_location={loc}'.format( sep=qs_char, loc=self.quote(original_location)) + if c.URL_ROOT.startswith("https"): + cherrypy.request.base = cherrypy.request.base.replace("http://", "https://") cherrypy.HTTPRedirect.__init__(self, query) def quote(self, s): diff --git a/uber/forms/__init__.py b/uber/forms/__init__.py index 0323c0c6d..c3c96cf13 100644 --- a/uber/forms/__init__.py +++ b/uber/forms/__init__.py @@ -8,7 +8,7 @@ from wtforms.validators import ValidationError from pockets.autolog import log from uber.config import c -from uber.forms.widgets import CountrySelect, IntSelect, MultiCheckbox, NumberInputGroup, SwitchInput +from uber.forms.widgets import CountrySelect, IntSelect, MultiCheckbox, NumberInputGroup, SwitchInput, Ranking from uber.model_checks import invalid_zip_code @@ -274,6 +274,8 @@ def get_field_type(self, field): return 'customselect' elif isinstance(widget, wtforms_widgets.HiddenInput): return 'hidden' + elif isinstance(widget, Ranking): + return 'ranking' else: return 'text' diff --git a/uber/forms/attendee.py b/uber/forms/attendee.py index a77a14acd..72d6f6f2b 100644 --- a/uber/forms/attendee.py +++ b/uber/forms/attendee.py @@ -9,7 +9,7 @@ from uber.config import c from uber.forms import (AddressForm, MultiCheckbox, MagForm, SelectAvailableField, SwitchInput, NumberInputGroup, - HiddenBoolField, HiddenIntField, CustomValidation) + HiddenBoolField, HiddenIntField, CustomValidation, Ranking) from uber.custom_tags import popup_link from uber.badge_funcs import get_real_badge_type from uber.models import Attendee, Session, PromoCodeGroup @@ -18,7 +18,8 @@ __all__ = ['AdminBadgeExtras', 'AdminBadgeFlags', 'AdminConsents', 'AdminStaffingInfo', 'BadgeExtras', - 'BadgeFlags', 'BadgeAdminNotes', 'PersonalInfo', 'PreregOtherInfo', 'OtherInfo', 'StaffingInfo', 'Consents'] + 'BadgeFlags', 'BadgeAdminNotes', 'PersonalInfo', 'PreregOtherInfo', 'OtherInfo', 'StaffingInfo', + 'LotteryApplication', 'Consents'] # TODO: turn this into a proper validation class @@ -28,6 +29,26 @@ def valid_cellphone(form, field): 'include a country code (e.g. +44) for international numbers.') +class LotteryApplication(MagForm): + wants_room = BooleanField('I would like to enter the hotel room lottery.', default=False) + earliest_room_checkin_date = DateField('Earliest acceptable Check-In Date', validators=[validators.DataRequired("Please enter your earliest check-in date.")]) + latest_room_checkin_date = DateField('Latest acceptable Check-In Date', validators=[validators.DataRequired("Please enter your latest check-in date.")]) + earliest_room_checkout_date = DateField('Earliest acceptable Check-Out Date', validators=[validators.DataRequired("Please enter your earliest check-out date.")]) + latest_room_checkout_date = DateField('Latest acceptable Check-Out Date', validators=[validators.DataRequired("Please enter your latest check-out date.")]) + hotel_preference = StringField('Hotel Preference', widget=Ranking(c.HOTEL_LOTTERY_HOTELS_OPTS, id="hotel_preference")) + room_type_preference = StringField('Room Type Preference', widget=Ranking(c.HOTEL_LOTTERY_ROOM_TYPES_OPTS, id="room_type_preference")) + selection_priorities = StringField('Room Priorities', widget=Ranking(c.HOTEL_LOTTERY_HOTEL_PRIORITIES_OPTS, id="selection_priorities")) + accessibility_contact = BooleanField('Please contact me in regards to an accessibility requirement.', default=False) + + wants_suite = BooleanField('I would like to enter the suite lottery.', default=False) + earliest_suite_checkin_date = DateField('Earliest acceptable Check-In Date', validators=[validators.DataRequired("Please enter your earliest check-in date.")]) + latest_suite_checkin_date = DateField('Latest acceptable Check-In Date', validators=[validators.DataRequired("Please enter your latest check-in date.")]) + earliest_suite_checkout_date = DateField('Earliest acceptable Check-Out Date', validators=[validators.DataRequired("Please enter your earliest check-out date.")]) + latest_suite_checkout_date = DateField('Latest acceptable Check-Out Date', validators=[validators.DataRequired("Please enter your latest check-out date.")]) + suite_type_preference = StringField('Hotel Preference', widget=Ranking(c.HOTEL_LOTTERY_SUITE_ROOM_TYPES_OPTS, id="suite_type_preference")) + + terms_accepted = BooleanField('I accept the terms of service of the hotel lottery.', default=False) + class PersonalInfo(AddressForm, MagForm): field_validation, new_or_changed_validation = CustomValidation(), CustomValidation() diff --git a/uber/forms/group.py b/uber/forms/group.py index 94a924040..cb68d21c0 100644 --- a/uber/forms/group.py +++ b/uber/forms/group.py @@ -5,12 +5,16 @@ from wtforms.validators import ValidationError from uber.config import c -from uber.forms import AddressForm, CustomValidation, MultiCheckbox, MagForm, IntSelect, NumberInputGroup +from uber.forms import AddressForm, CustomValidation, MultiCheckbox, MagForm, IntSelect, NumberInputGroup, Ranking from uber.forms.attendee import valid_cellphone from uber.custom_tags import format_currency, pluralize from uber.model_checks import invalid_phone_number -__all__ = ['GroupInfo', 'ContactInfo', 'TableInfo', 'AdminGroupInfo', 'AdminTableInfo'] +__all__ = ['HotelLotteryApplication', 'GroupInfo', 'ContactInfo', 'TableInfo', 'AdminGroupInfo', 'AdminTableInfo'] + + +class HotelLotteryApplication(MagForm): + ranked_hotels = Ranking(c.HOTEL_LOTTERY.keys()) class GroupInfo(MagForm): diff --git a/uber/forms/widgets.py b/uber/forms/widgets.py index 899d093fa..63104b128 100644 --- a/uber/forms/widgets.py +++ b/uber/forms/widgets.py @@ -102,3 +102,59 @@ def render_option(cls, value, label, selected, **kwargs): return Markup( "".format(html_params(**options), escape(label)) ) + +class Ranking(): + def __init__(self, choices=None, id=None, **kwargs): + self.choices = choices + self.id = id + + def __call__(self, field, choices=None, id=None, **kwargs): + choices = choices or self.choices or [('', {"name": "Error", "description": "No choices are configured"})] + id = id or self.id or "ranking" + selected_choices = field.data.split(",") + + deselected_html = [] + selected_html = [] + choice_dict = {key: val for key, val in choices} + for choice_id in selected_choices: + try: + choice_item = choice_dict[choice_id] + el = f""" +
  • +
    + {choice_item["name"]} +
    +
    + {choice_item["description"]} +
    +
  • """ + selected_html.append(el) + except KeyError: + continue + for choice_id, choice_item in choices: + if not choice_id in selected_choices: + el = f""" +
  • +
    + {choice_item["name"]} +
    +
    + {choice_item["description"]} +
    +
  • """ + deselected_html.append(el) + + html = [ + '
    ', + '
    ', + 'Available', + f'
    ', + 'Selected', + f'
    ' + ] + + return Markup(''.join(html)) \ No newline at end of file diff --git a/uber/models/__init__.py b/uber/models/__init__.py index b3b1482d6..554e13f9b 100644 --- a/uber/models/__init__.py +++ b/uber/models/__init__.py @@ -588,6 +588,7 @@ def minutestr(dt): from uber.models.email import Email # noqa: E402 from uber.models.group import Group # noqa: E402 from uber.models.guests import GuestGroup # noqa: E402 +from uber.models.hotel import LotteryApplication from uber.models.mits import MITSApplicant, MITSTeam # noqa: E402 from uber.models.mivs import IndieJudge, IndieGame, IndieStudio # noqa: E402 from uber.models.panels import PanelApplication, PanelApplicant # noqa: E402 diff --git a/uber/models/hotel.py b/uber/models/hotel.py index a96ea0ce2..85a11615c 100644 --- a/uber/models/hotel.py +++ b/uber/models/hotel.py @@ -105,3 +105,10 @@ def check_out_date(self): class RoomAssignment(MagModel): room_id = Column(UUID, ForeignKey('room.id')) attendee_id = Column(UUID, ForeignKey('attendee.id')) + + +class LotteryApplication(MagModel): + hotel_preference = Column(MultiChoice(c.HOTEL_LOTTERY_HOTELS_OPTS)) + room_type_preference = Column(MultiChoice(c.HOTEL_LOTTERY_ROOM_TYPES_OPTS)) + selection_priorities = Column(MultiChoice(c.HOTEL_LOTTERY_HOTEL_PRIORITIES_OPTS)) + suite_type_preference = Column(MultiChoice(c.HOTEL_LOTTERY_SUITE_ROOM_TYPES_OPTS)) \ No newline at end of file diff --git a/uber/site_sections/hotel_lottery.py b/uber/site_sections/hotel_lottery.py new file mode 100644 index 000000000..ef288f037 --- /dev/null +++ b/uber/site_sections/hotel_lottery.py @@ -0,0 +1,55 @@ +from datetime import datetime + +from uber.config import c +from uber.decorators import ajax, all_renderable +from uber.errors import HTTPRedirect +from uber.models import Attendee, Room, RoomAssignment, Shift, LotteryApplication +from uber.forms import load_forms +from uber.utils import validate_model + + +@all_renderable() +class Root: + def index(self, session, **params): + lottery_application = LotteryApplication() + params['id'] = 'None' + forms = load_forms(params, lottery_application, ['LotteryApplication']) + return { + "checkin_start": c.HOTEL_LOTTERY_CHECKIN_START, + "checkin_end": c.HOTEL_LOTTERY_CHECKIN_END, + "checkout_start": c.HOTEL_LOTTERY_CHECKOUT_START, + "checkout_end": c.HOTEL_LOTTERY_CHECKOUT_END, + "hotels": c.HOTEL_LOTTERY, + "forms": forms + } + + @ajax + def validate_hotel_lottery(self, session, form_list=[], **params): + if params.get('id') in [None, '', 'None']: + application = LotteryApplication() + else: + application = LotteryApplication.get(id=params.get('id')) + + if not form_list: + form_list = ["LotteryApplication"] + elif isinstance(form_list, str): + form_list = [form_list] + forms = load_forms(params, application, form_list, get_optional=False) + + all_errors = validate_model(forms, application, LotteryApplication(**application.to_dict())) + if all_errors: + return {"error": all_errors} + + return {"success": True} + + def form(self, session, message="", **params): + application = LotteryApplication() + forms_list = ["LotteryApplication"] + forms = load_forms(params, application, forms_list) + for form in forms.values(): + form.populate_obj(application) + return { + 'forms': forms, + 'message': message, + 'application': application + } \ No newline at end of file diff --git a/uber/templates/forms/macros.html b/uber/templates/forms/macros.html index 949204455..e5fd3d9b6 100644 --- a/uber/templates/forms/macros.html +++ b/uber/templates/forms/macros.html @@ -104,6 +104,12 @@ {{ form_label(field, label_text=label_text, required=label_required) }} {{ form_input_extras(field, help_text, admin_text, extra_field) }} +{% elif type == 'ranking' %} +
    + {{ form_label(field, label_text=label_text, required=label_required) }} + {{ field(**custom_kwargs) }} + {{ form_input_extras(field, help_text, admin_text, extra_field) }} +
    {% else %}
    {{ field(class="form-control", **custom_kwargs) }} diff --git a/uber/templates/hotel_lottery/form.html b/uber/templates/hotel_lottery/form.html new file mode 100644 index 000000000..f0856e774 --- /dev/null +++ b/uber/templates/hotel_lottery/form.html @@ -0,0 +1,71 @@ +{% extends "./preregistration/preregbase.html" %} +{% set title_text = "Hotel Lottery Application" %} +{% import 'macros.html' as macros with context %} +{% import 'forms/macros.html' as form_macros with context %} +{% set lottery_application = lottery_application or forms['lottery_application'] %} + +{% block content %} + + + + +

    Saved Hotel Preference!

    +
    +
    + {{ form_macros.form_validation('hotel-lottery-form', 'validate_hotel_lottery') }} +
    + {{ form_macros.form_input(lottery_application.hotel_preference) }} + {{ csrf_token() }} + +
    +
    +
    + + +{% endblock %} diff --git a/uber/templates/hotel_lottery/index.html b/uber/templates/hotel_lottery/index.html new file mode 100644 index 000000000..efa6e4269 --- /dev/null +++ b/uber/templates/hotel_lottery/index.html @@ -0,0 +1,74 @@ +{% extends "./preregistration/preregbase.html" %} +{% set title_text = "Hotel Lottery Application" %} +{% import 'macros.html' as macros with context %} +{% import 'forms/macros.html' as form_macros with context %} +{% set lottery_application = lottery_application or forms['lottery_application'] %} + +{% block content %} + + + + + +
    +
    + {{ form_macros.form_validation('hotel-lottery-form', 'validate_hotel_lottery') }} +
    + {{ form_macros.form_input(lottery_application.hotel_preference) }} + {{ form_macros.form_input(lottery_application.room_type_preference) }} + {{ form_macros.form_input(lottery_application.selection_priorities) }} + {{ form_macros.form_input(lottery_application.suite_type_preference) }} + {{ csrf_token() }} + +
    +
    +
    + + +{% endblock %}