# -*- coding: utf-8 -*- import json import logging from datetime import datetime, date # pyrefly: ignore [missing-import] from odoo import http # pyrefly: ignore [missing-import] from odoo.http import request _logger = logging.getLogger(__name__) def json_response(success, data=None, error_code=None, error_message=None, status=200): if success: body = { "success": True, "data": data or {} } else: body = { "success": False, "error": { "code": error_code or "ERROR", "message": error_message or "An error occurred" } } return request.make_response( json.dumps(body, default=str), headers=[('Content-Type', 'application/json')], status=status ) def require_student(func): def wrapper(self, *args, **kwargs): if request.env.user._is_public(): return json_response(False, error_code="AUTH_REQUIRED", error_message="Authentication required", status=401) student = request.env.user.sudo().student_id if not student: return json_response(False, error_code="STUDENT_NOT_LINKED", error_message="No student profile is linked to this account", status=403) return func(self, student, *args, **kwargs) return wrapper def format_float_time(float_time): hours = int(float_time) minutes = int(round((float_time - hours) * 60)) if minutes == 60: minutes = 0 hours += 1 hours = hours % 24 am_pm = 'AM' if hours < 12 else 'PM' display_hours = hours % 12 if display_hours == 0: display_hours = 12 return f"{display_hours:02d}:{minutes:02d} {am_pm}" class MobileStudent(http.Controller): def _get_rel(self, record): if record: return {"id": record.id, "name": record.name} return None @http.route('/api/mobile/v1/student/profile', type='http', auth='public', methods=['GET'], cors='*') @require_student def get_profile(self, student, **kwargs): try: data = { "id": student.id, "student_id": student.student_id, "admission_number": student.admission_number, "roll_number": student.roll_number, "full_name": student.full_name, "email": student.email, "mobile": student.mobile, "gender": student.gender, "date_of_birth": student.date_of_birth, "status": student.status, "campus": self._get_rel(student.campus_id), "department": self._get_rel(student.department_id), "program": self._get_rel(student.program_id), "level": self._get_rel(student.level_id), "academic_year": self._get_rel(student.academic_year_id), "academic_term": self._get_rel(student.academic_term_id), "batch": self._get_rel(student.batch_id), "semester": self._get_rel(student.semester_id), "section": self._get_rel(student.section_id) } return json_response(True, data=data) except Exception as e: return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500) def _get_attendance_summary(self, student, from_date=None, to_date=None): domain = [('student_id', '=', student.id)] if from_date: domain.append(('date', '>=', from_date)) if to_date: domain.append(('date', '<=', to_date)) records = request.env['education.attendance'].search(domain) total_sessions = len(records) present = len([a for a in records if a.status == 'present']) absent = len([a for a in records if a.status == 'absent']) late = len([a for a in records if a.status == 'late']) medical_leave = len([a for a in records if a.status == 'medical_leave']) on_duty = len([a for a in records if a.status == 'on_duty']) effective_present = present + on_duty + (late * 0.5) percentage = round((effective_present / total_sessions * 100), 1) if total_sessions > 0 else 100.0 return { "percentage": percentage, "present": present, "absent": absent, "late": late, "medical_leave": medical_leave, "on_duty": on_duty } def _get_timetable_slots(self, student, date_filter=None): if not student.program_id or not student.academic_year_id: return [] domain = [ ('program_id', '=', student.program_id.id), ('academic_year_id', '=', student.academic_year_id.id), ('active', '=', True) ] if student.academic_term_id: domain += ['|', ('term_id', '=', False), ('term_id', '=', student.academic_term_id.id)] else: domain += [('term_id', '=', False)] if student.batch_id: domain += ['|', ('batch_id', '=', False), ('batch_id', '=', student.batch_id.id)] else: domain += [('batch_id', '=', False)] if date_filter: domain += [('date', '=', date_filter)] slots = request.env['education.timetable.slot'].sudo().search(domain, order="date asc, start_time asc") return [{ "id": s.id, "date": s.date, "day": s.date.strftime('%A') if s.date else '', "start_time": format_float_time(s.start_time), "end_time": format_float_time(s.end_time), "subject": self._get_rel(s.subject_id), "faculty": self._get_rel(s.faculty_id), "room": self._get_rel(s.room_id) } for s in slots] @http.route('/api/mobile/v1/student/dashboard', type='http', auth='public', methods=['GET'], cors='*') @require_student def get_dashboard(self, student, **kwargs): try: today = date.today() # Upcoming Exams exams = request.env['education.exam'].sudo().search_read([ ('state', 'in', ['scheduled', 'ongoing']), ('program_id', '=', student.program_id.id), ('academic_year_id', '=', student.academic_year_id.id), ], fields=['id', 'name', 'state', 'start_date'], order='start_date asc', limit=5) # Announcements ann_records = request.env['education.announcement'].sudo().search( [('active', '=', True)], order='id desc', limit=5 ) announcements = [] for a in ann_records: ann_title = getattr(a, 'title', None) or getattr(a, 'name', 'Announcement') ann_date = getattr(a, 'date_published', None) or getattr(a, 'date', None) or '' ann_content = getattr(a, 'content', None) or getattr(a, 'summary', None) or '' announcements.append({ 'id': a.id, 'name': ann_title, 'date': str(ann_date), 'summary': str(ann_content)[:100] }) # Recent Results results = request.env['education.exam.result'].sudo().search([ ('student_id', '=', student.id), ('published', '=', True) ], order='id desc', limit=5) results_data = [] for r in results: results_data.append({ "id": r.id, "exam": self._get_rel(r.exam_id), "percentage": r.percentage, "grade": r.grade_id.name if r.grade_id else None, "status": r.result_status }) data = { "student": { "id": student.id, "full_name": student.full_name, "program": self._get_rel(student.program_id) }, "attendance": self._get_attendance_summary(student), "today_timetable": self._get_timetable_slots(student, date_filter=today), "upcoming_exams": exams, "recent_results": results_data, "announcements": announcements } return json_response(True, data=data) except Exception as e: return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500) @http.route('/api/mobile/v1/student/attendance', type='http', auth='public', methods=['GET'], cors='*') @require_student def get_attendance(self, student, **kwargs): try: from_date = kwargs.get('from_date') to_date = kwargs.get('to_date') if from_date: from_date = datetime.strptime(from_date, '%Y-%m-%d').date() if to_date: to_date = datetime.strptime(to_date, '%Y-%m-%d').date() summary = self._get_attendance_summary(student, from_date, to_date) domain = [('student_id', '=', student.id)] if from_date: domain.append(('date', '>=', from_date)) if to_date: domain.append(('date', '<=', to_date)) records = request.env['education.attendance'].search_read( domain, fields=['id', 'date', 'status'], order='date desc, id desc', limit=100 ) return json_response(True, data={ "summary": summary, "records": records }) except ValueError: return json_response(False, error_code="INVALID_DATE", error_message="Dates must be in YYYY-MM-DD format", status=400) except Exception as e: return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500) @http.route('/api/mobile/v1/student/timetable', type='http', auth='public', methods=['GET'], cors='*') @require_student def get_timetable(self, student, **kwargs): try: slots = self._get_timetable_slots(student) return json_response(True, data={"slots": slots}) except Exception as e: return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500) @http.route('/api/mobile/v1/student/exams', type='http', auth='public', methods=['GET'], cors='*') @require_student def get_exams(self, student, **kwargs): try: exams = request.env['education.exam'].sudo().search([ ('program_id', '=', student.program_id.id), ('academic_year_id', '=', student.academic_year_id.id), ], order='start_date desc') exams_data = [{ "id": exam.id, "name": exam.name, "state": exam.state, "start_date": exam.start_date, "end_date": exam.end_date, "program": self._get_rel(exam.program_id), "academic_year": self._get_rel(exam.academic_year_id) } for exam in exams] return json_response(True, data={"exams": exams_data}) except Exception as e: return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500) @http.route('/api/mobile/v1/student/results', type='http', auth='public', methods=['GET'], cors='*') @require_student def get_results(self, student, **kwargs): try: results = request.env['education.exam.result'].sudo().search([ ('student_id', '=', student.id), ('published', '=', True) ], order='id desc') results_data = [] for r in results: lines = [] for line in r.line_ids: lines.append({ "subject": self._get_rel(line.subject_id), "max_marks": line.max_marks, "pass_marks": line.pass_marks, "obtained_marks": line.obtained_marks, "percentage": line.percentage, "grade": line.grade_id.name if line.grade_id else None, "status": line.status }) results_data.append({ "id": r.id, "exam": self._get_rel(r.exam_id), "total_max_marks": r.total_max_marks, "total_obtained_marks": r.total_obtained_marks, "percentage": r.percentage, "grade": r.grade_id.name if r.grade_id else None, "status": r.result_status, "published_date": r.published_date, "lines": lines }) return json_response(True, data={"results": results_data}) except Exception as e: return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500)