Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions cms/djangoapps/contentstore/tests/test_programs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Tests covering the Programs listing on the Studio home."""
from django.core.urlresolvers import reverse
import httpretty
from oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL

from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase


class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, ModuleStoreTestCase):
"""Verify Program listing behavior."""
def setUp(self):
super(TestProgramListing, self).setUp()

ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)

self.user = UserFactory(is_staff=True)
self.client.login(username=self.user.username, password='test')

self.studio_home = reverse('home')

@httpretty.activate
def test_programs_config_disabled(self):
"""Verify that the programs tab and creation button aren't rendered when config is disabled."""
self.create_config(enable_studio_tab=False)
self.mock_programs_api()

response = self.client.get(self.studio_home)

self.assertNotIn("You haven't created any programs yet.", response.content)

for program_name in self.PROGRAM_NAMES:
self.assertNotIn(program_name, response.content)

@httpretty.activate
def test_programs_requires_staff(self):
"""Verify that the programs tab and creation button aren't rendered unless the user has global staff."""
self.user = UserFactory(is_staff=False)
self.client.login(username=self.user.username, password='test')

self.create_config()
self.mock_programs_api()

response = self.client.get(self.studio_home)
self.assertNotIn("You haven't created any programs yet.", response.content)

@httpretty.activate
def test_programs_displayed(self):
"""Verify that the programs tab and creation button can be rendered when config is enabled."""
self.create_config()

# When no data is provided, expect creation prompt.
self.mock_programs_api(data={'results': []})

response = self.client.get(self.studio_home)
self.assertIn("You haven't created any programs yet.", response.content)

# When data is provided, expect a program listing.
self.mock_programs_api()

