Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/volunteer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def volunteer_variables():
from . import (
api, # noqa: F401
bar_training, # noqa: F401
buildup, # noqa: F401
choose_roles, # noqa: F401
main, # noqa: F401
schedule, # noqa: F401
Expand Down
1 change: 1 addition & 0 deletions apps/volunteer/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@


from . import (
buildup, # noqa: F401
role, # noqa: F401
shift, # noqa: F401
venue, # noqa: F401
Expand Down
115 changes: 115 additions & 0 deletions apps/volunteer/admin/buildup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from typing import ClassVar

from dateutil.rrule import DAILY, rrule
from flask_admin import expose

from main import db
from models.volunteer.buildup import (
BuildupSignupKey,
BuildupVolunteer,
buildup_end,
buildup_start,
teardown_end,
teardown_start,
)

from ..flask_admin_base import VolunteerBaseView, VolunteerModelView
from . import volunteer_admin


class BuildupSignupKeyModelView(VolunteerModelView):
column_filters: ClassVar[list[str]] = ["team_name"]
form_columns: ClassVar[list[str]] = ["token", "team_name"]

def edit_form(self, obj=None):
form = super().edit_form(obj=obj)
if not form.token.render_kw:
form.token.render_kw = {}
form.token.render_kw["readonly"] = True
return form


volunteer_admin.add_view(
BuildupSignupKeyModelView(BuildupSignupKey, db.session, name="Signup keys", category="Buildup")
)


class BuildupVolunteerModelView(VolunteerModelView):
form_excluded_columns: ClassVar[list[str]] = ["versions"]

def _modify_widget_args(self, form, obj=None, create=False):
if not form.arrival_date.render_kw:
form.arrival_date.render_kw = {}
form.arrival_date.render_kw.update(
**{
"data-min-date": buildup_start(),
"data-max-date": teardown_end(),
}
)
if create:
form.arrival_date.render_kw["data-start-date"] = buildup_end()

if not form.departure_date.render_kw:
form.departure_date.render_kw = {}
form.departure_date.render_kw.update(
**{
"data-min-date": buildup_start(),
"data-max-date": teardown_end(),
}
)
if create:
form.departure_date.render_kw["data-start-date"] = teardown_start()

return form

def create_form(self, obj=None):
return self._modify_widget_args(super().create_form(obj), create=True)

def edit_form(self, obj=None):
return self._modify_widget_args(super().edit_form(obj))


volunteer_admin.add_view(
BuildupVolunteerModelView(BuildupVolunteer, db.session, name="Volunteers", category="Buildup")
)


class BuildupVolunteerBreakdownView(VolunteerBaseView):
@expose("/")
def index(self):
days_data = []
is_just_after_event = False
for dt in rrule(DAILY, dtstart=buildup_start(), until=teardown_end(), byhour=[6, 18]):
if buildup_end() <= dt <= teardown_start():
is_just_after_event = True
continue
predicted_volunteers = BuildupVolunteer.query.filter(
(BuildupVolunteer.arrival_date <= dt) & (BuildupVolunteer.departure_date >= dt)
)
arrived_volunteers = BuildupVolunteer.query.filter(BuildupVolunteer.recorded_on_site <= dt)
date_str = dt.date().strftime("%a %d-%b")
am_or_pm = "AM" if dt.time().hour < 12 else "PM"
days_data.append(
{
"date_str": date_str,
"am_or_pm": am_or_pm,
"is_just_after_event": is_just_after_event,
"predicted_onsite": predicted_volunteers.count(),
"arrived_onsite": arrived_volunteers.count(),
}
)
if is_just_after_event:
is_just_after_event = False
max_predicted = max(d["predicted_onsite"] for d in days_data)
max_arrived = max(d["arrived_onsite"] for d in days_data)
return self.render(
"volunteer/admin/buildup_breakdown.html",
days_data=days_data,
max_predicted=max_predicted,
max_arrived=max_arrived,
)


volunteer_admin.add_view(
BuildupVolunteerBreakdownView(name="Arrival breakdown", category="Buildup", endpoint="breakdown")
)
211 changes: 211 additions & 0 deletions apps/volunteer/buildup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
from datetime import datetime

from flask import (
abort,
flash,
redirect,
render_template,
request,
url_for,
)
from flask import (
current_app as app,
)
from flask_login import current_user, login_required
from sqlalchemy import select
from wtforms import BooleanField, DateTimeLocalField, StringField, SubmitField
from wtforms.validators import DataRequired, InputRequired

from apps.users import get_next_url
from main import db
from models.volunteer.buildup import BuildupSignupKey, BuildupVolunteer, buildup_start, teardown_end
from models.volunteer.volunteer import Volunteer as VolunteerUser

from ..common import create_current_user
from ..common.forms import Form
from . import volunteer
from .sign_up import VolunteerSignUpForm, update_volunteer_from_form


class BuildupSignUpForm(Form):
arrival_date = DateTimeLocalField(
"Arrival Date",
[
InputRequired(),
],
format="%Y-%m-%dT%H:%M",
)
departure_date = DateTimeLocalField(
"Departure Date",
[
InputRequired(),
],
format="%Y-%m-%dT%H:%M",
)

emergency_contact = StringField(
"Emergency Contact",
[
InputRequired(),
],
)

health_and_safety_briefing = BooleanField(
"I have read and agree to follow the rules in the Safety on Site briefing above",
[
DataRequired(),
],
)

save = SubmitField("Confirm buildup/teardown attendance")

def validate(self, *args, **kwargs):
rv = super().validate(*args, **kwargs)
if not rv:
return False

if self.arrival_date.data > self.departure_date.data:
self.departure_date.errors.append(
"You must depart after you arrive. Violating the laws of causality is not permitted."
)
return False
return True


def update_buildup_volunteer_from_form(buv: BuildupVolunteer, form: BuildupSignUpForm) -> BuildupVolunteer:
if form.arrival_date.data:
buv.arrival_date = form.arrival_date.data
if form.departure_date.data:
buv.departure_date = form.departure_date.data
if form.emergency_contact.data:
buv.emergency_contact = form.emergency_contact.data
if form.health_and_safety_briefing.data:
buv.acked_health_and_safety_briefing_at = datetime.now()
return buv


def _buildup_register(show_form_on_success: bool = False, key: BuildupSignupKey | None = None):
volunteer = current_user.is_authenticated and VolunteerUser.get_for_user(current_user)
passed_validation = request.method == "POST"
volunteer_form = VolunteerSignUpForm(prefix="v", obj=volunteer)
if not volunteer and request.method == "GET" and current_user.is_authenticated:
volunteer_form.volunteer_email.data = current_user.email
volunteer_form.nickname.data = current_user.name

volunteer_form.over_18.validators = [
DataRequired(message="Sorry, but you must be over 18 to be on the field during buildup and teardown")
]
if volunteer_form.validate_on_submit():
if volunteer:
volunteer = update_volunteer_from_form(volunteer, volunteer_form)
db.session.add(volunteer)
else:
if not volunteer_form.volunteer_email.data or not volunteer_form.nickname.data:
abort(400)
if current_user.is_anonymous:
create_current_user(volunteer_form.volunteer_email.data, volunteer_form.nickname.data)
new_volunteer = VolunteerUser()
new_volunteer.user_id = current_user.id
new_volunteer = update_volunteer_from_form(new_volunteer, volunteer_form)
db.session.add(new_volunteer)
volunteer = new_volunteer

current_user.grant_permission("volunteer:user")
else:
passed_validation = False

# Now the buildup form
buv = current_user.is_authenticated and BuildupVolunteer.get_for_user(current_user)
buildup_form = BuildupSignUpForm(prefix="b", obj=buv)
if buv and buv.acked_health_and_safety_briefing_at:
buildup_form.health_and_safety_briefing.data = True
if buildup_form.validate_on_submit():
buv = current_user.is_authenticated and BuildupVolunteer.get_for_user(current_user)
if buv:
buv = update_buildup_volunteer_from_form(buv, buildup_form)
db.session.add(buv)
else:
new_buv = BuildupVolunteer()
new_buv.user_id = current_user.id
if key:
new_buv.team_name = key.team_name
new_buv = update_buildup_volunteer_from_form(new_buv, buildup_form)
db.session.add(new_buv)

current_user.grant_permission("volunteer:buildup")
else:
passed_validation = False

if passed_validation:
db.session.commit()
app.logger.info("Added new volunteer user %s through buildup flow", volunteer)
flash("Thanks! Your buildup registration has been recorded.", "message")

if not show_form_on_success:
return None
else:
db.session.rollback()

return render_template(
"volunteer/buildup-sign-up.html",
user=current_user,
volunteer=volunteer,
volunteer_form=volunteer_form,
buildup_form=buildup_form,
buildup_start=buildup_start(),
teardown_end=teardown_end(),
)


@volunteer.route("buildup/arrived/<secret>", methods=["GET", "POST"])
@login_required
def buildup_arrived(secret):
if secret != app.config.get("BUILDUP_SECRET"):
abort(404)

buv = BuildupVolunteer.get_for_user(current_user)
if not buv:
if response := _buildup_register():
return response
# OK, they're registered now.
buv = BuildupVolunteer.get_for_user(current_user)

if not buv.recorded_on_site:
buv.recorded_on_site = datetime.now()
db.session.add(buv)
db.session.commit()

return render_template(
"volunteer/buildup-arrived.html",
user=current_user,
is_buildup=datetime.now() < buildup_start(),
buildup_signal_group=app.config.get("BUILDUP_SIGNAL_GROUP"),
)


@volunteer.route("buildup", methods=["GET", "POST"])
@login_required
def buildup_amend():
buv = BuildupVolunteer.get_for_user(current_user)
if not buv and not current_user.has_permission("volunteer:buildup"):
return abort(404)

return _buildup_register(show_form_on_success=True)


@volunteer.route("buildup/register/<token>", methods=["GET", "POST"])
def buildup_register(token: str):
key = db.session.execute(
select(BuildupSignupKey).where(BuildupSignupKey.token == token)
).scalar_one_or_none()
if not key:
abort(404)

if current_user.is_authenticated and BuildupVolunteer.get_for_user(current_user):
return redirect(url_for(".buildup_amend"))

if response := _buildup_register(key=key):
return response

# They completed registration:
return redirect(get_next_url(default=url_for(".buildup_amend")))

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
Loading
Loading