Commit bd1f9121 by Peter Baratta

Merge branch 'master' into pbaratta/calc-add-trig

parents 46686d8f 0667150f
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
.AppleDouble .AppleDouble
database.sqlite database.sqlite
requirements/private.txt requirements/private.txt
lms/envs/private.py
cms/envs/private.py
courseware/static/js/mathjax/* courseware/static/js/mathjax/*
flushdb.sh flushdb.sh
build build
...@@ -27,6 +29,7 @@ conf/locale/en/LC_MESSAGES/*.po ...@@ -27,6 +29,7 @@ conf/locale/en/LC_MESSAGES/*.po
!messages.po !messages.po
lms/static/sass/*.css lms/static/sass/*.css
lms/static/sass/application.scss lms/static/sass/application.scss
lms/static/sass/course.scss
cms/static/sass/*.css cms/static/sass/*.css
lms/lib/comment_client/python lms/lib/comment_client/python
nosetests.xml nosetests.xml
......
...@@ -72,4 +72,7 @@ Giulio Gratta <giulio@giuliogratta.com> ...@@ -72,4 +72,7 @@ Giulio Gratta <giulio@giuliogratta.com>
David Baumgold <david@davidbaumgold.com> David Baumgold <david@davidbaumgold.com>
Jason Bau <jbau@stanford.edu> Jason Bau <jbau@stanford.edu>
Frances Botsford <frances@edx.org> Frances Botsford <frances@edx.org>
Jonah Stanley <Jonah_Stanley@brown.edu>
Slater Victoroff <slater.r.victoroff@gmail.com> Slater Victoroff <slater.r.victoroff@gmail.com>
Peter Fogg <peter.p.fogg@gmail.com>
Renzo Lucioni <renzolucioni@gmail.com>
\ No newline at end of file
Change Log
----------
These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
LMS: Some errors handling Non-ASCII data in XML courses have been fixed.
LMS: Add page-load tracking using segment-io (if SEGMENT_IO_LMS_KEY and
SEGMENT_IO_LMS feature flag is on)
LMS: Background colors on login, register, and courseware have been corrected
back to white.
LMS: Accessibility improvements have been made to several courseware and
navigation elements.
LMS: Small design/presentation changes to login and register views.
LMS: Functionality added to instructor enrollment tab in LMS such that invited
student can be auto-enrolled in course or when activating if not current
student.
Blades: Staff debug info is now accessible for Graphical Slider Tool problems.
Blades: For Video Alpha the events ready, play, pause, seek, and speed change
are logged on the server (in the logs).
Common: Developers can now have private Django settings files.
Common: Safety code added to prevent anything above the vertical level in the
course tree from being marked as version='draft'. It will raise an exception if
the code tries to so mark a node. We need the backtraces to figure out where
this very infrequent intermittent marking was occurring. It was making courses
look different in Studio than in LMS.
Deploy: MKTG_URLS is now read from env.json.
Common: Theming makes it possible to change the look of the site, from
Stanford.
Common: Accessibility UI fixes.
Common: The "duplicate email" error message is more informative.
Studio: Component metadata settings editor.
Studio: Autoplay is disabled (only in Studio).
Studio: Single-click creation for video and discussion components.
Studio: fixed a bad link in the activation page.
LMS: Changed the help button text.
LMS: Fixed failing numeric response (decimal but no trailing digits).
LMS: XML Error module no longer shows students a stack trace.
Blades: Videoalpha.
XModules: Added partial credit for foldit module.
XModules: Added "randomize" XModule to list of XModule types.
XModules: Show errors with full descriptors.
XQueue: Fixed (hopefully) worker crash when the connection to RabbitMQ is
dropped suddenly.
XQueue: Upload file submissions to a specially named bucket in S3.
Common: Removed request debugger.
Common: Updated Django to version 1.4.5.
Common: Updated CodeJail.
Common: Allow setting of authentication session cookie name.
...@@ -111,11 +111,11 @@ CMS templates. Fortunately, `rake` will do all of this for you! Just run: ...@@ -111,11 +111,11 @@ CMS templates. Fortunately, `rake` will do all of this for you! Just run:
$ rake django-admin[syncdb] $ rake django-admin[syncdb]
$ rake django-admin[migrate] $ rake django-admin[migrate]
$ rake django-admin[update_templates] $ rake cms:update_templates
If you are running these commands using the [`zsh`](http://www.zsh.org/) shell, If you are running these commands using the [`zsh`](http://www.zsh.org/) shell,
zsh will assume that you are doing zsh will assume that you are doing
[shell globbing](https://en.wikipedia.org/wiki/Glob_(programming)), search for [shell globbing](https://en.wikipedia.org/wiki/Glob_%28programming%29), search for
a file in your directory named `django-adminsyncdb` or `django-adminmigrate`, a file in your directory named `django-adminsyncdb` or `django-adminmigrate`,
and fail. To fix this, just surround the argument with quotation marks, so that and fail. To fix this, just surround the argument with quotation marks, so that
you're running `rake "django-admin[syncdb]"`. you're running `rake "django-admin[syncdb]"`.
......
...@@ -11,8 +11,6 @@ Feature: Advanced (manual) course policy ...@@ -11,8 +11,6 @@ Feature: Advanced (manual) course policy
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
Then the settings are alphabetized Then the settings are alphabetized
# Skipped because Ubuntu ChromeDriver cannot click notification "Cancel"
@skip
Scenario: Test cancel editing key value Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key When I edit the value of a policy key
...@@ -21,8 +19,6 @@ Feature: Advanced (manual) course policy ...@@ -21,8 +19,6 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then the policy key value is unchanged Then the policy key value is unchanged
# Skipped because Ubuntu ChromeDriver cannot click notification "Save"
@skip
Scenario: Test editing key value Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key and save When I edit the value of a policy key and save
...@@ -30,8 +26,6 @@ Feature: Advanced (manual) course policy ...@@ -30,8 +26,6 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then the policy key value is changed Then the policy key value is changed
# Skipped because Ubuntu ChromeDriver cannot edit CodeMirror input
@skip
Scenario: Test how multi-line input appears Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value When I create a JSON object as a value
...@@ -39,8 +33,6 @@ Feature: Advanced (manual) course policy ...@@ -39,8 +33,6 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then it is displayed as formatted Then it is displayed as formatted
# Skipped because Ubuntu ChromeDriver cannot edit CodeMirror input
@skip
Scenario: Test automatic quoting of non-JSON values Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes When I create a non-JSON value not in quotes
......
...@@ -42,8 +42,9 @@ def edit_the_value_of_a_policy_key(step): ...@@ -42,8 +42,9 @@ def edit_the_value_of_a_policy_key(step):
It is hard to figure out how to get into the CodeMirror It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :) area, so cheat and do it from the policy key field :)
""" """
e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] world.css_find(".CodeMirror")[get_index_of(DISPLAY_NAME_KEY)].click()
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X') g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
g._element.send_keys(Keys.ARROW_LEFT, ' ', 'X')
@step(u'I edit the value of a policy key and save$') @step(u'I edit the value of a policy key and save$')
...@@ -123,10 +124,12 @@ def get_display_name_value(): ...@@ -123,10 +124,12 @@ def get_display_name_value():
def change_display_name_value(step, new_value): def change_display_name_value(step, new_value):
e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
world.css_find(".CodeMirror")[get_index_of(DISPLAY_NAME_KEY)].click()
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
display_name = get_display_name_value() display_name = get_display_name_value()
for count in range(len(display_name)): for count in range(len(display_name)):
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE) g._element.send_keys(Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value # Must delete "" before typing the JSON value
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value) g._element.send_keys(Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
press_the_notification_button(step, "Save") press_the_notification_button(step, "Save")
...@@ -161,3 +161,11 @@ def i_created_a_video_component(step): ...@@ -161,3 +161,11 @@ def i_created_a_video_component(step):
'i4x://edx/templates/video/default', 'i4x://edx/templates/video/default',
'.xmodule_VideoModule' '.xmodule_VideoModule'
) )
@step('I have clicked the new unit button')
def open_new_unit(step):
step.given('I have opened a new course section in Studio')
step.given('I have added a new subsection')
step.given('I expand the first section')
world.css_click('a.new-unit-item')
...@@ -14,20 +14,27 @@ def create_component_instance(step, component_button_css, instance_id, expected_ ...@@ -14,20 +14,27 @@ def create_component_instance(step, component_button_css, instance_id, expected_
@world.absorb @world.absorb
def click_new_component_button(step, component_button_css): def click_new_component_button(step, component_button_css):
step.given('I have opened a new course section in Studio') step.given('I have clicked the new unit button')
step.given('I have added a new subsection')
step.given('I expand the first section')
world.css_click('a.new-unit-item')
world.css_click(component_button_css) world.css_click(component_button_css)
@world.absorb @world.absorb
def click_component_from_menu(instance_id, expected_css): def click_component_from_menu(instance_id, expected_css):
"""
Creates a component from `instance_id`. For components with more
than one template, clicks on `elem_css` to create the new
component. Components with only one template are created as soon
as the user clicks the appropriate button, so we assert that the
expected component is present.
"""
elem_css = "a[data-location='%s']" % instance_id elem_css = "a[data-location='%s']" % instance_id
assert_equal(1, len(world.css_find(elem_css))) elements = world.css_find(elem_css)
world.css_click(elem_css) assert(len(elements) == 1)
if elements[0]['id'] == instance_id: # If this is a component with multiple templates
world.css_click(elem_css)
assert_equal(1, len(world.css_find(expected_css))) assert_equal(1, len(world.css_find(expected_css)))
@world.absorb @world.absorb
def edit_component_and_select_settings(): def edit_component_and_select_settings():
world.css_click('a.edit-button') world.css_click('a.edit-button')
......
...@@ -47,12 +47,6 @@ def i_see_the_course_in_my_courses(step): ...@@ -47,12 +47,6 @@ def i_see_the_course_in_my_courses(step):
assert world.css_has_text(course_css, 'Robot Super Course') assert world.css_has_text(course_css, 'Robot Super Course')
@step('the course is loaded$')
def course_is_loaded(step):
class_css = 'a.class-name'
assert world.css_has_text(course_css, 'Robot Super Cousre')
@step('I am on the "([^"]*)" tab$') @step('I am on the "([^"]*)" tab$')
def i_am_on_tab(step, tab_name): def i_am_on_tab(step, tab_name):
header_css = 'div.inner-wrapper h1' header_css = 'div.inner-wrapper h1'
......
...@@ -11,3 +11,7 @@ Feature: Discussion Component Editor ...@@ -11,3 +11,7 @@ Feature: Discussion Component Editor
And I edit and select Settings And I edit and select Settings
Then I can modify the display name Then I can modify the display name
And my display name change is persisted on save And my display name change is persisted on save
Scenario: Creating a discussion takes a single click
Given I have clicked the new unit button
Then creating a discussion takes a single click
...@@ -21,3 +21,10 @@ def i_see_only_the_settings_and_values(step): ...@@ -21,3 +21,10 @@ def i_see_only_the_settings_and_values(step):
['Display Name', "Discussion Tag", True], ['Display Name', "Discussion Tag", True],
['Subcategory', "Topic-Level Student-Visible Label", True] ['Subcategory', "Topic-Level Student-Visible Label", True]
]) ])
@step('creating a discussion takes a single click')
def discussion_takes_a_single_click(step):
assert(not world.is_css_present('.xmodule_DiscussionModule'))
world.css_click("a[data-location='i4x://edx/templates/discussion/Discussion_Tag']")
assert(world.is_css_present('.xmodule_DiscussionModule'))
...@@ -52,7 +52,7 @@ Feature: Problem Editor ...@@ -52,7 +52,7 @@ Feature: Problem Editor
Scenario: User cannot type out of range values in an integer number field Scenario: User cannot type out of range values in an integer number field
Given I have created a Blank Common Problem Given I have created a Blank Common Problem
And I edit and select Settings And I edit and select Settings
Then if I set the max attempts to "-3", it displays initially as "-3", and is persisted as "1" Then if I set the max attempts to "-3", it displays initially as "-3", and is persisted as "0"
Scenario: Settings changes are not saved on Cancel Scenario: Settings changes are not saved on Cancel
Given I have created a Blank Common Problem Given I have created a Blank Common Problem
......
...@@ -26,11 +26,9 @@ Feature: Create Section ...@@ -26,11 +26,9 @@ Feature: Create Section
And I save a new section release date And I save a new section release date
Then the section release date is updated Then the section release date is updated
# Skipped because Ubuntu ChromeDriver hangs on alert
@skip
Scenario: Delete section Scenario: Delete section
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I have added a new section And I have added a new section
When I press the "section" delete icon When I will confirm all alerts
And I confirm the alert And I press the "section" delete icon
Then the section does not exist Then the section does not exist
...@@ -9,34 +9,34 @@ from nose.tools import assert_equal ...@@ -9,34 +9,34 @@ from nose.tools import assert_equal
@step('I click the new section link$') @step('I click the new section link$')
def i_click_new_section_link(step): def i_click_new_section_link(_step):
link_css = 'a.new-courseware-section-button' link_css = 'a.new-courseware-section-button'
world.css_click(link_css) world.css_click(link_css)
@step('I enter the section name and click save$') @step('I enter the section name and click save$')
def i_save_section_name(step): def i_save_section_name(_step):
save_section_name('My Section') save_section_name('My Section')
@step('I enter a section name with a quote and click save$') @step('I enter a section name with a quote and click save$')
def i_save_section_name_with_quote(step): def i_save_section_name_with_quote(_step):
save_section_name('Section with "Quote"') save_section_name('Section with "Quote"')
@step('I have added a new section$') @step('I have added a new section$')
def i_have_added_new_section(step): def i_have_added_new_section(_step):
add_section() add_section()
@step('I click the Edit link for the release date$') @step('I click the Edit link for the release date$')
def i_click_the_edit_link_for_the_release_date(step): def i_click_the_edit_link_for_the_release_date(_step):
button_css = 'div.section-published-date a.edit-button' button_css = 'div.section-published-date a.edit-button'
world.css_click(button_css) world.css_click(button_css)
@step('I save a new section release date$') @step('I save a new section release date$')
def i_save_a_new_section_release_date(step): def i_save_a_new_section_release_date(_step):
set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013', set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013',
'input.start-time.time.ui-timepicker-input', '00:00') 'input.start-time.time.ui-timepicker-input', '00:00')
world.browser.click_link_by_text('Save') world.browser.click_link_by_text('Save')
...@@ -46,35 +46,35 @@ def i_save_a_new_section_release_date(step): ...@@ -46,35 +46,35 @@ def i_save_a_new_section_release_date(step):
@step('I see my section on the Courseware page$') @step('I see my section on the Courseware page$')
def i_see_my_section_on_the_courseware_page(step): def i_see_my_section_on_the_courseware_page(_step):
see_my_section_on_the_courseware_page('My Section') see_my_section_on_the_courseware_page('My Section')
@step('I see my section name with a quote on the Courseware page$') @step('I see my section name with a quote on the Courseware page$')
def i_see_my_section_name_with_quote_on_the_courseware_page(step): def i_see_my_section_name_with_quote_on_the_courseware_page(_step):
see_my_section_on_the_courseware_page('Section with "Quote"') see_my_section_on_the_courseware_page('Section with "Quote"')
@step('I click to edit the section name$') @step('I click to edit the section name$')
def i_click_to_edit_section_name(step): def i_click_to_edit_section_name(_step):
world.css_click('span.section-name-span') world.css_click('span.section-name-span')
@step('I see the complete section name with a quote in the editor$') @step('I see the complete section name with a quote in the editor$')
def i_see_complete_section_name_with_quote_in_editor(step): def i_see_complete_section_name_with_quote_in_editor(_step):
css = '.section-name-edit input[type=text]' css = '.section-name-edit input[type=text]'
assert world.is_css_present(css) assert world.is_css_present(css)
assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"') assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"')
@step('the section does not exist$') @step('the section does not exist$')
def section_does_not_exist(step): def section_does_not_exist(_step):
css = 'span.section-name-span' css = 'h3[data-name="My Section"]'
assert world.browser.is_element_not_present_by_css(css) assert world.is_css_not_present(css)
@step('I see a release date for my section$') @step('I see a release date for my section$')
def i_see_a_release_date_for_my_section(step): def i_see_a_release_date_for_my_section(_step):
import re import re
css = 'span.published-status' css = 'span.published-status'
...@@ -83,26 +83,32 @@ def i_see_a_release_date_for_my_section(step): ...@@ -83,26 +83,32 @@ def i_see_a_release_date_for_my_section(step):
# e.g. 11/06/2012 at 16:25 # e.g. 11/06/2012 at 16:25
msg = 'Will Release:' msg = 'Will Release:'
date_regex = '[01][0-9]\/[0-3][0-9]\/[12][0-9][0-9][0-9]' date_regex = r'(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d\d?, \d{4}'
time_regex = '[0-2][0-9]:[0-5][0-9]' if not re.search(date_regex, status_text):
match_string = '%s %s at %s' % (msg, date_regex, time_regex) print status_text, date_regex
time_regex = r'[0-2]\d:[0-5]\d( \w{3})?'
if not re.search(time_regex, status_text):
print status_text, time_regex
match_string = r'%s\s+%s at %s' % (msg, date_regex, time_regex)
if not re.match(match_string, status_text):
print status_text, match_string
assert re.match(match_string, status_text) assert re.match(match_string, status_text)
@step('I see a link to create a new subsection$') @step('I see a link to create a new subsection$')
def i_see_a_link_to_create_a_new_subsection(step): def i_see_a_link_to_create_a_new_subsection(_step):
css = 'a.new-subsection-item' css = 'a.new-subsection-item'
assert world.is_css_present(css) assert world.is_css_present(css)
@step('the section release date picker is not visible$') @step('the section release date picker is not visible$')
def the_section_release_date_picker_not_visible(step): def the_section_release_date_picker_not_visible(_step):
css = 'div.edit-subsection-publish-settings' css = 'div.edit-subsection-publish-settings'
assert not world.css_visible(css) assert not world.css_visible(css)
@step('the section release date is updated$') @step('the section release date is updated$')
def the_section_release_date_is_updated(step): def the_section_release_date_is_updated(_step):
css = 'span.published-status' css = 'span.published-status'
status_text = world.css_text(css) status_text = world.css_text(css)
assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC') assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC')
......
Feature: Overview Toggle Section Feature: Overview Toggle Section
In order to quickly view the details of a course's section or to scan the inventory of sections In order to quickly view the details of a course's section or to scan the inventory of sections
As a course author As a course author
I want to toggle the visibility of each section's subsection details in the overview listing 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 Scenario: The default layout for the overview page is to show sections in expanded view
Given I have a course with multiple sections Given I have a course with multiple sections
When I navigate to the course overview page When I navigate to the course overview page
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
And all sections are expanded And all sections are expanded
Scenario: Expand /collapse for a course with no sections Scenario: Expand /collapse for a course with no sections
Given I have a course with no sections Given I have a course with no sections
When I navigate to the course overview page When I navigate to the course overview page
Then I do not see the "Collapse All Sections" link Then I do not see the "Collapse All Sections" link
Scenario: Collapse link appears after creating first section of a course Scenario: Collapse link appears after creating first section of a course
Given I have a course with no sections Given I have a course with no sections
When I navigate to the course overview page When I navigate to the course overview page
And I add a section And I add a section
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
And all sections are expanded And all sections are expanded
# Skipped because Ubuntu ChromeDriver hangs on alert Scenario: Collapse link is not removed after last section of a course is deleted
@skip Given I have a course with 1 section
Scenario: Collapse link is not removed after last section of a course is deleted And I navigate to the course overview page
Given I have a course with 1 section When I will confirm all alerts
And I navigate to the course overview page And I press the "section" delete icon
When I press the "section" delete icon Then I see the "Collapse All Sections" link
And I confirm the alert
Then I 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
Scenario: Collapsing all sections when all sections are expanded And all sections are expanded
Given I navigate to the courseware page of a course with multiple sections When I click the "Collapse All Sections" link
And all sections are expanded Then I see the "Expand All Sections" link
When I click the "Collapse All Sections" link And all sections are collapsed
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
Scenario: Collapsing all sections when 1 or more sections are already collapsed And all sections are expanded
Given I navigate to the courseware page of a course with multiple sections When I collapse the first section
And all sections are expanded And I click the "Collapse All Sections" link
When I collapse the first section Then I see the "Expand All Sections" link
And I click the "Collapse All Sections" link And all sections are collapsed
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
Scenario: Expanding all sections when all sections are collapsed And I click the "Collapse All Sections" link
Given I navigate to the courseware page of a course with multiple sections When I click the "Expand All Sections" link
And I click the "Collapse All Sections" link Then I see the "Collapse All Sections" link
When I click the "Expand All Sections" link And all sections are expanded
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
Scenario: Expanding all sections when 1 or more sections are already expanded And I click the "Collapse All Sections" link
Given I navigate to the courseware page of a course with multiple sections When I expand the first section
And I click the "Collapse All Sections" link And I click the "Expand All Sections" link
When I expand the first section Then I see the "Collapse All Sections" link
And I click the "Expand All Sections" link And all sections are expanded
Then I see the "Collapse All Sections" link
And all sections are expanded
...@@ -112,7 +112,7 @@ def all_sections_are_expanded(step): ...@@ -112,7 +112,7 @@ def all_sections_are_expanded(step):
@step(u'all sections are collapsed$') @step(u'all sections are collapsed$')
def all_sections_are_expanded(step): def all_sections_are_collapsed(step):
subsection_locator = 'div.subsection-list' subsection_locator = 'div.subsection-list'
subsections = world.css_find(subsection_locator) subsections = world.css_find(subsection_locator)
for s in subsections: for s in subsections:
......
...@@ -32,12 +32,10 @@ Feature: Create Subsection ...@@ -32,12 +32,10 @@ Feature: Create Subsection
And I reload the page And I reload the page
Then I see the correct dates Then I see the correct dates
# Skipped because Ubuntu ChromeDriver hangs on alert
@skip
Scenario: Delete a subsection Scenario: Delete a subsection
Given I have opened a new course section in Studio Given I have opened a new course section in Studio
And I have added a new subsection And I have added a new subsection
And I see my subsection on the Courseware page And I see my subsection on the Courseware page
When I press the "subsection" delete icon When I will confirm all alerts
And I confirm the alert And I press the "subsection" delete icon
Then the subsection does not exist Then the subsection does not exist
...@@ -4,3 +4,12 @@ Feature: Video Component ...@@ -4,3 +4,12 @@ Feature: Video Component
Scenario: Autoplay is disabled in Studio Scenario: Autoplay is disabled in Studio
Given I have created a Video component Given I have created a Video component
Then when I view the video it does not have autoplay enabled Then when I view the video it does not have autoplay enabled
Scenario: Creating a video takes a single click
Given I have clicked the new unit button
Then creating a video takes a single click
Scenario: Captions are shown correctly
Given I have created a Video component
And I have hidden captions
Then when I view the video it does not show the captions
...@@ -9,3 +9,20 @@ from lettuce import world, step ...@@ -9,3 +9,20 @@ from lettuce import world, step
def does_not_autoplay(step): def does_not_autoplay(step):
assert world.css_find('.video')[0]['data-autoplay'] == 'False' assert world.css_find('.video')[0]['data-autoplay'] == 'False'
assert world.css_find('.video_control')[0].has_class('play') assert world.css_find('.video_control')[0].has_class('play')
@step('creating a video takes a single click')
def video_takes_a_single_click(step):
assert(not world.is_css_present('.xmodule_VideoModule'))
world.css_click("a[data-location='i4x://edx/templates/video/default']")
assert(world.is_css_present('.xmodule_VideoModule'))
@step('I have hidden captions')
def set_show_captions_false(step):
world.css_click('a.hide-subtitles')
@step('when I view the video it does not show the captions')
def does_not_show_captions(step):
assert world.css_find('.video')[0].has_class('closed')
...@@ -37,6 +37,9 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -37,6 +37,9 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from contentstore.views.component import ADVANCED_COMPONENT_TYPES from contentstore.views.component import ADVANCED_COMPONENT_TYPES
from django_comment_common.utils import are_permissions_roles_seeded from django_comment_common.utils import are_permissions_roles_seeded
from xmodule.exceptions import InvalidVersionError
import datetime
from pytz import UTC
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
...@@ -77,14 +80,25 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -77,14 +80,25 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client = Client() self.client = Client()
self.client.login(username=uname, password=password) self.client.login(username=uname, password=password)
def test_advanced_components_in_edit_unit(self): def check_components_on_page(self, component_types, expected_types):
"""
Ensure that the right types end up on the page.
component_types is the list of advanced components.
expected_types is the list of elements that should appear on the page.
expected_types and component_types should be similar, but not
exactly the same -- for example, 'videoalpha' in
component_types should cause 'Video Alpha' to be present.
"""
store = modulestore('direct') store = modulestore('direct')
import_from_xml(store, 'common/test/data/', ['simple']) import_from_xml(store, 'common/test/data/', ['simple'])
course = store.get_item(Location(['i4x', 'edX', 'simple', course = store.get_item(Location(['i4x', 'edX', 'simple',
'course', '2012_Fall', None]), depth=None) 'course', '2012_Fall', None]), depth=None)
course.advanced_modules = ADVANCED_COMPONENT_TYPES course.advanced_modules = component_types
store.update_metadata(course.location, own_metadata(course)) store.update_metadata(course.location, own_metadata(course))
...@@ -94,13 +108,31 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -94,13 +108,31 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()})) resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
for expected in expected_types:
self.assertIn(expected, resp.content)
def test_advanced_components_in_edit_unit(self):
# This could be made better, but for now let's just assert that we see the advanced modules mentioned in the page # This could be made better, but for now let's just assert that we see the advanced modules mentioned in the page
# response HTML # response HTML
self.assertIn('Video Alpha', resp.content) self.check_components_on_page(ADVANCED_COMPONENT_TYPES, ['Video Alpha',
self.assertIn('Word cloud', resp.content) 'Word cloud',
self.assertIn('Annotation', resp.content) 'Annotation',
self.assertIn('Open Ended Response', resp.content) 'Open Ended Response',
self.assertIn('Peer Grading Interface', resp.content) 'Peer Grading Interface'])
def test_advanced_components_require_two_clicks(self):
self.check_components_on_page(['videoalpha'], ['Video Alpha'])
def test_malformed_edit_unit_request(self):
store = modulestore('direct')
import_from_xml(store, 'common/test/data/', ['simple'])
# just pick one vertical
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
location = descriptor.location._replace(name='.' + descriptor.location.name)
resp = self.client.get(reverse('edit_unit', kwargs={'location': location.url()}))
self.assertEqual(resp.status_code, 400)
def check_edit_unit(self, test_course_name): def check_edit_unit(self, test_course_name):
import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name]) import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name])
...@@ -239,7 +271,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -239,7 +271,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
) )
self.assertTrue(getattr(draft_problem, 'is_draft', False)) self.assertTrue(getattr(draft_problem, 'is_draft', False))
#now requery with depth # now requery with depth
course = modulestore('draft').get_item( course = modulestore('draft').get_item(
Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]), Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]),
depth=None depth=None
...@@ -386,6 +418,32 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -386,6 +418,32 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()})) resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
def test_illegal_draft_crud_ops(self):
draft_store = modulestore('draft')
direct_store = modulestore('direct')
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
location = Location('i4x://MITx/999/chapter/neuvo')
self.assertRaises(InvalidVersionError, draft_store.clone_item, 'i4x://edx/templates/chapter/Empty',
location)
direct_store.clone_item('i4x://edx/templates/chapter/Empty', location)
self.assertRaises(InvalidVersionError, draft_store.clone_item, location,
location)
self.assertRaises(InvalidVersionError, draft_store.update_item, location,
'chapter data')
# taking advantage of update_children and other functions never checking that the ids are valid
self.assertRaises(InvalidVersionError, draft_store.update_children, location,
['i4x://MITx/999/problem/doesntexist'])
self.assertRaises(InvalidVersionError, draft_store.update_metadata, location,
{'due': datetime.datetime.now(UTC)})
self.assertRaises(InvalidVersionError, draft_store.unpublish, location)
def test_bad_contentstore_request(self): def test_bad_contentstore_request(self):
resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png') resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png')
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
...@@ -468,6 +526,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -468,6 +526,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# check for custom_tags # check for custom_tags
self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template') self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template')
# check for about content
self.verify_content_existence(module_store, root_dir, location, 'about', 'about', '.html')
# check for graiding_policy.json # check for graiding_policy.json
filesystem = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012') filesystem = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
self.assertTrue(filesystem.exists('grading_policy.json')) self.assertTrue(filesystem.exists('grading_policy.json'))
...@@ -478,7 +539,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -478,7 +539,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
on_disk = loads(grading_policy.read()) on_disk = loads(grading_policy.read())
self.assertEqual(on_disk, course.grading_policy) self.assertEqual(on_disk, course.grading_policy)
#check for policy.json # check for policy.json
self.assertTrue(filesystem.exists('policy.json')) self.assertTrue(filesystem.exists('policy.json'))
# compare what's on disk to what we have in the course module # compare what's on disk to what we have in the course module
......
...@@ -54,6 +54,7 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -54,6 +54,7 @@ class CourseDetailsTestCase(CourseTestCase):
def test_virgin_fetch(self): def test_virgin_fetch(self):
details = CourseDetails.fetch(self.course_location) details = CourseDetails.fetch(self.course_location)
self.assertEqual(details.course_location, self.course_location, "Location not copied into") self.assertEqual(details.course_location, self.course_location, "Location not copied into")
self.assertIsNotNone(details.start_date.tzinfo)
self.assertIsNone(details.end_date, "end date somehow initialized " + str(details.end_date)) self.assertIsNone(details.end_date, "end date somehow initialized " + str(details.end_date))
self.assertIsNone(details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start)) self.assertIsNone(details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start))
self.assertIsNone(details.enrollment_end, "enrollment_end date somehow initialized " + str(details.enrollment_end)) self.assertIsNone(details.enrollment_end, "enrollment_end date somehow initialized " + str(details.enrollment_end))
...@@ -67,7 +68,6 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -67,7 +68,6 @@ class CourseDetailsTestCase(CourseTestCase):
jsondetails = json.dumps(details, cls=CourseSettingsEncoder) jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
jsondetails = json.loads(jsondetails) jsondetails = json.loads(jsondetails)
self.assertTupleEqual(Location(jsondetails['course_location']), self.course_location, "Location !=") self.assertTupleEqual(Location(jsondetails['course_location']), self.course_location, "Location !=")
# Note, start_date is being initialized someplace. I'm not sure why b/c the default will make no sense.
self.assertIsNone(jsondetails['end_date'], "end date somehow initialized ") self.assertIsNone(jsondetails['end_date'], "end date somehow initialized ")
self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ") self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ")
self.assertIsNone(jsondetails['enrollment_end'], "enrollment_end date somehow initialized ") self.assertIsNone(jsondetails['enrollment_end'], "enrollment_end date somehow initialized ")
...@@ -76,6 +76,23 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -76,6 +76,23 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized") self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized")
self.assertIsNone(jsondetails['effort'], "effort somehow initialized") self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
def test_ooc_encoder(self):
"""
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())}
jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
jsondetails = json.loads(jsondetails)
self.assertIn('location', jsondetails)
self.assertIn('org', jsondetails['location'])
self.assertEquals('org', jsondetails['location'][1])
self.assertEquals(1, jsondetails['number'])
self.assertEqual(jsondetails['string'], 'string')
def test_update_and_fetch(self): def test_update_and_fetch(self):
# # NOTE: I couldn't figure out how to validly test time setting w/ all the conversions # # NOTE: I couldn't figure out how to validly test time setting w/ all the conversions
jsondetails = CourseDetails.fetch(self.course_location) jsondetails = CourseDetails.fetch(self.course_location)
...@@ -116,11 +133,8 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -116,11 +133,8 @@ class CourseDetailsViewTest(CourseTestCase):
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val)) self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
@staticmethod @staticmethod
def convert_datetime_to_iso(datetime): def convert_datetime_to_iso(dt):
if datetime is not None: return Date().to_json(dt)
return datetime.isoformat("T")
else:
return None
def test_update_and_fetch(self): def test_update_and_fetch(self):
details = CourseDetails.fetch(self.course_location) details = CourseDetails.fetch(self.course_location)
...@@ -151,22 +165,12 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -151,22 +165,12 @@ class CourseDetailsViewTest(CourseTestCase):
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==") self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==") self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
@staticmethod
def struct_to_datetime(struct_time):
return datetime.datetime(*struct_time[:6], tzinfo=UTC())
def compare_date_fields(self, details, encoded, context, field): def compare_date_fields(self, details, encoded, context, field):
if details[field] is not None: if details[field] is not None:
date = Date() date = Date()
if field in encoded and encoded[field] is not None: if field in encoded and encoded[field] is not None:
encoded_encoded = date.from_json(encoded[field]) dt1 = date.from_json(encoded[field])
dt1 = CourseDetailsViewTest.struct_to_datetime(encoded_encoded) dt2 = details[field]
if isinstance(details[field], datetime.datetime):
dt2 = details[field]
else:
details_encoded = date.from_json(details[field])
dt2 = CourseDetailsViewTest.struct_to_datetime(details_encoded)
expected_delta = datetime.timedelta(0) expected_delta = datetime.timedelta(0)
self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context) self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context)
......
...@@ -4,6 +4,7 @@ import mock ...@@ -4,6 +4,7 @@ import mock
import collections import collections
import copy import copy
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...@@ -11,11 +12,52 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase ...@@ -11,11 +12,52 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class LMSLinksTestCase(TestCase): class LMSLinksTestCase(TestCase):
""" Tests for LMS links. """ """ Tests for LMS links. """
def about_page_test(self): def about_page_test(self):
""" Get URL for about page. """ """ Get URL for about page, no marketing site """
# default for ENABLE_MKTG_SITE is False.
self.assertEquals(self.get_about_page_link(), "//localhost:8000/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
def about_page_marketing_site_test(self):
""" Get URL for about page, marketing root present. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//dummy-root/courses/mitX/101/test/about")
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}):
self.assertEquals(self.get_about_page_link(), "//localhost:8000/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'http://www.dummy'})
def about_page_marketing_site_remove_http_test(self):
""" Get URL for about page, marketing root present, remove http://. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummy/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'https://www.dummy'})
def about_page_marketing_site_remove_https_test(self):
""" Get URL for about page, marketing root present, remove https://. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummy/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'www.dummyhttps://x'})
def about_page_marketing_site_https__edge_test(self):
""" Get URL for about page, only remove https:// at the beginning of the string. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummyhttps://x/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={})
def about_page_marketing_urls_not_set_test(self):
""" Error case. ENABLE_MKTG_SITE is True, but there is either no MKTG_URLS, or no MKTG_URLS Root property. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), None)
@override_settings(LMS_BASE=None)
def about_page_no_lms_base_test(self):
""" No LMS_BASE, nor is ENABLE_MKTG_SITE True """
self.assertEquals(self.get_about_page_link(), None)
def get_about_page_link(self):
""" create mock course and return the about page link """
location = 'i4x', 'mitX', '101', 'course', 'test' location = 'i4x', 'mitX', '101', 'course', 'test'
utils.get_course_id = mock.Mock(return_value="mitX/101/test") utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_about_page(location) return utils.get_lms_link_for_about_page(location)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about")
def lms_link_test(self): def lms_link_test(self):
""" Tests get_lms_link_for_item. """ """ Tests get_lms_link_for_item. """
......
...@@ -4,8 +4,11 @@ from xmodule.modulestore.django import modulestore ...@@ -4,8 +4,11 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import copy import copy
import logging
import re
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] log = logging.getLogger(__name__)
#In order to instantiate an open ended tab automatically, need to have this data #In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"} OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"}
...@@ -107,9 +110,29 @@ def get_lms_link_for_about_page(location): ...@@ -107,9 +110,29 @@ def get_lms_link_for_about_page(location):
""" """
Returns the url to the course about page from the location tuple. Returns the url to the course about page from the location tuple.
""" """
if settings.LMS_BASE is not None: if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False):
lms_link = "//{lms_base}/courses/{course_id}/about".format( if not hasattr(settings, 'MKTG_URLS'):
lms_base=settings.LMS_BASE, log.exception("ENABLE_MKTG_SITE is True, but MKTG_URLS is not defined.")
about_base = None
else:
marketing_urls = settings.MKTG_URLS
if marketing_urls.get('ROOT', None) is None:
log.exception('There is no ROOT defined in MKTG_URLS')
about_base = None
else:
# Root will be "https://www.edx.org". The complete URL will still not be exactly correct,
# but redirects exist from www.edx.org to get to the Drupal course about page URL.
about_base = marketing_urls.get('ROOT')
# Strip off https:// (or http://) to be consistent with the formatting of LMS_BASE.
about_base = re.sub(r"^https?://", "", about_base)
elif settings.LMS_BASE is not None:
about_base = settings.LMS_BASE
else:
about_base = None
if about_base is not None:
lms_link = "//{about_base_url}/courses/{course_id}/about".format(
about_base_url=about_base,
course_id=get_course_id(location) course_id=get_course_id(location)
) )
else: else:
...@@ -205,7 +228,7 @@ def add_extra_panel_tab(tab_type, course): ...@@ -205,7 +228,7 @@ def add_extra_panel_tab(tab_type, course):
course_tabs = copy.copy(course.tabs) course_tabs = copy.copy(course.tabs)
changed = False changed = False
#Check to see if open ended panel is defined in the course #Check to see if open ended panel is defined in the course
tab_panel = EXTRA_TAB_PANELS.get(tab_type) tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel not in course_tabs: if tab_panel not in course_tabs:
#Add panel to the tabs if it is not defined #Add panel to the tabs if it is not defined
......
...@@ -62,7 +62,7 @@ def asset_index(request, org, course, name): ...@@ -62,7 +62,7 @@ def asset_index(request, org, course, name):
asset_id = asset['_id'] asset_id = asset['_id']
display_info = {} display_info = {}
display_info['displayname'] = asset['displayname'] display_info['displayname'] = asset['displayname']
display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple()) display_info['uploadDate'] = get_default_time_display(asset['uploadDate'])
asset_location = StaticContent.compute_location(asset_id['org'], asset_id['course'], asset_id['name']) asset_location = StaticContent.compute_location(asset_id['org'], asset_id['course'], asset_id['name'])
display_info['url'] = StaticContent.get_url_path_from_location(asset_location) display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
...@@ -103,6 +103,9 @@ def upload_asset(request, org, course, coursename): ...@@ -103,6 +103,9 @@ def upload_asset(request, org, course, coursename):
logging.error('Could not find course' + location) logging.error('Could not find course' + location)
return HttpResponseBadRequest() return HttpResponseBadRequest()
if 'file' not in request.FILES:
return HttpResponseBadRequest()
# compute a 'filename' which is similar to the location formatting, we're using the 'filename' # compute a 'filename' which is similar to the location formatting, we're using the 'filename'
# nomenclature since we're using a FileSystem paradigm here. We're just imposing # nomenclature since we're using a FileSystem paradigm here. We're just imposing
# the Location string formatting expectations to keep things a bit more consistent # the Location string formatting expectations to keep things a bit more consistent
...@@ -131,7 +134,7 @@ def upload_asset(request, org, course, coursename): ...@@ -131,7 +134,7 @@ def upload_asset(request, org, course, coursename):
readback = contentstore().find(content.location) readback = contentstore().find(content.location)
response_payload = {'displayname': content.name, response_payload = {'displayname': content.name,
'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()), 'uploadDate': get_default_time_display(readback.last_modified_at),
'url': StaticContent.get_url_path_from_location(content.location), 'url': StaticContent.get_url_path_from_location(content.location),
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None, 'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
'msg': 'Upload completed' 'msg': 'Upload completed'
...@@ -227,11 +230,9 @@ def generate_export_course(request, org, course, name): ...@@ -227,11 +230,9 @@ def generate_export_course(request, org, course, name):
root_dir = path(mkdtemp()) root_dir = path(mkdtemp())
# export out to a tempdir # export out to a tempdir
logging.debug('root = {0}'.format(root_dir)) logging.debug('root = {0}'.format(root_dir))
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore()) export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
#filename = root_dir / name + '.tar.gz'
logging.debug('tar file being generated at {0}'.format(export_file.name)) logging.debug('tar file being generated at {0}'.format(export_file.name))
tar_file = tarfile.open(name=export_file.name, mode='w:gz') tar_file = tarfile.open(name=export_file.name, mode='w:gz')
......
...@@ -7,7 +7,7 @@ from django.contrib.auth.decorators import login_required ...@@ -7,7 +7,7 @@ from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.conf import settings from django.conf import settings
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -50,11 +50,18 @@ ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' ...@@ -50,11 +50,18 @@ ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
@login_required @login_required
def edit_subsection(request, location): def edit_subsection(request, location):
# check that we have permissions to edit this item # check that we have permissions to edit this item
course = get_course_for_item(location) try:
course = get_course_for_item(location)
except InvalidLocationError:
return HttpResponseBadRequest()
if not has_access(request.user, course.location): if not has_access(request.user, course.location):
raise PermissionDenied() raise PermissionDenied()
item = modulestore().get_item(location, depth=1) try:
item = modulestore().get_item(location, depth=1)
except ItemNotFoundError:
return HttpResponseBadRequest()
lms_link = get_lms_link_for_item(location, course_id=course.location.course_id) lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True) preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
...@@ -113,11 +120,18 @@ def edit_unit(request, location): ...@@ -113,11 +120,18 @@ def edit_unit(request, location):
id: A Location URL id: A Location URL
""" """
course = get_course_for_item(location) try:
course = get_course_for_item(location)
except InvalidLocationError:
return HttpResponseBadRequest()
if not has_access(request.user, course.location): if not has_access(request.user, course.location):
raise PermissionDenied() raise PermissionDenied()
item = modulestore().get_item(location, depth=1) try:
item = modulestore().get_item(location, depth=1)
except ItemNotFoundError:
return HttpResponseBadRequest()
lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id) lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
Views related to operations on course objects Views related to operations on course objects
""" """
import json import json
import time
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
...@@ -32,6 +31,8 @@ from .component import OPEN_ENDED_COMPONENT_TYPES, \ ...@@ -32,6 +31,8 @@ from .component import OPEN_ENDED_COMPONENT_TYPES, \
NOTE_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY NOTE_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
from django_comment_common.utils import seed_permissions_roles from django_comment_common.utils import seed_permissions_roles
import datetime
from django.utils.timezone import UTC
# TODO: should explicitly enumerate exports with __all__ # TODO: should explicitly enumerate exports with __all__
...@@ -130,7 +131,7 @@ def create_new_course(request): ...@@ -130,7 +131,7 @@ def create_new_course(request):
new_course.display_name = display_name new_course.display_name = display_name
# set a default start date to now # set a default start date to now
new_course.start = time.gmtime() new_course.start = datetime.datetime.now(UTC())
initialize_course_tabs(new_course) initialize_course_tabs(new_course)
...@@ -357,49 +358,49 @@ def course_advanced_updates(request, org, course, name): ...@@ -357,49 +358,49 @@ def course_advanced_updates(request, org, course, name):
# Whether or not to filter the tabs key out of the settings metadata # Whether or not to filter the tabs key out of the settings metadata
filter_tabs = True filter_tabs = True
#Check to see if the user instantiated any advanced components. This is a hack # Check to see if the user instantiated any advanced components. This is a hack
#that does the following : # that does the following :
# 1) adds/removes the open ended panel tab to a course automatically if the user # 1) adds/removes the open ended panel tab to a course automatically if the user
# has indicated that they want to edit the combinedopendended or peergrading module # has indicated that they want to edit the combinedopendended or peergrading module
# 2) adds/removes the notes panel tab to a course automatically if the user has # 2) adds/removes the notes panel tab to a course automatically if the user has
# indicated that they want the notes module enabled in their course # indicated that they want the notes module enabled in their course
# TODO refactor the above into distinct advanced policy settings # TODO refactor the above into distinct advanced policy settings
if ADVANCED_COMPONENT_POLICY_KEY in request_body: if ADVANCED_COMPONENT_POLICY_KEY in request_body:
#Get the course so that we can scrape current tabs # Get the course so that we can scrape current tabs
course_module = modulestore().get_item(location) course_module = modulestore().get_item(location)
#Maps tab types to components # Maps tab types to components
tab_component_map = { tab_component_map = {
'open_ended': OPEN_ENDED_COMPONENT_TYPES, 'open_ended': OPEN_ENDED_COMPONENT_TYPES,
'notes': NOTE_COMPONENT_TYPES, 'notes': NOTE_COMPONENT_TYPES,
} }
#Check to see if the user instantiated any notes or open ended components # Check to see if the user instantiated any notes or open ended components
for tab_type in tab_component_map.keys(): for tab_type in tab_component_map.keys():
component_types = tab_component_map.get(tab_type) component_types = tab_component_map.get(tab_type)
found_ac_type = False found_ac_type = False
for ac_type in component_types: for ac_type in component_types:
if ac_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: if ac_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
#Add tab to the course if needed # Add tab to the course if needed
changed, new_tabs = add_extra_panel_tab(tab_type, course_module) changed, new_tabs = add_extra_panel_tab(tab_type, course_module)
#If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json # If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
if changed: if changed:
course_module.tabs = new_tabs course_module.tabs = new_tabs
request_body.update({'tabs': new_tabs}) request_body.update({'tabs': new_tabs})
#Indicate that tabs should not be filtered out of the metadata # Indicate that tabs should not be filtered out of the metadata
filter_tabs = False filter_tabs = False
#Set this flag to avoid the tab removal code below. # Set this flag to avoid the tab removal code below.
found_ac_type = True found_ac_type = True
break break
#If we did not find a module type in the advanced settings, # If we did not find a module type in the advanced settings,
# we may need to remove the tab from the course. # we may need to remove the tab from the course.
if not found_ac_type: if not found_ac_type:
#Remove tab from the course if needed # Remove tab from the course if needed
changed, new_tabs = remove_extra_panel_tab(tab_type, course_module) changed, new_tabs = remove_extra_panel_tab(tab_type, course_module)
if changed: if changed:
course_module.tabs = new_tabs course_module.tabs = new_tabs
request_body.update({'tabs': new_tabs}) request_body.update({'tabs': new_tabs})
#Indicate that tabs should *not* be filtered out of the metadata # Indicate that tabs should *not* be filtered out of the metadata
filter_tabs = False filter_tabs = False
response_json = json.dumps(CourseMetadata.update_from_json(location, response_json = json.dumps(CourseMetadata.update_from_json(location,
......
...@@ -3,26 +3,26 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -3,26 +3,26 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
import json import json
from json.encoder import JSONEncoder from json.encoder import JSONEncoder
import time
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from models.settings import course_grading from models.settings import course_grading
from contentstore.utils import update_item from contentstore.utils import update_item
from xmodule.fields import Date from xmodule.fields import Date
import re import re
import logging import logging
import datetime
class CourseDetails(object): class CourseDetails(object):
def __init__(self, location): def __init__(self, location):
self.course_location = location # a Location obj self.course_location = location # a Location obj
self.start_date = None # 'start' self.start_date = None # 'start'
self.end_date = None # 'end' self.end_date = None # 'end'
self.enrollment_start = None self.enrollment_start = None
self.enrollment_end = None self.enrollment_end = None
self.syllabus = None # a pdf file asset self.syllabus = None # a pdf file asset
self.overview = "" # html to render as the overview self.overview = "" # html to render as the overview
self.intro_video = None # a video pointer self.intro_video = None # a video pointer
self.effort = None # int hours/week self.effort = None # int hours/week
@classmethod @classmethod
def fetch(cls, course_location): def fetch(cls, course_location):
...@@ -73,9 +73,9 @@ class CourseDetails(object): ...@@ -73,9 +73,9 @@ class CourseDetails(object):
""" """
Decode the json into CourseDetails and save any changed attrs to the db Decode the json into CourseDetails and save any changed attrs to the db
""" """
## TODO make it an error for this to be undefined & for it to not be retrievable from modulestore # TODO make it an error for this to be undefined & for it to not be retrievable from modulestore
course_location = jsondict['course_location'] course_location = jsondict['course_location']
## Will probably want to cache the inflight courses because every blur generates an update # Will probably want to cache the inflight courses because every blur generates an update
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False dirty = False
...@@ -181,7 +181,7 @@ class CourseSettingsEncoder(json.JSONEncoder): ...@@ -181,7 +181,7 @@ class CourseSettingsEncoder(json.JSONEncoder):
return obj.__dict__ return obj.__dict__
elif isinstance(obj, Location): elif isinstance(obj, Location):
return obj.dict() return obj.dict()
elif isinstance(obj, time.struct_time): elif isinstance(obj, datetime.datetime):
return Date().to_json(obj) return Date().to_json(obj)
else: else:
return JSONEncoder.default(self, obj) return JSONEncoder.default(self, obj)
...@@ -23,7 +23,7 @@ MODULESTORE_OPTIONS = { ...@@ -23,7 +23,7 @@ MODULESTORE_OPTIONS = {
'db': 'test_xmodule', 'db': 'test_xmodule',
'collection': 'acceptance_modulestore', 'collection': 'acceptance_modulestore',
'fs_root': TEST_ROOT / "data", 'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string'
} }
MODULESTORE = { MODULESTORE = {
......
...@@ -91,11 +91,19 @@ CACHES = ENV_TOKENS['CACHES'] ...@@ -91,11 +91,19 @@ CACHES = ENV_TOKENS['CACHES']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
# allow for environments to specify what cookie name our login subsystem should use
# this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can
# happen with some browsers (e.g. Firefox)
if ENV_TOKENS.get('SESSION_COOKIE_NAME', None):
# NOTE, there's a bug in Django (http://bugs.python.org/issue18012) which necessitates this being a str()
SESSION_COOKIE_NAME = str(ENV_TOKENS.get('SESSION_COOKIE_NAME'))
#Email overrides #Email overrides
DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL) DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL) DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS) ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL) SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS)
#Timezone overrides #Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
......
...@@ -25,6 +25,7 @@ Longer TODO: ...@@ -25,6 +25,7 @@ Longer TODO:
import sys import sys
import lms.envs.common import lms.envs.common
from lms.envs.common import USE_TZ
from path import path from path import path
############################ FEATURE CONFIGURATION ############################# ############################ FEATURE CONFIGURATION #############################
...@@ -34,8 +35,8 @@ MITX_FEATURES = { ...@@ -34,8 +35,8 @@ MITX_FEATURES = {
'GITHUB_PUSH': False, 'GITHUB_PUSH': False,
'ENABLE_DISCUSSION_SERVICE': False, 'ENABLE_DISCUSSION_SERVICE': False,
'AUTH_USE_MIT_CERTIFICATES': False, 'AUTH_USE_MIT_CERTIFICATES': False,
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
'STAFF_EMAIL': '', # email address for staff (eg to request course creation) 'STAFF_EMAIL': '', # email address for staff (eg to request course creation)
'STUDIO_NPS_SURVEY': True, 'STUDIO_NPS_SURVEY': True,
'SEGMENT_IO': True, 'SEGMENT_IO': True,
...@@ -183,7 +184,7 @@ STATICFILES_DIRS = [ ...@@ -183,7 +184,7 @@ STATICFILES_DIRS = [
# Locale/Internationalization # Locale/Internationalization
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
...@@ -323,6 +324,7 @@ INSTALLED_APPS = ( ...@@ -323,6 +324,7 @@ INSTALLED_APPS = (
'track', 'track',
# For asset pipelining # For asset pipelining
'mitxmako',
'pipeline', 'pipeline',
'staticfiles', 'staticfiles',
'static_replace', 'static_replace',
...@@ -334,3 +336,14 @@ INSTALLED_APPS = ( ...@@ -334,3 +336,14 @@ INSTALLED_APPS = (
################# EDX MARKETING SITE ################################## ################# EDX MARKETING SITE ##################################
EDXMKTG_COOKIE_NAME = 'edxloggedin' EDXMKTG_COOKIE_NAME = 'edxloggedin'
MKTG_URLS = {}
MKTG_URL_LINK_MAP = {
'ABOUT': 'about_edx',
'CONTACT': 'contact',
'FAQ': 'help_edx',
'COURSES': 'courses',
'ROOT': 'root',
'TOS': 'tos',
'HONOR': 'honor',
'PRIVACY': 'privacy_edx',
}
...@@ -22,7 +22,7 @@ modulestore_options = { ...@@ -22,7 +22,7 @@ modulestore_options = {
'db': 'xmodule', 'db': 'xmodule',
'collection': 'modulestore', 'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT, 'fs_root': GITHUB_REPO_ROOT,
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string'
} }
MODULESTORE = { MODULESTORE = {
...@@ -64,7 +64,7 @@ REPOS = { ...@@ -64,7 +64,7 @@ REPOS = {
}, },
'content-mit-6002x': { 'content-mit-6002x': {
'branch': 'master', 'branch': 'master',
#'origin': 'git@github.com:MITx/6002x-fall-2012.git', # 'origin': 'git@github.com:MITx/6002x-fall-2012.git',
'origin': 'git@github.com:MITx/content-mit-6002x.git', 'origin': 'git@github.com:MITx/content-mit-6002x.git',
}, },
'6.00x': { '6.00x': {
...@@ -165,3 +165,11 @@ MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True ...@@ -165,3 +165,11 @@ MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
# segment-io key for dev # segment-io key for dev
SEGMENT_IO_KEY = 'mty8edrrsg' SEGMENT_IO_KEY = 'mty8edrrsg'
#####################################################################
# Lastly, see if the developer has any local overrides.
try:
from .private import *
except ImportError:
pass
...@@ -48,7 +48,7 @@ MODULESTORE_OPTIONS = { ...@@ -48,7 +48,7 @@ MODULESTORE_OPTIONS = {
'db': 'test_xmodule', 'db': 'test_xmodule',
'collection': 'test_modulestore', 'collection': 'test_modulestore',
'fs_root': TEST_ROOT / "data", 'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string'
} }
MODULESTORE = { MODULESTORE = {
...@@ -121,7 +121,7 @@ CELERY_RESULT_BACKEND = 'cache' ...@@ -121,7 +121,7 @@ CELERY_RESULT_BACKEND = 'cache'
BROKER_TRANSPORT = 'memory' BROKER_TRANSPORT = 'memory'
################### Make tests faster ################### Make tests faster
#http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/ # http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/
PASSWORD_HASHERS = ( PASSWORD_HASHERS = (
'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.SHA1PasswordHasher',
'django.contrib.auth.hashers.MD5PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher',
......
'''
Used for pydev eclipse. Should be innocuous for everyone else.
Created on May 8, 2013
@author: dmitchell
'''
#!/home/<username>/mitx_all/python/bin/python
from django.core import management
if __name__ == '__main__':
management.execute_from_command_line()
class CMS.Views.UnitEdit extends Backbone.View class CMS.Views.UnitEdit extends Backbone.View
events: events:
'click .new-component .new-component-type a': 'showComponentTemplates' 'click .new-component .new-component-type a.multiple-templates': 'showComponentTemplates'
'click .new-component .new-component-type a.single-template': 'saveNewComponent'
'click .new-component .cancel-button': 'closeNewComponent' 'click .new-component .cancel-button': 'closeNewComponent'
'click .new-component-templates .new-component-template a': 'saveNewComponent' 'click .new-component-templates .new-component-template a': 'saveNewComponent'
'click .new-component-templates .cancel-button': 'closeNewComponent' 'click .new-component-templates .cancel-button': 'closeNewComponent'
......
cms/static/img/large-advanced-icon.png

342 Bytes | W: | H:

cms/static/img/large-advanced-icon.png

633 Bytes | W: | H:

cms/static/img/large-advanced-icon.png
cms/static/img/large-advanced-icon.png
cms/static/img/large-advanced-icon.png
cms/static/img/large-advanced-icon.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -411,8 +411,12 @@ function showFileSelectionMenu(e) { ...@@ -411,8 +411,12 @@ function showFileSelectionMenu(e) {
} }
function startUpload(e) { function startUpload(e) {
var files = $('.file-input').get(0).files;
if (files.length === 0)
return;
$('.upload-modal h1').html(gettext('Uploading…')); $('.upload-modal h1').html(gettext('Uploading…'));
$('.upload-modal .file-name').html($('.file-input').val().replace('C:\\fakepath\\', '')); $('.upload-modal .file-name').html(files[0].name);
$('.upload-modal .file-chooser').ajaxSubmit({ $('.upload-modal .file-chooser').ajaxSubmit({
beforeSend: resetUploadBar, beforeSend: resetUploadBar,
uploadProgress: showUploadFeedback, uploadProgress: showUploadFeedback,
......
...@@ -14,7 +14,7 @@ body { ...@@ -14,7 +14,7 @@ body {
color: $gray-d2; color: $gray-d2;
} }
body, input { body, input, button {
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
} }
......
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! <%!
import logging import logging
from xmodule.util.date_utils import get_time_struct_display from xmodule.util.date_utils import get_default_time_display
%> %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
...@@ -36,11 +36,15 @@ ...@@ -36,11 +36,15 @@
<div class="datepair" data-language="javascript"> <div class="datepair" data-language="javascript">
<div class="field field-start-date"> <div class="field field-start-date">
<label for="start_date">Release Day</label> <label for="start_date">Release Day</label>
<input type="text" id="start_date" name="start_date" value="${get_time_struct_display(subsection.lms.start, '%m/%d/%Y')}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/> <input type="text" id="start_date" name="start_date"
value="${subsection.lms.start.strftime('%m/%d/%Y') if subsection.lms.start else ''}"
placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div> </div>
<div class="field field-start-time"> <div class="field field-start-time">
<label for="start_time">Release Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label> <label for="start_time">Release Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
<input type="text" id="start_time" name="start_time" value="${get_time_struct_display(subsection.lms.start, '%H:%M')}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/> <input type="text" id="start_time" name="start_time"
value="${subsection.lms.start.strftime('%H:%M') if subsection.lms.start else ''}"
placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div> </div>
</div> </div>
% if subsection.lms.start != parent_item.lms.start and subsection.lms.start: % if subsection.lms.start != parent_item.lms.start and subsection.lms.start:
...@@ -48,7 +52,7 @@ ...@@ -48,7 +52,7 @@
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset. <p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset.
% else: % else:
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} – <p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} –
${get_time_struct_display(parent_item.lms.start, '%m/%d/%Y at %H:%M UTC')}. ${get_default_time_display(parent_item.lms.start)}.
% endif % endif
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p> <a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p>
% endif % endif
...@@ -65,11 +69,15 @@ ...@@ -65,11 +69,15 @@
<div class="datepair date-setter"> <div class="datepair date-setter">
<div class="field field-start-date"> <div class="field field-start-date">
<label for="due_date">Due Day</label> <label for="due_date">Due Day</label>
<input type="text" id="due_date" name="due_date" value="${get_time_struct_display(subsection.lms.due, '%m/%d/%Y')}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/> <input type="text" id="due_date" name="due_date"
value="${subsection.lms.due.strftime('%m/%d/%Y') if subsection.lms.due else ''}"
placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div> </div>
<div class="field field-start-time"> <div class="field field-start-time">
<label for="due_time">Due Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label> <label for="due_time">Due Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
<input type="text" id="due_time" name="due_time" value="${get_time_struct_display(subsection.lms.due, '%H:%M')}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/> <input type="text" id="due_time" name="due_time"
value="${subsection.lms.due.strftime('%H:%M') if subsection.lms.due else ''}"
placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div> </div>
<a href="#" class="remove-date">Remove due date</a> <a href="#" class="remove-date">Remove due date</a>
</div> </div>
......
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! <%!
import logging import logging
from xmodule.util.date_utils import get_time_struct_display from xmodule.util import date_utils
%> %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="title">Course Outline</%block> <%block name="title">Course Outline</%block>
...@@ -154,14 +154,19 @@ ...@@ -154,14 +154,19 @@
<h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3> <h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3>
<div class="section-published-date"> <div class="section-published-date">
<% <%
start_date_str = get_time_struct_display(section.lms.start, '%m/%d/%Y') if section.lms.start is not None:
start_time_str = get_time_struct_display(section.lms.start, '%H:%M') start_date_str = section.lms.start.strftime('%m/%d/%Y')
start_time_str = section.lms.start.strftime('%H:%M')
else:
start_date_str = ''
start_time_str = ''
%> %>
%if section.lms.start is None: %if section.lms.start is None:
<span class="published-status">This section has not been released.</span> <span class="published-status">This section has not been released.</span>
<a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">Schedule</a> <a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">Schedule</a>
%else: %else:
<span class="published-status"><strong>Will Release:</strong> ${get_time_struct_display(section.lms.start, '%m/%d/%Y at %H:%M UTC')}</span> <span class="published-status"><strong>Will Release:</strong>
${date_utils.get_default_time_display(section.lms.start)}</span>
<a href="#" class="edit-button" data-date="${start_date_str}" data-time="${start_time_str}" data-id="${section.location}">Edit</a> <a href="#" class="edit-button" data-date="${start_date_str}" data-time="${start_time_str}" data-id="${section.location}">Edit</a>
%endif %endif
</div> </div>
......
...@@ -53,9 +53,15 @@ ...@@ -53,9 +53,15 @@
<div class="new-component"> <div class="new-component">
<h5>Add New Component</h5> <h5>Add New Component</h5>
<ul class="new-component-type"> <ul class="new-component-type">
% for type in sorted(component_templates.keys()): % for type, templates in sorted(component_templates.items()):
<li> <li>
<a href="#" data-type="${type}"> % if type == 'advanced' or len(templates) > 1:
<a href="#" class="multiple-templates" data-type="${type}">
% else:
% for _, location, _ in templates:
<a href="#" class="single-template" data-type="${type}" data-location="${location}">
% endfor
% endif
<span class="large-template-icon large-${type}-icon"></span> <span class="large-template-icon large-${type}-icon"></span>
<span class="name">${type}</span> <span class="name">${type}</span>
</a> </a>
...@@ -64,50 +70,52 @@ ...@@ -64,50 +70,52 @@
</ul> </ul>
</div> </div>
% for type, templates in sorted(component_templates.items()): % for type, templates in sorted(component_templates.items()):
% if len(templates) > 1 or type == 'advanced':
<div class="new-component-templates new-component-${type}"> <div class="new-component-templates new-component-${type}">
% if type == "problem": % if type == "problem":
<div class="tab-group tabs"> <div class="tab-group tabs">
<ul class="problem-type-tabs nav-tabs"> <ul class="problem-type-tabs nav-tabs">
<li class="current"> <li class="current">
<a class="link-tab" href="#tab1">Common Problem Types</a> <a class="link-tab" href="#tab1">Common Problem Types</a>
</li> </li>
<li> <li>
<a class="link-tab" href="#tab2">Advanced</a> <a class="link-tab" href="#tab2">Advanced</a>
</li> </li>
</ul>
% endif
<div class="tab current" id="tab1">
<ul class="new-component-template">
% for name, location, has_markdown in templates:
% if has_markdown or type != "problem":
<li class="editor-md">
<a href="#" id="${location}" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
%endfor
</ul>
</div>
% if type == "problem":
<div class="tab" id="tab2">
<ul class="new-component-template">
% for name, location, has_markdown in templates:
% if not has_markdown:
<li class="editor-manual">
<a href="#" id="${location}" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
% endfor
</ul> </ul>
</div> % endif
</div> <div class="tab current" id="tab1">
% endif <ul class="new-component-template">
<a href="#" class="cancel-button">Cancel</a> % for name, location, has_markdown in templates:
</div> % if has_markdown or type != "problem":
<li class="editor-md">
<a href="#" id="${location}" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
%endfor
</ul>
</div>
% if type == "problem":
<div class="tab" id="tab2">
<ul class="new-component-template">
% for name, location, has_markdown in templates:
% if not has_markdown:
<li class="editor-manual">
<a href="#" id="${location}" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
% endfor
</ul>
</div>
</div>
% endif
<a href="#" class="cancel-button">Cancel</a>
</div>
% endif
% endfor % endfor
</li> </li>
</ol> </ol>
......
import logging from django.http import HttpResponse, HttpResponseNotModified
import time
from django.http import HttpResponse, Http404, HttpResponseNotModified
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG
...@@ -20,7 +17,7 @@ class StaticContentServer(object): ...@@ -20,7 +17,7 @@ class StaticContentServer(object):
# return a 'Bad Request' to browser as we have a malformed Location # return a 'Bad Request' to browser as we have a malformed Location
response = HttpResponse() response = HttpResponse()
response.status_code = 400 response.status_code = 400
return response return response
# first look in our cache so we don't have to round-trip to the DB # first look in our cache so we don't have to round-trip to the DB
content = get_cached_content(loc) content = get_cached_content(loc)
......
...@@ -44,7 +44,11 @@ class MakoLoader(object): ...@@ -44,7 +44,11 @@ class MakoLoader(object):
if source.startswith("## mako\n"): if source.startswith("## mako\n"):
# This is a mako template # This is a mako template
template = Template(filename=file_path, module_directory=self.module_directory, uri=template_name) template = Template(filename=file_path,
module_directory=self.module_directory,
input_encoding='utf-8',
output_encoding='utf-8',
uri=template_name)
return template, None return template, None
else: else:
# This is a regular template # This is a regular template
......
"""
Preprocess templatized asset files, enabling asset authors to use
Python/Django inside of Sass and CoffeeScript. This preprocessing
will happen before the invocation of the asset compiler (currently
handled by the asset Rakefile).
For this to work, assets need to be named with the appropriate
template extension (e.g., .mako for Mako templates). Currently Mako
is the only template engine supported.
"""
import os
from django.core.management.base import NoArgsCommand
from django.conf import settings
from mako.template import Template
import textwrap
class Command(NoArgsCommand):
"""
Basic management command to preprocess asset template files.
"""
help = "Preprocess asset template files to ready them for compilation."
def handle_noargs(self, **options):
"""
Walk over all of the static files directories specified in the
settings file, looking for asset template files (indicated by
a file extension like .mako).
"""
for staticfiles_dir in getattr(settings, "STATICFILES_DIRS", []):
# Cribbed from the django-staticfiles app at:
# https://github.com/jezdez/django-staticfiles/blob/develop/staticfiles/finders.py#L52
if isinstance(staticfiles_dir, (list, tuple)):
prefix, staticfiles_dir = staticfiles_dir
# Walk over the current static files directory tree,
# preprocessing files that have a template extension.
for root, dirs, files in os.walk(staticfiles_dir):
for filename in files:
outfile, extension = os.path.splitext(filename)
# We currently only handle Mako templates
if extension == ".mako":
self.__preprocess(os.path.join(root, filename),
os.path.join(root, outfile))
def __context(self):
"""
Return a dict that contains all of the available context
variables to the asset template.
"""
# TODO: do we need to include anything else?
# TODO: do this with the django-settings-context-processor
return { "THEME_NAME" : getattr(settings, "THEME_NAME", None) }
def __preprocess(self, infile, outfile):
"""
Run `infile` through the Mako template engine, storing the
result in `outfile`.
"""
with open(outfile, "w") as _outfile:
_outfile.write(textwrap.dedent("""\
/*
* This file is dynamically generated and ignored by Git.
* DO NOT MAKE CHANGES HERE. Instead, go edit its template:
* %s
*/
""" % infile))
_outfile.write(Template(filename=str(infile)).render(env=self.__context()))
...@@ -41,7 +41,9 @@ def marketing_link(name): ...@@ -41,7 +41,9 @@ def marketing_link(name):
return settings.MKTG_URLS.get('ROOT') + settings.MKTG_URLS.get(name) return settings.MKTG_URLS.get('ROOT') + settings.MKTG_URLS.get(name)
# only link to the old pages when the marketing site isn't on # only link to the old pages when the marketing site isn't on
elif not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE') and name in link_map: elif not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE') and name in link_map:
return reverse(link_map[name]) # don't try to reverse disabled marketing links
if link_map[name] is not None:
return reverse(link_map[name])
else: else:
log.warning("Cannot find corresponding link for name: {name}".format(name=name)) log.warning("Cannot find corresponding link for name: {name}".format(name=name))
return '#' return '#'
......
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings
from mitxmako.shortcuts import marketing_link from mitxmako.shortcuts import marketing_link
from mock import patch from mock import patch
from util.testing import UrlResetMixin
class ShortcutsTests(TestCase): class ShortcutsTests(UrlResetMixin, TestCase):
""" """
Test the mitxmako shortcuts file Test the mitxmako shortcuts file
""" """
@override_settings(MKTG_URLS={'ROOT': 'dummy-root', 'ABOUT': '/about-us'}) @override_settings(MKTG_URLS={'ROOT': 'dummy-root', 'ABOUT': '/about-us'})
@override_settings(MKTG_URL_LINK_MAP={'ABOUT': 'login'}) @override_settings(MKTG_URL_LINK_MAP={'ABOUT': 'login'})
def test_marketing_link(self): def test_marketing_link(self):
......
...@@ -14,6 +14,7 @@ import sys ...@@ -14,6 +14,7 @@ import sys
import datetime import datetime
import json import json
from pytz import UTC
middleware.MakoMiddleware() middleware.MakoMiddleware()
...@@ -32,7 +33,7 @@ def group_from_value(groups, v): ...@@ -32,7 +33,7 @@ def group_from_value(groups, v):
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = \
''' Assign users to test groups. Takes a list ''' Assign users to test groups. Takes a list
of groups: of groups:
a:0.3,b:0.4,c:0.3 file.txt "Testing something" a:0.3,b:0.4,c:0.3 file.txt "Testing something"
...@@ -75,7 +76,7 @@ Will log what happened to file.txt. ...@@ -75,7 +76,7 @@ Will log what happened to file.txt.
utg = UserTestGroup() utg = UserTestGroup()
utg.name = group utg.name = group
utg.description = json.dumps({"description": args[2]}, utg.description = json.dumps({"description": args[2]},
{"time": datetime.datetime.utcnow().isoformat()}) {"time": datetime.datetime.now(UTC).isoformat()})
group_objects[group] = utg group_objects[group] = utg
group_objects[group].save() group_objects[group].save()
......
...@@ -8,6 +8,7 @@ from django.conf import settings ...@@ -8,6 +8,7 @@ from django.conf import settings
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser from student.models import TestCenterUser
from pytz import UTC
class Command(BaseCommand): class Command(BaseCommand):
...@@ -58,7 +59,7 @@ class Command(BaseCommand): ...@@ -58,7 +59,7 @@ class Command(BaseCommand):
def handle(self, **options): def handle(self, **options):
# update time should use UTC in order to be comparable to the user_updated_at # update time should use UTC in order to be comparable to the user_updated_at
# field # field
uploaded_at = datetime.utcnow() uploaded_at = datetime.now(UTC)
# if specified destination is an existing directory, then # if specified destination is an existing directory, then
# create a filename for it automatically. If it doesn't exist, # create a filename for it automatically. If it doesn't exist,
...@@ -100,7 +101,7 @@ class Command(BaseCommand): ...@@ -100,7 +101,7 @@ class Command(BaseCommand):
extrasaction='ignore') extrasaction='ignore')
writer.writeheader() writer.writeheader()
for tcu in TestCenterUser.objects.order_by('id'): for tcu in TestCenterUser.objects.order_by('id'):
if tcu.needs_uploading: # or dump_all if tcu.needs_uploading: # or dump_all
record = dict((csv_field, ensure_encoding(getattr(tcu, model_field))) record = dict((csv_field, ensure_encoding(getattr(tcu, model_field)))
for csv_field, model_field for csv_field, model_field
in Command.CSV_TO_MODEL_FIELDS.items()) in Command.CSV_TO_MODEL_FIELDS.items())
......
...@@ -8,6 +8,7 @@ from django.conf import settings ...@@ -8,6 +8,7 @@ from django.conf import settings
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterRegistration, ACCOMMODATION_REJECTED_CODE from student.models import TestCenterRegistration, ACCOMMODATION_REJECTED_CODE
from pytz import UTC
class Command(BaseCommand): class Command(BaseCommand):
...@@ -51,7 +52,7 @@ class Command(BaseCommand): ...@@ -51,7 +52,7 @@ class Command(BaseCommand):
def handle(self, **options): def handle(self, **options):
# update time should use UTC in order to be comparable to the user_updated_at # update time should use UTC in order to be comparable to the user_updated_at
# field # field
uploaded_at = datetime.utcnow() uploaded_at = datetime.now(UTC)
# if specified destination is an existing directory, then # if specified destination is an existing directory, then
# create a filename for it automatically. If it doesn't exist, # create a filename for it automatically. If it doesn't exist,
......
...@@ -13,6 +13,7 @@ from django.core.management.base import BaseCommand, CommandError ...@@ -13,6 +13,7 @@ from django.core.management.base import BaseCommand, CommandError
from django.conf import settings from django.conf import settings
from student.models import TestCenterUser, TestCenterRegistration from student.models import TestCenterUser, TestCenterRegistration
from pytz import UTC
class Command(BaseCommand): class Command(BaseCommand):
...@@ -68,7 +69,7 @@ class Command(BaseCommand): ...@@ -68,7 +69,7 @@ class Command(BaseCommand):
Command.datadog_error("Found authorization record for user {}".format(registration.testcenter_user.user.username), eacfile.name) Command.datadog_error("Found authorization record for user {}".format(registration.testcenter_user.user.username), eacfile.name)
# now update the record: # now update the record:
registration.upload_status = row['Status'] registration.upload_status = row['Status']
registration.upload_error_message = row['Message'] registration.upload_error_message = row['Message']
try: try:
registration.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S')) registration.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S'))
except ValueError as ve: except ValueError as ve:
...@@ -80,7 +81,7 @@ class Command(BaseCommand): ...@@ -80,7 +81,7 @@ class Command(BaseCommand):
except ValueError as ve: except ValueError as ve:
Command.datadog_error("Bad AuthorizationID value found for {}: message {}".format(client_authorization_id, ve), eacfile.name) Command.datadog_error("Bad AuthorizationID value found for {}: message {}".format(client_authorization_id, ve), eacfile.name)
registration.confirmed_at = datetime.utcnow() registration.confirmed_at = datetime.now(UTC)
registration.save() registration.save()
except TestCenterRegistration.DoesNotExist: except TestCenterRegistration.DoesNotExist:
Command.datadog_error("Failed to find record for client_auth_id {}".format(client_authorization_id), eacfile.name) Command.datadog_error("Failed to find record for client_auth_id {}".format(client_authorization_id), eacfile.name)
......
from optparse import make_option from optparse import make_option
from time import strftime
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
...@@ -128,8 +127,8 @@ class Command(BaseCommand): ...@@ -128,8 +127,8 @@ class Command(BaseCommand):
exam = CourseDescriptor.TestCenterExam(course_id, exam_name, exam_info) exam = CourseDescriptor.TestCenterExam(course_id, exam_name, exam_info)
# update option values for date_first and date_last to use YYYY-MM-DD format # update option values for date_first and date_last to use YYYY-MM-DD format
# instead of YYYY-MM-DDTHH:MM # instead of YYYY-MM-DDTHH:MM
our_options['eligibility_appointment_date_first'] = strftime("%Y-%m-%d", exam.first_eligible_appointment_date) our_options['eligibility_appointment_date_first'] = exam.first_eligible_appointment_date.strftime("%Y-%m-%d")
our_options['eligibility_appointment_date_last'] = strftime("%Y-%m-%d", exam.last_eligible_appointment_date) our_options['eligibility_appointment_date_last'] = exam.last_eligible_appointment_date.strftime("%Y-%m-%d")
if exam is None: if exam is None:
raise CommandError("Exam for course_id {} does not exist".format(course_id)) raise CommandError("Exam for course_id {} does not exist".format(course_id))
......
...@@ -16,7 +16,6 @@ import json ...@@ -16,7 +16,6 @@ import json
import logging import logging
import uuid import uuid
from random import randint from random import randint
from time import strftime
from django.conf import settings from django.conf import settings
...@@ -27,6 +26,7 @@ from django.dispatch import receiver ...@@ -27,6 +26,7 @@ from django.dispatch import receiver
from django.forms import ModelForm, forms from django.forms import ModelForm, forms
import comment_client as cc import comment_client as cc
from pytz import UTC
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -54,7 +54,7 @@ class UserProfile(models.Model): ...@@ -54,7 +54,7 @@ class UserProfile(models.Model):
class Meta: class Meta:
db_table = "auth_userprofile" db_table = "auth_userprofile"
## CRITICAL TODO/SECURITY # CRITICAL TODO/SECURITY
# Sanitize all fields. # Sanitize all fields.
# This is not visible to other users, but could introduce holes later # This is not visible to other users, but could introduce holes later
user = models.OneToOneField(User, unique=True, db_index=True, related_name='profile') user = models.OneToOneField(User, unique=True, db_index=True, related_name='profile')
...@@ -254,7 +254,7 @@ class TestCenterUserForm(ModelForm): ...@@ -254,7 +254,7 @@ class TestCenterUserForm(ModelForm):
def update_and_save(self): def update_and_save(self):
new_user = self.save(commit=False) new_user = self.save(commit=False)
# create additional values here: # create additional values here:
new_user.user_updated_at = datetime.utcnow() new_user.user_updated_at = datetime.now(UTC)
new_user.upload_status = '' new_user.upload_status = ''
new_user.save() new_user.save()
log.info("Updated demographic information for user's test center exam registration: username \"{}\" ".format(new_user.user.username)) log.info("Updated demographic information for user's test center exam registration: username \"{}\" ".format(new_user.user.username))
...@@ -429,8 +429,8 @@ class TestCenterRegistration(models.Model): ...@@ -429,8 +429,8 @@ class TestCenterRegistration(models.Model):
registration.course_id = exam.course_id registration.course_id = exam.course_id
registration.accommodation_request = accommodation_request.strip() registration.accommodation_request = accommodation_request.strip()
registration.exam_series_code = exam.exam_series_code registration.exam_series_code = exam.exam_series_code
registration.eligibility_appointment_date_first = strftime("%Y-%m-%d", exam.first_eligible_appointment_date) registration.eligibility_appointment_date_first = exam.first_eligible_appointment_date.strftime("%Y-%m-%d")
registration.eligibility_appointment_date_last = strftime("%Y-%m-%d", exam.last_eligible_appointment_date) registration.eligibility_appointment_date_last = exam.last_eligible_appointment_date.strftime("%Y-%m-%d")
registration.client_authorization_id = cls._create_client_authorization_id() registration.client_authorization_id = cls._create_client_authorization_id()
# accommodation_code remains blank for now, along with Pearson confirmation information # accommodation_code remains blank for now, along with Pearson confirmation information
return registration return registration
...@@ -556,7 +556,7 @@ class TestCenterRegistrationForm(ModelForm): ...@@ -556,7 +556,7 @@ class TestCenterRegistrationForm(ModelForm):
def update_and_save(self): def update_and_save(self):
registration = self.save(commit=False) registration = self.save(commit=False)
# create additional values here: # create additional values here:
registration.user_updated_at = datetime.utcnow() registration.user_updated_at = datetime.now(UTC)
registration.upload_status = '' registration.upload_status = ''
registration.save() registration.save()
log.info("Updated registration information for user's test center exam registration: username \"{}\" course \"{}\", examcode \"{}\"".format(registration.testcenter_user.user.username, registration.course_id, registration.exam_series_code)) log.info("Updated registration information for user's test center exam registration: username \"{}\" course \"{}\", examcode \"{}\"".format(registration.testcenter_user.user.username, registration.course_id, registration.exam_series_code))
...@@ -598,7 +598,7 @@ def unique_id_for_user(user): ...@@ -598,7 +598,7 @@ def unique_id_for_user(user):
return h.hexdigest() return h.hexdigest()
## TODO: Should be renamed to generic UserGroup, and possibly # TODO: Should be renamed to generic UserGroup, and possibly
# Given an optional field for type of group # Given an optional field for type of group
class UserTestGroup(models.Model): class UserTestGroup(models.Model):
users = models.ManyToManyField(User, db_index=True) users = models.ManyToManyField(User, db_index=True)
...@@ -626,7 +626,6 @@ class Registration(models.Model): ...@@ -626,7 +626,6 @@ class Registration(models.Model):
def activate(self): def activate(self):
self.user.is_active = True self.user.is_active = True
self.user.save() self.user.save()
#self.delete()
class PendingNameChange(models.Model): class PendingNameChange(models.Model):
...@@ -648,7 +647,7 @@ class CourseEnrollment(models.Model): ...@@ -648,7 +647,7 @@ class CourseEnrollment(models.Model):
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
class Meta: class Meta:
unique_together = (('user', 'course_id'), ) unique_together = (('user', 'course_id'),)
def __unicode__(self): def __unicode__(self):
return "[CourseEnrollment] %s: %s (%s)" % (self.user, self.course_id, self.created) return "[CourseEnrollment] %s: %s (%s)" % (self.user, self.course_id, self.created)
...@@ -662,16 +661,17 @@ class CourseEnrollmentAllowed(models.Model): ...@@ -662,16 +661,17 @@ class CourseEnrollmentAllowed(models.Model):
""" """
email = models.CharField(max_length=255, db_index=True) email = models.CharField(max_length=255, db_index=True)
course_id = models.CharField(max_length=255, db_index=True) course_id = models.CharField(max_length=255, db_index=True)
auto_enroll = models.BooleanField(default=0)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
class Meta: class Meta:
unique_together = (('email', 'course_id'), ) unique_together = (('email', 'course_id'),)
def __unicode__(self): def __unicode__(self):
return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created) return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created)
#cache_relation(User.profile) # cache_relation(User.profile)
#### Helper methods for use from python manage.py shell and other classes. #### Helper methods for use from python manage.py shell and other classes.
......
...@@ -5,6 +5,7 @@ from django.contrib.auth.models import Group ...@@ -5,6 +5,7 @@ from django.contrib.auth.models import Group
from datetime import datetime from datetime import datetime
from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence
from uuid import uuid4 from uuid import uuid4
from pytz import UTC
# Factories don't have __init__ methods, and are self documenting # Factories don't have __init__ methods, and are self documenting
# pylint: disable=W0232 # pylint: disable=W0232
...@@ -46,8 +47,8 @@ class UserFactory(DjangoModelFactory): ...@@ -46,8 +47,8 @@ class UserFactory(DjangoModelFactory):
is_staff = False is_staff = False
is_active = True is_active = True
is_superuser = False is_superuser = False
last_login = datetime(2012, 1, 1) last_login = datetime(2012, 1, 1, tzinfo=UTC)
date_joined = datetime(2011, 1, 1) date_joined = datetime(2011, 1, 1, tzinfo=UTC)
@post_generation @post_generation
def profile(obj, create, extracted, **kwargs): def profile(obj, create, extracted, **kwargs):
......
...@@ -32,7 +32,7 @@ from student.models import (Registration, UserProfile, TestCenterUser, TestCente ...@@ -32,7 +32,7 @@ from student.models import (Registration, UserProfile, TestCenterUser, TestCente
TestCenterRegistration, TestCenterRegistrationForm, TestCenterRegistration, TestCenterRegistrationForm,
PendingNameChange, PendingEmailChange, PendingNameChange, PendingEmailChange,
CourseEnrollment, unique_id_for_user, CourseEnrollment, unique_id_for_user,
get_testcenter_registration) get_testcenter_registration, CourseEnrollmentAllowed)
from certificates.models import CertificateStatuses, certificate_status_for_student from certificates.models import CertificateStatuses, certificate_status_for_student
...@@ -49,6 +49,7 @@ from courseware.views import get_module_for_descriptor, jump_to ...@@ -49,6 +49,7 @@ from courseware.views import get_module_for_descriptor, jump_to
from courseware.model_data import ModelDataCache from courseware.model_data import ModelDataCache
from statsd import statsd from statsd import statsd
from pytz import UTC
log = logging.getLogger("mitx.student") log = logging.getLogger("mitx.student")
Article = namedtuple('Article', 'title url author image deck publication publish_date') Article = namedtuple('Article', 'title url author image deck publication publish_date')
...@@ -77,7 +78,7 @@ def index(request, extra_context={}, user=None): ...@@ -77,7 +78,7 @@ def index(request, extra_context={}, user=None):
''' '''
# The course selection work is done in courseware.courses. # The course selection work is done in courseware.courses.
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
# do explicit check, because domain=None is valid # do explicit check, because domain=None is valid
if domain == False: if domain == False:
domain = request.META.get('HTTP_HOST') domain = request.META.get('HTTP_HOST')
...@@ -264,7 +265,6 @@ def dashboard(request): ...@@ -264,7 +265,6 @@ def dashboard(request):
if not user.is_active: if not user.is_active:
message = render_to_string('registration/activate_account_notice.html', {'email': user.email}) message = render_to_string('registration/activate_account_notice.html', {'email': user.email})
# Global staff can see what courses errored on their dashboard # Global staff can see what courses errored on their dashboard
staff_access = False staff_access = False
errored_courses = {} errored_courses = {}
...@@ -355,7 +355,7 @@ def change_enrollment(request): ...@@ -355,7 +355,7 @@ def change_enrollment(request):
course = course_from_id(course_id) course = course_from_id(course_id)
except ItemNotFoundError: except ItemNotFoundError:
log.warning("User {0} tried to enroll in non-existent course {1}" log.warning("User {0} tried to enroll in non-existent course {1}"
.format(user.username, course_id)) .format(user.username, course_id))
return HttpResponseBadRequest("Course id is invalid") return HttpResponseBadRequest("Course id is invalid")
if not has_access(user, course, 'enroll'): if not has_access(user, course, 'enroll'):
...@@ -363,9 +363,9 @@ def change_enrollment(request): ...@@ -363,9 +363,9 @@ def change_enrollment(request):
org, course_num, run = course_id.split("/") org, course_num, run = course_id.split("/")
statsd.increment("common.student.enrollment", statsd.increment("common.student.enrollment",
tags=["org:{0}".format(org), tags=["org:{0}".format(org),
"course:{0}".format(course_num), "course:{0}".format(course_num),
"run:{0}".format(run)]) "run:{0}".format(run)])
try: try:
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id) enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
...@@ -382,9 +382,9 @@ def change_enrollment(request): ...@@ -382,9 +382,9 @@ def change_enrollment(request):
org, course_num, run = course_id.split("/") org, course_num, run = course_id.split("/")
statsd.increment("common.student.unenrollment", statsd.increment("common.student.unenrollment",
tags=["org:{0}".format(org), tags=["org:{0}".format(org),
"course:{0}".format(course_num), "course:{0}".format(course_num),
"run:{0}".format(run)]) "run:{0}".format(run)])
return HttpResponse() return HttpResponse()
except CourseEnrollment.DoesNotExist: except CourseEnrollment.DoesNotExist:
...@@ -454,7 +454,6 @@ def login_user(request, error=""): ...@@ -454,7 +454,6 @@ def login_user(request, error=""):
expires_time = time.time() + max_age expires_time = time.time() + max_age
expires = cookie_date(expires_time) expires = cookie_date(expires_time)
response.set_cookie(settings.EDXMKTG_COOKIE_NAME, response.set_cookie(settings.EDXMKTG_COOKIE_NAME,
'true', max_age=max_age, 'true', max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
...@@ -515,8 +514,8 @@ def _do_create_account(post_vars): ...@@ -515,8 +514,8 @@ def _do_create_account(post_vars):
Note: this function is also used for creating test users. Note: this function is also used for creating test users.
""" """
user = User(username=post_vars['username'], user = User(username=post_vars['username'],
email=post_vars['email'], email=post_vars['email'],
is_active=False) is_active=False)
user.set_password(post_vars['password']) user.set_password(post_vars['password'])
registration = Registration() registration = Registration()
# TODO: Rearrange so that if part of the process fails, the whole process fails. # TODO: Rearrange so that if part of the process fails, the whole process fails.
...@@ -632,7 +631,7 @@ def create_account(request, post_override=None): ...@@ -632,7 +631,7 @@ def create_account(request, post_override=None):
# Ok, looks like everything is legit. Create the account. # Ok, looks like everything is legit. Create the account.
ret = _do_create_account(post_vars) ret = _do_create_account(post_vars)
if isinstance(ret, HttpResponse): # if there was an error then return that if isinstance(ret, HttpResponse): # if there was an error then return that
return ret return ret
(user, profile, registration) = ret (user, profile, registration) = ret
...@@ -670,7 +669,7 @@ def create_account(request, post_override=None): ...@@ -670,7 +669,7 @@ def create_account(request, post_override=None):
if DoExternalAuth: if DoExternalAuth:
eamap.user = login_user eamap.user = login_user
eamap.dtsignup = datetime.datetime.now() eamap.dtsignup = datetime.datetime.now(UTC)
eamap.save() eamap.save()
log.debug('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'], eamap)) log.debug('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'], eamap))
...@@ -698,7 +697,6 @@ def create_account(request, post_override=None): ...@@ -698,7 +697,6 @@ def create_account(request, post_override=None):
expires_time = time.time() + max_age expires_time = time.time() + max_age
expires = cookie_date(expires_time) expires = cookie_date(expires_time)
response.set_cookie(settings.EDXMKTG_COOKIE_NAME, response.set_cookie(settings.EDXMKTG_COOKIE_NAME,
'true', max_age=max_age, 'true', max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
...@@ -708,7 +706,6 @@ def create_account(request, post_override=None): ...@@ -708,7 +706,6 @@ def create_account(request, post_override=None):
return response return response
def exam_registration_info(user, course): def exam_registration_info(user, course):
""" Returns a Registration object if the user is currently registered for a current """ Returns a Registration object if the user is currently registered for a current
exam of the course. Returns None if the user is not registered, or if there is no exam of the course. Returns None if the user is not registered, or if there is no
...@@ -849,7 +846,6 @@ def create_exam_registration(request, post_override=None): ...@@ -849,7 +846,6 @@ def create_exam_registration(request, post_override=None):
response_data['non_field_errors'] = form.non_field_errors() response_data['non_field_errors'] = form.non_field_errors()
return HttpResponse(json.dumps(response_data), mimetype="application/json") return HttpResponse(json.dumps(response_data), mimetype="application/json")
# only do the following if there is accommodation text to send, # only do the following if there is accommodation text to send,
# and a destination to which to send it. # and a destination to which to send it.
# TODO: still need to create the accommodation email templates # TODO: still need to create the accommodation email templates
...@@ -872,7 +868,6 @@ def create_exam_registration(request, post_override=None): ...@@ -872,7 +868,6 @@ def create_exam_registration(request, post_override=None):
# response_data['non_field_errors'] = [ 'Could not send accommodation e-mail.', ] # response_data['non_field_errors'] = [ 'Could not send accommodation e-mail.', ]
# return HttpResponse(json.dumps(response_data), mimetype="application/json") # return HttpResponse(json.dumps(response_data), mimetype="application/json")
js = {'success': True} js = {'success': True}
return HttpResponse(json.dumps(js), mimetype="application/json") return HttpResponse(json.dumps(js), mimetype="application/json")
...@@ -916,6 +911,16 @@ def activate_account(request, key): ...@@ -916,6 +911,16 @@ def activate_account(request, key):
if not r[0].user.is_active: if not r[0].user.is_active:
r[0].activate() r[0].activate()
already_active = False already_active = False
#Enroll student in any pending courses he/she may have if auto_enroll flag is set
student = User.objects.filter(id=r[0].user_id)
if student:
ceas = CourseEnrollmentAllowed.objects.filter(email=student[0].email)
for cea in ceas:
if cea.auto_enroll:
course_id = cea.course_id
enrollment, created = CourseEnrollment.objects.get_or_create(user_id=student[0].id, course_id=course_id)
resp = render_to_response("registration/activation_complete.html", {'user_logged_in': user_logged_in, 'already_active': already_active}) resp = render_to_response("registration/activation_complete.html", {'user_logged_in': user_logged_in, 'already_active': already_active})
return resp return resp
if len(r) == 0: if len(r) == 0:
...@@ -1194,6 +1199,10 @@ def accept_name_change(request): ...@@ -1194,6 +1199,10 @@ def accept_name_change(request):
def _get_news(top=None): def _get_news(top=None):
"Return the n top news items on settings.RSS_URL" "Return the n top news items on settings.RSS_URL"
# Don't return anything if we're in a themed site
if settings.MITX_FEATURES["USE_CUSTOM_THEME"]:
return None
feed_data = cache.get("students_index_rss_feed_data") feed_data = cache.get("students_index_rss_feed_data")
if feed_data is None: if feed_data is None:
if hasattr(settings, 'RSS_URL'): if hasattr(settings, 'RSS_URL'):
......
...@@ -87,8 +87,8 @@ def reset_data(scenario): ...@@ -87,8 +87,8 @@ def reset_data(scenario):
LOGGER.debug("Flushing the test database...") LOGGER.debug("Flushing the test database...")
call_command('flush', interactive=False) call_command('flush', interactive=False)
# Uncomment below to trigger a screenshot on error
@after.each_scenario # @after.each_scenario
def screenshot_on_error(scenario): def screenshot_on_error(scenario):
""" """
Save a screenshot to help with debugging. Save a screenshot to help with debugging.
......
...@@ -129,9 +129,12 @@ def should_have_link_with_id_and_text(step, link_id, text): ...@@ -129,9 +129,12 @@ def should_have_link_with_id_and_text(step, link_id, text):
assert_equals(link.text, text) assert_equals(link.text, text)
@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page') @step(r'should( not)? see "(.*)" (?:somewhere|anywhere) (?:in|on) (?:the|this) page')
def should_see_in_the_page(step, text): def should_see_in_the_page(step, doesnt_appear, text):
assert_in(text, world.css_text('body')) if doesnt_appear:
assert world.browser.is_text_not_present(text, wait_time=5)
else:
assert world.browser.is_text_present(text, wait_time=5)
@step('I am logged in$') @step('I am logged in$')
...@@ -156,3 +159,33 @@ def registered_edx_user(step, uname): ...@@ -156,3 +159,33 @@ def registered_edx_user(step, uname):
@step(u'All dialogs should be closed$') @step(u'All dialogs should be closed$')
def dialogs_are_closed(step): def dialogs_are_closed(step):
assert world.dialogs_closed() assert world.dialogs_closed()
@step('I will confirm all alerts')
def i_confirm_all_alerts(step):
"""
Please note: This method must be called RIGHT BEFORE an expected alert
Window variables are page local and thus all changes are removed upon navigating to a new page
In addition, this method changes the functionality of ONLY future alerts
"""
world.browser.execute_script('window.confirm = function(){return true;} ; window.alert = function(){return;}')
@step('I will cancel all alerts')
def i_cancel_all_alerts(step):
"""
Please note: This method must be called RIGHT BEFORE an expected alert
Window variables are page local and thus all changes are removed upon navigating to a new page
In addition, this method changes the functionality of ONLY future alerts
"""
world.browser.execute_script('window.confirm = function(){return false;} ; window.alert = function(){return;}')
@step('I will answer all prompts with "([^"]*)"')
def i_answer_prompts_with(step, prompt):
"""
Please note: This method must be called RIGHT BEFORE an expected alert
Window variables are page local and thus all changes are removed upon navigating to a new page
In addition, this method changes the functionality of ONLY future alerts
"""
world.browser.execute_script('window.prompt = function(){return %s;}') % prompt
...@@ -32,8 +32,13 @@ def url_equals(url): ...@@ -32,8 +32,13 @@ def url_equals(url):
@world.absorb @world.absorb
def is_css_present(css_selector): def is_css_present(css_selector, wait_time=5):
return world.browser.is_element_present_by_css(css_selector, wait_time=4) return world.browser.is_element_present_by_css(css_selector, wait_time=wait_time)
@world.absorb
def is_css_not_present(css_selector, wait_time=5):
return world.browser.is_element_not_present_by_css(css_selector, wait_time=wait_time)
@world.absorb @world.absorb
...@@ -42,11 +47,11 @@ def css_has_text(css_selector, text): ...@@ -42,11 +47,11 @@ def css_has_text(css_selector, text):
@world.absorb @world.absorb
def css_find(css): def css_find(css, wait_time=5):
def is_visible(driver): def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR, css,)) return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
world.browser.is_element_present_by_css(css, 5) world.browser.is_element_present_by_css(css, wait_time=wait_time)
wait_for(is_visible) wait_for(is_visible)
return world.browser.find_by_css(css) return world.browser.find_by_css(css)
...@@ -56,6 +61,7 @@ def css_click(css_selector): ...@@ -56,6 +61,7 @@ def css_click(css_selector):
""" """
Perform a click on a CSS selector, retrying if it initially fails Perform a click on a CSS selector, retrying if it initially fails
""" """
assert is_css_present(css_selector)
try: try:
world.browser.find_by_css(css_selector).click() world.browser.find_by_css(css_selector).click()
...@@ -89,6 +95,7 @@ def id_click(elem_id): ...@@ -89,6 +95,7 @@ def id_click(elem_id):
@world.absorb @world.absorb
def css_fill(css_selector, text): def css_fill(css_selector, text):
assert is_css_present(css_selector)
world.browser.find_by_css(css_selector).first.fill(text) world.browser.find_by_css(css_selector).first.fill(text)
...@@ -114,6 +121,7 @@ def css_text(css_selector): ...@@ -114,6 +121,7 @@ def css_text(css_selector):
@world.absorb @world.absorb
def css_visible(css_selector): def css_visible(css_selector):
assert is_css_present(css_selector)
return world.browser.find_by_css(css_selector).visible return world.browser.find_by_css(css_selector).visible
......
'''
Created on Jun 6, 2013
@author: dmitchell
'''
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from student.tests.factories import AdminFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
import xmodule_modifiers
import datetime
from pytz import UTC
from xmodule.modulestore.tests import factories
class TestXmoduleModfiers(ModuleStoreTestCase):
# FIXME disabled b/c start date inheritance is not occuring and render_... in get_html is failing due
# to middleware.lookup['main'] not being defined
def _test_add_histogram(self):
instructor = AdminFactory.create()
self.client.login(username=instructor.username, password='test')
course = CourseFactory.create(org='test',
number='313', display_name='histogram test')
section = ItemFactory.create(
parent_location=course.location, display_name='chapter hist',
template='i4x://edx/templates/chapter/Empty')
problem = ItemFactory.create(
parent_location=section.location, display_name='problem hist 1',
template='i4x://edx/templates/problem/Blank_Common_Problem')
problem.has_score = False # don't trip trying to retrieve db data
late_problem = ItemFactory.create(
parent_location=section.location, display_name='problem hist 2',
template='i4x://edx/templates/problem/Blank_Common_Problem')
late_problem.lms.start = datetime.datetime.now(UTC) + datetime.timedelta(days=32)
late_problem.has_score = False
problem_module = factories.get_test_xmodule_for_descriptor(problem)
problem_module.get_html = xmodule_modifiers.add_histogram(lambda:'', problem_module, instructor)
self.assertRegexpMatches(
problem_module.get_html(), r'.*<font color=\'green\'>Not yet</font>.*')
problem_module = factories.get_test_xmodule_for_descriptor(late_problem)
problem_module.get_html = xmodule_modifiers.add_histogram(lambda: '', problem_module, instructor)
self.assertRegexpMatches(
problem_module.get_html(), r'.*<font color=\'red\'>Yes!</font>.*')
...@@ -14,6 +14,7 @@ from mitxmako.shortcuts import render_to_response ...@@ -14,6 +14,7 @@ from mitxmako.shortcuts import render_to_response
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from track.models import TrackingLog from track.models import TrackingLog
from pytz import UTC
log = logging.getLogger("tracking") log = logging.getLogger("tracking")
...@@ -59,7 +60,7 @@ def user_track(request): ...@@ -59,7 +60,7 @@ def user_track(request):
"event": request.GET['event'], "event": request.GET['event'],
"agent": agent, "agent": agent,
"page": request.GET['page'], "page": request.GET['page'],
"time": datetime.datetime.utcnow().isoformat(), "time": datetime.datetime.now(UTC).isoformat(),
"host": request.META['SERVER_NAME'], "host": request.META['SERVER_NAME'],
} }
log_event(event) log_event(event)
...@@ -85,11 +86,11 @@ def server_track(request, event_type, event, page=None): ...@@ -85,11 +86,11 @@ def server_track(request, event_type, event, page=None):
"event": event, "event": event,
"agent": agent, "agent": agent,
"page": page, "page": page,
"time": datetime.datetime.utcnow().isoformat(), "time": datetime.datetime.now(UTC).isoformat(),
"host": request.META['SERVER_NAME'], "host": request.META['SERVER_NAME'],
} }
if event_type.startswith("/event_logs") and request.user.is_staff: # don't log if event_type.startswith("/event_logs") and request.user.is_staff: # don't log
return return
log_event(event) log_event(event)
......
import sys
from django.conf import settings
from django.core.urlresolvers import clear_url_caches
class UrlResetMixin(object):
"""Mixin to reset urls.py before and after a test
Django memoizes the function that reads the urls module (whatever module
urlconf names). The module itself is also stored by python in sys.modules.
To fully reload it, we need to reload the python module, and also clear django's
cache of the parsed urls.
However, the order in which we do this doesn't matter, because neither one will
get reloaded until the next request
Doing this is expensive, so it should only be added to tests that modify settings
that affect the contents of urls.py
"""
def _reset_urls(self, urlconf=None):
if urlconf is None:
urlconf = settings.ROOT_URLCONF
if urlconf in sys.modules:
reload(sys.modules[urlconf])
clear_url_caches()
def setUp(self):
"""Reset django default urlconf before tests and after tests"""
super(UrlResetMixin, self).setUp()
self._reset_urls()
self.addCleanup(self._reset_urls)
...@@ -12,6 +12,7 @@ from django.core.validators import ValidationError, validate_email ...@@ -12,6 +12,7 @@ from django.core.validators import ValidationError, validate_email
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponseServerError from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponseServerError
from django.shortcuts import redirect from django.shortcuts import redirect
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from dogapi import dog_stats_api
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from urllib import urlencode from urllib import urlencode
import zendesk import zendesk
...@@ -73,11 +74,64 @@ class _ZendeskApi(object): ...@@ -73,11 +74,64 @@ class _ZendeskApi(object):
self._zendesk_instance.update_ticket(ticket_id=ticket_id, data=update) self._zendesk_instance.update_ticket(ticket_id=ticket_id, data=update)
def submit_feedback_via_zendesk(request): def _record_feedback_in_zendesk(realname, email, subject, details, tags, additional_info):
""" """
Create a new user-requested Zendesk ticket. Create a new user-requested Zendesk ticket.
If Zendesk submission is not enabled, any request will raise `Http404`. Once created, the ticket will be updated with a private comment containing
additional information from the browser and server, such as HTTP headers
and user state. Returns a boolean value indicating whether ticket creation
was successful, regardless of whether the private comment update succeeded.
"""
zendesk_api = _ZendeskApi()
additional_info_string = (
"Additional information:\n\n" +
"\n".join("%s: %s" % (key, value) for (key, value) in additional_info.items() if value is not None)
)
# Tag all issues with LMS to distinguish channel in Zendesk; requested by student support team
zendesk_tags = list(tags.values()) + ["LMS"]
new_ticket = {
"ticket": {
"requester": {"name": realname, "email": email},
"subject": subject,
"comment": {"body": details},
"tags": zendesk_tags
}
}
try:
ticket_id = zendesk_api.create_ticket(new_ticket)
except zendesk.ZendeskError as err:
log.error("Error creating Zendesk ticket: %s", str(err))
return False
# Additional information is provided as a private update so the information
# is not visible to the user.
ticket_update = {"ticket": {"comment": {"public": False, "body": additional_info_string}}}
try:
zendesk_api.update_ticket(ticket_id, ticket_update)
except zendesk.ZendeskError as err:
log.error("Error updating Zendesk ticket: %s", str(err))
# The update is not strictly necessary, so do not indicate failure to the user
pass
return True
DATADOG_FEEDBACK_METRIC = "lms_feedback_submissions"
def _record_feedback_in_datadog(tags):
datadog_tags = ["{k}:{v}".format(k=k, v=v) for k, v in tags.items()]
dog_stats_api.increment(DATADOG_FEEDBACK_METRIC, tags=datadog_tags)
def submit_feedback(request):
"""
Create a new user-requested ticket, currently implemented with Zendesk.
If feedback submission is not enabled, any request will raise `Http404`.
If any configuration parameter (`ZENDESK_URL`, `ZENDESK_USER`, or If any configuration parameter (`ZENDESK_URL`, `ZENDESK_USER`, or
`ZENDESK_API_KEY`) is missing, any request will raise an `Exception`. `ZENDESK_API_KEY`) is missing, any request will raise an `Exception`.
The request must be a POST request specifying `subject` and `details`. The request must be a POST request specifying `subject` and `details`.
...@@ -85,12 +139,9 @@ def submit_feedback_via_zendesk(request): ...@@ -85,12 +139,9 @@ def submit_feedback_via_zendesk(request):
`email`. If the user is authenticated, the `name` and `email` will be `email`. If the user is authenticated, the `name` and `email` will be
populated from the user's information. If any required parameter is populated from the user's information. If any required parameter is
missing, a 400 error will be returned indicating which field is missing and missing, a 400 error will be returned indicating which field is missing and
providing an error message. If Zendesk returns any error on ticket providing an error message. If Zendesk ticket creation fails, 500 error
creation, a 500 error will be returned with no body. Once created, the will be returned with no body; if ticket creation succeeds, an empty
ticket will be updated with a private comment containing additional successful response (200) will be returned.
information from the browser and server, such as HTTP headers and user
state. Whether or not the update succeeds, if the user's ticket is
successfully created, an empty successful response (200) will be returned.
""" """
if not settings.MITX_FEATURES.get('ENABLE_FEEDBACK_SUBMISSION', False): if not settings.MITX_FEATURES.get('ENABLE_FEEDBACK_SUBMISSION', False):
raise Http404() raise Http404()
...@@ -124,9 +175,9 @@ def submit_feedback_via_zendesk(request): ...@@ -124,9 +175,9 @@ def submit_feedback_via_zendesk(request):
subject = request.POST["subject"] subject = request.POST["subject"]
details = request.POST["details"] details = request.POST["details"]
tags = [] tags = dict(
if "tag" in request.POST: [(tag, request.POST[tag]) for tag in ["issue_type", "course_id"] if tag in request.POST]
tags = [request.POST["tag"]] )
if request.user.is_authenticated(): if request.user.is_authenticated():
realname = request.user.profile.name realname = request.user.profile.name
...@@ -140,41 +191,18 @@ def submit_feedback_via_zendesk(request): ...@@ -140,41 +191,18 @@ def submit_feedback_via_zendesk(request):
except ValidationError: except ValidationError:
return build_error_response(400, "email", required_field_errs["email"]) return build_error_response(400, "email", required_field_errs["email"])
for header in ["HTTP_REFERER", "HTTP_USER_AGENT"]: for header, pretty in [
additional_info[header] = request.META.get(header) ("HTTP_REFERER", "Page"),
("HTTP_USER_AGENT", "Browser"),
("REMOTE_ADDR", "Client IP"),
("SERVER_NAME", "Host")
]:
additional_info[pretty] = request.META.get(header)
zendesk_api = _ZendeskApi() success = _record_feedback_in_zendesk(realname, email, subject, details, tags, additional_info)
_record_feedback_in_datadog(tags)
additional_info_string = (
"Additional information:\n\n" +
"\n".join("%s: %s" % (key, value) for (key, value) in additional_info.items() if value is not None)
)
new_ticket = {
"ticket": {
"requester": {"name": realname, "email": email},
"subject": subject,
"comment": {"body": details},
"tags": tags
}
}
try:
ticket_id = zendesk_api.create_ticket(new_ticket)
except zendesk.ZendeskError as err:
log.error("Error creating Zendesk ticket: %s", str(err))
return HttpResponse(status=500)
# Additional information is provided as a private update so the information
# is not visible to the user.
ticket_update = {"ticket": {"comment": {"public": False, "body": additional_info_string}}}
try:
zendesk_api.update_ticket(ticket_id, ticket_update)
except zendesk.ZendeskError as err:
log.error("Error updating Zendesk ticket: %s", str(err))
# The update is not strictly necessary, so do not indicate failure to the user
pass
return HttpResponse() return HttpResponse(status=(200 if success else 500))
def info(request): def info(request):
......
import re import re
import json import json
import logging import logging
import time
import static_replace import static_replace
from django.conf import settings from django.conf import settings
...@@ -9,6 +8,8 @@ from functools import wraps ...@@ -9,6 +8,8 @@ from functools import wraps
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from xmodule.seq_module import SequenceModule from xmodule.seq_module import SequenceModule
from xmodule.vertical_module import VerticalModule from xmodule.vertical_module import VerticalModule
import datetime
from django.utils.timezone import UTC
log = logging.getLogger("mitx.xmodule_modifiers") log = logging.getLogger("mitx.xmodule_modifiers")
...@@ -83,7 +84,7 @@ def grade_histogram(module_id): ...@@ -83,7 +84,7 @@ def grade_histogram(module_id):
cursor.execute(q, [module_id]) cursor.execute(q, [module_id])
grades = list(cursor.fetchall()) grades = list(cursor.fetchall())
grades.sort(key=lambda x: x[0]) # Add ORDER BY to sql query? grades.sort(key=lambda x: x[0]) # Add ORDER BY to sql query?
if len(grades) >= 1 and grades[0][0] is None: if len(grades) >= 1 and grades[0][0] is None:
return [] return []
return grades return grades
...@@ -101,7 +102,7 @@ def add_histogram(get_html, module, user): ...@@ -101,7 +102,7 @@ def add_histogram(get_html, module, user):
@wraps(get_html) @wraps(get_html)
def _get_html(): def _get_html():
if type(module) in [SequenceModule, VerticalModule]: # TODO: make this more general, eg use an XModule attribute instead if type(module) in [SequenceModule, VerticalModule]: # TODO: make this more general, eg use an XModule attribute instead
return get_html() return get_html()
module_id = module.id module_id = module.id
...@@ -132,7 +133,7 @@ def add_histogram(get_html, module, user): ...@@ -132,7 +133,7 @@ def add_histogram(get_html, module, user):
# useful to indicate to staff if problem has been released or not # useful to indicate to staff if problem has been released or not
# TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here # TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
now = time.gmtime() now = datetime.datetime.now(UTC())
is_released = "unknown" is_released = "unknown"
mstart = module.descriptor.lms.start mstart = module.descriptor.lms.start
......
...@@ -470,6 +470,7 @@ class LoncapaProblem(object): ...@@ -470,6 +470,7 @@ class LoncapaProblem(object):
python_path=python_path, python_path=python_path,
cache=self.system.cache, cache=self.system.cache,
slug=self.problem_id, slug=self.problem_id,
unsafely=self.system.can_execute_unsafe_code(),
) )
except Exception as err: except Exception as err:
log.exception("Error while execing script code: " + all_code) log.exception("Error while execing script code: " + all_code)
......
...@@ -144,11 +144,11 @@ class InputTypeBase(object): ...@@ -144,11 +144,11 @@ class InputTypeBase(object):
self.tag = xml.tag self.tag = xml.tag
self.system = system self.system = system
## NOTE: ID should only come from one place. If it comes from multiple, # NOTE: ID should only come from one place. If it comes from multiple,
## we use state first, XML second (in case the xml changed, but we have # we use state first, XML second (in case the xml changed, but we have
## existing state with an old id). Since we don't make this guarantee, # existing state with an old id). Since we don't make this guarantee,
## we can swap this around in the future if there's a more logical # we can swap this around in the future if there's a more logical
## order. # order.
self.input_id = state.get('id', xml.get('id')) self.input_id = state.get('id', xml.get('id'))
if self.input_id is None: if self.input_id is None:
...@@ -769,7 +769,7 @@ class MatlabInput(CodeInput): ...@@ -769,7 +769,7 @@ class MatlabInput(CodeInput):
# construct xqueue headers # construct xqueue headers
qinterface = self.system.xqueue['interface'] qinterface = self.system.xqueue['interface']
qtime = datetime.strftime(datetime.utcnow(), xqueue_interface.dateformat) qtime = datetime.utcnow().strftime(xqueue_interface.dateformat)
callback_url = self.system.xqueue['construct_callback']('ungraded_response') callback_url = self.system.xqueue['construct_callback']('ungraded_response')
anonymous_student_id = self.system.anonymous_student_id anonymous_student_id = self.system.anonymous_student_id
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
......
...@@ -288,7 +288,14 @@ class LoncapaResponse(object): ...@@ -288,7 +288,14 @@ class LoncapaResponse(object):
} }
try: try:
safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'], slug=self.id) safe_exec.safe_exec(
code,
globals_dict,
python_path=self.context['python_path'],
slug=self.id,
random_seed=self.context['seed'],
unsafely=self.system.can_execute_unsafe_code(),
)
except Exception as err: except Exception as err:
msg = 'Error %s in evaluating hint function %s' % (err, hintfn) msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
msg += "\nSee XML source line %s" % getattr( msg += "\nSee XML source line %s" % getattr(
...@@ -973,7 +980,14 @@ class CustomResponse(LoncapaResponse): ...@@ -973,7 +980,14 @@ class CustomResponse(LoncapaResponse):
'ans': ans, 'ans': ans,
} }
globals_dict.update(kwargs) globals_dict.update(kwargs)
safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'], slug=self.id) safe_exec.safe_exec(
code,
globals_dict,
python_path=self.context['python_path'],
slug=self.id,
random_seed=self.context['seed'],
unsafely=self.system.can_execute_unsafe_code(),
)
return globals_dict['cfn_return'] return globals_dict['cfn_return']
return check_function return check_function
...@@ -1090,7 +1104,14 @@ class CustomResponse(LoncapaResponse): ...@@ -1090,7 +1104,14 @@ class CustomResponse(LoncapaResponse):
# exec the check function # exec the check function
if isinstance(self.code, basestring): if isinstance(self.code, basestring):
try: try:
safe_exec.safe_exec(self.code, self.context, cache=self.system.cache, slug=self.id) safe_exec.safe_exec(
self.code,
self.context,
cache=self.system.cache,
slug=self.id,
random_seed=self.context['seed'],
unsafely=self.system.can_execute_unsafe_code(),
)
except Exception as err: except Exception as err:
self._handle_exec_exception(err) self._handle_exec_exception(err)
...@@ -1815,7 +1836,14 @@ class SchematicResponse(LoncapaResponse): ...@@ -1815,7 +1836,14 @@ class SchematicResponse(LoncapaResponse):
] ]
self.context.update({'submission': submission}) self.context.update({'submission': submission})
try: try:
safe_exec.safe_exec(self.code, self.context, cache=self.system.cache, slug=self.id) safe_exec.safe_exec(
self.code,
self.context,
cache=self.system.cache,
slug=self.id,
random_seed=self.context['seed'],
unsafely=self.system.can_execute_unsafe_code(),
)
except Exception as err: except Exception as err:
msg = 'Error %s in evaluating SchematicResponse' % err msg = 'Error %s in evaluating SchematicResponse' % err
raise ResponseError(msg) raise ResponseError(msg)
......
"""Capa's specialized use of codejail.safe_exec.""" """Capa's specialized use of codejail.safe_exec."""
from codejail.safe_exec import safe_exec as codejail_safe_exec from codejail.safe_exec import safe_exec as codejail_safe_exec
from codejail.safe_exec import not_safe_exec as codejail_not_safe_exec
from codejail.safe_exec import json_safe, SafeExecException from codejail.safe_exec import json_safe, SafeExecException
from . import lazymod from . import lazymod
from statsd import statsd from statsd import statsd
...@@ -71,7 +72,7 @@ def update_hash(hasher, obj): ...@@ -71,7 +72,7 @@ def update_hash(hasher, obj):
@statsd.timed('capa.safe_exec.time') @statsd.timed('capa.safe_exec.time')
def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None, slug=None): def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None, slug=None, unsafely=False):
""" """
Execute python code safely. Execute python code safely.
...@@ -90,6 +91,8 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None ...@@ -90,6 +91,8 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None
`slug` is an arbitrary string, a description that's meaningful to the `slug` is an arbitrary string, a description that's meaningful to the
caller, that will be used in log messages. caller, that will be used in log messages.
If `unsafely` is true, then the code will actually be executed without sandboxing.
""" """
# Check the cache for a previous result. # Check the cache for a previous result.
if cache: if cache:
...@@ -111,9 +114,15 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None ...@@ -111,9 +114,15 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None
# Create the complete code we'll run. # Create the complete code we'll run.
code_prolog = CODE_PROLOG % random_seed code_prolog = CODE_PROLOG % random_seed
# Decide which code executor to use.
if unsafely:
exec_fn = codejail_not_safe_exec
else:
exec_fn = codejail_safe_exec
# Run the code! Results are side effects in globals_dict. # Run the code! Results are side effects in globals_dict.
try: try:
codejail_safe_exec( exec_fn(
code_prolog + LAZY_IMPORTS + code, globals_dict, code_prolog + LAZY_IMPORTS + code, globals_dict,
python_path=python_path, slug=slug, python_path=python_path, slug=slug,
) )
......
"""Test safe_exec.py""" """Test safe_exec.py"""
import hashlib import hashlib
import os
import os.path import os.path
import random import random
import textwrap import textwrap
import unittest import unittest
from nose.plugins.skip import SkipTest
from capa.safe_exec import safe_exec, update_hash from capa.safe_exec import safe_exec, update_hash
from codejail.safe_exec import SafeExecException from codejail.safe_exec import SafeExecException
from codejail.jail_code import is_configured
class TestSafeExec(unittest.TestCase): class TestSafeExec(unittest.TestCase):
...@@ -68,6 +72,24 @@ class TestSafeExec(unittest.TestCase): ...@@ -68,6 +72,24 @@ class TestSafeExec(unittest.TestCase):
self.assertIn("ZeroDivisionError", cm.exception.message) self.assertIn("ZeroDivisionError", cm.exception.message)
class TestSafeOrNot(unittest.TestCase):
def test_cant_do_something_forbidden(self):
# Can't test for forbiddenness if CodeJail isn't configured for python.
if not is_configured("python"):
raise SkipTest
g = {}
with self.assertRaises(SafeExecException) as cm:
safe_exec("import os; files = os.listdir('/')", g)
self.assertIn("OSError", cm.exception.message)
self.assertIn("Permission denied", cm.exception.message)
def test_can_do_something_forbidden_if_run_unsafely(self):
g = {}
safe_exec("import os; files = os.listdir('/')", g, unsafely=True)
self.assertEqual(g['files'], os.listdir('/'))
class DictCache(object): class DictCache(object):
"""A cache implementation over a simple dict, for testing.""" """A cache implementation over a simple dict, for testing."""
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
<div class="block block-comment">${comment}</div> <div class="block block-comment">${comment}</div>
<div class="block">${comment_prompt}</div> <div class="block">${comment_prompt}</div>
<textarea class="comment" id="input_${id}_comment" name="input_${id}_comment">${comment_value|h}</textarea> <textarea class="comment" id="input_${id}_comment" name="input_${id}_comment" aria-describedby="answer_${id}">${comment_value|h}</textarea>
<div class="block">${tag_prompt}</div> <div class="block">${tag_prompt}</div>
<ul class="tags"> <ul class="tags">
...@@ -22,11 +22,11 @@ ...@@ -22,11 +22,11 @@
<li> <li>
% if has_options_value: % if has_options_value:
% if all([c == 'correct' for c in option['choice'], status]): % if all([c == 'correct' for c in option['choice'], status]):
<span class="tag-status correct" id="status_${id}"></span> <span class="tag-status correct" id="status_${id}" aria-describedby="input_${id}_comment"><span class="sr">Status: Correct</span></span>
% elif all([c == 'partially-correct' for c in option['choice'], status]): % elif all([c == 'partially-correct' for c in option['choice'], status]):
<span class="tag-status partially-correct" id="status_${id}"></span> <span class="tag-status partially-correct" id="status_${id}" aria-describedby="input_${id}_comment"><span class="sr">Status: Partially Correct</span></span>
% elif all([c == 'incorrect' for c in option['choice'], status]): % elif all([c == 'incorrect' for c in option['choice'], status]):
<span class="tag-status incorrect" id="status_${id}"></span> <span class="tag-status incorrect" id="status_${id}" aria-describedby="input_${id}_comment"><span class="sr">Status: Incorrect</span></span>
% endif % endif
% endif % endif
...@@ -53,11 +53,11 @@ ...@@ -53,11 +53,11 @@
% endif % endif
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: Unanswered</span></span>
% elif status == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: Incorrect</span></span>
% elif status == 'incorrect' and not has_options_value: % elif status == 'incorrect' and not has_options_value:
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: Incorrect</span></span>
% endif % endif
<p id="answer_${id}" class="answer answer-annotation"></p> <p id="answer_${id}" class="answer answer-annotation"></p>
......
...@@ -11,13 +11,13 @@ ...@@ -11,13 +11,13 @@
<div class="incorrect" id="status_${id}"> <div class="incorrect" id="status_${id}">
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" data-input-id="${id}" value="${value|h}" <input type="text" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" data-input-id="${id}" value="${value|h}"
% if size: % if size:
size="${size}" size="${size}"
% endif % endif
/> />
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -3,12 +3,12 @@ ...@@ -3,12 +3,12 @@
% if input_type == 'checkbox' or not value: % if input_type == 'checkbox' or not value:
% if status == 'unsubmitted' or show_correctness == 'never': % if status == 'unsubmitted' or show_correctness == 'never':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif status == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"><span class="sr">Status: correct</span></span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"><span class="sr">Status: incorrect</span></span>
% elif status == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"><span class="sr">Status: incomplete</span></span>
% endif % endif
% endif % endif
</div> </div>
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
% for choice_id, choice_description in choices: % for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}" <label for="input_${id}_${choice_id}"
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ): % if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
<% <%
if status == 'correct': if status == 'correct':
correctness = 'correct' correctness = 'correct'
elif status == 'incorrect': elif status == 'incorrect':
...@@ -31,14 +31,29 @@ ...@@ -31,14 +31,29 @@
% endif % endif
% endif % endif
> >
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}" <input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" aria-describedby="answer_${id}" value="${choice_id}"
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ): % if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
checked="true" checked="true"
% elif input_type != 'radio' and choice_id in value: % elif input_type != 'radio' and choice_id in value:
checked="true" checked="true"
% endif % endif
/> ${choice_description} </label> /> ${choice_description}
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
<%
if status == 'correct':
correctness = 'correct'
elif status == 'incorrect':
correctness = 'incorrect'
else:
correctness = None
%>
% if correctness and not show_correctness=='never':
<span class="sr" aria-describedby="input_${id}_${choice_id}">Status: ${correctness}</span>
% endif
% endif
</label>
% endfor % endfor
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
</fieldset> </fieldset>
......
<section id="textbox_${id}" class="textbox"> <section id="textbox_${id}" class="textbox">
<textarea rows="${rows}" cols="${cols}" name="input_${id}" id="input_${id}" <textarea rows="${rows}" cols="${cols}" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}"
% if hidden: % if hidden:
style="display:none;" style="display:none;"
% endif % endif
...@@ -7,13 +7,13 @@ ...@@ -7,13 +7,13 @@
<div class="grader-status"> <div class="grader-status">
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span> <span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Unanswered</span>
% elif status == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}">Correct</span> <span class="correct" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Correct</span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}">Incorrect</span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Incorrect</span>
% elif status == 'queued': % elif status == 'queued':
<span class="processing" id="status_${id}">Queued</span> <span class="processing" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Queued</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span> <span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
......
...@@ -20,9 +20,9 @@ ...@@ -20,9 +20,9 @@
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}" style="display:none;"/> <input type="text" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}" value="${value|h}" style="display:none;"/>
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -11,12 +11,12 @@ ...@@ -11,12 +11,12 @@
% elif status == 'incomplete': % elif status == 'incomplete':
<div class="incomplete" id="status_${id}"> <div class="incomplete" id="status_${id}">
% endif % endif
<div id="protex_container"></div> <div id="protex_container"></div>
<input type="hidden" name="target_shape" id="target_shape" value ="${target_shape}"></input> <input type="hidden" name="target_shape" id="target_shape" value ="${target_shape}"></input>
<input type="hidden" name="input_${id}" id="input_${id}" value="${value|h}"/> <input type="hidden" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"/>
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -19,10 +19,10 @@ ...@@ -19,10 +19,10 @@
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}" <input type="text" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"
style="display:none;"/> style="display:none;"/>
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -11,13 +11,13 @@ ...@@ -11,13 +11,13 @@
% elif status == 'incomplete': % elif status == 'incomplete':
<div class="incomplete" id="status_${id}"> <div class="incomplete" id="status_${id}">
% endif % endif
<div id="genex_container"></div> <div id="genex_container"></div>
<input type="hidden" name="genex_dna_sequence" id="genex_dna_sequence" value ="${genex_dna_sequence}"></input> <input type="hidden" name="genex_dna_sequence" id="genex_dna_sequence" value ="${genex_dna_sequence}"></input>
<input type="hidden" name="genex_problem_number" id="genex_problem_number" value ="${genex_problem_number}"></input> <input type="hidden" name="genex_problem_number" id="genex_problem_number" value ="${genex_problem_number}"></input>
<input type="hidden" name="input_${id}" id="input_${id}" value="${value|h}"/> <input type="hidden" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}" value="${value|h}"/>
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -16,13 +16,13 @@ ...@@ -16,13 +16,13 @@
<br/> <br/>
<input type="hidden" name="input_${id}" id="input_${id}" value="${value|h}"/> <input type="hidden" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"/>
<button id="reset_${id}" class="reset">Reset</button> <button id="reset_${id}" class="reset">Reset</button>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -5,12 +5,20 @@ ...@@ -5,12 +5,20 @@
</div> </div>
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
% elif status == 'correct': <span class="sr">Status: unanswered</span>
<span class="correct" id="status_${id}"></span> </span>
% elif status == 'correct':
<span class="correct" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: correct</span>
</span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% elif status == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% endif % endif
</span> </span>
...@@ -19,13 +19,21 @@ ...@@ -19,13 +19,21 @@
% endif % endif
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: unanswered</span>
</span>
% elif status == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: correct</span>
</span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% elif status == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% endif % endif
% if msg: % if msg:
<br/> <br/>
......
<section id="textbox_${id}" class="textbox"> <section id="textbox_${id}" class="textbox">
<textarea rows="${rows}" cols="${cols}" name="input_${id}" id="input_${id}" <textarea rows="${rows}" cols="${cols}" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}"
% if hidden: % if hidden:
style="display:none;" style="display:none;"
% endif % endif
...@@ -7,13 +7,13 @@ ...@@ -7,13 +7,13 @@
<div class="grader-status"> <div class="grader-status">
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span> <span class="unanswered" style="display:inline-block;" id="status_${id}"><span class="sr">Status: </span>Unanswered</span>
% elif status == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}">Correct</span> <span class="correct" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Correct</span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}">Incorrect</span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Incorrect</span>
% elif status == 'queued': % elif status == 'queued':
<span class="processing" id="status_${id}">Queued</span> <span class="processing" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Queued</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span> <span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
$(parent_elt).find('.action').after(alert_elem); $(parent_elt).find('.action').after(alert_elem);
$(parent_elt).find('.capa_alert').css({opacity: 0}).animate({opacity: 1}, 700); $(parent_elt).find('.capa_alert').css({opacity: 0}).animate({opacity: 1}, 700);
} }
// hook up the plot button // hook up the plot button
var plot = function(event) { var plot = function(event) {
...@@ -97,10 +97,10 @@ ...@@ -97,10 +97,10 @@
} }
} }
var save_callback = function(response) { var save_callback = function(response) {
if(response.success) { if(response.success) {
// send information to the problem's plot functionality // send information to the problem's plot functionality
Problem.inputAjax(url, input_id, 'plot', Problem.inputAjax(url, input_id, 'plot',
{'submission': submission}, plot_callback); {'submission': submission}, plot_callback);
} }
else { else {
......
<form class="option-input"> <form class="option-input">
<select name="input_${id}" id="input_${id}" > <select name="input_${id}" id="input_${id}" aria-describedby="answer_${id}">
<option value="option_${id}_dummy_default"> </option> <option value="option_${id}_dummy_default"> </option>
% for option_id, option_description in options: % for option_id, option_description in options:
<option value="${option_id}" <option value="${option_id}"
...@@ -13,12 +13,20 @@ ...@@ -13,12 +13,20 @@
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
% elif status == 'correct': <span class="sr">Status: unsubmitted</span>
<span class="correct" id="status_${id}"></span> </span>
% elif status == 'correct':
<span class="correct" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: correct</span>
</span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% elif status == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incomplete</span>
</span>
% endif % endif
</form> </form>
<span> <span>
<input type="hidden" class="schematic" height="${height}" width="${width}" parts="${parts}" analyses="${analyses}" name="input_${id}" id="input_${id}" value="" initial_value=""/> <input type="hidden" class="schematic" height="${height}" width="${width}" parts="${parts}" analyses="${analyses}" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="" initial_value=""/>
<div id="value_${id}" style="display:none">${value}</div> <div id="value_${id}" style="display:none">${value}</div>
<div id="initial_value_${id}" style="display:none">${initial_value}</div> <div id="initial_value_${id}" style="display:none">${initial_value}</div>
...@@ -13,13 +13,21 @@ ...@@ -13,13 +13,21 @@
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
% if status == 'unsubmitted': % if status == 'unsubmitted':
<span class="ui-icon ui-icon-bullet" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-bullet" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
% elif status == 'correct': <span class="sr">Status: unsubmitted</span>
<span class="ui-icon ui-icon-check" style="display:inline-block;" id="status_${id}"></span> </span>
% elif status == 'correct':
<span class="ui-icon ui-icon-check" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: correct</span>
</span>
% elif status == 'incorrect': % elif status == 'incorrect':
<span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span>
% elif status == 'incomplete': % elif status == 'incomplete':
<span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incomplete</span>
</span>
% endif % endif
</span> </span>
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}" <input type="text" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"
% if do_math: % if do_math:
class="math" class="math"
% endif % endif
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
/> />
${trailing_text | h} ${trailing_text | h}
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -21,11 +21,11 @@ ...@@ -21,11 +21,11 @@
<div class="incorrect" id="status_${id}"> <div class="incorrect" id="status_${id}">
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}" <input type="text" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"
style="display:none;" style="display:none;"
/> />
<p class="status"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif status == 'correct': % elif status == 'correct':
......
...@@ -640,6 +640,23 @@ class StringResponseTest(ResponseTest): ...@@ -640,6 +640,23 @@ class StringResponseTest(ResponseTest):
correct_map = problem.grade_answers(input_dict) correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'), "Hello??") self.assertEquals(correct_map.get_hint('1_2_1'), "Hello??")
def test_hint_function_randomization(self):
# The hint function should get the seed from the problem.
problem = self.build_problem(
answer="1",
hintfn="gimme_a_random_hint",
script=textwrap.dedent("""
def gimme_a_random_hint(answer_ids, student_answers, new_cmap, old_cmap):
answer = str(random.randint(0, 1e9))
new_cmap.set_hint_and_mode(answer_ids[0], answer, "always")
""")
)
correct_map = problem.grade_answers({'1_2_1': '2'})
hint = correct_map.get_hint('1_2_1')
r = random.Random(problem.seed)
self.assertEqual(hint, str(r.randint(0, 1e9)))
class CodeResponseTest(ResponseTest): class CodeResponseTest(ResponseTest):
from response_xml_factory import CodeResponseXMLFactory from response_xml_factory import CodeResponseXMLFactory
...@@ -948,7 +965,6 @@ class CustomResponseTest(ResponseTest): ...@@ -948,7 +965,6 @@ class CustomResponseTest(ResponseTest):
xml_factory_class = CustomResponseXMLFactory xml_factory_class = CustomResponseXMLFactory
def test_inline_code(self): def test_inline_code(self):
# For inline code, we directly modify global context variables # For inline code, we directly modify global context variables
# 'answers' is a list of answers provided to us # 'answers' is a list of answers provided to us
# 'correct' is a list we fill in with True/False # 'correct' is a list we fill in with True/False
...@@ -961,15 +977,14 @@ class CustomResponseTest(ResponseTest): ...@@ -961,15 +977,14 @@ class CustomResponseTest(ResponseTest):
self.assert_grade(problem, '0', 'incorrect') self.assert_grade(problem, '0', 'incorrect')
def test_inline_message(self): def test_inline_message(self):
# Inline code can update the global messages list # Inline code can update the global messages list
# to pass messages to the CorrectMap for a particular input # to pass messages to the CorrectMap for a particular input
# The code can also set the global overall_message (str) # The code can also set the global overall_message (str)
# to pass a message that applies to the whole response # to pass a message that applies to the whole response
inline_script = textwrap.dedent(""" inline_script = textwrap.dedent("""
messages[0] = "Test Message" messages[0] = "Test Message"
overall_message = "Overall message" overall_message = "Overall message"
""") """)
problem = self.build_problem(answer=inline_script) problem = self.build_problem(answer=inline_script)
input_dict = {'1_2_1': '0'} input_dict = {'1_2_1': '0'}
...@@ -983,8 +998,19 @@ class CustomResponseTest(ResponseTest): ...@@ -983,8 +998,19 @@ class CustomResponseTest(ResponseTest):
overall_msg = correctmap.get_overall_message() overall_msg = correctmap.get_overall_message()
self.assertEqual(overall_msg, "Overall message") self.assertEqual(overall_msg, "Overall message")
def test_function_code_single_input(self): def test_inline_randomization(self):
# Make sure the seed from the problem gets fed into the script execution.
inline_script = """messages[0] = str(random.randint(0, 1e9))"""
problem = self.build_problem(answer=inline_script)
input_dict = {'1_2_1': '0'}
correctmap = problem.grade_answers(input_dict)
input_msg = correctmap.get_msg('1_2_1')
r = random.Random(problem.seed)
self.assertEqual(input_msg, str(r.randint(0, 1e9)))
def test_function_code_single_input(self):
# For function code, we pass in these arguments: # For function code, we pass in these arguments:
# #
# 'expect' is the expect attribute of the <customresponse> # 'expect' is the expect attribute of the <customresponse>
...@@ -1212,6 +1238,29 @@ class CustomResponseTest(ResponseTest): ...@@ -1212,6 +1238,29 @@ class CustomResponseTest(ResponseTest):
with self.assertRaises(ResponseError): with self.assertRaises(ResponseError):
problem.grade_answers({'1_2_1': '42'}) problem.grade_answers({'1_2_1': '42'})
def test_setup_randomization(self):
# Ensure that the problem setup script gets the random seed from the problem.
script = textwrap.dedent("""
num = random.randint(0, 1e9)
""")
problem = self.build_problem(script=script)
r = random.Random(problem.seed)
self.assertEqual(r.randint(0, 1e9), problem.context['num'])
def test_check_function_randomization(self):
# The check function should get random-seeded from the problem.
script = textwrap.dedent("""
def check_func(expect, answer_given):
return {'ok': True, 'msg': str(random.randint(0, 1e9))}
""")
problem = self.build_problem(script=script, cfn="check_func", expect="42")
input_dict = {'1_2_1': '42'}
correct_map = problem.grade_answers(input_dict)
msg = correct_map.get_msg('1_2_1')
r = random.Random(problem.seed)
self.assertEqual(msg, str(r.randint(0, 1e9)))
def test_module_imports_inline(self): def test_module_imports_inline(self):
''' '''
Check that the correct modules are available to custom Check that the correct modules are available to custom
...@@ -1275,7 +1324,6 @@ class SchematicResponseTest(ResponseTest): ...@@ -1275,7 +1324,6 @@ class SchematicResponseTest(ResponseTest):
xml_factory_class = SchematicResponseXMLFactory xml_factory_class = SchematicResponseXMLFactory
def test_grade(self): def test_grade(self):
# Most of the schematic-specific work is handled elsewhere # Most of the schematic-specific work is handled elsewhere
# (in client-side JavaScript) # (in client-side JavaScript)
# The <schematicresponse> is responsible only for executing the # The <schematicresponse> is responsible only for executing the
...@@ -1290,7 +1338,7 @@ class SchematicResponseTest(ResponseTest): ...@@ -1290,7 +1338,7 @@ class SchematicResponseTest(ResponseTest):
# The actual dictionary would contain schematic information # The actual dictionary would contain schematic information
# sent from the JavaScript simulation # sent from the JavaScript simulation
submission_dict = {'test': 'test'} submission_dict = {'test': 'the_answer'}
input_dict = {'1_2_1': json.dumps(submission_dict)} input_dict = {'1_2_1': json.dumps(submission_dict)}
correct_map = problem.grade_answers(input_dict) correct_map = problem.grade_answers(input_dict)
...@@ -1299,8 +1347,19 @@ class SchematicResponseTest(ResponseTest): ...@@ -1299,8 +1347,19 @@ class SchematicResponseTest(ResponseTest):
# is what we expect) # is what we expect)
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct') self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')
def test_script_exception(self): def test_check_function_randomization(self):
# The check function should get a random seed from the problem.
script = "correct = ['correct' if (submission[0]['num'] == random.randint(0, 1e9)) else 'incorrect']"
problem = self.build_problem(answer=script)
r = random.Random(problem.seed)
submission_dict = {'num': r.randint(0, 1e9)}
input_dict = {'1_2_1': json.dumps(submission_dict)}
correct_map = problem.grade_answers(input_dict)
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')
def test_script_exception(self):
# Construct a script that will raise an exception # Construct a script that will raise an exception
script = "raise Exception('test')" script = "raise Exception('test')"
problem = self.build_problem(answer=script) problem = self.build_problem(answer=script)
......
(Originally written by Ike.)
At a high level, the main challenges of checking symbolic math expressions are (1) making sure the expression is mathematically legal, and (2) simplifying the expression for comparison with what is expected.
(1) Generation (and testing) of legal input is done by using MathJax to provide input math in an XML format known as Presentation MathML (PMathML). Such expressions typeset correctly, but may not be mathematically legal, like "5 / (1 = 2)". The PMathML is converted into "Content MathML" (CMathML), which is by definition mathematically legal, using an XSLT 2.0 stylesheet, via a module in SnuggleTeX. CMathML is then converted into a sympy expression. This work is all done in `lms/lib/symmath/formula.py`
(2) Simplifying the expression and checking against what is expected is done by using sympy, and a set of heuristics based on options flags provided by the problem author. For example, the problem author may specify that the expected expression is a matrix, in which case the dimensionality of the input expression is checked. Other options include specifying that the comparison be checked numerically in addition to symbolically. The checking is done in stages, first with no simplification, then with increasing levels of testing; if a match is found at any stage, then an "ok" is returned. Helpful messages are also returned, eg if the input expression is of a different type than the expected. This work is all done in `lms/lib/symmath/symmath_check.py`
Links:
SnuggleTex: http://www2.ph.ed.ac.uk/snuggletex/documentation/overview-and-features.html
MathML: http://www.w3.org/TR/MathML2/overview.html
SymPy: http://sympy.org/en/index.html
...@@ -4,6 +4,7 @@ setup( ...@@ -4,6 +4,7 @@ setup(
name="sandbox-packages", name="sandbox-packages",
version="0.1.1", version="0.1.1",
packages=[ packages=[
"loncapa",
"verifiers", "verifiers",
], ],
py_modules=[ py_modules=[
......
from setuptools import setup
setup(
name="symmath",
version="0.1",
packages=["symmath"],
install_requires=[
"sympy",
],
)
(Originally written by Ike.)
At a high level, the main challenges of checking symbolic math expressions are
(1) making sure the expression is mathematically legal, and (2) simplifying the
expression for comparison with what is expected.
(1) Generation (and testing) of legal input is done by using MathJax to provide
input math in an XML format known as Presentation MathML (PMathML). Such
expressions typeset correctly, but may not be mathematically legal, like "5 /
(1 = 2)". The PMathML is converted into "Content MathML" (CMathML), which is
by definition mathematically legal, using an XSLT 2.0 stylesheet, via a module
in SnuggleTeX. CMathML is then converted into a sympy expression. This work is
all done in `symmath/formula.py`.
(2) Simplifying the expression and checking against what is expected is done by
using sympy, and a set of heuristics based on options flags provided by the
problem author. For example, the problem author may specify that the expected
expression is a matrix, in which case the dimensionality of the input
expression is checked. Other options include specifying that the comparison be
checked numerically in addition to symbolically. The checking is done in
stages, first with no simplification, then with increasing levels of testing;
if a match is found at any stage, then an "ok" is returned. Helpful messages
are also returned, eg if the input expression is of a different type than the
expected. This work is all done in `symmath/symmath_check.py`.
Links:
SnuggleTex: http://www2.ph.ed.ac.uk/snuggletex/documentation/overview-and-features.html
MathML: http://www.w3.org/TR/MathML2/overview.html
SymPy: http://sympy.org/en/index.html
...@@ -125,6 +125,5 @@ class AnnotatableModule(AnnotatableFields, XModule): ...@@ -125,6 +125,5 @@ class AnnotatableModule(AnnotatableFields, XModule):
class AnnotatableDescriptor(AnnotatableFields, RawDescriptor): class AnnotatableDescriptor(AnnotatableFields, RawDescriptor):
module_class = AnnotatableModule module_class = AnnotatableModule
stores_state = True
template_dir_name = "annotatable" template_dir_name = "annotatable"
mako_template = "widgets/raw-edit.html" mako_template = "widgets/raw-edit.html"
...@@ -11,7 +11,7 @@ import sys ...@@ -11,7 +11,7 @@ import sys
from pkg_resources import resource_string from pkg_resources import resource_string
from capa.capa_problem import LoncapaProblem from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError,\ from capa.responsetypes import StudentInputError, \
ResponseError, LoncapaProblemError ResponseError, LoncapaProblemError
from capa.util import convert_files_to_filenames from capa.util import convert_files_to_filenames
from .progress import Progress from .progress import Progress
...@@ -20,7 +20,7 @@ from xmodule.raw_module import RawDescriptor ...@@ -20,7 +20,7 @@ from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.exceptions import NotFoundError, ProcessingError
from xblock.core import Scope, String, Boolean, Object from xblock.core import Scope, String, Boolean, Object
from .fields import Timedelta, Date, StringyInteger, StringyFloat from .fields import Timedelta, Date, StringyInteger, StringyFloat
from xmodule.util.date_utils import time_to_datetime from django.utils.timezone import UTC
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -69,7 +69,7 @@ class CapaFields(object): ...@@ -69,7 +69,7 @@ class CapaFields(object):
max_attempts = StringyInteger( max_attempts = StringyInteger(
display_name="Maximum Attempts", display_name="Maximum Attempts",
help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.", help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.",
values={"min": 1}, scope=Scope.settings values={"min": 0}, scope=Scope.settings
) )
due = Date(help="Date that this problem is due by", scope=Scope.settings) due = Date(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings) graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
...@@ -134,7 +134,7 @@ class CapaModule(CapaFields, XModule): ...@@ -134,7 +134,7 @@ class CapaModule(CapaFields, XModule):
def __init__(self, system, location, descriptor, model_data): def __init__(self, system, location, descriptor, model_data):
XModule.__init__(self, system, location, descriptor, model_data) XModule.__init__(self, system, location, descriptor, model_data)
due_date = time_to_datetime(self.due) due_date = self.due
if self.graceperiod is not None and due_date: if self.graceperiod is not None and due_date:
self.close_date = due_date + self.graceperiod self.close_date = due_date + self.graceperiod
...@@ -502,7 +502,7 @@ class CapaModule(CapaFields, XModule): ...@@ -502,7 +502,7 @@ class CapaModule(CapaFields, XModule):
Is it now past this problem's due date, including grace period? Is it now past this problem's due date, including grace period?
""" """
return (self.close_date is not None and return (self.close_date is not None and
datetime.datetime.utcnow() > self.close_date) datetime.datetime.now(UTC()) > self.close_date)
def closed(self): def closed(self):
''' Is the student still allowed to submit answers? ''' ''' Is the student still allowed to submit answers? '''
...@@ -747,7 +747,7 @@ class CapaModule(CapaFields, XModule): ...@@ -747,7 +747,7 @@ class CapaModule(CapaFields, XModule):
# Problem queued. Students must wait a specified waittime before they are allowed to submit # Problem queued. Students must wait a specified waittime before they are allowed to submit
if self.lcp.is_queued(): if self.lcp.is_queued():
current_time = datetime.datetime.now() current_time = datetime.datetime.now(UTC())
prev_submit_time = self.lcp.get_recentmost_queuetime() prev_submit_time = self.lcp.get_recentmost_queuetime()
waittime_between_requests = self.system.xqueue['waittime'] waittime_between_requests = self.system.xqueue['waittime']
if (current_time - prev_submit_time).total_seconds() < waittime_between_requests: if (current_time - prev_submit_time).total_seconds() < waittime_between_requests:
...@@ -902,7 +902,6 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -902,7 +902,6 @@ class CapaDescriptor(CapaFields, RawDescriptor):
module_class = CapaModule module_class = CapaModule
stores_state = True
has_score = True has_score = True
template_dir_name = 'problem' template_dir_name = 'problem'
mako_template = "widgets/problem-edit.html" mako_template = "widgets/problem-edit.html"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment