التعليم والبحث العلمي الأدلة التقنية

ما هو SCORM؟ الإصدارات وكيف يتواصل المحتوى مع LMS

ماذا يحل SCORM، والفرق بين 1.2 و2004، وكيف تبني حزمة بيدك وتتابع كل استدعاء لها في Moodle حتى أخطائها الشائعة.

ما هو SCORM؟ الإصدارات وكيف يتواصل المحتوى مع LMS

لنفرض أن فريق التصميم التعليمي في جامعتك بنى وحدة تدريبية عن أساسيات الشبكات، فيها درس من ثلاث صفحات واختبار قصير، ورفعها إلى Moodle فعملت وظهرت الدرجة في سجل الدرجات، ثم طلبت كلية أخرى الوحدة نفسها لمنصة Open edX، وطلبتها جهة ثالثة لمنصتها الداخلية. فهل يعيد الفريق بناءها ثلاث مرات؟ الإجابة لا، والسبب أن الوحدة صدرت بصيغة SCORM، وهي الصيغة التي تجعل الحزمة نفسها تعمل في أي منصة تعلم LMS تدعمها، وتجعل المنصة تعرف أين وصل المتعلم وما درجته. وفي مقال أنظمة إدارة التعلم LMS ذكرنا أن المنصة يجب أن تستقبل المحتوى من الخارج، وهذا المقال يشرح الطريقة الأقدم والأكثر انتشاراً لذلك من الداخل.

المواضيع في السلسلة:

في هذا المقال سوف نتناول:

  • المشكلة التي يحلها SCORM، ومن أين جاء، وإصداراته من 1.2 إلى SCORM 2004 بإصداراته الأربعة.
  • الفرق العملي بين SCORM 1.2 وSCORM 2004 في جدول: حقول الحالة، والدرجة، وحفظ التقدم، والتسلسل Sequencing.
  • تشريح الحزمة: الملف imsmanifest.xml، والفهرس organizations، والموارد resources، والفرق بين وحدة SCO وملف Asset.
  • دوال التشغيل Runtime API، وكيف تجد الحزمة الكائن API في الصفحة.
  • مختبر: حزمة SCORM 1.2 كتبناها بأيدينا ونسخة SCORM 2004 منها، رفعناها إلى Moodle 5.3 وسجلنا كل استدعاء، ثم ثلاثة أخطاء حقيقية وما يظهر عند كل منها.

ما المشكلة التي يحلها SCORM؟

المحتوى التعليمي التفاعلي في النهاية صفحات HTML وملفات JavaScript، والطريقة البسيطة أن ترفعها إلى أي سيرفر وتضع رابطها في المقرر، وهذا يكفي للعرض فقط، فالمنصة لا تعرف من فتح الصفحة، ولا أين توقف، ولا ماذا أجاب. والحل التالي أن يكتب مزود المحتوى إضافة خاصة لكل منصة، وهذا الحل غير مناسب إطلاقاً، لأن عندك عشرات المنصات ومئات المزودين، وكل منصة جديدة تعني تكاملات جديدة. لذلك ظهر SCORM، واختصاره Sharable Content Object Reference Model، والصورة التالية تبين الفرق:

مخطط من جزأين: على اليسار بدون معيار ثلاثة مزودي محتوى وثلاث منصات Moodle وOpen edX وCanvas بخطوط حمراء متقاطعة بين كل مزود وكل منصة، أي 9 تكاملات خاصة، وعلى اليمين مع SCORM أداة التأليف Authoring Tool تصدر حزمة واحدة course.zip فيها imsmanifest.xml، وتخرج منها أسهم خضراء إلى Moodle بنشاط حزمة SCORM، وOpen edX عبر SCORM XBlock، وCanvas بأداة خارجية External Tool، وChamilo في مسار التعلم Learning Path، ومنصتك أنت بمشغل مثل scorm-again
بدون معيار تحتاج تكاملاً لكل زوج، ومع SCORM يصدر المزود حزمة واحدة تقرؤها كل المنصات

والمعيار يجيب عن سؤالين فقط، وهذا سر انتشاره:

  • شكل الحزمة Content Packaging: ملف Zip واحد، وفي جذره ملف وصف اسمه imsmanifest.xml يقول للمنصة ما عناوين الدروس، وأي ملف تفتحه لكل درس.
  • التشغيل Run-Time: المنصة تفتح الدرس في المتصفح، وتضع في الصفحة كائن JavaScript بدوال محددة، والدرس يستدعي هذه الدوال ليقرأ بيانات المتعلم ويكتب حالته ودرجته، والمنصة تحفظها في قاعدة بياناتها.

وصفحة شرح SCORM من Rustici تذكر أن الهدف الأساسي منه هو التشغيل البيني Interoperability، وأن مبادرة ADL، واختصاراً لـ Advanced Distributed Learning، لم تكتبه من الصفر، وإنما جمعت معايير كانت موجودة لكل جزء من المشكلة وربطتها في نموذج مرجعي Reference Model واحد، ومن هنا جاءت الكلمة الأخيرة في اسمه، وتذكر الصفحة أيضاً أن وزارة الدفاع الأمريكية تشترط تسليم محتواها التدريبي بصيغة SCORM.

إصدارات SCORM: من 1.2 إلى SCORM 2004

بحسب صفحة إصدارات SCORM، فالإصدارات التي تهمك عملياً كما يلي:

الإصدارالتاريخما الذي يميزه
SCORM 1.1يناير 2001أول إصدار قابل للتطبيق، وترك تفاصيل كثيرة بلا حسم، ولا تجده اليوم
SCORM 1.2أكتوبر 2001الإصدار الذي انتشر به المعيار، وما زال الأوسع دعماً
SCORM 2004 الإصدار الأول 1st Editionيناير 2004أضاف كتاب التسلسل والتنقل Sequencing and Navigation، والصفحة تقول إن تسلسله لم يكن قابلاً للتطبيق بالكامل
SCORM 2004 الإصدار الثاني 2nd Editionيوليو 2004إصلاحات سريعة بعد التجربة الأولى
SCORM 2004 الإصدار الثالث 3rd Editionأكتوبر 2006أزال الغموض من التسلسل، وألزم المنصة بعناصر واجهة للتنقل، والصفحة تصفه بأنه الأكثر استخداماً بين إصدارات 2004
SCORM 2004 الإصدار الرابع 4th Editionمارس 2009وضح التسلسل وأضاف له مزايا، وهو الذي توصي به الصفحة لمن يبدأ اليوم