response = self.client.get(self.studio_home)
for program_name in self.PROGRAM_NAMES:
self.assertIn(program_name, response.content)
127 changes: 69 additions & 58 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,97 +2,96 @@
Views related to operations on course objects
"""
import copy
from django.shortcuts import redirect
import json
import random
import logging
import string
from django.utils.translation import ugettext as _
import django.utils
from django.contrib.auth.decorators import login_required
import random
import string # pylint: disable=deprecated-module

from django.conf import settings
from django.views.decorators.http import require_http_methods, require_GET
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.http import HttpResponseBadRequest, HttpResponseNotFound, HttpResponse, Http404
from util.json_request import JsonResponse, JsonResponseBadRequest
from util.date_utils import get_default_time_display
from edxmako.shortcuts import render_to_response

from xmodule.course_module import DEFAULT_START_DATE
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
from xmodule.tabs import CourseTab, CourseTabList, InvalidTabsException
from openedx.core.lib.course_tabs import CourseTabPluginManager
from openedx.core.djangoapps.credit.api import is_credit_course, get_credit_requirements
from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements
from openedx.core.djangoapps.content.course_structures.api.v0 import api, errors
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from xmodule.modulestore import EdxJSONEncoder
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound, Http404
from django.shortcuts import redirect
import django.utils
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_http_methods, require_GET
from django.views.decorators.csrf import ensure_csrf_cookie
from opaque_keys import InvalidKeyError
from opaque_keys.edx.locations import Location
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import Location

from django.views.decorators.csrf import ensure_csrf_cookie
from openedx.core.lib.js_utils import escape_json_dumps
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
from .component import (
ADVANCED_COMPONENT_TYPES,
SPLIT_TEST_COMPONENT_TYPE,
)
from .item import create_xblock_info
from .library import LIBRARIES_ENABLED
from contentstore import utils
from contentstore.course_group_config import (
COHORT_SCHEME,
GroupConfiguration,
GroupConfigurationsValidationError,
RANDOM_SCHEME,
COHORT_SCHEME
)
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
from contentstore.courseware_index import CoursewareSearchIndexer, SearchIndexingError
from contentstore.push_notification import push_notification_enabled
from contentstore.tasks import rerun_course
from contentstore.utils import (
add_instructor,
initialize_permissions,
get_lms_link_for_item,
remove_all_instructors,
reverse_course_url,
reverse_library_url,
reverse_usage_url,
reverse_url,
remove_all_instructors,
)
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
from util.json_request import expect_json
from util.string_utils import _has_non_ascii_characters
from student.auth import has_studio_write_access, has_studio_read_access
from .component import (
SPLIT_TEST_COMPONENT_TYPE,
ADVANCED_COMPONENT_TYPES,
)
from contentstore.tasks import rerun_course
from contentstore.views.entrance_exam import (
create_entrance_exam,
delete_entrance_exam,
update_entrance_exam,
delete_entrance_exam
)

from .library import LIBRARIES_ENABLED
from .item import create_xblock_info
from contentstore.push_notification import push_notification_enabled
from course_action_state.managers import CourseActionStateItemNotFoundError
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from contentstore import utils
from edxmako.shortcuts import render_to_response
from microsite_configuration import microsite
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
from openedx.core.djangoapps.content.course_structures.api.v0 import api, errors
from openedx.core.djangoapps.credit.api import is_credit_course, get_credit_requirements
from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.utils import get_programs
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.lib.course_tabs import CourseTabPluginManager
from openedx.core.lib.js_utils import escape_json_dumps
from student import auth
from student.auth import has_course_author_access, has_studio_write_access, has_studio_read_access
from student.roles import (
CourseInstructorRole, CourseStaffRole, CourseCreatorRole, GlobalStaff, UserBasedRole
)
from student import auth
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
from course_action_state.managers import CourseActionStateItemNotFoundError
from microsite_configuration import microsite
from xmodule.course_module import CourseFields
from student.auth import has_course_author_access

from util.date_utils import get_default_time_display
from util.json_request import JsonResponse, JsonResponseBadRequest, expect_json
from util.milestones_helpers import (
set_prerequisite_courses,
is_valid_course_key,
is_entrance_exams_enabled,
is_prerequisite_courses_enabled,
is_entrance_exams_enabled
is_valid_course_key,
set_prerequisite_courses,
)
from util.string_utils import _has_non_ascii_characters
from xmodule.contentstore.content import StaticContent
from xmodule.course_module import CourseFields
from xmodule.course_module import DEFAULT_START_DATE
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore import EdxJSONEncoder
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
from xmodule.tabs import CourseTab, CourseTabList, InvalidTabsException

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not your fault, but this is an insane tangle of imports!



log = logging.getLogger(__name__)

Expand Down Expand Up @@ -219,6 +218,7 @@ def _dismiss_notification(request, course_action_state_id): # pylint: disable=u
return JsonResponse({'success': True})


# pylint: disable=unused-argument
@login_required
def course_handler(request, course_key_string=None):
"""
Expand Down Expand Up @@ -422,6 +422,13 @@ def course_listing(request):
courses, in_process_course_actions = get_courses_accessible_to_user(request)
libraries = _accessible_libraries_list(request.user) if LIBRARIES_ENABLED else []

programs_config = ProgramsApiConfig.current()
raw_programs = get_programs(request.user) if programs_config.is_studio_tab_enabled else []

# Sort programs alphabetically by name.
# TODO: Support ordering in the Programs API itself.
programs = sorted(raw_programs, key=lambda p: p['name'].lower())

