بدء استخدام قواعد التقييم

rubric هو نموذج يمكن للمعلّمين استخدامه عند وضع درجات على مهام الطلاب. تتيح لك Classroom API العمل نيابةً عن المعلّم لإدارة قواعد التقييم هذه.

عرض قواعد التقييم في واجهة مستخدم Classroom الشكل 1. عرض نموذج لقواعد التقييم في مهمة دراسية في Classroom

يشرح هذا الدليل المفاهيم الأساسية لواجهة برمجة التطبيقات Rubrics API ووظائفها. يمكنك الاطّلاع على مقالات "مركز المساعدة" هذه للتعرّف على البنية العامة لقواعد التقييم وآلية تطبيق درجات التقييم في واجهة مستخدم Classroom.

المتطلبات الأساسية

يفترض هذا الدليل أن لديك ما يلي:

تفويض بيانات الاعتماد لتطبيق متوافق مع أجهزة سطح المكتب

للمصادقة بصفتك مستخدمًا نهائيًا والوصول إلى بيانات المستخدم في تطبيقك، عليك إنشاء معرّف عميل OAuth 2.0 واحد أو أكثر. يُستخدم معرّف العميل لتحديد تطبيق واحد لخوادم OAuth من Google. إذا كان تطبيقك يعمل على أنظمة أساسية متعددة، يجب إنشاء معرِّف عميل منفصل لكل نظام أساسي.

  1. انتقِل إلى صفحة "بيانات اعتماد Google Cloud Platform" في Google Cloud Console.
  2. انقر على إنشاء بيانات اعتماد > معرِّف عميل OAuth.
  3. انقر على نوع التطبيق > تطبيق سطح المكتب.
  4. في الحقل الاسم، اكتب اسمًا لبيانات الاعتماد. لا يظهر هذا الاسم إلا في وحدة تحكم Google Cloud. على سبيل المثال، "عميل معاينة قواعد التقييم".
  5. انقر على إنشاء. ستظهر شاشة عميل OAuth تم إنشاؤها، وتعرض معرّف العميل وسر العميل الجديدين.
  6. انقر على تنزيل JSON، ثم حسنًا. تظهر بيانات الاعتماد التي تم إنشاؤها حديثًا ضمن معرِّفات عملاء OAuth 2.0.
  7. احفظ ملف JSON الذي تم تنزيله بتنسيق credentials.json، وانقل الملف إلى دليل العمل.
  8. انقر على إنشاء بيانات اعتماد > مفتاح واجهة برمجة التطبيقات ودوِّن مفتاح واجهة برمجة التطبيقات.

يُرجى الاطّلاع على مقالة إنشاء بيانات اعتماد الوصول لمعرفة المزيد من المعلومات.

إعداد نطاقات OAuth

بناءً على نطاقات OAuth الحالية لمشروعك، قد تحتاج إلى ضبط نطاقات الإضافة.

  1. انتقِل إلى شاشة موافقة OAuth.
  2. انقر على Edit App (تعديل التطبيق) > Save and Continue (حفظ ومتابعة) للوصول إلى شاشة "النطاقات".
  3. انقر على إضافة نطاقات أو إزالتها.
  4. أضِف النطاقات التالية إذا لم تكن لديك حاليًا:
    • https://www.googleapis.com/auth/classroom.coursework.students
    • https://www.googleapis.com/auth/classroom.courses
  5. ثم انقر على تحديث > حفظ ومتابعة > حفظ ومتابعة > الرجوع إلى لوحة التحكم.

يمكنك الاطّلاع على ضبط شاشة طلب الموافقة المتعلّقة ببروتوكول OAuth لمعرفة المزيد من المعلومات.

يتيح نطاق classroom.coursework.students إمكانية الوصول للقراءة والكتابة إلى قواعد التقييم (بالإضافة إلى CourseWork)، ويتيح نطاق classroom.courses إمكانية قراءة الدورات التدريبية وكتابتها.

