Skip to content

Commit

Permalink
Merge branch 'master' into kiram15/ENT-9670
Browse files Browse the repository at this point in the history
  • Loading branch information
kiram15 authored Dec 2, 2024
2 parents 1c7f628 + f9126bf commit cfbcb61
Show file tree
Hide file tree
Showing 30 changed files with 507 additions and 122 deletions.
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ openedx/core/djangoapps/enrollments/ @openedx/2U-
openedx/core/djangoapps/heartbeat/
openedx/core/djangoapps/oauth_dispatch
openedx/core/djangoapps/user_api/ @openedx/2U-aperture
openedx/core/djangoapps/user_authn/ @openedx/2U-vanguards
openedx/core/djangoapps/user_authn/ @openedx/2U-infinity
openedx/core/djangoapps/verified_track_content/ @openedx/2u-infinity
openedx/features/course_experience/
xmodule/
Expand Down
5 changes: 5 additions & 0 deletions cms/djangoapps/contentstore/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import urllib
from lxml import etree
from mimetypes import guess_type
import re

from attrs import frozen, Factory
from django.conf import settings
Expand Down Expand Up @@ -447,6 +448,10 @@ def _import_xml_node_to_parent(
temp_xblock = xblock_class.parse_xml(node_without_children, runtime, keys)
child_nodes = list(node)

if issubclass(xblock_class, XmlMixin) and "x-is-pointer-node" in getattr(temp_xblock, "data", ""):
# Undo the "pointer node" hack if needed (e.g. for capa problems)
temp_xblock.data = re.sub(r'([^>]+) x-is-pointer-node="no"', r'\1', temp_xblock.data, count=1)

# Restore the original id_generator
runtime.id_generator = original_id_generator

Expand Down
10 changes: 10 additions & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@
# Password Validator Settings
AUTH_PASSWORD_VALIDATORS
)
from lms.envs.common import (
USE_EXTRACTED_WORD_CLOUD_BLOCK,
USE_EXTRACTED_ANNOTATABLE_BLOCK,
USE_EXTRACTED_POLL_QUESTION_BLOCK,
USE_EXTRACTED_LTI_BLOCK,
USE_EXTRACTED_HTML_BLOCK,
USE_EXTRACTED_DISCUSSION_BLOCK,
USE_EXTRACTED_PROBLEM_BLOCK,
USE_EXTRACTED_VIDEO_BLOCK,
)
from path import Path as path
from django.urls import reverse_lazy

Expand Down
20 changes: 14 additions & 6 deletions common/djangoapps/student/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,17 @@ def user_has_role(user, role):
return False


def get_user_permissions(user, course_key, org=None):
def get_user_permissions(user, course_key, org=None, service_variant=None):
"""
Get the bitmask of permissions that this user has in the given course context.
Can also set course_key=None and pass in an org to get the user's
permissions for that organization as a whole.
:param user: a user
:param course_key: a CourseKey or None
:param org: an organization name or None
:param service_variant: the variant of the service (lms or cms). Permissions may differ between the two,
see the HACK comment in the function for more details.
"""
if org is None:
org = course_key.org
Expand All @@ -103,7 +109,7 @@ def get_user_permissions(user, course_key, org=None):
# the LMS and Studio permissions will be separated as a part of this project. Once this is done (and this code is
# not removed during its implementation), we can replace the Limited Staff permissions with more granular ones.
if course_key and user_has_role(user, CourseLimitedStaffRole(course_key)):
if settings.SERVICE_VARIANT == 'lms':
if (service_variant or settings.SERVICE_VARIANT) == 'lms':
return STUDIO_EDIT_CONTENT
else:
return STUDIO_NO_PERMISSIONS
Expand All @@ -119,7 +125,7 @@ def get_user_permissions(user, course_key, org=None):
return STUDIO_NO_PERMISSIONS


