Merge pull request #23902 from edx/ddumesnil/dates-external-api-AA-115
AA-115: Adding external API for dates tab
This commit is contained in:
0
lms/djangoapps/course_home_api/__init__.py
Normal file
0
lms/djangoapps/course_home_api/__init__.py
Normal file
0
lms/djangoapps/course_home_api/dates/v1/__init__.py
Normal file
0
lms/djangoapps/course_home_api/dates/v1/__init__.py
Normal file
43
lms/djangoapps/course_home_api/dates/v1/serializers.py
Normal file
43
lms/djangoapps/course_home_api/dates/v1/serializers.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Dates Tab Serializers. Represents the relevant dates for a Course.
|
||||
"""
|
||||
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from lms.djangoapps.courseware.date_summary import VerificationDeadlineDate
|
||||
|
||||
|
||||
class DateSummarySerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for Date Summary Objects.
|
||||
"""
|
||||
date = serializers.DateTimeField()
|
||||
date_type = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
learner_has_access = serializers.SerializerMethodField()
|
||||
link = serializers.SerializerMethodField()
|
||||
title = serializers.CharField()
|
||||
|
||||
def get_learner_has_access(self, block):
|
||||
learner_is_verified = self.context.get('learner_is_verified', False)
|
||||
block_is_verified = (getattr(block, 'contains_gated_content', False) or
|
||||
isinstance(block, VerificationDeadlineDate))
|
||||
return (not block_is_verified) or learner_is_verified
|
||||
|
||||
def get_link(self, block):
|
||||
if block.link:
|
||||
request = self.context.get('request')
|
||||
return request.build_absolute_uri(block.link)
|
||||
return ''
|
||||
|
||||
|
||||
class DatesTabSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for the Dates Tab
|
||||
"""
|
||||
course_date_blocks = DateSummarySerializer(many=True)
|
||||
display_reset_dates_text = serializers.BooleanField()
|
||||
learner_is_verified = serializers.BooleanField()
|
||||
user_timezone = serializers.CharField()
|
||||
verified_upgrade_link = serializers.URLField()
|
||||
54
lms/djangoapps/course_home_api/dates/v1/tests/test_views.py
Normal file
54
lms/djangoapps/course_home_api/dates/v1/tests/test_views.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Tests for Dates Tab API in the Course Home API
|
||||
"""
|
||||
|
||||
|
||||
import ddt
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class DatesTabTestViews(BaseCourseHomeTests):
|
||||
"""
|
||||
Tests for the Dates Tab API
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
BaseCourseHomeTests.setUpClass()
|
||||
cls.url = reverse('course-home-dates-tab', args=[cls.course.id])
|
||||
|
||||
@ddt.data(CourseMode.AUDIT, CourseMode.VERIFIED)
|
||||
def test_get_authenticated_enrolled_user(self, enrollment_mode):
|
||||
CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode)
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Pulling out the date blocks to check learner has access. The Verification Deadline Date
|
||||
# should not be accessible to the audit learner, but accessible to the verified learner.
|
||||
date_blocks = response.data.get('course_date_blocks')
|
||||
if enrollment_mode == CourseMode.AUDIT:
|
||||
self.assertFalse(response.data.get('learner_is_verified'))
|
||||
self.assertTrue(any(block.get('learner_has_access') is False for block in date_blocks))
|
||||
else:
|
||||
self.assertTrue(response.data.get('learner_is_verified'))
|
||||
self.assertTrue(all(block.get('learner_has_access') for block in date_blocks))
|
||||
|
||||
def test_get_authenticated_user_not_enrolled(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(response.data.get('learner_is_verified'))
|
||||
|
||||
def test_get_unauthenticated_user(self):
|
||||
self.client.logout()
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_get_unknown_course(self):
|
||||
url = reverse('course-home-dates-tab', args=['course-v1:unknown+course+2T2020'])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
90
lms/djangoapps/course_home_api/dates/v1/views.py
Normal file
90
lms/djangoapps/course_home_api/dates/v1/views.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Dates Tab Views
|
||||
"""
|
||||
|
||||
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from edx_django_utils import monitoring as monitoring_utils
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs
|
||||
from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_with_access
|
||||
from lms.djangoapps.courseware.date_summary import TodaysDate, verified_upgrade_deadline_link
|
||||
from lms.djangoapps.course_home_api.dates.v1.serializers import DatesTabSerializer
|
||||
from openedx.core.djangoapps.enrollments.api import get_enrollment
|
||||
from openedx.features.course_experience.utils import reset_deadlines_banner_should_display
|
||||
|
||||
|
||||
class DatesTabView(RetrieveAPIView):
|
||||
"""
|
||||
**Use Cases**
|
||||
|
||||
Request details for the Dates Tab
|
||||
|
||||
**Example Requests**
|
||||
|
||||
GET api/course_home/v1/dates/{course_key}
|
||||
|
||||
**Response Values**
|
||||
|
||||
Body consists of the following fields:
|
||||
|
||||
course_date_blocks: List of serialized DateSummary objects. Each serialization has the following fields:
|
||||
date: (datetime) The date time corresponding for the event
|
||||
date_type: (str) The type of date (ex. course-start-date, assignment-due-date, etc.)
|
||||
description: (str) The description for the date event
|
||||
learner_has_access: (bool) Indicates if the learner has access to the date event
|
||||
link: (str) An absolute link to content related to the date event
|
||||
(ex. verified link or link to assignment)
|
||||
title: (str) The title of the date event
|
||||
display_reset_dates_text: (bool) Indicates whether the reset dates banner should be shown
|
||||
for the given user
|
||||
learner_is_verified: (bool) Indicates if the user is verified in the course
|
||||
user_timezone: (str) The user's preferred timezone
|
||||
verified_upgrade_link: (str) The link for upgrading to the Verified track in a course
|
||||
|
||||
**Returns**
|
||||
|
||||
* 200 on success with above fields.
|
||||
* 403 if the user is not authenticated.
|
||||
* 404 if the course is not available or cannot be seen.
|
||||
"""
|
||||
|
||||
permission_classes = (IsAuthenticated,)
|
||||
serializer_class = DatesTabSerializer
|
||||
|
||||
def get(self, request, course_key_string):
|
||||
# Enable NR tracing for this view based on course
|
||||
monitoring_utils.set_custom_metric('course_id', course_key_string)
|
||||
monitoring_utils.set_custom_metric('user_id', request.user.id)
|
||||
monitoring_utils.set_custom_metric('is_staff', request.user.is_staff)
|
||||
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False)
|
||||
blocks = get_course_date_blocks(course, request.user, request, include_access=True, include_past_dates=True)
|
||||
display_reset_dates_text = reset_deadlines_banner_should_display(course_key, request)
|
||||
|
||||
learner_is_verified = False
|
||||
enrollment = get_enrollment(request.user.username, course_key_string)
|
||||
if enrollment:
|
||||
learner_is_verified = enrollment.get('mode') == 'verified'
|
||||
|
||||
# User locale settings
|
||||
user_timezone_locale = user_timezone_locale_prefs(request)
|
||||
user_timezone = user_timezone_locale['user_timezone']
|
||||
|
||||
data = {
|
||||
'course_date_blocks': [block for block in blocks if not isinstance(block, TodaysDate)],
|
||||
'display_reset_dates_text': display_reset_dates_text,
|
||||
'learner_is_verified': learner_is_verified,
|
||||
'user_timezone': user_timezone,
|
||||
'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course),
|
||||
}
|
||||
context = self.get_serializer_context()
|
||||
context['learner_is_verified'] = learner_is_verified
|
||||
serializer = self.get_serializer_class()(data, context=context)
|
||||
|
||||
return Response(serializer.data)
|
||||
0
lms/djangoapps/course_home_api/tests/__init__.py
Normal file
0
lms/djangoapps/course_home_api/tests/__init__.py
Normal file
68
lms/djangoapps/course_home_api/tests/utils.py
Normal file
68
lms/djangoapps/course_home_api/tests/utils.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Base classes or util functions for use in Course Home API tests
|
||||
"""
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from lms.djangoapps.verify_student.models import VerificationDeadline
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class BaseCourseHomeTests(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Base class for Course Home API tests.
|
||||
|
||||
Creates a course to
|
||||
"""
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.store = modulestore()
|
||||
cls.course = CourseFactory.create(
|
||||
start=datetime(2020, 1, 1),
|
||||
end=datetime(2028, 1, 1),
|
||||
enrollment_start=datetime(2020, 1, 1),
|
||||
enrollment_end=datetime(2028, 1, 1),
|
||||
emit_signals=True,
|
||||
modulestore=cls.store,
|
||||
)
|
||||
chapter = ItemFactory(parent=cls.course, category='chapter')
|
||||
ItemFactory(parent=chapter, category='sequential', display_name='sequence')
|
||||
|
||||
CourseModeFactory(course_id=cls.course.id, mode_slug=CourseMode.AUDIT)
|
||||
CourseModeFactory(
|
||||
course_id=cls.course.id,
|
||||
mode_slug=CourseMode.VERIFIED,
|
||||
expiration_datetime=datetime(2028, 1, 1)
|
||||
)
|
||||
VerificationDeadline.objects.create(course_key=cls.course.id, deadline=datetime(2028, 1, 1))
|
||||
|
||||
cls.user = UserFactory(
|
||||
username='student',
|
||||
email='user@example.com',
|
||||
password='foo',
|
||||
is_staff=False
|
||||
)
|
||||
CourseOverviewFactory.create(run='1T2020')
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
cls.store.delete_course(cls.course.id, cls.user.id)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client.login(username=self.user.username, password='foo')
|
||||
20
lms/djangoapps/course_home_api/urls.py
Normal file
20
lms/djangoapps/course_home_api/urls.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Contains all the URLs for the Course Home
|
||||
"""
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import re_path
|
||||
|
||||
from lms.djangoapps.course_home_api.dates.v1 import views
|
||||
|
||||
urlpatterns = []
|
||||
|
||||
# Dates Tab URLs
|
||||
urlpatterns += [
|
||||
re_path(
|
||||
r'v1/dates/{}'.format(settings.COURSE_KEY_PATTERN),
|
||||
views.DatesTabView.as_view(),
|
||||
name='course-home-dates-tab'
|
||||
),
|
||||
]
|
||||
@@ -6,7 +6,7 @@ courseware.
|
||||
|
||||
import logging
|
||||
from collections import defaultdict, namedtuple
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
import six
|
||||
@@ -60,7 +60,6 @@ from openedx.core.lib.api.view_utils import LazySequence
|
||||
from openedx.features.course_duration_limits.access import AuditExpiredError
|
||||
from openedx.features.course_experience import RELATIVE_DATES_FLAG
|
||||
from static_replace import replace_static_urls
|
||||
from student.models import CourseEnrollment
|
||||
from survey.utils import SurveyRequiredAccessError, check_survey_required_and_unanswered
|
||||
from util.date_utils import strftime_localized
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -503,6 +502,7 @@ def get_course_assignment_date_blocks(course, user, request, num_return=None,
|
||||
date_block.contains_gated_content = assignment.contains_gated_content
|
||||
date_block.complete = assignment.complete
|
||||
date_block.past_due = assignment.past_due
|
||||
date_block.link = assignment.url
|
||||
date_block.set_title(assignment.title, link=assignment.url)
|
||||
date_blocks.append(date_block)
|
||||
date_blocks = sorted((b for b in date_blocks if b.is_enabled or include_past_dates), key=date_block_key_fn)
|
||||
@@ -534,7 +534,7 @@ def get_course_assignments(course_key, user, request, include_access=False):
|
||||
subsection_key, 'contains_gated_content', False)
|
||||
title = block_data.get_xblock_field(subsection_key, 'display_name', _('Assignment'))
|
||||
|
||||
url = None
|
||||
url = ''
|
||||
start = block_data.get_xblock_field(subsection_key, 'start')
|
||||
assignment_released = not start or start < now
|
||||
if assignment_released:
|
||||
|
||||
@@ -57,6 +57,10 @@ class DateSummary(object):
|
||||
"""
|
||||
return ''
|
||||
|
||||
@property
|
||||
def date_type(self):
|
||||
return 'event'
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
"""The title of this summary."""
|
||||
@@ -226,6 +230,10 @@ class TodaysDate(DateSummary):
|
||||
def date(self):
|
||||
return self.current_time
|
||||
|
||||
@property
|
||||
def date_type(self):
|
||||
return 'todays-date'
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return 'current_datetime'
|
||||
@@ -242,6 +250,10 @@ class CourseStartDate(DateSummary):
|
||||
def date(self):
|
||||
return self.course.start
|
||||
|
||||
@property
|
||||
def date_type(self):
|
||||
return 'course-start-date'
|
||||
|
||||
def register_alerts(self, request, course):
|
||||
"""
|
||||
Registers an alert if the course has not started yet.
|
||||
@@ -305,6 +317,10 @@ class CourseEndDate(DateSummary):
|
||||
|
||||
return self.course.end
|
||||
|
||||
@property
|
||||
def date_type(self):
|
||||
return 'course-end-date'
|
||||
|
||||
def register_alerts(self, request, course):
|
||||
"""
|
||||
Registers an alert if the end date is approaching.
|
||||
@@ -344,6 +360,7 @@ class CourseAssignmentDate(DateSummary):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.assignment_date = None
|
||||
self.assignment_link = ''
|
||||
self.assignment_title = None
|
||||
self.assignment_title_html = None
|
||||
self.contains_gated_content = False
|
||||
@@ -358,6 +375,18 @@ class CourseAssignmentDate(DateSummary):
|
||||
def date(self, date):
|
||||
self.assignment_date = date
|
||||
|
||||
@property
|
||||
def date_type(self):
|
||||
return 'assignment-due-date'
|
||||
|
||||
@property
|
||||
def link(self):
|
||||
return self.assignment_link
|
||||
|
||||
@link.setter
|
||||
def link(self, link):
|
||||
self.assignment_link = link
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.assignment_title
|
||||
@@ -387,6 +416,10 @@ class CourseExpiredDate(DateSummary):
|
||||
return
|
||||
return get_user_course_expiration_date(self.user, self.course)
|
||||
|
||||
@property
|
||||
def date_type(self):
|
||||
return 'course-expired-date'
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return _('You lose all access to this course, including your progress.')
|
||||
@@ -428,6 +461,10 @@ class CertificateAvailableDate(DateSummary):
|
||||
def date(self):
|
||||
return self.course.certificate_available_date
|
||||
|
||||
@property
|
||||
def date_type(self):
|
||||
return 'certificate-available-date'
|
||||
|
||||
@property
|
||||
def has_certificate_modes(self):
|
||||
return any([
|
||||
@@ -499,6 +536,10 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def date_type(self):
|
||||
return 'verified-upgrade-deadline'
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
dynamic_deadline = self._dynamic_deadline()
|
||||
@@ -635,6 +676,10 @@ class VerificationDeadlineDate(DateSummary):
|
||||
def date(self):
|
||||
return VerificationDeadline.deadline_for_course(self.course_id)
|
||||
|
||||
@property
|
||||
def date_type(self):
|
||||
return 'verification-deadline-date'
|
||||
|
||||
@lazy
|
||||
def is_enabled(self):
|
||||
if self.date is None:
|
||||
|
||||
@@ -35,7 +35,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<% has_locked_assignments = any(hasattr(block, 'contains_gated_content') and block.contains_gated_content for block in course_date_blocks if isinstance(block, CourseAssignmentDate)) %>
|
||||
<% has_locked_assignments = any(hasattr(block, 'contains_gated_content') and block.contains_gated_content for block in course_date_blocks) %>
|
||||
% if has_locked_assignments and verified_upgrade_link:
|
||||
<div class="dates-banner">
|
||||
<div class="dates-banner-text banner-has-button">
|
||||
|
||||
@@ -999,3 +999,8 @@ if 'openedx.testing.coverage_context_listener' in settings.INSTALLED_APPS:
|
||||
]
|
||||
|
||||
urlpatterns.extend(plugin_urls.get_patterns(plugin_constants.ProjectType.LMS))
|
||||
|
||||
# Course Home API urls
|
||||
urlpatterns += [
|
||||
url(r'^api/course_home/', include('lms.djangoapps.course_home_api.urls')),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user