diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 1f14e29083..d910d73085 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -41,11 +41,14 @@ def i_press_the_category_delete_icon(step, category): ####### HELPER FUNCTIONS ############## def create_studio_user( uname='robot', - em='robot+studio@edx.org', - password='test'): + email='robot+studio@edx.org', + password='test', + is_staff=False): studio_user = UserFactory.build( username=uname, - email=em) + email=email, + password=password, + is_staff=is_staff) studio_user.set_password(password) studio_user.save() @@ -91,11 +94,12 @@ def fill_in_course_info( def log_into_studio( uname='robot', email='robot+studio@edx.org', - password='test'): - create_studio_user(uname, email) + password='test', + is_staff=False): + create_studio_user(uname=uname, email=email, is_staff=is_staff) world.browser.cookies.delete() world.browser.visit(django_url('/')) - world.browser.is_element_present_by_css('body.no-header', 10) + world.browser.is_element_present_by_css('body.no-header', 10) login_form = world.browser.find_by_css('form#login_form') login_form.find_by_name('email').fill(email) diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature new file mode 100644 index 0000000000..d9d7414648 --- /dev/null +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature @@ -0,0 +1,59 @@ +Feature: Overview Toggle Section + In order to quickly view the details of a course's section or to scan the inventory of sections + As a course author + I want to toggle the visibility of each section's subsection details in the overview listing + + Scenario: The default layout for the overview page is to show sections in expanded view + Given I have a course with multiple sections + When I navigate to the course overview page + Then I see the "Collapse All Sections" link + And all sections are expanded + + Scenario: Expand/collapse for a course with no sections + Given I have a course with no sections + When I navigate to the course overview page + Then I do not see the "Collapse All Sections" link + + Scenario: Collapse link appears after creating first section of a course + Given I have a course with no sections + When I navigate to the course overview page + And I add a section + Then I see the "Collapse All Sections" link + And all sections are expanded + + Scenario: Collapse link is removed after last section of a course is deleted + Given I have a course with 1 section + And I navigate to the course overview page + When I press the "section" delete icon + And I confirm the alert + Then I do not see the "Collapse All Sections" link + + Scenario: Collapsing all sections when all sections are expanded + Given I navigate to the courseware page of a course with multiple sections + And all sections are expanded + When I click the "Collapse All Sections" link + Then I see the "Expand All Sections" link + And all sections are collapsed + + Scenario: Collapsing all sections when 1 or more sections are already collapsed + Given I navigate to the courseware page of a course with multiple sections + And all sections are expanded + When I collapse the first section + And I click the "Collapse All Sections" link + Then I see the "Expand All Sections" link + And all sections are collapsed + + Scenario: Expanding all sections when all sections are collapsed + Given I navigate to the courseware page of a course with multiple sections + And I click the "Collapse All Sections" link + When I click the "Expand All Sections" link + Then I see the "Collapse All Sections" link + And all sections are expanded + + Scenario: Expanding all sections when 1 or more sections are already expanded + Given I navigate to the courseware page of a course with multiple sections + And I click the "Collapse All Sections" link + When I expand the first section + And I click the "Expand All Sections" link + Then I see the "Collapse All Sections" link + And all sections are expanded \ No newline at end of file diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py new file mode 100644 index 0000000000..010678c0e8 --- /dev/null +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py @@ -0,0 +1,104 @@ +from lettuce import world, step +from terrain.factories import * +from common import * +from nose.tools import assert_true, assert_false, assert_equal + +from logging import getLogger +logger = getLogger(__name__) + +@step(u'I have a course with no sections$') +def have_a_course(step): + clear_courses() + course = CourseFactory.create() + +@step(u'I have a course with 1 section$') +def have_a_course_with_1_section(step): + clear_courses() + course = CourseFactory.create() + section = ItemFactory.create(parent_location=course.location) + subsection1 = ItemFactory.create( + parent_location=section.location, + template = 'i4x://edx/templates/sequential/Empty', + display_name = 'Subsection One',) + +@step(u'I have a course with multiple sections$') +def have_a_course_with_two_sections(step): + clear_courses() + course = CourseFactory.create() + section = ItemFactory.create(parent_location=course.location) + subsection1 = ItemFactory.create( + parent_location=section.location, + template = 'i4x://edx/templates/sequential/Empty', + display_name = 'Subsection One',) + section2 = ItemFactory.create( + parent_location=course.location, + display_name='Section Two',) + subsection2 = ItemFactory.create( + parent_location=section2.location, + template = 'i4x://edx/templates/sequential/Empty', + display_name = 'Subsection Alpha',) + subsection3 = ItemFactory.create( + parent_location=section2.location, + template = 'i4x://edx/templates/sequential/Empty', + display_name = 'Subsection Beta',) + +@step(u'I navigate to the course overview page$') +def navigate_to_the_course_overview_page(step): + log_into_studio(is_staff=True) + course_locator = '.class-name' + css_click(course_locator) + +@step(u'I navigate to the courseware page of a course with multiple sections') +def nav_to_the_courseware_page_of_a_course_with_multiple_sections(step): + step.given('I have a course with multiple sections') + step.given('I navigate to the course overview page') + +@step(u'I add a section') +def i_add_a_section(step): + add_section(name='My New Section That I Just Added') + +@step(u'I click the "([^"]*)" link$') +def i_click_the_text_span(step, text): + span_locator = '.toggle-button-sections span' + assert_true(world.browser.is_element_present_by_css(span_locator, 5)) + # first make sure that the expand/collapse text is the one you expected + assert_equal(world.browser.find_by_css(span_locator).value, text) + css_click(span_locator) + +@step(u'I collapse the first section$') +def i_collapse_a_section(step): + collapse_locator = 'section.courseware-section a.collapse' + css_click(collapse_locator) + +@step(u'I expand the first section$') +def i_expand_a_section(step): + expand_locator = 'section.courseware-section a.expand' + css_click(expand_locator) + +@step(u'I see the "([^"]*)" link$') +def i_see_the_span_with_text(step, text): + span_locator = '.toggle-button-sections span' + assert_true(world.browser.is_element_present_by_css(span_locator, 5)) + assert_equal(world.browser.find_by_css(span_locator).value, text) + assert_true(world.browser.find_by_css(span_locator).visible) + +@step(u'I do not see the "([^"]*)" link$') +def i_do_not_see_the_span_with_text(step, text): + # Note that the span will exist on the page but not be visible + span_locator = '.toggle-button-sections span' + assert_true(world.browser.is_element_present_by_css(span_locator)) + assert_false(world.browser.find_by_css(span_locator).visible) + +@step(u'all sections are expanded$') +def all_sections_are_expanded(step): + subsection_locator = 'div.subsection-list' + subsections = world.browser.find_by_css(subsection_locator) + for s in subsections: + assert_true(s.visible) + +@step(u'all sections are collapsed$') +def all_sections_are_expanded(step): + subsection_locator = 'div.subsection-list' + subsections = world.browser.find_by_css(subsection_locator) + for s in subsections: + assert_false(s.visible) \ No newline at end of file diff --git a/lms/djangoapps/terrain/factories.py b/lms/djangoapps/terrain/factories.py index ddab9e2b06..377ce54d56 100644 --- a/lms/djangoapps/terrain/factories.py +++ b/lms/djangoapps/terrain/factories.py @@ -1,9 +1,13 @@ -import factory from student.models import User, UserProfile, Registration from datetime import datetime -import uuid +from factory import Factory +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from time import gmtime +from uuid import uuid4 +from xmodule.timeparse import stringify_time -class UserProfileFactory(factory.Factory): +class UserProfileFactory(Factory): FACTORY_FOR = UserProfile user = None @@ -13,13 +17,13 @@ class UserProfileFactory(factory.Factory): mailing_address = None goals = 'World domination' -class RegistrationFactory(factory.Factory): +class RegistrationFactory(Factory): FACTORY_FOR = Registration user = None - activation_key = uuid.uuid4().hex + activation_key = uuid4().hex -class UserFactory(factory.Factory): +class UserFactory(Factory): FACTORY_FOR = User username = 'robot' @@ -32,3 +36,113 @@ class UserFactory(factory.Factory): is_superuser = False last_login = datetime(2012, 1, 1) date_joined = datetime(2011, 1, 1) + +def XMODULE_COURSE_CREATION(class_to_create, **kwargs): + return XModuleCourseFactory._create(class_to_create, **kwargs) + +def XMODULE_ITEM_CREATION(class_to_create, **kwargs): + return XModuleItemFactory._create(class_to_create, **kwargs) + +class XModuleCourseFactory(Factory): + """ + Factory for XModule courses. + """ + + ABSTRACT_FACTORY = True + _creation_function = (XMODULE_COURSE_CREATION,) + + @classmethod + def _create(cls, target_class, *args, **kwargs): + + template = Location('i4x', 'edx', 'templates', 'course', 'Empty') + org = kwargs.get('org') + number = kwargs.get('number') + display_name = kwargs.get('display_name') + location = Location('i4x', org, number, + 'course', Location.clean(display_name)) + + store = modulestore('direct') + + # Write the data to the mongo datastore + new_course = store.clone_item(template, location) + + # This metadata code was copied from cms/djangoapps/contentstore/views.py + if display_name is not None: + new_course.metadata['display_name'] = display_name + + new_course.metadata['data_dir'] = uuid4().hex + new_course.metadata['start'] = stringify_time(gmtime()) + new_course.tabs = [{"type": "courseware"}, + {"type": "course_info", "name": "Course Info"}, + {"type": "discussion", "name": "Discussion"}, + {"type": "wiki", "name": "Wiki"}, + {"type": "progress", "name": "Progress"}] + + # Update the data in the mongo datastore + store.update_metadata(new_course.location.url(), new_course.own_metadata) + + return new_course + +class Course: + pass + +class CourseFactory(XModuleCourseFactory): + FACTORY_FOR = Course + + template = 'i4x://edx/templates/course/Empty' + org = 'MITx' + number = '999' + display_name = 'Robot Super Course' + +class XModuleItemFactory(Factory): + """ + Factory for XModule items. + """ + + ABSTRACT_FACTORY = True + _creation_function = (XMODULE_ITEM_CREATION,) + + @classmethod + def _create(cls, target_class, *args, **kwargs): + """ + kwargs must include parent_location, template. Can contain display_name + target_class is ignored + """ + + DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] + + parent_location = Location(kwargs.get('parent_location')) + template = Location(kwargs.get('template')) + display_name = kwargs.get('display_name') + + store = modulestore('direct') + + # This code was based off that in cms/djangoapps/contentstore/views.py + parent = store.get_item(parent_location) + dest_location = parent_location._replace(category=template.category, name=uuid4().hex) + + new_item = store.clone_item(template, dest_location) + + # TODO: This needs to be deleted when we have proper storage for static content + new_item.metadata['data_dir'] = parent.metadata['data_dir'] + + # replace the display name with an optional parameter passed in from the caller + if display_name is not None: + new_item.metadata['display_name'] = display_name + + store.update_metadata(new_item.location.url(), new_item.own_metadata) + + if new_item.location.category not in DETACHED_CATEGORIES: + store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()]) + + return new_item + +class Item: + pass + +class ItemFactory(XModuleItemFactory): + FACTORY_FOR = Item + + parent_location = 'i4x://MITx/999/course/Robot_Super_Course' + template = 'i4x://edx/templates/chapter/Empty' + display_name = 'Section One' \ No newline at end of file