Skip to content

Commit

Permalink
Merge branch 'master' into Fix_Improper_Method_Call_mktemp
Browse files Browse the repository at this point in the history
  • Loading branch information
e0d authored Sep 6, 2024
2 parents 58b00ba + b615a87 commit 4e630fa
Show file tree
Hide file tree
Showing 150 changed files with 4,552 additions and 4,692 deletions.
34 changes: 34 additions & 0 deletions cms/djangoapps/contentstore/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.conf import settings
from django.test import TestCase
from django.test.utils import override_settings
from edx_toggles.toggles.testutils import override_waffle_flag
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import CourseLocator, LibraryLocator
from path import Path as path
Expand All @@ -19,7 +20,11 @@
from cms.djangoapps.contentstore import utils
from cms.djangoapps.contentstore.tasks import ALL_ALLOWED_XBLOCKS, validate_course_olx
from cms.djangoapps.contentstore.tests.utils import TEST_DATA_DIR, CourseTestCase
from cms.djangoapps.contentstore.utils import send_course_update_notification
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import GlobalStaffFactory, InstructorFactory, UserFactory
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS
from openedx.core.djangoapps.notifications.models import CourseNotificationPreference, Notification
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
Expand Down Expand Up @@ -927,3 +932,32 @@ def test_update_course_details_instructor_paced(self, mock_update):

utils.update_course_details(mock_request, self.course.id, payload, None)
mock_update.assert_called_once_with(self.course.id, payload, mock_request.user)


@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True)
class CourseUpdateNotificationTests(ModuleStoreTestCase):
"""
Unit tests for the course_update notification.
"""

def setUp(self):
"""
Setup the test environment.
"""
super().setUp()
self.user = UserFactory()
self.course = CourseFactory.create(org='testorg', number='testcourse', run='testrun')
CourseNotificationPreference.objects.create(user_id=self.user.id, course_id=self.course.id)

def test_course_update_notification_sent(self):
"""
Test that the course_update notification is sent.
"""
user = UserFactory()
CourseEnrollment.enroll(user=user, course_key=self.course.id)
assert Notification.objects.all().count() == 0
content = "<p>content</p><img src='' />"
send_course_update_notification(self.course.id, content, self.user)
assert Notification.objects.all().count() == 1
notification = Notification.objects.first()
assert notification.content == "<p><strong><p>content</p></strong></p>"
32 changes: 29 additions & 3 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@
from urllib.parse import quote_plus
from uuid import uuid4

from bs4 import BeautifulSoup
from django.conf import settings
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.utils import translation
from django.utils.text import Truncator
from django.utils.translation import gettext as _
from eventtracking import tracker
from help_tokens.core import HelpUrlExpert
from lti_consumer.models import CourseAllowPIISharingInLTIFlag
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locator import LibraryLocator

from openedx.core.lib.teams_config import CONTENT_GROUPS_FOR_TEAMS, TEAM_SCHEME
from openedx_events.content_authoring.data import DuplicatedXBlockData
from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED
Expand Down Expand Up @@ -2239,11 +2242,34 @@ def track_course_update_event(course_key, user, course_update_content=None):
tracker.emit(event_name, event_data)


def clean_html_body(html_body):
"""
Get html body, remove tags and limit to 500 characters
"""
html_body = BeautifulSoup(Truncator(html_body).chars(500, html=True), 'html.parser')

tags_to_remove = [
"a", "link", # Link Tags
"img", "picture", "source", # Image Tags
"video", "track", # Video Tags
"audio", # Audio Tags
"embed", "object", "iframe", # Embedded Content
"script"
]

# Remove the specified tags while keeping their content
for tag in tags_to_remove:
for match in html_body.find_all(tag):
match.unwrap()

return str(html_body)


