Commit 066d103b by Will Daly

Merge pull request #274 from edx/will/instructor-due-dates

Due dates do not apply to course staff
parents 4af6cc59 cf2ff466
{% load i18n %}
{% load tz %}
<div class="wrapper--staff-info wrapper--ui-staff">
<div class="staff-info ui-staff ui-toggle-visibility is--collapsed">
<h2 class="staff-info__title ui-staff__title ui-toggle-visibility__control">
......@@ -16,6 +17,10 @@
</div>
<div class="staff-info__status ui-staff__content__section">
{% trans "Location" %}: {{ item_id }}
</div>
<div class="staff-info__status ui-staff__content__section">
<table class="staff-info__status__table" summary="{% trans "Where are your students currently in this problem" %}">
<caption class="title">{% trans "Student Progress" %}</caption>
......@@ -39,8 +44,40 @@
</div>
<div class="staff-info__status ui-staff__content__section">
{% trans "Location" %}: {{ item_id }}
<table class="staff-info__status__table" summary="{% trans "Dates" %}">
<caption class="title">{% trans "Dates" %}</caption>
<thead>
<tr>
<th abbr="Problem Step" scope="col">{% trans "Problem Step" %}</th>
<th abbr="Release" scope="col">{% trans "Release Date" %}</th>
<th abbr="Due" scope="col">{% trans "Due Date" %}</th>
</tr>
</thead>
<tbody>
{% for item in step_dates %}
<tr>
<td class="label">{{ item.step }}</td>
{% if item.start %}
<td class="value">{{ item.start|utc|date:"N j, Y H:i e" }}</td>
{% else %}
<td class="value">{% trans "N/A" %}</td>
{% endif %}
{% if item.due %}
<td class="value">{{ item.due|utc|date:"N j, Y H:i e" }}</td>
{% else %}
<td class="value">{% trans "N/A" %}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
......@@ -26,7 +26,7 @@ from openassessment.xblock.xml import update_from_xml, serialize_content_to_xml
from openassessment.xblock.workflow_mixin import WorkflowMixin
from openassessment.workflow import api as workflow_api
from openassessment.xblock.validation import validator
from openassessment.xblock.resolve_dates import resolve_dates
from openassessment.xblock.resolve_dates import resolve_dates, DISTANT_PAST, DISTANT_FUTURE
logger = logging.getLogger(__name__)
......@@ -198,12 +198,10 @@ class OpenAssessmentBlock(
"is_course_staff": False,
}
# If we're course staff, add the context necessary to render
# the course staff debug panel.
if self.is_course_staff and not self.in_studio_preview:
status_counts, num_submissions = self.get_workflow_status_counts()
context_dict['is_course_staff'] = True
context_dict['status_counts'] = status_counts
context_dict['num_submissions'] = num_submissions
context_dict['item_id'] = unicode(self.scope_ids.usage_id)
context_dict.update(self.staff_debug_template_context())
template = get_template("openassessmentblock/oa_base.html")
context = Context(context_dict)
......@@ -213,6 +211,40 @@ class OpenAssessmentBlock(
frag.initialize_js('OpenAssessmentBlock')
return frag
def staff_debug_template_context(self):
"""
Template context dictionary for course staff debug panel.
Returns:
dict: The template context specific to the course staff debug panel.
"""
context = dict()
# Enable the course staff debug panel
context['is_course_staff'] = True
# Calculate how many students are in each step of the workflow
status_counts, num_submissions = self.get_workflow_status_counts()
context['status_counts'] = status_counts
context['num_submissions'] = num_submissions
context['item_id'] = unicode(self.scope_ids.usage_id)
# Include release/due dates for each step in the problem
context['step_dates'] = list()
for step in ['submission', 'peer-assessment', 'self-assessment']:
# Get the dates as a student would see them
__, __, start_date, due_date = self.is_closed(step=step, course_staff=False)
context['step_dates'].append({
'step': step,
'start': start_date if start_date > DISTANT_PAST else None,
'due': due_date if due_date < DISTANT_FUTURE else None,
})
return context
@property
def is_course_staff(self):
"""
......@@ -330,13 +362,17 @@ class OpenAssessmentBlock(
template = get_template('openassessmentblock/oa_error.html')
return Response(template.render(context), content_type='application/html', charset='UTF-8')
def is_closed(self, step=None):
def is_closed(self, step=None, course_staff=None):
"""
Checks if the question is closed.
Determines if the start date is in the future or the end date has
passed. Optionally limited to a particular step in the workflow.
Start/due dates do NOT apply to course staff, since course staff may need to get to
the peer grading step AFTER the submission deadline has passed.
This may not be necessary when we implement a grading interface specifically for course staff.
Kwargs:
step (str): The step in the workflow to check. Options are:
None: check whether the problem as a whole is open.
......@@ -344,6 +380,9 @@ class OpenAssessmentBlock(
"peer-assessment": check whether the peer-assessment section is open.
"self-assessment": check whether the self-assessment section is open.
course_staff (bool): Whether to treat the user as course staff (disable start/due dates).
If not specified, default to the current user's status.
Returns:
tuple of the form (is_closed, reason, start_date, due_date), where
is_closed (bool): indicates whether the step is closed.
......@@ -380,6 +419,12 @@ class OpenAssessmentBlock(
if step == "self-assessment":
open_range = date_ranges[2]
# Course staff always have access to the problem
if course_staff is None:
course_staff = self.is_course_staff
if course_staff:
return False, None, DISTANT_PAST, DISTANT_FUTURE
# Check if we are in the open date range
now = dt.datetime.utcnow().replace(tzinfo=pytz.utc)
......
<openassessment submission_start="2014-03-01" submission_due="2014-04-01">
<title>Open Assessment Test</title>
<prompt>
Given the state of the world today, what do you think should be done to
combat poverty? Please answer in a short essay of 200-300 words.
</prompt>
<rubric>
<prompt>Read for conciseness, clarity of thought, and form.</prompt>
<criterion>
<name>Concise</name>
<prompt>How concise is it?</prompt>
<option points="0">
<name>Neal Stephenson (late)</name>
<explanation>Neal Stephenson explanation</explanation>
</option>
<option points="1">
<name>HP Lovecraft</name>
<explanation>HP Lovecraft explanation</explanation>
</option>
<option points="3">
<name>Robert Heinlein</name>
<explanation>Robert Heinlein explanation</explanation>
</option>
<option points="4">
<name>Neal Stephenson (early)</name>
<explanation>Neal Stephenson (early) explanation</explanation>
</option>
<option points="5">
<name>Earnest Hemingway</name>
<explanation>Earnest Hemingway</explanation>
</option>
</criterion>
<criterion>
<name>Clear-headed</name>
<prompt>How clear is the thinking?</prompt>
<option points="0">
<name>Yogi Berra</name>
<explanation>Yogi Berra explanation</explanation>
</option>
<option points="1">
<name>Hunter S. Thompson</name>
<explanation>Hunter S. Thompson explanation</explanation>
</option>
<option points="2">
<name>Robert Heinlein</name>
<explanation>Robert Heinlein explanation</explanation>
</option>
<option points="3">
<name>Isaac Asimov</name>
<explanation>Isaac Asimov explanation</explanation>
</option>
<option points="10">
<name>Spock</name>
<explanation>Spock explanation</explanation>
</option>
</criterion>
<criterion>
<name>Form</name>
<prompt>Lastly, how is its form? Punctuation, grammar, and spelling all count.</prompt>
<option points="0">
<name>lolcats</name>
<explanation>lolcats explanation</explanation>
</option>
<option points="1">
<name>Facebook</name>
<explanation>Facebook explanation</explanation>
</option>
<option points="2">
<name>Reddit</name>
<explanation>Reddit explanation</explanation>
</option>
<option points="3">
<name>metafilter</name>
<explanation>metafilter explanation</explanation>
</option>
<option points="4">
<name>Usenet, 1996</name>
<explanation>Usenet, 1996 explanation</explanation>
</option>
<option points="5">
<name>The Elements of Style</name>
<explanation>The Elements of Style explanation</explanation>
</option>
</criterion>
</rubric>
<assessments>
<assessment name="peer-assessment" must_grade="5" must_be_graded_by="3" start="2015-01-02" due="2015-04-01"/>
<assessment name="self-assessment" start="2016-01-02" due="2016-04-01"/>
</assessments>
</openassessment>
......@@ -7,6 +7,7 @@ import pytz
from mock import Mock, patch
from openassessment.xblock import openassessmentblock
from openassessment.xblock.resolve_dates import DISTANT_PAST, DISTANT_FUTURE
from openassessment.workflow import api as workflow_api
from .base import XBlockHandlerTestCase, scenario
......@@ -145,6 +146,12 @@ class TestOpenAssessment(XBlockHandlerTestCase):
self.assertEqual(student_item['course_id'], 'test_course')
self.assertEqual(student_item['student_id'], 'test_student')
class TestCourseStaff(XBlockHandlerTestCase):
"""
Tests for course staff debug panel.
"""
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_is_course_staff(self, xblock):
# By default, we shouldn't be course staff
......@@ -189,6 +196,60 @@ class TestOpenAssessment(XBlockHandlerTestCase):
xblock_fragment = self.runtime.render(xblock, "student_view")
self.assertNotIn("course staff information", xblock_fragment.body_html().lower())
@scenario('data/staff_dates_scenario.xml')
def test_staff_debug_dates_table(self, xblock):
# Simulate that we are course staff
xblock.xmodule_runtime = Mock(
course_id='test_course',
anonymous_student_id='test_student',
user_is_staff=True
)
# Get the context for the course staff debug panel
# and check that the dates match the scenario.
context = xblock.staff_debug_template_context()
self.assertEqual(context['step_dates'], [
{
'step': 'submission',
'start': dt.datetime(2014, 3, 1).replace(tzinfo=pytz.utc),
'due': dt.datetime(2014, 4, 1).replace(tzinfo=pytz.utc),
},
{
'step': 'peer-assessment',
'start': dt.datetime(2015, 1, 2).replace(tzinfo=pytz.utc),
'due': dt.datetime(2015, 4, 1).replace(tzinfo=pytz.utc),
},
{
'step': 'self-assessment',
'start': dt.datetime(2016, 1, 2).replace(tzinfo=pytz.utc),
'due': dt.datetime(2016, 4, 1).replace(tzinfo=pytz.utc),
},
])
# Verify that we can render without error
self.runtime.render(xblock, 'student_view')
@scenario('data/basic_scenario.xml')
def test_staff_debug_dates_distant_past_and_future(self, xblock):
# Simulate that we are course staff
xblock.xmodule_runtime = Mock(
course_id='test_course',
anonymous_student_id='test_student',
user_is_staff=True
)
# Get the context for the course staff debug panel
# and check that the dates match the scenario.
context = xblock.staff_debug_template_context()
self.assertEqual(context['step_dates'], [
{'step': 'submission', 'start': None, 'due': None},
{'step': 'peer-assessment', 'start': None, 'due': None},
{'step': 'self-assessment', 'start': None, 'due': None},
])
# Verify that we can render without error
self.runtime.render(xblock, 'student_view')
class TestDates(XBlockHandlerTestCase):
......@@ -433,9 +494,127 @@ class TestDates(XBlockHandlerTestCase):
# If the runtime doesn't provide a published_date field, assume we've been published
self.assertTrue(xblock.is_released())
@scenario('data/basic_scenario.xml')
def test_is_released_course_staff(self, xblock):
# Simulate being course staff
xblock.xmodule_runtime = Mock(user_is_staff=True)
# Not published, should be not released
xblock.published_date = None
self.assertFalse(xblock.is_released())
# Published, should be released
xblock.published_date = dt.datetime(2013, 1, 1).replace(tzinfo=pytz.utc)
self.assertTrue(xblock.is_released())
@scenario('data/staff_dates_scenario.xml')
def test_course_staff_dates(self, xblock):
xblock.start = None
xblock.due = None
# The problem should always be open for course staff
# The following assertions check before/during/after dates
# for submission/peer/self
self.assert_is_closed(
xblock,
dt.datetime(2014, 2, 28, 23, 59, 59).replace(tzinfo=pytz.utc),
"submission", False, None,
DISTANT_PAST, DISTANT_FUTURE,
course_staff=True
)
self.assert_is_closed(
xblock,
dt.datetime(2014, 3, 1, 1, 1, 1).replace(tzinfo=pytz.utc),
"submission", False, None,
DISTANT_PAST, DISTANT_FUTURE,
course_staff=True
)
self.assert_is_closed(
xblock,
dt.datetime(2014, 3, 31, 23, 59, 59).replace(tzinfo=pytz.utc),
"submission", False, None,
DISTANT_PAST, DISTANT_FUTURE,
course_staff=True
)
self.assert_is_closed(
xblock,
dt.datetime(2014, 4, 1, 1, 1, 1, 1).replace(tzinfo=pytz.utc),
"submission", False, None,
DISTANT_PAST, DISTANT_FUTURE,
course_staff=True
)
self.assert_is_closed(
xblock,
dt.datetime(2015, 1, 1, 23, 59, 59).replace(tzinfo=pytz.utc),
"peer-assessment", False, None,
DISTANT_PAST, DISTANT_FUTURE,
course_staff=True
)
self.assert_is_closed(
xblock,
dt.datetime(2015, 1, 2, 1, 1, 1).replace(tzinfo=pytz.utc),
"peer-assessment", False, None,
DISTANT_PAST, DISTANT_FUTURE,
course_staff=True
)
self.assert_is_closed(
xblock,
dt.datetime(2015, 3, 31, 23, 59, 59).replace(tzinfo=pytz.utc),
"peer-assessment", False, None,
DISTANT_PAST, DISTANT_FUTURE,
course_staff=True
)
self.assert_is_closed(
xblock,
dt.datetime(2015, 4, 1, 1, 1, 1, 1).replace(tzinfo=pytz.utc),
"peer-assessment", False, None,
DISTANT_PAST, DISTANT_FUTURE,
course_staff=True
)
self.assert_is_closed(
xblock,
dt.datetime(2016, 1, 1, 23, 59, 59).replace(tzinfo=pytz.utc),
"self-assessment", False, None,
DISTANT_PAST, DISTANT_FUTURE,
course_staff=True
)
self.assert_is_closed(
xblock,
dt.datetime(2016, 1, 2, 1, 1, 1).replace(tzinfo=pytz.utc),
"self-assessment", False, None,
DISTANT_PAST, DISTANT_FUTURE,
course_staff=True
)
self.assert_is_closed(
xblock,
dt.datetime(2016, 3, 31, 23, 59, 59).replace(tzinfo=pytz.utc),
"self-assessment", False, None,
DISTANT_PAST, DISTANT_FUTURE,
course_staff=True
)
self.assert_is_closed(
xblock,
dt.datetime(2016, 4, 1, 1, 1, 1, 1).replace(tzinfo=pytz.utc),
"self-assessment", False, None,
DISTANT_PAST, DISTANT_FUTURE,
course_staff=True
)
def assert_is_closed(
self, xblock, now, step, expected_is_closed, expected_reason,
expected_start, expected_due, released=None
expected_start, expected_due, released=None, course_staff=False,
):
"""
Assert whether the XBlock step is open/closed.
......@@ -451,6 +630,7 @@ class TestDates(XBlockHandlerTestCase):
Kwargs:
released (bool): If set, check whether the XBlock has been released.
course_staff (bool): Whether to treat the user as course staff.
Raises:
AssertionError
......@@ -463,7 +643,7 @@ class TestDates(XBlockHandlerTestCase):
self.addCleanup(datetime_patcher.stop)
mocked_datetime.datetime.utcnow.return_value = now
is_closed, reason, start, due = xblock.is_closed(step=step)
is_closed, reason, start, due = xblock.is_closed(step=step, course_staff=course_staff)
self.assertEqual(is_closed, expected_is_closed)
self.assertEqual(reason, expected_reason)
self.assertEqual(start, expected_start)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment