Merge branch 'rc/2015-06-24' into release
This commit is contained in:
3
AUTHORS
3
AUTHORS
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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 ->
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
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")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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", ->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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')
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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')
|
||||
|
||||
|
||||
1
cms/static/common
Symbolic link
1
cms/static/common
Symbolic link
@@ -0,0 +1 @@
|
||||
../../common/static/common
|
||||
@@ -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'
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// 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({
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../../common/static/js/spec_helpers
|
||||
@@ -1 +1 @@
|
||||
define(['domReady!', 'jquery', 'backbone', 'underscore', 'gettext']);
|
||||
define(['domReady!', 'jquery', 'backbone', 'underscore', 'gettext', 'text']);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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 () {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
define(
|
||||
[
|
||||
'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'squire'
|
||||
'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'squire'
|
||||
],
|
||||
function ($, _, AjaxHelpers, Squire) {
|
||||
'use strict';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
define(
|
||||
[
|
||||
'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'squire'
|
||||
'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'squire'
|
||||
],
|
||||
function ($, _, AjaxHelpers, Squire) {
|
||||
'use strict';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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() {
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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) {
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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) {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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 () {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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 () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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) {
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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) {
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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) {
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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 () {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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 () {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* 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,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging", "js/views/asset",
|
||||
"js/views/paging_header", "js/views/paging_footer", "js/utils/modal", "js/views/utils/view_utils",
|
||||
"js/views/feedback_notification", "jquery.fileupload-process", "jquery.fileupload-validate"],
|
||||
function($, _, gettext, AssetModel, PagingView, AssetView, PagingHeader, PagingFooter, ModalUtils, ViewUtils, NotificationView) {
|
||||
define(["jquery", "underscore", "gettext", "js/views/baseview", "js/models/asset", "common/js/components/views/paging",
|
||||
"js/views/asset", "common/js/components/views/paging_header", "common/js/components/views/paging_footer",
|
||||
"js/utils/modal", "js/views/utils/view_utils", "js/views/feedback_notification",
|
||||
"text!templates/asset-library.underscore",
|
||||
"jquery.fileupload-process", "jquery.fileupload-validate"],
|
||||
function($, _, gettext, BaseView, AssetModel, PagingView, AssetView, PagingHeader, PagingFooter,
|
||||
ModalUtils, ViewUtils, NotificationView, asset_library_template) {
|
||||
|
||||
var CONVERSION_FACTOR_MBS_TO_BYTES = 1000 * 1000;
|
||||
|
||||
var AssetsView = PagingView.extend({
|
||||
var AssetsView = BaseView.extend({
|
||||
// takes AssetCollection as model
|
||||
|
||||
events : {
|
||||
@@ -19,21 +22,14 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
|
||||
|
||||
allLabel: 'ALL',
|
||||
|
||||
|
||||
initialize : function(options) {
|
||||
options = options || {};
|
||||
|
||||
PagingView.prototype.initialize.call(this);
|
||||
BaseView.prototype.initialize.call(this);
|
||||
var collection = this.collection;
|
||||
this.template = this.loadTemplate("asset-library");
|
||||
this.pagingView = this.createPagingView();
|
||||
this.listenTo(collection, 'destroy', this.handleDestroy);
|
||||
this.registerSortableColumn('js-asset-name-col', gettext('Name'), 'display_name', 'asc');
|
||||
this.registerSortableColumn('js-asset-date-col', gettext('Date Added'), 'date_added', 'desc');
|
||||
this.registerFilterableColumn('js-asset-type-col', gettext('Type'), 'asset_type');
|
||||
this.setInitialSortColumn('js-asset-date-col');
|
||||
this.setInitialFilterColumn('js-asset-type-col');
|
||||
ViewUtils.showLoadingIndicator();
|
||||
this.setPage(0);
|
||||
// set default file size for uploads via template var,
|
||||
// and default to static old value if none exists
|
||||
this.uploadChunkSizeInMBs = options.uploadChunkSizeInMBs || 10;
|
||||
@@ -41,68 +37,83 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
|
||||
this.uploadChunkSizeInBytes = this.uploadChunkSizeInMBs * CONVERSION_FACTOR_MBS_TO_BYTES;
|
||||
this.maxFileSizeInBytes = this.maxFileSizeInMBs * CONVERSION_FACTOR_MBS_TO_BYTES;
|
||||
this.maxFileSizeRedirectUrl = options.maxFileSizeRedirectUrl || '';
|
||||
assetsView = this;
|
||||
// error message modal for large file uploads
|
||||
this.largeFileErrorMsg = null;
|
||||
},
|
||||
|
||||
PagingAssetView: PagingView.extend({
|
||||
renderPageItems: function() {
|
||||
var self = this,
|
||||
assets = this.collection,
|
||||
hasAssets = this.collection.assetType !== '' || assets.length > 0,
|
||||
tableBody = this.getTableBody();
|
||||
tableBody.empty();
|
||||
if (hasAssets) {
|
||||
assets.each(
|
||||
function(asset) {
|
||||
var view = new AssetView({model: asset});
|
||||
tableBody.append(view.render().el);
|
||||
}
|
||||
);
|
||||
}
|
||||
self.$('.assets-library').toggle(hasAssets);
|
||||
self.$('.no-asset-content').toggle(!hasAssets);
|
||||
return this;
|
||||
},
|
||||
|
||||
getTableBody: function() {
|
||||
var tableBody = this.tableBody;
|
||||
if (!tableBody) {
|
||||
ViewUtils.hideLoadingIndicator();
|
||||
|
||||
// Create the table
|
||||
this.$el.html(_.template(asset_library_template, {typeData: this.typeData}));
|
||||
tableBody = this.$('#asset-table-body');
|
||||
this.tableBody = tableBody;
|
||||
this.pagingHeader = new PagingHeader({view: this, el: $('#asset-paging-header')});
|
||||
this.pagingFooter = new PagingFooter({view: this, el: $('#asset-paging-footer')});
|
||||
this.pagingHeader.render();
|
||||
this.pagingFooter.render();
|
||||
|
||||
// Hide the contents until the collection has loaded the first time
|
||||
this.$('.assets-library').hide();
|
||||
this.$('.no-asset-content').hide();
|
||||
}
|
||||
return tableBody;
|
||||
},
|
||||
|
||||
onError: function() {
|
||||
ViewUtils.hideLoadingIndicator();
|
||||
}
|
||||
}),
|
||||
|
||||
createPagingView: function() {
|
||||
var pagingView = new this.PagingAssetView({
|
||||
el: this.$el,
|
||||
collection: this.collection
|
||||
});
|
||||
pagingView.registerSortableColumn('js-asset-name-col', gettext('Name'), 'display_name', 'asc');
|
||||
pagingView.registerSortableColumn('js-asset-date-col', gettext('Date Added'), 'date_added', 'desc');
|
||||
pagingView.registerFilterableColumn('js-asset-type-col', gettext('Type'), 'asset_type');
|
||||
pagingView.setInitialSortColumn('js-asset-date-col');
|
||||
pagingView.setInitialFilterColumn('js-asset-type-col');
|
||||
pagingView.setPage(0);
|
||||
return pagingView;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// Wait until the content is loaded the first time to render
|
||||
this.pagingView.render();
|
||||
return this;
|
||||
},
|
||||
|
||||
afterRender: function(){
|
||||
// Bind events with html elements
|
||||
$('li a.upload-button').on('click', this.showUploadModal);
|
||||
$('.upload-modal .close-button').on('click', this.hideModal);
|
||||
$('.upload-modal .choose-file-button').on('click', this.showFileSelectionMenu);
|
||||
$('li a.upload-button').on('click', _.bind(this.showUploadModal, this));
|
||||
$('.upload-modal .close-button').on('click', _.bind(this.hideModal, this));
|
||||
$('.upload-modal .choose-file-button').on('click', _.bind(this.showFileSelectionMenu, this));
|
||||
return this;
|
||||
},
|
||||
|
||||
getTableBody: function() {
|
||||
var tableBody = this.tableBody;
|
||||
if (!tableBody) {
|
||||
ViewUtils.hideLoadingIndicator();
|
||||
|
||||
// Create the table
|
||||
this.$el.html(this.template({typeData: this.typeData}));
|
||||
tableBody = this.$('#asset-table-body');
|
||||
this.tableBody = tableBody;
|
||||
this.pagingHeader = new PagingHeader({view: this, el: $('#asset-paging-header')});
|
||||
this.pagingFooter = new PagingFooter({view: this, el: $('#asset-paging-footer')});
|
||||
this.pagingHeader.render();
|
||||
this.pagingFooter.render();
|
||||
|
||||
// Hide the contents until the collection has loaded the first time
|
||||
this.$('.assets-library').hide();
|
||||
this.$('.no-asset-content').hide();
|
||||
}
|
||||
return tableBody;
|
||||
},
|
||||
|
||||
renderPageItems: function() {
|
||||
var self = this,
|
||||
assets = this.collection,
|
||||
hasAssets = this.collection.assetType !== '' || assets.length > 0,
|
||||
tableBody = this.getTableBody();
|
||||
tableBody.empty();
|
||||
if (hasAssets) {
|
||||
assets.each(
|
||||
function(asset) {
|
||||
var view = new AssetView({model: asset});
|
||||
tableBody.append(view.render().el);
|
||||
}
|
||||
);
|
||||
}
|
||||
self.$('.assets-library').toggle(hasAssets);
|
||||
self.$('.no-asset-content').toggle(!hasAssets);
|
||||
return this;
|
||||
},
|
||||
|
||||
onError: function() {
|
||||
ViewUtils.hideLoadingIndicator();
|
||||
},
|
||||
|
||||
handleDestroy: function(model) {
|
||||
this.collection.fetch({reset: true}); // reload the collection to get a fresh page full of items
|
||||
analytics.track('Deleted Asset', {
|
||||
@@ -114,9 +125,9 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
|
||||
addAsset: function (model) {
|
||||
// Switch the sort column back to the default (most recent date added) and show the first page
|
||||
// so that the new asset is shown at the top of the page.
|
||||
this.setInitialSortColumn('js-asset-date-col');
|
||||
this.setInitialFilterColumn('js-asset-type-col');
|
||||
this.setPage(0);
|
||||
this.pagingView.setInitialSortColumn('js-asset-date-col');
|
||||
this.pagingView.setInitialFilterColumn('js-asset-type-col');
|
||||
this.pagingView.setPage(0);
|
||||
|
||||
analytics.track('Uploaded a File', {
|
||||
'course': course_location_analytics,
|
||||
@@ -126,7 +137,7 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
|
||||
|
||||
onToggleColumn: function(event) {
|
||||
var columnName = event.target.id;
|
||||
this.toggleSortOrder(columnName);
|
||||
this.pagingView.toggleSortOrder(columnName);
|
||||
},
|
||||
|
||||
onFilterColumn: function(event) {
|
||||
@@ -140,13 +151,13 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
|
||||
}
|
||||
$('.file-input').unbind('change.startUpload');
|
||||
ModalUtils.hideModal();
|
||||
if (assetsView.largeFileErrorMsg) {
|
||||
assetsView.largeFileErrorMsg.hide();
|
||||
if (this.largeFileErrorMsg) {
|
||||
this.largeFileErrorMsg.hide();
|
||||
}
|
||||
},
|
||||
|
||||
showUploadModal: function (event) {
|
||||
var self = assetsView;
|
||||
var self = this;
|
||||
event.preventDefault();
|
||||
self.resetUploadModal();
|
||||
ModalUtils.showModal();
|
||||
@@ -180,33 +191,33 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
|
||||
error = error + " " + instructions;
|
||||
}
|
||||
|
||||
assetsView.largeFileErrorMsg = new NotificationView.Error({
|
||||
self.largeFileErrorMsg = new NotificationView.Error({
|
||||
"title": gettext("Your file could not be uploaded"),
|
||||
"message": error
|
||||
});
|
||||
assetsView.largeFileErrorMsg.show();
|
||||
self.largeFileErrorMsg.show();
|
||||
|
||||
assetsView.displayFailedUpload({
|
||||
self.displayFailedUpload({
|
||||
"msg": gettext("Max file size exceeded")
|
||||
});
|
||||
},
|
||||
processdone: function(event, data) {
|
||||
assetsView.largeFileErrorMsg = null;
|
||||
self.largeFileErrorMsg = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showFileSelectionMenu: function(event) {
|
||||
event.preventDefault();
|
||||
if (assetsView.largeFileErrorMsg) {
|
||||
assetsView.largeFileErrorMsg.hide();
|
||||
if (this.largeFileErrorMsg) {
|
||||
this.largeFileErrorMsg.hide();
|
||||
}
|
||||
$('.file-input').click();
|
||||
},
|
||||
|
||||
startUpload: function (event) {
|
||||
var file = event.target.value;
|
||||
if (!assetsView.largeFileErrorMsg) {
|
||||
if (!this.largeFileErrorMsg) {
|
||||
$('.upload-modal h1').text(gettext('Uploading'));
|
||||
$('.upload-modal .file-name').html(file.substring(file.lastIndexOf("\\") + 1));
|
||||
$('.upload-modal .choose-file-button').hide();
|
||||
@@ -228,7 +239,7 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
|
||||
$('.upload-modal .embeddable-xml-input').val('');
|
||||
$('.upload-modal .embeddable').hide();
|
||||
|
||||
assetsView.largeFileErrorMsg = null;
|
||||
this.largeFileErrorMsg = null;
|
||||
},
|
||||
|
||||
showUploadFeedback: function (event, percentComplete) {
|
||||
@@ -285,8 +296,8 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
|
||||
title.addClass('column-selected-link');
|
||||
}
|
||||
|
||||
this.filterableColumns['js-asset-type-col'].displayName = assettypeLabel;
|
||||
this.selectFilter('js-asset-type-col');
|
||||
this.pagingView.filterableColumns['js-asset-type-col'].displayName = assettypeLabel;
|
||||
this.pagingView.selectFilter('js-asset-type-col');
|
||||
this.closeFilterPopup(this.$el.find(
|
||||
'.column-filter-link[data-assetfilter="' + assettype + '"]'));
|
||||
},
|
||||
@@ -307,7 +318,7 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
|
||||
$('.upload-modal .choose-file-button').text(gettext('Load Another File')).show();
|
||||
$('.upload-modal .progress-fill').width('100%');
|
||||
|
||||
assetsView.addAsset(new AssetModel(asset));
|
||||
this.addAsset(new AssetModel(asset));
|
||||
},
|
||||
|
||||
displayFailedUpload: function (resp) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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){
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
define(["underscore", "js/views/baseview"], function(_, BaseView) {
|
||||
|
||||
var PagingFooter = BaseView.extend({
|
||||
events : {
|
||||
"click .next-page-link": "nextPage",
|
||||
"click .previous-page-link": "previousPage",
|
||||
"change .page-number-input": "changePage"
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
var view = options.view,
|
||||
collection = view.collection;
|
||||
this.view = view;
|
||||
this.template = this.loadTemplate('paging-footer');
|
||||
collection.bind('add', _.bind(this.render, this));
|
||||
collection.bind('remove', _.bind(this.render, this));
|
||||
collection.bind('reset', _.bind(this.render, this));
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var view = this.view,
|
||||
collection = view.collection,
|
||||
currentPage = collection.currentPage,
|
||||
lastPage = collection.totalPages - 1;
|
||||
this.$el.html(this.template({
|
||||
current_page: collection.currentPage,
|
||||
total_pages: collection.totalPages
|
||||
}));
|
||||
this.$(".previous-page-link").toggleClass("is-disabled", currentPage === 0).attr('aria-disabled', currentPage === 0);;
|
||||
this.$(".next-page-link").toggleClass("is-disabled", currentPage === lastPage).attr('aria-disabled', currentPage === lastPage);
|
||||
return this;
|
||||
},
|
||||
|
||||
changePage: function() {
|
||||
var view = this.view,
|
||||
collection = view.collection,
|
||||
currentPage = collection.currentPage + 1,
|
||||
pageInput = this.$("#page-number-input"),
|
||||
pageNumber = parseInt(pageInput.val(), 10);
|
||||
if (pageNumber > collection.totalPages) {
|
||||
pageNumber = false;
|
||||
}
|
||||
if (pageNumber <= 0) {
|
||||
pageNumber = false;
|
||||
}
|
||||
// If we still have a page number by this point,
|
||||
// and it's not the current page, load it.
|
||||
if (pageNumber && pageNumber !== currentPage) {
|
||||
view.setPage(pageNumber - 1);
|
||||
}
|
||||
pageInput.val(""); // Clear the value as the label will show beneath it
|
||||
},
|
||||
|
||||
nextPage: function() {
|
||||
this.view.nextPage();
|
||||
},
|
||||
|
||||
previousPage: function() {
|
||||
this.view.previousPage();
|
||||
}
|
||||
});
|
||||
|
||||
return PagingFooter;
|
||||
}); // end define();
|
||||
@@ -1,113 +0,0 @@
|
||||
define(["underscore", "gettext", "js/views/baseview"], function(_, gettext, BaseView) {
|
||||
|
||||
var PagingHeader = BaseView.extend({
|
||||
events : {
|
||||
"click .next-page-link": "nextPage",
|
||||
"click .previous-page-link": "previousPage"
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
var view = options.view,
|
||||
collection = view.collection;
|
||||
this.view = view;
|
||||
this.template = this.loadTemplate('paging-header');
|
||||
collection.bind('add', _.bind(this.render, this));
|
||||
collection.bind('remove', _.bind(this.render, this));
|
||||
collection.bind('reset', _.bind(this.render, this));
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var view = this.view,
|
||||
collection = view.collection,
|
||||
currentPage = collection.currentPage,
|
||||
lastPage = collection.totalPages - 1,
|
||||
messageHtml = this.messageHtml();
|
||||
this.$el.html(this.template({
|
||||
messageHtml: messageHtml
|
||||
}));
|
||||
this.$(".previous-page-link").toggleClass("is-disabled", currentPage === 0).attr('aria-disabled', currentPage === 0);
|
||||
this.$(".next-page-link").toggleClass("is-disabled", currentPage === lastPage).attr('aria-disabled', currentPage === lastPage);
|
||||
return this;
|
||||
},
|
||||
|
||||
messageHtml: function() {
|
||||
var message = '';
|
||||
var asset_type = false;
|
||||
if (this.view.collection.assetType) {
|
||||
if (this.view.collection.sortDirection === 'asc') {
|
||||
// Translators: sample result:
|
||||
// "Showing 0-9 out of 25 total, filtered by Images, sorted by Date Added ascending"
|
||||
message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, filtered by %(asset_type)s, sorted by %(sort_name)s ascending');
|
||||
} else {
|
||||
// Translators: sample result:
|
||||
// "Showing 0-9 out of 25 total, filtered by Images, sorted by Date Added descending"
|
||||
message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, filtered by %(asset_type)s, sorted by %(sort_name)s descending');
|
||||
}
|
||||
asset_type = this.filterNameLabel();
|
||||
}
|
||||
else {
|
||||
if (this.view.collection.sortDirection === 'asc') {
|
||||
// Translators: sample result:
|
||||
// "Showing 0-9 out of 25 total, sorted by Date Added ascending"
|
||||
message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, sorted by %(sort_name)s ascending');
|
||||
} else {
|
||||
// Translators: sample result:
|
||||
// "Showing 0-9 out of 25 total, sorted by Date Added descending"
|
||||
message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, sorted by %(sort_name)s descending');
|
||||
}
|
||||
}
|
||||
|
||||
return '<p>' + interpolate(message, {
|
||||
current_item_range: this.currentItemRangeLabel(),
|
||||
total_items_count: this.totalItemsCountLabel(),
|
||||
asset_type: asset_type,
|
||||
sort_name: this.sortNameLabel()
|
||||
}, true) + "</p>";
|
||||
},
|
||||
|
||||
currentItemRangeLabel: function() {
|
||||
var view = this.view,
|
||||
collection = view.collection,
|
||||
start = collection.start,
|
||||
count = collection.size(),
|
||||
end = start + count;
|
||||
return interpolate('<span class="count-current-shown">%(start)s-%(end)s</span>', {
|
||||
start: Math.min(start + 1, end),
|
||||
end: end
|
||||
}, true);
|
||||
},
|
||||
|
||||
totalItemsCountLabel: function() {
|
||||
var totalItemsLabel;
|
||||
// Translators: turns into "25 total" to be used in other sentences, e.g. "Showing 0-9 out of 25 total".
|
||||
totalItemsLabel = interpolate(gettext('%(total_items)s total'), {
|
||||
total_items: this.view.collection.totalCount
|
||||
}, true);
|
||||
return interpolate('<span class="count-total">%(total_items_label)s</span>', {
|
||||
total_items_label: totalItemsLabel
|
||||
}, true);
|
||||
},
|
||||
|
||||
sortNameLabel: function() {
|
||||
return interpolate('<span class="sort-order">%(sort_name)s</span>', {
|
||||
sort_name: this.view.sortDisplayName()
|
||||
}, true);
|
||||
},
|
||||
|
||||
filterNameLabel: function() {
|
||||
return interpolate('<span class="filter-column">%(filter_name)s</span>', {
|
||||
filter_name: this.view.filterDisplayName()
|
||||
}, true);
|
||||
},
|
||||
|
||||
nextPage: function() {
|
||||
this.view.nextPage();
|
||||
},
|
||||
|
||||
previousPage: function() {
|
||||
this.view.previousPage();
|
||||
}
|
||||
});
|
||||
|
||||
return PagingHeader;
|
||||
}); // end define();
|
||||
@@ -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);
|
||||
position: relative;
|
||||
display: none;
|
||||
width: 700px;
|
||||
padding: ($baseline);
|
||||
border: 1px solid $gray-d1;
|
||||
background: $white;
|
||||
box-shadow: 0 2px 4px $shadow-d1;
|
||||
overflow: hidden;
|
||||
|
||||
// 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;
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $blue;
|
||||
top: 0;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
.description {
|
||||
@extend %t-copy-sub2;
|
||||
margin-top: ($baseline/2);
|
||||
color: $gray-l1;
|
||||
&:hover {
|
||||
top: 0;
|
||||
background: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
cms/static/templates
Symbolic link
1
cms/static/templates
Symbolic link
@@ -0,0 +1 @@
|
||||
../templates/js
|
||||
@@ -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)
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,76 +0,0 @@
|
||||
"""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
|
||||
@@ -1,23 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,6 +0,0 @@
|
||||
"""
|
||||
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.
|
||||
|
||||
"""
|
||||
@@ -1,67 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,54 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,66 +0,0 @@
|
||||
"""
|
||||
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")
|
||||
@@ -1,149 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
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"
|
||||
),
|
||||
)
|
||||
@@ -1,63 +0,0 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# -*- 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
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
# -*- 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):
|
||||
# Adding model 'CourseEnrollmentAttribute'
|
||||
db.create_table('student_courseenrollmentattribute', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('enrollment', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['student.CourseEnrollment'])),
|
||||
('namespace', self.gf('django.db.models.fields.CharField')(max_length=255)),
|
||||
('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
|
||||
('value', self.gf('django.db.models.fields.CharField')(max_length=255)),
|
||||
))
|
||||
db.send_create_signal('student', ['CourseEnrollmentAttribute'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'CourseEnrollmentAttribute'
|
||||
db.delete_table('student_courseenrollmentattribute')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'student.anonymoususerid': {
|
||||
'Meta': {'object_name': 'AnonymousUserId'},
|
||||
'anonymous_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.courseaccessrole': {
|
||||
'Meta': {'unique_together': "(('user', 'org', 'course_id', 'role'),)", 'object_name': 'CourseAccessRole'},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'org': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
|
||||
'role': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.courseenrollmentallowed': {
|
||||
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
|
||||
'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'student.courseenrollmentattribute': {
|
||||
'Meta': {'object_name': 'CourseEnrollmentAttribute'},
|
||||
'enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'namespace': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
|
||||
},
|
||||
'student.dashboardconfiguration': {
|
||||
'Meta': {'ordering': "('-change_date',)", 'object_name': 'DashboardConfiguration'},
|
||||
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'recent_enrollment_time_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
|
||||
},
|
||||
'student.entranceexamconfiguration': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'EntranceExamConfiguration'},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'skip_entrance_exam': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.languageproficiency': {
|
||||
'Meta': {'unique_together': "(('code', 'user_profile'),)", 'object_name': 'LanguageProficiency'},
|
||||
'code': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user_profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'language_proficiencies'", 'to': "orm['student.UserProfile']"})
|
||||
},
|
||||
'student.linkedinaddtoprofileconfiguration': {
|
||||
'Meta': {'ordering': "('-change_date',)", 'object_name': 'LinkedInAddToProfileConfiguration'},
|
||||
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||
'company_identifier': ('django.db.models.fields.TextField', [], {}),
|
||||
'dashboard_tracking_code': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'trk_partner_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'})
|
||||
},
|
||||
'student.loginfailures': {
|
||||
'Meta': {'object_name': 'LoginFailures'},
|
||||
'failure_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'lockout_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.manualenrollmentaudit': {
|
||||
'Meta': {'object_name': 'ManualEnrollmentAudit'},
|
||||
'enrolled_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
|
||||
'enrolled_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']", 'null': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'state_transition': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'time_stamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'student.passwordhistory': {
|
||||
'Meta': {'object_name': 'PasswordHistory'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'time_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.pendingemailchange': {
|
||||
'Meta': {'object_name': 'PendingEmailChange'},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.pendingnamechange': {
|
||||
'Meta': {'object_name': 'PendingNameChange'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.registration': {
|
||||
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.userprofile': {
|
||||
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||
'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'bio': ('django.db.models.fields.CharField', [], {'max_length': '3000', 'null': 'True', 'blank': 'True'}),
|
||||
'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
|
||||
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
|
||||
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'profile_image_uploaded_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
|
||||
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'student.usersignupsource': {
|
||||
'Meta': {'object_name': 'UserSignupSource'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'site': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.userstanding': {
|
||||
'Meta': {'object_name': 'UserStanding'},
|
||||
'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.usertestgroup': {
|
||||
'Meta': {'object_name': 'UserTestGroup'},
|
||||
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user