Skip to content

Commit

Permalink
Merge branch 'master' into xblock-v2-iframe
Browse files Browse the repository at this point in the history
  • Loading branch information
rpenido authored Sep 13, 2024
2 parents 254384c + dd59dc6 commit 45f392c
Show file tree
Hide file tree
Showing 41 changed files with 1,044 additions and 429 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class CourseHomeSerializer(serializers.Serializer):
allow_empty=True
)
archived_courses = CourseCommonSerializer(required=False, many=True)
can_access_advanced_settings = serializers.BooleanField()
can_create_organizations = serializers.BooleanField()
course_creator_status = serializers.CharField()
courses = CourseCommonSerializer(required=False, many=True)
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def get(self, request: Request):
"allow_unicode_course_id": false,
"allowed_organizations": [],
"archived_courses": [],
"can_access_advanced_settings": true,
"can_create_organizations": true,
"course_creator_status": "granted",
"courses": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def test_home_page_courses_response(self):
"allow_unicode_course_id": False,
"allowed_organizations": [],
"archived_courses": [],
"can_access_advanced_settings": True,
"can_create_organizations": True,
"course_creator_status": "granted",
"courses": [],
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1716,6 +1716,7 @@ def get_home_context(request, no_course=False):
'allowed_organizations': get_allowed_organizations(user),
'allowed_organizations_for_libraries': get_allowed_organizations_for_libraries(user),
'can_create_organizations': user_can_create_organizations(user),
'can_access_advanced_settings': auth.has_studio_advanced_settings_access(user),
}

return home_context
Expand Down
10 changes: 1 addition & 9 deletions common/static/sass/_builtin-block-variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

