Skip to content

Commit

Permalink
MDL-73549 Course: My course page menu improvement
Browse files Browse the repository at this point in the history
 - Introduce core_course_category::get_nearest_editable_subcategory()
 - This function will return the first creatable/manageable category
for current user
 - With this new function, we can fix the issue that the users with
course management or creation permision at category level cannot see
the manage menu on My courses page
  • Loading branch information
HuongNV13 committed Mar 8, 2022
1 parent 1d99ba1 commit 481cfdc
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 9 deletions.
58 changes: 58 additions & 0 deletions course/classes/category.php
Original file line number Diff line number Diff line change
Expand Up @@ -3100,6 +3100,36 @@ public function can_request_course() {
return course_request::can_request($this->get_context());
}

/**
* Returns true if the user has all the given permissions.
*
* @param array $permissionstocheck The value can be create, manage or any specific capability.
* @return bool
*/
private function has_capabilities(array $permissionstocheck): bool {
if (empty($permissionstocheck)) {
throw new coding_exception('Invalid permissionstocheck parameter');
}
foreach ($permissionstocheck as $permission) {
if ($permission == 'create') {
if (!$this->can_create_course()) {
return false;
}
} else if ($permission == 'manage') {
if (!$this->has_manage_capability()) {
return false;
}
} else {
// Specific capability.
if (!$this->is_uservisible() || !has_capability($permission, $this->get_context())) {
return false;
}
}
}

return true;
}

/**
* Returns true if the user can approve course requests.
* @return bool
Expand Down Expand Up @@ -3146,4 +3176,32 @@ public static function page_setup() {
}
}
}

/**
* Returns the core_course_category object for the first category that the current user have the permission for the course.
*
* Only returns if it exists and is creatable/manageable to the current user
*
* @param core_course_category $parentcat Parent category to check.
* @param array $permissionstocheck The value can be create, manage or any specific capability.
* @return core_course_category|null
*/
public static function get_nearest_editable_subcategory(core_course_category $parentcat,
array $permissionstocheck): ?core_course_category {
// First, check the parent category.
if ($parentcat->has_capabilities($permissionstocheck)) {
return $parentcat;
}

// Check the child categories.
$subcategoryids = $parentcat->get_all_children_ids();
foreach ($subcategoryids as $subcategoryid) {
$subcategory = static::get($subcategoryid);
if ($subcategory->has_capabilities($permissionstocheck)) {
return $subcategory;
}
}

return null;
}
}
105 changes: 105 additions & 0 deletions course/tests/category_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -1102,4 +1102,109 @@ public function test_get_courses_during_delete() {
$this->assertCount(1, $courses);
$this->assertArrayHasKey($othercourse->id, $courses);
}

/**
* Test get_nearest_editable_subcategory() method.
*
* @covers \core_course_category::get_nearest_editable_subcategory
*/
public function test_get_nearest_editable_subcategory(): void {
global $DB;

$coursecreatorrole = $DB->get_record('role', ['shortname' => 'coursecreator']);
$managerrole = $DB->get_record('role', ['shortname' => 'manager']);

// Create categories.
$category1 = core_course_category::create(['name' => 'Cat1']);
$category2 = core_course_category::create(['name' => 'Cat2']);
$category3 = core_course_category::create(['name' => 'Cat3']);
// Get the category contexts.
$category1context = $category1->get_context();
$category2context = $category2->get_context();
$category3context = $category3->get_context();
// Create user.
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
// Assign the user1 to 'Course creator' role for Cat1.
role_assign($coursecreatorrole->id, $user1->id, $category1context->id);
// Assign the user2 to 'Manager' role for Cat3.
role_assign($managerrole->id, $user2->id, $category3context->id);

// Start scenario 1.
// user3 has no permission to create course or manage category.
$this->setUser($user3);
$coursecat = core_course_category::user_top();
$this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create']));
$this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create']));
$this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['manage']));
$this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage']));
$this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage']));
// End scenario 1.

