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-rest.pre-rebase
  • Loading branch information
pomegranited committed Sep 2, 2024
2 parents 23f5d5b + 366a7e9 commit 0a1732e
Show file tree
Hide file tree
Showing 19 changed files with 624 additions and 45 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>"
30 changes: 28 additions & 2 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,7 +2278,7 @@ 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_updates",
Expand Down
6 changes: 3 additions & 3 deletions lms/djangoapps/grades/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"""


class DatabaseNotReadyError(IOError):
class ScoreNotFoundError(IOError):
"""
Subclass of IOError to indicate the database has not yet committed
the data we're trying to find.
Subclass of IOError to indicate the staff has not yet graded the problem or
the database has not yet committed the data we're trying to find.
"""
pass # lint-amnesty, pylint: disable=unnecessary-pass
6 changes: 3 additions & 3 deletions lms/djangoapps/grades/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from .config.waffle import DISABLE_REGRADE_ON_POLICY_CHANGE
from .constants import ScoreDatabaseTableEnum
from .course_grade_factory import CourseGradeFactory
from .exceptions import DatabaseNotReadyError
from .exceptions import ScoreNotFoundError
from .grade_utils import are_grades_frozen
from .signals.signals import SUBSECTION_SCORE_CHANGED
from .subsection_grade_factory import SubsectionGradeFactory
Expand All @@ -45,7 +45,7 @@
KNOWN_RETRY_ERRORS = ( # Errors we expect occasionally, should be resolved on retry
DatabaseError,
ValidationError,
DatabaseNotReadyError,
ScoreNotFoundError,
UsageKeyNotInBlockStructure,
)
RECALCULATE_GRADE_DELAY_SECONDS = 2 # to prevent excessive _has_db_updated failures. See TNL-6424.
Expand Down Expand Up @@ -239,7 +239,7 @@ def _recalculate_subsection_grade(self, **kwargs):
has_database_updated = _has_db_updated_with_new_score(self, scored_block_usage_key, **kwargs)

if not has_database_updated:
raise DatabaseNotReadyError
raise ScoreNotFoundError

_update_subsection_grades(
course_key,
Expand Down
45 changes: 28 additions & 17 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx.core.djangoapps.course_groups.cohorts import get_cohort_by_name
from rest_framework.exceptions import MethodNotAllowed
from rest_framework import serializers, status # lint-amnesty, pylint: disable=wrong-import-order
from rest_framework.permissions import IsAdminUser, IsAuthenticated # lint-amnesty, pylint: disable=wrong-import-order
from rest_framework.response import Response # lint-amnesty, pylint: disable=wrong-import-order
Expand Down Expand Up @@ -1510,28 +1511,38 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red
return JsonResponse({"status": success_status})


@transaction.non_atomic_requests
@require_POST
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_course_permission(permissions.CAN_RESEARCH)
@common_exceptions_400
def get_students_who_may_enroll(request, course_id):
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
@method_decorator(transaction.non_atomic_requests, name='dispatch')
class GetStudentsWhoMayEnroll(DeveloperErrorViewMixin, APIView):
"""
Initiate generation of a CSV file containing information about
students who may enroll in a course.
"""
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
permission_name = permissions.CAN_RESEARCH

Responds with JSON
{"status": "... status message ..."}
@method_decorator(ensure_csrf_cookie)
@method_decorator(transaction.non_atomic_requests)
def post(self, request, course_id):
"""
Initiate generation of a CSV file containing information about
students who may enroll in a course.
"""
course_key = CourseKey.from_string(course_id)
query_features = ['email']
report_type = _('enrollment')
task_api.submit_calculate_may_enroll_csv(request, course_key, query_features)
success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type)
Responds with JSON
{"status": "... status message ..."}
"""
course_key = CourseKey.from_string(course_id)
query_features = ['email']
report_type = _('enrollment')
try:
task_api.submit_calculate_may_enroll_csv(request, course_key, query_features)
success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type)
except Exception as e:
raise self.api_error(status.HTTP_400_BAD_REQUEST, str(e), 'Requested task is already running')

return JsonResponse({"status": success_status})
return JsonResponse({"status": success_status})

def get(self, request, *args, **kwargs):
raise MethodNotAllowed('GET')


def _cohorts_csv_validator(file_storage, file_to_validate):
Expand Down
2 changes: 1 addition & 1 deletion lms/djangoapps/instructor/views/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
path('get_grading_config', api.get_grading_config, name='get_grading_config'),
re_path(r'^get_students_features(?P<csv>/csv)?$', api.get_students_features, name='get_students_features'),
path('get_issued_certificates/', api.get_issued_certificates, name='get_issued_certificates'),
path('get_students_who_may_enroll', api.get_students_who_may_enroll, name='get_students_who_may_enroll'),
path('get_students_who_may_enroll', api.GetStudentsWhoMayEnroll.as_view(), name='get_students_who_may_enroll'),
path('get_anon_ids', api.get_anon_ids, name='get_anon_ids'),
path('get_student_enrollment_status', api.get_student_enrollment_status, name="get_student_enrollment_status"),
path('get_student_progress_url', api.StudentProgressUrl.as_view(), name='get_student_progress_url'),
Expand Down
Loading

0 comments on commit 0a1732e

Please sign in to comment.