Skip to content

Commit

Permalink
Merge branch 'master' into rpenido/fal-3621-paste-tags-when-pasting-x…
Browse files Browse the repository at this point in the history
…blocks-with-tag-data
  • Loading branch information
rpenido committed Feb 29, 2024
2 parents 59e37db + f544a48 commit d44efc0
Show file tree
Hide file tree
Showing 57 changed files with 970 additions and 117 deletions.
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ openedx/core/djangoapps/credit @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/core/djangoapps/user_authn/ @openedx/2U-vanguards
openedx/features/course_experience/
xmodule/

Expand Down
13 changes: 13 additions & 0 deletions cms/djangoapps/contentstore/config/waffle.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,16 @@
# .. toggle_warning: Flag course_experience.relative_dates should also be active for relative dates functionalities to work.
# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-844
CUSTOM_RELATIVE_DATES = CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.custom_relative_dates', __name__)


# .. toggle_name: studio.enable_course_update_notifications
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: Waffle flag to enable course update notifications.
# .. toggle_use_cases: temporary, open_edx
# .. toggle_creation_date: 14-Feb-2024
# .. toggle_target_removal_date: 14-Mar-2024
ENABLE_COURSE_UPDATE_NOTIFICATIONS = CourseWaffleFlag(
f'{WAFFLE_NAMESPACE}.enable_course_update_notifications',
__name__
)
18 changes: 14 additions & 4 deletions cms/djangoapps/contentstore/course_info_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
from django.http import HttpResponseBadRequest
from django.utils.translation import gettext as _

from cms.djangoapps.contentstore.utils import track_course_update_event
from cms.djangoapps.contentstore.config.waffle import ENABLE_COURSE_UPDATE_NOTIFICATIONS
from cms.djangoapps.contentstore.utils import track_course_update_event, send_course_update_notification
from openedx.core.lib.xblock_utils import get_course_update_items
from xmodule.html_block import CourseInfoBlock # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
Expand All @@ -43,7 +44,7 @@ def get_course_updates(location, provided_id, user_id):
return _get_visible_update(course_update_items)


