Serves Demo Scene 2 end to end: mc.applicant with the stage pipeline (Applied -> Document Verification -> Interview -> Offered -> Accepted -> Enrolled/Rejected) on mail.thread, a public admission page on the website, and a convert wizard that turns an accepted applicant into a real mc.student + mc.enrollment with zero re-typing. The public form uses Odoo's stock /website/form/<model> mechanism, not a custom controller (CLAUDE.md sec 1.3 - writing a custom version of stock infrastructure is a bug). Verified the real mechanism against core source first rather than assuming: website_hr_recruitment's own data/config_data.xml is the template this follows (ir.model. website_form_access + ir.model.fields.formbuilder_whitelist()). This is the module's actual security boundary, and it's worth being explicit about why it holds. The generic controller creates the record as SUPERUSER - normal ir.model.access rows do not apply to it at all. The only thing stopping a submitter from setting state, student_id, application_no or company_id is that those fields are not in the formbuilder_whitelist() call in data/mc_applicant_website_form_data.xml (every field defaults to website_form_blacklisted=True and stays that way unless explicitly opted in). Confirmed this isn't just theoretical: posted state=enrolled and application_no=HACKED-0001 directly at /website/form/mc.applicant on a live instance, and the resulting record came back with the model's own default state=applied and a server-generated APP20260004 - the injected values were silently dropped, exactly as the whitelist should do. Also exercised a real file upload (birth certificate) and the full convert-to-student path (guardian dedup by email, application_no -> student.application_no, enrollment, attachment reparenting) via odoo shell against the live container, not just read by inspection. mc.student gets a new application_no field (_inherit from this module, not O1 - it only makes sense where admission is installed) so "the application number persists on the student" is a stored fact, not just a claim in the demo script. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
147 lines
10 KiB
XML
147 lines
10 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<odoo>
|
|
<!--
|
|
Hand-written to match the DOM structure/classes of Odoo core's own
|
|
s_website_form snippet (website/views/snippets/s_website_form.xml)
|
|
exactly, so the stock frontend widget (form.js, loaded by every
|
|
website page via website.layout) picks it up automatically - no
|
|
custom JS, no custom submit controller. Posting still goes through
|
|
Odoo's generic /website/form/<model> controller; see
|
|
data/mc_applicant_website_form_data.xml for the field whitelist
|
|
that is the actual security boundary on that path.
|
|
-->
|
|
<template id="admission_apply_page" name="Admission Application">
|
|
<t t-call="website.layout">
|
|
<div id="wrap">
|
|
<section class="s_website_form pt32 pb32" data-snippet="s_website_form">
|
|
<div class="container-fluid">
|
|
<h1 class="mb-4">Apply for Admission</h1>
|
|
<form action="/website/form/" method="post" enctype="multipart/form-data"
|
|
class="o_mark_required" data-mark="*"
|
|
data-model_name="mc.applicant"
|
|
data-success-mode="message">
|
|
<div class="s_website_form_rows row s_col_no_bgcolor">
|
|
|
|
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_required" data-type="char">
|
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_name">
|
|
<span class="s_website_form_label_content">Student Name</span>
|
|
<span class="s_website_form_mark"> *</span>
|
|
</label>
|
|
<div class="col-sm-9">
|
|
<input class="form-control s_website_form_input" type="text"
|
|
name="name" id="admission_name" required="1"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div data-name="Field" class="s_website_form_field mb-3 col-12" data-type="date">
|
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_dob">
|
|
<span class="s_website_form_label_content">Date of Birth</span>
|
|
</label>
|
|
<div class="col-sm-9">
|
|
<input class="form-control s_website_form_input" type="date"
|
|
name="dob" id="admission_dob"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_required" data-type="many2one">
|
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_program_id">
|
|
<span class="s_website_form_label_content">Grade Applied For</span>
|
|
<span class="s_website_form_mark"> *</span>
|
|
</label>
|
|
<div class="col-sm-9">
|
|
<select class="form-select s_website_form_input"
|
|
name="program_id" id="admission_program_id" required="1">
|
|
<option value="">Select a grade...</option>
|
|
<!-- Program display labels are public information (a
|
|
school's grade list is meant to be seen by anyone
|
|
filling this form) - sudo() only reads name/id for
|
|
this dropdown, nothing is written here. -->
|
|
<t t-foreach="request.env['mc.program'].sudo().search([], order='sequence_no')" t-as="program">
|
|
<option t-att-value="program.id" t-esc="program.display_label"/>
|
|
</t>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_required" data-type="char">
|
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_guardian_name">
|
|
<span class="s_website_form_label_content">Parent / Guardian Name</span>
|
|
<span class="s_website_form_mark"> *</span>
|
|
</label>
|
|
<div class="col-sm-9">
|
|
<input class="form-control s_website_form_input" type="text"
|
|
name="guardian_name" id="admission_guardian_name" required="1"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_required" data-type="tel">
|
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_guardian_phone">
|
|
<span class="s_website_form_label_content">Parent / Guardian Phone</span>
|
|
<span class="s_website_form_mark"> *</span>
|
|
</label>
|
|
<div class="col-sm-9">
|
|
<input class="form-control s_website_form_input" type="tel"
|
|
name="guardian_phone" id="admission_guardian_phone" required="1"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_required" data-type="email">
|
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_guardian_email">
|
|
<span class="s_website_form_label_content">Parent / Guardian Email</span>
|
|
<span class="s_website_form_mark"> *</span>
|
|
</label>
|
|
<div class="col-sm-9">
|
|
<input class="form-control s_website_form_input" type="email"
|
|
name="guardian_email" id="admission_guardian_email" required="1"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div data-name="Field" class="s_website_form_field mb-3 col-12" data-type="binary">
|
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_birth_certificate">
|
|
<span class="s_website_form_label_content">Birth Certificate</span>
|
|
</label>
|
|
<div class="col-sm-9">
|
|
<!-- "birth_certificate" matches no real field on mc.applicant,
|
|
so Odoo's generic form controller stores it as a plain
|
|
ir.attachment linked to the new record - exactly the
|
|
"file uploads to ir.attachment" the spec asks for,
|
|
with no dedicated Binary field needed on the model. -->
|
|
<input class="form-control s_website_form_input" type="file"
|
|
name="birth_certificate" id="admission_birth_certificate"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-0 py-2 col-12 s_website_form_submit text-end s_website_form_no_submit_label" data-name="Submit Button">
|
|
<div class="s_website_form_label"/>
|
|
<span id="s_website_form_result"/>
|
|
<a href="#" role="button" class="btn btn-primary s_website_form_send">Submit Application</a>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</t>
|
|
</template>
|
|
|
|
<record id="website_page_admission_apply" model="website.page">
|
|
<field name="url">/admissions/apply</field>
|
|
<field name="view_id" ref="admission_apply_page"/>
|
|
<field name="is_published">True</field>
|
|
</record>
|
|
</odoo>
|