Commit 886c9880 by Jason Bau

Merge remote-tracking branch 'origin/feature/nate/simple-chat' into…

Merge remote-tracking branch 'origin/feature/nate/simple-chat' into edx-west/release-candidate-20130613

Conflicts:
	lms/templates/courseware/accordion.html
	lms/templates/courseware/course_navigation.html
	lms/templates/courseware/progress.html
	lms/templates/navigation.html
parents 2d161d83 9263f554
...@@ -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
......
...@@ -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 = 'h3[data-name="My Section"]' css = 'h3[data-name="My Section"]'
assert world.is_css_not_present(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')
......
...@@ -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')
...@@ -120,6 +123,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -120,6 +123,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_advanced_components_require_two_clicks(self): def test_advanced_components_require_two_clicks(self):
self.check_components_on_page(['videoalpha'], ['Video Alpha']) 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])
...@@ -257,7 +271,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -257,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
...@@ -404,6 +418,32 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -404,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)
...@@ -499,7 +539,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -499,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)
......
...@@ -6,11 +6,10 @@ from django.core.urlresolvers import reverse ...@@ -6,11 +6,10 @@ from django.core.urlresolvers import reverse
import copy import copy
import logging import logging
import re import re
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
#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"}
NOTES_PANEL = {"name": "My Notes", "type": "notes"} NOTES_PANEL = {"name": "My Notes", "type": "notes"}
...@@ -229,7 +228,7 @@ def add_extra_panel_tab(tab_type, course): ...@@ -229,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 = {
......
...@@ -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
......
...@@ -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': {
......
...@@ -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()
...@@ -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,
......
<%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>
......
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)
......
...@@ -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.
......
...@@ -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:
......
'''
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)
......
...@@ -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)
...@@ -1814,7 +1835,14 @@ class SchematicResponse(LoncapaResponse): ...@@ -1814,7 +1835,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."""
......
...@@ -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)
......
...@@ -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:
......
...@@ -4,7 +4,6 @@ from math import exp ...@@ -4,7 +4,6 @@ from math import exp
from lxml import etree from lxml import etree
from path import path # NOTE (THK): Only used for detecting presence of syllabus from path import path # NOTE (THK): Only used for detecting presence of syllabus
import requests import requests
import time
from datetime import datetime from datetime import datetime
import dateutil.parser import dateutil.parser
...@@ -14,11 +13,12 @@ from xmodule.seq_module import SequenceDescriptor, SequenceModule ...@@ -14,11 +13,12 @@ from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time from xmodule.timeparse import parse_time
from xmodule.util.decorators import lazyproperty from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf from xmodule.graders import grader_from_conf
from xmodule.util.date_utils import time_to_datetime
import json import json
from xblock.core import Scope, List, String, Object, Boolean from xblock.core import Scope, List, String, Object, Boolean
from .fields import Date from .fields import Date
from django.utils.timezone import UTC
from xmodule.util import date_utils
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -93,7 +93,7 @@ class Textbook(object): ...@@ -93,7 +93,7 @@ class Textbook(object):
# see if we already fetched this # see if we already fetched this
if toc_url in _cached_toc: if toc_url in _cached_toc:
(table_of_contents, timestamp) = _cached_toc[toc_url] (table_of_contents, timestamp) = _cached_toc[toc_url]
age = datetime.now() - timestamp age = datetime.now(UTC) - timestamp
# expire every 10 minutes # expire every 10 minutes
if age.seconds < 600: if age.seconds < 600:
return table_of_contents return table_of_contents
...@@ -156,6 +156,7 @@ class CourseFields(object): ...@@ -156,6 +156,7 @@ class CourseFields(object):
advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings) advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings)
grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content) grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content)
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings) show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
show_chat = Boolean(help="Whether to show the chat widget in this course", default=False, scope=Scope.settings)
display_name = String(help="Display name for this module", scope=Scope.settings) display_name = String(help="Display name for this module", scope=Scope.settings)
tabs = List(help="List of tabs to enable in this course", scope=Scope.settings) tabs = List(help="List of tabs to enable in this course", scope=Scope.settings)
end_of_course_survey_url = String(help="Url for the end-of-course survey", scope=Scope.settings) end_of_course_survey_url = String(help="Url for the end-of-course survey", scope=Scope.settings)
...@@ -219,8 +220,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -219,8 +220,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
msg = None msg = None
if self.start is None: if self.start is None:
msg = "Course loaded without a valid start date. id = %s" % self.id msg = "Course loaded without a valid start date. id = %s" % self.id
# hack it -- start in 1970 self.start = datetime.now(UTC())
self.start = time.gmtime(0)
log.critical(msg) log.critical(msg)
self.system.error_tracker(msg) self.system.error_tracker(msg)
...@@ -392,7 +392,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -392,7 +392,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
textbook_xml_object.set('book_url', textbook.book_url) textbook_xml_object.set('book_url', textbook.book_url)
xml_object.append(textbook_xml_object) xml_object.append(textbook_xml_object)
return xml_object return xml_object
def has_ended(self): def has_ended(self):
...@@ -403,10 +403,10 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -403,10 +403,10 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
if self.end is None: if self.end is None:
return False return False
return time.gmtime() > self.end return datetime.now(UTC()) > self.end
def has_started(self): def has_started(self):
return time.gmtime() > self.start return datetime.now(UTC()) > self.start
@property @property
def grader(self): def grader(self):
...@@ -547,14 +547,16 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -547,14 +547,16 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
announcement = self.announcement announcement = self.announcement
if announcement is not None: if announcement is not None:
announcement = time_to_datetime(announcement) announcement = announcement
try: try:
start = dateutil.parser.parse(self.advertised_start) start = dateutil.parser.parse(self.advertised_start)
if start.tzinfo is None:
start = start.replace(tzinfo=UTC())
except (ValueError, AttributeError): except (ValueError, AttributeError):
start = time_to_datetime(self.start) start = self.start
now = datetime.utcnow() now = datetime.now(UTC())
return announcement, start, now return announcement, start, now
...@@ -656,7 +658,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -656,7 +658,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
elif self.advertised_start is None and self.start is None: elif self.advertised_start is None and self.start is None:
return 'TBD' return 'TBD'
else: else:
return time.strftime("%b %d, %Y", self.advertised_start or self.start) return (self.advertised_start or self.start).strftime("%b %d, %Y")
@property @property
def end_date_text(self): def end_date_text(self):
...@@ -665,7 +667,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -665,7 +667,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
If the course does not have an end date set (course.end is None), an empty string will be returned. If the course does not have an end date set (course.end is None), an empty string will be returned.
""" """
return '' if self.end is None else time.strftime("%b %d, %Y", self.end) return '' if self.end is None else self.end.strftime("%b %d, %Y")
@property @property
def forum_posts_allowed(self): def forum_posts_allowed(self):
...@@ -673,7 +675,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -673,7 +675,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
blackout_periods = [(parse_time(start), parse_time(end)) blackout_periods = [(parse_time(start), parse_time(end))
for start, end for start, end
in self.discussion_blackouts] in self.discussion_blackouts]
now = time.gmtime() now = datetime.now(UTC())
for start, end in blackout_periods: for start, end in blackout_periods:
if start <= now <= end: if start <= now <= end:
return False return False
...@@ -699,7 +701,8 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -699,7 +701,8 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
if self.last_eligible_appointment_date is None: if self.last_eligible_appointment_date is None:
raise ValueError("Last appointment date must be specified") raise ValueError("Last appointment date must be specified")
self.registration_start_date = self._try_parse_time('Registration_Start_Date') or time.gmtime(0) self.registration_start_date = (self._try_parse_time('Registration_Start_Date') or
datetime.utcfromtimestamp(0))
self.registration_end_date = self._try_parse_time('Registration_End_Date') or self.last_eligible_appointment_date self.registration_end_date = self._try_parse_time('Registration_End_Date') or self.last_eligible_appointment_date
# do validation within the exam info: # do validation within the exam info:
if self.registration_start_date > self.registration_end_date: if self.registration_start_date > self.registration_end_date:
...@@ -725,32 +728,32 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -725,32 +728,32 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
return None return None
def has_started(self): def has_started(self):
return time.gmtime() > self.first_eligible_appointment_date return datetime.now(UTC()) > self.first_eligible_appointment_date
def has_ended(self): def has_ended(self):
return time.gmtime() > self.last_eligible_appointment_date return datetime.now(UTC()) > self.last_eligible_appointment_date
def has_started_registration(self): def has_started_registration(self):
return time.gmtime() > self.registration_start_date return datetime.now(UTC()) > self.registration_start_date
def has_ended_registration(self): def has_ended_registration(self):
return time.gmtime() > self.registration_end_date return datetime.now(UTC()) > self.registration_end_date
def is_registering(self): def is_registering(self):
now = time.gmtime() now = datetime.now(UTC())
return now >= self.registration_start_date and now <= self.registration_end_date return now >= self.registration_start_date and now <= self.registration_end_date
@property @property
def first_eligible_appointment_date_text(self): def first_eligible_appointment_date_text(self):
return time.strftime("%b %d, %Y", self.first_eligible_appointment_date) return self.first_eligible_appointment_date.strftime("%b %d, %Y")
@property @property
def last_eligible_appointment_date_text(self): def last_eligible_appointment_date_text(self):
return time.strftime("%b %d, %Y", self.last_eligible_appointment_date) return self.last_eligible_appointment_date.strftime("%b %d, %Y")
@property @property
def registration_end_date_text(self): def registration_end_date_text(self):
return time.strftime("%b %d, %Y at %H:%M UTC", self.registration_end_date) return date_utils.get_default_time_display(self.registration_end_date)
@property @property
def current_test_center_exam(self): def current_test_center_exam(self):
......
...@@ -87,7 +87,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor): ...@@ -87,7 +87,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
# but url_names aren't guaranteed to be unique between descriptor types, # but url_names aren't guaranteed to be unique between descriptor types,
# and ErrorDescriptor can wrap any type. When the wrapped module is fixed, # and ErrorDescriptor can wrap any type. When the wrapped module is fixed,
# it will be written out with the original url_name. # it will be written out with the original url_name.
name=hashlib.sha1(contents).hexdigest() name=hashlib.sha1(contents.encode('utf8')).hexdigest()
) )
# real metadata stays in the content, but add a display name # real metadata stays in the content, but add a display name
......
...@@ -12,3 +12,12 @@ class ProcessingError(Exception): ...@@ -12,3 +12,12 @@ class ProcessingError(Exception):
For example: if an exception occurs while checking a capa problem. For example: if an exception occurs while checking a capa problem.
''' '''
pass pass
class InvalidVersionError(Exception):
"""
Tried to save an item with a location that a store cannot support (e.g., draft version
for a non-leaf node)
"""
def __init__(self, location):
super(InvalidVersionError, self).__init__()
self.location = location
...@@ -2,19 +2,19 @@ import time ...@@ -2,19 +2,19 @@ import time
import logging import logging
import re import re
from datetime import timedelta
from xblock.core import ModelType from xblock.core import ModelType
import datetime import datetime
import dateutil.parser import dateutil.parser
from xblock.core import Integer, Float, Boolean from xblock.core import Integer, Float, Boolean
from django.utils.timezone import UTC
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Date(ModelType): class Date(ModelType):
''' '''
Date fields know how to parse and produce json (iso) compatible formats. Date fields know how to parse and produce json (iso) compatible formats. Converts to tz aware datetimes.
''' '''
def from_json(self, field): def from_json(self, field):
""" """
...@@ -27,11 +27,15 @@ class Date(ModelType): ...@@ -27,11 +27,15 @@ class Date(ModelType):
elif field is "": elif field is "":
return None return None
elif isinstance(field, basestring): elif isinstance(field, basestring):
d = dateutil.parser.parse(field) result = dateutil.parser.parse(field)
return d.utctimetuple() if result.tzinfo is None:
result = result.replace(tzinfo=UTC())
return result
elif isinstance(field, (int, long, float)): elif isinstance(field, (int, long, float)):
return time.gmtime(field / 1000) return datetime.datetime.fromtimestamp(field / 1000, UTC())
elif isinstance(field, time.struct_time): elif isinstance(field, time.struct_time):
return datetime.datetime.fromtimestamp(time.mktime(field), UTC())
elif isinstance(field, datetime.datetime):
return field return field
else: else:
msg = "Field {0} has bad value '{1}'".format( msg = "Field {0} has bad value '{1}'".format(
...@@ -49,7 +53,11 @@ class Date(ModelType): ...@@ -49,7 +53,11 @@ class Date(ModelType):
# struct_times are always utc # struct_times are always utc
return time.strftime('%Y-%m-%dT%H:%M:%SZ', value) return time.strftime('%Y-%m-%dT%H:%M:%SZ', value)
elif isinstance(value, datetime.datetime): elif isinstance(value, datetime.datetime):
return value.isoformat() + 'Z' if value.tzinfo is None or value.utcoffset().total_seconds() == 0:
# isoformat adds +00:00 rather than Z
return value.strftime('%Y-%m-%dT%H:%M:%SZ')
else:
return value.isoformat()
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$') TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
...@@ -66,6 +74,8 @@ class Timedelta(ModelType): ...@@ -66,6 +74,8 @@ class Timedelta(ModelType):
Returns a datetime.timedelta parsed from the string Returns a datetime.timedelta parsed from the string
""" """
if time_str is None:
return None
parts = TIMEDELTA_REGEX.match(time_str) parts = TIMEDELTA_REGEX.match(time_str)
if not parts: if not parts:
return return
...@@ -74,7 +84,7 @@ class Timedelta(ModelType): ...@@ -74,7 +84,7 @@ class Timedelta(ModelType):
for (name, param) in parts.iteritems(): for (name, param) in parts.iteritems():
if param: if param:
time_params[name] = int(param) time_params[name] = int(param)
return timedelta(**time_params) return datetime.timedelta(**time_params)
def to_json(self, value): def to_json(self, value):
values = [] values = []
...@@ -93,7 +103,7 @@ class StringyInteger(Integer): ...@@ -93,7 +103,7 @@ class StringyInteger(Integer):
def from_json(self, value): def from_json(self, value):
try: try:
return int(value) return int(value)
except: except Exception:
return None return None
......
...@@ -8,7 +8,6 @@ from xmodule.x_module import XModule ...@@ -8,7 +8,6 @@ from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xblock.core import Scope, Integer, String from xblock.core import Scope, Integer, String
from .fields import Date from .fields import Date
from xmodule.util.date_utils import time_to_datetime
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -31,9 +30,7 @@ class FolditModule(FolditFields, XModule): ...@@ -31,9 +30,7 @@ class FolditModule(FolditFields, XModule):
css = {'scss': [resource_string(__name__, 'css/foldit/leaderboard.scss')]} css = {'scss': [resource_string(__name__, 'css/foldit/leaderboard.scss')]}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
""" """
Example: Example:
<foldit show_basic_score="true" <foldit show_basic_score="true"
required_level="4" required_level="4"
...@@ -42,8 +39,8 @@ class FolditModule(FolditFields, XModule): ...@@ -42,8 +39,8 @@ class FolditModule(FolditFields, XModule):
required_sublevel_half_credit="3" required_sublevel_half_credit="3"
show_leaderboard="false"/> show_leaderboard="false"/>
""" """
XModule.__init__(self, *args, **kwargs)
self.due_time = time_to_datetime(self.due) self.due_time = self.due
def is_complete(self): def is_complete(self):
""" """
...@@ -102,7 +99,7 @@ class FolditModule(FolditFields, XModule): ...@@ -102,7 +99,7 @@ class FolditModule(FolditFields, XModule):
from foldit.models import Score from foldit.models import Score
leaders = [(e['username'], e['score']) for e in Score.get_tops_n(10)] leaders = [(e['username'], e['score']) for e in Score.get_tops_n(10)]
leaders.sort(key=lambda x: -x[1]) leaders.sort(key=lambda x:-x[1])
return leaders return leaders
......
...@@ -3,8 +3,12 @@ from datetime import datetime ...@@ -3,8 +3,12 @@ from datetime import datetime
from . import ModuleStoreBase, Location, namedtuple_to_son from . import ModuleStoreBase, Location, namedtuple_to_son
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
from .inheritance import own_metadata from .inheritance import own_metadata
from xmodule.exceptions import InvalidVersionError
from pytz import UTC
DRAFT = 'draft' DRAFT = 'draft'
# Things w/ these categories should never be marked as version='draft'
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
def as_draft(location): def as_draft(location):
...@@ -111,6 +115,8 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -111,6 +115,8 @@ class DraftModuleStore(ModuleStoreBase):
Clone a new item that is a copy of the item at the location `source` Clone a new item that is a copy of the item at the location `source`
and writes it to `location` and writes it to `location`
""" """
if Location(location).category in DIRECT_ONLY_CATEGORIES:
raise InvalidVersionError(location)
return wrap_draft(super(DraftModuleStore, self).clone_item(source, as_draft(location))) return wrap_draft(super(DraftModuleStore, self).clone_item(source, as_draft(location)))
def update_item(self, location, data, allow_not_found=False): def update_item(self, location, data, allow_not_found=False):
...@@ -192,7 +198,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -192,7 +198,7 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft = self.get_item(location) draft = self.get_item(location)
draft.cms.published_date = datetime.utcnow() draft.cms.published_date = datetime.now(UTC)
draft.cms.published_by = published_by_id draft.cms.published_by = published_by_id
super(DraftModuleStore, self).update_item(location, draft._model_data._kvs._data) super(DraftModuleStore, self).update_item(location, draft._model_data._kvs._data)
super(DraftModuleStore, self).update_children(location, draft._model_data._kvs._children) super(DraftModuleStore, self).update_children(location, draft._model_data._kvs._children)
...@@ -203,6 +209,8 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -203,6 +209,8 @@ class DraftModuleStore(ModuleStoreBase):
""" """
Turn the published version into a draft, removing the published version Turn the published version into a draft, removing the published version
""" """
if Location(location).category in DIRECT_ONLY_CATEGORIES:
raise InvalidVersionError(location)
super(DraftModuleStore, self).clone_item(location, as_draft(location)) super(DraftModuleStore, self).clone_item(location, as_draft(location))
super(DraftModuleStore, self).delete_item(location) super(DraftModuleStore, self).delete_item(location)
......
...@@ -231,6 +231,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -231,6 +231,7 @@ class MongoModuleStore(ModuleStoreBase):
self.collection = pymongo.connection.Connection( self.collection = pymongo.connection.Connection(
host=host, host=host,
port=port, port=port,
tz_aware=True,
**kwargs **kwargs
)[db][collection] )[db][collection]
......
...@@ -4,6 +4,11 @@ from uuid import uuid4 ...@@ -4,6 +4,11 @@ from uuid import uuid4
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
from xblock.runtime import InvalidScopeError
import datetime
from pytz import UTC
class XModuleCourseFactory(Factory): class XModuleCourseFactory(Factory):
...@@ -35,7 +40,7 @@ class XModuleCourseFactory(Factory): ...@@ -35,7 +40,7 @@ class XModuleCourseFactory(Factory):
if display_name is not None: if display_name is not None:
new_course.display_name = display_name new_course.display_name = display_name
new_course.lms.start = gmtime() new_course.lms.start = datetime.datetime.now(UTC)
new_course.tabs = kwargs.get( new_course.tabs = kwargs.get(
'tabs', 'tabs',
[ [
...@@ -159,3 +164,32 @@ class ItemFactory(XModuleItemFactory): ...@@ -159,3 +164,32 @@ class ItemFactory(XModuleItemFactory):
@lazy_attribute_sequence @lazy_attribute_sequence
def display_name(attr, n): def display_name(attr, n):
return "{} {}".format(attr.category.title(), n) return "{} {}".format(attr.category.title(), n)
def get_test_xmodule_for_descriptor(descriptor):
"""
Attempts to create an xmodule which responds usually correctly from the descriptor. Not guaranteed.
:param descriptor:
"""
module_sys = ModuleSystem(
ajax_url='',
track_function=None,
get_module=None,
render_template=render_to_string,
replace_urls=None,
xblock_model_data=_test_xblock_model_data_accessor(descriptor)
)
return descriptor.xmodule(module_sys)
def _test_xblock_model_data_accessor(descriptor):
simple_map = {}
for field in descriptor.fields:
try:
simple_map[field.name] = getattr(descriptor, field.name)
except InvalidScopeError:
simple_map[field.name] = field.default
for field in descriptor.module_class.fields:
if field.name not in simple_map:
simple_map[field.name] = field.default
return lambda o: simple_map
...@@ -19,7 +19,7 @@ DB = 'test' ...@@ -19,7 +19,7 @@ DB = 'test'
COLLECTION = 'modulestore' COLLECTION = 'modulestore'
FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing load_item FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing load_item
DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor' DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor'
RENDER_TEMPLATE = lambda t_n, d, ctx=None, nsp='main': '' RENDER_TEMPLATE = lambda t_n, d, ctx = None, nsp = 'main': ''
class TestMongoModuleStore(object): class TestMongoModuleStore(object):
...@@ -42,7 +42,8 @@ class TestMongoModuleStore(object): ...@@ -42,7 +42,8 @@ class TestMongoModuleStore(object):
@staticmethod @staticmethod
def initdb(): def initdb():
# connect to the db # connect to the db
store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS) store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, RENDER_TEMPLATE,
default_class=DEFAULT_CLASS)
# Explicitly list the courses to load (don't want the big one) # Explicitly list the courses to load (don't want the big one)
courses = ['toy', 'simple'] courses = ['toy', 'simple']
import_from_xml(store, DATA_DIR, courses) import_from_xml(store, DATA_DIR, courses)
......
from xmodule.modulestore import Location import os.path
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore.xml_importer import import_from_xml
from nose.tools import assert_raises
from .test_modulestore import check_path_to_location from .test_modulestore import check_path_to_location
from . import DATA_DIR from . import DATA_DIR
...@@ -15,3 +18,22 @@ class TestXMLModuleStore(object): ...@@ -15,3 +18,22 @@ class TestXMLModuleStore(object):
print "finished import" print "finished import"
check_path_to_location(modulestore) check_path_to_location(modulestore)
def test_unicode_chars_in_xml_content(self):
# edX/full/6.002_Spring_2012 has non-ASCII chars, and during
# uniquification of names, would raise a UnicodeError. It no longer does.
# Ensure that there really is a non-ASCII character in the course.
with open(os.path.join(DATA_DIR, "full/sequential/Administrivia_and_Circuit_Elements.xml")) as xmlf:
xml = xmlf.read()
with assert_raises(UnicodeDecodeError):
xml.decode('ascii')
# Load the course, but don't make error modules. This will succeed,
# but will record the errors.
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['full'], load_error_modules=False)
# Look up the errors during load. There should be none.
location = CourseDescriptor.id_to_location("edX/full/6.002_Spring_2012")
errors = modulestore.get_item_errors(location)
assert errors == []
...@@ -52,7 +52,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -52,7 +52,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
xmlstore: the XMLModuleStore to store the loaded modules in xmlstore: the XMLModuleStore to store the loaded modules in
""" """
self.unnamed = defaultdict(int) # category -> num of new url_names for that category self.unnamed = defaultdict(int) # category -> num of new url_names for that category
self.used_names = defaultdict(set) # category -> set of used url_names self.used_names = defaultdict(set) # category -> set of used url_names
self.org, self.course, self.url_name = course_id.split('/') self.org, self.course, self.url_name = course_id.split('/')
# cdodge: adding the course_id as passed in for later reference rather than having to recomine the org/course/url_name # cdodge: adding the course_id as passed in for later reference rather than having to recomine the org/course/url_name
...@@ -108,7 +108,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -108,7 +108,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
orig_name = orig_name[len(tag) + 1:-12] orig_name = orig_name[len(tag) + 1:-12]
# append the hash of the content--the first 12 bytes should be plenty. # append the hash of the content--the first 12 bytes should be plenty.
orig_name = "_" + orig_name if orig_name not in (None, "") else "" orig_name = "_" + orig_name if orig_name not in (None, "") else ""
return tag + orig_name + "_" + hashlib.sha1(xml).hexdigest()[:12] xml_bytes = xml.encode('utf8')
return tag + orig_name + "_" + hashlib.sha1(xml_bytes).hexdigest()[:12]
# Fallback if there was nothing we could use: # Fallback if there was nothing we could use:
if url_name is None or url_name == "": if url_name is None or url_name == "":
...@@ -123,7 +124,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -123,7 +124,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
else: else:
# TODO (vshnayder): We may want to enable this once course repos are cleaned up. # TODO (vshnayder): We may want to enable this once course repos are cleaned up.
# (or we may want to give up on the requirement for non-state-relevant issues...) # (or we may want to give up on the requirement for non-state-relevant issues...)
#error_tracker("WARNING: no name specified for module. xml='{0}...'".format(xml[:100])) # error_tracker("WARNING: no name specified for module. xml='{0}...'".format(xml[:100]))
pass pass
# Make sure everything is unique # Make sure everything is unique
...@@ -322,7 +323,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -322,7 +323,7 @@ class XMLModuleStore(ModuleStoreBase):
''' '''
String representation - for debugging String representation - for debugging
''' '''
return '<XMLModuleStore>data_dir=%s, %d courses, %d modules' % ( return '<XMLModuleStore data_dir=%r, %d courses, %d modules>' % (
self.data_dir, len(self.courses), len(self.modules)) self.data_dir, len(self.courses), len(self.modules))
def load_policy(self, policy_path, tracker): def load_policy(self, policy_path, tracker):
...@@ -446,7 +447,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -446,7 +447,7 @@ class XMLModuleStore(ModuleStoreBase):
def load_extra_content(self, system, course_descriptor, category, base_dir, course_dir, url_name): def load_extra_content(self, system, course_descriptor, category, base_dir, course_dir, url_name):
self._load_extra_content(system, course_descriptor, category, base_dir, course_dir) self._load_extra_content(system, course_descriptor, category, base_dir, course_dir)
# then look in a override folder based on the course run # then look in a override folder based on the course run
if os.path.isdir(base_dir / url_name): if os.path.isdir(base_dir / url_name):
self._load_extra_content(system, course_descriptor, category, base_dir / url_name, course_dir) self._load_extra_content(system, course_descriptor, category, base_dir / url_name, course_dir)
......
...@@ -16,6 +16,7 @@ from .peer_grading_service import PeerGradingService, MockPeerGradingService ...@@ -16,6 +16,7 @@ from .peer_grading_service import PeerGradingService, MockPeerGradingService
import controller_query_service import controller_query_service
from datetime import datetime from datetime import datetime
from django.utils.timezone import UTC
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -56,7 +57,7 @@ class OpenEndedChild(object): ...@@ -56,7 +57,7 @@ class OpenEndedChild(object):
POST_ASSESSMENT = 'post_assessment' POST_ASSESSMENT = 'post_assessment'
DONE = 'done' DONE = 'done'
#This is used to tell students where they are at in the module # This is used to tell students where they are at in the module
HUMAN_NAMES = { HUMAN_NAMES = {
'initial': 'Not started', 'initial': 'Not started',
'assessing': 'In progress', 'assessing': 'In progress',
...@@ -102,7 +103,7 @@ class OpenEndedChild(object): ...@@ -102,7 +103,7 @@ class OpenEndedChild(object):
if system.open_ended_grading_interface: if system.open_ended_grading_interface:
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system) self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
self.controller_qs = controller_query_service.ControllerQueryService( self.controller_qs = controller_query_service.ControllerQueryService(
system.open_ended_grading_interface,system system.open_ended_grading_interface, system
) )
else: else:
self.peer_gs = MockPeerGradingService() self.peer_gs = MockPeerGradingService()
...@@ -130,7 +131,7 @@ class OpenEndedChild(object): ...@@ -130,7 +131,7 @@ class OpenEndedChild(object):
pass pass
def closed(self): def closed(self):
if self.close_date is not None and datetime.utcnow() > self.close_date: if self.close_date is not None and datetime.now(UTC()) > self.close_date:
return True return True
return False return False
...@@ -138,13 +139,13 @@ class OpenEndedChild(object): ...@@ -138,13 +139,13 @@ class OpenEndedChild(object):
if self.closed(): if self.closed():
return True, { return True, {
'success': False, 'success': False,
#This is a student_facing_error # This is a student_facing_error
'error': 'The problem close date has passed, and this problem is now closed.' 'error': 'The problem close date has passed, and this problem is now closed.'
} }
elif self.child_attempts > self.max_attempts: elif self.child_attempts > self.max_attempts:
return True, { return True, {
'success': False, 'success': False,
#This is a student_facing_error # This is a student_facing_error
'error': 'You have attempted this problem {0} times. You are allowed {1} attempts.'.format( 'error': 'You have attempted this problem {0} times. You are allowed {1} attempts.'.format(
self.child_attempts, self.max_attempts self.child_attempts, self.max_attempts
) )
...@@ -272,7 +273,7 @@ class OpenEndedChild(object): ...@@ -272,7 +273,7 @@ class OpenEndedChild(object):
try: try:
return Progress(int(self.get_score()['score']), int(self._max_score)) return Progress(int(self.get_score()['score']), int(self._max_score))
except Exception as err: except Exception as err:
#This is a dev_facing_error # This is a dev_facing_error
log.exception("Got bad progress from open ended child module. Max Score: {0}".format(self._max_score)) log.exception("Got bad progress from open ended child module. Max Score: {0}".format(self._max_score))
return None return None
return None return None
...@@ -281,10 +282,10 @@ class OpenEndedChild(object): ...@@ -281,10 +282,10 @@ class OpenEndedChild(object):
""" """
return dict out-of-sync error message, and also log. return dict out-of-sync error message, and also log.
""" """
#This is a dev_facing_error # This is a dev_facing_error
log.warning("Open ended child state out sync. state: %r, get: %r. %s", log.warning("Open ended child state out sync. state: %r, get: %r. %s",
self.child_state, get, msg) self.child_state, get, msg)
#This is a student_facing_error # This is a student_facing_error
return {'success': False, return {'success': False,
'error': 'The problem state got out-of-sync. Please try reloading the page.'} 'error': 'The problem state got out-of-sync. Please try reloading the page.'}
...@@ -391,7 +392,7 @@ class OpenEndedChild(object): ...@@ -391,7 +392,7 @@ class OpenEndedChild(object):
""" """
overall_success = False overall_success = False
if not self.accept_file_upload: if not self.accept_file_upload:
#If the question does not accept file uploads, do not do anything # If the question does not accept file uploads, do not do anything
return True, get_data return True, get_data
has_file_to_upload, uploaded_to_s3, image_ok, image_tag = self.check_for_image_and_upload(get_data) has_file_to_upload, uploaded_to_s3, image_ok, image_tag = self.check_for_image_and_upload(get_data)
...@@ -399,19 +400,19 @@ class OpenEndedChild(object): ...@@ -399,19 +400,19 @@ class OpenEndedChild(object):
get_data['student_answer'] += image_tag get_data['student_answer'] += image_tag
overall_success = True overall_success = True
elif has_file_to_upload and not uploaded_to_s3 and image_ok: elif has_file_to_upload and not uploaded_to_s3 and image_ok:
#In this case, an image was submitted by the student, but the image could not be uploaded to S3. Likely # In this case, an image was submitted by the student, but the image could not be uploaded to S3. Likely
#a config issue (development vs deployment). For now, just treat this as a "success" # a config issue (development vs deployment). For now, just treat this as a "success"
log.exception("Student AJAX post to combined open ended xmodule indicated that it contained an image, " log.exception("Student AJAX post to combined open ended xmodule indicated that it contained an image, "
"but the image was not able to be uploaded to S3. This could indicate a config" "but the image was not able to be uploaded to S3. This could indicate a config"
"issue with this deployment, but it could also indicate a problem with S3 or with the" "issue with this deployment, but it could also indicate a problem with S3 or with the"
"student image itself.") "student image itself.")
overall_success = True overall_success = True
elif not has_file_to_upload: elif not has_file_to_upload:
#If there is no file to upload, probably the student has embedded the link in the answer text # If there is no file to upload, probably the student has embedded the link in the answer text
success, get_data['student_answer'] = self.check_for_url_in_text(get_data['student_answer']) success, get_data['student_answer'] = self.check_for_url_in_text(get_data['student_answer'])
overall_success = success overall_success = success
#log.debug("Has file: {0} Uploaded: {1} Image Ok: {2}".format(has_file_to_upload, uploaded_to_s3, image_ok)) # log.debug("Has file: {0} Uploaded: {1} Image Ok: {2}".format(has_file_to_upload, uploaded_to_s3, image_ok))
return overall_success, get_data return overall_success, get_data
...@@ -441,7 +442,7 @@ class OpenEndedChild(object): ...@@ -441,7 +442,7 @@ class OpenEndedChild(object):
success = False success = False
allowed_to_submit = True allowed_to_submit = True
response = {} response = {}
#This is a student_facing_error # This is a student_facing_error
error_string = ("You need to peer grade {0} more in order to make another submission. " error_string = ("You need to peer grade {0} more in order to make another submission. "
"You have graded {1}, and {2} are required. You have made {3} successful peer grading submissions.") "You have graded {1}, and {2} are required. You have made {3} successful peer grading submissions.")
try: try:
...@@ -451,17 +452,17 @@ class OpenEndedChild(object): ...@@ -451,17 +452,17 @@ class OpenEndedChild(object):
student_sub_count = response['student_sub_count'] student_sub_count = response['student_sub_count']
success = True success = True
except: except:
#This is a dev_facing_error # This is a dev_facing_error
log.error("Could not contact external open ended graders for location {0} and student {1}".format( log.error("Could not contact external open ended graders for location {0} and student {1}".format(
self.location_string, student_id)) self.location_string, student_id))
#This is a student_facing_error # This is a student_facing_error
error_message = "Could not contact the graders. Please notify course staff." error_message = "Could not contact the graders. Please notify course staff."
return success, allowed_to_submit, error_message return success, allowed_to_submit, error_message
if count_graded >= count_required: if count_graded >= count_required:
return success, allowed_to_submit, "" return success, allowed_to_submit, ""
else: else:
allowed_to_submit = False allowed_to_submit = False
#This is a student_facing_error # This is a student_facing_error
error_message = error_string.format(count_required - count_graded, count_graded, count_required, error_message = error_string.format(count_required - count_graded, count_graded, count_required,
student_sub_count) student_sub_count)
return success, allowed_to_submit, error_message return success, allowed_to_submit, error_message
......
...@@ -15,6 +15,7 @@ from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean ...@@ -15,6 +15,7 @@ from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
from open_ended_grading_classes import combined_open_ended_rubric from open_ended_grading_classes import combined_open_ended_rubric
from django.utils.timezone import UTC
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -76,7 +77,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -76,7 +77,7 @@ class PeerGradingModule(PeerGradingFields, 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)
#We need to set the location here so the child modules can use it # We need to set the location here so the child modules can use it
system.set('location', location) system.set('location', location)
self.system = system self.system = system
if (self.system.open_ended_grading_interface): if (self.system.open_ended_grading_interface):
...@@ -112,7 +113,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -112,7 +113,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
if not self.ajax_url.endswith("/"): if not self.ajax_url.endswith("/"):
self.ajax_url = self.ajax_url + "/" self.ajax_url = self.ajax_url + "/"
#StringyInteger could return None, so keep this check. # StringyInteger could return None, so keep this check.
if not isinstance(self.max_grade, int): if not isinstance(self.max_grade, int):
raise TypeError("max_grade needs to be an integer.") raise TypeError("max_grade needs to be an integer.")
...@@ -120,7 +121,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -120,7 +121,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
return self._closed(self.timeinfo) return self._closed(self.timeinfo)
def _closed(self, timeinfo): def _closed(self, timeinfo):
if timeinfo.close_date is not None and datetime.utcnow() > timeinfo.close_date: if timeinfo.close_date is not None and datetime.now(UTC()) > timeinfo.close_date:
return True return True
return False return False
...@@ -166,9 +167,9 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -166,9 +167,9 @@ class PeerGradingModule(PeerGradingFields, XModule):
} }
if dispatch not in handlers: if dispatch not in handlers:
#This is a dev_facing_error # This is a dev_facing_error
log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch)) log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
#This is a dev_facing_error # This is a dev_facing_error
return json.dumps({'error': 'Error handling action. Please try again.', 'success': False}) return json.dumps({'error': 'Error handling action. Please try again.', 'success': False})
d = handlers[dispatch](get) d = handlers[dispatch](get)
...@@ -187,7 +188,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -187,7 +188,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
count_required = response['count_required'] count_required = response['count_required']
success = True success = True
except GradingServiceError: except GradingServiceError:
#This is a dev_facing_error # This is a dev_facing_error
log.exception("Error getting location data from controller for location {0}, student {1}" log.exception("Error getting location data from controller for location {0}, student {1}"
.format(location, student_id)) .format(location, student_id))
...@@ -220,7 +221,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -220,7 +221,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
count_graded = response['count_graded'] count_graded = response['count_graded']
count_required = response['count_required'] count_required = response['count_required']
if count_required > 0 and count_graded >= count_required: if count_required > 0 and count_graded >= count_required:
#Ensures that once a student receives a final score for peer grading, that it does not change. # Ensures that once a student receives a final score for peer grading, that it does not change.
self.student_data_for_location = response self.student_data_for_location = response
if self.weight is not None: if self.weight is not None:
...@@ -271,10 +272,10 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -271,10 +272,10 @@ class PeerGradingModule(PeerGradingFields, XModule):
response = self.peer_gs.get_next_submission(location, grader_id) response = self.peer_gs.get_next_submission(location, grader_id)
return response return response
except GradingServiceError: except GradingServiceError:
#This is a dev_facing_error # This is a dev_facing_error
log.exception("Error getting next submission. server url: {0} location: {1}, grader_id: {2}" log.exception("Error getting next submission. server url: {0} location: {1}, grader_id: {2}"
.format(self.peer_gs.url, location, grader_id)) .format(self.peer_gs.url, location, grader_id))
#This is a student_facing_error # This is a student_facing_error
return {'success': False, return {'success': False,
'error': EXTERNAL_GRADER_NO_CONTACT_ERROR} 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR}
...@@ -314,13 +315,13 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -314,13 +315,13 @@ class PeerGradingModule(PeerGradingFields, XModule):
score, feedback, submission_key, rubric_scores, submission_flagged) score, feedback, submission_key, rubric_scores, submission_flagged)
return response return response
except GradingServiceError: except GradingServiceError:
#This is a dev_facing_error # This is a dev_facing_error
log.exception("""Error saving grade to open ended grading service. server url: {0}, location: {1}, submission_id:{2}, log.exception("""Error saving grade to open ended grading service. server url: {0}, location: {1}, submission_id:{2},
submission_key: {3}, score: {4}""" submission_key: {3}, score: {4}"""
.format(self.peer_gs.url, .format(self.peer_gs.url,
location, submission_id, submission_key, score) location, submission_id, submission_key, score)
) )
#This is a student_facing_error # This is a student_facing_error
return { return {
'success': False, 'success': False,
'error': EXTERNAL_GRADER_NO_CONTACT_ERROR 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR
...@@ -356,10 +357,10 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -356,10 +357,10 @@ class PeerGradingModule(PeerGradingFields, XModule):
response = self.peer_gs.is_student_calibrated(location, grader_id) response = self.peer_gs.is_student_calibrated(location, grader_id)
return response return response
except GradingServiceError: except GradingServiceError:
#This is a dev_facing_error # This is a dev_facing_error
log.exception("Error from open ended grading service. server url: {0}, grader_id: {0}, location: {1}" log.exception("Error from open ended grading service. server url: {0}, grader_id: {0}, location: {1}"
.format(self.peer_gs.url, grader_id, location)) .format(self.peer_gs.url, grader_id, location))
#This is a student_facing_error # This is a student_facing_error
return { return {
'success': False, 'success': False,
'error': EXTERNAL_GRADER_NO_CONTACT_ERROR 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR
...@@ -401,17 +402,17 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -401,17 +402,17 @@ class PeerGradingModule(PeerGradingFields, XModule):
response = self.peer_gs.show_calibration_essay(location, grader_id) response = self.peer_gs.show_calibration_essay(location, grader_id)
return response return response
except GradingServiceError: except GradingServiceError:
#This is a dev_facing_error # This is a dev_facing_error
log.exception("Error from open ended grading service. server url: {0}, location: {0}" log.exception("Error from open ended grading service. server url: {0}, location: {0}"
.format(self.peer_gs.url, location)) .format(self.peer_gs.url, location))
#This is a student_facing_error # This is a student_facing_error
return {'success': False, return {'success': False,
'error': EXTERNAL_GRADER_NO_CONTACT_ERROR} 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR}
# if we can't parse the rubric into HTML, # if we can't parse the rubric into HTML,
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
#This is a dev_facing_error # This is a dev_facing_error
log.exception("Cannot parse rubric string.") log.exception("Cannot parse rubric string.")
#This is a student_facing_error # This is a student_facing_error
return {'success': False, return {'success': False,
'error': 'Error displaying submission. Please notify course staff.'} 'error': 'Error displaying submission. Please notify course staff.'}
...@@ -455,11 +456,11 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -455,11 +456,11 @@ class PeerGradingModule(PeerGradingFields, XModule):
response['actual_rubric'] = rubric_renderer.render_rubric(response['actual_rubric'])['html'] response['actual_rubric'] = rubric_renderer.render_rubric(response['actual_rubric'])['html']
return response return response
except GradingServiceError: except GradingServiceError:
#This is a dev_facing_error # This is a dev_facing_error
log.exception( log.exception(
"Error saving calibration grade, location: {0}, submission_key: {1}, grader_id: {2}".format( "Error saving calibration grade, location: {0}, submission_key: {1}, grader_id: {2}".format(
location, submission_key, grader_id)) location, submission_key, grader_id))
#This is a student_facing_error # This is a student_facing_error
return self._err_response('There was an error saving your score. Please notify course staff.') return self._err_response('There was an error saving your score. Please notify course staff.')
def peer_grading_closed(self): def peer_grading_closed(self):
...@@ -491,13 +492,13 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -491,13 +492,13 @@ class PeerGradingModule(PeerGradingFields, XModule):
problem_list = problem_list_dict['problem_list'] problem_list = problem_list_dict['problem_list']
except GradingServiceError: except GradingServiceError:
#This is a student_facing_error # This is a student_facing_error
error_text = EXTERNAL_GRADER_NO_CONTACT_ERROR error_text = EXTERNAL_GRADER_NO_CONTACT_ERROR
log.error(error_text) log.error(error_text)
success = False success = False
# catch error if if the json loads fails # catch error if if the json loads fails
except ValueError: except ValueError:
#This is a student_facing_error # This is a student_facing_error
error_text = "Could not get list of problems to peer grade. Please notify course staff." error_text = "Could not get list of problems to peer grade. Please notify course staff."
log.error(error_text) log.error(error_text)
success = False success = False
...@@ -557,8 +558,8 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -557,8 +558,8 @@ class PeerGradingModule(PeerGradingFields, XModule):
''' '''
if get is None or get.get('location') is None: if get is None or get.get('location') is None:
if self.use_for_single_location not in TRUE_DICT: if self.use_for_single_location not in TRUE_DICT:
#This is an error case, because it must be set to use a single location to be called without get parameters # This is an error case, because it must be set to use a single location to be called without get parameters
#This is a dev_facing_error # This is a dev_facing_error
log.error( log.error(
"Peer grading problem in peer_grading_module called with no get parameters, but use_for_single_location is False.") "Peer grading problem in peer_grading_module called with no get parameters, but use_for_single_location is False.")
return {'html': "", 'success': False} return {'html': "", 'success': False}
......
...@@ -33,8 +33,8 @@ def test_system(): ...@@ -33,8 +33,8 @@ def test_system():
""" """
Construct a test ModuleSystem instance. Construct a test ModuleSystem instance.
By default, the render_template() method simply returns the context it is By default, the render_template() method simply returns the repr of the
passed as a string. You can override this behavior by monkey patching:: context it is passed. You can override this behavior by monkey patching::
system = test_system() system = test_system()
system.render_template = my_render_func system.render_template = my_render_func
...@@ -46,7 +46,7 @@ def test_system(): ...@@ -46,7 +46,7 @@ def test_system():
ajax_url='courses/course_id/modx/a_location', ajax_url='courses/course_id/modx/a_location',
track_function=Mock(), track_function=Mock(),
get_module=Mock(), get_module=Mock(),
render_template=lambda template, context: str(context), render_template=lambda template, context: repr(context),
replace_urls=lambda html: str(html), replace_urls=lambda html: str(html),
user=Mock(is_staff=False), user=Mock(is_staff=False),
filestore=Mock(), filestore=Mock(),
......
...@@ -18,6 +18,7 @@ from xmodule.modulestore import Location ...@@ -18,6 +18,7 @@ from xmodule.modulestore import Location
from django.http import QueryDict from django.http import QueryDict
from . import test_system from . import test_system
from pytz import UTC
class CapaFactory(object): class CapaFactory(object):
...@@ -126,7 +127,7 @@ class CapaFactory(object): ...@@ -126,7 +127,7 @@ class CapaFactory(object):
class CapaModuleTest(unittest.TestCase): class CapaModuleTest(unittest.TestCase):
def setUp(self): def setUp(self):
now = datetime.datetime.now() now = datetime.datetime.now(UTC)
day_delta = datetime.timedelta(days=1) day_delta = datetime.timedelta(days=1)
self.yesterday_str = str(now - day_delta) self.yesterday_str = str(now - day_delta)
self.today_str = str(now) self.today_str = str(now)
...@@ -475,12 +476,12 @@ class CapaModuleTest(unittest.TestCase): ...@@ -475,12 +476,12 @@ class CapaModuleTest(unittest.TestCase):
# Simulate that the problem is queued # Simulate that the problem is queued
with patch('capa.capa_problem.LoncapaProblem.is_queued') \ with patch('capa.capa_problem.LoncapaProblem.is_queued') \
as mock_is_queued,\ as mock_is_queued, \
patch('capa.capa_problem.LoncapaProblem.get_recentmost_queuetime') \ patch('capa.capa_problem.LoncapaProblem.get_recentmost_queuetime') \
as mock_get_queuetime: as mock_get_queuetime:
mock_is_queued.return_value = True mock_is_queued.return_value = True
mock_get_queuetime.return_value = datetime.datetime.now() mock_get_queuetime.return_value = datetime.datetime.now(UTC)
get_request_dict = {CapaFactory.input_key(): '3.14'} get_request_dict = {CapaFactory.input_key(): '3.14'}
result = module.check_problem(get_request_dict) result = module.check_problem(get_request_dict)
......
import unittest import unittest
from time import strptime
import datetime import datetime
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
...@@ -8,13 +7,13 @@ from mock import Mock, patch ...@@ -8,13 +7,13 @@ from mock import Mock, patch
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
import xmodule.course_module import xmodule.course_module
from xmodule.util.date_utils import time_to_datetime from django.utils.timezone import UTC
ORG = 'test_org' ORG = 'test_org'
COURSE = 'test_course' COURSE = 'test_course'
NOW = strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00') NOW = datetime.datetime.strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00').replace(tzinfo=UTC())
class DummySystem(ImportSystem): class DummySystem(ImportSystem):
...@@ -81,10 +80,10 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -81,10 +80,10 @@ class IsNewCourseTestCase(unittest.TestCase):
Mock(wraps=datetime.datetime) Mock(wraps=datetime.datetime)
) )
mocked_datetime = datetime_patcher.start() mocked_datetime = datetime_patcher.start()
mocked_datetime.utcnow.return_value = time_to_datetime(NOW) mocked_datetime.now.return_value = NOW
self.addCleanup(datetime_patcher.stop) self.addCleanup(datetime_patcher.stop)
@patch('xmodule.course_module.time.gmtime') @patch('xmodule.course_module.datetime.now')
def test_sorting_score(self, gmtime_mock): def test_sorting_score(self, gmtime_mock):
gmtime_mock.return_value = NOW gmtime_mock.return_value = NOW
...@@ -125,7 +124,7 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -125,7 +124,7 @@ class IsNewCourseTestCase(unittest.TestCase):
print "Comparing %s to %s" % (a, b) print "Comparing %s to %s" % (a, b)
assertion(a_score, b_score) assertion(a_score, b_score)
@patch('xmodule.course_module.time.gmtime') @patch('xmodule.course_module.datetime.now')
def test_start_date_text(self, gmtime_mock): def test_start_date_text(self, gmtime_mock):
gmtime_mock.return_value = NOW gmtime_mock.return_value = NOW
......
...@@ -3,19 +3,12 @@ ...@@ -3,19 +3,12 @@
from nose.tools import assert_equals from nose.tools import assert_equals
from xmodule.util import date_utils from xmodule.util import date_utils
import datetime import datetime
import time from pytz import UTC
def test_get_time_struct_display():
assert_equals("", date_utils.get_time_struct_display(None, ""))
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0))
assert_equals("03/12/1992", date_utils.get_time_struct_display(test_time, '%m/%d/%Y'))
assert_equals("15:03", date_utils.get_time_struct_display(test_time, '%H:%M'))
def test_get_default_time_display(): def test_get_default_time_display():
assert_equals("", date_utils.get_default_time_display(None)) assert_equals("", date_utils.get_default_time_display(None))
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0)) test_time = datetime.datetime(1992, 3, 12, 15, 3, 30, tzinfo=UTC)
assert_equals( assert_equals(
"Mar 12, 1992 at 15:03 UTC", "Mar 12, 1992 at 15:03 UTC",
date_utils.get_default_time_display(test_time)) date_utils.get_default_time_display(test_time))
...@@ -26,10 +19,36 @@ def test_get_default_time_display(): ...@@ -26,10 +19,36 @@ def test_get_default_time_display():
"Mar 12, 1992 at 15:03", "Mar 12, 1992 at 15:03",
date_utils.get_default_time_display(test_time, False)) date_utils.get_default_time_display(test_time, False))
def test_get_default_time_display_notz():
test_time = datetime.datetime(1992, 3, 12, 15, 3, 30)
assert_equals(
"Mar 12, 1992 at 15:03 UTC",
date_utils.get_default_time_display(test_time))
assert_equals(
"Mar 12, 1992 at 15:03 UTC",
date_utils.get_default_time_display(test_time, True))
assert_equals(
"Mar 12, 1992 at 15:03",
date_utils.get_default_time_display(test_time, False))
def test_time_to_datetime(): # pylint: disable=W0232
assert_equals(None, date_utils.time_to_datetime(None)) class NamelessTZ(datetime.tzinfo):
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0))
def utcoffset(self, _dt):
return datetime.timedelta(hours=-3)
def dst(self, _dt):
return datetime.timedelta(0)
def test_get_default_time_display_no_tzname():
assert_equals("", date_utils.get_default_time_display(None))
test_time = datetime.datetime(1992, 3, 12, 15, 3, 30, tzinfo=NamelessTZ())
assert_equals(
"Mar 12, 1992 at 15:03-0300",
date_utils.get_default_time_display(test_time))
assert_equals(
"Mar 12, 1992 at 15:03-0300",
date_utils.get_default_time_display(test_time, True))
assert_equals( assert_equals(
datetime.datetime(1992, 3, 12, 15, 3, 30), "Mar 12, 1992 at 15:03",
date_utils.time_to_datetime(test_time)) date_utils.get_default_time_display(test_time, False))
...@@ -18,8 +18,7 @@ class TestErrorModule(unittest.TestCase): ...@@ -18,8 +18,7 @@ class TestErrorModule(unittest.TestCase):
self.org = "org" self.org = "org"
self.course = "course" self.course = "course"
self.location = Location(['i4x', self.org, self.course, None, None]) self.location = Location(['i4x', self.org, self.course, None, None])
self.valid_xml = "<problem />" self.valid_xml = u"<problem>ABC \N{SNOWMAN}</problem>"
self.broken_xml = "<problem>"
self.error_msg = "Error" self.error_msg = "Error"
def test_error_module_xml_rendering(self): def test_error_module_xml_rendering(self):
...@@ -27,9 +26,9 @@ class TestErrorModule(unittest.TestCase): ...@@ -27,9 +26,9 @@ class TestErrorModule(unittest.TestCase):
self.valid_xml, self.system, self.org, self.course, self.error_msg) self.valid_xml, self.system, self.org, self.course, self.error_msg)
self.assertTrue(isinstance(descriptor, error_module.ErrorDescriptor)) self.assertTrue(isinstance(descriptor, error_module.ErrorDescriptor))
module = descriptor.xmodule(self.system) module = descriptor.xmodule(self.system)
rendered_html = module.get_html() context_repr = module.get_html()
self.assertIn(self.error_msg, rendered_html) self.assertIn(self.error_msg, context_repr)
self.assertIn(self.valid_xml, rendered_html) self.assertIn(repr(self.valid_xml), context_repr)
def test_error_module_from_descriptor(self): def test_error_module_from_descriptor(self):
descriptor = MagicMock([XModuleDescriptor], descriptor = MagicMock([XModuleDescriptor],
...@@ -41,9 +40,9 @@ class TestErrorModule(unittest.TestCase): ...@@ -41,9 +40,9 @@ class TestErrorModule(unittest.TestCase):
descriptor, self.error_msg) descriptor, self.error_msg)
self.assertTrue(isinstance(error_descriptor, error_module.ErrorDescriptor)) self.assertTrue(isinstance(error_descriptor, error_module.ErrorDescriptor))
module = error_descriptor.xmodule(self.system) module = error_descriptor.xmodule(self.system)
rendered_html = module.get_html() context_repr = module.get_html()
self.assertIn(self.error_msg, rendered_html) self.assertIn(self.error_msg, context_repr)
self.assertIn(str(descriptor), rendered_html) self.assertIn(repr(descriptor), context_repr)
class TestNonStaffErrorModule(TestErrorModule): class TestNonStaffErrorModule(TestErrorModule):
...@@ -60,9 +59,9 @@ class TestNonStaffErrorModule(TestErrorModule): ...@@ -60,9 +59,9 @@ class TestNonStaffErrorModule(TestErrorModule):
descriptor = error_module.NonStaffErrorDescriptor.from_xml( descriptor = error_module.NonStaffErrorDescriptor.from_xml(
self.valid_xml, self.system, self.org, self.course) self.valid_xml, self.system, self.org, self.course)
module = descriptor.xmodule(self.system) module = descriptor.xmodule(self.system)
rendered_html = module.get_html() context_repr = module.get_html()
self.assertNotIn(self.error_msg, rendered_html) self.assertNotIn(self.error_msg, context_repr)
self.assertNotIn(self.valid_xml, rendered_html) self.assertNotIn(repr(self.valid_xml), context_repr)
def test_error_module_from_descriptor(self): def test_error_module_from_descriptor(self):
descriptor = MagicMock([XModuleDescriptor], descriptor = MagicMock([XModuleDescriptor],
...@@ -74,6 +73,6 @@ class TestNonStaffErrorModule(TestErrorModule): ...@@ -74,6 +73,6 @@ class TestNonStaffErrorModule(TestErrorModule):
descriptor, self.error_msg) descriptor, self.error_msg)
self.assertTrue(isinstance(error_descriptor, error_module.ErrorDescriptor)) self.assertTrue(isinstance(error_descriptor, error_module.ErrorDescriptor))
module = error_descriptor.xmodule(self.system) module = error_descriptor.xmodule(self.system)
rendered_html = module.get_html() context_repr = module.get_html()
self.assertNotIn(self.error_msg, rendered_html) self.assertNotIn(self.error_msg, context_repr)
self.assertNotIn(str(descriptor), rendered_html) self.assertNotIn(str(descriptor), context_repr)
"""Tests for classes defined in fields.py.""" """Tests for classes defined in fields.py."""
import datetime import datetime
import unittest import unittest
from django.utils.timezone import UTC
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
import time from django.utils.timezone import UTC
class DateTest(unittest.TestCase): class DateTest(unittest.TestCase):
date = Date() date = Date()
@staticmethod def compare_dates(self, dt1, dt2, expected_delta):
def struct_to_datetime(struct_time): self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "-"
return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, + str(dt2) + "!=" + str(expected_delta))
struct_time.tm_mday, struct_time.tm_hour,
struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
def compare_dates(self, date1, date2, expected_delta):
dt1 = DateTest.struct_to_datetime(date1)
dt2 = DateTest.struct_to_datetime(date2)
self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-"
+ str(date2) + "!=" + str(expected_delta))
def test_from_json(self): def test_from_json(self):
'''Test conversion from iso compatible date strings to struct_time''' '''Test conversion from iso compatible date strings to struct_time'''
...@@ -55,10 +46,10 @@ class DateTest(unittest.TestCase): ...@@ -55,10 +46,10 @@ class DateTest(unittest.TestCase):
def test_old_due_date_format(self): def test_old_due_date_format(self):
current = datetime.datetime.today() current = datetime.datetime.today()
self.assertEqual( self.assertEqual(
time.struct_time((current.year, 3, 12, 12, 0, 0, 1, 71, 0)), datetime.datetime(current.year, 3, 12, 12, tzinfo=UTC()),
DateTest.date.from_json("March 12 12:00")) DateTest.date.from_json("March 12 12:00"))
self.assertEqual( self.assertEqual(
time.struct_time((current.year, 12, 4, 16, 30, 0, 2, 338, 0)), datetime.datetime(current.year, 12, 4, 16, 30, tzinfo=UTC()),
DateTest.date.from_json("December 4 16:30")) DateTest.date.from_json("December 4 16:30"))
def test_to_json(self): def test_to_json(self):
...@@ -67,7 +58,7 @@ class DateTest(unittest.TestCase): ...@@ -67,7 +58,7 @@ class DateTest(unittest.TestCase):
''' '''
self.assertEqual( self.assertEqual(
DateTest.date.to_json( DateTest.date.to_json(
time.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")), datetime.datetime.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")),
"2012-12-31T23:59:59Z") "2012-12-31T23:59:59Z")
self.assertEqual( self.assertEqual(
DateTest.date.to_json( DateTest.date.to_json(
...@@ -76,7 +67,7 @@ class DateTest(unittest.TestCase): ...@@ -76,7 +67,7 @@ class DateTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
DateTest.date.to_json( DateTest.date.to_json(
DateTest.date.from_json("2012-12-31T23:00:01-01:00")), DateTest.date.from_json("2012-12-31T23:00:01-01:00")),
"2013-01-01T00:00:01Z") "2012-12-31T23:00:01-01:00")
class StringyIntegerTest(unittest.TestCase): class StringyIntegerTest(unittest.TestCase):
......
...@@ -13,6 +13,8 @@ from xmodule.modulestore.inheritance import compute_inherited_metadata ...@@ -13,6 +13,8 @@ from xmodule.modulestore.inheritance import compute_inherited_metadata
from xmodule.fields import Date from xmodule.fields import Date
from .test_export import DATA_DIR from .test_export import DATA_DIR
import datetime
from django.utils.timezone import UTC
ORG = 'test_org' ORG = 'test_org'
COURSE = 'test_course' COURSE = 'test_course'
...@@ -40,8 +42,8 @@ class DummySystem(ImportSystem): ...@@ -40,8 +42,8 @@ class DummySystem(ImportSystem):
load_error_modules=load_error_modules, load_error_modules=load_error_modules,
) )
def render_template(self, template, context): def render_template(self, _template, _context):
raise Exception("Shouldn't be called") raise Exception("Shouldn't be called")
class BaseCourseTestCase(unittest.TestCase): class BaseCourseTestCase(unittest.TestCase):
...@@ -62,17 +64,18 @@ class BaseCourseTestCase(unittest.TestCase): ...@@ -62,17 +64,18 @@ class BaseCourseTestCase(unittest.TestCase):
class ImportTestCase(BaseCourseTestCase): class ImportTestCase(BaseCourseTestCase):
date = Date()
def test_fallback(self): def test_fallback(self):
'''Check that malformed xml loads as an ErrorDescriptor.''' '''Check that malformed xml loads as an ErrorDescriptor.'''
bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>''' # Use an exotic character to also flush out Unicode issues.
bad_xml = u'''<sequential display_name="oops\N{SNOWMAN}"><video url="hi"></sequential>'''
system = self.get_system() system = self.get_system()
descriptor = system.process_xml(bad_xml) descriptor = system.process_xml(bad_xml)
self.assertEqual(descriptor.__class__.__name__, self.assertEqual(descriptor.__class__.__name__, 'ErrorDescriptor')
'ErrorDescriptor')
def test_unique_url_names(self): def test_unique_url_names(self):
'''Check that each error gets its very own url_name''' '''Check that each error gets its very own url_name'''
...@@ -145,15 +148,18 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -145,15 +148,18 @@ class ImportTestCase(BaseCourseTestCase):
descriptor = system.process_xml(start_xml) descriptor = system.process_xml(start_xml)
compute_inherited_metadata(descriptor) compute_inherited_metadata(descriptor)
# pylint: disable=W0212
print(descriptor, descriptor._model_data) print(descriptor, descriptor._model_data)
self.assertEqual(descriptor.lms.due, Date().from_json(v)) self.assertEqual(descriptor.lms.due, ImportTestCase.date.from_json(v))
# Check that the child inherits due correctly # Check that the child inherits due correctly
child = descriptor.get_children()[0] child = descriptor.get_children()[0]
self.assertEqual(child.lms.due, Date().from_json(v)) self.assertEqual(child.lms.due, ImportTestCase.date.from_json(v))
self.assertEqual(child._inheritable_metadata, child._inherited_metadata) self.assertEqual(child._inheritable_metadata, child._inherited_metadata)
self.assertEqual(2, len(child._inherited_metadata)) self.assertEqual(2, len(child._inherited_metadata))
self.assertEqual('1970-01-01T00:00:00Z', child._inherited_metadata['start']) self.assertLessEqual(ImportTestCase.date.from_json(
child._inherited_metadata['start']),
datetime.datetime.now(UTC()))
self.assertEqual(v, child._inherited_metadata['due']) self.assertEqual(v, child._inherited_metadata['due'])
# Now export and check things # Now export and check things
...@@ -209,9 +215,13 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -209,9 +215,13 @@ class ImportTestCase(BaseCourseTestCase):
# Check that the child does not inherit a value for due # Check that the child does not inherit a value for due
child = descriptor.get_children()[0] child = descriptor.get_children()[0]
self.assertEqual(child.lms.due, None) self.assertEqual(child.lms.due, None)
# pylint: disable=W0212
self.assertEqual(child._inheritable_metadata, child._inherited_metadata) self.assertEqual(child._inheritable_metadata, child._inherited_metadata)
self.assertEqual(1, len(child._inherited_metadata)) self.assertEqual(1, len(child._inherited_metadata))
self.assertEqual('1970-01-01T00:00:00Z', child._inherited_metadata['start']) # why do these tests look in the internal structure v just calling child.start?
self.assertLessEqual(
ImportTestCase.date.from_json(child._inherited_metadata['start']),
datetime.datetime.now(UTC()))
def test_metadata_override_default(self): def test_metadata_override_default(self):
""" """
...@@ -230,14 +240,17 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -230,14 +240,17 @@ class ImportTestCase(BaseCourseTestCase):
</course>'''.format(due=course_due, org=ORG, course=COURSE, url_name=url_name) </course>'''.format(due=course_due, org=ORG, course=COURSE, url_name=url_name)
descriptor = system.process_xml(start_xml) descriptor = system.process_xml(start_xml)
child = descriptor.get_children()[0] child = descriptor.get_children()[0]
# pylint: disable=W0212
child._model_data['due'] = child_due child._model_data['due'] = child_due
compute_inherited_metadata(descriptor) compute_inherited_metadata(descriptor)
self.assertEqual(descriptor.lms.due, Date().from_json(course_due)) self.assertEqual(descriptor.lms.due, ImportTestCase.date.from_json(course_due))
self.assertEqual(child.lms.due, Date().from_json(child_due)) self.assertEqual(child.lms.due, ImportTestCase.date.from_json(child_due))
# Test inherited metadata. Due does not appear here (because explicitly set on child). # Test inherited metadata. Due does not appear here (because explicitly set on child).
self.assertEqual(1, len(child._inherited_metadata)) self.assertEqual(1, len(child._inherited_metadata))
self.assertEqual('1970-01-01T00:00:00Z', child._inherited_metadata['start']) self.assertLessEqual(
ImportTestCase.date.from_json(child._inherited_metadata['start']),
datetime.datetime.now(UTC()))
# Test inheritable metadata. This has the course inheritable value for due. # Test inheritable metadata. This has the course inheritable value for due.
self.assertEqual(2, len(child._inheritable_metadata)) self.assertEqual(2, len(child._inheritable_metadata))
self.assertEqual(course_due, child._inheritable_metadata['due']) self.assertEqual(course_due, child._inheritable_metadata['due'])
......
from .timeparse import parse_timedelta from .timeparse import parse_timedelta
from xmodule.util.date_utils import time_to_datetime
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -17,7 +16,7 @@ class TimeInfo(object): ...@@ -17,7 +16,7 @@ class TimeInfo(object):
""" """
def __init__(self, due_date, grace_period_string): def __init__(self, due_date, grace_period_string):
if due_date is not None: if due_date is not None:
self.display_due_date = time_to_datetime(due_date) self.display_due_date = due_date
else: else:
self.display_due_date = None self.display_due_date = None
......
""" """
Helper functions for handling time in the format we like. Helper functions for handling time in the format we like.
""" """
import time
import re import re
from datetime import timedelta from datetime import timedelta, datetime
TIME_FORMAT = "%Y-%m-%dT%H:%M" TIME_FORMAT = "%Y-%m-%dT%H:%M"
...@@ -17,14 +16,14 @@ def parse_time(time_str): ...@@ -17,14 +16,14 @@ def parse_time(time_str):
Raises ValueError if the string is not in the right format. Raises ValueError if the string is not in the right format.
""" """
return time.strptime(time_str, TIME_FORMAT) return datetime.strptime(time_str, TIME_FORMAT)
def stringify_time(time_struct): def stringify_time(dt):
""" """
Convert a time struct to a string Convert a datetime struct to a string
""" """
return time.strftime(TIME_FORMAT, time_struct) return dt.isoformat()
def parse_timedelta(time_str): def parse_timedelta(time_str):
""" """
......
import time def get_default_time_display(dt, show_timezone=True):
import datetime
def get_default_time_display(time_struct, show_timezone=True):
""" """
Converts a time struct to a string representation. This is the default Converts a datetime to a string representation. This is the default
representation used in Studio and LMS. representation used in Studio and LMS.
It is of the form "Apr 09, 2013 at 16:00" or "Apr 09, 2013 at 16:00 UTC", It is of the form "Apr 09, 2013 at 16:00" or "Apr 09, 2013 at 16:00 UTC",
depending on the value of show_timezone. depending on the value of show_timezone.
If None is passed in for time_struct, an empty string will be returned. If None is passed in for dt, an empty string will be returned.
The default value of show_timezone is True. The default value of show_timezone is True.
""" """
timezone = "" if time_struct is None or not show_timezone else " UTC" if dt is None:
return get_time_struct_display(time_struct, "%b %d, %Y at %H:%M") + timezone return ""
timezone = ""
if dt is not None and show_timezone:
def get_time_struct_display(time_struct, format): if dt.tzinfo is not None:
""" try:
Converts a time struct to a string based on the given format. timezone = " " + dt.tzinfo.tzname(dt)
except NotImplementedError:
If None is passed in, an empty string will be returned. timezone = dt.strftime('%z')
""" else:
return '' if time_struct is None else time.strftime(format, time_struct) timezone = " UTC"
return dt.strftime("%b %d, %Y at %H:%M") + timezone
def time_to_datetime(time_struct):
"""
Convert a time struct to a datetime.
If None is passed in, None will be returned.
"""
return datetime.datetime(*time_struct[:6]) if time_struct else None
<sequential> <sequential>
<vertical filename="vertical_58" slug="vertical_58" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"/> <vertical filename="vertical_58" slug="vertical_58"
<vertical slug="vertical_66" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"> graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted"
<problem filename="S1E3_AC_power" slug="S1E3_AC_power" name="S1E3: AC power"/> rerandomize="never" />
<customtag tag="S1E3" slug="discuss_67" impl="discuss"/> <vertical slug="vertical_66" graceperiod="1 day 12 hours 59 minutes 59 seconds"
<!-- utf-8 characters acceptable, but not HTML entities --> showanswer="attempted" rerandomize="never">
<html slug="html_68"> S1E4 has been removed…</html> <problem filename="S1E3_AC_power" slug="S1E3_AC_power"
</vertical> name="S1E3: AC power" />
<vertical filename="vertical_89" slug="vertical_89" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"/> <customtag tag="S1E3" slug="discuss_67" impl="discuss" />
<vertical slug="vertical_94" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"> <!-- utf-8 characters acceptable, but not HTML entities -->
<video youtube="0.75:XNh13VZhThQ,1.0:XbDRmF6J0K0,1.25:JDty12WEQWk,1.50:wELKGj-5iyM" slug="What_s_next" name="What's next"/> <html slug="html_68"> S1E4 has been removed…</html>
<html slug="html_95">Minor correction: Six elements (five resistors)…</html> </vertical>
<customtag tag="S1" slug="discuss_96" impl="discuss"/> <vertical filename="vertical_89" slug="vertical_89"
</vertical> graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted"
rerandomize="never" />
<vertical slug="vertical_94" graceperiod="1 day 12 hours 59 minutes 59 seconds"
showanswer="attempted" rerandomize="never">
<video
youtube="0.75:XNh13VZhThQ,1.0:XbDRmF6J0K0,1.25:JDty12WEQWk,1.50:wELKGj-5iyM"
slug="What_s_next" name="What's next" />
<html slug="html_95">Minor correction: Six elements (five resistors)…
</html>
<customtag tag="S1" slug="discuss_96" impl="discuss" />
</vertical>
<randomize url_name="PS1_Q4" display_name="Problem 4: Read a Molecule"> <randomize url_name="PS1_Q4" display_name="Problem 4: Read a Molecule">
<vertical> <vertical>
<html slug="html_900"> <html slug="html_900">
<!-- UTF-8 characters are acceptable… HTML entities are not --> <!-- UTF-8 characters are acceptable… HTML entities are not -->
<h1>Inline content…</h1> <h1>Inline content…</h1>
</html> </html>
</vertical> </vertical>
</randomize> </randomize>
</sequential> </sequential>
...@@ -16,6 +16,7 @@ from xmodule.x_module import XModule, XModuleDescriptor ...@@ -16,6 +16,7 @@ from xmodule.x_module import XModule, XModuleDescriptor
from student.models import CourseEnrollmentAllowed from student.models import CourseEnrollmentAllowed
from courseware.masquerade import is_masquerading_as_student from courseware.masquerade import is_masquerading_as_student
from django.utils.timezone import UTC
DEBUG_ACCESS = False DEBUG_ACCESS = False
...@@ -133,7 +134,7 @@ def _has_access_course_desc(user, course, action): ...@@ -133,7 +134,7 @@ def _has_access_course_desc(user, course, action):
(staff can always enroll) (staff can always enroll)
""" """
now = time.gmtime() now = datetime.now(UTC())
start = course.enrollment_start start = course.enrollment_start
end = course.enrollment_end end = course.enrollment_end
...@@ -242,7 +243,7 @@ def _has_access_descriptor(user, descriptor, action, course_context=None): ...@@ -242,7 +243,7 @@ def _has_access_descriptor(user, descriptor, action, course_context=None):
# Check start date # Check start date
if descriptor.lms.start is not None: if descriptor.lms.start is not None:
now = time.gmtime() now = datetime.now(UTC())
effective_start = _adjust_start_date_for_beta_testers(user, descriptor) effective_start = _adjust_start_date_for_beta_testers(user, descriptor)
if now > effective_start: if now > effective_start:
# after start date, everyone can see it # after start date, everyone can see it
...@@ -365,7 +366,7 @@ def _course_org_staff_group_name(location, course_context=None): ...@@ -365,7 +366,7 @@ def _course_org_staff_group_name(location, course_context=None):
def group_names_for(role, location, course_context=None): def group_names_for(role, location, course_context=None):
"""Returns the group names for a given role with this location. Plural """Returns the group names for a given role with this location. Plural
because it will return both the name we expect now as well as the legacy because it will return both the name we expect now as well as the legacy
group name we support for backwards compatibility. This should not check group name we support for backwards compatibility. This should not check
the DB for existence of a group (like some of its callers do) because that's the DB for existence of a group (like some of its callers do) because that's
...@@ -483,8 +484,7 @@ def _adjust_start_date_for_beta_testers(user, descriptor): ...@@ -483,8 +484,7 @@ def _adjust_start_date_for_beta_testers(user, descriptor):
non-None start date. non-None start date.
Returns: Returns:
A time, in the same format as returned by time.gmtime(). Either the same as A datetime. Either the same as start, or earlier for beta testers.
start, or earlier for beta testers.
NOTE: number of days to adjust should be cached to avoid looking it up thousands of NOTE: number of days to adjust should be cached to avoid looking it up thousands of
times per query. times per query.
...@@ -505,15 +505,11 @@ def _adjust_start_date_for_beta_testers(user, descriptor): ...@@ -505,15 +505,11 @@ def _adjust_start_date_for_beta_testers(user, descriptor):
beta_group = course_beta_test_group_name(descriptor.location) beta_group = course_beta_test_group_name(descriptor.location)
if beta_group in user_groups: if beta_group in user_groups:
debug("Adjust start time: user in group %s", beta_group) debug("Adjust start time: user in group %s", beta_group)
# time_structs don't support subtraction, so convert to datetimes, start_as_datetime = descriptor.lms.start
# subtract, convert back.
# (fun fact: datetime(*a_time_struct[:6]) is the beautiful syntax for
# converting time_structs into datetimes)
start_as_datetime = datetime(*descriptor.lms.start[:6])
delta = timedelta(descriptor.lms.days_early_for_beta) delta = timedelta(descriptor.lms.days_early_for_beta)
effective = start_as_datetime - delta effective = start_as_datetime - delta
# ...and back to time_struct # ...and back to time_struct
return effective.timetuple() return effective
return descriptor.lms.start return descriptor.lms.start
...@@ -564,7 +560,7 @@ def _has_access_to_location(user, location, access_level, course_context): ...@@ -564,7 +560,7 @@ def _has_access_to_location(user, location, access_level, course_context):
return True return True
debug("Deny: user not in groups %s", staff_groups) debug("Deny: user not in groups %s", staff_groups)
if access_level == 'instructor' or access_level == 'staff': # instructors get staff privileges if access_level == 'instructor' or access_level == 'staff': # instructors get staff privileges
instructor_groups = group_names_for_instructor(location, course_context) + \ instructor_groups = group_names_for_instructor(location, course_context) + \
[_course_org_instructor_group_name(location, course_context)] [_course_org_instructor_group_name(location, course_context)]
for instructor_group in instructor_groups: for instructor_group in instructor_groups:
......
...@@ -112,10 +112,10 @@ Feature: Answer problems ...@@ -112,10 +112,10 @@ Feature: Answer problems
Scenario: I can view and hide the answer if the problem has it: Scenario: I can view and hide the answer if the problem has it:
Given I am viewing a "numerical" that shows the answer "always" Given I am viewing a "numerical" that shows the answer "always"
When I press the "Show Answer" button When I press the button with the label "Show Answer(s)"
Then The "Hide Answer" button does appear Then the button with the label "Hide Answer(s)" does appear
And The "Show Answer" button does not appear And the button with the label "Show Answer(s)" does not appear
And I should see "4.14159" somewhere in the page And I should see "4.14159" somewhere in the page
When I press the "Hide Answer" button When I press the button with the label "Hide Answer(s)"
Then The "Show Answer" button does appear Then the button with the label "Show Answer(s)" does appear
And I should not see "4.14159" anywhere on the page And I should not see "4.14159" anywhere on the page
...@@ -9,7 +9,7 @@ from lettuce import world, step ...@@ -9,7 +9,7 @@ from lettuce import world, step
from lettuce.django import django_url from lettuce.django import django_url
from common import i_am_registered_for_the_course, TEST_SECTION_NAME from common import i_am_registered_for_the_course, TEST_SECTION_NAME
from problems_setup import PROBLEM_DICT, answer_problem, problem_has_answer, add_problem_to_course from problems_setup import PROBLEM_DICT, answer_problem, problem_has_answer, add_problem_to_course
from nose.tools import assert_equal, assert_not_equal
@step(u'I am viewing a "([^"]*)" problem with "([^"]*)" attempt') @step(u'I am viewing a "([^"]*)" problem with "([^"]*)" attempt')
def view_problem_with_attempts(step, problem_type, attempts): def view_problem_with_attempts(step, problem_type, attempts):
...@@ -116,6 +116,14 @@ def reset_problem(step): ...@@ -116,6 +116,14 @@ def reset_problem(step):
world.css_click('input.reset') world.css_click('input.reset')
@step(u'I press the button with the label "([^"]*)"$')
def press_the_button_with_label(step, buttonname):
button_css = 'button span.show-label'
elem = world.css_find(button_css).first
assert_equal(elem.text, buttonname)
elem.click()
@step(u'The "([^"]*)" button does( not)? appear') @step(u'The "([^"]*)" button does( not)? appear')
def action_button_present(step, buttonname, doesnt_appear): def action_button_present(step, buttonname, doesnt_appear):
button_css = 'section.action input[value*="%s"]' % buttonname button_css = 'section.action input[value*="%s"]' % buttonname
...@@ -125,6 +133,16 @@ def action_button_present(step, buttonname, doesnt_appear): ...@@ -125,6 +133,16 @@ def action_button_present(step, buttonname, doesnt_appear):
assert world.is_css_present(button_css) assert world.is_css_present(button_css)
@step(u'the button with the label "([^"]*)" does( not)? appear')
def button_with_label_present(step, buttonname, doesnt_appear):
button_css = 'button span.show-label'
elem = world.css_find(button_css).first
if doesnt_appear:
assert_not_equal(elem.text, buttonname)
else:
assert_equal(elem.text, buttonname)
@step(u'My "([^"]*)" answer is marked "([^"]*)"') @step(u'My "([^"]*)" answer is marked "([^"]*)"')
def assert_answer_mark(step, problem_type, correctness): def assert_answer_mark(step, problem_type, correctness):
""" """
......
...@@ -12,6 +12,7 @@ from courseware.models import StudentModule, XModuleContentField, XModuleSetting ...@@ -12,6 +12,7 @@ from courseware.models import StudentModule, XModuleContentField, XModuleSetting
from courseware.models import XModuleStudentInfoField, XModuleStudentPrefsField from courseware.models import XModuleStudentInfoField, XModuleStudentPrefsField
from xmodule.modulestore import Location from xmodule.modulestore import Location
from pytz import UTC
location = partial(Location, 'i4x', 'edX', 'test_course', 'problem') location = partial(Location, 'i4x', 'edX', 'test_course', 'problem')
...@@ -28,8 +29,8 @@ class RegistrationFactory(StudentRegistrationFactory): ...@@ -28,8 +29,8 @@ class RegistrationFactory(StudentRegistrationFactory):
class UserFactory(StudentUserFactory): class UserFactory(StudentUserFactory):
email = 'robot@edx.org' email = 'robot@edx.org'
last_name = 'Tester' last_name = 'Tester'
last_login = datetime.now() last_login = datetime.now(UTC)
date_joined = datetime.now() date_joined = datetime.now(UTC)
class GroupFactory(StudentGroupFactory): class GroupFactory(StudentGroupFactory):
......
import unittest from mock import Mock, patch
import logging
import time
from mock import Mock, MagicMock, patch
from django.conf import settings
from django.test import TestCase from django.test import TestCase
from xmodule.course_module import CourseDescriptor
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.timeparse import parse_time
from xmodule.x_module import XModule, XModuleDescriptor
import courseware.access as access import courseware.access as access
from .factories import CourseEnrollmentAllowedFactory from .factories import CourseEnrollmentAllowedFactory
import datetime
from django.utils.timezone import UTC
class AccessTestCase(TestCase): class AccessTestCase(TestCase):
...@@ -77,7 +71,7 @@ class AccessTestCase(TestCase): ...@@ -77,7 +71,7 @@ class AccessTestCase(TestCase):
# TODO: override DISABLE_START_DATES and test the start date branch of the method # TODO: override DISABLE_START_DATES and test the start date branch of the method
u = Mock() u = Mock()
d = Mock() d = Mock()
d.start = time.gmtime(time.time() - 86400) # make sure the start time is in the past d.start = datetime.datetime.now(UTC()) - datetime.timedelta(days=1) # make sure the start time is in the past
# Always returns true because DISABLE_START_DATES is set in test.py # Always returns true because DISABLE_START_DATES is set in test.py
self.assertTrue(access._has_access_descriptor(u, d, 'load')) self.assertTrue(access._has_access_descriptor(u, d, 'load'))
...@@ -85,8 +79,8 @@ class AccessTestCase(TestCase): ...@@ -85,8 +79,8 @@ class AccessTestCase(TestCase):
def test__has_access_course_desc_can_enroll(self): def test__has_access_course_desc_can_enroll(self):
u = Mock() u = Mock()
yesterday = time.gmtime(time.time() - 86400) yesterday = datetime.datetime.now(UTC()) - datetime.timedelta(days=1)
tomorrow = time.gmtime(time.time() + 86400) tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1)
c = Mock(enrollment_start=yesterday, enrollment_end=tomorrow) c = Mock(enrollment_start=yesterday, enrollment_end=tomorrow)
# User can enroll if it is between the start and end dates # User can enroll if it is between the start and end dates
......
...@@ -139,3 +139,26 @@ class ViewsTestCase(TestCase): ...@@ -139,3 +139,26 @@ class ViewsTestCase(TestCase):
self.assertContains(result, expected_end_text) self.assertContains(result, expected_end_text)
else: else:
self.assertNotContains(result, "Classes End") self.assertNotContains(result, "Classes End")
def test_chat_settings(self):
mock_user = MagicMock()
mock_user.username = "johndoe"
mock_course = MagicMock()
mock_course.id = "a/b/c"
# Stub this out in the case that it's not in the settings
domain = "jabber.edx.org"
settings.JABBER_DOMAIN = domain
chat_settings = views.chat_settings(mock_course, mock_user)
# Test the proper format of all chat settings
self.assertEquals(chat_settings['domain'], domain)
self.assertEquals(chat_settings['room'], "a-b-c_class")
self.assertEquals(chat_settings['username'], "johndoe@%s" % domain)
# TODO: this needs to be changed once we figure out how to
# generate/store a real password.
self.assertEquals(chat_settings['password'], "johndoe@%s" % domain)
...@@ -3,7 +3,6 @@ Test for lms courseware app ...@@ -3,7 +3,6 @@ Test for lms courseware app
''' '''
import logging import logging
import json import json
import time
import random import random
from urlparse import urlsplit, urlunsplit from urlparse import urlsplit, urlunsplit
...@@ -30,6 +29,8 @@ from xmodule.modulestore.django import modulestore ...@@ -30,6 +29,8 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.xml import XMLModuleStore
import datetime
from django.utils.timezone import UTC
log = logging.getLogger("mitx." + __name__) log = logging.getLogger("mitx." + __name__)
...@@ -64,7 +65,7 @@ def mongo_store_config(data_dir): ...@@ -64,7 +65,7 @@ def mongo_store_config(data_dir):
'db': 'test_xmodule', 'db': 'test_xmodule',
'collection': 'modulestore_%s' % uuid4().hex, 'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir, 'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string'
} }
} }
} }
...@@ -287,7 +288,7 @@ class PageLoaderTestCase(LoginEnrollmentTestCase): ...@@ -287,7 +288,7 @@ class PageLoaderTestCase(LoginEnrollmentTestCase):
''' '''
Choose a page in the course randomly, and assert that it loads Choose a page in the course randomly, and assert that it loads
''' '''
# enroll in the course before trying to access pages # enroll in the course before trying to access pages
courses = module_store.get_courses() courses = module_store.get_courses()
self.assertEqual(len(courses), 1) self.assertEqual(len(courses), 1)
course = courses[0] course = courses[0]
...@@ -603,9 +604,9 @@ class TestViewAuth(LoginEnrollmentTestCase): ...@@ -603,9 +604,9 @@ class TestViewAuth(LoginEnrollmentTestCase):
"""Actually do the test, relying on settings to be right.""" """Actually do the test, relying on settings to be right."""
# Make courses start in the future # Make courses start in the future
tomorrow = time.time() + 24 * 3600 tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1)
self.toy.lms.start = time.gmtime(tomorrow) self.toy.lms.start = tomorrow
self.full.lms.start = time.gmtime(tomorrow) self.full.lms.start = tomorrow
self.assertFalse(self.toy.has_started()) self.assertFalse(self.toy.has_started())
self.assertFalse(self.full.has_started()) self.assertFalse(self.full.has_started())
...@@ -728,18 +729,18 @@ class TestViewAuth(LoginEnrollmentTestCase): ...@@ -728,18 +729,18 @@ class TestViewAuth(LoginEnrollmentTestCase):
"""Actually do the test, relying on settings to be right.""" """Actually do the test, relying on settings to be right."""
# Make courses start in the future # Make courses start in the future
tomorrow = time.time() + 24 * 3600 tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1)
nextday = tomorrow + 24 * 3600 nextday = tomorrow + datetime.timedelta(days=1)
yesterday = time.time() - 24 * 3600 yesterday = datetime.datetime.now(UTC()) - datetime.timedelta(days=1)
print "changing" print "changing"
# toy course's enrollment period hasn't started # toy course's enrollment period hasn't started
self.toy.enrollment_start = time.gmtime(tomorrow) self.toy.enrollment_start = tomorrow
self.toy.enrollment_end = time.gmtime(nextday) self.toy.enrollment_end = nextday
# full course's has # full course's has
self.full.enrollment_start = time.gmtime(yesterday) self.full.enrollment_start = yesterday
self.full.enrollment_end = time.gmtime(tomorrow) self.full.enrollment_end = tomorrow
print "login" print "login"
# First, try with an enrolled student # First, try with an enrolled student
...@@ -778,12 +779,10 @@ class TestViewAuth(LoginEnrollmentTestCase): ...@@ -778,12 +779,10 @@ class TestViewAuth(LoginEnrollmentTestCase):
self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES'])
# Make courses start in the future # Make courses start in the future
tomorrow = time.time() + 24 * 3600 tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1)
# nextday = tomorrow + 24 * 3600
# yesterday = time.time() - 24 * 3600
# toy course's hasn't started # toy course's hasn't started
self.toy.lms.start = time.gmtime(tomorrow) self.toy.lms.start = tomorrow
self.assertFalse(self.toy.has_started()) self.assertFalse(self.toy.has_started())
# but should be accessible for beta testers # but should be accessible for beta testers
...@@ -854,7 +853,7 @@ class TestSubmittingProblems(LoginEnrollmentTestCase): ...@@ -854,7 +853,7 @@ class TestSubmittingProblems(LoginEnrollmentTestCase):
modx_url = self.modx_url(problem_location, 'problem_check') modx_url = self.modx_url(problem_location, 'problem_check')
answer_key_prefix = 'input_i4x-edX-{}-problem-{}_'.format(self.course_slug, problem_url_name) answer_key_prefix = 'input_i4x-edX-{}-problem-{}_'.format(self.course_slug, problem_url_name)
resp = self.client.post(modx_url, resp = self.client.post(modx_url,
{ (answer_key_prefix + k): v for k,v in responses.items() } { (answer_key_prefix + k): v for k, v in responses.items() }
) )
return resp return resp
...@@ -925,7 +924,7 @@ class TestCourseGrader(TestSubmittingProblems): ...@@ -925,7 +924,7 @@ class TestCourseGrader(TestSubmittingProblems):
# Only get half of the first problem correct # Only get half of the first problem correct
self.submit_question_answer('H1P1', {'2_1': 'Correct', '2_2': 'Incorrect'}) self.submit_question_answer('H1P1', {'2_1': 'Correct', '2_2': 'Incorrect'})
self.check_grade_percent(0.06) self.check_grade_percent(0.06)
self.assertEqual(earned_hw_scores(), [1.0, 0, 0]) # Order matters self.assertEqual(earned_hw_scores(), [1.0, 0, 0]) # Order matters
self.assertEqual(score_for_hw('Homework1'), [1.0, 0.0]) self.assertEqual(score_for_hw('Homework1'), [1.0, 0.0])
# Get both parts of the first problem correct # Get both parts of the first problem correct
...@@ -958,16 +957,16 @@ class TestCourseGrader(TestSubmittingProblems): ...@@ -958,16 +957,16 @@ class TestCourseGrader(TestSubmittingProblems):
# Third homework # Third homework
self.submit_question_answer('H3P1', {'2_1': 'Correct', '2_2': 'Correct'}) self.submit_question_answer('H3P1', {'2_1': 'Correct', '2_2': 'Correct'})
self.check_grade_percent(0.42) # Score didn't change self.check_grade_percent(0.42) # Score didn't change
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0]) self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0])
self.submit_question_answer('H3P2', {'2_1': 'Correct', '2_2': 'Correct'}) self.submit_question_answer('H3P2', {'2_1': 'Correct', '2_2': 'Correct'})
self.check_grade_percent(0.5) # Now homework2 dropped. Score changes self.check_grade_percent(0.5) # Now homework2 dropped. Score changes
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0]) self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0])
# Now we answer the final question (worth half of the grade) # Now we answer the final question (worth half of the grade)
self.submit_question_answer('FinalQuestion', {'2_1': 'Correct', '2_2': 'Correct'}) self.submit_question_answer('FinalQuestion', {'2_1': 'Correct', '2_2': 'Correct'})
self.check_grade_percent(1.0) # Hooray! We got 100% self.check_grade_percent(1.0) # Hooray! We got 100%
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
...@@ -1000,7 +999,7 @@ class TestSchematicResponse(TestSubmittingProblems): ...@@ -1000,7 +999,7 @@ class TestSchematicResponse(TestSubmittingProblems):
{ '2_1': json.dumps( { '2_1': json.dumps(
[['transient', {'Z': [ [['transient', {'Z': [
[0.0000004, 2.8], [0.0000004, 2.8],
[0.0000009, 0.0], # wrong. [0.0000009, 0.0], # wrong.
[0.0000014, 2.8], [0.0000014, 2.8],
[0.0000019, 2.8], [0.0000019, 2.8],
[0.0000024, 2.8], [0.0000024, 2.8],
......
...@@ -5,7 +5,7 @@ from functools import partial ...@@ -5,7 +5,7 @@ from functools import partial
from django.conf import settings from django.conf import settings
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.exceptions import PermissionDenied from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
...@@ -35,12 +35,12 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr ...@@ -35,12 +35,12 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr
from xmodule.modulestore.search import path_to_location from xmodule.modulestore.search import path_to_location
import comment_client import comment_client
import jabber.utils
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
template_imports = {'urllib': urllib} template_imports = {'urllib': urllib}
def user_groups(user): def user_groups(user):
""" """
TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately. TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
...@@ -298,6 +298,17 @@ def index(request, course_id, chapter=None, section=None, ...@@ -298,6 +298,17 @@ def index(request, course_id, chapter=None, section=None,
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa') 'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa')
} }
# Only show the chat if it's enabled both in the settings and
# by the course.
context['show_chat'] = course.show_chat and settings.MITX_FEATURES.get('ENABLE_CHAT')
if context['show_chat']:
context['chat'] = {
'bosh_url': jabber.utils.get_bosh_url(),
'course_room': jabber.utils.get_room_name_for_course(course.id),
'username': "%s@%s" % (user.username, settings.JABBER.get('HOST')),
'password': jabber.utils.get_password_for_user(user.username)
}
chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter) chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
if chapter_descriptor is not None: if chapter_descriptor is not None:
save_child_position(course_module, chapter) save_child_position(course_module, chapter)
......
import time
from collections import defaultdict from collections import defaultdict
import logging import logging
import time
import urllib import urllib
from datetime import datetime from datetime import datetime
from courseware.module_render import get_module from courseware.module_render import get_module
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.search import path_to_location
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import connection from django.db import connection
...@@ -16,13 +11,12 @@ from django.http import HttpResponse ...@@ -16,13 +11,12 @@ from django.http import HttpResponse
from django.utils import simplejson from django.utils import simplejson
from django_comment_common.models import Role from django_comment_common.models import Role
from django_comment_client.permissions import check_permissions_by_view from django_comment_client.permissions import check_permissions_by_view
from xmodule.modulestore.exceptions import NoPathToItem
from mitxmako import middleware from mitxmako import middleware
import pystache_custom as pystache import pystache_custom as pystache
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from django.utils.timezone import UTC
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -100,7 +94,7 @@ def get_discussion_category_map(course): ...@@ -100,7 +94,7 @@ def get_discussion_category_map(course):
def filter_unstarted_categories(category_map): def filter_unstarted_categories(category_map):
now = time.gmtime() now = datetime.now(UTC())
result_map = {} result_map = {}
...@@ -220,12 +214,12 @@ def initialize_discussion_info(course): ...@@ -220,12 +214,12 @@ def initialize_discussion_info(course):
for topic, entry in course.discussion_topics.items(): for topic, entry in course.discussion_topics.items():
category_map['entries'][topic] = {"id": entry["id"], category_map['entries'][topic] = {"id": entry["id"],
"sort_key": entry.get("sort_key", topic), "sort_key": entry.get("sort_key", topic),
"start_date": time.gmtime()} "start_date": datetime.now(UTC())}
sort_map_entries(category_map) sort_map_entries(category_map)
_DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map _DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map
_DISCUSSIONINFO[course.id]['category_map'] = category_map _DISCUSSIONINFO[course.id]['category_map'] = category_map
_DISCUSSIONINFO[course.id]['timestamp'] = datetime.now() _DISCUSSIONINFO[course.id]['timestamp'] = datetime.now(UTC())
class JsonResponse(HttpResponse): class JsonResponse(HttpResponse):
...@@ -292,7 +286,7 @@ def get_ability(course_id, content, user): ...@@ -292,7 +286,7 @@ def get_ability(course_id, content, user):
'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if content['type'] == 'thread' else "vote_for_comment"), 'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if content['type'] == 'thread' else "vote_for_comment"),
} }
#TODO: RENAME # TODO: RENAME
def get_annotated_content_info(course_id, content, user, user_info): def get_annotated_content_info(course_id, content, user, user_info):
...@@ -310,7 +304,7 @@ def get_annotated_content_info(course_id, content, user, user_info): ...@@ -310,7 +304,7 @@ def get_annotated_content_info(course_id, content, user, user_info):
'ability': get_ability(course_id, content, user), 'ability': get_ability(course_id, content, user),
} }
#TODO: RENAME # TODO: RENAME
def get_annotated_content_infos(course_id, thread, user, user_info): def get_annotated_content_infos(course_id, thread, user, user_info):
......
...@@ -13,6 +13,7 @@ from foldit.models import PuzzleComplete, Score ...@@ -13,6 +13,7 @@ from foldit.models import PuzzleComplete, Score
from student.models import UserProfile, unique_id_for_user from student.models import UserProfile, unique_id_for_user
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pytz import UTC
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -28,7 +29,7 @@ class FolditTestCase(TestCase): ...@@ -28,7 +29,7 @@ class FolditTestCase(TestCase):
self.user2 = User.objects.create_user('testuser2', 'test2@test.com', pwd) self.user2 = User.objects.create_user('testuser2', 'test2@test.com', pwd)
self.unique_user_id = unique_id_for_user(self.user) self.unique_user_id = unique_id_for_user(self.user)
self.unique_user_id2 = unique_id_for_user(self.user2) self.unique_user_id2 = unique_id_for_user(self.user2)
now = datetime.now() now = datetime.now(UTC)
self.tomorrow = now + timedelta(days=1) self.tomorrow = now + timedelta(days=1)
self.yesterday = now - timedelta(days=1) self.yesterday = now - timedelta(days=1)
...@@ -222,7 +223,7 @@ class FolditTestCase(TestCase): ...@@ -222,7 +223,7 @@ class FolditTestCase(TestCase):
verify = {"Verify": verify_code(self.user.email, puzzles_str), verify = {"Verify": verify_code(self.user.email, puzzles_str),
"VerifyMethod":"FoldItVerify"} "VerifyMethod":"FoldItVerify"}
data = {'SetPuzzlesCompleteVerify': json.dumps(verify), data = {'SetPuzzlesCompleteVerify': json.dumps(verify),
'SetPuzzlesComplete': puzzles_str} 'SetPuzzlesComplete': puzzles_str}
request = self.make_request(data) request = self.make_request(data)
......
'''
Unit tests for enrollment methods in views.py
'''
from django.test.utils import override_settings
from django.contrib.auth.models import Group, User
from django.core.urlresolvers import reverse
from courseware.access import _course_staff_group_name
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
from student.models import CourseEnrollment, CourseEnrollmentAllowed
from instructor.views import get_and_clean_student_list
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestInstructorEnrollsStudent(LoginEnrollmentTestCase):
'''
Check Enrollment/Unenrollment with/without auto-enrollment on activation
'''
def setUp(self):
self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
self.toy = modulestore().get_course("edX/toy/2012_Fall")
#Create instructor and student accounts
self.instructor = 'instructor1@test.com'
self.student1 = 'student1@test.com'
self.student2 = 'student2@test.com'
self.password = 'foo'
self.create_account('it1', self.instructor, self.password)
self.create_account('st1', self.student1, self.password)
self.create_account('st2', self.student2, self.password)
self.activate_user(self.instructor)
self.activate_user(self.student1)
self.activate_user(self.student2)
def make_instructor(course):
group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name)
g.user_set.add(get_user(self.instructor))
make_instructor(self.toy)
#Enroll Students
self.logout()
self.login(self.student1, self.password)
self.enroll(self.toy)
self.logout()
self.login(self.student2, self.password)
self.enroll(self.toy)
#Enroll Instructor
self.logout()
self.login(self.instructor, self.password)
self.enroll(self.toy)
def test_unenrollment(self):
'''
Do un-enrollment test
'''
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
response = self.client.post(url, {'action': 'Unenroll multiple students', 'multiple_students': 'student1@test.com, student2@test.com'})
#Check the page output
self.assertContains(response, '<td>student1@test.com</td>')
self.assertContains(response, '<td>student2@test.com</td>')
self.assertContains(response, '<td>un-enrolled</td>')
#Check the enrollment table
user = User.objects.get(email='student1@test.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
user = User.objects.get(email='student2@test.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
def test_enrollment_new_student_autoenroll_on(self):
'''
Do auto-enroll on test
'''
#Run the Enroll students command
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'test1_1@student.com, test1_2@student.com', 'auto_enroll': 'on'})
#Check the page output
self.assertContains(response, '<td>test1_1@student.com</td>')
self.assertContains(response, '<td>test1_2@student.com</td>')
self.assertContains(response, '<td>user does not exist, enrollment allowed, pending with auto enrollment on</td>')
#Check the enrollmentallowed db entries
cea = CourseEnrollmentAllowed.objects.filter(email='test1_1@student.com', course_id=course.id)
self.assertEqual(1, cea[0].auto_enroll)
cea = CourseEnrollmentAllowed.objects.filter(email='test1_2@student.com', course_id=course.id)
self.assertEqual(1, cea[0].auto_enroll)
#Check there is no enrollment db entry other than for the setup instructor and students
ce = CourseEnrollment.objects.filter(course_id=course.id)
self.assertEqual(3, len(ce))
#Create and activate student accounts with same email
self.student1 = 'test1_1@student.com'
self.password = 'bar'
self.create_account('s1_1', self.student1, self.password)
self.activate_user(self.student1)
self.student2 = 'test1_2@student.com'
self.create_account('s1_2', self.student2, self.password)
self.activate_user(self.student2)
#Check students are enrolled
user = User.objects.get(email='test1_1@student.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(1, len(ce))
user = User.objects.get(email='test1_2@student.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(1, len(ce))
def test_enrollmemt_new_student_autoenroll_off(self):
'''
Do auto-enroll off test
'''
#Run the Enroll students command
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'test2_1@student.com, test2_2@student.com'})
#Check the page output
self.assertContains(response, '<td>test2_1@student.com</td>')
self.assertContains(response, '<td>test2_2@student.com</td>')
self.assertContains(response, '<td>user does not exist, enrollment allowed, pending with auto enrollment off</td>')
#Check the enrollmentallowed db entries
cea = CourseEnrollmentAllowed.objects.filter(email='test2_1@student.com', course_id=course.id)
self.assertEqual(0, cea[0].auto_enroll)
cea = CourseEnrollmentAllowed.objects.filter(email='test2_2@student.com', course_id=course.id)
self.assertEqual(0, cea[0].auto_enroll)
#Check there is no enrollment db entry other than for the setup instructor and students
ce = CourseEnrollment.objects.filter(course_id=course.id)
self.assertEqual(3, len(ce))
#Create and activate student accounts with same email
self.student = 'test2_1@student.com'
self.password = 'bar'
self.create_account('s2_1', self.student, self.password)
self.activate_user(self.student)
self.student = 'test2_2@student.com'
self.create_account('s2_2', self.student, self.password)
self.activate_user(self.student)
#Check students are not enrolled
user = User.objects.get(email='test2_1@student.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
user = User.objects.get(email='test2_2@student.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
def test_get_and_clean_student_list(self):
'''
Clean user input test
'''
string = "abc@test.com, def@test.com ghi@test.com \n \n jkl@test.com "
cleaned_string, cleaned_string_lc = get_and_clean_student_list(string)
self.assertEqual(cleaned_string, ['abc@test.com', 'def@test.com', 'ghi@test.com', 'jkl@test.com'])
from django.db import models
class JabberUser(models.Model):
class Meta:
app_label = 'jabber'
db_table = 'users'
# This is the primary key for our table, since ejabberd doesn't
# put an ID column on this table. This will match the edX
# username chosen by the user.
username = models.CharField(max_length=255, db_index=True, primary_key=True)
# Yes, this is stored in plaintext. ejabberd only knows how to do
# basic string matching, so we don't hash/salt this or anything.
password = models.TextField(default="")
created_at = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
class JabberRouter(object):
"""
A router to control all database operations on models in the
Jabber application.
"""
def db_for_read(self, model, **hints):
"""
Attempts to read Jabber models go to the Jabber DB.
"""
if model._meta.app_label == 'jabber':
return 'jabber'
return None
def db_for_write(self, model, **hints):
"""
Attempts to write Jabber models go to the Jabber DB.
"""
if model._meta.app_label == 'jabber':
return 'jabber'
return None
def allow_relation(self, obj1, obj2, **hints):
"""
Allow relations if a model in the Jabber app is involved.
"""
if obj1._meta.app_label == 'jabber' or \
obj2._meta.app_label == 'jabber':
return True
return None
def allow_syncdb(self, db, model):
"""
Make sure the Jabber app only appears in the 'jabber'
database.
"""
if db == 'jabber':
return model._meta.app_label == 'jabber'
elif model._meta.app_label == 'jabber':
return False
return None
from django.test import TestCase
from django.test.utils import override_settings
from django.conf import settings
from mock import patch
from nose.plugins.skip import SkipTest
import jabber.utils
class JabberSettingsTests(TestCase):
@override_settings()
def test_valid_settings(self):
pass
def test_missing_settings(self):
pass
class UtilsTests(TestCase):
def test_get_bosh_url(self):
# USE_SSL present (True/False) and absent
# HOST present (something/empty) and absent
# PORT present (int/str) and absent
# PATH present (something/empty) and absent
pass
def test_get_password_for_user(self):
# Test JabberUser present/absent
pass
def test_get_room_name_for_course(self):
# HOST present (something/empty) and absent
# Test course_id parsing
pass
"""
Various utilities for working with Jabber chat. Includes helper
functions to parse the settings, create and retrieve chat-specific
passwords for users, etc.
"""
import base64
import os
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from .models import JabberUser
# The default length of the Jabber passwords we create. We set a
# really long default since we're storing these passwords in
# plaintext (ejabberd implementation detail).
DEFAULT_PASSWORD_LENGTH = 256
def get_bosh_url():
"""
Build a "Bidirectional-streams Over Synchronous HTTP" (BOSH) URL
for connecting to a Jabber server. It has the following format:
<protocol>://<host>:<port>/<path>
The Candy.js widget connects to ejabberd at this endpoint.
By default, the BOSH URL uses HTTP, not HTTPS. The port and the
path are optional; only the host is required.
"""
__validate_settings()
protocol = "http"
use_ssl = settings.JABBER.get("USE_SSL", False)
if use_ssl:
protocol = "https"
host = settings.JABBER.get("HOST")
bosh_url = "%s://%s" % (protocol, host)
# The port is an optional setting
port = settings.JABBER.get("PORT")
if port is not None:
# Convert port to a string in case it's specified as a number
bosh_url += ":%s" % str(port)
# Also optional is the "path", which could possibly use a better
# name...help @jrbl?
path = settings.JABBER.get("PATH")
if path is not None:
bosh_url += "/%s" % path
return bosh_url
def get_password_for_user(username):
"""
Retrieve the password for the user with the given username. If
a password doesn't exist, then we'll create one by generating a
random string.
"""
try:
jabber_user = JabberUser.objects.get(username=username)
except JabberUser.DoesNotExist:
password = __generate_random_string(DEFAULT_PASSWORD_LENGTH)
jabber_user = JabberUser(username=username,
password=password)
jabber_user.save()
return jabber_user.password
def get_room_name_for_course(course_id):
"""
Build a Jabber chat room name given a course ID with format:
<room>@<domain>
The room name will just be the course name (parsed from the
course_id), and the domain will be the Jabber host with the
optional multi-user chat (MUC) subdomain.
"""
__validate_settings()
host = settings.JABBER.get("HOST")
# The "multi-user chat" subdomain is a convention in Jabber to
# keep chatroom traffic from blowing up your one-to-one traffic.
# This is an optional setting.
muc_subdomain = settings.JABBER.get("MUC_SUBDOMAIN")
if muc_subdomain is not None:
host = "%s.%s" % (muc_subdomain, host)
# Rather than using the whole course ID, which is rather ugly for
# display, we'll just grab the name portion.
# TODO: is there a better way to just grab the name out?
org, num, name = course_id.split('/')
return "%s_class@%s" % (name, host)
def __generate_random_string(length):
"""
Generate a Base64-encoded random string of the specified length,
suitable for a password that can be stored in a database.
"""
# Base64 encoding gives us 4 chars for every 3 bytes we give it,
# so figure out how many random bytes we need to get a string of
# just the right length
num_bytes = length / 4 * 3
return base64.b64encode(os.urandom(num_bytes))
def __validate_settings():
"""
Ensure that the Jabber settings are properly configured. This
is intended for internal use only to prevent code duplication.
"""
if getattr(settings, "JABBER") is None:
raise ImproperlyConfigured("Missing Jabber dict in settings")
host = settings.JABBER.get("HOST")
if host is None or host == "":
raise ImproperlyConfigured("Missing Jabber HOST in settings")
...@@ -18,6 +18,7 @@ from django.core.management.base import BaseCommand ...@@ -18,6 +18,7 @@ from django.core.management.base import BaseCommand
from student.models import UserProfile, Registration from student.models import UserProfile, Registration
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from pytz import UTC
class MyCompleter(object): # Custom completer class MyCompleter(object): # Custom completer
...@@ -124,7 +125,7 @@ class Command(BaseCommand): ...@@ -124,7 +125,7 @@ class Command(BaseCommand):
external_credentials=json.dumps(credentials), external_credentials=json.dumps(credentials),
) )
eamap.user = user eamap.user = user
eamap.dtsignup = datetime.datetime.now() eamap.dtsignup = datetime.datetime.now(UTC)
eamap.save() eamap.save()
print "User %s created successfully!" % user print "User %s created successfully!" % user
......
...@@ -15,6 +15,7 @@ from scipy.optimize import curve_fit ...@@ -15,6 +15,7 @@ from scipy.optimize import curve_fit
from django.conf import settings from django.conf import settings
from django.db.models import Sum, Max from django.db.models import Sum, Max
from psychometrics.models import * from psychometrics.models import *
from pytz import UTC
log = logging.getLogger("mitx.psychometrics") log = logging.getLogger("mitx.psychometrics")
...@@ -110,7 +111,7 @@ def make_histogram(ydata, bins=None): ...@@ -110,7 +111,7 @@ def make_histogram(ydata, bins=None):
nbins = len(bins) nbins = len(bins)
hist = dict(zip(bins, [0] * nbins)) hist = dict(zip(bins, [0] * nbins))
for y in ydata: for y in ydata:
for b in bins[::-1]: # in reverse order for b in bins[::-1]: # in reverse order
if y > b: if y > b:
hist[b] += 1 hist[b] += 1
break break
...@@ -149,7 +150,7 @@ def generate_plots_for_problem(problem): ...@@ -149,7 +150,7 @@ def generate_plots_for_problem(problem):
agdat = pmdset.aggregate(Sum('attempts'), Max('attempts')) agdat = pmdset.aggregate(Sum('attempts'), Max('attempts'))
max_attempts = agdat['attempts__max'] max_attempts = agdat['attempts__max']
total_attempts = agdat['attempts__sum'] # not used yet total_attempts = agdat['attempts__sum'] # not used yet
msg += "max attempts = %d" % max_attempts msg += "max attempts = %d" % max_attempts
...@@ -200,7 +201,7 @@ def generate_plots_for_problem(problem): ...@@ -200,7 +201,7 @@ def generate_plots_for_problem(problem):
dtsv = StatVar() dtsv = StatVar()
for pmd in pmdset: for pmd in pmdset:
try: try:
checktimes = eval(pmd.checktimes) # update log of attempt timestamps checktimes = eval(pmd.checktimes) # update log of attempt timestamps
except: except:
continue continue
if len(checktimes) < 2: if len(checktimes) < 2:
...@@ -208,7 +209,7 @@ def generate_plots_for_problem(problem): ...@@ -208,7 +209,7 @@ def generate_plots_for_problem(problem):
ct0 = checktimes[0] ct0 = checktimes[0]
for ct in checktimes[1:]: for ct in checktimes[1:]:
dt = (ct - ct0).total_seconds() / 60.0 dt = (ct - ct0).total_seconds() / 60.0
if dt < 20: # ignore if dt too long if dt < 20: # ignore if dt too long
dtset.append(dt) dtset.append(dt)
dtsv += dt dtsv += dt
ct0 = ct ct0 = ct
...@@ -244,7 +245,7 @@ def generate_plots_for_problem(problem): ...@@ -244,7 +245,7 @@ def generate_plots_for_problem(problem):
ylast = y + ylast ylast = y + ylast
yset['ydat'] = ydat yset['ydat'] = ydat
if len(ydat) > 3: # try to fit to logistic function if enough data points if len(ydat) > 3: # try to fit to logistic function if enough data points
try: try:
cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0]) cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0])
yset['fitparam'] = cfp yset['fitparam'] = cfp
...@@ -337,10 +338,10 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key): ...@@ -337,10 +338,10 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key):
log.exception("no attempts for %s (state=%s)" % (sm, sm.state)) log.exception("no attempts for %s (state=%s)" % (sm, sm.state))
try: try:
checktimes = eval(pmd.checktimes) # update log of attempt timestamps checktimes = eval(pmd.checktimes) # update log of attempt timestamps
except: except:
checktimes = [] checktimes = []
checktimes.append(datetime.datetime.now()) checktimes.append(datetime.datetime.now(UTC))
pmd.checktimes = checktimes pmd.checktimes = checktimes
try: try:
pmd.save() pmd.save()
......
...@@ -11,6 +11,7 @@ from markdown import markdown ...@@ -11,6 +11,7 @@ from markdown import markdown
from .wiki_settings import * from .wiki_settings import *
from util.cache import cache from util.cache import cache
from pytz import UTC
class ShouldHaveExactlyOneRootSlug(Exception): class ShouldHaveExactlyOneRootSlug(Exception):
...@@ -265,7 +266,7 @@ class Revision(models.Model): ...@@ -265,7 +266,7 @@ class Revision(models.Model):
return return
else: else:
import datetime import datetime
self.article.modified_on = datetime.datetime.now() self.article.modified_on = datetime.datetime.now(UTC)
self.article.save() self.article.save()
# Increment counter according to previous revision # Increment counter according to previous revision
......
...@@ -24,7 +24,7 @@ modulestore_options = { ...@@ -24,7 +24,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 = {
......
...@@ -21,7 +21,7 @@ modulestore_options = { ...@@ -21,7 +21,7 @@ modulestore_options = {
'db': 'xmodule', 'db': 'xmodule',
'collection': 'modulestore', 'collection': 'modulestore',
'fs_root': DATA_DIR, 'fs_root': DATA_DIR,
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string'
} }
MODULESTORE = { MODULESTORE = {
......
...@@ -50,8 +50,8 @@ MITX_FEATURES = { ...@@ -50,8 +50,8 @@ MITX_FEATURES = {
'SAMPLE': False, 'SAMPLE': False,
'USE_DJANGO_PIPELINE': True, 'USE_DJANGO_PIPELINE': True,
'DISPLAY_HISTOGRAMS_TO_STAFF': True, 'DISPLAY_HISTOGRAMS_TO_STAFF': True,
'REROUTE_ACTIVATION_EMAIL': False, # nonempty string = address for all activation emails 'REROUTE_ACTIVATION_EMAIL': False, # nonempty string = address for all activation emails
'DEBUG_LEVEL': 0, # 0 = lowest level, least verbose, 255 = max level, most verbose 'DEBUG_LEVEL': 0, # 0 = lowest level, least verbose, 255 = max level, most verbose
## DO NOT SET TO True IN THIS FILE ## DO NOT SET TO True IN THIS FILE
## Doing so will cause all courses to be released on production ## Doing so will cause all courses to be released on production
...@@ -67,13 +67,13 @@ MITX_FEATURES = { ...@@ -67,13 +67,13 @@ MITX_FEATURES = {
# university to use for branding purposes # university to use for branding purposes
'SUBDOMAIN_BRANDING': False, 'SUBDOMAIN_BRANDING': False,
'FORCE_UNIVERSITY_DOMAIN': False, # set this to the university domain to use, as an override to HTTP_HOST 'FORCE_UNIVERSITY_DOMAIN': False, # set this to the university domain to use, as an override to HTTP_HOST
# set to None to do no university selection # set to None to do no university selection
'ENABLE_TEXTBOOK': True, 'ENABLE_TEXTBOOK': True,
'ENABLE_DISCUSSION_SERVICE': True, 'ENABLE_DISCUSSION_SERVICE': True,
'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard) 'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard)
'ENABLE_DJANGO_ADMIN_SITE': False, # set true to enable django's admin site, even on prod (e.g. for course ops) 'ENABLE_DJANGO_ADMIN_SITE': False, # set true to enable django's admin site, even on prod (e.g. for course ops)
'ENABLE_SQL_TRACKING_LOGS': False, 'ENABLE_SQL_TRACKING_LOGS': False,
...@@ -84,7 +84,7 @@ MITX_FEATURES = { ...@@ -84,7 +84,7 @@ MITX_FEATURES = {
'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL 'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL
'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
# extrernal access methods # extrernal access methods
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False, 'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
...@@ -122,7 +122,11 @@ MITX_FEATURES = { ...@@ -122,7 +122,11 @@ MITX_FEATURES = {
'USE_CUSTOM_THEME': False, 'USE_CUSTOM_THEME': False,
# Do autoplay videos for students # Do autoplay videos for students
'AUTOPLAY_VIDEOS': True 'AUTOPLAY_VIDEOS': True,
# Toggle to enable chat availability (configured on a per-course
# basis in Studio)
'ENABLE_CHAT': False
} }
# Used for A/B testing # Used for A/B testing
...@@ -132,7 +136,7 @@ DEFAULT_GROUPS = [] ...@@ -132,7 +136,7 @@ DEFAULT_GROUPS = []
GENERATE_PROFILE_SCORES = False GENERATE_PROFILE_SCORES = False
# Used with XQueue # Used with XQueue
XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
############################# SET PATH INFORMATION ############################# ############################# SET PATH INFORMATION #############################
...@@ -192,8 +196,8 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -192,8 +196,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.static', 'django.core.context_processors.static',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
#'django.core.context_processors.i18n', #'django.core.context_processors.i18n',
'django.contrib.auth.context_processors.auth', # this is required for admin 'django.contrib.auth.context_processors.auth', # this is required for admin
'django.core.context_processors.csrf', # necessary for csrf protection 'django.core.context_processors.csrf', # necessary for csrf protection
# Added for django-wiki # Added for django-wiki
'django.core.context_processors.media', 'django.core.context_processors.media',
...@@ -209,7 +213,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -209,7 +213,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'settings_context_processor.context_processors.settings' 'settings_context_processor.context_processors.settings'
) )
STUDENT_FILEUPLOAD_MAX_SIZE = 4 * 1000 * 1000 # 4 MB STUDENT_FILEUPLOAD_MAX_SIZE = 4 * 1000 * 1000 # 4 MB
MAX_FILEUPLOADS_PER_INPUT = 20 MAX_FILEUPLOADS_PER_INPUT = 20
# FIXME: # FIXME:
...@@ -219,7 +223,7 @@ LIB_URL = '/static/js/' ...@@ -219,7 +223,7 @@ LIB_URL = '/static/js/'
# Dev machines shouldn't need the book # Dev machines shouldn't need the book
# BOOK_URL = '/static/book/' # BOOK_URL = '/static/book/'
BOOK_URL = 'https://mitxstatic.s3.amazonaws.com/book_images/' # For AWS deploys BOOK_URL = 'https://mitxstatic.s3.amazonaws.com/book_images/' # For AWS deploys
# RSS_URL = r'lms/templates/feed.rss' # RSS_URL = r'lms/templates/feed.rss'
# PRESS_URL = r'' # PRESS_URL = r''
RSS_TIMEOUT = 600 RSS_TIMEOUT = 600
...@@ -243,14 +247,14 @@ COURSE_TITLE = "Circuits and Electronics" ...@@ -243,14 +247,14 @@ COURSE_TITLE = "Circuits and Electronics"
### Dark code. Should be enabled in local settings for devel. ### Dark code. Should be enabled in local settings for devel.
ENABLE_MULTICOURSE = False # set to False to disable multicourse display (see lib.util.views.mitxhome) ENABLE_MULTICOURSE = False # set to False to disable multicourse display (see lib.util.views.mitxhome)
WIKI_ENABLED = False WIKI_ENABLED = False
### ###
COURSE_DEFAULT = '6.002x_Fall_2012' COURSE_DEFAULT = '6.002x_Fall_2012'
COURSE_SETTINGS = {'6.002x_Fall_2012': {'number': '6.002x', COURSE_SETTINGS = {'6.002x_Fall_2012': {'number': '6.002x',
'title': 'Circuits and Electronics', 'title': 'Circuits and Electronics',
'xmlpath': '6002x/', 'xmlpath': '6002x/',
'location': 'i4x://edx/6002xs12/course/6.002x_Fall_2012', 'location': 'i4x://edx/6002xs12/course/6.002x_Fall_2012',
...@@ -311,6 +315,7 @@ import monitoring.exceptions # noqa ...@@ -311,6 +315,7 @@ import monitoring.exceptions # noqa
# Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here # Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here
DEBUG = False DEBUG = False
TEMPLATE_DEBUG = False TEMPLATE_DEBUG = False
USE_TZ = True
# Site info # Site info
SITE_ID = 1 SITE_ID = 1
...@@ -345,8 +350,8 @@ STATICFILES_DIRS = [ ...@@ -345,8 +350,8 @@ STATICFILES_DIRS = [
FAVICON_PATH = 'images/favicon.ico' FAVICON_PATH = 'images/favicon.ico'
# 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
...@@ -367,7 +372,7 @@ ALLOWED_GITRELOAD_IPS = ['207.97.227.253', '50.57.128.197', '108.171.174.178'] ...@@ -367,7 +372,7 @@ ALLOWED_GITRELOAD_IPS = ['207.97.227.253', '50.57.128.197', '108.171.174.178']
# setting is, I'm just bumping the expiration time to something absurd (100 # setting is, I'm just bumping the expiration time to something absurd (100
# years). This is only used if DEFAULT_FILE_STORAGE is overriden to use S3 # years). This is only used if DEFAULT_FILE_STORAGE is overriden to use S3
# in the global settings.py # in the global settings.py
AWS_QUERYSTRING_EXPIRE = 10 * 365 * 24 * 60 * 60 # 10 years AWS_QUERYSTRING_EXPIRE = 10 * 365 * 24 * 60 * 60 # 10 years
################################# SIMPLEWIKI ################################### ################################# SIMPLEWIKI ###################################
SIMPLE_WIKI_REQUIRE_LOGIN_EDIT = True SIMPLE_WIKI_REQUIRE_LOGIN_EDIT = True
...@@ -376,8 +381,8 @@ SIMPLE_WIKI_REQUIRE_LOGIN_VIEW = False ...@@ -376,8 +381,8 @@ SIMPLE_WIKI_REQUIRE_LOGIN_VIEW = False
################################# WIKI ################################### ################################# WIKI ###################################
WIKI_ACCOUNT_HANDLING = False WIKI_ACCOUNT_HANDLING = False
WIKI_EDITOR = 'course_wiki.editors.CodeMirror' WIKI_EDITOR = 'course_wiki.editors.CodeMirror'
WIKI_SHOW_MAX_CHILDREN = 0 # We don't use the little menu that shows children of an article in the breadcrumb WIKI_SHOW_MAX_CHILDREN = 0 # We don't use the little menu that shows children of an article in the breadcrumb
WIKI_ANONYMOUS = False # Don't allow anonymous access until the styling is figured out WIKI_ANONYMOUS = False # Don't allow anonymous access until the styling is figured out
WIKI_CAN_CHANGE_PERMISSIONS = lambda article, user: user.is_staff or user.is_superuser WIKI_CAN_CHANGE_PERMISSIONS = lambda article, user: user.is_staff or user.is_superuser
WIKI_CAN_ASSIGN = lambda article, user: user.is_staff or user.is_superuser WIKI_CAN_ASSIGN = lambda article, user: user.is_staff or user.is_superuser
...@@ -595,7 +600,7 @@ if os.path.isdir(DATA_DIR): ...@@ -595,7 +600,7 @@ if os.path.isdir(DATA_DIR):
new_filename = os.path.splitext(filename)[0] + ".js" new_filename = os.path.splitext(filename)[0] + ".js"
if os.path.exists(js_dir / new_filename): if os.path.exists(js_dir / new_filename):
coffee_timestamp = os.stat(js_dir / filename).st_mtime coffee_timestamp = os.stat(js_dir / filename).st_mtime
js_timestamp = os.stat(js_dir / new_filename).st_mtime js_timestamp = os.stat(js_dir / new_filename).st_mtime
if coffee_timestamp <= js_timestamp: if coffee_timestamp <= js_timestamp:
continue continue
os.system("rm %s" % (js_dir / new_filename)) os.system("rm %s" % (js_dir / new_filename))
...@@ -700,9 +705,9 @@ INSTALLED_APPS = ( ...@@ -700,9 +705,9 @@ INSTALLED_APPS = (
'course_groups', 'course_groups',
#For the wiki #For the wiki
'wiki', # The new django-wiki from benjaoming 'wiki', # The new django-wiki from benjaoming
'django_notify', 'django_notify',
'course_wiki', # Our customizations 'course_wiki', # Our customizations
'mptt', 'mptt',
'sekizai', 'sekizai',
#'wiki.plugins.attachments', #'wiki.plugins.attachments',
...@@ -714,7 +719,7 @@ INSTALLED_APPS = ( ...@@ -714,7 +719,7 @@ INSTALLED_APPS = (
'foldit', 'foldit',
# For testing # For testing
'django.contrib.admin', # only used in DEBUG mode 'django.contrib.admin', # only used in DEBUG mode
'debug', 'debug',
# Discussion forums # Discussion forums
......
...@@ -19,7 +19,7 @@ MODULESTORE = { ...@@ -19,7 +19,7 @@ MODULESTORE = {
'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'
} }
} }
} }
This source diff could not be displayed because it is too large. You can view the blob instead.
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