Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(FFI-3): adds openedx-filter hook to the course enrollments site (DS-599) #767

Merged
merged 4 commits into from
Jul 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cms/djangoapps/contentstore/views/certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.student.auth import has_studio_write_access
from common.djangoapps.student.roles import GlobalStaff
from common.djangoapps.student.roles import GlobalStaff, CourseInstructorRole
from common.djangoapps.util.db import MYSQL_MAX_INT, generate_int_id
from common.djangoapps.util.json_request import JsonResponse
from xmodule.modulestore import EdxJSONEncoder # lint-amnesty, pylint: disable=wrong-import-order
Expand Down Expand Up @@ -427,6 +427,7 @@ def certificates_list_handler(request, course_key_string):
'certificate_web_view_url': certificate_web_view_url,
'is_active': is_active,
'is_global_staff': GlobalStaff().has_user(request.user),
'is_course_instructor': CourseInstructorRole(course.id).has_user(request.user),
'certificate_activation_handler_url': activation_handler_url,
'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course.id),
})
Expand Down
67 changes: 66 additions & 1 deletion common/djangoapps/student/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
from django.http import HttpResponse
from django.test import override_settings
from django.urls import reverse
from openedx.core.djangoapps.enrollments.data import get_course_enrollments
from openedx_filters import PipelineStep
from openedx_filters.learning.filters import DashboardRenderStarted, CourseEnrollmentStarted, CourseUnenrollmentStarted
from openedx_filters.learning.filters import (
DashboardRenderStarted,
CourseEnrollmentStarted,
CourseUnenrollmentStarted,
)
from rest_framework import status
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
Expand Down Expand Up @@ -110,6 +115,20 @@ def run_filter(self, context, template_name): # pylint: disable=arguments-diffe
)


class TestCourseEnrollmentsPipelineStep(PipelineStep):
"""
Utility function used when getting steps for pipeline.
"""

def run_filter(self, enrollments): # pylint: disable=arguments-differ
"""Pipeline steps that modifies course enrollments when make a queryset request."""

enrollments = [enrollment for enrollment in enrollments if enrollment.course_id.org == "demo"]
return {
"enrollments": enrollments,
}


@skip_unless_lms
class EnrollmentFiltersTest(ModuleStoreTestCase):
"""
Expand All @@ -118,17 +137,23 @@ class EnrollmentFiltersTest(ModuleStoreTestCase):
This class guarantees that the following filters are triggered during the user's enrollment:

- CourseEnrollmentStarted
- CourseEnrollmentQuerysetRequested
"""

def setUp(self): # pylint: disable=arguments-differ
super().setUp()
self.course = CourseFactory.create()
demo_course = CourseFactory.create(org='demo')
test_course = CourseFactory.create(org='test')
self.user = UserFactory.create(
username="test",
email="test@example.com",
password="password",
)
self.user_profile = UserProfileFactory.create(user=self.user, name="Test Example")
CourseEnrollment.enroll(self.user, demo_course.id, mode='audit')
CourseEnrollment.enroll(self.user, test_course.id, mode='audit')
self.enrollment = get_course_enrollments(self.user)

@override_settings(
OPEN_EDX_FILTERS_CONFIG={
Expand Down Expand Up @@ -189,6 +214,46 @@ def test_enrollment_without_filter_configuration(self):
self.assertEqual('audit', enrollment.mode)
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))

@override_settings()
def test_enrollment_queryset_filter_unexecuted_data(self):
"""
Test filter enrollment queryset when a request is made.

Expected result:
- CourseEnrollmentQuerysetRequested is triggered and executes TestCourseEnrollmentsPipelineStep.
- The result is a list of course enrollments queryset filter by org
"""
enrollments = get_course_enrollments(self.user)

self.assertListEqual(self.enrollment, enrollments)

@override_settings(
OPEN_EDX_FILTERS_CONFIG={
"org.openedx.learning.course_enrollment_queryset.requested.v1": {
"pipeline": [
"common.djangoapps.student.tests.test_filters.TestCourseEnrollmentsPipelineStep",
],
"fail_silently": False,
},
},
)
def test_enrollment_queryset_filter_executed_data(self):
"""
Test filter enrollment queryset when a request is made.

Expected result:
- CourseEnrollmentQuerysetRequested is triggered and executes TestCourseEnrollmentsPipelineStep.
- The result is a list of course enrollments queryset filter by org
"""
expected_enrollment = self.enrollment
expected_enrollment = expected_enrollment[0]['course_details']['course_id']

