لنفرض أن فريق التصميم التعليمي في جامعتك بنى وحدة تدريبية عن أساسيات الشبكات، فيها درس من ثلاث صفحات واختبار قصير، ورفعها إلى Moodle فعملت وظهرت الدرجة في سجل الدرجات، ثم طلبت كلية أخرى الوحدة نفسها لمنصة Open edX، وطلبتها جهة ثالثة لمنصتها الداخلية. فهل يعيد الفريق بناءها ثلاث مرات؟ الإجابة لا، والسبب أن الوحدة صدرت بصيغة SCORM، وهي الصيغة التي تجعل الحزمة نفسها تعمل في أي منصة تعلم LMS تدعمها، وتجعل المنصة تعرف أين وصل المتعلم وما درجته. وفي مقال أنظمة إدارة التعلم LMS ذكرنا أن المنصة يجب أن تستقبل المحتوى من الخارج، وهذا المقال يشرح الطريقة الأقدم والأكثر انتشاراً لذلك من الداخل.
المواضيع في السلسلة:
- ما هو SCORM؟ الإصدارات وكيف يتواصل المحتوى مع LMS (الموضوع الحالي).
- أدوات SCORM من التأليف Authoring إلى التشغيل في منصتك.
- تتبع تقدم المتعلم Learner Tracking من SCORM إلى xAPI.
في هذا المقال سوف نتناول:
- المشكلة التي يحلها 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، والصورة التالية تبين الفرق:

والمعيار يجيب عن سؤالين فقط، وهذا سر انتشاره:
- شكل الحزمة 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
الفكرة واحدة في الإصدارين، ولكن أسماء الدوال والحقول مختلفة، لذلك تكتب الحزمة لإصدار واحد ولا يقرأ أحدهما بيانات الآخر. والصورة التالية تبين الفرق الأهم، وهو حالة المتعلم:

والجدول التالي يجمع الفروق التي سوف تقابلها في العمل، والأسماء من مرجع نموذج البيانات Run-Time Reference، والقيم في العمود الأخير مما رأيناه في المختبر:
| البند | SCORM 1.2 | SCORM 2004 |
|---|---|---|
| اسم كائن ال API في الصفحة | API | API_1484_11 |
| الدوال | LMSInitialize، LMSGetValue، LMSSetValue، LMSCommit، LMSFinish | Initialize، GetValue، SetValue، Commit، Terminate |
| حالة المتعلم | cmi.core.lesson_status حقل واحد | cmi.completion_status وcmi.success_status |
| الدرجة Score | cmi.core.score.raw مع min وmax | كذلك، ومعها cmi.score.scaled من -1 إلى 1 |
| درجة النجاح | adlcp:masteryscore في ملف الوصف، وتقرؤها الحزمة من cmi.student_data.mastery_score | minNormalizedMeasure في ملف الوصف، وتقرؤها الحزمة من cmi.scaled_passing_score (قرأنا 0.7) |
| مكان التوقف | cmi.core.lesson_location | cmi.location |
بيانات الاستئناف suspend_data | حتى 4096 حرفاً | 4000 في الإصدار الثاني، و64000 في الثالث والرابع |
صيغة مدة الجلسة session_time | HHHH: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 عادي، والصورة التالية تبين ما فيه من الحزمة التي سوف نبنيها في المختبر:

وهذا ملف الوصف الكامل للحزمة:
<?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 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، وأعاد مكان التوقف وبيانات الاستئناف، فبدأ الدرس من الصفحة الثانية مباشرة:

لاحظ في القائمة الجانبية أن الدرس والاختبار بجانبهما مربع للحالة، وأن قاموس المصطلحات بلا مربع، والسبب أنه 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، فالحزمة لا تحتاج أن تثبت درجة النجاح في كودها، والمصمم يغيرها في ملف الوصف. والصورة التالية للاختبار بعد التسليم:

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

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

وهنا فائدة التفاعلات: لولاها لعرف المعلم أن الدرجة 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=1LMSSetValue(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 الحزمة دون أي رسالة، وعند فتح النشاط ظهر الإطار فارغاً:

والسبب لا يظهر إلا في سجل خادم الويب:
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¤torg=org1&mode=&attempt=1" "Mozilla/5.0 ..."أي أن Moodle لا يتحقق عند الرفع من أن كل href في ملف الوصف موجود في الحزمة، والطالب يرى صفحة فارغة ويظن أن المنصة متوقفة. لذلك افتح كل درس في الحزمة مرة واحدة على المنصة نفسها قبل أن تعلن المقرر، ولا تكتف بأن الرفع نجح.
مشكلات شائعة وحلولها
| العرض | السبب | الحل |
|---|---|---|
| المنصة ترفض الحزمة عند الرفع | imsmanifest.xml داخل مجلد في الملف المضغوط | اضغط محتوى المجلد وليس المجلد نفسه |
| المحتوى يعمل ولا تصل أي درجة | الحزمة لم تجد ال API، أو فتحت الملف خارج ال LMS | تحقق من دالة البحث، ومن أن الإصدار في ملف الوصف يطابق الكود (API أو API_1484_11) |
| المتعلم يعود فيجد الدورة من البداية | لم تحفظ suspend_data أو lesson_location، أو تجاوزت 4096 حرفاً في منصة صارمة | اقرأ نتيجة كل LMSSetValue، وأبق البيانات تحت الحد، واستدع LMSCommit |
| درجة المحاولة الأخيرة لم تحفظ | أغلق المتعلم الصفحة قبل LMSCommit | Commit بعد كل خطوة، وFinish عند الخروج |
| إطار المحتوى فارغ | رابط href لا يطابق اسم الملف، والحروف الكبيرة والصغيرة تفرق على Linux | ابحث عن 404 في سجل خادم الويب، وصحح الاسم في ملف الوصف |
| حزمة SCORM 2004 تعمل جزئياً في Moodle | Moodle لا يدعم 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 الإصدار الرابع كتبناهما للمقال.