// Start scenario 2.
// user1 has permission to create course but has no permission to manage category.
$this->setUser($user1);
$coursecat = core_course_category::user_top();
$this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create']));
$this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create']));
$this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['manage']));
$this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage']));
$this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage']));
// The get_nearest_editable_subcategory should return Cat1.
$this->assertEquals($category1->id, core_course_category::get_nearest_editable_subcategory($coursecat, ['create'])->id);
$this->assertEquals($category1->id,
core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create'])->id);
// Assign the user1 to 'Course creator' role for Cat2.
role_assign($coursecreatorrole->id, $user1->id, $category2context->id);
// The get_nearest_editable_subcategory should still return Cat1 (First creatable subcategory) for create course capability.
$this->assertEquals($category1->id, core_course_category::get_nearest_editable_subcategory($coursecat, ['create'])->id);
$this->assertEquals($category1->id,
core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create'])->id);
// End scenario 2.

// Start scenario 3.
// user2 has no permission to create course but has permission to manage category.
$this->setUser($user2);
// Remove the moodle/course:create capability for the manager role.
unassign_capability('moodle/course:create', $managerrole->id);
$coursecat = core_course_category::user_top();
$this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create']));
$this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create']));
$this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['manage']));
$this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage']));
$this->assertEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage']));
// The get_nearest_editable_subcategory should return Cat3.
$this->assertEquals($category3->id, core_course_category::get_nearest_editable_subcategory($coursecat, ['manage'])->id);
$this->assertEquals($category3->id,
core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage'])->id);
// End scenario 3.

// Start scenario 4.
// user2 has both permission to create course and manage category.
// Add the moodle/course:create capability back again for the manager role.
assign_capability('moodle/course:create', CAP_ALLOW, $managerrole->id, $category3context->id);
$this->setUser($user2);
$coursecat = core_course_category::user_top();
$this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create']));
$this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:create']));
$this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['manage']));
$this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/category:manage']));
$this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage']));
// The get_nearest_editable_subcategory should return Cat3.
$this->assertEquals($category3->id,
core_course_category::get_nearest_editable_subcategory($coursecat, ['create', 'manage'])->id);
$this->assertEquals($category3->id, core_course_category::get_nearest_editable_subcategory($coursecat,
['moodle/course:create', 'moodle/category:manage'])->id);
// End scenario 4.

// Start scenario 5.
// Exception will be thrown if $permissionstocheck is empty.
$this->setUser($user1);
$coursecat = core_course_category::user_top();
$this->expectException('coding_exception');
$this->expectExceptionMessage('Invalid permissionstocheck parameter');
$this->assertNotEmpty(core_course_category::get_nearest_editable_subcategory($coursecat, []));
// End scenario 5.
}
}
3 changes: 2 additions & 1 deletion course/upgrade.txt
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ course formats don't have their own renderer.
- print_course_request_buttons
* New page_setup() method in the core_course_category class. This method can be used for a general page setup in the course
category pages.
* New core_course_category::get_nearest_editable_subcategory():
- Return the core_course_category object for the first subcategory that the current user has the permission on it.

=== 3.11 ===
* A new callback xxx_coursemodule_definition_after_data that allows plugins to extend activity forms after the data is set.
Expand All @@ -100,7 +102,6 @@ course formats don't have their own renderer.
- activity_dates_information_in_activity_should_not_exist()
- Given the activity date information in "<ActivityName>" should not exist
* A user preference usemodchooser has been removed and the activities/resources (non-ajax) activity chooser has been deprecated and will be removed in the future.

=== 3.10 ===

* The function make_categories_options() has now been deprecated. Please use \core_course_category::make_categories_list() instead.
Expand Down
18 changes: 12 additions & 6 deletions my/courses.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,18 @@

