From f26137d9fe055623e869ea50d0c0d6e0fe61c37f Mon Sep 17 00:00:00 2001 From: Thomas Hackl <hackl@data-quest.de> Date: Fri, 1 Dec 2023 12:15:33 +0000 Subject: [PATCH] Resolve "Erweiterung der Courseware-Zertifikate" Closes #3319 Merge request studip/studip!2269 --- .../5.5.9_extend_cw_certificates.php | 46 +++++ lib/classes/CoursewarePDFCertificate.php | 23 ++- lib/classes/JsonApi/RouteMap.php | 4 + .../Routes/Courseware/CertificateShow.php | 67 +++++++ lib/cronjobs/courseware.php | 166 ++++++++++++------ lib/models/Courseware/Certificate.php | 37 +++- lib/models/Courseware/Instance.php | 7 + .../assets/stylesheets/scss/courseware.scss | 2 +- .../scss/courseware/layouts/tile.scss | 3 +- .../courseware/unit/CoursewareUnitItem.vue | 35 +++- .../unit/CoursewareUnitItemDialogSettings.vue | 86 +++++---- templates/courseware/mails/certificate.php | 34 ++-- templates/layouts/base.php | 1 + 13 files changed, 391 insertions(+), 120 deletions(-) create mode 100644 db/migrations/5.5.9_extend_cw_certificates.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/CertificateShow.php diff --git a/db/migrations/5.5.9_extend_cw_certificates.php b/db/migrations/5.5.9_extend_cw_certificates.php new file mode 100644 index 00000000000..ed05beb77da --- /dev/null +++ b/db/migrations/5.5.9_extend_cw_certificates.php @@ -0,0 +1,46 @@ +<?php + +final class ExtendCwCertificates extends Migration +{ + use DatabaseMigrationTrait; + + public function description() + { + return 'Provide global config entry for Courseware certificates and add a fileref_id to the ' . + 'cw_certificates table to track which certificate was generated'; + } + + protected function up() + { + // Create global config entry for (de-)activating Courseware certificate and reminder functionality. + DBManager::get()->execute("INSERT IGNORE INTO `config` + (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) + VALUES + (:field, :value, :type, 'global', '', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), :description)", + [ + 'field' => 'COURSEWARE_CERTIFICATES_ENABLE', + 'value' => 1, + 'type' => 'boolean', + 'description' => 'Schaltet Courseware-Zertifikate, -Erinnerungen und -Fortschrittsrücksetzung ein oder aus' + ] + ); + + if (!$this->columnExists('cw_certificates', 'fileref_id')) { + DBManager::get()->execute( + "ALTER TABLE `cw_certificates` ADD `fileref_id` CHAR(32) NULL DEFAULT NULL COLLATE latin1_bin AFTER `unit_id`" + ); + } + } + + protected function down() + { + if ($this->columnExists('cw_certificates', 'fileref_id')) { + DBManager::get()->execute("ALTER TABLE `cw_certificates` DROP `fileref_id`"); + } + + DBManager::get()->execute("DELETE FROM `config_values` WHERE `field` = :field", + ['field' => 'COURSEWARE_CERTIFICATES_ENABLE']); + DBManager::get()->execute("DELETE FROM `config` WHERE `field` = :field", + ['field' => 'COURSEWARE_CERTIFICATES_ENABLE']); + } +} diff --git a/lib/classes/CoursewarePDFCertificate.php b/lib/classes/CoursewarePDFCertificate.php index ca6e704c27b..b70c2bea99f 100644 --- a/lib/classes/CoursewarePDFCertificate.php +++ b/lib/classes/CoursewarePDFCertificate.php @@ -3,19 +3,25 @@ class CoursewarePDFCertificate extends TCPDF { protected $background; + protected $isCustomBackground = false; public function __construct($background = false, $orientation = 'P', $unit = 'mm', $format = 'A4', $unicode = true, $encoding = 'UTF-8') { parent::__construct($orientation, $unit, $format, $unicode, $encoding, false); - if ($background) { + $fileRef = null; + if ($background !== false) { $fileRef = FileRef::find($background); - $this->background = $fileRef->file->getPath(); - } else { - $this->background = $GLOBALS['STUDIP_BASE_PATH'] . '/public/assets/images/pdf/pdf_default_background.jpg'; } + $this->background = $fileRef !== null ? $fileRef->file->getPath() : + $GLOBALS['STUDIP_BASE_PATH'] . '/public/assets/images/pdf/pdf_default_background.jpg'; + $this->isCustomBackground = $fileRef !== null; $this->setDefaults(); + + $fontname = TCPDF_FONTS::addTTFfont( + $GLOBALS['STUDIP_BASE_PATH'] . '/public/assets/fonts/LatoLatin/LatoLatin-Regular.ttf'); + $this->setFont($fontname, '', 50); } public function Header() @@ -23,16 +29,17 @@ class CoursewarePDFCertificate extends TCPDF $bMargin = $this->getBreakMargin(); $auto_page_break = $this->AutoPageBreak; $this->SetAutoPageBreak(false, 0); - $this->Image($this->background, 0, 0, 210, 297, '', '', '', false, 300, '', false, false, 0); - $this->setFont('helvetica', 'B', 50); - $this->Cell(0, 160, 'Z E R T I F I K A T', 0, false, 'C', 0, '', 0, false, 'T', 'M'); + list($width, $height) = getimagesize($this->background); + $this->Image($this->background, $this->isCustomBackground ? 10 : 0, $this->isCustomBackground ? 10 : 0, + min($this->getPageWidth(), $width / 10), min($this->getPageHeight(), $height / 10), + '', '', '', false, 300, '', false, false, 0); $this->SetAutoPageBreak($auto_page_break, $bMargin); $this->setPageMark(); } private function setDefaults() { - $this->SetTopMargin(110); + $this->SetTopMargin(50); $this->SetLeftMargin(20); $this->SetRightMargin(20); } diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index 5542c31a4cb..e86617bcfbf 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -470,6 +470,10 @@ class RouteMap $group->get('/courseware-units/{id}/courseware-user-progresses', Routes\Courseware\UserProgressesOfUnitsShow::class); $group->patch('/courseware-user-progresses/{id}', Routes\Courseware\UserProgressesUpdate::class); + // not a JSON route + $group->get('/courseware-units/{id}/certificate', Routes\Courseware\CertificateShow::class); + $group->get('/courseware-units/{id}/certificate/{user}', Routes\Courseware\CertificateShow::class); + $group->get('/courseware-blocks/{id}/comments', Routes\Courseware\BlockCommentsOfBlocksIndex::class); $group->post('/courseware-block-comments', Routes\Courseware\BlockCommentsCreate::class); $group->get('/courseware-block-comments/{id}', Routes\Courseware\BlockCommentsShow::class); diff --git a/lib/classes/JsonApi/Routes/Courseware/CertificateShow.php b/lib/classes/JsonApi/Routes/Courseware/CertificateShow.php new file mode 100644 index 00000000000..4a88b9b49a4 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/CertificateShow.php @@ -0,0 +1,67 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\Errors\UnsupportedRequestError; +use JsonApi\NonJsonApiController; +use Courseware\Unit; +use Courseware\Certificate; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Displays a certificate for a given courseware. + */ +class CertificateShow extends NonJsonApiController +{ + protected $allowedIncludePaths = []; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + if (!\Config::get()->COURSEWARE_CERTIFICATES_ENABLE) { + throw new UnsupportedRequestError(); + } + + $unit = Unit::find($args['id']); + if (!$unit) { + throw new RecordNotFoundException('Unit could not be found'); + } + + $user = null; + if (isset($args['user'])) { + $user = \User::find($args['user']); + if (!$user) { + throw new RecordNotFoundException('User could not be found'); + } + } + + $config = $unit->config; + + // No user given: create a preview PDF certificate + if (!$user) { + $file = Certificate::createPDF($unit, time(), null, $config['certificate']['image'] ?? ''); + + $response->getBody()->write(file_get_contents($file)); + + return $response->withHeader('Content-type', 'application/pdf'); + // User ID given: check if a certificate exists for the given unit and output the file ID. + } else { + $certificate = Certificate::findOneBySQL( + "`unit_id` = :unit AND `user_id` = :user", + ['unit' => $unit->id, 'user' => $user->id] + ); + if (!$certificate) { + throw new RecordNotFoundException(); + } + + $response->getBody()->write($certificate->fileref_id); + + return $response->withHeader('Content-type', 'text/plain'); + } + } +} diff --git a/lib/cronjobs/courseware.php b/lib/cronjobs/courseware.php index d0eb491bd04..38c9807574a 100644 --- a/lib/cronjobs/courseware.php +++ b/lib/cronjobs/courseware.php @@ -40,13 +40,16 @@ class CoursewareCronjob extends CronJob { $verbose = $parameters['verbose']; - /* - * Fetch all units that have some relevant settings. - */ - $todo = Courseware\Unit::findBySQL( - "`range_type` = 'course' AND (`config` LIKE (:cert) OR `config` LIKE (:reminder) OR `config` LIKE (:reset))", - ['cert' => '%"certificate":%', 'reminder' => '%"reminder":%', 'reset' => '%"reset_progress":%'] - ); + $todo = []; + if (Config::get()->COURSEWARE_CERTIFICATES_ENABLE) { + /* + * Fetch all units that have some relevant settings. + */ + $todo = Courseware\Unit::findBySQL( + "`range_type` = 'course' AND (`config` LIKE (:cert) OR `config` LIKE (:reminder) OR `config` LIKE (:reset))", + ['cert' => '%"certificate":%', 'reminder' => '%"reminder":%', 'reset' => '%"reset_progress":%'] + ); + } if (count($todo) > 0) { @@ -72,7 +75,7 @@ class CoursewareCronjob extends CronJob // Fetch accumulated progress values for all users in this course. $progresses = DBManager::get()->fetchAll( - "SELECT DISTINCT p.`user_id`, SUM(p.`grade`) AS progress + "SELECT DISTINCT p.`user_id`, SUM(p.`grade`) AS progress, MAX(p.`chdate`) AS pdate FROM `cw_user_progresses` p WHERE `block_id` IN (:blocks) AND NOT EXISTS ( @@ -92,8 +95,8 @@ class CoursewareCronjob extends CronJob $progress['user_id'], $unit->range_id, $unit->id); } - if (!$this->sendCertificate($unit, $progress['user_id'], $percent, - $unit->config['certificate']['image'])) { + if (!$this->sendCertificate($unit, $progress['user_id'], $percent, $progress['pdate'], + $unit->config['certificate']['image'])) { printf("Could not send certificate for course %s and unit %u to user %s.\n", $unit->range_id, $unit->id, $progress['user_id']); } @@ -218,56 +221,56 @@ class CoursewareCronjob extends CronJob } } - private function sendCertificate($unit, $user_id, $progress, $image = '') + /** + * @param Courseware\Unit $unit + * @param string $user_id + * @param int $progress + * @param int $timestamp + * @param string|null $image + * @return bool|int|number + * @throws Exception + */ + private function sendCertificate(Courseware\Unit $unit, string $user_id, int $progress, int $timestamp, string $image = null) { $user = User::find($user_id); $course = Course::find($unit->range_id); - setTempLanguage('', $user->preferred_language); - - $template = $GLOBALS['template_factory']->open('courseware/mails/certificate'); - $html = $template->render( - compact('user', 'course') - ); + $pdf = Courseware\Certificate::createPDF($unit, $timestamp, $user, $image); - // Generate the PDF. - $pdf = new CoursewarePDFCertificate($image); - $pdf->AddPage(); - $pdf->writeHTML($html, true, false, true, false, ''); - $pdf_file_name = $user->nachname . '_' . $course->name . '_' . _('Zertifikat') . '.pdf'; - $filename = $GLOBALS['TMP_PATH'] . '/' . $pdf_file_name; - $pdf->Output($filename, 'F'); + $folder = $this->requireCertificateFolder($unit); + $data = [ + 'name'=> $user->getFullname('full') . '-' . date('ymd') . '.pdf', + 'tmp_name'=> $pdf, + 'type' => 'application/pdf', + 'size' => @filesize($pdf) + ]; + $file = $folder->addFile(StandardFile::create($data), $user->id); + @unlink($pdf); - // Send the mail with PDF attached. - $mail = new StudipMail(); + setTempLanguage('', $user->preferred_language); + // Send the message containing a link to the PDF certificate. + $subject = _('Courseware: Zertifikat') . ' - ' . $course->getFullname() . + ' (' . $unit->structural_element->title . ')'; $message = sprintf( - _('Anbei erhalten Sie Ihr Courseware-Zertifikat zur Veranstaltung %1$s, in der Sie einen Fortschritt ' . - 'von %2$u %% im Lernmaterial "%s" erreicht haben.'), - $course->getFullname(), $progress, $unit->structural_element->title); - $message .= "\n\n" . _('Über folgenden Link kommen Sie direkt zur Courseware') . ': ' . - URLHelper::getURL('dispatch.php/course/courseware/courseware/' . $unit->id, ['cid' => $course->id]); - - $sent = $mail->addRecipient($user->email, $user->getFullname()) - ->setSubject(_('Courseware: Zertifikat') . ' - ' . $course->getFullname()) - ->setBodyText($message) - ->addFileAttachment($filename, $pdf_file_name) - ->send(); + _('Sie haben einen Fortschritt von %1$u % % im Lernmaterial "%2$s" erreicht und können daher Ihr ' . + '[Zertifikat herunterladen]%3$s .'), + $progress, $unit->structural_element->title, $file->getDownloadURL()); + $message .= "\n\n" . sprintf(_('Sie können das Lernmaterial [direkt hier aufrufen]%s .'), + URLHelper::getURL('dispatch.php/course/courseware/courseware/' . $unit->id, ['cid' => $course->id])); + ; - @unlink($filename); + messaging::sendSystemMessage($user, $subject, $message); restoreLanguage(); // Add database entry for the certificate. - if ($sent) { - $cert = new Courseware\Certificate(); - $cert->user_id = $user_id; - $cert->course_id = $course->id; - $cert->unit_id = $unit->id; - return $cert->store(); - } else { - return false; - } + $cert = new Courseware\Certificate(); + $cert->user_id = $user_id; + $cert->course_id = $course->id; + $cert->unit_id = $unit->id; + $cert->fileref_id = $file->id; + return $cert->store(); } private function sendReminders($unit) @@ -286,8 +289,10 @@ class CoursewareCronjob extends CronJob ); } - $message = $unit->config['reminder']['mailText'] . "\n\n" . _('Über folgenden Link kommen Sie direkt zur Courseware') . ': ' . - URLHelper::getURL('dispatch.php/course/courseware/courseware/' . $unit->id, ['cid' => $course->id]); + $message = $unit->config['reminder']['mailText'] . "\n\n" . + sprintf(_('Sie können das Lernmaterial [direkt hier aufrufen]%s .'), + URLHelper::getURL('dispatch.php/course/courseware/courseware/' . $unit->id, ['cid' => $course->id])); + ; $mail->setSubject(_('Courseware: Erinnerung') . ' - ' . $course->getFullname() . ', ' . $unit->structural_element->title) @@ -305,6 +310,12 @@ class CoursewareCronjob extends CronJob ['blocks' => $block_ids] ); + /* + * If certificates are active, remove all existing entries for this unit. + * Note that existing PDF files will stay in their course folders. + */ + Courseware\Certificate::deleteByUnit_id($unit->id); + $recipients = $course->getMembersWithStatus('autor', true); $mail = new StudipMail(); @@ -318,12 +329,65 @@ class CoursewareCronjob extends CronJob } $message = $unit->config['reset_progress']['mailText'] . "\n\n" . - _('Über folgenden Link kommen Sie direkt zur Courseware') . ': ' . - URLHelper::getURL('dispatch.php/course/courseware/courseware/' . $unit->id, ['cid' => $course->id]); + sprintf(_('Sie können das Lernmaterial [direkt hier aufrufen]%s .'), + URLHelper::getURL('dispatch.php/course/courseware/courseware/' . $unit->id, ['cid' => $course->id])); $mail->setSubject(_('Courseware: Fortschritt zurückgesetzt') . ' - ' . $course->getFullname()) ->setBodyText($message); return $mail->send(); } + + /** + * Create or fetch the folder where certificates shall be put in this course. + * @param Courseware\Unit $unit + * @return FolderType + */ + private function requireCertificateFolder(Courseware\Unit $unit) + { + // Try to find existing unit folder in database. + $unitFolder = Folder::findOneBySQL( + "`range_id` = :range AND `data_content` = :unit", + ['range' => $unit->range_id, 'unit' => json_encode(['unit_id' => $unit->id, 'download_allowed' => 1])] + ); + + // We need to create a new folder for this unit. + if (!$unitFolder) { + // Try to find existing certificate folder in database. + $certFolder = Folder::findOneBySQL( + "`range_id` = :range AND `data_content` = :data", + [ + 'range' => $unit->range_id, + 'data' => json_encode(['purpose' => 'cw-certificates', 'download_allowed' => 1]) + ] + ); + + // Create parent folder, collecting all certificates for all units of this course. + if (!$certFolder) { + $certFolder = FileManager::createSubFolder( + Folder::findTopFolder($unit->range_id)->getTypedFolder(), + User::findCurrent(), + 'HiddenFolder', + _('Courseware-Zertifikate'), + _('Erteilte Zertifikate für den Fortschritt in Courseware-Inhalten dieser Veranstaltung ') + ); + $certFolder->data_content = json_encode(['purpose' => 'cw-certificates', 'download_allowed' => 1]); + $certFolder->store(); + } + + // General folder for certificates exists now, create the subfolder for this unit. + $unitFolder = FileManager::createSubFolder( + is_a($certFolder, FolderType::class) ? $certFolder : $certFolder->getTypedFolder(), + User::findCurrent(), + 'HiddenFolder', + $unit->structural_element->title, + sprintf(_('Zertifikate für Lernmaterial %u'), $unit->id) + ); + $unitFolder->data_content = json_encode(['unit_id' => $unit->id, 'download_allowed' => 1]); + $unitFolder->store(); + } + + return is_a($unitFolder, FolderType::class) ? $unitFolder : $unitFolder->getTypedFolder(); + } + } diff --git a/lib/models/Courseware/Certificate.php b/lib/models/Courseware/Certificate.php index 579c729d4be..2948a9cfc94 100644 --- a/lib/models/Courseware/Certificate.php +++ b/lib/models/Courseware/Certificate.php @@ -2,7 +2,7 @@ namespace Courseware; -use \User, \Course; +use \User, \Course, \CoursewarePDFCertificate; /** * Courseware's certificates. @@ -22,6 +22,40 @@ use \User, \Course; */ class Certificate extends \SimpleORMap { + /** + * Generates a PDF certificate for + * @param Courseeware\Unit $unit + * @param User|null $user + * @param int $timestamp timestamp that shall be used as certificate date + * @param string|null $image optional background image fileref ID + * @return string Full path to the generated PDF file + */ + public static function createPDF(Unit $unit, int $timestamp, ?\User $user = null, $image = null) + { + if ($user === null) { + $user = new User(); + $user->vorname = 'Vorname'; + $user->nachname = 'Nachname'; + $user->geschlecht = 3; + } + + $template = $GLOBALS['template_factory']->open('courseware/mails/certificate'); + $html = $template->render( + compact('user', 'unit', 'timestamp') + ); + + // Generate the PDF. + $pdf = new CoursewarePDFCertificate($image ?? false); + $pdf->AddPage(); + $pdf->writeHTML($html, true, false, true, false, ''); + $pdf_file_name = ($user->isNew() ? 'Vorschau' : $user->nachname) . '_' . $unit->course->name . '_' . + _('Zertifikat') . '.pdf'; + $filename = $GLOBALS['TMP_PATH'] . '/' . \FileManager::cleanFileName($pdf_file_name); + $pdf->Output($filename, 'F'); + + return $filename; + } + protected static function configure($config = []) { $config['db_table'] = 'cw_certificates'; @@ -38,5 +72,4 @@ class Certificate extends \SimpleORMap parent::configure($config); } - } diff --git a/lib/models/Courseware/Instance.php b/lib/models/Courseware/Instance.php index b200f33ca78..b2308b93770 100644 --- a/lib/models/Courseware/Instance.php +++ b/lib/models/Courseware/Instance.php @@ -273,6 +273,7 @@ class Instance { if (count($certificateSettings) > 0) { $this->validateCertificateSettings($certificateSettings); + $certificateSettings['text'] = \Studip\Markup::purifyHtml($certificateSettings['text']); $this->unit->config['certificate'] = $certificateSettings; } else { unset($this->unit->config['certificate']); @@ -289,6 +290,10 @@ class Instance public function isValidCertificateSettings($certificateSettings): bool { return !isset($certificateSettings['threshold']) + || !isset($certificateSettings['title']) + || trim($certificateSettings['title']) !== '' + || !isset($certificateSettings['text']) + || trim($certificateSettings['text']) !== '' || ( $certificateSettings['threshold'] >= 0 && $certificateSettings['threshold'] <= 100 @@ -327,6 +332,7 @@ class Instance { if (count($reminderSettings) > 0) { $this->validateReminderSettings($reminderSettings); + $reminderSettings['mailText'] = \Studip\Markup::purifyHtml($reminderSettings['mailText']); $this->unit->config['reminder'] = $reminderSettings; } else { unset($this->unit->config['reminder']); @@ -380,6 +386,7 @@ class Instance { if (count($resetProgressSettings) > 0) { $this->validateResetProgressSettings($resetProgressSettings); + $resetProgressSettings['mailText'] = \Studip\Markup::purifyHtml($resetProgressSettings['mailText']); $this->unit->config['reset_progress'] = $resetProgressSettings; } else { unset($this->unit->config['reset_progress']); diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index 2ec74e45855..8fdce408649 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -26,4 +26,4 @@ @import './courseware/layouts/tabs.scss'; @import './courseware/layouts/talk-bubble.scss'; @import './courseware/layouts/tile.scss'; -@import './courseware/layouts/tree.scss'; \ No newline at end of file +@import './courseware/layouts/tree.scss'; diff --git a/resources/assets/stylesheets/scss/courseware/layouts/tile.scss b/resources/assets/stylesheets/scss/courseware/layouts/tile.scss index d668e696a9a..c9cfd15c245 100644 --- a/resources/assets/stylesheets/scss/courseware/layouts/tile.scss +++ b/resources/assets/stylesheets/scss/courseware/layouts/tile.scss @@ -129,7 +129,7 @@ .description-text-wrapper { overflow: hidden; - height: 10em; + height: 8em; margin-top: 0.5em; display: -webkit-box; margin-bottom: 1em; @@ -142,7 +142,6 @@ footer { width: 242px; - text-align: right; color: var(--white); white-space: nowrap; overflow: hidden; diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue index 80bf0a7d2bf..bf637aedc82 100644 --- a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue +++ b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue @@ -16,7 +16,7 @@ <template #image-overlay-with-action-menu> <studip-action-menu class="cw-unit-action-menu" - :items="menuItems" + :items="menuItems" :context="title" :collapseAt="0" @showDelete="openDeleteDialog" @@ -30,6 +30,9 @@ <template #description> {{ description }} </template> + <template #footer v-if="certificate"> + <studip-icon shape="medal" :size="32" role="info_alt"></studip-icon> + </template> </courseware-tile> <studip-dialog v-if="showDeleteDialog" @@ -69,6 +72,7 @@ import CoursewareUnitItemDialogExport from './CoursewareUnitItemDialogExport.vue import CoursewareUnitItemDialogSettings from './CoursewareUnitItemDialogSettings.vue'; import CoursewareUnitItemDialogLayout from './CoursewareUnitItemDialogLayout.vue'; import CoursewareUnitProgress from './CoursewareUnitProgress.vue'; +import axios from 'axios'; import { mapActions, mapGetters } from 'vuex'; @@ -95,7 +99,8 @@ export default { showSettingsDialog: false, showProgressDialog: false, showLayoutDialog: false, - progresses: null + progresses: null, + certificate: null } }, computed: { @@ -108,6 +113,18 @@ export default { let menu = []; if (this.inCourseContext) { menu.push({ id: 1, label: this.$gettext('Fortschritt'), icon: 'progress', emit: 'showProgress' }); + if (this.certificate) { + menu.push({ + id: 2, + label: this.$gettext('Zertifikat'), + icon: 'medal', + url: STUDIP.URLHelper.getURL('sendfile.php', { + type: 0, + file_id: this.certificate, + file_name: this.$gettext('Zertifikat') + '.pdf' + }) + }); + } } if(this.userIsTeacher && this.inCourseContext) { menu.push({ id: 2, label: this.$gettext('Einstellungen'), icon: 'settings', emit: 'showSettings' }); @@ -156,6 +173,7 @@ export default { async mounted() { if (this.inCourseContext) { this.progresses = await this.loadUnitProgresses({unitId: this.unit.id}); + this.checkCertificate(); } }, methods: { @@ -165,6 +183,15 @@ export default { copyUnit: 'copyUnit', companionSuccess: 'companionSuccess' }), + async checkCertificate() { + if (this.getStudipConfig('COURSEWARE_CERTIFICATES_ENABLE')) { + const response = await axios.get(STUDIP.URLHelper.getURL('jsonapi.php/v1/courseware-units/' + + this.unit.id + '/certificate/' + STUDIP.USER_ID)); + if (response.status === 200) { + this.certificate = response.data; + } + } + }, executeDelete() { this.deleteUnit({id: this.unit.id}); }, @@ -185,13 +212,13 @@ export default { this.showProgressDialog = false; }, openSettingsDialog() { - this.showSettingsDialog = true; + this.showSettingsDialog = true; }, closeSettingsDialog() { this.showSettingsDialog = false; }, openLayoutDialog() { - this.showLayoutDialog = true; + this.showLayoutDialog = true; }, closeLayoutDialog() { this.showLayoutDialog = false; diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue index 34718fd9c13..399a87c49a1 100644 --- a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue +++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue @@ -30,10 +30,10 @@ </select> </label> </fieldset> - <fieldset> + <fieldset v-if="certificatesRemindersEnabled"> <legend>{{ $gettext('Zertifikate') }}</legend> <label> - <input type="checkbox" name="makecert" v-model="makeCert"> + <input type="checkbox" v-model="makeCert"> <span> {{ $gettext('Zertifikat bei Erreichen einer Fortschrittsgrenze versenden') }} </span> @@ -42,35 +42,55 @@ </label> <label v-if="makeCert"> <span> - {{ $gettext('Erforderlicher Fortschritt (in Prozent), um ein Zertifikat zu erhalten') }} + {{ $gettext('Erforderlicher Fortschritt (in Prozent)') }} </span> - <input type="number" min="1" max="100" name="threshold" v-model="certThreshold"> + <input type="number" min="1" max="100" v-model="certThreshold"> </label> <label v-if="makeCert"> <span> - {{ $gettext('Hintergrundbild des Zertifikats wählen') }} + {{ $gettext('Bild wählen') }} </span> + <studip-tooltip-icon :text="$gettext('Wählen Sie hier ein Bild, das auf ' + + 'dem Zertifikat in der linken oberen Ecke in Originalgröße angezeigt wird. Wenn Sie das ' + + 'Bild als Hintergrund des Zertifikats verwenden möchten, muss es nur die passende Größe ' + + 'haben, um die ganze Seite auszufüllen.')"></studip-tooltip-icon> <courseware-file-chooser :isImage="true" v-model="certImage" @selectFile="updateCertImage" /> </label> + <label v-if="makeCert" class="col-3"> + <span> + {{ $gettext('Titel') }} + <input type="text" v-model="certTitle"> + </span> + </label> + <label v-if="makeCert"> + <span> + {{ $gettext('Text') }} + <studip-wysiwyg v-model="certText"></studip-wysiwyg> + </span> + </label> + <button v-if="makeCert" type="button" class="button" @click.prevent="previewCertificate"> + {{ $gettext('Vorschau') }} + </button> </fieldset> - <fieldset> + <fieldset v-if="certificatesRemindersEnabled"> <legend> {{ $gettext('Erinnerungen') }} </legend> <label> - <input type="checkbox" name="sendreminders" v-model="sendReminders"> + <input type="checkbox" v-model="sendReminders"> <span> {{ $gettext('Erinnerungsnachrichten an alle Teilnehmenden schicken') }} </span> <studip-tooltip-icon :text="$gettext('Hier können periodisch Nachrichten an alle ' + - 'Teilnehmenden verschickt werden, um z.B. an die Bearbeitung dieses Lernmaterials zu erinnern.')"/> + 'Teilnehmenden verschickt werden, um z.B. an die Bearbeitung dieses Lernmaterials ' + + 'zu erinnern.')"/> </label> <label v-if="sendReminders"> <span> {{ $gettext('Zeitraum zwischen Erinnerungen') }} </span> - <select name="reminder_interval" v-model="reminderInterval"> + <select v-model="reminderInterval"> <option value="7"> {{ $gettext('wöchentlich') }} </option> @@ -94,31 +114,28 @@ <label v-if="sendReminders" class="col-3"> <span> {{ $gettext('Erstmalige Erinnerung am') }} - <input type="date" name="reminder_start_date" - v-model="reminderStartDate"> + <input type="date" v-model="reminderStartDate"> </span> </label> <label v-if="sendReminders" class="col-3"> <span> {{ $gettext('Letztmalige Erinnerung am') }} - <input type="date" name="reminder_end_date" - v-model="reminderEndDate"> + <input type="date" v-model="reminderEndDate"> </span> </label> <label v-if="sendReminders"> <span> {{ $gettext('Text der Erinnerungsmail') }} - <textarea cols="70" rows="4" name="reminder_mail_text" data-editor="minimal" - v-model="reminderMailText"></textarea> + <studip-wysiwyg v-model="reminderMailText"></studip-wysiwyg> </span> </label> </fieldset> - <fieldset> + <fieldset v-if="certificatesRemindersEnabled"> <legend> {{ $gettext('Fortschritt') }} </legend> <label> - <input type="checkbox" name="resetprogress" v-model="resetProgress"> + <input type="checkbox" v-model="resetProgress"> <span> {{ $gettext('Fortschritt periodisch auf 0 zurücksetzen') }} </span> @@ -129,7 +146,7 @@ <span> {{ $gettext('Zeitraum zum Rücksetzen des Fortschritts') }} </span> - <select name="reset_progress_interval" v-model="resetProgressInterval"> + <select v-model="resetProgressInterval"> <option value="14"> {{ $gettext('14-tägig') }} </option> @@ -150,22 +167,19 @@ <label v-if="resetProgress" class="col-3"> <span> {{ $gettext('Erstmaliges Zurücksetzen am') }} - <input type="date" dataformatas="" name="reset_progress_start_date" - v-model="resetProgressStartDate"> + <input type="date" v-model="resetProgressStartDate"> </span> </label> <label v-if="resetProgress" class="col-3"> <span> {{ $gettext('Letztmaliges Zurücksetzen am') }} - <input type="date" name="reset_progress_end_date" - v-model="resetProgressEndDate"> + <input type="date" v-model="resetProgressEndDate"> </span> </label> <label v-if="resetProgress"> <span> {{ $gettext('Text der Rücksetzungsmail') }} - <textarea cols="70" rows="4" name="reset_progress_mail_text" data-editor="minimal" - v-model="resetProgressMailText"></textarea> + <studip-wysiwyg v-model="resetProgressMailText"></studip-wysiwyg> </span> </label> </fieldset> @@ -200,6 +214,8 @@ export default { makeCert: false, certThreshold: 0, certImage: '', + certTitle: '', + certText: '', sendReminders: false, reminderInterval: 7, reminderStartDate: '', @@ -224,10 +240,13 @@ export default { } else { return this.instanceById({id: 'user_' + this.context.id + '_' + this.unit.id}); } - + }, inCourseContext() { return this.context.type === 'courses'; + }, + certificatesRemindersEnabled() { + return this.getStudipConfig('COURSEWARE_CERTIFICATES_ENABLE'); } }, methods: { @@ -248,6 +267,8 @@ export default { Object.keys(this.certSettings).length > 0; this.certThreshold = this.certSettings.threshold; this.certImage = this.certSettings.image; + this.certTitle = this.certSettings.title; + this.certText = this.certSettings.text; this.reminderSettings = this.currentInstance.attributes['reminder-settings']; this.sendReminders = typeof(this.reminderSettings) === 'object' && Object.keys(this.reminderSettings).length > 0; @@ -275,13 +296,15 @@ export default { }); }, generateCertificateSettings() { - return this.makeCert ? { + return this.certificatesRemindersEnabled && this.makeCert ? { threshold: this.certThreshold, - image: this.certImage + image: this.certImage, + title: this.certTitle, + text: this.certText } : {}; }, generateReminderSettings() { - return this.sendReminders ? { + return this.certificatesRemindersEnabled && this.sendReminders ? { interval: this.reminderInterval, startDate: this.reminderStartDate, endDate: this.reminderEndDate, @@ -289,7 +312,7 @@ export default { } : {}; }, generateResetProgressSettings() { - return this.resetProgress ? { + return this.certificatesRemindersEnabled && this.resetProgress ? { interval: this.resetProgressInterval, startDate: this.resetProgressStartDate, endDate: this.resetProgressEndDate, @@ -298,6 +321,9 @@ export default { }, updateCertImage(file) { this.certImage = file.id; + }, + previewCertificate() { + window.open(STUDIP.URLHelper.getURL('jsonapi.php/v1/courseware-units/' + this.unit.id + '/certificate')); } }, async mounted() { @@ -308,4 +334,4 @@ export default { this.initData(); } } -</script> \ No newline at end of file +</script> diff --git a/templates/courseware/mails/certificate.php b/templates/courseware/mails/certificate.php index 743db6f2bbf..1e948be9862 100644 --- a/templates/courseware/mails/certificate.php +++ b/templates/courseware/mails/certificate.php @@ -1,22 +1,12 @@ -<?php -$p = '<p style="font-size: 20px; text-align: center;">'; -$span_bold = '<br /><br /><span style="font-size: 20px; text-align: center; font-weight: bold">'; -$span_close = '</span><br /><br />'; -switch($user->geschlecht) { - case 1: - $anrede = _('Herr'); - break; - case 2: - $anrede = _('Frau'); - break; - default: - $anrede= ''; -} -echo $p; -printf( - _("Hiermit wird bescheinigt, dass %1$s am %2$s erfolgreich am Seminar %3$s teilgenommen hat."), - $span_bold . $anrede . ' ' . $user->getFullname() . $span_close, - $span_bold . date('d.m.Y', time()) . $span_close, - $span_bold . $course->name . $span_close -); -echo '</p>'; +<p style="font-size: 14px; text-align: right;"> + <?= strftime('%x', $timestamp) ?> +</p> +<h1 style="font-size: 20px; text-align: center"> + <?= htmlReady($unit->config['certificate']['title']) ?> +</h1> +<h2 style="font-size: 14px; text-align: center"> + <?= sprintf(_('für %s'), htmlReady($user->getFullname())) ?> +</h2> +<p style="font-size: 14px; text-align: center;"> + <?= $unit->config['certificate']['text'] ?> +</p> diff --git a/templates/layouts/base.php b/templates/layouts/base.php index 3378d881998..180e0207d09 100644 --- a/templates/layouts/base.php +++ b/templates/layouts/base.php @@ -53,6 +53,7 @@ $lang_attr = str_replace('_', '-', $_SESSION['_language']); 'ACTIONMENU_THRESHOLD' => Config::get()->ACTION_MENU_THRESHOLD, 'ENTRIES_PER_PAGE' => Config::get()->ENTRIES_PER_PAGE, 'OPENGRAPH_ENABLE' => Config::get()->OPENGRAPH_ENABLE, + 'COURSEWARE_CERTIFICATES_ENABLE' => Config::get()->COURSEWARE_CERTIFICATES_ENABLE ]) ?>, } </script> -- GitLab