Fixes hidden tabs showing up in courseware MFE. TNL-7149
Refactors courseware metadata code. Enables masquerading in the courseware metadata API.
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.12 on 2020-04-30 15:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('course_overviews', '0021_courseoverviewtab_link'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='courseoverviewtab',
|
||||
name='is_hidden',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -59,7 +59,7 @@ class CourseOverview(TimeStampedModel):
|
||||
app_label = 'course_overviews'
|
||||
|
||||
# IMPORTANT: Bump this whenever you modify this model and/or add a migration.
|
||||
VERSION = 10
|
||||
VERSION = 11 # this one goes to eleven
|
||||
|
||||
# Cache entry versioning.
|
||||
version = IntegerField()
|
||||
@@ -262,6 +262,7 @@ class CourseOverview(TimeStampedModel):
|
||||
course_staff_only=tab.course_staff_only,
|
||||
url_slug=tab.get('url_slug'),
|
||||
link=tab.get('link'),
|
||||
is_hidden=tab.get('is_hidden', False),
|
||||
course_overview=course_overview)
|
||||
for tab in course.tabs
|
||||
])
|
||||
@@ -864,6 +865,7 @@ class CourseOverviewTab(models.Model):
|
||||
course_staff_only = models.BooleanField(default=False)
|
||||
url_slug = models.TextField(null=True)
|
||||
link = models.TextField(null=True)
|
||||
is_hidden = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.tab_id
|
||||
|
||||
@@ -2,16 +2,8 @@
|
||||
Course API Serializers. Representing course catalog data
|
||||
"""
|
||||
|
||||
from babel.numbers import get_currency_symbol
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework import serializers
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from edxnotes.helpers import is_feature_enabled
|
||||
from lms.djangoapps.courseware.tabs import get_course_tab_list
|
||||
from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link
|
||||
|
||||
from openedx.core.lib.api.fields import AbsoluteURLField
|
||||
|
||||
|
||||
@@ -87,12 +79,12 @@ class CourseInfoSerializer(serializers.Serializer): # pylint: disable=abstract-
|
||||
start_type = serializers.CharField()
|
||||
pacing = serializers.CharField()
|
||||
enrollment = serializers.DictField()
|
||||
tabs = serializers.SerializerMethodField()
|
||||
verified_mode = serializers.SerializerMethodField()
|
||||
tabs = serializers.ListField()
|
||||
verified_mode = serializers.DictField()
|
||||
show_calculator = serializers.BooleanField()
|
||||
is_staff = serializers.BooleanField()
|
||||
can_load_courseware = serializers.DictField()
|
||||
notes = serializers.SerializerMethodField()
|
||||
notes = serializers.DictField()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
@@ -106,41 +98,3 @@ class CourseInfoSerializer(serializers.Serializer): # pylint: disable=abstract-
|
||||
existing = set(self.fields)
|
||||
for field_name in existing - allowed:
|
||||
self.fields.pop(field_name)
|
||||
|
||||
def get_tabs(self, course_overview):
|
||||
"""
|
||||
Return course tab metadata.
|
||||
"""
|
||||
tabs = []
|
||||
for priority, tab in enumerate(get_course_tab_list(course_overview.effective_user, course_overview)):
|
||||
tabs.append({
|
||||
'title': tab.title or tab.get('name', ''),
|
||||
'slug': tab.tab_id,
|
||||
'priority': priority,
|
||||
'type': tab.type,
|
||||
'url': tab.link_func(course_overview, reverse),
|
||||
})
|
||||
return tabs
|
||||
|
||||
def get_verified_mode(self, course_overview):
|
||||
"""
|
||||
Return verified mode information, or None.
|
||||
"""
|
||||
mode = CourseMode.verified_mode_for_course(course_overview.id)
|
||||
if mode:
|
||||
return {
|
||||
'price': mode.min_price,
|
||||
'currency': mode.currency.upper(),
|
||||
'currency_symbol': get_currency_symbol(mode.currency.upper()),
|
||||
'sku': mode.sku,
|
||||
'upgrade_url': verified_upgrade_deadline_link(course_overview.effective_user, course_overview),
|
||||
}
|
||||
|
||||
def get_notes(self, course_overview):
|
||||
"""
|
||||
Return whether edxnotes is enabled and visible.
|
||||
"""
|
||||
return {
|
||||
'enabled': is_feature_enabled(course_overview, course_overview.effective_user),
|
||||
'visible': course_overview.edxnotes_visibility,
|
||||
}
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
"""
|
||||
Tests for courseware API
|
||||
"""
|
||||
from datetime import datetime
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from lms.djangoapps.courseware.access_utils import (
|
||||
ACCESS_DENIED,
|
||||
ACCESS_GRANTED
|
||||
)
|
||||
from lms.djangoapps.courseware.access_utils import ACCESS_DENIED, ACCESS_GRANTED
|
||||
from lms.djangoapps.courseware.tabs import ExternalLinkCourseTab
|
||||
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, ToyCourseFactory
|
||||
from student.tests.factories import UserFactory
|
||||
from student.models import CourseEnrollment
|
||||
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, ToyCourseFactory
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@@ -69,6 +62,9 @@ class CourseApiTestViews(BaseCoursewareTests):
|
||||
def setUpClass(cls):
|
||||
BaseCoursewareTests.setUpClass()
|
||||
cls.course.tabs.append(ExternalLinkCourseTab.load('external_link', name='Zombo', link='http://zombo.com'))
|
||||
cls.course.tabs.append(
|
||||
ExternalLinkCourseTab.load('external_link', name='Hidden', link='http://hidden.com', is_hidden=True)
|
||||
)
|
||||
cls.store.update_item(cls.course, cls.user.id)
|
||||
|
||||
@ddt.data(
|
||||
@@ -96,11 +92,11 @@ class CourseApiTestViews(BaseCoursewareTests):
|
||||
assert len(response.data['tabs']) == 5
|
||||
found = False
|
||||
for tab in response.data['tabs']:
|
||||
if tab['type'] == 'external_link' and tab['url'] == 'http://zombo.com':
|
||||
found = True
|
||||
break
|
||||
else:
|
||||
assert found, 'external link not in course tabs'
|
||||
if tab['type'] == 'external_link':
|
||||
assert tab['url'] != 'http://hidden.com', "Hidden tab is not hidden"
|
||||
if tab['url'] == 'http://zombo.com':
|
||||
found = True
|
||||
assert found, 'external link not in course tabs'
|
||||
elif enable_anonymous and not logged_in:
|
||||
# multiple checks use this handler
|
||||
check_public_access.assert_called()
|
||||
|
||||
@@ -4,26 +4,135 @@ Course API Views
|
||||
|
||||
import json
|
||||
|
||||
from babel.numbers import get_currency_symbol
|
||||
from django.urls import reverse
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from edxnotes.helpers import is_feature_enabled
|
||||
from lms.djangoapps.course_api.api import course_detail
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.courses import check_course_access
|
||||
from lms.djangoapps.courseware.module_render import get_module_by_usage_id
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
from lms.djangoapps.courseware.tabs import get_course_tab_list
|
||||
from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
|
||||
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
||||
from openedx.features.course_duration_limits.access import generate_course_expired_message
|
||||
from openedx.features.discounts.utils import generate_offer_html
|
||||
from xmodule.course_module import COURSE_VISIBILITY_PUBLIC
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
from .serializers import CourseInfoSerializer
|
||||
|
||||
|
||||
class CoursewareMeta:
|
||||
"""
|
||||
Encapsulates courseware and enrollment metadata.
|
||||
"""
|
||||
def __init__(self, course_key, request, username=''):
|
||||
self.overview = course_detail(
|
||||
request,
|
||||
username or request.user.username,
|
||||
course_key,
|
||||
)
|
||||
self.effective_user = self.overview.effective_user
|
||||
self.course_key = course_key
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.overview, name)
|
||||
|
||||
@property
|
||||
def is_staff(self):
|
||||
return has_access(self.effective_user, 'staff', self.overview).has_access
|
||||
|
||||
@property
|
||||
def enrollment(self):
|
||||
"""
|
||||
Return enrollment information.
|
||||
"""
|
||||
if self.effective_user.is_anonymous:
|
||||
mode = None
|
||||
is_active = False
|
||||
else:
|
||||
mode, is_active = CourseEnrollment.enrollment_mode_for_user(
|
||||
self.effective_user,
|
||||
self.course_key
|
||||
)
|
||||
return {'mode': mode, 'is_active': is_active}
|
||||
|
||||
@property
|
||||
def course_expired_message(self):
|
||||
# TODO: TNL-7185 Legacy: Refactor to return the expiration date and format the message in the MFE
|
||||
return generate_course_expired_message(self.effective_user, self.overview)
|
||||
|
||||
@property
|
||||
def offer_html(self):
|
||||
# TODO: TNL-7185 Legacy: Refactor to return the offer data and format the message in the MFE
|
||||
return generate_offer_html(self.effective_user, self.overview)
|
||||
|
||||
@property
|
||||
def content_type_gating_enabled(self):
|
||||
return ContentTypeGatingConfig.enabled_for_enrollment(
|
||||
user=self.effective_user,
|
||||
course_key=self.course_key,
|
||||
)
|
||||
|
||||
@property
|
||||
def can_load_courseware(self):
|
||||
return check_course_access(
|
||||
self.overview,
|
||||
self.effective_user,
|
||||
'load',
|
||||
check_if_enrolled=True,
|
||||
check_survey_complete=False,
|
||||
check_if_authenticated=True,
|
||||
).to_json()
|
||||
|
||||
@property
|
||||
def tabs(self):
|
||||
"""
|
||||
Return course tab metadata.
|
||||
"""
|
||||
tabs = []
|
||||
for priority, tab in enumerate(get_course_tab_list(self.effective_user, self.overview)):
|
||||
tabs.append({
|
||||
'title': tab.title or tab.get('name', ''),
|
||||
'slug': tab.tab_id,
|
||||
'priority': priority,
|
||||
'type': tab.type,
|
||||
'url': tab.link_func(self.overview, reverse),
|
||||
})
|
||||
return tabs
|
||||
|
||||
@property
|
||||
def verified_mode(self):
|
||||
"""
|
||||
Return verified mode information, or None.
|
||||
"""
|
||||
mode = CourseMode.verified_mode_for_course(self.course_key)
|
||||
if mode:
|
||||
return {
|
||||
'price': mode.min_price,
|
||||
'currency': mode.currency.upper(),
|
||||
'currency_symbol': get_currency_symbol(mode.currency.upper()),
|
||||
'sku': mode.sku,
|
||||
'upgrade_url': verified_upgrade_deadline_link(self.effective_user, self.overview),
|
||||
}
|
||||
|
||||
@property
|
||||
def notes(self):
|
||||
"""
|
||||
Return whether edxnotes is enabled and visible.
|
||||
"""
|
||||
return {
|
||||
'enabled': is_feature_enabled(self.overview, self.effective_user),
|
||||
'visible': self.overview.edxnotes_visibility,
|
||||
}
|
||||
|
||||
|
||||
class CoursewareInformation(RetrieveAPIView):
|
||||
"""
|
||||
**Use Cases**
|
||||
@@ -71,6 +180,7 @@ class CoursewareInformation(RetrieveAPIView):
|
||||
|
||||
requested_fields (optional) comma separated list:
|
||||
If set, then only those fields will be returned.
|
||||
username (optional) username to masquerade as (if requesting user is staff)
|
||||
|
||||
**Returns**
|
||||
|
||||
@@ -89,43 +199,16 @@ class CoursewareInformation(RetrieveAPIView):
|
||||
Return the requested course object, if the user has appropriate
|
||||
permissions.
|
||||
"""
|
||||
|
||||
overview = course_detail(
|
||||
self.request,
|
||||
self.request.user.username,
|
||||
CourseKey.from_string(self.kwargs['course_key_string']),
|
||||
)
|
||||
|
||||
if self.request.user.is_anonymous:
|
||||
mode = None
|
||||
is_active = False
|
||||
if self.request.user.is_staff:
|
||||
username = self.request.GET.get('username', '') or self.request.user.username
|
||||
else:
|
||||
mode, is_active = CourseEnrollment.enrollment_mode_for_user(
|
||||
overview.effective_user,
|
||||
overview.id
|
||||
)
|
||||
overview.enrollment = {'mode': mode, 'is_active': is_active}
|
||||
|
||||
overview.is_staff = has_access(self.request.user, 'staff', overview).has_access
|
||||
overview.can_load_courseware = check_course_access(
|
||||
overview,
|
||||
self.request.user,
|
||||
'load',
|
||||
check_if_enrolled=True,
|
||||
check_survey_complete=False,
|
||||
check_if_authenticated=True,
|
||||
).to_json()
|
||||
|
||||
# TODO: TNL-7185 Legacy: Refactor to return the expiration date and format the message in the MFE
|
||||
overview.course_expired_message = generate_course_expired_message(self.request.user, overview)
|
||||
# TODO: TNL-7185 Legacy: Refactor to return the offer data and format the message in the MFE
|
||||
overview.offer_html = generate_offer_html(self.request.user, overview)
|
||||
|
||||
course_key = CourseKey.from_string(self.kwargs['course_key_string'])
|
||||
overview.content_type_gating_enabled = ContentTypeGatingConfig.enabled_for_enrollment(
|
||||
user=self.request.user,
|
||||
course_key=course_key,
|
||||
username = self.request.user.username
|
||||
overview = CoursewareMeta(
|
||||
CourseKey.from_string(self.kwargs['course_key_string']),
|
||||
self.request,
|
||||
username=username,
|
||||
)
|
||||
|
||||
return overview
|
||||
|
||||
def get_serializer_context(self):
|
||||
@@ -160,7 +243,7 @@ class SequenceMetadata(DeveloperErrorViewMixin, APIView):
|
||||
another user specifies a username other than their own.
|
||||
* 404 if the course is not available or cannot be seen.
|
||||
"""
|
||||
def get(self, request, usage_key_string, *args, **kwargs): # pylint: disable=unused-argument
|
||||
def get(self, request, usage_key_string, *args, **kwargs):
|
||||
"""
|
||||
Return response to a GET request.
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user