لاحظ أن آخر إصدار صدر في 2009، فالمعيار متوقف عن التطوير منذ سنوات، والتطوير انتقل إلى xAPI وcmi5 اللذين نشرحهما في مقال المقارنة. ومع ذلك فكل أداة تأليف تصدر SCORM، وكل LMS تقريباً يقرؤه، وهذا ما يجعله الخيار الآمن لوحدة يفتحها المتعلم داخل المقرر ويأخذ عليها درجة.

الفرق العملي بين SCORM 1.2 وSCORM 2004

الفكرة واحدة في الإصدارين، ولكن أسماء الدوال والحقول مختلفة، لذلك تكتب الحزمة لإصدار واحد ولا يقرأ أحدهما بيانات الآخر. والصورة التالية تبين الفرق الأهم، وهو حالة المتعلم:

مقارنة بين SCORM 1.2 من أكتوبر 2001 بحقل واحد cmi.core.lesson_status له ست قيم passed وcompleted وfailed وincomplete وbrowsed وnot attempted، وقيمة واحدة فقط فإذا كتبت الحزمة completed ثم passed ضاعت المعلومة الأولى، وكائن window.API بدوال LMSInitialize وLMSSetValue وLMSFinish، وبين SCORM 2004 بحقلين cmi.completion_status لسؤال هل أكمل المحتوى بقيم completed وincomplete وnot attempted وunknown، وcmi.success_status لسؤال هل نجح بقيم passed وfailed وunknown، والمتعلم يمكن أن يكون completed وfailed معاً، والدرجة النسبية cmi.score.scaled تساوي 0.67، وكائن window.API_1484_11 بدوال Initialize وSetValue وTerminate
في SCORM 1.2 حقل واحد للإكمال والنجاح معاً، وفي SCORM 2004 حقلان منفصلان

والجدول التالي يجمع الفروق التي سوف تقابلها في العمل، والأسماء من مرجع نموذج البيانات Run-Time Reference، والقيم في العمود الأخير مما رأيناه في المختبر:

البندSCORM 1.2SCORM 2004
اسم كائن ال API في الصفحةAPIAPI_1484_11
الدوالLMSInitialize، LMSGetValue، LMSSetValue، LMSCommit، LMSFinishInitialize، GetValue، SetValue، Commit، Terminate
حالة المتعلمcmi.core.lesson_status حقل واحدcmi.completion_status وcmi.success_status
الدرجة Scorecmi.core.score.raw مع min وmaxكذلك، ومعها cmi.score.scaled من -1 إلى 1
درجة النجاحadlcp:masteryscore في ملف الوصف، وتقرؤها الحزمة من cmi.student_data.mastery_scoreminNormalizedMeasure في ملف الوصف، وتقرؤها الحزمة من cmi.scaled_passing_score (قرأنا 0.7)
مكان التوقفcmi.core.lesson_locationcmi.location
بيانات الاستئناف suspend_dataحتى 4096 حرفاً4000 في الإصدار الثاني، و64000 في الثالث والرابع
صيغة مدة الجلسة session_timeHHHH:MM:SS مثل 0000:00:04صيغة ISO 8601 مثل PT2S
التسلسل والتنقل Sequencing and Navigationلا يوجد، والمتعلم يتنقل بحرية في القائمةقواعد في ملف الوصف تحدد الترتيب والشروط بين الدروس
الدعم في Moodleمدعوم ويجتاز اختبارات المطابقة بحسب سؤال وجواب SCORM في Moodleالصفحة نفسها تقول إنه غير مدعوم، وإن بعض الحزم قد تعمل جزئياً

وقد يتساءل البعض: إذا كان SCORM 2004 أحدث وأدق، فلماذا لا نطلبه دائماً؟ والإجابة أن الحقلين المنفصلين والتسلسل لا قيمة لهما إذا كانت منصتك لا تطبقهما، وMoodle، وهو أكثر LMS مفتوح المصدر انتشاراً، يقول صراحة إنه لا يدعم SCORM 2004، والحزمة البسيطة تعمل فيه كما سيأتي، ولكن قواعد التسلسل لا ضمان لها. لذلك القاعدة العملية: اطلب SCORM 1.2 ما لم تكن منصتك تدعم SCORM 2004 بالإصدار الرابع وتحتاج فعلاً إلى فصل الإكمال عن النجاح، أو إلى قواعد تسلسل بين الدروس.

ما الذي داخل حزمة SCORM؟

الحزمة ملف Zip عادي، والصورة التالية تبين ما فيه من الحزمة التي سوف نبنيها في المختبر:

مخطط الحزمة dns-scorm12.zip: ملف imsmanifest.xml في جذر الملف المضغوط وفيه البيانات الوصفية metadata بقيم schema ADL SCORM وschemaversion 1.2، وفهرس المقرر organizations الذي يرسمه ال LMS في القائمة الجانبية بثلاثة عناصر item-lesson إلى res-lesson وitem-quiz إلى res-quiz بدرجة نجاح masteryscore 70 وitem-glossary إلى res-glossary، والموارد resources لكل منها scormtype وhref وقائمة ملفات، وأسهم إلى وحدة SCO فيها lesson.html وquiz.html تستدعي ال API، وملف عادي Asset هو glossary.html يعرضه ال LMS فقط، وملفات مشتركة common/scorm12.js وcommon/style.css، وعلى اليمين ما يفعله ال LMS: يقرأ الملف ويحفظ الفهرس، ويرسم القائمة، ويفتح ملف href في iframe، وينتظر استدعاءات ال API من وحدات SCO فقط
العنصر item هو ما يراه المتعلم، والمورد resource هو ما يشغله المتصفح

وهذا ملف الوصف الكامل للحزمة:

<?xml version="1.0" encoding="UTF-8"?>
<manifest identifier="arabroot.dns.course" version="1.0"
  xmlns="http://www.imsproject.org/xsd/imscp_rootv1p1p2"
  xmlns:adlcp="http://www.adlnet.org/xsd/adlcp_rootv1p2">
  <metadata>
    <schema>ADL SCORM</schema>
    <schemaversion>1.2</schemaversion>
  </metadata>
  <organizations default="org1">
    <organization identifier="org1">
      <title>أساسيات DNS</title>
      <item identifier="item-lesson" identifierref="res-lesson">
        <title>الدرس: كيف يعمل DNS</title>
      </item>
      <item identifier="item-quiz" identifierref="res-quiz">
        <title>الاختبار القصير</title>
        <adlcp:masteryscore>70</adlcp:masteryscore>
      </item>
      <item identifier="item-glossary" identifierref="res-glossary">
        <title>قاموس المصطلحات</title>
      </item>
    </organization>
  </organizations>
  <resources>
    <resource identifier="res-lesson" type="webcontent" adlcp:scormtype="sco" href="lesson.html">
      <file href="lesson.html"/>
      <dependency identifierref="res-common"/>
    </resource>
    <resource identifier="res-quiz" type="webcontent" adlcp:scormtype="sco" href="quiz.html">
      <file href="quiz.html"/>
      <dependency identifierref="res-common"/>
    </resource>
    <resource identifier="res-glossary" type="webcontent" adlcp:scormtype="asset" href="glossary.html">
      <file href="glossary.html"/>
      <dependency identifierref="res-common"/>
    </resource>
    <resource identifier="res-common" type="webcontent" adlcp:scormtype="asset">
      <file href="common/scorm12.js"/>
      <file href="common/style.css"/>
    </resource>
  </resources>
