Skip to content

Commit

Permalink
Merge branch 'yusuf-musleh/collections-crud-rest-api' into jill/colle…
Browse files Browse the repository at this point in the history
…ction-components-search
  • Loading branch information
pomegranited committed Sep 11, 2024
2 parents 66b2aa8 + e6b469d commit 4bc7c22
Show file tree
Hide file tree
Showing 44 changed files with 1,184 additions and 686 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -277,18 +277,14 @@ def test_update_exam_settings_invalid_value(self):

# response is correct
assert response.status_code == status.HTTP_400_BAD_REQUEST
self.assertDictEqual(
response.data,
self.assertIn(
{
"detail": [
{
"proctoring_provider": (
"The selected proctoring provider, notvalidprovider, is not a valid provider. "
"Please select from one of ['test_proctoring_provider']."
)
}
]
"proctoring_provider": (
"The selected proctoring provider, notvalidprovider, is not a valid provider. "
"Please select from one of ['test_proctoring_provider']."
)
},
response.data['detail'],
)

# course settings have been updated
Expand Down Expand Up @@ -408,18 +404,14 @@ def test_400_for_disabled_lti(self):

# response is correct
assert response.status_code == status.HTTP_400_BAD_REQUEST
self.assertDictEqual(
response.data,
self.assertIn(
{
"detail": [
{
"proctoring_provider": (
"The selected proctoring provider, lti_external, is not a valid provider. "
"Please select from one of ['null']."
)
}
]
"proctoring_provider": (
"The selected proctoring provider, lti_external, is not a valid provider. "
"Please select from one of ['null']."
)
},
response.data['detail'],
)

# course settings have been updated
Expand Down
3 changes: 2 additions & 1 deletion cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1537,6 +1537,7 @@ def get_library_context(request, request_is_json=False):
)
from cms.djangoapps.contentstore.views.library import (
LIBRARIES_ENABLED,
user_can_view_create_library_button,
)

libraries = _accessible_libraries_iter(request.user) if LIBRARIES_ENABLED else []
Expand All @@ -1550,7 +1551,7 @@ def get_library_context(request, request_is_json=False):
'in_process_course_actions': [],
'courses': [],
'libraries_enabled': LIBRARIES_ENABLED,
'show_new_library_button': LIBRARIES_ENABLED and request.user.is_active,
'show_new_library_button': user_can_view_create_library_button(request.user) and request.user.is_active,
'user': request.user,
'request_course_creator_url': reverse('request_course_creator'),
'course_creator_status': _get_course_creator_status(request.user),
Expand Down
56 changes: 24 additions & 32 deletions cms/djangoapps/contentstore/views/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,31 +69,7 @@ def should_redirect_to_library_authoring_mfe():
)


def user_can_view_create_library_button(user):
"""
Helper method for displaying the visibilty of the create_library_button.
"""
if not LIBRARIES_ENABLED:
return False
elif user.is_staff:
return True
elif settings.FEATURES.get('ENABLE_CREATOR_GROUP', False):
is_course_creator = get_course_creator_status(user) == 'granted'
has_org_staff_role = OrgStaffRole().get_orgs_for_user(user).exists()
has_course_staff_role = UserBasedRole(user=user, role=CourseStaffRole.ROLE).courses_with_role().exists()
has_course_admin_role = UserBasedRole(user=user, role=CourseInstructorRole.ROLE).courses_with_role().exists()
return is_course_creator or has_org_staff_role or has_course_staff_role or has_course_admin_role
else:
# EDUCATOR-1924: DISABLE_LIBRARY_CREATION overrides DISABLE_COURSE_CREATION, if present.
disable_library_creation = settings.FEATURES.get('DISABLE_LIBRARY_CREATION', None)
disable_course_creation = settings.FEATURES.get('DISABLE_COURSE_CREATION', False)
if disable_library_creation is not None:
return not disable_library_creation
else:
return not disable_course_creation


def user_can_create_library(user, org):
def _user_can_create_library_for_org(user, org=None):
"""
Helper method for returning the library creation status for a particular user,
taking into account the value LIBRARIES_ENABLED.
Expand All @@ -109,29 +85,29 @@ def user_can_create_library(user, org):
Course Staff: Can make libraries in the organization which has courses of which they are staff.
Course Admin: Can make libraries in the organization which has courses of which they are Admin.
"""
if org is None:
return False
if not LIBRARIES_ENABLED:
return False
elif user.is_staff:
return True
if settings.FEATURES.get('ENABLE_CREATOR_GROUP', False):
elif settings.FEATURES.get('ENABLE_CREATOR_GROUP', False):
org_filter_params = {}
if org:
org_filter_params['org'] = org
is_course_creator = get_course_creator_status(user) == 'granted'
has_org_staff_role = org in OrgStaffRole().get_orgs_for_user(user)
has_org_staff_role = OrgStaffRole().get_orgs_for_user(user).filter(**org_filter_params).exists()
has_course_staff_role = (
UserBasedRole(user=user, role=CourseStaffRole.ROLE)
.courses_with_role()
.filter(org=org)
.filter(**org_filter_params)
.exists()
)
has_course_admin_role = (
UserBasedRole(user=user, role=CourseInstructorRole.ROLE)
.courses_with_role()
.filter(org=org)
.filter(**org_filter_params)
.exists()
)
return is_course_creator or has_org_staff_role or has_course_staff_role or has_course_admin_role

