metatroncubeswdev e05448b939 feat(community_school): multi-step registration, waitlist, LMS glue (Session 3-C)
Adds the multi-step website registration at /school/register (parent ->
student -> class), carrying state across steps in the request session
(auth='public' - a family with no account yet can register). Finalizing
creates/finds the parent partner by email, creates the child partner +
student, and creates the enrollment as 'enrolled' or 'waitlist' depending
on whether the chosen class.fee_product_id/max_students still has room -
generating a draft fee invoice when the class has a fee (a new
class.fee/fee_product_id, following the same auto-created-product pattern
as community_membership's tiers).

LMS glue: enrollment create/write now auto-enrols the student's partner
into the class's slide.channel via _action_add_members() when state
becomes 'enrolled', and deactivates the slide.channel.partner membership
on withdrawal. An hourly cron (_cron_promote_waitlist) fills freed seats
from the waitlist in enrollment-date order and emails the parent.

Adds scripts/migrate_classroom.py: a standalone, dependency-free (stdlib
only) JSON-RPC script that reads a title,url CSV and creates slide.slide
records in a target channel - YouTube links become published video slides
(source_type='external' lets Odoo's own compute fields resolve youtube_id
automatically), everything else becomes an unpublished document slide
flagged for manual re-upload/review, since the script can't read Google
Drive content itself. Idempotent by (channel_id, title); --dry-run and
--force supported.

Verified against a live Odoo 19 + Postgres 16 container: 14/14 automated
tests pass, plus full manual live runs - walked all three registration
steps over real HTTP (with CSRF tokens) and confirmed the resulting
enrollment and draft invoice; ran the migration script twice against a
real channel and confirmed the second run skipped both already-created
slides, with the YouTube slide's youtube_id correctly auto-derived.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 21:42:51 -04:00

87 lines
3.9 KiB
Python

from odoo import http
from odoo.http import request
SESSION_PARENT_KEY = 'school_reg_parent'
SESSION_STUDENT_KEY = 'school_reg_student'
class SchoolRegistrationController(http.Controller):
@http.route(['/school/register'], type='http', auth='public', website=True)
def register_parent(self, **kwargs):
if http.request.httprequest.method == 'POST':
request.session[SESSION_PARENT_KEY] = {
'name': kwargs.get('name', '').strip(),
'email': kwargs.get('email', '').strip(),
'phone': kwargs.get('phone', '').strip(),
}
return request.redirect('/school/register/student')
return request.render('community_school.registration_step_parent', {})
@http.route(['/school/register/student'], type='http', auth='public', website=True)
def register_student(self, **kwargs):
if not request.session.get(SESSION_PARENT_KEY):
return request.redirect('/school/register')
if request.httprequest.method == 'POST':
request.session[SESSION_STUDENT_KEY] = {
'name': kwargs.get('name', '').strip(),
'date_of_birth': kwargs.get('date_of_birth') or False,
'health_notes': kwargs.get('health_notes', '').strip(),
'emergency_contact_name': kwargs.get('emergency_contact_name', '').strip(),
'emergency_contact_phone': kwargs.get('emergency_contact_phone', '').strip(),
}
return request.redirect('/school/register/class')
return request.render('community_school.registration_step_student', {})
@http.route(['/school/register/class'], type='http', auth='public', website=True)
def register_class(self, **kwargs):
if not request.session.get(SESSION_PARENT_KEY) or not request.session.get(SESSION_STUDENT_KEY):
return request.redirect('/school/register')
if request.httprequest.method == 'POST':
return self._finalize_registration(int(kwargs.get('class_id', 0)))
classes = request.env['community.school.class'].sudo().search([('state', '=', 'open')])
return request.render('community_school.registration_step_class', {'classes': classes})
def _finalize_registration(self, class_id):
Partner = request.env['res.partner'].sudo()
parent_data = request.session[SESSION_PARENT_KEY]
student_data = request.session[SESSION_STUDENT_KEY]
parent = Partner.search([('email', '=', parent_data['email'])], limit=1) if parent_data['email'] else Partner
if not parent:
parent = Partner.create({
'name': parent_data['name'],
'email': parent_data['email'],
'phone': parent_data['phone'],
})
child_partner = Partner.create({'name': student_data['name']})
student = request.env['community.school.student'].sudo().create({
'partner_id': child_partner.id,
'parent_partner_id': parent.id,
'date_of_birth': student_data['date_of_birth'],
'health_notes': student_data['health_notes'],
'emergency_contact_name': student_data['emergency_contact_name'],
'emergency_contact_phone': student_data['emergency_contact_phone'],
})
klass = request.env['community.school.class'].sudo().browse(class_id)
state = 'enrolled' if klass.enrolled_count < klass.max_students else 'waitlist'
enrollment = request.env['community.school.enrollment'].sudo().create({
'student_id': student.id,
'class_id': klass.id,
'state': state,
})
if state == 'enrolled' and klass.fee:
enrollment._create_fee_invoice()
request.session.pop(SESSION_PARENT_KEY, None)
request.session.pop(SESSION_STUDENT_KEY, None)
return request.render('community_school.registration_done', {
'enrollment': enrollment,
'waitlisted': state == 'waitlist',
})