</manifest>

في الملف أعلاه لاحظ التالي:

  • البيانات الوصفية metadata: القيمة schemaversion هي التي يحدد منها ال LMS الإصدار، ومعها مسارات أسماء XML Namespaces، فإذا كتبت 1.2 ومسارات SCORM 2004 فقد تتعامل المنصة مع الحزمة بإصدار غير الذي كتبت له الكود.
  • الفهرس organizations: شجرة العناصر item التي يراها المتعلم في القائمة الجانبية، ويمكن أن تتداخل فيها العناصر لتكون وحدات وفصولاً، وكل عنصر يشير إلى مورد بالحقل identifierref.
  • الموارد resources: كل مورد له نوع adlcp:scormtype، فالقيمة sco تعني وحدة SCO، واختصاراً لـ Sharable Content Object، أي صفحة تستدعي ال API وتحفظ تقدم المتعلم، والقيمة asset تعني ملفاً عادياً يعرضه ال LMS ولا ينتظر منه شيئاً، مثل قاموس المصطلحات هنا.
  • الاعتماد dependency: المورد res-common لا يفتح وحده، وإنما يجمع الملفات المشتركة، والموارد الأخرى تعتمد عليه بدل تكرار قائمة الملفات.
  • درجة النجاح masteryscore: 70 للاختبار، وال LMS يعطيها للحزمة عند التشغيل كما سنرى، فيغيرها المصمم في ملف الوصف دون أن يلمس الكود.
⚠️
ضع imsmanifest.xml في جذر الملف المضغوط وليس داخل مجلد، فإذا ضغطت المجلد نفسه من مدير الملفات فسوف يكون الملف في dns-scorm12/imsmanifest.xml، والمنصة لن تجده، وتوثيق Moodle يذكر هذا السبب بالاسم عند رفض الحزمة.

دوال التشغيل Runtime API: كيف يتكلم المحتوى مع ال LMS؟

هنا الجزء الذي يحير أغلب المطورين في البداية: الحزمة لا تعرف عنوان السيرفر، ولا ترسل أي طلب HTTP بنفسها. فال LMS يفتح صفحة مشغل Player، ويضع فيها كائن JavaScript، ثم يفتح ملف الدرس داخل iframe، والدرس يصعد في النوافذ الأم حتى يجد هذا الكائن ويستدعي دواله، والمشغل هو الذي يرسل القيم إلى السيرفر. والصورة التالية تبين ذلك كما رأيناه في Moodle:

مخطط تشغيل SCORM في Moodle: في متصفح المتعلم صفحة player.php فيها window.API الذي يضعه ال LMS من ملف scorm_12.js، وiframe فيه lesson.html وهي وحدة SCO تبحث عن ال API في النوافذ الأم بالدالة findAPI(window.parent)، ثم الخطوات 1 LMSInitialize و2 LMSGetValue لقيمة cmi.core.entry وتعود ab-initio أو resume و3 LMSSetValue لقيمة cmi.core.lesson_location وcmi.suspend_data و4 LMSCommit و5 LMSFinish، وصندوق ذاكرة ال API يقول إن القيم تبقى في المتصفح حتى يأتي LMSCommit وإذا أغلق المتعلم الصفحة قبله ضاعت القيم، وعند LMSCommit يرسل المشغل القيم التي تغيرت فقط بطلب POST إلى /mod/scorm/datamodel.php، وعلى اليمين سيرفر Moodle يستقبل جسم الطلب الحقيقي cmi__core__lesson_location=1 وcmi__suspend_data، ويحفظه في قاعدة البيانات في الجدولين mdl_scorm_attempt وmdl_scorm_scoes_value
الحزمة تكلم كائناً في الصفحة، والمشغل هو الذي يرسل إلى السيرفر

والدوال ثمانية في كل إصدار، وفيما يلي دوال SCORM 1.2 وما تفعله، وأسماء SCORM 2004 بين القوسين:

الدالةما تفعله
LMSInitialize("") (Initialize)تفتح الجلسة، ولا يقبل ال LMS أي قيمة قبلها
LMSGetValue(element) (GetValue)تقرأ قيمة مثل اسم المتعلم أو مكان توقفه
LMSSetValue(element, value) (SetValue)تكتب قيمة، وتعيد "true" أو "false"
LMSCommit("") (Commit)تطلب حفظ ما كتب حتى الآن على السيرفر
LMSFinish("") (Terminate)تغلق الجلسة وتحفظ، ولا تقبل بعدها أي قيمة
LMSGetLastError() وLMSGetErrorString(code) وLMSGetDiagnostic(code)رقم آخر خطأ ونصه وتفاصيل إضافية، والرقم 0 يعني لا يوجد خطأ

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

// SCORM 1.2 runtime helper: finds the LMS API object and logs every call with its result and error code.
(function () {
  function findAPI(win) {
    for (var i = 0; i < 10 && win; i++) {
      if (win.API) return win.API;
      if (win.parent === win) break;
      win = win.parent;
    }
    return null;
  }
  var api = findAPI(window) || (window.opener && findAPI(window.opener));
  function log(line) {
    var el = document.getElementById('log');
    if (el) el.textContent += line + '\n';
    console.log('[SCORM] ' + line);
  }
  function call(name, args) {
    if (!api) { log(name + ' -> no API found'); return 'false'; }
    var result = api[name].apply(api, args);
    var err = String(api.LMSGetLastError());
    var shown = args.map(function (a) { a = String(a); return a.length > 40 ? a.slice(0, 20) + '...(' + a.length + ' chars)' : a; });
    log(name + '(' + shown.join(', ') + ') -> "' + result + '"' + (err !== '0' ? '  error ' + err + ' ' + api.LMSGetErrorString(err) : ''));
    return result;
  }
  window.SCORM = {
    found: !!api,
    init: function () { return call('LMSInitialize', ['']); },
    get: function (k) { return call('LMSGetValue', [k]); },
    set: function (k, v) { return call('LMSSetValue', [k, String(v)]); },
    commit: function () { return call('LMSCommit', ['']); },
    finish: function () { return call('LMSFinish', ['']); },
    log: log
  };
})();

