divid_teacher / level_test_manager.py
Asem75's picture
Update level_test_manager.py
d4ef8ed verified
Raw
History Blame Contribute Delete
52.3 kB
# 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)