MDL-66017 block_myoverview: filter by custom course field

This commit is contained in:
Davo Smith 2019-08-22 16:10:34 +01:00 committed by Davo Smith
parent a31719f91a
commit 8d166d7745
26 changed files with 939 additions and 32 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
define ("block_myoverview/view_nav",["jquery","core/custom_interaction_events","block_myoverview/repository","block_myoverview/view","block_myoverview/selectors"],function(a,b,c,d,f){var g={FILTERS:"[data-region=\"filter\"]",FILTER_OPTION:"[data-filter]",DISPLAY_OPTION:"[data-display-option]"},h=function(a,b){var d=null;if("display"==a){d="block_myoverview_user_view_preference"}else if("sort"==a){d="block_myoverview_user_sort_preference"}else{d="block_myoverview_user_grouping_preference"}c.updateUserPreferences({preferences:[{type:d,value:b}]})},i=function(c){var e=c.find(g.FILTERS);b.define(e,[b.events.activate]);e.on(b.events.activate,g.FILTER_OPTION,function(b,e){var g=a(b.target);if(g.hasClass("active")){return}var i=g.attr("data-filter"),j=g.attr("data-pref");c.find(f.courseView.region).attr("data-"+i,g.attr("data-value"));h(i,j);d.init(c);e.originalEvent.preventDefault()});b.define(e,[b.events.activate]);e.on(b.events.activate,g.DISPLAY_OPTION,function(b,e){var g=a(b.target);if(g.hasClass("active")){return}var i=g.attr("data-display-option"),j=g.attr("data-pref");c.find(f.courseView.region).attr("data-display",g.attr("data-value"));h(i,j);d.reset(c);e.originalEvent.preventDefault()})};return{init:function init(b){b=a(b);i(b)}}});
define ("block_myoverview/view_nav",["jquery","core/custom_interaction_events","block_myoverview/repository","block_myoverview/view","block_myoverview/selectors"],function(a,b,c,d,f){var g={FILTERS:"[data-region=\"filter\"]",FILTER_OPTION:"[data-filter]",DISPLAY_OPTION:"[data-display-option]"},h=function(a,b){var d=null;if("display"==a){d="block_myoverview_user_view_preference"}else if("sort"==a){d="block_myoverview_user_sort_preference"}else if("customfieldvalue"==a){d="block_myoverview_user_grouping_customfieldvalue_preference"}else{d="block_myoverview_user_grouping_preference"}c.updateUserPreferences({preferences:[{type:d,value:b}]})},i=function(c){var e=c.find(g.FILTERS);b.define(e,[b.events.activate]);e.on(b.events.activate,g.FILTER_OPTION,function(b,e){var g=a(b.target);if(g.hasClass("active")){return}var i=g.attr("data-filter"),j=g.attr("data-pref"),k=g.attr("data-customfieldvalue");c.find(f.courseView.region).attr("data-"+i,g.attr("data-value"));h(i,j);if(k){c.find(f.courseView.region).attr("data-customfieldvalue",k);h("customfieldvalue",k)}d.init(c);e.originalEvent.preventDefault()});b.define(e,[b.events.activate]);e.on(b.events.activate,g.DISPLAY_OPTION,function(b,e){var g=a(b.target);if(g.hasClass("active")){return}var i=g.attr("data-display-option"),j=g.attr("data-pref");c.find(f.courseView.region).attr("data-display",g.attr("data-value"));h(i,j);d.reset(c);e.originalEvent.preventDefault()})};return{init:function init(b){b=a(b);i(b)}}});
//# sourceMappingURL=view_nav.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -102,6 +102,8 @@ function(
grouping: courseRegion.attr('data-grouping'),
sort: courseRegion.attr('data-sort'),
displaycategories: courseRegion.attr('data-displaycategories'),
customfieldname: courseRegion.attr('data-customfieldname'),
customfieldvalue: courseRegion.attr('data-customfieldvalue'),
};
};
@ -126,7 +128,9 @@ function(
offset: courseOffset,
limit: limit,
classification: filters.grouping,
sort: filters.sort
sort: filters.sort,
customfieldname: filters.customfieldname,
customfieldvalue: filters.customfieldvalue
});
};

View File

