From a3e603396c0818355969bcf4aec70aebf2a3809d Mon Sep 17 00:00:00 2001 From: Victoria Earl Date: Mon, 4 Nov 2024 21:29:34 -0500 Subject: [PATCH] Completely overhaul signups view Fixes https://magfest.atlassian.net/browse/MAGDEV-1198, including: - Swapping the existing calendar widget for one that doesn't have nested scrolling, improving experience on mobile devices - Adding a "details" box when you click on an event in the calendar view, allowing you to use the calendar view on mobile without text spilling out everywhere - Showing which department each shift is in and how many slots it has filled out of its total, and allowing you to highlight empty shifts - A ton of other improvements, including making sure you never need to refresh the page (this is to prevent your view getting reset -- everything is purely clientside so saving its state is pretty difficult) and allowing you to filter by department --- uber/config.py | 11 + uber/models/__init__.py | 11 +- uber/models/commerce.py | 4 + uber/site_sections/staffing.py | 148 ++- uber/templates/base.html | 2 +- uber/templates/signup_base.html | 75 +- uber/templates/staffing/credits.html | 4 +- .../staffing/emergency_procedures.html | 4 +- .../templates/staffing/food_restrictions.html | 4 +- uber/templates/staffing/hotel.html | 4 +- uber/templates/staffing/index.html | 4 +- uber/templates/staffing/onsite_jobs.html | 4 +- uber/templates/staffing/shifts.html | 843 +++++++++++------- uber/templates/staffing/shirt_size.html | 4 +- uber/templates/staffing/volunteer.html | 4 +- .../staffing/volunteer_agreement.html | 4 +- 16 files changed, 748 insertions(+), 382 deletions(-) diff --git a/uber/config.py b/uber/config.py index ac322b9b2..f9d960119 100644 --- a/uber/config.py +++ b/uber/config.py @@ -900,6 +900,17 @@ def CURRENT_ADMIN(self): except Exception: return {} + @request_cached_property + @dynamic + def CURRENT_VOLUNTEER(self): + try: + from uber.models import Session + with Session() as session: + attendee = session.logged_in_volunteer() + return attendee.to_dict() + except Exception: + return {} + @request_cached_property @dynamic def DEPARTMENTS(self): diff --git a/uber/models/__init__.py b/uber/models/__init__.py index b58cf24ca..1c4893508 100644 --- a/uber/models/__init__.py +++ b/uber/models/__init__.py @@ -961,20 +961,15 @@ def checklist_status(self, slug, department_id): } def jobs_for_signups(self, all=False): - fields = [ - 'name', 'department_id', 'department_name', 'description', - 'weight', 'start_time_local', 'end_time_local', 'duration', - 'weighted_hours', 'restricted', 'extra15', 'taken', - 'visibility', 'is_public', 'is_setup', 'is_teardown'] - jobs = self.logged_in_volunteer().possible_and_current + jobs = self.logged_in_volunteer().possible restricted_minutes = set() for job in jobs: if job.required_roles: restricted_minutes.add(frozenset(job.minutes)) if all: - return [job.to_dict(fields) for job in jobs] + return jobs return [ - job.to_dict(fields) + job for job in jobs if (job.required_roles or frozenset(job.minutes) not in restricted_minutes)] def possible_match_list(self): diff --git a/uber/models/commerce.py b/uber/models/commerce.py index 39cbea9dd..4c49198f9 100644 --- a/uber/models/commerce.py +++ b/uber/models/commerce.py @@ -107,6 +107,10 @@ def all_sorted_items_and_txns(self): @property def sorted_txns(self): return sorted([txn for txn in self.receipt_txns], key=lambda x: x.added) + + @property + def sorted_items(self): + return sorted([item for item in self.receipt_items], key=lambda x: x.added) @property def total_processing_fees(self): diff --git a/uber/site_sections/staffing.py b/uber/site_sections/staffing.py index 79e1af76e..7afaf2bac 100644 --- a/uber/site_sections/staffing.py +++ b/uber/site_sections/staffing.py @@ -2,13 +2,22 @@ from datetime import datetime, timedelta from pockets.autolog import log import ics +from sqlalchemy.orm.exc import NoResultFound 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 +from uber.models import Attendee, Job +from uber.utils import check_csrf, create_valid_user_supplied_redirect_url, ensure_csrf_token_exists, localized_now, extract_urls + + +def _convert_urls(desc): + urls = extract_urls(desc) + log.error(urls) + for url in urls: + desc = desc.replace(url, f'{url}') + return desc @all_renderable() @@ -208,8 +217,7 @@ def shifts(self, session, view='', start='', all=''): assigned_dept_ids = set(volunteer.assigned_depts_ids) has_public_jobs = False for job in joblist: - job['is_public_to_volunteer'] = job['is_public'] and job['department_id'] not in assigned_dept_ids - if job['is_public_to_volunteer']: + if job.is_public and job.department_id not in assigned_dept_ids: has_public_jobs = True has_setup = volunteer.can_work_setup or any(d.is_setup_approval_exempt for d in volunteer.assigned_depts) @@ -226,28 +234,111 @@ def shifts(self, session, view='', start='', all=''): else: start = datetime.strptime(start, '%Y-%m-%dT%H:%M:%S.%f') - if has_setup and has_teardown: - cal_length = c.CON_TOTAL_DAYS - elif has_setup: - cal_length = con_days + c.SETUP_SHIFT_DAYS - elif has_teardown: - cal_length = con_days + 2 # There's no specific config for # of shift signup days - else: - cal_length = con_days + end = c.TEARDOWN_JOB_END if has_teardown else c.ESCHATON + + total_duration = 0 + event_dates = [] + day = start + while day <= end: + total_duration += 1 + if c.EPOCH <= day and day <= c.ESCHATON: + event_dates.append(day.strftime('%Y-%m-%d')) + day += timedelta(days=1) + + default_filters = [] + for department in volunteer.assigned_depts: + default_filters.append({ + 'id': department.id, + 'title': department.name, + }) + other_filters = [ + {'id': 'public', 'title': "Public Shifts",}, + ] return { 'jobs': joblist, - 'has_public_jobs': has_public_jobs, + 'has_public_jobs': session.query(Job).filter(Job.is_public == True).count(), 'depts_with_roles': [membership.department.name for membership in volunteer.dept_memberships_with_role], + 'assigned_depts_list': [(dept.id, dept.name) for dept in volunteer.assigned_depts], 'name': volunteer.full_name, 'hours': volunteer.weighted_hours, 'assigned_depts_labels': volunteer.assigned_depts_labels, + 'default_filters': default_filters, + 'all_filters': default_filters + other_filters, 'view': view, - 'start': start, - 'end': start + timedelta(days=cal_length), + 'start': start.date(), + 'end': end.date(), + 'total_duration': total_duration, + 'highlighted_dates': event_dates, + 'setup_duration': 0 if not has_setup else (c.EPOCH - c.SETUP_JOB_START).days, + 'teardown_duration': 0 if not has_teardown else (c.TEARDOWN_JOB_END - c.ESCHATON).days, 'start_day': c.SHIFTS_START_DAY if has_setup else c.EPOCH, 'show_all': all, } + + @ajax_gettable + def get_available_jobs(self, session, all=False, highlight=False, **params): + joblist = session.jobs_for_signups(all=all) + + volunteer = session.logged_in_volunteer() + assigned_dept_ids = set(volunteer.assigned_depts_ids) + event_list = [] + + for job in joblist: + resource_id = job.department_id + bg_color = "#0d6efd" + if job.is_public and job.department_id not in assigned_dept_ids: + resource_id = "public" + bg_color = "#0dcaf0" + if highlight and len(job.shifts) == 0: + bg_color = "#dc3545" + event_list.append({ + 'id': job.id, + 'resourceIds': [resource_id], + 'allDay': False, + 'start': job.start_time_local.isoformat(), + 'end': job.end_time_local.isoformat(), + 'title': f"{job.name}", + 'backgroundColor': bg_color, + 'extendedProps': { + 'department_name': job.department_name, + 'desc': _convert_urls(job.description), + 'desc_text': job.description, + 'weight': job.weight, + 'slots': f"{len(job.shifts)}/{job.slots}", + 'is_public': job.is_public, + 'assigned': False, + } + }) + return event_list + + @ajax_gettable + def get_assigned_jobs(self, session, **params): + volunteer = session.logged_in_volunteer() + event_list = [] + + for shift in volunteer.shifts: + job = shift.job + event_list.append({ + 'id': shift.id, + 'resourceIds': [job.department_id], + 'allDay': False, + 'start': job.start_time_local.isoformat(), + 'end': job.end_time_local.isoformat(), + 'title': f"{job.name}", + 'backgroundColor': '#198754', + 'extendedProps': { + 'department_name': job.department_name, + 'desc': _convert_urls(job.description), + 'desc_text': job.description, + 'weight': job.weight, + 'slots': f"{len(job.shifts)}/{job.slots}", + 'is_public': job.is_public, + 'assigned': True, + } + }) + return event_list + def shifts_ical(self, session, **params): attendee = session.logged_in_volunteer() @@ -277,28 +368,31 @@ def jobs(self, session, all=False): @check_shutdown @ajax - def sign_up(self, session, job_id, all=False): - return { - 'error': session.assign(session.logged_in_volunteer().id, job_id), - 'jobs': session.jobs_for_signups(all=all) - } + def sign_up(self, session, job_id, **params): + message = session.assign(session.logged_in_volunteer().id, job_id) + if message: + return {'success': False, 'message': message} + return {'success': True, 'message': "Signup complete!"} @check_shutdown @ajax - def drop(self, session, job_id, all=False): + def drop(self, session, shift_id, all=False): if c.AFTER_DROP_SHIFTS_DEADLINE: return { - 'error': "You can no longer drop shifts.", - 'jobs': session.jobs_for_signups(all=all) + 'success': False, + 'message': "You can no longer drop shifts." } try: - shift = session.shift(job_id=job_id, attendee_id=session.logged_in_volunteer().id) + shift = session.shift(shift_id) session.delete(shift) session.commit() - except Exception: - pass + except NoResultFound: + return { + 'success': True, + 'message': "You've already dropped or have been unassigned from this shift." + } finally: - return {'jobs': session.jobs_for_signups(all=all)} + return {'success': True, 'message': "Shift dropped."} @public def login(self, session, message='', first_name='', last_name='', email='', zip_code='', original_location=None): diff --git a/uber/templates/base.html b/uber/templates/base.html index 5fabb7c0a..b0f7d9bee 100644 --- a/uber/templates/base.html +++ b/uber/templates/base.html @@ -1,6 +1,6 @@ {% import 'macros.html' as macros with context %} {% set page_ro = false %} -{% set bootstrap5 = 'preregistration' in c.PAGE_PATH or 'landing' in c.PAGE_PATH or 'art_show_applications' in c.PAGE_PATH or 'attractions' in c.PAGE_PATH or 'hotel_lottery' in c.PAGE_PATH or 'marketplace' in c.PAGE_PATH or admin_area %} +{% set bootstrap5 = 'preregistration' in c.PAGE_PATH or 'landing' in c.PAGE_PATH or 'art_show_applications' in c.PAGE_PATH or 'attractions' in c.PAGE_PATH or 'hotel_lottery' in c.PAGE_PATH or 'marketplace' in c.PAGE_PATH or 'staffing' in c.PAGE_PATH or admin_area %} diff --git a/uber/templates/signup_base.html b/uber/templates/signup_base.html index 612f97fc2..a0d5074e5 100644 --- a/uber/templates/signup_base.html +++ b/uber/templates/signup_base.html @@ -1,26 +1,49 @@ -{%- import 'macros.html' as macros -%} - - - - {{ name }}'s shifts - - {{ "styles/styles.css"|serve_static_content }} -{{ "fullcalendar-5.3.2/lib/main.min.css"|serve_static_content }} - -{% block page_style %}{% endblock %} - - -
-{% block main_content %} {% endblock %} -
- -{% block page_script %} {% endblock %} - +{% extends "preregistration/preregbase.html" %} + +{% block head_styles %} + {{ "deps/combined.min.css"|serve_static_content }} + + {{ "deps/bootstrap5/bootstrap.min.css"|serve_static_content }} + {{ "deps/bootstrap5/font-awesome.min.css"|serve_static_content }} + {{ "deps/bootstrap5/datatables.min.css"|serve_static_content }} + + + {% block additional_styles %} + {% block page_styles %}{% endblock %} + {% endblock %} +{% endblock %} + +{% block masthead %}{% endblock %} + +{% block backlink %} + +{% endblock %} \ No newline at end of file diff --git a/uber/templates/staffing/credits.html b/uber/templates/staffing/credits.html index 5b5939e3c..6fc28def0 100644 --- a/uber/templates/staffing/credits.html +++ b/uber/templates/staffing/credits.html @@ -1,6 +1,6 @@ -{% extends "base.html" %}{% set admin_area=True %} +{% extends 'signup_base.html' %} {% block title %}Volunteer Agreement{% endblock %} -{% block backlink %}{% endblock %} + {% block content %}