يتم سرد النطاقات المطلوبة لطريقة معينة في الوثائق المرجعية للطريقة. يمكنك الاطّلاع على courses.courseWork.rubrics.create نطاقات التفويض كمثال. يمكنك الاطّلاع على جميع نطاقات Classroom في نطاقات OAuth 2.0 لواجهات Google API. لم يتم ذكر قواعد التقييم هنا لأن واجهة برمجة التطبيقات لا تزال في مرحلة المعاينة.

ضبط النموذج

في دليل العمل، ثبِّت مكتبة برامج Google للغة Python:

pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib

أنشئ ملفًا باسم main.py ينشئ مكتبة البرامج ويمنح المستخدم الإذن باستخدام مفتاح واجهة برمجة التطبيقات بدلاً من YOUR_API_KEY:

import json
import os.path

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/classroom.courses',
          'https://www.googleapis.com/auth/classroom.coursework.students']

def build_authenticated_service(api_key):
    """Builds the Classroom service."""
    creds = None
    # The file token.json stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run.
        with open('token.json', 'w') as token:
            token.write(creds.to_json())

    try:
        # Build the Classroom service.
        service = build(
            serviceName="classroom",
            version="v1",
            credentials=creds,
            discoveryServiceUrl=f"https://classroom.googleapis.com/$discovery/rest?labels=DEVELOPER_PREVIEW&key={api_key}")

        return service

    except HttpError as error:
        print('An error occurred: %s' % error)

if __name__ == '__main__':
    service = build_authenticated_service(YOUR_API_KEY)

شغِّل النص البرمجي باستخدام python main.py. يجب أن يُطلب منك تسجيل الدخول والموافقة على نطاقات OAuth.

إنشاء مَهمّة دراسية

ترتبط قواعد التقييم بالمهمة الدراسية أو السمة CourseWork، وليس لها قيمة إلا في سياق سمة CourseWork هذه. لا يمكن إنشاء قواعد التقييم إلا من خلال مشروع Google Cloud الذي أنشأ العنصر الرئيسي CourseWork. لأغراض هذا الدليل، عليك إنشاء مهمة CourseWork جديدة باستخدام نص برمجي.

إضافة ما يلي إلى main.py:

def get_latest_course(service):
    """Retrieves the last created course."""
    try:
        response = service.courses().list(pageSize=1).execute()
        courses = response.get("courses", [])
        if not courses:
            print("No courses found. Did you remember to create one in the UI?")
            return
        course = courses[0]
        return course

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

def create_coursework(service, course_id):
    """Creates and returns a sample coursework."""
    try:
        coursework = {
            "title": "Romeo and Juliet analysis.",
            "description": """Write a paper arguing that Romeo and Juliet were
                                time travelers from the future.""",
            "workType": "ASSIGNMENT",
            "state": "PUBLISHED",
        }
        coursework = service.courses().courseWork().create(
            courseId=course_id, body=coursework).execute()
        return coursework

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

والآن، عدِّل main.py لاسترداد course_id لفئة الاختبار التي أنشأتها للتو، وإنشاء نموذج مَهمة جديدة، واسترداد coursework_id الخاص بالمهمة:

if __name__ == '__main__':
    service = build_authenticated_service(YOUR_API_KEY)

    course = get_latest_course(service)
    course_id = course.get("id")
    course_name = course.get("name")
    print(f"'{course_name}' course ID: {course_id}")

    coursework = create_coursework(service, course_id)
    coursework_id = coursework.get("id")
    print(f"Assignment created with ID {coursework_id}")

    #TODO(developer): Save the printed course and coursework IDs.

احفظ course_id وcoursework_id. وهي مطلوبة لجميع عمليات CRUD لقواعد التقييم.

من المفترض أن يتوفّر لديك الآن نموذج CourseWork في Classroom.

عرض مهمة في واجهة مستخدم Classroom الشكل 2. عرض نموذج لمَهمة دراسية في Classroom

إنشاء قواعد تقييم

أصبحت الآن جاهزًا لبدء إدارة قواعد التقييم.

يمكن إنشاء قواعد تقييم على CourseWork باستخدام استدعاء Create الذي يتضمن عنصر قواعد التقييم الكامل، حيث يتم حذف خصائص رقم التعريف للمعايير والمستويات (يتم إنشاؤها عند الإنشاء).