لاحظ أن البحث يتوقف بعد عشر نوافذ، وأن الدالة تجرب window.opener إذا فتح الدرس في نافذة جديدة، وأن الحزمة إذا لم تجد الكائن تعمل للعرض فقط، وهذا ما يحدث عندما تفتح ملف الدرس مباشرة من المتصفح دون LMS. وفي نسخة SCORM 2004 من الحزمة الكود نفسه مع تغيير API إلى API_1484_11 وأسماء الدوال كما في الجدول.

التجربة العملية: حزمة كتبناها بأيدينا داخل Moodle 5.3

استخدمنا Moodle 5.3، وهو إصدار الدعم الطويل LTS الحالي بحسب صفحة إصدارات Moodle، والتثبيت نفسه شرحناه خطوة بخطوة في دليل Moodle، فلن نكرره هنا. وهذا ملف compose.yaml للمختبر:

name: edu6-lms
services:
  db:
    image: postgres:17.11
    container_name: edu6-lms-db
    restart: unless-stopped
    mem_limit: 512m
    environment:
      POSTGRES_DB: moodle
      POSTGRES_USER: moodle
      POSTGRES_PASSWORD: change-me-db
    volumes:
      - db:/var/lib/postgresql/data

  web:
    image: moodlehq/moodle-php-apache:8.3-bookworm@sha256:501f19fa75ad3cdc8962f2a742f39d179b8aa8ffdc2ff233d8d27a32f14e0e05
    container_name: edu6-lms-web
    restart: unless-stopped
    mem_limit: 768m
    depends_on: [db]
    environment:
      PHP_INI-max_input_vars: "5000"
      PHP_INI-display_errors: "Off"
    ports:
      - "127.0.0.1:32080:80"
    volumes:
      - code:/var/www/html
      - data:/var/www/moodledata
      - ./moodle-router.conf:/etc/apache2/conf-enabled/moodle-router.conf:ro

volumes:
  db:
    name: edu6-lms-db
  code:
    name: edu6-lms-code
  data:
    name: edu6-lms-data

ثم وضعنا كود Moodle 5.3.0 في ال Volume وثبتناه بالعربية من سطر الأوامر كما في الدليل:

docker compose up -d
curl -sL -o moodle.tgz https://github.com/moodle/moodle/archive/refs/tags/v5.3.0.tar.gz
docker run --rm -v edu6-lms-code:/var/www/html -v "$PWD":/src \
  moodlehq/moodle-php-apache:8.3-bookworm@sha256:501f19fa75ad3cdc8962f2a742f39d179b8aa8ffdc2ff233d8d27a32f14e0e05 \
  sh -c 'tar xzf /src/moodle.tgz -C /var/www/html --strip-components=1 && chown -R www-data:www-data /var/www/html'
docker restart edu6-lms-web
docker exec -u www-data edu6-lms-web php /var/www/html/admin/cli/install.php --lang=ar \
  --wwwroot=http://127.0.0.1:32080 --dataroot=/var/www/moodledata --dbtype=pgsql --dbhost=db \
  --dbname=moodle --dbuser=moodle --dbpass=change-me-db --fullname="جامعة المثال" --shortname=EXU \
  --adminuser=admin --adminpass='change-me-admin' [email protected] --agree-license --non-interactive
++ Success (37.42 seconds) ++
Installation completed successfully.

ولاحظ docker restart قبل التثبيت، فال Docker Image الخاصة بـ PHP تختار المجلد الرئيسي للموقع Document Root عند بدء ال Container، وإذا بدأ قبل وجود مجلد public في كود Moodle 5.3 بقي على /var/www/html.

الدرس: حفظ مكان التوقف وبيانات الاستئناف

الدرس lesson.html ثلاث صفحات، وعند فتحه يقرأ حالة المتعلم ومكان توقفه وبيانات الاستئناف، وعند كل صفحة يكتب رقمها في lesson_location، ويكتب الصفحات التي زارها بصيغة JSON في suspend_data، وعندما يزور الصفحات كلها يكتب الحالة completed. وهذا الجزء الذي يتكلم مع ال LMS منه:

SCORM.init();
var status = SCORM.get('cmi.core.lesson_status');
var entry = SCORM.get('cmi.core.entry');
var saved = SCORM.get('cmi.core.lesson_location');
var data = {};
try { data = JSON.parse(SCORM.get('cmi.suspend_data') || '{}'); } catch (e) { data = {}; }
data.visited = data.visited || [];
var page = saved ? parseInt(saved, 10) : 0;
if (status === 'not attempted') SCORM.set('cmi.core.lesson_status', 'incomplete');
function show() {
  document.getElementById('title').textContent = pages[page][0];
  document.getElementById('text').textContent = pages[page][1];
  document.getElementById('pos').textContent = 'الصفحة ' + (page + 1) + ' من ' + pages.length;
  if (data.visited.indexOf(page) < 0) data.visited.push(page);
  SCORM.set('cmi.core.lesson_location', page);
  SCORM.set('cmi.suspend_data', JSON.stringify(data));
  if (data.visited.length === pages.length) SCORM.set('cmi.core.lesson_status', 'completed');
  SCORM.commit();
}
document.getElementById('next').onclick = function () { if (page < pages.length - 1) { page++; show(); } };
document.getElementById('prev').onclick = function () { if (page > 0) { page--; show(); } };
window.addEventListener('beforeunload', function () { SCORM.set('cmi.core.exit', 'suspend'); SCORM.finish(); });
show();

ضغطنا الملفات في dns-scorm12.zip، وأضفنا المقرر والطالب ونشاط SCORM بسكربت PHP يستخدم مولد البيانات في Moodle، وهو ما تفعله من الواجهة بإضافة نشاط «حزمة سكورم» ورفع الملف:

<?php
// Lab data: a course, a student and three SCORM activities (our SCORM 1.2 package, the SCORM 2004 variant, and the package with a wrong href).
define('CLI_SCRIPT', true);
require('/var/www/html/config.php');
require_once($CFG->libdir . '/testing/generator/lib.php');
require_once($CFG->dirroot . '/mod/scorm/locallib.php');
\core\session\manager::set_user(get_admin());
$gen = new testing_data_generator();

