Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add compatibility with individual date extensions for submissions #2026

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion openassessment/xblock/config_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
USER_STATE_UPLOAD_DATA = 'user_state_upload_data'
RUBRIC_REUSE = 'rubric_reuse'
ENHANCED_STAFF_GRADER = 'enhanced_staff_grader'
DUE_DATE_EXTENSION = 'due_date_extension'

FEATURE_TOGGLES_BY_FLAG_NAME = {
ALL_FILES_URLS: 'ENABLE_ORA_ALL_FILE_URLS',
TEAM_SUBMISSIONS: 'ENABLE_ORA_TEAM_SUBMISSIONS',
USER_STATE_UPLOAD_DATA: 'ENABLE_ORA_USER_STATE_UPLOAD_DATA',
RUBRIC_REUSE: 'ENABLE_ORA_RUBRIC_REUSE',
ENHANCED_STAFF_GRADER: 'ENABLE_ENHANCED_STAFF_GRADER'
ENHANCED_STAFF_GRADER: 'ENABLE_ENHANCED_STAFF_GRADER',
DUE_DATE_EXTENSION: 'ENABLE_ORA_DUE_DATE_EXTENSION',
}


Expand Down Expand Up @@ -153,3 +155,17 @@ def is_enhanced_staff_grader_enabled(self):
# .. toggle_creation_date: 2021-08-29
# .. toggle_tickets: https://openedx.atlassian.net/browse/AU-50
return self.is_feature_enabled(ENHANCED_STAFF_GRADER)

@cached_property
def is_due_date_extension_enabled(self):
"""
Return a boolean indicating the due date extension feature is enabled or not.
"""
# .. toggle_name: FEATURES['ENABLE_ORA_DUE_DATE_EXTENSION']
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Set to True to enable the due date extension feature
# .. toggle_use_cases: circuit_breaker
# .. toggle_creation_date: 2023-08-11
# .. toggle_tickets:
return self.is_feature_enabled(DUE_DATE_EXTENSION)
73 changes: 68 additions & 5 deletions openassessment/xblock/openassessmentblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,73 @@
template = get_template('openassessmentblock/oa_error.html')
return Response(template.render(context), content_type='application/html', charset='UTF-8')

def can_use_date_extensions(self):
"""
Check if the due date extension feature is enabled.

The due date extension feature is enabled if the due date extension
feature flag is enabled and the student has received an date extension.
"""
if not self.is_due_date_extension_enabled:
return False

if not self.due:
return False

if not self.submission_due:
return False

Check warning on line 1083 in openassessment/xblock/openassessmentblock.py

View check run for this annotation

Codecov / codecov/patch

openassessment/xblock/openassessmentblock.py#L1083

Added line #L1083 was not covered by tests

block_due = parse_date_value(self.due, self._)
submission_due = parse_date_value(self.submission_due, self._)

Check warning on line 1086 in openassessment/xblock/openassessmentblock.py

View check run for this annotation

Codecov / codecov/patch

openassessment/xblock/openassessmentblock.py#L1085-L1086

Added lines #L1085 - L1086 were not covered by tests

has_due_date_extension = block_due > submission_due

Check warning on line 1088 in openassessment/xblock/openassessmentblock.py

View check run for this annotation

Codecov / codecov/patch

openassessment/xblock/openassessmentblock.py#L1088

Added line #L1088 was not covered by tests

if not has_due_date_extension:
return False

Check warning on line 1091 in openassessment/xblock/openassessmentblock.py

View check run for this annotation

Codecov / codecov/patch

openassessment/xblock/openassessmentblock.py#L1091

Added line #L1091 was not covered by tests

return True

Check warning on line 1093 in openassessment/xblock/openassessmentblock.py

View check run for this annotation

Codecov / codecov/patch

openassessment/xblock/openassessmentblock.py#L1093

Added line #L1093 was not covered by tests

