From 91075eb367e46b4a6059d33609361cc587cc397e Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Thu, 3 Oct 2013 12:17:38 -0400 Subject: [PATCH 01/19] nfs and testing artifacts --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 4fd90cfe036f..a4a9c6958682 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,12 @@ *.orig *.DS_Store *.mo +*.nfs* :2e_* :2e# .AppleDouble +.noseids +.testids database.sqlite requirements/private.txt lms/envs/private.py From ebd2759d79f1639f8d60afbca55eb842b6d228c8 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Thu, 10 Oct 2013 09:52:38 -0400 Subject: [PATCH 02/19] An individual due date extension may be set for a student for a particular problem in the capa module. --- common/lib/xmodule/xmodule/capa_module.py | 10 +++- .../xmodule/combined_open_ended_module.py | 7 +++ common/lib/xmodule/xmodule/foldit_module.py | 7 +++ .../xmodule/xmodule/peer_grading_module.py | 7 +++ common/lib/xmodule/xmodule/utils.py | 7 +++ lms/djangoapps/instructor/views/extensions.py | 28 +++++++++++ lms/djangoapps/instructor/views/legacy.py | 46 +++++++++++++++++++ lms/envs/common.py | 3 ++ .../courseware/instructor_dashboard.html | 38 +++++++++++++++ lms/xmodule_namespace.py | 7 +++ 10 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 common/lib/xmodule/xmodule/utils.py create mode 100644 lms/djangoapps/instructor/views/extensions.py diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index dbd535a471be..c16235641535 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -21,6 +21,7 @@ from xblock.core import Scope, String, Boolean, Dict, Integer, Float from .fields import Timedelta, Date from django.utils.timezone import UTC +from .utils import get_extended_due_date log = logging.getLogger("mitx.courseware") @@ -94,6 +95,13 @@ class CapaFields(object): values={"min": 0}, scope=Scope.settings ) due = Date(help="Date that this problem is due by", scope=Scope.settings) + extended_due = Date( + help="Date that this problem is due by for a particular student. This " + "may differ from the global due date if an instructor has granted " + "an extension to the student.", + default=None, + scope=Scope.user_state, + ) graceperiod = Timedelta( help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings @@ -179,7 +187,7 @@ def __init__(self, *args, **kwargs): """ XModule.__init__(self, *args, **kwargs) - due_date = self.due + due_date = get_extended_due_date(self) if self.graceperiod is not None and due_date: self.close_date = due_date + self.graceperiod diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index e01ae49149dd..3ce74fb3e221 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -229,6 +229,13 @@ class CombinedOpenEndedFields(object): default=None, scope=Scope.settings ) + extended_due = Date( + help="Date that this problem is due by for a particular student. This " + "may differ from the global due date if an instructor has granted " + "an extension to the student.", + default=None, + scope=Scope.user_state, + ) graceperiod = String( help="Amount of time after the due date that submissions will be accepted", default=None, diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py index cadf6cef0bcd..6629444a9e7b 100644 --- a/common/lib/xmodule/xmodule/foldit_module.py +++ b/common/lib/xmodule/xmodule/foldit_module.py @@ -20,6 +20,13 @@ class FolditFields(object): required_level = Integer(default=4, scope=Scope.settings) required_sublevel = Integer(default=5, scope=Scope.settings) due = Date(help="Date that this problem is due by", scope=Scope.settings) + extended_due = Date( + help="Date that this problem is due by for a particular student. This " + "may differ from the global due date if an instructor has granted " + "an extension to the student.", + default=None, + scope=Scope.user_state, + ) show_basic_score = String(scope=Scope.settings, default='false') show_leaderboard = String(scope=Scope.settings, default='false') diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index 1ef3883d8274..0216171c6f6a 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -48,6 +48,13 @@ class PeerGradingFields(object): help="Due date that should be displayed.", default=None, scope=Scope.settings) + extended_due = Date( + help="Date that this problem is due by for a particular student. This " + "may differ from the global due date if an instructor has granted " + "an extension to the student.", + default=None, + scope=Scope.user_state, + ) graceperiod = Timedelta( help="Amount of grace to give on the due date.", scope=Scope.settings diff --git a/common/lib/xmodule/xmodule/utils.py b/common/lib/xmodule/xmodule/utils.py new file mode 100644 index 000000000000..92e4a0d14d3c --- /dev/null +++ b/common/lib/xmodule/xmodule/utils.py @@ -0,0 +1,7 @@ + + +def get_extended_due_date(node): + due_date = getattr(node, 'extended_due', None) + if not due_date: + due_date = getattr(node, 'due', None) + return due_date diff --git a/lms/djangoapps/instructor/views/extensions.py b/lms/djangoapps/instructor/views/extensions.py new file mode 100644 index 000000000000..f10fa86477b5 --- /dev/null +++ b/lms/djangoapps/instructor/views/extensions.py @@ -0,0 +1,28 @@ +import json +from courseware.models import StudentModule +from xmodule.fields import Date + +datetime_to_json = Date().to_json + + +def set_due_date_extension(request, course_id, section, student, due_date): + """ + Sets a due date extension. Factored to be usable in both legacy and beta + instructor dashboards. + """ + try: + student_module = StudentModule.objects.get( + student_id=student.id, + course_id=course_id, + module_state_key=section + ) + state = json.loads(student_module.state) + state['extended_due'] = datetime_to_json(due_date) + student_module.state = json.dumps(state) + student_module.save() + return None # no error + + except StudentModule.DoesNotExist: + return "Couldn't find module with that urlname: {0} {1}. ".format( + section, student + ) diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index 6a02b5be7d4c..23adc6041410 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -3,6 +3,7 @@ """ from collections import defaultdict import csv +import datetime import json import logging from markupsafe import escape @@ -50,6 +51,8 @@ import track.views from mitxmako.shortcuts import render_to_string +from .extensions import set_due_date_extension + log = logging.getLogger(__name__) @@ -187,6 +190,22 @@ def get_student_from_identifier(unique_student_identifier): msg += "Couldn't find student with that email or username. " return msg, student + def parse_datetime(s): + """ + Constructs a datetime object in UTC from user input. + """ + msg = "" + dt = None + try: + d, t = s.split() + mm, dd, yy = map(int, d.split('/')) + h, m = map(int, t.split(':')) + dt = datetime.datetime(yy, mm, dd, h, m, tzinfo=timezone.utc) + return msg, dt + except: + msg = "Unable to parse date: {0} ".format(s) + return msg, dt + # process actions from form POST action = request.POST.get('action', '') use_offline = request.POST.get('use_offline_grades', False) @@ -469,6 +488,33 @@ def domatch(x): msg2, _ = _do_remote_gradebook(request.user, course, 'post-grades', files=files) msg += msg2 + #---------------------------------------- + # Extensions + + elif "Change due date for student" in action: + # get the form data + unique_student_identifier = request.POST.get( + 'unique_student_identifier', '' + ) + section = get_module_url(request.POST.get('section')) + + # try to uniquely id student by email address or username + message, student = get_student_from_identifier(unique_student_identifier) + msg += message + + # parse datetime + message, due_date = parse_datetime(request.POST.get('due_datetime')) + msg += message + + if section and student and due_date: + error = set_due_date_extension( + request, course_id, section, student, due_date) + if error: + msg += '{0}'.format(error) + log.debug(error) + else: + msg += 'Successfully changed due date.' + #---------------------------------------- # Admin diff --git a/lms/envs/common.py b/lms/envs/common.py index 0cbcbb774ab3..faf6a054ce76 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -144,6 +144,9 @@ # Enable instructor dash to submit background tasks 'ENABLE_INSTRUCTOR_BACKGROUND_TASKS': True, + # Enable instructor to assign individual due dates + 'INDIVIDUAL_DUE_DATES': False, + # Enable instructor dash beta version link 'ENABLE_INSTRUCTOR_BETA_DASHBOARD': False, diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 5b254fc86e58..2ff43f2c0468 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -110,6 +110,9 @@