Name in Credits

diff --git a/uber/templates/staffing/emergency_procedures.html b/uber/templates/staffing/emergency_procedures.html index 880eafe1b..057aea9d7 100644 --- a/uber/templates/staffing/emergency_procedures.html +++ b/uber/templates/staffing/emergency_procedures.html @@ -1,6 +1,6 @@ -{% extends "base.html" %}{% set admin_area=True %} +{% extends 'signup_base.html' %} {% block title %}Volunteer Agreement{% endblock %} -{% block backlink %}{% endblock %} + {% block content %}

Emergency Procedures

diff --git a/uber/templates/staffing/food_restrictions.html b/uber/templates/staffing/food_restrictions.html index 05f5bd73c..a4cb60ba4 100644 --- a/uber/templates/staffing/food_restrictions.html +++ b/uber/templates/staffing/food_restrictions.html @@ -1,6 +1,6 @@ -{% extends "base.html" %}{% set admin_area=True %} +{% extends 'signup_base.html' %} {% block title %}Dietary Restrictions{% endblock %} -{% block backlink %}{% endblock %} + {% block content %}

Dietary Restrictions

diff --git a/uber/templates/staffing/hotel.html b/uber/templates/staffing/hotel.html index 8829c5790..334a8f56c 100644 --- a/uber/templates/staffing/hotel.html +++ b/uber/templates/staffing/hotel.html @@ -1,6 +1,6 @@ -{% extends "base.html" %}{% set admin_area=True %} +{% extends 'signup_base.html' %} {% block title %}Hotel Requests{% endblock %} -{% block backlink %}{% endblock %} + {% block content %}

