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:
Dillon Dumesnil
2020-05-19 13:01:50 -07:00
committed by GitHub
13 changed files with 329 additions and 4 deletions

View 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()

View 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)

View 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)

View 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')

View 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'
),
]

View File

@@ -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:

View File

@@ -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:

View File

@@ -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">

View File

@@ -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')),
]