Mobile App - additions
This commit is contained in:
parent
cead33cbfc
commit
7f352fe899
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -137,7 +137,8 @@ def generate_demo_data(env):
|
||||
'program_id': program.id,
|
||||
'campus_id': campus.id,
|
||||
'academic_year_id': year.id,
|
||||
'status': 'active'
|
||||
'status': 'active',
|
||||
'portal_user_id': student_user.id
|
||||
})
|
||||
|
||||
# Map student ID to portal user if required by ERP architecture
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3
addons/education_mobile_api/__init__.py
Normal file
3
addons/education_mobile_api/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
from . import controllers
|
||||
18
addons/education_mobile_api/__manifest__.py
Normal file
18
addons/education_mobile_api/__manifest__.py
Normal file
@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Education Mobile API',
|
||||
'version': '1.0',
|
||||
'summary': 'REST API for Mobile Application',
|
||||
'description': """
|
||||
Dedicated REST API layer in Odoo specifically for the mobile application.
|
||||
""",
|
||||
'category': 'Education',
|
||||
'author': 'Antigravity',
|
||||
'depends': ['base', 'web', 'education_portal'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
}
|
||||
BIN
addons/education_mobile_api/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
addons/education_mobile_api/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
3
addons/education_mobile_api/controllers/__init__.py
Normal file
3
addons/education_mobile_api/controllers/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import auth
|
||||
from . import student
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
106
addons/education_mobile_api/controllers/auth.py
Normal file
106
addons/education_mobile_api/controllers/auth.py
Normal file
@ -0,0 +1,106 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import logging
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.exceptions import AccessError, AccessDenied
|
||||
|
||||
_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),
|
||||
headers=[('Content-Type', 'application/json')],
|
||||
status=status
|
||||
)
|
||||
|
||||
class MobileAuth(http.Controller):
|
||||
|
||||
@http.route('/api/mobile/v1/auth/login', type='http', auth='none', methods=['POST'], csrf=False, cors='*')
|
||||
def login(self, **kwargs):
|
||||
try:
|
||||
data = json.loads(request.httprequest.data)
|
||||
email = data.get('email')
|
||||
password = data.get('password')
|
||||
|
||||
if not email or not password:
|
||||
return json_response(False, error_code="INVALID_REQUEST", error_message="Email and password are required", status=400)
|
||||
|
||||
db = request.db or request.env.registry.db_name
|
||||
|
||||
# Authenticate creates a session securely
|
||||
credential = {'login': email, 'password': password, 'type': 'password'}
|
||||
auth_info = request.session.authenticate(request.env, credential)
|
||||
uid = auth_info.get('uid')
|
||||
if not uid:
|
||||
return json_response(False, error_code="AUTH_FAILED", error_message="Invalid email or password", status=401)
|
||||
|
||||
request.update_env(user=uid)
|
||||
user = request.env['res.users'].sudo().browse(uid)
|
||||
student = user.student_id
|
||||
|
||||
return json_response(True, data={
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"name": user.name,
|
||||
"email": user.login,
|
||||
"role": "student" if student else "user"
|
||||
},
|
||||
"student": {
|
||||
"id": student.id,
|
||||
"name": student.full_name
|
||||
} if student else None
|
||||
})
|
||||
|
||||
except (AccessError, AccessDenied):
|
||||
return json_response(False, error_code="AUTH_FAILED", error_message="Invalid email or password", status=401)
|
||||
except Exception as e:
|
||||
_logger.exception("Login error")
|
||||
return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500)
|
||||
|
||||
@http.route('/api/mobile/v1/auth/logout', type='http', auth='public', methods=['POST'], csrf=False, cors='*')
|
||||
def logout(self, **kwargs):
|
||||
if request.env.user._is_public():
|
||||
return json_response(False, error_code="AUTH_REQUIRED", error_message="Authentication required", status=401)
|
||||
try:
|
||||
request.session.logout(keep_db=True)
|
||||
return json_response(True, data={"message": "Logged out successfully"})
|
||||
except Exception as e:
|
||||
return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500)
|
||||
|
||||
@http.route('/api/mobile/v1/auth/me', type='http', auth='public', methods=['GET'], cors='*')
|
||||
def get_me(self, **kwargs):
|
||||
if request.env.user._is_public():
|
||||
return json_response(False, error_code="AUTH_REQUIRED", error_message="Authentication required", status=401)
|
||||
try:
|
||||
user = request.env.user
|
||||
student = user.student_id
|
||||
|
||||
return json_response(True, data={
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"name": user.name,
|
||||
"email": user.login,
|
||||
"role": "student" if student else "user"
|
||||
},
|
||||
"student": {
|
||||
"id": student.id,
|
||||
"name": student.full_name
|
||||
} if student else None
|
||||
})
|
||||
except Exception as e:
|
||||
return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500)
|
||||
314
addons/education_mobile_api/controllers/student.py
Normal file
314
addons/education_mobile_api/controllers/student.py
Normal file
@ -0,0 +1,314 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, date
|
||||
from odoo import http
|
||||
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
|
||||
announcements = request.env['education.announcement'].search_read(
|
||||
[('active', '=', True)],
|
||||
fields=['id', 'name', 'date', 'summary'],
|
||||
order='date desc, id desc',
|
||||
limit=5
|
||||
)
|
||||
|
||||
# Recent Results
|
||||
results = request.env['education.exam.result'].search([
|
||||
('student_id', '=', student.id),
|
||||
('published', '=', True)
|
||||
], order='published_date desc, 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'].search([
|
||||
('student_id', '=', student.id),
|
||||
('published', '=', True)
|
||||
], order='published_date desc, 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)
|
||||
2
addons/education_mobile_api/models/__init__.py
Normal file
2
addons/education_mobile_api/models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import mobile_device
|
||||
Binary file not shown.
Binary file not shown.
11
addons/education_mobile_api/models/mobile_device.py
Normal file
11
addons/education_mobile_api/models/mobile_device.py
Normal file
@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields
|
||||
|
||||
class MobileDevice(models.Model):
|
||||
_name = 'education.mobile.device'
|
||||
_description = 'Mobile Device Information'
|
||||
|
||||
user_id = fields.Many2one('res.users', string='User', required=True)
|
||||
device_token = fields.Char(string='Device Token')
|
||||
platform = fields.Selection([('ios', 'iOS'), ('android', 'Android')], string='Platform')
|
||||
last_login = fields.Datetime(string='Last Login')
|
||||
2
addons/education_mobile_api/security/ir.model.access.csv
Normal file
2
addons/education_mobile_api/security/ir.model.access.csv
Normal file
@ -0,0 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_education_mobile_device,education.mobile.device,model_education_mobile_device,base.group_user,1,1,1,1
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user