Merge pull request #10948 from edx/ziafazal/SOL-1375
ziafazal/SOL-1375: Create course to org link at the time of course creation
This commit is contained in:
@@ -2,8 +2,10 @@
|
||||
Test view handler for rerun (and eventually create)
|
||||
"""
|
||||
import ddt
|
||||
from mock import patch
|
||||
|
||||
from django.test.client import RequestFactory
|
||||
from django.core.urlresolvers import reverse
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
@@ -15,6 +17,10 @@ from student.tests.factories import UserFactory
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient, parse_json
|
||||
from datetime import datetime
|
||||
from xmodule.course_module import CourseFields
|
||||
from util.organizations_helpers import (
|
||||
add_organization,
|
||||
get_course_organizations,
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -33,7 +39,7 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self.factory = RequestFactory()
|
||||
self.client = AjaxEnabledTestClient()
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
self.course_create_rerun_url = reverse('course_handler')
|
||||
source_course = CourseFactory.create(
|
||||
org='origin',
|
||||
number='the_beginning',
|
||||
@@ -57,7 +63,7 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
"""
|
||||
Just testing the functionality the view handler adds over the tasks tested in test_clone_course
|
||||
"""
|
||||
response = self.client.ajax_post('/course/', {
|
||||
response = self.client.ajax_post(self.course_create_rerun_url, {
|
||||
'source_course_key': unicode(self.source_course_key),
|
||||
'org': self.source_course_key.org, 'course': self.source_course_key.course, 'run': 'copy',
|
||||
'display_name': 'not the same old name',
|
||||
@@ -76,7 +82,7 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
Tests newly created course has web certs enabled by default.
|
||||
"""
|
||||
with modulestore().default_store(store):
|
||||
response = self.client.ajax_post('/course/', {
|
||||
response = self.client.ajax_post(self.course_create_rerun_url, {
|
||||
'org': 'orgX',
|
||||
'number': 'CS101',
|
||||
'display_name': 'Course with web certs enabled',
|
||||
@@ -87,3 +93,66 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
new_course_key = CourseKey.from_string(data['course_key'])
|
||||
course = self.store.get_course(new_course_key)
|
||||
self.assertTrue(course.cert_html_view_enabled)
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': False})
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_course_creation_without_org_app_enabled(self, store):
|
||||
"""
|
||||
Tests course creation workflow should not create course to org
|
||||
link if organizations_app is not enabled.
|
||||
"""
|
||||
with modulestore().default_store(store):
|
||||
response = self.client.ajax_post(self.course_create_rerun_url, {
|
||||
'org': 'orgX',
|
||||
'number': 'CS101',
|
||||
'display_name': 'Course with web certs enabled',
|
||||
'run': '2015_T2'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = parse_json(response)
|
||||
new_course_key = CourseKey.from_string(data['course_key'])
|
||||
course_orgs = get_course_organizations(new_course_key)
|
||||
self.assertEqual(course_orgs, [])
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': True})
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_course_creation_with_org_not_in_system(self, store):
|
||||
"""
|
||||
Tests course creation workflow when course organization does not exist
|
||||
in system.
|
||||
"""
|
||||
with modulestore().default_store(store):
|
||||
response = self.client.ajax_post(self.course_create_rerun_url, {
|
||||
'org': 'orgX',
|
||||
'number': 'CS101',
|
||||
'display_name': 'Course with web certs enabled',
|
||||
'run': '2015_T2'
|
||||
})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
data = parse_json(response)
|
||||
self.assertIn(u'Organization you selected does not exist in the system', data['error'])
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': True})
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_course_creation_with_org_in_system(self, store):
|
||||
"""
|
||||
Tests course creation workflow when course organization exist in system.
|
||||
"""
|
||||
add_organization({
|
||||
'name': 'Test Organization',
|
||||
'short_name': 'orgX',
|
||||
'description': 'Testing Organization Description',
|
||||
})
|
||||
with modulestore().default_store(store):
|
||||
response = self.client.ajax_post(self.course_create_rerun_url, {
|
||||
'org': 'orgX',
|
||||
'number': 'CS101',
|
||||
'display_name': 'Course with web certs enabled',
|
||||
'run': '2015_T2'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = parse_json(response)
|
||||
new_course_key = CourseKey.from_string(data['course_key'])
|
||||
course_orgs = get_course_organizations(new_course_key)
|
||||
self.assertEqual(len(course_orgs), 1)
|
||||
self.assertEqual(course_orgs[0]['short_name'], 'orgX')
|
||||
|
||||
@@ -84,6 +84,11 @@ from util.milestones_helpers import (
|
||||
is_valid_course_key,
|
||||
set_prerequisite_courses,
|
||||
)
|
||||
from util.organizations_helpers import (
|
||||
add_organization_course,
|
||||
get_organization_by_short_name,
|
||||
organizations_enabled,
|
||||
)
|
||||
from util.string_utils import _has_non_ascii_characters
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.course_module import CourseFields
|
||||
@@ -738,8 +743,17 @@ def _create_new_course(request, org, number, run, fields):
|
||||
Returns the URL for the course overview page.
|
||||
Raises DuplicateCourseError if the course already exists
|
||||
"""
|
||||
org_data = get_organization_by_short_name(org)
|
||||
if not org_data and organizations_enabled():
|
||||
return JsonResponse(
|
||||
{'error': _('You must link this course to an organization in order to continue. '
|
||||
'Organization you selected does not exist in the system, '
|
||||
'you will need to add it to the system')},
|
||||
status=400
|
||||
)
|
||||
store_for_new_course = modulestore().default_modulestore.get_modulestore_type()
|
||||
new_course = create_new_course_in_store(store_for_new_course, request.user, org, number, run, fields)
|
||||
add_organization_course(org_data, new_course.id)
|
||||
return JsonResponse({
|
||||
'url': reverse_course_url('course_handler', new_course.id),
|
||||
'course_key': unicode(new_course.id),
|
||||
|
||||
23
cms/djangoapps/contentstore/views/organization.py
Normal file
23
cms/djangoapps/contentstore/views/organization.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Organizations views for use with Studio."""
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import View
|
||||
from django.http import HttpResponse
|
||||
|
||||
from openedx.core.lib.js_utils import escape_json_dumps
|
||||
from util.organizations_helpers import get_organizations
|
||||
|
||||
|
||||
class OrganizationListView(View):
|
||||
"""View rendering organization list as json.
|
||||
|
||||
This view renders organization list json which is used in org
|
||||
autocomplete while creating new course.
|
||||
"""
|
||||
|
||||
@method_decorator(login_required)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Returns organization list as json."""
|
||||
organizations = get_organizations()
|
||||
org_names_list = [(org["short_name"]) for org in organizations]
|
||||
return HttpResponse(escape_json_dumps(org_names_list), content_type='application/json; charset=utf-8')
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Tests covering the Organizations listing on the Studio home."""
|
||||
import json
|
||||
from mock import patch
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from util.organizations_helpers import add_organization
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': True})
|
||||
class TestOrganizationListing(TestCase):
|
||||
"""Verify Organization listing behavior."""
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': True})
|
||||
def setUp(self):
|
||||
super(TestOrganizationListing, self).setUp()
|
||||
self.staff = UserFactory(is_staff=True)
|
||||
self.client.login(username=self.staff.username, password='test')
|
||||
self.org_names_listing_url = reverse('organizations')
|
||||
self.org_short_names = ["alphaX", "betaX", "orgX"]
|
||||
for index, short_name in enumerate(self.org_short_names):
|
||||
add_organization(organization_data={
|
||||
'name': 'Test Organization %s' % index,
|
||||
'short_name': short_name,
|
||||
'description': 'Testing Organization %s Description' % index,
|
||||
})
|
||||
|
||||
def test_organization_list(self):
|
||||
"""Verify that the organization names list api returns list of organization short names."""
|
||||
response = self.client.get(self.org_names_listing_url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
org_names = json.loads(response.content)
|
||||
self.assertEqual(org_names, self.org_short_names)
|
||||
@@ -109,6 +109,8 @@ YOUTUBE['TEXT_API']['url'] = "127.0.0.1:{0}/test_transcripts_youtube/".format(YO
|
||||
|
||||
FEATURES['ENABLE_COURSEWARE_INDEX'] = True
|
||||
FEATURES['ENABLE_LIBRARY_INDEX'] = True
|
||||
|
||||
FEATURES['ORGANIZATIONS_APP'] = True
|
||||
SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine"
|
||||
# Path at which to store the mock index
|
||||
MOCK_SEARCH_BACKING_FILE = (
|
||||
|
||||
@@ -180,6 +180,8 @@ FEATURES = {
|
||||
|
||||
# Special Exams, aka Timed and Proctored Exams
|
||||
'ENABLE_SPECIAL_EXAMS': False,
|
||||
|
||||
'ORGANIZATIONS_APP': False,
|
||||
}
|
||||
|
||||
ENABLE_JASMINE = False
|
||||
@@ -924,6 +926,9 @@ OPTIONAL_APPS = (
|
||||
|
||||
# milestones
|
||||
'milestones',
|
||||
|
||||
# Organizations App (http://github.com/edx/edx-organizations)
|
||||
'organizations',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ define(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape", "js/vie
|
||||
$('.new-course-save').on('click', saveNewCourse);
|
||||
$cancelButton.bind('click', makeCancelHandler('course'));
|
||||
CancelOnEscape($cancelButton);
|
||||
|
||||
CreateCourseUtils.setupOrgAutocomplete();
|
||||
CreateCourseUtils.configureHandlers();
|
||||
};
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
var redirectSpy = spyOn(ViewUtils, 'redirect');
|
||||
$('.new-course-button').click()
|
||||
AjaxHelpers.expectJsonRequest(requests, 'GET', '/organizations');
|
||||
AjaxHelpers.respondWithJson(requests, ['DemoX', 'DemoX2', 'DemoX3']);
|
||||
fillInFields('DemoX', 'DM101', '2014', 'Demo course');
|
||||
$('.new-course-save').click();
|
||||
AjaxHelpers.expectJsonRequest(requests, 'POST', '/course/', {
|
||||
@@ -53,11 +55,14 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers
|
||||
url: 'dummy_test_url'
|
||||
});
|
||||
expect(redirectSpy).toHaveBeenCalledWith('dummy_test_url');
|
||||
$(".new-course-org").autocomplete("destroy");
|
||||
});
|
||||
|
||||
it("displays an error when saving fails", function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
$('.new-course-button').click();
|
||||
AjaxHelpers.expectJsonRequest(requests, 'GET', '/organizations');
|
||||
AjaxHelpers.respondWithJson(requests, ['DemoX', 'DemoX2', 'DemoX3']);
|
||||
fillInFields('DemoX', 'DM101', '2014', 'Demo course');
|
||||
$('.new-course-save').click();
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
@@ -67,6 +72,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers
|
||||
expect($('#course_creation_error')).toContainText('error message');
|
||||
expect($('.new-course-save')).toHaveClass('is-disabled');
|
||||
expect($('.new-course-save')).toHaveAttr('aria-disabled', 'true');
|
||||
$(".new-course-org").autocomplete("destroy");
|
||||
});
|
||||
|
||||
it("saves new libraries", function () {
|
||||
|
||||
@@ -11,6 +11,14 @@ define(["jquery", "gettext", "common/js/components/utils/view_utils", "js/views/
|
||||
|
||||
CreateUtilsFactory.call(this, selectors, classes, keyLengthViolationMessage, keyFieldSelectors, nonEmptyCheckFieldSelectors);
|
||||
|
||||
this.setupOrgAutocomplete = function(){
|
||||
$.getJSON('/organizations', function (data) {
|
||||
$(selectors.org).autocomplete({
|
||||
source: data
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.create = function (courseInfo, errorHandler) {
|
||||
$.postJSON(
|
||||
'/course/',
|
||||
|
||||
@@ -157,3 +157,35 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-autocomplete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: default;
|
||||
@include linear-gradient($gray-l5, $white);
|
||||
border-right: 1px solid $gray-l2;
|
||||
border-bottom: 1px solid $gray-l2;
|
||||
border-left: 1px solid $gray-l2;
|
||||
background-color: $gray-l5;
|
||||
box-shadow: inset 0 1px 2px $shadow-l1;
|
||||
color: $color-copy-emphasized;
|
||||
|
||||
li.ui-menu-item{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
color: $color-copy-emphasized;
|
||||
}
|
||||
|
||||
a.ui-state-focus{
|
||||
border: none;
|
||||
background-color: $blue;
|
||||
background: $blue;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.conf.urls import patterns, include, url
|
||||
from ratelimitbackend import admin
|
||||
|
||||
from cms.djangoapps.contentstore.views.program import ProgramAuthoringView, ProgramsIdTokenView
|
||||
|
||||
from cms.djangoapps.contentstore.views.organization import OrganizationListView
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
@@ -41,6 +41,7 @@ urlpatterns = patterns(
|
||||
|
||||
url(r'^not_found$', 'contentstore.views.not_found', name='not_found'),
|
||||
url(r'^server_error$', 'contentstore.views.server_error', name='server_error'),
|
||||
url(r'^organizations$', OrganizationListView.as_view(), name='organizations'),
|
||||
|
||||
# temporary landing page for edge
|
||||
url(r'^edge$', 'contentstore.views.edge', name='edge'),
|
||||
|
||||
@@ -10,7 +10,7 @@ def add_organization(organization_data):
|
||||
"""
|
||||
Client API operation adapter/wrapper
|
||||
"""
|
||||
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
|
||||
if not organizations_enabled():
|
||||
return None
|
||||
from organizations import api as organizations_api
|
||||
return organizations_api.add_organization(organization_data=organization_data)
|
||||
@@ -20,7 +20,7 @@ def add_organization_course(organization_data, course_id):
|
||||
"""
|
||||
Client API operation adapter/wrapper
|
||||
"""
|
||||
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
|
||||
if not organizations_enabled():
|
||||
return None
|
||||
from organizations import api as organizations_api
|
||||
return organizations_api.add_organization_course(organization_data=organization_data, course_key=course_id)
|
||||
@@ -30,17 +30,31 @@ def get_organization(organization_id):
|
||||
"""
|
||||
Client API operation adapter/wrapper
|
||||
"""
|
||||
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
|
||||
if not organizations_enabled():
|
||||
return []
|
||||
from organizations import api as organizations_api
|
||||
return organizations_api.get_organization(organization_id)
|
||||
|
||||
|
||||
def get_organization_by_short_name(organization_short_name):
|
||||
"""
|
||||
Client API operation adapter/wrapper
|
||||
"""
|
||||
if not organizations_enabled():
|
||||
return None
|
||||
from organizations import api as organizations_api
|
||||
from organizations.exceptions import InvalidOrganizationException
|
||||
try:
|
||||
return organizations_api.get_organization_by_short_name(organization_short_name)
|
||||
except InvalidOrganizationException:
|
||||
return None
|
||||
|
||||
|
||||
def get_organizations():
|
||||
"""
|
||||
Client API operation adapter/wrapper
|
||||
"""
|
||||
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
|
||||
if not organizations_enabled():
|
||||
return []
|
||||
from organizations import api as organizations_api
|
||||
# Due to the way unit tests run for edx-platform, models are not yet available at the time
|
||||
@@ -58,7 +72,7 @@ def get_organization_courses(organization_id):
|
||||
"""
|
||||
Client API operation adapter/wrapper
|
||||
"""
|
||||
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
|
||||
if not organizations_enabled():
|
||||
return []
|
||||
from organizations import api as organizations_api
|
||||
return organizations_api.get_organization_courses(organization_id)
|
||||
@@ -68,7 +82,14 @@ def get_course_organizations(course_id):
|
||||
"""
|
||||
Client API operation adapter/wrapper
|
||||
"""
|
||||
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
|
||||
if not organizations_enabled():
|
||||
return []
|
||||
from organizations import api as organizations_api
|
||||
return organizations_api.get_course_organizations(course_id)
|
||||
|
||||
|
||||
def organizations_enabled():
|
||||
"""
|
||||
Returns boolean indication if organizations app is enabled on not.
|
||||
"""
|
||||
return settings.FEATURES.get('ORGANIZATIONS_APP', False)
|
||||
|
||||
@@ -23,6 +23,7 @@ class OrganizationsHelpersTestCase(ModuleStoreTestCase):
|
||||
|
||||
self.organization = {
|
||||
'name': 'Test Organization',
|
||||
'short_name': 'Orgx',
|
||||
'description': 'Testing Organization Helpers Library',
|
||||
}
|
||||
|
||||
@@ -49,3 +50,25 @@ class OrganizationsHelpersTestCase(ModuleStoreTestCase):
|
||||
def test_add_organization_course_returns_none_when_app_disabled(self):
|
||||
response = organizations_helpers.add_organization_course(self.organization, self.course.id)
|
||||
self.assertIsNone(response)
|
||||
|
||||
def test_get_organization_by_short_name_when_app_disabled(self):
|
||||
"""
|
||||
Tests get_organization_by_short_name api when app is disabled.
|
||||
"""
|
||||
response = organizations_helpers.get_organization_by_short_name(self.organization['short_name'])
|
||||
self.assertIsNone(response)
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': True})
|
||||
def test_get_organization_by_short_name_when_app_enabled(self):
|
||||
"""
|
||||
Tests get_organization_by_short_name api when app is enabled.
|
||||
"""
|
||||
response = organizations_helpers.add_organization(organization_data=self.organization)
|
||||
self.assertIsNotNone(response['id'])
|
||||
|
||||
response = organizations_helpers.get_organization_by_short_name(self.organization['short_name'])
|
||||
self.assertIsNotNone(response['id'])
|
||||
|
||||
# fetch non existing org
|
||||
response = organizations_helpers.get_organization_by_short_name('non_existing')
|
||||
self.assertIsNone(response)
|
||||
|
||||
@@ -85,6 +85,85 @@ class DashboardPage(PageObject):
|
||||
"""
|
||||
self.q(css='.wrapper-create-library .new-library-save').click()
|
||||
|
||||
@property
|
||||
def new_course_button(self):
|
||||
"""
|
||||
Returns "New Course" button.
|
||||
"""
|
||||
return self.q(css='.new-course-button')
|
||||
|
||||
def is_new_course_form_visible(self):
|
||||
"""
|
||||
Is the new course form visible?
|
||||
"""
|
||||
return self.q(css='.wrapper-create-course').visible
|
||||
|
||||
def click_new_course_button(self):
|
||||
"""
|
||||
Click "New Course" button
|
||||
"""
|
||||
self.q(css='.new-course-button').first.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def fill_new_course_form(self, display_name, org, number, run):
|
||||
"""
|
||||
Fill out the form to create a new course.
|
||||
"""
|
||||
field = lambda fn: self.q(css='.wrapper-create-course #new-course-{}'.format(fn))
|
||||
field('name').fill(display_name)
|
||||
field('org').fill(org)
|
||||
field('number').fill(number)
|
||||
field('run').fill(run)
|
||||
|
||||
def is_new_course_form_valid(self):
|
||||
"""
|
||||
Returns `True` if new course form is valid otherwise `False`.
|
||||
"""
|
||||
return (
|
||||
self.q(css='.wrapper-create-course .new-course-save:not(.is-disabled)').present and
|
||||
not self.q(css='.wrapper-create-course .wrap-error.is-shown').present
|
||||
)
|
||||
|
||||
def submit_new_course_form(self):
|
||||
"""
|
||||
Submit the new course form.
|
||||
"""
|
||||
self.q(css='.wrapper-create-course .new-course-save').first.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
@property
|
||||
def error_notification(self):
|
||||
"""
|
||||
Returns error notification element.
|
||||
"""
|
||||
return self.q(css='.wrapper-notification-error.is-shown')
|
||||
|
||||
@property
|
||||
def error_notification_message(self):
|
||||
"""
|
||||
Returns text of error message.
|
||||
"""
|
||||
self.wait_for_element_visibility(
|
||||
".wrapper-notification-error.is-shown .message", "Error message is visible"
|
||||
)
|
||||
return self.error_notification.results[0].find_element_by_css_selector('.message').text
|
||||
|
||||
@property
|
||||
def course_org_field(self):
|
||||
"""
|
||||
Returns course organization input.
|
||||
"""
|
||||
return self.q(css='.wrapper-create-course #new-course-org')
|
||||
|
||||
def select_item_in_autocomplete_widget(self, item_text):
|
||||
"""
|
||||
Selects item in autocomplete where text of item matches item_text.
|
||||
"""
|
||||
self.wait_for_element_visibility(
|
||||
".ui-autocomplete .ui-menu-item", "Autocomplete widget is visible"
|
||||
)
|
||||
self.q(css='.ui-autocomplete .ui-menu-item a').filter(lambda el: el.text == item_text)[0].click()
|
||||
|
||||
def list_courses(self):
|
||||
"""
|
||||
List all the courses found on the page's list of libraries.
|
||||
@@ -102,6 +181,15 @@ class DashboardPage(PageObject):
|
||||
}
|
||||
return self.q(css='.courses li.course-item').map(div2info).results
|
||||
|
||||
def has_course(self, org, number, run):
|
||||
"""
|
||||
Returns `True` if course for given org, number and run exists on the page otherwise `False`
|
||||
"""
|
||||
for course in self.list_courses():
|
||||
if course['org'] == org and course['number'] == number and course['run'] == run:
|
||||
return True
|
||||
return False
|
||||
|
||||
def list_libraries(self):
|
||||
"""
|
||||
Click the tab to display the available libraries, and return detail of them.
|
||||
|
||||
@@ -3,6 +3,7 @@ Test course discovery.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from bok_choy.web_app_test import WebAppTest
|
||||
from ..helpers import remove_file
|
||||
@@ -34,29 +35,14 @@ class CourseDiscoveryTest(WebAppTest):
|
||||
super(CourseDiscoveryTest, self).setUp()
|
||||
self.page = CourseDiscoveryPage(self.browser)
|
||||
|
||||
for i in range(10):
|
||||
org = self.unique_id
|
||||
number = unicode(i)
|
||||
for i in range(12):
|
||||
org = 'test_org'
|
||||
number = "{}{}".format(str(i), str(uuid.uuid4().get_hex().upper()[0:6]))
|
||||
run = "test_run"
|
||||
name = "test course"
|
||||
name = "test course" if i < 10 else "grass is always greener"
|
||||
settings = {'enrollment_start': datetime.datetime(1970, 1, 1).isoformat()}
|
||||
CourseFixture(org, number, run, name, settings=settings).install()
|
||||
|
||||
for i in range(2):
|
||||
org = self.unique_id
|
||||
number = unicode(i)
|
||||
run = "test_run"
|
||||
name = "grass is always greener"
|
||||
CourseFixture(
|
||||
org,
|
||||
number,
|
||||
run,
|
||||
name,
|
||||
settings={
|
||||
'enrollment_start': datetime.datetime(1970, 1, 1).isoformat()
|
||||
}
|
||||
).install()
|
||||
|
||||
def _auto_auth(self, username, email, staff):
|
||||
"""
|
||||
Logout and login with given credentials.
|
||||
|
||||
140
common/test/acceptance/tests/studio/test_studio_course_create.py
Normal file
140
common/test/acceptance/tests/studio/test_studio_course_create.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Acceptance tests for course creation.
|
||||
"""
|
||||
import uuid
|
||||
from bok_choy.web_app_test import WebAppTest
|
||||
|
||||
from ...pages.studio.auto_auth import AutoAuthPage
|
||||
from ...pages.studio.index import DashboardPage
|
||||
from ...pages.studio.overview import CourseOutlinePage
|
||||
|
||||
|
||||
class CreateCourseTest(WebAppTest):
|
||||
"""
|
||||
Test that we can create a new course the studio home page.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Load the helper for the home page (dashboard page)
|
||||
"""
|
||||
super(CreateCourseTest, self).setUp()
|
||||
|
||||
self.auth_page = AutoAuthPage(self.browser, staff=True)
|
||||
self.dashboard_page = DashboardPage(self.browser)
|
||||
self.course_name = "New Course Name"
|
||||
self.course_org = "orgX"
|
||||
self.course_number = str(uuid.uuid4().get_hex().upper()[0:6])
|
||||
self.course_run = "2015_T2"
|
||||
|
||||
def test_create_course_with_non_existing_org(self):
|
||||
"""
|
||||
Scenario: Ensure that the course creation with non existing org display proper error message.
|
||||
Given I have filled course creation form with a non existing and all required fields
|
||||
When I click 'Create' button
|
||||
Form validation should pass
|
||||
Then I see the error message explaining reason for failure to create course
|
||||
"""
|
||||
|
||||
self.auth_page.visit()
|
||||
self.dashboard_page.visit()
|
||||
self.assertFalse(self.dashboard_page.has_course(
|
||||
org='testOrg', number=self.course_number, run=self.course_run
|
||||
))
|
||||
self.assertTrue(self.dashboard_page.new_course_button.present)
|
||||
|
||||
self.dashboard_page.click_new_course_button()
|
||||
self.assertTrue(self.dashboard_page.is_new_course_form_visible())
|
||||
self.dashboard_page.fill_new_course_form(
|
||||
self.course_name, 'testOrg', self.course_number, self.course_run
|
||||
)
|
||||
self.assertTrue(self.dashboard_page.is_new_course_form_valid())
|
||||
self.dashboard_page.submit_new_course_form()
|
||||
self.assertTrue(self.dashboard_page.error_notification.present)
|
||||
self.assertIn(
|
||||
u'Organization you selected does not exist in the system', self.dashboard_page.error_notification_message
|
||||
)
|
||||
|
||||
def test_create_course_with_existing_org(self):
|
||||
"""
|
||||
Scenario: Ensure that the course creation with an existing org should be successful.
|
||||
Given I have filled course creation form with an existing org and all required fields
|
||||
When I click 'Create' button
|
||||
Form validation should pass
|
||||
Then I see the course listing page with newly created course
|
||||
"""
|
||||
|
||||
self.auth_page.visit()
|
||||
self.dashboard_page.visit()
|
||||
self.assertFalse(self.dashboard_page.has_course(
|
||||
org=self.course_org, number=self.course_number, run=self.course_run
|
||||
))
|
||||
self.assertTrue(self.dashboard_page.new_course_button.present)
|
||||
|
||||
self.dashboard_page.click_new_course_button()
|
||||
self.assertTrue(self.dashboard_page.is_new_course_form_visible())
|
||||
self.dashboard_page.fill_new_course_form(
|
||||
self.course_name, self.course_org, self.course_number, self.course_run
|
||||
)
|
||||
self.assertTrue(self.dashboard_page.is_new_course_form_valid())
|
||||
self.dashboard_page.submit_new_course_form()
|
||||
|
||||
# Successful creation of course takes user to course outline page
|
||||
course_outline_page = CourseOutlinePage(
|
||||
self.browser,
|
||||
self.course_org,
|
||||
self.course_number,
|
||||
self.course_run
|
||||
)
|
||||
course_outline_page.visit()
|
||||
course_outline_page.wait_for_page()
|
||||
|
||||
# Go back to dashboard and verify newly created course exists there
|
||||
self.dashboard_page.visit()
|
||||
self.assertTrue(self.dashboard_page.has_course(
|
||||
org=self.course_org, number=self.course_number, run=self.course_run
|
||||
))
|
||||
|
||||
def test_create_course_with_existing_org_via_autocomplete(self):
|
||||
"""
|
||||
Scenario: Ensure that the course creation with an existing org should be successful.
|
||||
Given I have filled course creation form with an existing org and all required fields
|
||||
And I selected `Course Organization` input via autocomplete
|
||||
When I click 'Create' button
|
||||
Form validation should pass
|
||||
Then I see the course listing page with newly created course
|
||||
"""
|
||||
|
||||
self.auth_page.visit()
|
||||
self.dashboard_page.visit()
|
||||
new_org = 'orgX2'
|
||||
self.assertFalse(self.dashboard_page.has_course(
|
||||
org=new_org, number=self.course_number, run=self.course_run
|
||||
))
|
||||
self.assertTrue(self.dashboard_page.new_course_button.present)
|
||||
|
||||
self.dashboard_page.click_new_course_button()
|
||||
self.assertTrue(self.dashboard_page.is_new_course_form_visible())
|
||||
self.dashboard_page.fill_new_course_form(
|
||||
self.course_name, '', self.course_number, self.course_run
|
||||
)
|
||||
self.dashboard_page.course_org_field.fill('org')
|
||||
self.dashboard_page.select_item_in_autocomplete_widget(new_org)
|
||||
self.assertTrue(self.dashboard_page.is_new_course_form_valid())
|
||||
self.dashboard_page.submit_new_course_form()
|
||||
|
||||
# Successful creation of course takes user to course outline page
|
||||
course_outline_page = CourseOutlinePage(
|
||||
self.browser,
|
||||
new_org,
|
||||
self.course_number,
|
||||
self.course_run
|
||||
)
|
||||
course_outline_page.visit()
|
||||
course_outline_page.wait_for_page()
|
||||
|
||||
# Go back to dashboard and verify newly created course exists there
|
||||
self.dashboard_page.visit()
|
||||
self.assertTrue(self.dashboard_page.has_course(
|
||||
org=new_org, number=self.course_number, run=self.course_run
|
||||
))
|
||||
46
common/test/db_fixtures/edx-organizations.json
Normal file
46
common/test/db_fixtures/edx-organizations.json
Normal file
@@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"pk": 99,
|
||||
"model": "organizations.organization",
|
||||
"fields": {
|
||||
"name": "Demo org 1",
|
||||
"short_name": "orgX",
|
||||
"description": "Description of organization 1",
|
||||
"logo": "org1_logo.png",
|
||||
"active": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 100,
|
||||
"model": "organizations.organization",
|
||||
"fields": {
|
||||
"name": "Demo org 2",
|
||||
"short_name": "orgX2",
|
||||
"description": "Description of organization 2",
|
||||
"logo": "org2_logo.png",
|
||||
"active": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 101,
|
||||
"model": "organizations.organization",
|
||||
"fields": {
|
||||
"name": "Demo org 3",
|
||||
"short_name": "orgX3",
|
||||
"description": "Description of organization 3",
|
||||
"logo": "org3_logo.png",
|
||||
"active": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 102,
|
||||
"model": "organizations.organization",
|
||||
"fields": {
|
||||
"name": "Demo org 4",
|
||||
"short_name": "test_org",
|
||||
"description": "Description of organization 4",
|
||||
"logo": "org4_logo.png",
|
||||
"active": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@ git+https://github.com/edx/xblock-utils.git@v1.0.0#egg=xblock-utils==v1.0.0
|
||||
-e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive
|
||||
-e git+https://github.com/edx/edx-reverification-block.git@0.0.5#egg=edx-reverification-block==0.0.5
|
||||
-e git+https://github.com/edx/edx-user-state-client.git@30c0ad4b9f57f8d48d6943eb585ec8a9205f4469#egg=edx-user-state-client
|
||||
git+https://github.com/edx/edx-organizations.git@release-2015-11-25#egg=edx-organizations==0.1.9
|
||||
git+https://github.com/edx/edx-organizations.git@release-2015-12-08#egg=edx-organizations==0.2.0
|
||||
git+https://github.com/edx/edx-proctoring.git@0.11.6#egg=edx-proctoring==0.11.6
|
||||
git+https://github.com/edx/xblock-lti-consumer.git@v1.0.0#egg=xblock-lti-consumer==v1.0.0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user