Commit 4b643164 by Nimisha Asthagiri

Merge branch 'rc/2015-06-24' into release

parents 9480a365 66e4e28a
......@@ -222,3 +222,6 @@ Xiaolu Xiong <beardeer@gmail.com>
Tim Krones <t.krones@gmx.net>
Linda Liu <lliu@edx.org>
Alessandro Verdura <finalmente2@tin.it>
Sven Marnach <sven@marnach.net>
Richard Moch <richard.moch@gmail.com>
......@@ -47,6 +47,8 @@ LMS: Support adding students to a cohort via the instructor dashboard. TNL-163
LMS: Show cohorts on the new instructor dashboard. TNL-161
LMS: Extended hints feature
LMS: Mobile API available for courses that opt in using the Course Advanced
Setting "Mobile Course Available" (only used in limited closed beta).
......
......@@ -59,12 +59,12 @@ def click_new_component_button(step, component_button_css):
def _click_advanced():
css = 'ul.problem-type-tabs a[href="#tab2"]'
css = 'ul.problem-type-tabs a[href="#tab3"]'
world.css_click(css)
# Wait for the advanced tab items to be displayed
tab2_css = 'div.ui-tabs-panel#tab2'
world.wait_for_visible(tab2_css)
tab3_css = 'div.ui-tabs-panel#tab3'
world.wait_for_visible(tab3_css)
def _find_matching_link(category, component_type):
......
......@@ -1109,6 +1109,17 @@ class ContentStoreTest(ContentStoreTestCase):
self.assertFalse(instructor_role.has_user(self.user))
self.assertEqual(len(instructor_role.users_with_role()), 0)
def test_create_course_after_delete(self):
"""
Test that course creation works after deleting a course with the same URL
"""
test_course_data = self.assert_created_course()
course_id = _get_course_id(self.store, test_course_data)
delete_course_and_groups(course_id, self.user.id)
self.assert_created_course()
def test_create_course_duplicate_course(self):
"""Test new course creation - error path"""
self.client.ajax_post('/course/', self.course_data)
......
......@@ -11,15 +11,16 @@ from django.contrib.auth.models import User
from django.test.client import Client
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
from contentstore.utils import reverse_url
from student.models import Registration
from contentstore.utils import reverse_url # pylint: disable=import-error
from student.models import Registration # pylint: disable=import-error
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
from xmodule.contentstore.django import contentstore
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
......@@ -67,7 +68,7 @@ class AjaxEnabledTestClient(Client):
return self.get(path, data or {}, follow, HTTP_ACCEPT="application/json", **extra)
class CourseTestCase(ModuleStoreTestCase):
class CourseTestCase(ProceduralCourseTestMixin, ModuleStoreTestCase):
"""
Base class for Studio tests that require a logged in user and a course.
Also provides helper methods for manipulating and verifying the course.
......@@ -100,26 +101,6 @@ class CourseTestCase(ModuleStoreTestCase):
nonstaff.is_authenticated = lambda: authenticate
return client, nonstaff
def populate_course(self, branching=2):
"""
Add k chapters, k^2 sections, k^3 verticals, k^4 problems to self.course (where k = branching)
"""
user_id = self.user.id
self.populated_usage_keys = {}
def descend(parent, stack):
if not stack:
return
xblock_type = stack[0]
for _ in range(branching):
child = ItemFactory.create(category=xblock_type, parent_location=parent.location, user_id=user_id)
print child.location
self.populated_usage_keys.setdefault(xblock_type, []).append(child.location)
descend(child, stack[1:])
descend(self.course, ['chapter', 'sequential', 'vertical', 'problem'])
def reload_course(self):
"""
Reloads the course object from the database
......
......@@ -58,9 +58,9 @@ ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
ADVANCED_PROBLEM_TYPES = settings.ADVANCED_PROBLEM_TYPES
CONTAINER_TEMPATES = [
CONTAINER_TEMPLATES = [
"basic-modal", "modal-button", "edit-xblock-modal",
"editor-mode-button", "upload-dialog", "image-modal",
"editor-mode-button", "upload-dialog",
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu",
"add-xblock-component-menu-problem", "xblock-string-field-editor", "publish-xblock", "publish-history",
"unit-outline", "container-message", "license-selector",
......@@ -217,7 +217,7 @@ def container_handler(request, usage_key_string):
'xblock_info': xblock_info,
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'templates': CONTAINER_TEMPATES
'templates': CONTAINER_TEMPLATES
})
else:
return HttpResponseBadRequest("Only supports HTML requests")
......@@ -227,7 +227,7 @@ def get_component_templates(courselike, library=False):
"""
Returns the applicable component templates that can be used by the specified course or library.
"""
def create_template_dict(name, cat, boilerplate_name=None, is_common=False):
def create_template_dict(name, cat, boilerplate_name=None, tab="common"):
"""
Creates a component template dict.
......@@ -235,14 +235,14 @@ def get_component_templates(courselike, library=False):
display_name: the user-visible name of the component
category: the type of component (problem, html, etc.)
boilerplate_name: name of boilerplate for filling in default values. May be None.
is_common: True if "common" problem, False if "advanced". May be None, as it is only used for problems.
tab: common(default)/advanced/hint, which tab it goes in
"""
return {
"display_name": name,
"category": cat,
"boilerplate_name": boilerplate_name,
"is_common": is_common
"tab": tab
}
component_display_names = {
......@@ -268,8 +268,8 @@ def get_component_templates(courselike, library=False):
# add the default template with localized display name
# TODO: Once mixins are defined per-application, rather than per-runtime,
# this should use a cms mixed-in class. (cpennington)
display_name = xblock_type_display_name(category, _('Blank'))
templates_for_category.append(create_template_dict(display_name, category))
display_name = xblock_type_display_name(category, _('Blank')) # this is the Blank Advanced problem
templates_for_category.append(create_template_dict(display_name, category, None, 'advanced'))
categories.add(category)
# add boilerplates
......@@ -277,12 +277,20 @@ def get_component_templates(courselike, library=False):
for template in component_class.templates():
filter_templates = getattr(component_class, 'filter_templates', None)
if not filter_templates or filter_templates(template, courselike):
# Tab can be 'common' 'advanced' 'hint'
# Default setting is common/advanced depending on the presence of markdown
tab = 'common'
if template['metadata'].get('markdown') is None:
tab = 'advanced'
# Then the problem can override that with a tab: setting
tab = template['metadata'].get('tab', tab)
templates_for_category.append(
create_template_dict(
_(template['metadata'].get('display_name')), # pylint: disable=translation-of-non-string
category,
template.get('template_id'),
template['metadata'].get('markdown') is not None
tab
)
)
......@@ -297,7 +305,7 @@ def get_component_templates(courselike, library=False):
log.warning('Unable to load xblock type %s to read display_name', component, exc_info=True)
else:
templates_for_category.append(
create_template_dict(component_display_name, component, boilerplate_name)
create_template_dict(component_display_name, component, boilerplate_name, 'advanced')
)
categories.add(component)
......
......@@ -26,7 +26,7 @@ from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from .user import user_with_role
from .component import get_component_templates, CONTAINER_TEMPATES
from .component import get_component_templates, CONTAINER_TEMPLATES
from student.auth import (
STUDIO_VIEW_USERS, STUDIO_EDIT_ROLES, get_user_permissions, has_studio_read_access, has_studio_write_access
)
......@@ -197,7 +197,7 @@ def library_blocks_view(library, user, response_format):
'context_library': library,
'component_templates': json.dumps(component_templates),
'xblock_info': xblock_info,
'templates': CONTAINER_TEMPATES,
'templates': CONTAINER_TEMPLATES,
})
......
......@@ -8,7 +8,6 @@ from PIL import Image
import json
from django.conf import settings
from django.test.utils import override_settings
from contentstore.tests.utils import CourseTestCase
from contentstore.views import assets
......@@ -56,18 +55,19 @@ class AssetsTestCase(CourseTestCase):
"""
Returns an in-memory file of the specified type with the given name for testing
"""
sample_asset = BytesIO()
sample_file_contents = "This file is generated by python unit test"
if asset_type == 'text':
sample_asset = BytesIO(name)
sample_asset.name = '{name}.txt'.format(name=name)
sample_asset.write(sample_file_contents)
elif asset_type == 'image':
image = Image.new("RGB", size=(50, 50), color=(256, 0, 0))
sample_asset = BytesIO()
image.save(unicode(sample_asset), 'jpeg')
image.save(sample_asset, 'jpeg')
sample_asset.name = '{name}.jpg'.format(name=name)
sample_asset.seek(0)
elif asset_type == 'opendoc':
sample_asset = BytesIO(name)
sample_asset.name = '{name}.odt'.format(name=name)
sample_asset.write(sample_file_contents)
sample_asset.seek(0)
return sample_asset
......@@ -324,7 +324,7 @@ class DownloadTestCase(AssetsTestCase):
# Now, download it.
resp = self.client.get(self.uploaded_url, HTTP_ACCEPT='text/html')
self.assertEquals(resp.status_code, 200)
self.assertEquals(resp.content, self.asset_name)
self.assertContains(resp, 'This file is generated by python unit test')
def test_download_not_found_throw(self):
url = self.uploaded_url.replace(self.asset_name, 'not_the_asset_name')
......
......@@ -16,11 +16,9 @@ from mock import Mock, patch
from edxval.api import create_profile, create_video, get_video_info
from contentstore.models import VideoUploadConfig
from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, VIDEO_ASSET_TYPE, StatusDisplayStrings
from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, StatusDisplayStrings
from contentstore.tests.utils import CourseTestCase
from contentstore.utils import reverse_course_url
from xmodule.assetstore import AssetMetadata
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory
......@@ -47,6 +45,7 @@ class VideoUploadTestMixin(object):
"client_video_id": "test1.mp4",
"duration": 42.0,
"status": "upload",
"courses": [unicode(self.course.id)],
"encoded_videos": [],
},
{
......@@ -54,6 +53,7 @@ class VideoUploadTestMixin(object):
"client_video_id": "test2.mp4",
"duration": 128.0,
"status": "file_complete",
"courses": [unicode(self.course.id)],
"encoded_videos": [
{
"profile": "profile1",
......@@ -74,6 +74,7 @@ class VideoUploadTestMixin(object):
"client_video_id": u"nón-ascii-näme.mp4",
"duration": 256.0,
"status": "transcode_active",
"courses": [unicode(self.course.id)],
"encoded_videos": [
{
"profile": "profile1",
......@@ -91,6 +92,7 @@ class VideoUploadTestMixin(object):
"client_video_id": "status_test.mp4",
"duration": 3.14,
"status": status,
"courses": [unicode(self.course.id)],
"encoded_videos": [],
}
for status in (
......@@ -102,12 +104,6 @@ class VideoUploadTestMixin(object):
create_profile(profile)
for video in self.previous_uploads:
create_video(video)
modulestore().save_asset_metadata(
AssetMetadata(
self.course.id.make_asset_key(VIDEO_ASSET_TYPE, video["edx_video_id"])
),
self.user.id
)
def _get_previous_upload(self, edx_video_id):
"""Returns the previous upload with the given video id."""
......@@ -289,13 +285,6 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
headers={"Content-Type": file_info["content_type"]}
)
# Ensure asset store was updated and the created_by field was set
asset_metadata = modulestore().find_asset_metadata(
self.course.id.make_asset_key(VIDEO_ASSET_TYPE, video_id)
)
self.assertIsNotNone(asset_metadata)
self.assertEquals(asset_metadata.created_by, self.user.id)
# Ensure VAL was updated
val_info = get_video_info(video_id)
self.assertEqual(val_info["status"], "upload")
......
......@@ -12,15 +12,13 @@ from django.utils.translation import ugettext as _, ugettext_noop
from django.views.decorators.http import require_GET, require_http_methods
import rfc6266
from edxval.api import create_video, get_videos_for_ids, SortDirection, VideoSortField
from edxval.api import create_video, get_videos_for_course, SortDirection, VideoSortField
from opaque_keys.edx.keys import CourseKey
from contentstore.models import VideoUploadConfig
from contentstore.utils import reverse_course_url
from edxmako.shortcuts import render_to_response
from util.json_request import expect_json, JsonResponse
from xmodule.assetstore import AssetMetadata
from xmodule.modulestore.django import modulestore
from .course import get_course_and_check_access
......@@ -28,9 +26,6 @@ from .course import get_course_and_check_access
__all__ = ["videos_handler", "video_encodings_download"]
# String constant used in asset keys to identify video assets.
VIDEO_ASSET_TYPE = "video"
# Default expiration, in seconds, of one-time URLs used for uploading videos.
KEY_EXPIRATION_IN_SECONDS = 86400
......@@ -217,15 +212,9 @@ def _get_and_validate_course(course_key_string, user):
def _get_videos(course):
"""
Retrieves the list of videos from VAL corresponding to the videos listed in
the asset metadata store.
Retrieves the list of videos from VAL corresponding to this course.
"""
edx_videos_ids = [
v.asset_id.path
for v in modulestore().get_all_asset_metadata(course.id, VIDEO_ASSET_TYPE)
]
videos = list(get_videos_for_ids(edx_videos_ids, VideoSortField.created, SortDirection.desc))
videos = list(get_videos_for_course(course.id, VideoSortField.created, SortDirection.desc))
# convert VAL's status to studio's Video Upload feature status.
for video in videos:
......@@ -333,11 +322,6 @@ def videos_post(course, request):
headers={"Content-Type": req_file["content_type"]}
)
# persist edx_video_id as uploaded through this course
user_id = request.user.id
video_meta_data = AssetMetadata(course.id.make_asset_key(VIDEO_ASSET_TYPE, edx_video_id), created_by=user_id)
modulestore().save_asset_metadata(video_meta_data, user_id)
# persist edx_video_id in VAL
create_video({
"edx_video_id": edx_video_id,
......
......@@ -332,10 +332,6 @@ VIDEO_UPLOAD_PIPELINE = ENV_TOKENS.get('VIDEO_UPLOAD_PIPELINE', VIDEO_UPLOAD_PIP
PARSE_KEYS = AUTH_TOKENS.get("PARSE_KEYS", {})
#date format the api will be formatting the datetime values
API_DATE_FORMAT = '%Y-%m-%d'
API_DATE_FORMAT = ENV_TOKENS.get('API_DATE_FORMAT', API_DATE_FORMAT)
# Video Caching. Pairing country codes with CDN URLs.
# Example: {'CN': 'http://api.xuetangx.com/edx/video?s3_url='}
VIDEO_CDN_URL = ENV_TOKENS.get('VIDEO_CDN_URL', {})
......
......@@ -175,6 +175,9 @@ FEATURES = {
# Enable credit eligibility feature
'ENABLE_CREDIT_ELIGIBILITY': False,
# Can the visibility of the discussion tab be configured on a per-course basis?
'ALLOW_HIDING_DISCUSSION_TAB': False,
}
ENABLE_JASMINE = False
......@@ -207,6 +210,7 @@ MAKO_TEMPLATES['main'] = [
COMMON_ROOT / 'templates',
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates',
COMMON_ROOT / 'djangoapps' / 'pipeline_js' / 'templates',
COMMON_ROOT / 'static', # required to statically include common Underscore templates
]
for namespace, template_dirs in lms.envs.common.MAKO_TEMPLATES.iteritems():
......@@ -946,8 +950,6 @@ ADVANCED_PROBLEM_TYPES = [
}
]
#date format the api will be formatting the datetime values
API_DATE_FORMAT = '%Y-%m-%d'
# Files and Uploads type filter values
......
......@@ -30,6 +30,11 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
LMS_BASE = "localhost:8000"
FEATURES['PREVIEW_LMS_BASE'] = "preview." + LMS_BASE
########################### PIPELINE #################################
# Skip RequireJS optimizer in development
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
############################# ADVANCED COMPONENTS #############################
# Make it easier to test advanced components in local dev
......@@ -92,6 +97,11 @@ FEATURES['ENABLE_COURSEWARE_INDEX'] = True
FEATURES['ENABLE_LIBRARY_INDEX'] = True
SEARCH_ENGINE = "search.elastic.ElasticSearchEngine"
################################# DJANGO-REQUIRE ###############################
# Whether to run django-require in debug mode.
REQUIRE_DEBUG = DEBUG
###############################################################################
# See if the developer has any local overrides.
try:
......
......@@ -39,6 +39,7 @@
'js/certificates/factories/certificates_page_factory',
'js/factories/import',
'js/factories/index',
'js/factories/library',
'js/factories/login',
'js/factories/manage_users',
'js/factories/outline',
......@@ -118,7 +119,7 @@
* As of 1.0.3, this value can also be a string that is converted to a
* RegExp via new RegExp().
*/
fileExclusionRegExp: /^\.|spec/,
fileExclusionRegExp: /^\.|spec|spec_helpers/,
/**
* Allow CSS optimizations. Allowed values:
* - "standard": @import inlining and removal of comments, unnecessary
......@@ -153,6 +154,6 @@
* SILENT: 4
* Default is 0.
*/
logLevel: 4
logLevel: 1
};
} ())
......@@ -23,6 +23,7 @@ requirejs.config({
"jquery.simulate": "xmodule_js/common_static/js/vendor/jquery.simulate",
"datepair": "xmodule_js/common_static/js/vendor/timepicker/datepair",
"date": "xmodule_js/common_static/js/vendor/date",
"text": "xmodule_js/common_static/js/vendor/requirejs/text",
"underscore": "xmodule_js/common_static/js/vendor/underscore-min",
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
"backbone": "xmodule_js/common_static/js/vendor/backbone-min",
......@@ -240,13 +241,11 @@ define([
"js/spec/views/active_video_upload_list_spec",
"js/spec/views/previous_video_upload_spec",
"js/spec/views/previous_video_upload_list_spec",
"js/spec/views/paging_spec",
"js/spec/views/assets_spec",
"js/spec/views/baseview_spec",
"js/spec/views/container_spec",
"js/spec/views/paged_container_spec",
"js/spec/views/group_configuration_spec",
"js/spec/views/paging_spec",
"js/spec/views/unit_outline_spec",
"js/spec/views/xblock_spec",
"js/spec/views/xblock_editor_spec",
......@@ -279,6 +278,7 @@ define([
"js/certificates/spec/views/certificate_details_spec",
"js/certificates/spec/views/certificate_editor_spec",
"js/certificates/spec/views/certificates_list_spec",
"js/certificates/spec/views/certificate_preview_spec",
# these tests are run separately in the cms-squire suite, due to process
# isolation issues with Squire.js
......
require ["jquery", "backbone", "coffee/src/main", "js/common_helpers/ajax_helpers", "jasmine-stealth", "jquery.cookie"],
require ["jquery", "backbone", "coffee/src/main", "common/js/spec_helpers/ajax_helpers", "jasmine-stealth", "jquery.cookie"],
($, Backbone, main, AjaxHelpers) ->
describe "CMS", ->
it "should initialize URL", ->
......
......@@ -21,6 +21,7 @@ requirejs.config({
"jquery.immediateDescendents": "xmodule_js/common_static/coffee/src/jquery.immediateDescendents",
"datepair": "xmodule_js/common_static/js/vendor/timepicker/datepair",
"date": "xmodule_js/common_static/js/vendor/date",
"text": "xmodule_js/common_static/js/vendor/requirejs/text",
"underscore": "xmodule_js/common_static/js/vendor/underscore-min",
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
"backbone": "xmodule_js/common_static/js/vendor/backbone-min",
......
define ["js/models/section", "js/common_helpers/ajax_helpers", "js/utils/module"], (Section, AjaxHelpers, ModuleUtils) ->
define ["js/models/section", "common/js/spec_helpers/ajax_helpers", "js/utils/module"], (Section, AjaxHelpers, ModuleUtils) ->
describe "Section", ->
describe "basic", ->
beforeEach ->
......
define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
define ["jquery", "jasmine", "common/js/spec_helpers/ajax_helpers", "squire"],
($, jasmine, AjaxHelpers, Squire) ->
feedbackTpl = readFixtures('system-feedback.underscore')
assetLibraryTpl = readFixtures('asset-library.underscore')
assetTpl = readFixtures('asset.underscore')
pagingHeaderTpl = readFixtures('paging-header.underscore')
pagingFooterTpl = readFixtures('paging-footer.underscore')
describe "Asset view", ->
beforeEach ->
......@@ -141,8 +139,6 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
beforeEach ->
setFixtures($("<script>", {id: "asset-library-tpl", type: "text/template"}).text(assetLibraryTpl))
appendSetFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl))
appendSetFixtures($("<script>", {id: "paging-header-tpl", type: "text/template"}).text(pagingHeaderTpl))
appendSetFixtures($("<script>", {id: "paging-footer-tpl", type: "text/template"}).text(pagingFooterTpl))
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
window.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy()
......@@ -241,7 +237,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
describe "Basic", ->
# Separate setup method to work-around mis-parenting of beforeEach methods
setup = (requests) ->
@view.setPage(0)
@view.pagingView.setPage(0)
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
$.fn.fileupload = ->
......@@ -285,7 +281,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
{view: @view, requests: requests} = @createAssetsView(this)
appendSetFixtures('<div class="ui-loading"/>')
expect($('.ui-loading').is(':visible')).toBe(true)
@view.setPage(0)
@view.pagingView.setPage(0)
AjaxHelpers.respondWithError(requests)
expect($('.ui-loading').is(':visible')).toBe(false)
......@@ -333,27 +329,27 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
describe "Sorting", ->
# Separate setup method to work-around mis-parenting of beforeEach methods
setup = (requests) ->
@view.setPage(0)
@view.pagingView.setPage(0)
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
it "should have the correct default sort order", ->
{view: @view, requests: requests} = @createAssetsView(this)
setup.call(this, requests)
expect(@view.sortDisplayName()).toBe("Date Added")
expect(@view.pagingView.sortDisplayName()).toBe("Date Added")
expect(@view.collection.sortDirection).toBe("desc")
it "should toggle the sort order when clicking on the currently sorted column", ->
{view: @view, requests: requests} = @createAssetsView(this)
setup.call(this, requests)
expect(@view.sortDisplayName()).toBe("Date Added")
expect(@view.pagingView.sortDisplayName()).toBe("Date Added")
expect(@view.collection.sortDirection).toBe("desc")
@view.$("#js-asset-date-col").click()
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
expect(@view.sortDisplayName()).toBe("Date Added")
expect(@view.pagingView.sortDisplayName()).toBe("Date Added")
expect(@view.collection.sortDirection).toBe("asc")
@view.$("#js-asset-date-col").click()
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
expect(@view.sortDisplayName()).toBe("Date Added")
expect(@view.pagingView.sortDisplayName()).toBe("Date Added")
expect(@view.collection.sortDirection).toBe("desc")
it "should switch the sort order when clicking on a different column", ->
......@@ -361,11 +357,11 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
setup.call(this, requests)
@view.$("#js-asset-name-col").click()
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
expect(@view.sortDisplayName()).toBe("Name")
expect(@view.pagingView.sortDisplayName()).toBe("Name")
expect(@view.collection.sortDirection).toBe("asc")
@view.$("#js-asset-name-col").click()
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
expect(@view.sortDisplayName()).toBe("Name")
expect(@view.pagingView.sortDisplayName()).toBe("Name")
expect(@view.collection.sortDirection).toBe("desc")
it "should switch sort to most recent date added when a new asset is added", ->
......@@ -375,5 +371,5 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
addMockAsset.call(this, requests)
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
expect(@view.sortDisplayName()).toBe("Date Added")
expect(@view.pagingView.sortDisplayName()).toBe("Date Added")
expect(@view.collection.sortDirection).toBe("desc")
define ["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info", "js/collections/course_update", "js/common_helpers/ajax_helpers"],
define ["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info", "js/collections/course_update", "common/js/spec_helpers/ajax_helpers"],
(CourseInfoHandoutsView, CourseInfoUpdateView, ModuleInfo, CourseUpdateCollection, AjaxHelpers) ->
describe "Course Updates and Handouts", ->
......
define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js/models/course",
"js/collections/textbook", "js/views/show_textbook", "js/views/edit_textbook", "js/views/list_textbooks",
"js/views/edit_chapter", "js/views/feedback_prompt", "js/views/feedback_notification", "js/views/utils/view_utils",
"js/common_helpers/ajax_helpers", "js/spec_helpers/modal_helpers", "jasmine-stealth"],
"common/js/spec_helpers/ajax_helpers", "js/spec_helpers/modal_helpers", "jasmine-stealth"],
(Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTextbooks, EditChapter, Prompt, Notification, ViewUtils, AjaxHelpers, modal_helpers) ->
feedbackTpl = readFixtures('system-feedback.underscore')
......
define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/common_helpers/ajax_helpers", "js/spec_helpers/modal_helpers"], (FileUpload, UploadDialog, Chapter, AjaxHelpers, modal_helpers) ->
define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "common/js/spec_helpers/ajax_helpers", "js/spec_helpers/modal_helpers"], (FileUpload, UploadDialog, Chapter, AjaxHelpers, modal_helpers) ->
feedbackTpl = readFixtures('system-feedback.underscore')
......
../../common/static/common
\ No newline at end of file
......@@ -15,15 +15,6 @@ domReady(function() {
$body.addClass('js');
// lean/simple modal
$('a[rel*=modal]').leanModal({
overlay: 0.80,
closeButton: '.action-modal-close'
});
$('a.action-modal-close').click(function(e) {
(e).preventDefault();
});
// alerts/notifications - manual close
$('.action-alert-close, .alert.has-actions .nav-actions a').bind('click', hideAlert);
$('.action-notification-close').bind('click', hideNotification);
......
......@@ -8,8 +8,8 @@ define([ // jshint ignore:line
'js/certificates/views/certificate_details',
'js/certificates/views/certificate_preview',
'js/views/feedback_notification',
'js/common_helpers/ajax_helpers',
'js/common_helpers/template_helpers',
'common/js/spec_helpers/ajax_helpers',
'common/js/spec_helpers/template_helpers',
'js/spec_helpers/view_helpers',
'js/spec_helpers/validation_helpers',
'js/certificates/spec/custom_matchers'
......
......@@ -8,8 +8,8 @@ define([ // jshint ignore:line
'js/certificates/collections/certificates',
'js/certificates/views/certificate_editor',
'js/views/feedback_notification',
'js/common_helpers/ajax_helpers',
'js/common_helpers/template_helpers',
'common/js/spec_helpers/ajax_helpers',
'common/js/spec_helpers/template_helpers',
'js/spec_helpers/view_helpers',
'js/spec_helpers/validation_helpers',
'js/certificates/spec/custom_matchers'
......
// Jasmine Test Suite: Certificate Web Preview
define([ // jshint ignore:line
'underscore',
'jquery',
'js/models/course',
'js/certificates/views/certificate_preview',
'common/js/spec_helpers/template_helpers',
'js/spec_helpers/view_helpers',
'common/js/spec_helpers/ajax_helpers'
],
function(_, $, Course, CertificatePreview, TemplateHelpers, ViewHelpers, AjaxHelpers) {
'use strict';
var SELECTORS = {
course_modes: '#course-modes',
activate_certificate: '.activate-cert',
preview_certificate: '.preview-certificate-link'
};
beforeEach(function() {
window.course = new Course({
id: '5',
name: 'Course Name',
url_name: 'course_name',
org: 'course_org',
num: 'course_num',
revision: 'course_rev'
});
});
afterEach(function() {
delete window.course;
});
describe('Certificate Web Preview Spec:', function() {
var selectDropDownByText = function ( element, value ) {
if (value) {
element.val(value);
element.trigger('change');
}
};
beforeEach(function() {
TemplateHelpers.installTemplate('certificate-web-preview', true);
appendSetFixtures('<div class="preview-certificate nav-actions"></div>');
this.view = new CertificatePreview({
el: $('.preview-certificate'),
course_modes: ['test1', 'test2', 'test3'],
certificate_web_view_url: '/users/1/courses/orgX/009/2016?preview=test1',
certificate_activation_handler_url: '/certificates/activation/'+ window.course.id,
is_active: true
});
appendSetFixtures(this.view.render().el);
});
describe('Certificate preview', function() {
it('course mode event should call when user choose a new mode', function () {
spyOn(this.view, 'courseModeChanged');
this.view.delegateEvents();
selectDropDownByText(this.view.$(SELECTORS.course_modes), 'test3');
expect(this.view.courseModeChanged).toHaveBeenCalled();
});
it('course mode selection updating the link successfully', function () {
selectDropDownByText(this.view.$(SELECTORS.course_modes), 'test1');
expect(this.view.$(SELECTORS.preview_certificate).attr('href')).
toEqual('/users/1/courses/orgX/009/2016?preview=test1');
selectDropDownByText(this.view.$(SELECTORS.course_modes), 'test2');
expect(this.view.$(SELECTORS.preview_certificate).attr('href')).
toEqual('/users/1/courses/orgX/009/2016?preview=test2');
selectDropDownByText(this.view.$(SELECTORS.course_modes), 'test3');
expect(this.view.$(SELECTORS.preview_certificate).attr('href')).
toEqual('/users/1/courses/orgX/009/2016?preview=test3');
});
it('toggle certificate activation event works fine', function () {
spyOn(this.view, 'toggleCertificateActivation');
this.view.delegateEvents();
this.view.$(SELECTORS.activate_certificate).click();
expect(this.view.toggleCertificateActivation).toHaveBeenCalled();
});
it('certificate deactivation works fine', function () {
var requests = AjaxHelpers.requests(this),
notificationSpy = ViewHelpers.createNotificationSpy();
this.view.$(SELECTORS.activate_certificate).click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/certificates/activation/'+ window.course.id, {
is_active: false
});
ViewHelpers.verifyNotificationShowing(notificationSpy, /Deactivating/);
});
it('certificate activation works fine', function () {
var requests = AjaxHelpers.requests(this),
notificationSpy = ViewHelpers.createNotificationSpy();
this.view.is_active = false;
this.view.$(SELECTORS.activate_certificate).click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/certificates/activation/'+ window.course.id, {
is_active: true
});
ViewHelpers.verifyNotificationShowing(notificationSpy, /Activating/);
});
it('certificate should be deactivate when method "remove" called', function () {
this.view.remove();
expect(this.view.is_active).toBe(false);
});
it('certificate web preview should be removed when method "remove" called', function () {
this.view.remove();
expect(this.view.el.innerHTML).toContain("");
});
it('method "show" should call the render function', function () {
spyOn(this.view, "render");
this.view.show();
expect(this.view.render).toHaveBeenCalled();
});
});
});
});
......@@ -11,8 +11,8 @@ define([ // jshint ignore:line
'js/certificates/views/certificates_list',
'js/certificates/views/certificate_preview',
'js/views/feedback_notification',
'js/common_helpers/ajax_helpers',
'js/common_helpers/template_helpers',
'common/js/spec_helpers/ajax_helpers',
'common/js/spec_helpers/template_helpers',
'js/certificates/spec/custom_matchers'
],
function(_, Course, CertificatesCollection, CertificateModel, CertificateDetailsView, CertificateEditorView,
......
......@@ -4,11 +4,10 @@ define([ // jshint ignore:line
'jquery',
'underscore',
'gettext',
'js/common_helpers/page_helpers',
'js/views/pages/base_page',
'js/certificates/views/certificates_list'
],
function ($, _, gettext, PageHelpers, BasePage, CertificatesListView) {
function ($, _, gettext, BasePage, CertificatesListView) {
'use strict';
var CertificatesPage = BasePage.extend({
......
../../../common/static/js/spec_helpers
\ No newline at end of file
define(['domReady!', 'jquery', 'backbone', 'underscore', 'gettext']);
define(['domReady!', 'jquery', 'backbone', 'underscore', 'gettext', 'text']);
define(['jquery', 'js/factories/xblock_validation', 'js/common_helpers/template_helpers'],
define(['jquery', 'js/factories/xblock_validation', 'common/js/spec_helpers/template_helpers'],
function($, XBlockValidationFactory, TemplateHelpers) {
describe('XBlockValidationFactory', function() {
......
define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/common_helpers/ajax_helpers", "jquery", "underscore"],
define(["js/utils/drag_and_drop", "js/views/feedback_notification", "common/js/spec_helpers/ajax_helpers", "jquery", "underscore"],
function (ContentDragger, Notification, AjaxHelpers, $, _) {
describe("Overview drag and drop functionality", function () {
beforeEach(function () {
......
define(
[
'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'squire'
'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'squire'
],
function ($, _, AjaxHelpers, Squire) {
'use strict';
......
define(
[
'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'squire'
'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'squire'
],
function ($, _, AjaxHelpers, Squire) {
'use strict';
......
define(
["jquery", "js/models/active_video_upload", "js/views/active_video_upload_list", "js/common_helpers/template_helpers", "mock-ajax", "jasmine-jquery"],
["jquery", "js/models/active_video_upload", "js/views/active_video_upload_list", "common/js/spec_helpers/template_helpers", "mock-ajax", "jasmine-jquery"],
function($, ActiveVideoUpload, ActiveVideoUploadListView, TemplateHelpers) {
"use strict";
var concurrentUploadLimit = 2;
......
define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/asset", "js/views/assets",
define([ "jquery", "common/js/spec_helpers/ajax_helpers", "URI", "js/views/asset", "js/views/assets",
"js/models/asset", "js/collections/asset", "js/spec_helpers/view_helpers"],
function ($, AjaxHelpers, URI, AssetView, AssetsView, AssetModel, AssetCollection, ViewHelpers) {
......@@ -8,15 +8,11 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/asset", "j
assetLibraryTpl = readFixtures('asset-library.underscore');
assetTpl = readFixtures('asset.underscore');
pagingHeaderTpl = readFixtures('paging-header.underscore');
pagingFooterTpl = readFixtures('paging-footer.underscore');
uploadModalTpl = readFixtures('asset-upload-modal.underscore');
beforeEach(function () {
setFixtures($("<script>", { id: "asset-library-tpl", type: "text/template" }).text(assetLibraryTpl));
appendSetFixtures($("<script>", { id: "asset-tpl", type: "text/template" }).text(assetTpl));
appendSetFixtures($("<script>", { id: "paging-header-tpl", type: "text/template" }).text(pagingHeaderTpl));
appendSetFixtures($("<script>", { id: "paging-footer-tpl", type: "text/template" }).text(pagingFooterTpl));
appendSetFixtures(uploadModalTpl);
appendSetFixtures(sandbox({ id: "asset_table_body" }));
......@@ -139,7 +135,7 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/asset", "j
var setup;
setup = function(responseData) {
var requests = AjaxHelpers.requests(this);
assetsView.setPage(0);
assetsView.pagingView.setPage(0);
if (!responseData){
AjaxHelpers.respondWithJson(requests, mockEmptyAssetsResponse);
}
......@@ -188,8 +184,8 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/asset", "j
expect(assetsView).toBeDefined();
spyOn(assetsView, "addAsset").andCallFake(function () {
assetsView.collection.add(mockAssetUploadResponse.asset);
assetsView.renderPageItems();
assetsView.setPage(0);
assetsView.pagingView.renderPageItems();
assetsView.pagingView.setPage(0);
});
$('a:contains("Upload your first asset")').click();
......@@ -248,9 +244,9 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/asset", "j
});
it('returns the registered info for a filter column', function () {
assetsView.registerSortableColumn('test-col', 'Test Column', 'testField', 'asc');
assetsView.registerFilterableColumn('js-asset-type-col', 'Type', 'asset_type');
var filterInfo = assetsView.filterableColumnInfo('js-asset-type-col');
assetsView.pagingView.registerSortableColumn('test-col', 'Test Column', 'testField', 'asc');
assetsView.pagingView.registerFilterableColumn('js-asset-type-col', 'Type', 'asset_type');
var filterInfo = assetsView.pagingView.filterableColumnInfo('js-asset-type-col');
expect(filterInfo.displayName).toBe('Type');
expect(filterInfo.fieldName).toBe('asset_type');
});
......@@ -265,16 +261,16 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/asset", "j
it('make sure selectFilter sets collection filter if undefined', function () {
expect(assetsView).toBeDefined();
assetsView.collection.filterField = '';
assetsView.selectFilter('js-asset-type-col');
assetsView.pagingView.selectFilter('js-asset-type-col');
expect(assetsView.collection.filterField).toEqual('asset_type');
});
it('make sure _toggleFilterColumn filters asset list', function () {
expect(assetsView).toBeDefined();
var requests = AjaxHelpers.requests(this);
$.each(assetsView.filterableColumns, function(columnID, columnData){
$.each(assetsView.pagingView.filterableColumns, function(columnID, columnData){
var $typeColumn = $('#' + columnID);
assetsView.setPage(0);
assetsView.pagingView.setPage(0);
respondWithMockAssets(requests);
var assetsNumber = assetsView.collection.length;
assetsView._toggleFilterColumn('Images', 'Images');
......@@ -288,7 +284,7 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/asset", "j
it('opens and closes select type menu', function () {
expect(assetsView).toBeDefined();
setup.call(this, mockExampleAssetsResponse);
$.each(assetsView.filterableColumns, function(columnID, columnData){
$.each(assetsView.pagingView.filterableColumns, function(columnID, columnData){
var $typeColumn = $('#' + columnID);
expect($typeColumn).toBeVisible();
var assetsNumber = $('#asset-table-body .type-col').length;
......@@ -304,12 +300,12 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/asset", "j
it('check filtering works with sorting by column on', function () {
expect(assetsView).toBeDefined();
var requests = AjaxHelpers.requests(this);
assetsView.registerSortableColumn('name-col', 'Name Column', 'nameField', 'asc');
assetsView.registerFilterableColumn('js-asset-type-col', gettext('Type'), 'asset_type');
assetsView.setInitialSortColumn('name-col');
assetsView.setPage(0);
assetsView.pagingView.registerSortableColumn('name-col', 'Name Column', 'nameField', 'asc');
assetsView.pagingView.registerFilterableColumn('js-asset-type-col', gettext('Type'), 'asset_type');
assetsView.pagingView.setInitialSortColumn('name-col');
assetsView.pagingView.setPage(0);
respondWithMockAssets(requests);
var sortInfo = assetsView.sortableColumnInfo('name-col');
var sortInfo = assetsView.pagingView.sortableColumnInfo('name-col');
expect(sortInfo.defaultSortDirection).toBe('asc');
var $firstFilter = $($('#js-asset-type-col').find('li.nav-item a')[1]);
$firstFilter.trigger('click');
......@@ -322,8 +318,8 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/asset", "j
it('shows type select menu, selects type, and filters results', function () {
expect(assetsView).toBeDefined();
var requests = AjaxHelpers.requests(this);
$.each(assetsView.filterableColumns, function(columnID, columnData) {
assetsView.setPage(0);
$.each(assetsView.pagingView.filterableColumns, function(columnID, columnData) {
assetsView.pagingView.setPage(0);
respondWithMockAssets(requests);
var $typeColumn = $('#' + columnID);
expect($typeColumn).toBeVisible();
......
define([ "jquery", "js/common_helpers/ajax_helpers", "js/spec_helpers/edit_helpers",
define([ "jquery", "common/js/spec_helpers/ajax_helpers", "js/spec_helpers/edit_helpers",
"js/views/container", "js/models/xblock_info", "jquery.simulate",
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
function ($, AjaxHelpers, EditHelpers, ContainerView, XBlockInfo) {
......
......@@ -4,7 +4,7 @@ define([
'js/views/group_configuration_details', 'js/views/group_configurations_list', 'js/views/group_configuration_editor',
'js/views/group_configuration_item', 'js/views/experiment_group_edit', 'js/views/content_group_list',
'js/views/content_group_details', 'js/views/content_group_editor', 'js/views/content_group_item',
'js/views/feedback_notification', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
'js/views/feedback_notification', 'common/js/spec_helpers/ajax_helpers', 'common/js/spec_helpers/template_helpers',
'js/spec_helpers/view_helpers', 'jasmine-stealth'
], function(
_, Course, GroupConfigurationModel, GroupModel, GroupConfigurationCollection, GroupCollection,
......
define(["js/views/license", "js/models/license", "js/common_helpers/template_helpers"],
define(["js/views/license", "js/models/license", "common/js/spec_helpers/template_helpers"],
function(LicenseView, LicenseModel, TemplateHelpers) {
describe("License view", function() {
......
define(["jquery", "underscore", "js/common_helpers/ajax_helpers", "js/spec_helpers/edit_helpers",
define(["jquery", "underscore", "common/js/spec_helpers/ajax_helpers", "js/spec_helpers/edit_helpers",
"js/views/modals/edit_xblock", "js/models/xblock_info"],
function ($, _, AjaxHelpers, EditHelpers, EditXBlockModal, XBlockInfo) {
......
define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/models/xblock_info",
"js/views/paged_container", "js/views/paging_header", "js/views/paging_footer", "js/views/xblock"],
define(["jquery", "underscore", "common/js/spec_helpers/ajax_helpers", "URI", "js/models/xblock_info",
"js/views/paged_container", "common/js/components/views/paging_header",
"common/js/components/views/paging_footer", "js/views/xblock"],
function ($, _, AjaxHelpers, URI, XBlockInfo, PagedContainer, PagingHeader, PagingFooter, XBlockView) {
var htmlResponseTpl = _.template('' +
......@@ -175,11 +176,6 @@ define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/mo
});
describe("PagingHeader", function () {
beforeEach(function () {
var pagingFooterTpl = readFixtures('paging-header.underscore');
appendSetFixtures($("<script>", { id: "paging-header-tpl", type: "text/template" }).text(pagingFooterTpl));
});
describe("Next page button", function () {
beforeEach(function () {
pagingContainer.render();
......@@ -331,11 +327,6 @@ define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/mo
});
describe("PagingFooter", function () {
beforeEach(function () {
var pagingFooterTpl = readFixtures('paging-footer.underscore');
appendSetFixtures($("<script>", { id: "paging-footer-tpl", type: "text/template" }).text(pagingFooterTpl));
});
describe("Next page button", function () {
beforeEach(function () {
// Render the page and header so that they can react to events
......
define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_helpers",
"js/common_helpers/template_helpers", "js/spec_helpers/edit_helpers",
define(["jquery", "underscore", "underscore.string", "common/js/spec_helpers/ajax_helpers",
"common/js/spec_helpers/template_helpers", "js/spec_helpers/edit_helpers",
"js/views/pages/container", "js/views/pages/paged_container", "js/models/xblock_info", "jquery.simulate"],
function ($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, ContainerPage, PagedContainerPage, XBlockInfo) {
......
define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_helpers",
"js/common_helpers/template_helpers", "js/spec_helpers/edit_helpers",
define(["jquery", "underscore", "underscore.string", "common/js/spec_helpers/ajax_helpers",
"common/js/spec_helpers/template_helpers", "js/spec_helpers/edit_helpers",
"js/views/feedback_prompt", "js/views/pages/container", "js/views/pages/container_subviews",
"js/models/xblock_info", "js/views/utils/xblock_utils"],
function ($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, Prompt, ContainerPage, ContainerSubviews,
......
define(["jquery", "sinon", "js/common_helpers/ajax_helpers", "js/views/utils/view_utils", "js/views/pages/course_outline",
define(["jquery", "sinon", "common/js/spec_helpers/ajax_helpers", "js/views/utils/view_utils", "js/views/pages/course_outline",
"js/models/xblock_outline_info", "js/utils/date_utils", "js/spec_helpers/edit_helpers",
"js/common_helpers/template_helpers"],
"common/js/spec_helpers/template_helpers"],
function($, Sinon, AjaxHelpers, ViewUtils, CourseOutlinePage, XBlockOutlineInfo, DateUtils, EditHelpers, TemplateHelpers) {
describe("CourseOutlinePage", function() {
......
define(["jquery", "js/common_helpers/ajax_helpers", "js/spec_helpers/view_helpers", "js/views/course_rerun",
define(["jquery", "common/js/spec_helpers/ajax_helpers", "js/spec_helpers/view_helpers", "js/views/course_rerun",
"js/views/utils/create_course_utils", "js/views/utils/view_utils", "jquery.simulate"],
function ($, AjaxHelpers, ViewHelpers, CourseRerunUtils, CreateCourseUtilsFactory, ViewUtils) {
describe("Create course rerun page", function () {
......
define([
'jquery', 'underscore', 'js/views/pages/group_configurations',
'js/models/group_configuration', 'js/collections/group_configuration',
'js/common_helpers/template_helpers'
'common/js/spec_helpers/template_helpers'
], function ($, _, GroupConfigurationsPage, GroupConfigurationModel, GroupConfigurationCollection, TemplateHelpers) {
'use strict';
describe('GroupConfigurationsPage', function() {
......
define(["jquery", "js/common_helpers/ajax_helpers", "js/spec_helpers/view_helpers", "js/index",
define(["jquery", "common/js/spec_helpers/ajax_helpers", "js/spec_helpers/view_helpers", "js/index",
"js/views/utils/view_utils"],
function ($, AjaxHelpers, ViewHelpers, IndexUtils, ViewUtils) {
describe("Course listing page", function () {
......
define([
"jquery", "js/common_helpers/ajax_helpers", "js/spec_helpers/view_helpers",
"jquery", "common/js/spec_helpers/ajax_helpers", "js/spec_helpers/view_helpers",
"js/factories/manage_users_lib", "js/views/utils/view_utils"
],
function ($, AjaxHelpers, ViewHelpers, ManageUsersFactory, ViewUtils) {
......
define(
["jquery", "underscore", "backbone", "js/views/previous_video_upload_list", "js/common_helpers/template_helpers"],
["jquery", "underscore", "backbone", "js/views/previous_video_upload_list", "common/js/spec_helpers/template_helpers"],
function($, _, Backbone, PreviousVideoUploadListView, TemplateHelpers) {
"use strict";
describe("PreviousVideoUploadListView", function() {
......
define(
["jquery", "backbone", "js/views/previous_video_upload", "js/common_helpers/template_helpers"],
["jquery", "backbone", "js/views/previous_video_upload", "common/js/spec_helpers/template_helpers"],
function($, Backbone, PreviousVideoUploadView, TemplateHelpers) {
"use strict";
describe("PreviousVideoUploadView", function() {
......
define([
'jquery', 'js/models/settings/course_details', 'js/views/settings/main',
'js/common_helpers/ajax_helpers'
'common/js/spec_helpers/ajax_helpers'
], function($, CourseDetailsModel, MainView, AjaxHelpers) {
'use strict';
......
define(["jquery", "js/common_helpers/ajax_helpers", "js/common_helpers/template_helpers",
define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers/template_helpers",
"js/spec_helpers/view_helpers", "js/views/utils/view_utils", "js/views/unit_outline", "js/models/xblock_info"],
function ($, AjaxHelpers, TemplateHelpers, ViewHelpers, ViewUtils, UnitOutlineView, XBlockInfo) {
......
define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "js/spec_helpers/edit_helpers",
define([ "jquery", "underscore", "common/js/spec_helpers/ajax_helpers", "js/spec_helpers/edit_helpers",
"js/views/xblock_editor", "js/models/xblock_info"],
function ($, _, AjaxHelpers, EditHelpers, XBlockEditorView, XBlockInfo) {
......
define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/xblock", "js/models/xblock_info",
define([ "jquery", "common/js/spec_helpers/ajax_helpers", "URI", "js/views/xblock", "js/models/xblock_info",
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
function ($, AjaxHelpers, URI, XBlockView, XBlockInfo) {
......
define(["jquery", "js/common_helpers/ajax_helpers", "js/common_helpers/template_helpers",
define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers/template_helpers",
"js/spec_helpers/edit_helpers", "js/models/xblock_info", "js/views/xblock_string_field_editor"],
function ($, AjaxHelpers, TemplateHelpers, EditHelpers, XBlockInfo, XBlockStringFieldEditor) {
describe("XBlockStringFieldEditorView", function () {
......
define(['jquery', 'js/models/xblock_validation', 'js/views/xblock_validation', 'js/common_helpers/template_helpers'],
define(['jquery', 'js/models/xblock_validation', 'js/views/xblock_validation', 'common/js/spec_helpers/template_helpers'],
function($, XBlockValidationModel, XBlockValidationView, TemplateHelpers) {
beforeEach(function () {
......
/**
* Provides helper methods for invoking Studio modal windows in Jasmine tests.
*/
define(["jquery", "js/views/feedback_notification", "js/views/feedback_prompt", "js/common_helpers/template_helpers"],
define(["jquery", "js/views/feedback_notification", "js/views/feedback_prompt", "common/js/spec_helpers/template_helpers"],
function($, NotificationView, Prompt, TemplateHelpers) {
var installViewTemplates, createFeedbackSpy, verifyFeedbackShowing,
verifyFeedbackHidden, createNotificationSpy, verifyNotificationShowing,
......
/**
* Provides helper methods for invoking Studio editors in Jasmine tests.
*/
define(["jquery", "underscore", "js/common_helpers/ajax_helpers", "js/common_helpers/template_helpers",
define(["jquery", "underscore", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers/template_helpers",
"js/spec_helpers/modal_helpers", "js/views/modals/edit_xblock", "js/collections/component_template",
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
function($, _, AjaxHelpers, TemplateHelpers, modal_helpers, EditXBlockModal, ComponentTemplates) {
......
/**
* Provides helper methods for invoking Studio modal windows in Jasmine tests.
*/
define(["jquery", "js/common_helpers/template_helpers", "js/spec_helpers/view_helpers"],
define(["jquery", "common/js/spec_helpers/template_helpers", "js/spec_helpers/view_helpers"],
function($, TemplateHelpers, ViewHelpers) {
var installModalTemplates, getModalElement, getModalTitle, isShowingModal, hideModalIfShowing,
pressModalButton, cancelModal, cancelModalIfShowing;
......
/**
* Provides helper methods for invoking Validation modal in Jasmine tests.
*/
define(['jquery', 'js/spec_helpers/modal_helpers', 'js/common_helpers/template_helpers'],
define(['jquery', 'js/spec_helpers/modal_helpers', 'common/js/spec_helpers/template_helpers'],
function($, ModalHelpers, TemplateHelpers) {
var installValidationTemplates, checkErrorContents, undoChanges;
......
/**
* Provides helper methods for invoking Studio modal windows in Jasmine tests.
*/
define(["jquery", "js/views/feedback_notification", "js/views/feedback_prompt", 'js/common_helpers/ajax_helpers',
"js/common_helpers/template_helpers"],
define(["jquery", "js/views/feedback_notification", "js/views/feedback_prompt", 'common/js/spec_helpers/ajax_helpers',
"common/js/spec_helpers/template_helpers"],
function($, NotificationView, Prompt, AjaxHelpers, TemplateHelpers) {
var installViewTemplates, createFeedbackSpy, verifyFeedbackShowing,
verifyFeedbackHidden, createNotificationSpy, verifyNotificationShowing,
......
define(["jquery", "underscore", "js/views/utils/view_utils", "js/views/container", "js/utils/module", "gettext",
"js/views/feedback_notification", "js/views/paging_header", "js/views/paging_footer", "js/views/paging_mixin"],
"js/views/feedback_notification", "common/js/components/views/paging_header",
"common/js/components/views/paging_footer", "common/js/components/views/paging_mixin"],
function ($, _, ViewUtils, ContainerView, ModuleUtils, gettext, NotificationView, PagingHeader, PagingFooter, PagingMixin) {
var PagedContainerView = ContainerView.extend(PagingMixin).extend({
initialize: function(options){
......
......@@ -70,14 +70,15 @@ lib_paths:
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-process.js
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-validate.js
- xmodule_js/common_static/js/vendor/mock-ajax.js
- xmodule_js/common_static/js/vendor/requirejs/text.js
# Paths to source JavaScript files
src_paths:
- coffee/src
- js
- js/common_helpers
- js/factories
- js/certificates
- js/factories
- common/js
# Paths to spec (test) JavaScript files
# We should define the custom path mapping in /coffee/spec/main.coffee as well e.g. certificates etc.
......@@ -98,6 +99,8 @@ spec_paths:
#
fixture_paths:
- coffee/fixtures
- templates
- common/templates
requirejs:
paths:
......
......@@ -62,12 +62,13 @@ lib_paths:
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload.js
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-process.js
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-validate.js
- xmodule_js/common_static/js/vendor/requirejs/text.js
# Paths to source JavaScript files
src_paths:
- coffee/src
- js
- js/common_helpers
- common/js
# Paths to spec (test) JavaScript files
spec_paths:
......@@ -86,6 +87,8 @@ spec_paths:
#
fixture_paths:
- coffee/fixtures
- templates
- common/templates
requirejs:
paths:
......
......@@ -27,6 +27,7 @@ require.config({
"jquery.immediateDescendents": "coffee/src/jquery.immediateDescendents",
"datepair": "js/vendor/timepicker/datepair",
"date": "js/vendor/date",
"text": 'js/vendor/requirejs/text',
"moment": "js/vendor/moment.min",
"underscore": "js/vendor/underscore-min",
"underscore.string": "js/vendor/underscore.string.min",
......
......@@ -666,67 +666,63 @@ hr.divider {
}
}
// +JS Dependent
// ====================
body.js {
// lean/simple modal window
.content-modal {
@include border-bottom-radius(2px);
@include box-sizing(border-box);
box-shadow: 0 2px 4px $shadow-d1;
position: relative;
display: none;
width: 700px;
overflow: hidden;
border: 1px solid $gray-d1;
padding: ($baseline);
background: $white;
.action-modal-close {
@include transition(top $tmg-f3 ease-in-out 0s);
@include border-bottom-radius(3px);
position: absolute;
top: -3px;
right: $baseline;
padding: ($baseline/4) ($baseline/2) 0 ($baseline/2);
background: $gray-l3;
text-align: center;
.label {
@extend %cont-text-sr;
}
.icon {
@extend %t-action1;
color: $white;
}
// lean/simple modal window
.content-modal {
@include border-bottom-radius(2px);
@include box-sizing(border-box);
position: relative;
display: none;
width: 700px;
padding: ($baseline);
border: 1px solid $gray-d1;
background: $white;
box-shadow: 0 2px 4px $shadow-d1;
overflow: hidden;
&:hover {
background: $blue;
top: 0;
}
.action-modal-close {
@include transition(top $tmg-f3 ease-in-out 0s);
@include border-bottom-radius(3px);
position: absolute;
top: -3px;
right: $baseline;
padding: ($baseline/4) ($baseline/2) 0 ($baseline/2);
background: $gray-l3;
text-align: center;
.label {
@extend %cont-text-sr;
}
img {
@include box-sizing(border-box);
width: 100%;
overflow-y: scroll;
padding: ($baseline/10);
border: 1px solid $gray-l4;
.icon {
@extend %t-action1;
color: $white;
}
.title {
@extend %t-title5;
@extend %t-strong;
margin: 0 0 ($baseline/2) 0;
color: $gray-d3;
&:hover {
top: 0;
background: $blue;
}
}
.description {
@extend %t-copy-sub2;
margin-top: ($baseline/2);
color: $gray-l1;
}
img {
@include box-sizing(border-box);
width: 100%;
overflow-y: scroll;
padding: ($baseline/10);
border: 1px solid $gray-l4;
}
.title {
@extend %t-title5;
@extend %t-strong;
margin: 0 0 ($baseline/2) 0;
color: $gray-d3;
}
.description {
@extend %t-copy-sub2;
margin-top: ($baseline/2);
color: $gray-l1;
}
}
../templates/js
\ No newline at end of file
......@@ -10,7 +10,7 @@
<%namespace name='static' file='static_content.html'/>
<%block name="header_extras">
% for template_name in ["asset-library", "asset", "paging-header", "paging-footer"]:
% for template_name in ["asset-library", "asset"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
......
......@@ -24,6 +24,9 @@ from django.utils.translation import ugettext as _
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
<script type="text/template" id="image-modal-tpl">
<%static:include path="common/templates/image-modal.underscore" />
</script>
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
</%block>
......
......@@ -4,13 +4,13 @@
<a class="link-tab" href="#tab1"><%= gettext("Common Problem Types") %></a>
</li>
<li>
<a class="link-tab" href="#tab2"><%= gettext("Advanced") %></a>
<a class="link-tab" href="#tab3"><%= gettext("Advanced") %></a>
</li>
</ul>
<div class="tab current" id="tab1">
<ul class="new-component-template">
<% for (var i = 0; i < templates.length; i++) { %>
<% if (templates[i].is_common) { %>
<% if (templates[i].tab == "common") { %>
<% if (!templates[i].boilerplate_name) { %>
<li class="editor-md empty">
<a href="#" data-category="<%= templates[i].category %>">
......@@ -32,7 +32,21 @@
<div class="tab" id="tab2">
<ul class="new-component-template">
<% for (var i = 0; i < templates.length; i++) { %>
<% if (!templates[i].is_common) { %>
<% if (templates[i].tab == "hint") { %>
<li class="editor-manual">
<a href="#" data-category="<%= templates[i].category %>"
data-boilerplate="<%= templates[i].boilerplate_name %>">
<span class="name"><%= templates[i].display_name %></span>
</a>
</li>
<% } %>
<% } %>
</ul>
</div>
<div class="tab" id="tab3">
<ul class="new-component-template">
<% for (var i = 0; i < templates.length; i++) { %>
<% if (templates[i].tab == "advanced") { %>
<li class="editor-manual">
<a href="#" data-category="<%= templates[i].category %>"
data-boilerplate="<%= templates[i].boilerplate_name %>">
......
......@@ -13,39 +13,6 @@
<div class="xblock" data-locator="locator-container" data-request-token="page-render-token"
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
<script type="text/template" id="paging-header-tpl">
<div class="meta-wrap">
<div class="meta">
<%= messageHtml %>
</div>
<nav class="pagination pagination-compact top">
<ol>
<li class="nav-item previous"><a class="nav-link previous-page-link" href="#"><i class="icon fa fa-angle-left"></i> <span class="nav-label">Previous</span></a></li>
<li class="nav-item next"><a class="nav-link next-page-link" href="#"><span class="nav-label">Next</span> <i class="icon fa fa-angle-right"></i></a></li>
</ol>
</nav>
</div>
</script>
<script type="text/template" id="paging-footer-tpl">
<nav class="pagination pagination-full bottom">
<ol>
<li class="nav-item previous"><a class="nav-link previous-page-link" href="#"><i class="icon fa fa-angle-left"></i> <span class="nav-label">Previous</span></a></li>
<li class="nav-item page">
<div class="pagination-form">
<label class="page-number-label" for="page-number">Page number</label>
<input id="page-number-input" class="page-number-input" name="page-number" type="text" size="4" />
</div>
<span class="current-page"><%= current_page + 1 %></span>
<span class="page-divider">/</span>
<span class="total-pages"><%= total_pages %></span>
</li>
<li class="nav-item next"><a class="nav-link next-page-link" href="#"><span class="nav-label">Next</span> <i class="icon fa fa-angle-right"></i></a></li>
</ol>
</nav>
</script>
<div class="container-paging-header"></div>
<div class="studio-xblock-wrapper" data-locator="locator-group-A">
......
......@@ -13,39 +13,6 @@
<div class="xblock" data-locator="locator-container" data-request-token="page-render-token"
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
<script type="text/template" id="paging-header-tpl">
<div class="meta-wrap">
<div class="meta">
<%= messageHtml %>
</div>
<nav class="pagination pagination-compact top">
<ol>
<li class="nav-item previous"><a class="nav-link previous-page-link" href="#"><i class="icon fa fa-angle-left"></i> <span class="nav-label">Previous</span></a></li>
<li class="nav-item next"><a class="nav-link next-page-link" href="#"><span class="nav-label">Next</span> <i class="icon fa fa-angle-right"></i></a></li>
</ol>
</nav>
</div>
</script>
<script type="text/template" id="paging-footer-tpl">
<nav class="pagination pagination-full bottom">
<ol>
<li class="nav-item previous"><a class="nav-link previous-page-link" href="#"><i class="icon fa fa-angle-left"></i> <span class="nav-label">Previous</span></a></li>
<li class="nav-item page">
<div class="pagination-form">
<label class="page-number-label" for="page-number">Page number</label>
<input id="page-number-input" class="page-number-input" name="page-number" type="text" size="4" />
</div>
<span class="current-page"><%= current_page + 1 %></span>
<span class="page-divider">/</span>
<span class="total-pages"><%= total_pages %></span>
</li>
<li class="nav-item next"><a class="nav-link next-page-link" href="#"><span class="nav-label">Next</span> <i class="icon fa fa-angle-right"></i></a></li>
</ol>
</nav>
</script>
<div class="container-paging-header"></div>
<div class="studio-xblock-wrapper" data-locator="locator-group-A">
......
......@@ -17,6 +17,9 @@ from django.utils.translation import ugettext as _
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
<script type="text/template" id="image-modal-tpl">
<%static:include path="common/templates/image-modal.underscore" />
</script>
</%block>
<%block name="requirejs">
......
......@@ -10,6 +10,7 @@ from provider.oauth2.forms import ScopeChoiceField, ScopeMixin
from provider.oauth2.models import Client
from requests import HTTPError
from social.backends import oauth as social_oauth
from social.exceptions import AuthException
from third_party_auth import pipeline
......@@ -54,7 +55,7 @@ class AccessTokenExchangeForm(ScopeMixin, OAuthForm):
if self._errors:
return {}
backend = self.request.social_strategy.backend
backend = self.request.backend
if not isinstance(backend, social_oauth.BaseOAuth2):
raise OAuthValidationError(
{
......@@ -88,8 +89,8 @@ class AccessTokenExchangeForm(ScopeMixin, OAuthForm):
user = None
try:
user = backend.do_auth(self.cleaned_data.get("access_token"))
except HTTPError:
user = backend.do_auth(self.cleaned_data.get("access_token"), allow_inactive_user=True)
except (HTTPError, AuthException):
pass
if user and isinstance(user, User):
self.cleaned_data["user"] = user
......
......@@ -24,8 +24,11 @@ class AccessTokenExchangeFormTest(AccessTokenExchangeTestMixin):
def setUp(self):
super(AccessTokenExchangeFormTest, self).setUp()
self.request = RequestFactory().post("dummy_url")
redirect_uri = 'dummy_redirect_url'
SessionMiddleware().process_request(self.request)
self.request.social_strategy = social_utils.load_strategy(self.request, self.BACKEND)
self.request.social_strategy = social_utils.load_strategy(self.request)
# pylint: disable=no-member
self.request.backend = social_utils.load_backend(self.request.social_strategy, self.BACKEND, redirect_uri)
def _assert_error(self, data, expected_error, expected_error_description):
form = AccessTokenExchangeForm(request=self.request, data=data)
......
"""
The Python API layer of the Course About API. Essentially the middle tier of the project, responsible for all
business logic that is not directly tied to the data itself.
Data access is managed through the configured data module, or defaults to the project's data.py module.
This API is exposed via the RESTful layer (views.py) but may be used directly in-process.
"""
import logging
from django.conf import settings
from django.utils import importlib
from django.core.cache import cache
from course_about import errors
DEFAULT_DATA_API = 'course_about.data'
COURSE_ABOUT_API_CACHE_PREFIX = 'course_about_api_'
log = logging.getLogger(__name__)
def get_course_about_details(course_id):
"""Get course about details for the given course ID.
Given a Course ID, retrieve all the metadata necessary to fully describe the Course.
First its checks the default cache for given course id if its exists then returns
the course otherwise it get the course from module store and set the cache.
By default cache expiry set to 5 minutes.
Args:
course_id (str): The String representation of a Course ID. Used to look up the requested
course.
Returns:
A JSON serializable dictionary of metadata describing the course.
Example:
>>> get_course_about_details('edX/Demo/2014T2')
{
"advertised_start": "FALL",
"announcement": "YYYY-MM-DD",
"course_id": "edx/DemoCourse",
"course_number": "DEMO101",
"start": "YYYY-MM-DD",
"end": "YYYY-MM-DD",
"effort": "HH:MM",
"display_name": "Demo Course",
"is_new": true,
"media": {
"course_image": "/some/image/location.png"
},
}
"""
cache_key = "{}_{}".format(course_id, COURSE_ABOUT_API_CACHE_PREFIX)
cache_course_info = cache.get(cache_key)
if cache_course_info:
return cache_course_info
course_info = _data_api().get_course_about_details(course_id)
time_out = getattr(settings, 'COURSE_INFO_API_CACHE_TIME_OUT', 300)
cache.set(cache_key, course_info, time_out)
return course_info
def _data_api():
"""Returns a Data API.
This relies on Django settings to find the appropriate data API.
We retrieve the settings in-line here (rather than using the
top-level constant), so that @override_settings will work
in the test suite.
"""
api_path = getattr(settings, "COURSE_ABOUT_DATA_API", DEFAULT_DATA_API)
try:
return importlib.import_module(api_path)
except (ImportError, ValueError):
log.exception(u"Could not load module at '{path}'".format(path=api_path))
raise errors.CourseAboutApiLoadError(api_path)
"""Data Aggregation Layer for the Course About API.
This is responsible for combining data from the following resources:
* CourseDescriptor
* CourseAboutDescriptor
"""
import logging
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from course_about.serializers import serialize_content
from course_about.errors import CourseNotFoundError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
log = logging.getLogger(__name__)
ABOUT_ATTRIBUTES = [
'effort',
'overview',
'title',
'university',
'number',
'short_description',
'description',
'key_dates',
'video',
'course_staff_short',
'course_staff_extended',
'requirements',
'syllabus',
'textbook',
'faq',
'more_info',
'ocw_links',
]
def get_course_about_details(course_id): # pylint: disable=unused-argument
"""
Return course information for a given course id.
Args:
course_id(str) : The course id to retrieve course information for.
Returns:
Serializable dictionary of the Course About Information.
Raises:
CourseNotFoundError
"""
try:
course_key = CourseKey.from_string(course_id)
course_descriptor = modulestore().get_course(course_key)
if course_descriptor is None:
raise CourseNotFoundError("course not found")
except InvalidKeyError as err:
raise CourseNotFoundError(err.message)
about_descriptor = {
attribute: _fetch_course_detail(course_key, attribute)
for attribute in ABOUT_ATTRIBUTES
}
course_info = serialize_content(course_descriptor=course_descriptor, about_descriptor=about_descriptor)
return course_info
def _fetch_course_detail(course_key, attribute):
"""
Fetch the course about attribute for the given course's attribute from persistence and return its value.
"""
usage_key = course_key.make_usage_key('about', attribute)
try:
value = modulestore().get_item(usage_key).data
except ItemNotFoundError:
value = None
return value
"""
Contains all the errors associated with the Course About API.
"""
class CourseAboutError(Exception):
"""Generic Course About Error"""
def __init__(self, msg, data=None):
super(CourseAboutError, self).__init__(msg)
# Corresponding information to help resolve the error.
self.data = data
class CourseAboutApiLoadError(CourseAboutError):
"""The data API could not be loaded. """
pass
class CourseNotFoundError(CourseAboutError):
"""The Course Not Found. """
pass
"""
A models.py is required to make this an app (until we move to Django 1.7)
The Course About API is responsible for aggregating descriptive course information into a single response.
This should eventually hold some initial Marketing Meta Data objects that are platform-specific.
"""
"""
Serializers for all Course Descriptor and Course About Descriptor related return objects.
"""
from xmodule.contentstore.content import StaticContent
from django.conf import settings
DATE_FORMAT = getattr(settings, 'API_DATE_FORMAT', '%Y-%m-%d')
def serialize_content(course_descriptor, about_descriptor):
"""
Returns a serialized representation of the course_descriptor and about_descriptor
Args:
course_descriptor(CourseDescriptor) : course descriptor object
about_descriptor(dict) : Dictionary of CourseAboutDescriptor objects
return:
serialize data for course information.
"""
data = {
'media': {},
'display_name': getattr(course_descriptor, 'display_name', None),
'course_number': course_descriptor.location.course,
'course_id': None,
'advertised_start': getattr(course_descriptor, 'advertised_start', None),
'is_new': getattr(course_descriptor, 'is_new', None),
'start': _formatted_datetime(course_descriptor, 'start'),
'end': _formatted_datetime(course_descriptor, 'end'),
'announcement': None,
}
data.update(about_descriptor)
content_id = unicode(course_descriptor.id)
data["course_id"] = unicode(content_id)
if getattr(course_descriptor, 'course_image', False):
data['media']['course_image'] = course_image_url(course_descriptor)
announcement = getattr(course_descriptor, 'announcement', None)
data["announcement"] = announcement.strftime(DATE_FORMAT) if announcement else None
return data
def course_image_url(course):
"""
Return url of course image.
Args:
course(CourseDescriptor) : The course id to retrieve course image url.
Returns:
Absolute url of course image.
"""
loc = StaticContent.compute_location(course.id, course.course_image)
url = StaticContent.serialize_asset_key_with_slash(loc)
return url
def _formatted_datetime(course_descriptor, date_type):
"""
Return formatted date.
Args:
course_descriptor(CourseDescriptor) : The CourseDescriptor Object.
date_type (str) : Either start or end.
Returns:
formatted date or None .
"""
course_date_ = getattr(course_descriptor, date_type, None)
return course_date_.strftime(DATE_FORMAT) if course_date_ else None
"""
Tests the logical Python API layer of the Course About API.
"""
import ddt
import json
import unittest
from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from django.conf import settings
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, CourseAboutFactory
from student.tests.factories import UserFactory
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class CourseInfoTest(ModuleStoreTestCase, APITestCase):
"""
Test course information.
"""
USERNAME = "Bob"
EMAIL = "bob@example.com"
PASSWORD = "edx"
def setUp(self):
""" Create a course"""
super(CourseInfoTest, self).setUp()
self.course = CourseFactory.create()
self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
self.client.login(username=self.USERNAME, password=self.PASSWORD)
def test_get_course_details_from_cache(self):
kwargs = dict()
kwargs["course_id"] = self.course.id
kwargs["course_runtime"] = self.course.runtime
kwargs["user_id"] = self.user.id
CourseAboutFactory.create(**kwargs)
resp = self.client.get(
reverse('courseabout', kwargs={"course_id": unicode(self.course.id)})
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
resp_data = json.loads(resp.content)
self.assertIsNotNone(resp_data)
resp = self.client.get(
reverse('courseabout', kwargs={"course_id": unicode(self.course.id)})
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
resp_data = json.loads(resp.content)
self.assertIsNotNone(resp_data)
"""
Tests specific to the Data Aggregation Layer of the Course About API.
"""
import unittest
from datetime import datetime
from django.conf import settings
from nose.tools import raises
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory
from course_about import data
from course_about.errors import CourseNotFoundError
from xmodule.modulestore.django import modulestore
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class CourseAboutDataTest(ModuleStoreTestCase):
"""
Test course enrollment data aggregation.
"""
USERNAME = "Bob"
EMAIL = "bob@example.com"
PASSWORD = "edx"
def setUp(self):
"""Create a course and user, then log in. """
super(CourseAboutDataTest, self).setUp()
self.course = CourseFactory.create()
self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
self.client.login(username=self.USERNAME, password=self.PASSWORD)
def test_get_course_about_details(self):
course_info = data.get_course_about_details(unicode(self.course.id))
self.assertIsNotNone(course_info)
def test_get_course_about_valid_date(self):
module_store = modulestore()
self.course.start = datetime.now()
self.course.end = datetime.now()
self.course.announcement = datetime.now()
module_store.update_item(self.course, self.user.id)
course_info = data.get_course_about_details(unicode(self.course.id))
self.assertIsNotNone(course_info["start"])
self.assertIsNotNone(course_info["end"])
self.assertIsNotNone(course_info["announcement"])
def test_get_course_about_none_date(self):
module_store = modulestore()
self.course.start = None
self.course.end = None
self.course.announcement = None
module_store.update_item(self.course, self.user.id)
course_info = data.get_course_about_details(unicode(self.course.id))
self.assertIsNone(course_info["start"])
self.assertIsNone(course_info["end"])
self.assertIsNone(course_info["announcement"])
@raises(CourseNotFoundError)
def test_non_existent_course(self):
data.get_course_about_details("this/is/bananas")
@raises(CourseNotFoundError)
def test_invalid_key(self):
data.get_course_about_details("invalid:key:k")
"""
Tests for user enrollment.
"""
import ddt
import json
import unittest
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from django.conf import settings
from datetime import datetime
from mock import patch
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, CourseAboutFactory
from student.tests.factories import UserFactory
from course_about.serializers import course_image_url
from course_about import api
from course_about.errors import CourseNotFoundError, CourseAboutError
from xmodule.modulestore.django import modulestore
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class CourseInfoTest(ModuleStoreTestCase, APITestCase):
"""
Test course information.
"""
USERNAME = "Bob"
EMAIL = "bob@example.com"
PASSWORD = "edx"
def setUp(self):
""" Create a course"""
super(CourseInfoTest, self).setUp()
self.course = CourseFactory.create()
self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
self.client.login(username=self.USERNAME, password=self.PASSWORD)
def test_user_not_authenticated(self):
# Log out, so we're no longer authenticated
self.client.logout()
resp_data, status_code = self._get_course_about(self.course.id)
self.assertEqual(status_code, status.HTTP_200_OK)
self.assertIsNotNone(resp_data)
def test_with_valid_course_id(self):
_resp_data, status_code = self._get_course_about(self.course.id)
self.assertEqual(status_code, status.HTTP_200_OK)
def test_with_invalid_course_id(self):
resp = self.client.get(
reverse('courseabout', kwargs={"course_id": 'not/a/validkey'})
)
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
def test_get_course_details_all_attributes(self):
kwargs = dict()
kwargs["course_id"] = self.course.id
kwargs["course_runtime"] = self.course.runtime
CourseAboutFactory.create(**kwargs)
resp_data, status_code = self._get_course_about(self.course.id)
all_attributes = ['display_name', 'start', 'end', 'announcement', 'advertised_start', 'is_new', 'course_number',
'course_id',
'effort', 'media', 'course_image']
for attr in all_attributes:
self.assertIn(attr, str(resp_data))
self.assertEqual(status_code, status.HTTP_200_OK)
def test_get_course_about_valid_date(self):
module_store = modulestore()
self.course.start = datetime.now()
self.course.end = datetime.now()
self.course.announcement = datetime.now()
module_store.update_item(self.course, self.user.id)
resp_data, _status_code = self._get_course_about(self.course.id)
self.assertIsNotNone(resp_data["start"])
self.assertIsNotNone(resp_data["end"])
self.assertIsNotNone(resp_data["announcement"])
def test_get_course_about_none_date(self):
module_store = modulestore()
self.course.start = None
self.course.end = None
self.course.announcement = None
module_store.update_item(self.course, self.user.id)
resp_data, _status_code = self._get_course_about(self.course.id)
self.assertIsNone(resp_data["start"])
self.assertIsNone(resp_data["end"])
self.assertIsNone(resp_data["announcement"])
def test_get_course_details(self):
kwargs = dict()
kwargs["course_id"] = self.course.id
kwargs["course_runtime"] = self.course.runtime
kwargs["user_id"] = self.user.id
CourseAboutFactory.create(**kwargs)
resp_data, status_code = self._get_course_about(self.course.id)
self.assertEqual(status_code, status.HTTP_200_OK)
self.assertEqual(unicode(self.course.id), resp_data['course_id'])
self.assertIn('Run', resp_data['display_name'])
url = course_image_url(self.course)
self.assertEquals(url, resp_data['media']['course_image'])
@patch.object(api, "get_course_about_details")
def test_get_enrollment_course_not_found_error(self, mock_get_course_about_details):
mock_get_course_about_details.side_effect = CourseNotFoundError("Something bad happened.")
_resp_data, status_code = self._get_course_about(self.course.id)
self.assertEqual(status_code, status.HTTP_404_NOT_FOUND)
@patch.object(api, "get_course_about_details")
def test_get_enrollment_invalid_key_error(self, mock_get_course_about_details):
mock_get_course_about_details.side_effect = CourseNotFoundError('a/a/a', "Something bad happened.")
resp_data, status_code = self._get_course_about(self.course.id)
self.assertEqual(status_code, status.HTTP_404_NOT_FOUND)
self.assertIn('An error occurred', resp_data["message"])
@patch.object(api, "get_course_about_details")
def test_get_enrollment_internal_error(self, mock_get_course_about_details):
mock_get_course_about_details.side_effect = CourseAboutError('error')
resp_data, status_code = self._get_course_about(self.course.id)
self.assertEqual(status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
self.assertIn('An error occurred', resp_data["message"])
@override_settings(COURSE_ABOUT_DATA_API='foo')
def test_data_api_config_error(self):
# Retrive the invalid course
resp_data, status_code = self._get_course_about(self.course.id)
self.assertEqual(status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
self.assertIn('An error occurred', resp_data["message"])
def _get_course_about(self, course_id):
"""
helper function to get retrieve course about information.
args course_id (str): course id
"""
resp = self.client.get(
reverse('courseabout', kwargs={"course_id": unicode(course_id)})
)
return json.loads(resp.content), resp.status_code
"""
URLs for exposing the RESTful HTTP endpoints for the Course About API.
"""
from django.conf import settings
from django.conf.urls import patterns, url
from course_about.views import CourseAboutView
urlpatterns = patterns(
'course_about.views',
url(
r'^{course_key}'.format(course_key=settings.COURSE_ID_PATTERN),
CourseAboutView.as_view(), name="courseabout"
),
)
"""
Implementation of the RESTful endpoints for the Course About API.
"""
from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView
from course_about import api
from rest_framework import status
from rest_framework.response import Response
from course_about.errors import CourseNotFoundError, CourseAboutError
class CourseAboutThrottle(UserRateThrottle):
"""Limit the number of requests users can make to the Course About API."""
# TODO Limit based on expected throughput # pylint: disable=fixme
rate = '50/second'
class CourseAboutView(APIView):
""" RESTful Course About API view.
Used to retrieve JSON serialized Course About information.
"""
authentication_classes = []
permission_classes = []
throttle_classes = CourseAboutThrottle,
def get(self, request, course_id=None): # pylint: disable=unused-argument
"""Read course information.
HTTP Endpoint for course info api.
Args:
Course Id = URI element specifying the course location. Course information will be
returned for this particular course.
Return:
A JSON serialized representation of the course information
"""
try:
return Response(api.get_course_about_details(course_id))
except CourseNotFoundError:
return Response(
status=status.HTTP_404_NOT_FOUND,
data={
"message": (
u"An error occurred while retrieving course information"
u" for course '{course_id}' no course found"
).format(course_id=course_id)
}
)
except CourseAboutError:
return Response(
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
data={
"message": (
u"An error occurred while retrieving course information"
u" for course '{course_id}'"
).format(course_id=course_id)
}
)
......@@ -6,6 +6,7 @@ from django.contrib.auth.models import User
from django.dispatch import receiver
from django.db.models.signals import post_save
from django.utils.translation import ugettext_noop
from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore
......@@ -84,15 +85,14 @@ class Role(models.Model):
self.permissions.add(Permission.objects.get_or_create(name=permission)[0])
def has_permission(self, permission):
"""Returns True if this role has the given permission, False otherwise."""
course = modulestore().get_course(self.course_id)
if course is None:
raise ItemNotFoundError(self.course_id)
if self.name == FORUM_ROLE_STUDENT and \
(permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \
(not course.forum_posts_allowed):
if permission_blacked_out(course, {self.name}, permission):
return False
return self.permissions.filter(name=permission).exists()
return self.permissions.filter(name=permission).exists() # pylint: disable=no-member
class Permission(models.Model):
......@@ -105,3 +105,35 @@ class Permission(models.Model):
def __unicode__(self):
return self.name
def permission_blacked_out(course, role_names, permission_name):
"""Returns true if a user in course with the given roles would have permission_name blacked out.
This will return true if it is a permission that the user might have normally had for the course, but does not have
right this moment because we are in a discussion blackout period (as defined by the settings on the course module).
Namely, they can still view, but they can't edit, update, or create anything. This only applies to students, as
moderators of any kind still have posting privileges during discussion blackouts.
"""
return (
not course.forum_posts_allowed and
role_names == {FORUM_ROLE_STUDENT} and
any([permission_name.startswith(prefix) for prefix in ['edit', 'update', 'create']])
)
def all_permissions_for_user_in_course(user, course_id): # pylint: disable=invalid-name
"""Returns all the permissions the user has in the given course."""
course = modulestore().get_course(course_id)
if course is None:
raise ItemNotFoundError(course_id)
all_roles = {role.name for role in Role.objects.filter(users=user, course_id=course_id)}
permissions = {
permission.name
for permission
in Permission.objects.filter(roles__users=user, roles__course_id=course_id)
if not permission_blacked_out(course, all_roles, permission.name)
}
return permissions
......@@ -20,6 +20,7 @@ from external_auth.views import (
shib_login, course_specific_login, course_specific_register, _flatten_to_ascii
)
from mock import patch
from urllib import urlencode
from student.views import create_account, change_enrollment
from student.models import UserProfile, CourseEnrollment
......@@ -169,7 +170,7 @@ class ShibSPTest(ModuleStoreTestCase):
if idp == "https://idp.stanford.edu/" and remote_user == 'withmap@stanford.edu':
self.assertIsInstance(response, HttpResponseRedirect)
self.assertEqual(request.user, user_w_map)
self.assertEqual(response['Location'], '/')
self.assertEqual(response['Location'], '/dashboard')
# verify logging:
self.assertEquals(len(audit_log_calls), 2)
self._assert_shib_login_is_logged(audit_log_calls[0], remote_user)
......@@ -193,7 +194,7 @@ class ShibSPTest(ModuleStoreTestCase):
self.assertIsNotNone(ExternalAuthMap.objects.get(user=user_wo_map))
self.assertIsInstance(response, HttpResponseRedirect)
self.assertEqual(request.user, user_wo_map)
self.assertEqual(response['Location'], '/')
self.assertEqual(response['Location'], '/dashboard')
# verify logging:
self.assertEquals(len(audit_log_calls), 2)
self._assert_shib_login_is_logged(audit_log_calls[0], remote_user)
......@@ -242,7 +243,7 @@ class ShibSPTest(ModuleStoreTestCase):
self.assertTrue(inactive_user.is_active)
self.assertIsInstance(response, HttpResponseRedirect)
self.assertEqual(request.user, inactive_user)
self.assertEqual(response['Location'], '/')
self.assertEqual(response['Location'], '/dashboard')
# verify logging:
self.assertEquals(len(audit_log_calls), 3)
self._assert_shib_login_is_logged(audit_log_calls[0], log_user_string)
......@@ -549,29 +550,20 @@ class ShibSPTest(ModuleStoreTestCase):
# no enrollment before trying
self.assertFalse(CourseEnrollment.is_enrolled(student, course.id))
self.client.logout()
params = [
('course_id', course.id.to_deprecated_string()),
('enrollment_action', 'enroll'),
('next', '/testredirect')
]
request_kwargs = {'path': '/shib-login/',
'data': {'enrollment_action': 'enroll', 'course_id': course.id.to_deprecated_string(), 'next': '/testredirect'},
'data': dict(params),
'follow': False,
'REMOTE_USER': 'testuser@stanford.edu',
'Shib-Identity-Provider': 'https://idp.stanford.edu/'}
response = self.client.get(**request_kwargs)
# successful login is a redirect to "/"
# successful login is a redirect to the URL that handles auto-enrollment
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], 'http://testserver/testredirect')
# now there is enrollment
self.assertTrue(CourseEnrollment.is_enrolled(student, course.id))
# Clean up and try again with POST (doesn't happen with real production shib, doing this for test coverage)
self.client.logout()
CourseEnrollment.unenroll(student, course.id)
self.assertFalse(CourseEnrollment.is_enrolled(student, course.id))
response = self.client.post(**request_kwargs)
# successful login is a redirect to "/"
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], 'http://testserver/testredirect')
# now there is enrollment
self.assertTrue(CourseEnrollment.is_enrolled(student, course.id))
self.assertEqual(response['location'], 'http://testserver/account/finish_auth?{}'.format(urlencode(params)))
class ShibUtilFnTest(TestCase):
......
......@@ -22,6 +22,7 @@ from django.core.exceptions import ValidationError
if settings.FEATURES.get('AUTH_USE_CAS'):
from django_cas.views import login as django_cas_login
from student.helpers import get_next_url_for_login_page
from student.models import UserProfile
from django.http import HttpResponse, HttpResponseRedirect, HttpRequest, HttpResponseForbidden
......@@ -118,7 +119,8 @@ def openid_login_complete(request,
external_domain,
details,
details.get('email', ''),
fullname
fullname,
retfun=functools.partial(redirect, get_next_url_for_login_page(request)),
)
return render_failure(request, 'Openid failure')
......@@ -236,14 +238,6 @@ def _external_login_or_signup(request,
login(request, user)
request.session.set_expiry(0)
# Now to try enrollment
# Need to special case Shibboleth here because it logs in via a GET.
# testing request.method for extra paranoia
if uses_shibboleth and request.method == 'GET':
enroll_request = _make_shib_enrollment_request(request)
student.views.try_change_enrollment(enroll_request)
else:
student.views.try_change_enrollment(request)
if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
AUDIT_LOG.info(u"Login success - user.id: {0}".format(user.id))
else:
......@@ -449,9 +443,7 @@ def ssl_login(request):
(_user, email, fullname) = _ssl_dn_extract_info(cert)
redirect_to = request.GET.get('next')
if not redirect_to:
redirect_to = '/'
redirect_to = get_next_url_for_login_page(request)
retfun = functools.partial(redirect, redirect_to)
return _external_login_or_signup(
request,
......@@ -528,10 +520,8 @@ def shib_login(request):
fullname = shib['displayName'] if shib['displayName'] else u'%s %s' % (shib['givenName'], shib['sn'])
redirect_to = request.REQUEST.get('next')
retfun = None
if redirect_to:
retfun = functools.partial(_safe_postlogin_redirect, redirect_to, request.get_host())
redirect_to = get_next_url_for_login_page(request)
retfun = functools.partial(_safe_postlogin_redirect, redirect_to, request.get_host())
return _external_login_or_signup(
request,
......@@ -558,31 +548,6 @@ def _safe_postlogin_redirect(redirect_to, safehost, default_redirect='/'):
return redirect(default_redirect)
def _make_shib_enrollment_request(request):
"""
Need this hack function because shibboleth logins don't happen over POST
but change_enrollment expects its request to be a POST, with
enrollment_action and course_id POST parameters.
"""
enroll_request = HttpRequest()
enroll_request.user = request.user
enroll_request.session = request.session
enroll_request.method = "POST"
# copy() also makes GET and POST mutable
# See https://docs.djangoproject.com/en/dev/ref/request-response/#django.http.QueryDict.update
enroll_request.GET = request.GET.copy()
enroll_request.POST = request.POST.copy()
# also have to copy these GET parameters over to POST
if "enrollment_action" not in enroll_request.POST and "enrollment_action" in enroll_request.GET:
enroll_request.POST.setdefault('enrollment_action', enroll_request.GET.get('enrollment_action'))
if "course_id" not in enroll_request.POST and "course_id" in enroll_request.GET:
enroll_request.POST.setdefault('course_id', enroll_request.GET.get('course_id'))
return enroll_request
def course_specific_login(request, course_id):
"""
Dispatcher function for selecting the specific login method
......
......@@ -18,7 +18,11 @@ class RequestCache(object):
"""
return _request_cache_threadlocal.request
def clear_request_cache(self):
@classmethod
def clear_request_cache(cls):
"""
Empty the request cache.
"""
_request_cache_threadlocal.data = {}
_request_cache_threadlocal.request = None
......
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Deleting model 'MidcourseReverificationWindow'
db.delete_table('reverification_midcoursereverificationwindow')
def backwards(self, orm):
# Adding model 'MidcourseReverificationWindow'
db.create_table('reverification_midcoursereverificationwindow', (
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('end_date', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True)),
('start_date', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True)),
))
db.send_create_signal('reverification', ['MidcourseReverificationWindow'])
models = {
}
complete_apps = ['reverification']
......@@ -4,9 +4,11 @@ from datetime import datetime
from pytz import UTC
from django.utils.http import cookie_date
from django.conf import settings
from django.core.urlresolvers import reverse, NoReverseMatch
import third_party_auth
import urllib
from verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=F0401
from course_modes.models import CourseMode
from student_account.helpers import auth_pipeline_urls # pylint: disable=unused-import,import-error
def set_logged_in_cookie(request, response):
......@@ -199,3 +201,70 @@ def check_verify_status_by_course(user, course_enrollment_pairs, all_course_mode
status_by_course[key]['verification_good_until'] = recent_verification_datetime.strftime("%m/%d/%Y")
return status_by_course
def auth_pipeline_urls(auth_entry, redirect_url=None):
"""Retrieve URLs for each enabled third-party auth provider.
These URLs are used on the "sign up" and "sign in" buttons
on the login/registration forms to allow users to begin
authentication with a third-party provider.
Optionally, we can redirect the user to an arbitrary
url after auth completes successfully. We use this
to redirect the user to a page that required login,
or to send users to the payment flow when enrolling
in a course.
Args:
auth_entry (string): Either `pipeline.AUTH_ENTRY_LOGIN` or `pipeline.AUTH_ENTRY_REGISTER`
Keyword Args:
redirect_url (unicode): If provided, send users to this URL
after they successfully authenticate.
Returns:
dict mapping provider IDs to URLs
"""
if not third_party_auth.is_enabled():
return {}
return {
provider.NAME: third_party_auth.pipeline.get_login_url(provider.NAME, auth_entry, redirect_url=redirect_url)
for provider in third_party_auth.provider.Registry.enabled()
}
# Query string parameters that can be passed to the "finish_auth" view to manage
# things like auto-enrollment.
POST_AUTH_PARAMS = ('course_id', 'enrollment_action', 'course_mode', 'email_opt_in')
def get_next_url_for_login_page(request):
"""
Determine the URL to redirect to following login/registration/third_party_auth
The user is currently on a login or reigration page.
If 'course_id' is set, or other POST_AUTH_PARAMS, we will need to send the user to the
/account/finish_auth/ view following login, which will take care of auto-enrollment in
the specified course.
Otherwise, we go to the ?next= query param or to the dashboard if nothing else is
specified.
"""
redirect_to = request.GET.get('next', None)
if not redirect_to:
try:
redirect_to = reverse('dashboard')
except NoReverseMatch:
redirect_to = reverse('home')
if any(param in request.GET for param in POST_AUTH_PARAMS):
# Before we redirect to next/dashboard, we need to handle auto-enrollment:
params = [(param, request.GET[param]) for param in POST_AUTH_PARAMS if param in request.GET]
params.append(('next', redirect_to)) # After auto-enrollment, user will be sent to payment page or to this URL
redirect_to = '{}?{}'.format(reverse('finish_auth'), urllib.urlencode(params))
# Note: if we are resuming a third party auth pipeline, then the next URL will already
# be saved in the session as part of the pipeline state. That URL will take priority
# over this one.
return redirect_to
......@@ -1819,3 +1819,23 @@ class LanguageProficiency(models.Model):
choices=settings.ALL_LANGUAGES,
help_text=_("The ISO 639-1 language code for this language.")
)
class CourseEnrollmentAttribute(models.Model):
"""Represents Student's enrollment record for Credit Course.
This is populated when the user's order for a credit seat is fulfilled.
"""
enrollment = models.ForeignKey(CourseEnrollment)
namespace = models.CharField(
max_length=255,
help_text=_("Namespace of enrollment attribute e.g. credit")
)
name = models.CharField(
max_length=255,
help_text=_("Name of the enrollment attribute e.g. provider_id")
)
value = models.CharField(
max_length=255,
help_text=_("Value of the enrollment attribute e.g. ASU")
)
......@@ -59,7 +59,8 @@ class CertificateDisplayTest(ModuleStoreTestCase):
def test_linked_student_to_web_view_credential(self, enrollment_mode):
test_url = get_certificate_url(
user_id=self.user.id,
course_id=unicode(self.course.id)
course_id=unicode(self.course.id),
verify_uuid='abcdefg12345678'
)
self._create_certificate(enrollment_mode)
......
......@@ -278,24 +278,6 @@ class LoginTest(TestCase):
self.assertIsNone(response_content["redirect_url"])
self._assert_response(response, success=True)
def test_change_enrollment_200_redirect(self):
"""
Tests that "redirect_url" is the content of the HttpResponse returned
by change_enrollment, if there is content
"""
# add this post param to trigger a call to change_enrollment
extra_post_params = {"enrollment_action": "enroll"}
with patch('student.views.change_enrollment') as mock_change_enrollment:
mock_change_enrollment.return_value = HttpResponse("in/nature/there/is/nothing/melancholy")
response, _ = self._login_response(
'test@edx.org',
'test_password',
extra_post_params=extra_post_params,
)
response_content = json.loads(response.content)
self.assertEqual(response_content["redirect_url"], "in/nature/there/is/nothing/melancholy")
self._assert_response(response, success=True)
def _login_response(self, email, password, patched_audit_log='student.views.AUDIT_LOG', extra_post_params=None):
''' Post the login info '''
post_params = {'email': email, 'password': password}
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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