def update_course_updates(location, update, passed_id=None, user=None):
def update_course_updates(location, update, passed_id=None, user=None, request_method=None):
"""
Either add or update the given course update.
Add:
Expand Down Expand Up @@ -86,8 +87,17 @@ def update_course_updates(location, update, passed_id=None, user=None):

# update db record
save_course_update_items(location, course_updates, course_update_items, user)
# track course update event
track_course_update_event(location.course_key, user, course_update_dict)

if request_method == "POST":
# track course update event
track_course_update_event(location.course_key, user, course_update_dict)

# send course update notification
if ENABLE_COURSE_UPDATE_NOTIFICATIONS.is_enabled(location.course_key):
send_course_update_notification(
location.course_key, course_update_dict["content"], user,
)

# remove status key
if "status" in course_update_dict:
del course_update_dict["status"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ class XblockSerializer(StrictSerializer):
target_index = serializers.IntegerField(required=False, allow_null=True)
boilerplate = serializers.JSONField(required=False, allow_null=True)
staged_content = serializers.CharField(required=False, allow_null=True)
hide_from_toc = serializers.BooleanField(required=False, allow_null=True)
17 changes: 17 additions & 0 deletions cms/djangoapps/contentstore/tests/test_contentstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,23 @@ def test_create_block(self):
).replace('REPLACE', r'([0-9]|[a-f]){3,}')
self.assertRegex(data['locator'], retarget)

@ddt.data(True, False)
def test_hide_xblock_from_toc_via_handler(self, hide_from_toc):
"""Test that the hide_from_toc field can be set via the xblock_handler."""
course = CourseFactory.create()
sequential = BlockFactory.create(parent_location=course.location)
data = {
"metadata": {
"hide_from_toc": hide_from_toc
}
}

response = self.client.ajax_post(get_url("xblock_handler", sequential.location), data)
sequential = self.store.get_item(sequential.location)

self.assertEqual(response.status_code, 200)
self.assertEqual(hide_from_toc, sequential.hide_from_toc)

def test_capa_block(self):
"""Test that a problem treats markdown specially."""
course = CourseFactory.create()
Expand Down
29 changes: 29 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations
import configparser
import logging
import re
from collections import defaultdict
from contextlib import contextmanager
from datetime import datetime, timezone
Expand All @@ -22,6 +23,9 @@
from opaque_keys.edx.locator import LibraryLocator
from openedx_events.content_authoring.data import DuplicatedXBlockData
from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED
from openedx_events.learning.data import CourseNotificationData
from openedx_events.learning.signals import COURSE_NOTIFICATION_REQUESTED

from milestones import api as milestones_api
from pytz import UTC
from xblock.fields import Scope
Expand Down Expand Up @@ -62,6 +66,7 @@
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.lib.courses import course_image_url
from openedx.core.lib.html_to_text import html_to_text
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from openedx.features.content_type_gating.partitions import CONTENT_TYPE_GATING_SCHEME
from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML
Expand Down Expand Up @@ -1951,3 +1956,27 @@ def track_course_update_event(course_key, user, event_data=None):
context = contexts.course_context_from_course_id(course_key)
with tracker.get_tracker().context(event_name, context):
tracker.emit(event_name, event_data)


def send_course_update_notification(course_key, content, user):
"""
Send course update notification
"""
text_content = re.sub(r"(\s| |//)+", " ", html_to_text(content))
course = modulestore().get_course(course_key)
extra_context = {
'author_id': user.id,
'course_name': course.display_name,
}
notification_data = CourseNotificationData(
course_key=course_key,
content_context={
"course_update_content": text_content,
**extra_context,
},
notification_type="course_update",
content_url=f"{settings.LMS_BASE}/courses/{str(course_key)}/course/updates",
app_name="updates",
audience_filters={},
)
COURSE_NOTIFICATION_REQUESTED.send_event(course_notification_data=notification_data)
4 changes: 3 additions & 1 deletion cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,9 @@ def course_info_update_handler(request, course_key_string, provided_id=None):
# can be either and sometimes django is rewriting one to the other:
elif request.method in ('POST', 'PUT'):
try:
return JsonResponse(update_course_updates(usage_key, request.json, provided_id, request.user))
return JsonResponse(update_course_updates(
usage_key, request.json, provided_id, request.user, request.method
))
except: # lint-amnesty, pylint: disable=bare-except
return HttpResponseBadRequest(
"Failed to save",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,8 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
"group_access": xblock.group_access,
"user_partitions": user_partitions,
"show_correctness": xblock.show_correctness,
"hide_from_toc": xblock.hide_from_toc,
"enable_hide_from_toc_ui": settings.FEATURES.get("ENABLE_HIDE_FROM_TOC_UI", False),
}
)

Expand Down Expand Up @@ -1217,6 +1219,15 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
else:
xblock_info["staff_only_message"] = False

if xblock_info["hide_from_toc"]:
xblock_info["hide_from_toc_message"] = True
elif child_info and child_info["children"]:
xblock_info["hide_from_toc_message"] = all(
child["hide_from_toc_message"] for child in child_info["children"]
)
else:
xblock_info["hide_from_toc_message"] = False

# If the ENABLE_TAGGING_TAXONOMY_LIST_PAGE feature flag is enabled, we show the "Manage Tags" options
if use_tagging_taxonomy_list_page():
xblock_info["use_tagging_taxonomy_list_page"] = True
Expand Down Expand Up @@ -1345,6 +1356,7 @@ class VisibilityState:
needs_attention = "needs_attention"
staff_only = "staff_only"
gated = "gated"
hide_from_toc = "hide_from_toc"


def _compute_visibility_state(
Expand All @@ -1355,29 +1367,36 @@ def _compute_visibility_state(
"""
if xblock.visible_to_staff_only:
return VisibilityState.staff_only
elif xblock.hide_from_toc:
return VisibilityState.hide_from_toc
elif is_unit_with_changes:
# Note that a unit that has never been published will fall into this category,
# as well as previously published units with draft content.
return VisibilityState.needs_attention

