Commit e44e1859 by Afzal Wali

Programs list conditionally added to the context of index and courses page.

parent bde0f7b2
...@@ -127,6 +127,7 @@ from openedx.core.djangoapps.programs.models import ProgramsApiConfig ...@@ -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.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming import helpers as theming_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.user_api.preferences import api as preferences_api
from openedx.core.djangoapps.catalog.utils import get_programs_data
log = logging.getLogger("edx.student") log = logging.getLogger("edx.student")
...@@ -173,6 +174,7 @@ def index(request, extra_context=None, user=AnonymousUser()): ...@@ -173,6 +174,7 @@ def index(request, extra_context=None, user=AnonymousUser()):
if extra_context is None: if extra_context is None:
extra_context = {} extra_context = {}
programs_list = []
courses = get_courses(user) courses = get_courses(user)
if configuration_helpers.get_value( if configuration_helpers.get_value(
...@@ -206,6 +208,16 @@ def index(request, extra_context=None, user=AnonymousUser()): ...@@ -206,6 +208,16 @@ def index(request, extra_context=None, user=AnonymousUser()):
# Insert additional context for use in the template # Insert additional context for use in the template
context.update(extra_context) 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) return render_to_response('index.html', context)
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
Tests for branding page Tests for branding page
""" """
import mock
import datetime import datetime
from django.conf import settings from django.conf import settings
...@@ -287,3 +288,37 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase): ...@@ -287,3 +288,37 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase):
self.assertEqual(context['courses'][0].id, self.starting_later.id) self.assertEqual(context['courses'][0].id, self.starting_later.id)
self.assertEqual(context['courses'][1].id, self.starting_earlier.id) self.assertEqual(context['courses'][1].id, self.starting_earlier.id)
self.assertEqual(context['courses'][2].id, self.course_with_default_start_date.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 ...@@ -34,18 +34,22 @@ from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from rest_framework import status from rest_framework import status
from lms.djangoapps.instructor.views.api import require_global_staff 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 shoppingcart
import survey.utils import survey.utils
import survey.views import survey.views
from lms.djangoapps.ccx.utils import prep_course_for_grading
from certificates import api as certs_api from certificates import api as certs_api
from certificates.models import CertificateStatuses from certificates.models import CertificateStatuses
from openedx.core.djangoapps.models.course_details import CourseDetails from openedx.core.djangoapps.models.course_details import CourseDetails
from commerce.utils import EcommerceService from commerce.utils import EcommerceService
from enrollment.api import add_enrollment from enrollment.api import add_enrollment
from course_modes.models import CourseMode 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 import has_access, has_ccx_coach_role, _adjust_start_date_for_beta_testers
from courseware.access_response import StartDateError from courseware.access_response import StartDateError
from courseware.access_utils import in_preview_mode from courseware.access_utils import in_preview_mode
...@@ -67,8 +71,6 @@ from courseware.models import StudentModule, BaseStudentModuleHistory ...@@ -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.url_helpers import get_redirect_url, get_redirect_url_for_global_staff
from courseware.user_state_client import DjangoXBlockUserStateClient from courseware.user_state_client import DjangoXBlockUserStateClient
from edxmako.shortcuts import render_to_response, render_to_string, marketing_link 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.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.coursetalk.helpers import inject_coursetalk_keys_into_context from openedx.core.djangoapps.coursetalk.helpers import inject_coursetalk_keys_into_context
from openedx.core.djangoapps.credit.api import ( from openedx.core.djangoapps.credit.api import (
...@@ -91,11 +93,9 @@ from xmodule.modulestore.django import modulestore ...@@ -91,11 +93,9 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
from xmodule.tabs import CourseTabList from xmodule.tabs import CourseTabList
from xmodule.x_module import STUDENT_VIEW 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 ..entrance_exams import user_must_complete_entrance_exam
from ..module_render import get_module_for_descriptor, get_module, get_module_by_usage_id from ..module_render import get_module_for_descriptor, get_module, get_module_by_usage_id
log = logging.getLogger("edx.courseware") log = logging.getLogger("edx.courseware")
...@@ -136,21 +136,32 @@ def courses(request): ...@@ -136,21 +136,32 @@ def courses(request):
Render "find courses" page. The course selection work is done in courseware.courses. Render "find courses" page. The course selection work is done in courseware.courses.
""" """
courses_list = [] courses_list = []
programs_list = []
course_discovery_meanings = getattr(settings, 'COURSE_DISCOVERY_MEANINGS', {}) course_discovery_meanings = getattr(settings, 'COURSE_DISCOVERY_MEANINGS', {})
if not settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'): if not settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'):
courses_list = get_courses(request.user) courses_list = get_courses(request.user)
if configuration_helpers.get_value( if configuration_helpers.get_value("ENABLE_COURSE_SORTING_BY_START_DATE",
"ENABLE_COURSE_SORTING_BY_START_DATE", settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]):
settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]
):
courses_list = sort_by_start_date(courses_list) courses_list = sort_by_start_date(courses_list)
else: else:
courses_list = sort_by_announcement(courses_list) 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( return render_to_response(
"courseware/courses.html", "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 = { ...@@ -249,11 +249,15 @@ FEATURES = {
# False to not redirect the user # False to not redirect the user
'ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER': True, '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. # 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. # Set to True to change the course sorting behavior by their start dates, latest first.
'ENABLE_COURSE_SORTING_BY_START_DATE': True, '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 # Expose Mobile REST API. Note that if you use this, you must also set
# ENABLE_OAUTH2_PROVIDER to True # ENABLE_OAUTH2_PROVIDER to True
'ENABLE_MOBILE_REST_API': False, 'ENABLE_MOBILE_REST_API': False,
......
# -*- 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): ...@@ -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 @property
def is_cache_enabled(self): def is_cache_enabled(self):
"""Whether responses from the catalog API will be cached.""" """Whether responses from the catalog API will be cached."""
......
...@@ -70,3 +70,14 @@ class Program(factory.Factory): ...@@ -70,3 +70,14 @@ class Program(factory.Factory):
banner_image = { banner_image = {
size: BannerImage() for size in ['large', 'medium', 'small', 'x-small'] 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 @@ ...@@ -2,6 +2,7 @@
Tests covering utilities for integrating with the catalog service. Tests covering utilities for integrating with the catalog service.
""" """
import uuid import uuid
import copy
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
...@@ -12,9 +13,8 @@ from opaque_keys.edx.keys import CourseKey ...@@ -12,9 +13,8 @@ from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.catalog import utils from openedx.core.djangoapps.catalog import utils
from openedx.core.djangoapps.catalog.models import CatalogIntegration from openedx.core.djangoapps.catalog.models import CatalogIntegration
from openedx.core.djangoapps.catalog.tests import factories, mixins 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 openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from student.tests.factories import UserFactory
UTILS_MODULE = 'openedx.core.djangoapps.catalog.utils' UTILS_MODULE = 'openedx.core.djangoapps.catalog.utils'
...@@ -76,6 +76,73 @@ class TestGetPrograms(mixins.CatalogIntegrationMixin, TestCase): ...@@ -76,6 +76,73 @@ class TestGetPrograms(mixins.CatalogIntegrationMixin, TestCase):
self.assert_contract(mock_get_catalog_data.call_args) self.assert_contract(mock_get_catalog_data.call_args)
self.assertEqual(data, programs) 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): def test_get_one_program(self, _mock_cache, mock_get_catalog_data):
program = factories.Program() program = factories.Program()
mock_get_catalog_data.return_value = program mock_get_catalog_data.return_value = program
......
...@@ -4,6 +4,7 @@ import logging ...@@ -4,6 +4,7 @@ import logging
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.contrib.auth.models import User
from edx_rest_api_client.client import EdxRestApiClient from edx_rest_api_client.client import EdxRestApiClient
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
...@@ -24,7 +25,20 @@ def create_catalog_api_client(user, catalog_integration): ...@@ -24,7 +25,20 @@ def create_catalog_api_client(user, catalog_integration):
return EdxRestApiClient(catalog_integration.internal_api_url, jwt=jwt) 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. """Retrieve marketable programs from the catalog service.
Keyword Arguments: Keyword Arguments:
...@@ -36,8 +50,11 @@ def get_programs(user, uuid=None, type=None): # pylint: disable=redefined-built ...@@ -36,8 +50,11 @@ def get_programs(user, uuid=None, type=None): # pylint: disable=redefined-built
dict, if a specific program is requested. dict, if a specific program is requested.
""" """
catalog_integration = CatalogIntegration.current() catalog_integration = CatalogIntegration.current()
if catalog_integration.enabled: 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) api = create_catalog_api_client(user, catalog_integration)
cache_key = '{base}.programs{type}'.format( cache_key = '{base}.programs{type}'.format(
...@@ -66,6 +83,46 @@ def get_programs(user, uuid=None, type=None): # pylint: disable=redefined-built ...@@ -66,6 +83,46 @@ def get_programs(user, uuid=None, type=None): # pylint: disable=redefined-built
return [] 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): def munge_catalog_program(catalog_program):
"""Make a program from the catalog service look like it came from the programs service. """Make a program from the catalog service look like it came from the programs service.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment