feat(v3): restructure project + add complete frontend

- Restructure: move backend from new_project/ to backend/
- Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks)
- Add docs/ with SRS specs, user stories, and workflow documentation
- Update .gitignore for new directory layout

Workflows implemented:
  WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration,
  WF4 General English Exam, WF5 Course Generation,
  WF6 Entity Student Onboarding, AI Course Generation,
  Adaptive Learning Engine UI, White-Label Branding, Score Release

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-10 17:26:42 +04:00
commit 907a5c0e92
331 changed files with 23511 additions and 0 deletions

View File

@@ -0,0 +1 @@
from . import services

View File

@@ -0,0 +1,18 @@
{
'name': 'EnCoach Math',
'version': '19.0.1.0',
'category': 'Education',
'summary': 'Mathematical question types: numerical, expression, matrix scoring via SymPy',
'author': 'EnCoach',
'license': 'LGPL-3',
'depends': ['encoach_core', 'encoach_exam_template'],
'data': [
'security/ir.model.access.csv',
],
'external_dependencies': {
'python': ['sympy'],
},
'installable': True,
'application': False,
'auto_install': False,
}

View File

@@ -0,0 +1 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink

View File

@@ -0,0 +1 @@
from .math_scorer import MathScorer

View File

@@ -0,0 +1,113 @@
import logging
_logger = logging.getLogger(__name__)
class MathScorer:
"""Score mathematical question types using SymPy."""
@staticmethod
def score_numerical(student_answer, correct_answer):
"""Score a numerical answer with tolerance.
correct_answer: {"value": float, "tolerance": float}
"""
try:
student_val = float(student_answer)
correct_val = float(correct_answer.get('value', 0))
tolerance = float(correct_answer.get('tolerance', 0.01))
correct = abs(student_val - correct_val) <= tolerance
return {
'correct': correct,
'score': 1.0 if correct else 0.0,
'feedback': 'Correct!' if correct else f'Expected {correct_val}{tolerance}), got {student_val}',
}
except (ValueError, TypeError) as e:
return {'correct': False, 'score': 0.0, 'feedback': f'Invalid numerical input: {e}'}
@staticmethod
def score_expression(student_expr, correct_expr):
"""Score mathematical expressions using symbolic equivalence."""
try:
import sympy
student_sym = sympy.sympify(student_expr, evaluate=True)
correct_sym = sympy.sympify(correct_expr, evaluate=True)
diff = sympy.simplify(student_sym - correct_sym)
correct = diff == 0 or sympy.simplify(diff).is_zero
if not correct:
try:
student_float = float(student_sym.evalf())
correct_float = float(correct_sym.evalf())
correct = abs(student_float - correct_float) < 1e-6
except Exception:
pass
return {
'correct': correct,
'score': 1.0 if correct else 0.0,
'feedback': 'Correct!' if correct else f'Your answer {student_expr} is not equivalent to {correct_expr}',
'simplified_student': str(sympy.simplify(student_sym)),
'simplified_correct': str(sympy.simplify(correct_sym)),
}
except ImportError:
return {'correct': False, 'score': 0.0, 'feedback': 'SymPy not available for expression scoring'}
except Exception as e:
return {'correct': False, 'score': 0.0, 'feedback': f'Error parsing expression: {e}'}
@staticmethod
def score_matrix(student_matrix, correct_matrix, tolerance=0.01):
"""Score matrix answers with element-wise comparison."""
try:
import sympy
if isinstance(student_matrix, str):
import json
student_matrix = json.loads(student_matrix)
if isinstance(correct_matrix, str):
import json
correct_matrix = json.loads(correct_matrix)
s_mat = sympy.Matrix(student_matrix)
c_mat = sympy.Matrix(correct_matrix)
if s_mat.shape != c_mat.shape:
return {
'correct': False, 'score': 0.0,
'feedback': f'Matrix dimensions mismatch: expected {c_mat.shape}, got {s_mat.shape}',
}
total_elements = s_mat.rows * s_mat.cols
correct_elements = 0
for i in range(s_mat.rows):
for j in range(s_mat.cols):
if abs(float(s_mat[i, j]) - float(c_mat[i, j])) <= tolerance:
correct_elements += 1
all_correct = correct_elements == total_elements
partial_score = correct_elements / total_elements if total_elements > 0 else 0
return {
'correct': all_correct,
'score': 1.0 if all_correct else partial_score,
'feedback': 'Correct!' if all_correct else f'{correct_elements}/{total_elements} elements correct',
'correct_elements': correct_elements,
'total_elements': total_elements,
}
except ImportError:
return {'correct': False, 'score': 0.0, 'feedback': 'SymPy not available for matrix scoring'}
except Exception as e:
return {'correct': False, 'score': 0.0, 'feedback': f'Error scoring matrix: {e}'}
@staticmethod
def evaluate_expression(expr_str):
"""Safely evaluate a mathematical expression."""
try:
import sympy
result = sympy.sympify(expr_str, evaluate=True)
return float(result.evalf())
except ImportError:
raise ValueError("SymPy not available")
except Exception as e:
raise ValueError(f"Cannot evaluate expression: {e}")