Hotel Room Space

diff --git a/uber/templates/staffing/index.html b/uber/templates/staffing/index.html index d69270e2b..ba398cd9d 100644 --- a/uber/templates/staffing/index.html +++ b/uber/templates/staffing/index.html @@ -1,6 +1,6 @@ -{% extends "base.html" %}{% set admin_area=True %} +{% extends 'signup_base.html' %} {% block title %}Volunteer Checklist{% endblock %} -{% block backlink %}{% endblock %} + {% block content %}

Volunteer Checklist for {{ attendee.full_name }}

diff --git a/uber/templates/staffing/onsite_jobs.html b/uber/templates/staffing/onsite_jobs.html index b4370594a..b96c6c04a 100644 --- a/uber/templates/staffing/onsite_jobs.html +++ b/uber/templates/staffing/onsite_jobs.html @@ -1,6 +1,6 @@ -{% extends "base.html" %}{% set admin_area=True %} +{% extends 'signup_base.html' %} {% block title %}On-Site Shifts{% endblock %} -{% block backlink %}{% endblock %} + {% block content %}

All Available Shifts

diff --git a/uber/templates/staffing/shifts.html b/uber/templates/staffing/shifts.html index 5f5e63a6d..287b725c2 100644 --- a/uber/templates/staffing/shifts.html +++ b/uber/templates/staffing/shifts.html @@ -1,121 +1,172 @@ {% extends 'signup_base.html' %} -{% block page_style %} +{% block page_styles %} {% endblock %} {% block main_content %}
{{ csrf_token() }}
- - -
-
- {{ name }}'s available shifts -