$course = $DB->get_record('course', ['shortname' => 'NET101']) ?: $gen->create_course(['fullname' => 'مقدمة في الشبكات', 'shortname' => 'NET101',
    'lang' => 'ar', 'startdate' => time() - DAYSECS]);
$student = $DB->get_record('user', ['username' => 'student1']) ?: $gen->create_user(['username' => 'student1', 'password' => 'change-me-student',
    'firstname' => 'خالد', 'lastname' => 'الطالب', 'email' => '[email protected]', 'lang' => 'ar']);
$gen->enrol_user($student->id, $course->id, 'student');

$packages = [
    'dns-scorm12.zip' => 'أساسيات DNS (SCORM 1.2)',
    'dns-scorm2004.zip' => 'أساسيات DNS (SCORM 2004)',
    'dns-scorm12-badhref.zip' => 'حزمة فيها خطأ في href',
];
foreach ($packages as $file => $name) {
    copy("/tmp/$file", $CFG->dirroot . "/lab-$file");
    $scorm = $gen->get_plugin_generator('mod_scorm')->create_instance([
        'course' => $course->id, 'name' => $name,
        'packagefilepath' => $CFG->dirroot . "/lab-$file",
        'maxgrade' => 100, 'grademethod' => GRADEHIGHEST, 'maxattempt' => 0]);
    unlink($CFG->dirroot . "/lab-$file");
    $v = $DB->get_field('scorm', 'version', ['id' => $scorm->id]);
    echo "$file: scorm={$scorm->id} cmid={$scorm->cmid} version=$v\n";
}
echo "course={$course->id} student={$student->id}\n";
dns-scorm12.zip: scorm=1 cmid=1 version=SCORM_1.2
dns-scorm2004.zip: scorm=2 cmid=2 version=SCORM_1.3
dns-scorm12-badhref.zip: scorm=3 cmid=3 version=SCORM_1.2
course=2 student=3

لاحظ أن Moodle يسمي SCORM 2004 باسمه الداخلي SCORM_1.3. ثم سألنا قاعدة البيانات كيف فهم Moodle ملف الوصف:

docker exec edu6-lms-db psql -U moodle -d moodle -c "select id, scorm, identifier, title, scormtype, launch, parent from mdl_scorm_scoes where scorm = 1 order by id;"
 id | scorm |  identifier   |          title           | scormtype |    launch     | parent
----+-------+---------------+--------------------------+-----------+---------------+--------
  1 |     1 | org1          | أساسيات DNS              |           |               | /
  2 |     1 | item-lesson   | الدرس: كيف يعمل DNS      | sco       | lesson.html   | org1
  3 |     1 | item-quiz     | الاختبار القصير          | sco       | quiz.html     | org1
  4 |     1 | item-glossary | قاموس المصطلحات          | asset     | glossary.html | org1

أي أن Moodle حول كل عنصر إلى صف، وأخذ نوعه وملفه من المورد الذي يشير إليه، والمورد res-common لم يظهر لأنه لا يرتبط بعنصر.

بعد ذلك دخلنا بحساب الطالب في متصفح Chrome، وفتحنا النشاط، وضغطنا «التالي» مرة واحدة ثم خرجنا من النشاط، فكان سجل الاستدعاءات في الدرس كما يلي:

LMSInitialize() -> "true"
LMSGetValue(cmi.core.lesson_status) -> "not attempted"
LMSGetValue(cmi.core.entry) -> "ab-initio"
LMSGetValue(cmi.core.lesson_location) -> ""
LMSGetValue(cmi.suspend_data) -> ""
LMSSetValue(cmi.core.lesson_status, incomplete) -> "true"
LMSSetValue(cmi.core.lesson_location, 0) -> "true"
LMSSetValue(cmi.suspend_data, {"visited":[0]}) -> "true"
LMSCommit() -> "true"
LMSSetValue(cmi.core.lesson_location, 1) -> "true"
LMSSetValue(cmi.suspend_data, {"visited":[0,1]}) -> "true"
LMSCommit() -> "true"

وفي الوقت نفسه سجلنا ما يرسله مشغل Moodle إلى السيرفر عند LMSCommit الثاني، وهذا جسم الطلب الحقيقي إلى /mod/scorm/datamodel.php بعد حذف مفتاح الجلسة:

id=1&a=1&sesskey=...&attempt=1&scoid=2&cmi__core__lesson_location=1&cmi__suspend_data={"visited":[0,1]}

في المخرجين أعلاه لاحظ التالي:

  • القيمة ab-initio في cmi.core.entry تعني أن هذه أول مرة يفتح فيها المتعلم الدرس، وسوف تتغير بعد قليل.
  • المشغل لا يرسل كل القيم في كل مرة، وإنما ما تغير منذ آخر حفظ، فلم يرسل lesson_status لأنها لم تتغير.
  • النقطة في أسماء العناصر تصبح شرطتين سفليتين في الطلب، أي cmi__core__lesson_location، وهذه طريقة Moodle الداخلية وليست من المعيار.

وعند الخروج استدعى الدرس LMSFinish من حدث beforeunload، وهذا ما حفظه Moodle للمحاولة:

docker exec edu6-lms-db psql -U moodle -d moodle -c "select a.attempt, v.scoid, e.element, v.value from mdl_scorm_attempt a join mdl_scorm_scoes_value v on v.attemptid=a.id join mdl_scorm_element e on e.id=v.elementid where a.userid=3 order by v.scoid, e.element;"
 attempt | scoid |         element          |       value
---------+-------+--------------------------+-------------------
       1 |     2 | cmi.core.exit            | suspend
       1 |     2 | cmi.core.lesson_location | 1
       1 |     2 | cmi.core.lesson_status   | incomplete
       1 |     2 | cmi.core.total_time      | 00:00:00.00
       1 |     2 | cmi.suspend_data         | {"visited":[0,1]}
       1 |     2 | x.start.time             | 1792631807
(6 rows)

العودة إلى الدرس: الاستئناف Resume

فتحنا النشاط من جديد بحساب الطالب نفسه، فكانت بداية السجل مختلفة:

LMSInitialize() -> "true"
LMSGetValue(cmi.core.lesson_status) -> "incomplete"
LMSGetValue(cmi.core.entry) -> "resume"
LMSGetValue(cmi.core.lesson_location) -> "1"
LMSGetValue(cmi.suspend_data) -> "{"visited":[0,1]}"
LMSSetValue(cmi.core.lesson_location, 1) -> "true"
LMSSetValue(cmi.suspend_data, {"visited":[0,1]}) -> "true"
LMSCommit() -> "true"

