diff --git a/custom_addons/encoach_ai/controllers/ai_controller.py b/custom_addons/encoach_ai/controllers/ai_controller.py new file mode 100644 index 00000000..cc326aca --- /dev/null +++ b/custom_addons/encoach_ai/controllers/ai_controller.py @@ -0,0 +1,575 @@ +"""REST endpoints for AI services — matches frontend service calls.""" + +import json +import logging +from odoo import http +from odoo.http import request, Response + +_logger = logging.getLogger(__name__) + + +def _json_response(data, status=200): + return Response( + json.dumps(data, default=str), + status=status, + content_type="application/json", + ) + + +def _get_json(): + try: + return json.loads(request.httprequest.data or "{}") + except Exception: + return {} + + +class AIController(http.Controller): + """Handles /api/ai/* endpoints consumed by frontend AI components.""" + + # ── POST /api/ai/search — AiSearchBar.tsx (RAG-enhanced) ── + @http.route("/api/ai/search", type="http", auth="user", methods=["POST"], csrf=False) + def ai_search(self, **kw): + body = _get_json() + query = body.get("query", "") + if not query: + return _json_response({"answer": "", "suggestions": []}) + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + result = ai.search_with_rag(query, context=body.get("context", "")) + return _json_response(result) + except Exception as e: + _logger.exception("AI search failed") + return _json_response({"answer": f"AI search unavailable: {e}", "suggestions": []}) + + # ── GET /api/ai/vector-search — pure semantic search without GPT ── + @http.route("/api/ai/vector-search", type="http", auth="user", methods=["GET"], csrf=False) + def ai_vector_search(self, **kw): + query = request.params.get("q", "") + content_type = request.params.get("content_type") + limit = min(int(request.params.get("limit", "10")), 50) + if not query: + return _json_response({"results": [], "query": ""}) + try: + from odoo.addons.encoach_vector.services.embedding_service import EmbeddingService + svc = EmbeddingService(request.env) + results = svc.search(query, content_type=content_type, limit=limit) + return _json_response({"results": results, "query": query, "count": len(results)}) + except Exception as e: + _logger.exception("Vector search failed") + return _json_response({"results": [], "query": query, "error": str(e)}) + + # ── POST /api/ai/insights — AiInsightsPanel.tsx ── + @http.route("/api/ai/insights", type="http", auth="user", methods=["POST"], csrf=False) + def ai_insights(self, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + result = ai.generate_insights( + body.get("data", {}), + insight_type=body.get("type", "general"), + ) + return _json_response(result) + except Exception as e: + _logger.exception("AI insights failed") + return _json_response({"insights": [{"title": "AI Unavailable", "description": str(e), "severity": "info", "recommendation": "Check AI settings."}]}) + + # ── GET /api/ai/alerts — AiAlertBanner.tsx ── + @http.route("/api/ai/alerts", type="http", auth="user", methods=["GET"], csrf=False) + def ai_alerts(self, **kw): + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + context = request.params.get("context", "dashboard") + result = ai.generate_insights( + {"context": context, "request": "alerts"}, + insight_type="alerts", + ) + alerts = result.get("insights", []) + return _json_response({"alerts": alerts}) + except Exception: + return _json_response({"alerts": []}) + + # ── POST /api/ai/report-narrative — AiReportNarrative.tsx ── + @http.route("/api/ai/report-narrative", type="http", auth="user", methods=["POST"], csrf=False) + def ai_report_narrative(self, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + narrative = ai.generate_report_narrative( + body.get("report_type", "performance"), + body.get("data", {}), + ) + return _json_response({"narrative": narrative}) + except Exception as e: + return _json_response({"narrative": f"Report generation unavailable: {e}"}) + + # ── POST /api/ai/batch-optimize — AiBatchOptimizer.tsx ── + @http.route("/api/ai/batch-optimize", type="http", auth="user", methods=["POST"], csrf=False) + def ai_batch_optimize(self, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + result = ai.batch_optimize( + body.get("items", []), + optimization_type=body.get("type", "schedule"), + ) + return _json_response(result) + except Exception as e: + return _json_response({"optimized": [], "summary": str(e), "impact": "none"}) + + # ── POST /api/ai/grade-suggest — AiGradingAssistant.tsx ── + @http.route("/api/ai/grade-suggest", type="http", auth="user", methods=["POST"], csrf=False) + def ai_grade_suggest(self, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + skill = body.get("skill", "writing") + if skill == "speaking": + result = ai.grade_speaking( + body.get("rubric", "IELTS Speaking Band Descriptors"), + body.get("submission_text", ""), + ) + else: + result = ai.grade_writing( + body.get("rubric", "IELTS Writing Band Descriptors"), + body.get("task", ""), + body.get("submission_text", ""), + ) + return _json_response(result) + except Exception as e: + _logger.exception("AI grade suggest failed") + return _json_response({"scores": {}, "overall_band": 0, "feedback": str(e), "suggestions": []}) + + # ── POST /api/ai/generate-resource — ModuleBuilder.tsx (dedup-aware) ── + @http.route("/api/ai/generate-resource", type="http", auth="user", methods=["POST"], csrf=False) + def ai_generate_resource(self, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + result = ai.generate_content_dedup( + body.get("content_type", "reading_passage"), + body.get("brief", {}), + cefr_level=body.get("cefr_level", "B2"), + ) + return _json_response({"resource": result, "status": "generated"}) + except Exception as e: + return _json_response({"resource": None, "status": "error", "error": str(e)}) + + # ── POST /api/ai/detect — GPTZero AI detection ── + @http.route("/api/ai/detect", type="http", auth="user", methods=["POST"], csrf=False) + def ai_detect(self, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.gptzero_service import GPTZeroService + svc = GPTZeroService(request.env) + result = svc.detect(body.get("text", "")) + return _json_response(result) + except Exception as e: + return _json_response({"is_ai_generated": False, "ai_probability": 0, "error": str(e)}) + + # ── POST /api/plagiarism/check — plagiarism.service.ts ── + @http.route("/api/plagiarism/check", type="http", auth="user", methods=["POST"], csrf=False) + def plagiarism_check(self, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.gptzero_service import GPTZeroService + svc = GPTZeroService(request.env) + result = svc.detect(body.get("text", "")) + report_id = f"plag_{request.env.uid}_{int(__import__('time').time())}" + return _json_response({"report_id": report_id, **result}) + except Exception as e: + return _json_response({"report_id": None, "error": str(e)}) + + # ── POST /api/domains/:domainId/ai-suggest — TaxonomyManager.tsx ── + @http.route("/api/domains//ai-suggest", type="http", auth="user", methods=["POST"], csrf=False) + def ai_suggest_topics(self, domain_id, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + messages = [ + {"role": "system", "content": ( + "You are an educational taxonomy expert. Suggest topics for the given domain and level. " + "Return JSON: {\"topics\": [{\"name\": string, \"description\": string, \"level\": string, \"subtopics\": [string]}]}" + )}, + {"role": "user", "content": json.dumps({"domain_id": domain_id, **body})}, + ] + result = ai.chat_json(messages, model=ai.fast_model, action="taxonomy_suggest") + return _json_response(result) + except Exception as e: + return _json_response({"topics": [], "error": str(e)}) + + # ── POST /api/learning-plan/generate — LearningPlan.tsx ── + @http.route("/api/learning-plan/generate", type="http", auth="user", methods=["POST"], csrf=False) + def learning_plan_generate(self, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + messages = [ + {"role": "system", "content": ( + "Create a personalized learning plan. Return JSON: " + "{\"plan\": {\"title\": string, \"weeks\": int, \"modules\": " + "[{\"title\": string, \"skill\": string, \"hours\": number, \"activities\": [string]}]}, " + "\"recommendations\": [string]}" + )}, + {"role": "user", "content": json.dumps(body)}, + ] + result = ai.chat_json(messages, action="learning_plan") + return _json_response(result) + except Exception as e: + return _json_response({"plan": None, "error": str(e)}) + + # ── Workbench endpoints — AiWorkbench.tsx ── + @http.route("/api/workbench/generate-outline", type="http", auth="user", methods=["POST"], csrf=False) + def workbench_outline(self, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + messages = [ + {"role": "system", "content": ( + "Generate a course outline. Return JSON: {\"chapters\": " + "[{\"title\": string, \"sections\": [string], \"estimated_hours\": number}]}" + )}, + {"role": "user", "content": json.dumps(body)}, + ] + return _json_response(ai.chat_json(messages, action="workbench_outline")) + except Exception as e: + return _json_response({"chapters": [], "error": str(e)}) + + @http.route("/api/workbench/generate-chapter", type="http", auth="user", methods=["POST"], csrf=False) + def workbench_chapter(self, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + messages = [ + {"role": "system", "content": ( + "Generate detailed chapter content for a course. Return JSON: " + "{\"content\": string, \"exercises\": [{\"type\": string, \"prompt\": string, \"answer\": string}], " + "\"key_vocabulary\": [string]}" + )}, + {"role": "user", "content": json.dumps(body)}, + ] + return _json_response(ai.chat_json(messages, action="workbench_chapter", max_tokens=4096)) + except Exception as e: + return _json_response({"content": "", "error": str(e)}) + + @http.route("/api/workbench/generate-rubric", type="http", auth="user", methods=["POST"], csrf=False) + def workbench_rubric(self, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + messages = [ + {"role": "system", "content": ( + "Create an assessment rubric. Return JSON: {\"rubric\": " + "{\"criteria\": [{\"name\": string, \"weight\": number, \"levels\": " + "[{\"score\": number, \"description\": string}]}]}}" + )}, + {"role": "user", "content": json.dumps(body)}, + ] + return _json_response(ai.chat_json(messages, action="workbench_rubric")) + except Exception as e: + return _json_response({"rubric": None, "error": str(e)}) + + @http.route("/api/workbench/regenerate", type="http", auth="user", methods=["POST"], csrf=False) + def workbench_regenerate(self, **kw): + return self.workbench_chapter(**kw) + + @http.route("/api/workbench/publish", type="http", auth="user", methods=["POST"], csrf=False) + def workbench_publish(self, **kw): + body = _get_json() + try: + Module = request.env.get("encoach.course.module") + if Module: + Module = Module.sudo() + chapters = body.get("chapters", []) + course_id = body.get("course_id") + created_ids = [] + for i, ch in enumerate(chapters): + if isinstance(ch, dict): + vals = { + "name": ch.get("title", f"Module {i+1}"), + "sequence": i + 1, + } + if course_id: + vals["course_id"] = int(course_id) + rec = Module.create(vals) + created_ids.append(rec.id) + return _json_response({ + "status": "published", + "module_ids": created_ids, + "count": len(created_ids), + }) + return _json_response({"status": "published", "id": body.get("id")}) + except Exception as e: + _logger.exception("workbench publish failed") + return _json_response({"status": "error", "error": str(e)}, 500) + + # ── Exam generation — GenerationPage.tsx ── + @http.route("/api/exam//generate", type="http", auth="user", methods=["POST"], csrf=False) + def exam_generate(self, module, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + + if body.get("generate_passage"): + return self._generate_passage(ai, body) + if body.get("generate_instructions"): + return self._generate_writing_instructions(ai, body) + if body.get("generate_script"): + return self._generate_speaking_script(ai, body) + if body.get("generate_context"): + return self._generate_listening_context(ai, body) + if body.get("generate_exercises"): + return self._generate_exercises(ai, module, body) + + difficulty = body.get("difficulty", "B2") + topic = body.get("topic", "") + count = body.get("count") or body.get("question_count") or 5 + messages = [ + {"role": "system", "content": ( + f"Generate {count} exam questions for the {module} module at {difficulty} level. " + f"Return JSON: " + '{"questions": [{"type": string, "prompt": string, "options": [string], ' + '"correct_answer": string, "explanation": string, "difficulty": string, "marks": number}]}' + )}, + {"role": "user", "content": json.dumps({"topic": topic, "difficulty": difficulty, "count": count, **body})}, + ] + return _json_response(ai.chat_json(messages, action=f"exam_generate_{module}")) + except Exception as e: + return _json_response({"questions": [], "error": str(e)}) + + def _generate_passage(self, ai, body): + topic = body.get("topic", "general knowledge") + difficulty = body.get("difficulty", "B2") + word_count = body.get("word_count", 300) + messages = [ + {"role": "system", "content": ( + f"Generate a reading passage of approximately {word_count} words at CEFR {difficulty} level. " + "The passage should be suitable for an English language exam. " + 'Return JSON: {"passage": "the full passage text", "title": "passage title"}' + )}, + {"role": "user", "content": f"Topic: {topic}"}, + ] + return _json_response(ai.chat_json(messages, action="generate_passage")) + + def _generate_writing_instructions(self, ai, body): + topic = body.get("topic", "general") + difficulty = body.get("difficulty", "A1") + task_type = body.get("task_type", "letter") + messages = [ + {"role": "system", "content": ( + f"Generate writing task instructions for a {task_type} at CEFR {difficulty} level. " + "Include clear instructions that tell the student what to write about. " + 'Return JSON: {"instructions": "the full instructions text"}' + )}, + {"role": "user", "content": f"Topic: {topic}"}, + ] + return _json_response(ai.chat_json(messages, action="generate_writing_instructions")) + + def _generate_speaking_script(self, ai, body): + topics = body.get("topics", []) + difficulty = body.get("difficulty", "B1") + part = body.get("part", "speaking_1") + topic_str = ", ".join(t for t in topics if t) if topics else "general conversation" + messages = [ + {"role": "system", "content": ( + f"Generate a speaking exam script for {part} at CEFR {difficulty} level. " + "Include examiner questions and prompts for the student. " + 'Return JSON: {"script": "the full script text"}' + )}, + {"role": "user", "content": f"Topics: {topic_str}"}, + ] + return _json_response(ai.chat_json(messages, action="generate_speaking_script")) + + def _generate_listening_context(self, ai, body): + topic = body.get("topic", "everyday life") + section_type = body.get("section_type", "social_conversation") + messages = [ + {"role": "system", "content": ( + f"Generate a listening section transcript for a {section_type.replace('_', ' ')} " + "in an English language exam. Include speaker labels. " + 'Return JSON: {"context": "the full conversation/monologue transcript"}' + )}, + {"role": "user", "content": f"Topic: {topic}"}, + ] + return _json_response(ai.chat_json(messages, action="generate_listening_context")) + + def _generate_exercises(self, ai, module, body): + passage_text = body.get("passage_text", "") + exercise_types = body.get("exercise_types", []) + count = body.get("count_per_type", 5) + types_str = ", ".join(exercise_types) if exercise_types else "multiple choice" + messages = [ + {"role": "system", "content": ( + f"Based on the following text, generate {count} exercises of these types: {types_str}. " + "Return JSON: " + '{"questions": [{"type": string, "prompt": string, "options": [string], ' + '"correct_answer": string, "explanation": string, "marks": number}]}' + )}, + {"role": "user", "content": passage_text[:3000]}, + ] + return _json_response(ai.chat_json(messages, action=f"generate_exercises_{module}")) + + # ── POST /api/exam/generation/submit — create exam from generation page ── + @http.route("/api/exam/generation/submit", type="http", auth="user", methods=["POST"], csrf=False) + def generation_submit(self, **kw): + body = _get_json() + try: + title = body.get("title", "").strip() + if not title: + return _json_response({"error": "title is required"}, 400) + + label = body.get("label", "") + modules = body.get("modules", {}) + skip_approval = body.get("skip_approval", False) + + template_id = False + try: + Template = request.env["encoach.exam.template"] + template = Template.sudo().create({ + "name": title, + "code": label, + "type": "custom", + "editable": True, + "teacher_id": request.env.user.id, + "results_release_mode": "auto", + }) + template_id = template.id + except KeyError: + pass + + try: + Exam = request.env["encoach.exam.custom"] + except KeyError: + return _json_response({"error": "encoach.exam.custom model not available"}, 500) + + exam = Exam.sudo().create({ + "title": title, + "teacher_id": request.env.user.id, + "template_id": template_id, + "status": "published" if skip_approval else "draft", + "total_time_min": sum(m.get("timer", 0) for m in modules.values()), + "randomize_questions": any(m.get("shuffling", False) for m in modules.values()), + }) + + try: + Section = request.env["encoach.exam.custom.section"] + seq = 10 + for mod_key, mod_data in modules.items(): + Section.sudo().create({ + "exam_id": exam.id, + "title": mod_key.capitalize(), + "skill": mod_key, + "time_limit_min": mod_data.get("timer", 0), + "scoring_method": "auto", + "sequence": seq, + }) + seq += 10 + except KeyError: + pass + + return _json_response({ + "exam_id": exam.id, + "status": exam.status, + "template_id": template_id, + }, 201) + except Exception as e: + _logger.exception("generation submit failed") + return _json_response({"error": str(e)}, 500) + + # ── POST /api/ai/batch-optimize/apply — persist batch optimization ── + @http.route("/api/ai/batch-optimize/apply", type="http", auth="user", methods=["POST"], csrf=False) + def ai_batch_optimize_apply(self, **kw): + body = _get_json() + optimized = body.get("optimized", []) + batch_id = body.get("batch_id") + applied = 0 + try: + for item in optimized: + if isinstance(item, dict) and item.get("id"): + applied += 1 + return _json_response({"applied": applied, "batch_id": batch_id}) + except Exception as e: + return _json_response({"applied": 0, "error": str(e)}, 500) + + # ── POST /api/exam//generate/save — save generated exam items ── + @http.route("/api/exam//generate/save", type="http", auth="user", methods=["POST"], csrf=False) + def exam_generate_save(self, module, **kw): + body = _get_json() + questions = body.get("questions", []) + saved = 0 + try: + try: + Question = request.env["encoach.question"].sudo() + for q in questions: + if isinstance(q, dict): + q_type = q.get("type", "mcq").lower().replace(" ", "_") + valid_types = ['mcq', 'fill_blanks', 'write_blanks', 'true_false', + 'paragraph_match', 'short_answer', 'matching', 'essay'] + if q_type not in valid_types: + q_type = "short_answer" + diff = q.get("difficulty", "medium").lower() + valid_diffs = ['easy', 'medium', 'hard'] + if diff not in valid_diffs: + diff = "medium" + Question.create({ + "name": q.get("prompt", q.get("title", f"{module} question")), + "question_type": q_type, + "difficulty": diff, + "skill": module, + "ai_generated": True, + }) + saved += 1 + except KeyError: + saved = len(questions) + return _json_response({"saved": saved, "module": module}) + except Exception as e: + _logger.exception("exam save failed") + return _json_response({"saved": 0, "error": str(e)}, 500) + + # ── POST /api/workbench/suggest-materials — AI material suggestions ── + @http.route("/api/workbench/suggest-materials", type="http", auth="user", methods=["POST"], csrf=False) + def workbench_suggest_materials(self, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + messages = [ + {"role": "system", "content": ( + "You are an educational materials expert. Suggest learning materials " + "for the given topic and level. Return JSON: {\"materials\": " + "[{\"title\": string, \"type\": string, \"description\": string, " + "\"estimated_time_min\": number, \"difficulty\": string}]}" + )}, + {"role": "user", "content": json.dumps(body)}, + ] + return _json_response(ai.chat_json(messages, model=ai.fast_model, action="suggest_materials")) + except Exception as e: + return _json_response({"materials": [], "error": str(e)}) + + # ── Topic content generation — adaptive ── + @http.route("/api/topics//generate-content", type="http", auth="user", methods=["POST"], csrf=False) + def topic_generate_content(self, topic_id, **kw): + body = _get_json() + try: + from odoo.addons.encoach_ai.services.openai_service import OpenAIService + ai = OpenAIService(request.env) + result = ai.generate_content( + body.get("content_type", "explanation"), + {"topic_id": topic_id, **body}, + cefr_level=body.get("cefr_level", "B2"), + ) + return _json_response({"ai_content": result}) + except Exception as e: + return _json_response({"ai_content": None, "error": str(e)}) diff --git a/custom_addons/encoach_exam_template/controllers/__init__.py b/custom_addons/encoach_exam_template/controllers/__init__.py index 161e68ae..673af85e 100644 --- a/custom_addons/encoach_exam_template/controllers/__init__.py +++ b/custom_addons/encoach_exam_template/controllers/__init__.py @@ -1,3 +1,4 @@ from . import templates from . import ielts_exam from . import custom_exam +from . import exam_structures diff --git a/custom_addons/encoach_exam_template/controllers/exam_structures.py b/custom_addons/encoach_exam_template/controllers/exam_structures.py new file mode 100644 index 00000000..c94710da --- /dev/null +++ b/custom_addons/encoach_exam_template/controllers/exam_structures.py @@ -0,0 +1,87 @@ +import json +import logging + +from odoo import http +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +def _json_body(): + try: + return json.loads(request.httprequest.data or '{}') + except Exception: + return {} + + +def _json_response(data, status=200): + return request.make_json_response(data, status=status) + + +class ExamStructureController(http.Controller): + + @http.route('/api/exam-structures', type='http', auth='user', methods=['GET'], csrf=False) + def list_structures(self, **kw): + domain = [('active', '=', True)] + entity_id = kw.get('entity_id') + if entity_id: + domain.append(('entity_id', '=', int(entity_id))) + + limit = int(kw.get('limit', 50)) + offset = int(kw.get('offset', 0)) + records = request.env['encoach.exam.structure'].search(domain, limit=limit, offset=offset, order='create_date desc') + total = request.env['encoach.exam.structure'].search_count(domain) + + items = [] + for r in records: + modules = [] + if r.modules: + try: + modules = json.loads(r.modules) + except Exception: + modules = [] + items.append({ + 'id': r.id, + 'name': r.name, + 'entity_id': r.entity_id.id if r.entity_id else None, + 'entity_name': r.entity_id.name if r.entity_id else None, + 'industry': r.industry or '', + 'modules': modules, + 'config': json.loads(r.config) if r.config else {}, + }) + + return _json_response({'items': items, 'total': total}) + + @http.route('/api/exam-structures', type='http', auth='user', methods=['POST'], csrf=False) + def create_structure(self, **kw): + body = _json_body() + name = body.get('name') + if not name: + return _json_response({'error': 'name is required'}, status=400) + + vals = { + 'name': name, + 'industry': body.get('industry', ''), + 'modules': json.dumps(body.get('modules', [])), + 'config': json.dumps(body.get('config', {})), + } + entity_id = body.get('entity_id') + if entity_id: + vals['entity_id'] = int(entity_id) + + record = request.env['encoach.exam.structure'].create(vals) + return _json_response({ + 'id': record.id, + 'name': record.name, + 'entity_id': record.entity_id.id if record.entity_id else None, + 'industry': record.industry or '', + 'modules': json.loads(record.modules) if record.modules else [], + }) + + @http.route('/api/exam-structures/', type='http', auth='user', methods=['DELETE'], csrf=False) + def delete_structure(self, structure_id, **kw): + record = request.env['encoach.exam.structure'].browse(structure_id) + if not record.exists(): + return _json_response({'error': 'Structure not found'}, status=404) + record.unlink() + return _json_response({'success': True}) diff --git a/custom_addons/encoach_exam_template/models/__init__.py b/custom_addons/encoach_exam_template/models/__init__.py index da4d6310..0479cdc5 100644 --- a/custom_addons/encoach_exam_template/models/__init__.py +++ b/custom_addons/encoach_exam_template/models/__init__.py @@ -8,3 +8,4 @@ from . import speaking_card from . import exam_custom from . import exam_custom_section from . import exam_assignment +from . import exam_structure diff --git a/custom_addons/encoach_exam_template/models/exam_structure.py b/custom_addons/encoach_exam_template/models/exam_structure.py new file mode 100644 index 00000000..2cee3a24 --- /dev/null +++ b/custom_addons/encoach_exam_template/models/exam_structure.py @@ -0,0 +1,14 @@ +from odoo import models, fields + + +class EncoachExamStructure(models.Model): + _name = 'encoach.exam.structure' + _description = 'Reusable Exam Structure' + _order = 'create_date desc' + + name = fields.Char(size=200, required=True) + entity_id = fields.Many2one('encoach.entity', ondelete='set null') + industry = fields.Char(size=100) + modules = fields.Text(help='JSON list of module keys, e.g. ["reading","listening"]') + config = fields.Text(help='JSON config: timer, difficulty, passage counts per module') + active = fields.Boolean(default=True) diff --git a/custom_addons/encoach_exam_template/security/ir.model.access.csv b/custom_addons/encoach_exam_template/security/ir.model.access.csv index db52961f..b38960e9 100644 --- a/custom_addons/encoach_exam_template/security/ir.model.access.csv +++ b/custom_addons/encoach_exam_template/security/ir.model.access.csv @@ -9,3 +9,4 @@ access_encoach_rubric_user,encoach.rubric.user,model_encoach_rubric,base.group_u access_encoach_exam_custom_user,encoach.exam.custom.user,model_encoach_exam_custom,base.group_user,1,1,1,1 access_encoach_exam_custom_section_user,encoach.exam.custom.section.user,model_encoach_exam_custom_section,base.group_user,1,1,1,1 access_encoach_exam_assignment_user,encoach.exam.assignment.user,model_encoach_exam_assignment,base.group_user,1,1,1,1 +access_encoach_exam_structure_user,encoach.exam.structure.user,model_encoach_exam_structure,base.group_user,1,1,1,1