From c857c2c6db59d853211d7397339fdb2c3a8419c2 Mon Sep 17 00:00:00 2001 From: Vincent Vanwaelscappel Date: Tue, 28 Jun 2022 16:26:45 +0200 Subject: [PATCH] wip #5319 @2 --- .../Controllers/Admin/Base/QuizController.php | 26 -- .../Admin/CompanyCrudController.php | 1 + app/Http/Controllers/Admin/CrudController.php | 27 ++ .../Admin/CubemailsCrudController.php | 29 +++ .../Admin/ElearningMediaCrudController.php | 32 +++ .../Admin/ElearningPackageCrudController.php | 32 +++ .../FluidbookCollectionCrudController.php | 30 +++ .../Admin/FluidbookIconsetCrudController.php | 29 +++ .../FluidbookPublicationCrudController.php | 1 + .../Admin/FluidbookStatsController.php | 159 ++++++++++++ .../Admin/FluidbookThemeCrudController.php | 1 + .../FluidbookTranslateCrudController.php | 30 +++ .../Admin/LocaleCrudController.php | 1 + .../Admin/Operations/DownloadOperation.php | 39 --- .../Operations/Quiz/DownloadOperation.php | 29 +++ .../Operations/{ => Quiz}/ImportOperation.php | 2 +- .../Operations/{ => Quiz}/LogOperation.php | 4 +- .../{ => Quiz}/PreviewOperation.php | 2 +- .../Operations/{ => Quiz}/ReportOperation.php | 2 +- .../Controllers/Admin/PageCrudController.php | 29 +++ .../Controllers/Admin/QuizCrudController.php | 10 +- .../Admin/QuizatttemptCrudController.php | 1 + .../Admin/QuiztranslationCrudController.php | 1 + .../Admin/SettingsCrudController.php | 1 + .../Admin/SignatureCrudController.php | 1 + .../Admin/ToolboxTranslateCrudController.php | 1 + .../Controllers/Admin/UsersCrudController.php | 1 + app/Jobs/QuizDownload.php | 54 ++++ app/Models/ELearningPackage.php | 3 +- app/Models/Quiz.php | 84 ++++-- resources/elearningpackage/js/app.js | 240 +----------------- .../fonts/Roboto-Bold-webfont.woff | Bin .../fonts/Roboto-Light-webfont.woff | Bin .../fonts/Roboto-Regular-webfont.woff | Bin .../fonts/RobotoCondensed-Bold-webfont.woff | Bin .../fonts/RobotoCondensed-Light-webfont.woff | Bin .../RobotoCondensed-Regular-webfont.woff | Bin .../fonts/fontawesome-webfont.woff | Bin .../fonts/fontawesome-webfont.woff2 | Bin resources/quiz/index.html | 10 +- resources/quiz/js/app.js | 42 ++- resources/quiz/js/bootstrap.js | 11 - resources/quiz/js/scorm.js | 65 ----- resources/quiz/style/004-fonts.sass | 6 +- resources/scorm/scorm.js | 235 +++++++++++++++++ .../crud/buttons/quiz/download.blade.php | 53 +++- 46 files changed, 887 insertions(+), 437 deletions(-) delete mode 100644 app/Http/Controllers/Admin/Base/QuizController.php create mode 100644 app/Http/Controllers/Admin/CrudController.php create mode 100644 app/Http/Controllers/Admin/CubemailsCrudController.php create mode 100644 app/Http/Controllers/Admin/ElearningMediaCrudController.php create mode 100644 app/Http/Controllers/Admin/ElearningPackageCrudController.php create mode 100644 app/Http/Controllers/Admin/FluidbookCollectionCrudController.php create mode 100644 app/Http/Controllers/Admin/FluidbookIconsetCrudController.php create mode 100644 app/Http/Controllers/Admin/FluidbookStatsController.php create mode 100644 app/Http/Controllers/Admin/FluidbookTranslateCrudController.php delete mode 100644 app/Http/Controllers/Admin/Operations/DownloadOperation.php create mode 100644 app/Http/Controllers/Admin/Operations/Quiz/DownloadOperation.php rename app/Http/Controllers/Admin/Operations/{ => Quiz}/ImportOperation.php (99%) rename app/Http/Controllers/Admin/Operations/{ => Quiz}/LogOperation.php (90%) rename app/Http/Controllers/Admin/Operations/{ => Quiz}/PreviewOperation.php (94%) rename app/Http/Controllers/Admin/Operations/{ => Quiz}/ReportOperation.php (98%) create mode 100644 app/Http/Controllers/Admin/PageCrudController.php create mode 100644 app/Jobs/QuizDownload.php rename resources/quiz/{style => }/fonts/Roboto-Bold-webfont.woff (100%) rename resources/quiz/{style => }/fonts/Roboto-Light-webfont.woff (100%) rename resources/quiz/{style => }/fonts/Roboto-Regular-webfont.woff (100%) rename resources/quiz/{style => }/fonts/RobotoCondensed-Bold-webfont.woff (100%) rename resources/quiz/{style => }/fonts/RobotoCondensed-Light-webfont.woff (100%) rename resources/quiz/{style => }/fonts/RobotoCondensed-Regular-webfont.woff (100%) rename resources/quiz/{style => }/fonts/fontawesome-webfont.woff (100%) rename resources/quiz/{style => }/fonts/fontawesome-webfont.woff2 (100%) delete mode 100644 resources/quiz/js/bootstrap.js delete mode 100644 resources/quiz/js/scorm.js create mode 100644 resources/scorm/scorm.js diff --git a/app/Http/Controllers/Admin/Base/QuizController.php b/app/Http/Controllers/Admin/Base/QuizController.php deleted file mode 100644 index 4ee096989..000000000 --- a/app/Http/Controllers/Admin/Base/QuizController.php +++ /dev/null @@ -1,26 +0,0 @@ -crud->addClause('whereIn', 'owner', auth()->user()->getManagedUsers()); - } - -} diff --git a/app/Http/Controllers/Admin/CompanyCrudController.php b/app/Http/Controllers/Admin/CompanyCrudController.php index f397a4cd1..10e66dd44 100644 --- a/app/Http/Controllers/Admin/CompanyCrudController.php +++ b/app/Http/Controllers/Admin/CompanyCrudController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin; class CompanyCrudController extends \Cubist\Backpack\Magic\Controllers\CubistMagicController { use \Cubist\Backpack\Magic\Operations\CreateOperation; + use \Cubist\Backpack\Http\Controllers\Operations\CloneEditOperation; use \Cubist\Backpack\Magic\Operations\UpdateOperation; use \Cubist\Backpack\Http\Controllers\Operations\ReviseOperation; diff --git a/app/Http/Controllers/Admin/CrudController.php b/app/Http/Controllers/Admin/CrudController.php new file mode 100644 index 000000000..a5aba83f7 --- /dev/null +++ b/app/Http/Controllers/Admin/CrudController.php @@ -0,0 +1,27 @@ += 21210 (even numbers) = stats4.fluidbook.com + // ID >= 21211 (odd numbers) = stats5.fluidbook.com + + // Each stats server has a different instance of Matamo, so we need to provide different API tokens for each + // Normally this information would be stored in the .env but there's no good way to do that with an array, so + // it is simpler to keep it here. These are also stored in the shared Bitwarden entry for Matomo. + $matomo_tokens = [ + 'stats3.fluidbook.com' => '9df722a0bd30878ddc4d737352427502', + 'stats4.fluidbook.com' => '3ffdbe052ae625f065573df9fa9515df', + 'stats5.fluidbook.com' => '85e9cc307b6e5083249949e9472a80b8', + ]; + + $fluidbook_id = intval($fluidbook_id); + + if ($fluidbook_id < 21210) { + $server = 'stats3.fluidbook.com'; + } elseif ($fluidbook_id >= 21210 && $fluidbook_id % 2 === 0) { + $server = 'stats4.fluidbook.com'; + } else { + $server = 'stats5.fluidbook.com'; + } + + //dump("Server is $server"); + + return new Reporting("https://{$server}/", $matomo_tokens[$server]); + } + + + private function _parseDate($date) { + // Match possible date strings: + // - YYYY + // - YYYY-MM + // - YYYY-MM-DD + // - YYYY-MM-DD,YYYY-MM-DD + // https://regex101.com/r/BLrqm0/1 + $regex = '/^(?(?2\d{3})-?(?0[1-9]|1[012])?-?(?0[1-9]|[12][0-9]|3[01])?),?(?2\d{3}-(?>0[1-9]|1[012])-(?>0[1-9]|[12][0-9]|3[01]))?/'; + + preg_match($regex, $date, $date_matches); + + return $date_matches; + } + + protected function summary($fluidbook_id, $date = null) + { + $dates = $date ? $this->_parseDate($date) : false; + + $fluidbook = FluidbookPublication::findOrFail($fluidbook_id); + + + // TODO: year(s)? view like the old version: https://workshop.fluidbook.com/stats/10003_ab3cacc39ebbf2478e0931629d114e74 + // Need to calculate all the available dates, probably based on creation date of the Fluidbook + + // TODO: month view, breakdown of individual day stats: https://workshop.fluidbook.com/stats/10003_ab3cacc39ebbf2478e0931629d114e74/2017/10 + // These would be linked from the "Year(s)" view above... + + // Matomo API + // We need to pass it a date (eg. "2022-01-01") or date range (eg. "2022-03-01,2022-05-15") + // We can then specify the granularity of stats by specifying the period: + // - range = aggregated summary of stats for the specified date range + // - year = summary of stats broken down by year(s) + // - month = summary of stats broken down by month(s) + // - day = summary of stats broken down by day(s) + + // Which mode are we in? + if (isset($dates['start_date']) && isset($dates['end_date'])) { // Custom date range + $mode = 'range'; + $date_range = "{$dates['start_date']},{$dates['end_date']}"; + $period = 'day'; // Segregate stats by day + + } elseif (isset($dates['start_year']) && isset($dates['start_month'])) { // Month view + $mode = 'month'; + $month = $dates['start_month']; + $year = $dates['start_year']; + $last_day_of_month = cal_days_in_month(CAL_GREGORIAN, $month, $year); + $date_range = "{$year}-{$month}-01,{$year}-{$month}-{$last_day_of_month}"; + $period = 'day'; // Segregate stats by day + + } elseif (isset($dates['start_year'])) { + $mode = 'year'; + $year = $dates['start_year']; + $end_date = $year == date('Y') ? date('Y-m-d') : "{$year}-12-31"; // If it's the current year, don't get future dates + $date_range = "{$year}-01-01,{$end_date}"; // Full range of specified year + $period = 'month'; // Segregate stats by month + + } else { // No valid dates specified, display the full data set + $mode = 'overview'; + $start_date = $fluidbook->created_at->isoFormat('YYYY-MM-DD'); + $date_range = $start_date . ',' . date('Y-m-d'); + $period = 'month'; // Segregate stats by month + } + + // TODO: support the ability to specify a date range from a date-picker and also maybe choose the breakdown (by year/month/day/range) + + $report = $this->_getReporting($fluidbook_id); + + echo "Getting stats for date range $date_range, segregated by $period"; + //dump(collect($report->getVisits($fluidbook_id, $date_range, $period))->sum('nb_visits')); + //dump($report->getVisits($fluidbook_id, $date_range, 'range')); + // dd($report->getVisits($fluidbook_id, $date_range, $period)); + $visits = collect($report->getVisits($fluidbook_id, $date_range, $period)); + $pageviews = collect($report->getPageViews($fluidbook_id, $date_range, $period)); + + // Get the search keywords as a range because we don't need to display them by date + $searches = collect($report->getSearchKeywords($fluidbook_id, $date_range, 'range')); + + // Format dates for display as labels on the x-axis + $labels = $visits->keys()->map(function($label, $index) use ($period) { + return match ($period) { + 'day' => Carbon::parse($label)->isoFormat('DD'), // Convert YYYY-MM-DD string from API into zero-padded day alone + 'month' => Carbon::parse($label)->isoFormat('MMM'), // Convert to abbreviated month name + default => $label, + }; + })->toArray(); + + // Format dates for display in the tooltip title + $formatted_dates = $visits->keys()->map(function($label, $index) use ($period) { + return match ($period) { + 'day' => Carbon::parse($label)->isoFormat('dddd Do MMMM YYYY'), + 'month' => Carbon::parse($label)->isoFormat('MMMM YYYY'), + default => $label, + }; + })->toArray(); + + $chart = Chartisan::build() + ->labels($labels) + ->extra(['tooltip_labels' => array_combine($labels, $formatted_dates)]) + ->dataset('Visits', $visits->pluck('nb_visits')->toArray()) + ->dataset('Page Views', $pageviews->pluck('nb_pageviews')->toArray()) + ->toJSON(); + + // header('Content-Type: application/json; charset=utf-8'); + // return $chart; + + return view('fluidbook_stats.summary', compact('fluidbook', 'visits', 'pageviews', 'searches', 'chart', 'mode', 'period', 'dates')); + + + } + + +} diff --git a/app/Http/Controllers/Admin/FluidbookThemeCrudController.php b/app/Http/Controllers/Admin/FluidbookThemeCrudController.php index d7ef37e17..9acbdce5d 100644 --- a/app/Http/Controllers/Admin/FluidbookThemeCrudController.php +++ b/app/Http/Controllers/Admin/FluidbookThemeCrudController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin; class FluidbookThemeCrudController extends \Cubist\Backpack\Magic\Controllers\CubistMagicController { use \Cubist\Backpack\Magic\Operations\CreateOperation; + use \Cubist\Backpack\Http\Controllers\Operations\CloneEditOperation; use \Cubist\Backpack\Magic\Operations\UpdateOperation; use \Cubist\Backpack\Http\Controllers\Operations\BulkPublishOperation; use \Backpack\CRUD\app\Http\Controllers\Operations\CloneOperation; diff --git a/app/Http/Controllers/Admin/FluidbookTranslateCrudController.php b/app/Http/Controllers/Admin/FluidbookTranslateCrudController.php new file mode 100644 index 000000000..ca308fe70 --- /dev/null +++ b/app/Http/Controllers/Admin/FluidbookTranslateCrudController.php @@ -0,0 +1,30 @@ +crud->addButtonFromView('line', 'download', 'quiz.download', 'end'); - } - - protected function download($id) - { - $compilepath = protected_path('quiz/final/' . $id); - $entry = $this->crud->getEntry($id); - $entry->compile($compilepath); - - $translation = QuizTranslation::find($entry->getAttribute('translation')); - $fname = Str::slugCase($entry->getAttribute('client') . ' ' . $entry->getAttribute('project') . ' ' . date_format($entry->getAttribute('updated_at'), 'Ymd') . ' ' . $translation->locale . ' ' . $id) . '.zip'; - $dest = protected_path('quiz/download/' . $fname); - - Zip::archive($compilepath, $dest); - - return response(null)->header('Content-Type', 'application/zip') - ->header('Content-Disposition', 'attachment; filename="' . $fname . '"') - ->header('X-Sendfile', $dest); - } -} diff --git a/app/Http/Controllers/Admin/Operations/Quiz/DownloadOperation.php b/app/Http/Controllers/Admin/Operations/Quiz/DownloadOperation.php new file mode 100644 index 000000000..ee798def4 --- /dev/null +++ b/app/Http/Controllers/Admin/Operations/Quiz/DownloadOperation.php @@ -0,0 +1,29 @@ +crud->addButtonFromView('line', 'download', 'quiz.download', 'end'); + } + + protected function download($id, $action) + { + QuizDownload::dispatch(Quiz::find($id), $action, backpack_user())->onQueue('download');; + Alert::add('success', __('La compilation a été placée en file d\'attente. Vous recevrez un email lorsqu\'elle sera terminée.'))->flash(); + return redirect(backpack_url('quiz')); + } +} diff --git a/app/Http/Controllers/Admin/Operations/ImportOperation.php b/app/Http/Controllers/Admin/Operations/Quiz/ImportOperation.php similarity index 99% rename from app/Http/Controllers/Admin/Operations/ImportOperation.php rename to app/Http/Controllers/Admin/Operations/Quiz/ImportOperation.php index 1ba768b49..b476ccb28 100644 --- a/app/Http/Controllers/Admin/Operations/ImportOperation.php +++ b/app/Http/Controllers/Admin/Operations/Quiz/ImportOperation.php @@ -1,6 +1,6 @@ entry->getFinalPath(); + $this->entry->compile($compilepath, $this->user); + + $fname = $this->_fname(); + $dest = Files::mkdir(storage_path('app/public/quiz/download/')) . $fname; + + Zip::archive($compilepath, $dest); + if (!file_exists($dest)) { + throw new \Exception('An error occured while compiling the quiz'); + } + + $url = $this->_url($fname); + + $subject = __('Quiz ":title" (#:nb) prêt au téléchargement', ['title' => $this->entry->title, 'nb' => $this->entry->id]); + $body = __('Le fichier est disponible à l\'adresse suivante : :url', ['url' => $url]); + + try { + if ($this->action === 'scormcloud') { + $scormURL = ScormCloud::send($url, 'toolbox_' . $this->type . '_' . $this->entry->id); + $body .= "

