Çok Kiracılı İçerik Yönetimi: Sürüm Oluşturma ve SCORM
Büyük bir eğitim şirketi, her birinin kendine ait 500 kurumsal müşteriye hizmet vermektedir. çalışanları, görsel kimliği ve uyumluluk gereklilikleri. Bir e-Öğrenim içerik sağlayıcısının bir politika formunu güncellemesi gerekiyor Güvenlik konusunda (her yıl değişir) ve değişikliği 500 müşterinin tamamına yaymak anında, hiçbirinin hiçbir şey yapmasına gerek kalmadan. Ne zaman bir müşteri Formu kendi logonuz ve örneklerinizle özelleştirmek istiyorsanız, bunu yapabilmeniz gerekir. otomatik güncellemeleri bozmadan yapın.
Bu sorun Çok Kiracılı İçerik Yönetimi Eğitim Teknolojisi için: Paylaşılan ve kişiselleştirilmiş içeriği yönetmek, doğru şekilde sürümlendirmek, dağıtmak verimli bir şekilde ve uyumun sağlanması TASLAK (Paylaşılabilir İçerik Nesne Referans Modeli), kurumsal e-Öğrenim paketleri için en yaygın standarttır.
Bu makalede veri yapısından içeriğe kadar eksiksiz bir sistem oluşturacağız kiracı başına geçersiz kılma ile çok kiracılı, paketlerin semantik versiyonlanması, üçüncü taraf LMS ile iletişim için SCORM uyumlu uç noktaya, Verimli küresel dağıtım için CDN stratejisine kadar.
Bu Makalede Neler Öğreneceksiniz?
- Kiracı başına özelleştirme ile paylaşılan içerik için çok kiracılı veri modeli
- E-Öğrenim içerik paketleri için anlamsal sürüm oluşturma (semver)
- SCORM 1.2 ve SCORM 2004: farklar, yapı ve iletişim API'si
- Çok kiracılı SCORM paketleri için merkezi ve dağıtılmış barındırma karşılaştırması
- İçerik paketleme: ZIP, imsmanifest.xml bildirimi ve dosya yapısı
- Gelişmiş izleme için SCORM API JavaScript sarmalayıcısı
- Düşük gecikme süreli küresel dağıtım için CDN stratejisi
- SCORM'dan xAPI'ye geçiş: ne zaman ve neden
1. İçerik için Çok Kiracılı Veri Modeli
Çok kiracılı içerik yönetiminin zorluğu iki karşıt ihtiyacı dengelemektir: paylaşım (güncellenen içerik tüm kiracılara yayılır) e özelleştirme (her kiracı belirli öğeleri geçersiz kılabilir). Bir model kullanıyoruz üç seviye: küresel içerik (ana), kategori geçersiz kılmaları (benzer özelliklere sahip kiracı grupları) ve geçersiz kılmalar kiracıya özel.
# models/content.py
from dataclasses import dataclass, field
from typing import Optional, Dict, List, Any
from datetime import datetime
from enum import Enum
class ContentStatus(Enum):
DRAFT = "draft"
REVIEW = "review"
PUBLISHED = "published"
DEPRECATED = "deprecated"
ARCHIVED = "archived"
class SCORMVersion(Enum):
SCORM_12 = "1.2"
SCORM_2004_3 = "2004_3rd"
SCORM_2004_4 = "2004_4th"
XAPI = "xapi"
@dataclass
class ContentVersion:
"""Una versione specifica di un pacchetto di contenuto."""
version_id: str
content_id: str
semver: str # "1.2.3" (MAJOR.MINOR.PATCH)
status: ContentStatus
scorm_version: SCORMVersion
title: Dict[str, str] # {"it-IT": "...", "en-US": "..."} - multilingue
description: Dict[str, str]
# Path nel bucket S3/Cloud Storage al pacchetto ZIP
package_path: str
# Hash SHA256 del pacchetto per integrity check
package_hash: str
package_size_bytes: int
# URL del manifest per accesso diretto
manifest_url: str
created_at: datetime
created_by: str
changelog: str # Cosa e cambiato rispetto alla versione precedente
# Metadati curriculum
estimated_duration_minutes: int
topics: List[str]
prerequisites: List[str] # IDs di contenuti prerequisito
difficulty: int # 1-5
@dataclass
class ContentMaster:
"""Il contenuto 'master' condiviso tra tutti i tenant."""
content_id: str
category_id: str
global_id: str # Identificatore universale (es: "compliance-gdpr-intro")
current_version: str # Semver della versione corrente pubblicata
versions: List[ContentVersion] = field(default_factory=list)
auto_update_policy: str = "minor" # "none", "patch", "minor", "major"
tags: List[str] = field(default_factory=list)
@dataclass
class TenantContentOverride:
"""Override specifico per tenant di un contenuto master."""
override_id: str
tenant_id: str
content_id: str # Riferimento al ContentMaster
version_locked: Optional[str] = None # Se impostato, questo tenant usa sempre questa versione
# Personalizzazioni UI
custom_logo_url: Optional[str] = None
custom_colors: Optional[Dict[str, str]] = None # {"primary": "#..."}
custom_css_url: Optional[str] = None
# Override testi (es. esempi locali al posto di quelli globali)
text_overrides: Dict[str, str] = field(default_factory=dict)
# Slides o moduli da nascondere per questo tenant
hidden_modules: List[str] = field(default_factory=list)
# Moduli aggiuntivi solo per questo tenant
additional_modules: List[str] = field(default_factory=list)
enabled: bool = True
created_at: datetime = field(default_factory=datetime.utcnow)
updated_at: datetime = field(default_factory=datetime.utcnow)
@dataclass
class TenantContentAssignment:
"""Assegnazione di un contenuto a un tenant, con configurazione."""
assignment_id: str
tenant_id: str
content_id: str
effective_version: str # Versione effettivamente usata (dopo risoluzione override)
required: bool = False # Formazione obbligatoria?
deadline: Optional[datetime] = None
completion_certificate: bool = False
passing_score_percent: int = 70
max_attempts: int = 3
2. Anlamsal Sürüm Oluşturma ve Yayılım
hadi kullanalım Anlamsal Sürüm Oluşturma (birkaç) içerik için: MAJOR.MINOR.PATCH. YAMA = düzeltmeler (yazım hatası, hata): Tüm kiracılara otomatik olarak yayılır kilitli sürümü olmayanlar. KÜÇÜK = yeni isteğe bağlı bölümler, iyileştirmeler: kiracı yöneticilerine bildirimde bulunularak yayılır. ANA = Müfredatta temel değişiklikler: kiracı başına açık onay gerektirir.
# services/content_version_manager.py
from dataclasses import dataclass
from typing import List, Optional, Tuple
import semver
from datetime import datetime
@dataclass
class VersionBump:
content_id: str
from_version: str
to_version: str
bump_type: str # "major", "minor", "patch"
changelog: str
affected_tenants: List[str] # Tenant che riceveranno l'aggiornamento automatico
notify_tenants: List[str] # Tenant da notificare ma non aggiornare automaticamente
require_approval: List[str] # Tenant che richiedono approvazione esplicita
class ContentVersionManager:
def __init__(self, db, notification_service, event_bus):
self.db = db
self.notifications = notification_service
self.events = event_bus
async def publish_new_version(
self,
content_id: str,
new_version: str,
changelog: str,
package_path: str,
) -> VersionBump:
"""Pubblica una nuova versione e gestisce la propagazione ai tenant."""
content = await self._get_content(content_id)
current = content.current_version
bump_type = self._classify_bump(current, new_version)
# Recupera tutti i tenant che hanno questo contenuto
assignments = await self._get_tenant_assignments(content_id)
overrides = await self._get_tenant_overrides(content_id)
# Classifica i tenant per politica di aggiornamento
auto_update = []
notify_only = []
require_approval = []
for assignment in assignments:
override = overrides.get(assignment.tenant_id)
# Se la versione e bloccata: non aggiornare
if override and override.version_locked:
continue
# Applica la politica del contenuto master
if bump_type == "patch":
auto_update.append(assignment.tenant_id)
elif bump_type == "minor":
if content.auto_update_policy in ("minor", "major"):
auto_update.append(assignment.tenant_id)
else:
notify_only.append(assignment.tenant_id)
else: # major
if content.auto_update_policy == "major":
auto_update.append(assignment.tenant_id)
else:
require_approval.append(assignment.tenant_id)
# Esegui aggiornamenti automatici
for tenant_id in auto_update:
await self._apply_version_to_tenant(tenant_id, content_id, new_version)
# Notifiche
if notify_only:
await self.notifications.send_bulk(
tenant_ids=notify_only,
subject=f"Disponibile nuova versione: {content.global_id} v{new_version}",
body=f"Una nuova versione minore e disponibile.\nCambiamenti: {changelog}\nApprovazione manuale richiesta.",
action_url=f"/admin/content/{content_id}/versions/{new_version}",
)
if require_approval:
await self._create_approval_requests(require_approval, content_id, new_version, changelog)
# Aggiorna versione corrente del master
await self.db.execute(
"UPDATE content_masters SET current_version = :v WHERE content_id = :cid",
{"v": new_version, "cid": content_id},
)
await self.db.commit()
# Pubblica evento
await self.events.publish({
"type": "content.version.published",
"content_id": content_id,
"version": new_version,
"bump_type": bump_type,
"auto_updated_tenants": len(auto_update),
})
return VersionBump(
content_id=content_id,
from_version=current,
to_version=new_version,
bump_type=bump_type,
changelog=changelog,
affected_tenants=auto_update,
notify_tenants=notify_only,
require_approval=require_approval,
)
def _classify_bump(self, current: str, new_version: str) -> str:
try:
v_current = semver.VersionInfo.parse(current)
v_new = semver.VersionInfo.parse(new_version)
if v_new.major > v_current.major:
return "major"
if v_new.minor > v_current.minor:
return "minor"
return "patch"
except ValueError:
return "major" # Default conservativo se parsing fallisce
async def _apply_version_to_tenant(self, tenant_id: str, content_id: str, version: str) -> None:
await self.db.execute(
"""UPDATE tenant_content_assignments
SET effective_version = :v, updated_at = NOW()
WHERE tenant_id = :tid AND content_id = :cid""",
{"v": version, "tid": tenant_id, "cid": content_id},
)
await self.db.commit()
3. SCORM ve Manifest yapısı imsmanifest.xml
SCORM, kesin bir yapıya sahip bir ZIP paketi formatını tanımlar.
Dosya imsmanifest.xml ve giriş noktası: açıklar
kurs yapısı (organizasyon), kaynaklar (HTML dosyaları, videolar, resimler)
ve meta veriler. Bunu sağlamak için bu bildirimi programlı olarak oluşturuyoruz.
Her iki SCORM sürümü için doğruluk ve destek.
# packaging/scorm_packager.py
import zipfile
import xml.etree.ElementTree as ET
from io import BytesIO
from dataclasses import dataclass
from typing import List, Optional
from pathlib import Path
import hashlib
@dataclass
class SCORMModule:
identifier: str # Univoco nel pacchetto
title: str
launch_url: str # Es: "content/module01/index.html"
scorm_type: str = "sco" # "sco" o "asset"
mastery_score: int = 70 # Punteggio minimo per superamento (%)
time_limit_minutes: Optional[int] = None
@dataclass
class SCORMPackageConfig:
course_id: str
course_title: str
course_description: str
scorm_version: str # "1.2" o "2004_4th"
language: str # "it-IT", "en-US"
modules: List[SCORMModule]
tenant_id: str
version: str
class SCORMPackager:
"""Genera pacchetti SCORM conformi allo standard ADL."""
SCORM_12_SCHEMA = "http://www.adlnet.org/xsd/adlcp_rootv1p2"
SCORM_2004_SCHEMA = "http://www.adlnet.org/xsd/adlcp_v1p3"
def package(self, config: SCORMPackageConfig, content_dir: Path) -> bytes:
"""
Genera un pacchetto SCORM ZIP con imsmanifest.xml.
content_dir: directory con i file HTML/CSS/JS del corso.
"""
zip_buffer = BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
# Aggiungi il manifest
manifest = self._generate_manifest(config)
zf.writestr('imsmanifest.xml', manifest)
# Aggiungi API wrapper SCORM JavaScript
scorm_api = self._get_scorm_api_js(config.scorm_version)
zf.writestr('scorm_api.js', scorm_api)
# Aggiungi tutti i file del corso
for file_path in content_dir.rglob('*'):
if file_path.is_file():
relative = file_path.relative_to(content_dir)
zf.write(file_path, str(relative))
return zip_buffer.getvalue()
def _generate_manifest(self, config: SCORMPackageConfig) -> str:
"""Genera il file imsmanifest.xml secondo lo standard SCORM."""
if config.scorm_version == "1.2":
return self._manifest_12(config)
return self._manifest_2004(config)
def _manifest_12(self, config: SCORMPackageConfig) -> str:
"""Manifest per SCORM 1.2."""
return f"""<?xml version="1.0" encoding="UTF-8"?>
<manifest identifier="{config.course_id}" version="1.0"
xmlns="http://www.imsproject.org/xsd/imscp_rootv1p1p2"
xmlns:adlcp="{self.SCORM_12_SCHEMA}"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.imsproject.org/xsd/imscp_rootv1p1p2
imscp_rootv1p1p2.xsd {self.SCORM_12_SCHEMA} adlcp_rootv1p2.xsd">
<metadata>
<schema>ADL SCORM</schema>
<schemaversion>1.2</schemaversion>
</metadata>
<organizations default="ORG-{config.course_id}">
<organization identifier="ORG-{config.course_id}" structure="hierarchical">
{config.course_title}
{self._generate_items_12(config.modules)}
</organization>
</organizations>
<resources>
{self._generate_resources_12(config.modules)}
</resources>
</manifest>"""
def _generate_items_12(self, modules: List[SCORMModule]) -> str:
items = []
for module in modules:
mastery = f'<adlcp:masteryscore>{module.mastery_score}</adlcp:masteryscore>' if module.scorm_type == 'sco' else ''
items.append(f"""
<item identifier="ITEM-{module.identifier}" identifierref="RES-{module.identifier}" isvisible="true">
{module.title}
{mastery}
</item>""")
return "".join(items)
def _generate_resources_12(self, modules: List[SCORMModule]) -> str:
resources = []
for module in modules:
resources.append(f"""
<resource identifier="RES-{module.identifier}" type="webcontent"
adlcp:scormtype="{module.scorm_type}" href="{module.launch_url}">
<file href="{module.launch_url}" />
</resource>""")
return "".join(resources)
def _get_scorm_api_js(self, version: str) -> str:
"""
SCORM API wrapper JavaScript per comunicazione con LMS.
Semplificato: in produzione usa scorm-again o pipwerks-scorm-api.
"""
if version == "1.2":
return """
// SCORM 1.2 API Wrapper - Simplified
var API = null;
function findAPI(win) {
var searchLimit = 7;
var currentWindow = win;
while ((currentWindow.API == null) && (currentWindow.parent != null) && (currentWindow.parent != currentWindow)) {
searchLimit--;
if (searchLimit <= 0) return null;
currentWindow = currentWindow.parent;
}
return currentWindow.API;
}
function getAPI() {
if (API == null) {
API = findAPI(window);
if (API == null && window.opener != null) {
API = findAPI(window.opener);
}
}
return API;
}
var SCORM = {
init: function() {
var api = getAPI();
if (api) return api.LMSInitialize("") === "true";
return false;
},
finish: function() {
var api = getAPI();
if (api) {
api.LMSCommit("");
return api.LMSFinish("") === "true";
}
return false;
},
getValue: function(element) {
var api = getAPI();
if (api) return api.LMSGetValue(element);
return "";
},
setValue: function(element, value) {
var api = getAPI();
if (api) return api.LMSSetValue(element, value) === "true";
return false;
},
commit: function() {
var api = getAPI();
if (api) return api.LMSCommit("") === "true";
return false;
},
setProgress: function(percent) {
this.setValue("cmi.core.score.raw", percent.toString());
if (percent >= 70) {
this.setValue("cmi.core.lesson_status", "passed");
} else {
this.setValue("cmi.core.lesson_status", "failed");
}
this.commit();
},
setCompleted: function() {
this.setValue("cmi.core.lesson_status", "completed");
this.commit();
}
};"""
return "// SCORM 2004 API wrapper"
4. Küresel Dağıtım için CDN Stratejisi
SCORM paketleri ağır varlıklar (videolar, HD görüntüler) içerir. Bunları dağıtın tek bir sunucudan uzak öğrenciler için yüksek gecikmeye neden olur. Çok seviyeli bir CDN stratejisi kullanıyoruz: CloudFront/Cloudflare depolama açıkken küresel uç önbelleğe alma için S3/Bulut Depolama erişim kontrolü için kaynak ve imzalı URL'ler gibi.
# cdn/content_delivery.py
import boto3
from botocore.signers import CloudFrontSigner
import rsa
from datetime import datetime, timedelta
from typing import Optional
import json
import hashlib
class SCORMContentDelivery:
"""
Gestisce distribuzione sicura di pacchetti SCORM via CDN.
Usa URL firmati CloudFront per controllo accesso.
"""
def __init__(
self,
cloudfront_domain: str,
s3_bucket: str,
private_key_path: str,
key_pair_id: str,
):
self.cdn_domain = cloudfront_domain
self.s3_bucket = s3_bucket
self.key_pair_id = key_pair_id
self.s3_client = boto3.client('s3')
self.cf_client = boto3.client('cloudfront')
with open(private_key_path, 'rb') as f:
self._private_key = rsa.PrivateKey.load_pkcs1(f.read())
def get_signed_launch_url(
self,
content_id: str,
tenant_id: str,
version: str,
module_path: str,
expires_in_hours: int = 8,
) -> str:
"""
Genera un URL firmato CloudFront per un modulo SCORM specifico.
L'URL scade dopo expires_in_hours ore per sicurezza.
"""
s3_path = f"tenants/{tenant_id}/content/{content_id}/{version}/{module_path}"
cdn_url = f"https://{self.cdn_domain}/{s3_path}"
expire_date = datetime.utcnow() + timedelta(hours=expires_in_hours)
signer = CloudFrontSigner(self.key_pair_id, self._rsa_signer)
signed_url = signer.generate_presigned_url(
cdn_url,
date_less_than=expire_date,
)
return signed_url
def _rsa_signer(self, message: bytes) -> bytes:
return rsa.sign(message, self._private_key, 'SHA-1')
async def upload_package(
self,
content_id: str,
tenant_id: str,
version: str,
package_bytes: bytes,
) -> dict:
"""
Carica un pacchetto SCORM su S3 e invalida la cache CDN.
"""
prefix = f"tenants/{tenant_id}/content/{content_id}/{version}"
# Calcola hash per integrity check
package_hash = hashlib.sha256(package_bytes).hexdigest()
# Carica il pacchetto ZIP
package_key = f"{prefix}/package.zip"
self.s3_client.put_object(
Bucket=self.s3_bucket,
Key=package_key,
Body=package_bytes,
ContentType='application/zip',
Metadata={
'content-id': content_id,
'tenant-id': tenant_id,
'version': version,
'sha256': package_hash,
},
)
# Invalida cache CDN per questo contenuto
self.cf_client.create_invalidation(
DistributionId='YOUR_DISTRIBUTION_ID',
InvalidationBatch={
'Paths': {
'Quantity': 1,
'Items': [f"/tenants/{tenant_id}/content/{content_id}/{version}/*"],
},
'CallerReference': f"{content_id}-{version}-{int(datetime.utcnow().timestamp())}",
},
)
return {
"package_key": package_key,
"package_hash": package_hash,
"cdn_url": f"https://{self.cdn_domain}/{prefix}/",
}
async def generate_tenant_manifest_url(
self,
content_id: str,
tenant_id: str,
version: str,
) -> str:
"""URL del manifest SCORM per un tenant specifico."""
return self.get_signed_launch_url(
content_id=content_id,
tenant_id=tenant_id,
version=version,
module_path="imsmanifest.xml",
)
5. SCORM'dan xAPI'ye Ne Zaman Geçilmeli?
SCORM, kurumsal eğitim için baskın standarttır ancak sınırlamaları vardır 2025'te önemli: sınırlı izleme (yalnızca tamamlandı/başarısız/tamamlanmamış) sürüm 1.2'de), LMS'in çalıştırılmasına bağımlılık, mobil cihazlarda zor. xAPI (Teneke Kutu API'si) tüm bu sınırlamaların üstesinden gelir ancak gerektirir bir Öğrenme Kayıt Deposu (LRS) ayırmak.
SCORM ve xAPI: Ne Zaman Seçilmeli?
| Kriter | SCORM 1.2 / 2004 | xAPI (Teneke Kutu) |
|---|---|---|
| Eski LMS uyumluluğu | Evrensel | Yalnızca modern ÖYS/ÖYS |
| Ayrıntı düzeyini izleme | Tamamlandı/Başarısız/Puan | Herhangi bir aktivite |
| Mobil destek | Sorunlu (iframe) | Yerli |
| Çevrimdışı destek | Desteklenmiyor | Evet (yerel LRS ile) |
| Gerekli altyapı | Yalnızca ÖYS/ÖYS | LMS + LRS |
| Birlikte çalışabilirlik | Konsolide standart | Yükselen standart |
| Gelişmiş analitik | Sınırlı | Tamamlamak |
| Şunun için önerilir: | Eski kurumsal eğitim | Yeni Eğitim Teknolojisi sistemleri |
Kaçınılması Gereken Anti-Desenler
- Tüm kiracılar için tek URL: Kiracı başına ayrı URL'ler olmadan kontrollü erişim veya CSS özelleştirmelerini uygulayamazsınız. tenant_id ile S3 yolunu kullanın.
- Zaman damgasıyla sürüm oluşturma: Semver yerine zaman damgasının kullanılması, otomatik güncelleme politikalarının yönetilmesini imkansız hale getirir.
- API sarmalayıcısı olmayan SCORM: LMS'in SCORM API'sine sarmalayıcı olmadan doğrudan erişim, kodu taşınabilir olmaktan çıkarır ve test edilmesini zorlaştırır.
- Geçersiz Kılma Olmadan CDN Önbelleği: İçerik güncellemesinden sonra eski sürüm, geçersiz kılınmadığı sürece önbellekte kalır. Yüklemeden sonra her zaman geçersiz kıl.
- SCORM paketlerinde doğrudan değişiklikler: Bir SCORM paketini yayınlandıktan sonra asla değiştirmeyin. Her zaman yeni bir sürüm oluşturun.
- SCORM 1.2'yi göz ardı edin: 2025'te kurumsal ÖYS/ÖYS'lerin %60'ından fazlası yalnızca SCORM 1.2'yi destekliyor. Herkesin 2004 kullandığını varsaymayın.
EdTech Mühendislik Serisinin Sonuçları
EdTech mühendisliğinin tüm turunu tamamladık: çok kiracılı mimariden ÖYS/LMS'nin (Madde 1) video akışından uyarlanabilir öğrenme algoritmalarına kadar LLM+RAG eğitmenlerinden oyunlaştırma motorlarına kadar yapay zeka gözetmenlik sistemlerine, öğrenme analitiğinden CRDT ile gerçek zamanlı işbirliğine, önce çevrimdışına kadar mobilden SCORM içerik yönetimine.
Tüm bu sistemlerin ortak paydası düşünme ihtiyacıdır. büyük ölçekte: sonradan düşünüldüğü gibi değil, başlangıçtan itibaren çok kiracılı; binlerce değil milyonlarca kullanıcıya yönelik performans; gizlilik ve uyumluluk nihai kontrol listesi olarak değil, tasarım kısıtlamaları olarak.
Eğitim Teknolojisi pazarı hızla büyümeye devam ediyor: 2030'a kadar bir milyar oranında öğrenci eğitim içeriğine çoğunlukla platformlar aracılığıyla erişecek dijital. Bugün inşa ettiğimiz mimariler o neslin nasıl olduğunu tanımlayacak öğren. Onları iyi inşa etmeye değer.
EdTech Mühendislik Serisi - Tam Özet
- Ölçeklenebilir LMS Mimarisi: Çok Kiracılı Model
- Uyarlanabilir Öğrenme Algoritmaları: Teoriden Üretime
- Eğitim için Video Yayını: WebRTC vs HLS vs DASH
- Yapay Zeka Gözetleme Sistemleri: Bilgisayarlı Görme ile Öncelik Gizlilik
- LLM'de Kişiselleştirilmiş Öğretmen: Bilgi Temellendirme için RAG
- Oyunlaştırma Motoru: Mimari ve Durum Makinesi
- Öğrenme Analitiği: xAPI ve Kafka ile Veri Hattı
- Eğitim Teknolojisinde Gerçek Zamanlı İşbirliği: CRDT ve WebSocket
- Mobil Öncelikli Eğitim Teknolojisi: Çevrimdışı Öncelikli Mimari
- Çok Kiracılı İçerik Yönetimi: Sürüm Oluşturma ve SCORM (bu makale)







