Aug 6 - Update enrollment page
This commit is contained in:
+130
-37
@@ -5,32 +5,50 @@ The JQC Enrollment Form, expressed as data.
|
||||
|
||||
This is the SINGLE source of truth for the form's shape. The public template
|
||||
renders from it, the POST handler parses against it, and the admin detail view
|
||||
re-renders a stored submission through it. Changing a task label or adding an
|
||||
inspector column is a one-line edit here — no template or parser change.
|
||||
re-renders a stored submission through it. Changing a task label or adding a
|
||||
role is a one-line edit here — no template or parser change.
|
||||
|
||||
Deliberately free of any app model / DB import: the enrollment form describes
|
||||
what a prospective customer *wants set up*, not anything that exists in the
|
||||
system yet. Keep it that way (see app/enrollment/__init__.py).
|
||||
system yet. Keep it that way (see app/enrollment/__init__.py). The ROLES below
|
||||
happen to mirror the app's user roles, but they are a COPY on purpose — the
|
||||
public form must not import the User model.
|
||||
"""
|
||||
|
||||
# ── Step 1 columns ───────────────────────────────────────────────────────────
|
||||
# key -> display label. 'admin' is the Admin/Director column; the rest are the
|
||||
# five inspector seats on the printed form.
|
||||
COLUMNS = [
|
||||
('admin', 'Admin /\nDirector'),
|
||||
('inspector_1', 'User /\nInspector 1'),
|
||||
('inspector_2', 'User /\nInspector 2'),
|
||||
('inspector_3', 'User /\nInspector 3'),
|
||||
('inspector_4', 'User /\nInspector 4'),
|
||||
('inspector_5', 'User /\nInspector 5'),
|
||||
# ── Roles a person can be enrolled as ────────────────────────────────────────
|
||||
# key -> label, shown in the Step 1 role dropdown.
|
||||
ROLES = [
|
||||
('admin', 'Admin'),
|
||||
('director', 'Director'),
|
||||
('auditor', 'Auditor'),
|
||||
('inspector', 'Inspector'),
|
||||
('external_inspector', 'External Inspector'),
|
||||
]
|
||||
|
||||
INSPECTOR_COLUMNS = [c for c in COLUMNS if c[0] != 'admin']
|
||||
ROLE_LABELS = dict(ROLES)
|
||||
ROLE_KEYS = [k for k, _ in ROLES]
|
||||
|
||||
#: Roles that act on the administrative side of the printed form (the
|
||||
#: "Admin / Director" column). Everything else is an inspector seat. Drives
|
||||
#: both the recommendation preset and eligibility for admin-only tasks.
|
||||
ADMIN_ROLES = {'admin', 'director', 'auditor'}
|
||||
|
||||
#: Role pre-selected for the first row — the form starts with one
|
||||
#: administrative contact, as on the printed sheet.
|
||||
DEFAULT_FIRST_ROLE = 'admin'
|
||||
|
||||
#: Upper bound on people per submission. Generous for a real enrollment, but
|
||||
#: bounded so a scripted POST cannot make us build an unbounded matrix.
|
||||
MAX_PEOPLE = 25
|
||||
|
||||
|
||||
# ── Step 1 rows ──────────────────────────────────────────────────────────────
|
||||
# ref, label, columns_offered. Ref 10 (Search/Export Reports) is an
|
||||
# Admin/Director-only capability on the printed form, so it offers one cell.
|
||||
def is_admin_role(role):
|
||||
return role in ADMIN_ROLES
|
||||
|
||||
|
||||
# ── Task rows ────────────────────────────────────────────────────────────────
|
||||
# ref, label, scope. scope 'admin_only' means the cell is offered only to
|
||||
# people in an ADMIN_ROLES role (ref 10 on the printed form).
|
||||
TASKS = [
|
||||
(1, 'Receive new inspection submitted notification', 'all'),
|
||||
(2, 'Receive issue-related notification', 'all'),
|
||||
@@ -44,30 +62,26 @@ TASKS = [
|
||||
(10, 'Search/Export Reports (inspection/issue)', 'admin_only'),
|
||||
]
|
||||
|
||||
|
||||
def columns_for(scope):
|
||||
"""Return the column list a task row offers."""
|
||||
return COLUMNS if scope == 'all' else [('admin', 'Admin /\nDirector')]
|
||||
TASK_LABELS = {ref: label for ref, label, _ in TASKS}
|
||||
|
||||
|
||||
# ── Step 2 registrants ───────────────────────────────────────────────────────
|
||||
# key -> row label on the printed form.
|
||||
REGISTRANTS = [
|
||||
('admin', 'Administrative Roles (Admin/Director/Auditor)'),
|
||||
('inspector_1', 'User / Inspector 1'),
|
||||
('inspector_2', 'User / Inspector 2'),
|
||||
('inspector_3', 'User / Inspector 3'),
|
||||
('inspector_4', 'User / Inspector 4'),
|
||||
('inspector_5', 'User / Inspector 5'),
|
||||
]
|
||||
def task_applies(scope, role):
|
||||
"""True when a task row offers a checkbox to someone in `role`."""
|
||||
return scope == 'all' or is_admin_role(role)
|
||||
|
||||
|
||||
# ── Step 3 ───────────────────────────────────────────────────────────────────
|
||||
MOBILE_APP_LABEL = 'JQC Mobile App For Smart Device'
|
||||
|
||||
|
||||
# ── Recommendation block (static reference, not an input) ────────────────────
|
||||
# ref -> (admin_recommended, inspector_recommended). None = no cell on the form.
|
||||
# ── Recommended defaults ─────────────────────────────────────────────────────
|
||||
# ref -> (recommended for admin-side roles, recommended for inspector roles).
|
||||
# None = the row offers that side no cell.
|
||||
#
|
||||
# The printed form showed this as a separate RECOMMENDATION table for the
|
||||
# customer to copy by hand. It is now applied by the "Recommendation selection"
|
||||
# button instead, so the table is no longer rendered — but this mapping is
|
||||
# still the authority, and is handed to the page as JSON.
|
||||
RECOMMENDATION = {
|
||||
1: (False, True),
|
||||
2: (False, True),
|
||||
@@ -81,10 +95,30 @@ RECOMMENDATION = {
|
||||
10: (True, None),
|
||||
}
|
||||
|
||||
RECOMMENDATION_INTRO = (
|
||||
'To prevent the administrator or director from receiving an overwhelming '
|
||||
'number of email notifications, we recommend the following:'
|
||||
)
|
||||
|
||||
def recommendation_for(role):
|
||||
"""Return {task_ref: bool} — the recommended preset for one role.
|
||||
|
||||
Rows that offer this role no cell are omitted rather than set False, so
|
||||
the caller never ticks a checkbox that does not exist.
|
||||
"""
|
||||
admin_side = is_admin_role(role)
|
||||
preset = {}
|
||||
for ref, _label, scope in TASKS:
|
||||
if not task_applies(scope, role):
|
||||
continue
|
||||
rec = RECOMMENDATION.get(ref, (False, False))
|
||||
value = rec[0] if admin_side else rec[1]
|
||||
if value is None:
|
||||
continue
|
||||
preset[ref] = bool(value)
|
||||
return preset
|
||||
|
||||
|
||||
def recommendation_map():
|
||||
"""{role_key: {task_ref: bool}} for every role — serialised to the page."""
|
||||
return {role: recommendation_for(role) for role in ROLE_KEYS}
|
||||
|
||||
|
||||
NOTES = [
|
||||
'Each user will receive instructions on how to sign up and install the app '
|
||||
@@ -108,3 +142,62 @@ STATUS_LABELS = {
|
||||
'in_progress': 'In Progress',
|
||||
'completed': 'Completed',
|
||||
}
|
||||
|
||||
|
||||
# ── Legacy record support ────────────────────────────────────────────────────
|
||||
# Submissions taken before the form moved to free-form people used six fixed
|
||||
# seats. Stored files are never rewritten, so the admin views normalise on
|
||||
# read instead — one shape to render, whichever format is on disk.
|
||||
_LEGACY_SEAT_ROLES = {
|
||||
'admin': 'admin',
|
||||
'inspector_1': 'inspector',
|
||||
'inspector_2': 'inspector',
|
||||
'inspector_3': 'inspector',
|
||||
'inspector_4': 'inspector',
|
||||
'inspector_5': 'inspector',
|
||||
}
|
||||
|
||||
|
||||
def people_of(record):
|
||||
"""Return a submission's people as a uniform list, old format or new.
|
||||
|
||||
Each entry: {key, role, role_label, name, job_title, email}.
|
||||
"""
|
||||
if record.get('people'):
|
||||
out = []
|
||||
for p in record['people']:
|
||||
role = p.get('role', 'inspector')
|
||||
out.append({
|
||||
'key': p.get('key', ''),
|
||||
'role': role,
|
||||
'role_label': ROLE_LABELS.get(role, role.replace('_', ' ').title()),
|
||||
'name': p.get('name', ''),
|
||||
'job_title': p.get('job_title', ''),
|
||||
'email': p.get('email', ''),
|
||||
})
|
||||
return out
|
||||
|
||||
# Legacy: fixed seats under 'registrants'.
|
||||
out = []
|
||||
for reg in record.get('registrants', []):
|
||||
if not (reg.get('name') or reg.get('email')):
|
||||
continue
|
||||
role = _LEGACY_SEAT_ROLES.get(reg.get('key'), 'inspector')
|
||||
out.append({
|
||||
'key': reg.get('key', ''),
|
||||
'role': role,
|
||||
'role_label': ROLE_LABELS.get(role, role.title()),
|
||||
'name': reg.get('name', ''),
|
||||
'job_title': reg.get('job_title', ''),
|
||||
'email': reg.get('email', ''),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def cell(record, ref, person_key):
|
||||
"""True when `person_key` was ticked for task `ref` in this submission."""
|
||||
return bool(record.get('matrix', {}).get(str(ref), {}).get(person_key))
|
||||
|
||||
|
||||
def wants_mobile(record, person_key):
|
||||
return bool(record.get('mobile_app', {}).get(person_key))
|
||||
|
||||
Reference in New Issue
Block a user