enrollments = get_course_enrollments(self.user)
enrollments = enrollments[0]['course_details']['course_id']

self.assertEqual(expected_enrollment, enrollments)
self.assertAlmostEqual(len(enrollments), len(expected_enrollment), 1)


@skip_unless_lms
class UnenrollmentFiltersTest(ModuleStoreTestCase):
Expand Down
2 changes: 1 addition & 1 deletion lms/djangoapps/ccx/api/v0/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def prepare_auth_token(self, user):
user=user,
client_type='confidential',
authorization_grant_type='authorization-code',
redirect_uris='http://localhost:8079/complete/edxorg/'
redirect_uris='http://localhost:8079/complete/edxorg/ http://testserver/'
)
# create an authorization code
auth_oauth2_provider = dot_models.AccessToken.objects.create(
Expand Down
16 changes: 13 additions & 3 deletions lms/djangoapps/instructor/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
ENABLE_CERTIFICATE_GENERATION = 'instructor.enable_certificate_generation'
GENERATE_CERTIFICATE_EXCEPTIONS = 'instructor.generate_certificate_exceptions'
GENERATE_BULK_CERTIFICATE_EXCEPTIONS = 'instructor.generate_bulk_certificate_exceptions'
GENERATE_EXAMPLE_CERTIFICATES = 'instructor.generate_example_certificates'
START_CERTIFICATE_GENERATION = 'instructor.start_certificate_generation'
START_CERTIFICATE_REGENERATION = 'instructor.start_certificate_regeneration'
CERTIFICATE_EXCEPTION_VIEW = 'instructor.certificate_exception_view'
CERTIFICATE_INVALIDATION_VIEW = 'instructor.certificate_invalidation_view'
GIVE_STUDENT_EXTENSION = 'instructor.give_student_extension'
VIEW_ISSUED_CERTIFICATES = 'instructor.view_issued_certificates'
CAN_RESEARCH = 'instructor.research'
Expand All @@ -36,9 +41,14 @@
perms[EDIT_COURSE_ACCESS] = HasAccessRule('instructor')
perms[EDIT_FORUM_ROLES] = HasAccessRule('staff')
perms[EDIT_INVOICE_VALIDATION] = HasAccessRule('staff')
perms[ENABLE_CERTIFICATE_GENERATION] = is_staff
perms[GENERATE_CERTIFICATE_EXCEPTIONS] = is_staff
perms[GENERATE_BULK_CERTIFICATE_EXCEPTIONS] = is_staff
perms[ENABLE_CERTIFICATE_GENERATION] = HasAccessRule('instructor')
perms[GENERATE_CERTIFICATE_EXCEPTIONS] = HasAccessRule('instructor')
perms[GENERATE_BULK_CERTIFICATE_EXCEPTIONS] = HasAccessRule('instructor')
perms[GENERATE_EXAMPLE_CERTIFICATES] = HasAccessRule('instructor')
perms[START_CERTIFICATE_GENERATION] = HasAccessRule('instructor')
perms[START_CERTIFICATE_REGENERATION] = HasAccessRule('instructor')
perms[CERTIFICATE_EXCEPTION_VIEW] = HasAccessRule('instructor')
perms[CERTIFICATE_INVALIDATION_VIEW] = HasAccessRule('instructor')
perms[GIVE_STUDENT_EXTENSION] = HasAccessRule('staff')
perms[VIEW_ISSUED_CERTIFICATES] = HasAccessRule('staff') | HasRolesRule('data_researcher')
# only global staff or those with the data_researcher role can access the data download tab
Expand Down
10 changes: 5 additions & 5 deletions lms/djangoapps/instructor/tests/test_certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ def setUp(self):
CertificateGenerationConfiguration.objects.create(enabled=True)

def test_visible_only_to_global_staff(self):
# Instructors don't see the certificates section
# Instructors see the certificates section
self.client.login(username=self.instructor.username, password="test")
self._assert_certificates_visible(False)
self._assert_certificates_visible(True)

# Global staff can see the certificates section
self.client.login(username=self.global_staff.username, password="test")
Expand Down Expand Up @@ -232,10 +232,10 @@ def setUp(self):
def test_allow_only_global_staff(self, url_name):
url = reverse(url_name, kwargs={'course_id': self.course.id})

# Instructors do not have access
# Instructors have access
self.client.login(username=self.instructor.username, password='test')
response = self.client.post(url)
assert response.status_code == 403
assert response.status_code == 302

# Global staff have access
self.client.login(username=self.global_staff.username, password='test')
Expand Down Expand Up @@ -285,7 +285,7 @@ def test_certificate_generation_api_without_global_staff(self):

self.client.login(username=self.instructor.username, password='test')
response = self.client.post(url)
assert response.status_code == 403
assert response.status_code == 200

def test_certificate_generation_api_with_global_staff(self):
"""
Expand Down
10 changes: 5 additions & 5 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
store_uploaded_file,
)
from common.djangoapps.util.json_request import JsonResponse, JsonResponseBadRequest
from common.djangoapps.util.views import require_global_staff
from common.djangoapps.util.views import require_global_staff # pylint: disable=unused-import
from lms.djangoapps.bulk_email.api import is_bulk_email_feature_enabled, create_course_email
from lms.djangoapps.certificates import api as certs_api
from lms.djangoapps.certificates.models import (
Expand Down Expand Up @@ -3017,7 +3017,7 @@ def mark_student_can_skip_entrance_exam(request, course_id):
@transaction.non_atomic_requests
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_global_staff
@require_course_permission(permissions.START_CERTIFICATE_GENERATION)
@require_POST
@common_exceptions_400
def start_certificate_generation(request, course_id):
Expand All @@ -3039,7 +3039,7 @@ def start_certificate_generation(request, course_id):
@transaction.non_atomic_requests
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_global_staff
@require_course_permission(permissions.START_CERTIFICATE_REGENERATION)
@require_POST
@common_exceptions_400
def start_certificate_regeneration(request, course_id):
Expand Down Expand Up @@ -3081,7 +3081,7 @@ def start_certificate_regeneration(request, course_id):
@transaction.non_atomic_requests
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_global_staff
@require_course_permission(permissions.CERTIFICATE_EXCEPTION_VIEW)
@require_http_methods(['POST', 'DELETE'])
def certificate_exception_view(request, course_id):
"""
Expand Down Expand Up @@ -3392,7 +3392,7 @@ def build_row_errors(key, _user, row_count):
@transaction.non_atomic_requests
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_global_staff
@require_course_permission(permissions.CERTIFICATE_INVALIDATION_VIEW)
@require_http_methods(['POST', 'DELETE'])
def certificate_invalidation_view(request, course_id):
"""
Expand Down
2 changes: 1 addition & 1 deletion lms/djangoapps/instructor/views/instructor_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def instructor_dashboard_2(request, course_id): # lint-amnesty, pylint: disable
# and enable self-generated certificates for a course.
# Note: This is hidden for all CCXs
certs_enabled = CertificateGenerationConfiguration.current().enabled and not hasattr(course_key, 'ccx')
if certs_enabled and access['admin']:
if certs_enabled and access['instructor']:
sections.append(_section_certificates(course))

openassessment_blocks = modulestore().get_items(
Expand Down
16 changes: 16 additions & 0 deletions lms/djangoapps/mobile_api/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.utils.decorators import method_decorator
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey
from openedx_filters.learning.filters import CourseEnrollmentQuerysetRequested
from rest_framework import generics, views
from rest_framework.decorators import api_view
from rest_framework.permissions import SAFE_METHODS
Expand Down Expand Up @@ -341,6 +342,13 @@ def get_queryset(self):
).order_by('created').reverse()
org = self.request.query_params.get('org', None)

try:
## .. filter_implemented_name: CourseEnrollmentQuerysetRequested
## .. filter_type: org.openedx.learning.course_enrollment_queryset.requested.v1
enrollments = CourseEnrollmentQuerysetRequested.run_filter(enrollments=enrollments)
except CourseEnrollmentQuerysetRequested.PreventEnrollmentQuerysetRequest as exc:
raise EnrollmentRequestNotAllowed(str(exc)) from exc

same_org = (
enrollment for enrollment in enrollments
if enrollment.course_overview and self.is_org(org, enrollment.course_overview.org)
Expand Down Expand Up @@ -375,6 +383,14 @@ def list(self, request, *args, **kwargs):
return response


class EnrollmentRequestException(Exception):
pass


class EnrollmentRequestNotAllowed(EnrollmentRequestException):
pass


@api_view(["GET"])
@mobile_view()
def my_user_info(request, api_version):
Expand Down
3 changes: 3 additions & 0 deletions lms/envs/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,9 @@ def get_env_setting(setting):
######################## Setting for content libraries ########################
MAX_BLOCKS_PER_CONTENT_LIBRARY = ENV_TOKENS.get('MAX_BLOCKS_PER_CONTENT_LIBRARY', MAX_BLOCKS_PER_CONTENT_LIBRARY)

####################### ALLOWED APPLICATIONS####################
#The applications in this list won't be restricted by site.
ALLOWED_AUTH_APPLICATIONS = ENV_TOKENS.get('ALLOWED_AUTH_APPLICATIONS', [])
########################## Derive Any Derived Settings #######################

derive_settings(__name__)
Expand Down
1 change: 0 additions & 1 deletion lms/static/certificates/sass/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,6 @@
}

// hide the fancy
.accomplishment-signatories .signatory-signature,
.accomplishment-type-symbol {
display: none;
}
Expand Down
16 changes: 16 additions & 0 deletions openedx/core/djangoapps/enrollments/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.db import transaction
from opaque_keys.edx.keys import CourseKey
from openedx_filters.learning.filters import CourseEnrollmentQuerysetRequested

from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.enrollments.errors import (
Expand Down Expand Up @@ -54,6 +55,13 @@ def get_course_enrollments(username, include_inactive=False):
if not include_inactive:
qset = qset.filter(is_active=True)

try:
## .. filter_implemented_name: CourseEnrollmentQuerysetRequested
## .. filter_type: org.openedx.learning.course_enrollment_queryset.requested.v1
qset = CourseEnrollmentQuerysetRequested.run_filter(enrollments=qset)
except CourseEnrollmentQuerysetRequested.PreventEnrollmentQuerysetRequest as exc:
raise EnrollmentRequestNotAllowed(str(exc)) from exc

enrollments = CourseEnrollmentSerializer(qset, many=True).data

# Find deleted courses and filter them out of the results
Expand All @@ -76,6 +84,14 @@ def get_course_enrollments(username, include_inactive=False):
return valid


class EnrollmentRequestException(Exception):
pass


class EnrollmentRequestNotAllowed(EnrollmentRequestException):
pass


def get_course_enrollment(username, course_id):
"""Retrieve an object representing all aggregated data for a user's course enrollment.

Expand Down
1 change: 1 addition & 0 deletions openedx/core/djangoapps/oauth_dispatch/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Meta:
client_type = 'confidential'
authorization_grant_type = Application.CLIENT_CONFIDENTIAL
name = FuzzyText(prefix='name', length=8)
redirect_uris = 'http://testserver/'


class ApplicationAccessFactory(DjangoModelFactory):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_jwt_access_token(self):
name='test dot application',
user=self.user,
authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS,
redirect_uri=DUMMY_REDIRECT_URL,
redirect_uri=f'{DUMMY_REDIRECT_URL} http://testserver/',
client_id='dot-app-client-id',
)
scopes = ['read', 'write', 'email']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def _create_restricted_app(self): # lint-amnesty, pylint: disable=missing-funct
restricted_app = self.dot_adapter.create_confidential_client(
name='test restricted dot application',
user=self.user,
redirect_uri=DUMMY_REDIRECT_URL,
redirect_uri=f'{DUMMY_REDIRECT_URL} http://testserver/',
client_id='dot-restricted-app-client-id',
)
models.RestrictedApplication.objects.create(application=restricted_app)
Expand Down
Loading
Loading