${_("Instructor Dashboard")}

${_("Export grades to remote gradebook")}


%endif + %if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):

${_("Course-specific grade adjustment")}

@@ -271,6 +275,40 @@

${_("Student-specific grade inspection and adjustment")}

%endif + +##----------------------------------------------------------------------------- +%if modeflag.get('Extensions'): + %if settings.MITX_FEATURES.get('INDIVIDUAL_DUE_DATES'): +

${_("Individual due date extensions")}

+ +

+ ${_("Specify the {platform_name} email address or username of a student here:").format(platform_name=settings.PLATFORM_NAME)} + +

+

+ ${_("Specify a particular unit or problem in the course here by its url:")} + +

+

+ ${_('You may use just the "urlname" if a problem, or "modulename/urlname" if not. ' + '(For example, if the location is i4x://university/course/problem/problemname, ' + 'then just provide the problemname. ' + 'If the location is i4x://university/course/notaproblem/someothername, then ' + 'provide notaproblem/someothername.)')} +

+

+ ${_("Specify the individual due date and time in UTC:")} + +

+

+ +

+
+ %endif +%endif + + ##----------------------------------------------------------------------------- %if modeflag.get('Psychometrics'): diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py index d57ad9ce5238..235724a3bc57 100644 --- a/lms/xmodule_namespace.py +++ b/lms/xmodule_namespace.py @@ -33,6 +33,13 @@ class LmsNamespace(Namespace): scope=Scope.settings ) due = Date(help="Date that this problem is due by", scope=Scope.settings) + extended_due = Date( + help="Date that this problem is due by for a particular student. This " + "may differ from the global due date if an instructor has granted " + "an extension to the student.", + default=None, + scope=Scope.user_state, + ) source_file = String(help="source file name (eg for latex)", scope=Scope.settings) giturl = String(help="url root for course data git repository", scope=Scope.settings) xqa_key = String(help="DO NOT USE", scope=Scope.settings) From c8794b84faef494b480b5b11368731ef291a3e83 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Thu, 10 Oct 2013 15:53:33 -0400 Subject: [PATCH 03/19] Ability to set due date extension for entire homework or exam. --- lms/djangoapps/instructor/views/extensions.py | 55 +++++++++++++------ lms/djangoapps/instructor/views/legacy.py | 6 +- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/lms/djangoapps/instructor/views/extensions.py b/lms/djangoapps/instructor/views/extensions.py index f10fa86477b5..7b615bb8b400 100644 --- a/lms/djangoapps/instructor/views/extensions.py +++ b/lms/djangoapps/instructor/views/extensions.py @@ -5,24 +5,45 @@ datetime_to_json = Date().to_json -def set_due_date_extension(request, course_id, section, student, due_date): +def set_due_date_extension(course, url, student, due_date): """ Sets a due date extension. Factored to be usable in both legacy and beta instructor dashboards. """ - try: - student_module = StudentModule.objects.get( - student_id=student.id, - course_id=course_id, - module_state_key=section - ) - state = json.loads(student_module.state) - state['extended_due'] = datetime_to_json(due_date) - student_module.state = json.dumps(state) - student_module.save() - return None # no error - - except StudentModule.DoesNotExist: - return "Couldn't find module with that urlname: {0} {1}. ".format( - section, student - ) + def find_node(node): + """ + Find node in course tree for url. + """ + if node.location.url() == url: + return node + for child in node.get_children(): + found = find_node(child) + if found: + return found + return None + + node = find_node(course) + if not node: + return "Couldn't find module for url: {0}" % url + + def set_due_date(node): + try: + student_module = StudentModule.objects.get( + student_id=student.id, + course_id=course.id, + module_state_key=node.location.url() + ) + + state = json.loads(student_module.state) + state['extended_due'] = datetime_to_json(due_date) + student_module.state = json.dumps(state) + student_module.save() + except StudentModule.DoesNotExist: + pass + + for child in node.get_children(): + set_due_date(child) + + set_due_date(node) + + return None # no error diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index 23adc6041410..cd72e90c1eaa 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -496,7 +496,7 @@ def domatch(x): unique_student_identifier = request.POST.get( 'unique_student_identifier', '' ) - section = get_module_url(request.POST.get('section')) + url = get_module_url(request.POST.get('section')) # try to uniquely id student by email address or username message, student = get_student_from_identifier(unique_student_identifier) @@ -506,9 +506,9 @@ def domatch(x): message, due_date = parse_datetime(request.POST.get('due_datetime')) msg += message - if section and student and due_date: + if url and student and due_date: error = set_due_date_extension( - request, course_id, section, student, due_date) + course, url, student, due_date) if error: msg += '{0}'.format(error) log.debug(error) From 8f57c59c60cdde0a08f35702cf589d1077abaa7a Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Thu, 10 Oct 2013 16:23:48 -0400 Subject: [PATCH 04/19] Display correct dates in UI. --- lms/djangoapps/courseware/grades.py | 3 ++- lms/djangoapps/courseware/module_render.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index e3c40079c3fd..b47feb11063d 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -14,6 +14,7 @@ from xmodule import graders from xmodule.capa_module import CapaModule from xmodule.graders import Score +from xmodule.utils import get_extended_due_date from .models import StudentModule log = logging.getLogger("mitx.courseware") @@ -325,7 +326,7 @@ def progress_summary(student, request, course, model_data_cache): 'scores': scores, 'section_total': section_total, 'format': module_format, - 'due': section_module.lms.due, + 'due': get_extended_due_date(section_module.lms), 'graded': graded, }) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 0a48c56f874d..7bdc3295ae32 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -24,6 +24,7 @@ from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError +from xmodule.utils import get_extended_due_date from xmodule.x_module import ModuleSystem from xmodule_modifiers import replace_course_urls, replace_jump_to_id_urls, replace_static_urls, add_histogram, wrap_xmodule, save_module # pylint: disable=F0401 @@ -110,7 +111,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, model_ sections.append({'display_name': section.display_name_with_default, 'url_name': section.url_name, 'format': section.lms.format if section.lms.format is not None else '', - 'due': section.lms.due, + 'due': get_extended_due_date(section.lms), 'active': active, 'graded': section.lms.graded, }) From eb76cf117e1da3311c5aa6be3b308fcbb88021e9 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Fri, 11 Oct 2013 10:43:58 -0400 Subject: [PATCH 05/19] Fix InvalidWriteError when viewing a combined open ended problem in the LMS as an instructor. --- lms/djangoapps/courseware/model_data.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py index 44be16e44182..bb9a490d8ef0 100644 --- a/lms/djangoapps/courseware/model_data.py +++ b/lms/djangoapps/courseware/model_data.py @@ -353,7 +353,13 @@ def set_many(self, kv_dict): for field in kv_dict: # Check field for validity if field.field_name in self._descriptor_model_data: - raise InvalidWriteError("Not allowed to overwrite descriptor model data", field.field_name) + # xblock model data will set any mutable field as dirty whether + # it's been mutated or not. In light of that it's better to + # silently skip these fields rather than raise an error. + #raise InvalidWriteError( + # "Not allowed to overwrite descriptor model data", + # field.field_name) + continue if field.scope not in self._allowed_scopes: raise InvalidScopeError(field.scope) From 7f400717de79ae219ad177ab24bad07de649db32 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Fri, 11 Oct 2013 11:26:28 -0400 Subject: [PATCH 06/19] Get extended due date to work with combined open ended module. --- .../xmodule/xmodule/combined_open_ended_module.py | 10 ++++++---- .../combined_open_ended_modulev1.py | 4 +++- common/lib/xmodule/xmodule/peer_grading_module.py | 14 ++++++++------ 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 3ce74fb3e221..dab818dac90a 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -13,11 +13,13 @@ log = logging.getLogger("mitx.courseware") -V1_SETTINGS_ATTRIBUTES = ["display_name", "max_attempts", "graded", "accept_file_upload", - "skip_spelling_checks", "due", "graceperiod", "weight"] +V1_SETTINGS_ATTRIBUTES = [ + "display_name", "max_attempts", "graded", "accept_file_upload", + "skip_spelling_checks", "due", "graceperiod", "weight", "extended_due"] -V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state", - "student_attempts", "ready_to_reset"] +V1_STUDENT_ATTRIBUTES = [ + "current_task_number", "task_states", "state", + "student_attempts", "ready_to_reset"] V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py index 933eb0b5bb1f..c829aeb8b5c8 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py @@ -106,7 +106,9 @@ def __init__(self, system, location, definition, descriptor, self.accept_file_upload = instance_state.get('accept_file_upload', ACCEPT_FILE_UPLOAD) in TRUE_DICT self.skip_basic_checks = instance_state.get('skip_spelling_checks', SKIP_BASIC_CHECKS) in TRUE_DICT - due_date = instance_state.get('due', None) + due_date = instance_state.get('extended_due', None) + if due_date is None: + due_date = instance_state.get('due', None) grace_period_string = instance_state.get('graceperiod', None) try: diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index 0216171c6f6a..fd722a1aa917 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -7,10 +7,11 @@ from pkg_resources import resource_string from .capa_module import ComplexEncoder from .x_module import XModule -from xmodule.raw_module import RawDescriptor -from xmodule.modulestore.django import modulestore -from xmodule.modulestore.exceptions import ItemNotFoundError +from .raw_module import RawDescriptor +from .modulestore.django import modulestore +from .modulestore.exceptions import ItemNotFoundError from .timeinfo import TimeInfo +from .utils import get_extended_due_date from xblock.core import Dict, String, Scope, Boolean, Integer, Float from xmodule.fields import Date, Timedelta @@ -117,12 +118,13 @@ def __init__(self, *args, **kwargs): log.error("Linked location {0} for peer grading module {1} does not exist".format( self.link_to_location, self.location)) raise - due_date = self.linked_problem.lms.due + due_date = get_extended_due_date(self.linked_problem.lms) if due_date: self.lms.due = due_date try: - self.timeinfo = TimeInfo(self.due, self.graceperiod) + self.timeinfo = TimeInfo( + get_extended_due_date(self), self.graceperiod) except Exception: log.error("Error parsing due date information in location {0}".format(self.location)) raise @@ -529,7 +531,7 @@ def _find_corresponding_module_for_location(location): problem_location = problem['location'] descriptor = _find_corresponding_module_for_location(problem_location) if descriptor: - problem['due'] = descriptor.lms.due + problem['due'] = get_extended_due_date(descriptor.lms) grace_period = descriptor.lms.graceperiod try: problem_timeinfo = TimeInfo(problem['due'], grace_period) From b1716f458d52c0872895dd8adca3524278379f3f Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Fri, 11 Oct 2013 13:58:50 -0400 Subject: [PATCH 07/19] Use extended due date (unable to test FoldIt module). --- common/lib/xmodule/xmodule/foldit_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py index 6629444a9e7b..8fad45aa4a49 100644 --- a/common/lib/xmodule/xmodule/foldit_module.py +++ b/common/lib/xmodule/xmodule/foldit_module.py @@ -8,6 +8,7 @@ from xmodule.xml_module import XmlDescriptor from xblock.core import Scope, Integer, String from .fields import Date +from .utils import get_extended_due_date log = logging.getLogger(__name__) @@ -47,7 +48,7 @@ def __init__(self, *args, **kwargs): show_leaderboard="false"/> """ XModule.__init__(self, *args, **kwargs) - self.due_time = self.due + self.due_time = get_extended_due_date(self) def is_complete(self): """ From 083ab0d2687db781eaff743ed0fa50ad50038081 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Fri, 11 Oct 2013 14:00:41 -0400 Subject: [PATCH 08/19] More user friendly unit selection. --- common/lib/xmodule/xmodule/seq_module.py | 20 ++++++++++++---- lms/djangoapps/instructor/views/legacy.py | 23 +++++++++++++++++-- .../courseware/instructor_dashboard.html | 15 +++++------- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 580475e1ae7d..9ef866dafe27 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -3,14 +3,16 @@ from lxml import etree -from xmodule.mako_module import MakoModuleDescriptor -from xmodule.xml_module import XmlDescriptor -from xmodule.x_module import XModule -from xmodule.progress import Progress -from xmodule.exceptions import NotFoundError from xblock.core import Integer, Scope from pkg_resources import resource_string +from .exceptions import NotFoundError +from .fields import Date +from .mako_module import MakoModuleDescriptor +from .progress import Progress +from .x_module import XModule +from .xml_module import XmlDescriptor + log = logging.getLogger(__name__) # HACK: This shouldn't be hard-coded to two types @@ -24,6 +26,14 @@ class SequenceFields(object): # NOTE: Position is 1-indexed. This is silly, but there are now student # positions saved on prod, so it's not easy to fix. position = Integer(help="Last tab viewed in this sequence", scope=Scope.user_state) + due = Date(help="Date that this problem is due by", scope=Scope.settings) + extended_due = Date( + help="Date that this problem is due by for a particular student. This " + "may differ from the global due date if an instructor has granted " + "an extension to the student.", + default=None, + scope=Scope.user_state, + ) class SequenceModule(SequenceFields, XModule): diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index cd72e90c1eaa..99a3e5471fea 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -496,7 +496,7 @@ def domatch(x): unique_student_identifier = request.POST.get( 'unique_student_identifier', '' ) - url = get_module_url(request.POST.get('section')) + url = request.POST.get('url') # try to uniquely id student by email address or username message, student = get_student_from_identifier(unique_student_identifier) @@ -817,7 +817,7 @@ def get_analytics_result(analytics_name): 'instructor_tasks': instructor_tasks, 'offline_grade_log': offline_grades_available(course_id), 'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}), - + 'units_with_due_dates': get_graded_units_with_due_dates(course), 'analytics_results': analytics_results, } @@ -827,6 +827,25 @@ def get_analytics_result(analytics_name): return render_to_response('courseware/instructor_dashboard.html', context) +def get_graded_units_with_due_dates(course): + units = [] + def visit(node, level=0): + if getattr(node, 'due', None): + url = node.location.url() + title = getattr(node, 'display_name', None) + if not title: + title = url + else: + title += " (%s)" % url + units.append((title, url)) + else: + for child in node.get_children(): + visit(child, level+1) + visit(course) + units.sort(key=lambda x: x[0].lower()) + return units + + def _do_remote_gradebook(user, course, action, args=None, files=None): ''' Perform remote gradebook action. Returns msg, datatable. diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 2ff43f2c0468..fbfc022ec14a 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -286,15 +286,12 @@

