From: Vincent Vanwaelscappel Date: Tue, 28 Jun 2022 14:26:45 +0000 (+0200) Subject: wip #5319 @2 X-Git-Url: http://git.cubedesigners.com/?a=commitdiff_plain;h=c857c2c6db59d853211d7397339fdb2c3a8419c2;p=fluidbook-toolbox.git wip #5319 @2 --- 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/ImportOperation.php b/app/Http/Controllers/Admin/Operations/ImportOperation.php deleted file mode 100644 index 1ba768b49..000000000 --- a/app/Http/Controllers/Admin/Operations/ImportOperation.php +++ /dev/null @@ -1,164 +0,0 @@ -crud->addButtonFromView('top', 'import', 'quiz.import', 'end'); - } - - protected function import() - { - $files = $_FILES['file']['tmp_name']; - - if (!count($files)) { - Alert::warning('No file were imported')->flash(); - return; - } - - $default = ['title' => '', - 'translation' => '1', - 'scorm' => '1', - 'review' => 'always', - 'instantReview' => '1', - 'threshold' => '0', - 'owner' => auth()->user()->id]; - - foreach (Quiz::getColors() as $name => $color) { - $default[$name] = $color['default']; - } - - foreach (Quiz::getMessages() as $name => $message) { - $default[$name] = ''; - } - - - $j = 0; - foreach ($files as $file) { - $z = new ZipArchive(); - $ok = $z->open($file); - if ($ok !== true) { - Alert::warning('Unable to open ' . $file)->flash(); - continue; - } - - - $data = []; - $datacontent = $z->getFromName('data.xml'); - if (!$datacontent || stripos($datacontent, 'flash(); - continue; - } - $x = simplexml_load_string($datacontent, "SimpleXMLElement", LIBXML_NOERROR | LIBXML_ERR_NONE); - - // Discover translation - $validatetrans = $x->xpath('/quiz/translations/validateAnswer'); - if ($validatetrans) { - $validatetrans = (string)$validatetrans[0]; - $translation = QuizTranslation::where('validateAnswer', '=', $validatetrans)->first(); - if ($translation) { - $data['translation'] = $translation->id; - } - } - // Handle message from XML - foreach (Quiz::getMessages() as $name => $message) { - $e = $x->xpath('/quiz/' . $name); - if (!$e) { - continue; - } - $m = (string)$e[0]; - // We only define the message if different from the translation default - if (isset($translation) && $translation->$name !== $m) { - $data[$name] = $m; - } - } - - // Handle other attributes from XML - $attributes = ['title', 'review', 'thresehold']; - foreach (Quiz::getColors() as $name => $color) { - $attributes[] = $name; - } - foreach (Quiz::getActions() as $name => $action) { - $attributes[] = $name; - } - foreach ($attributes as $attribute => $xpath) { - if (is_int($attribute)) { - $attribute = $xpath; - $xpath = '/quiz/' . $xpath; - } - $e = $x->xpath($xpath); - if (!$e) { - continue; - } - $data[$attribute] = (string)$e[0]; - } - - // Handle Questions - $xquestions = $x->xpath('/quiz/questions/question'); - $questions = []; - foreach ($xquestions as $xquestion) { - $q = [ - 'type' => 'multiple', - 'count_for_score' => true, - 'report_label' => '', - 'placeholder' => '', - 'min_score' => 0, - 'question' => (string)$xquestion->label, - 'explaination' => (string)$xquestion->correction, - 'multiple' => isset($xquestion['multiple']) ? (bool)$xquestion['multiple'] : false, - 'answers' => [], - ]; - foreach ($xquestion->answers[0]->answer as $xanswer) { - $q['answers'][] = [ - 'answer' => (string)$xanswer, - 'correct' => isset($xanswer['correct']) ? (bool)$xanswer['correct'] : false, - ]; - } - $questions[] = $q; - - } - $data['questions'] = $questions; - $data = array_merge($default, $data); - - /** @var Quiz $q */ - $q = new Quiz(); - $q = $q->create($data); - - $temp = Files::tmpdir(); - $assets = ['logo' => 'logo.png', 'banner' => 'banner.jpg']; - foreach ($assets as $field => $asset) { - $f = $temp . '/' . $asset; - $c = $z->getFromName('assets/' . $asset); - if ($c) { - file_put_contents($f, $c); - $q->addMediaToField($field, $f); - } - } - $z->close(); - $j++; - } - - - if ($j === 0) { - Alert::warning('No quiz were imported')->flash(); - } else { - Alert::success('' . $j . ' quiz(zes) were imported')->flash(); - } - return redirect($this->crud->route); - } -} diff --git a/app/Http/Controllers/Admin/Operations/LogOperation.php b/app/Http/Controllers/Admin/Operations/LogOperation.php deleted file mode 100644 index 89d52093f..000000000 --- a/app/Http/Controllers/Admin/Operations/LogOperation.php +++ /dev/null @@ -1,34 +0,0 @@ -withoutMiddleware([VerifyCsrfToken::class, Authenticate::class, CheckIfAdmin::class]); - } - - protected function log($id) - { - $request = request(); - - $log = new QuizAttempt(); - $log->quiz = $id; - $log->score = $request->get('score'); - $log->passed = $request->get('passed') !== 'false' ? '1' : '0'; - $log->answers = json_encode($request->get('questions')); - $log->save(); - - return response()->json(['ok' => true])->header('Access-Control-Allow-Origin', '*'); - } -} diff --git a/app/Http/Controllers/Admin/Operations/PreviewOperation.php b/app/Http/Controllers/Admin/Operations/PreviewOperation.php deleted file mode 100644 index dab52cfb5..000000000 --- a/app/Http/Controllers/Admin/Operations/PreviewOperation.php +++ /dev/null @@ -1,33 +0,0 @@ -where(['id' => '[0-9]+', 'path' => '.*']); - } - - protected function setupPreviewDefaults() - { - $this->crud->addButtonFromView('line', 'open_preview', 'quiz.preview', 'begining'); - } - - protected function preview($id, $path = 'index.html') - { - $dest = protected_path('quiz/final/' . $id); - - if ($path === 'index.html') { - $entry = $this->crud->getEntry($id); - $entry->compile($dest); - } - - $p = $dest . '/' . $path; - return response(null)->header('Content-Type', Files::_getMimeType($p))->header('X-Sendfile', $p); - } -} 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/Quiz/ImportOperation.php b/app/Http/Controllers/Admin/Operations/Quiz/ImportOperation.php new file mode 100644 index 000000000..b476ccb28 --- /dev/null +++ b/app/Http/Controllers/Admin/Operations/Quiz/ImportOperation.php @@ -0,0 +1,164 @@ +crud->addButtonFromView('top', 'import', 'quiz.import', 'end'); + } + + protected function import() + { + $files = $_FILES['file']['tmp_name']; + + if (!count($files)) { + Alert::warning('No file were imported')->flash(); + return; + } + + $default = ['title' => '', + 'translation' => '1', + 'scorm' => '1', + 'review' => 'always', + 'instantReview' => '1', + 'threshold' => '0', + 'owner' => auth()->user()->id]; + + foreach (Quiz::getColors() as $name => $color) { + $default[$name] = $color['default']; + } + + foreach (Quiz::getMessages() as $name => $message) { + $default[$name] = ''; + } + + + $j = 0; + foreach ($files as $file) { + $z = new ZipArchive(); + $ok = $z->open($file); + if ($ok !== true) { + Alert::warning('Unable to open ' . $file)->flash(); + continue; + } + + + $data = []; + $datacontent = $z->getFromName('data.xml'); + if (!$datacontent || stripos($datacontent, 'flash(); + continue; + } + $x = simplexml_load_string($datacontent, "SimpleXMLElement", LIBXML_NOERROR | LIBXML_ERR_NONE); + + // Discover translation + $validatetrans = $x->xpath('/quiz/translations/validateAnswer'); + if ($validatetrans) { + $validatetrans = (string)$validatetrans[0]; + $translation = QuizTranslation::where('validateAnswer', '=', $validatetrans)->first(); + if ($translation) { + $data['translation'] = $translation->id; + } + } + // Handle message from XML + foreach (Quiz::getMessages() as $name => $message) { + $e = $x->xpath('/quiz/' . $name); + if (!$e) { + continue; + } + $m = (string)$e[0]; + // We only define the message if different from the translation default + if (isset($translation) && $translation->$name !== $m) { + $data[$name] = $m; + } + } + + // Handle other attributes from XML + $attributes = ['title', 'review', 'thresehold']; + foreach (Quiz::getColors() as $name => $color) { + $attributes[] = $name; + } + foreach (Quiz::getActions() as $name => $action) { + $attributes[] = $name; + } + foreach ($attributes as $attribute => $xpath) { + if (is_int($attribute)) { + $attribute = $xpath; + $xpath = '/quiz/' . $xpath; + } + $e = $x->xpath($xpath); + if (!$e) { + continue; + } + $data[$attribute] = (string)$e[0]; + } + + // Handle Questions + $xquestions = $x->xpath('/quiz/questions/question'); + $questions = []; + foreach ($xquestions as $xquestion) { + $q = [ + 'type' => 'multiple', + 'count_for_score' => true, + 'report_label' => '', + 'placeholder' => '', + 'min_score' => 0, + 'question' => (string)$xquestion->label, + 'explaination' => (string)$xquestion->correction, + 'multiple' => isset($xquestion['multiple']) ? (bool)$xquestion['multiple'] : false, + 'answers' => [], + ]; + foreach ($xquestion->answers[0]->answer as $xanswer) { + $q['answers'][] = [ + 'answer' => (string)$xanswer, + 'correct' => isset($xanswer['correct']) ? (bool)$xanswer['correct'] : false, + ]; + } + $questions[] = $q; + + } + $data['questions'] = $questions; + $data = array_merge($default, $data); + + /** @var Quiz $q */ + $q = new Quiz(); + $q = $q->create($data); + + $temp = Files::tmpdir(); + $assets = ['logo' => 'logo.png', 'banner' => 'banner.jpg']; + foreach ($assets as $field => $asset) { + $f = $temp . '/' . $asset; + $c = $z->getFromName('assets/' . $asset); + if ($c) { + file_put_contents($f, $c); + $q->addMediaToField($field, $f); + } + } + $z->close(); + $j++; + } + + + if ($j === 0) { + Alert::warning('No quiz were imported')->flash(); + } else { + Alert::success('' . $j . ' quiz(zes) were imported')->flash(); + } + return redirect($this->crud->route); + } +} diff --git a/app/Http/Controllers/Admin/Operations/Quiz/LogOperation.php b/app/Http/Controllers/Admin/Operations/Quiz/LogOperation.php new file mode 100644 index 000000000..fba1210d9 --- /dev/null +++ b/app/Http/Controllers/Admin/Operations/Quiz/LogOperation.php @@ -0,0 +1,32 @@ +withoutMiddleware([VerifyCsrfToken::class, Authenticate::class, CheckIfAdmin::class]); + } + + protected function log($id) + { + $request = request(); + + $log = new QuizAttempt(); + $log->quiz = $id; + $log->score = $request->get('score'); + $log->passed = $request->get('passed') !== 'false' ? '1' : '0'; + $log->answers = json_encode($request->get('questions')); + $log->save(); + + return response()->json(['ok' => true])->header('Access-Control-Allow-Origin', '*'); + } +} diff --git a/app/Http/Controllers/Admin/Operations/Quiz/PreviewOperation.php b/app/Http/Controllers/Admin/Operations/Quiz/PreviewOperation.php new file mode 100644 index 000000000..72fef6f96 --- /dev/null +++ b/app/Http/Controllers/Admin/Operations/Quiz/PreviewOperation.php @@ -0,0 +1,33 @@ +where(['id' => '[0-9]+', 'path' => '.*']); + } + + protected function setupPreviewDefaults() + { + $this->crud->addButtonFromView('line', 'open_preview', 'quiz.preview', 'begining'); + } + + protected function preview($id, $path = 'index.html') + { + $dest = protected_path('quiz/final/' . $id); + + if ($path === 'index.html') { + $entry = $this->crud->getEntry($id); + $entry->compile($dest); + } + + $p = $dest . '/' . $path; + return response(null)->header('Content-Type', Files::_getMimeType($p))->header('X-Sendfile', $p); + } +} diff --git a/app/Http/Controllers/Admin/Operations/Quiz/ReportOperation.php b/app/Http/Controllers/Admin/Operations/Quiz/ReportOperation.php new file mode 100644 index 000000000..2dd68feb4 --- /dev/null +++ b/app/Http/Controllers/Admin/Operations/Quiz/ReportOperation.php @@ -0,0 +1,129 @@ +crud->addButtonFromView('line', 'report', 'quiz.report', 'end'); + } + + protected function report($id) + { + $quiz = Quiz::where('id', $id)->first()->getPageData(); + $first = ['#', 'Date']; + if ($quiz->display_score) { + $first = array_merge($first, ['Score', 'Passed']); + } + + $emailQuestion = false; + $countForScore = []; + + foreach ($quiz->questions as $q => $question) { + $label = $question['report_label'] ?: $question['question']; + $first[] = $label; + if ($quiz->display_score && $question['count_for_score']) { + $first[] = $label . ' status'; + $countForScore[] = $q; + } + if ($question['type'] === 'email') { + $emailQuestion = $q; + } + } + + $attemptsList = [$first]; + $users = []; + /** @var QuizAttempt[] $attempts */ + $attempts = QuizAttempt::where('quiz', $id)->orderBy('created_at', 'ASC')->get(); + foreach ($attempts as $attempt) { + $email = $attempt->id; + $data = $attempt->getPageData(); + $a = [$data->get('id'), $data->get('created_at')]; + if ($quiz->display_score) { + $a[] = $data->get('score'); + $a[] = ($data->get('passed') ? '1' : '0'); + } + $answers = $data->get('answers', []); + + if (null === $answers || !is_array($answers)) { + continue; + } + foreach ($answers as $aid => $answer) { + if (null === $answer) { + continue; + } + $aa = $answer['anwser'] ?? $answer['answer'] ?? ''; + if ($emailQuestion !== false) { + if ($aid == $emailQuestion + 1) { + $email = trim(mb_strtolower($aa)); + } + } + $a[] = is_array($aa) ? implode(', ', $aa) : $aa; + if ($quiz->display_score && in_array($aid - 1, $countForScore, true)) { + $a[] = $answer['score']; + } + } + + if ($emailQuestion !== false) { + if (!isset($users[$email])) { + $users[$email] = ['totalAttempts' => 0, 'attemptsBeforePassing' => 0, 'passed' => false, 'worstScore' => 100, 'bestScore' => 0]; + } + $users[$email]['totalAttempts']++; + $users[$email]['worstScore'] = min($users[$email]['worstScore'], $data->get('score')); + $users[$email]['bestScore'] = max($users[$email]['bestScore'], $data->get('score')); + if (!$users[$email]['passed']) { + $users[$email]['attemptsBeforePassing']++; + } + if ($data->get('passed')) { + $users[$email]['passed'] = true; + } + } + + $attemptsList[] = $a; + } + + + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + if ($emailQuestion !== false) { + $usersList = [['Email', 'Passed', 'Attempts before passed', 'Total attempts', 'Best score', 'Worst score']]; + foreach ($users as $email => $user) { + $usersList[] = [$email, $user['passed'] ? '1' : '0', $user['attemptsBeforePassing'], $user['totalAttempts'], $user['bestScore'], $user['worstScore']]; + } + + $sheet->setTitle('USERS'); + $sheet->fromArray($usersList); + foreach (range('A', 'Z') as $columnID) { + $sheet->getColumnDimension($columnID)->setAutoSize(true); + } + $sheet = $spreadsheet->createSheet(); + } + + array_reverse($attemptsList); + $sheet->setTitle('DATA'); + $sheet->fromArray($attemptsList); + foreach (range('A', 'Z') as $columnID) { + $sheet->getColumnDimension($columnID)->setAutoSize(true); + } + + $writer = new Xlsx($spreadsheet); + $tmp = Files::tempnam(); + $writer->save($tmp); + + return response()->download($tmp, 'report_' . $id . '_' . date('YmdHi') . '.xlsx')->deleteFileAfterSend(); + } +} diff --git a/app/Http/Controllers/Admin/Operations/ReportOperation.php b/app/Http/Controllers/Admin/Operations/ReportOperation.php deleted file mode 100644 index a42f5db6c..000000000 --- a/app/Http/Controllers/Admin/Operations/ReportOperation.php +++ /dev/null @@ -1,129 +0,0 @@ -crud->addButtonFromView('line', 'report', 'quiz.report', 'end'); - } - - protected function report($id) - { - $quiz = Quiz::where('id', $id)->first()->getPageData(); - $first = ['#', 'Date']; - if ($quiz->display_score) { - $first = array_merge($first, ['Score', 'Passed']); - } - - $emailQuestion = false; - $countForScore = []; - - foreach ($quiz->questions as $q => $question) { - $label = $question['report_label'] ?: $question['question']; - $first[] = $label; - if ($quiz->display_score && $question['count_for_score']) { - $first[] = $label . ' status'; - $countForScore[] = $q; - } - if ($question['type'] === 'email') { - $emailQuestion = $q; - } - } - - $attemptsList = [$first]; - $users = []; - /** @var QuizAttempt[] $attempts */ - $attempts = QuizAttempt::where('quiz', $id)->orderBy('created_at', 'ASC')->get(); - foreach ($attempts as $attempt) { - $email = $attempt->id; - $data = $attempt->getPageData(); - $a = [$data->get('id'), $data->get('created_at')]; - if ($quiz->display_score) { - $a[] = $data->get('score'); - $a[] = ($data->get('passed') ? '1' : '0'); - } - $answers = $data->get('answers', []); - - if (null === $answers || !is_array($answers)) { - continue; - } - foreach ($answers as $aid => $answer) { - if (null === $answer) { - continue; - } - $aa = $answer['anwser'] ?? $answer['answer'] ?? ''; - if ($emailQuestion !== false) { - if ($aid == $emailQuestion + 1) { - $email = trim(mb_strtolower($aa)); - } - } - $a[] = is_array($aa) ? implode(', ', $aa) : $aa; - if ($quiz->display_score && in_array($aid - 1, $countForScore, true)) { - $a[] = $answer['score']; - } - } - - if ($emailQuestion !== false) { - if (!isset($users[$email])) { - $users[$email] = ['totalAttempts' => 0, 'attemptsBeforePassing' => 0, 'passed' => false, 'worstScore' => 100, 'bestScore' => 0]; - } - $users[$email]['totalAttempts']++; - $users[$email]['worstScore'] = min($users[$email]['worstScore'], $data->get('score')); - $users[$email]['bestScore'] = max($users[$email]['bestScore'], $data->get('score')); - if (!$users[$email]['passed']) { - $users[$email]['attemptsBeforePassing']++; - } - if ($data->get('passed')) { - $users[$email]['passed'] = true; - } - } - - $attemptsList[] = $a; - } - - - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - - if ($emailQuestion !== false) { - $usersList = [['Email', 'Passed', 'Attempts before passed', 'Total attempts', 'Best score', 'Worst score']]; - foreach ($users as $email => $user) { - $usersList[] = [$email, $user['passed'] ? '1' : '0', $user['attemptsBeforePassing'], $user['totalAttempts'], $user['bestScore'], $user['worstScore']]; - } - - $sheet->setTitle('USERS'); - $sheet->fromArray($usersList); - foreach (range('A', 'Z') as $columnID) { - $sheet->getColumnDimension($columnID)->setAutoSize(true); - } - $sheet = $spreadsheet->createSheet(); - } - - array_reverse($attemptsList); - $sheet->setTitle('DATA'); - $sheet->fromArray($attemptsList); - foreach (range('A', 'Z') as $columnID) { - $sheet->getColumnDimension($columnID)->setAutoSize(true); - } - - $writer = new Xlsx($spreadsheet); - $tmp = Files::tempnam(); - $writer->save($tmp); - - return response()->download($tmp, 'report_' . $id . '_' . date('YmdHi') . '.xlsx')->deleteFileAfterSend(); - } -} diff --git a/app/Http/Controllers/Admin/PageCrudController.php b/app/Http/Controllers/Admin/PageCrudController.php new file mode 100644 index 000000000..1f77d06ae --- /dev/null +++ b/app/Http/Controllers/Admin/PageCrudController.php @@ -0,0 +1,29 @@ +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/fonts/Roboto-Bold-webfont.woff b/resources/quiz/fonts/Roboto-Bold-webfont.woff new file mode 100644 index 000000000..8c9b02410 Binary files /dev/null and b/resources/quiz/fonts/Roboto-Bold-webfont.woff differ diff --git a/resources/quiz/fonts/Roboto-Light-webfont.woff b/resources/quiz/fonts/Roboto-Light-webfont.woff new file mode 100644 index 000000000..983083c7f Binary files /dev/null and b/resources/quiz/fonts/Roboto-Light-webfont.woff differ diff --git a/resources/quiz/fonts/Roboto-Regular-webfont.woff b/resources/quiz/fonts/Roboto-Regular-webfont.woff new file mode 100644 index 000000000..7245f5cae Binary files /dev/null and b/resources/quiz/fonts/Roboto-Regular-webfont.woff differ diff --git a/resources/quiz/fonts/RobotoCondensed-Bold-webfont.woff b/resources/quiz/fonts/RobotoCondensed-Bold-webfont.woff new file mode 100644 index 000000000..f56461221 Binary files /dev/null and b/resources/quiz/fonts/RobotoCondensed-Bold-webfont.woff differ diff --git a/resources/quiz/fonts/RobotoCondensed-Light-webfont.woff b/resources/quiz/fonts/RobotoCondensed-Light-webfont.woff new file mode 100644 index 000000000..6a8ae661a Binary files /dev/null and b/resources/quiz/fonts/RobotoCondensed-Light-webfont.woff differ diff --git a/resources/quiz/fonts/RobotoCondensed-Regular-webfont.woff b/resources/quiz/fonts/RobotoCondensed-Regular-webfont.woff new file mode 100644 index 000000000..dd61f2414 Binary files /dev/null and b/resources/quiz/fonts/RobotoCondensed-Regular-webfont.woff differ diff --git a/resources/quiz/fonts/fontawesome-webfont.woff b/resources/quiz/fonts/fontawesome-webfont.woff new file mode 100644 index 000000000..400014a4b Binary files /dev/null and b/resources/quiz/fonts/fontawesome-webfont.woff differ diff --git a/resources/quiz/fonts/fontawesome-webfont.woff2 b/resources/quiz/fonts/fontawesome-webfont.woff2 new file mode 100644 index 000000000..4d13fc604 Binary files /dev/null and b/resources/quiz/fonts/fontawesome-webfont.woff2 differ 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/quiz/style/fonts/Roboto-Bold-webfont.woff b/resources/quiz/style/fonts/Roboto-Bold-webfont.woff deleted file mode 100644 index 8c9b02410..000000000 Binary files a/resources/quiz/style/fonts/Roboto-Bold-webfont.woff and /dev/null differ diff --git a/resources/quiz/style/fonts/Roboto-Light-webfont.woff b/resources/quiz/style/fonts/Roboto-Light-webfont.woff deleted file mode 100644 index 983083c7f..000000000 Binary files a/resources/quiz/style/fonts/Roboto-Light-webfont.woff and /dev/null differ diff --git a/resources/quiz/style/fonts/Roboto-Regular-webfont.woff b/resources/quiz/style/fonts/Roboto-Regular-webfont.woff deleted file mode 100644 index 7245f5cae..000000000 Binary files a/resources/quiz/style/fonts/Roboto-Regular-webfont.woff and /dev/null differ diff --git a/resources/quiz/style/fonts/RobotoCondensed-Bold-webfont.woff b/resources/quiz/style/fonts/RobotoCondensed-Bold-webfont.woff deleted file mode 100644 index f56461221..000000000 Binary files a/resources/quiz/style/fonts/RobotoCondensed-Bold-webfont.woff and /dev/null differ diff --git a/resources/quiz/style/fonts/RobotoCondensed-Light-webfont.woff b/resources/quiz/style/fonts/RobotoCondensed-Light-webfont.woff deleted file mode 100644 index 6a8ae661a..000000000 Binary files a/resources/quiz/style/fonts/RobotoCondensed-Light-webfont.woff and /dev/null differ diff --git a/resources/quiz/style/fonts/RobotoCondensed-Regular-webfont.woff b/resources/quiz/style/fonts/RobotoCondensed-Regular-webfont.woff deleted file mode 100644 index dd61f2414..000000000 Binary files a/resources/quiz/style/fonts/RobotoCondensed-Regular-webfont.woff and /dev/null differ diff --git a/resources/quiz/style/fonts/fontawesome-webfont.woff b/resources/quiz/style/fonts/fontawesome-webfont.woff deleted file mode 100644 index 400014a4b..000000000 Binary files a/resources/quiz/style/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/resources/quiz/style/fonts/fontawesome-webfont.woff2 b/resources/quiz/style/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 4d13fc604..000000000 Binary files a/resources/quiz/style/fonts/fontawesome-webfont.woff2 and /dev/null differ 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')}} + +