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

Hotel Lottery Forms #4360

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 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
6 changes: 4 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion test-defaults.ini
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

path = "/rams"
hostname = "localhost"
url_root = "https://localhost"
url_root = "http://localhost"

consent_form_url = "http://magfest.org/parentalconsentform"

Expand Down
76 changes: 76 additions & 0 deletions uber.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
[hotel_lottery]
[[gaylord]]
name = "Gaylord National Harbor"
description = "The drive can't be that bad, can it?"
[[[Double Queen]]]
name = "Double Queen"
description = "Two beds in a box"
price = 10000
capacity = 4
[[[King]]]
name = "King"
description = "One bed in a box"
price = 10000
capacity = 4
[[[Suite]]]
name = "Suite"
description = "Baller over here"
price = 20000
capacity = 8

[[roof]]
name = "Rooftop Room"
description = "Camping out on the roof of the Donald E. Stephens Convention Center"
[[[Double Queen]]]
name = "Double Queen"
description = "Two beds in a box"
price = 10000
capacity = 4
[[[King]]]
name = "King"
description = "One bed in a box"
price = 10000
capacity = 4
[[[Suite]]]
name = "Suite"
description = "Baller over here"
price = 20000
capacity = 8

[[cardboard]]
name = "Cardboard Box"
description = "Literally a big box. Do you fits?"
[[[Double Queen]]]
name = "Double Queen"
description = "Two beds in a box"
price = 10000
capacity = 4
[[[King]]]
name = "King"
description = "One bed in a box"
price = 10000
capacity = 4
[[[Suite]]]
name = "Suite"
description = "Baller over here"
price = 20000
capacity = 8

[[mark_center]]
name = "Hilton Mark Center"
description = "The tall one"
[[[Double Queen]]]
name = "Double Queen"
description = "Two beds in a box"
price = 10000
capacity = 4
[[[King]]]
name = "King"
description = "One bed in a box"
price = 10000
capacity = 4
[[[Suite]]]
name = "Suite"
description = "Baller over here"
price = 20000
capacity = 8
6 changes: 6 additions & 0 deletions uber/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,12 @@ def _unrepr(d):

c.SAME_NUMBER_REPEATED = r'^(\d)\1+$'

c.HOTEL_LOTTERY = _config.get('hotel_lottery', {})
c.HOTEL_LOTTERY_HOTEL_OPTS = []
for name, item in c.HOTEL_LOTTERY.items():
if isinstance(item, dict):
c.HOTEL_LOTTERY_HOTEL_OPTS.append((name, item))

# 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()
Expand Down
23 changes: 23 additions & 0 deletions uber/configspec.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions uber/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to double-check, is/was this a temporary statement for debugging?

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')
Expand Down
3 changes: 3 additions & 0 deletions uber/errors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from urllib.parse import quote
from uber.config import c

import cherrypy

Expand Down Expand Up @@ -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):
Expand Down
4 changes: 3 additions & 1 deletion uber/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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'

Expand Down
9 changes: 7 additions & 2 deletions uber/forms/attendee.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -28,6 +29,10 @@ def valid_cellphone(form, field):
'include a country code (e.g. +44) for international numbers.')


class LotteryApplication(MagForm):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this into its own forms/hotel.py file, to more closely reflect where it is in models/. I'm sure we'll have plenty of lottery-related forms as we progress so it won't be a file with just one class in it.

Also, just wondering, why is this a StringField and not a SelectMultipleField or similar? Is it because the choices have to be ordered?

hotel_preference = StringField('Hotel Preference', widget=Ranking(c.HOTEL_LOTTERY_HOTEL_OPTS, id="hotel_preference"),
validators=[validators.Regexp(r"^roof.*$", message="You didn't select the roof as your most preferred hotel. What, are you too good for the great outdoors?")])

class PersonalInfo(AddressForm, MagForm):
field_validation, new_or_changed_validation = CustomValidation(), CustomValidation()

Expand Down
8 changes: 6 additions & 2 deletions uber/forms/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Ranking() a widget or a field class? I see this form isn't being used, is it just left over from something else you were trying?



class GroupInfo(MagForm):
Expand Down
56 changes: 56 additions & 0 deletions uber/forms/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,59 @@ def render_option(cls, value, label, selected, **kwargs):
return Markup(
"<option {}>{}</option>".format(html_params(**options), escape(label))
)

class Ranking():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to double-check, should this be subclassing an existing WTForms widget?

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"""
<li class="choice" value="{choice_id}">
<div class="choice-name">
{choice_item["name"]}
</div>
<div class="choice-description">
{choice_item["description"]}
</div>
</li>"""
selected_html.append(el)
except KeyError:
continue
for choice_id, choice_item in choices:
if not choice_id in selected_choices:
el = f"""
<li class="choice" value="{choice_id}">
<div class="choice-name">
{choice_item["name"]}
</div>
<div class="choice-description">
{choice_item["description"]}
</div>
</li>"""
deselected_html.append(el)

html = [
'<div class="row">',
'<div class="col-md-6">',
'Available',
f'<ul class="choice-list" id="deselected_{id}">',
*deselected_html,
'</ul></div><div class="col-md-6">',
'Selected',
f'<ul class="choice-list" id="selected_{id}">',
*selected_html,
f'</ul></div></div><input type="hidden" id="{id}" name="{id}" value="None" />'
]

return Markup(''.join(html))
1 change: 1 addition & 0 deletions uber/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions uber/models/hotel.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,7 @@ 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_HOTEL_OPTS))
55 changes: 55 additions & 0 deletions uber/site_sections/hotel_lottery.py
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions uber/templates/forms/macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@
{{ form_label(field, label_text=label_text, required=label_required) }}
{{ form_input_extras(field, help_text, admin_text, extra_field) }}
</div>
{% elif type == 'ranking' %}
<div class="form-floating{% if not no_margin %} mb-3{% endif %}">
{{ form_label(field, label_text=label_text, required=label_required) }}
{{ field(**custom_kwargs) }}
{{ form_input_extras(field, help_text, admin_text, extra_field) }}
</div>
{% else %}
<div class="form-floating{% if not no_margin %} mb-3{% endif %}">
{{ field(class="form-control", **custom_kwargs) }}
Expand Down
Loading
Loading