${_("Individual due date extensions")}

- ${_("Specify a particular unit or problem in the course here by its url:")} - -

-

- ${_('You may use just the "urlname" if a problem, or "modulename/urlname" if not. ' - '(For example, if the location is i4x://university/course/problem/problemname, ' - 'then just provide the problemname. ' - 'If the location is i4x://university/course/notaproblem/someothername, then ' - 'provide notaproblem/someothername.)')} + ${_("Choose the graded unit:")} +

${_("Specify the individual due date and time in UTC:")} From 0eaafba061c983eadfd8415cb4ce48e8efd7ca12 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Fri, 11 Oct 2013 17:06:15 -0400 Subject: [PATCH 09/19] Dump due date extension data and reset due date extensions. --- lms/djangoapps/instructor/views/extensions.py | 108 +++++++++++++++--- lms/djangoapps/instructor/views/legacy.py | 68 +++++++---- .../courseware/instructor_dashboard.html | 5 + 3 files changed, 143 insertions(+), 38 deletions(-) diff --git a/lms/djangoapps/instructor/views/extensions.py b/lms/djangoapps/instructor/views/extensions.py index 7b615bb8b400..b58d2ae4a97a 100644 --- a/lms/djangoapps/instructor/views/extensions.py +++ b/lms/djangoapps/instructor/views/extensions.py @@ -2,7 +2,7 @@ from courseware.models import StudentModule from xmodule.fields import Date -datetime_to_json = Date().to_json +date_field = Date() def set_due_date_extension(course, url, student, due_date): @@ -10,19 +10,7 @@ def set_due_date_extension(course, url, student, due_date): Sets a due date extension. Factored to be usable in both legacy and beta instructor dashboards. """ - def find_node(node): - """ - Find node in course tree for url. - """ - if node.location.url() == url: - return node - for child in node.get_children(): - found = find_node(child) - if found: - return found - return None - - node = find_node(course) + node = find_unit(course, url) if not node: return "Couldn't find module for url: {0}" % url @@ -35,7 +23,7 @@ def set_due_date(node): ) state = json.loads(student_module.state) - state['extended_due'] = datetime_to_json(due_date) + state['extended_due'] = date_field.to_json(due_date) student_module.state = json.dumps(state) student_module.save() except StudentModule.DoesNotExist: @@ -47,3 +35,93 @@ def set_due_date(node): set_due_date(node) return None # no error + + +def find_unit(node, url): + """ + Find node in course tree for url. + """ + if node.location.url() == url: + return node + for child in node.get_children(): + found = find_unit(child, url) + if found: + return found + return None + + +def get_units_with_due_date(course): + units = [] + def visit(node, level=0): + if getattr(node, 'due', None): + units.append(node) + else: + for child in node.get_children(): + visit(child, level+1) + visit(course) + units.sort(key=title_or_url) + return units + + +def get_units_with_due_date_options(course): + def make_option(node): + return title_or_url(node), node.location.url() + return map(make_option, get_units_with_due_date(course)) + + +def title_or_url(node): + title = getattr(node, 'display_name', None) + if not title: + title = node.location.url() + return title + + +def dump_students_with_due_date_extensions(course, url): + unit = find_unit(course, url) + if not unit: + return "Couldn't find module for url: {0}" % url, {} + + data = [] + query = StudentModule.objects.filter( + course_id=course.id, + module_state_key=url) + for sm in query: + state = json.loads(sm.state) + extended_due = state.get("extended_due") + if not extended_due: + continue + extended_due = date_field.from_json(extended_due) + extended_due = extended_due.strftime("%Y-%m-%d %H:%M") + fullname = '%s %s' % (sm.student.first_name, sm.student.last_name) + data.append((sm.student.username, fullname, extended_due)) + return None, { + "header": ["Username", "Full Name", "Extended Due Date"], + "title": "Users with due date extensions for {0}".format( + title_or_url(unit)), + "data": data + } + + +def dump_due_date_extensions_for_student(course, student): + data = [] + units = get_units_with_due_date(course) + units = dict([(u.location.url(), u) for u in units]) + query = StudentModule.objects.filter( + course_id=course.id, + student_id=student.id) + for sm in query: + state = json.loads(sm.state) + if sm.module_state_key not in units: + continue + extended_due = state.get("extended_due") + if not extended_due: + continue + extended_due = date_field.from_json(extended_due) + extended_due = extended_due.strftime("%Y-%m-%d %H:%M") + title = title_or_url(units[sm.module_state_key]) + data.append((title, extended_due)) + return None, { + "header": ["Unit", "Extended Due Date"], + "title": "Due date extensions for {0} {1} ({2})".format( + student.first_name, student.last_name, student.username), + "data": data} diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index 99a3e5471fea..d80eb730e564 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -51,7 +51,11 @@ import track.views from mitxmako.shortcuts import render_to_string -from .extensions import set_due_date_extension +from .extensions import ( + dump_students_with_due_date_extensions, + dump_due_date_extensions_for_student, + get_units_with_due_date_options, + set_due_date_extension) log = logging.getLogger(__name__) @@ -494,8 +498,7 @@ def domatch(x): elif "Change due date for student" in action: # get the form data unique_student_identifier = request.POST.get( - 'unique_student_identifier', '' - ) + 'unique_student_identifier', '') url = request.POST.get('url') # try to uniquely id student by email address or username @@ -515,6 +518,44 @@ def domatch(x): else: msg += 'Successfully changed due date.' + elif "Reset due date for student" in action: + # get the form data + unique_student_identifier = request.POST.get( + 'unique_student_identifier', '') + url = request.POST.get('url') + + # try to uniquely id student by email address or username + message, student = get_student_from_identifier(unique_student_identifier) + msg += message + + if url and student: + error = set_due_date_extension( + course, url, student, None) + if error: + msg += '{0}'.format(error) + log.debug(error) + else: + msg += 'Successfully reset due date.' + + + elif "Dump list of students with due date extensions" in action: + url = request.POST.get('url') + error, datatable = dump_students_with_due_date_extensions(course, url) + if error: + msg += '{0}'.format(error) + + elif "Dump due date extensions for student" in action: + unique_student_identifier = request.POST.get( + 'unique_student_identifier', '') + message, student = get_student_from_identifier(unique_student_identifier) + msg += message + + if student: + error, datatable = dump_due_date_extensions_for_student( + course, student) + if error: + msg += '{0}'.format(error) + #---------------------------------------- # Admin @@ -817,7 +858,7 @@ def get_analytics_result(analytics_name): 'instructor_tasks': instructor_tasks, 'offline_grade_log': offline_grades_available(course_id), 'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}), - 'units_with_due_dates': get_graded_units_with_due_dates(course), + 'units_with_due_dates': get_units_with_due_date_options(course), 'analytics_results': analytics_results, } @@ -827,25 +868,6 @@ def get_analytics_result(analytics_name): return render_to_response('courseware/instructor_dashboard.html', context) -def get_graded_units_with_due_dates(course): - units = [] - def visit(node, level=0): - if getattr(node, 'due', None): - url = node.location.url() - title = getattr(node, 'display_name', None) - if not title: - title = url - else: - title += " (%s)" % url - units.append((title, url)) - else: - for child in node.get_children(): - visit(child, level+1) - visit(course) - units.sort(key=lambda x: x[0].lower()) - return units - - def _do_remote_gradebook(user, course, action, args=None, files=None): ''' Perform remote gradebook action. Returns msg, datatable. diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index fbfc022ec14a..ba0b54d34f73 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -300,6 +300,11 @@

${_("Individual due date extensions")}

+ +

+

+ +


%endif From 12579d152cb17bf04b430dfcb9912cb00f860c6d Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Sat, 12 Oct 2013 10:02:17 -0400 Subject: [PATCH 10/19] Test closing date respects due date extension. --- .../xmodule/xmodule/tests/test_capa_module.py | 33 ++++++------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 80c4e41e8f1a..102ef4cb582d 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -66,16 +66,10 @@ def answer_key(): "_2_1") @staticmethod - def create(graceperiod=None, - due=None, - max_attempts=None, - showanswer=None, - rerandomize=None, - force_save_button=None, - attempts=None, + def create(attempts=None, problem_state=None, correct=False, - done=None + **kwargs ): """ All parameters are optional, and are added to the created problem if specified. @@ -97,21 +91,7 @@ def create(graceperiod=None, "SampleProblem{0}".format(CapaFactory.next_num())]) model_data = {'data': CapaFactory.sample_problem_xml, 'location': location} - if graceperiod is not None: - model_data['graceperiod'] = graceperiod - if due is not None: - model_data['due'] = due - if max_attempts is not None: - model_data['max_attempts'] = max_attempts - if showanswer is not None: - model_data['showanswer'] = showanswer - if force_save_button is not None: - model_data['force_save_button'] = force_save_button - if rerandomize is not None: - model_data['rerandomize'] = rerandomize - if done is not None: - model_data['done'] = done - + model_data.update(kwargs) descriptor = Mock(weight="1") if problem_state is not None: model_data.update(problem_state) @@ -318,6 +298,13 @@ def test_closed(self): due=self.yesterday_str) self.assertTrue(module.closed()) + def test_due_date_extension(self): + + module = CapaFactory.create( + max_attempts="1", attempts="0", due=self.yesterday_str, + extended_due=self.tomorrow_str) + self.assertFalse(module.closed()) + def test_parse_get_params(self): # We have to set up Django settings in order to use QueryDict From 7e9cbac85207e3ceceeeaf011b781b995f896f89 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Sat, 12 Oct 2013 10:08:00 -0400 Subject: [PATCH 11/19] Fix test broken by eb76cf117e1da3311c5aa6be3b308fcbb88021e9 --- lms/djangoapps/courseware/tests/test_model_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_model_data.py b/lms/djangoapps/courseware/tests/test_model_data.py index 0368bb040bee..8d88fb99b370 100644 --- a/lms/djangoapps/courseware/tests/test_model_data.py +++ b/lms/djangoapps/courseware/tests/test_model_data.py @@ -66,12 +66,12 @@ def test_get_from_descriptor(self): self.assertEquals('settings', self.kvs.get(settings_key('field_b'))) def test_write_to_descriptor(self): - self.assertRaises(InvalidWriteError, self.kvs.set, content_key('field_a'), 'foo') + self.kvs.set(content_key('field_a'), 'foo') self.assertEquals('content', self.desc_md['field_a']) - self.assertRaises(InvalidWriteError, self.kvs.set, settings_key('field_b'), 'foo') + self.kvs.set(settings_key('field_b'), 'foo') self.assertEquals('settings', self.desc_md['field_b']) - self.assertRaises(InvalidWriteError, self.kvs.set_many, {content_key('field_a'): 'foo'}) + self.kvs.set_many({content_key('field_a'): 'foo'}) self.assertEquals('content', self.desc_md['field_a']) self.assertRaises(InvalidWriteError, self.kvs.delete, content_key('field_a')) From 087b5396d80bf09939e207fa3a5a9e15fdf884c5 Mon Sep 17 00:00:00 2001 From: vagrant Date: Sat, 12 Oct 2013 15:34:22 +0000 Subject: [PATCH 12/19] Unit tests for due date extensions. --- lms/djangoapps/instructor/views/extensions.py | 5 +- .../instructor/views/tests/__init__.py | 0 .../instructor/views/tests/test_extensions.py | 125 ++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 lms/djangoapps/instructor/views/tests/__init__.py create mode 100644 lms/djangoapps/instructor/views/tests/test_extensions.py diff --git a/lms/djangoapps/instructor/views/extensions.py b/lms/djangoapps/instructor/views/extensions.py index b58d2ae4a97a..e8e346bc597a 100644 --- a/lms/djangoapps/instructor/views/extensions.py +++ b/lms/djangoapps/instructor/views/extensions.py @@ -12,7 +12,7 @@ def set_due_date_extension(course, url, student, due_date): """ node = find_unit(course, url) if not node: - return "Couldn't find module for url: {0}" % url + return "Couldn't find module for url: {0}".format(url) def set_due_date(node): try: @@ -79,7 +79,7 @@ def title_or_url(node): def dump_students_with_due_date_extensions(course, url): unit = find_unit(course, url) if not unit: - return "Couldn't find module for url: {0}" % url, {} + return "Couldn't find module for url: {0}".format(url), {} data = [] query = StudentModule.objects.filter( @@ -94,6 +94,7 @@ def dump_students_with_due_date_extensions(course, url): extended_due = extended_due.strftime("%Y-%m-%d %H:%M") fullname = '%s %s' % (sm.student.first_name, sm.student.last_name) data.append((sm.student.username, fullname, extended_due)) + data.sort(key=lambda x: x[0]) return None, { "header": ["Username", "Full Name", "Extended Due Date"], "title": "Users with due date extensions for {0}".format( diff --git a/lms/djangoapps/instructor/views/tests/__init__.py b/lms/djangoapps/instructor/views/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lms/djangoapps/instructor/views/tests/test_extensions.py b/lms/djangoapps/instructor/views/tests/test_extensions.py new file mode 100644 index 000000000000..6e0731bb3f3a --- /dev/null +++ b/lms/djangoapps/instructor/views/tests/test_extensions.py @@ -0,0 +1,125 @@ +import datetime +import json +import mock +import unittest + + +due_date = datetime.datetime(2010, 5, 12, 2, 42) +extended_due_date = datetime.datetime(2013, 10, 12, 10, 30) + + +class ExtensionsTests(unittest.TestCase): + + def setUp(self): + self.course = DummyCourseNode( + 'Dummy Course', 'i4x://dummy', + children = [ + DummyCourseNode( + 'Homework 1', 'i4x://dummy/homework', + due=due_date, + children=[ + DummyCourseNode( + 'Problem 1', 'i4x://dummy/homework/problem')]), + DummyCourseNode( + 'Final Exam', 'i4x://dummy/exam', + due=due_date, + children=[ + DummyCourseNode( + 'Problem 2', 'i4x://dummy/exam/problem')])]) + self.homework, self.exam = self.course.children + + patcher = mock.patch('instructor.views.extensions.StudentModule') + self.StudentModule = patcher.start() + self.addCleanup(patcher.stop) + + self.student = mock.Mock(username='fred', first_name='Fred', + last_name='Flintstone') + + def test_set_due_date_extension_success(self): + from ..extensions import set_due_date_extension as fut + self.StudentModule.objects.get.return_value.state = json.dumps({}) + error = fut(self.course, 'i4x://dummy/homework', self.student, + extended_due_date) + self.assertEqual(error, None) + state = json.loads(self.StudentModule.objects.get.return_value.state) + self.assertEqual(state['extended_due'], u'2013-10-12T10:30:00Z') + + def test_set_due_date_extension_bad_url(self): + from ..extensions import set_due_date_extension as fut + error = fut(self.course, 'i4x://foo', self.student, extended_due_date) + self.assertTrue(error.startswith("Couldn't find")) + + def test_dump_students_with_due_date_extensions(self): + from ..extensions import dump_students_with_due_date_extensions as fut + self.StudentModule.objects.filter.return_value = [ + mock.Mock( + student=mock.Mock( + username='fred', + first_name='Fred', + last_name='Flintstone'), + state=json.dumps({'extended_due': u'2013-10-12T10:30:00Z'})), + mock.Mock( + student=mock.Mock( + username='barney', + first_name='Barney', + last_name='Rubble'), + state=json.dumps({'extended_due': u'2013-10-13T10:30:00Z'})), + mock.Mock( + student=mock.Mock( + username='bambam', + first_name='Bam Bam', + last_name='Flintstone'), + state=json.dumps({}))] + error, table = fut(self.course, 'i4x://dummy/homework') + self.assertEqual(error, None) + self.assertEqual(table['header'], + ["Username", "Full Name", "Extended Due Date"]) + self.assertEqual(table['title'], + "Users with due date extensions for Homework 1") + self.assertEqual(table['data'], + [('barney', 'Barney Rubble', '2013-10-13 10:30'), + ('fred', 'Fred Flintstone', '2013-10-12 10:30')]) + + def test_dump_students_with_due_date_extensions_bad_url(self): + from ..extensions import dump_students_with_due_date_extensions as fut + error, table = fut(self.course, 'i4x://foo') + self.assertTrue(error.startswith("Couldn't find")) + self.assertEqual(table, {}) + + def test_dump_due_date_extensions_for_student(self): + from ..extensions import dump_due_date_extensions_for_student as fut + self.StudentModule.objects.filter.return_value = [ + mock.Mock( + module_state_key='i4x://dummy/homework', + state=json.dumps({})), + mock.Mock( + module_state_key='i4x://dummy/exam', + state=json.dumps({'extended_due': u'2013-10-13T10:30:00Z'}))] + error, table = fut(self.course, self.student) + self.assertEqual(error, None) + self.assertEqual(table['header'], ["Unit", "Extended Due Date"]) + self.assertEqual(table['title'], + "Due date extensions for Fred Flintstone (fred)") + self.assertEqual(table['data'], + [('Final Exam', '2013-10-13 10:30')]) + + +class DummyCourseNode(object): + id = 1 + children = [] + + def __init__(self, display_name, url, **kw): + self.display_name = display_name + self._url = url + self.__dict__.update(kw) + + @property + def location(self): + return self + + def url(self): + return self._url + + def get_children(self): + return self.children + From 04b70a589899afd38a587121a55aa9e08d7216e5 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Mon, 14 Oct 2013 15:34:49 -0400 Subject: [PATCH 13/19] Don't gather units with due dates unless due date extension is active. --- lms/djangoapps/instructor/views/legacy.py | 3 ++- lms/envs/dev.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index d80eb730e564..9bd04574dcc5 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -858,10 +858,11 @@ def get_analytics_result(analytics_name): 'instructor_tasks': instructor_tasks, 'offline_grade_log': offline_grades_available(course_id), 'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}), - 'units_with_due_dates': get_units_with_due_date_options(course), 'analytics_results': analytics_results, } + if settings.MITX_FEATURES.get('INDIVIDUAL_DUE_DATES'): + context['units_with_due_dates'] = get_units_with_due_date_options(course) if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'): context['beta_dashboard_url'] = reverse('instructor_dashboard_2', kwargs={'course_id': course_id}) diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 622ff6acf7e2..5deca8628fbd 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -18,6 +18,7 @@ DEBUG = True TEMPLATE_DEBUG = True +MITX_FEATURES['INDIVIDUAL_DUE_DATES'] = True # crossi dev, delete if checked in MITX_FEATURES['DISABLE_START_DATES'] = True MITX_FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True From aff69709ce2bfb8889c0eeb6be6dc2e78e23fd8c Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Thu, 17 Oct 2013 09:29:48 -0400 Subject: [PATCH 14/19] Require user to actively choose a unit for due date extensions. User friendly error message if user fails to do so. --- lms/djangoapps/instructor/views/legacy.py | 19 +++++++++++++------ .../courseware/instructor_dashboard.html | 1 + 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index 9bd04574dcc5..2240e7d582cf 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -500,6 +500,8 @@ def domatch(x): unique_student_identifier = request.POST.get( 'unique_student_identifier', '') url = request.POST.get('url') + if not url: + msg += 'Must choose a unit. ' # try to uniquely id student by email address or username message, student = get_student_from_identifier(unique_student_identifier) @@ -513,7 +515,7 @@ def domatch(x): error = set_due_date_extension( course, url, student, due_date) if error: - msg += '{0}'.format(error) + msg += '{0} '.format(error) log.debug(error) else: msg += 'Successfully changed due date.' @@ -523,6 +525,8 @@ def domatch(x): unique_student_identifier = request.POST.get( 'unique_student_identifier', '') url = request.POST.get('url') + if not url: + msg += 'Must choose a unit. ' # try to uniquely id student by email address or username message, student = get_student_from_identifier(unique_student_identifier) @@ -532,7 +536,7 @@ def domatch(x): error = set_due_date_extension( course, url, student, None) if error: - msg += '{0}'.format(error) + msg += '{0} '.format(error) log.debug(error) else: msg += 'Successfully reset due date.' @@ -540,9 +544,12 @@ def domatch(x): elif "Dump list of students with due date extensions" in action: url = request.POST.get('url') - error, datatable = dump_students_with_due_date_extensions(course, url) - if error: - msg += '{0}'.format(error) + if not url: + msg += 'Must choose a unit. ' + else: + error, datatable = dump_students_with_due_date_extensions(course, url) + if error: + msg += '{0} '.format(error) elif "Dump due date extensions for student" in action: unique_student_identifier = request.POST.get( @@ -554,7 +561,7 @@ def domatch(x): error, datatable = dump_due_date_extensions_for_student( course, student) if error: - msg += '{0}'.format(error) + msg += '{0} '.format(error) #---------------------------------------- # Admin diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index ba0b54d34f73..68acc689cae3 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -288,6 +288,7 @@

