diff --git a/backup/moodle2/backup_course_task.class.php b/backup/moodle2/backup_course_task.class.php index 784b7eeac88..83162080a61 100644 --- a/backup/moodle2/backup_course_task.class.php +++ b/backup/moodle2/backup_course_task.class.php @@ -165,7 +165,7 @@ class backup_course_task extends backup_task { // A few other key course links. $content = self::encode_links_helper($content, 'GRADEINDEXBYID', '/grade/index.php?id='); $content = self::encode_links_helper($content, 'GRADEREPORTINDEXBYID', '/grade/report/index.php?id='); - $content = self::encode_links_helper($content, 'BADGESVIEWBYID', '/badges/view.php?type=2&id='); + $content = self::encode_links_helper($content, 'BADGESVIEWBYID', '/badges/index.php?type=2&id='); $content = self::encode_links_helper($content, 'USERINDEXVIEWBYID', '/user/index.php?id='); $content = self::encode_links_helper($content, 'PLUGINFILEBYCONTEXT', '/pluginfile.php/'); $content = self::encode_links_helper($content, 'PLUGINFILEBYCONTEXTURLENCODED', '/pluginfile.php/', true); diff --git a/backup/moodle2/restore_course_task.class.php b/backup/moodle2/restore_course_task.class.php index b34987e0879..46629222f6a 100644 --- a/backup/moodle2/restore_course_task.class.php +++ b/backup/moodle2/restore_course_task.class.php @@ -164,7 +164,7 @@ class restore_course_task extends restore_task { // A few other key course links. $rules[] = new restore_decode_rule('GRADEINDEXBYID', '/grade/index.php?id=$1', 'course'); $rules[] = new restore_decode_rule('GRADEREPORTINDEXBYID', '/grade/report/index.php?id=$1', 'course'); - $rules[] = new restore_decode_rule('BADGESVIEWBYID', '/badges/view.php?type=2&id=$1', 'course'); + $rules[] = new restore_decode_rule('BADGESVIEWBYID', '/badges/index.php?type=2&id=$1', 'course'); $rules[] = new restore_decode_rule('USERINDEXVIEWBYID', '/user/index.php?id=$1', 'course'); $rules[] = new restore_decode_rule('PLUGINFILEBYCONTEXT', '/pluginfile.php/$1', 'context'); $rules[] = new restore_decode_rule('PLUGINFILEBYCONTEXTURLENCODED', '/pluginfile.php/$1', 'context', true); diff --git a/backup/util/helper/tests/backup_encode_content_test.php b/backup/util/helper/tests/backup_encode_content_test.php index f4145efdd64..eac9f44c0a9 100644 --- a/backup/util/helper/tests/backup_encode_content_test.php +++ b/backup/util/helper/tests/backup_encode_content_test.php @@ -54,7 +54,7 @@ class backup_encode_content_test extends \basic_testcase { $httpsroot . '/course/view.php?id=123, ' . $httpsroot . '/grade/index.php?id=123, ' . $httpsroot . '/grade/report/index.php?id=123, ' . - $httpsroot . '/badges/view.php?type=2&id=123, ' . + $httpsroot . '/badges/index.php?type=2&id=123, ' . $httpsroot . '/user/index.php?id=123, ' . $httpsroot . '/pluginfile.php/123 and ' . urlencode($httpsroot . '/pluginfile.php/123') . '.' @@ -70,7 +70,7 @@ class backup_encode_content_test extends \basic_testcase { $httpsroot . '/course/view.php?id=123, ' . $httproot . '/grade/index.php?id=123, ' . $httproot . '/grade/report/index.php?id=123, ' . - $httproot . '/badges/view.php?type=2&id=123, ' . + $httproot . '/badges/index.php?type=2&id=123, ' . $httproot . '/user/index.php?id=123, ' . $httproot . '/pluginfile.php/123 and ' . urlencode($httproot . '/pluginfile.php/123') . '.' diff --git a/badges/classes/output/manage_badge_action_bar.php b/badges/classes/output/manage_badge_action_bar.php index f2864796ad5..a887bffe903 100644 --- a/badges/classes/output/manage_badge_action_bar.php +++ b/badges/classes/output/manage_badge_action_bar.php @@ -67,7 +67,10 @@ class manage_badge_action_bar extends base_action_bar { $params['id'] = $this->page->context->instanceid; } $elements['button'] = new single_button(new moodle_url('/badges/index.php', $params), get_string('back'), 'get'); - $elements['urlselect'] = new url_select($this->generate_badge_navigation(), $this->page->url->out(false), null); + $badgenav = $this->generate_badge_navigation(); + if ($badgenav) { + $elements['urlselect'] = new url_select($badgenav, $this->page->url->out(false), null); + } foreach ($elements as $key => $element) { $elements[$key] = $element->export_for_template($output); } @@ -132,6 +135,7 @@ class manage_badge_action_bar extends base_action_bar { */ protected function generate_badge_navigation(): array { global $DB; + $params = ['id' => $this->badge->id]; $options = []; $construct = $this->get_badge_administration_mapping_construct(); @@ -168,6 +172,10 @@ class manage_badge_action_bar extends base_action_bar { $url = new moodle_url($checks['url'], $params + ($checks['additionalparams'] ?? [])); $options[get_string($stringidentifier, 'core_badges', $content)] = $url->out(false); } + if (count($options) <= 1) { + return []; + } + return array_flip($options); } } diff --git a/badges/classes/reportbuilder/local/systemreports/badges.php b/badges/classes/reportbuilder/local/systemreports/badges.php index 01d0a8753fb..9c1c4f0af00 100644 --- a/badges/classes/reportbuilder/local/systemreports/badges.php +++ b/badges/classes/reportbuilder/local/systemreports/badges.php @@ -20,6 +20,7 @@ namespace core_badges\reportbuilder\local\systemreports; use core\context\{course, system}; use core_badges\reportbuilder\local\entities\badge; +use core_badges\reportbuilder\local\entities\badge_issued; use core_reportbuilder\local\helpers\database; use core_reportbuilder\local\report\{action, column}; use core_reportbuilder\system_report; @@ -50,6 +51,8 @@ class badges extends system_report { * Initialise report, we need to set the main table, load our entities and set columns/filters */ protected function initialise(): void { + global $USER; + // Our main entity, it contains all of the column definitions that we need. $badgeentity = new badge(); $entityalias = $badgeentity->get_table_alias('badge'); @@ -69,14 +72,29 @@ class badges extends system_report { [$paramtype => $type, $paramcourseid => $context->instanceid]); } + if (!$this->can_view_draft_badges()) { + $this->add_base_condition_sql("({$entityalias}.status = " . BADGE_STATUS_ACTIVE . + " OR {$entityalias}.status = " . BADGE_STATUS_ACTIVE_LOCKED . ")"); + } + // Any columns required by actions should be defined here to ensure they're always available. $this->add_base_fields("{$entityalias}.id, {$entityalias}.type, {$entityalias}.courseid, {$entityalias}.status"); + $badgeissuedentity = new badge_issued(); + $badgeissuedalias = $badgeissuedentity->get_table_alias('badge_issued'); + $this->add_entity($badgeissuedentity + ->add_join("LEFT JOIN {badge_issued} {$badgeissuedalias} + ON {$entityalias}.id = {$badgeissuedalias}.badgeid AND {$badgeissuedalias}.userid = ".$USER->id) + ); + + $this->add_base_fields("{$badgeissuedalias}.uniquehash"); + // Now we can call our helper methods to add the content we want to include in the report. - $this->add_columns(); + $this->add_columns($badgeissuedalias); $this->add_filters(); $this->add_actions(); + // Set initial sorting by name. $this->set_initial_sort_column('badge:namewithlink', SORT_ASC); $this->set_default_no_results_notice(new lang_string('nomatchingbadges', 'core_badges')); @@ -91,6 +109,7 @@ class badges extends system_report { */ protected function can_view(): bool { return has_any_capability([ + 'moodle/badges:viewbadges', 'moodle/badges:viewawarded', 'moodle/badges:createbadge', 'moodle/badges:awardbadge', @@ -105,47 +124,76 @@ class badges extends system_report { * * They are provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier. If custom columns are needed just for this report, they can be defined here. + * + * @param string $badgeissuedalias */ - protected function add_columns(): void { - $badgeentity = $this->get_entity('badge'); - - $this->add_columns_from_entities([ + public function add_columns(string $badgeissuedalias): void { + $columns = [ 'badge:image', 'badge:namewithlink', - 'badge:version', 'badge:status', 'badge:criteria', - ]); + ]; - // Issued badges column. - $tempbadgealias = database::generate_alias(); - $badgeentityalias = $badgeentity->get_table_alias('badge'); - $this->add_column((new column( - 'issued', - new lang_string('awards', 'core_badges'), - $badgeentity->get_entity_name() - )) - ->add_joins($this->get_joins()) - ->set_type(column::TYPE_INTEGER) - ->add_field("(SELECT COUNT({$tempbadgealias}.userid) - FROM {badge_issued} {$tempbadgealias} - INNER JOIN {user} u - ON {$tempbadgealias}.userid = u.id - WHERE {$tempbadgealias}.badgeid = {$badgeentityalias}.id AND u.deleted = 0)", 'issued') - ->set_is_sortable(true) - ->set_callback(function(int $count): string { - if (!has_capability('moodle/badges:viewawarded', $this->get_context())) { - return (string) $count; - } - - return html_writer::link(new moodle_url('/badges/recipients.php', ['id' => $this->badgeid]), $count); - })); + $canviewdraftbadges = $this->can_view_draft_badges(); + if (!$canviewdraftbadges) { + // Remove status and recipients column. + unset($columns[2]); + } + $this->add_columns_from_entities($columns); // Remove title from image column. $this->get_column('badge:image')->set_title(null); // Change title from namewithlink column. $this->get_column('badge:namewithlink')->set_title(new lang_string('name')); + + // Recipients column. + if ($canviewdraftbadges) { + $badgeentity = $this->get_entity('badge'); + $tempbadgealias = database::generate_alias(); + $badgeentityalias = $badgeentity->get_table_alias('badge'); + $this->add_column((new column( + 'issued', + new lang_string('awards', 'core_badges'), + $badgeentity->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_INTEGER) + ->add_field("(SELECT COUNT({$tempbadgealias}.userid) + FROM {badge_issued} {$tempbadgealias} + INNER JOIN {user} u + ON {$tempbadgealias}.userid = u.id + WHERE {$tempbadgealias}.badgeid = {$badgeentityalias}.id AND u.deleted = 0)", 'issued') + ->set_is_sortable(true) + ->set_callback(function(int $count): string { + if (!has_capability('moodle/badges:viewawarded', $this->get_context())) { + return (string) $count; + } + + return html_writer::link(new moodle_url('/badges/recipients.php', ['id' => $this->badgeid]), $count); + })); + } + + // Add the date the badge was issued at the end of the report. + $this->add_column_from_entity('badge_issued:issued'); + $this->get_column('badge_issued:issued') + ->set_title(new lang_string('awardedtoyou', 'core_badges')) + ->add_fields("{$badgeissuedalias}.uniquehash") + ->set_callback(static function(?int $value, stdClass $row) { + global $OUTPUT; + + if (!$value) { + return ''; + } + $format = get_string('strftimedatefullshort', 'core_langconfig'); + $date = $value ? userdate($value, $format) : ''; + $badgeurl = new moodle_url('/badges/badge.php', ['hash' => $row->uniquehash]); + $icon = new pix_icon('i/valid', get_string('dateearned', 'badges', $date)); + return $OUTPUT->action_icon($badgeurl, $icon, null, null, true); + }); + + $this->set_initial_sort_column('badge:namewithlink', SORT_ASC); } /** @@ -155,12 +203,19 @@ class badges extends system_report { * unique identifier */ protected function add_filters(): void { - $this->add_filters_from_entities([ + $filters = [ 'badge:name', 'badge:version', 'badge:status', 'badge:expiry', - ]); + 'badge_issued:issued', + ]; + if (!$this->can_view_draft_badges()) { + // Remove version and status filters. + unset($filters[1]); + unset($filters[2]); + } + $this->add_filters_from_entities($filters); } /** @@ -295,6 +350,22 @@ class badges extends system_report { } } + /** + * Check whether the user can view unpublished badges. + * + * @return bool True if the user can edit badges, false otherwise. + */ + private function can_view_draft_badges(): bool { + return has_any_capability([ + 'moodle/badges:viewawarded', + 'moodle/badges:createbadge', + 'moodle/badges:awardbadge', + 'moodle/badges:configurecriteria', + 'moodle/badges:configuremessages', + 'moodle/badges:configuredetails', + 'moodle/badges:deletebadge'], $this->get_context()); + } + /** * Store the ID of the badge within each row * diff --git a/badges/index.php b/badges/index.php index c3a3b77eac4..3bb0fbae4f6 100644 --- a/badges/index.php +++ b/badges/index.php @@ -48,13 +48,16 @@ if (empty($CFG->badges_allowcoursebadges) && ($type == BADGE_TYPE_COURSE)) { throw new \moodle_exception('coursebadgesdisabled', 'badges'); } +if ($type == BADGE_TYPE_COURSE && !$courseid) { + throw new \moodle_exception('courseidnotfound'); +} + $urlparams = ['type' => $type]; if ($course = $DB->get_record('course', ['id' => $courseid])) { $urlparams['id'] = $course->id; } -$hdr = get_string('managebadges', 'badges'); $returnurl = new moodle_url('/badges/index.php', $urlparams); $PAGE->set_url($returnurl); $PAGE->add_body_class('limitedwidth'); @@ -64,31 +67,34 @@ if ($type == BADGE_TYPE_SITE) { $PAGE->set_context(context_system::instance()); $PAGE->set_pagelayout('admin'); $PAGE->set_heading(get_string('administrationsite')); - navigation_node::override_active_url(new moodle_url('/badges/index.php', array('type' => BADGE_TYPE_SITE)), true); + navigation_node::override_active_url(new moodle_url('/badges/index.php', ['type' => BADGE_TYPE_SITE]), true); + $eventotherparams = ['badgetype' => BADGE_TYPE_SITE]; } else { require_login($course); $coursecontext = context_course::instance($course->id); $title = get_string('coursebadges', 'badges'); $PAGE->set_context($coursecontext); $PAGE->set_pagelayout('incourse'); - $PAGE->set_heading(format_string($course->fullname, true, array('context' => $coursecontext))); + $PAGE->set_heading(format_string($course->fullname, true, ['context' => $coursecontext])); navigation_node::override_active_url( - new moodle_url('/badges/index.php', array('type' => BADGE_TYPE_COURSE, 'id' => $course->id)) + new moodle_url('/badges/index.php', ['type' => BADGE_TYPE_COURSE, 'id' => $course->id]) ); + $eventotherparams = ['badgetype' => BADGE_TYPE_COURSE, 'courseid' => $course->id]; } -if (!has_any_capability(array( +if (!has_any_capability([ + 'moodle/badges:viewbadges', 'moodle/badges:viewawarded', 'moodle/badges:createbadge', 'moodle/badges:awardbadge', 'moodle/badges:configurecriteria', 'moodle/badges:configuremessages', 'moodle/badges:configuredetails', - 'moodle/badges:deletebadge'), $PAGE->context)) { + 'moodle/badges:deletebadge'], $PAGE->context)) { redirect($CFG->wwwroot); } -$PAGE->set_title($hdr); +$PAGE->set_title($title); /** @var core_badges_renderer $output */ $output = $PAGE->get_renderer('core', 'badges'); @@ -102,14 +108,14 @@ if ($delete || $archive) { // Archive this badge? echo $output->heading(get_string('archivebadge', 'badges', $badge->name)); $archivebutton = $output->single_button( - new moodle_url($PAGE->url, array('archive' => $badge->id, 'confirm' => 1)), + new moodle_url($PAGE->url, ['archive' => $badge->id, 'confirm' => 1]), get_string('archiveconfirm', 'badges')); echo $output->box(get_string('archivehelp', 'badges') . $archivebutton, 'generalbox'); // Delete this badge? echo $output->heading(get_string('delbadge', 'badges', $badge->name)); $deletebutton = $output->single_button( - new moodle_url($PAGE->url, array('delete' => $badge->id, 'confirm' => 1)), + new moodle_url($PAGE->url, ['delete' => $badge->id, 'confirm' => 1]), get_string('delconfirm', 'badges')); echo $output->box(get_string('deletehelp', 'badges') . $deletebutton, 'generalbox'); @@ -140,20 +146,28 @@ if ($deactivate && has_capability('moodle/badges:configuredetails', $PAGE->conte } echo $OUTPUT->header(); -$backurl = $type == BADGE_TYPE_SITE ? null : new moodle_url('/badges/view.php', ['type' => $type, 'id' => $courseid]); -$actionbar = new \core_badges\output\standard_action_bar($PAGE, $type, false, true, $backurl); -echo $output->render_tertiary_navigation($actionbar); +echo $OUTPUT->container_start('badges-heading'); if ($type == BADGE_TYPE_SITE) { - echo $OUTPUT->heading_with_help($hdr, 'sitebadges', 'badges'); + echo $OUTPUT->heading_with_help($title, 'sitebadges', 'badges'); } else { - echo $OUTPUT->heading($hdr); + echo $OUTPUT->heading($title); } +$actionbar = new \core_badges\output\standard_action_bar( + page: $PAGE, + type: $type, + showaddbadge: true, +); +echo $output->render_tertiary_navigation($actionbar); +echo $OUTPUT->container_end(); echo $OUTPUT->box('', 'notifyproblem hide', 'check_connection'); if ($course && $course->startdate > time()) { - echo $OUTPUT->box(get_string('error:notifycoursedate', 'badges'), 'generalbox notifyproblem'); + echo $OUTPUT->notification( + get_string('error:notifycoursedate', 'badges', userdate($course->startdate)), + 'warning' + ); } if ($msg !== '') { @@ -161,8 +175,13 @@ if ($msg !== '') { } $report = system_report_factory::create(badges::class, $PAGE->context); -echo $report->output(); +echo $report->output(); $PAGE->requires->js_call_amd('core_badges/actions', 'init'); +// Trigger event, badge listing viewed. +$eventparams = ['context' => $PAGE->context, 'other' => $eventotherparams]; +$event = \core\event\badge_listing_viewed::create($eventparams); +$event->trigger(); + echo $OUTPUT->footer(); diff --git a/lang/en/badges.php b/lang/en/badges.php index d0bfe834c86..9d482592eb1 100644 --- a/lang/en/badges.php +++ b/lang/en/badges.php @@ -323,7 +323,7 @@ $string['error:nosuchfield'] = 'Warning: This user profile field is no longer av $string['error:nosuchmod'] = 'Warning: This activity is no longer available.'; $string['error:nosuchrole'] = 'Warning: This role is no longer available.'; $string['error:nosuchuser'] = 'User with this email address does not have an account with the current backpack provider.'; -$string['error:notifycoursedate'] = 'Warning: Badges associated with course and activity completions will not be issued until the course start date.'; +$string['error:notifycoursedate'] = 'Badges associated with course and activity completions will not be issued until the course start date ({$a}).'; $string['error:parameter'] = 'Warning: At least one parameter should be selected to ensure correct badge issuing workflow.'; $string['error:requesttimeout'] = 'The connection request timed out before it could complete.'; $string['error:requesterror'] = 'The connection request failed (error code {$a}).'; diff --git a/lib/classes/event/badge_listing_viewed.php b/lib/classes/event/badge_listing_viewed.php index 3554ce50c4b..573d375fac6 100644 --- a/lib/classes/event/badge_listing_viewed.php +++ b/lib/classes/event/badge_listing_viewed.php @@ -84,7 +84,7 @@ class badge_listing_viewed extends base { } else { $params = array('id' => $this->other['courseid'], 'type' => $this->other['badgetype']); } - return new \moodle_url('/badges/view.php', $params ); + return new \moodle_url('/badges/index.php', $params ); } /** diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 0ca2414f93c..829efae2a20 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -2975,7 +2975,7 @@ class global_navigation extends navigation_node { // Badges. if ($navoptions->badges) { - $url = new moodle_url('/badges/view.php', array('type' => 2, 'id' => $course->id)); + $url = new moodle_url('/badges/index.php', ['type' => 2, 'id' => $course->id]); $coursenode->add(get_string('coursebadges', 'badges'), $url, navigation_node::TYPE_SETTING, null, 'badgesview', @@ -3059,7 +3059,7 @@ class global_navigation extends navigation_node { // Badges. if ($navoptions->badges) { - $url = new moodle_url($CFG->wwwroot . '/badges/view.php', array('type' => 1)); + $url = new moodle_url($CFG->wwwroot . '/badges/index.php', ['type' => 1]); $coursenode->add(get_string('sitebadges', 'badges'), $url, navigation_node::TYPE_CUSTOM); } diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index e3ec2aa4f91..7f27bcce5c9 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -1854,6 +1854,28 @@ ul.badges { color: map-get($theme-colors, 'warning'); } +#page-badges-index #page { + .badges-heading { + display: flex; + justify-content: space-between; + align-items: center; + } + + h2 { + margin: 0; /* Removes default margins */ + } + + #action_bar { + margin: 0; /* Removes any default margin that might affect alignment */ + } + + .tertiary-navigation.container-fluid { + width: auto; + height: 50px; + padding-top: 0; + } +} + #page-badges-award .recipienttable tr td { vertical-align: top; } diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 4ca9d8a606a..fec14894cf4 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -24902,6 +24902,23 @@ ul.badges { color: #f0ad4e; } +#page-badges-index #page .badges-heading { + display: flex; + justify-content: space-between; + align-items: center; +} +#page-badges-index #page h2 { + margin: 0; /* Removes default margins */ +} +#page-badges-index #page #action_bar { + margin: 0; /* Removes any default margin that might affect alignment */ +} +#page-badges-index #page .tertiary-navigation.container-fluid, #page-badges-index #page .tertiary-navigation.container-sm, #page-badges-index #page .tertiary-navigation.container-md, #page-badges-index #page .tertiary-navigation.container-lg, #page-badges-index #page .tertiary-navigation.container-xl { + width: auto; + height: 50px; + padding-top: 0; +} + #page-badges-award .recipienttable tr td { vertical-align: top; } diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 8091f1f81f0..230950b1021 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -24902,6 +24902,23 @@ ul.badges { color: #f0ad4e; } +#page-badges-index #page .badges-heading { + display: flex; + justify-content: space-between; + align-items: center; +} +#page-badges-index #page h2 { + margin: 0; /* Removes default margins */ +} +#page-badges-index #page #action_bar { + margin: 0; /* Removes any default margin that might affect alignment */ +} +#page-badges-index #page .tertiary-navigation.container-fluid, #page-badges-index #page .tertiary-navigation.container-sm, #page-badges-index #page .tertiary-navigation.container-md, #page-badges-index #page .tertiary-navigation.container-lg, #page-badges-index #page .tertiary-navigation.container-xl { + width: auto; + height: 50px; + padding-top: 0; +} + #page-badges-award .recipienttable tr td { vertical-align: top; }