diff --git a/AUTHORS b/AUTHORS index 1af7349491..9bb4ede121 100644 --- a/AUTHORS +++ b/AUTHORS @@ -77,3 +77,4 @@ Slater Victoroff Peter Fogg Bethany LaPenta Renzo Lucioni +Felix Sun diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 69d35c8dfd..0e161e4f72 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,22 @@ the top. Include a label indicating the component affected. Studio: Remove XML from the video component editor. All settings are moved to be edited as metadata. +XModule: Only write out assets files if the contents have changed. + +XModule: Don't delete generated xmodule asset files when compiling (for +instance, when XModule provides a coffeescript file, don't delete +the associated javascript) + +Studio: For courses running on edx.org (marketing site), disable fields in +Course Settings that do not apply. + +Common: Make asset watchers run as singletons (so they won't start if the +watcher is already running in another shell). + +Common: Use coffee directly when watching for coffeescript file changes. + +Common: Make rake provide better error messages if packages are missing. + Common: Repairs development documentation generation by sphinx. LMS: Problem rescoring. Added options on the Grades tab of the @@ -17,6 +33,8 @@ students' number of attempts to zero. Provides a list of background tasks that are currently running for the course, and an option to see a history of background tasks for a given problem. +LMS: Fixed the preferences scope for storing data in xmodules. + LMS: Forums. Added handling for case where discussion module can get `None` as value of lms.start in `lms/djangoapps/django_comment_client/utils.py` diff --git a/Gemfile b/Gemfile index 7f7b146978..1ad685c34d 100644 --- a/Gemfile +++ b/Gemfile @@ -4,3 +4,4 @@ gem 'sass', '3.1.15' gem 'bourbon', '~> 1.3.6' gem 'colorize', '~> 0.5.8' gem 'launchy', '~> 2.1.2' +gem 'sys-proctable', '~> 0.9.3' diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 3113603467..473fc20a68 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -31,11 +31,10 @@ def press_the_notification_button(step, name): # Save was clicked if either the save notification bar is gone, or we have a error notification # overlaying it (expected in the case of typing Object into display_name). - save_clicked = lambda : world.is_css_not_present('.is-shown.wrapper-notification-warning') or \ - world.is_css_present('.is-shown.wrapper-notification-error') + save_clicked = lambda: world.is_css_not_present('.is-shown.wrapper-notification-warning') or\ + world.is_css_present('.is-shown.wrapper-notification-error') - assert_true(world.css_click(css, success_condition=save_clicked), - 'The save button was not clicked after 5 attempts.') + assert_true(world.css_click(css, success_condition=save_clicked), 'Save button not clicked after 5 attempts.') @step(u'I edit the value of a policy key$') diff --git a/cms/djangoapps/contentstore/features/grading.feature b/cms/djangoapps/contentstore/features/grading.feature new file mode 100644 index 0000000000..78634cb964 --- /dev/null +++ b/cms/djangoapps/contentstore/features/grading.feature @@ -0,0 +1,53 @@ +Feature: Course Grading + As a course author, I want to be able to configure how my course is graded + + Scenario: Users can add grading ranges + Given I have opened a new course in Studio + And I am viewing the grading settings + When I add "1" new grade + Then I see I now have "3" grades + + Scenario: Users can only have up to 5 grading ranges + Given I have opened a new course in Studio + And I am viewing the grading settings + When I add "6" new grades + Then I see I now have "5" grades + + #Cannot reliably make the delete button appear so using javascript instead + Scenario: Users can delete grading ranges + Given I have opened a new course in Studio + And I am viewing the grading settings + When I add "1" new grade + And I delete a grade + Then I see I now have "2" grades + + Scenario: Users can move grading ranges + Given I have opened a new course in Studio + And I am viewing the grading settings + When I move a grading section + Then I see that the grade range has changed + + Scenario: Users can modify Assignment types + Given I have opened a new course in Studio + And I have populated the course + And I am viewing the grading settings + When I change assignment type "Homework" to "New Type" + And I go back to the main course page + Then I do see the assignment name "New Type" + And I do not see the assignment name "Homework" + + Scenario: Users can delete Assignment types + Given I have opened a new course in Studio + And I have populated the course + And I am viewing the grading settings + When I delete the assignment type "Homework" + And I go back to the main course page + Then I do not see the assignment name "Homework" + + Scenario: Users can add Assignment types + Given I have opened a new course in Studio + And I have populated the course + And I am viewing the grading settings + When I add a new assignment type "New Type" + And I go back to the main course page + Then I do see the assignment name "New Type" diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py new file mode 100644 index 0000000000..4e59897c1c --- /dev/null +++ b/cms/djangoapps/contentstore/features/grading.py @@ -0,0 +1,108 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step +from common import * + + +@step(u'I am viewing the grading settings') +def view_grading_settings(step): + world.click_course_settings() + link_css = 'li.nav-course-settings-grading a' + world.css_click(link_css) + + +@step(u'I add "([^"]*)" new grade') +def add_grade(step, many): + grade_css = '.new-grade-button' + for i in range(int(many)): + world.css_click(grade_css) + + +@step(u'I delete a grade') +def delete_grade(step): + #grade_css = 'li.grade-specific-bar > a.remove-button' + #range_css = '.grade-specific-bar' + #world.css_find(range_css)[1].mouseover() + #world.css_click(grade_css) + world.browser.execute_script('document.getElementsByClassName("remove-button")[0].click()') + + +@step(u'I see I now have "([^"]*)" grades$') +def view_grade_slider(step, how_many): + grade_slider_css = '.grade-specific-bar' + all_grades = world.css_find(grade_slider_css) + assert len(all_grades) == int(how_many) + + +@step(u'I move a grading section') +def move_grade_slider(step): + moveable_css = '.ui-resizable-e' + f = world.css_find(moveable_css).first + f.action_chains.drag_and_drop_by_offset(f._element, 100, 0).perform() + + +@step(u'I see that the grade range has changed') +def confirm_change(step): + range_css = '.range' + all_ranges = world.css_find(range_css) + for i in range(len(all_ranges)): + assert all_ranges[i].html != '0-50' + + +@step(u'I change assignment type "([^"]*)" to "([^"]*)"$') +def change_assignment_name(step, old_name, new_name): + name_id = '#course-grading-assignment-name' + index = get_type_index(old_name) + f = world.css_find(name_id)[index] + assert index != -1 + for count in range(len(old_name)): + f._element.send_keys(Keys.END, Keys.BACK_SPACE) + f._element.send_keys(new_name) + + +@step(u'I go back to the main course page') +def main_course_page(step): + main_page_link_css = 'a[href="/MITx/999/course/Robot_Super_Course"]' + world.css_click(main_page_link_css) + + +@step(u'I do( not)? see the assignment name "([^"]*)"$') +def see_assignment_name(step, do_not, name): + assignment_menu_css = 'ul.menu > li > a' + assignment_menu = world.css_find(assignment_menu_css) + allnames = [item.html for item in assignment_menu] + if do_not: + assert not name in allnames + else: + assert name in allnames + + +@step(u'I delete the assignment type "([^"]*)"$') +def delete_assignment_type(step, to_delete): + delete_css = '.remove-grading-data' + world.css_click(delete_css, index=get_type_index(to_delete)) + + +@step(u'I add a new assignment type "([^"]*)"$') +def add_assignment_type(step, new_name): + add_button_css = '.add-grading-data' + world.css_click(add_button_css) + name_id = '#course-grading-assignment-name' + f = world.css_find(name_id)[4] + f._element.send_keys(new_name) + + +@step(u'I have populated the course') +def populate_course(step): + step.given('I have added a new section') + step.given('I have added a new subsection') + + +def get_type_index(name): + name_id = '#course-grading-assignment-name' + f = world.css_find(name_id) + for i in range(len(f)): + if f[i].value == name: + return i + return -1 diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 8c15b1ae95..6b8622f992 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -1,11 +1,16 @@ +""" +Tests for Studio Course Settings. +""" import datetime import json import copy +import mock from django.contrib.auth.models import User from django.test.client import Client from django.core.urlresolvers import reverse from django.utils.timezone import UTC +from django.test.utils import override_settings from xmodule.modulestore import Location from models.settings.course_details import (CourseDetails, CourseSettingsEncoder) @@ -21,6 +26,9 @@ from xmodule.fields import Date class CourseTestCase(ModuleStoreTestCase): + """ + Base class for test classes below. + """ def setUp(self): """ These tests need a user in the DB so that the django Test Client @@ -51,6 +59,9 @@ class CourseTestCase(ModuleStoreTestCase): class CourseDetailsTestCase(CourseTestCase): + """ + Tests the first course settings page (course dates, overview, etc.). + """ def test_virgin_fetch(self): details = CourseDetails.fetch(self.course_location) self.assertEqual(details.course_location, self.course_location, "Location not copied into") @@ -81,9 +92,9 @@ class CourseDetailsTestCase(CourseTestCase): Test the encoder out of its original constrained purpose to see if it functions for general use """ details = {'location': Location(['tag', 'org', 'course', 'category', 'name']), - 'number': 1, - 'string': 'string', - 'datetime': datetime.datetime.now(UTC())} + 'number': 1, + 'string': 'string', + 'datetime': datetime.datetime.now(UTC())} jsondetails = json.dumps(details, cls=CourseSettingsEncoder) jsondetails = json.loads(jsondetails) @@ -118,8 +129,50 @@ class CourseDetailsTestCase(CourseTestCase): jsondetails.effort, "After set effort" ) + @override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) + def test_marketing_site_fetch(self): + settings_details_url = reverse('settings_details', + kwargs={'org': self.course_location.org, 'name': self.course_location.name, + 'course': self.course_location.course}) + + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}): + response = self.client.get(settings_details_url) + self.assertContains(response, "Course Summary Page") + self.assertContains(response, "course summary page will not be viewable") + + self.assertContains(response, "Course Start Date") + self.assertContains(response, "Course End Date") + self.assertNotContains(response, "Enrollment Start Date") + self.assertNotContains(response, "Enrollment End Date") + self.assertContains(response, "not the dates shown on your course summary page") + + self.assertNotContains(response, "Introducing Your Course") + self.assertNotContains(response, "Requirements") + + def test_regular_site_fetch(self): + settings_details_url = reverse('settings_details', + kwargs={'org': self.course_location.org, 'name': self.course_location.name, + 'course': self.course_location.course}) + + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}): + response = self.client.get(settings_details_url) + self.assertContains(response, "Course Summary Page") + self.assertNotContains(response, "course summary page will not be viewable") + + self.assertContains(response, "Course Start Date") + self.assertContains(response, "Course End Date") + self.assertContains(response, "Enrollment Start Date") + self.assertContains(response, "Enrollment End Date") + self.assertNotContains(response, "not the dates shown on your course summary page") + + self.assertContains(response, "Introducing Your Course") + self.assertContains(response, "Requirements") + class CourseDetailsViewTest(CourseTestCase): + """ + Tests for modifying content on the first course settings page (course dates, overview, etc.). + """ def alter_field(self, url, details, field, val): setattr(details, field, val) # Need to partially serialize payload b/c the mock doesn't handle it correctly @@ -181,6 +234,9 @@ class CourseDetailsViewTest(CourseTestCase): class CourseGradingTest(CourseTestCase): + """ + Tests for the course settings grading page. + """ def test_initial_grader(self): descriptor = get_modulestore(self.course_location).get_item(self.course_location) test_grader = CourseGradingModel(descriptor) @@ -256,6 +312,9 @@ class CourseGradingTest(CourseTestCase): class CourseMetadataEditingTest(CourseTestCase): + """ + Tests for CourseMetadata. + """ def setUp(self): CourseTestCase.setUp(self) # add in the full class too diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 8a29a637b8..dd7573bad5 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -227,7 +227,8 @@ def get_course_settings(request, org, course, name): kwargs={"org": org, "course": course, "name": name, - "section": "details"}) + "section": "details"}), + 'about_page_editable': not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False) }) diff --git a/cms/static/sass/elements/_system-help.scss b/cms/static/sass/elements/_system-help.scss index 7fcb218282..a9a3e16128 100644 --- a/cms/static/sass/elements/_system-help.scss +++ b/cms/static/sass/elements/_system-help.scss @@ -1,2 +1,40 @@ // studio - elements - system help // ==================== + +// notices - in-context: to be used as notices to users within the context of a form/action +.notice-incontext { + @extend .ui-well; + @include border-radius(($baseline/10)); + + .title { + @extend .t-title7; + margin-bottom: ($baseline/4); + font-weight: 600; + } + + .copy { + @extend .t-copy-sub1; + @include transition(opacity 0.25s ease-in-out 0); + opacity: 0.75; + } + + strong { + font-weight: 600; + } + + &:hover { + + .copy { + opacity: 1.0; + } + } +} + +// particular warnings around a workflow for something +.notice-workflow { + background: $yellow-l5; + + .copy { + color: $gray-d1; + } +} diff --git a/cms/static/sass/views/_settings.scss b/cms/static/sass/views/_settings.scss index 735774511f..cbb1034626 100644 --- a/cms/static/sass/views/_settings.scss +++ b/cms/static/sass/views/_settings.scss @@ -21,7 +21,7 @@ body.course.settings { font-size: 14px; } - .message-status { + .message-status { display: none; @include border-top-radius(2px); @include box-sizing(border-box); @@ -52,6 +52,12 @@ body.course.settings { } } + // notices - used currently for edx mktg + .notice-workflow { + margin-top: ($baseline); + } + + // in form - elements .group-settings { margin: 0 0 ($baseline*2) 0; diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 2adc0cd980..14c79e586a 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -1,3 +1,5 @@ +<%! from django.utils.translation import ugettext as _ %> + <%inherit file="base.html" /> <%block name="title">Schedule & Details Settings <%block name="bodyclass">is-signedin course schedule settings @@ -50,8 +52,8 @@ from contentstore import utils

- Settings - > Schedule & Details + ${_("Settings")} + > ${_("Schedule & Details")}

@@ -62,59 +64,68 @@ from contentstore import utils
-

Basic Information

- The nuts and bolts of your course +

${_("Basic Information")}

+ ${_("The nuts and bolts of your course")}
  1. - - + +
  2. - - + +
  3. - - + +
-

Course Summary Page (for student enrollment and access)

+

${_("Course Summary Page")} ${_("(for student enrollment and access)")}

+ + % if not about_page_editable: +
+

${_("Promoting Your Course with edX")}

+
+

${_('Your course summary page will not be viewable until your course has been announced. To provide content for the page and preview it, follow the instructions provided by your PM or Conrad Warre (conrad@edx.org).')}

+
+
+ % endif

-

Course Schedule

- Important steps and segments of your course +

${_('Course Schedule')}

+ ${_('Dates that control when your course can be viewed.')}
  1. - + - First day the course begins + ${_("First day the course begins")}
    - +
    @@ -122,29 +133,30 @@ from contentstore import utils
  2. - + - Last day your course is active + ${_("Last day your course is active")}
    - +
+ % if about_page_editable:
  1. - + - First day students can enroll + ${_("First day students can enroll")}
    - +
    @@ -152,91 +164,106 @@ from contentstore import utils
  2. - + - Last day students can enroll + ${_("Last day students can enroll")}
    - +
-
+ % endif + % if not about_page_editable: +
+

${_("These Dates Are Not Used When Promoting Your Course")}

+
+

${_('These dates impact when your courseware can be viewed, but they are not the dates shown on your course summary page. To provide the course start and registration dates as shown on your course summary page, follow the instructions provided by your PM or Conrad Warre (conrad@edx.org).')}

+
+
+ % endif +
+ % if about_page_editable: +
+
+

${_("Introducing Your Course")}

+ ${_("Information for prospective students")} +
-
-
-

Introducing Your Course

- Information for prospective students -
+
    +
  1. + + + <%def name='overview_text()'><% + a_link_start = '' + _("your course summary page") + '' + a_link = a_link_start + utils.get_lms_link_for_about_page(course_location) + a_link_end + text = _("Introductions, prerequisites, FAQs that are used on %s (formatted in HTML)") % a_link + %>${text} + ${overview_text()} +
  2. -
      -
    1. - - - Introductions, prerequisites, FAQs that are used on your course summary page (formatted in HTML) -
    2. +
    3. + + -
    4. - -
      -
      - -
      - -
      +
      + + ${_("Enter your YouTube video's ID (along with any restriction parameters)")} +
      +
    5. +
    +
-
- - Enter your YouTube video's ID (along with any restriction parameters) -
- - -
+
-
+
+
+

${_("Requirements")}

+ ${_("Expectations of the students taking this course")} +
-
-
-

Requirements

- Expectations of the students taking this course -
- -
    -
  1. - - - Time spent on all course work -
  2. -
-
+
    +
  1. + + + ${_("Time spent on all course work")} +
  2. +
+
+ % endif
-