${_("Individual due date extensions")}

${_("Choose the graded unit:")} +

+

+ ${_("Choose the graded unit:")} + +

+

+ ${_("Specify the individual due date and time in UTC:")} + +

+

+

+

+ + +

+

+ + +

From 28a2bce8c868e0c3f2338edf0cda3a4fbe173cd6 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Fri, 25 Oct 2013 10:41:26 -0400 Subject: [PATCH 17/19] Show students with due date extensions. --- lms/djangoapps/instructor/views/api.py | 30 ++++++++- .../instructor_dashboard/extensions.coffee | 61 ++++++++++++++++++- .../instructor_dashboard_2/extensions.html | 9 ++- 3 files changed, 93 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 0348b0ea77b6..32f00ba44db5 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -45,6 +45,7 @@ from bulk_email.models import CourseEmail from .extensions import ( + dump_students_with_due_date_extensions, set_due_date_extension) log = logging.getLogger(__name__) @@ -943,8 +944,25 @@ def change_due_date(request, course_id): @require_level('staff') @require_query_params('student', 'url') def reset_due_date(request, course_id): + course = get_course_by_id(course_id) student = get_student_from_identifier(request.GET.get('student')) - return HttpResponseBadRequest(json.dumps({'error': 'Not implemented'})) + url = request.GET.get('url') + error, unit = set_due_date_extension(course, url, student, None) + if error: + return HttpResponseBadRequest(json.dumps({'error': error})) + + studentname = '{0} {1} ({2})'.format( + student.first_name, student.last_name, student.username) + unitname = getattr(unit, 'display_name', None) + if unitname: + unitname = '{0} ({1})'.format(unitname, unit.location.url()) + due_date = unit.due + msg = ( + 'Successfully reset due date for student {0} for {1} ' + 'to {2}').format(studentname, unitname, + due_date.strftime('%Y-%m-%d %H:%M')) + + return JsonResponse(msg) @ensure_csrf_cookie @@ -952,7 +970,15 @@ def reset_due_date(request, course_id): @require_level('staff') @require_query_params('url') def show_unit_extensions(request, course_id): - return HttpResponseBadRequest(json.dumps({'error': 'Not implemented'})) + course = get_course_by_id(course_id) + url = request.GET.get('url') + error, data = dump_students_with_due_date_extensions(course, url) + if error: + return HttpResponseBadRequest(json.dumps({'error': error})) + data['header'] = header = [ + col.lower().replace(' ', '_') for col in data['header']] + data['data'] = [dict(zip(header, row)) for row in data['data']] + return JsonResponse(data) @ensure_csrf_cookie diff --git a/lms/static/coffee/src/instructor_dashboard/extensions.coffee b/lms/static/coffee/src/instructor_dashboard/extensions.coffee index bb6d147ec25d..eaca0e67dc14 100644 --- a/lms/static/coffee/src/instructor_dashboard/extensions.coffee +++ b/lms/static/coffee/src/instructor_dashboard/extensions.coffee @@ -35,8 +35,14 @@ class Extensions @$task_response.hide() @$task_error.hide() + # Gather grid elements + $grid_display = @$section.find '.data-display' + @$grid_text = $grid_display.find '.data-display-text' + @$grid_table = $grid_display.find '.data-display-table' + # Click handlers @$change_due_date.click => + @clear_display() send_data = student: @$student_input.val() url: @$url_input.val() @@ -48,11 +54,39 @@ class Extensions data: send_data success: (data) => @display_response data error: (xhr) => @fail_with_error "Error changing due date", xhr - + + @$reset_due_date.click => + @clear_display() + send_data = + student: @$student_input.val() + url: @$url_input.val() + + $.ajax + dataType: 'json' + url: @$reset_due_date.data 'endpoint' + data: send_data + success: (data) => @display_response data + error: (xhr) => @fail_with_error "Error reseting due date", xhr + + @$show_unit_extensions.click => + @clear_display() + @$grid_table.text 'Loading...' + + url = @$show_unit_extensions.data 'endpoint' + send_data = + url: @$url_input.val() + $.ajax + dataType: 'json' + url: url + data: send_data + error: (xhr) => @fail_with_error "Error getting due dates", xhr + success: (data) => @display_grid data + # handler for when the section title is clicked. onClickTitle: -> fail_with_error: (msg, xhr) -> + @clear_display() data = $.parseJSON xhr.responseText msg += ": " + data['error'] console.warn msg @@ -61,11 +95,32 @@ class Extensions @$task_error.text msg @$task_error.show() - display_response: (data_from_server) -> + display_response: (data) -> @$task_error.empty().hide() - @$task_response.empty().text data_from_server + @$task_response.empty().text data @$task_response.show() + display_grid: (data) -> + @clear_display() + + # display on a SlickGrid + options = + enableCellNavigation: true + enableColumnReorder: false + forceFitColumns: true + + columns = ({id: col, field: col, name: col} for col in data.header) + grid_data = data.data + + $table_placeholder = $ '
', class: 'slickgrid', style: 'min-height: 400px' + @$grid_table.append $table_placeholder + grid = new Slick.Grid($table_placeholder, grid_data, columns, options) + + clear_display: -> + @$grid_text.empty() + @$grid_table.empty() + @$task_error.empty().hide() + @$task_response.empty().hide() # export for use # create parent namespaces if they do not already exist. diff --git a/lms/templates/instructor/instructor_dashboard_2/extensions.html b/lms/templates/instructor/instructor_dashboard_2/extensions.html index 96a7533ee494..e01ca77a5a69 100644 --- a/lms/templates/instructor/instructor_dashboard_2/extensions.html +++ b/lms/templates/instructor/instructor_dashboard_2/extensions.html @@ -30,8 +30,13 @@

