diff --git a/common/djangoapps/student/__init__.py b/common/djangoapps/student/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a77f14c551f940b61bcde89b1558c0bec7eed2f2 100644 --- a/common/djangoapps/student/__init__.py +++ b/common/djangoapps/student/__init__.py @@ -0,0 +1,8 @@ +""" +Student app helpers and settings +""" +from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace + + +# Namespace for student app waffle switches +STUDENT_WAFFLE_NAMESPACE = WaffleSwitchNamespace(name='student') diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py index 060295b819a164991ff28a29cd7565e4c33f48bd..9446a02249e102594a408e2616e385085322dd6b 100644 --- a/common/djangoapps/student/admin.py +++ b/common/djangoapps/student/admin.py @@ -9,7 +9,9 @@ from django.utils.translation import ugettext_lazy as _ from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey +from openedx.core.djangoapps.waffle_utils import WaffleSwitch from openedx.core.lib.courses import clean_course_id +from student import STUDENT_WAFFLE_NAMESPACE from student.models import ( CourseAccessRole, CourseEnrollment, @@ -28,6 +30,11 @@ from xmodule.modulestore.django import modulestore User = get_user_model() # pylint:disable=invalid-name +# This switch exists because the CourseEnrollment admin views make DB queries that impact performance. +# In a large enough deployment of Open edX, this is enough to cause a site outage. +# See https://openedx.atlassian.net/browse/OPS-2943 +COURSE_ENROLLMENT_ADMIN_SWITCH = WaffleSwitch(STUDENT_WAFFLE_NAMESPACE, 'courseenrollment_admin') + class CourseAccessRoleForm(forms.ModelForm): """Form for adding new Course Access Roles view the Django Admin Panel.""" @@ -165,13 +172,7 @@ class CourseEnrollmentForm(forms.ModelForm): fields = '__all__' -# Page disabled because it makes DB quries that impact performance enough to -# cause a site outage. It may be re-enabled when it is updated to make more -# efficent DB queries -# https://openedx.atlassian.net/browse/OPS-2943 -# Learner ticket to add functionality to /support -# https://openedx.atlassian.net/browse/LEARNER-4744 -#@admin.register(CourseEnrollment) +@admin.register(CourseEnrollment) class CourseEnrollmentAdmin(admin.ModelAdmin): """ Admin interface for the CourseEnrollment model. """ list_display = ('id', 'course_id', 'mode', 'user', 'is_active',) @@ -183,6 +184,38 @@ class CourseEnrollmentAdmin(admin.ModelAdmin): def queryset(self, request): return super(CourseEnrollmentAdmin, self).queryset(request).select_related('user') + def has_permission(self, request, method): + """ + Returns True if the given admin method is allowed. + """ + if COURSE_ENROLLMENT_ADMIN_SWITCH.is_enabled(): + return getattr(super(CourseEnrollmentAdmin, self), method)(request) + return False + + def has_add_permission(self, request): + """ + Returns True if CourseEnrollment objects can be added via the admin view. + """ + return self.has_permission(request, 'has_add_permission') + + def has_change_permission(self, request, obj=None): + """ + Returns True if CourseEnrollment objects can be modified via the admin view. + """ + return self.has_permission(request, 'has_change_permission') + + def has_delete_permission(self, request, obj=None): + """ + Returns True if CourseEnrollment objects can be deleted via the admin view. + """ + return self.has_permission(request, 'has_delete_permission') + + def has_module_permission(self, request): + """ + Returns True if links to the CourseEnrollment admin view can be displayed. + """ + return self.has_permission(request, 'has_module_permission') + class UserProfileInline(admin.StackedInline): """ Inline admin interface for UserProfile model. """ diff --git a/common/djangoapps/student/tests/test_admin_views.py b/common/djangoapps/student/tests/test_admin_views.py index bd52275dcd91498627e9946cf3ba3075ec939bc0..641d93439728058738fec527a9ea6ea7dd793d83 100644 --- a/common/djangoapps/student/tests/test_admin_views.py +++ b/common/djangoapps/student/tests/test_admin_views.py @@ -1,14 +1,15 @@ """ Tests student admin.py """ +import ddt from django.contrib.admin.sites import AdminSite from django.contrib.auth.models import User from django.urls import reverse from django.test import TestCase from mock import Mock -from student.admin import UserAdmin -from student.tests.factories import UserFactory +from student.admin import COURSE_ENROLLMENT_ADMIN_SWITCH, UserAdmin +from student.tests.factories import CourseEnrollmentFactory, UserFactory from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -186,3 +187,48 @@ class AdminUserPageTest(TestCase): request = Mock() user = Mock() self.assertIn('username', self.admin.get_readonly_fields(request, user)) + + +@ddt.ddt +class CourseEnrollmentAdminTest(SharedModuleStoreTestCase): + """ + Unit tests for the CourseEnrollmentAdmin view. + """ + ADMIN_URLS = ( + ('get', reverse('admin:student_courseenrollment_add')), + ('get', reverse('admin:student_courseenrollment_changelist')), + ('get', reverse('admin:student_courseenrollment_change', args=(1,))), + ('get', reverse('admin:student_courseenrollment_delete', args=(1,))), + ('post', reverse('admin:student_courseenrollment_add')), + ('post', reverse('admin:student_courseenrollment_changelist')), + ('post', reverse('admin:student_courseenrollment_change', args=(1,))), + ('post', reverse('admin:student_courseenrollment_delete', args=(1,))), + ) + + def setUp(self): + super(CourseEnrollmentAdminTest, self).setUp() + self.user = UserFactory.create(is_staff=True, is_superuser=True) + self.client.login(username=self.user.username, password='test') + CourseEnrollmentFactory( + user=self.user, + course_id=CourseFactory().id, # pylint: disable=no-member + ) + + @ddt.data(*ADMIN_URLS) + @ddt.unpack + def test_view_disabled(self, method, url): + """ + All CourseEnrollmentAdmin views are disabled by default. + """ + response = getattr(self.client, method)(url) + self.assertEqual(response.status_code, 403) + + @ddt.data(*ADMIN_URLS) + @ddt.unpack + def test_view_enabled(self, method, url): + """ + Ensure CourseEnrollmentAdmin views can be enabled with the waffle switch. + """ + with COURSE_ENROLLMENT_ADMIN_SWITCH.override(active=True): + response = getattr(self.client, method)(url) + self.assertEqual(response.status_code, 200)