"; + $body .= __('Le package peut être testé sur SCORM Cloud : :url', ['url' => $scormURL]); + } + } catch (\Exception $e) { + $body .= "

"; + $body .= __('Une erreur s\'est produite lors de l\'envoi sur SCORM Cloud (App ID :appid) : :error', ['error' => $e->getMessage(), 'appid' => env('SCORM_CLOUD_APP_ID')]); + } + + + } catch (\Exception $e) { + $subject = __('Erreur lors de la compilation du quiz :nb', ['nb' => $this->entry->id]); + $body = __('Détails de l\'erreur :message', ['message' => $e->getMessage() . ' at line ' . $e->getLine() . ' of ' . $e->getFile()]); + } + + $this->sendEmail($subject, $body); + } + + +} diff --git a/app/Models/ELearningPackage.php b/app/Models/ELearningPackage.php index c2d72c9bd..e6dccaeda 100644 --- a/app/Models/ELearningPackage.php +++ b/app/Models/ELearningPackage.php @@ -12,6 +12,7 @@ use Cubist\Backpack\Magic\Fields\BunchOfFieldsMultiple; use Cubist\Backpack\Magic\Fields\Text; use Cubist\Backpack\Magic\Fields\Textarea; use Cubist\Scorm\Manifest; +use Cubist\Scorm\Version; use Cubist\Util\Files\Files; use Cubist\Util\Files\VirtualDirectory; use Cubist\Util\Zip; @@ -72,7 +73,7 @@ class ELearningPackage extends ToolboxModel $data = ['title' => $this->title, 'description' => $this->description, 'modules' => $modules]; $vdir->file_put_contents('data.js', 'const DATA=' . json_encode($data) . ';'); - $vdir->file_put_contents('imsmanifest.xml', new Manifest($this->title, Manifest::SCORM_2004, $organization, 'PACKAGE_' . $this->id)); + $vdir->file_put_contents('imsmanifest.xml', new Manifest($this->title, Version::SCORM_2004, $organization, 'PACKAGE_' . $this->id)); $vdir->copyDirectory(resource_path('elearningpackage/dist/css'), 'css'); $vdir->copyDirectory(resource_path('elearningpackage/dist/js'), 'js'); $vdir->copyDirectory(resource_path('elearningpackage/dist/fonts'), 'fonts'); diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index 278f930c0..dbc03bcbd 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -1,20 +1,23 @@ '', 'complete_when_opened' => false, 'mandatory' => true, + 'quiz_id' => '', 'fb_id' => '', 'audio_id' => '', 'pdf_id' => '', 'video_id' => '']; + + + + $modules = []; + + foreach ($this->contents as $id => $content) { + $m = $this->_compileModule($id, array_merge($defaultModuleContent, $content), $vdir, $user); + if ($m !== false) { + $modules[] = $m; + } + } + + $data = ['title' => $this->title, 'description' => $this->description, 'modules' => $modules]; + + $vdir->file_put_contents('data.js', 'const DATA=' . json_encode($data) . ';'); + $vdir->file_put_contents('imsmanifest.xml', new Manifest($this->title, Version::SCORM_2004, $organization, 'PACKAGE_' . $this->id)); + $vdir->copyDirectory(resource_path('elearningpackage/dist/css'), 'css'); + $vdir->copyDirectory(resource_path('elearningpackage/dist/js'), 'js'); + $vdir->copyDirectory(resource_path('elearningpackage/dist/fonts'), 'fonts'); + $vdir->copy(resource_path('elearningpackage/index.html'), 'index.html'); + $vdir->sync(true);*/ - // Extract template into the final dir - $from = resource_path('quiz') . '/'; - `rsync -a --exclude '*.less' --exclude '*.less' $from $dest/`; + $wdir = resource_path('quiz'); + $vdir = new VirtualDirectory($dest); + + + $data = $this->getData(); + $json = json_encode($data, JSON_THROW_ON_ERROR); + $vdir->file_put_contents('data.js', 'var DATA=' . $json . ';'); + $vdir->copy($wdir . '/index.html', 'index.html'); + $vdir->copyDirectory($wdir . '/dist/css', 'css'); + $vdir->copyDirectory($wdir . '/dist/js', 'js'); + $vdir->copyDirectory($wdir . '/fonts', 'fonts'); + $vdir->copyDirectory($wdir . '/assets', 'assets'); // Copy assets $assets = ['banner' => 'banner.jpg', 'logo' => 'logo.png']; @@ -258,30 +293,18 @@ class Quiz extends ToolboxModel $path = $media->getPath($conversionName); if (file_exists($path)) { - copy($path, $dest . '/assets/' . $filename); + $vdir->copy($path, 'assets/' . $filename); } } } } - $data = $this->getData(); - - $json = json_encode($data, JSON_THROW_ON_ERROR); - - file_put_contents($dest . '/data.js', 'var DATA=' . $json . ';'); if ($forceScorm || $this->getAttribute('scorm')) { - $scorm_js = ' -'; $manifest = new Manifest($this->getAttribute('title'), $this->getAttribute('scorm_version'), $this->getAttribute('client') ?: backpack_user()->getCompanyNameAttribute(), $this->getAttribute('project') ?: 'Quiz'); - file_put_contents($dest . '/imsmanifest.xml', $manifest); - } else { - $scorm_js = ''; - unlink($dest . '/js/scorm.js'); - unlink($dest . '/js/libs/scorm/apiwrapper.js'); + $vdir->file_put_contents('imsmanifest.xml', $manifest); } - - file_put_contents($dest . '/index.html', str_replace('$SCORM_JS', $scorm_js, file_get_contents($dest . '/index.html'))); + $vdir->sync(true); } public function getData() @@ -367,4 +390,9 @@ class Quiz extends ToolboxModel return parent::create($data); } + public function getFinalPath() + { + return protected_path('quiz/final/' . $this->id); + } + } diff --git a/resources/elearningpackage/js/app.js b/resources/elearningpackage/js/app.js index 33d1ee0b9..6272b99b2 100644 --- a/resources/elearningpackage/js/app.js +++ b/resources/elearningpackage/js/app.js @@ -1,48 +1,13 @@ require('./bootstrap'); -import {SCORM} from 'pipwerks-scorm-api-wrapper'; +require('../../scorm/scorm'); window.savedState = {}; window.currentModule = null; -var SCORM_INITED = false; -var SCORM_START_TIME = null; -var SCORM_INTERACTION_TIMESTAMPS = []; -var SCORM_CORRECT_ANSWERS = []; -var SCORM_ID_TO_N = {}; -var SCORM_WEIGHTING = 0; -var SCORM_QUESTIONS = []; -var SCORM_SUCCESS_STATUS = 'unknown'; -var SCORM_SUCCESS_SCORE = 0; -var SCORM_EVENTS_INITED = false; -var SCORM_INTERACTIONS_INITED = false; -var SCORM_LOCATION_INITED = false; -var SCORM_OK = false; - -var _CMI12 = { - 'location': 'cmi.core.lesson_location', - 'status': "cmi.core.lesson_status", - 'session_time': 'cmi.core.session_time', - 'success_status': '', - 'exit': 'cmi.core.exit', - 'cmi.score.raw': 'cmi.core.score.raw', - 'cmi.score.min': 'cmi.core.score.min', - 'cmi.score.max': 'cmi.core.score.max', - 'cmi.score.scaled': '', -}; - -var _CMI2004 = { - 'location': 'cmi.location', - 'status': 'cmi.completion_status', - 'session_time': 'cmi.session_time', - 'success_status': 'cmi.success_status', - 'exit': 'cmi.exit', -}; - - $(function () { $("header #logo").html(getSpriteIcon('logo')); $("header #tile").html(getSpriteIcon('tile')); - initScorm(); + initPackage(); $(window).on('resize', function () { resize(); }); @@ -57,58 +22,20 @@ function resize() { } } -function _cmi(key) { - var res = null; - switch (SCORM.version) { - case "1.2" : - res = _CMI12[key]; - break; - case '2004': - res = _CMI2004[key]; - break; - } - if (res == undefined || res == null) { - res = key; - } - return res; -} - -function initScorm() { +function initPackage() { if (SCORM_INITED) { return; } - SCORM_INITED = true; - try { - if (SCORM.init()) { - SCORM_OK = true; - } - } catch (e) { - - } - - try { - if (FORCE_SCORM) { - SCORM_OK = true; - } - } catch (e) { - - } - - if (SCORM_OK) { - scormExit(); - startScormTimer(); - initScormEvents(); - } + var res=initScorm(); initState(); setContents(); initEvents(); window.API = window.API_1484_11 = new SCORMFacade(); - return SCORM_OK; + return res; } function initState() { - if (SCORM_LOCATION_INITED) { return; } @@ -145,47 +72,6 @@ function initState() { } }); } - -function scormMarkAsComplete() { - if (!SCORM_OK) { - return; - } - scormExit(); -} - - -function finishScorm() { - if (!SCORM_OK) { - return; - } - setSessionTime(); - SCORM.save(); - SCORM.quit(); -} - -function scormExit() { - if (!SCORM_OK) { - return; - } - var v = 'suspend'; - setScormValue('exit', v); -} - -function startScormTimer() { - SCORM_START_TIME = new Date(); -} - -function scormCompleteAndClose() { - scormComplete(); - scormClose(); -} - -function scormClose() { - parent.close(); - top.close(); - window.close(); -} - function initEvents() { $(document).on('click', '[data-id]:not([data-lock="locked"])', function () { openSubSCO($(this).data('id')); @@ -389,61 +275,6 @@ function doubleQuestionMark() { // Using `this` for web workers & supports Browserify / Webpack. })(typeof window === 'undefined' ? this : window); - -function initScormEvents() { - if (!SCORM_OK || SCORM_EVENTS_INITED) { - return; - } - SCORM_EVENTS_INITED = true; - - $(window).on('unload', function () { - finishScorm(); - }); - - setInterval(function () { - SCORM.save(); - }, 5000); -} - -function setSCORMLocation(location) { - return setScormValue('cmi.location', JSON.stringify(location)); -} - -function setSCORMScore(score, max, min) { - if (min === undefined) { - min = 0; - } - - setScormValue('cmi.core.score.raw', score); - setScormValue('cmi.core.score.min', min); - setScormValue('cmi.core.score.max', max); -} - - -function getScormValue(elementName) { - if (!SCORM_OK) { - return null; - } - var cmi = _cmi(elementName); - if (cmi == '') { - return null; - } - var result = SCORM.get(cmi); - return result; -} - -function setScormValue(elementName, value) { - if (!SCORM_OK) { - return; - } - var cmi = _cmi(elementName); - if (cmi == '') { - return false; - } - var result = SCORM.set(cmi, value); - return result; -} - function getSpriteIcon(icon, attrs, dimensions) { var a = []; var iconSymbol = $('svg symbol[id="' + icon + '"]'); @@ -484,67 +315,6 @@ function getSpriteIcon(icon, attrs, dimensions) { return ''; } - -function setSessionTime() { - if (!SCORM_OK) { - return; - } - var currentTime = new Date(); - var sessionTime; - - var endTime = currentTime.getTime() - var calculatedTime = endTime - SCORM_START_TIME; - - if (SCORM.version == '1.2') { - var totalHours = Math.floor(calculatedTime / 1000 / 60 / 60); - calculatedTime = calculatedTime - totalHours * 1000 * 60 * 60 - if (totalHours < 1000 && totalHours > 99) { - totalHours = "0" + totalHours; - } else if (totalHours < 100 && totalHours > 9) { - totalHours = "00" + totalHours; - } else if (totalHours < 10) { - totalHours = "000" + totalHours; - } - - var totalMinutes = Math.floor(calculatedTime / 1000 / 60); - calculatedTime = calculatedTime - totalMinutes * 1000 * 60; - if (totalMinutes < 10) { - totalMinutes = "0" + totalMinutes; - } - - var totalSeconds = Math.floor(calculatedTime / 1000); - if (totalSeconds < 10) { - totalSeconds = "0" + totalSeconds; - } - sessionTime = totalHours + ":" + totalMinutes + ":" + totalSeconds; - setScormValue('session_time', sessionTime); - } else { - setScormValue('session_time', scormSecondsToTimeInterval(calculatedTime / 1000)); - } - -} - -function dateToScormTime(date) { - var res = date.toISOString(); - var e = res.split('.'); - e.pop(); - return e.join('.'); -} - -function getScormTimeInterval(start, end) { - var diff = Math.round((end.getTime() - start.getTime()) / 1000); - return scormSecondsToTimeInterval(diff); -} - -function scormSecondsToTimeInterval(diff) { - var diff = Math.round(diff); - var h = Math.floor(diff / 3600); - diff = diff % 3600; - var m = Math.floor(diff / 60); - var s = diff % 60; - return 'PT' + h + 'H' + m + 'M' + s + 'S'; -} - function SCORMFacade() { } diff --git a/resources/quiz/style/fonts/Roboto-Bold-webfont.woff b/resources/quiz/fonts/Roboto-Bold-webfont.woff similarity index 100% rename from resources/quiz/style/fonts/Roboto-Bold-webfont.woff rename to resources/quiz/fonts/Roboto-Bold-webfont.woff diff --git a/resources/quiz/style/fonts/Roboto-Light-webfont.woff b/resources/quiz/fonts/Roboto-Light-webfont.woff similarity index 100% rename from resources/quiz/style/fonts/Roboto-Light-webfont.woff rename to resources/quiz/fonts/Roboto-Light-webfont.woff diff --git a/resources/quiz/style/fonts/Roboto-Regular-webfont.woff b/resources/quiz/fonts/Roboto-Regular-webfont.woff similarity index 100% rename from resources/quiz/style/fonts/Roboto-Regular-webfont.woff rename to resources/quiz/fonts/Roboto-Regular-webfont.woff diff --git a/resources/quiz/style/fonts/RobotoCondensed-Bold-webfont.woff b/resources/quiz/fonts/RobotoCondensed-Bold-webfont.woff similarity index 100% rename from resources/quiz/style/fonts/RobotoCondensed-Bold-webfont.woff rename to resources/quiz/fonts/RobotoCondensed-Bold-webfont.woff diff --git a/resources/quiz/style/fonts/RobotoCondensed-Light-webfont.woff b/resources/quiz/fonts/RobotoCondensed-Light-webfont.woff similarity index 100% rename from resources/quiz/style/fonts/RobotoCondensed-Light-webfont.woff rename to resources/quiz/fonts/RobotoCondensed-Light-webfont.woff diff --git a/resources/quiz/style/fonts/RobotoCondensed-Regular-webfont.woff b/resources/quiz/fonts/RobotoCondensed-Regular-webfont.woff similarity index 100% rename from resources/quiz/style/fonts/RobotoCondensed-Regular-webfont.woff rename to resources/quiz/fonts/RobotoCondensed-Regular-webfont.woff diff --git a/resources/quiz/style/fonts/fontawesome-webfont.woff b/resources/quiz/fonts/fontawesome-webfont.woff similarity index 100% rename from resources/quiz/style/fonts/fontawesome-webfont.woff rename to resources/quiz/fonts/fontawesome-webfont.woff diff --git a/resources/quiz/style/fonts/fontawesome-webfont.woff2 b/resources/quiz/fonts/fontawesome-webfont.woff2 similarity index 100% rename from resources/quiz/style/fonts/fontawesome-webfont.woff2 rename to resources/quiz/fonts/fontawesome-webfont.woff2 diff --git a/resources/quiz/index.html b/resources/quiz/index.html index 2cd105b35..ef0dceb21 100644 --- a/resources/quiz/index.html +++ b/resources/quiz/index.html @@ -7,7 +7,7 @@ - +
@@ -21,12 +21,6 @@
- - - - - -$SCORM_JS - + diff --git a/resources/quiz/js/app.js b/resources/quiz/js/app.js index 482a74535..76c12af5e 100644 --- a/resources/quiz/js/app.js +++ b/resources/quiz/js/app.js @@ -1,12 +1,39 @@ -require('./bootstrap'); +window.$ = window.jQuery = require('jquery'); import {SCORM} from 'pipwerks-scorm-api-wrapper'; import {gsap} from "gsap"; +require('../../scorm/scorm'); + (function (global) { $(function () { var data; var score; var questionStatus = {}; + var showReview, threshold, instantReview, logAttempts, displayScore, countQuestions, passedAction, failedAction, + logQuestions; + + function initApp() { + if (SCORM_INITED) { + return; + } + var scormok = initScorm(); + var defaultState = {q: 1}; + var state; + if (!scormok) { + state = defaultState; + } else { + var currentLocation = getScormValue('location'); + try { + state = JSON.parse(currentLocation); + } catch (e) { + state = defaultState; + } + if (state['q'] === undefined || state['q'] === null) { + state = defaultState; + } + } + init(state); + } $(window).on('resize', resize); @@ -73,7 +100,6 @@ import {gsap} from "gsap"; document.documentElement.style.setProperty('--nok-color', DATA.nokColor); document.documentElement.style.setProperty('--review-background', DATA.reviewBackground); $('head').append(''); - cssVars({}); showReview = DATA.review; threshold = 0; @@ -189,12 +215,9 @@ import {gsap} from "gsap"; $(document).on('quizinit', function (event, state) { init(state); }); - if (DATA.scorm) { - initScormEvents(); - } else { - SCORM = false; - $(document).trigger('quizinit', {q: 1}); - } + + initApp(); + resizeContainer(); resize(); } @@ -211,7 +234,6 @@ import {gsap} from "gsap"; return false; }); - if (state.a) { $.each($(state.a), function (k, v) { var q = $('section.question[data-q="' + (k + 1) + '"]'); @@ -605,5 +627,3 @@ import {gsap} from "gsap"; return {sprintf: y, vsprintf: e} })) }(); - -require('./scorm'); diff --git a/resources/quiz/js/bootstrap.js b/resources/quiz/js/bootstrap.js deleted file mode 100644 index 48c9cff59..000000000 --- a/resources/quiz/js/bootstrap.js +++ /dev/null @@ -1,11 +0,0 @@ -window._ = require('lodash'); - -/** - * We'll load the axios HTTP library which allows us to easily issue requests - * to our Laravel back-end. This library automatically handles sending the - * CSRF token as a header based on the value of the "XSRF" token cookie. - */ - -window.axios = require('axios'); -window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; -window.$ = window.jQuery = require('jquery'); diff --git a/resources/quiz/js/scorm.js b/resources/quiz/js/scorm.js deleted file mode 100644 index 31692e92d..000000000 --- a/resources/quiz/js/scorm.js +++ /dev/null @@ -1,65 +0,0 @@ -SCORM = true; - -function initScormEvents() { - SCORM = doLMSInitialize(); - var defaultState = {q: 1}; - if (!SCORM) { - // No SCORM, we init at question 1 - $(document).trigger('quizinit', defaultState); - return false; - } - $(window).on('unload', function () { - doLMSFinish(); - }); - - var currentStatus = getScormValue('cmi.core.lesson_status'); - if (currentStatus != 'passed' || currentStatus != 'completed') { - setScormValue('cmi.core.lesson_status', 'incomplete'); - } - - var savedState = getScormValue('cmi.core.lesson_location'); - try { - if (savedState == '') { - savedState = defaultState; - } else { - savedState = JSON.parse(savedState); - } - $(document).trigger('quizinit', savedState); - } catch (err) { - console.error(err); - } - - return true; -} - -function setSCORMLocation(location) { - return setScormValue('cmi.core.lesson_location', JSON.stringify(location)); -} - -function setSCORMScore(score, max, min) { - if (min === undefined) { - min = 0; - } - - setScormValue('cmi.core.score.raw', ((score - min) / (max - min)) * 100); - setScormValue('cmi.core.score.min', 0); - setScormValue('cmi.core.score.max', 100); -} - - -function getScormValue(elementName) { - if (!SCORM) { - return null; - } - var result = String(doLMSGetValue(elementName)); - return result; -} - -function setScormValue(elementName, value) { - if (!SCORM) { - return; - } - var result = doLMSSetValue(elementName, value); - doLMSCommit(); - return result; -} diff --git a/resources/quiz/style/004-fonts.sass b/resources/quiz/style/004-fonts.sass index ec0991554..1282e602d 100644 --- a/resources/quiz/style/004-fonts.sass +++ b/resources/quiz/style/004-fonts.sass @@ -1,17 +1,17 @@ @font-face font-family: 'RobotoCondensed' - src: url('fonts/RobotoCondensed-Regular-webfont.woff') format('woff') + src: url('../fonts/RobotoCondensed-Regular-webfont.woff') format('woff') font-weight: 400 font-style: normal @font-face font-family: 'RobotoCondensed' - src: url('fonts/RobotoCondensed-Light-webfont.woff') format('woff') + src: url('../fonts/RobotoCondensed-Light-webfont.woff') format('woff') font-weight: 300 font-style: normal @font-face font-family: 'RobotoCondensed' - src: url('fonts/RobotoCondensed-Bold-webfont.woff') format('woff') + src: url('../fonts/RobotoCondensed-Bold-webfont.woff') format('woff') font-weight: 700 font-style: normal diff --git a/resources/scorm/scorm.js b/resources/scorm/scorm.js new file mode 100644 index 000000000..31f8c73ef --- /dev/null +++ b/resources/scorm/scorm.js @@ -0,0 +1,235 @@ +import {SCORM} from "pipwerks-scorm-api-wrapper"; + +window.SCORM_INITED = false; +window.SCORM_START_TIME = null; +window.SCORM_INTERACTION_TIMESTAMPS = []; +window.SCORM_CORRECT_ANSWERS = []; +window.SCORM_ID_TO_N = {}; +window.SCORM_WEIGHTING = 0; +window.SCORM_QUESTIONS = []; +window.SCORM_SUCCESS_STATUS = 'unknown'; +window.SCORM_SUCCESS_SCORE = 0; +window.SCORM_EVENTS_INITED = false; +window.SCORM_INTERACTIONS_INITED = false; +window.SCORM_LOCATION_INITED = false; +window.SCORM_OK = false; + +window._CMI12 = { + 'location': 'cmi.core.lesson_location', + 'status': "cmi.core.lesson_status", + 'session_time': 'cmi.core.session_time', + 'success_status': '', + 'exit': 'cmi.core.exit', + 'cmi.score.raw': 'cmi.core.score.raw', + 'cmi.score.min': 'cmi.core.score.min', + 'cmi.score.max': 'cmi.core.score.max', + 'cmi.score.scaled': '', +}; + +window._CMI2004 = { + 'location': 'cmi.location', + 'status': 'cmi.completion_status', + 'session_time': 'cmi.session_time', + 'success_status': 'cmi.success_status', + 'exit': 'cmi.exit', +}; + +window.initScorm=function() +{ + if (SCORM_INITED) { + return; + } + + SCORM_INITED = true; + try { + if (SCORM.init()) { + SCORM_OK = true; + } + } catch (e) { + + } + + try { + if (FORCE_SCORM) { + SCORM_OK = true; + } + } catch (e) { + + } + + if (SCORM_OK) { + scormExit(); + startScormTimer(); + initScormEvents(); + } + return SCORM_OK; +} + +window._cmi = function (key) { + var res = null; + switch (SCORM.version) { + case "1.2" : + res = _CMI12[key]; + break; + case '2004': + res = _CMI2004[key]; + break; + } + if (res == undefined || res == null) { + res = key; + } + return res; +}; + +window.initScormEvents = function () { + if (!SCORM_OK || SCORM_EVENTS_INITED) { + return; + } + SCORM_EVENTS_INITED = true; + + $(window).on('unload', function () { + finishScorm(); + }); + + setInterval(function () { + SCORM.save(); + }, 5000); +}; + +window.setSCORMLocation = function (location) { + return setScormValue('cmi.location', JSON.stringify(location)); +}; + +window.setSCORMScore = function (score, max, min) { + if (min === undefined) { + min = 0; + } + + setScormValue('cmi.core.score.raw', score); + setScormValue('cmi.core.score.min', min); + setScormValue('cmi.core.score.max', max); +}; +window.getScormValue = function (elementName) { + if (!SCORM_OK) { + return null; + } + var cmi = _cmi(elementName); + if (cmi == '') { + return null; + } + return result = SCORM.get(cmi); +}; + +window.setScormValue=function(elementName, value) { + if (!SCORM_OK) { + return; + } + var cmi = _cmi(elementName); + if (cmi == '') { + return false; + } + var result = SCORM.set(cmi, value); + return result; +}; + + +window.scormMarkAsComplete=function() { + if (!SCORM_OK) { + return; + } + scormExit(); +}; + + +window.finishScorm=function() { + if (!SCORM_OK) { + return; + } + setSessionTime(); + SCORM.save(); + SCORM.quit(); +}; + +window.scormExit = function () { + if (!SCORM_OK) { + return; + } + var v = 'suspend'; + setScormValue('exit', v); +} + +window.startScormTimer = function () { + SCORM_START_TIME = new Date(); +} + +window.scormCompleteAndClose = function () { + scormComplete(); + scormClose(); +} + +window.scormClose = function () { + parent.close(); + top.close(); + window.close(); +}; + + +window.setSessionTime = function () { + if (!SCORM_OK) { + return; + } + var currentTime = new Date(); + var sessionTime; + + var endTime = currentTime.getTime() + var calculatedTime = endTime - SCORM_START_TIME; + + if (SCORM.version == '1.2') { + var totalHours = Math.floor(calculatedTime / 1000 / 60 / 60); + calculatedTime = calculatedTime - totalHours * 1000 * 60 * 60 + if (totalHours < 1000 && totalHours > 99) { + totalHours = "0" + totalHours; + } else if (totalHours < 100 && totalHours > 9) { + totalHours = "00" + totalHours; + } else if (totalHours < 10) { + totalHours = "000" + totalHours; + } + + var totalMinutes = Math.floor(calculatedTime / 1000 / 60); + calculatedTime = calculatedTime - totalMinutes * 1000 * 60; + if (totalMinutes < 10) { + totalMinutes = "0" + totalMinutes; + } + + var totalSeconds = Math.floor(calculatedTime / 1000); + if (totalSeconds < 10) { + totalSeconds = "0" + totalSeconds; + } + sessionTime = totalHours + ":" + totalMinutes + ":" + totalSeconds; + setScormValue('session_time', sessionTime); + } else { + setScormValue('session_time', scormSecondsToTimeInterval(calculatedTime / 1000)); + } + +}; + +window.dateToScormTime = function (date) { + var res = date.toISOString(); + var e = res.split('.'); + e.pop(); + return e.join('.'); +}; + +window.getScormTimeInterval = function (start, end) { + var diff = Math.round((end.getTime() - start.getTime()) / 1000); + return scormSecondsToTimeInterval(diff); +}; + +window.scormSecondsToTimeInterval = function (diff) { + var diff = Math.round(diff); + var h = Math.floor(diff / 3600); + diff = diff % 3600; + var m = Math.floor(diff / 60); + var s = diff % 60; + return 'PT' + h + 'H' + m + 'M' + s + 'S'; +}; diff --git a/resources/views/vendor/backpack/crud/buttons/quiz/download.blade.php b/resources/views/vendor/backpack/crud/buttons/quiz/download.blade.php index 377cf9b89..91606b1e5 100644 --- a/resources/views/vendor/backpack/crud/buttons/quiz/download.blade.php +++ b/resources/views/vendor/backpack/crud/buttons/quiz/download.blade.php @@ -1,2 +1,51 @@ - {{__('Télécharger')}} +@once + @php + $showjs=false; + if($crud->getValue('seenExportJS')===null){ + $showjs =true; + $crud->setValue('seenExportJS',true); + } + @endphp + @if($showjs) + + + @endif +@endonce + + {{__('Exporter')}} + + -- 2.39.5