@ -55,6 +55,8 @@ function(
type = 'block_myoverview_user_view_preference';
} else if (filter == 'sort') {
type = 'block_myoverview_user_sort_preference';
} else if (filter == 'customfieldvalue') {
type = 'block_myoverview_user_grouping_customfieldvalue_preference';
} else {
type = 'block_myoverview_user_grouping_preference';
}
@ -92,10 +94,16 @@ function(
var filter = option.attr('data-filter');
var pref = option.attr('data-pref');
var customfieldvalue = option.attr('data-customfieldvalue');
root.find(Selectors.courseView.region).attr('data-' + filter, option.attr('data-value'));
updatePreferences(filter, pref);
if (customfieldvalue) {
root.find(Selectors.courseView.region).attr('data-customfieldvalue', customfieldvalue);
updatePreferences('customfieldvalue', customfieldvalue);
}
// Reset the views.
View.init(root);

View File

@ -53,8 +53,9 @@ class block_myoverview extends block_base {
$sort = get_user_preferences('block_myoverview_user_sort_preference');
$view = get_user_preferences('block_myoverview_user_view_preference');
$paging = get_user_preferences('block_myoverview_user_paging_preference');
$customfieldvalue = get_user_preferences('block_myoverview_user_grouping_customfieldvalue_preference');
$renderable = new \block_myoverview\output\main($group, $sort, $view, $paging);
$renderable = new \block_myoverview\output\main($group, $sort, $view, $paging, $customfieldvalue);
$renderer = $this->page->get_renderer('block_myoverview');
$this->content = new stdClass();

View File

@ -130,6 +130,27 @@ class main implements renderable, templatable {
*/
private $displaygroupinghidden;
/**
* Store a course grouping option setting.
*
* @var bool
*/
private $displaygroupingcustomfield;
/**
* Store the custom field used by customfield grouping.
*
* @var string
*/
private $customfiltergrouping;
/**
* Store the selected custom field value to group by.
*
* @var string
*/
private $customfieldvalue;
/**
* main constructor.
* Initialize the user preferences
@ -137,10 +158,12 @@ class main implements renderable, templatable {
* @param string $grouping Grouping user preference
* @param string $sort Sort user preference
* @param string $view Display user preference
* @param int $paging
* @param string $customfieldvalue
*
* @throws \dml_exception
*/
public function __construct($grouping, $sort, $view, $paging) {
public function __construct($grouping, $sort, $view, $paging, $customfieldvalue = null) {
// Get plugin config.
$config = get_config('block_myoverview');
@ -153,28 +176,14 @@ class main implements renderable, templatable {
// Otherwise fall back to another grouping in a reasonable order.
// This is done to prevent one-time UI glitches in the case when a user has chosen a grouping option previously which
// was then disabled by the admin in the meantime.
} else if ($config->displaygroupingall == true) {
$this->grouping = BLOCK_MYOVERVIEW_GROUPING_ALL;
} else if ($config->displaygroupingallincludinghidden == true) {
$this->grouping = BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN;
} else if ($config->displaygroupinginprogress == true) {
$this->grouping = BLOCK_MYOVERVIEW_GROUPING_INPROGRESS;
} else if ($config->displaygroupingfuture == true) {
$this->grouping = BLOCK_MYOVERVIEW_GROUPING_FUTURE;
} else if ($config->displaygroupingpast == true) {
$this->grouping = BLOCK_MYOVERVIEW_GROUPING_PAST;
} else if ($config->displaygroupingstarred == true) {
$this->grouping = BLOCK_MYOVERVIEW_GROUPING_FAVOURITES;
} else if ($config->displaygroupinghidden == true) {
$this->grouping = BLOCK_MYOVERVIEW_GROUPING_HIDDEN;
// In this case, no grouping option is enabled and the grouping is not needed at all.
// But it's better not to leave $this->grouping unset for any unexpected case.
} else {
$this->grouping = BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN;
$this->grouping = $this->get_fallback_grouping($config);
}
unset ($groupingconfigname);
// Remember which custom field value we were using, if grouping by custom field.
$this->customfieldvalue = $customfieldvalue;
// Check and remember the given sorting.
$this->sort = $sort ? $sort : BLOCK_MYOVERVIEW_SORTING_TITLE;
@ -207,6 +216,8 @@ class main implements renderable, templatable {
$this->displaygroupingpast = $config->displaygroupingpast;
$this->displaygroupingstarred = $config->displaygroupingstarred;
$this->displaygroupinghidden = $config->displaygroupinghidden;
$this->displaygroupingcustomfield = ($config->displaygroupingcustomfield && $config->customfiltergrouping);
$this->customfiltergrouping = $config->customfiltergrouping;
// Check and remember if the grouping selector should be shown at all or not.
// It will be shown if more than 1 grouping option is enabled.
@ -218,7 +229,7 @@ class main implements renderable, templatable {
$this->displaygroupingstarred,
$this->displaygroupinghidden);
$displaygroupingselectorscount = count(array_filter($displaygroupingselectors));
if ($displaygroupingselectorscount > 1) {
if ($displaygroupingselectorscount > 1 || $this->displaygroupingcustomfield) {
$this->displaygroupingselector = true;
} else {
$this->displaygroupingselector = false;
@ -226,6 +237,41 @@ class main implements renderable, templatable {
unset ($displaygroupingselectors, $displaygroupingselectorscount);
}
/**
* Determine the most sensible fallback grouping to use (in cases where the stored selection
* is no longer available).
* @param object $config
* @return string
*/
private function get_fallback_grouping($config) {
if ($config->displaygroupingall == true) {
return BLOCK_MYOVERVIEW_GROUPING_ALL;
}
if ($config->displaygroupingallincludinghidden == true) {
return BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN;
}
if ($config->displaygroupinginprogress == true) {
return BLOCK_MYOVERVIEW_GROUPING_INPROGRESS;
}
if ($config->displaygroupingfuture == true) {
return BLOCK_MYOVERVIEW_GROUPING_FUTURE;
}
if ($config->displaygroupingpast == true) {
return BLOCK_MYOVERVIEW_GROUPING_PAST;
}
if ($config->displaygroupingstarred == true) {
return BLOCK_MYOVERVIEW_GROUPING_FAVOURITES;
}
if ($config->displaygroupinghidden == true) {
return BLOCK_MYOVERVIEW_GROUPING_HIDDEN;
}
if ($config->displaygroupingcustomfield == true) {
return BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD;
}
// In this case, no grouping option is enabled and the grouping is not needed at all.
// But it's better not to leave $this->grouping unset for any unexpected case.
return BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN;
}
/**
* Set the available layouts based on the config table settings,
@ -292,6 +338,49 @@ class main implements renderable, templatable {
}
/**
* Get the list of values to add to the grouping dropdown
* @return object[] containing name, value and active fields
*/
public function get_customfield_values_for_export() {
global $DB, $USER;
if (!$this->displaygroupingcustomfield) {
return [];
}
$fieldid = $DB->get_field('customfield_field', 'id', ['shortname' => $this->customfiltergrouping]);
if (!$fieldid) {
return [];
}
$courses = enrol_get_all_users_courses($USER->id, true);
if (!$courses) {
return [];
}
list($csql, $params) = $DB->get_in_or_equal(array_keys($courses), SQL_PARAMS_NAMED);
$select = "instanceid $csql AND fieldid = :fieldid";
$params['fieldid'] = $fieldid;
$values = $DB->get_records_select_menu('customfield_data', $select, $params, 'value',
'DISTINCT value, value AS value2');
$values = array_filter($values);
if (!$values) {
return [];
}
$field = \core_customfield\field_controller::create($fieldid);
if (!$field->supports_course_grouping()) {
return []; // The field shouldn't have been selectable in the global settings, but just skip it now.
}
$values = $field->course_grouping_format_values($values);
$customfieldactive = ($this->grouping === BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD);
$ret = [];
foreach ($values as $value => $name) {
$ret[] = (object)[
'name' => $name,
'value' => $value,
'active' => ($customfieldactive && ($this->customfieldvalue == $value)),
];
}
return $ret;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
@ -305,6 +394,29 @@ class main implements renderable, templatable {
$nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
$customfieldvalues = $this->get_customfield_values_for_export();
$selectedcustomfield = '';
if ($this->grouping == BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD) {
foreach ($customfieldvalues as $field) {
if ($field->value == $this->customfieldvalue) {
$selectedcustomfield = $field->name;
break;
}
}
// If the selected custom field value has not been found (possibly because the field has
// been changed in the settings) find a suitable fallback.
if (!$selectedcustomfield) {
$this->grouping = $this->get_fallback_grouping(get_config('block_myoverview'));
if ($this->grouping == BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD) {
// If the fallback grouping is still customfield, then select the first field.
$firstfield = reset($customfieldvalues);
if ($firstfield) {
$selectedcustomfield = $firstfield->name;
$this->customfieldvalue = $firstfield->value;
}
}
}
}
$preferences = $this->get_preferences_as_booleans();
$availablelayouts = $this->get_formatted_available_layouts_for_export();
@ -327,8 +439,13 @@ class main implements renderable, templatable {
'displaygroupingstarred' => $this->displaygroupingstarred,
'displaygroupinghidden' => $this->displaygroupinghidden,
'displaygroupingselector' => $this->displaygroupingselector,
'displaygroupingcustomfield' => $this->displaygroupingcustomfield && $customfieldvalues,
'customfieldname' => $this->customfiltergrouping,
'customfieldvalue' => $this->customfieldvalue,
'customfieldvalues' => $customfieldvalues,
'selectedcustomfield' => $selectedcustomfield,
];
return array_merge($defaultvariables, $preferences);
}
}
}

View File

@ -33,6 +33,7 @@ $string['aria:controls'] = 'Course overview controls';
$string['aria:courseactions'] = 'Actions for current course';
$string['aria:coursesummary'] = 'Course summary text:';
$string['aria:courseprogress'] = 'Course progress:';
$string['aria:customfield'] = 'Show {$a} courses';
$string['aria:displaydropdown'] = 'Display drop-down menu';
$string['aria:favourites'] = 'Show starred courses';
$string['aria:future'] = 'Show future courses';
@ -51,6 +52,9 @@ $string['card'] = 'Card';
$string['cards'] = 'Cards';
$string['courseprogress'] = 'Course progress:';
$string['completepercent'] = '{$a}% complete';
$string['customfield'] = 'Custom field';
$string['customfiltergrouping'] = 'Field to use';
$string['customfiltergrouping_nofields'] = 'You need to add at least one course custom field in order to use this setting';
$string['displaycategories'] = 'Display categories';
$string['displaycategories_help'] = 'Display the course category on dashboard course items including cards, list items and summary items.';
$string['favourites'] = 'Starred';
@ -61,6 +65,7 @@ $string['layouts'] = 'Available layouts';
$string['layouts_help'] = 'Course overview layouts which are available for selection by users. If none are selected, the card layout will be used.';
$string['list'] = 'List';
$string['myoverview:myaddinstance'] = 'Add a new course overview block to Dashboard';
$string['nocustomvalue'] = 'No {$a}';
$string['past'] = 'Past';
$string['pluginname'] = 'Course overview';
$string['privacy:metadata:overviewsortpreference'] = 'The Course overview block sort preference.';

View File

@ -34,6 +34,12 @@ define('BLOCK_MYOVERVIEW_GROUPING_FUTURE', 'future');
define('BLOCK_MYOVERVIEW_GROUPING_PAST', 'past');
define('BLOCK_MYOVERVIEW_GROUPING_FAVOURITES', 'favourites');
define('BLOCK_MYOVERVIEW_GROUPING_HIDDEN', 'hidden');
define('BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD', 'customfield');
/**
* Allows selection of all courses without a value for the custom field.
*/
define('BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY', -1);
/**
* Constants for the user preferences sorting options
@ -81,9 +87,17 @@ function block_myoverview_user_preferences() {
BLOCK_MYOVERVIEW_GROUPING_FUTURE,
BLOCK_MYOVERVIEW_GROUPING_PAST,
BLOCK_MYOVERVIEW_GROUPING_FAVOURITES,
BLOCK_MYOVERVIEW_GROUPING_HIDDEN
BLOCK_MYOVERVIEW_GROUPING_HIDDEN,
BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD,
)
);
$preferences['block_myoverview_user_grouping_customfieldvalue_preference'] = [
'null' => NULL_ALLOWED,
'default' => null,
'type' => PARAM_RAW,
];
$preferences['block_myoverview_user_sort_preference'] = array(
'null' => NULL_NOT_ALLOWED,
'default' => BLOCK_MYOVERVIEW_SORTING_TITLE,

View File

@ -86,6 +86,29 @@ if ($ADMIN->fulltree) {
'',
1));
$settings->add(new admin_setting_configcheckbox(
'block_myoverview/displaygroupingcustomfield',
get_string('customfield', 'block_myoverview'),
'',
0));
$choices = \core_customfield\api::get_fields_supporting_course_grouping();
if ($choices) {
$choices = ['' => get_string('choosedots')] + $choices;
$settings->add(new admin_setting_configselect(
'block_myoverview/customfiltergrouping',
get_string('customfiltergrouping', 'block_myoverview'),
'',
'',
$choices));
} else {
$settings->add(new admin_setting_configempty(
'block_myoverview/customfiltergrouping',
get_string('customfiltergrouping', 'block_myoverview'),
get_string('customfiltergrouping_nofields', 'block_myoverview')));
}
$settings->hide_if('block_myoverview/customfiltergrouping', 'block_myoverview/displaygroupingcustomfield');
$settings->add(new admin_setting_configcheckbox(
'block_myoverview/displaygroupingstarred',
get_string('favourites', 'block_myoverview'),

View File

@ -31,6 +31,8 @@
data-region="courses-view"
data-display="{{view}}"
data-grouping="{{grouping}}"
data-customfieldname="{{customfieldname}}"
data-customfieldvalue="{{customfieldvalue}}"
data-sort="{{sort}}"
data-prev-display="{{view}}"
data-paging="{{paging}}"
@ -40,4 +42,4 @@
<div data-region="course-view-content">
{{> block_myoverview/placeholders }}
</div>
</div>
</div>

View File

@ -50,6 +50,7 @@
{{#past}}{{#str}} past, block_myoverview {{/str}}{{/past}}
{{#favourites}}{{#str}} favourites, block_myoverview {{/str}}{{/favourites}}
{{#hidden}}{{#str}} hiddencourses, block_myoverview {{/str}}{{/hidden}}
{{selectedcustomfield}}
</span>
</button>
<ul class="dropdown-menu" data-show-active-item data-active-item-text aria-labelledby="groupingdropdown">
@ -106,6 +107,21 @@
</a>
</li>
{{/displaygroupingpast}}
{{#displaygroupingcustomfield}}
<li class="dropdown-divider" role="presentation">
<span class="filler">&nbsp;</span>
</li>
{{#customfieldvalues}}
<li>
<a class="dropdown-item {{#active}}active{{/active}}" href="#" data-filter="grouping"
data-value="customfield" data-pref="customfield" data-customfieldvalue="{{value}}"
aria-label="{{#str}}aria:customfield, block_myoverview, {{name}}{{/str}}"
aria-controls="courses-view-{{uniqid}}">
{{name}}
</a>
</li>
{{/customfieldvalues}}
{{/displaygroupingcustomfield}}
{{#displaygroupingstarred}}
<li class="dropdown-divider" role="presentation">
<span class="filler">&nbsp;</span>

View File

@ -0,0 +1,174 @@
@block @block_myoverview @javascript
Feature: The my overview block allows users to group courses by custom fields
Background:
Given the following "users" exist:
| username | firstname | lastname | email | idnumber |
| student1 | Student | X | student1@example.com | S1 |
And the following "custom field categories" exist:
| name | component | area | itemid |
| Course fields | core_course | course | 0 |
And the following "custom fields" exist:
| name | category | type | shortname | configdata |
| Checkbox field | Course fields | checkbox | checkboxfield | |
| Date field | Course fields | date | datefield | {"mindate":0, "maxdate":0} |
| Select field | Course fields | select | selectfield | {"options":"Option 1\nOption 2\nOption 3\nOption 4"} |
| Text field | Course fields | text | textfield | |
And the following "courses" exist:
| fullname | shortname | category | customfield_checkboxfield | customfield_datefield | customfield_selectfield | customfield_textfield |
| Course 1 | C1 | 0 | 1 | 981028800 | 1 | fish |
| Course 2 | C2 | 0 | 0 | 334324800 | | |
| Course 3 | C3 | 0 | 0 | 981028800 | 2 | dog |
| Course 4 | C4 | 0 | 1 | | 3 | cat |
| Course 5 | C5 | 0 | | 334411200 | 2 | fish |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student1 | C2 | student |
| student1 | C3 | student |
| student1 | C4 | student |
| student1 | C5 | student |
Scenario: Group courses by checkbox: Yes
Given the following config values are set as admin:
| displaygroupingcustomfield | 1 | block_myoverview |
| customfiltergrouping | checkboxfield | block_myoverview |
And I log in as "student1"
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
When I click on "Checkbox field: Yes" "link" in the "Course overview" "block"
Then I should see "Course 1" in the "Course overview" "block"
And I should not see "Course 2" in the "Course overview" "block"
And I should not see "Course 3" in the "Course overview" "block"
And I should see "Course 4" in the "Course overview" "block"
And I should not see "Course 5" in the "Course overview" "block"
Scenario: Group courses by checkbox: No
Given the following config values are set as admin:
| displaygroupingcustomfield | 1 | block_myoverview |
| customfiltergrouping | checkboxfield | block_myoverview |
And I log in as "student1"
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
When I click on "Checkbox field: No" "link" in the "Course overview" "block"
Then I should not see "Course 1" in the "Course overview" "block"
And I should see "Course 2" in the "Course overview" "block"
And I should see "Course 3" in the "Course overview" "block"
And I should not see "Course 4" in the "Course overview" "block"
And I should see "Course 5" in the "Course overview" "block"
Scenario: Group courses by date: 1 February 2001
Given the following config values are set as admin:
| displaygroupingcustomfield | 1 | block_myoverview |
| customfiltergrouping | datefield | block_myoverview |
And I log in as "student1"
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
When I click on "1 February 2001" "link" in the "Course overview" "block"
Then I should see "Course 1" in the "Course overview" "block"
And I should not see "Course 2" in the "Course overview" "block"
And I should see "Course 3" in the "Course overview" "block"
And I should not see "Course 4" in the "Course overview" "block"
And I should not see "Course 5" in the "Course overview" "block"
Scenario: Group courses by date: 6 August 1980
Given the following config values are set as admin:
| displaygroupingcustomfield | 1 | block_myoverview |
| customfiltergrouping | datefield | block_myoverview |
And I log in as "student1"
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
When I click on "6 August 1980" "link" in the "Course overview" "block"
Then I should not see "Course 1" in the "Course overview" "block"
And I should not see "Course 2" in the "Course overview" "block"
And I should not see "Course 3" in the "Course overview" "block"
And I should not see "Course 4" in the "Course overview" "block"
And I should see "Course 5" in the "Course overview" "block"
Scenario: Group courses by date: No Date field
Given the following config values are set as admin:
| displaygroupingcustomfield | 1 | block_myoverview |
| customfiltergrouping | datefield | block_myoverview |
And I log in as "student1"
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
When I click on "No Date field" "link" in the "Course overview" "block"
Then I should not see "Course 1" in the "Course overview" "block"
And I should not see "Course 2" in the "Course overview" "block"
And I should not see "Course 3" in the "Course overview" "block"
And I should see "Course 4" in the "Course overview" "block"
And I should not see "Course 5" in the "Course overview" "block"
Scenario: Group courses by select: Option 1
Given the following config values are set as admin:
| displaygroupingcustomfield | 1 | block_myoverview |
| customfiltergrouping | selectfield | block_myoverview |
And I log in as "student1"
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
And I should not see "Option 4" in the "Course overview" "block"
When I click on "Option 1" "link" in the "Course overview" "block"
Then I should see "Course 1" in the "Course overview" "block"
And I should not see "Course 2" in the "Course overview" "block"
And I should not see "Course 3" in the "Course overview" "block"
And I should not see "Course 4" in the "Course overview" "block"
And I should not see "Course 5" in the "Course overview" "block"
Scenario: Group courses by select: Option 2
Given the following config values are set as admin:
| displaygroupingcustomfield | 1 | block_myoverview |
| customfiltergrouping | selectfield | block_myoverview |
And I log in as "student1"
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
When I click on "Option 2" "link" in the "Course overview" "block"
Then I should not see "Course 1" in the "Course overview" "block"
And I should not see "Course 2" in the "Course overview" "block"
And I should see "Course 3" in the "Course overview" "block"
And I should not see "Course 4" in the "Course overview" "block"
And I should see "Course 5" in the "Course overview" "block"
Scenario: Group courses by select: No Select field
Given the following config values are set as admin:
| displaygroupingcustomfield | 1 | block_myoverview |
| customfiltergrouping | selectfield | block_myoverview |
And I log in as "student1"
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
When I click on "No Select field" "link" in the "Course overview" "block"
Then I should not see "Course 1" in the "Course overview" "block"
And I should see "Course 2" in the "Course overview" "block"
And I should not see "Course 3" in the "Course overview" "block"
And I should not see "Course 4" in the "Course overview" "block"
And I should not see "Course 5" in the "Course overview" "block"
Scenario: Group courses by text: fish
Given the following config values are set as admin:
| displaygroupingcustomfield | 1 | block_myoverview |
| customfiltergrouping | textfield | block_myoverview |
And I log in as "student1"
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
When I click on "fish" "link" in the "Course overview" "block"
Then I should see "Course 1" in the "Course overview" "block"
And I should not see "Course 2" in the "Course overview" "block"
And I should not see "Course 3" in the "Course overview" "block"
And I should not see "Course 4" in the "Course overview" "block"
And I should see "Course 5" in the "Course overview" "block"
Scenario: Group courses by text: dog
Given the following config values are set as admin:
| displaygroupingcustomfield | 1 | block_myoverview |
| customfiltergrouping | textfield | block_myoverview |
And I log in as "student1"
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
When I click on "dog" "link" in the "Course overview" "block"
Then I should not see "Course 1" in the "Course overview" "block"
And I should not see "Course 2" in the "Course overview" "block"
And I should see "Course 3" in the "Course overview" "block"
And I should not see "Course 4" in the "Course overview" "block"
And I should not see "Course 5" in the "Course overview" "block"
Scenario: Group courses by text: No Text field
Given the following config values are set as admin:
| displaygroupingcustomfield | 1 | block_myoverview |
| customfiltergrouping | textfield | block_myoverview |
And I log in as "student1"
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
When I click on "No Text field" "link" in the "Course overview" "block"
Then I should not see "Course 1" in the "Course overview" "block"
And I should see "Course 2" in the "Course overview" "block"
And I should not see "Course 3" in the "Course overview" "block"
And I should not see "Course 4" in the "Course overview" "block"
And I should not see "Course 5" in the "Course overview" "block"

View File

@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2019091800; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2019100900; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2019051100; // Requires this Moodle version.
$plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).

View File

@ -3694,7 +3694,11 @@ class core_course_external extends external_api {
'classification' => new external_value(PARAM_ALPHA, 'future, inprogress, or past'),
'limit' => new external_value(PARAM_INT, 'Result set limit', VALUE_DEFAULT, 0),
'offset' => new external_value(PARAM_INT, 'Result set offset', VALUE_DEFAULT, 0),
'sort' => new external_value(PARAM_TEXT, 'Sort string', VALUE_DEFAULT, null)
'sort' => new external_value(PARAM_TEXT, 'Sort string', VALUE_DEFAULT, null),
'customfieldname' => new external_value(PARAM_ALPHANUMEXT, 'Used when classification = customfield',
VALUE_DEFAULT, null),
'customfieldvalue' => new external_value(PARAM_RAW, 'Used when classification = customfield',
VALUE_DEFAULT, null),
)
);
}
@ -3717,6 +3721,8 @@ class core_course_external extends external_api {
* @param int $limit Result set limit
* @param int $offset Offset the full course set before timeline classification is applied
* @param string $sort SQL sort string for results
* @param string $customfieldname
* @param string $customfieldvalue
* @return array list of courses and warnings
* @throws invalid_parameter_exception
*/
@ -3724,7 +3730,9 @@ class core_course_external extends external_api {
string $classification,
int $limit = 0,
int $offset = 0,
string $sort = null
string $sort = null,
string $customfieldname = null,
string $customfieldvalue = null
) {
global $CFG, $PAGE, $USER;
require_once($CFG->dirroot . '/course/lib.php');
@ -3735,6 +3743,7 @@ class core_course_external extends external_api {
'limit' => $limit,
'offset' => $offset,
'sort' => $sort,
'customfieldvalue' => $customfieldvalue,
)
);
@ -3742,6 +3751,7 @@ class core_course_external extends external_api {
$limit = $params['limit'];
$offset = $params['offset'];
$sort = $params['sort'];
$customfieldvalue = $params['customfieldvalue'];
switch($classification) {
case COURSE_TIMELINE_ALLINCLUDINGHIDDEN:
@ -3758,6 +3768,8 @@ class core_course_external extends external_api {
break;
case COURSE_TIMELINE_HIDDEN:
break;
case COURSE_CUSTOMFIELD:
break;
default:
throw new invalid_parameter_exception('Invalid classification');
}
@ -3801,6 +3813,13 @@ class core_course_external extends external_api {
$favouritecourseids,
$limit
);
} else if ($classification == COURSE_CUSTOMFIELD) {
list($filteredcourses, $processedcount) = course_filter_courses_by_customfield(
$courses,
$customfieldname,
$customfieldvalue,
$limit
);
} else {
list($filteredcourses, $processedcount) = course_filter_courses_by_timeline_classification(
$courses,

View File

@ -63,7 +63,10 @@ define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
define('COURSE_TIMELINE_FUTURE', 'future');
define('COURSE_FAVOURITES', 'favourites');
define('COURSE_TIMELINE_HIDDEN', 'hidden');
define('COURSE_CUSTOMFIELD', 'customfield');
define('COURSE_DB_QUERY_LIMIT', 1000);
/** Searching for all courses that have no value for the specified custom field. */
define('COURSE_CUSTOMFIELD_EMPTY', -1);
function make_log_url($module, $url) {
switch ($module) {
@ -4397,6 +4400,101 @@ function course_filter_courses_by_favourites(
return [$filteredcourses, $numberofcoursesprocessed];
}
/**
* Search the given $courses for any that have a $customfieldname value that matches the given
* $customfieldvalue, up to the specified $limit.
*
* This function will return the subset of courses that matches the value as well as the
* number of courses it had to process to build that subset.
*
* It is recommended that for larger sets of courses this function is given a Generator that loads
* the courses from the database in chunks.
*
* @param array|Traversable $courses List of courses to process
* @param string $customfieldname the shortname of the custom field to match against
* @param string $customfieldvalue the value this custom field needs to match
* @param int $limit Limit the number of results to this amount
* @return array First value is the filtered courses, second value is the number of courses processed
*/
function course_filter_courses_by_customfield(
$courses,
$customfieldname,
$customfieldvalue,
int $limit = 0
) : array {
global $DB;
if (!$courses) {
return [[], 0];
}
// Prepare the list of courses to search through.
$coursesbyid = [];
foreach ($courses as $course) {
$coursesbyid[$course->id] = $course;
}
if (!$coursesbyid) {
return [[], 0];
}
list($csql, $params) = $DB->get_in_or_equal(array_keys($coursesbyid), SQL_PARAMS_NAMED);
// Get the id of the custom field.
$sql = "
SELECT f.id
FROM {customfield_field} f
JOIN {customfield_category} cat ON cat.id = f.categoryid
WHERE f.shortname = ?
AND cat.component = 'core_course'
AND cat.area = 'course'
";
$fieldid = $DB->get_field_sql($sql, [$customfieldname]);
if (!$fieldid) {
return [[], 0];
}
// Get a list of courseids that match that custom field value.
if ($customfieldvalue == COURSE_CUSTOMFIELD_EMPTY) {
$sql = "
SELECT c.id
FROM {course} c
LEFT JOIN {customfield_data} cd ON cd.instanceid = c.id AND cd.fieldid = :fieldid
WHERE c.id $csql
AND (cd.value IS NULL OR cd.value = '' OR cd.value = '0')
";
$params['fieldid'] = $fieldid;
$matchcourseids = $DB->get_fieldset_sql($sql, $params);
} else {
$select = "fieldid = :fieldid AND value = :customfieldvalue AND instanceid $csql";
$params['fieldid'] = $fieldid;
$params['customfieldvalue'] = $customfieldvalue;
$matchcourseids = $DB->get_fieldset_select('customfield_data', 'instanceid', $select, $params);
}
// Prepare the list of courses to return.
$filteredcourses = [];
$numberofcoursesprocessed = 0;
$filtermatches = 0;
foreach ($coursesbyid as $course) {
$numberofcoursesprocessed++;
if (in_array($course->id, $matchcourseids)) {
$filteredcourses[] = $course;
$filtermatches++;
}
if ($limit && $filtermatches >= $limit) {
// We've found the number of requested courses. No need to continue searching.
break;
}
}
// Return the number of filtered courses as well as the number of courses that were searched
// in order to find the matching courses. This allows the calling code to do some kind of
// pagination.
return [$filteredcourses, $numberofcoursesprocessed];
}
/**
* Check module updates since a given time.
* This function checks for updates in the module config, file areas, completion, grades, comments and ratings.

View File

@ -4866,6 +4866,262 @@ class core_course_courselib_testcase extends advanced_testcase {
$this->assertEquals($expectedprocessedcount, $processedcount);
}
/**
* Test cases for the course_filter_courses_by_timeline_classification tests.
*/
public function get_course_filter_courses_by_customfield_test_cases() {
global $CFG;
require_once($CFG->dirroot.'/blocks/myoverview/lib.php');
$coursedata = [
[
'shortname' => 'C1',
'customfield_checkboxfield' => 1,
'customfield_datefield' => strtotime('2001-02-01T12:00:00Z'),
'customfield_selectfield' => 1,
'customfield_textfield' => 'fish',
],
[
'shortname' => 'C2',
'customfield_checkboxfield' => 0,
'customfield_datefield' => strtotime('1980-08-05T13:00:00Z'),
],
[
'shortname' => 'C3',
'customfield_checkboxfield' => 0,
'customfield_datefield' => strtotime('2001-02-01T12:00:00Z'),
'customfield_selectfield' => 2,
'customfield_textfield' => 'dog',
],
[
'shortname' => 'C4',
'customfield_checkboxfield' => 1,
'customfield_selectfield' => 3,
'customfield_textfield' => 'cat',
],
[
'shortname' => 'C5',
'customfield_datefield' => strtotime('1980-08-06T13:00:00Z'),
'customfield_selectfield' => 2,
'customfield_textfield' => 'fish',
],
];
return [
'empty set' => [
'coursedata' => [],
'customfield' => 'checkboxfield',
'customfieldvalue' => 1,
'limit' => 10,
'offset' => 0,
'expectedcourses' => [],
'expectedprocessedcount' => 0
],
'checkbox yes' => [
'coursedata' => $coursedata,
'customfield' => 'checkboxfield',
'customfieldvalue' => 1,
'limit' => 10,
'offset' => 0,
'expectedcourses' => ['C1', 'C4'],
'expectedprocessedcount' => 5
],
'checkbox no' => [
'coursedata' => $coursedata,
'customfield' => 'checkboxfield',
'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
'limit' => 10,
'offset' => 0,
'expectedcourses' => ['C2', 'C3', 'C5'],
'expectedprocessedcount' => 5
],
'date 1 Feb 2001' => [
'coursedata' => $coursedata,
'customfield' => 'datefield',
'customfieldvalue' => strtotime('2001-02-01T12:00:00Z'),
'limit' => 10,
'offset' => 0,
'expectedcourses' => ['C1', 'C3'],
'expectedprocessedcount' => 5
],
'date 6 Aug 1980' => [
'coursedata' => $coursedata,
'customfield' => 'datefield',
'customfieldvalue' => strtotime('1980-08-06T13:00:00Z'),
'limit' => 10,
'offset' => 0,
'expectedcourses' => ['C5'],
'expectedprocessedcount' => 5
],
'date no date' => [
'coursedata' => $coursedata,
'customfield' => 'datefield',
'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
'limit' => 10,
'offset' => 0,
'expectedcourses' => ['C4'],
'expectedprocessedcount' => 5
],
'select Option 1' => [
'coursedata' => $coursedata,
'customfield' => 'selectfield',
'customfieldvalue' => 1,
'limit' => 10,
'offset' => 0,
'expectedcourses' => ['C1'],
'expectedprocessedcount' => 5
],
'select Option 2' => [
'coursedata' => $coursedata,
'customfield' => 'selectfield',
'customfieldvalue' => 2,
'limit' => 10,
'offset' => 0,
'expectedcourses' => ['C3', 'C5'],
'expectedprocessedcount' => 5
],
'select no select' => [
'coursedata' => $coursedata,
'customfield' => 'selectfield',
'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
'limit' => 10,
'offset' => 0,
'expectedcourses' => ['C2'],
'expectedprocessedcount' => 5
],
'text fish' => [
'coursedata' => $coursedata,
'customfield' => 'textfield',
'customfieldvalue' => 'fish',
'limit' => 10,
'offset' => 0,
'expectedcourses' => ['C1', 'C5'],
'expectedprocessedcount' => 5
],
'text dog' => [
'coursedata' => $coursedata,
'customfield' => 'textfield',
'customfieldvalue' => 'dog',
'limit' => 10,
'offset' => 0,
'expectedcourses' => ['C3'],
'expectedprocessedcount' => 5
],
'text no text' => [
'coursedata' => $coursedata,
'customfield' => 'textfield',
'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
'limit' => 10,
'offset' => 0,
'expectedcourses' => ['C2'],
'expectedprocessedcount' => 5
],
'checkbox limit no' => [
'coursedata' => $coursedata,
'customfield' => 'checkboxfield',
'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
'limit' => 2,
'offset' => 0,
'expectedcourses' => ['C2', 'C3'],
'expectedprocessedcount' => 3
],
'checkbox limit offset no' => [
'coursedata' => $coursedata,
'customfield' => 'checkboxfield',
'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
'limit' => 2,
'offset' => 3,
'expectedcourses' => ['C5'],
'expectedprocessedcount' => 2
],
];
}
/**
* Test the course_filter_courses_by_customfield function.
*
* @dataProvider get_course_filter_courses_by_customfield_test_cases()
* @param array $coursedata Course test data to create.
* @param string $customfield Shortname of the customfield.
* @param string $customfieldvalue the value to filter by.
* @param int $limit Maximum number of results to return.
* @param int $offset Results to skip at the start of the result set.
* @param string[] $expectedcourses Expected courses in results.
* @param int $expectedprocessedcount Expected number of course records to be processed.
*/
public function test_course_filter_courses_by_customfield(
$coursedata,
$customfield,
$customfieldvalue,
$limit,
$offset,
$expectedcourses,
$expectedprocessedcount
) {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
// Create the custom fields.
$generator->create_custom_field_category([
'name' => 'Course fields',
'component' => 'core_course',
'area' => 'course',
'itemid' => 0,
]);
$generator->create_custom_field([
'name' => 'Checkbox field',
'category' => 'Course fields',
'type' => 'checkbox',
'shortname' => 'checkboxfield',
]);
$generator->create_custom_field([
'name' => 'Date field',
'category' => 'Course fields',
'type' => 'date',
'shortname' => 'datefield',
'configdata' => '{"mindate":0, "maxdate":0}',
]);
$generator->create_custom_field([
'name' => 'Select field',
'category' => 'Course fields',
'type' => 'select',
'shortname' => 'selectfield',
'configdata' => '{"options":"Option 1\nOption 2\nOption 3\nOption 4"}',
]);
$generator->create_custom_field([
'name' => 'Text field',
'category' => 'Course fields',
'type' => 'text',
'shortname' => 'textfield',
]);
$courses = array_map(function($coursedata) use ($generator) {
return $generator->create_course($coursedata);
}, $coursedata);
$student = $generator->create_user();
foreach ($courses as $course) {
$generator->enrol_user($student->id, $course->id, 'student');
}
$this->setUser($student);
$coursesgenerator = course_get_enrolled_courses_for_logged_in_user(0, $offset, 'shortname ASC', 'shortname');
list($result, $processedcount) = course_filter_courses_by_customfield(
$coursesgenerator,
$customfield,
$customfieldvalue,
$limit
);
$actual = array_map(function($course) {
return $course->shortname;
}, $result);
$this->assertEquals($expectedcourses, $actual);
$this->assertEquals($expectedprocessedcount, $processedcount);
}
/**
* Test cases for the course_filter_courses_by_timeline_classification w/ hidden courses tests.
*/

View File

@ -412,4 +412,29 @@ class api {
$field->prepare_for_config_form($formdata);
return $formdata;
}
/**
* Get a list of the course custom fields that support course grouping in
* block_myoverview
* @return array $shortname => $name
*/
public static function get_fields_supporting_course_grouping() {
global $DB;
$sql = "
SELECT f.*
FROM {customfield_field} f
JOIN {customfield_category} cat ON cat.id = f.categoryid
WHERE cat.component = 'core_course' AND cat.area = 'course'
ORDER BY f.name
";
$ret = [];
$fields = $DB->get_records_sql($sql);
foreach ($fields as $field) {
$inst = field_controller::create(0, $field);
if ($inst->supports_course_grouping()) {
$ret[$inst->get('shortname')] = $inst->get('name');
}
}
return $ret;
}
}

View File

@ -249,4 +249,23 @@ abstract class field_controller {
$context = $this->get_handler()->get_configuration_context();
return format_string($this->get('name'), true, ['context' => $context]);
}
/**
* Does this custom field type support being used as part of the block_myoverview
* custom field grouping?
* @return bool
*/
public function supports_course_grouping(): bool {
return false;
}
/**
* If this field supports course filtering, then this function needs overriding to
* return the formatted values for this.
* @param array $values the used values that need grouping
* @return array
*/
public function course_grouping_format_values($values): array {
return [];
}
}

View File

@ -68,4 +68,27 @@ class field_controller extends \core_customfield\field_controller {
return $errors;
}
/**
* Does this custom field type support being used as part of the block_myoverview
* custom field grouping?
* @return bool
*/
public function supports_course_grouping(): bool {
return true;
}
/**
* If this field supports course grouping, then this function needs overriding to
* return the formatted values for this.
* @param array $values the used values that need formatting
* @return array
*/
public function course_grouping_format_values($values): array {
$name = $this->get_formatted_name();
return [
1 => $name.': '.get_string('yes'),
BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY => $name.': '.get_string('no'),
];
}
}

View File

@ -83,4 +83,35 @@ class field_controller extends \core_customfield\field_controller {
$mform->hideIf('configdata[mindate][hour]', 'configdata[includetime]');
$mform->hideIf('configdata[mindate][minute]', 'configdata[includetime]');
}
/**
* Does this custom field type support being used as part of the block_myoverview
* custom field grouping?
* @return bool
*/
public function supports_course_grouping(): bool {
return true;
}
/**
* If this field supports course grouping, then this function needs overriding to
* return the formatted values for this.
* @param array $values the used values that need formatting
* @return array
*/
public function course_grouping_format_values($values): array {
$format = get_string('strftimedate', 'langconfig');
$ret = [];
foreach ($values as $value) {
if ($value) {
$ret[$value] = userdate($value, $format);
}
}
if (!$ret) {
return []; // If the only dates found are 0, then do not show any options.
}
$ret[BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY] = get_string('nocustomvalue', 'block_myoverview',
$this->get_formatted_name());
return $ret;
}
}

View File

@ -91,4 +91,32 @@ class field_controller extends \core_customfield\field_controller {
}
return $errors;
}
/**
* Does this custom field type support being used as part of the block_myoverview
* custom field grouping?
* @return bool
*/
public function supports_course_grouping(): bool {
return true;
}
/**
* If this field supports course grouping, then this function needs overriding to
* return the formatted values for this.
* @param array $values the used values that need formatting
* @return array
*/
public function course_grouping_format_values($values): array {
$options = self::get_options_array($this);
$ret = [];
foreach ($values as $value) {
if (isset($options[$value])) {
$ret[$value] = format_string($options[$value]);
}
}
$ret[BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY] = get_string('nocustomvalue', 'block_myoverview',
$this->get_formatted_name());
return $ret;
}
}

View File

@ -120,4 +120,29 @@ class field_controller extends \core_customfield\field_controller {
return $errors;
}
/**
* Does this custom field type support being used as part of the block_myoverview
* custom field grouping?
* @return bool
*/
public function supports_course_grouping(): bool {
return true;
}
/**
* If this field supports course grouping, then this function needs overriding to
* return the formatted values for this.
* @param array $values the used values that need formatting
* @return array
*/
public function course_grouping_format_values($values): array {
$ret = [];
foreach ($values as $value) {
$ret[$value] = format_string($value);
}
$ret[BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY] = get_string('nocustomvalue', 'block_myoverview',
$this->get_formatted_name());
return $ret;
}
}

View File

@ -0,0 +1,5 @@
This files describes API changes in /customfield/field/* - customfield field types,
information provided here is intended especially for developers.
=== 3.8 ===
* supports_course_grouping() and course_grouping_format_values() functions added to support use of custom fields in block_myoverview

View File

@ -308,6 +308,20 @@ class behat_data_generators extends behat_base {
}
/**
* Remove any empty custom fields, to avoid errors when creating the course.
* @param array $data
* @return array
*/
protected function preprocess_course($data) {
foreach ($data as $fieldname => $value) {
if ($value === '' && strpos($fieldname, 'customfield_') === 0) {
unset($data[$fieldname]);
}
}
return $data;
}
/**
* If password is not set it uses the username.
* @param array $data