def date_ranges(self):
"""
Generate a list of dates ranges for the submission and assessment steps.

Returns:
submission_range (tuple): A tuple of the form (start, end) where
start and end are datetime objects.
assesment_ranges (list): A list of tuples of the form (start, end)
where start and end are datetime objects.

"""
if self.can_use_date_extensions():

block_due = parse_date_value(self.due, self._)

Check warning on line 1108 in openassessment/xblock/openassessmentblock.py

View check run for this annotation

Codecov / codecov/patch

openassessment/xblock/openassessmentblock.py#L1108

Added line #L1108 was not covered by tests

submission_range = (self.submission_start, self.due)
assessment_ranges = []

Check warning on line 1111 in openassessment/xblock/openassessmentblock.py

View check run for this annotation

Codecov / codecov/patch

openassessment/xblock/openassessmentblock.py#L1110-L1111

Added lines #L1110 - L1111 were not covered by tests

for assesment in self.valid_assessments:
assesment_due = assesment.get('due')

Check warning on line 1114 in openassessment/xblock/openassessmentblock.py

View check run for this annotation

Codecov / codecov/patch

openassessment/xblock/openassessmentblock.py#L1114

Added line #L1114 was not covered by tests

if assesment_due is None:
assessment_ranges.append((assesment.get('start'), assesment_due))
continue

Check warning on line 1118 in openassessment/xblock/openassessmentblock.py

View check run for this annotation

Codecov / codecov/patch

openassessment/xblock/openassessmentblock.py#L1117-L1118

Added lines #L1117 - L1118 were not covered by tests

parsed_assesment_due = parse_date_value(assesment_due, self._)

Check warning on line 1120 in openassessment/xblock/openassessmentblock.py

View check run for this annotation

Codecov / codecov/patch

openassessment/xblock/openassessmentblock.py#L1120

Added line #L1120 was not covered by tests

is_extended = block_due < parsed_assesment_due

Check warning on line 1122 in openassessment/xblock/openassessmentblock.py

View check run for this annotation

Codecov / codecov/patch

openassessment/xblock/openassessmentblock.py#L1122

Added line #L1122 was not covered by tests

if is_extended:
assessment_ranges.append((assesment.get('start'), assesment_due))

Check warning on line 1125 in openassessment/xblock/openassessmentblock.py

View check run for this annotation

Codecov / codecov/patch

openassessment/xblock/openassessmentblock.py#L1125

Added line #L1125 was not covered by tests
else:
assessment_ranges.append((assesment.get('start'), self.due))

Check warning on line 1127 in openassessment/xblock/openassessmentblock.py

View check run for this annotation

Codecov / codecov/patch

openassessment/xblock/openassessmentblock.py#L1127

Added line #L1127 was not covered by tests
else:
submission_range = (self.submission_start, self.submission_due)
assessment_ranges = [
(asmnt.get('start'), asmnt.get('due'))
for asmnt in self.valid_assessments
]
return submission_range, assessment_ranges

def is_closed(self, step=None, course_staff=None):
"""
Checks if the question is closed.
Expand Down Expand Up @@ -1106,11 +1173,7 @@
datetime.datetime(2015, 3, 27, 22, 7, 38, 788861)

"""
submission_range = (self.submission_start, self.submission_due)
assessment_ranges = [
(asmnt.get('start'), asmnt.get('due'))
for asmnt in self.valid_assessments
]
submission_range, assessment_ranges = self.date_ranges()

