diff --git a/.github/workflows/unit-test-shards.json b/.github/workflows/unit-test-shards.json index 4c95f2510fa1..80b941ad4df0 100644 --- a/.github/workflows/unit-test-shards.json +++ b/.github/workflows/unit-test-shards.json @@ -100,6 +100,7 @@ "openedx/core/djangoapps/course_apps/", "openedx/core/djangoapps/course_date_signals/", "openedx/core/djangoapps/course_groups/", + "openedx/core/djangoapps/course_roles/", "openedx/core/djangoapps/courseware_api/", "openedx/core/djangoapps/crawlers/", "openedx/core/djangoapps/credentials/", @@ -182,6 +183,7 @@ "openedx/core/djangoapps/course_apps/", "openedx/core/djangoapps/course_date_signals/", "openedx/core/djangoapps/course_groups/", + "openedx/core/djangoapps/course_roles/", "openedx/core/djangoapps/courseware_api/", "openedx/core/djangoapps/crawlers/", "openedx/core/djangoapps/credentials/", diff --git a/openedx/core/djangoapps/course_roles/helpers.py b/openedx/core/djangoapps/course_roles/helpers.py new file mode 100644 index 000000000000..e7cc35775f4c --- /dev/null +++ b/openedx/core/djangoapps/course_roles/helpers.py @@ -0,0 +1,30 @@ +""" +Helpers for the course roles app. +""" +from openedx.core.djangoapps.course_roles.models import CourseRolesUserRole +from openedx.core.lib.cache_utils import request_cached + + +@request_cached() +def course_permission_check(user, permission_name, course_id): + """ + Check if a user has a permission in a course. + """ + return CourseRolesUserRole.objects.filter( + user=user, + role__permissions__name=permission_name, + course=course_id, + ).exists() + + +@request_cached() +def organization_permission_check(user, permission_name, organization_name): + """ + Check if a user has a permission in an organization. + """ + return CourseRolesUserRole.objects.filter( + user=user, + role__permissions__name=permission_name, + course__isnull=True, + org__name=organization_name, + ).exists() diff --git a/openedx/core/djangoapps/course_roles/migrations/0001_initial.py b/openedx/core/djangoapps/course_roles/migrations/0001_initial.py index 234d1327684a..a47d796e7fac 100644 --- a/openedx/core/djangoapps/course_roles/migrations/0001_initial.py +++ b/openedx/core/djangoapps/course_roles/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.20 on 2023-09-12 22:03 +# Generated by Django 3.2.20 on 2023-09-13 20:50 from django.conf import settings from django.db import migrations, models @@ -10,9 +10,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('organizations', '0004_auto_20230727_2054'), - ('course_overviews', '0029_alter_historicalcourseoverview_options'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('course_overviews', '0029_alter_historicalcourseoverview_options'), + ('organizations', '0004_auto_20230727_2054'), ] operations = [ @@ -42,7 +42,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('course', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='course_overviews.courseoverview')), - ('org', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='organizations.organization')), + ('org', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.organization')), ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course_roles.courserolesrole')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], diff --git a/openedx/core/djangoapps/course_roles/models.py b/openedx/core/djangoapps/course_roles/models.py index c107af798a4d..9bfc56eb81b9 100644 --- a/openedx/core/djangoapps/course_roles/models.py +++ b/openedx/core/djangoapps/course_roles/models.py @@ -68,7 +68,7 @@ class CourseRolesUserRole(models.Model): on_delete=models.CASCADE, null=True, ) - org = models.ForeignKey(Organization, on_delete=models.CASCADE, null=False) + org = models.ForeignKey(Organization, on_delete=models.CASCADE, null=True) class Meta: unique_together = ('user', 'role', 'course') diff --git a/openedx/core/djangoapps/course_roles/tests/test_helpers.py b/openedx/core/djangoapps/course_roles/tests/test_helpers.py new file mode 100644 index 000000000000..da2117e00080 --- /dev/null +++ b/openedx/core/djangoapps/course_roles/tests/test_helpers.py @@ -0,0 +1,120 @@ +""" +Tests of the course_roles.helpers module +""" +from organizations.tests.factories import OrganizationFactory + +from common.djangoapps.student.tests.factories import UserFactory +from openedx.core.djangoapps.course_roles.helpers import course_permission_check, organization_permission_check +from openedx.core.djangoapps.course_roles.models import ( + CourseRolesPermission, + CourseRolesRole, + CourseRolesService, + CourseRolesUserRole +) +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + + +class PermissionCheckTestCase(SharedModuleStoreTestCase): + """ + Tests of the permission check functions in course_roles.helpers module + """ + def setUp(self): + super().setUp() + self.user_1 = UserFactory(username="test_user_1") + self.organization_1 = OrganizationFactory(name="test_organization_1") + self.organization_2 = OrganizationFactory(name="test_organization_2") + self.course_1 = CourseFactory.create( + display_name="test course 1", run="Testing_course_1", org=self.organization_1.name + ) + self.course_2 = CourseFactory.create( + display_name="test course 2", run="Testing_course_2", org=self.organization_1.name + ) + self.role_1 = CourseRolesRole.objects.create(name="test_role_1") + self.service = CourseRolesService.objects.create(name="test_service") + self.role_1.services.add(self.service) + self.permission_1 = CourseRolesPermission.objects.create(name="test_permission_1") + self.role_1.permissions.add(self.permission_1) + + def test_course_permission_check_with_course_level_permission(self): + """ + Test that course_permission_check returns True when the user has the correct permission at the course level + """ + CourseRolesUserRole.objects.create( + user=self.user_1, role=self.role_1, course_id=self.course_1.id, org=self.organization_1 + ) + assert course_permission_check(self.user_1, self.permission_1.name, self.course_1.id) + + def test_course_permission_check_without_course_level_permission(self): + """ + Test that course_permission_check returns False when the user does not have the correct permission at the + course level + """ + assert not course_permission_check(self.user_1, self.permission_1.name, self.course_1.id) + + def test_course_permision_check_with_organization_level_permission(self): + """ + Test that course_permission_check returns False when the user has the permission but at the organization + level, and has not been granted the permission at the course level + """ + CourseRolesUserRole.objects.create(user=self.user_1, role=self.role_1, org=self.organization_1) + assert not course_permission_check(self.user_1, self.permission_1.name, self.course_1.id) + + def test_course_permission_check_with_instance_level_permission(self): + """ + Test that course_permission_check returns False when the user has the permission but at the instance level, + and has not been granted the permission at the course level + """ + CourseRolesUserRole.objects.create(user=self.user_1, role=self.role_1) + assert not course_permission_check(self.user_1, self.permission_1.name, self.course_1.id) + + def test_course_permission_check_with_permission_in_another_course(self): + """ + Test that course_permission_check returns False when the user has the permission but at the course level, + but in another course + """ + CourseRolesUserRole.objects.create( + user=self.user_1, role=self.role_1, course_id=self.course_2.id, org=self.organization_1 + ) + assert not course_permission_check(self.user_1, self.permission_1.name, self.course_1.id) + + def test_organization_permission_check_with_organization_level_permission(self): + """ + Test that organization_permission_check returns True when the user has the correct permission at the + organization level + """ + CourseRolesUserRole.objects.create(user=self.user_1, role=self.role_1, org=self.organization_1) + assert organization_permission_check(self.user_1, self.permission_1.name, self.organization_1.name) + + def test_organization_permission_check_without_organization_level_permission(self): + """ + Test that organization_permission_check returns False when the user does not have the correct permission at + the organization level + """ + assert not organization_permission_check(self.user_1, self.permission_1.name, self.organization_1.name) + + def test_organization_permission_check_with_course_level_permission(self): + """ + Test that organization_permission_check returns False when the user has the permission but at the course + level, and has not been granted the permission at the organization level + """ + CourseRolesUserRole.objects.create( + user=self.user_1, role=self.role_1, course_id=self.course_1.id, org=self.organization_1 + ) + assert not organization_permission_check(self.user_1, self.permission_1.name, self.organization_1.name) + + def test_organization_permission_check_with_instance_level_permission(self): + """ + Test that organization_permission_check returns False when the user has the permission but at the instance + level, and has not been granted the permission at the organization level + """ + CourseRolesUserRole.objects.create(user=self.user_1, role=self.role_1) + assert not organization_permission_check(self.user_1, self.permission_1.name, self.organization_1.name) + + def test_organization_permission_check_with_permission_in_another_organization(self): + """ + Test that organization_permission_check returns False when the user has the permission but at the + organization level, but in another organization + """ + CourseRolesUserRole.objects.create(user=self.user_1, role=self.role_1, org=self.organization_2) + assert not organization_permission_check(self.user_1, self.permission_1.name, self.organization_1.name)