View Printable Schedule

-
-
-
-
- (If you are not {{ name }}, log in here.) -
-
-
-
- - {{ macros.popup_link(c.VOLUNTEER_PERKS_URL, "What do I get for volunteering?") }} - - {% if not c.HIDE_SCHEDULE %} - - View the {{ c.EVENT_NAME }} Schedule - + .ec-day.ec-highlight { + background-color: #f8f9fa !important; + } + +{% endblock %} + +{% block content %}
{{ csrf_token() }}
+
+
+

{{ c.CURRENT_VOLUNTEER.first_name }} {{ c.CURRENT_VOLUNTEER.last_name }}'s Available Shifts

+ {% if c.BEFORE_SHIFTS_CREATED %} +

Shifts will be available starting {{ c.SHIFTS_CREATED.astimezone(c.EVENT_TIMEZONE).strftime('%B %-e') }}.

+ {% else %} + {% if not c.HIDE_SCHEDULE %} +

View the {{ c.EVENT_NAME }} Schedule here.

+ {% endif %} +

Click Here to download your shifts in ical format.

+
+
+

+ +

+
+
+

+ You are currently signed up for {{ hours }} weighted hours worth of shifts. +

+

+ Shifts that will overlap with your existing shifts will be hidden, and you will need to drop a shift to view them. +
+ {% if c.AFTER_DROP_SHIFTS_DEADLINE %} + After {{ c.DROP_SHIFTS_DEADLINE|datetime_local }}, you must contact {{ c.STAFF_EMAIL|email_only|email_to_link }} to drop shifts. You may continue to add shifts through the event. + {% else %} + You can drop shifts until {{ c.DROP_SHIFTS_DEADLINE|datetime_local }}. + {% endif %} +

+

+ You are assigned to the following department{{ assigned_depts_labels|length|pluralize }}: {{ assigned_depts_labels|join(' / ') }}. +

+
+
+
+ {% if has_public_jobs %} +
+

+ +

+
+
+

Select "Show Public Shifts" to see and sign up for shifts outside of your assigned departments.

+

+ {% if c.MAX_DEPTS_WHERE_WORKING > 0 %} + You'll be prevented from working in more than {{ c.MAX_DEPTS_WHERE_WORKING }} + department{{ c.MAX_DEPTS_WHERE_WORKING|pluralize }}. + {% endif %} +

+
+
+
{% endif %} -

- You are assigned to the following department{{ assigned_depts_labels|length|pluralize }}: {{ assigned_depts_labels|join(' / ') }}. -
- {% if c.AFTER_SHIFTS_CREATED %} - {% if c.AFTER_DROP_SHIFTS_DEADLINE %} - After {{ c.DROP_SHIFTS_DEADLINE|datetime_local }}, you must contact {{ c.STAFF_EMAIL|email_only|email_to_link }} to drop shifts. You may continue to add shifts through the event. -
- {% else %} - You can drop shifts until {{ c.DROP_SHIFTS_DEADLINE|datetime_local }}.
- {% endif %} - {% if has_public_jobs %} -
- Some of the listed jobs are in departments you are not assigned to - (marked like this public). -
- {% if c.MAX_DEPTS_WHERE_WORKING > 0 %} - You may sign up any of the listed jobs, but you'll be prevented from - working in more than {{ c.MAX_DEPTS_WHERE_WORKING }} - department{{ c.MAX_DEPTS_WHERE_WORKING|pluralize }}. -
- {% endif %} - {% endif %} -
-
- Click Here - - to show public jobs in departments you are not assigned to. - - - to hide public jobs in departments you are not assigned to. - -
+ {% if depts_with_roles %} +
+

+ +

+
+
+

You have been assigned special roles in the following departments: {{ depts_with_roles|join(", ") }}.

+

As a result, some shifts without required roles are hidden by default.

+

You can toggle this behavior with the "Prioritize Restricted Shifts" button below.

+
- {% if depts_with_roles %} -
-
- You have been assigned special roles in the following departments: {{ depts_with_roles|join(", ") }}.
- {% if show_all %} - All available shifts are currently shown regardless of required role.
You can instead - prioritize shifts with required roles. - {% else %} - As a result, some shifts without required roles may be hidden.
You can opt to - show all shifts regardless of required roles. +
+ {% endif %} +
+ +
+
Shift View Options
+
+
+
+ + +
+
+ + +
+
+
+ + +
+ {% if has_public_jobs %} +
+
+ + +
+
+ + +
+
+ {% endif %} + {% if depts_with_roles %} +
+
+ + +
+
+ + +
+
+ {% endif %} + {% if assigned_depts_list|length >= 1 %} +
+ +
{% endif %} -

- {% endif %} - Click Here - to see the {{hours}} weighted hours worth of shifts you are signed up for - to sign up for more shifts; you are currently signed up for {{hours}} weighted hours -
- {% else %} - Shifts will be available starting {{ c.SHIFTS_CREATED.astimezone(c.EVENT_TIMEZONE).strftime('%B %-e') }}. -

- {% endif %} - Click Here to return to the main page of the Volunteer Checklist.
- Click Here to download your shifts in ical format +
+ {% endif %} +
{% if c.AFTER_SHIFTS_CREATED %} +
@@ -123,215 +174,403 @@

View Printable Schedule

{% endif %} {% endblock %} -{% block page_script %} +{% block scripts %} +{{ super() }} {% if c.AFTER_SHIFTS_CREATED %} -{{ "deps/combined.min.js"|serve_static_content }} - + + -{{ "js/moment.js"|serve_static_content }} + {% endif %} {% endblock %} diff --git a/uber/templates/staffing/shirt_size.html b/uber/templates/staffing/shirt_size.html index d0f2d5eb6..2917af454 100644 --- a/uber/templates/staffing/shirt_size.html +++ b/uber/templates/staffing/shirt_size.html @@ -1,6 +1,6 @@ -{% extends "base.html" %}{% set admin_area=True %} +{% extends 'signup_base.html' %} {% block title %}Shirt Size{% endblock %} -{% block backlink %}{% endblock %} + {% block content %}

Tell Us Your Shirt Size

diff --git a/uber/templates/staffing/volunteer.html b/uber/templates/staffing/volunteer.html index b91308cf8..fd947a364 100644 --- a/uber/templates/staffing/volunteer.html +++ b/uber/templates/staffing/volunteer.html @@ -1,6 +1,6 @@ -{% extends "base.html" %}{% set admin_area=True %} +{% extends 'signup_base.html' %} {% block title %}Sign up to Volunteer{% endblock %} -{% block backlink %}{% endblock %} + {% block content %}

Sign up to Volunteer

diff --git a/uber/templates/staffing/volunteer_agreement.html b/uber/templates/staffing/volunteer_agreement.html index 795ce38bd..66d04ec83 100644 --- a/uber/templates/staffing/volunteer_agreement.html +++ b/uber/templates/staffing/volunteer_agreement.html @@ -1,6 +1,6 @@ -{% extends "base.html" %}{% set admin_area=True %} +{% extends 'signup_base.html' %} {% block title %}Volunteer Agreement{% endblock %} -{% block backlink %}{% endblock %} + {% block content %}

Volunteer Agreement