:root {
--action-primary-active-bg: $action-primary-active-bg;
--all-text-inputs: $all-text-inputs;
--base-font-size: $base-font-size;
--base-line-height: $base-line-height;
--baseline: $baseline;
Expand All @@ -25,7 +26,6 @@
--blue-d1: $blue-d1;
--blue-d2: $blue-d2;
--blue-d4: $blue-d4;
--blue-s1: $blue-s1;
--body-color: $body-color;
--border-color: $border-color;
--bp-screen-lg: $bp-screen-lg;
Expand All @@ -34,8 +34,6 @@
--danger: $danger;
--darkGrey: $darkGrey;
--error-color: $error-color;
--error-color-dark: darken($error-color, 11%);
--error-color-light: lighten($error-color, 25%);
--font-bold: $font-bold;
--font-family-sans-serif: $font-family-sans-serif;
--general-color-accent: $general-color-accent;
Expand All @@ -46,12 +44,6 @@
--gray-l3: $gray-l3;
--gray-l4: $gray-l4;
--gray-l6: $gray-l6;
--icon-correct: url($static-path + '/images/correct-icon.png');
--icon-incorrect: url($static-path + '/images/incorrect-icon.png');
--icon-info: url($static-path + '/images/info-icon.png');
--icon-partially-correct: url($static-path + '/images/partially-correct-icon.png');
--icon-spinner: url($static-path + '/images/spinner.gif');
--icon-unanswered: url($static-path + '/images/unanswered-icon.png');
--incorrect: $incorrect;
--lightGrey: $lightGrey;
--lighter-base-font-color: $lighter-base-font-color;
Expand Down
8 changes: 4 additions & 4 deletions docs/hooks/events.rst
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,6 @@ Content Authoring Events
- org.openedx.content_authoring.library_block.deleted.v1
- 2023-07-20

* - `CONTENT_OBJECT_TAGS_CHANGED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L207>`_
- org.openedx.content_authoring.content.object.tags.changed.v1
- 2024-03-31

* - `LIBRARY_COLLECTION_CREATED <https://github.com/openedx/openedx-events/blob/main/openedx_events/content_authoring/signals.py#L219>`_
- org.openedx.content_authoring.content_library.collection.created.v1
- 2024-08-23
Expand All @@ -259,3 +255,7 @@ Content Authoring Events
* - `LIBRARY_COLLECTION_DELETED <https://github.com/openedx/openedx-events/blob/main/openedx_events/content_authoring/signals.py#L241>`_
- org.openedx.content_authoring.content_library.collection.deleted.v1
- 2024-08-23

* - `CONTENT_OBJECT_ASSOCIATIONS_CHANGED <https://github.com/openedx/openedx-events/blob/eb17e03f075b272ad8a29e8435d6a514f8884131/openedx_events/content_authoring/signals.py#L205-L214>`_
- org.openedx.content_authoring.content.object.associations.changed.v1
- 2024-09-06
15 changes: 15 additions & 0 deletions lms/djangoapps/instructor/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4160,6 +4160,21 @@ def test_change_due_date(self):
# This operation regenerates the cache, so we can use cached results from edx-when.
assert get_date_for_block(self.course, self.week1, self.user1, use_cached=True) == due_date

def test_change_due_date_with_reason(self):
url = reverse('change_due_date', kwargs={'course_id': str(self.course.id)})
due_date = datetime.datetime(2013, 12, 30, tzinfo=UTC)
response = self.client.post(url, {
'student': self.user1.username,
'url': str(self.week1.location),
'due_datetime': '12/30/2013 00:00',
'reason': 'Testing reason.' # this is optional field.
})
assert response.status_code == 200, response.content

assert get_extended_due(self.course, self.week1, self.user1) == due_date
# This operation regenerates the cache, so we can use cached results from edx-when.
assert get_date_for_block(self.course, self.week1, self.user1, use_cached=True) == due_date

def test_change_to_invalid_due_date(self):
url = reverse('change_due_date', kwargs={'course_id': str(self.course.id)})
response = self.client.post(url, {
Expand Down
60 changes: 41 additions & 19 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@
from lms.djangoapps.instructor_task.data import InstructorTaskTypes
from lms.djangoapps.instructor_task.models import ReportStore
from lms.djangoapps.instructor.views.serializer import (
AccessSerializer, RoleNameSerializer, ShowStudentExtensionSerializer,
UserSerializer, SendEmailSerializer, StudentAttemptsSerializer
AccessSerializer, BlockDueDateSerializer, RoleNameSerializer, ShowStudentExtensionSerializer, UserSerializer,
SendEmailSerializer, StudentAttemptsSerializer
)
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, is_course_cohorted
Expand Down Expand Up @@ -2967,28 +2967,50 @@ def _display_unit(unit):
return str(unit.location)


@handle_dashboard_error
@require_POST
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_course_permission(permissions.GIVE_STUDENT_EXTENSION)
@require_post_params('student', 'url', 'due_datetime')
def change_due_date(request, course_id):
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
class ChangeDueDate(APIView):
"""
Grants a due date extension to a student for a particular unit.
"""
course = get_course_by_id(CourseKey.from_string(course_id))
student = require_student_from_identifier(request.POST.get('student'))
unit = find_unit(course, request.POST.get('url'))
due_date = parse_datetime(request.POST.get('due_datetime'))
reason = strip_tags(request.POST.get('reason', ''))
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
permission_name = permissions.GIVE_STUDENT_EXTENSION
serializer_class = BlockDueDateSerializer

set_due_date_extension(course, unit, student, due_date, request.user, reason=reason)
@method_decorator(ensure_csrf_cookie)
def post(self, request, course_id):
"""
Grants a due date extension to a student for a particular unit.
return JsonResponse(_(
'Successfully changed due date for student {0} for {1} '
'to {2}').format(student.profile.name, _display_unit(unit),
due_date.strftime('%Y-%m-%d %H:%M')))
params:
url (str): The URL related to the block that needs the due date update.
due_datetime (str): The new due date and time for the block.
student (str): The email or username of the student whose access is being modified.
"""
serializer_data = self.serializer_class(data=request.data)
if not serializer_data.is_valid():
return HttpResponseBadRequest(reason=serializer_data.errors)

student = serializer_data.validated_data.get('student')
if not student:
response_payload = {
'error': f'Could not find student matching identifier: {request.data.get("student")}'
}
return JsonResponse(response_payload)

course = get_course_by_id(CourseKey.from_string(course_id))

unit = find_unit(course, serializer_data.validated_data.get('url'))
due_date = parse_datetime(serializer_data.validated_data.get('due_datetime'))
reason = strip_tags(serializer_data.validated_data.get('reason', ''))
try:
set_due_date_extension(course, unit, student, due_date, request.user, reason=reason)
except Exception as error: # pylint: disable=broad-except
return JsonResponse({'error': str(error)}, status=400)

return JsonResponse(_(
'Successfully changed due date for student {0} for {1} '
'to {2}').format(student.profile.name, _display_unit(unit),
due_date.strftime('%Y-%m-%d %H:%M')))


@handle_dashboard_error
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 @@ -49,8 +49,8 @@
path('list_email_content', api.ListEmailContent.as_view(), name='list_email_content'),
path('list_forum_members', api.list_forum_members, name='list_forum_members'),
path('update_forum_role_membership', api.update_forum_role_membership, name='update_forum_role_membership'),
path('change_due_date', api.ChangeDueDate.as_view(), name='change_due_date'),
path('send_email', api.SendEmail.as_view(), name='send_email'),
path('change_due_date', api.change_due_date, name='change_due_date'),
path('reset_due_date', api.reset_due_date, name='reset_due_date'),
path('show_unit_extensions', api.show_unit_extensions, name='show_unit_extensions'),
path('show_student_extensions', api.ShowStudentExtensions.as_view(), name='show_student_extensions'),
Expand Down
29 changes: 29 additions & 0 deletions lms/djangoapps/instructor/views/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,32 @@ class SendEmailSerializer(serializers.Serializer):
subject = serializers.CharField(max_length=128, write_only=True, required=True)
message = serializers.CharField(required=True)
schedule = serializers.CharField(required=False)


class BlockDueDateSerializer(serializers.Serializer):
"""
Serializer for handling block due date updates for a specific student.
Fields:
url (str): The URL related to the block that needs the due date update.
due_datetime (str): The new due date and time for the block.
student (str): The email or username of the student whose access is being modified.
reason (str): Reason why updating this.
"""
url = serializers.CharField()
due_datetime = serializers.CharField()
student = serializers.CharField(
max_length=255,
help_text="Email or username of user to change access"
)
reason = serializers.CharField(required=False)

def validate_student(self, value):
"""
Validate that the student corresponds to an existing user.
"""
try:
user = get_student_from_identifier(value)
except User.DoesNotExist:
return None

return user
35 changes: 34 additions & 1 deletion openedx/core/djangoapps/content/search/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
searchable_doc_for_course_block,
searchable_doc_for_collection,
searchable_doc_for_library_block,
searchable_doc_collections,
searchable_doc_tags,
)

Expand Down Expand Up @@ -322,6 +323,9 @@ def rebuild_index(status_cb: Callable[[str], None] | None = None) -> None:
Fields.tags + "." + Fields.tags_level1,
Fields.tags + "." + Fields.tags_level2,
Fields.tags + "." + Fields.tags_level3,
Fields.collections,
Fields.collections + "." + Fields.collections_display_name,
Fields.collections + "." + Fields.collections_key,
Fields.type,
Fields.access_id,
Fields.last_published,
Expand All @@ -333,8 +337,9 @@ def rebuild_index(status_cb: Callable[[str], None] | None = None) -> None:
Fields.display_name,
Fields.block_id,
Fields.content,
Fields.tags,
Fields.description,
Fields.tags,
Fields.collections,
# If we don't list the following sub-fields _explicitly_, they're only sometimes searchable - that is, they
# are searchable only if at least one document in the index has a value. If we didn't list them here and,
# say, there were no tags.level3 tags in the index, the client would get an error if trying to search for
Expand All @@ -344,6 +349,8 @@ def rebuild_index(status_cb: Callable[[str], None] | None = None) -> None:
Fields.tags + "." + Fields.tags_level1,
Fields.tags + "." + Fields.tags_level2,
Fields.tags + "." + Fields.tags_level3,
Fields.collections + "." + Fields.collections_display_name,
Fields.collections + "." + Fields.collections_key,
])
# Mark which attributes can be used for sorting search results:
client.index(temp_index_name).update_sortable_attributes([
Expand Down Expand Up @@ -375,6 +382,7 @@ def index_library(lib_key: str) -> list:
doc = {}
doc.update(searchable_doc_for_library_block(metadata))
doc.update(searchable_doc_tags(metadata.usage_key))
doc.update(searchable_doc_collections(metadata.usage_key))
docs.append(doc)
except Exception as err: # pylint: disable=broad-except
status_cb(f"Error indexing library component {component}: {err}")
Expand Down Expand Up @@ -549,6 +557,22 @@ def upsert_library_block_index_doc(usage_key: UsageKey) -> None:
_update_index_docs(docs)


def upsert_library_collection_index_doc(library_key: LibraryLocatorV2, collection_key: str) -> None:
"""
Creates or updates the document for the given Library Collection in the search index
"""
content_library = lib_api.ContentLibrary.objects.get_by_key(library_key)
collection = authoring_api.get_collection(
learning_package_id=content_library.learning_package_id,
collection_key=collection_key,
)
docs = [
searchable_doc_for_collection(collection)
]

_update_index_docs(docs)


def upsert_content_library_index_docs(library_key: LibraryLocatorV2) -> None:
"""
Creates or updates the documents for the given Content Library in the search index
Expand All @@ -571,6 +595,15 @@ def upsert_block_tags_index_docs(usage_key: UsageKey):
_update_index_docs([doc])


def upsert_block_collections_index_docs(usage_key: UsageKey):
"""
Updates the collections data in documents for the given Course/Library block
"""
doc = {Fields.id: meili_id_from_opaque_key(usage_key)}
doc.update(searchable_doc_collections(usage_key))
_update_index_docs([doc])


def _get_user_orgs(request: Request) -> list[str]:
"""
Get the org.short_names for the organizations that the requesting user has OrgStaffRole or OrgInstructorRole.
Expand Down
Loading

0 comments on commit 45f392c

Please sign in to comment.