def send_course_update_notification(course_key, content, user):
"""
Send course update notification
"""
text_content = re.sub(r"(\s|&nbsp;|//)+", " ", html_to_text(content))
text_content = re.sub(r"(\s|&nbsp;|//)+", " ", clean_html_body(content))
course = modulestore().get_course(course_key)
extra_context = {
'author_id': user.id,
Expand All @@ -2252,10 +2278,10 @@ def send_course_update_notification(course_key, content, user):
notification_data = CourseNotificationData(
course_key=course_key,
content_context={
"course_update_content": text_content if len(text_content.strip()) < 10 else "Click here to view",
"course_update_content": text_content,
**extra_context,
},
notification_type="course_update",
notification_type="course_updates",
content_url=f"{settings.LMS_ROOT_URL}/courses/{str(course_key)}/course/updates",
app_name="updates",
audience_filters={},
Expand Down
7 changes: 3 additions & 4 deletions 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 Expand Up @@ -1449,9 +1448,8 @@
'edx-ui-toolkit/js/utils/string-utils.js',
'edx-ui-toolkit/js/utils/html-utils.js',

# Load Bootstrap and supporting libraries
'common/js/vendor/popper.js',
'common/js/vendor/bootstrap.js',
# Here we were loading Bootstrap and supporting libraries, but it no longer seems to be needed for any Studio UI.
# 'common/js/vendor/bootstrap.bundle.js',

# Finally load RequireJS
'common/js/vendor/require.js'
Expand Down Expand Up @@ -1880,6 +1878,7 @@
'openedx_events',

# Learning Core Apps, used by v2 content libraries (content_libraries app)
"openedx_learning.apps.authoring.collections",
"openedx_learning.apps.authoring.components",
"openedx_learning.apps.authoring.contents",
"openedx_learning.apps.authoring.publishing",
Expand Down
2 changes: 2 additions & 0 deletions cms/static/sass/studio-main-v1.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

// +Libs and Resets - *do not edit*
// ====================

@import '_builtin-block-variables';
@import 'bourbon/bourbon'; // lib - bourbon
@import 'vendor/bi-app/bi-app-ltr'; // set the layout for left to right languages
@import 'build-v1'; // shared app style assets/rendering
10 changes: 0 additions & 10 deletions common/djangoapps/entitlements/rest_api/v1/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"""


from django.conf import settings
from rest_framework.permissions import SAFE_METHODS, BasePermission

from lms.djangoapps.courseware.access import has_access
Expand All @@ -22,12 +21,3 @@ def has_permission(self, request, view):
return request.user.is_authenticated
else:
return request.user.is_staff or has_access(request.user, "support", "global")


class IsSubscriptionWorkerUser(BasePermission):
"""
Method that will require the request to be coming from the subscriptions service worker user.
"""

def has_permission(self, request, view):
return request.user.username == settings.SUBSCRIPTIONS_SERVICE_WORKER_USERNAME
158 changes: 0 additions & 158 deletions common/djangoapps/entitlements/rest_api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import uuid
from datetime import datetime, timedelta
from unittest.mock import patch
from uuid import uuid4

from django.conf import settings
from django.urls import reverse
Expand Down Expand Up @@ -1236,160 +1235,3 @@ def test_user_is_not_unenrolled_on_failed_refund(
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
assert course_entitlement.enrollment_course_run is not None
assert course_entitlement.expired_at is None


@skip_unless_lms
class RevokeSubscriptionsVerifiedAccessViewTest(ModuleStoreTestCase):
"""
Tests for the RevokeVerifiedAccessView
"""
REVOKE_VERIFIED_ACCESS_PATH = 'entitlements_api:v1:revoke_subscriptions_verified_access'

def setUp(self):
super().setUp()
self.user = UserFactory(username="subscriptions_worker", is_staff=True)
self.client.login(username=self.user.username, password=TEST_PASSWORD)
self.course = CourseFactory()
self.course_mode1 = CourseModeFactory(
course_id=self.course.id, # pylint: disable=no-member
mode_slug=CourseMode.VERIFIED,
expiration_datetime=now() + timedelta(days=1)
)
self.course_mode2 = CourseModeFactory(
course_id=self.course.id, # pylint: disable=no-member
mode_slug=CourseMode.AUDIT,
expiration_datetime=now() + timedelta(days=1)
)

@patch('common.djangoapps.entitlements.rest_api.v1.views.get_courses_completion_status')
def test_revoke_access_success(self, mock_get_courses_completion_status):
mock_get_courses_completion_status.return_value = ([], False)
enrollment = CourseEnrollmentFactory.create(
user=self.user,
course_id=self.course.id, # pylint: disable=no-member
is_active=True,
mode=CourseMode.VERIFIED
)
course_entitlement = CourseEntitlementFactory.create(user=self.user, enrollment_course_run=enrollment)
url = reverse(self.REVOKE_VERIFIED_ACCESS_PATH)

assert course_entitlement.enrollment_course_run is not None

response = self.client.post(
url,
data={
"entitlement_uuids": [str(course_entitlement.uuid)],
"lms_user_id": self.user.id
},
content_type='application/json',
)
assert response.status_code == 204

course_entitlement.refresh_from_db()
enrollment.refresh_from_db()
assert course_entitlement.expired_at is not None
assert course_entitlement.enrollment_course_run is None
assert enrollment.mode == CourseMode.AUDIT

@patch('common.djangoapps.entitlements.rest_api.v1.views.get_courses_completion_status')
def test_already_completed_course(self, mock_get_courses_completion_status):
enrollment = CourseEnrollmentFactory.create(
user=self.user,
course_id=self.course.id, # pylint: disable=no-member
is_active=True,
mode=CourseMode.VERIFIED
)
mock_get_courses_completion_status.return_value = ([str(enrollment.course_id)], False)
course_entitlement = CourseEntitlementFactory.create(user=self.user, enrollment_course_run=enrollment)
url = reverse(self.REVOKE_VERIFIED_ACCESS_PATH)

assert course_entitlement.enrollment_course_run is not None

response = self.client.post(
url,
data={
"entitlement_uuids": [str(course_entitlement.uuid)],
"lms_user_id": self.user.id
},
content_type='application/json',
)
assert response.status_code == 204

course_entitlement.refresh_from_db()
assert course_entitlement.expired_at is None
assert course_entitlement.enrollment_course_run.mode == CourseMode.VERIFIED

@patch('common.djangoapps.entitlements.rest_api.v1.views.log.info')
def test_revoke_access_invalid_uuid(self, mock_log):
url = reverse(self.REVOKE_VERIFIED_ACCESS_PATH)
entitlement_uuids = [str(uuid4())]
response = self.client.post(
url,
data={
"entitlement_uuids": entitlement_uuids,
"lms_user_id": self.user.id
},
content_type='application/json',
)

mock_log.assert_called_once_with("B2C_SUBSCRIPTIONS: Entitlements not found for the provided"
" entitlements data: %s and user: %s",
entitlement_uuids,
self.user.id)
assert response.status_code == 204

def test_revoke_access_unauthorized_user(self):
user = UserFactory(is_staff=True, username='not_subscriptions_worker')
self.client.login(username=user.username, password=TEST_PASSWORD)

enrollment = CourseEnrollmentFactory.create(
user=self.user,
course_id=self.course.id, # pylint: disable=no-member
is_active=True,
mode=CourseMode.VERIFIED
)
course_entitlement = CourseEntitlementFactory.create(user=self.user, enrollment_course_run=enrollment)
url = reverse(self.REVOKE_VERIFIED_ACCESS_PATH)

assert course_entitlement.enrollment_course_run is not None

response = self.client.post(
url,
data={
"entitlement_uuids": [],
"lms_user_id": self.user.id
},
content_type='application/json',
)
assert response.status_code == 403

course_entitlement.refresh_from_db()
assert course_entitlement.expired_at is None
assert course_entitlement.enrollment_course_run.mode == CourseMode.VERIFIED

@patch('common.djangoapps.entitlements.tasks.retry_revoke_subscriptions_verified_access.apply_async')
@patch('common.djangoapps.entitlements.rest_api.v1.views.get_courses_completion_status')
def test_course_completion_exception_triggers_task(self, mock_get_courses_completion_status, mock_task):
mock_get_courses_completion_status.return_value = ([], True)
enrollment = CourseEnrollmentFactory.create(
user=self.user,
course_id=self.course.id, # pylint: disable=no-member
is_active=True,
mode=CourseMode.VERIFIED
)
course_entitlement = CourseEntitlementFactory.create(user=self.user, enrollment_course_run=enrollment)

url = reverse(self.REVOKE_VERIFIED_ACCESS_PATH)

response = self.client.post(
url,
data={
"entitlement_uuids": [str(course_entitlement.uuid)],
"lms_user_id": self.user.id
},
content_type='application/json',
)
assert response.status_code == 204
mock_task.assert_called_once_with(args=([str(course_entitlement.uuid)],
[str(enrollment.course_id)],
self.user.username))
21 changes: 0 additions & 21 deletions common/djangoapps/entitlements/rest_api/v1/throttles.py

This file was deleted.

7 changes: 1 addition & 6 deletions common/djangoapps/entitlements/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.urls import path, re_path
from rest_framework.routers import DefaultRouter

from .views import EntitlementEnrollmentViewSet, EntitlementViewSet, SubscriptionsRevokeVerifiedAccessView
from .views import EntitlementEnrollmentViewSet, EntitlementViewSet

router = DefaultRouter()
router.register(r'entitlements', EntitlementViewSet, basename='entitlements')
Expand All @@ -24,9 +24,4 @@
ENROLLMENTS_VIEW,
name='enrollments'
),
path(
'subscriptions/entitlements/revoke',
SubscriptionsRevokeVerifiedAccessView.as_view(),
name='revoke_subscriptions_verified_access'
)
]
Loading

0 comments on commit 4e630fa

Please sign in to comment.