diff --git a/cms/djangoapps/contentstore/tests/test_course_create_rerun.py b/cms/djangoapps/contentstore/tests/test_course_create_rerun.py index 4db95d3bed12a1dcabea20b7c303ced6bf960df9..e70fc9a67142d68bd354177e10e53e44462c11c2 100644 --- a/cms/djangoapps/contentstore/tests/test_course_create_rerun.py +++ b/cms/djangoapps/contentstore/tests/test_course_create_rerun.py @@ -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') diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index e40fbe5ea7364e6517464739469d8cec7b89ad01..3e8268097150895ca657d9756c231195b0de87d0 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -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), diff --git a/cms/djangoapps/contentstore/views/organization.py b/cms/djangoapps/contentstore/views/organization.py new file mode 100644 index 0000000000000000000000000000000000000000..13ac777ee9096c9d3604dc95f7b5fe0be0c338f0 --- /dev/null +++ b/cms/djangoapps/contentstore/views/organization.py @@ -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') diff --git a/cms/djangoapps/contentstore/views/tests/test_organizations.py b/cms/djangoapps/contentstore/views/tests/test_organizations.py new file mode 100644 index 0000000000000000000000000000000000000000..3ba81f36b4cb97de93bb5e177abe5785a1b03f0f --- /dev/null +++ b/cms/djangoapps/contentstore/views/tests/test_organizations.py @@ -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) diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index 20671f9c7b66b3b465348c1c276b991a6490cf34..765920aba189b360ffd582aa51315a09aeaa9d69 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -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 = ( diff --git a/cms/envs/common.py b/cms/envs/common.py index 71493e06ec427a2e50a907574bba09afadbb42f3..2f203dc4d33479552fbc15d790523cf68cd0af58 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -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', ) diff --git a/cms/static/js/index.js b/cms/static/js/index.js index 243c6021251c5201b7e944fbf3dbd10d6a53d67b..8f8d74eb2f77efcd1b8b24adf78fcbd69999b4bd 100644 --- a/cms/static/js/index.js +++ b/cms/static/js/index.js @@ -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(); }; diff --git a/cms/static/js/spec/views/pages/index_spec.js b/cms/static/js/spec/views/pages/index_spec.js index e9a1d4310a4a85c3355c9d5fff648f427de5f0b6..4d857e151cf5dfd7f9ad8b3a75bff36b41d935a6 100644 --- a/cms/static/js/spec/views/pages/index_spec.js +++ b/cms/static/js/spec/views/pages/index_spec.js @@ -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 () { diff --git a/cms/static/js/views/utils/create_course_utils.js b/cms/static/js/views/utils/create_course_utils.js index d294001291b7e0bf700316a41d8723ded382ef45..b9cc0abcdbfbdec104dc123e12e79e0be9db77ae 100644 --- a/cms/static/js/views/utils/create_course_utils.js +++ b/cms/static/js/views/utils/create_course_utils.js @@ -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/', diff --git a/cms/static/sass/elements/_vendor.scss b/cms/static/sass/elements/_vendor.scss index e28ec307ce01f2a27bab4151d6269c14ea154ee4..9795e24693fbbd5ed0ebd75ff69a74f94f8d5c55 100644 --- a/cms/static/sass/elements/_vendor.scss +++ b/cms/static/sass/elements/_vendor.scss @@ -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; + } + } +} diff --git a/cms/urls.py b/cms/urls.py index 2439987c541d2b88599d78e9ac3abd3d2670fc0b..338d241f6f681a1d565cbd166701384ae609e6b6 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -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'), diff --git a/common/djangoapps/util/organizations_helpers.py b/common/djangoapps/util/organizations_helpers.py index 65f3673d1070fb0f6b0d4056a50a074d06edd834..5164ae3dd9b5e9545c8330dcebdee1b3be228d2d 100644 --- a/common/djangoapps/util/organizations_helpers.py +++ b/common/djangoapps/util/organizations_helpers.py @@ -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) diff --git a/common/djangoapps/util/tests/test_organizations_helpers.py b/common/djangoapps/util/tests/test_organizations_helpers.py index 8a93a9512908d22343412cb04f04f975f48cb71e..3c607401ea6f6aa3b4bea5f7700fd47c5ff29ed7 100644 --- a/common/djangoapps/util/tests/test_organizations_helpers.py +++ b/common/djangoapps/util/tests/test_organizations_helpers.py @@ -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) diff --git a/common/test/acceptance/pages/studio/index.py b/common/test/acceptance/pages/studio/index.py index 3ced5bf2f9c7266a40959027e9547f8b2a75504e..d38172a731d147d194c556953ca8bd793738077c 100644 --- a/common/test/acceptance/pages/studio/index.py +++ b/common/test/acceptance/pages/studio/index.py @@ -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. diff --git a/common/test/acceptance/tests/lms/test_lms_course_discovery.py b/common/test/acceptance/tests/lms/test_lms_course_discovery.py index 8bfe04477004d0e3510c8668b5ec5c1f9f93ff49..9f232f35fe8bbbd647451b619a08bfd14a02639c 100644 --- a/common/test/acceptance/tests/lms/test_lms_course_discovery.py +++ b/common/test/acceptance/tests/lms/test_lms_course_discovery.py @@ -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. diff --git a/common/test/acceptance/tests/studio/test_studio_course_create.py b/common/test/acceptance/tests/studio/test_studio_course_create.py new file mode 100644 index 0000000000000000000000000000000000000000..c9625b3e529ccb75010919d3c82db5fe5fadf2ee --- /dev/null +++ b/common/test/acceptance/tests/studio/test_studio_course_create.py @@ -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 + )) diff --git a/common/test/db_fixtures/edx-organizations.json b/common/test/db_fixtures/edx-organizations.json new file mode 100644 index 0000000000000000000000000000000000000000..f16fe7afc601e78da77d291d22df2aa61327a112 --- /dev/null +++ b/common/test/db_fixtures/edx-organizations.json @@ -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 + } + } +] diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index f0a4de005106a99c8491746c3359acb4435b2fe9..b3e0ea0085d9d8cb27b3ba2036a9998f10ade353 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -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