is_unscheduled = xblock.start == DEFAULT_START_DATE
is_live = is_course_self_paced or datetime.now(UTC) > xblock.start
if child_info and child_info.get("children", []):
if child_info and child_info.get("children", []): # pylint: disable=too-many-nested-blocks
all_staff_only = True
all_unscheduled = True
all_live = True
all_hide_from_toc = True
for child in child_info["children"]:
child_state = child["visibility_state"]
if child_state == VisibilityState.needs_attention:
return child_state
elif not child_state == VisibilityState.staff_only:
all_staff_only = False
if not child_state == VisibilityState.unscheduled:
all_unscheduled = False
if not child_state == VisibilityState.live:
all_live = False
if not child_state == VisibilityState.hide_from_toc:
all_hide_from_toc = False
if not child_state == VisibilityState.unscheduled:
all_unscheduled = False
if not child_state == VisibilityState.live:
all_live = False
if all_staff_only:
return VisibilityState.staff_only
elif all_hide_from_toc:
return VisibilityState.hide_from_toc
elif all_unscheduled:
return (
VisibilityState.unscheduled
Expand Down
8 changes: 8 additions & 0 deletions cms/static/js/models/xblock_info.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@ define(
* List of tags of the unit. This list is managed by the content_tagging module.
*/
tags: null,
/**
* True if the xblock is not visible to students only via links.
*/
hide_from_toc: null,
/**
* True iff this xblock should display a "Contains staff only content" message.
*/
hide_from_toc_message: null,
},

initialize: function() {
Expand Down
26 changes: 21 additions & 5 deletions cms/static/js/spec/views/pages/course_outline_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ describe('CourseOutlinePage', function() {
user_partition_info: {},
highlights_enabled: true,
highlights_enabled_for_messaging: false,
hide_from_toc: false,
enable_hide_from_toc_ui: true
}, options, {child_info: {children: children}});
};

Expand All @@ -68,7 +70,9 @@ describe('CourseOutlinePage', function() {
show_review_rules: true,
user_partition_info: {},
highlights_enabled: true,
highlights_enabled_for_messaging: false
highlights_enabled_for_messaging: false,
hide_from_toc: false,
enable_hide_from_toc_ui: true
}, options, {child_info: {children: children}});
};

Expand All @@ -93,7 +97,9 @@ describe('CourseOutlinePage', function() {
group_access: {},
user_partition_info: {},
highlights: [],
highlights_enabled: true
highlights_enabled: true,
hide_from_toc: false,
enable_hide_from_toc_ui: true
}, options, {child_info: {children: children}});
};

Expand Down Expand Up @@ -123,7 +129,9 @@ describe('CourseOutlinePage', function() {
},
user_partitions: [],
group_access: {},
user_partition_info: {}
user_partition_info: {},
hide_from_toc: false,
enable_hide_from_toc_ui: true
}, options, {child_info: {children: children}});
};

Expand All @@ -141,7 +149,9 @@ describe('CourseOutlinePage', function() {
edited_by: 'MockUser',
user_partitions: [],
group_access: {},
user_partition_info: {}
user_partition_info: {},
hide_from_toc: false,
enable_hide_from_toc_ui: true
}, options);
};

Expand Down Expand Up @@ -1214,7 +1224,9 @@ describe('CourseOutlinePage', function() {
is_practice_exam: false,
is_proctored_exam: false,
default_time_limit_minutes: 150,
hide_after_due: true
hide_after_due: true,
hide_from_toc: false,
enable_hide_from_toc_ui: true,
}, [
createMockVerticalJSON({
has_changes: true,
Expand Down Expand Up @@ -1397,6 +1409,7 @@ describe('CourseOutlinePage', function() {
default_time_limit_minutes: 150,
hide_after_due: true,
is_onboarding_exam: false,
hide_from_toc: null,
}
});
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
Expand Down Expand Up @@ -2240,6 +2253,8 @@ describe('CourseOutlinePage', function() {
is_practice_exam: false,
is_proctored_exam: false,
default_time_limit_minutes: null,
hide_from_toc: false,
enable_hide_from_toc_ui: true,
}, [
createMockVerticalJSON({
has_changes: true,
Expand Down Expand Up @@ -2521,6 +2536,7 @@ describe('CourseOutlinePage', function() {
publish: 'republish',
metadata: {
visible_to_staff_only: null,
hide_from_toc: null
}
});
})
Expand Down
Loading

0 comments on commit d44efc0

Please sign in to comment.