else:
# EDUCATOR-1924: DISABLE_LIBRARY_CREATION overrides DISABLE_COURSE_CREATION, if present.
disable_library_creation = settings.FEATURES.get('DISABLE_LIBRARY_CREATION', None)
Expand All @@ -142,6 +118,22 @@ def user_can_create_library(user, org):
return not disable_course_creation


def user_can_view_create_library_button(user):
"""
Helper method for displaying the visibilty of the create_library_button.
"""
return _user_can_create_library_for_org(user)


def user_can_create_library(user, org):
"""
Helper method for to check if user can create library for given org.
"""
if org is None:
return False
return _user_can_create_library_for_org(user, org)


@login_required
@ensure_csrf_cookie
@require_http_methods(('GET', 'POST'))
Expand Down
33 changes: 33 additions & 0 deletions cms/djangoapps/contentstore/views/tests/test_exam_settings_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,39 @@ def test_exam_settings_alert_with_exam_settings_disabled(self, page_handler):
else:
assert 'To update these settings go to the Advanced Settings page.' in alert_text

@override_settings(
PROCTORING_BACKENDS={
'DEFAULT': 'test_proctoring_provider',
'proctortrack': {},
'test_proctoring_provider': {},
},
FEATURES=FEATURES_WITH_EXAM_SETTINGS_ENABLED,
)
@ddt.data(
"advanced_settings_handler",
"course_handler",
)
def test_invalid_provider_alert(self, page_handler):
"""
An alert should appear if the course has a proctoring provider that is not valid.
"""
# create an error by setting an invalid proctoring provider
self.course.proctoring_provider = 'invalid_provider'
self.course.enable_proctored_exams = True
self.save_course()

url = reverse_course_url(page_handler, self.course.id)
resp = self.client.get(url, HTTP_ACCEPT='text/html')
alert_text = self._get_exam_settings_alert_text(resp.content)
assert (
'This course has proctored exam settings that are incomplete or invalid.'
in alert_text
)
assert (
'The proctoring provider configured for this course, \'invalid_provider\', is not valid.'
in alert_text
)

