diff --git a/common/djangoapps/util/views.py b/common/djangoapps/util/views.py index 73a164561c..3204c9805d 100644 --- a/common/djangoapps/util/views.py +++ b/common/djangoapps/util/views.py @@ -213,3 +213,13 @@ def reset_course_deadlines(request): referrer = request.META.get('HTTP_REFERER') return redirect(referrer) if referrer else HttpResponse() + + +def expose_header(header, response): + """ + Add a header name to Access-Control-Expose-Headers to allow client code to access that header's value + """ + exposedHeaders = response.get('Access-Control-Expose-Headers', '') + exposedHeaders += f', {header}' if exposedHeaders else header + response['Access-Control-Expose-Headers'] = exposedHeaders + return response diff --git a/lms/djangoapps/course_home_api/outline/v1/views.py b/lms/djangoapps/course_home_api/outline/v1/views.py index 5869d33234..7bbb48530e 100644 --- a/lms/djangoapps/course_home_api/outline/v1/views.py +++ b/lms/djangoapps/course_home_api/outline/v1/views.py @@ -19,6 +19,7 @@ from rest_framework.response import Response from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import CourseEnrollment +from common.djangoapps.util.views import expose_header from lms.djangoapps.course_goals.api import ( add_course_goal, get_course_goal, @@ -295,6 +296,20 @@ class OutlineTabView(RetrieveAPIView): return Response(serializer.data) + def finalize_response(self, request, response, *args, **kwargs): + """ + Return the final response, exposing the 'Date' header for computing relative time to the dates in the data. + + Important dates such as 'access_expiration' are enforced server-side based on correct time; client-side clocks + are frequently substantially far off which could lead to inaccurate messaging and incorrect expectations. + Therefore, any messaging about those dates should be based on the server time and preferably in relative terms + (time remaining); the 'Date' header is a straightforward and generalizable way for client-side code to get this + reference. + """ + response = super().finalize_response(request, response, *args, **kwargs) + # Adding this header should be moved to global middleware, not just this endpoint + return expose_header('Date', response) + @api_view(['POST']) @authentication_classes((JwtAuthentication,)) diff --git a/openedx/core/djangoapps/courseware_api/views.py b/openedx/core/djangoapps/courseware_api/views.py index 5db49a5c72..9d946cd6ac 100644 --- a/openedx/core/djangoapps/courseware_api/views.py +++ b/openedx/core/djangoapps/courseware_api/views.py @@ -19,6 +19,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from common.djangoapps.course_modes.models import CourseMode +from common.djangoapps.util.views import expose_header from lms.djangoapps.edxnotes.helpers import is_feature_enabled from lms.djangoapps.certificates.api import get_certificate_url from lms.djangoapps.certificates.models import GeneratedCertificate @@ -444,13 +445,16 @@ class CoursewareInformation(RetrieveAPIView): def finalize_response(self, request, response, *args, **kwargs): """ Return the final response, exposing the 'Date' header for computing relative time to the dates in the data. + + Important dates such as 'access_expiration' are enforced server-side based on correct time; client-side clocks + are frequently substantially far off which could lead to inaccurate messaging and incorrect expectations. + Therefore, any messaging about those dates should be based on the server time and preferably in relative terms + (time remaining); the 'Date' header is a straightforward and generalizable way for client-side code to get this + reference. """ response = super().finalize_response(request, response, *args, **kwargs) - # Adding this header should be moved somewhere global, not just this endpoint - exposedHeaders = response.get('Access-Control-Expose-Headers', '') - exposedHeaders += ', Date' if exposedHeaders else 'Date' - response['Access-Control-Expose-Headers'] = exposedHeaders - return response + # Adding this header should be moved to global middleware, not just this endpoint + return expose_header('Date', response) class SequenceMetadata(DeveloperErrorViewMixin, APIView):