From 2cb2233dded5ab698d37836ccc1012e93b7ad0d0 Mon Sep 17 00:00:00 2001 From: Zia Fazal <zia.fazal@arbisoft.com> Date: Tue, 8 Dec 2015 16:41:15 +0500 Subject: [PATCH] initial change set. add org autocomplete added unit tests and fixed JS tests added bok choy tests fixed broken botchoy tests fixed course discovery broken test removed monkey patch at class level changes after feedback from matte --- .../tests/test_course_create_rerun.py | 75 +++++++++- cms/djangoapps/contentstore/views/course.py | 14 ++ .../contentstore/views/organization.py | 23 +++ .../views/tests/test_organizations.py | 32 ++++ cms/envs/bok_choy.py | 2 + cms/envs/common.py | 5 + cms/static/js/index.js | 2 +- cms/static/js/spec/views/pages/index_spec.js | 6 + .../js/views/utils/create_course_utils.js | 8 + cms/static/sass/elements/_vendor.scss | 32 ++++ cms/urls.py | 3 +- .../djangoapps/util/organizations_helpers.py | 33 ++++- .../util/tests/test_organizations_helpers.py | 23 +++ common/test/acceptance/pages/studio/index.py | 88 +++++++++++ .../tests/lms/test_lms_course_discovery.py | 24 +-- .../tests/studio/test_studio_course_create.py | 140 ++++++++++++++++++ .../test/db_fixtures/edx-organizations.json | 46 ++++++ requirements/edx/github.txt | 2 +- 18 files changed, 527 insertions(+), 31 deletions(-) create mode 100644 cms/djangoapps/contentstore/views/organization.py create mode 100644 cms/djangoapps/contentstore/views/tests/test_organizations.py create mode 100644 common/test/acceptance/tests/studio/test_studio_course_create.py create mode 100644 common/test/db_fixtures/edx-organizations.json diff --git a/cms/djangoapps/contentstore/tests/test_course_create_rerun.py b/cms/djangoapps/contentstore/tests/test_course_create_rerun.py index 4db95d3bed1..e70fc9a6714 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 e40fbe5ea73..3e826809715 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 00000000000..13ac777ee90 --- /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 00000000000..3ba81f36b4c --- /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 20671f9c7b6..765920aba18 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 71493e06ec4..2f203dc4d33 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 243c6021251..8f8d74eb2f7 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 e9a1d4310a4..4d857e151cf 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 d294001291b..b9cc0abcdbf 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 e28ec307ce0..9795e24693f 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 2439987c541..338d241f6f6 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 65f3673d107..5164ae3dd9b 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 8a93a951290..3c607401ea6 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 3ced5bf2f9c..d38172a731d 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 8bfe0447700..9f232f35fe8 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 00000000000..c9625b3e529 --- /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 00000000000..f16fe7afc60 --- /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 f0a4de00510..b3e0ea0085d 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 -- GitLab