أضف الدالة التالية إلى main.py:

def create_rubric(service, course_id, coursework_id):
    """Creates an example rubric on a coursework."""
    try:
        body = {
            "criteria": [
                {
                    "title": "Argument",
                    "description": "How well structured your argument is.",
                    "levels": [
                        {"title": "Convincing",
                         "description": "A compelling case is made.", "points": 30},
                        {"title": "Passable",
                         "description": "Missing some evidence.", "points": 20},
                        {"title": "Needs Work",
                         "description": "Not enough strong evidence..", "points": 0},
                    ]
                },
                {
                    "title": "Spelling",
                    "description": "How well you spelled all the words.",
                    "levels": [
                        {"title": "Perfect",
                         "description": "No mistakes.", "points": 20},
                        {"title": "Great",
                         "description": "A mistake or two.", "points": 15},
                        {"title": "Needs Work",
                         "description": "Many mistakes.", "points": 5},
                    ]
                },
                {
                    "title": "Grammar",
                    "description": "How grammatically correct your sentences are.",
                    "levels": [
                        {"title": "Perfect",
                         "description": "No mistakes.", "points": 20},
                        {"title": "Great",
                         "description": "A mistake or two.", "points": 15},
                        {"title": "Needs Work",
                         "description": "Many mistakes.", "points": 5},
                    ]
                },
            ]
        }

        rubric = service.courses().courseWork().rubrics().create(
            courseId=course_id, courseWorkId=coursework_id, body=body,
            # Specify the preview version. Rubrics CRUD capabilities are
            # supported in V1_20231110_PREVIEW and later.
            previewVersion="V1_20231110_PREVIEW"
            ).execute()
        print(f"Rubric created with ID {rubric.get('id')}")
        return rubric

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

بعد ذلك، يمكنك تحديث main.py وتشغيله لإنشاء نموذج قواعد التقييم باستخدام معرّفَي Course وCourseWork السابقَين:

if __name__ == '__main__':
    service = build_authenticated_service(YOUR_API_KEY)

    rubric = create_rubric(service, YOUR_COURSE_ID, YOUR_COURSEWORK_ID)
    print(json.dumps(rubric, indent=4))

بعض النقاط حول تمثيل قواعد التقييم:

  • يظهر ترتيب المعيار والمستوى في واجهة مستخدم Classroom.
  • المستويات التي تم تقييمها (تلك التي تتضمّن السمة points)، يجب ترتيبها حسب النقاط بترتيب تصاعدي أو تنازلي (لا يمكن ترتيبها بشكل عشوائي).
  • يستطيع المعلمون إعادة فرز المعايير والمستويات المقيّمة (ولكن ليس المستويات غير المصنفة) في واجهة المستخدم، وهذا يغير ترتيبهم في البيانات.

راجِع القيود للاطّلاع على مزيد من التنبيهات بشأن بنية قواعد التقييم.

مرة أخرى في واجهة المستخدم، من المفترض أن تظهر لك قواعد التقييم المتعلقة بالمهمة.

عرض قواعد التقييم في واجهة مستخدم Classroom الشكل 3. عرض نموذج لقواعد التقييم في مهمة دراسية في Classroom

قراءة قواعد التقييم

يمكن قراءة قواعد التقييم باستخدام الطريقتَين العاديتَين List وGet.

يمكن أن يكون هناك نموذج تقييم واحد كحد أقصى في المهمة، لذا قد يبدو List غير سهل، لكنه مفيد إذا لم يكن لديك رقم تعريف نموذج التقييم. في حال عدم توفّر قواعد تقييم مرتبطة بسمة CourseWork، ستكون استجابة List فارغة.

أضف الدالة التالية إلى main.py:

def get_rubric(service, course_id, coursework_id):
    """
    Get the rubric on a coursework. There can only be at most one.
    Returns null if there is no rubric.
    """
    try:
        response = service.courses().courseWork().rubrics().list(
            courseId=course_id, courseWorkId=coursework_id,
            # Specify the preview version. Rubrics CRUD capabilities are
            # supported in V1_20231110_PREVIEW and later.
            previewVersion="V1_20231110_PREVIEW"
            ).execute()

        rubrics = response.get("rubrics", [])
        if not rubrics:
            print("No rubric found for this assignment.")
            return
        rubric = rubrics[0]
        return rubric

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

يمكنك تحديث main.py وتشغيله لجلب قواعد التقييم التي أضفتها:

if __name__ == '__main__':
    service = build_authenticated_service(YOUR_API_KEY)

    rubric = get_rubric(service, YOUR_COURSE_ID, YOUR_COURSEWORK_ID)
    print(json.dumps(rubric, indent=4))

    #TODO(developer): Save the printed rubric ID.

دوِّن السمة id في قواعد التقييم للاطّلاع على الخطوات اللاحقة.

تعمل السمة Get بشكل جيد عند توفّر رقم تعريف قواعد التقييم. قد يبدو استخدام Get في الدالة بدلاً من ذلك كما يلي:

def get_rubric(service, course_id, coursework_id, rubric_id):
    """
    Get the rubric on a coursework. There can only be at most one.
    Returns a 404 if there is no rubric.
    """
    try:
        rubric = service.courses().courseWork().rubrics().get(
            courseId=course_id,
            courseWorkId=coursework_id,
            id=rubric_id,
            # Specify the preview version. Rubrics CRUD capabilities are
            # supported in V1_20231110_PREVIEW and later.
            previewVersion="V1_20231110_PREVIEW"
        ).execute()

        return rubric

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

تعرض عملية التنفيذ هذه خطأ 404 في حال عدم توفُّر قواعد تقييم.

تعديل قواعد التقييم

يتم تطبيق تعديلات قواعد التقييم باستخدام مكالمات Patch. بسبب البنية المعقّدة لقواعد التقييم، يجب إجراء التعديلات باستخدام نمط القراءة والتعديل والكتابة، حيث يتم استبدال سمة criteria بالكامل.

قواعد التحديث هي كما يلي:

  1. تُعتبر المعايير أو المستويات التي تتم إضافتها بدون معرّف إضافات.
  2. تُعتبر المعايير أو المستويات المفقودة من قبل عمليات حذف.
  3. تُعتبر المعايير أو المستويات التي تتضمّن معرّف حالي ولكن بيانات معدَّلة تعديلات. يتم ترك الخصائص غير المعدّلة كما هي.
  4. تُعتبر المعايير أو المستويات التي يتم توفيرها بمعرّفات جديدة أو غير معروفة أخطاءً.
  5. ويُعد ترتيب المعايير والمستويات الجديدة بمثابة ترتيب واجهة المستخدم الجديد (مع القيود المذكورة أعلاه).

إضافة دالة لتعديل قواعد التقييم:

def update_rubric(service, course_id, coursework_id, rubric_id, body):
    """
    Updates the rubric on a coursework.
    """
    try:
        rubric = service.courses().courseWork().rubrics().patch(
            courseId=course_id,
            courseWorkId=coursework_id,
            id=rubric_id,
            body=body,
            updateMask='criteria',
            # Specify the preview version. Rubrics CRUD capabilities are
            # supported in V1_20231110_PREVIEW and later.
            previewVersion="V1_20231110_PREVIEW"
        ).execute()

        return rubric

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

في هذا المثال، تم تحديد الحقل criteria للتعديل باستخدام updateMask.

بعد ذلك، عدِّل main.py لإجراء تغيير على كل قاعدة من قواعد التعديل المذكورة أعلاه:

if __name__ == '__main__':
    service = build_authenticated_service(YOUR_API_KEY)

    # Get the latest rubric.
    rubric = get_rubric(service, YOUR_COURSE_ID, YOUR_COURSEWORK_ID)
    criteria = rubric.get("criteria")
    """
    The "criteria" property should look like this:
    [
        {
            "id": "NkEyMdMyMzM2Nxkw",
            "title": "Argument",
            "description": "How well structured your argument is.",
            "levels": [
                {
                    "id": "NkEyMdMyMzM2Nxkx",
                    "title": "Convincing",
                    "description": "A compelling case is made.",
                    "points": 30
                },
                {
                    "id": "NkEyMdMyMzM2Nxky",
                    "title": "Passable",
                    "description": "Missing some evidence.",
                    "points": 20
                },
                {
                    "id": "NkEyMdMyMzM2Nxkz",
                    "title": "Needs Work",
                    "description": "Not enough strong evidence..",
                    "points": 0
                }
            ]
        },
        {
            "id": "NkEyMdMyMzM2Nxk0",
            "title": "Spelling",
            "description": "How well you spelled all the words.",
            "levels": [...]
        },
        {
            "id": "NkEyMdMyMzM2Nxk4",
            "title": "Grammar",
            "description": "How grammatically correct your sentences are.",
            "levels": [...]
        }
    ]
    """

    # Make edits. This example will make one of each type of change.

    # Add a new level to the first criteria. Levels must remain sorted by
    # points.
    new_level = {
        "title": "Profound",
        "description": "Truly unique insight.",
        "points": 50
    }
    criteria[0]["levels"].insert(0, new_level)

    # Remove the last criteria.
    del criteria[-1]

    # Update the criteria titles with numeric prefixes.
    for index, criterion in enumerate(criteria):
        criterion["title"] = f"{index}: {criterion['title']}"

    # Resort the levels from descending to ascending points.
    for criterion in criteria:
        criterion["levels"].sort(key=lambda level: level["points"])

    # Update the rubric with a patch call.
    new_rubric = update_rubric(
        service, YOUR_COURSE_ID, YOUR_COURSEWORK_ID, YOUR_RUBRIC_ID, rubric)

    print(json.dumps(new_rubric, indent=4))

يجب أن تظهر التغييرات الآن بالنسبة إلى المعلّم في Classroom.

عرض قواعد التقييم المعدّلة في واجهة مستخدم Classroom الشكل 4. عرض قواعد التقييم المعدّلة

الاطّلاع على عمليات الإرسال التي تم تطبيق درجاتها على قواعد التقييم

في الوقت الحالي، لا يمكن وضع درجات على مهام الطلاب باستخدام قواعد تقييم من خلال واجهة برمجة التطبيقات، ولكن يمكنك قراءة درجات قواعد التقييم لعمليات الإرسال التي تم وضع الدرجات لها باستخدام قواعد التقييم في واجهة مستخدم Classroom.

بصفتك طالبًا في واجهة مستخدم Classroom، عليك إكمال نموذج مهمة دراسية وتسليمها. بصفتك معلِّمًا، يمكنك وضع الدرجات على المهمة يدويًا باستخدام قواعد التقييم.

عرض درجة قواعد التقييم في واجهة مستخدم Classroom الشكل 5. صورة للمعلّم لقواعد التقييم أثناء وضع الدرجات

إنّ عمليات إرسال الطلاب التي تم وضع الدرجات لها باستخدام قواعد التقييم تتضمّن خاصيتَين جديدتَين: draftRubricGrades وassignedRubricGrades، وتمثلان النقاط والمستويات التي اختارها المعلّم أثناء المسودة وحالة وضع الدرجات التي تم إسنادها، على التوالي.

بالإضافة إلى ذلك، تحتوي عمليات الإرسال التي يجريها الطلاب والتي لها قواعد تقييم مرتبطة على حقل rubricId حتى قبل وضع الدرجات. ويمثّل هذا أحدث قواعد تقييم مرتبطة بالسمة CourseWork، وقد تتغير هذه القيمة إذا حذف المعلّمون قواعد التقييم وأعادوا إنشاؤها.

يمكنك استخدام طريقتَي studentSubmissions.Get وstudentSubmissions.List الحاليتَين لعرض عمليات الإرسال المقيّمة.

أضِف الدالة التالية إلى main.py لإدراج المهام التي يرسلها الطلاب:

