Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a1dc566
Updated subplugins.json file to match latest format (#688)
Thaveesha222 May 30, 2025
2cdc66c
Updated GHA for Moodle 5.0
mdjnelson Jun 7, 2025
b41d838
Updated CHANGES.md
mdjnelson Jun 7, 2025
13565ec
Fix code style complaints
mdjnelson Jun 7, 2025
1479075
Fixed null array offset warning in QR code element on PHP 8.4 in GHA
mdjnelson Jun 7, 2025
8484023
Bumped version
mdjnelson Jun 7, 2025
c7f98e3
Fix notifications when the instance of the activity is on site home #693
andrewhancox Jun 16, 2025
3fe710b
Added customisable filename options for certificates (#684)
Raza403 May 11, 2025
00c6da5
Fixed version bump issue (#684)
mdjnelson Jul 6, 2025
5f982ff
Added newly introduced fields to install.xml (#684)
mdjnelson Jul 6, 2025
cf542a6
Make language string consistent with others
mdjnelson Jul 6, 2025
733428c
Removed unused language strings (#684)
mdjnelson Jul 6, 2025
349a4b4
Removed unnecessary code for filling in form inputs (#684)
mdjnelson Jul 6, 2025
06af2ac
Fixed issue with language strings not matching logic for placeholders…
mdjnelson Jul 6, 2025
8305d26
Added missing fields to backups (#705)
mdjnelson Jul 6, 2025
1472921
Added fields to backup file (#684)
mdjnelson Jul 6, 2025
ac2fdda
Removed hack for Oracle databases as they are no longer supported (#701)
mdjnelson Jul 13, 2025
1d74bcb
Add setting to add a button to return to course (#655)
gbarat87 Nov 27, 2024
df719c4
Make the setting description more descriptive (#655)
mdjnelson Jul 13, 2025
9b462e5
Removed incorrect passing of boolean value no longer used (#655)
mdjnelson Jul 13, 2025
14e91a8
Do not refer to the course page as a menu (#655)
mdjnelson Jul 13, 2025
0637ca1
Implement new feature keeplocalcopy:
HobieCat Oct 13, 2023
ab5e388
rename methods, add return types and remove downloadcerts custom feature
HobieCat Jul 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .github/workflows/moodle-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ jobs:

services:
postgres:
image: postgres:13
image: postgres:14
env:
POSTGRES_USER: 'postgres'
POSTGRES_HOST_AUTH_METHOD: 'trust'
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3
mariadb:
image: mariadb:10.6.10
image: mariadb:10.11
env:
MYSQL_USER: 'root'
MYSQL_ALLOW_EMPTY_PASSWORD: "true"
Expand All @@ -28,17 +28,17 @@ jobs:
fail-fast: false
matrix:
include:
- php: '8.3'
moodle-branch: 'MOODLE_404_STABLE'
- php: '8.4'
moodle-branch: 'MOODLE_500_STABLE'
database: pgsql
- php: '8.3'
moodle-branch: 'MOODLE_404_STABLE'
- php: '8.4'
moodle-branch: 'MOODLE_500_STABLE'
database: mariadb
- php: '8.1'
moodle-branch: 'MOODLE_404_STABLE'
- php: '8.2'
moodle-branch: 'MOODLE_500_STABLE'
database: pgsql
- php: '8.1'
moodle-branch: 'MOODLE_404_STABLE'
- php: '8.2'
moodle-branch: 'MOODLE_500_STABLE'
database: mariadb

steps:
Expand All @@ -56,7 +56,7 @@ jobs:

- name: Initialise moodle-plugin-ci
run: |
composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3
composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4
echo $(cd ci/bin; pwd) >> $GITHUB_PATH
echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH
sudo locale-gen en_AU.UTF-8
Expand Down
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

Note - All hash comments refer to the issue number. Eg. #169 refers to https://github.com/mdjnelson/moodle-mod_customcert/issues/169.

## [5.0.0] - 2025-06-07

### Fixed

- Updated subplugins.json file to match Moodle 5.0 format (#688).

## [4.4.6] - 2025-06-07

### Added
Expand Down
6 changes: 4 additions & 2 deletions backup/moodle2/backup_customcert_stepslib.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ protected function define_structure() {

// The instance.
$customcert = new backup_nested_element('customcert', ['id'], [
'templateid', 'name', 'intro', 'introformat', 'requiredtime', 'verifyany', 'emailstudents',
'emailteachers', 'emailothers', 'protection', 'timecreated', 'timemodified']);
'templateid', 'name', 'intro', 'introformat', 'requiredtime', 'verifyany',
'deliveryoption', 'usecustomfilename', 'customfilenamepattern', 'emailstudents',
'emailteachers', 'emailothers', 'protection', 'language', 'keeplocalcopy', 'timecreated',
'timemodified']);

// The template.
$template = new backup_nested_element('template', ['id'], [
Expand Down
2 changes: 1 addition & 1 deletion classes/certificate.php
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ public static function issue_certificate($certificateid, $userid) {
$event = \mod_customcert\event\issue_created::create([
'objectid' => $issueid,
'context' => $context,
'relateduserid' => $userid
'relateduserid' => $userid,
]);
$event->trigger();

Expand Down
254 changes: 254 additions & 0 deletions classes/localfile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* Class represents a local file of an issued certificate.
*
* @package mod_customcert
* @copyright 2023 Giorgio Consorti <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace mod_customcert;

use file_exception;

defined('MOODLE_INTERNAL') || die();

/**
* Class represents a local file of an issued certificate.
*
* @package mod_customcert
* @copyright 2023 Giorgio Consorti <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class localfile {

/**
* @var \mod_customcert\template the template representing the content of the file.
*/
protected $template;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the type here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't get this, how do you need the code to look like?


/**
* The component name for the file storage.
*/
private const component = 'mod_customcert';

/**
* The filearea name for the file storage.
*/
private const filearea = 'customcert_issues';

/**
* The constructor.
*
* @param \mod_customcert\template $template
*/
public function __construct(\mod_customcert\template $template) {
$this->template = $template;
}

/**
* Save the PDF to the file storage.
*
* @param string $pdfcontent string content of the pdf
* @param int|null $userid the id of the user whose certificate we want to save
*
* @return \stored_file|bool the stored_file object on success, false on error
*/
public function save_pdf(string $pdfcontent, ?int $userid = null): \stored_file|bool {
global $CFG, $USER;
require_once($CFG->libdir . '/filelib.php');

if (empty($userid)) {
$userid = $USER->id;
}

try {
$file = $this->get_pdf($userid);
if (!$file) {
// Create file containing the pdf
$fs = get_file_storage();
$file = $fs->create_file_from_string($this->buildFileInfo($userid), $pdfcontent);
}
return $file;
} catch (file_exception $e) {
// maybe log the exception
return false;
}
}

/**
* Get the PDF from the file storage.
*
* @param int|null $userid the id of the user whose certificate we want to get
*
* @return \stored_file|bool the stored_file object on success, false on error
*/
public function get_pdf(?int $userid = null): \stored_file|bool {
global $CFG, $USER;
require_once($CFG->libdir . '/filelib.php');

if (empty($userid)) {
$userid = $USER->id;
}

$fileinfo = $this->buildFileInfo($userid);
$fs = get_file_storage();
return $fs->get_file($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'],
$fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']);
}

/**
* Delete the PDF from the file storage.
*
* @param int|null $userid the id of the user whose certificate we want to get
*
* @return bool true on success
*/
public function delete_pdf(?int $userid = null): bool {
global $USER;

if (empty($userid)) {
$userid = $USER->id;
}

try {
$file = $this->get_pdf($userid);
if ($file) {
return $file->delete();
}
return false;
} catch (file_exception $e) {
return false;
}
}

/**
* Send the PDF to the browser or return it as a string.
*
* @param int $userid the id of the user whose certificate we want to view
* @param string $deliveryoption the delivery option of the customcert
* @param bool $return Do we want to return the contents of the PDF?
*
* @return string|null Can return the PDF in string format if specified.
*/
public function send_pdf(?int $userid = NULL, string $deliveryoption = certificate::DELIVERY_OPTION_DOWNLOAD, bool $return = false): string|null {
global $USER;

if (empty($userid)) {
$userid = $USER->id;
}

$file = $this->get_pdf($userid);
if ($file) {
if ($return) {
return $file->get_content();
} else {
// send the file to the browser
send_stored_file(
$file,
0,
0,
$deliveryoption == certificate::DELIVERY_OPTION_DOWNLOAD,
['filename' => $file->get_filename()]
);
die();
}
}
return null;
}

/**
* Check if a pdf exists in the file storage area.
*
* @param \stdClass $cm the course module
* @param int|null $userid the id of the user whose PDF we want to check
* @param int|null $templateid the template id of the customcert we want to check
*
* @return \stored_file|false the stored_file object on success, false on error
*/
public static function does_pdf_exist($cm, ?int $userid = null, ?int $templateid = null): \stored_file|bool {

$fileinfo = self::build_file_info($cm, $userid, $templateid);
$fs = get_file_storage();
return $fs->get_file($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'],
$fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']);
}

/**
* Build the fileinfo array needed by the file storage.
*
* @param int|null $userid the id of the user whose fileinfo array we want to generate
*
* @return array the fileinfo array
*/
protected function buildFileInfo(?int $userid = null): array {

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space needed here.

return self::build_file_info($this->template->get_cm(), $userid, $this->template->get_id());
}

/**
* Build the fileinfo array needed by the file storage, static version.
*
* @param \stdClass $cm the course module
* @param int|null $userid the id of the user whose fileinfo array we want to generate
* @param int|null $templateid the template id of the customcert of the array we want to generate
*
* @return array the fileinfo array
*/
private static function build_file_info ($cm, ?int $userid = null, ?int $templateid = null): array {

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space needed here.

/** @var \moodle_database $DB */
global $DB, $USER;

if (empty($userid)) {
$userid = $USER->id;
}

if (empty($templateid)) {
$customcert = $DB->get_record('customcert', array('id' => $cm->instance), '*', MUST_EXIST);
$templateid = $customcert->templateid;
}

$course = $DB->get_record('course', ['id' => $cm->course]);
$context = $DB->get_record('context', ['contextlevel' => '50', 'instanceid' => $course->id]);
$user_info = $DB->get_record('user', ['id' => $userid]);

return [
'contextid' => $context->id,
'component' => self::component,
'filearea' => self::filearea,
'itemid' => $templateid,
'userid' => $USER->id,
'author' => fullname($USER),
'filepath' => '/' . $course->id . '/',
'filename' => self::buildFileName($user_info->username, $templateid, $course->shortname),
];
}

/**
* Build the PDF filename.
*
* @param string $username
* @param string $templateid
* @param string $courseShortname
* @return string the PDF file name
*/
public static function buildFileName($username, $templateid, $courseShortname): string {
return $username . '_cert-' . $templateid . '_course-' . $courseShortname . '.pdf';
}
}
40 changes: 33 additions & 7 deletions classes/report_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -212,16 +212,42 @@ public function col_download($user) {
public function col_actions($user) {
global $OUTPUT;

$icon = new \pix_icon('i/delete', get_string('delete'));
$link = new \moodle_url('/mod/customcert/view.php',
$actions = [
[
'id' => $this->cm->id,
'deleteissue' => $user->issueid,
'sesskey' => sesskey(),
'icon' => new \pix_icon('i/delete', get_string('delete')),
'link' => new \moodle_url(
'/mod/customcert/view.php',
[
'id' => $this->cm->id,
'deleteissue' => $user->issueid,
'sesskey' => sesskey()
]
),
'attributes' => ['class' => 'action-icon delete-icon'],
]
);
];

if (has_capability('mod/customcert:deletelocalcopy', \context_module::instance($this->cm->id)) && localfile::does_pdf_exist($this->cm, $user->id)) {
$actions[] = [
'icon' => new \pix_icon('deletelocalcopy', get_string('deletelocalcopy', 'customcert'), 'customcert'),
'link' => new \moodle_url(
'/mod/customcert/view.php',
[
'id' => $this->cm->id,
'deleteissue' => $user->issueid,
'deletelocalcopy' => 1,
'sesskey' => sesskey()
]
),
'attributes' => ['class' => 'action-icon deletelocalcopy-icon'],
];
}

return $OUTPUT->action_icon($link, $icon, null, ['class' => 'action-icon delete-icon']);
return implode(" ", array_map(
fn ($action) =>
$OUTPUT->action_icon($action['link'], $action['icon'], null, $action['attributes'] ?? []),
$actions
));
}

/**
Expand Down
Loading