mirror of
https://github.com/moodle/moodle.git
synced 2025-03-14 12:40:01 +01:00
MDL-76851 core_courseformat: plugin custom editor strings
Most section related actions require the frontend to use alternative strings depending on the format plugin lang file. This patch adds overridden strings to the current setViewFormat course editor setup object.
This commit is contained in:
parent
9c583f5ec5
commit
856b295569
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
||||
define("core_courseformat/local/content/section/header",["exports","core_courseformat/local/courseeditor/dndsectionitem","core/str","core/prefetch"],(function(_exports,_dndsectionitem,_str,_prefetch){var obj;
|
||||
define("core_courseformat/local/content/section/header",["exports","core_courseformat/local/courseeditor/dndsectionitem"],(function(_exports,_dndsectionitem){var obj;
|
||||
/**
|
||||
* Course section header component.
|
||||
*
|
||||
@ -8,6 +8,6 @@ define("core_courseformat/local/content/section/header",["exports","core_coursef
|
||||
* @class core_courseformat/local/content/section/header
|
||||
* @copyright 2021 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndsectionitem=(obj=_dndsectionitem)&&obj.__esModule?obj:{default:obj},(0,_prefetch.prefetchStrings)("core_courseformat",["selectsection"]);class _default extends _dndsectionitem.default{create(descriptor){this.name="content_section_header",this.selectors={ACTIONSMENU:".section_action_menu",BULKSELECT:"[data-for='sectionBulkSelect']",BULKCHECKBOX:"[data-bulkcheckbox]"},this.classes={HIDE:"d-none",SELECTED:"selected"},this.id=descriptor.id,this.section=descriptor.section,this.course=descriptor.course,this.fullregion=descriptor.fullregion}stateReady(state){this.configDragDrop(this.id,state,this.fullregion),this._refreshBulk({state:state})}getWatchers(){return[{watch:"bulk:updated",handler:this._refreshBulk},{watch:"section[".concat(this.id,"].title:updated"),handler:this._refreshSectionBulkSelector}]}async _refreshSectionBulkSelector(_ref){let{element:element}=_ref;const checkbox=this.getElement(this.selectors.BULKCHECKBOX);if(!checkbox)return;const newLabel=await(0,_str.get_string)("selectsection","core_courseformat",element.title);checkbox.title=newLabel;const label=this.getElement("label[for='".concat(checkbox.id,"']"));label&&(label.innerText=newLabel)}_refreshBulk(_ref2){var _this$getElement;let{state:state}=_ref2;const bulk=state.bulk;if(!this._isSectionBulkEditable())return;this.setDraggable(!bulk.enabled),null===(_this$getElement=this.getElement(this.selectors.BULKSELECT))||void 0===_this$getElement||_this$getElement.classList.toggle(this.classes.HIDE,!bulk.enabled);const disabled=!this._isSectionBulkEnabled(bulk),selected=this._isSelected(bulk);this.element.classList.toggle(this.classes.SELECTED,selected),this._setCheckboxValue(selected,disabled)}_setCheckboxValue(checked,disabled){const checkbox=this.getElement(this.selectors.BULKCHECKBOX);checkbox&&(checkbox.checked=checked,checkbox.disabled=disabled,disabled?checkbox.removeAttribute("data-is-selectable"):checkbox.dataset.isSelectable=1)}_isSectionBulkEnabled(bulk){return!!bulk.enabled&&(""===bulk.selectedType||"section"===bulk.selectedType)}_isSectionBulkEditable(){var _section$bulkeditable;const section=this.reactive.get("section",this.id);return null!==(_section$bulkeditable=null==section?void 0:section.bulkeditable)&&void 0!==_section$bulkeditable&&_section$bulkeditable}_isSelected(bulk){return"section"===bulk.selectedType&&bulk.selection.includes(this.id)}}return _exports.default=_default,_exports.default}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndsectionitem=(obj=_dndsectionitem)&&obj.__esModule?obj:{default:obj};class _default extends _dndsectionitem.default{create(descriptor){this.name="content_section_header",this.selectors={ACTIONSMENU:".section_action_menu",BULKSELECT:"[data-for='sectionBulkSelect']",BULKCHECKBOX:"[data-bulkcheckbox]",CHEVRON:"[data-for='sectiontoggler']"},this.classes={HIDE:"d-none",SELECTED:"selected"},this.id=descriptor.id,this.section=descriptor.section,this.course=descriptor.course,this.fullregion=descriptor.fullregion}stateReady(state){this.configDragDrop(this.id,state,this.fullregion),this._refreshBulk({state:state})}getWatchers(){return[{watch:"bulk:updated",handler:this._refreshBulk},{watch:"section[".concat(this.id,"].title:updated"),handler:this._refreshSectionTitle}]}_refreshSectionTitle(param){var _this$getElement;const element=param.element;null===(_this$getElement=this.getElement(this.selectors.CHEVRON))||void 0===_this$getElement||_this$getElement.setAttribute("aria-label",element.title),this._refreshSectionBulkSelector(param)}async _refreshSectionBulkSelector(_ref){let{element:element}=_ref;const checkbox=this.getElement(this.selectors.BULKCHECKBOX);if(!checkbox)return;const newLabel=await this.reactive.getFormatString("selectsection",element.title);checkbox.title=newLabel;const label=this.getElement("label[for='".concat(checkbox.id,"']"));label&&(label.innerText=newLabel)}_refreshBulk(_ref2){var _this$getElement2;let{state:state}=_ref2;const bulk=state.bulk;if(!this._isSectionBulkEditable())return;this.setDraggable(!bulk.enabled),null===(_this$getElement2=this.getElement(this.selectors.BULKSELECT))||void 0===_this$getElement2||_this$getElement2.classList.toggle(this.classes.HIDE,!bulk.enabled);const disabled=!this._isSectionBulkEnabled(bulk),selected=this._isSelected(bulk);this.element.classList.toggle(this.classes.SELECTED,selected),this._setCheckboxValue(selected,disabled)}_setCheckboxValue(checked,disabled){const checkbox=this.getElement(this.selectors.BULKCHECKBOX);checkbox&&(checkbox.checked=checked,checkbox.disabled=disabled,disabled?checkbox.removeAttribute("data-is-selectable"):checkbox.dataset.isSelectable=1)}_isSectionBulkEnabled(bulk){return!!bulk.enabled&&(""===bulk.selectedType||"section"===bulk.selectedType)}_isSectionBulkEditable(){var _section$bulkeditable;const section=this.reactive.get("section",this.id);return null!==(_section$bulkeditable=null==section?void 0:section.bulkeditable)&&void 0!==_section$bulkeditable&&_section$bulkeditable}_isSelected(bulk){return"section"===bulk.selectedType&&bulk.selection.includes(this.id)}}return _exports.default=_default,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=header.min.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -62,7 +62,8 @@ function dispatchStateChangedEvent(detail, target) {
|
||||
* @param {setup} setup format, page and course settings
|
||||
* @param {boolean} setup.editing if the page is in edit mode
|
||||
* @param {boolean} setup.supportscomponents if the format supports components for content
|
||||
* @param {boolean} setup.statekey the backend cached state revision
|
||||
* @param {String} setup.statekey the backend cached state revision
|
||||
* @param {Array} setup.overriddenStrings optional overridden strings
|
||||
*/
|
||||
export const setViewFormat = (courseId, setup) => {
|
||||
courseId = parseInt(courseId);
|
||||
|
@ -403,15 +403,18 @@ export default class extends BaseComponent {
|
||||
}
|
||||
|
||||
let bodyText = null;
|
||||
let titleText = null;
|
||||
if (sectionIds.length == 1) {
|
||||
titleText = this.reactive.getFormatString('sectiondelete_title');
|
||||
const sectionInfo = this.reactive.get('section', sectionIds[0]);
|
||||
bodyText = getString('confirmdeletesection', 'moodle', sectionInfo.title);
|
||||
bodyText = this.reactive.getFormatString('sectiondelete_info', {name: sectionInfo.title});
|
||||
} else {
|
||||
bodyText = getString('sectionsdelete_confirm', 'core_courseformat');
|
||||
titleText = this.reactive.getFormatString('sectionsdelete_title');
|
||||
bodyText = this.reactive.getFormatString('sectionsdelete_info', {count: sectionIds.length});
|
||||
}
|
||||
|
||||
const modalParams = {
|
||||
title: getString('confirm', 'core'),
|
||||
title: titleText,
|
||||
body: bodyText,
|
||||
type: ModalFactory.types.DELETE_CANCEL,
|
||||
};
|
||||
@ -505,22 +508,29 @@ export default class extends BaseComponent {
|
||||
event.preventDefault();
|
||||
|
||||
let bodyText = null;
|
||||
let titleText = null;
|
||||
if (cmIds.length == 1) {
|
||||
const cmInfo = this.reactive.get('cm', cmIds[0]);
|
||||
titleText = getString('cmdelete_title', 'core_courseformat');
|
||||
bodyText = getString(
|
||||
'deletechecktypename',
|
||||
'moodle',
|
||||
'cmdelete_info',
|
||||
'core_courseformat',
|
||||
{
|
||||
type: cmInfo.modname,
|
||||
name: cmInfo.name,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
bodyText = getString('cmsdelete_confirm', 'core_courseformat');
|
||||
titleText = getString('cmsdelete_title', 'core_courseformat');
|
||||
bodyText = getString(
|
||||
'cmsdelete_info',
|
||||
'core_courseformat',
|
||||
{count: cmIds.length}
|
||||
);
|
||||
}
|
||||
|
||||
const modalParams = {
|
||||
title: getString('confirm', 'core'),
|
||||
title: titleText,
|
||||
body: bodyText,
|
||||
type: ModalFactory.types.DELETE_CANCEL,
|
||||
};
|
||||
|
@ -25,12 +25,6 @@
|
||||
*/
|
||||
|
||||
import DndSectionItem from 'core_courseformat/local/courseeditor/dndsectionitem';
|
||||
import {get_string as getString} from 'core/str';
|
||||
import {prefetchStrings} from 'core/prefetch';
|
||||
|
||||
prefetchStrings('core_courseformat', [
|
||||
'selectsection',
|
||||
]);
|
||||
|
||||
export default class extends DndSectionItem {
|
||||
|
||||
@ -47,6 +41,7 @@ export default class extends DndSectionItem {
|
||||
ACTIONSMENU: `.section_action_menu`,
|
||||
BULKSELECT: `[data-for='sectionBulkSelect']`,
|
||||
BULKCHECKBOX: `[data-bulkcheckbox]`,
|
||||
CHEVRON: `[data-for='sectiontoggler']`,
|
||||
};
|
||||
this.classes = {
|
||||
HIDE: 'd-none',
|
||||
@ -77,12 +72,27 @@ export default class extends DndSectionItem {
|
||||
getWatchers() {
|
||||
return [
|
||||
{watch: `bulk:updated`, handler: this._refreshBulk},
|
||||
{watch: `section[${this.id}].title:updated`, handler: this._refreshSectionBulkSelector},
|
||||
{watch: `section[${this.id}].title:updated`, handler: this._refreshSectionTitle},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the bulk checkbox when the topic name changes.
|
||||
* Update the section when the section name changes.
|
||||
*
|
||||
* The section header have several HTML that uses the section name
|
||||
* for accessibility and behat tests. This method updates them all.
|
||||
*
|
||||
* @param {object} param
|
||||
* @param {Object} param.element the section info
|
||||
*/
|
||||
_refreshSectionTitle(param) {
|
||||
const element = param.element;
|
||||
this.getElement(this.selectors.CHEVRON)?.setAttribute("aria-label", element.title);
|
||||
this._refreshSectionBulkSelector(param);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the bulk checkbox when the section name changes.
|
||||
*
|
||||
* @param {object} param
|
||||
* @param {Object} param.element the section info
|
||||
@ -92,7 +102,7 @@ export default class extends DndSectionItem {
|
||||
if (!checkbox) {
|
||||
return;
|
||||
}
|
||||
const newLabel = await getString('selectsection', 'core_courseformat', element.title);
|
||||
const newLabel = await this.reactive.getFormatString('selectsection', element.title);
|
||||
checkbox.title = newLabel;
|
||||
const label = this.getElement(`label[for='${checkbox.id}']`);
|
||||
if (label) {
|
||||
|
@ -13,6 +13,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import {get_string as getString} from 'core/str';
|
||||
import {Reactive} from 'core/reactive';
|
||||
import notification from 'core/notification';
|
||||
import Exporter from 'core_courseformat/local/courseeditor/exporter';
|
||||
@ -179,10 +180,34 @@ export default class extends Reactive {
|
||||
* @param {boolean} setup.editing if the page is in edit mode
|
||||
* @param {boolean} setup.supportscomponents if the format supports components for content
|
||||
* @param {string} setup.cacherev the backend cached state revision
|
||||
* @param {Array} setup.overriddenStrings optional overridden strings
|
||||
*/
|
||||
setViewFormat(setup) {
|
||||
this._editing = setup.editing ?? false;
|
||||
this._supportscomponents = setup.supportscomponents ?? false;
|
||||
const overriddenStrings = setup.overriddenStrings ?? [];
|
||||
this._overriddenStrings = overriddenStrings.reduce(
|
||||
(indexed, currentValue) => indexed.set(currentValue.key, currentValue),
|
||||
new Map()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a get string for a possible format overriden editor string.
|
||||
*
|
||||
* Return the proper getString promise for an editor string using the core_courseformat
|
||||
* of the format_PLUGINNAME compoment depending on the current view format setup.
|
||||
* @param {String} key the string key
|
||||
* @param {string|undefined} param The param for variable expansion in the string.
|
||||
* @returns {Promise<String>} a getString promise
|
||||
*/
|
||||
getFormatString(key, param) {
|
||||
if (this._overriddenStrings.has(key)) {
|
||||
const override = this._overriddenStrings.get(key);
|
||||
return getString(key, override.component ?? 'core_courseformat', param);
|
||||
}
|
||||
// All format overridable strings are from core_courseformat lang file.
|
||||
return getString(key, 'core_courseformat', param);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -793,6 +793,52 @@ abstract class base {
|
||||
return $blocknames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return custom strings for the course editor.
|
||||
*
|
||||
* This method is mainly used to translate the "section" related strings into
|
||||
* the specific format plugins name such as "Topics" or "Weeks".
|
||||
*
|
||||
* @return stdClass[] an array of objects with string "component" and "key"
|
||||
*/
|
||||
public function get_editor_custom_strings(): array {
|
||||
$result = [];
|
||||
$stringmanager = get_string_manager();
|
||||
$component = 'format_' . $this->format;
|
||||
$formatoverridbles = [
|
||||
'sectiondelete_title',
|
||||
'sectiondelete_info',
|
||||
'sectionsdelete_title',
|
||||
'sectionsdelete_info',
|
||||
'selectsection'
|
||||
];
|
||||
foreach ($formatoverridbles as $key) {
|
||||
if ($stringmanager->string_exists($key, $component)) {
|
||||
$result[] = (object)['component' => $component, 'key' => $key];
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the proper format plugin string if it exists.
|
||||
*
|
||||
* If the format_PLUGINNAME does not provide a valid string,
|
||||
* core_courseformat will be user as the component.
|
||||
*
|
||||
* @param string $key the string key
|
||||
* @param string|object|array $data extra data that can be used within translation strings
|
||||
* @param string|null $lang moodle translation language, null means use current
|
||||
* @return string the get_string result
|
||||
*/
|
||||
public function get_format_string(string $key, $data = null, $lang = null): string {
|
||||
$component = 'format_' . $this->get_format();
|
||||
if (!get_string_manager()->string_exists($key, $component)) {
|
||||
$component = 'core_courseformat';
|
||||
}
|
||||
return get_string($key, $component, $data, $lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the localised name of this course format plugin
|
||||
*
|
||||
|
@ -158,7 +158,7 @@ class bulkedittools implements named_templatable, renderable {
|
||||
'icon' => 'i/delete',
|
||||
'action' => 'deleteSection',
|
||||
'name' => get_string('delete'),
|
||||
'title' => get_string('sectionsdelete', 'core_courseformat'),
|
||||
'title' => $this->format->get_format_string('sectionsdelete'),
|
||||
'bulk' => 'section',
|
||||
];
|
||||
}
|
||||
|
@ -106,6 +106,7 @@ class header implements named_templatable, renderable {
|
||||
}
|
||||
}
|
||||
$data->name = get_section_name($course, $section);
|
||||
$data->selecttext = $format->get_format_string('selectsection', $data->name);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
@ -34,6 +34,9 @@
|
||||
data-bulkcheckbox="1"
|
||||
/>
|
||||
<label class="sr-only" for="sectionCheckbox{{id}}">
|
||||
{{#str}} selectsection, core_courseformat, {{name}}{{/str}}
|
||||
{{#selecttext}} {{selecttext}} {{/selecttext}}
|
||||
{{^selecttext}}
|
||||
{{#str}} selectsection, core_courseformat, {{name}}{{/str}}
|
||||
{{/selecttext}}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -46,15 +46,17 @@
|
||||
{{/sitehome}}
|
||||
{{^sitehome}}
|
||||
<div class="d-flex align-items-start position-relative">
|
||||
<a role="button" data-toggle="collapse"
|
||||
href="#coursecontentcollapse{{num}}"
|
||||
id="collapssesection{{num}}"
|
||||
aria-expanded="{{^contentcollapsed}}true{{/contentcollapsed}}{{#contentcollapsed}}false{{/contentcollapsed}}"
|
||||
aria-controls="coursecontentcollapse{{num}}"
|
||||
class="btn btn-icon mr-1 icons-collapse-expand justify-content-center
|
||||
{{^editing}} stretched-link {{/editing}}
|
||||
{{#contentcollapsed}} collapsed {{/contentcollapsed}}"
|
||||
aria-label="{{name}}">
|
||||
<a role="button"
|
||||
data-toggle="collapse"
|
||||
data-for="sectiontoggler"
|
||||
href="#coursecontentcollapse{{num}}"
|
||||
id="collapssesection{{num}}"
|
||||
aria-expanded="{{^contentcollapsed}}true{{/contentcollapsed}}{{#contentcollapsed}}false{{/contentcollapsed}}"
|
||||
aria-controls="coursecontentcollapse{{num}}"
|
||||
class="btn btn-icon mr-1 icons-collapse-expand justify-content-center
|
||||
{{^editing}} stretched-link {{/editing}}
|
||||
{{#contentcollapsed}} collapsed {{/contentcollapsed}}"
|
||||
aria-label="{{name}}">
|
||||
<span class="expanded-icon icon-no-margin p-2" title="{{#str}} collapse, core {{/str}}">
|
||||
{{#pix}} t/expandedchevron, core {{/pix}}
|
||||
</span>
|
||||
|
@ -475,6 +475,68 @@ class base_test extends advanced_testcase {
|
||||
$newmodcount = $DB->count_records('course_modules', ['course' => $course->id, 'section' => $newsection->id]);
|
||||
$this->assertEquals($originalmodcount, $newmodcount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for the default delete format data behaviour.
|
||||
*
|
||||
* @covers ::get_format_string
|
||||
* @dataProvider get_format_string_provider
|
||||
* @param string $key the string key
|
||||
* @param string|null $data any string data
|
||||
* @param array|null $expectedstring the expected string (null for exception)
|
||||
*/
|
||||
public function test_get_format_string(string $key, ?string $data, ?array $expectedstring) {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course(['format' => 'topics']);
|
||||
|
||||
if ($expectedstring) {
|
||||
$expected = get_string($expectedstring[0], $expectedstring[1], $expectedstring[2]);
|
||||
} else {
|
||||
$this->expectException(\coding_exception::class);
|
||||
}
|
||||
$format = course_get_format($course);
|
||||
$result = $format->get_format_string($key, $data);
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_get_format_string.
|
||||
*
|
||||
* @return array the testing scenarios
|
||||
*/
|
||||
public function get_format_string_provider(): array {
|
||||
return [
|
||||
'Existing in format lang' => [
|
||||
'key' => 'sectionsdelete',
|
||||
'data' => null,
|
||||
'expectedstring' => ['sectionsdelete', 'format_topics', null],
|
||||
],
|
||||
'Not existing in format lang' => [
|
||||
'key' => 'bulkedit',
|
||||
'data' => null,
|
||||
'expectedstring' => ['bulkedit', 'core_courseformat', null],
|
||||
],
|
||||
'Existing in format lang with data' => [
|
||||
'key' => 'selectsection',
|
||||
'data' => 'Example',
|
||||
'expectedstring' => ['selectsection', 'format_topics', 'Example'],
|
||||
],
|
||||
'Not existing in format lang with data' => [
|
||||
'key' => 'bulkselection',
|
||||
'data' => 'X',
|
||||
'expectedstring' => ['bulkselection', 'core_courseformat', 'X'],
|
||||
],
|
||||
'Non existing string' => [
|
||||
'key' => '%&non_existing_string_in_lang_files$%@#',
|
||||
'data' => null,
|
||||
'expectedstring' => null,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -9,7 +9,7 @@ Feature: Bulk course activity actions.
|
||||
| fullname | Course 1 |
|
||||
| shortname | C1 |
|
||||
| category | 0 |
|
||||
| numsections | 2 |
|
||||
| numsections | 4 |
|
||||
And the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber | section |
|
||||
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
|
||||
@ -109,4 +109,19 @@ Feature: Bulk course activity actions.
|
||||
And I should see "Activity sample 3" in the "Topic 2" "section"
|
||||
And I should see "Activity sample 3 (copy)" in the "Topic 2" "section"
|
||||
And "Activity sample 3 (copy)" "activity" should appear after "Activity sample 3" "activity"
|
||||
|
||||
Scenario: Bulk delete activities
|
||||
Given I should see "Activity sample 1" in the "Topic 1" "section"
|
||||
And I should see "Activity sample 2" in the "Topic 1" "section"
|
||||
And I should see "Activity sample 3" in the "Topic 2" "section"
|
||||
And I should see "Activity sample 4" in the "Topic 2" "section"
|
||||
And I click on "Select activity Activity sample 1" "checkbox"
|
||||
And I click on "Select activity Activity sample 3" "checkbox"
|
||||
And I should see "2 selected" in the "sticky-footer" "region"
|
||||
When I click on "Delete activities" "button" in the "sticky-footer" "region"
|
||||
And I click on "Delete" "button" in the "Delete selected activities?" "dialogue"
|
||||
Then I should not see "Activity sample 1" in the "Topic 1" "section"
|
||||
And I should see "Activity sample 2" in the "Topic 1" "section"
|
||||
And I should not see "Activity sample 3" in the "Topic 2" "section"
|
||||
And I should see "Activity sample 4" in the "Topic 2" "section"
|
||||
And I should see "0 selected" in the "sticky-footer" "region"
|
||||
|
@ -38,8 +38,8 @@ Feature: Bulk course section actions.
|
||||
And I should not see "Hidden from students" in the "Topic 2" "section"
|
||||
And I should not see "Hidden from students" in the "Topic 3" "section"
|
||||
And I should not see "Hidden from students" in the "Topic 4" "section"
|
||||
When I click on "Select section Topic 1" "checkbox"
|
||||
And I click on "Select section Topic 2" "checkbox"
|
||||
When I click on "Select topic Topic 1" "checkbox"
|
||||
And I click on "Select topic Topic 2" "checkbox"
|
||||
And I should see "2 selected" in the "sticky-footer" "region"
|
||||
And I click on "Section availability" "button" in the "sticky-footer" "region"
|
||||
And I click on "Hide on course page" "radio" in the "Availability" "dialogue"
|
||||
@ -55,8 +55,8 @@ Feature: Bulk course section actions.
|
||||
And I should see "0 selected" in the "sticky-footer" "region"
|
||||
|
||||
Scenario: Bulk show sections
|
||||
Given I click on "Select section Topic 1" "checkbox"
|
||||
Given I click on "Select section Topic 3" "checkbox"
|
||||
Given I click on "Select topic Topic 1" "checkbox"
|
||||
Given I click on "Select topic Topic 3" "checkbox"
|
||||
And I click on "Section availability" "button" in the "sticky-footer" "region"
|
||||
And I click on "Hide on course page" "radio" in the "Availability" "dialogue"
|
||||
And I click on "Apply" "button" in the "Availability" "dialogue"
|
||||
@ -68,8 +68,8 @@ Feature: Bulk course section actions.
|
||||
And I should not see "Hidden from students" in the "Topic 2" "section"
|
||||
And I should see "Hidden from students" in the "Topic 3" "section"
|
||||
And I should not see "Hidden from students" in the "Topic 4" "section"
|
||||
When I click on "Select section Topic 1" "checkbox"
|
||||
And I click on "Select section Topic 2" "checkbox"
|
||||
When I click on "Select topic Topic 1" "checkbox"
|
||||
And I click on "Select topic Topic 2" "checkbox"
|
||||
And I should see "2 selected" in the "sticky-footer" "region"
|
||||
And I click on "Section availability" "button" in the "sticky-footer" "region"
|
||||
And I click on "Show on course page" "radio" in the "Availability" "dialogue"
|
||||
@ -82,4 +82,75 @@ Feature: Bulk course section actions.
|
||||
And I should not see "Hidden from students" in the "Topic 2" "section"
|
||||
And I should see "Hidden from students" in the "Topic 3" "section"
|
||||
And I should not see "Hidden from students" in the "Topic 4" "section"
|
||||
|
||||
Scenario: Bulk delete sections with content ask for confirmation
|
||||
Given I should see "Topic 1" in the "region-main" "region"
|
||||
And I should see "Topic 2" in the "region-main" "region"
|
||||
And I should see "Topic 3" in the "region-main" "region"
|
||||
And I should see "Topic 4" in the "region-main" "region"
|
||||
And I should see "Activity sample 1" in the "Topic 1" "section"
|
||||
And I should see "Activity sample 2" in the "Topic 1" "section"
|
||||
And I should see "Activity sample 3" in the "Topic 2" "section"
|
||||
And I should see "Activity sample 4" in the "Topic 2" "section"
|
||||
And I click on "Select topic Topic 1" "checkbox"
|
||||
And I click on "Select topic Topic 2" "checkbox"
|
||||
And I should see "2 selected" in the "sticky-footer" "region"
|
||||
When I click on "Delete topics" "button" in the "sticky-footer" "region"
|
||||
And I click on "Delete" "button" in the "Delete selected topics?" "dialogue"
|
||||
Then I should see "Topic 1" in the "region-main" "region"
|
||||
And I should see "Topic 2" in the "region-main" "region"
|
||||
And I should not see "Topic 3" in the "region-main" "region"
|
||||
And I should not see "Topic 4" in the "region-main" "region"
|
||||
And I should not see "Activity sample 1" in the "Topic 1" "section"
|
||||
And I should not see "Activity sample 2" in the "Topic 1" "section"
|
||||
And I should not see "Activity sample 3" in the "Topic 2" "section"
|
||||
And I should not see "Activity sample 4" in the "Topic 2" "section"
|
||||
And I should see "0 selected" in the "sticky-footer" "region"
|
||||
|
||||
Scenario: Bulk delete sections without content does not ask for confirmation
|
||||
Given I should see "Topic 1" in the "region-main" "region"
|
||||
And I should see "Topic 2" in the "region-main" "region"
|
||||
And I should see "Topic 3" in the "region-main" "region"
|
||||
And I should see "Topic 4" in the "region-main" "region"
|
||||
And I should see "Activity sample 1" in the "Topic 1" "section"
|
||||
And I should see "Activity sample 2" in the "Topic 1" "section"
|
||||
And I should see "Activity sample 3" in the "Topic 2" "section"
|
||||
And I should see "Activity sample 4" in the "Topic 2" "section"
|
||||
And I click on "Select topic Topic 3" "checkbox"
|
||||
And I click on "Select topic Topic 4" "checkbox"
|
||||
And I should see "2 selected" in the "sticky-footer" "region"
|
||||
When I click on "Delete topics" "button" in the "sticky-footer" "region"
|
||||
Then I should see "Topic 1" in the "region-main" "region"
|
||||
And I should see "Topic 2" in the "region-main" "region"
|
||||
And I should not see "Topic 3" in the "region-main" "region"
|
||||
And I should not see "Topic 4" in the "region-main" "region"
|
||||
And I should see "Activity sample 1" in the "Topic 1" "section"
|
||||
And I should see "Activity sample 2" in the "Topic 1" "section"
|
||||
And I should see "Activity sample 3" in the "Topic 2" "section"
|
||||
And I should see "Activity sample 4" in the "Topic 2" "section"
|
||||
And I should see "0 selected" in the "sticky-footer" "region"
|
||||
|
||||
Scenario: Bulk delete both section with content and empty section ask for confirmation
|
||||
Given I should see "Topic 1" in the "region-main" "region"
|
||||
And I should see "Topic 2" in the "region-main" "region"
|
||||
And I should see "Topic 3" in the "region-main" "region"
|
||||
And I should see "Topic 4" in the "region-main" "region"
|
||||
And I should see "Activity sample 1" in the "Topic 1" "section"
|
||||
And I should see "Activity sample 2" in the "Topic 1" "section"
|
||||
And I should see "Activity sample 3" in the "Topic 2" "section"
|
||||
And I should see "Activity sample 4" in the "Topic 2" "section"
|
||||
And I click on "Select topic Topic 2" "checkbox"
|
||||
And I click on "Select topic Topic 3" "checkbox"
|
||||
And I should see "2 selected" in the "sticky-footer" "region"
|
||||
When I click on "Delete topics" "button" in the "sticky-footer" "region"
|
||||
And I click on "Delete" "button" in the "Delete selected topics?" "dialogue"
|
||||
Then I should see "Topic 1" in the "region-main" "region"
|
||||
And I should see "Topic 2" in the "region-main" "region"
|
||||
And I should not see "Topic 3" in the "region-main" "region"
|
||||
And I should not see "Topic 4" in the "region-main" "region"
|
||||
And I should see "Activity sample 1" in the "Topic 1" "section"
|
||||
And I should see "Activity sample 1" in the "Topic 1" "section"
|
||||
And I should see "Activity sample 2" in the "Topic 1" "section"
|
||||
And I should not see "Activity sample 3" in the "Topic 2" "section"
|
||||
And I should not see "Activity sample 4" in the "Topic 2" "section"
|
||||
And I should see "0 selected" in the "sticky-footer" "region"
|
||||
|
@ -28,7 +28,7 @@ Feature: Bulk activity and section selection.
|
||||
Scenario: Enable and disable bulk editing
|
||||
When I click on "Bulk edit" "button"
|
||||
Then I should see "0 selected" in the "sticky-footer" "region"
|
||||
And the focused element is "Select section Topic 1" "checkbox"
|
||||
And the focused element is "Select topic Topic 1" "checkbox"
|
||||
And I click on "Close bulk edit" "button" in the "sticky-footer" "region"
|
||||
And "sticky-footer" "region" should not be visible
|
||||
And the focused element is "Bulk edit" "button"
|
||||
@ -38,12 +38,12 @@ Feature: Bulk activity and section selection.
|
||||
And I should see "0 selected" in the "sticky-footer" "region"
|
||||
When I click on "Select activity Activity sample 1" "checkbox"
|
||||
And I should see "1 selected" in the "sticky-footer" "region"
|
||||
Then the "Select section Topic 1" "checkbox" should be disabled
|
||||
Then the "Select topic Topic 1" "checkbox" should be disabled
|
||||
|
||||
Scenario: Selecting sections disable activity selection
|
||||
Given I click on "Bulk edit" "button"
|
||||
And I should see "0 selected" in the "sticky-footer" "region"
|
||||
When I click on "Select section Topic 1" "checkbox"
|
||||
When I click on "Select topic Topic 1" "checkbox"
|
||||
And I should see "1 selected" in the "sticky-footer" "region"
|
||||
Then the "Select activity Activity sample 1" "checkbox" should be disabled
|
||||
|
||||
@ -66,7 +66,7 @@ Feature: Bulk activity and section selection.
|
||||
Scenario: Select all is disabled until a section is selected
|
||||
Given I click on "Bulk edit" "button"
|
||||
And the "Select all" "checkbox" should be disabled
|
||||
When I click on "Select section Topic 1" "checkbox"
|
||||
When I click on "Select topic Topic 1" "checkbox"
|
||||
And I should see "1 selected" in the "sticky-footer" "region"
|
||||
Then the "Select all" "checkbox" should be enabled
|
||||
|
||||
@ -80,7 +80,7 @@ Feature: Bulk activity and section selection.
|
||||
|
||||
Scenario: Select all when a section is selected will select all sections
|
||||
Given I click on "Bulk edit" "button"
|
||||
And I click on "Select section Topic 1" "checkbox"
|
||||
And I click on "Select topic Topic 1" "checkbox"
|
||||
And I should see "1 selected" in the "sticky-footer" "region"
|
||||
And the "Select all" "checkbox" should be enabled
|
||||
When I click on "Select all" "checkbox" in the "sticky-footer" "region"
|
||||
@ -88,13 +88,13 @@ Feature: Bulk activity and section selection.
|
||||
|
||||
Scenario: Click on a select all with all sections selected unselects all sections
|
||||
Given I click on "Bulk edit" "button"
|
||||
And I click on "Select section Topic 1" "checkbox"
|
||||
And I click on "Select section Topic 2" "checkbox"
|
||||
And I click on "Select topic Topic 1" "checkbox"
|
||||
And I click on "Select topic Topic 2" "checkbox"
|
||||
And I should see "2 selected" in the "sticky-footer" "region"
|
||||
And the "Select all" "checkbox" should be enabled
|
||||
When I click on "Select all" "checkbox" in the "sticky-footer" "region"
|
||||
Then I should see "0 selected" in the "sticky-footer" "region"
|
||||
And the focused element is "Select section Topic 1" "checkbox"
|
||||
And the focused element is "Select topic Topic 1" "checkbox"
|
||||
|
||||
Scenario: Click on a select all with all activity selected unselects all activities
|
||||
Given I click on "Bulk edit" "button"
|
||||
@ -106,7 +106,7 @@ Feature: Bulk activity and section selection.
|
||||
And the "Select all" "checkbox" should be enabled
|
||||
When I click on "Select all" "checkbox" in the "sticky-footer" "region"
|
||||
Then I should see "0 selected" in the "sticky-footer" "region"
|
||||
And the focused element is "Select section Topic 1" "checkbox"
|
||||
And the focused element is "Select topic Topic 1" "checkbox"
|
||||
|
||||
Scenario: Click an activity name in bulk mode select and unselects the activity
|
||||
Given I click on "Bulk edit" "button"
|
||||
|
@ -24,17 +24,22 @@
|
||||
|
||||
$string['addsections'] = 'Add topic';
|
||||
$string['currentsection'] = 'This topic';
|
||||
$string['deletesection'] = 'Delete topic';
|
||||
$string['editsection'] = 'Edit topic';
|
||||
$string['editsectionname'] = 'Edit topic name';
|
||||
$string['deletesection'] = 'Delete topic';
|
||||
$string['hidefromothers'] = 'Hide topic';
|
||||
$string['newsectionname'] = 'New name for topic {$a}';
|
||||
$string['sectionname'] = 'Topic';
|
||||
$string['pluginname'] = 'Topics format';
|
||||
$string['section0name'] = 'General';
|
||||
$string['page-course-view-topics'] = 'Any course main page in topics format';
|
||||
$string['page-course-view-topics-x'] = 'Any course page in topics format';
|
||||
$string['hidefromothers'] = 'Hide topic';
|
||||
$string['showfromothers'] = 'Show topic';
|
||||
$string['pluginname'] = 'Topics format';
|
||||
$string['privacy:metadata'] = 'The Topics format plugin does not store any personal data.';
|
||||
$string['indentation'] = 'Allow indentation on course page';
|
||||
$string['indentation_help'] = 'Allow teachers, and other users with the manage activities capability, to indent items on the course page.';
|
||||
$string['section0name'] = 'General';
|
||||
$string['sectiondelete_title'] = 'Delete topic?';
|
||||
$string['sectionname'] = 'Topic';
|
||||
$string['sectionsdelete'] = 'Delete topics';
|
||||
$string['sectionsdelete_info'] = 'This will delete {$a->count} topics and all the activities they contain.';
|
||||
$string['sectionsdelete_title'] = 'Delete selected topics?';
|
||||
$string['selectsection'] = 'Select topic {$a}';
|
||||
$string['showfromothers'] = 'Show topic';
|
||||
|
@ -25,6 +25,11 @@ Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
|
||||
should be used by format plugins to determine if a section should be rendered collapsed or expanded at first.
|
||||
This method can also be overwritten by course formats, but should respect the new 'expandsection' parameter when
|
||||
doing so.
|
||||
* New core_courseformat\base::get_editor_custom_strings() method to customize course editor strings.
|
||||
The returned string array is used to override section related strings in the frontend.
|
||||
* New core_courseformat\base::get_format_string() to get strings that can be overridden by the format plugin.
|
||||
The method will check first the string in the format_PLUGINNAME.php lang file and, if not, it will return the
|
||||
core_courseformat string instead.
|
||||
|
||||
=== 4.1 ===
|
||||
* New \core_courseformat\stateupdates methods add_section_remove() and add_cm_remove() have been added to replace
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
@ -24,20 +23,25 @@
|
||||
*/
|
||||
|
||||
$string['addsections'] = 'Add week';
|
||||
$string['currentsection'] = 'This week';
|
||||
$string['editsection'] = 'Edit week';
|
||||
$string['editsectionname'] = 'Edit week name';
|
||||
$string['deletesection'] = 'Delete week';
|
||||
$string['newsectionname'] = 'New name for week {$a}';
|
||||
$string['sectionname'] = 'Week';
|
||||
$string['pluginname'] = 'Weekly format';
|
||||
$string['section0name'] = 'General';
|
||||
$string['page-course-view-weeks'] = 'Any course main page in weeks format';
|
||||
$string['page-course-view-weeks-x'] = 'Any course page in weeks format';
|
||||
$string['hidefromothers'] = 'Hide week';
|
||||
$string['showfromothers'] = 'Show week';
|
||||
$string['automaticenddate'] = 'Calculate the end date from the number of sections';
|
||||
$string['automaticenddate_help'] = 'If enabled, the end date for the course will be automatically calculated from the number of sections and the course start date.';
|
||||
$string['currentsection'] = 'This week';
|
||||
$string['deletesection'] = 'Delete week';
|
||||
$string['editsection'] = 'Edit week';
|
||||
$string['editsectionname'] = 'Edit week name';
|
||||
$string['hidefromothers'] = 'Hide week';
|
||||
$string['newsectionname'] = 'New name for week {$a}';
|
||||
$string['page-course-view-weeks'] = 'Any course main page in weeks format';
|
||||
$string['page-course-view-weeks-x'] = 'Any course page in weeks format';
|
||||
$string['pluginname'] = 'Weekly format';
|
||||
$string['privacy:metadata'] = 'The Weekly format plugin does not store any personal data.';
|
||||
$string['indentation'] = 'Allow indentation on course page';
|
||||
$string['indentation_help'] = 'Allow teachers, and other users with the manage activities capability, to indent items on the course page.';
|
||||
$string['section0name'] = 'General';
|
||||
$string['sectiondelete_title'] = 'Delete week?';
|
||||
$string['sectionname'] = 'Week';
|
||||
$string['sectionsdelete'] = 'Delete weeks';
|
||||
$string['sectionsdelete_info'] = 'This will delete {$a->count} weeks and all the activities they contain.';
|
||||
$string['sectionsdelete_title'] = 'Delete selected weeks?';
|
||||
$string['selectsection'] = 'Select week {$a}';
|
||||
$string['showfromothers'] = 'Show week';
|
||||
|
81
course/format/weeks/tests/behat/bulk_actions.feature
Normal file
81
course/format/weeks/tests/behat/bulk_actions.feature
Normal file
@ -0,0 +1,81 @@
|
||||
@core @core_courseformat @core_course @format_weeks @show_editor
|
||||
Feature: Weeks format bulk activity actions.
|
||||
In order to edit the course weeks
|
||||
As a teacher
|
||||
I need to be able to edit weeks in bulk.
|
||||
|
||||
Background:
|
||||
Given the following "course" exists:
|
||||
| fullname | Course 1 |
|
||||
| shortname | C1 |
|
||||
| category | 0 |
|
||||
| numsections | 4 |
|
||||
| format | weeks |
|
||||
| startdate | 957139200 |
|
||||
And the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber | section |
|
||||
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
|
||||
| assign | Activity sample 2 | Test assignment description | C1 | sample2 | 1 |
|
||||
| assign | Activity sample 3 | Test assignment description | C1 | sample3 | 2 |
|
||||
| assign | Activity sample 4 | Test assignment description | C1 | sample4 | 2 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And I am on the "C1" "Course" page logged in as "teacher1"
|
||||
And I turn editing mode on
|
||||
And I click on "Bulk edit" "button"
|
||||
And I should see "0 selected" in the "sticky-footer" "region"
|
||||
|
||||
@javascript
|
||||
Scenario: Delete a single week using bulk action
|
||||
Given I should see "1 May - 7 May" in the "region-main" "region"
|
||||
And I should see "8 May - 14 May" in the "region-main" "region"
|
||||
And I should see "15 May - 21 May" in the "region-main" "region"
|
||||
And I should see "22 May - 28 May" in the "region-main" "region"
|
||||
And I should see "Activity sample 1" in the "1 May - 7 May" "section"
|
||||
And I should see "Activity sample 2" in the "1 May - 7 May" "section"
|
||||
And I should see "Activity sample 3" in the "8 May - 14 May" "section"
|
||||
And I should see "Activity sample 4" in the "8 May - 14 May" "section"
|
||||
And I click on "Select week 1 May - 7 May" "checkbox"
|
||||
And I click on "Select week 8 May - 14 May" "checkbox"
|
||||
And I should see "2 selected" in the "sticky-footer" "region"
|
||||
When I click on "Delete weeks" "button" in the "sticky-footer" "region"
|
||||
And I click on "Delete" "button" in the ".modal" "css_element"
|
||||
Then I should see "1 May - 7 May" in the "region-main" "region"
|
||||
And I should see "8 May - 14 May" in the "region-main" "region"
|
||||
And I should not see "15 May - 21 May" in the "region-main" "region"
|
||||
And I should not see "22 May - 28 May" in the "region-main" "region"
|
||||
And I should not see "Activity sample 1" in the "1 May - 7 May" "section"
|
||||
And I should not see "Activity sample 2" in the "1 May - 7 May" "section"
|
||||
And I should not see "Activity sample 3" in the "8 May - 14 May" "section"
|
||||
And I should not see "Activity sample 4" in the "8 May - 14 May" "section"
|
||||
And I should see "0 selected" in the "sticky-footer" "region"
|
||||
|
||||
@javascript
|
||||
Scenario: Delete several weeks in bulk
|
||||
Given I should see "1 May - 7 May" in the "region-main" "region"
|
||||
And I should see "8 May - 14 May" in the "region-main" "region"
|
||||
And I should see "15 May - 21 May" in the "region-main" "region"
|
||||
And I should see "22 May - 28 May" in the "region-main" "region"
|
||||
And I should see "Activity sample 1" in the "1 May - 7 May" "section"
|
||||
And I should see "Activity sample 2" in the "1 May - 7 May" "section"
|
||||
And I should see "Activity sample 3" in the "8 May - 14 May" "section"
|
||||
And I should see "Activity sample 4" in the "8 May - 14 May" "section"
|
||||
And I click on "Select week 8 May - 14 May" "checkbox"
|
||||
And I click on "Select week 15 May - 21 May" "checkbox"
|
||||
And I should see "2 selected" in the "sticky-footer" "region"
|
||||
When I click on "Delete weeks" "button" in the "sticky-footer" "region"
|
||||
And I click on "Delete" "button" in the ".modal" "css_element"
|
||||
Then I should see "1 May - 7 May" in the "region-main" "region"
|
||||
And I should see "8 May - 14 May" in the "region-main" "region"
|
||||
And I should not see "15 May - 21 May" in the "region-main" "region"
|
||||
And I should not see "22 May - 28 May" in the "region-main" "region"
|
||||
And I should see "Activity sample 1" in the "1 May - 7 May" "section"
|
||||
And I should see "Activity sample 1" in the "1 May - 7 May" "section"
|
||||
And I should see "Activity sample 2" in the "1 May - 7 May" "section"
|
||||
And I should not see "Activity sample 3" in the "8 May - 14 May" "section"
|
||||
And I should not see "Activity sample 4" in the "8 May - 14 May" "section"
|
||||
And I should see "0 selected" in the "sticky-footer" "region"
|
@ -3199,6 +3199,7 @@ function include_course_editor(course_format $format) {
|
||||
'editing' => $format->show_editor(),
|
||||
'supportscomponents' => $format->supports_components(),
|
||||
'statekey' => $statekey,
|
||||
'overriddenStrings' => $format->get_editor_custom_strings(),
|
||||
];
|
||||
// All the new editor elements will be loaded after the course is presented and
|
||||
// the initial course state will be generated using core_course_get_state webservice.
|
||||
|
@ -1038,8 +1038,14 @@ class behat_course extends behat_base {
|
||||
// Not using chain steps here because the exceptions catcher have problems detecting
|
||||
// JS modal windows and avoiding interacting them at the same time.
|
||||
if ($this->running_javascript()) {
|
||||
$this->execute('behat_general::i_click_on_in_the',
|
||||
array(get_string('delete'), "button", "Confirm", "dialogue")
|
||||
$this->execute(
|
||||
'behat_general::i_click_on_in_the',
|
||||
[
|
||||
get_string('delete'),
|
||||
"button",
|
||||
get_string('cmdelete_title', 'core_courseformat'),
|
||||
"dialogue"
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$this->execute("behat_forms::press_button", get_string('yes'));
|
||||
|
@ -33,8 +33,8 @@ $string['bulkeditoff'] = 'Close bulk edit';
|
||||
$string['bulkcancel'] = 'Close bulk editing';
|
||||
$string['bulkselection'] = '{$a} selected';
|
||||
$string['cmavailability'] = 'Activity availability';
|
||||
$string['cmdelete_info'] = 'This will delete "{$a->name}" and any user data it contains';
|
||||
$string['cmdelete_title'] = 'Delete activity?';
|
||||
$string['cmdelete_info'] = 'This will delete the {$a->type} "{$a->name}" and any user data it contains';
|
||||
$string['cmsdelete'] = 'Delete activities';
|
||||
$string['cmsdelete_info'] = 'This will delete {$a->count} activities and any user data they contain';
|
||||
$string['cmsdelete_title'] = 'Delete selected activities?';
|
||||
|
Loading…
x
Reference in New Issue
Block a user