From 99bd10a279eb4744a19751c521fef5a31514072d Mon Sep 17 00:00:00 2001 From: Yamen Ahmad Date: Sun, 12 Apr 2026 11:25:00 +0400 Subject: [PATCH] feat: add assignments and rubrics API endpoints - GET/POST /api/assignments - list and create exam assignments - GET /api/rubrics - list rubrics from encoach.rubric model - POST /api/rubrics - create rubrics Made-with: Cursor --- .../controllers/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 352 -> 418 bytes .../__pycache__/assignments.cpython-312.pyc | Bin 0 -> 6523 bytes .../__pycache__/rubrics.cpython-312.pyc | Bin 0 -> 5003 bytes .../controllers/assignments.py | 110 ++++++++++++++++++ .../controllers/rubrics.py | 80 +++++++++++++ 6 files changed, 192 insertions(+) create mode 100644 custom_addons/encoach_exam_template/controllers/__pycache__/assignments.cpython-312.pyc create mode 100644 custom_addons/encoach_exam_template/controllers/__pycache__/rubrics.cpython-312.pyc create mode 100644 custom_addons/encoach_exam_template/controllers/assignments.py create mode 100644 custom_addons/encoach_exam_template/controllers/rubrics.py diff --git a/custom_addons/encoach_exam_template/controllers/__init__.py b/custom_addons/encoach_exam_template/controllers/__init__.py index 673af85e..a1995d3b 100644 --- a/custom_addons/encoach_exam_template/controllers/__init__.py +++ b/custom_addons/encoach_exam_template/controllers/__init__.py @@ -2,3 +2,5 @@ from . import templates from . import ielts_exam from . import custom_exam from . import exam_structures +from . import assignments +from . import rubrics diff --git a/custom_addons/encoach_exam_template/controllers/__pycache__/__init__.cpython-312.pyc b/custom_addons/encoach_exam_template/controllers/__pycache__/__init__.cpython-312.pyc index a8186e1b8f0587ad64552ead2eebf79a7b236195..d13f86e779b2691a97b0c5db4dac64042401f4bc 100644 GIT binary patch delta 162 zcmaFBw1}DaG%qg~0}xDcznwXABCjN)&qQ@onG}W;)*Oah)+km+1|XXa$YzUTV`Qjg z*W{R(;AExAc#Au+xHvOCFE=%>q!`F$FDgwc%1kcy(`3KJP{a&0v4{mku!0CS5Wx;2 tI416B)#L@T7=gH097ud%W@Kc%%OHN2LErIJ;XZDNI!n4c!=ErudypnMSvh+qW~ oY!knwDTO_3)t}ox%bip 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 literal 0 HcmV?d00001 diff --git a/custom_addons/encoach_exam_template/controllers/__pycache__/rubrics.cpython-312.pyc b/custom_addons/encoach_exam_template/controllers/__pycache__/rubrics.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4f87a2d3650c2ccda67e661c1221cafde909afe GIT binary patch literal 5003 zcmcgwT~J%c72f-+9|Q;_8DtqEg}*2S2qeJ<+Ys|-$8|_j9FMKkqsqD$S+ah}-b=8l zlyNhj3GJj4(}xOXJcB0n10DQ8-!grvV>>hPbTUB}DatjS$xPb^zhUFfByJyi_Fi2H z2yWiGGdkz&?%A`uXLrAIuKrS9ZbndkbM$_Ae;Go5Cl$SL1z~drgftS6Kt+*HVNZN3O3jpV}A2t~GP zK_4kel%#G@ONE{>{9ylKh2ptnR5fOv3)b(OlcSk}#X!t2i?NAlKo&cKiMWgt(I~NvC(q$sJq*QEQ5+>(K zev$*oWwc&ay;jzgEo;gM%iXJGy(?_*CUl@+(IHSF`l_?;!5-4!jtEiNQ7%#v4JS>d zD1lDVNeX{xLUQ3`z^%kbJ4%w$PdvrQ2E8-M$VOe(U5+g>+&--&nc^rq{`R(U#as2G z<9fE(qau~M?GhJ?vkak+*&rQ4LHaDXEX5|-NL5KsmzD~GM15pWva|F@H7txsv8A$fS?SlX#xE!QWO2A1S}Wiu9RNpOMtOTp`o!#sd-mqcz~$HTjy;!0 zzcJW1Xp`v_weE1HeY3tydCt*3GqPUkoNK<`xLVn=?rF@N`n_wmV!g^WH*$SwwaQy; z`iG}+xo)jI=_TEq7bK^nT)j>i4UjlexOL ze?7XvkTFlBL*T*R9cdaWL!TXV4DrTWRV<+n1GL=XIYP@ihntx@haAH-=Fh8HLidzI z%jZoL&>)O279CnQuRv28iHHPe9}R(+i4>u>s7)R>EieKbq6F>+zf=hGg5e6%LC(kh z$XMf!v8F9!`AcSRnNl;X1+ky1k!r9QyQVE2fr+plz%Yp)3Ofk_Q(=w{pB(YB3M-T6 z#Rewj^NJxR%I6dCUf7_7$G62SvM~}fHvbCuFpVG<6?eE>h!-gGHX?dXQ`AMpO{eHB zv4)C}+rOg_x9O2b$IESzZTC^E!2Mtpo1`Pfh*z2>gpy2>75E#5r9wPSaiCdV($J+4 zDz1Q+i}5YVK{PWIq8W@z8RTLtOBVAlh+dUy^fDxQ!FY|a6C?^a4N6&rL1Tx!Ts8{e_v~bw}-7^}?R{JsIPoC+B=2 z=V+T5$?w^hj?Kn01B>FPBl+hKZ7`^&aT8HBeKSYDweMZCw`A=tV8h-D25g>hx)0}^ zeSZusS6%B^^nAVdh2>Dz*_U_LKjfH(icQ4WD>o2RUirP^?ECI9kF3r|h=z4qY-^Uf ztfg+H{-ssR%U~$8|Hj}lwKTHw>`TjIIa|-ymY#oaaG?4`BB}oe1N?v&-7;1WG#YR1 zH4(XyBh+gee2%%*)jHU~+^(ZQzTLn9RoFM*7(2NQX$ZWkNHd~l$AB~>g;2|GNFx_s zE}#whEZ9+Y17tn3)87YV*Sn&_R{Yrw6P5~drI;imRcv{MfI#r(c>T?d5tP||27_SK zwVtA3Dq-MLG|Z*fEm*FxDRzrTtUKHRRiRu(X4?;Y^!-H0r_7F$EN0#psT7ymf`M`o zosyjHQK3TL)fmZSiy66!YzDcQi%05qOaoP+Qn#`V*;x4*UAKg+;AN{mQvulw?;BKP z1HJ{>sz#^jU!=6mji5X32QqCXxu!cTxycu87>h#qDqJ-+wNngJkZ*?LAvKxQ5z(u2FiuwBDBw}z1gpu?|1m)DBPw*w}d(Xg(Jd{!4esWFMN zc!-cDLdb_AK0?R<5QW$5sc?snlICNCyhg}zAYLQ>36W0#QP^_{;gY&eg?%p&mGJYV z>vclj1R{~CbYRnRPe`mu23
87Uv+cves#MXa+5M;9gV%TcZy|ca7LTmM%+4|14 z`tEFf_r3bQPaAW#{+XdXbRU>KFgKDJ$k|%fY;9RvTh7)oGxUwEX1%5fFAU^L3tu)AOe@7nXZ- zb$t&_oV{XZ=&=RuYgjlqe{iK`f7W$i&2>2II-GO$uenCDu92H^&UIqNa$;uiYBK=Z z^^WC++_T-EGCA9!70aRT)+=jwLf^})mRJ5a^pVs0;D~$BgFb6Gav0cck7>xw+`oP$1uNaX?4C2F2+2lZ)r~D`vkxmJlYRM7H?-?@k7yT91~faX2vv-#l-Dar_RD zKOjtiQ9Uh8&{id05Tw&UuApzZ%F8D|^!?IzwIXwBxf$Sn&X5rwGZf$Ukgec)H#v|t zh_vBGymhtV2pnTb?wHg_nLg!DCSbCs_p3KM5(w-#dt__P#+FK2MGicxp9!!Q`CJ_ zb{|#z6FL5d>b^C*W?I(FuB_QLcPwXax@_3wDeB-OV=cu$?rx!a=Y)mCeBxgSMAiI% E0i@MOlK=n! literal 0 HcmV?d00001 diff --git a/custom_addons/encoach_exam_template/controllers/assignments.py b/custom_addons/encoach_exam_template/controllers/assignments.py new file mode 100644 index 00000000..4c356bca --- /dev/null +++ b/custom_addons/encoach_exam_template/controllers/assignments.py @@ -0,0 +1,110 @@ +import json +import logging + +from odoo import http +from odoo.http import request +from odoo.addons.encoach_api.controllers.base import ( + jwt_required, _json_response, _error_response, _get_json_body, _paginate +) + +_logger = logging.getLogger(__name__) + + +def _assignment_to_dict(rec): + return { + 'id': rec.id, + 'title': rec.exam_id.title if rec.exam_id else f'Assignment #{rec.id}', + 'exam_id': rec.exam_id.id if rec.exam_id else None, + 'student_id': rec.student_id.id if rec.student_id else None, + 'student_name': rec.student_id.name if rec.student_id else None, + 'batch_id': rec.batch_id.id if rec.batch_id else None, + 'batch_name': rec.batch_id.name if rec.batch_id else None, + 'entity_name': rec.exam_id.entity_id.name if rec.exam_id and rec.exam_id.entity_id else None, + 'start_date': rec.access_start.isoformat() if rec.access_start else None, + 'end_date': rec.access_end.isoformat() if rec.access_end else None, + 'state': rec.status, + 'assignee_count': 1, + 'completed_count': 1 if rec.status == 'completed' else 0, + } + + +class EncoachAssignmentController(http.Controller): + + @http.route('/api/assignments', type='http', auth='none', + methods=['GET'], csrf=False) + @jwt_required + def list_assignments(self, **kw): + try: + Assignment = request.env['encoach.exam.assignment'].sudo() + search = kw.get('search', '').strip() + domain = [] + if search: + domain = [('exam_id.title', 'ilike', search)] + + total = Assignment.search_count(domain) + page, per_page, offset = _paginate(kw) + records = Assignment.search(domain, limit=per_page, offset=offset, + order='id desc') + return _json_response({ + 'items': [_assignment_to_dict(r) for r in records], + 'total': total, + 'page': page, + 'per_page': per_page, + }) + except Exception as e: + _logger.exception('assignments list failed') + return _error_response(str(e), 500) + + @http.route('/api/assignments', type='http', auth='none', + methods=['POST'], csrf=False) + @jwt_required + def create_assignment(self, **kw): + try: + body = _get_json_body() + title = (body.get('title') or '').strip() + if not title: + return _error_response('title is required', 400) + + exam_id = body.get('exam_id') + if not exam_id: + Exam = request.env['encoach.exam.custom'].sudo() + exam = Exam.create({ + 'title': title, + 'teacher_id': request.env.user.id, + 'status': 'draft', + 'total_time_min': 0, + }) + exam_id = exam.id + + vals = { + 'exam_id': exam_id, + 'student_id': body.get('student_id') or False, + 'batch_id': body.get('batch_id') or False, + 'status': 'assigned', + } + + start_date = body.get('start_date') + end_date = body.get('end_date') + if start_date: + vals['access_start'] = start_date + if end_date: + vals['access_end'] = end_date + + rec = request.env['encoach.exam.assignment'].sudo().create(vals) + return _json_response(_assignment_to_dict(rec), 201) + except Exception as e: + _logger.exception('assignment create failed') + return _error_response(str(e), 500) + + @http.route('/api/assignments/', type='http', + auth='none', methods=['GET'], csrf=False) + @jwt_required + def get_assignment(self, assignment_id, **kw): + try: + rec = request.env['encoach.exam.assignment'].sudo().browse(assignment_id) + if not rec.exists(): + return _error_response('Assignment not found', 404) + return _json_response(_assignment_to_dict(rec)) + except Exception as e: + _logger.exception('assignment get failed') + return _error_response(str(e), 500) diff --git a/custom_addons/encoach_exam_template/controllers/rubrics.py b/custom_addons/encoach_exam_template/controllers/rubrics.py new file mode 100644 index 00000000..ae9ba783 --- /dev/null +++ b/custom_addons/encoach_exam_template/controllers/rubrics.py @@ -0,0 +1,80 @@ +import json +import logging + +from odoo import http +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +def _json_response(data, status=200): + return request.make_json_response(data, status=status) + + +def _rubric_to_dict(rec): + criteria_text = rec.criteria or '' + criteria_count = 0 + if criteria_text: + try: + parsed = json.loads(criteria_text) + if isinstance(parsed, list): + criteria_count = len(parsed) + elif isinstance(parsed, dict): + criteria_count = len(parsed) + else: + criteria_count = len([l for l in criteria_text.split('\n') if l.strip()]) + except (json.JSONDecodeError, ValueError): + criteria_count = len([l for l in criteria_text.split('\n') if l.strip()]) + + return { + 'id': rec.id, + 'name': rec.name, + 'skill': rec.skill or '', + 'exam_type': rec.exam_type or '', + 'criteria': criteria_count or 1, + 'criteria_text': criteria_text, + 'levels': ['A1', 'A2', 'B1', 'B2', 'C1', 'C2'], + 'created': rec.create_date.strftime('%Y-%m-%d') if rec.create_date else '', + } + + +class EncoachRubricController(http.Controller): + + @http.route('/api/rubrics', type='http', auth='user', + methods=['GET'], csrf=False) + def list_rubrics(self, **kw): + try: + Rubric = request.env['encoach.rubric'].sudo() + limit = int(kw.get('limit', 50)) + offset = int(kw.get('offset', 0)) + records = Rubric.search([], limit=limit, offset=offset, + order='create_date desc') + total = Rubric.search_count([]) + return _json_response({ + 'items': [_rubric_to_dict(r) for r in records], + 'total': total, + }) + except Exception as e: + _logger.exception('rubrics list failed') + return _json_response({'error': str(e)}, 500) + + @http.route('/api/rubrics', type='http', auth='user', + methods=['POST'], csrf=False) + def create_rubric(self, **kw): + try: + body = json.loads(request.httprequest.data or '{}') + name = body.get('name', '').strip() + if not name: + return _json_response({'error': 'name is required'}, 400) + + vals = { + 'name': name, + 'skill': body.get('skill', 'writing'), + 'criteria': body.get('criteria', ''), + 'exam_type': body.get('exam_type', 'academic'), + } + rec = Rubric = request.env['encoach.rubric'].sudo().create(vals) + return _json_response(_rubric_to_dict(rec), 201) + except Exception as e: + _logger.exception('rubric create failed') + return _json_response({'error': str(e)}, 500)