@ddt.data(
"advanced_settings_handler",
"course_handler",
Expand Down
33 changes: 26 additions & 7 deletions cms/djangoapps/models/settings/course_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,10 @@ def update_from_json(cls, block, jsondict, user, filter_tabs=True):
try:
val = model['value']
if hasattr(block, key) and getattr(block, key) != val:
key_values[key] = block.fields[key].from_json(val)
if key == 'proctoring_provider':
key_values[key] = block.fields[key].from_json(val, validate_providers=True)
else:
key_values[key] = block.fields[key].from_json(val)
except (TypeError, ValueError) as err:
raise ValueError(_("Incorrect format for field '{name}'. {detailed_message}").format( # lint-amnesty, pylint: disable=raise-missing-from
name=model['display_name'], detailed_message=str(err)))
Expand Down Expand Up @@ -253,7 +256,10 @@ def validate_and_update_from_json(cls, block, jsondict, user, filter_tabs=True):
try:
val = model['value']
if hasattr(block, key) and getattr(block, key) != val:
key_values[key] = block.fields[key].from_json(val)
if key == 'proctoring_provider':
key_values[key] = block.fields[key].from_json(val, validate_providers=True)
else:
key_values[key] = block.fields[key].from_json(val)
except (TypeError, ValueError, ValidationError) as err:
did_validate = False
errors.append({'key': key, 'message': str(err), 'model': model})
Expand Down Expand Up @@ -484,18 +490,31 @@ def validate_proctoring_settings(cls, block, settings_dict, user):
enable_proctoring = block.enable_proctored_exams

if enable_proctoring:

if proctoring_provider_model:
proctoring_provider = proctoring_provider_model.get('value')
else:
proctoring_provider = block.proctoring_provider

# If the proctoring provider stored in the course block no longer
# matches the available providers for this instance, show an error
if proctoring_provider not in available_providers:
message = (
f'The proctoring provider configured for this course, \'{proctoring_provider}\', is not valid.'
)
errors.append({
'key': 'proctoring_provider',
'message': message,
'model': proctoring_provider_model
})

# Require a valid escalation email if Proctortrack is chosen as the proctoring provider
escalation_email_model = settings_dict.get('proctoring_escalation_email')
if escalation_email_model:
escalation_email = escalation_email_model.get('value')
else:
escalation_email = block.proctoring_escalation_email

if proctoring_provider_model:
proctoring_provider = proctoring_provider_model.get('value')
else:
proctoring_provider = block.proctoring_provider

missing_escalation_email_msg = 'Provider \'{provider}\' requires an exam escalation contact.'
if proctoring_provider_model and proctoring_provider == 'proctortrack':
if not escalation_email:
Expand Down
1 change: 0 additions & 1 deletion cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,6 @@
'openedx.core.djangoapps.cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',

'common.djangoapps.student.middleware.UserStandingMiddleware',
'openedx.core.djangoapps.contentserver.middleware.StaticContentServerMiddleware',

'django.contrib.messages.middleware.MessageMiddleware',
'common.djangoapps.track.middleware.TrackMiddleware',
Expand Down
4 changes: 0 additions & 4 deletions common/djangoapps/util/tests/test_db.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Tests for util.db module."""

import unittest
from io import StringIO

import ddt
Expand Down Expand Up @@ -121,9 +120,6 @@ class MigrationTests(TestCase):
Tests for migrations.
"""

@unittest.skip(
"Temporary skip for ENT-8971 while the client id and secret columns in Canvas replaced."
)
@override_settings(MIGRATION_MODULES={})
def test_migrations_are_in_sync(self):
"""
Expand Down
Binary file modified common/static/data/geoip/GeoLite2-Country.mmdb
Binary file not shown.
7 changes: 6 additions & 1 deletion lms/djangoapps/bulk_email/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,15 @@ def ace_email_sent_handler(sender, **kwargs):
course_id = message.context.get('course_id')
if not course_id:
course_id = course_email.course_id if course_email else None
try:
channel = sender.__class__.__name__
except AttributeError:
channel = 'Other'
tracker.emit(
'edx.bulk_email.sent',
'edx.ace.message_sent',
{
'message_type': message.name,
'channel': channel,
'course_id': course_id,
'user_id': user_id,
}
Expand Down
18 changes: 11 additions & 7 deletions lms/djangoapps/bulk_email/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,16 @@ def opt_out_email_updates(request, token, course_id):
course_id,
)

tracker.emit(
'edx.bulk_email.opt_out',
{
'course_id': course_id,
'user_id': user.id,
}
)
event_name = 'edx.bulk_email.opt_out'
event_data = {
"username": user.username,
"user_id": user.id,
"course_id": course_id,
}
with tracker.get_tracker().context(event_name, event_data):
tracker.emit(
event_name,
event_data
)

return render_to_response('bulk_email/unsubscribe_success.html', context)
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class CourseHomeMetadataSerializer(VerifiedModeSerializer):
"""
celebrations = serializers.DictField()
course_access = serializers.DictField()
studio_access = serializers.BooleanField()
course_id = serializers.CharField()
is_enrolled = serializers.BooleanField()
is_self_paced = serializers.BooleanField()
Expand Down
3 changes: 2 additions & 1 deletion lms/djangoapps/course_home_api/course_metadata/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from lms.djangoapps.course_api.api import course_detail
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_home_api.course_metadata.serializers import CourseHomeMetadataSerializer
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.access import has_access, has_cms_access
from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs
from lms.djangoapps.courseware.courses import check_course_access
from lms.djangoapps.courseware.masquerade import setup_masquerade
Expand Down Expand Up @@ -124,6 +124,7 @@ def get(self, request, *args, **kwargs):
data = {
'course_id': course.id,
'username': username,
'studio_access': has_cms_access(request.user, course_key),
'is_staff': has_access(request.user, 'staff', course_key).has_access,
'original_user_is_staff': original_user_is_staff,
'number': course.display_number_with_default,
Expand Down
28 changes: 27 additions & 1 deletion lms/djangoapps/courseware/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
GlobalStaff,
OrgInstructorRole,
OrgStaffRole,
SupportStaffRole
SupportStaffRole,
CourseLimitedStaffRole,
)
from common.djangoapps.util import milestones_helpers as milestones_helpers # lint-amnesty, pylint: disable=useless-import-alias
from common.djangoapps.util.milestones_helpers import (
Expand Down Expand Up @@ -97,6 +98,31 @@ def has_ccx_coach_role(user, course_key):
return False


def has_cms_access(user, course_key):
"""
Check if user has access to the CMS. When requesting from the LMS, a user with the
limited staff access role needs access to the CMS APIs, but not the CMS site. This
function accounts for this edge case when determining if a user has access to the CMS
site.
Arguments:
user (User): the user whose course access we are checking.
course_key: Key to course.
Returns:
bool: whether user has access to the CMS site.
"""
has_course_author_access = auth.has_course_author_access(user, course_key)
is_limited_staff = auth.user_has_role(
user, CourseLimitedStaffRole(course_key)
) and not GlobalStaff().has_user(user)

if is_limited_staff and has_course_author_access:
return False

return has_course_author_access


@function_trace('has_access')
def has_access(user, action, obj, course_key=None):
"""
Expand Down
Loading

0 comments on commit 4bc7c22

Please sign in to comment.