# level_test_manager.py import re import json from datetime import datetime class LevelTestManager: """ Global diagnostic level test manager. هذا الملف لا يعتمد على المنهاج الأردني في فحص المستوى. المنهاج الأردني يبقى للتدريس والشرح فقط. فحص المستوى هنا مبني على خريطة مهارات عالمية عامة: - الرياضيات: تدرج مهاري من الأعداد إلى ما قبل الجبر. - الإنجليزية: تدرج قريب من CEFR من Pre-A1 إلى B1/B1+. الواجهة المتوقعة مع بقية النظام: - create_level_test(subject, grade=None, student=None, identity=None) - format_test_for_student(test_data) - evaluate_test(test_data, answers_text) - submit_level_test(test_data, answers_text) - format_result_for_display(result) """ def __init__(self): self.version = "global-diagnostic-1.0" self.answer_letters = { "أ": 0, "ا": 0, "A": 0, "a": 0, "1": 0, "ب": 1, "B": 1, "b": 1, "2": 1, "ج": 2, "C": 2, "c": 2, "3": 2, "د": 3, "D": 3, "d": 3, "4": 3 } self.subject_aliases = { "math": "math", "mathematics": "math", "رياضيات": "math", "الرياضيات": "math", "رياضي": "math", "english": "english", "لغة انجليزية": "english", "لغة إنجليزية": "english", "انجليزي": "english", "إنجليزي": "english", "الانجليزي": "english", "الإنجليزي": "english" } self.skills_map = self.build_skills_map() self.questions_bank = self.build_questions_bank() self.plan_rules = self.build_plan_rules() def build_skills_map(self): return { "math": { "subject_title": "الرياضيات - فحص عالمي", "global_basis": "تدرج عالمي عام من المفاهيم الأساسية إلى ما قبل الجبر", "levels": [ { "level_id": "math_l1_number_sense", "title": "Number Sense / الحس العددي", "expected_grade_min": 1, "expected_grade_max": 3, "skills": [ {"id": "math_l1_counting", "title": "العد والترتيب"}, {"id": "math_l1_place_value", "title": "القيمة المنزلية"}, {"id": "math_l1_compare_numbers", "title": "مقارنة الأعداد"} ] }, { "level_id": "math_l2_operations", "title": "Operations / العمليات الأساسية", "expected_grade_min": 2, "expected_grade_max": 5, "skills": [ {"id": "math_l2_add_sub", "title": "الجمع والطرح"}, {"id": "math_l2_multiply_divide", "title": "الضرب والقسمة"}, {"id": "math_l2_word_problems", "title": "مسائل لفظية بسيطة"} ] }, { "level_id": "math_l3_fractions_decimals", "title": "Fractions & Decimals / الكسور والعشريات", "expected_grade_min": 4, "expected_grade_max": 7, "skills": [ {"id": "math_l3_fraction_concept", "title": "مفهوم الكسر"}, {"id": "math_l3_fraction_operations", "title": "عمليات على الكسور"}, {"id": "math_l3_decimals", "title": "الأعداد العشرية"} ] }, { "level_id": "math_l4_ratio_percent", "title": "Ratio & Percent / النسبة والمئوية", "expected_grade_min": 5, "expected_grade_max": 8, "skills": [ {"id": "math_l4_ratio", "title": "النسب والتناسب"}, {"id": "math_l4_percent", "title": "النسبة المئوية"}, {"id": "math_l4_rate", "title": "المعدل والسرعة"} ] }, { "level_id": "math_l5_integers_expressions", "title": "Integers & Expressions / الأعداد الصحيحة والتعابير", "expected_grade_min": 6, "expected_grade_max": 9, "skills": [ {"id": "math_l5_integers", "title": "الأعداد السالبة والموجبة"}, {"id": "math_l5_order_operations", "title": "ترتيب العمليات"}, {"id": "math_l5_expressions", "title": "التعابير الجبرية"} ] }, { "level_id": "math_l6_equations_geometry_data", "title": "Equations, Geometry & Data / المعادلات والهندسة والبيانات", "expected_grade_min": 7, "expected_grade_max": 10, "skills": [ {"id": "math_l6_linear_equations", "title": "المعادلات الخطية البسيطة"}, {"id": "math_l6_geometry", "title": "الهندسة والقياس"}, {"id": "math_l6_data", "title": "قراءة البيانات والمتوسط"} ] }, { "level_id": "math_l7_pre_algebra", "title": "Pre-Algebra / ما قبل الجبر", "expected_grade_min": 8, "expected_grade_max": 11, "skills": [ {"id": "math_l7_patterns", "title": "الأنماط والدوال البسيطة"}, {"id": "math_l7_slope", "title": "الميل والعلاقة الخطية"}, {"id": "math_l7_multi_step", "title": "مسائل متعددة الخطوات"} ] } ] }, "english": { "subject_title": "اللغة الإنجليزية - فحص عالمي", "global_basis": "تدرج قريب من CEFR من Pre-A1 إلى B1/B1+", "levels": [ { "level_id": "eng_pre_a1", "title": "Pre-A1 / قبل المبتدئ", "skills": [ {"id": "eng_pre_a1_letters", "title": "الحروف والكلمات الأساسية"}, {"id": "eng_pre_a1_basic_vocab", "title": "مفردات يومية بسيطة"} ] }, { "level_id": "eng_a1", "title": "A1 / مبتدئ", "skills": [ {"id": "eng_a1_be", "title": "فعل be والجمل البسيطة"}, {"id": "eng_a1_pronouns", "title": "الضمائر"}, {"id": "eng_a1_basic_reading", "title": "فهم جمل قصيرة"} ] }, { "level_id": "eng_a2", "title": "A2 / أساسي", "skills": [ {"id": "eng_a2_present_simple", "title": "المضارع البسيط"}, {"id": "eng_a2_past_simple", "title": "الماضي البسيط"}, {"id": "eng_a2_prepositions", "title": "حروف الجر"}, {"id": "eng_a2_reading", "title": "فهم فقرة قصيرة"} ] }, { "level_id": "eng_b1", "title": "B1 / متوسط", "skills": [ {"id": "eng_b1_tenses", "title": "الأزمنة في السياق"}, {"id": "eng_b1_modals", "title": "الأفعال المساعدة modal verbs"}, {"id": "eng_b1_inference", "title": "الاستنتاج من نص"}, {"id": "eng_b1_connectors", "title": "أدوات الربط"} ] } ] } } def build_plan_rules(self): return { "weak": { "title": "فجوة واضحة", "action": "شرح تأسيسي قصير + لعبة تقوية + 5 تدريبات + إعادة اختبار مصغر", "lesson_time_minutes": 20, "practice_questions": 5 }, "medium": { "title": "مهارة متوسطة", "action": "مراجعة مركزة + 3 تدريبات + سؤال تحدي", "lesson_time_minutes": 12, "practice_questions": 3 }, "strong": { "title": "مهارة قوية", "action": "تثبيت سريع + سؤال تحدي أو انتقال للمهارة التالية", "lesson_time_minutes": 5, "practice_questions": 1 } } def build_questions_bank(self): return { "math": self.build_math_questions(), "english": self.build_english_questions() } def q( self, qid, skill_id, level_id, difficulty, question, choices, correct_index, teacher_hint="", needs_whiteboard=False, whiteboard_prompt="", tags=None ): return { "id": qid, "skill_id": skill_id, "level_id": level_id, "difficulty": difficulty, "question": question, "choices": choices, "answer_index": correct_index, "correct_answer": choices[correct_index] if 0 <= correct_index < len(choices) else "", "teacher_hint": teacher_hint, "needs_whiteboard": needs_whiteboard, "whiteboard_prompt": whiteboard_prompt, "tags": tags or [] } def build_math_questions(self): return [ self.q( "math_001", "math_l1_counting", "math_l1_number_sense", 1, "أي عدد يأتي مباشرة بعد العدد 49؟", ["48", "50", "59", "40"], 1, "فكر في العد التصاعدي: 47، 48، 49، ماذا يأتي بعدها؟" ), self.q( "math_002", "math_l1_place_value", "math_l1_number_sense", 1, "في العدد 372، ما قيمة الرقم 7؟", ["7", "70", "700", "17"], 1, "انظر إلى منزلة الرقم 7: آحاد أم عشرات أم مئات؟" ), self.q( "math_003", "math_l1_compare_numbers", "math_l1_number_sense", 1, "أي عدد هو الأكبر؟", ["305", "350", "315", "299"], 1, "قارن منزلة المئات أولًا، ثم العشرات." ), self.q( "math_004", "math_l2_add_sub", "math_l2_operations", 2, "احسب: 468 + 257 = ؟", ["615", "725", "715", "735"], 1, "اجمع الآحاد ثم العشرات ثم المئات مع الانتباه للحمل.", True, "اكتب العددين تحت بعض، ثم اجمع من اليمين إلى اليسار." ), self.q( "math_005", "math_l2_add_sub", "math_l2_operations", 2, "احسب: 900 - 476 = ؟", ["424", "434", "524", "414"], 0, "استعمل الاستلاف خطوة خطوة.", True, "اكتب 900 فوق 476 وابدأ من الآحاد." ), self.q( "math_006", "math_l2_multiply_divide", "math_l2_operations", 2, "احسب: 12 × 8 = ؟", ["84", "88", "96", "108"], 2, "يمكنك حساب 10×8 ثم 2×8 ثم الجمع." ), self.q( "math_007", "math_l2_multiply_divide", "math_l2_operations", 2, "إذا كان 56 ÷ 7 = ؟", ["6", "7", "8", "9"], 2, "ابحث عن عدد إذا ضربته في 7 يعطي 56." ), self.q( "math_008", "math_l2_word_problems", "math_l2_operations", 3, "مع أحمد 24 قلمًا، أعطى 9 أقلام لصديقه. كم قلمًا بقي معه؟", ["13", "15", "16", "33"], 1, "الكلمة المهمة هنا: بقي. إذن العملية طرح." ), self.q( "math_009", "math_l3_fraction_concept", "math_l3_fractions_decimals", 3, "أي كسر يساوي نصف الشيء؟", ["1/3", "1/2", "2/3", "3/4"], 1, "النصف يعني جزء واحد من جزأين متساويين." ), self.q( "math_010", "math_l3_fraction_concept", "math_l3_fractions_decimals", 3, "أي كسر أكبر؟", ["1/4", "1/2", "1/8", "1/10"], 1, "عندما يكون البسط 1، المقام الأصغر يعطي كسرًا أكبر." ), self.q( "math_011", "math_l3_fraction_operations", "math_l3_fractions_decimals", 4, "احسب: 1/4 + 2/4 = ؟", ["3/4", "3/8", "2/8", "1/2"], 0, "المقامات متساوية، نجمع البسط فقط." ), self.q( "math_012", "math_l3_decimals", "math_l3_fractions_decimals", 4, "أي عدد عشري يساوي 3/10؟", ["0.03", "0.3", "3.0", "30.0"], 1, "ثلاثة أعشار تكتب 0.3." ), self.q( "math_013", "math_l3_decimals", "math_l3_fractions_decimals", 4, "احسب: 2.5 + 1.75 = ؟", ["3.25", "4.25", "4.75", "3.75"], 1, "رتب الفواصل العشرية تحت بعضها قبل الجمع.", True, "اكتب 2.50 + 1.75 ثم اجمع." ), self.q( "math_014", "math_l4_ratio", "math_l4_ratio_percent", 4, "النسبة 2 : 3 تعني أن مجموع الأجزاء هو؟", ["2", "3", "5", "6"], 2, "اجمع أجزاء النسبة: 2 + 3." ), self.q( "math_015", "math_l4_percent", "math_l4_ratio_percent", 5, "ما قيمة 25% من 80؟", ["10", "15", "20", "25"], 2, "25% تعني الربع. ربع 80 يساوي 20.", True, "اكتب 80 ÷ 4." ), self.q( "math_016", "math_l4_rate", "math_l4_ratio_percent", 5, "سيارة قطعت 120 كم في ساعتين. ما سرعتها المتوسطة؟", ["40 كم/ساعة", "60 كم/ساعة", "80 كم/ساعة", "120 كم/ساعة"], 1, "السرعة = المسافة ÷ الزمن." ), self.q( "math_017", "math_l5_integers", "math_l5_integers_expressions", 5, "احسب: -3 + 8 = ؟", ["-11", "-5", "5", "11"], 2, "ابدأ من -3 وتحرك 8 خطوات إلى اليمين على خط الأعداد." ), self.q( "math_018", "math_l5_order_operations", "math_l5_integers_expressions", 5, "احسب: 6 + 2 × 5 = ؟", ["40", "16", "30", "20"], 1, "ننفذ الضرب قبل الجمع." ), self.q( "math_019", "math_l5_expressions", "math_l5_integers_expressions", 6, "إذا كانت x = 4، فما قيمة 3x + 2؟", ["9", "12", "14", "18"], 2, "عوّض x بالعدد 4: 3×4 + 2." ), self.q( "math_020", "math_l6_linear_equations", "math_l6_equations_geometry_data", 6, "حل المعادلة: x + 7 = 15", ["7", "8", "9", "22"], 1, "اطرح 7 من الطرفين." ), self.q( "math_021", "math_l6_linear_equations", "math_l6_equations_geometry_data", 6, "حل المعادلة: 3x = 21", ["6", "7", "8", "9"], 1, "اقسم الطرفين على 3." ), self.q( "math_022", "math_l6_geometry", "math_l6_equations_geometry_data", 5, "مستطيل طوله 8 سم وعرضه 5 سم. ما مساحته؟", ["13 سم²", "26 سم²", "40 سم²", "80 سم²"], 2, "مساحة المستطيل = الطول × العرض." ), self.q( "math_023", "math_l6_geometry", "math_l6_equations_geometry_data", 6, "محيط مربع طول ضلعه 9 سم يساوي؟", ["18 سم", "27 سم", "36 سم", "81 سم"], 2, "محيط المربع = 4 × طول الضلع." ), self.q( "math_024", "math_l6_data", "math_l6_equations_geometry_data", 6, "ما متوسط الأعداد: 4، 6، 10؟", ["6", "7", "8", "20"], 1, "المتوسط = مجموع الأعداد ÷ عددها." ), self.q( "math_025", "math_l7_patterns", "math_l7_pre_algebra", 7, "ما العدد التالي في النمط: 3، 6، 12، 24، ...؟", ["30", "36", "42", "48"], 3, "كل مرة نضرب في 2." ), self.q( "math_026", "math_l7_slope", "math_l7_pre_algebra", 7, "في العلاقة y = 2x + 1، إذا كانت x = 3 فما قيمة y؟", ["5", "6", "7", "8"], 2, "عوّض x بـ 3: 2×3 + 1." ), self.q( "math_027", "math_l7_multi_step", "math_l7_pre_algebra", 8, "اشترى طالب 3 دفاتر بسعر 2 دينار للدفتر، وقلمًا بدينار واحد. كم دفع؟", ["5", "6", "7", "8"], 2, "احسب تكلفة الدفاتر أولًا، ثم أضف ثمن القلم." ), self.q( "math_028", "math_l7_multi_step", "math_l7_pre_algebra", 8, "إذا كان 2x + 5 = 17، فما قيمة x؟", ["5", "6", "7", "11"], 1, "اطرح 5 أولًا، ثم اقسم على 2.", True, "اكتب: 2x + 5 = 17، ثم 2x = 12، ثم x = 6." ) ] def build_english_questions(self): return [ self.q( "eng_001", "eng_pre_a1_letters", "eng_pre_a1", 1, "Which word starts with the letter B?", ["apple", "ball", "cat", "dog"], 1, "انظر إلى أول حرف في كل كلمة." ), self.q( "eng_002", "eng_pre_a1_basic_vocab", "eng_pre_a1", 1, "Choose the correct meaning: book", ["كتاب", "قلم", "باب", "بيت"], 0, "هذه كلمة من مفردات المدرسة الأساسية." ), self.q( "eng_003", "eng_pre_a1_basic_vocab", "eng_pre_a1", 1, "Choose the correct word for: قطة", ["dog", "cat", "car", "pen"], 1, "فكر في أسماء الحيوانات الشائعة." ), self.q( "eng_004", "eng_a1_be", "eng_a1", 2, "Choose: I ___ a student.", ["am", "is", "are", "be"], 0, "مع I نستخدم am." ), self.q( "eng_005", "eng_a1_be", "eng_a1", 2, "Choose: She ___ happy.", ["am", "is", "are", "be"], 1, "مع she نستخدم is." ), self.q( "eng_006", "eng_a1_pronouns", "eng_a1", 2, "Choose the correct pronoun: Ahmad is my friend. ___ is kind.", ["He", "She", "It", "They"], 0, "Ahmad اسم ولد، والضمير المناسب He." ), self.q( "eng_007", "eng_a1_basic_reading", "eng_a1", 2, "Read: 'Sara has a red bag.' What color is the bag?", ["blue", "red", "green", "black"], 1, "ابحث عن كلمة اللون في الجملة." ), self.q( "eng_008", "eng_a1_basic_reading", "eng_a1", 2, "Choose the correct sentence.", ["He are a teacher.", "He is a teacher.", "He am a teacher.", "He be a teacher."], 1, "مع He نستخدم is." ), self.q( "eng_009", "eng_a2_present_simple", "eng_a2", 3, "Choose: She ___ to school every day.", ["go", "goes", "going", "went"], 1, "في المضارع البسيط مع she نضيف s للفعل." ), self.q( "eng_010", "eng_a2_present_simple", "eng_a2", 3, "Choose: They ___ football on Fridays.", ["plays", "play", "playing", "played"], 1, "مع They نستخدم الفعل بدون s في المضارع البسيط." ), self.q( "eng_011", "eng_a2_past_simple", "eng_a2", 3, "Choose: Yesterday, I ___ my homework.", ["finish", "finishes", "finished", "finishing"], 2, "Yesterday تدل على الماضي." ), self.q( "eng_012", "eng_a2_past_simple", "eng_a2", 3, "Choose the past form of 'go'.", ["goed", "went", "goes", "going"], 1, "go فعل غير منتظم، الماضي منه went." ), self.q( "eng_013", "eng_a2_prepositions", "eng_a2", 3, "Choose: The cat is ___ the table.", ["under", "happy", "quickly", "blue"], 0, "نحتاج هنا حرف جر للمكان." ), self.q( "eng_014", "eng_a2_prepositions", "eng_a2", 3, "Choose: I wake up ___ 7 o'clock.", ["in", "on", "at", "under"], 2, "مع الساعة نستخدم at." ), self.q( "eng_015", "eng_a2_reading", "eng_a2", 4, "Read: 'Omar likes science because he enjoys experiments.' What does Omar enjoy?", ["stories", "experiments", "sports", "music"], 1, "ابحث عن كلمة enjoys في الجملة." ), self.q( "eng_016", "eng_a2_reading", "eng_a2", 4, "Read: 'The library closes at five.' When does the library close?", ["at four", "at five", "at six", "at seven"], 1, "ابحث عن الوقت المذكور." ), self.q( "eng_017", "eng_b1_tenses", "eng_b1", 5, "Choose: I have ___ this movie before.", ["see", "saw", "seen", "seeing"], 2, "بعد have في present perfect نستخدم التصريف الثالث." ), self.q( "eng_018", "eng_b1_tenses", "eng_b1", 5, "Choose: While I ___ TV, the phone rang.", ["watch", "watched", "was watching", "have watched"], 2, "While غالبًا تأتي مع past continuous للفعل المستمر." ), self.q( "eng_019", "eng_b1_modals", "eng_b1", 5, "Choose: You ___ wear a seat belt in a car.", ["must", "might", "could", "would"], 0, "نحتاج كلمة تدل على الإلزام." ), self.q( "eng_020", "eng_b1_connectors", "eng_b1", 5, "Choose: I was tired, ___ I finished my work.", ["because", "but", "so", "or"], 1, "يوجد تضاد بين التعب والجملة الثانية، فنستخدم but." ), self.q( "eng_021", "eng_b1_connectors", "eng_b1", 5, "Choose: I stayed home ___ it was raining.", ["because", "but", "although", "or"], 0, "الجملة الثانية تشرح السبب." ), self.q( "eng_022", "eng_b1_inference", "eng_b1", 6, "Read: 'Lina packed her swimsuit and towel. She was excited.' Where is Lina probably going?", ["to the library", "to the beach", "to the hospital", "to the bank"], 1, "استنتج من swimsuit و towel." ), self.q( "eng_023", "eng_b1_inference", "eng_b1", 6, "Read: 'The streets were wet, and people carried umbrellas.' What probably happened?", ["It rained", "It snowed", "It was very hot", "There was a test"], 0, "استنتج من wet streets و umbrellas." ), self.q( "eng_024", "eng_b1_tenses", "eng_b1", 6, "Choose the best sentence.", ["She has lived here since 2020.", "She have lived here since 2020.", "She lived here since 2020.", "She living here since 2020."], 0, "مع since وفترة مستمرة غالبًا نستخدم present perfect." ) ] def normalize_subject(self, subject): raw = str(subject or "").strip() return self.subject_aliases.get(raw, self.subject_aliases.get(raw.lower(), raw.lower())) def parse_grade_number(self, grade): if grade is None: return None text = str(grade).strip() arabic_words = { "الأول": 1, "الاول": 1, "اول": 1, "الثاني": 2, "ثاني": 2, "الثالث": 3, "ثالث": 3, "الرابع": 4, "رابع": 4, "الخامس": 5, "خامس": 5, "السادس": 6, "سادس": 6, "السابع": 7, "سابع": 7, "الثامن": 8, "ثامن": 8, "التاسع": 9, "تاسع": 9, "العاشر": 10, "عاشر": 10, "الحادي عشر": 11, "حادي عشر": 11, "الثاني عشر": 12, "ثاني عشر": 12 } for word, number in arabic_words.items(): if word in text: return number arabic_digits = "٠١٢٣٤٥٦٧٨٩" western_digits = "0123456789" trans = str.maketrans(arabic_digits, western_digits) normalized = text.translate(trans) match = re.search(r"\d+", normalized) if match: try: value = int(match.group(0)) if 1 <= value <= 12: return value except Exception: return None return None def grade_band_for_subject(self, subject_key, grade_number): if grade_number is None: return { "min_difficulty": 1, "max_difficulty": 8, "target_count": 18 if subject_key == "math" else 18 } if subject_key == "math": if grade_number <= 3: return {"min_difficulty": 1, "max_difficulty": 3, "target_count": 12} if grade_number <= 5: return {"min_difficulty": 1, "max_difficulty": 5, "target_count": 16} if grade_number <= 7: return {"min_difficulty": 2, "max_difficulty": 7, "target_count": 20} if grade_number <= 9: return {"min_difficulty": 3, "max_difficulty": 8, "target_count": 22} return {"min_difficulty": 4, "max_difficulty": 8, "target_count": 22} if subject_key == "english": if grade_number <= 3: return {"min_difficulty": 1, "max_difficulty": 3, "target_count": 12} if grade_number <= 6: return {"min_difficulty": 1, "max_difficulty": 5, "target_count": 18} if grade_number <= 9: return {"min_difficulty": 2, "max_difficulty": 6, "target_count": 20} return {"min_difficulty": 3, "max_difficulty": 6, "target_count": 20} return {"min_difficulty": 1, "max_difficulty": 8, "target_count": 18} def select_questions(self, subject_key, grade=None): questions = list(self.questions_bank.get(subject_key, [])) grade_number = self.parse_grade_number(grade) band = self.grade_band_for_subject(subject_key, grade_number) selected = [ q for q in questions if band["min_difficulty"] <= int(q.get("difficulty", 1)) <= band["max_difficulty"] ] if len(selected) < 8: selected = questions target_count = band["target_count"] if len(selected) > target_count: selected = selected[:target_count] output = [] for i, question in enumerate(selected, start=1): q2 = dict(question) q2["number"] = i output.append(q2) return output def create_level_test(self, subject="رياضيات", grade=None, student=None, identity=None, **kwargs): subject_key = self.normalize_subject(subject) if isinstance(student, dict): grade = grade or student.get("grade") or student.get("class") or student.get("level") if subject_key not in self.questions_bank: subject_key = "math" questions = self.select_questions(subject_key, grade=grade) grade_number = self.parse_grade_number(grade) test_data = { "success": True, "test_id": self.make_test_id(subject_key, identity), "version": self.version, "basis": "global_level_not_curriculum", "subject": subject, "subject_key": subject_key, "grade": grade, "grade_number": grade_number, "created_at": datetime.utcnow().isoformat(), "instructions": "هذا فحص مستوى عالمي عام لتحديد الفجوات. لا يعتمد على ترتيب المنهاج الأردني.", "questions": questions, "answer_format": "اكتب الإجابات مثل: q1: أ أو q1: A" } return test_data def make_test_id(self, subject_key, identity=None): stamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S") safe_identity = str(identity or "student").replace(" ", "_")[:30] return f"global_{subject_key}_{safe_identity}_{stamp}" def create_test(self, subject="رياضيات", grade=None, student=None, identity=None, **kwargs): return self.create_level_test(subject=subject, grade=grade, student=student, identity=identity, **kwargs) def create_diagnostic_test(self, subject="رياضيات", grade=None, student=None, identity=None, **kwargs): return self.create_level_test(subject=subject, grade=grade, student=student, identity=identity, **kwargs) def build_level_test(self, subject="رياضيات", grade=None, student=None, identity=None, **kwargs): return self.create_level_test(subject=subject, grade=grade, student=student, identity=identity, **kwargs) def create_level_test_for_subject(self, subject="رياضيات", grade=None, student=None, identity=None, **kwargs): return self.create_level_test(subject=subject, grade=grade, student=student, identity=identity, **kwargs) def format_test_for_student(self, test_data): if not isinstance(test_data, dict): return "لا يوجد اختبار صالح." questions = test_data.get("questions", []) subject = test_data.get("subject", "") basis = test_data.get("basis", "") text = f""" # فحص المستوى العالمي المادة: {subject} نوع الفحص: {basis} التعليمات: اكتب الإجابات في صندوق الإجابة بهذا الشكل: q1: أ q2: ب q3: ج --- """.strip() letters = ["أ", "ب", "ج", "د"] for question in questions: number = question.get("number", "") text += f"\n\n## السؤال {number}\n" text += f"{question.get('question', '')}\n" choices = question.get("choices", []) for i, choice in enumerate(choices): letter = letters[i] if i < len(letters) else str(i + 1) text += f"\n{letter}) {choice}" if question.get("needs_whiteboard"): text += "\n\nملاحظة: هذا السؤال يستفيد من الكتابة أو الحل على ورقة." return text.strip() def parse_answers(self, answers_text): answers = {} text = str(answers_text or "").strip() if not text: return answers lines = re.split(r"[\n,؛;]+", text) for line in lines: line = line.strip() if not line: continue match = re.search(r"(?:q|س|السؤال)?\s*(\d+)\s*[:=\-\)]\s*([^\s]+)", line, flags=re.IGNORECASE) if match: number = int(match.group(1)) answer = match.group(2).strip() answers[number] = answer continue simple = re.search(r"^(\d+)\s+([^\s]+)$", line) if simple: answers[int(simple.group(1))] = simple.group(2).strip() return answers def answer_to_index(self, answer, choices=None): raw = str(answer or "").strip() raw = raw.replace("ـ", "") if raw in self.answer_letters: return self.answer_letters[raw] upper = raw.upper() if upper in self.answer_letters: return self.answer_letters[upper] if choices: for i, choice in enumerate(choices): if raw == str(choice).strip(): return i return None def evaluate_test(self, test_data, answers_text=None, answers=None, **kwargs): if not isinstance(test_data, dict): return { "success": False, "message": "بيانات الاختبار غير صالحة." } questions = test_data.get("questions", []) if not isinstance(questions, list) or not questions: return { "success": False, "message": "لا توجد أسئلة للتصحيح." } if answers is None: answers = self.parse_answers(answers_text) total = len(questions) correct = 0 details = [] skill_stats = {} level_stats = {} for question in questions: number = int(question.get("number", len(details) + 1)) choices = question.get("choices", []) correct_index = int(question.get("answer_index", 0)) student_raw = answers.get(number, "") student_index = self.answer_to_index(student_raw, choices=choices) is_correct = student_index == correct_index if is_correct: correct += 1 skill_id = question.get("skill_id", "unknown_skill") level_id = question.get("level_id", "unknown_level") if skill_id not in skill_stats: skill_stats[skill_id] = { "skill_id": skill_id, "skill_title": self.get_skill_title(test_data.get("subject_key"), skill_id), "total": 0, "correct": 0, "questions": [] } skill_stats[skill_id]["total"] += 1 if is_correct: skill_stats[skill_id]["correct"] += 1 skill_stats[skill_id]["questions"].append(number) if level_id not in level_stats: level_stats[level_id] = { "level_id": level_id, "level_title": self.get_level_title(test_data.get("subject_key"), level_id), "total": 0, "correct": 0 } level_stats[level_id]["total"] += 1 if is_correct: level_stats[level_id]["correct"] += 1 details.append({ "number": number, "question_id": question.get("id"), "skill_id": skill_id, "skill_title": self.get_skill_title(test_data.get("subject_key"), skill_id), "student_answer": student_raw, "student_index": student_index, "correct_index": correct_index, "correct_answer": question.get("correct_answer", ""), "is_correct": is_correct, "teacher_hint": question.get("teacher_hint", "") }) percent = round((correct / total) * 100, 2) if total else 0 skill_analysis = self.analyze_skill_stats(skill_stats) level_analysis = self.analyze_level_stats(level_stats) subject_key = test_data.get("subject_key") global_level = self.estimate_global_level(subject_key, percent, level_analysis) gap_plan = self.build_gap_plan(subject_key, skill_analysis, global_level) return { "success": True, "test_id": test_data.get("test_id"), "version": self.version, "basis": "global_level_not_curriculum", "subject": test_data.get("subject"), "subject_key": subject_key, "grade": test_data.get("grade"), "total_questions": total, "correct_answers": correct, "percent": percent, "global_level": global_level, "skill_analysis": skill_analysis, "level_analysis": level_analysis, "weak_skills": [s for s in skill_analysis if s.get("status") == "weak"], "medium_skills": [s for s in skill_analysis if s.get("status") == "medium"], "strong_skills": [s for s in skill_analysis if s.get("status") == "strong"], "gap_plan": gap_plan, "details": details, "created_at": datetime.utcnow().isoformat(), "next_step": gap_plan.get("next_step", "ابدأ بخطة تقوية قصيرة حسب أضعف المهارات.") } def submit_level_test(self, test_data, answers_text=None, **kwargs): return self.evaluate_test(test_data, answers_text=answers_text, **kwargs) def grade_test(self, test_data, answers_text=None, **kwargs): return self.evaluate_test(test_data, answers_text=answers_text, **kwargs) def evaluate_answers(self, test_data, answers_text=None, **kwargs): return self.evaluate_test(test_data, answers_text=answers_text, **kwargs) def analyze_skill_stats(self, skill_stats): output = [] for skill_id, item in skill_stats.items(): total = item.get("total", 0) correct = item.get("correct", 0) percent = round((correct / total) * 100, 2) if total else 0 if percent < 50: status = "weak" elif percent < 80: status = "medium" else: status = "strong" output.append({ "skill_id": skill_id, "skill_title": item.get("skill_title", skill_id), "total": total, "correct": correct, "percent": percent, "status": status, "questions": item.get("questions", []) }) status_order = {"weak": 0, "medium": 1, "strong": 2} output.sort(key=lambda x: (status_order.get(x.get("status"), 9), x.get("percent", 0))) return output def analyze_level_stats(self, level_stats): output = [] for level_id, item in level_stats.items(): total = item.get("total", 0) correct = item.get("correct", 0) percent = round((correct / total) * 100, 2) if total else 0 output.append({ "level_id": level_id, "level_title": item.get("level_title", level_id), "total": total, "correct": correct, "percent": percent }) return output def estimate_global_level(self, subject_key, percent, level_analysis): if subject_key == "english": if percent < 25: return { "code": "pre_a1_gap", "title": "Pre-A1 مع فجوات واضحة", "description": "الطالب يحتاج تأسيسًا في الكلمات والجمل الأساسية." } if percent < 45: return { "code": "a1", "title": "A1 مبتدئ", "description": "الطالب يفهم بعض الجمل الأساسية ويحتاج بناء القواعد والمفردات." } if percent < 65: return { "code": "a2", "title": "A2 أساسي", "description": "الطالب قادر على فهم جمل وفقرات قصيرة ويحتاج تطوير القراءة والقواعد." } if percent < 82: return { "code": "b1", "title": "B1 متوسط", "description": "الطالب جيد في الأساسيات ويحتاج تقوية الاستنتاج والأزمنة في السياق." } return { "code": "b1_plus", "title": "B1+ / بداية B2", "description": "الطالب متقدم نسبيًا ويحتاج تحديات قراءة وكتابة أعمق." } if percent < 35: return { "code": "foundation_gap", "title": "فجوة تأسيسية واضحة", "description": "يحتاج الطالب إلى إعادة بناء المهارات الأساسية قبل التقدم." } if percent < 55: return { "code": "basic", "title": "مستوى أساسي", "description": "يمتلك الطالب بعض الأساسيات مع فجوات تحتاج خطة قصيرة." } if percent < 75: return { "code": "developing", "title": "مستوى نامٍ", "description": "الطالب قريب من المستوى المناسب ويحتاج تثبيتًا وممارسة." } if percent < 90: return { "code": "on_track", "title": "على المسار", "description": "الطالب جيد ويمكنه متابعة الدروس مع معالجة فجوات محددة." } return { "code": "advanced", "title": "متقدم", "description": "الطالب قوي ويحتاج أسئلة تحدي وتوسيع." } def build_gap_plan(self, subject_key, skill_analysis, global_level): weak = [s for s in skill_analysis if s.get("status") == "weak"] medium = [s for s in skill_analysis if s.get("status") == "medium"] strong = [s for s in skill_analysis if s.get("status") == "strong"] priority_skills = weak[:5] if weak else medium[:3] steps = [] for skill in priority_skills: status = skill.get("status") rule = self.plan_rules.get(status, self.plan_rules["medium"]) steps.append({ "skill_id": skill.get("skill_id"), "skill_title": skill.get("skill_title"), "status": status, "action": rule.get("action"), "lesson_time_minutes": rule.get("lesson_time_minutes"), "practice_questions": rule.get("practice_questions"), "suggested_games": self.suggest_games_for_skill(subject_key, skill.get("skill_id")) }) if weak: next_step = "ابدأ بخطة قصر الفجوة من أضعف مهارة، ولا تدخل درسًا جديدًا قبل نشاط تقوية قصير." elif medium: next_step = "ابدأ الدرس مع مراجعة مركزة للمهارات المتوسطة." else: next_step = "الطالب جاهز للدرس، استخدم أسئلة تحدي وألعاب مراجعة قصيرة." return { "global_level": global_level, "priority_skills": priority_skills, "steps": steps, "summary": { "weak_count": len(weak), "medium_count": len(medium), "strong_count": len(strong) }, "next_step": next_step } def suggest_games_for_skill(self, subject_key, skill_id): skill_id = str(skill_id or "") if subject_key == "english": if "reading" in skill_id or "inference" in skill_id: return ["treasure_hunt", "mailman"] if "vocab" in skill_id or "letters" in skill_id: return ["mickey_mouse", "treasure_hunt"] return ["beauty_beast", "obstacle_race"] if "fraction" in skill_id or "decimal" in skill_id: return ["obstacle_race", "magic_cube"] if "operations" in skill_id or "add" in skill_id or "multiply" in skill_id: return ["fishing_friends", "sonic"] if "ratio" in skill_id or "percent" in skill_id: return ["light_bulbs", "obstacle_race"] if "geometry" in skill_id or "data" in skill_id: return ["science_world", "magic_cube"] if "patterns" in skill_id or "integers" in skill_id: return ["treasure_hunt", "obstacle_race"] return ["fishing_friends", "treasure_hunt"] def get_skill_title(self, subject_key, skill_id): subject_key = self.normalize_subject(subject_key) subject = self.skills_map.get(subject_key, {}) for level in subject.get("levels", []): for skill in level.get("skills", []): if skill.get("id") == skill_id: return skill.get("title", skill_id) return skill_id def get_level_title(self, subject_key, level_id): subject_key = self.normalize_subject(subject_key) subject = self.skills_map.get(subject_key, {}) for level in subject.get("levels", []): if level.get("level_id") == level_id: return level.get("title", level_id) return level_id def format_result_for_display(self, result): if not isinstance(result, dict): return "لا توجد نتيجة صالحة." if not result.get("success", True): return f"❌ {result.get('message', 'فشل التصحيح.')}" global_level = result.get("global_level", {}) gap_plan = result.get("gap_plan", {}) weak_skills = result.get("weak_skills", []) medium_skills = result.get("medium_skills", []) strong_skills = result.get("strong_skills", []) text = f""" # نتيجة فحص المستوى العالمي المادة: {result.get('subject', '')} النتيجة: {result.get('correct_answers', 0)} من {result.get('total_questions', 0)} النسبة: {result.get('percent', 0)}% ## المستوى العالمي التقديري {global_level.get('title', '')} {global_level.get('description', '')} --- ## المهارات الضعيفة """.strip() if weak_skills: for skill in weak_skills: text += f"\n- {skill.get('skill_title')}: {skill.get('correct')}/{skill.get('total')} ({skill.get('percent')}%)" else: text += "\nلا توجد مهارات ضعيفة واضحة في هذا الفحص." text += "\n\n## المهارات المتوسطة\n" if medium_skills: for skill in medium_skills: text += f"\n- {skill.get('skill_title')}: {skill.get('correct')}/{skill.get('total')} ({skill.get('percent')}%)" else: text += "\nلا توجد مهارات متوسطة تحتاج متابعة واضحة." text += "\n\n## المهارات القوية\n" if strong_skills: for skill in strong_skills[:8]: text += f"\n- {skill.get('skill_title')}: {skill.get('correct')}/{skill.get('total')} ({skill.get('percent')}%)" else: text += "\nلم تظهر مهارات قوية كافية بعد." text += "\n\n---\n\n## خطة قصر الفجوة المقترحة\n" steps = gap_plan.get("steps", []) if steps: for i, step in enumerate(steps, start=1): games = ", ".join(step.get("suggested_games", [])) text += f"\n### خطوة {i}: {step.get('skill_title')}\n" text += f"- الحالة: {step.get('status')}\n" text += f"- الإجراء: {step.get('action')}\n" text += f"- الزمن المقترح: {step.get('lesson_time_minutes')} دقيقة\n" text += f"- تدريبات مقترحة: {step.get('practice_questions')}\n" text += f"- ألعاب مناسبة: {games}\n" else: text += "\nلا توجد فجوات واضحة. استخدم نشاط تحدي قصير." text += f"\n\n## الخطوة التالية\n{result.get('next_step', '')}" return text.strip() def export_skills_map_json(self): return json.dumps(self.skills_map, ensure_ascii=False, indent=2) def export_questions_json(self): return json.dumps(self.questions_bank, ensure_ascii=False, indent=2)