def format_in_process_course_view(uca):
"""
Return a dict of the data which the view requires for each unsucceeded course
Expand Down Expand Up @@ -470,7 +477,9 @@ def format_library_for_view(library):
'course_creator_status': _get_course_creator_status(request.user),
'rerun_creator_status': GlobalStaff().has_user(request.user),
'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False),
'allow_course_reruns': settings.FEATURES.get('ALLOW_COURSE_RERUNS', True)
'allow_course_reruns': settings.FEATURES.get('ALLOW_COURSE_RERUNS', True),
'is_programs_enabled': programs_config.is_studio_tab_enabled and request.user.is_staff,
'programs': programs,
})


Expand Down Expand Up @@ -805,6 +814,7 @@ def _rerun_course(request, org, number, run, fields):
})


# pylint: disable=unused-argument
@login_required
@ensure_csrf_cookie
@require_http_methods(["GET"])
Expand Down Expand Up @@ -837,6 +847,7 @@ def course_info_handler(request, course_key_string):
return HttpResponseBadRequest("Only supports html requests")


# pylint: disable=unused-argument
@login_required
@ensure_csrf_cookie
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
Expand Down
5 changes: 5 additions & 0 deletions cms/envs/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,8 @@

PROCTORING_BACKEND_PROVIDER = AUTH_TOKENS.get("PROCTORING_BACKEND_PROVIDER", PROCTORING_BACKEND_PROVIDER)
PROCTORING_SETTINGS = ENV_TOKENS.get("PROCTORING_SETTINGS", PROCTORING_SETTINGS)

############################ OAUTH2 Provider ###################################

# OpenID Connect issuer ID. Normally the URL of the authentication endpoint.
OAUTH_OIDC_ISSUER = ENV_TOKENS['OAUTH_OIDC_ISSUER']
3 changes: 2 additions & 1 deletion cms/envs/bok_choy.env.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,6 @@
"TECH_SUPPORT_EMAIL": "technical@example.com",
"THEME_NAME": "",
"TIME_ZONE": "America/New_York",
"WIKI_ENABLED": true
"WIKI_ENABLED": true,
"OAUTH_OIDC_ISSUER": "https://www.example.com/oauth2"
}
11 changes: 11 additions & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,11 @@
# Self-paced course configuration
'openedx.core.djangoapps.self_paced',

# OAuth2 Provider
'provider',
'provider.oauth2',
'oauth2_provider',

# These are apps that aren't strictly needed by Studio, but are imported by
# other apps that are. Django 1.8 wants to have imported models supported
# by installed apps.
Expand Down Expand Up @@ -1112,3 +1117,9 @@
'options': {},
}
PROCTORING_SETTINGS = {}


############################ OAUTH2 Provider ###################################

# OpenID Connect issuer ID. Normally the URL of the authentication endpoint.
OAUTH_OIDC_ISSUER = 'https://www.example.com/oauth2'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For JWT auth to succeed, this value must match the issuer expected by Programs. At the moment, Programs expects the value of OAUTH_OIDC_ISSUER configured for the LMS. Does it make more sense to configure that same value for use in CMS settings (via a PR against configuration-secure), or customize Programs' JWT-DRF jwt_decode_handler such that it will accept multiple issuer values?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My opinion is that the "issuer" is our auth provider, whose host is courses.edx.org (more by convenience than by design). I think that would be the correct value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I/we need to open a DevOps ticket to make sure this setting is overridden with the correct value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jimabramson it looks like our configuration is already set up correctly, so we should get this for free. OAUTH_OIDC_ISSUER is defined as generic_env_config, so it should be included in cms_env_config.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice.

3 changes: 3 additions & 0 deletions cms/envs/devstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ def should_show_debug_toolbar(_):
# Whether to run django-require in debug mode.
REQUIRE_DEBUG = DEBUG

########################### OAUTH2 #################################
OAUTH_OIDC_ISSUER = 'http://127.0.0.1:8000/oauth2'

###############################################################################
# See if the developer has any local overrides.
if os.path.isfile(join(dirname(abspath(__file__)), 'private.py')):
Expand Down
3 changes: 0 additions & 3 deletions cms/envs/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@
DEFAULT_FILE_STORAGE,
MEDIA_ROOT,
MEDIA_URL,
# This is practically unused but needed by the oauth2_provider package, which
# some tests in common/ rely on.
OAUTH_OIDC_ISSUER,
)

# mongo connection settings
Expand Down
64 changes: 0 additions & 64 deletions cms/lib/studio_tabs.py

This file was deleted.

Empty file removed cms/lib/tests/__init__.py
Empty file.
Loading