أي أن Moodle أعاد للحزمة resume لأن آخر خروج كان بالقيمة suspend، وأعاد مكان التوقف وبيانات الاستئناف، فبدأ الدرس من الصفحة الثانية مباشرة:

مشغل SCORM في Moodle بالعربية: عنوان النشاط أساسيات DNS (SCORM 1.2) وزر الخروج من النشاط، وفي القائمة الجانبية للحزمة أساسيات DNS وتحتها الدرس كيف يعمل DNS بعلامة إيقاف مؤقت والاختبار القصير وقاموس المصطلحات بلا علامة، وفي الوسط صفحة الدرس الثانية بعنوان من يجيب عن السؤال؟ وزرا السابق والتالي والصفحة 2 من 3، وتحتها سجل الاستدعاءات LMSInitialize وLMSGetValue لقيمة cmi.core.entry بقيمة resume وcmi.core.lesson_location بقيمة 1 وcmi.suspend_data بقيمة visited 0 و1
الدرس يعود إلى الصفحة 2 من 3 لأن ال LMS أعاد له مكان التوقف، ولاحظ أن قاموس المصطلحات بلا مربع لأنه Asset

لاحظ في القائمة الجانبية أن الدرس والاختبار بجانبهما مربع للحالة، وأن قاموس المصطلحات بلا مربع، والسبب أنه Asset لا يرسل حالة. ثم ضغطنا «التالي» فزار الطالب الصفحات الثلاث، وكتب الدرس completed.

الاختبار: الدرجة والتفاعلات Interactions

الاختبار quiz.html يقرأ درجة النجاح من ال LMS، ويرسل لكل سؤال عناصر التفاعل cmi.interactions، ثم الدرجة والحالة ومدة الجلسة. أجاب الطالب عن سؤالين صحيحاً واختار 443 منفذاً لخدمة DNS، فكان السجل كما يلي:

LMSInitialize() -> "true"
LMSGetValue(cmi.student_data.mastery_score) -> "70"
LMSSetValue(cmi.interactions.0.id, q1) -> "true"
LMSSetValue(cmi.interactions.0.type, choice) -> "true"
LMSSetValue(cmi.interactions.0.student_response, a) -> "true"
LMSSetValue(cmi.interactions.0.result, correct) -> "true"
LMSSetValue(cmi.interactions.1.id, q2) -> "true"
LMSSetValue(cmi.interactions.1.type, choice) -> "true"
LMSSetValue(cmi.interactions.1.student_response, b) -> "true"
LMSSetValue(cmi.interactions.1.result, correct) -> "true"
LMSSetValue(cmi.interactions.2.id, q3) -> "true"
LMSSetValue(cmi.interactions.2.type, choice) -> "true"
LMSSetValue(cmi.interactions.2.student_response, b) -> "true"
LMSSetValue(cmi.interactions.2.result, wrong) -> "true"
LMSSetValue(cmi.core.score.raw, 67) -> "true"
LMSSetValue(cmi.core.score.min, 0) -> "true"
LMSSetValue(cmi.core.score.max, 100) -> "true"
LMSSetValue(cmi.core.lesson_status, failed) -> "true"
LMSSetValue(cmi.core.session_time, 0000:00:04) -> "true"
LMSCommit() -> "true"
LMSFinish() -> "true"

لاحظ أن القيمة 70 التي كتبناها في masteryscore وصلت إلى الحزمة من ال LMS، فالحزمة لا تحتاج أن تثبت درجة النجاح في كودها، والمصمم يغيرها في ملف الوصف. والصورة التالية للاختبار بعد التسليم:

الاختبار القصير داخل مشغل Moodle بعد التسليم: ثلاثة أسئلة عن DNS، والطالب اختار تحويل اسم النطاق إلى عنوان IP، والسجل A، والمنفذ 443، والنتيجة 67% (2 من 3)، وفي القائمة الجانبية الدرس بعلامة صح والاختبار القصير بعلامة X
النتيجة 67%، والقائمة تعرض الدرس مكتملاً والاختبار بعلامة رسوب

والمعلم يرى المحاولة في تقرير النشاط، والدرجة 67 لأن طريقة التقييم أعلى محاولة:

تقرير نشاط SCORM الأساسي في Moodle بالعربية: نتيجة واحدة لمستخدم واحد، خالد الطالب ببريد student1@example.com، حاول 1، بدأ في الخميس 22 أكتوبر 2026، والدرجة 67
التقرير الأساسي: محاولة واحدة بدرجة 67

ومن رقم المحاولة يفتح تفاصيل المسار للاختبار، وفيها كل عنصر أرسلته الحزمة:

صفحة تفاصيل المسار في Moodle للمحاولة 1 للطالب خالد في الاختبار القصير: جدول العنصر والقيمة، cmi.core.lesson_status بقيمة failed، وcmi.core.score.max بقيمة 100، وcmi.core.score.min بقيمة 0، وcmi.core.score.raw بقيمة 67، وcmi.core.total_time بقيمة 4 ثواني، ثم cmi.interactions_0.id بقيمة q1 والنتيجة correct والإجابة a والنوع choice، وcmi.interactions_1 بقيمة q2 وcorrect وb، وcmi.interactions_2 بقيمة q3 والنتيجة wrong والإجابة b
تفاصيل المحاولة: الحالة والدرجة، والتفاعلات التي تبين أن السؤال الثالث هو الخاطئ

وهنا فائدة التفاعلات: لولاها لعرف المعلم أن الدرجة 67 فقط، ومعها يعرف أي سؤال أخطأ فيه الطالب وبأي إجابة، وكثير من الحزم التجارية لا ترسلها إلا إذا فعلت الخيار في أداة التأليف، فاسأل عنه عند الطلب.

النسخة نفسها بصيغة SCORM 2004

كتبنا الاختبار نفسه بصيغة SCORM 2004، والفرق في ملف الوصف مسارات الأسماء، والقيمة 2004 4th Edition، وقاعدة النجاح بالهدف الأساسي Primary Objective بدل masteryscore، وقاعدة تنقل للمقرر:

<manifest identifier="arabroot.dns.course.2004" version="1.0"
  xmlns="http://www.imsglobal.org/xsd/imscp_v1p1"
  xmlns:adlcp="http://www.adlnet.org/xsd/adlcp_v1p3"
  xmlns:adlseq="http://www.adlnet.org/xsd/adlseq_v1p3"
  xmlns:adlnav="http://www.adlnet.org/xsd/adlnav_v1p3"
  xmlns:imsss="http://www.imsglobal.org/xsd/imsss">
  <metadata>
    <schema>ADL SCORM</schema>
    <schemaversion>2004 4th Edition</schemaversion>
  </metadata>
  <organizations default="org1">
    <organization identifier="org1">
      <title>أساسيات DNS (SCORM 2004)</title>
      <item identifier="item-quiz" identifierref="res-quiz">
        <title>الاختبار القصير</title>
        <imsss:sequencing>
          <imsss:objectives>
            <imsss:primaryObjective objectiveID="dns-basics" satisfiedByMeasure="true">
              <imsss:minNormalizedMeasure>0.7</imsss:minNormalizedMeasure>
            </imsss:primaryObjective>
          </imsss:objectives>
        </imsss:sequencing>
      </item>
      <imsss:sequencing>
        <imsss:controlMode choice="true" flow="true"/>
      </imsss:sequencing>
    </organization>
  </organizations>
  <resources>
    <resource identifier="res-quiz" type="webcontent" adlcp:scormType="sco" href="quiz.html">
      <file href="quiz.html"/>
      <file href="scorm2004.js"/>
    </resource>
  </resources>
</manifest>

لاحظ أن الخاصية أصبحت adlcp:scormType بحرف T كبير في SCORM 2004، بينما هي adlcp:scormtype في SCORM 1.2، وهذا من الفروق الصغيرة التي تنتبه لها عندما تنسخ ملف وصف من إصدار إلى آخر. وأجاب الطالب عن الأسئلة كلها صحيحاً، فكان السجل:

Initialize() -> "true"
GetValue(cmi.completion_status) -> "unknown"
GetValue(cmi.scaled_passing_score) -> "0.7"
SetValue(cmi.completion_status, incomplete) -> "true"
SetValue(cmi.score.raw, 100) -> "true"
SetValue(cmi.score.min, 0) -> "true"
SetValue(cmi.score.max, 100) -> "true"
SetValue(cmi.score.scaled, 1) -> "true"
SetValue(cmi.completion_status, completed) -> "true"
SetValue(cmi.success_status, passed) -> "true"
SetValue(cmi.session_time, PT2S) -> "true"
SetValue(cmi.exit, normal) -> "true"
Commit() -> "true"
Terminate() -> "true"

في المخرج أعلاه لاحظ التالي:

  • الحالة الأولى unknown وليست not attempted كما في SCORM 1.2، فالحقل الجديد له مفرداته.
  • Moodle حول minNormalizedMeasure من ملف الوصف إلى cmi.scaled_passing_score بقيمة 0.7، أي أن هذا الجزء من SCORM 2004 يعمل في Moodle رغم أن التوثيق يقول إن الإصدار غير مدعوم.
  • مدة الجلسة بصيغة ISO 8601، أي PT2S لثانيتين، وليست 0000:00:02 كما في SCORM 1.2.

إذن الحزمة البسيطة بوحدة SCO واحدة عملت، ولكن هذا لا يعني أن قواعد التسلسل في حزمة تجارية من عشرين درساً سوف تعمل، والتوثيق واضح في أن الأخطاء التي تبلغ عنها في SCORM 2004 تغلق دون إصلاح، فاختبر أي حزمة SCORM 2004 على Moodle قبل أن تعتمدها، أو اطلب نسختها بصيغة SCORM 1.2.

ثلاثة أخطاء حقيقية وما يظهر عند كل منها

بيانات استئناف أطول من 4096 حرفاً

أدوات التأليف تحفظ في suspend_data حالة كل شريحة وكل سؤال، وفي الدورات الطويلة يكبر هذا النص بسرعة. فماذا يحدث إذا كتبت حزمة SCORM 1.2 فيه 5000 حرف، أي أكثر من حد المعيار؟ جربنا ذلك في Moodle بإعداداته الافتراضية:

LMSSetValue(cmi.suspend_data, xxxxxxxxxxxxxxxxxxxx...(5000 chars)) -> "true"
LMSCommit() -> "true"

قبلها Moodle وحفظ 5000 حرف كاملة في قاعدة البيانات، والسبب إعداد اسمه «وضع معايير SCORM» SCORM standards mode، وهو غير مفعل افتراضياً، ووصفه في Moodle أنه عندما يكون غير مفعل يسمح لحزم SCORM 1.2 بحفظ أكثر مما يسمح به المعيار. وفي كود Moodle يرفع هذا الإعداد الحد إلى 64000 حرف. ثم فعلنا الإعداد وكررنا التجربة:

docker exec -u www-data edu6-lms-web php /var/www/html/admin/cli/cfg.php --component=scorm --name=scormstandard --set=1
LMSSetValue(cmi.suspend_data, xxxxxxxxxxxxxxxxxxxx...(5000 chars)) -> "false"  error 405 Incorrect data type
LMSCommit() -> "true"

رفض ال API القيمة بالخطأ 405، وLMSCommit بعدها نجح ولكنه لم يحمل القيمة الجديدة، والحزمة التي لا تتحقق من نتيجة LMSSetValue سوف تكمل وكأن شيئاً لم يحدث، ثم يعود المتعلم غداً فيجد الدورة من البداية. والدرس هنا أن الحزمة التي تعمل في Moodle بالإعداد الافتراضي قد لا تعمل في منصة تطبق المعيار بدقة، فإذا كانت حزمتك SCORM 1.2 فأبق suspend_data تحت 4096 حرفاً، وإذا كان المزود يحتاج أكثر فهذا سبب حقيقي لطلب SCORM 2004 بالإصدار الثالث أو الرابع.

قيم بلا Commit

فتحنا محاولة جديدة في نسخة SCORM 2004، وكتبنا درجة 90 وحالة نجاح، ثم أغلقنا الصفحة دون Commit ودون Terminate، وهذا ما يحدث عندما يغلق المتعلم التبويب في حزمة لا تحفظ عند كل خطوة:

Initialize() -> "true"
GetValue(cmi.completion_status) -> "unknown"
GetValue(cmi.scaled_passing_score) -> "0.7"
SetValue(cmi.completion_status, incomplete) -> "true"
SetValue(cmi.score.raw, 90) -> "true"
SetValue(cmi.success_status, passed) -> "true"
 attempt | scoid |        element        |   value