# Resolve unspecified dates and date strings to datetimes
start, due, date_ranges = resolve_dates(
Expand Down
5 changes: 5 additions & 0 deletions openassessment/xblock/test/test_grade.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import copy
import json
from unittest.mock import Mock

import ddt

Expand All @@ -22,6 +23,7 @@ class TestGrade(XBlockHandlerTestCase, SubmitAssessmentsMixin):
@scenario('data/grade_scenario.xml', user_id='Greggs')
def test_render_grade(self, xblock):
# Submit, assess, and render the grade view
xblock.is_due_date_extension_enabled = Mock(return_value=True)
self.create_submission_and_assessments(
xblock, self.SUBMISSION, self.PEERS, PEER_ASSESSMENTS, SELF_ASSESSMENT
)
Expand Down Expand Up @@ -56,6 +58,8 @@ def test_render_grade(self, xblock):
@scenario('data/grade_scenario_self_only.xml', user_id='Greggs')
def test_render_grade_self_only(self, xblock):
# Submit, assess, and render the grade view
xblock.is_due_date_extension_enabled = Mock(return_value=True)

self.create_submission_and_assessments(
xblock, self.SUBMISSION, [], [], SELF_ASSESSMENT,
waiting_for_peer=True
Expand Down Expand Up @@ -402,6 +406,7 @@ def _test_incomplete_helper(self, xblock, peers, self_assessment):
"""
Check assessment completition status is shown correctly on assessment page.
"""
xblock.is_due_date_extension_enabled = Mock(return_value=True)
self.create_submission_and_assessments(
xblock, self.SUBMISSION, peers, [PEER_ASSESSMENTS[0]] if peers else [], self_assessment
)
Expand Down
3 changes: 3 additions & 0 deletions openassessment/xblock/test/test_openassessment.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def test_load_student_view(self, xblock):
xblock_fragment = self.runtime.render(xblock, "student_view")
self.assertIn("OpenAssessmentBlock", xblock_fragment.body_html())

xblock.is_due_date_extension_enabled = Mock(return_value=True)

# Validate Submission Rendering.
submission_response = xblock.render_submission({})
self.assertIsNotNone(submission_response)
Expand Down Expand Up @@ -292,6 +294,7 @@ def test_load_student_view_with_dates(self, time_zone, expected_date):
time_zone_fn.return_value['user_timezone'] = pytz.timezone(time_zone)

xblock = self.load_scenario('data/dates_scenario.xml')
xblock.is_due_date_extension_enabled = Mock(return_value=True)
xblock_fragment = self.runtime.render(xblock, "student_view")
self.assertIn("OpenAssessmentBlock", xblock_fragment.body_html())

Expand Down
4 changes: 4 additions & 0 deletions openassessment/xblock/test/test_save_files_descriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def test_save_files_descriptions(self, xblock):
# We're not worried about looking up shared uploads in this test
xblock.has_team = mock.Mock(return_value=False)

xblock.is_due_date_extension_enabled = mock.Mock(return_value=True)

xblock.xmodule_runtime = mock.Mock(
user_is_staff=False,
user_is_beta_tester=False,
Expand Down Expand Up @@ -66,6 +68,8 @@ def test_append_files_descriptions(self, xblock):
# We're not worried about looking up shared uploads in this test
xblock.has_team = mock.Mock(return_value=False)

xblock.is_due_date_extension_enabled = mock.Mock(return_value=True)

xblock.xmodule_runtime = mock.Mock(
user_is_staff=False,
user_is_beta_tester=False,
Expand Down
4 changes: 4 additions & 0 deletions openassessment/xblock/test/test_save_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class SaveResponseTest(XBlockHandlerTestCase):
def test_default_saved_response_blank(self, xblock):
xblock.get_team_info = mock.Mock(return_value={})

xblock.is_due_date_extension_enabled = mock.Mock(return_value=True)

xblock.xmodule_runtime = mock.Mock(
user_is_staff=False,
user_is_beta_tester=False,
Expand All @@ -34,6 +36,8 @@ def test_default_saved_response_blank(self, xblock):
def test_save_response(self, xblock, data):
xblock.get_team_info = mock.Mock(return_value={})

xblock.is_due_date_extension_enabled = mock.Mock(return_value=True)

xblock.xmodule_runtime = mock.Mock(
user_is_staff=False,
user_is_beta_tester=False,
Expand Down
2 changes: 2 additions & 0 deletions openassessment/xblock/test/test_self.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,8 @@ def test_integration(self, xblock):
xblock.get_student_item_dict(), ("Test submission 1", "Test submission 2")
)

xblock.is_due_date_extension_enabled = mock.Mock(return_value=True)

xblock.get_workflow_info = mock.Mock(return_value={
'status': 'self', 'submission_uuid': submission['uuid']
})
Expand Down
8 changes: 8 additions & 0 deletions openassessment/xblock/test/test_staff_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def test_is_course_staff(self, xblock):
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_course_staff_area(self, xblock):
# If we're not course staff, we shouldn't see the staff area
xblock.is_due_date_extension_enabled = Mock(return_value=True)
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, False, False, "Bob"
)
Expand All @@ -140,6 +141,7 @@ def test_course_staff_area(self, xblock):

@scenario('data/basic_scenario.xml', user_id='Bob')
def test_view_in_studio_button(self, xblock):
xblock.is_due_date_extension_enabled = Mock(return_value=True)
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, False, False, "Bob"
)
Expand Down Expand Up @@ -210,6 +212,8 @@ def test_hide_course_staff_area_in_studio_preview(self, xblock):
@scenario('data/staff_dates_scenario.xml', user_id='Bob')
def test_staff_area_dates_table(self, xblock):
# Simulate that we are course staff
xblock.is_due_date_extension_enabled = Mock(return_value=True)

xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, False, "Bob"
)
Expand All @@ -232,6 +236,8 @@ def test_staff_area_dates_table(self, xblock):
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_staff_area_dates_distant_past_and_future(self, xblock):
# Simulate that we are course staff
xblock.is_due_date_extension_enabled = Mock(return_value=True)

xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, False, "Bob"
)
Expand Down Expand Up @@ -1116,6 +1122,7 @@ def test_staff_area_student_user_state_not_used(self, xblock, waffle_patch):

@scenario('data/team_submission.xml', user_id='Bob')
def test_staff_area_has_team_info(self, xblock):
xblock.is_due_date_extension_enabled = Mock(return_value=True)
# Given that we are course staff, managing a team assignment
self._setup_xblock_and_create_submission(xblock)

Expand All @@ -1140,6 +1147,7 @@ def test_staff_area_student_info_has_team_info(self, xblock):
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_staff_area_has_team_info_individual(self, xblock):
# Given that we are course staff, managing an individual assignment
xblock.is_due_date_extension_enabled = Mock(return_value=True)
self._setup_xblock_and_create_submission(xblock)

# When I get the staff context
Expand Down
4 changes: 4 additions & 0 deletions openassessment/xblock/test/test_student_training.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ def test_updates_workflow(self, xblock, data):

@scenario('data/feedback_only_criterion_student_training.xml', user_id='Bob')
def test_feedback_only_criterion(self, xblock):
xblock.is_due_date_extension_enabled = Mock(return_value=True)
xblock.create_submission(xblock.get_student_item_dict(), self.SUBMISSION)
self.request(xblock, 'render_student_training', json.dumps({}))

Expand Down Expand Up @@ -264,6 +265,7 @@ def test_no_submission(self, xblock):
'Grammar': 'Poor'
}
}
xblock.is_due_date_extension_enabled = Mock(return_value=True)
resp = self.request(xblock, 'training_assess', json.dumps(selected_data))
self.assertIn("Your scores could not be checked", resp.decode('utf-8'))