// Add course management if the user has the capabilities for it.
$coursecat = core_course_category::user_top();
if ($coursecat->can_create_course() || $coursecat->has_manage_capability()) {
$data = [
'newcourseurl' => new moodle_url('/course/edit.php', ['category' => $coursecat->id]),
'manageurl' => new moodle_url('/course/management.php', ['categoryid' => $coursecat->id]),
];
$PAGE->add_header_action($OUTPUT->render_from_template('my/dropdown', $data));
$coursemanagemenu = [];
if ($category = core_course_category::get_nearest_editable_subcategory($coursecat, ['create'])) {
// The user has the capability to create course.
$coursemanagemenu['newcourseurl'] = new moodle_url('/course/edit.php', ['category' => $category->id]);
}
if ($category = core_course_category::get_nearest_editable_subcategory($coursecat, ['manage'])) {
// The user has the capability to manage the course category.
$coursemanagemenu['manageurl'] = new moodle_url('/course/management.php', ['categoryid' => $category->id]);
}
if (!empty($coursemanagemenu)) {
// Render the course management menu.
$PAGE->add_header_action($OUTPUT->render_from_template('my/dropdown', $coursemanagemenu));
}

echo $OUTPUT->header();
Expand Down
8 changes: 6 additions & 2 deletions my/templates/dropdown.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@
<i class="fa fa-ellipsis-v text-dark py-2" aria-hidden="true"></i>
</a>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="{{newcourseurl}}">{{#str}}newcourse, core{{/str}}</a>
<a class="dropdown-item" href="{{manageurl}}">{{#str}}managecourses, core{{/str}}</a>
{{#newcourseurl}}
<a class="dropdown-item" href="{{newcourseurl}}">{{#str}}newcourse, core{{/str}}</a>
{{/newcourseurl}}
{{#manageurl}}
<a class="dropdown-item" href="{{manageurl}}">{{#str}}managecourses, core{{/str}}</a>
{{/manageurl}}
</div>
</div>
68 changes: 68 additions & 0 deletions my/tests/behat/my_courses.feature
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
@core @core_my
Feature: Run tests over my courses.

Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| user1 | User | 1 | user1@example.com |
And the following "categories" exist:
| name | category | idnumber |
| CatA | 0 | cata |
And the following "roles" exist:
| shortname | name | archetype |
| role1 | Role 1 | |
And the following "system role assigns" exist:
| user | role | contextlevel | reference |
| user1 | role1 | Category | CatA |

Scenario: Admin can add new courses or manage them from my courses
Given I am on the "My courses" page logged in as "admin"
And I click on "Course management options" "link"
Expand All @@ -11,3 +25,57 @@ Feature: Run tests over my courses.
And I click on "Course management options" "link"
And I click on "Manage courses" "link"
And I should see "Manage course categories and courses"

Scenario: User without creating a course and managing category permissions cannot see any link
Given I am on the "My courses" page logged in as "user1"
Then "Course management options" "link" should not exist

@javascript
Scenario: User with creating a course permission can see the Create course link only
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/course:create | Allow | role1 | Category | cata |
When I am on the "My courses" page logged in as "user1"
Then "Course management options" "link" should exist
And I click on "Course management options" "link"
And I should see "New course"
And I should not see "Manage courses"
And I click on "New course" "link"
And I wait to be redirected
And I should see "Add a new course"
And "CatA" "autocomplete_selection" should exist

@javascript
Scenario: User with managing a category permission can see the Manage course link only
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/category:manage | Allow | role1 | Category | cata |
When I am on the "My courses" page logged in as "user1"
Then "Course management options" "link" should exist
And I click on "Course management options" "link"
And I should not see "New course"
And I should see "Manage courses"
And I click on "Manage courses" "link"
And I wait to be redirected
And I should see "Manage course categories and courses"

@javascript
Scenario: User with both creating a course and managing a category permission can see both links
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/course:create | Allow | role1 | Category | cata |
| moodle/category:manage | Allow | role1 | Category | cata |
When I am on the "My courses" page logged in as "user1"
Then "Course management options" "link" should exist
And I click on "Course management options" "link"
And I should see "New course"
And I should see "Manage courses"
And I click on "New course" "link"
And I wait to be redirected
And I should see "Add a new course"
And "CatA" "autocomplete_selection" should exist
And I am on the "My courses" page
And I click on "Course management options" "link"
And I click on "Manage courses" "link"
And I wait to be redirected
And I should see "Manage course categories and courses"

0 comments on commit 481cfdc

Please sign in to comment.