def has_studio_write_access(user, course_key):
def has_studio_write_access(user, course_key, service_variant=None):
"""
Return True if user has studio write access to the given course.
Note that the CMS permissions model is with respect to courses.
Expand All @@ -131,15 +137,17 @@ def has_studio_write_access(user, course_key):
:param user:
:param course_key: a CourseKey
:param service_variant: the variant of the service (lms or cms). Permissions may differ between the two,
see the comment in get_user_permissions for more details.
"""
return bool(STUDIO_EDIT_CONTENT & get_user_permissions(user, course_key))
return bool(STUDIO_EDIT_CONTENT & get_user_permissions(user, course_key, service_variant=service_variant))


def has_course_author_access(user, course_key):
def has_course_author_access(user, course_key, service_variant=None):
"""
Old name for has_studio_write_access
"""
return has_studio_write_access(user, course_key)
return has_studio_write_access(user, course_key, service_variant=service_variant)


def has_studio_advanced_settings_access(user):
Expand Down
13 changes: 12 additions & 1 deletion common/djangoapps/third_party_auth/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,16 +715,27 @@ def login_analytics(strategy, auth_entry, current_partial=None, *args, **kwargs)
""" Sends login info to Segment """

event_name = None
anonymous_id = ""
additional_params = {}

try:
request = kwargs['request']
anonymous_id = request.COOKIES.get('ajs_anonymous_id', "")
except: # pylint: disable=bare-except
pass

if auth_entry == AUTH_ENTRY_LOGIN:
event_name = 'edx.bi.user.account.authenticated'
additional_params['anonymous_id'] = anonymous_id
elif auth_entry in [AUTH_ENTRY_ACCOUNT_SETTINGS]:
event_name = 'edx.bi.user.account.linked'

if event_name is not None:
segment.track(kwargs['user'].id, event_name, {
'category': "conversion",
'label': None,
'provider': kwargs['backend'].name
'provider': kwargs['backend'].name,
**additional_params
})


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@ class CourseHomeMetadataSerializer(VerifiedModeSerializer):
can_view_certificate = serializers.BooleanField()
course_modes = CourseModeSerrializer(many=True)
is_new_discussion_sidebar_view_enabled = serializers.BooleanField()
has_course_author_access = serializers.BooleanField()
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@

from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.roles import CourseInstructorRole
from common.djangoapps.student.roles import (
CourseBetaTesterRole,
CourseInstructorRole,
CourseLimitedStaffRole,
CourseStaffRole
)
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests
from lms.djangoapps.courseware.toggles import (
Expand Down Expand Up @@ -247,3 +252,32 @@ def test_discussion_tab_visible(self, visible):
assert 'discussion' in tab_ids
else:
assert 'discussion' not in tab_ids

@ddt.data(
{
'course_team_role': None,
'has_course_author_access': False
},
{
'course_team_role': CourseBetaTesterRole,
'has_course_author_access': False
},
{
'course_team_role': CourseStaffRole,
'has_course_author_access': True
},
{
'course_team_role': CourseLimitedStaffRole,
'has_course_author_access': False
},
)
@ddt.unpack
def test_has_course_author_access_for_staff_roles(self, course_team_role, has_course_author_access):
CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED)

if course_team_role:
course_team_role(self.course.id).add_users(self.user)

response = self.client.get(self.url)
assert response.status_code == 200
assert response.data['has_course_author_access'] == has_course_author_access
7 changes: 7 additions & 0 deletions lms/djangoapps/course_home_api/course_metadata/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from openedx.core.djangoapps.courseware_api.utils import get_celebrations_dict