Expand Down Expand Up @@ -315,11 +317,13 @@ def test_no_student_training_defined(self, xblock):

@scenario('data/student_training.xml', user_id="Plato")
def test_no_submission(self, xblock):
xblock.is_due_date_extension_enabled = Mock(return_value=True)
resp = self.request(xblock, 'render_student_training', json.dumps({}))
self.assertIn("Not Available", resp.decode('utf-8'))

@scenario('data/student_training.xml')
def test_studio_preview(self, xblock):
xblock.is_due_date_extension_enabled = Mock(return_value=True)
resp = self.request(xblock, 'render_student_training', json.dumps({}))
self.assertIn("Not Available", resp.decode('utf-8'))

Expand Down
10 changes: 10 additions & 0 deletions openassessment/xblock/test/test_submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,19 +150,22 @@ def test_cannot_submit_in_preview_mode(self, xblock):

@scenario('data/over_grade_scenario.xml', user_id='Alice')
def test_closed_submissions(self, xblock):
xblock.is_due_date_extension_enabled = Mock(return_value=True)
resp = self.request(xblock, 'render_submission', json.dumps({}))
self.assertIn("Incomplete", resp.decode('utf-8'))

@scenario('data/line_breaks.xml')
def test_prompt_line_breaks(self, xblock):
# Verify that prompts with multiple lines retain line breaks
# (backward compatibility in case if prompt_type == 'text')
xblock.is_due_date_extension_enabled = Mock(return_value=True)
resp = self.request(xblock, 'render_submission', json.dumps({}))
expected_prompt = "<p><br>Line 1</p><p>Line 2</p><p>Line 3<br></p>"
self.assertIn(expected_prompt, resp.decode('utf-8'))

