Merge pull request #1798 from MITx/bug/christina/studio
Fix due date bug.
This commit is contained in:
@@ -9,6 +9,9 @@ from xmodule.modulestore.django import _MODULESTORES, modulestore
|
||||
from xmodule.templates import update_templates
|
||||
from auth.authz import get_user_by_email
|
||||
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
import time
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
@@ -140,3 +143,14 @@ def add_subsection(name='Subsection One'):
|
||||
save_css = 'input.new-subsection-name-save'
|
||||
world.css_fill(name_css, name)
|
||||
world.css_click(save_css)
|
||||
|
||||
|
||||
def set_date_and_time(date_css, desired_date, time_css, desired_time):
|
||||
world.css_fill(date_css, desired_date)
|
||||
# hit TAB to get to the time field
|
||||
e = world.css_find(date_css).first
|
||||
e._element.send_keys(Keys.TAB)
|
||||
world.css_fill(time_css, desired_time)
|
||||
e = world.css_find(time_css).first
|
||||
e._element.send_keys(Keys.TAB)
|
||||
time.sleep(float(1))
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_equal
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
import time
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
@@ -39,16 +37,8 @@ def i_click_the_edit_link_for_the_release_date(step):
|
||||
|
||||
@step('I save a new section release date$')
|
||||
def i_save_a_new_section_release_date(step):
|
||||
date_css = 'input.start-date.date.hasDatepicker'
|
||||
time_css = 'input.start-time.time.ui-timepicker-input'
|
||||
world.css_fill(date_css, '12/25/2013')
|
||||
# hit TAB to get to the time field
|
||||
e = world.css_find(date_css).first
|
||||
e._element.send_keys(Keys.TAB)
|
||||
world.css_fill(time_css, '12:00am')
|
||||
e = world.css_find(time_css).first
|
||||
e._element.send_keys(Keys.TAB)
|
||||
time.sleep(float(1))
|
||||
set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013',
|
||||
'input.start-time.time.ui-timepicker-input', '12:00am')
|
||||
world.browser.click_link_by_text('Save')
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,13 @@ Feature: Create Subsection
|
||||
And I reload the page
|
||||
Then I see it marked as Homework
|
||||
|
||||
Scenario: Set a due date in a different year (bug #256)
|
||||
Given I have opened a new subsection in Studio
|
||||
And I have set a release date and due date in different years
|
||||
Then I see the correct dates
|
||||
And I reload the page
|
||||
Then I see the correct dates
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Delete a subsection
|
||||
Given I have opened a new course section in Studio
|
||||
@@ -33,3 +40,5 @@ Feature: Create Subsection
|
||||
When I press the "subsection" delete icon
|
||||
And I confirm the alert
|
||||
Then the subsection does not exist
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,18 @@ def i_have_opened_a_new_course_section(step):
|
||||
add_section()
|
||||
|
||||
|
||||
@step('I have added a new subsection$')
|
||||
def i_have_added_a_new_subsection(step):
|
||||
add_subsection()
|
||||
|
||||
|
||||
@step('I have opened a new subsection in Studio$')
|
||||
def i_have_opened_a_new_subsection(step):
|
||||
step.given('I have opened a new course section in Studio')
|
||||
step.given('I have added a new subsection')
|
||||
world.css_click('span.subsection-name-value')
|
||||
|
||||
|
||||
@step('I click the New Subsection link')
|
||||
def i_click_the_new_subsection_link(step):
|
||||
world.css_click('a.new-subsection-item')
|
||||
@@ -43,9 +55,20 @@ def i_see_complete_subsection_name_with_quote_in_editor(step):
|
||||
assert_equal(world.css_find(css).value, 'Subsection With "Quote"')
|
||||
|
||||
|
||||
@step('I have added a new subsection$')
|
||||
def i_have_added_a_new_subsection(step):
|
||||
add_subsection()
|
||||
@step('I have set a release date and due date in different years$')
|
||||
def test_have_set_dates_in_different_years(step):
|
||||
set_date_and_time('input#start_date', '12/25/2011', 'input#start_time', '3:00am')
|
||||
world.css_click('.set-date')
|
||||
# Use a year in the past so that current year will always be different.
|
||||
set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '4:00am')
|
||||
|
||||
|
||||
@step('I see the correct dates$')
|
||||
def i_see_the_correct_dates(step):
|
||||
assert_equal('12/25/2011', world.css_find('input#start_date').first.value)
|
||||
assert_equal('3:00am', world.css_find('input#start_time').first.value)
|
||||
assert_equal('01/02/2012', world.css_find('input#due_date').first.value)
|
||||
assert_equal('4:00am', world.css_find('input#due_time').first.value)
|
||||
|
||||
|
||||
@step('I mark it as Homework$')
|
||||
|
||||
@@ -151,10 +151,6 @@ def compute_unit_state(unit):
|
||||
return UnitState.public
|
||||
|
||||
|
||||
def get_date_display(date):
|
||||
return date.strftime("%d %B, %Y at %I:%M %p")
|
||||
|
||||
|
||||
def update_item(location, value):
|
||||
"""
|
||||
If value is None, delete the db entry. Otherwise, update it using the correct modulestore.
|
||||
|
||||
@@ -6,7 +6,6 @@ import sys
|
||||
import time
|
||||
import tarfile
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
from uuid import uuid4
|
||||
from path import path
|
||||
@@ -47,12 +46,13 @@ from functools import partial
|
||||
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.util.date_utils import get_default_time_display
|
||||
|
||||
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
|
||||
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
|
||||
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
|
||||
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \
|
||||
get_date_display, UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \
|
||||
UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \
|
||||
remove_open_ended_panel_tab
|
||||
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
@@ -365,7 +365,7 @@ def edit_unit(request, location):
|
||||
'draft_preview_link': preview_lms_link,
|
||||
'published_preview_link': lms_link,
|
||||
'subsection': containing_subsection,
|
||||
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.lms.start))) if containing_subsection.lms.start is not None else None,
|
||||
'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
|
||||
'section': containing_section,
|
||||
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
||||
'unit_state': unit_state,
|
||||
@@ -828,7 +828,7 @@ def upload_asset(request, org, course, coursename):
|
||||
readback = contentstore().find(content.location)
|
||||
|
||||
response_payload = {'displayname': content.name,
|
||||
'uploadDate': get_date_display(readback.last_modified_at),
|
||||
'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
|
||||
'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,
|
||||
'msg': 'Upload completed'
|
||||
@@ -1433,7 +1433,7 @@ def asset_index(request, org, course, name):
|
||||
id = asset['_id']
|
||||
display_info = {}
|
||||
display_info['displayname'] = asset['displayname']
|
||||
display_info['uploadDate'] = get_date_display(asset['uploadDate'])
|
||||
display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple())
|
||||
|
||||
asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
|
||||
display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
|
||||
|
||||
@@ -4,6 +4,9 @@ var $modalCover;
|
||||
var $newComponentItem;
|
||||
var $changedInput;
|
||||
var $spinner;
|
||||
var $newComponentTypePicker;
|
||||
var $newComponentTemplatePickers;
|
||||
var $newComponentButton;
|
||||
|
||||
$(document).ready(function () {
|
||||
$body = $('body');
|
||||
@@ -242,7 +245,7 @@ function syncReleaseDate(e) {
|
||||
$("#start_time").val("");
|
||||
}
|
||||
|
||||
function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
|
||||
function getEdxTimeFromDateTimeVals(date_val, time_val) {
|
||||
var edxTimeStr = null;
|
||||
|
||||
if (date_val != '') {
|
||||
@@ -251,20 +254,17 @@ function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
|
||||
|
||||
// Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing
|
||||
var date = Date.parse(date_val + " " + time_val);
|
||||
if (format == null)
|
||||
format = 'yyyy-MM-ddTHH:mm';
|
||||
|
||||
edxTimeStr = date.toString(format);
|
||||
edxTimeStr = date.toString('yyyy-MM-ddTHH:mm');
|
||||
}
|
||||
|
||||
return edxTimeStr;
|
||||
}
|
||||
|
||||
function getEdxTimeFromDateTimeInputs(date_id, time_id, format) {
|
||||
function getEdxTimeFromDateTimeInputs(date_id, time_id) {
|
||||
var input_date = $('#' + date_id).val();
|
||||
var input_time = $('#' + time_id).val();
|
||||
|
||||
return getEdxTimeFromDateTimeVals(input_date, input_time, format);
|
||||
return getEdxTimeFromDateTimeVals(input_date, input_time);
|
||||
}
|
||||
|
||||
function autosaveInput(e) {
|
||||
@@ -305,10 +305,8 @@ function saveSubsection() {
|
||||
}
|
||||
|
||||
// Piece back together the date/time UI elements into one date/time string
|
||||
// NOTE: our various "date/time" metadata elements don't always utilize the same formatting string
|
||||
// so make sure we're passing back the correct format
|
||||
metadata['start'] = getEdxTimeFromDateTimeInputs('start_date', 'start_time');
|
||||
metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time', 'MMMM dd HH:mm');
|
||||
metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time');
|
||||
|
||||
$.ajax({
|
||||
url: "/save_item",
|
||||
@@ -330,8 +328,8 @@ function saveSubsection() {
|
||||
function createNewUnit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
parent = $(this).data('parent');
|
||||
template = $(this).data('template');
|
||||
var parent = $(this).data('parent');
|
||||
var template = $(this).data('template');
|
||||
|
||||
$.post('/clone_item',
|
||||
{'parent_location': parent,
|
||||
|
||||
@@ -271,7 +271,7 @@ body.course.outline {
|
||||
|
||||
.section-published-date {
|
||||
float: right;
|
||||
width: 265px;
|
||||
width: 278px;
|
||||
margin-right: 220px;
|
||||
@include border-radius(3px);
|
||||
background: $lightGrey;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<%inherit file="base.html" />
|
||||
<%!
|
||||
from time import mktime
|
||||
import dateutil.parser
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from xmodule.util.date_utils import get_time_struct_display
|
||||
%>
|
||||
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
@@ -13,7 +11,6 @@
|
||||
|
||||
<%namespace name="units" file="widgets/units.html" />
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%namespace name='datetime' module='datetime'/>
|
||||
|
||||
<%block name="content">
|
||||
<div class="main-wrapper">
|
||||
@@ -38,18 +35,15 @@
|
||||
<div class="scheduled-date-input row">
|
||||
<label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label>
|
||||
<div class="datepair" data-language="javascript">
|
||||
<%
|
||||
start_date = datetime.fromtimestamp(mktime(subsection.lms.start)) if subsection.lms.start is not None else None
|
||||
parent_start_date = datetime.fromtimestamp(mktime(parent_item.lms.start)) if parent_item.lms.start is not None else None
|
||||
%>
|
||||
<input type="text" id="start_date" name="start_date" value="${start_date.strftime('%m/%d/%Y') if start_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
<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_time" name="start_time" value="${get_time_struct_display(subsection.lms.start, '%H:%M')}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
</div>
|
||||
% if subsection.lms.start != parent_item.lms.start and subsection.lms.start:
|
||||
% if parent_start_date is None:
|
||||
% if parent_item.lms.start is None:
|
||||
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset.
|
||||
% else:
|
||||
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
|
||||
<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 %I:%M %p')}.
|
||||
% endif
|
||||
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p>
|
||||
% endif
|
||||
@@ -66,12 +60,8 @@
|
||||
<a href="#" class="set-date">Set a due date</a>
|
||||
<div class="datepair date-setter">
|
||||
<p class="date-description">
|
||||
<%
|
||||
# due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use
|
||||
due_date = dateutil.parser.parse(subsection.lms.due) if subsection.lms.due else None
|
||||
%>
|
||||
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
<input type="text" id="due_time" name="due_time" value="${due_date.strftime('%H:%M') if due_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
<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_time" name="due_time" value="${get_time_struct_display(subsection.lms.due, '%H:%M')}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
<a href="#" class="remove-date">Remove due date</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<%inherit file="base.html" />
|
||||
<%!
|
||||
from time import mktime
|
||||
import dateutil.parser
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from xmodule.util.date_utils import get_time_struct_display
|
||||
%>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%block name="title">Course Outline</%block>
|
||||
@@ -163,11 +161,10 @@
|
||||
</h3>
|
||||
<div class="section-published-date">
|
||||
<%
|
||||
start_date = datetime.fromtimestamp(mktime(section.lms.start)) if section.lms.start is not None else None
|
||||
start_date_str = start_date.strftime('%m/%d/%Y') if start_date is not None else ''
|
||||
start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
|
||||
start_date_str = get_time_struct_display(section.lms.start, '%m/%d/%Y')
|
||||
start_time_str = get_time_struct_display(section.lms.start, '%I:%M %p')
|
||||
%>
|
||||
%if start_date is None:
|
||||
%if section.lms.start is None:
|
||||
<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>
|
||||
%else:
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import cgi
|
||||
import datetime
|
||||
import dateutil
|
||||
import dateutil.parser
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from capa.capa_problem import LoncapaProblem
|
||||
from capa.responsetypes import StudentInputError, \
|
||||
ResponseError, LoncapaProblemError
|
||||
from capa.responsetypes import StudentInputError,\
|
||||
ResponseError, LoncapaProblemError
|
||||
from capa.util import convert_files_to_filenames
|
||||
from .progress import Progress
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float
|
||||
from .fields import Timedelta
|
||||
from xblock.core import Integer, Scope, String, Boolean, Object, Float
|
||||
from .fields import Timedelta, Date
|
||||
from xmodule.util.date_utils import time_to_datetime
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
@@ -87,7 +85,7 @@ class ComplexEncoder(json.JSONEncoder):
|
||||
class CapaFields(object):
|
||||
attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state)
|
||||
max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
|
||||
due = String(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)
|
||||
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
|
||||
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
|
||||
@@ -125,10 +123,7 @@ class CapaModule(CapaFields, XModule):
|
||||
def __init__(self, system, location, descriptor, model_data):
|
||||
XModule.__init__(self, system, location, descriptor, model_data)
|
||||
|
||||
if self.due:
|
||||
due_date = dateutil.parser.parse(self.due)
|
||||
else:
|
||||
due_date = None
|
||||
due_date = time_to_datetime(self.due)
|
||||
|
||||
if self.graceperiod is not None and due_date:
|
||||
self.close_date = due_date + self.graceperiod
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
import logging
|
||||
from lxml import etree
|
||||
|
||||
@@ -6,9 +5,10 @@ from pkg_resources import resource_string
|
||||
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from .x_module import XModule
|
||||
from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, List
|
||||
from xblock.core import Integer, Scope, String, Boolean, List
|
||||
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
|
||||
from collections import namedtuple
|
||||
from .fields import Date
|
||||
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
@@ -64,7 +64,7 @@ class CombinedOpenEndedFields(object):
|
||||
scope=Scope.settings)
|
||||
skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True,
|
||||
scope=Scope.settings)
|
||||
due = String(help="Date that this problem is due by", default=None, scope=Scope.settings)
|
||||
due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings)
|
||||
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
|
||||
scope=Scope.settings)
|
||||
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
|
||||
@@ -105,10 +105,11 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
|
||||
|
||||
icon_class = 'problem'
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
|
||||
resource_string(__name__, 'js/src/collapsible.coffee'),
|
||||
resource_string(__name__, 'js/src/javascript_loader.coffee'),
|
||||
]}
|
||||
js = {'coffee':
|
||||
[resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
|
||||
resource_string(__name__, 'js/src/collapsible.coffee'),
|
||||
resource_string(__name__, 'js/src/javascript_loader.coffee'),
|
||||
]}
|
||||
js_module_name = "CombinedOpenEnded"
|
||||
|
||||
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
|
||||
@@ -219,4 +220,3 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
|
||||
stores_state = True
|
||||
has_score = True
|
||||
template_dir_name = "combinedopenended"
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from xmodule.seq_module import SequenceDescriptor, SequenceModule
|
||||
from xmodule.timeparse import parse_time
|
||||
from xmodule.util.decorators import lazyproperty
|
||||
from xmodule.graders import grader_from_conf
|
||||
from xmodule.util.date_utils import time_to_datetime
|
||||
import json
|
||||
|
||||
from xblock.core import Scope, List, String, Object, Boolean
|
||||
@@ -533,19 +534,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
def _sorting_dates(self):
|
||||
# utility function to get datetime objects for dates used to
|
||||
# compute the is_new flag and the sorting_score
|
||||
def to_datetime(timestamp):
|
||||
return datetime(*timestamp[:6])
|
||||
|
||||
announcement = self.announcement
|
||||
if announcement is not None:
|
||||
announcement = to_datetime(announcement)
|
||||
announcement = time_to_datetime(announcement)
|
||||
|
||||
try:
|
||||
start = dateutil.parser.parse(self.advertised_start)
|
||||
except (ValueError, AttributeError):
|
||||
start = to_datetime(self.start)
|
||||
start = time_to_datetime(self.start)
|
||||
|
||||
now = to_datetime(time.gmtime())
|
||||
now = datetime.utcnow()
|
||||
|
||||
return announcement, start, now
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
from lxml import etree
|
||||
from dateutil import parser
|
||||
|
||||
from pkg_resources import resource_string
|
||||
|
||||
@@ -8,6 +7,9 @@ from xmodule.editing_module import EditingDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xblock.core import Scope, Integer, String
|
||||
from .fields import Date
|
||||
from xmodule.util.date_utils import time_to_datetime
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,7 +18,7 @@ class FolditFields(object):
|
||||
# default to what Spring_7012x uses
|
||||
required_level = Integer(default=4, scope=Scope.settings)
|
||||
required_sublevel = Integer(default=5, scope=Scope.settings)
|
||||
due = String(help="Date that this problem is due by", scope=Scope.settings, default='')
|
||||
due = Date(help="Date that this problem is due by", scope=Scope.settings)
|
||||
|
||||
show_basic_score = String(scope=Scope.settings, default='false')
|
||||
show_leaderboard = String(scope=Scope.settings, default='false')
|
||||
@@ -36,17 +38,8 @@ class FolditModule(FolditFields, XModule):
|
||||
required_sublevel="3"
|
||||
show_leaderboard="false"/>
|
||||
"""
|
||||
def parse_due_date():
|
||||
"""
|
||||
Pull out the date, or None
|
||||
"""
|
||||
s = self.due
|
||||
if s:
|
||||
return parser.parse(s)
|
||||
else:
|
||||
return None
|
||||
|
||||
self.due_time = parse_due_date()
|
||||
self.due_time = time_to_datetime(self.due)
|
||||
|
||||
def is_complete(self):
|
||||
"""
|
||||
@@ -178,8 +171,8 @@ class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
return ({}, [])
|
||||
return {}, []
|
||||
|
||||
def definition_to_xml(self):
|
||||
def definition_to_xml(self, resource_fs):
|
||||
xml_object = etree.Element('foldit')
|
||||
return xml_object
|
||||
|
||||
@@ -139,11 +139,11 @@ class CombinedOpenEndedV1Module():
|
||||
self.accept_file_upload = self.instance_state.get('accept_file_upload', ACCEPT_FILE_UPLOAD) in TRUE_DICT
|
||||
self.skip_basic_checks = self.instance_state.get('skip_spelling_checks', SKIP_BASIC_CHECKS) in TRUE_DICT
|
||||
|
||||
display_due_date_string = self.instance_state.get('due', None)
|
||||
due_date = self.instance_state.get('due', None)
|
||||
|
||||
grace_period_string = self.instance_state.get('graceperiod', None)
|
||||
try:
|
||||
self.timeinfo = TimeInfo(display_due_date_string, grace_period_string)
|
||||
self.timeinfo = TimeInfo(due_date, grace_period_string)
|
||||
except:
|
||||
log.error("Error parsing due date information in location {0}".format(location))
|
||||
raise
|
||||
|
||||
@@ -6,14 +6,13 @@ from lxml import etree
|
||||
from datetime import datetime
|
||||
from pkg_resources import resource_string
|
||||
from .capa_module import ComplexEncoder
|
||||
from .stringify import stringify_children
|
||||
from .x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from .timeinfo import TimeInfo
|
||||
from xblock.core import Object, Integer, Boolean, String, Scope
|
||||
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
|
||||
from xmodule.fields import Date
|
||||
|
||||
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
|
||||
|
||||
@@ -34,7 +33,7 @@ class PeerGradingFields(object):
|
||||
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
|
||||
scope=Scope.settings)
|
||||
is_graded = Boolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
|
||||
display_due_date_string = String(help="Due date that should be displayed.", default=None, scope=Scope.settings)
|
||||
due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings)
|
||||
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
|
||||
max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
|
||||
scope=Scope.settings)
|
||||
@@ -78,7 +77,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
self._model_data['due'] = due_date
|
||||
|
||||
try:
|
||||
self.timeinfo = TimeInfo(self.display_due_date_string, self.grace_period_string)
|
||||
self.timeinfo = TimeInfo(self.due_date, self.grace_period_string)
|
||||
except:
|
||||
log.error("Error parsing due date information in location {0}".format(location))
|
||||
raise
|
||||
|
||||
26
common/lib/xmodule/xmodule/tests/test_date_utils.py
Normal file
26
common/lib/xmodule/xmodule/tests/test_date_utils.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Tests for xmodule.util.date_utils
|
||||
|
||||
from nose.tools import assert_equals
|
||||
from xmodule.util import date_utils
|
||||
import datetime
|
||||
import time
|
||||
|
||||
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():
|
||||
assert_equals("", date_utils.get_default_time_display(None))
|
||||
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0))
|
||||
assert_equals("Mar 12, 1992 at 03:03 PM",
|
||||
date_utils.get_default_time_display(test_time))
|
||||
|
||||
|
||||
def test_time_to_datetime():
|
||||
assert_equals(None, date_utils.time_to_datetime(None))
|
||||
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0))
|
||||
assert_equals(datetime.datetime(1992, 3, 12, 15, 3, 30),
|
||||
date_utils.time_to_datetime(test_time))
|
||||
@@ -1,20 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from path import path
|
||||
import unittest
|
||||
from fs.memoryfs import MemoryFS
|
||||
|
||||
from lxml import etree
|
||||
from mock import Mock, patch
|
||||
from collections import defaultdict
|
||||
|
||||
from xmodule.x_module import XMLParsingSystem, XModuleDescriptor
|
||||
from xmodule.xml_module import is_pointer_tag
|
||||
from xmodule.errortracker import make_error_tracker
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.inheritance import compute_inherited_metadata
|
||||
from xmodule.fields import Date
|
||||
|
||||
from .test_export import DATA_DIR
|
||||
|
||||
@@ -137,7 +133,7 @@ class ImportTestCase(BaseCourseTestCase):
|
||||
- inherited metadata doesn't leak to children.
|
||||
"""
|
||||
system = self.get_system()
|
||||
v = '1 hour'
|
||||
v = 'March 20 17:00'
|
||||
url_name = 'test1'
|
||||
start_xml = '''
|
||||
<course org="{org}" course="{course}"
|
||||
@@ -150,11 +146,11 @@ class ImportTestCase(BaseCourseTestCase):
|
||||
compute_inherited_metadata(descriptor)
|
||||
|
||||
print descriptor, descriptor._model_data
|
||||
self.assertEqual(descriptor.lms.due, v)
|
||||
self.assertEqual(descriptor.lms.due, Date().from_json(v))
|
||||
|
||||
# Check that the child inherits due correctly
|
||||
child = descriptor.get_children()[0]
|
||||
self.assertEqual(child.lms.due, v)
|
||||
self.assertEqual(child.lms.due, Date().from_json(v))
|
||||
|
||||
# Now export and check things
|
||||
resource_fs = MemoryFS()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import dateutil
|
||||
import dateutil.parser
|
||||
import datetime
|
||||
from .timeparse import parse_timedelta
|
||||
from xmodule.util.date_utils import time_to_datetime
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -9,7 +7,7 @@ log = logging.getLogger(__name__)
|
||||
class TimeInfo(object):
|
||||
"""
|
||||
This is a simple object that calculates and stores datetime information for an XModule
|
||||
based on the due date string and the grace period string
|
||||
based on the due date and the grace period string
|
||||
|
||||
So far it parses out three different pieces of time information:
|
||||
self.display_due_date - the 'official' due date that gets displayed to students
|
||||
@@ -17,13 +15,10 @@ class TimeInfo(object):
|
||||
self.close_date - the real due date
|
||||
|
||||
"""
|
||||
def __init__(self, display_due_date_string, grace_period_string):
|
||||
if display_due_date_string is not None:
|
||||
try:
|
||||
self.display_due_date = dateutil.parser.parse(display_due_date_string)
|
||||
except ValueError:
|
||||
log.error("Could not parse due date {0}".format(display_due_date_string))
|
||||
raise
|
||||
def __init__(self, due_date, grace_period_string):
|
||||
if due_date is not None:
|
||||
self.display_due_date = time_to_datetime(due_date)
|
||||
|
||||
else:
|
||||
self.display_due_date = None
|
||||
|
||||
|
||||
31
common/lib/xmodule/xmodule/util/date_utils.py
Normal file
31
common/lib/xmodule/xmodule/util/date_utils.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import time
|
||||
import datetime
|
||||
|
||||
|
||||
def get_default_time_display(time_struct):
|
||||
"""
|
||||
Converts a time struct to a string representation. This is the default
|
||||
representation used in Studio and LMS.
|
||||
It is of the form "Apr 09, 2013 at 04:00 PM".
|
||||
|
||||
If None is passed in, an empty string will be returned.
|
||||
"""
|
||||
return get_time_struct_display(time_struct, "%b %d, %Y at %I:%M %p")
|
||||
|
||||
|
||||
def get_time_struct_display(time_struct, format):
|
||||
"""
|
||||
Converts a time struct to a string based on the given format.
|
||||
|
||||
If None is passed in, an empty string will be returned.
|
||||
"""
|
||||
return '' if time_struct is None else time.strftime(format, time_struct)
|
||||
|
||||
|
||||
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
|
||||
@@ -124,17 +124,17 @@ class TestTOC(TestCase):
|
||||
|
||||
expected = ([{'active': True, 'sections':
|
||||
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
|
||||
'format': u'Lecture Sequence', 'due': '', 'active': False},
|
||||
'format': u'Lecture Sequence', 'due': None, 'active': False},
|
||||
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False},
|
||||
'format': '', 'due': None, 'active': False},
|
||||
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False},
|
||||
'format': '', 'due': None, 'active': False},
|
||||
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False}],
|
||||
'format': '', 'due': None, 'active': False}],
|
||||
'url_name': 'Overview', 'display_name': u'Overview'},
|
||||
{'active': False, 'sections':
|
||||
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False}],
|
||||
'format': '', 'due': None, 'active': False}],
|
||||
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
|
||||
|
||||
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, model_data_cache)
|
||||
@@ -151,17 +151,17 @@ class TestTOC(TestCase):
|
||||
|
||||
expected = ([{'active': True, 'sections':
|
||||
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
|
||||
'format': u'Lecture Sequence', 'due': '', 'active': False},
|
||||
'format': u'Lecture Sequence', 'due': None, 'active': False},
|
||||
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
|
||||
'format': '', 'due': '', 'active': True},
|
||||
'format': '', 'due': None, 'active': True},
|
||||
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False},
|
||||
'format': '', 'due': None, 'active': False},
|
||||
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False}],
|
||||
'format': '', 'due': None, 'active': False}],
|
||||
'url_name': 'Overview', 'display_name': u'Overview'},
|
||||
{'active': False, 'sections':
|
||||
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False}],
|
||||
'format': '', 'due': None, 'active': False}],
|
||||
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
|
||||
|
||||
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, section, model_data_cache)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%! from xmodule.util.date_utils import get_default_time_display %>
|
||||
|
||||
<%def name="make_chapter(chapter)">
|
||||
<div class="chapter">
|
||||
@@ -10,7 +11,7 @@
|
||||
<li class="${'active' if 'active' in section and section['active'] else ''} ${'graded' if 'graded' in section and section['graded'] else ''}">
|
||||
<a href="${reverse('courseware_section', args=[course_id, chapter['url_name'], section['url_name']])}">
|
||||
<p>${section['display_name']}</p>
|
||||
<p class="subtitle">${section['format']} ${"due " + section['due'] if 'due' in section and section['due'] != '' else ''}</p>
|
||||
<p class="subtitle">${section['format']} ${"due " + get_default_time_display(section['due']) if section.get('due') is not None else ''}</p>
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
%>
|
||||
|
||||
<%! from xmodule.util.date_utils import get_default_time_display %>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
|
||||
@@ -60,9 +62,9 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph",
|
||||
<p>
|
||||
${section['format']}
|
||||
|
||||
%if 'due' in section and section['due']!="":
|
||||
%if section.get('due') is not None:
|
||||
<em>
|
||||
due ${section['due']}
|
||||
due ${get_default_time_display(section['due'])}
|
||||
</em>
|
||||
%endif
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<%!
|
||||
from xmodule.util.date_utils import get_default_time_display
|
||||
%>
|
||||
<div class="folditbasic">
|
||||
<p><strong>Due:</strong> ${due}
|
||||
<p><strong>Due:</strong> ${get_default_time_display(due)}
|
||||
|
||||
<p>
|
||||
<strong>Status:</strong>
|
||||
|
||||
@@ -51,7 +51,7 @@ class LmsNamespace(Namespace):
|
||||
)
|
||||
|
||||
start = Date(help="Start time when this module is visible", scope=Scope.settings)
|
||||
due = String(help="Date that this problem is due by", scope=Scope.settings, default='')
|
||||
due = Date(help="Date that this problem is due by", scope=Scope.settings)
|
||||
source_file = String(help="DO NOT USE", scope=Scope.settings)
|
||||
xqa_key = String(help="DO NOT USE", scope=Scope.settings)
|
||||
ispublic = Boolean(help="Whether this course is open to the public, or only to admins", scope=Scope.settings)
|
||||
|
||||
Reference in New Issue
Block a user