${_("Individual due date extensions")}

data-endpoint="${section_data['reset_due_date_url']}">

- -

+ +
+
+
+
From 7834d822233528c93e113d39fd6b5533017eed37 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Fri, 25 Oct 2013 11:04:03 -0400 Subject: [PATCH 18/19] Looks like they changed where the user's name is kept. --- lms/djangoapps/instructor/views/api.py | 9 +++------ lms/djangoapps/instructor/views/extensions.py | 2 +- lms/djangoapps/instructor/views/legacy.py | 6 ++---- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 32f00ba44db5..e1c09d5ae1e7 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -926,8 +926,7 @@ def change_due_date(request, course_id): if error: return HttpResponseBadRequest(json.dumps({'error': error})) - studentname = '{0} {1} ({2})'.format( - student.first_name, student.last_name, student.username) + studentname = student.profile.name unitname = getattr(unit, 'display_name', None) if unitname: unitname = '{0} ({1})'.format(unitname, unit.location.url()) @@ -951,8 +950,7 @@ def reset_due_date(request, course_id): if error: return HttpResponseBadRequest(json.dumps({'error': error})) - studentname = '{0} {1} ({2})'.format( - student.first_name, student.last_name, student.username) + studentname = student.profile.name unitname = getattr(unit, 'display_name', None) if unitname: unitname = '{0} ({1})'.format(unitname, unit.location.url()) @@ -975,8 +973,7 @@ def show_unit_extensions(request, course_id): error, data = dump_students_with_due_date_extensions(course, url) if error: return HttpResponseBadRequest(json.dumps({'error': error})) - data['header'] = header = [ - col.lower().replace(' ', '_') for col in data['header']] + header = data['header'] data['data'] = [dict(zip(header, row)) for row in data['data']] return JsonResponse(data) diff --git a/lms/djangoapps/instructor/views/extensions.py b/lms/djangoapps/instructor/views/extensions.py index fc531d259975..327180a292d4 100644 --- a/lms/djangoapps/instructor/views/extensions.py +++ b/lms/djangoapps/instructor/views/extensions.py @@ -92,7 +92,7 @@ def dump_students_with_due_date_extensions(course, url): continue extended_due = date_field.from_json(extended_due) extended_due = extended_due.strftime("%Y-%m-%d %H:%M") - fullname = '%s %s' % (sm.student.first_name, sm.student.last_name) + fullname = sm.student.profile.name data.append((sm.student.username, fullname, extended_due)) data.sort(key=lambda x: x[0]) return None, { diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index fe65731b22c8..1bf6f66dcbbe 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -541,8 +541,7 @@ def domatch(x): msg += '{0} '.format(error) log.debug(error) else: - studentname = '{0} {1} ({2})'.format( - student.first_name, student.last_name, student.username) + studentname = student.profile.name unitname = getattr(unit, 'display_name', None) if unitname: unitname = '{0} ({1})'.format(unitname, unit.location.url()) @@ -570,8 +569,7 @@ def domatch(x): msg += '{0} '.format(error) log.debug(error) else: - studentname = '{0} {1} ({2})'.format( - student.first_name, student.last_name, student.username) + studentname = student.profile.name unitname = getattr(unit, 'display_name', None) if unitname: unitname = '{0} ({1})'.format(unitname, unit.location.url()) From b95a991d83b53b9b0619251e5e20c33e53eb3242 Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Fri, 25 Oct 2013 11:16:31 -0400 Subject: [PATCH 19/19] Show due date extensions for student. --- lms/djangoapps/instructor/views/api.py | 9 ++++++++- .../src/instructor_dashboard/extensions.coffee | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index e1c09d5ae1e7..5af743c59c81 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -45,6 +45,7 @@ from bulk_email.models import CourseEmail from .extensions import ( + dump_due_date_extensions_for_student, dump_students_with_due_date_extensions, set_due_date_extension) @@ -984,7 +985,13 @@ def show_unit_extensions(request, course_id): @require_query_params('student') def show_student_extensions(request, course_id): student = get_student_from_identifier(request.GET.get('student')) - return HttpResponseBadRequest(json.dumps({'error': 'Not implemented'})) + course = get_course_by_id(course_id) + error, data = dump_due_date_extensions_for_student(course, student) + if error: + return HttpResponseBadRequest(json.dumps({'error': error})) + header = data['header'] + data['data'] = [dict(zip(header, row)) for row in data['data']] + return JsonResponse(data) def _split_input_list(str_list): diff --git a/lms/static/coffee/src/instructor_dashboard/extensions.coffee b/lms/static/coffee/src/instructor_dashboard/extensions.coffee index eaca0e67dc14..4d59bc214f71 100644 --- a/lms/static/coffee/src/instructor_dashboard/extensions.coffee +++ b/lms/static/coffee/src/instructor_dashboard/extensions.coffee @@ -81,6 +81,20 @@ class Extensions data: send_data error: (xhr) => @fail_with_error "Error getting due dates", xhr success: (data) => @display_grid data + + @$show_student_extensions.click => + @clear_display() + @$grid_table.text 'Loading...' + + url = @$show_student_extensions.data 'endpoint' + send_data = + student: @$student_input.val() + $.ajax + dataType: 'json' + url: url + data: send_data + error: (xhr) => @fail_with_error "Error getting due dates", xhr + success: (data) => @display_grid data # handler for when the section title is clicked. onClickTitle: -> @@ -102,6 +116,7 @@ class Extensions display_grid: (data) -> @clear_display() + @$grid_text.text data.title # display on a SlickGrid options =