from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.auth import has_course_author_access
from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.course_api.api import course_detail
from lms.djangoapps.course_goals.models import UserActivity
Expand Down Expand Up @@ -140,6 +141,12 @@ def get(self, request, *args, **kwargs):
'can_view_certificate': certificates_viewable_for_course(course),
'course_modes': course_modes,
'is_new_discussion_sidebar_view_enabled': new_discussion_sidebar_view_is_enabled(course_key),
# We check the course author access in the context of CMS here because this field is used
# to determine whether the user can access the course authoring tools in the CMS.
# This is a temporary solution until the course author role is split into "Course Author" and
# "Course Editor" as described in the permission matrix here:
# https://github.com/openedx/platform-roadmap/issues/246
'has_course_author_access': has_course_author_access(request.user, course_key, 'cms'),
}
context = self.get_serializer_context()
context['course'] = course
Expand Down
6 changes: 6 additions & 0 deletions lms/djangoapps/courseware/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
from django.views.generic import View
from edx_django_utils.monitoring import set_custom_attribute, set_custom_attributes_for_course_key
from ipware.ip import get_client_ip
from xblock.core import XBlock

from lms.djangoapps.static_template_view.views import render_500
from markupsafe import escape
from opaque_keys import InvalidKeyError
Expand Down Expand Up @@ -1562,6 +1564,10 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True, disable_sta
set_custom_attributes_for_course_key(course_key)
set_custom_attribute('usage_key', usage_key_string)
set_custom_attribute('block_type', usage_key.block_type)
block_class = XBlock.load_class(usage_key.block_type)
if hasattr(block_class, 'is_extracted'):
is_extracted = block_class.is_extracted
set_custom_attribute('block_extracted', is_extracted)

requested_view = request.GET.get('view', 'student_view')
if requested_view != 'student_view' and requested_view != 'public_view': # lint-amnesty, pylint: disable=consider-using-in
Expand Down
37 changes: 32 additions & 5 deletions lms/djangoapps/instructor/tests/test_certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ def test_certificate_regeneration_error(self):

# Assert Error Message
assert res_json['message'] ==\
'Please select one or more certificate statuses that require certificate regeneration.'
'Please select certificate statuses from the list only.'

# Access the url passing 'certificate_statuses' that are not present in db
url = reverse('start_certificate_regeneration', kwargs={'course_id': str(self.course.id)})
Expand All @@ -378,7 +378,8 @@ def test_certificate_regeneration_error(self):
res_json = json.loads(response.content.decode('utf-8'))

# Assert Error Message
assert res_json['message'] == 'Please select certificate statuses from the list only.'
assert (res_json['message'] ==
'Please select certificate statuses from the list only.')


@override_settings(CERT_QUEUE='certificates')
Expand Down Expand Up @@ -488,9 +489,7 @@ def test_certificate_exception_missing_username_and_email_error(self):
assert not res_json['success']

# Assert Error Message
assert res_json['message'] ==\
'Student username/email field is required and can not be empty.' \
' Kindly fill in username/email and then press "Add to Exception List" button.'
assert res_json['message'] == {'user': ['This field may not be blank.']}

def test_certificate_exception_duplicate_user_error(self):
"""
Expand Down Expand Up @@ -604,6 +603,34 @@ def test_certificate_exception_removed_successfully(self):
# Verify that certificate exception does not exist
assert not certs_api.is_on_allowlist(self.user2, self.course.id)

def test_certificate_exception_removed_successfully_form_url(self):
"""
In case of deletion front-end is sending content-type x-www-form-urlencoded.
Just to handle that some logic added in api and this test is for that part.
Test certificates exception removal api endpoint returns success status
when called with valid course key and certificate exception id
"""
GeneratedCertificateFactory.create(
user=self.user2,
course_id=self.course.id,
status=CertificateStatuses.downloadable,
grade='1.0'
)
# Verify that certificate exception exists
assert certs_api.is_on_allowlist(self.user2, self.course.id)

response = self.client.post(
self.url,
data=json.dumps(self.certificate_exception_in_db),
content_type='application/x-www-form-urlencoded',
REQUEST_METHOD='DELETE'
)
# Assert successful request processing
assert response.status_code == 204

# Verify that certificate exception does not exist
assert not certs_api.is_on_allowlist(self.user2, self.course.id)

def test_remove_certificate_exception_invalid_request_error(self):
"""
Test certificates exception removal api endpoint returns error
Expand Down
Loading

0 comments on commit cfbcb61

Please sign in to comment.