def get_latest_submission(service, course_id, coursework_id):
    """Retrieves the last submission for an assignment."""
    try:
        response = service.courses().courseWork().studentSubmissions().list(
            courseId = course_id,
            courseWorkId = coursework_id,
            pageSize=1,
            # Specify the preview version. Rubrics CRUD capabilities are
            # supported in V1_20231110_PREVIEW and later.
            previewVersion="V1_20231110_PREVIEW"
        ).execute()
        submissions = response.get("studentSubmissions", [])
        if not submissions:
            print(
                """No submissions found. Did you remember to turn in and grade
                   the assignment in the UI?""")
            return
        submission = submissions[0]
        return submission

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

بعد ذلك، يمكنك تعديل main.py وتنفيذه لعرض درجات التسليم.

if __name__ == '__main__':
    service = build_authenticated_service(YOUR_API_KEY)

    submission = get_latest_submission(
        service, YOUR_COURSE_ID, YOUR_COURSEWORK_ID)
    print(json.dumps(submission, indent=4))

يحتوي كل من draftRubricGrades وassignedRubricGrades على:

  • تمثّل هذه السمة criterionId لمعايير قواعد التقييم المقابلة.
  • تمثّل هذه السمة points التي خصصها المعلّم لكل معيار. يمكن أن يكون هذا من المستوى المحدد، ولكن يمكن للمعلم استبدال ذلك أيضًا.
  • تمثّل هذه السمة levelId للمستوى الذي تم اختياره لكل معيار. إذا لم يختر المعلم مستوى، ولكنه لا يزال يحدد نقاطًا للمعيار، فإن هذا الحقل غير موجود.

تحتوي هذه القوائم فقط على إدخالات للمعايير التي اختار فيها المعلّم مستوى أو نقاطًا. على سبيل المثال، إذا اختار المعلّم التفاعل مع معيار واحد فقط أثناء وضع الدرجات، سيكون لكل من draftRubricGrades وassignedRubricGrades عنصر واحد فقط، حتى إذا تضمّن نموذج التقييم العديد من المعايير.

حذف قواعد تقييم

يمكن حذف قواعد التقييم باستخدام طلب Delete عادي. يعرض الكود التالي مثالاً على دالة الاكتمال، ولكن نظرًا لأن عملية وضع الدرجات قد بدأت بالفعل، لا يمكنك حذف قواعد التقييم الحالية:

def delete_rubric(service, course_id, coursework_id, rubric_id):
    """Deletes the rubric on a coursework."""
    try:
        service.courses().courseWork().rubrics().delete(
            courseId=course_id,
            courseWorkId=coursework_id,
            id=rubric_id,
            # Specify the preview version. Rubrics CRUD capabilities are
            # supported in V1_20231110_PREVIEW and later.
            previewVersion="V1_20231110_PREVIEW"
        ).execute()

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

تصدير قواعد التقييم واستيرادها

يمكن تصدير قواعد التقييم يدويًا إلى "جداول بيانات Google" ليتمكّن المعلّمون من إعادة استخدامها.

بالإضافة إلى تحديد معايير قواعد التقييم في الرموز البرمجية، يمكن إنشاء وتحديث قواعد تقييم من أوراق البيانات التي تم تصديرها هذه من خلال تحديد sourceSpreadsheetId في نص قواعد التقييم بدلاً من criteria:

def create_rubric_from_sheet(service, course_id, coursework_id, sheet_id):
    """Creates an example rubric on a coursework."""
    try:
        body = {
            "sourceSpreadsheetId": sheet_id
        }

        rubric = service.courses().courseWork().rubrics().create(
            courseId=course_id, courseWorkId=coursework_id, body=body,
            # Specify the preview version. Rubrics CRUD capabilities are
            # supported in V1_20231110_PREVIEW and later.
            previewVersion="V1_20231110_PREVIEW"
            ).execute()

        print(f"Rubric created with ID {rubric.get('id')}")
        return rubric

    except HttpError as error:
        print(f"An error occurred: {error}")
        return error

إضافة ملاحظات

إذا واجهت أي مشاكل أو كان لديك ملاحظات، يُرجى مشاركة ملاحظاتك.