---------+-------+-----------------------+------------
       1 |     6 | cmi.completion_status | completed
       1 |     6 | cmi.exit              | normal
       1 |     6 | cmi.objectives.0.id   | dns-basics
       1 |     6 | cmi.score.max         | 100
       1 |     6 | cmi.score.min         | 0
       1 |     6 | cmi.score.raw         | 100
       1 |     6 | cmi.score.scaled      | 1
       1 |     6 | cmi.session_time      | PT2S
       1 |     6 | cmi.success_status    | passed
       1 |     6 | cmi.total_time        | PT2S
       1 |     6 | x.start.time          | 1792632372
       2 |     6 | x.start.time          | 1792632536
(12 rows)

المحاولة الأولى محفوظة كاملة، وفيها أيضاً cmi.objectives.0.id الذي أنشأه Moodle من الهدف الأساسي في ملف الوصف، وأما المحاولة الثانية فليس فيها إلا وقت البدء، والدرجة 90 لم تصل إلى السيرفر أصلاً، لأنها بقيت في ذاكرة ال API في المتصفح. والقاعدة: الحزمة تستدعي Commit بعد كل خطوة مهمة، وتستدعي LMSFinish أو Terminate عند الخروج كما فعل درسنا في beforeunload، وسؤال وجواب SCORM في Moodle ينصح باستدعاء LMSCommit بعد كل LMSSetValue. ولاحظ أن في إعدادات نشاط SCORM في Moodle خياراً للحفظ التلقائي Auto-commit، وكود مشغل SCORM 1.2 فيه يستدعي عندها LMSCommit بعد 60 ثانية من آخر قيمة، ولم نعتمد عليه في التجربة، فلا تجعل حزمتك تعتمد عليه.

اسم ملف بحرف كبير في href

نسخنا الحزمة نفسها وغيرنا في ملف الوصف حرفاً واحداً، فأصبح الرابط href="Lesson.html" والملف الحقيقي اسمه lesson.html. هذا الخطأ لا يظهر على جهاز المصمم إذا كان Windows، لأن نظام الملفات فيه لا يفرق بين الحروف الكبيرة والصغيرة، ولكن سيرفر Linux يفرق. رفع Moodle الحزمة دون أي رسالة، وعند فتح النشاط ظهر الإطار فارغاً:

مشغل SCORM في Moodle لنشاط حزمة فيها خطأ في href: القائمة الجانبية تعرض أساسيات DNS والدرس والاختبار القصير وقاموس المصطلحات، ومنطقة المحتوى فارغة تماماً دون أي رسالة خطأ
القائمة تظهر والمحتوى فارغ، ولا رسالة للطالب ولا للمعلم

والسبب لا يظهر إلا في سجل خادم الويب:

docker logs edu6-lms-web 2>&1 | grep "Lesson.html"
192.168.128.1 - - [22/Oct/2026:01:27:02 +0000] "GET /pluginfile.php/16/mod_scorm/content/1/Lesson.html HTTP/1.1" 404 309 "http://127.0.0.1:32080/mod/scorm/loadSCO.php?a=3&scoid=8&currentorg=org1&mode=&attempt=1" "Mozilla/5.0 ..."

أي أن Moodle لا يتحقق عند الرفع من أن كل href في ملف الوصف موجود في الحزمة، والطالب يرى صفحة فارغة ويظن أن المنصة متوقفة. لذلك افتح كل درس في الحزمة مرة واحدة على المنصة نفسها قبل أن تعلن المقرر، ولا تكتف بأن الرفع نجح.

مشكلات شائعة وحلولها

العرضالسببالحل
المنصة ترفض الحزمة عند الرفعimsmanifest.xml داخل مجلد في الملف المضغوطاضغط محتوى المجلد وليس المجلد نفسه
المحتوى يعمل ولا تصل أي درجةالحزمة لم تجد ال API، أو فتحت الملف خارج ال LMSتحقق من دالة البحث، ومن أن الإصدار في ملف الوصف يطابق الكود (API أو API_1484_11)
المتعلم يعود فيجد الدورة من البدايةلم تحفظ suspend_data أو lesson_location، أو تجاوزت 4096 حرفاً في منصة صارمةاقرأ نتيجة كل LMSSetValue، وأبق البيانات تحت الحد، واستدع LMSCommit
درجة المحاولة الأخيرة لم تحفظأغلق المتعلم الصفحة قبل LMSCommitCommit بعد كل خطوة، وFinish عند الخروج
إطار المحتوى فارغرابط href لا يطابق اسم الملف، والحروف الكبيرة والصغيرة تفرق على Linuxابحث عن 404 في سجل خادم الويب، وصحح الاسم في ملف الوصف
حزمة SCORM 2004 تعمل جزئياً في MoodleMoodle لا يدعم SCORM 2004 رسمياًاطلب نسخة SCORM 1.2 من المزود، أو اختبرها كاملة قبل الاعتماد

تنظيف المختبر

docker compose down -v

تدريب عملي

  • غير masteryscore في ملف الوصف إلى 60، وارفع الحزمة من جديد، وتأكد أن الاختبار يقرأ القيمة الجديدة وأن 67 أصبحت passed دون تعديل سطر واحد من الكود.
  • احذف السطر الذي يكتب cmi.core.exit بالقيمة suspend من الدرس، ثم اخرج وعد، وقارن قيمة cmi.core.entry بما رأيته في المقال.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • SCORM يحل مشكلة واحدة بوضوح: حزمة Zip واحدة يقرؤها أي LMS، ودوال JavaScript ثابتة يكلم بها المحتوى المنصة.
  • SCORM 1.2 هو الأوسع دعماً وحقل الحالة فيه واحد، وSCORM 2004 يفصل الإكمال عن النجاح ويضيف التسلسل، وMoodle يدعم الأول فقط رسمياً.
  • ملف imsmanifest.xml فيه الفهرس والموارد، والوحدة SCO تكلم ال API، والملف Asset يعرض فقط.
  • القيم تبقى في المتصفح حتى LMSCommit، ومشغل Moodle يرسل ما تغير فقط إلى datamodel.php.
  • Moodle افتراضياً يقبل suspend_data أطول من حد SCORM 1.2، والمنصة الصارمة ترفضه بالخطأ 405، والرابط الخاطئ في href يظهر إطاراً فارغاً دون رسالة.

وفي المقال التالي بإذن الله سوف نتحدث عن أدوات SCORM: أدوات التأليف التجارية والمفتوحة بأسعارها، وكيف تختبر الحزمة قبل رفعها، وكيف تشغلها في منصتك أنت.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال وتجربته على Moodle 5.3.0 (Build 20261005) بحزمتي SCORM 1.2 وSCORM 2004 الإصدار الرابع كتبناهما للمقال.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.