@scenario('data/prompt_html.xml')
def test_prompt_html_to_text(self, xblock):
xblock.is_due_date_extension_enabled = Mock(return_value=True)
resp = self.request(xblock, 'render_submission', json.dumps({}))
expected_prompt = "<code><strong>Question 123</strong></code>"
self.assertIn(expected_prompt, resp.decode('utf-8'))
Expand Down Expand Up @@ -948,6 +951,8 @@ def test_open_saved_response(self, xblock):

xblock.file_manager.append_uploads(*file_uploads)

xblock.is_due_date_extension_enabled = Mock(return_value=True)

# Save a response
payload = json.dumps({'submission': ('A man must have a code', 'A man must have an umbrella too.')})
resp = self.request(xblock, 'save_submission', payload, response_format='json')
Expand Down Expand Up @@ -1031,6 +1036,8 @@ def test_open_saved_response_deleted_file_uploads(self, xblock):
anonymous_student_id='Pmn'
)

xblock.is_due_date_extension_enabled = Mock(return_value=True)

# delete file-2
with patch('openassessment.fileupload.api.remove_file'):
xblock.file_manager.delete_upload(1)
Expand Down Expand Up @@ -1197,6 +1204,7 @@ def test_render_shared_files(self, xblock, mock_get_download_url):
Test that we render files owned by Valchek and
their teammates when files are shared with a team.
"""
xblock.is_due_date_extension_enabled = Mock(return_value=True)
xblock.file_manager.get_uploads = Mock(return_value=[
api.FileUpload(
description='file 1 description',
Expand Down Expand Up @@ -1600,6 +1608,8 @@ def test_closed_graded(self, xblock):
@scenario('data/submission_open.xml', user_id="Bob")
def test_integration(self, xblock):
# Expect that the response step is open and displays the deadline
xblock.is_due_date_extension_enabled = Mock(return_value=True)

resp = self.request(xblock, 'render_submission', json.dumps({}))
self.assertIn('Enter your response to the prompt', resp.decode('utf-8'))
self.assertIn('2999-05-06T00:00:00+00:00', resp.decode('utf-8'))
Expand Down
5 changes: 4 additions & 1 deletion settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@

# Set to True to enable copying/reusing rubric data
# See: https://openedx.atlassian.net/browse/EDUCATOR-5751
'ENABLE_ORA_RUBRIC_REUSE': False
'ENABLE_ORA_RUBRIC_REUSE': False,

# Set to True to enable individual due date extension for ORA
'ENABLE_ORA_DUE_DATE_EXTENSION': False,
}

# disable indexing on history_date
Expand Down
Loading