Merge pull request #20605 from edx/mroytman/EDUCATOR-4276-get-course-enrollments
program course enrollment overview GET endpoint
This commit is contained in:
@@ -6,9 +6,12 @@ from __future__ import absolute_import
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
|
||||
import six.moves.urllib.parse
|
||||
from completion.exceptions import UnavailableCompletionData
|
||||
from completion.utilities import get_key_to_last_completed_course_block
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import load_backend
|
||||
from django.contrib.auth.models import User
|
||||
@@ -661,3 +664,32 @@ def do_create_account(form, custom_form=None):
|
||||
raise
|
||||
|
||||
return user, profile, registration
|
||||
|
||||
|
||||
def get_resume_urls_for_enrollments(user, enrollments):
|
||||
'''
|
||||
For a given user, return a list of urls to the user's last completed block in
|
||||
a course run for each course run in the user's enrollments.
|
||||
|
||||
Arguments:
|
||||
user: the user object for which we want resume course urls
|
||||
enrollments (list): a list of user enrollments
|
||||
|
||||
Returns:
|
||||
resume_course_urls (OrderedDict): an OrderdDict of urls
|
||||
key: CourseKey
|
||||
value: url to the last completed block
|
||||
if the value is '', then the user has not completed any blocks in the course run
|
||||
'''
|
||||
resume_course_urls = OrderedDict()
|
||||
for enrollment in enrollments:
|
||||
try:
|
||||
block_key = get_key_to_last_completed_course_block(user, enrollment.course_id)
|
||||
url_to_block = reverse(
|
||||
'jump_to',
|
||||
kwargs={'course_id': enrollment.course_id, 'location': block_key}
|
||||
)
|
||||
except UnavailableCompletionData:
|
||||
url_to_block = ''
|
||||
resume_course_urls[enrollment.course_id] = url_to_block
|
||||
return resume_course_urls
|
||||
|
||||
@@ -8,8 +8,6 @@ import datetime
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from completion.exceptions import UnavailableCompletionData
|
||||
from completion.utilities import get_key_to_last_completed_course_block
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
@@ -50,7 +48,7 @@ from openedx.features.enterprise_support.api import get_dashboard_consent_notifi
|
||||
from openedx.features.journals.api import journals_enabled
|
||||
from shoppingcart.api import order_history
|
||||
from shoppingcart.models import CourseRegistrationCode, DonationConfiguration
|
||||
from student.helpers import cert_info, check_verify_status_by_course
|
||||
from student.helpers import cert_info, check_verify_status_by_course, get_resume_urls_for_enrollments
|
||||
from student.models import (
|
||||
AccountRecovery,
|
||||
CourseEnrollment,
|
||||
@@ -533,24 +531,6 @@ def _credit_statuses(user, course_enrollments):
|
||||
return statuses
|
||||
|
||||
|
||||
def _get_urls_for_resume_buttons(user, enrollments):
|
||||
'''
|
||||
Checks whether a user has made progress in any of a list of enrollments.
|
||||
'''
|
||||
resume_button_urls = []
|
||||
for enrollment in enrollments:
|
||||
try:
|
||||
block_key = get_key_to_last_completed_course_block(user, enrollment.course_id)
|
||||
url_to_block = reverse(
|
||||
'jump_to',
|
||||
kwargs={'course_id': enrollment.course_id, 'location': block_key}
|
||||
)
|
||||
except UnavailableCompletionData:
|
||||
url_to_block = ''
|
||||
resume_button_urls.append(url_to_block)
|
||||
return resume_button_urls
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@add_maintenance_banner
|
||||
@@ -896,7 +876,7 @@ def student_dashboard(request):
|
||||
|
||||
# Gather urls for course card resume buttons.
|
||||
resume_button_urls = ['' for entitlement in course_entitlements]
|
||||
for url in _get_urls_for_resume_buttons(user, course_enrollments):
|
||||
for url in get_resume_urls_for_enrollments(user, course_enrollments).values():
|
||||
resume_button_urls.append(url)
|
||||
# There must be enough urls for dashboard.html. Template creates course
|
||||
# cards for "enrollments + entitlements".
|
||||
|
||||
@@ -33,3 +33,12 @@ class CourseEnrollmentResponseStatuses(object):
|
||||
NOT_FOUND,
|
||||
INTERNAL_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class CourseRunProgressStatuses(object):
|
||||
"""
|
||||
Class to group statuses that a course run can be in with respect to user progress.
|
||||
"""
|
||||
IN_PROGRESS = 'in_progress'
|
||||
UPCOMING = 'upcoming'
|
||||
COMPLETED = 'completed'
|
||||
|
||||
@@ -7,6 +7,7 @@ from rest_framework import serializers
|
||||
from six import text_type
|
||||
|
||||
from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment
|
||||
from lms.djangoapps.program_enrollments.api.v1.constants import CourseRunProgressStatuses
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
@@ -100,3 +101,42 @@ class ProgramCourseEnrollmentListSerializer(serializers.Serializer):
|
||||
|
||||
def get_curriculum_uuid(self, obj):
|
||||
return text_type(obj.program_enrollment.curriculum_uuid)
|
||||
|
||||
|
||||
class DueDateSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for a due date.
|
||||
"""
|
||||
name = serializers.CharField()
|
||||
url = serializers.CharField()
|
||||
date = serializers.DateTimeField()
|
||||
|
||||
|
||||
class CourseRunOverviewSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for a course run overview.
|
||||
"""
|
||||
STATUS_CHOICES = [
|
||||
CourseRunProgressStatuses.IN_PROGRESS,
|
||||
CourseRunProgressStatuses.UPCOMING,
|
||||
CourseRunProgressStatuses.COMPLETED
|
||||
]
|
||||
|
||||
course_run_id = serializers.CharField()
|
||||
display_name = serializers.CharField()
|
||||
resume_course_run_url = serializers.CharField(required=False)
|
||||
course_run_url = serializers.CharField()
|
||||
start_date = serializers.DateTimeField()
|
||||
end_date = serializers.DateTimeField()
|
||||
course_run_status = serializers.ChoiceField(allow_blank=False, choices=STATUS_CHOICES)
|
||||
emails_enabled = serializers.BooleanField(required=False)
|
||||
due_dates = serializers.ListField(child=DueDateSerializer())
|
||||
micromasters_title = serializers.CharField(required=False)
|
||||
certificate_download_url = serializers.CharField(required=False)
|
||||
|
||||
|
||||
class CourseRunOverviewListSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for a list of course run overviews.
|
||||
"""
|
||||
course_runs = serializers.ListField(child=CourseRunOverviewSerializer())
|
||||
|
||||
@@ -3,6 +3,7 @@ Unit tests for ProgramEnrollment views.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -11,23 +12,34 @@ import mock
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.cache import cache
|
||||
from django.urls import reverse
|
||||
from freezegun import freeze_time
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
from six import text_type
|
||||
from six.moves import range, zip
|
||||
|
||||
from bulk_email.models import BulkEmailFlag, Optout
|
||||
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
|
||||
from lms.djangoapps.certificates.models import CertificateStatuses
|
||||
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory
|
||||
from lms.djangoapps.program_enrollments.api.v1.constants import MAX_ENROLLMENT_RECORDS, REQUEST_STUDENT_KEY
|
||||
from lms.djangoapps.program_enrollments.api.v1.constants import CourseEnrollmentResponseStatuses as CourseStatuses
|
||||
from lms.djangoapps.program_enrollments.api.v1.constants import (
|
||||
CourseEnrollmentResponseStatuses as CourseStatuses,
|
||||
CourseRunProgressStatuses,
|
||||
MAX_ENROLLMENT_RECORDS,
|
||||
REQUEST_STUDENT_KEY,
|
||||
)
|
||||
from lms.djangoapps.program_enrollments.models import ProgramEnrollment, ProgramCourseEnrollment
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL
|
||||
from openedx.core.djangoapps.catalog.tests.factories import CourseFactory
|
||||
from openedx.core.djangoapps.catalog.tests.factories import OrganizationFactory as CatalogOrganizationFactory
|
||||
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationMixin
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from xmodule.modulestore.tests.factories import CourseFactory as ModulestoreCourseFactory, ItemFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from .factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory
|
||||
|
||||
|
||||
@@ -222,9 +234,12 @@ class ProgramCacheTestCaseMixin(CacheIsolationMixin):
|
||||
uuid=program_uuid,
|
||||
authoring_organizations=[catalog_org]
|
||||
)
|
||||
cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=program_uuid), program, None)
|
||||
self.set_program_in_catalog_cache(program_uuid, program)
|
||||
return program
|
||||
|
||||
def set_program_in_catalog_cache(self, program_uuid, program):
|
||||
cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=program_uuid), program, None)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class BaseCourseEnrollmentTestsMixin(ProgramCacheTestCaseMixin):
|
||||
@@ -1083,3 +1098,501 @@ class ProgramEnrollmentViewPatchTests(APITestCase):
|
||||
'user-3': 'withdrawn',
|
||||
'user-who-is-not-in-program': 'not-in-program',
|
||||
})
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ProgramCourseEnrollmentOverviewViewTests(ProgramCacheTestCaseMixin, SharedModuleStoreTestCase, APITestCase):
|
||||
"""
|
||||
Tests for the ProgramCourseEnrollmentOverview view GET method.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(ProgramCourseEnrollmentOverviewViewTests, cls).setUpClass()
|
||||
|
||||
cls.program_uuid = '00000000-1111-2222-3333-444444444444'
|
||||
cls.curriculum_uuid = 'aaaaaaaa-1111-2222-3333-444444444444'
|
||||
cls.other_curriculum_uuid = 'bbbbbbbb-1111-2222-3333-444444444444'
|
||||
|
||||
cls.course_id = CourseKey.from_string('course-v1:edX+ToyX+Toy_Course')
|
||||
|
||||
cls.password = 'password'
|
||||
cls.student = UserFactory.create(username='student', password=cls.password)
|
||||
|
||||
# only freeze time when defining these values and not on the whole test case
|
||||
# as test_multiple_enrollments_all_enrolled relies on actual differences in modified datetimes
|
||||
with freeze_time('2019-01-01'):
|
||||
cls.yesterday = datetime.now() - timedelta(1)
|
||||
cls.tomorrow = datetime.now() + timedelta(1)
|
||||
|
||||
cls.certificate_download_url = 'www.certificates.com'
|
||||
|
||||
def setUp(self):
|
||||
super(ProgramCourseEnrollmentOverviewViewTests, self).setUp()
|
||||
|
||||
# create program
|
||||
self.program = self.setup_catalog_cache(self.program_uuid, 'organization_key')
|
||||
|
||||
# create program enrollment
|
||||
self.program_enrollment = ProgramEnrollmentFactory.create(
|
||||
program_uuid=self.program_uuid,
|
||||
curriculum_uuid=self.curriculum_uuid,
|
||||
user=self.student,
|
||||
)
|
||||
|
||||
# create course enrollment
|
||||
self.course_enrollment = CourseEnrollmentFactory(
|
||||
course_id=self.course_id,
|
||||
user=self.student,
|
||||
)
|
||||
|
||||
# create course overview
|
||||
self.course_overview = CourseOverviewFactory.create(
|
||||
id=self.course_id,
|
||||
start=self.yesterday,
|
||||
end=self.tomorrow,
|
||||
)
|
||||
|
||||
# create program course enrollment
|
||||
self.program_course_enrollment = ProgramCourseEnrollmentFactory.create(
|
||||
program_enrollment=self.program_enrollment,
|
||||
course_enrollment=self.course_enrollment,
|
||||
course_key=self.course_id,
|
||||
status='active',
|
||||
)
|
||||
|
||||
def create_generated_certificate(self):
|
||||
return GeneratedCertificateFactory.create(
|
||||
user=self.student,
|
||||
course_id=self.course_id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
mode='verified',
|
||||
download_url=self.certificate_download_url,
|
||||
grade="0.88",
|
||||
verify_uuid=uuid4(),
|
||||
)
|
||||
|
||||
def get_url(self, program_uuid=None):
|
||||
""" Returns the primary URL requested by the test case. """
|
||||
kwargs = {'program_uuid': program_uuid or self.program_uuid}
|
||||
|
||||
return reverse('programs_api:v1:program_course_enrollments_overview', kwargs=kwargs)
|
||||
|
||||
def test_401_if_anonymous(self):
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
assert status.HTTP_401_UNAUTHORIZED == response.status_code
|
||||
|
||||
def test_404_if_no_program_with_key(self):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
self.set_program_in_catalog_cache(self.program_uuid, None)
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
assert status.HTTP_404_NOT_FOUND == response.status_code
|
||||
|
||||
def test_403_if_not_enrolled_in_program(self):
|
||||
# delete program enrollment
|
||||
ProgramEnrollment.objects.all().delete()
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
assert status.HTTP_403_FORBIDDEN == response.status_code
|
||||
|
||||
@ddt.data(
|
||||
'pending',
|
||||
'suspended',
|
||||
'withdrawn',
|
||||
)
|
||||
def test_multiple_enrollments_with_not_enrolled(self, program_enrollment_status):
|
||||
# add a second program enrollment
|
||||
program_enrollment = ProgramEnrollmentFactory.create(
|
||||
program_uuid=self.program_uuid,
|
||||
curriculum_uuid=self.other_curriculum_uuid,
|
||||
user=self.student,
|
||||
status=program_enrollment_status,
|
||||
)
|
||||
|
||||
other_course_key_string = 'course-v1:edX+ToyX+Other_Course'
|
||||
other_course_key = CourseKey.from_string(other_course_key_string)
|
||||
|
||||
# add a second course enrollment
|
||||
course_enrollment = CourseEnrollmentFactory(
|
||||
course_id=other_course_key,
|
||||
user=self.student,
|
||||
)
|
||||
|
||||
# add a second program course enrollment
|
||||
ProgramCourseEnrollmentFactory.create(
|
||||
program_enrollment=program_enrollment,
|
||||
course_enrollment=course_enrollment,
|
||||
course_key=other_course_key,
|
||||
status='active',
|
||||
)
|
||||
|
||||
# add a course over view for other_course_key
|
||||
CourseOverviewFactory.create(
|
||||
id=other_course_key,
|
||||
start=self.yesterday,
|
||||
)
|
||||
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
# we expect data associated with the last modified program enrollment
|
||||
# with 'enrolled' status
|
||||
self.assertEqual(
|
||||
text_type(self.program_course_enrollment.course_key),
|
||||
response.data['course_runs'][0]['course_run_id']
|
||||
)
|
||||
|
||||
def test_multiple_enrollments_all_enrolled(self):
|
||||
# add a second program enrollment
|
||||
program_enrollment = ProgramEnrollmentFactory.create(
|
||||
program_uuid=self.program_uuid,
|
||||
curriculum_uuid=self.other_curriculum_uuid,
|
||||
user=self.student,
|
||||
)
|
||||
|
||||
other_course_key_string = 'course-v1:edX+ToyX+Other_Course'
|
||||
other_course_key = CourseKey.from_string(other_course_key_string)
|
||||
|
||||
# add a second course enrollment
|
||||
course_enrollment = CourseEnrollmentFactory(
|
||||
course_id=other_course_key,
|
||||
user=self.student
|
||||
)
|
||||
|
||||
# add a second program course enrollment
|
||||
ProgramCourseEnrollmentFactory.create(
|
||||
program_enrollment=program_enrollment,
|
||||
course_enrollment=course_enrollment,
|
||||
course_key=other_course_key,
|
||||
status='active',
|
||||
)
|
||||
|
||||
# add a course over view for other_course_key
|
||||
CourseOverviewFactory.create(
|
||||
id=other_course_key,
|
||||
start=self.yesterday,
|
||||
)
|
||||
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
# we expect data associated with the last modified program enrollment
|
||||
self.assertEqual(other_course_key_string, response.data['course_runs'][0]['course_run_id'])
|
||||
|
||||
@mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_resume_urls_for_enrollments')
|
||||
def test_resume_urls(self, mock_get_resume_urls):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
mock_get_resume_urls.return_value = {self.course_id: ''}
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertNotIn('resume_course_run_url', response.data['course_runs'][0])
|
||||
|
||||
resume_url = 'www.resume.com'
|
||||
mock_get_resume_urls.return_value = {self.course_id: resume_url}
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(resume_url, response.data['course_runs'][0]['resume_course_run_url'])
|
||||
|
||||
def test_no_certificate_available(self):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
assert 'certificate_download_url' not in response.data['course_runs'][0]
|
||||
|
||||
def test_certificate_available(self):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
self.create_generated_certificate()
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertEqual(response.data['course_runs'][0]['certificate_download_url'], self.certificate_download_url)
|
||||
|
||||
def test_no_due_dates(self):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
assert [] == response.data['course_runs'][0]['due_dates']
|
||||
|
||||
def test_due_dates(self):
|
||||
course = ModulestoreCourseFactory.create(
|
||||
org="edX",
|
||||
course="ToyX",
|
||||
run="Toy_Course",
|
||||
)
|
||||
section_1 = ItemFactory.create(
|
||||
category='chapter',
|
||||
start=self.yesterday,
|
||||
due=self.tomorrow,
|
||||
parent=course,
|
||||
display_name='section 1'
|
||||
)
|
||||
|
||||
subsection_1 = ItemFactory.create(
|
||||
category='sequential',
|
||||
due=self.tomorrow,
|
||||
parent=section_1,
|
||||
display_name='subsection 1'
|
||||
)
|
||||
|
||||
subsection_2 = ItemFactory.create(
|
||||
category='sequential',
|
||||
due=self.tomorrow - timedelta(1),
|
||||
parent=section_1,
|
||||
display_name='subsection 2'
|
||||
)
|
||||
|
||||
subsection_3 = ItemFactory.create(
|
||||
category='sequential',
|
||||
parent=section_1,
|
||||
display_name='subsection 3'
|
||||
)
|
||||
|
||||
unit_1 = ItemFactory.create(
|
||||
category='vertical',
|
||||
due=self.tomorrow + timedelta(2),
|
||||
parent=subsection_3,
|
||||
display_name='unit_1'
|
||||
)
|
||||
|
||||
with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_dates_for_course') as mock_get_dates:
|
||||
mock_get_dates.return_value = {
|
||||
(section_1.location, 'due'): section_1.due,
|
||||
(section_1.location, 'start'): section_1.start,
|
||||
(subsection_1.location, 'due'): subsection_1.due,
|
||||
(subsection_2.location, 'due'): subsection_2.due,
|
||||
(unit_1.location, 'due'): unit_1.due,
|
||||
}
|
||||
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
|
||||
block_data = [
|
||||
{
|
||||
'name': section_1.display_name,
|
||||
'url': ('http://testserver/courses/course-v1:edX+ToyX+Toy_Course/'
|
||||
'jump_to/i4x://edX/ToyX/chapter/section_1'),
|
||||
'date': '2019-01-02T00:00:00Z',
|
||||
},
|
||||
{
|
||||
'name': subsection_1.display_name,
|
||||
'url': ('http://testserver/courses/course-v1:edX+ToyX+Toy_Course/'
|
||||
'jump_to/i4x://edX/ToyX/sequential/subsection_1'),
|
||||
'date': '2019-01-02T00:00:00Z',
|
||||
},
|
||||
{
|
||||
'name': subsection_2.display_name,
|
||||
'url': ('http://testserver/courses/course-v1:edX+ToyX+Toy_Course/'
|
||||
'jump_to/i4x://edX/ToyX/sequential/subsection_2'),
|
||||
'date': '2019-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
'name': unit_1.display_name,
|
||||
'url': ('http://testserver/courses/course-v1:edX+ToyX+Toy_Course/'
|
||||
'jump_to/i4x://edX/ToyX/vertical/unit_1'),
|
||||
'date': '2019-01-04T00:00:00Z',
|
||||
},
|
||||
]
|
||||
due_dates = response.data['course_runs'][0]['due_dates']
|
||||
|
||||
for block in block_data:
|
||||
self.assertIn(block, due_dates)
|
||||
|
||||
@mock.patch.object(CourseOverview, 'has_ended')
|
||||
def test_course_run_status_instructor_paced_completed(self, mock_has_ended):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
# set as instructor paced
|
||||
self.course_overview.self_paced = False
|
||||
self.course_overview.save()
|
||||
|
||||
mock_has_ended.return_value = True
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertEqual(CourseRunProgressStatuses.COMPLETED, response.data['course_runs'][0]['course_run_status'])
|
||||
|
||||
@mock.patch.object(CourseOverview, 'has_ended')
|
||||
@mock.patch.object(CourseOverview, 'has_started')
|
||||
def test_course_run_status_instructor_paced_in_progress(self, mock_has_started, mock_has_ended):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
# set as instructor paced
|
||||
self.course_overview.self_paced = False
|
||||
self.course_overview.save()
|
||||
|
||||
mock_has_started.return_value = True
|
||||
mock_has_ended.return_value = False
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertEqual(CourseRunProgressStatuses.IN_PROGRESS, response.data['course_runs'][0]['course_run_status'])
|
||||
|
||||
@mock.patch.object(CourseOverview, 'has_ended')
|
||||
@mock.patch.object(CourseOverview, 'has_started')
|
||||
def test_course_run_status_instructor_paced_upcoming(self, mock_has_started, mock_has_ended):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
# set as instructor paced
|
||||
self.course_overview.self_paced = False
|
||||
self.course_overview.save()
|
||||
|
||||
mock_has_started.return_value = False
|
||||
mock_has_ended.return_value = False
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertEqual(CourseRunProgressStatuses.UPCOMING, response.data['course_runs'][0]['course_run_status'])
|
||||
|
||||
@mock.patch.object(CourseOverview, 'has_ended')
|
||||
def test_course_run_status_self_paced_completed(self, mock_has_ended):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
# set as self paced
|
||||
self.course_overview.self_paced = True
|
||||
self.course_overview.save()
|
||||
|
||||
# course run has ended
|
||||
mock_has_ended.return_value = True
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertEqual(CourseRunProgressStatuses.COMPLETED, response.data['course_runs'][0]['course_run_status'])
|
||||
|
||||
# course run has not ended and user has earned a passing certificate more than 30 days ago
|
||||
certificate = self.create_generated_certificate()
|
||||
certificate.created_date = datetime.now() - timedelta(30)
|
||||
certificate.save()
|
||||
mock_has_ended.return_value = False
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertEqual(CourseRunProgressStatuses.COMPLETED, response.data['course_runs'][0]['course_run_status'])
|
||||
|
||||
# course run has ended and user has earned a passing certificate
|
||||
mock_has_ended.return_value = True
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertEqual(CourseRunProgressStatuses.COMPLETED, response.data['course_runs'][0]['course_run_status'])
|
||||
|
||||
@mock.patch.object(CourseOverview, 'has_ended')
|
||||
@mock.patch.object(CourseOverview, 'has_started')
|
||||
def test_course_run_status_self_paced_in_progress(self, mock_has_started, mock_has_ended):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
# set as self paced
|
||||
self.course_overview.self_paced = True
|
||||
self.course_overview.save()
|
||||
|
||||
# course run has started and has not ended
|
||||
mock_has_started.return_value = True
|
||||
mock_has_ended.return_value = False
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertEqual(CourseRunProgressStatuses.IN_PROGRESS, response.data['course_runs'][0]['course_run_status'])
|
||||
|
||||
# course run has not ended and user has earned a passing certificate fewer than 30 days ago
|
||||
certificate = self.create_generated_certificate()
|
||||
certificate.created_date = datetime.now() - timedelta(5)
|
||||
certificate.save()
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertEqual(CourseRunProgressStatuses.IN_PROGRESS, response.data['course_runs'][0]['course_run_status'])
|
||||
|
||||
@mock.patch.object(CourseOverview, 'has_ended')
|
||||
@mock.patch.object(CourseOverview, 'has_started')
|
||||
def test_course_run_status_self_paced_upcoming(self, mock_has_started, mock_has_ended):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
# set as self paced
|
||||
self.course_overview.self_paced = True
|
||||
self.course_overview.save()
|
||||
|
||||
# course run has not started and has not ended
|
||||
mock_has_started.return_value = False
|
||||
mock_has_ended.return_value = False
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertEqual(CourseRunProgressStatuses.UPCOMING, response.data['course_runs'][0]['course_run_status'])
|
||||
|
||||
def test_course_run_url(self):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
course_run_url = 'http://testserver/courses/{}/course/'.format(text_type(self.course_id))
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertEqual(course_run_url, response.data['course_runs'][0]['course_run_url'])
|
||||
|
||||
def test_course_run_dates(self):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
|
||||
course_run_overview = response.data['course_runs'][0]
|
||||
|
||||
self.assertEqual(course_run_overview['start_date'], '2018-12-31T05:00:00Z')
|
||||
self.assertEqual(course_run_overview['end_date'], '2019-01-02T05:00:00Z')
|
||||
|
||||
# course run end date may not exist
|
||||
self.course_overview.end = None
|
||||
self.course_overview.save()
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertEqual(response.data['course_runs'][0]['end_date'], None)
|
||||
|
||||
def test_course_run_id_and_display_name(self):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
|
||||
course_run_overview = response.data['course_runs'][0]
|
||||
|
||||
self.assertEqual(course_run_overview['course_run_id'], text_type(self.course_id))
|
||||
self.assertEqual(course_run_overview['display_name'], "{} Course".format(text_type(self.course_id)))
|
||||
|
||||
def test_emails_enabled(self):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
# by default, BulkEmailFlag is not enabled, so 'emails_enabled' won't be in the response
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertNotIn('emails_enabled', response.data['course_runs'][0])
|
||||
|
||||
with mock.patch.object(BulkEmailFlag, 'feature_enabled', return_value=True):
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertTrue(response.data['course_runs'][0]['emails_enabled'])
|
||||
|
||||
Optout.objects.create(
|
||||
user=self.student,
|
||||
course_id=self.course_id
|
||||
)
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertFalse(response.data['course_runs'][0]['emails_enabled'])
|
||||
|
||||
def test_micromasters_title(self):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertNotIn('micromasters_title', response.data['course_runs'][0])
|
||||
|
||||
self.program['type'] = 'MicroMasters'
|
||||
# update the program in the catalog cache
|
||||
self.set_program_in_catalog_cache(self.program_uuid, self.program)
|
||||
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||
self.assertIn('micromasters_title', response.data['course_runs'][0])
|
||||
|
||||
@@ -4,7 +4,11 @@ from __future__ import absolute_import
|
||||
from django.conf.urls import url
|
||||
|
||||
from lms.djangoapps.program_enrollments.api.v1.constants import PROGRAM_UUID_PATTERN
|
||||
from lms.djangoapps.program_enrollments.api.v1.views import ProgramCourseEnrollmentsView, ProgramEnrollmentsView
|
||||
from lms.djangoapps.program_enrollments.api.v1.views import (
|
||||
ProgramEnrollmentsView,
|
||||
ProgramCourseEnrollmentsView,
|
||||
ProgramCourseEnrollmentOverviewView,
|
||||
)
|
||||
from openedx.core.constants import COURSE_ID_PATTERN
|
||||
|
||||
app_name = 'lms.djangoapps.program_enrollments'
|
||||
@@ -23,4 +27,11 @@ urlpatterns = [
|
||||
ProgramCourseEnrollmentsView.as_view(),
|
||||
name="program_course_enrollments"
|
||||
),
|
||||
url(
|
||||
r'^programs/{program_uuid}/overview/'.format(
|
||||
program_uuid=PROGRAM_UUID_PATTERN,
|
||||
),
|
||||
ProgramCourseEnrollmentOverviewView.as_view(),
|
||||
name="program_course_enrollments_overview"
|
||||
),
|
||||
]
|
||||
|
||||
@@ -4,38 +4,56 @@ ProgramEnrollment Views
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
from collections import Counter, OrderedDict
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
from pytz import UTC
|
||||
|
||||
from django.http import Http404
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.urls import reverse
|
||||
from edx_rest_framework_extensions import permissions
|
||||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
||||
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework import status
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.pagination import CursorPagination
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from six import iteritems
|
||||
|
||||
from bulk_email.api import is_bulk_email_feature_enabled, is_user_opted_out_for_course
|
||||
from edx_when.api import get_dates_for_course
|
||||
from lms.djangoapps.certificates.api import get_certificate_for_user
|
||||
from lms.djangoapps.program_enrollments.api.v1.constants import (
|
||||
CourseEnrollmentResponseStatuses,
|
||||
CourseRunProgressStatuses,
|
||||
MAX_ENROLLMENT_RECORDS,
|
||||
REQUEST_STUDENT_KEY,
|
||||
CourseEnrollmentResponseStatuses
|
||||
)
|
||||
from lms.djangoapps.program_enrollments.api.v1.serializers import (
|
||||
CourseRunOverviewListSerializer,
|
||||
ProgramCourseEnrollmentListSerializer,
|
||||
ProgramCourseEnrollmentRequestSerializer,
|
||||
ProgramEnrollmentListSerializer,
|
||||
ProgramEnrollmentSerializer
|
||||
ProgramEnrollmentSerializer,
|
||||
)
|
||||
from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment
|
||||
from lms.djangoapps.program_enrollments.utils import get_user_by_program_id
|
||||
from student.helpers import get_resume_urls_for_enrollments
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, PaginatedAPIView, verify_course_exists
|
||||
from util.query import use_read_replica_if_available
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def verify_program_exists(view_func):
|
||||
"""
|
||||
@@ -703,3 +721,302 @@ class ProgramCourseEnrollmentsView(DeveloperErrorViewMixin, ProgramCourseRunSpec
|
||||
if program_course_enrollment is None:
|
||||
return CourseEnrollmentResponseStatuses.NOT_FOUND
|
||||
return program_course_enrollment.change_status(enrollment_request['status'])
|
||||
|
||||
|
||||
class ProgramCourseEnrollmentOverviewView(DeveloperErrorViewMixin, ProgramSpecificViewMixin, APIView):
|
||||
"""
|
||||
A view for getting data associated with a user's course enrollments
|
||||
as part of a program enrollment.
|
||||
|
||||
Path: ``/api/program_enrollments/v1/programs/{program_uuid}/overview/``
|
||||
|
||||
Accepts: [GET]
|
||||
|
||||
------------------------------------------------------------------------------------
|
||||
GET
|
||||
------------------------------------------------------------------------------------
|
||||
|
||||
**Returns**
|
||||
|
||||
* 200: OK - Contains an object of user program course enrollment data.
|
||||
* 401: Unauthorized - The requesting user is not authenticated.
|
||||
* 403: Forbidden -The requesting user lacks access for the given program.
|
||||
* 404: Not Found - The requested program does not exist.
|
||||
|
||||
**Response**
|
||||
|
||||
In the case of a 200 response code, the response will include a
|
||||
data set. The `course_runs` section of the response consists of a list of
|
||||
program course enrollment overview, where each overview contains the following keys:
|
||||
* course_run_id: the id for the course run
|
||||
* display_name: display name of the course run
|
||||
* resume_course_run_url: the url that takes the user back to their position in the course run;
|
||||
if absent, user has not made progress in the course
|
||||
* course_run_url: the url for the course run
|
||||
* start_date: the start date for the course run; null if no start date
|
||||
* end_date: the end date for the course run' null if no end date
|
||||
* course_status: the status of the course; one of "in-progress", "upcoming", and "completed"
|
||||
* emails_enabled: boolean representing whether emails are enabled for the course;
|
||||
if absent, the bulk email feature is either not enable at the platform level or is not enabled for the course;
|
||||
if True or False, bulk email feature is enabled, and value represents whether or not user wants to receive emails
|
||||
* due_dates: a list of subsection due dates for the course run:
|
||||
** name: name of the subsection
|
||||
** url: deep link to the subsection
|
||||
** date: due date for the subsection
|
||||
* micromasters_title: title of the MicroMasters program that the course run is a part of;
|
||||
if absent, the course run is not a part of a MicroMasters program
|
||||
* certificate_download_url: url to download a certificate, if available;
|
||||
if absent, certificate is not downloadable
|
||||
|
||||
**Example**
|
||||
|
||||
{
|
||||
"course_runs": [
|
||||
{
|
||||
"course_run_id": "edX+AnimalsX+Aardvarks",
|
||||
"display_name": "Astonishing Aardvarks",
|
||||
"course_run_url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Aardvarks/course/",
|
||||
"start_date": "2017-02-05T05:00:00Z",
|
||||
"end_date": "2018-02-05T05:00:00Z",
|
||||
"course_status": "completed"
|
||||
"emails_enabled": true,
|
||||
"due_dates": [
|
||||
{
|
||||
"name": "Introduction: What even is an aardvark?",
|
||||
"url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Aardvarks/jump_to/block-v1:edX+AnimalsX+Aardvarks+type@chapter+block@1414ffd5143b4b508f739b563ab468b7",
|
||||
"date": "2017-05-01T05:00:00Z"
|
||||
},
|
||||
{
|
||||
"name": "Quiz: Aardvark or Anteater?",
|
||||
"url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Aardvarks/jump_to/block-v1:edX+AnimalsX+Aardvarks+type@sequential+block@edx_introduction",
|
||||
"date": "2017-03-05T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"micromasters_title": "Animals",
|
||||
"certificate_download_url": "https://courses.edx.org/certificates/123"
|
||||
},
|
||||
{
|
||||
"course_run_id": "edX+AnimalsX+Baboons",
|
||||
"display_name": "Breathtaking Baboons",
|
||||
"course_run_url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Baboons/course/",
|
||||
"start_date": "2018-02-05T05:00:00Z",
|
||||
"end_date": null,
|
||||
"course_status": "in-progress"
|
||||
"emails_enabled": false,
|
||||
"due_dates": [],
|
||||
"micromasters_title": "Animals",
|
||||
"certificate_download_url": "https://courses.edx.org/certificates/123",
|
||||
"resume_course_run_url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Baboons/jump_to/block-v1:edX+AnimalsX+Baboons+type@sequential+block@edx_introduction"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
OAuth2AuthenticationAllowInactiveUser,
|
||||
SessionAuthenticationAllowInactiveUser,
|
||||
)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
@verify_program_exists
|
||||
def get(self, request, program_uuid=None):
|
||||
"""
|
||||
Defines the GET endpoint for overviews of course enrollments
|
||||
for a user as part of a program.
|
||||
"""
|
||||
user = request.user
|
||||
|
||||
user_program_enrollment = ProgramEnrollment.objects.filter(
|
||||
program_uuid=program_uuid,
|
||||
user=user,
|
||||
status='enrolled',
|
||||
).order_by(
|
||||
'-modified',
|
||||
)
|
||||
|
||||
user_program_enrollment_count = user_program_enrollment.count()
|
||||
|
||||
if user_program_enrollment_count > 1:
|
||||
# in the unusual and unlikely case of a user having two
|
||||
# active program enrollments for the same program,
|
||||
# choose the most recently modified enrollment and log
|
||||
# a warning
|
||||
user_program_enrollment = user_program_enrollment[0]
|
||||
logger.warning(
|
||||
('User with user_id {} has more than program enrollment'
|
||||
'with an enrolled status for program uuid {}.').format(
|
||||
user.id,
|
||||
program_uuid,
|
||||
)
|
||||
)
|
||||
elif user_program_enrollment_count == 0:
|
||||
# if the user is not enrolled in the program, they are not authorized
|
||||
# to view the information returned by this endpoint
|
||||
raise PermissionDenied
|
||||
|
||||
user_program_course_enrollments = ProgramCourseEnrollment.objects.filter(
|
||||
program_enrollment=user_program_enrollment
|
||||
).select_related('course_enrollment')
|
||||
|
||||
enrollment_dict = {enrollment.course_key: enrollment.course_enrollment for enrollment in user_program_course_enrollments}
|
||||
|
||||
overviews = CourseOverview.get_from_ids_if_exists(enrollment_dict.keys())
|
||||
|
||||
resume_course_run_urls = get_resume_urls_for_enrollments(user, enrollment_dict.values())
|
||||
|
||||
response = {
|
||||
'course_runs': [],
|
||||
}
|
||||
|
||||
for enrollment in user_program_course_enrollments:
|
||||
overview = overviews[enrollment.course_key]
|
||||
|
||||
certificate_download_url = None
|
||||
is_certificate_passing = None
|
||||
certificate_creation_date = None
|
||||
certificate_info = get_certificate_for_user(user.username, enrollment.course_key)
|
||||
|
||||
if certificate_info:
|
||||
certificate_download_url = certificate_info['download_url']
|
||||
is_certificate_passing = certificate_info['is_passing']
|
||||
certificate_creation_date = certificate_info['created']
|
||||
|
||||
course_run_dict = {
|
||||
'course_run_id': enrollment.course_key,
|
||||
'display_name': overview.display_name_with_default,
|
||||
'course_run_status': self.get_course_run_status(overview, is_certificate_passing, certificate_creation_date),
|
||||
'course_run_url': self.get_course_run_url(request, enrollment.course_key),
|
||||
'start_date': overview.start,
|
||||
'end_date': overview.end,
|
||||
'due_dates': self.get_due_dates(request, enrollment.course_key, user),
|
||||
}
|
||||
|
||||
if certificate_download_url:
|
||||
course_run_dict['certificate_download_url'] = certificate_download_url
|
||||
|
||||
emails_enabled = self.get_emails_enabled(user, enrollment.course_key)
|
||||
if emails_enabled is not None:
|
||||
course_run_dict['emails_enabled'] = emails_enabled
|
||||
|
||||
micromasters_title = self.program['title'] if self.program['type'] == 'MicroMasters' else None
|
||||
if micromasters_title:
|
||||
course_run_dict['micromasters_title'] = micromasters_title
|
||||
|
||||
# if the url is '', then the url is None so we can omit it from the response
|
||||
resume_course_run_url = resume_course_run_urls[enrollment.course_key]
|
||||
if resume_course_run_url:
|
||||
course_run_dict['resume_course_run_url'] = resume_course_run_url
|
||||
|
||||
response['course_runs'].append(course_run_dict)
|
||||
|
||||
serializer = CourseRunOverviewListSerializer(response)
|
||||
return Response(serializer.data)
|
||||
|
||||
@staticmethod
|
||||
def get_due_dates(request, course_key, user):
|
||||
"""
|
||||
Get due date information for a user for blocks in a course.
|
||||
|
||||
Arguments:
|
||||
request: the request object
|
||||
course_key (CourseKey): the CourseKey for the course
|
||||
user: the user object for which we want due date information
|
||||
|
||||
Returns:
|
||||
due_dates (list): a list of dictionaries containing due date information
|
||||
keys:
|
||||
name: the display name of the block
|
||||
url: the deep link to the block
|
||||
date: the due date for the block
|
||||
"""
|
||||
dates = get_dates_for_course(
|
||||
course_key,
|
||||
user,
|
||||
)
|
||||
|
||||
store = modulestore()
|
||||
|
||||
due_dates = []
|
||||
for (block_key, date_type), date in iteritems(dates):
|
||||
if date_type == 'due':
|
||||
block = store.get_item(block_key)
|
||||
|
||||
# get url to the block in the course
|
||||
block_url = reverse('jump_to', args=[course_key, block_key])
|
||||
block_url = request.build_absolute_uri(block_url)
|
||||
|
||||
due_dates.append({
|
||||
'name': block.display_name,
|
||||
'url': block_url,
|
||||
'date': date,
|
||||
})
|
||||
return due_dates
|
||||
|
||||
@staticmethod
|
||||
def get_course_run_url(request, course_id):
|
||||
"""
|
||||
Get the URL to a course run.
|
||||
|
||||
Arguments:
|
||||
request: the request object
|
||||
course_id (string): the course id of the course
|
||||
|
||||
Returns:
|
||||
(string): the URL to the course run associated with course_id
|
||||
"""
|
||||
course_run_url = reverse('openedx.course_experience.course_home', args=[course_id])
|
||||
return request.build_absolute_uri(course_run_url)
|
||||
|
||||
@staticmethod
|
||||
def get_emails_enabled(user, course_id):
|
||||
"""
|
||||
Get whether or not emails are enabled in the context of a course.
|
||||
|
||||
Arguments:
|
||||
user: the user object for which we want to check whether emails are enabled
|
||||
course_id (string): the course id of the course
|
||||
|
||||
Returns:
|
||||
(bool): True if emails are enabled for the course associated with course_id for the user;
|
||||
False otherwise
|
||||
"""
|
||||
if is_bulk_email_feature_enabled(course_id=course_id):
|
||||
return not is_user_opted_out_for_course(user=user, course_id=course_id)
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_course_run_status(course_overview, is_certificate_passing, certificate_creation_date):
|
||||
"""
|
||||
Get the progress status of a course run.
|
||||
|
||||
Arguments:
|
||||
course_overview (CourseOverview): the overview for the course run
|
||||
is_certificate_passing (bool): True if the user has a passing certificate in
|
||||
this course run; False otherwise
|
||||
certificate_creation_date: the date the certificate was created
|
||||
|
||||
Returns:
|
||||
status: one of CourseRunProgressStatuses.COMPLETE,
|
||||
CourseRunProgressStatuses.IN_PROGRESS,
|
||||
or CourseRunProgressStatuses.UPCOMING
|
||||
"""
|
||||
if course_overview.pacing == 'instructor':
|
||||
if course_overview.has_ended():
|
||||
return CourseRunProgressStatuses.COMPLETED
|
||||
elif course_overview.has_started():
|
||||
return CourseRunProgressStatuses.IN_PROGRESS
|
||||
else:
|
||||
return CourseRunProgressStatuses.UPCOMING
|
||||
elif course_overview.pacing == 'self':
|
||||
has_ended = course_overview.has_ended()
|
||||
thirty_days_ago = datetime.now(UTC) - timedelta(30)
|
||||
# a self paced course run is completed when either the course run has ended
|
||||
# OR the user has earned a certificate 30 days ago or more
|
||||
if has_ended or is_certificate_passing and (certificate_creation_date and certificate_creation_date <= thirty_days_ago):
|
||||
return CourseRunProgressStatuses.COMPLETED
|
||||
elif course_overview.has_started():
|
||||
return CourseRunProgressStatuses.IN_PROGRESS
|
||||
else:
|
||||
return CourseRunProgressStatuses.UPCOMING
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user