341 lines
14 KiB
Python
341 lines
14 KiB
Python
# -*- 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'])
|
|
|
|
attended = present + on_duty + late + medical_leave
|
|
percentage = round((attended / total_sessions * 100.0), 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_str = kwargs.get('from_date')
|
|
to_date_str = kwargs.get('to_date')
|
|
from_date = None
|
|
to_date = None
|
|
|
|
if from_date_str:
|
|
from_date = datetime.strptime(from_date_str, '%Y-%m-%d').date()
|
|
if to_date_str:
|
|
to_date = datetime.strptime(to_date_str, '%Y-%m-%d').date()
|
|
|
|
if from_date and to_date and from_date > to_date:
|
|
return json_response(False, error_code="INVALID_DATE_RANGE", error_message="'from_date' cannot be later than 'to_date'", status=400)
|
|
|
|
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', 'remarks'],
|
|
order='date desc, id desc',
|
|
limit=100
|
|
)
|
|
|
|
formatted_records = []
|
|
for r in records:
|
|
formatted_records.append({
|
|
"id": r["id"],
|
|
"date": str(r["date"]),
|
|
"status": r["status"],
|
|
"remarks": r.get("remarks") or ""
|
|
})
|
|
|
|
return json_response(True, data={
|
|
"summary": summary,
|
|
"records": formatted_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)
|