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 @@
+ ${_("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 @@