220 lines
9.1 KiB
Python
220 lines
9.1 KiB
Python
# pyrefly: ignore [missing-import]
|
|
from odoo import http, fields
|
|
# pyrefly: ignore [missing-import]
|
|
from odoo.http import request
|
|
# pyrefly: ignore [missing-import]
|
|
from odoo.addons.portal.controllers.portal import CustomerPortal
|
|
|
|
|
|
class HostelPortalController(CustomerPortal):
|
|
"""Student portal controller for hostel management.
|
|
|
|
SECURITY DESIGN:
|
|
- Identity is ALWAYS derived from request.env.user.student_id
|
|
- URL-supplied IDs (complaint_id, etc.) are NEVER trusted for authorization
|
|
- All lookups use additional domain [('student_id', '=', student.id)]
|
|
- Record rules provide a second layer of enforcement at ORM level
|
|
|
|
sudo() AUDIT:
|
|
- sudo() is NOT used in any route below
|
|
- Portal user has read access to own records via record rules
|
|
- Create (complaint, visitor) is allowed via group_portal model access
|
|
with record-rule enforcement on student_id.portal_user_id = user.id
|
|
"""
|
|
|
|
def _get_student(self):
|
|
"""Authoritative student lookup — NEVER trust URL parameters."""
|
|
return request.env.user.student_id
|
|
|
|
def _get_hostel_member(self):
|
|
"""Returns the student's active allocation (if any)."""
|
|
student = self._get_student()
|
|
if not student:
|
|
return request.env['education.hostel.member'].browse()
|
|
return request.env['education.hostel.member'].search([
|
|
('student_id', '=', student.id),
|
|
('status', '=', 'active'),
|
|
], limit=1)
|
|
|
|
def _prepare_home_portal_values(self, counters):
|
|
values = super()._prepare_home_portal_values(counters)
|
|
student = self._get_student()
|
|
if student:
|
|
member = self._get_hostel_member()
|
|
values['hostel_member_count'] = 1 if member else 0
|
|
return values
|
|
|
|
# ── /my/hostel ────────────────────────────────────────────────────────
|
|
|
|
@http.route(['/my/hostel'], type='http', auth='user', website=True)
|
|
def hostel_dashboard(self, **kw):
|
|
student = self._get_student()
|
|
if not student:
|
|
return request.render('education_portal.portal_no_student')
|
|
|
|
member = self._get_hostel_member()
|
|
|
|
# Complaints — only student's own (record rule also enforces)
|
|
complaints = request.env['education.hostel.complaint'].search([
|
|
('student_id', '=', student.id),
|
|
], order='submitted_date desc', limit=5)
|
|
|
|
# Fees — only student's own
|
|
fees = request.env['education.hostel.fee'].search([
|
|
('student_id', '=', student.id),
|
|
('state', 'in', ('due', 'overdue')),
|
|
], order='due_date asc', limit=5)
|
|
|
|
# Recent visitors — only student's own
|
|
visitors = request.env['education.hostel.visitor'].search([
|
|
('student_id', '=', student.id),
|
|
], order='visit_date desc', limit=3)
|
|
|
|
return request.render('education_hostel.portal_hostel_dashboard', {
|
|
'student': student,
|
|
'member': member,
|
|
'complaints': complaints,
|
|
'fees': fees,
|
|
'visitors': visitors,
|
|
'page_name': 'hostel_dashboard',
|
|
})
|
|
|
|
# ── /my/hostel/allocation ─────────────────────────────────────────────
|
|
|
|
@http.route(['/my/hostel/allocation'], type='http', auth='user', website=True)
|
|
def hostel_allocation(self, **kw):
|
|
student = self._get_student()
|
|
if not student:
|
|
return request.render('education_portal.portal_no_student')
|
|
|
|
member = self._get_hostel_member()
|
|
if not member:
|
|
return request.render('education_hostel.portal_hostel_no_allocation')
|
|
|
|
return request.render('education_hostel.portal_hostel_allocation', {
|
|
'student': student,
|
|
'member': member,
|
|
'page_name': 'hostel_allocation',
|
|
})
|
|
|
|
# ── /my/hostel/complaints ─────────────────────────────────────────────
|
|
|
|
@http.route(['/my/hostel/complaints'], type='http', auth='user', website=True)
|
|
def hostel_complaints(self, **kw):
|
|
student = self._get_student()
|
|
if not student:
|
|
return request.render('education_portal.portal_no_student')
|
|
|
|
complaints = request.env['education.hostel.complaint'].search([
|
|
('student_id', '=', student.id),
|
|
], order='submitted_date desc')
|
|
|
|
return request.render('education_hostel.portal_hostel_complaints', {
|
|
'student': student,
|
|
'complaints': complaints,
|
|
'page_name': 'hostel_complaints',
|
|
})
|
|
|
|
# ── /my/hostel/complaint/new ──────────────────────────────────────────
|
|
|
|
@http.route(['/my/hostel/complaint/new'], type='http', auth='user', website=True,
|
|
methods=['GET', 'POST'])
|
|
def hostel_complaint_new(self, **post):
|
|
student = self._get_student()
|
|
if not student:
|
|
return request.render('education_portal.portal_no_student')
|
|
|
|
member = self._get_hostel_member()
|
|
if not member:
|
|
return request.redirect('/my/hostel')
|
|
|
|
if request.httprequest.method == 'POST':
|
|
title = post.get('title', '').strip()
|
|
category = post.get('category', 'other')
|
|
description = post.get('description', '').strip()
|
|
|
|
if not title:
|
|
return request.render('education_hostel.portal_hostel_complaint_form', {
|
|
'student': student,
|
|
'member': member,
|
|
'error': 'Complaint title is required.',
|
|
'post': post,
|
|
'page_name': 'hostel_complaint_new',
|
|
})
|
|
|
|
# Create complaint — student_id is set server-side, NEVER from POST
|
|
complaint = request.env['education.hostel.complaint'].create({
|
|
'student_id': student.id, # server-derived identity
|
|
'hostel_id': member.hostel_id.id,
|
|
'room_id': member.room_id.id,
|
|
'category': category,
|
|
'title': title,
|
|
'description': description,
|
|
'state': 'submitted',
|
|
'submitted_date': fields.Date.today(),
|
|
'company_id': student.company_id.id,
|
|
})
|
|
return request.redirect('/my/hostel/complaints')
|
|
|
|
return request.render('education_hostel.portal_hostel_complaint_form', {
|
|
'student': student,
|
|
'member': member,
|
|
'page_name': 'hostel_complaint_new',
|
|
})
|
|
|
|
# ── /my/hostel/fees ───────────────────────────────────────────────────
|
|
|
|
@http.route(['/my/hostel/fees'], type='http', auth='user', website=True)
|
|
def hostel_fees(self, **kw):
|
|
student = self._get_student()
|
|
if not student:
|
|
return request.render('education_portal.portal_no_student')
|
|
|
|
fees = request.env['education.hostel.fee'].search([
|
|
('student_id', '=', student.id),
|
|
], order='due_date desc')
|
|
|
|
return request.render('education_hostel.portal_hostel_fees', {
|
|
'student': student,
|
|
'fees': fees,
|
|
'page_name': 'hostel_fees',
|
|
})
|
|
|
|
# ── /my/hostel/visitors ───────────────────────────────────────────────
|
|
|
|
@http.route(['/my/hostel/visitors'], type='http', auth='user', website=True,
|
|
methods=['GET', 'POST'])
|
|
def hostel_visitors(self, **post):
|
|
student = self._get_student()
|
|
if not student:
|
|
return request.render('education_portal.portal_no_student')
|
|
|
|
member = self._get_hostel_member()
|
|
|
|
if request.httprequest.method == 'POST' and member:
|
|
visitor_name = post.get('visitor_name', '').strip()
|
|
if visitor_name:
|
|
request.env['education.hostel.visitor'].create({
|
|
'student_id': student.id, # server-derived identity
|
|
'hostel_id': member.hostel_id.id,
|
|
'visitor_name': visitor_name,
|
|
'relationship': post.get('relationship', '').strip(),
|
|
'phone': post.get('phone', '').strip(),
|
|
'purpose': post.get('purpose', '').strip(),
|
|
'visit_date': post.get('visit_date') or fields.Date.today(),
|
|
'state': 'pending',
|
|
'company_id': student.company_id.id,
|
|
})
|
|
return request.redirect('/my/hostel/visitors')
|
|
|
|
visitors = request.env['education.hostel.visitor'].search([
|
|
('student_id', '=', student.id),
|
|
], order='visit_date desc')
|
|
|
|
return request.render('education_hostel.portal_hostel_visitors', {
|
|
'student': student,
|
|
'member': member,
|
|
'visitors': visitors,
|
|
'page_name': 'hostel_visitors',
|
|
})
|