Merge pull request #14045 from edx/afzaledx/WL-829_add_programs_to_courses_view_context
Add programs list to the context for index.html and courses.html template.
This commit is contained in:
@@ -127,6 +127,7 @@ from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.theming import helpers as theming_helpers
|
||||
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs_data
|
||||
|
||||
|
||||
log = logging.getLogger("edx.student")
|
||||
@@ -173,6 +174,7 @@ def index(request, extra_context=None, user=AnonymousUser()):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
|
||||
programs_list = []
|
||||
courses = get_courses(user)
|
||||
|
||||
if configuration_helpers.get_value(
|
||||
@@ -206,6 +208,16 @@ def index(request, extra_context=None, user=AnonymousUser()):
|
||||
# Insert additional context for use in the template
|
||||
context.update(extra_context)
|
||||
|
||||
# Getting all the programs from course-catalog service. The programs_list is being added to the context but it's
|
||||
# not being used currently in lms/templates/index.html. To use this list, you need to create a custom theme that
|
||||
# overrides index.html. The modifications to index.html to display the programs will be done after the support
|
||||
# for edx-pattern-library is added.
|
||||
if configuration_helpers.get_value("DISPLAY_PROGRAMS_ON_MARKETING_PAGES",
|
||||
settings.FEATURES.get("DISPLAY_PROGRAMS_ON_MARKETING_PAGES")):
|
||||
programs_list = get_programs_data(user)
|
||||
|
||||
context["programs_list"] = programs_list
|
||||
|
||||
return render_to_response('index.html', context)
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Tests for branding page
|
||||
"""
|
||||
|
||||
import mock
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
@@ -287,3 +288,37 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase):
|
||||
self.assertEqual(context['courses'][0].id, self.starting_later.id)
|
||||
self.assertEqual(context['courses'][1].id, self.starting_earlier.id)
|
||||
self.assertEqual(context['courses'][2].id, self.course_with_default_start_date.id)
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class IndexPageProgramsTests(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for Programs List in Marketing Pages.
|
||||
"""
|
||||
@patch.dict('django.conf.settings.FEATURES', {'DISPLAY_PROGRAMS_ON_MARKETING_PAGES': False})
|
||||
def test_get_programs_not_called(self):
|
||||
with mock.patch("student.views.get_programs_data") as patched_get_programs_data:
|
||||
# check the /dashboard
|
||||
response = self.client.get('/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(patched_get_programs_data.call_count, 0)
|
||||
|
||||
with mock.patch("courseware.views.views.get_programs_data") as patched_get_programs_data:
|
||||
# check the /courses view
|
||||
response = self.client.get(reverse('branding.views.courses'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(patched_get_programs_data.call_count, 0)
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'DISPLAY_PROGRAMS_ON_MARKETING_PAGES': True})
|
||||
def test_get_programs_called(self):
|
||||
with mock.patch("student.views.get_programs_data") as patched_get_programs_data:
|
||||
# check the /dashboard
|
||||
response = self.client.get('/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(patched_get_programs_data.call_count, 1)
|
||||
|
||||
with mock.patch("courseware.views.views.get_programs_data") as patched_get_programs_data:
|
||||
# check the /courses view
|
||||
response = self.client.get(reverse('branding.views.courses'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(patched_get_programs_data.call_count, 1)
|
||||
|
||||
@@ -34,18 +34,22 @@ from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from rest_framework import status
|
||||
from lms.djangoapps.instructor.views.api import require_global_staff
|
||||
from lms.djangoapps.ccx.utils import prep_course_for_grading
|
||||
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
|
||||
from lms.djangoapps.instructor.enrollment import uses_shib
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
|
||||
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs_data
|
||||
import shoppingcart
|
||||
import survey.utils
|
||||
import survey.views
|
||||
from lms.djangoapps.ccx.utils import prep_course_for_grading
|
||||
from certificates import api as certs_api
|
||||
from certificates.models import CertificateStatuses
|
||||
from openedx.core.djangoapps.models.course_details import CourseDetails
|
||||
from commerce.utils import EcommerceService
|
||||
from enrollment.api import add_enrollment
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
|
||||
from courseware.access import has_access, has_ccx_coach_role, _adjust_start_date_for_beta_testers
|
||||
from courseware.access_response import StartDateError
|
||||
from courseware.access_utils import in_preview_mode
|
||||
@@ -67,8 +71,6 @@ from courseware.models import StudentModule, BaseStudentModuleHistory
|
||||
from courseware.url_helpers import get_redirect_url, get_redirect_url_for_global_staff
|
||||
from courseware.user_state_client import DjangoXBlockUserStateClient
|
||||
from edxmako.shortcuts import render_to_response, render_to_string, marketing_link
|
||||
from lms.djangoapps.instructor.enrollment import uses_shib
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.coursetalk.helpers import inject_coursetalk_keys_into_context
|
||||
from openedx.core.djangoapps.credit.api import (
|
||||
@@ -91,11 +93,9 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
|
||||
from xmodule.tabs import CourseTabList
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
|
||||
from ..entrance_exams import user_must_complete_entrance_exam
|
||||
from ..module_render import get_module_for_descriptor, get_module, get_module_by_usage_id
|
||||
|
||||
|
||||
log = logging.getLogger("edx.courseware")
|
||||
|
||||
|
||||
@@ -136,21 +136,32 @@ def courses(request):
|
||||
Render "find courses" page. The course selection work is done in courseware.courses.
|
||||
"""
|
||||
courses_list = []
|
||||
programs_list = []
|
||||
course_discovery_meanings = getattr(settings, 'COURSE_DISCOVERY_MEANINGS', {})
|
||||
if not settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'):
|
||||
courses_list = get_courses(request.user)
|
||||
|
||||
if configuration_helpers.get_value(
|
||||
"ENABLE_COURSE_SORTING_BY_START_DATE",
|
||||
settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]
|
||||
):
|
||||
if configuration_helpers.get_value("ENABLE_COURSE_SORTING_BY_START_DATE",
|
||||
settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]):
|
||||
courses_list = sort_by_start_date(courses_list)
|
||||
else:
|
||||
courses_list = sort_by_announcement(courses_list)
|
||||
|
||||
# Getting all the programs from course-catalog service. The programs_list is being added to the context but it's
|
||||
# not being used currently in courseware/courses.html. To use this list, you need to create a custom theme that
|
||||
# overrides courses.html. The modifications to courses.html to display the programs will be done after the support
|
||||
# for edx-pattern-library is added.
|
||||
if configuration_helpers.get_value("DISPLAY_PROGRAMS_ON_MARKETING_PAGES",
|
||||
settings.FEATURES.get("DISPLAY_PROGRAMS_ON_MARKETING_PAGES")):
|
||||
programs_list = get_programs_data(request.user)
|
||||
|
||||
return render_to_response(
|
||||
"courseware/courses.html",
|
||||
{'courses': courses_list, 'course_discovery_meanings': course_discovery_meanings}
|
||||
{
|
||||
'courses': courses_list,
|
||||
'course_discovery_meanings': course_discovery_meanings,
|
||||
'programs_list': programs_list
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -249,11 +249,15 @@ FEATURES = {
|
||||
# False to not redirect the user
|
||||
'ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER': True,
|
||||
|
||||
# When a user goes to the homepage ('/') the user see the
|
||||
# When a user goes to the homepage ('/') the user sees the
|
||||
# courses listed in the announcement dates order - this is default Open edX behavior.
|
||||
# Set to True to change the course sorting behavior by their start dates, latest first.
|
||||
'ENABLE_COURSE_SORTING_BY_START_DATE': True,
|
||||
|
||||
# When set to True, a list of programs is displayed along with the list of courses
|
||||
# when the user visits the homepage or the find courses page.
|
||||
'DISPLAY_PROGRAMS_ON_MARKETING_PAGES': False,
|
||||
|
||||
# Expose Mobile REST API. Note that if you use this, you must also set
|
||||
# ENABLE_OAUTH2_PROVIDER to True
|
||||
'ENABLE_MOBILE_REST_API': False,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('catalog', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='catalogintegration',
|
||||
name='service_username',
|
||||
field=models.CharField(default=b'lms_catalog_service_user', help_text='Username created for Course Catalog Integration, e.g. lms_catalog_service_user.', max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -25,6 +25,16 @@ class CatalogIntegration(ConfigurationModel):
|
||||
)
|
||||
)
|
||||
|
||||
service_username = models.CharField(
|
||||
max_length=100,
|
||||
default="lms_catalog_service_user",
|
||||
null=False,
|
||||
blank=False,
|
||||
help_text=_(
|
||||
'Username created for Course Catalog Integration, e.g. lms_catalog_service_user.'
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def is_cache_enabled(self):
|
||||
"""Whether responses from the catalog API will be cached."""
|
||||
|
||||
@@ -70,3 +70,14 @@ class Program(factory.Factory):
|
||||
banner_image = {
|
||||
size: BannerImage() for size in ['large', 'medium', 'small', 'x-small']
|
||||
}
|
||||
|
||||
|
||||
class ProgramType(factory.Factory):
|
||||
"""
|
||||
Factory for stubbing ProgramType resources from the catalog API.
|
||||
"""
|
||||
class Meta(object):
|
||||
model = dict
|
||||
|
||||
name = FuzzyText()
|
||||
logo_image = FuzzyText(prefix='https://example.com/program/logo')
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Tests covering utilities for integrating with the catalog service.
|
||||
"""
|
||||
import uuid
|
||||
import copy
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
@@ -12,9 +13,8 @@ from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.djangoapps.catalog import utils
|
||||
from openedx.core.djangoapps.catalog.models import CatalogIntegration
|
||||
from openedx.core.djangoapps.catalog.tests import factories, mixins
|
||||
from student.tests.factories import UserFactory, AnonymousUserFactory
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
UTILS_MODULE = 'openedx.core.djangoapps.catalog.utils'
|
||||
|
||||
@@ -76,6 +76,73 @@ class TestGetPrograms(mixins.CatalogIntegrationMixin, TestCase):
|
||||
self.assert_contract(mock_get_catalog_data.call_args)
|
||||
self.assertEqual(data, programs)
|
||||
|
||||
def test_get_programs_anonymous_user(self, _mock_cache, mock_get_catalog_data):
|
||||
programs = [factories.Program() for __ in range(3)]
|
||||
mock_get_catalog_data.return_value = programs
|
||||
|
||||
anonymous_user = AnonymousUserFactory()
|
||||
|
||||
# The user is an Anonymous user but the Catalog Service User has not been created yet.
|
||||
data = utils.get_programs(anonymous_user)
|
||||
# This should not return programs.
|
||||
self.assertEqual(data, [])
|
||||
|
||||
UserFactory(username='lms_catalog_service_user')
|
||||
# After creating the service user above,
|
||||
data = utils.get_programs(anonymous_user)
|
||||
# the programs should be returned successfully.
|
||||
self.assertEqual(data, programs)
|
||||
|
||||
def test_get_program_types(self, _mock_cache, mock_get_catalog_data):
|
||||
program_types = [factories.ProgramType() for __ in range(3)]
|
||||
mock_get_catalog_data.return_value = program_types
|
||||
|
||||
# Creating Anonymous user but the Catalog Service User has not been created yet.
|
||||
anonymous_user = AnonymousUserFactory()
|
||||
data = utils.get_program_types(anonymous_user)
|
||||
# This should not return programs.
|
||||
self.assertEqual(data, [])
|
||||
|
||||
# Creating Catalog Service User user
|
||||
UserFactory(username='lms_catalog_service_user')
|
||||
data = utils.get_program_types(anonymous_user)
|
||||
# the programs should be returned successfully.
|
||||
self.assertEqual(data, program_types)
|
||||
|
||||
# Catalog integration is disabled now.
|
||||
self.catalog_integration = self.create_catalog_integration(enabled=False)
|
||||
data = utils.get_program_types(anonymous_user)
|
||||
# This should not return programs.
|
||||
self.assertEqual(data, [])
|
||||
|
||||
def test_get_programs_data(self, _mock_cache, mock_get_catalog_data): # pylint: disable=unused-argument
|
||||
programs = []
|
||||
program_types = []
|
||||
programs_data = []
|
||||
|
||||
for index in range(3):
|
||||
# Creating the Programs and their corresponding program types.
|
||||
type_name = "type_name_{postfix}".format(postfix=index)
|
||||
program = factories.Program(type=type_name)
|
||||
program_type = factories.ProgramType(name=type_name)
|
||||
|
||||
# Maintaining the programs, program types and program data(program+logo_image) lists.
|
||||
programs.append(program)
|
||||
program_types.append(program_type)
|
||||
programs_data.append(copy.deepcopy(program))
|
||||
|
||||
# Adding the logo image in program data.
|
||||
programs_data[-1]['logo_image'] = program_type["logo_image"]
|
||||
|
||||
with mock.patch("openedx.core.djangoapps.catalog.utils.get_programs") as patched_get_programs:
|
||||
with mock.patch("openedx.core.djangoapps.catalog.utils.get_program_types") as patched_get_program_types:
|
||||
# Mocked the "get_programs" and "get_program_types"
|
||||
patched_get_programs.return_value = programs
|
||||
patched_get_program_types.return_value = program_types
|
||||
|
||||
programs_data = utils.get_programs_data()
|
||||
self.assertEqual(programs_data, programs)
|
||||
|
||||
def test_get_one_program(self, _mock_cache, mock_get_catalog_data):
|
||||
program = factories.Program()
|
||||
mock_get_catalog_data.return_value = program
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.contrib.auth.models import User
|
||||
from edx_rest_api_client.client import EdxRestApiClient
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
@@ -24,7 +25,20 @@ def create_catalog_api_client(user, catalog_integration):
|
||||
return EdxRestApiClient(catalog_integration.internal_api_url, jwt=jwt)
|
||||
|
||||
|
||||
def get_programs(user, uuid=None, type=None): # pylint: disable=redefined-builtin
|
||||
def _get_service_user(user, service_username):
|
||||
"""
|
||||
Retrieve and return the Catalog Integration Service User Object
|
||||
if the passed user is None or anonymous
|
||||
"""
|
||||
if not user or user.is_anonymous():
|
||||
try:
|
||||
user = User.objects.get(username=service_username)
|
||||
except User.DoesNotExist:
|
||||
user = None
|
||||
return user
|
||||
|
||||
|
||||
def get_programs(user=None, uuid=None, type=None): # pylint: disable=redefined-builtin
|
||||
"""Retrieve marketable programs from the catalog service.
|
||||
|
||||
Keyword Arguments:
|
||||
@@ -36,8 +50,11 @@ def get_programs(user, uuid=None, type=None): # pylint: disable=redefined-built
|
||||
dict, if a specific program is requested.
|
||||
"""
|
||||
catalog_integration = CatalogIntegration.current()
|
||||
|
||||
if catalog_integration.enabled:
|
||||
user = _get_service_user(user, catalog_integration.service_username)
|
||||
if not user:
|
||||
return []
|
||||
|
||||
api = create_catalog_api_client(user, catalog_integration)
|
||||
|
||||
cache_key = '{base}.programs{type}'.format(
|
||||
@@ -66,6 +83,46 @@ def get_programs(user, uuid=None, type=None): # pylint: disable=redefined-built
|
||||
return []
|
||||
|
||||
|
||||
def get_program_types(user=None): # pylint: disable=redefined-builtin
|
||||
"""Retrieve all program types from the catalog service.
|
||||
|
||||
Returns:
|
||||
list of dict, representing program types.
|
||||
"""
|
||||
catalog_integration = CatalogIntegration.current()
|
||||
if catalog_integration.enabled:
|
||||
user = _get_service_user(user, catalog_integration.service_username)
|
||||
if not user:
|
||||
return []
|
||||
|
||||
api = create_catalog_api_client(user, catalog_integration)
|
||||
cache_key = '{base}.program_types'.format(base=catalog_integration.CACHE_KEY)
|
||||
|
||||
return get_edx_api_data(
|
||||
catalog_integration,
|
||||
user,
|
||||
'program_types',
|
||||
cache_key=cache_key if catalog_integration.is_cache_enabled else None,
|
||||
api=api
|
||||
)
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def get_programs_data(user=None):
|
||||
"""Return the list of Programs after adding the ProgramType Logo Image"""
|
||||
|
||||
programs_list = get_programs(user)
|
||||
program_types = get_program_types(user)
|
||||
|
||||
program_types_lookup_dict = {program_type["name"]: program_type for program_type in program_types}
|
||||
|
||||
for program in programs_list:
|
||||
program["logo_image"] = program_types_lookup_dict[program["type"]]["logo_image"]
|
||||
|
||||
return programs_list
|
||||
|
||||
|
||||
def munge_catalog_program(catalog_program):
|
||||
"""Make a program from the catalog service look like it came from the programs service.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user