diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 828a09f53d..5b48ec8a81 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -698,7 +698,7 @@ class MiscCourseTests(ContentStoreTestCase): self.check_components_on_page( ADVANCED_COMPONENT_TYPES, ['Word cloud', 'Annotation', 'Text Annotation', 'Video Annotation', 'Image Annotation', - 'Open Response Assessment', 'Peer Grading Interface', 'split_test'], + 'split_test'], ) @ddt.data('/Fake/asset/displayname', '\\Fake\\asset\\displayname') diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 53af73fdb0..f880df7b27 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -771,7 +771,7 @@ class CourseMetadataEditingTest(CourseTestCase): { "advertised_start": {"value": "start A"}, "days_early_for_beta": {"value": 2}, - "advanced_modules": {"value": ['combinedopenended']}, + "advanced_modules": {"value": ['notes']}, }, user=self.user ) @@ -781,7 +781,7 @@ class CourseMetadataEditingTest(CourseTestCase): # Tab gets tested in test_advanced_settings_munge_tabs self.assertIn('advanced_modules', test_model, 'Missing advanced_modules') - self.assertEqual(test_model['advanced_modules']['value'], ['combinedopenended'], 'advanced_module is not updated') + self.assertEqual(test_model['advanced_modules']['value'], ['notes'], 'advanced_module is not updated') def test_validate_from_json_wrong_inputs(self): # input incorrectly formatted data @@ -905,48 +905,21 @@ class CourseMetadataEditingTest(CourseTestCase): """ Test that adding and removing specific advanced components adds and removes tabs. """ - open_ended_tab = {"type": "open_ended", "name": "Open Ended Panel"} - peer_grading_tab = {"type": "peer_grading", "name": "Peer grading"} - # First ensure that none of the tabs are visible - self.assertNotIn(open_ended_tab, self.course.tabs) - self.assertNotIn(peer_grading_tab, self.course.tabs) self.assertNotIn(self.notes_tab, self.course.tabs) - # Now add the "combinedopenended" component and verify that the tab has been added - self.client.ajax_post(self.course_setting_url, { - ADVANCED_COMPONENT_POLICY_KEY: {"value": ["combinedopenended"]} - }) - course = modulestore().get_course(self.course.id) - self.assertIn(open_ended_tab, course.tabs) - self.assertIn(peer_grading_tab, course.tabs) - self.assertNotIn(self.notes_tab, course.tabs) - - # Now enable student notes and verify that the "My Notes" tab has also been added - self.client.ajax_post(self.course_setting_url, { - ADVANCED_COMPONENT_POLICY_KEY: {"value": ["combinedopenended", "notes"]} - }) - course = modulestore().get_course(self.course.id) - self.assertIn(open_ended_tab, course.tabs) - self.assertIn(peer_grading_tab, course.tabs) - self.assertIn(self.notes_tab, course.tabs) - - # Now remove the "combinedopenended" component and verify that the tab is gone + # Now enable student notes and verify that the "My Notes" tab has been added self.client.ajax_post(self.course_setting_url, { ADVANCED_COMPONENT_POLICY_KEY: {"value": ["notes"]} }) course = modulestore().get_course(self.course.id) - self.assertNotIn(open_ended_tab, course.tabs) - self.assertNotIn(peer_grading_tab, course.tabs) self.assertIn(self.notes_tab, course.tabs) - # Finally disable student notes and verify that the "My Notes" tab is gone + # Disable student notes and verify that the "My Notes" tab is gone self.client.ajax_post(self.course_setting_url, { ADVANCED_COMPONENT_POLICY_KEY: {"value": [""]} }) course = modulestore().get_course(self.course.id) - self.assertNotIn(open_ended_tab, course.tabs) - self.assertNotIn(peer_grading_tab, course.tabs) self.assertNotIn(self.notes_tab, course.tabs) def test_advanced_components_munge_tabs_validation_failure(self): diff --git a/cms/djangoapps/contentstore/tests/test_import.py b/cms/djangoapps/contentstore/tests/test_import.py index b48a80cf51..a4675632a2 100644 --- a/cms/djangoapps/contentstore/tests/test_import.py +++ b/cms/djangoapps/contentstore/tests/test_import.py @@ -219,26 +219,6 @@ class ContentStoreImportTest(SignalDisconnectTestMixin, ModuleStoreTestCase): conditional_module.show_tag_list ) - def test_rewrite_reference(self): - module_store = modulestore() - target_id = module_store.make_course_key('testX', 'peergrading_copy', 'copy_run') - import_course_from_xml( - module_store, - self.user.id, - TEST_DATA_DIR, - ['open_ended'], - target_id=target_id, - create_if_not_present=True - ) - peergrading_module = module_store.get_item( - target_id.make_usage_key('peergrading', 'PeerGradingLinked') - ) - self.assertIsNotNone(peergrading_module) - self.assertEqual( - target_id.make_usage_key('combinedopenended', 'SampleQuestion'), - peergrading_module.link_to_location - ) - def test_rewrite_reference_value_dict_published(self): """ Test rewriting references in ReferenceValueDict, specifically with published content. diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 0cc6011024..05bb06ba50 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -30,11 +30,11 @@ from student.auth import has_course_author_access from django.utils.translation import ugettext as _ from models.settings.course_grading import CourseGradingModel -__all__ = ['OPEN_ENDED_COMPONENT_TYPES', - 'ADVANCED_COMPONENT_POLICY_KEY', - 'container_handler', - 'component_handler' - ] +__all__ = [ + 'ADVANCED_COMPONENT_POLICY_KEY', + 'container_handler', + 'component_handler' +] log = logging.getLogger(__name__) @@ -43,7 +43,6 @@ COMPONENT_TYPES = ['discussion', 'html', 'problem', 'video'] # Constants for determining if these components should be enabled for this course SPLIT_TEST_COMPONENT_TYPE = 'split_test' -OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] NOTE_COMPONENT_TYPES = ['notes'] if settings.FEATURES.get('ALLOW_ALL_ADVANCED_COMPONENTS'): diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index 0d3e5932b2..b9004217cb 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -10,6 +10,7 @@ import pytz from django.conf import settings from django.core.exceptions import PermissionDenied +from django.test.utils import override_settings from django.utils.translation import ugettext as _ from contentstore.courseware_index import CoursewareSearchIndexer, SearchIndexingError @@ -440,6 +441,7 @@ class TestCourseOutline(CourseTestCase): info['block_types_enabled'], any(component in advanced_modules for component in deprecated_block_types) ) + self.assertItemsEqual(info['blocks'], expected_blocks) self.assertEqual( info['advance_settings_url'], @@ -455,27 +457,29 @@ class TestCourseOutline(CourseTestCase): """ Verify deprecated warning info for single deprecated feature. """ - block_types = settings.DEPRECATED_BLOCK_TYPES - course_module = modulestore().get_item(self.course.location) - self._create_test_data(course_module, create_blocks=True, block_types=block_types, publish=publish) - info = _deprecated_blocks_info(course_module, block_types) - self._verify_deprecated_info( - course_module.id, - course_module.advanced_modules, - info, - block_types - ) + block_types = ['notes'] + with override_settings(DEPRECATED_BLOCK_TYPES=block_types): + course_module = modulestore().get_item(self.course.location) + self._create_test_data(course_module, create_blocks=True, block_types=block_types, publish=publish) + info = _deprecated_blocks_info(course_module, block_types) + self._verify_deprecated_info( + course_module.id, + course_module.advanced_modules, + info, + block_types + ) def test_verify_deprecated_warning_message_with_multiple_features(self): """ Verify deprecated warning info for multiple deprecated features. """ - block_types = ['peergrading', 'combinedopenended', 'openassessment'] - course_module = modulestore().get_item(self.course.location) - self._create_test_data(course_module, create_blocks=True, block_types=block_types) + block_types = ['notes', 'lti'] + with override_settings(DEPRECATED_BLOCK_TYPES=block_types): + course_module = modulestore().get_item(self.course.location) + self._create_test_data(course_module, create_blocks=True, block_types=block_types) - info = _deprecated_blocks_info(course_module, block_types) - self._verify_deprecated_info(course_module.id, course_module.advanced_modules, info, block_types) + info = _deprecated_blocks_info(course_module, block_types) + self._verify_deprecated_info(course_module.id, course_module.advanced_modules, info, block_types) @ddt.data( {'delete_vertical': True}, @@ -492,7 +496,7 @@ class TestCourseOutline(CourseTestCase): un-published block(s). This behavior should be same if we delete unpublished vertical or problem. """ - block_types = ['peergrading'] + block_types = ['notes'] course_module = modulestore().get_item(self.course.location) vertical1 = ItemFactory.create( @@ -500,8 +504,8 @@ class TestCourseOutline(CourseTestCase): ) problem1 = ItemFactory.create( parent_location=vertical1.location, - category='peergrading', - display_name='peergrading problem in vert1', + category='notes', + display_name='notes problem in vert1', publish_item=False ) @@ -515,8 +519,8 @@ class TestCourseOutline(CourseTestCase): ) ItemFactory.create( parent_location=vertical2.location, - category='peergrading', - display_name='peergrading problem in vert2', + category='notes', + display_name='notes problem in vert2', pubish_item=True ) # At this point CourseStructure will contain both the above @@ -526,8 +530,8 @@ class TestCourseOutline(CourseTestCase): self.assertItemsEqual( info['blocks'], [ - [reverse_usage_url('container_handler', vertical1.location), 'peergrading problem in vert1'], - [reverse_usage_url('container_handler', vertical2.location), 'peergrading problem in vert2'] + [reverse_usage_url('container_handler', vertical1.location), 'notes problem in vert1'], + [reverse_usage_url('container_handler', vertical2.location), 'notes problem in vert2'] ] ) @@ -542,7 +546,7 @@ class TestCourseOutline(CourseTestCase): # There shouldn't be any info present about un-published vertical1 self.assertEqual( info['blocks'], - [[reverse_usage_url('container_handler', vertical2.location), 'peergrading problem in vert2']] + [[reverse_usage_url('container_handler', vertical2.location), 'notes problem in vert2']] ) diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index ce923e47d1..6ecc9f97eb 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -1388,28 +1388,28 @@ class TestComponentTemplates(CourseTestCase): Test the handling of advanced problem templates. """ problem_templates = self.get_templates_of_type('problem') - ora_template = self.get_template(problem_templates, u'Peer Assessment') - self.assertIsNotNone(ora_template) - self.assertEqual(ora_template.get('category'), 'openassessment') - self.assertIsNone(ora_template.get('boilerplate_name', None)) + circuit_template = self.get_template(problem_templates, u'Circuit Schematic Builder') + self.assertIsNotNone(circuit_template) + self.assertEqual(circuit_template.get('category'), 'problem') + self.assertEqual(circuit_template.get('boilerplate_name'), 'circuitschematic.yaml') - @patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', ["combinedopenended", "peergrading"]) - def test_ora1_no_advance_component_button(self): + @patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', ["poll", "survey"]) + def test_deprecated_no_advance_component_button(self): """ - Test that there will be no `Advanced` button on unit page if `combinedopenended` and `peergrading` are - deprecated provided that there are only 'combinedopenended', 'peergrading' modules in `Advanced Module List` + Test that there will be no `Advanced` button on unit page if units are + deprecated provided that they are the only modules in `Advanced Module List` """ - self.course.advanced_modules.extend(['combinedopenended', 'peergrading']) + self.course.advanced_modules.extend(['poll', 'survey']) templates = get_component_templates(self.course) button_names = [template['display_name'] for template in templates] self.assertNotIn('Advanced', button_names) - @patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', ["combinedopenended", "peergrading"]) - def test_cannot_create_ora1_problems(self): + @patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', ["poll", "survey"]) + def test_cannot_create_deprecated_problems(self): """ - Test that we can't create ORA1 problems if `combinedopenended` and `peergrading` are deprecated + Test that we can't create problems if they are deprecated """ - self.course.advanced_modules.extend(['annotatable', 'combinedopenended', 'peergrading']) + self.course.advanced_modules.extend(['annotatable', 'poll', 'survey']) templates = get_component_templates(self.course) button_names = [template['display_name'] for template in templates] self.assertIn('Advanced', button_names) @@ -1418,17 +1418,17 @@ class TestComponentTemplates(CourseTestCase): self.assertEqual(template_display_names, ['Annotation']) @patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', []) - def test_create_ora1_problems(self): + def test_create_non_deprecated_problems(self): """ - Test that we can create ORA1 problems if `combinedopenended` and `peergrading` are not deprecated + Test that we can create problems if they are not deprecated """ - self.course.advanced_modules.extend(['annotatable', 'combinedopenended', 'peergrading']) + self.course.advanced_modules.extend(['annotatable', 'poll', 'survey']) templates = get_component_templates(self.course) button_names = [template['display_name'] for template in templates] self.assertIn('Advanced', button_names) self.assertEqual(len(templates[0]['templates']), 3) template_display_names = [template['display_name'] for template in templates[0]['templates']] - self.assertEqual(template_display_names, ['Annotation', 'Open Response Assessment', 'Peer Grading Interface']) + self.assertEqual(template_display_names, ['Annotation', 'Poll', 'Survey']) @ddt.ddt diff --git a/cms/envs/bok_choy.auth.json b/cms/envs/bok_choy.auth.json index e0b60afbd3..79dbf904c1 100644 --- a/cms/envs/bok_choy.auth.json +++ b/cms/envs/bok_choy.auth.json @@ -81,14 +81,6 @@ } } }, - "OPEN_ENDED_GRADING_INTERFACE": { - "grading_controller": "grading_controller", - "password": "password", - "peer_grading": "peer_grading", - "staff_grading": "staff_grading", - "url": "http://localhost:18060/", - "username": "lms" - }, "DJFS": { "type": "s3fs", "bucket": "test", diff --git a/cms/envs/bok_choy.env.json b/cms/envs/bok_choy.env.json index d9856eec73..98dfbd0fd1 100644 --- a/cms/envs/bok_choy.env.json +++ b/cms/envs/bok_choy.env.json @@ -104,5 +104,9 @@ "THEME_NAME": "", "TIME_ZONE": "America/New_York", "WIKI_ENABLED": true, - "OAUTH_OIDC_ISSUER": "https://www.example.com/oauth2" + "OAUTH_OIDC_ISSUER": "https://www.example.com/oauth2", + "DEPRECATED_BLOCK_TYPES": [ + "poll", + "survey" + ] } diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index 765920aba1..ff95191919 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -96,6 +96,9 @@ FEATURES['ENABLE_VIDEO_BUMPER'] = True # Enable video bumper in Studio settings # Enable partner support link in Studio footer FEATURES['PARTNER_SUPPORT_EMAIL'] = 'partner-support@example.com' +# Disable some block types to test block deprecation logic +DEPRECATED_BLOCK_TYPES = ['poll', 'survey'] + ########################### Entrance Exams ################################# FEATURES['ENTRANCE_EXAMS'] = True diff --git a/cms/envs/common.py b/cms/envs/common.py index 2f203dc4d3..bcd96c52b4 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1016,8 +1016,6 @@ ADVANCED_COMPONENT_TYPES = [ 'rate', # Allows up-down voting of course content. See https://github.com/pmitros/RateXBlock 'split_test', - 'combinedopenended', - 'peergrading', 'notes', 'schoolyourself_review', 'schoolyourself_lesson', diff --git a/cms/templates/widgets/open-ended-edit.html b/cms/templates/widgets/open-ended-edit.html deleted file mode 100644 index f7d157d010..0000000000 --- a/cms/templates/widgets/open-ended-edit.html +++ /dev/null @@ -1,97 +0,0 @@ -
'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author -
- -- Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading. -
- -'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author -
- -- Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading. -
- [prompt] - [rubric] - + Ideas - - Difficult for the reader to discern the main idea. Too brief or too repetitive to establish or maintain a focus. - - Attempts a main idea. Sometimes loses focus or ineffectively displays focus. - - Presents a unifying theme or main idea, but may include minor tangents. Stays somewhat focused on topic and task. - - Presents a unifying theme or main idea without going off on tangents. Stays completely focused on topic and task. - + Content - - Includes little information with few or no details or unrelated details. Unsuccessful in attempts to explore any facets of the topic. - - Includes little information and few or no details. Explores only one or two facets of the topic. - - Includes sufficient information and supporting details. (Details may not be fully developed; ideas may be listed.) Explores some facets of the topic. - - Includes in-depth information and exceptional supporting details that are fully developed. Explores all facets of the topic. - + Organization - - Ideas organized illogically, transitions weak, and response difficult to follow. - - Attempts to logically organize ideas. Attempts to progress in an order that enhances meaning, and demonstrates use of transitions. - - Ideas organized logically. Progresses in an order that enhances meaning. Includes smooth transitions. - + Style - - Contains limited vocabulary, with many words used incorrectly. Demonstrates problems with sentence patterns. - - Contains basic vocabulary, with words that are predictable and common. Contains mostly simple sentences (although there may be an attempt at more varied sentence patterns). - - Includes vocabulary to make explanations detailed and precise. Includes varied sentence patterns, including complex sentences. - + Voice - - Demonstrates language and tone that may be inappropriate to task and reader. - - Demonstrates an attempt to adjust language and tone to task and reader. - - Demonstrates effective adjustment of language and tone to task and reader. - [rubric] - [tasks] - (Self), ({4-12}AI), ({9-12}Peer) - [tasks] - - """), - scope=Scope.settings - ) - - -class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): - """ - This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc). - It transitions between problems, and support arbitrary ordering. - Each combined open ended module contains one or multiple "child" modules. - Child modules track their own state, and can transition between states. They also implement get_html and - handle_ajax. - The combined open ended module transitions between child modules as appropriate, tracks its own state, and passess - ajax requests from the browser to the child module or handles them itself (in the cases of reset and next problem) - ajax actions implemented by all children are: - 'save_answer' -- Saves the student answer - 'save_assessment' -- Saves the student assessment (or external grader assessment) - 'save_post_assessment' -- saves a post assessment (hint, feedback on feedback, etc) - ajax actions implemented by combined open ended module are: - 'reset' -- resets the whole combined open ended module and returns to the first child module - 'next_problem' -- moves to the next child module - 'get_results' -- gets results from a given child module - - Types of children. Task is synonymous with child module, so each combined open ended module - incorporates multiple children (tasks): - openendedmodule - selfassessmentmodule - - CombinedOpenEndedModule.__init__ takes the same arguments as xmodule.x_module:XModule.__init__ - """ - STATE_VERSION = 1 - - # states - INITIAL = 'initial' - ASSESSING = 'assessing' - INTERMEDIATE_DONE = 'intermediate_done' - DONE = 'done' - - icon_class = 'problem' - - js = { - 'coffee': [ - resource_string(__name__, 'js/src/combinedopenended/display.coffee'), - resource_string(__name__, 'js/src/javascript_loader.coffee'), - ], - 'js': [ - resource_string(__name__, 'js/src/collapsible.js'), - ] - } - js_module_name = "CombinedOpenEnded" - - css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]} - - def __init__(self, *args, **kwargs): - """ - Definition file should have one or many task blocks, a rubric block, and a prompt block. - - See DEFAULT_DATA for a sample. - - """ - super(CombinedOpenEndedModule, self).__init__(*args, **kwargs) - - self.system.set('location', self.location) - - if self.task_states is None: - self.task_states = [] - - if self.old_task_states is None: - self.old_task_states = [] - - version_tuple = VERSION_TUPLES[self.version] - - self.student_attributes = version_tuple.student_attributes - self.settings_attributes = version_tuple.settings_attributes - - attributes = self.student_attributes + self.settings_attributes - - static_data = {} - instance_state = {k: getattr(self, k) for k in attributes} - self.child_descriptor = version_tuple.descriptor(self.system) - self.child_definition = version_tuple.descriptor.definition_from_xml(etree.fromstring(self.data), self.system) - self.child_module = version_tuple.module(self.system, self.location, self.child_definition, self.child_descriptor, - instance_state=instance_state, static_data=static_data, - attributes=attributes) - self.save_instance_data() - - def get_html(self): - self.save_instance_data() - return_value = self.child_module.get_html() - return return_value - - def handle_ajax(self, dispatch, data): - self.save_instance_data() - return_value = self.child_module.handle_ajax(dispatch, data) - self.save_instance_data() - return return_value - - def get_instance_state(self): - return self.child_module.get_instance_state() - - def get_score(self): - return self.child_module.get_score() - - def max_score(self): - return self.child_module.max_score() - - def get_progress(self): - return self.child_module.get_progress() - - @property - def due_date(self): - return self.child_module.due_date - - def save_instance_data(self): - for attribute in self.student_attributes: - setattr(self, attribute, getattr(self.child_module, attribute)) - - def validate(self): - """ - Message for either error or warning validation message/s. - - Returns message and type. Priority given to error type message. - """ - return self.descriptor.validate() - - -class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor): - """ - Module for adding combined open ended questions - """ - mako_template = "widgets/open-ended-edit.html" - module_class = CombinedOpenEndedModule - - has_score = True - always_recalculate_grades = True - template_dir_name = "combinedopenended" - - #Specify whether or not to pass in S3 interface - needs_s3_interface = True - - #Specify whether or not to pass in open ended interface - needs_open_ended_interface = True - - js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/edit.coffee')]} - js_module_name = "OpenEndedMarkdownEditingDescriptor" - css = {'scss': [resource_string(__name__, 'css/editor/edit.scss'), resource_string(__name__, 'css/combinedopenended/edit.scss')]} - - metadata_translations = { - 'is_graded': 'graded', - 'attempts': 'max_attempts', - } - - def get_context(self): - _context = RawDescriptor.get_context(self) - _context.update({'markdown': self.markdown, - 'enable_markdown': self.markdown is not None}) - return _context - - @property - def non_editable_metadata_fields(self): - non_editable_fields = super(CombinedOpenEndedDescriptor, self).non_editable_metadata_fields - non_editable_fields.extend([CombinedOpenEndedDescriptor.due, CombinedOpenEndedDescriptor.graceperiod, - CombinedOpenEndedDescriptor.markdown, CombinedOpenEndedDescriptor.version]) - return non_editable_fields - - # Proxy to CombinedOpenEndedModule so that external callers don't have to know if they're working - # with a module or a descriptor - child_module = module_attr('child_module') - - def validate(self): - """ - Validates the state of this instance. This is the override of the general XBlock method, - and it will also ask its superclass to validate. - """ - validation = super(CombinedOpenEndedDescriptor, self).validate() - validation = StudioValidation.copy(validation) - - i18n_service = self.runtime.service(self, "i18n") - - validation.summary = StudioValidationMessage( - StudioValidationMessage.ERROR, - i18n_service.ugettext( - "ORA1 is no longer supported. To use this assessment, " - "replace this ORA1 component with an ORA2 component." - ) - ) - return validation diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss deleted file mode 100644 index 71908f9e5e..0000000000 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ /dev/null @@ -1,993 +0,0 @@ -// lms - xmodule - combinedopenended -// ==================== - -h2 { - margin-top: 0; - margin-bottom: ($baseline*0.75); - - &.problem-header { - section.staff { - margin-top: ($baseline*1.5); - font-size: 80%; - } - } - - @media print { - display: block; - width: auto; - border-right: 0; - } -} - - // Problem Header -div.name{ - padding-bottom: ($baseline*0.75); - - h2 { - display: inline; - } - - .progress-container { - display: inline; - float: right; - padding-top: 3px; - } -} - -.inline-error { - color: darken($error-color, 10%); -} - -section.combined-open-ended { - @include clearfix(); - - .written-feedback { - position: relative; - margin: 0; - height: 150px; - border: 1px solid lightgray; - padding: ($baseline/4); - resize: vertical; - width: 99%; - overflow: auto; - - .del { - text-decoration: line-through; - background-color: #ffc3c3; - } - .ins { - background-color: #c3ffc3; - } - } -} - - -div.problemwrapper { - border: 1px solid lightgray; - border-radius: ($baseline/2); - - .status-bar { - background-color: #eee; - border-radius: ($baseline/2) ($baseline/2) 0 0; - border-bottom: 1px solid lightgray; - - .statustable { - width: 100%; - padding: $baseline; - } - - .status-container { - display: table-cell; - text-align: center; - - .status-elements { - border-radius: ($baseline/4); - border: 1px solid lightgray; - } - } - - .problemtype-container { - padding: ($baseline/2); - width: 60%; - } - - .problemtype{ - padding: ($baseline/2); - } - - .assessments-container { - float: right; - padding: ($baseline/2) $baseline ($baseline/2) ($baseline/2); - - .assessment-text { - display: inline-block; - display: table-cell; - padding-right: ($baseline/2); - } - } - } - .item-container { - padding-bottom: ($baseline/2); - margin: 15px; - } - - .result-container { - float: left; - width: 100%; - position: relative; - } -} - -section.legend-container { - margin: 15px; - border-radius: ($baseline/4); - - .legenditem { - display: inline; - padding: ($baseline/2); - width: 20%; - background-color: #eee; - font-size: .9em; - } -} - -section.combined-open-ended-status { - vertical-align: center; - - .statusitem { - display: table-cell; - padding: ($baseline/2); - width: 30px; - border-right: 1px solid lightgray; - background-color: #eee; - color: #2c2c2c; - font-size: .9em; - - &:first-child { - border-radius: ($baseline/4) 0 0 ($baseline/4); - } - - &:last-child { - border-right: 0; - border-radius: 0 ($baseline/4) ($baseline/4) 0; - } - - &:only-child { - border-radius: ($baseline/4); - } - - .show-results { - margin-top: .3em; - text-align:right; - } - - .show-results-button { - font: 1em monospace; - } - } - - .statusitem-current { - background-color: $white; - color: #222; - } - - span { - &.unanswered { - display: inline-block; - position: relative; - float: right; - width: 14px; - height: 14px; - background: url('#{$static-path}/images/unanswered-icon.png') center center no-repeat; - } - - &.correct { - display: inline-block; - position: relative; - float: right; - width: 25px; - height: 20px; - background: url('#{$static-path}/images/correct-icon.png') center center no-repeat; - } - - &.incorrect { - display: inline-block; - position: relative; - float: right; - width: 20px; - height: 20px; - background: url('#{$static-path}/images/incorrect-icon.png') center center no-repeat; - } - } - - .icon-caret-right { - display: inline-block; - margin-right: ($baseline/4); - vertical-align: baseline; - } -} - -// Problem Section Controls - -.visibility-control, .visibility-control-prompt { - display: block; - width: 100%; - height: 40px; - - .inner { - float: left; - margin-top: $baseline; - width: 85%; - height: 5px; - border-top: 1px dotted #ddd; - } -} - -.section-header { - display: block; - float: right; - padding-top: ($baseline/2); - width: 15%; - text-align: center; - font-size: .9em; -} - -// Rubric Styling - -.wrapper-score-selection { - display: table-cell; - padding: 0 ($baseline/2); - width: 20px; - vertical-align: middle; -} - -.wrappable { - display: table-cell; - padding: ($baseline/4); -} - -.rubric-list-item { - margin-bottom: ($baseline/10); - padding: ($baseline/2); - - &:hover, &:focus { - background-color: #eee; - } - .rubric-label-selected{ - border-radius: ($baseline/4); - background-color: #eee; - } -} - -span.rubric-category { - display: block; - margin-bottom: ($baseline/2); - padding-top: ($baseline/2); - width: 100%; - border-bottom: 1px solid lightgray; - font-size: 1.1em; -} - -div.combined-rubric-container { - margin: 15px; - padding-top: ($baseline/2); - padding-bottom: ($baseline/4); - - ul.rubric-list { - margin: 0 $baseline ($baseline/2) $baseline; - padding: 0; - list-style-type: none; - - li { - - &.rubric-list-item { - margin-bottom: ($baseline/10); - padding: ($baseline/2); - } - } - } - - h4 { - padding-top: ($baseline/2); - } - - span.rubric-category { - display: block; - width: 100%; - border-bottom: 1px solid lightgray; - font-weight: bold; - font-size: .9em; - } - - label.choicegroup_correct { - &:before { - margin-right: ($baseline*0.75); - content: url('#{$static-path}/images/correct-icon.png'); - } - } - - label.choicegroup_partialcorrect { - &:before { - margin-right: ($baseline*0.75); - content: url('#{$static-path}/images/partially-correct-icon.png'); - } - } - - label.choicegroup_incorrect { - &:before { - margin-right: ($baseline*0.75); - content: url('#{$static-path}/images/incorrect-icon.png'); - } - } - - div.written-feedback { - background: $gray-l6; - padding: ($baseline/4); - } -} - -div.result-container { - padding-top: ($baseline/2); - padding-bottom: ($baseline/4); - - .evaluation { - p { - margin-bottom: 1px; - } - } - - .feedback-on-feedback { - height: 100px; - margin-right: 0; - } - - .evaluation-response { - margin-bottom: ($baseline/10); - - header { - a { - font-size: .85em; - } - } - } - - .evaluation-scoring { - .scoring-list { - margin-left: 3px; - list-style-type: none; - - li { - display:inline; - margin-left: 0; - - &:first-child { - margin-left: 0; - } - - label { - font-size: .9em; - } - } - } - } - - .submit-message-container { - margin: ($baseline/2) 0; - } - - .external-grader-message { - margin-bottom: ($baseline/4); - - section { - padding-left: $baseline; - background-color: #fafafa; - color: #2c2c2c; - font-family: monospace; - font-size: 1em; - padding-top: ($baseline/2); - padding-bottom: 30px; - - header { - font-size: 1.4em; - } - - .shortform { - font-weight: bold; - } - - .longform { - padding: 0; - margin: 0; - - .result-errors { - margin: ($baseline/4); - padding: ($baseline/2) ($baseline/2) ($baseline/2) ($baseline*2); - background: url('#{$static-path}/images/incorrect-icon.png') center left no-repeat; - - li { - color: #B00; - } - } - - .result-output { - margin: ($baseline/4); - padding: $baseline 0 ($baseline*0.75) ($baseline*2.5); - border-top: 1px solid #ddd; - border-left: 20px solid #fafafa; - - h4 { - font-size: 1em; - font-family: monospace; - } - - dl { - margin: 0; - } - - dt { - margin-top: $baseline; - } - - dd { - margin-left: 24pt; - } - } - - .markup-text{ - margin: ($baseline/4); - padding: $baseline 0 ($baseline*0.75) ($baseline*2.5); - border-top: 1px solid #ddd; - border-left: 20px solid #fafafa; - - bs { - color: #bb0000; - } - - bg { - color: #bda046; - } - } - } - } - } - - .rubric-result-container { - padding: ($baseline/10); - margin: 0; - display: inline; - - .rubric-result { - font-size: .9em; - padding: ($baseline/10); - display: inline-table; - } - } -} - -div.rubric { - ul.rubric-list{ - margin: 0 $baseline ($baseline/2) $baseline; - padding: 0; - list-style: none; - list-style-type: none; - - li { - &.rubric-list-item { - margin-bottom: ($baseline/10); - padding: ($baseline/2); - border-radius: ($baseline/4); - - &:hover, &:focus { - background-color: #eee; - } - - .wrapper-score-selection { - display: table-cell; - padding: 0 ($baseline/2); - width: 20px; - vertical-align: middle; - } - - .wrappable { - display: table-cell; - padding: ($baseline/4); - } - } - } - } - - span.rubric-category { - display: block; - width: 100%; - border-bottom: 1px solid lightgray; - font-weight: bold; - font-size: .9em; - } -} - - -section.open-ended-child { - @media print { - display: block; - padding: 0; - width: auto; - - canvas, img { - page-break-inside: avoid; - } - } - - .inline { - display: inline; - } - - ol.enumerate { - li { - &:before { - display: block; - visibility: hidden; - height: 0; - content: " "; - } - } - } - - .solution-span { - > span { - position: relative; - display: block; - margin: $baseline 0; - padding: 9px 15px $baseline; - border: 1px solid #ddd; - border-radius: 3px; - background: $white; - box-shadow: inset 0 0 0 1px #eee; - - &:empty { - display: none; - } - } - } - - p { - &.answer { - margin-top: -2px; - } - &.status { - margin: 8px 0 0 ($baseline/2); - text-indent: -9999px; - } - } - - div.unanswered { - p.status { - display: inline-block; - width: 14px; - height: 14px; - background: url('#{$static-path}/images/unanswered-icon.png') center center no-repeat; - } - } - - div.correct, div.ui-icon-check { - p.status { - display: inline-block; - width: 25px; - height: 20px; - background: url('#{$static-path}/images/correct-icon.png') center center no-repeat; - } - - input { - border-color: green; - } - } - - div.processing { - p.status { - display: inline-block; - width: 20px; - height: 20px; - background: url('#{$static-path}/images/spinner.gif') center center no-repeat; - } - - input { - border-color: #aaa; - } - } - - div.incorrect, div.ui-icon-close { - p.status { - display: inline-block; - width: 20px; - height: 20px; - background: url('#{$static-path}/images/incorrect-icon.png') center center no-repeat; - text-indent: -9999px; - } - - input { - border-color: red; - } - } - - > span { - display: block; - margin-bottom: lh(0.5); - } - - p.answer { - display: inline-block; - margin-bottom: 0; - margin-left: ($baseline/2); - - &:before { - content: "Answer: "; - font-weight: bold; - display: inline; - - } - &:empty { - &:before { - display: none; - } - } - } - - span { - &.unanswered, &.ui-icon-bullet { - display: inline-block; - position: relative; - top: 4px; - width: 14px; - height: 14px; - background: url('#{$static-path}/images/unanswered-icon.png') center center no-repeat; - } - - &.processing, &.ui-icon-processing { - display: inline-block; - position: relative; - top: 6px; - width: 25px; - height: 20px; - background: url('#{$static-path}/images/spinner.gif') center center no-repeat; - } - - &.correct, &.ui-icon-check { - display: inline-block; - position: relative; - top: 6px; - width: 25px; - height: 20px; - background: url('#{$static-path}/images/correct-icon.png') center center no-repeat; - } - - &.incorrect, &.ui-icon-close { - display: inline-block; - position: relative; - top: 6px; - width: 20px; - height: 20px; - background: url('#{$static-path}/images/incorrect-icon.png') center center no-repeat; - } - } - - .reload { - float:right; - margin: ($baseline/2); - } - - div.short-form-response { - @include clearfix(); - overflow-y: auto; - margin-bottom: 0; - padding: ($baseline/2); - min-height: 20px; - height: auto; - border: 1px solid #ddd; - background: $gray-l6; - } - - .grader-status { - @include clearfix(); - margin: ($baseline/2) 0; - padding: ($baseline/2); - border-radius: 5px; - background: $gray-l6; - - span { - display: block; - float: left; - overflow: hidden; - margin: -7px 7px 0 0; - text-indent: -9999px; - } - - .grading { - margin: 0 7px 0 0; - padding-left: 25px; - background: url('#{$static-path}/images/info-icon.png') left center no-repeat; - text-indent: 0; - } - - p { - float: left; - margin-bottom: 0; - line-height: 20px; - } - - &.file { - margin-top: $baseline; - padding: $baseline 0 0 0; - border: 0; - border-top: 1px solid #eee; - background: $white; - - p.debug { - display: none; - } - - input { - float: left; - } - } - } - - form.option-input { - margin: -($baseline/2) 0 $baseline; - padding-bottom: $baseline; - - select { - margin-right: flex-gutter(); - } - } - - ul { - margin-bottom: lh(); - margin-left: 0.75em; - margin-left: 0.75rem; - } - - ul.rubric-list{ - margin: 0; - padding: 0; - list-style-type: none; - list-style: none; - - li { - &.rubric-list-item { - margin-bottom: 0; - padding: 0; - border-radius: ($baseline/4); - } - } - } - - ol { - margin-bottom: lh(); - margin-left: .75em; - margin-left: .75rem; - list-style: decimal outside none; - } - - dl { - line-height: 1.4em; - } - - dl dt { - font-weight: bold; - } - - dl dd { - margin-bottom: 0; - } - - dd { - margin-left: .5em; - margin-left: .5rem; - } - - li { - margin-bottom: 0; - padding: 0; - - &:last-child { - margin-bottom: 0; - } - } - - p { - margin-bottom: lh(); - } - - hr { - float: none; - clear: both; - margin: 0 0 .75rem; - width: 100%; - height: 1px; - border: none; - background: #ddd; - color: #ddd; - } - - .hidden { - display: none; - visibility: hidden; - } - - #{$all-text-inputs} { - display: inline; - width: auto; - } - - div.action { - margin-top: $baseline; - - input.save { - @extend .blue-button !optional; - } - - .submission_feedback { - display: inline-block; - margin: 8px 0 0 ($baseline/2); - color: #666; - font-style: italic; - -webkit-font-smoothing: antialiased; - } - } - - .detailed-solution { - > p:first-child { - color: #aaa; - text-transform: uppercase; - font-weight: bold; - font-style: normal; - font-size: 0.9em; - } - - p:last-child { - margin-bottom: 0; - } - } - - div.open-ended-alert, - .save_message { - margin-top: ($baseline/2); - margin-bottom: ($baseline/4); - padding: 8px 12px; - border: 1px solid #ebe8bf; - border-radius: 3px; - background: #fffcdd; - font-size: 0.9em; - } - - div.capa_reset { - margin-top: ($baseline/2); - margin-bottom: ($baseline/2); - padding: 25px; - border: 1px solid $error-color; - border-radius: 3px; - background-color: lighten($error-color, 25%); - font-size: 1em; - } - - .capa_reset > h2 { - color: #aa0000; - } - - .capa_reset li { - font-size: 0.9em; - } - - .assessment-container { - margin: ($baseline*2) 0 ($baseline*1.5) 0; - - .scoring-container { - p { - margin-bottom: 1em; - } - - label { - display: inline-block; - margin: ($baseline/2); - padding: ($baseline/4); - min-width: 50px; - background-color: $gray-l3; - text-size: 1.5em; - } - - input[type=radio]:checked + label { - background: #666; - color: white; - } - - input[class='grade-selection'] { - display: none; - } - } - } - - div.prompt { - background-color: white; - } - - h4 { - padding: $baseline/2 0; - } -} - -//OE Tool Area Styling - -.oe-tools { - display: inline-block; - width: 100%; - border-radius: 5px; - - .oe-tools-label, .oe-tools-scores-label { - display: inline-block; - padding: $baseline/2; - vertical-align: middle; - font-size: 0.8em; - } - - .rubric-button { - padding: 8px $baseline/4; - } - - .rubric-previous-button { - margin-right: $baseline/4; - } - - .rubric-next-button { - margin-left: $baseline/4; - } - - .next-step-button { - margin: $baseline/2; - } - .reset-button { - vertical-align: middle; - } -} - -// Staff Grading -.problem-list-container { - margin: $baseline/2; - - .instructions { - padding-bottom: $baseline/2; - } -} - -.staff-grading { - - .breadcrumbs { - padding: ($baseline/10); - background-color: $gray-l6; - border-radius: 5px; - margin-bottom: ($baseline/2); - } - - .prompt-wrapper { - padding-top: ($baseline/2); - - .meta-info-wrapper { - padding: ($baseline/2); - border-radius: 5px; - } - } -} - -section.peer-grading-container{ - div.peer-grading{ - section.calibration-feedback { - padding: $baseline; - } - } -} - -div.staff-info{ - background-color: #eee; - border-radius: 10px; - border-bottom: 1px solid lightgray; - padding: ($baseline/2); - margin: ($baseline/2) 0 ($baseline/2) 0; -} diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/edit.scss b/common/lib/xmodule/xmodule/css/combinedopenended/edit.scss deleted file mode 100644 index 186de7925e..0000000000 --- a/common/lib/xmodule/xmodule/css/combinedopenended/edit.scss +++ /dev/null @@ -1,105 +0,0 @@ -.editor-bar { - - .editor-tabs { - - .advanced-toggle { - @include white-button; - height: auto; - margin-top: -1px; - padding: 3px 9px; - font-size: 12px; - - &.current { - border: 1px solid $lightGrey !important; - border-radius: 3px !important; - background: $lightGrey !important; - color: $darkGrey !important; - pointer-events: none; - cursor: none; - - &:hover, &:focus { - box-shadow: 0 0 0 0 !important; - } - } - } - - .cheatsheet-toggle { - width: 21px; - height: 21px; - padding: 0; - margin: 0 ($baseline/4) 0 ($baseline*0.75); - border-radius: 22px; - border: 1px solid #a5aaaf; - background: #e5ecf3; - font-size: 13px; - font-weight: 700; - color: #565d64; - text-align: center; - } - } -} - -.simple-editor-open-ended-cheatsheet { - position: absolute; - top: 0; - left: 100%; - width: 0; - border-radius: 0 3px 3px 0; - @include linear-gradient(left, $shadow-l1, $transparent 4px); - background-color: $white; - overflow: hidden; - @include transition(width .3s linear 0s); - - &.shown { - width: 20%; - height: 100%; - overflow-y: scroll; - } - - .cheatsheet-wrapper { - padding: 10%; - } - - h6 { - margin-bottom: 7px; - font-size: 15px; - font-weight: 700; - } - - .row { - @include clearfix(); - padding-bottom: 5px !important; - margin-bottom: 10px !important; - border-bottom: 1px solid #ddd !important; - - &:last-child { - border-bottom: none !important; - margin-bottom: 0 !important; - } - } - - .col { - float: left; - - &.sample { - width: 60px; - margin-right: 30px; - } - } - - pre { - font-size: 12px; - line-height: 18px; - } - - code { - padding: 0; - background: none; - } -} - -.combinedopenended-editor-icon { - display: inline-block; - vertical-align: middle; - color: #333; -} diff --git a/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html b/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html deleted file mode 100644 index ae2c195a85..0000000000 --- a/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html +++ /dev/null @@ -1,128 +0,0 @@ -None-metadata = { - "showanswer": "attempted", - "display_name": "Problem 1", - "graceperiod": "1 day 12 hours 59 minutes 59 seconds", - "xqa_key": "KUBrWtK3RAaBALLbccHrXeD3RHOpmZ2A", - "rerandomize": "never", - "start": "2012-09-05T12:00", - "attempts": "10000", - "data_dir": "content-mit-6002x", - "max_score": "1" -} -category = CombinedOpenEndedModule -
'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author -
- --Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading. -
- [prompt]\n - """ - - constructor: (element) -> - @element = element - - if $(".markdown-box", @element).length != 0 - @markdown_editor = CodeMirror.fromTextArea($(".markdown-box", element)[0], { - lineWrapping: true - mode: null - }) - @setCurrentEditor(@markdown_editor) - selection = @markdown_editor.getSelection() - #Auto-add in the needed template if it isn't already in there. - if(@markdown_editor.getValue() == "") - @markdown_editor.setValue(OpenEndedMarkdownEditingDescriptor.promptTemplate + "\n" + OpenEndedMarkdownEditingDescriptor.rubricTemplate + "\n" + OpenEndedMarkdownEditingDescriptor.tasksTemplate) - # Add listeners for toolbar buttons (only present for markdown editor) - @element.on('click', '.xml-tab', @onShowXMLButton) - @element.on('click', '.format-buttons a', @onToolbarButton) - @element.on('click', '.cheatsheet-toggle', @toggleCheatsheet) - # Hide the XML text area - $(@element.find('.xml-box')).hide() - else - @createXMLEditor() - - @alertTaskRubricModification() - - ### - Creates the XML Editor and sets it as the current editor. If text is passed in, - it will replace the text present in the HTML template. - - text: optional argument to override the text passed in via the HTML template - ### - createXMLEditor: (text) -> - @xml_editor = CodeMirror.fromTextArea($(".xml-box", @element)[0], { - mode: "xml" - lineNumbers: true - lineWrapping: true - }) - if text - @xml_editor.setValue(text) - @setCurrentEditor(@xml_editor) - $(@xml_editor.getWrapperElement()).toggleClass("CodeMirror-advanced"); - # Need to refresh to get line numbers to display properly. - @xml_editor.refresh() - - ### - User has clicked to show the XML editor. Before XML editor is swapped in, - the user will need to confirm the one-way conversion. - ### - onShowXMLButton: (e) => - e.preventDefault(); - if @cheatsheet && @cheatsheet.hasClass('shown') - @cheatsheet.toggleClass('shown') - @toggleCheatsheetVisibility() - if @confirmConversionToXml() - @createXMLEditor(OpenEndedMarkdownEditingDescriptor.markdownToXml(@markdown_editor.getValue())) - # Put cursor position to 0. - @xml_editor.setCursor(0) - # Hide markdown-specific toolbar buttons - $(@element.find('.editor-bar')).hide() - - alertTaskRubricModification: -> - return alert("Before you edit, please note that if you alter the tasks block or the rubric block of this question after students have submitted responses, it may result in their responses and grades being deleted! Use caution when altering problems that have already been released to students.") - ### - Have the user confirm the one-way conversion to XML. - Returns true if the user clicked OK, else false. - ### - confirmConversionToXml: -> - # TODO: use something besides a JavaScript confirm dialog? - return confirm("If you use the Advanced Editor, this problem will be converted to XML and you will not be able to return to the Simple Editor Interface.\n\nProceed to the Advanced Editor and convert this problem to XML?") - - ### - Event listener for toolbar buttons (only possible when markdown editor is visible). - ### - onToolbarButton: (e) => - e.preventDefault(); - selection = @markdown_editor.getSelection() - revisedSelection = null - switch $(e.currentTarget).attr('class') - when "rubric-button" then revisedSelection = OpenEndedMarkdownEditingDescriptor.insertRubric(selection) - when "prompt-button" then revisedSelection = OpenEndedMarkdownEditingDescriptor.insertPrompt(selection) - when "tasks-button" then revisedSelection = OpenEndedMarkdownEditingDescriptor.insertTasks(selection) - else # ignore click - - if revisedSelection != null - @markdown_editor.replaceSelection(revisedSelection) - @markdown_editor.focus() - - ### - Event listener for toggling cheatsheet (only possible when markdown editor is visible). - ### - toggleCheatsheet: (e) => - e.preventDefault(); - if !$(@markdown_editor.getWrapperElement()).find('.simple-editor-open-ended-cheatsheet')[0] - @cheatsheet = $($('#simple-editor-open-ended-cheatsheet').html()) - $(@markdown_editor.getWrapperElement()).append(@cheatsheet) - - @toggleCheatsheetVisibility() - - setTimeout (=> @cheatsheet.toggleClass('shown')), 10 - - - ### - Function to toggle cheatsheet visibility. - ### - toggleCheatsheetVisibility: () => - $('.modal-content').toggleClass('cheatsheet-is-shown') - - - ### - Stores the current editor and hides the one that is not displayed. - ### - setCurrentEditor: (editor) -> - if @current_editor - $(@current_editor.getWrapperElement()).hide() - @current_editor = editor - $(@current_editor.getWrapperElement()).show() - $(@current_editor).focus(); - - ### - Called when save is called. Listeners are unregistered because editing the block again will - result in a new instance of the descriptor. Note that this is NOT the case for cancel-- - when cancel is called the instance of the descriptor is reused if edit is selected again. - ### - save: -> - @element.off('click', '.xml-tab', @changeEditor) - @element.off('click', '.format-buttons a', @onToolbarButton) - @element.off('click', '.cheatsheet-toggle', @toggleCheatsheet) - if @current_editor == @markdown_editor - { - data: OpenEndedMarkdownEditingDescriptor.markdownToXml(@markdown_editor.getValue()) - metadata: - markdown: @markdown_editor.getValue() - } - else - { - data: @xml_editor.getValue() - nullout: ['markdown'] - } - - @insertRubric: (selectedText) -> - return OpenEndedMarkdownEditingDescriptor.insertGenericInput(selectedText, '[rubric]', '[rubric]', OpenEndedMarkdownEditingDescriptor.rubricTemplate) - - @insertPrompt: (selectedText) -> - return OpenEndedMarkdownEditingDescriptor.insertGenericInput(selectedText, '[prompt]', '[prompt]', OpenEndedMarkdownEditingDescriptor.promptTemplate) - - @insertTasks: (selectedText) -> - return OpenEndedMarkdownEditingDescriptor.insertGenericInput(selectedText, '[tasks]', '[tasks]', OpenEndedMarkdownEditingDescriptor.tasksTemplate) - - @insertGenericInput: (selectedText, lineStart, lineEnd, template) -> - if selectedText.length > 0 - new_string = selectedText.replace(/^\s+|\s+$/g,'') - if new_string.substring(0,lineStart.length) != lineStart - new_string = lineStart + new_string - if new_string.substring((new_string.length)-lineEnd.length,new_string.length) != lineEnd - new_string = new_string + lineEnd - return new_string - else - return template - - @markdownToXml: (markdown)-> - toXml = `function(markdown) { - - function template(template_html,data){ - return template_html.replace(/%(\w*)%/g,function(m,key){return data.hasOwnProperty(key)?data[key]:"";}); - } - - var xml = markdown; - - // group rubrics - xml = xml.replace(/\[rubric\]\n?([^\]]*)\[\/?rubric\]/gmi, function(match, p) { - var groupString = 'Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.
-* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled?
-This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.
- ''' - rubric: ''' -| Purpose | - -- - | - -- - | - -- - | - -- - | -
|---|---|---|---|---|
| Organization | - -- - | - -- - | - -- - | - -- - | -
Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.
-* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled?
-This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.
- ''' - rubric: ''' -| Purpose | - -- - | - -- - | - -- - | - -- - | -
|---|---|---|---|---|
| Organization | - -- - | - -- - | - -- - | - -- - | -
Successfully saved your feedback. Fetching the next essay." - if response.required_done - message = message + " You have done the required number of peer assessments but may continue grading if you like." - message = message + "
" - @grading_message.html(message) - else - if response.error - @render_error(response.error) - else - @render_error("Error occurred while submitting grade") - - # called after a grade is selected on the interface - graded_callback: (event) => - ev = @$(event.target).parent().parent() - ul = ev.parent().parent() - ul.find(".rubric-label-selected").removeClass('rubric-label-selected') - ev.addClass('rubric-label-selected') - # check to see whether or not any categories have not been scored - if @rub.check_complete() - # show button if we have scores for all categories - @grading_message.hide() - @show_submit_button() - @grade = @rub.get_total_score() - - keydown_handler: (event) => - #Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed. - if event.which == 17 && @is_ctrl==false - @is_ctrl=true - else if event.which == 13 && @submit_button.is(':visible') && @is_ctrl==true - if @calibration - @submit_calibration_essay() - else - @submit_grade() - - keyup_handler: (event) => - #Handle keyup event when ctrl key is released - if event.which == 17 && @is_ctrl==true - @is_ctrl=false - - - ########## - # - # Rendering methods and helpers - # - ########## - # renders a calibration essay - render_calibration: (response) => - if response.success - - # load in all the data - @submission_container.html("") - @render_submission_data(response) - # TODO: indicate that we're in calibration mode - @calibration_panel.addClass('current-state') - @grading_panel.removeClass('current-state') - - # Display the right text - # both versions of the text are written into the template itself - # we only need to show/hide the correct ones at the correct time - @calibration_panel.find(@calibration_text_sel).show() - @grading_panel.find(@calibration_text_sel).show() - @calibration_panel.find(@grading_text_sel).hide() - @grading_panel.find(@grading_text_sel).hide() - @flag_student_container.hide() - @peer_grading_instructions.hide() - @feedback_area.attr('disabled', true) - feedback_text = "Once you are done learning to grade, and are grading your peers' work, you will be asked to share written feedback with them in addition to scoring them." - if @tracking_changes() - @ice_legend.hide() - @feedback_area.attr('contenteditable', false) - @feedback_area.text(feedback_text) - else - @feedback_area.val(feedback_text) - @submit_button.show() - @submit_button.unbind('click') - @submit_button.click @submit_calibration_essay - @submit_button.attr('disabled', true) - @scroll_to_top() - else if response.error - @render_error(response.error) - else - @render_error("An error occurred while retrieving the next calibration essay") - - tracking_changes: () => - return @grading_wrapper.data('track-changes') == true - - # Renders a student submission to be graded - render_submission: (response) => - if response.success - @submit_button.hide() - @submission_container.html("") - @render_submission_data(response) - - @calibration_panel.removeClass('current-state') - @grading_panel.addClass('current-state') - - # Display the correct text - # both versions of the text are written into the template itself - # we only need to show/hide the correct ones at the correct time - @calibration_panel.find(@calibration_text_sel).hide() - @grading_panel.find(@calibration_text_sel).hide() - @calibration_panel.find(@grading_text_sel).show() - @grading_panel.find(@grading_text_sel).show() - @flag_student_container.show() - @peer_grading_instructions.show() - if @tracking_changes() - @ice_legend.show() - @feedback_area.html(@make_paragraphs(response.student_response)) - @change_tracker.rebindTracker() - else - @feedback_area.val("") - @feedback_area.attr('disabled', false) - @flag_student_checkbox.removeAttr("checked") - @submit_button.show() - @submit_button.unbind('click') - @submit_button.click @submit_grade - @submit_button.attr('disabled', true) - @scroll_to_top() - else if response.error - @render_error(response.error) - else - @render_error("An error occurred when retrieving the next submission.") - - make_paragraphs: (text) -> - paragraph_split = text.split(/\n\s*\n/) - new_text = '' - for paragraph in paragraph_split - new_text += "#{paragraph}
" - return new_text - - # render common information between calibration and grading - render_submission_data: (response) => - @content_panel.show() - @error_container.hide() - - @submission_container.append(@make_paragraphs(response.student_response)) - @prompt_container.html(response.prompt) - @rubric_selection_container.html(response.rubric) - @submission_key_input.val(response.submission_key) - @essay_id_input.val(response.submission_id) - @setup_score_selection(response.max_score) - - @submit_button.hide() - @action_button.hide() - @calibration_feedback_panel.hide() - @rub = new Rubric(@el) - @rub.initialize(@location) - - - render_calibration_feedback: (response) => - # display correct grade - @calibration_feedback_panel.slideDown() - calibration_wrapper = @$(@calibration_feedback_wrapper_sel) - calibration_wrapper.html("The score you gave was: #{@grade}. The instructor score is: #{response.actual_score}
") - - score = parseInt(@grade) - actual_score = parseInt(response.actual_score) - - if score == actual_score - calibration_wrapper.append("Your score matches the instructor score!
") - else - calibration_wrapper.append("You may want to review the rubric again.
") - - if response.actual_rubric != undefined - calibration_wrapper.append("" + msg + "
") - - collapse_question: (event) => - @prompt_container.slideToggle() - @prompt_container.toggleClass('open') - if @question_header.text() == "Hide Question" - new_text = "Show Question" - Logger.log 'oe_hide_question', {location: @location} - else - Logger.log 'oe_show_question', {location: @location} - new_text = "Hide Question" - @question_header.text(new_text) - return false - - scroll_to_top: () => - $('html, body').animate({ - scrollTop: $(".peer-grading").offset().top - }, 200) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index b635a67110..2965b2e5ca 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -210,13 +210,7 @@ class TestMongoModuleStore(TestMongoModuleStoreBase): '''Make sure the course objects loaded properly''' courses = self.draft_store.get_courses() - # note, the number of courses expected is really - # 6, but due to a lack of cache flushing between - # test case runs, we will get back 7. - # When we fix the caching issue, we should reduce this - # to 6 and remove the 'treexport_peer_component' course_id - # from the list below - assert_equals(len(courses), 7) + assert_equals(len(courses), 6) course_ids = [course.id for course in courses] for course_key in [ @@ -229,9 +223,6 @@ class TestMongoModuleStore(TestMongoModuleStoreBase): ['edX', 'test_unicode', '2012_Fall'], ['edX', 'toy', '2012_Fall'], ['guestx', 'foo', 'bar'], - # This course below is due to a caching issue in the modulestore - # which is not cleared between test runs. This means - ['edX', 'treeexport_peer_component', 'export_peer_component'], ] ]: assert_in(course_key, course_ids) @@ -263,13 +254,7 @@ class TestMongoModuleStore(TestMongoModuleStoreBase): assert_in(course_key, course_ids) courses = self.draft_store.get_courses(org='edX') - # note, the number of courses expected is really - # 5, but due to a lack of cache flushing between - # test case runs, we will get back 6. - # When we fix the caching issue, we should reduce this - # to 6 and remove the 'treexport_peer_component' course_id - # from the list below - assert_equals(len(courses), 6) + assert_equals(len(courses), 5) course_ids = [course.id for course in courses] for course_key in [ @@ -280,9 +265,6 @@ class TestMongoModuleStore(TestMongoModuleStoreBase): ['edX', 'test_import_course', '2012_Fall'], ['edX', 'test_unicode', '2012_Fall'], ['edX', 'toy', '2012_Fall'], - # This course below is due to a caching issue in the modulestore - # which is not cleared between test runs. This means - ['edX', 'treeexport_peer_component', 'export_peer_component'], ] ]: assert_in(course_key, course_ids) @@ -678,57 +660,6 @@ class TestMongoModuleStore(TestMongoModuleStoreBase): self.assertEqual(component.published_on, published_date) self.assertEqual(component.published_by, published_by) - def test_export_course_with_peer_component(self): - """ - Test export course when link_to_location is given in peer grading interface settings. - """ - - name = "export_peer_component" - - locations = self._create_test_tree(name) - - # Insert the test block directly into the module store - problem_location = Location('edX', 'tree{}'.format(name), name, 'combinedopenended', 'test_peer_problem') - - self.draft_store.create_child( - self.dummy_user, - locations["child"], - problem_location.block_type, - block_id=problem_location.block_id - ) - - interface_location = Location('edX', 'tree{}'.format(name), name, 'peergrading', 'test_peer_interface') - - self.draft_store.create_child( - self.dummy_user, - locations["child"], - interface_location.block_type, - block_id=interface_location.block_id - ) - - self.draft_store._update_single_item( - as_draft(interface_location), - { - 'definition.data': {}, - 'metadata': { - 'link_to_location': unicode(problem_location), - 'use_for_single_location': True, - }, - }, - ) - - component = self.draft_store.get_item(interface_location) - self.assertEqual(unicode(component.link_to_location), unicode(problem_location)) - - root_dir = path(mkdtemp()) - self.addCleanup(shutil.rmtree, root_dir) - - # export_course_to_xml should work. - export_course_to_xml( - self.draft_store, self.content_store, interface_location.course_key, - root_dir, 'test_export' - ) - def test_draft_modulestore_create_child_with_position(self): """ This test is designed to hit a specific set of use cases having to do with diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py index 4e9b336bc8..b7384575ff 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py @@ -95,27 +95,27 @@ class CountMongoCallsCourseTraversal(TestCase): # These two lines show the way this traversal *should* be done # (if you'll eventually access all the fields and load all the definitions anyway). # 'lazy' does not matter in old Mongo. - (MIXED_OLD_MONGO_MODULESTORE_BUILDER, None, False, True, 189), - (MIXED_OLD_MONGO_MODULESTORE_BUILDER, None, True, True, 189), - (MIXED_OLD_MONGO_MODULESTORE_BUILDER, 0, False, True, 387), - (MIXED_OLD_MONGO_MODULESTORE_BUILDER, 0, True, True, 387), + (MIXED_OLD_MONGO_MODULESTORE_BUILDER, None, False, True, 175), + (MIXED_OLD_MONGO_MODULESTORE_BUILDER, None, True, True, 175), + (MIXED_OLD_MONGO_MODULESTORE_BUILDER, 0, False, True, 359), + (MIXED_OLD_MONGO_MODULESTORE_BUILDER, 0, True, True, 359), # As shown in these two lines: whether or not the XBlock fields are accessed, # the same number of mongo calls are made in old Mongo for depth=None. - (MIXED_OLD_MONGO_MODULESTORE_BUILDER, None, False, False, 189), - (MIXED_OLD_MONGO_MODULESTORE_BUILDER, None, True, False, 189), - (MIXED_OLD_MONGO_MODULESTORE_BUILDER, 0, False, False, 387), - (MIXED_OLD_MONGO_MODULESTORE_BUILDER, 0, True, False, 387), + (MIXED_OLD_MONGO_MODULESTORE_BUILDER, None, False, False, 175), + (MIXED_OLD_MONGO_MODULESTORE_BUILDER, None, True, False, 175), + (MIXED_OLD_MONGO_MODULESTORE_BUILDER, 0, False, False, 359), + (MIXED_OLD_MONGO_MODULESTORE_BUILDER, 0, True, False, 359), # The line below shows the way this traversal *should* be done # (if you'll eventually access all the fields and load all the definitions anyway). (MIXED_SPLIT_MODULESTORE_BUILDER, None, False, True, 4), - (MIXED_SPLIT_MODULESTORE_BUILDER, None, True, True, 41), - (MIXED_SPLIT_MODULESTORE_BUILDER, 0, False, True, 143), - (MIXED_SPLIT_MODULESTORE_BUILDER, 0, True, True, 41), + (MIXED_SPLIT_MODULESTORE_BUILDER, None, True, True, 38), + (MIXED_SPLIT_MODULESTORE_BUILDER, 0, False, True, 131), + (MIXED_SPLIT_MODULESTORE_BUILDER, 0, True, True, 38), (MIXED_SPLIT_MODULESTORE_BUILDER, None, False, False, 4), (MIXED_SPLIT_MODULESTORE_BUILDER, None, True, False, 4), # TODO: The call count below seems like a bug - should be 4? # Seems to be related to using self.lazy in CachingDescriptorSystem.get_module_data(). - (MIXED_SPLIT_MODULESTORE_BUILDER, 0, False, False, 143), + (MIXED_SPLIT_MODULESTORE_BUILDER, 0, False, False, 131), (MIXED_SPLIT_MODULESTORE_BUILDER, 0, True, False, 4) ) @ddt.unpack diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/__init__.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/__init__.py deleted file mode 100644 index 9aa77fde52..0000000000 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'vik' 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 deleted file mode 100644 index b4f9b46d5d..0000000000 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py +++ /dev/null @@ -1,1246 +0,0 @@ -import json -import logging -import traceback - -from lxml import etree - -from xmodule.timeinfo import TimeInfo -from xmodule.capa_module import ComplexEncoder -from xmodule.progress import Progress -from xmodule.stringify import stringify_children -from xmodule.open_ended_grading_classes import self_assessment_module -from xmodule.open_ended_grading_classes import open_ended_module -from .combined_open_ended_rubric import CombinedOpenEndedRubric, GRADER_TYPE_IMAGE_DICT, HUMAN_GRADER_TYPE, LEGEND_LIST -from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, MockPeerGradingService -from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild -from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError - -log = logging.getLogger("edx.courseware") - -# Set the default number of max attempts. Should be 1 for production -# Set higher for debugging/testing -# attempts specified in xml definition overrides this. -MAX_ATTEMPTS = 1 - -# The highest score allowed for the overall xmodule and for each rubric point -MAX_SCORE_ALLOWED = 50 - -# If true, default behavior is to score module as a practice problem. Otherwise, no grade at all is shown in progress -# Metadata overrides this. -IS_SCORED = False - -# If true, then default behavior is to require a file upload or pasted link from a student for this problem. -# Metadata overrides this. -ACCEPT_FILE_UPLOAD = False - -# Contains all reasonable bool and case combinations of True -TRUE_DICT = ["True", True, "TRUE", "true"] - -# Make '_' a no-op so we can scrape strings. Using lambda instead of -# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file -_ = lambda text: text - -HUMAN_TASK_TYPE = { - # Translators: "Self" is used to denote an openended response that is self-graded - 'selfassessment': _("Self"), - 'openended': "edX", - # Translators: "AI" is used to denote an openended response that is machine-graded - 'ml_grading.conf': _("AI"), - # Translators: "Peer" is used to denote an openended response that is peer-graded - 'peer_grading.conf': _("Peer"), -} - -HUMAN_STATES = { - # Translators: "Not started" is used to communicate to a student that their response - # has not yet been graded - 'intitial': _("Not started."), - # Translators: "Being scored." is used to communicate to a student that their response - # are in the process of being scored - 'assessing': _("Being scored."), - # Translators: "Scoring finished" is used to communicate to a student that their response - # have been scored, but the full scoring process is not yet complete - 'intermediate_done': _("Scoring finished."), - # Translators: "Complete" is used to communicate to a student that their - # openended response has been fully scored - 'done': _("Complete."), -} - -# Default value that controls whether or not to skip basic spelling checks in the controller -# Metadata overrides this -SKIP_BASIC_CHECKS = False - - -class CombinedOpenEndedV1Module(object): - """ - This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc). - It transitions between problems, and support arbitrary ordering. - Each combined open ended module contains one or multiple "child" modules. - Child modules track their own state, and can transition between states. They also implement get_html and - handle_ajax. - The combined open ended module transitions between child modules as appropriate, tracks its own state, and passess - ajax requests from the browser to the child module or handles them itself (in the cases of reset and next problem) - ajax actions implemented by all children are: - 'save_answer' -- Saves the student answer - 'save_assessment' -- Saves the student assessment (or external grader assessment) - 'save_post_assessment' -- saves a post assessment (hint, feedback on feedback, etc) - ajax actions implemented by combined open ended module are: - 'reset' -- resets the whole combined open ended module and returns to the first child moduleresource_string - 'next_problem' -- moves to the next child module - - Types of children. Task is synonymous with child module, so each combined open ended module - incorporates multiple children (tasks): - openendedmodule - selfassessmentmodule - """ - STATE_VERSION = 1 - - # states - INITIAL = 'initial' - ASSESSING = 'assessing' - INTERMEDIATE_DONE = 'intermediate_done' - DONE = 'done' - - # Where the templates live for this problem - TEMPLATE_DIR = "combinedopenended" - - # hack: included to make this class act enough like an xblock to get i18n - _services_requested = {"i18n": "need"} - _combined_services = _services_requested - - def __init__(self, system, location, definition, descriptor, - instance_state=None, shared_state=None, metadata=None, static_data=None, **kwargs): - - """ - Definition file should have one or many task blocks, a rubric block, and a prompt block. See DEFAULT_DATA in combined_open_ended_module for a sample. - - """ - self.instance_state = instance_state - self.display_name = instance_state.get('display_name', "Open Ended") - - # We need to set the location here so the child modules can use it - system.set('location', location) - self.system = system - - # Tells the system which xml definition to load - self.current_task_number = instance_state.get('current_task_number', 0) - # This loads the states of the individual children - self.task_states = instance_state.get('task_states', []) - #This gets any old task states that have been persisted after the instructor changed the tasks. - self.old_task_states = instance_state.get('old_task_states', []) - # Overall state of the combined open ended module - self.state = instance_state.get('state', self.INITIAL) - - self.student_attempts = instance_state.get('student_attempts', 0) - self.weight = instance_state.get('weight', 1) - - # Allow reset is true if student has failed the criteria to move to the next child task - self.ready_to_reset = instance_state.get('ready_to_reset', False) - self.max_attempts = instance_state.get('max_attempts', MAX_ATTEMPTS) - self.is_scored = instance_state.get('graded', IS_SCORED) in TRUE_DICT - 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 - - if system.open_ended_grading_interface: - self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system.render_template) - else: - self.peer_gs = MockPeerGradingService() - - self.required_peer_grading = instance_state.get('required_peer_grading', 3) - self.peer_grader_count = instance_state.get('peer_grader_count', 3) - self.min_to_calibrate = instance_state.get('min_to_calibrate', 3) - self.max_to_calibrate = instance_state.get('max_to_calibrate', 6) - self.peer_grade_finished_submissions_when_none_pending = instance_state.get( - 'peer_grade_finished_submissions_when_none_pending', False - ) - - due_date = instance_state.get('due', None) - grace_period_string = instance_state.get('graceperiod', None) - try: - self.timeinfo = TimeInfo(due_date, grace_period_string) - except Exception: - log.error("Error parsing due date information in location {0}".format(location)) - raise - self.display_due_date = self.timeinfo.display_due_date - - self.rubric_renderer = CombinedOpenEndedRubric(system.render_template, True) - rubric_string = stringify_children(definition['rubric']) - self._max_score = self.rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED) - - # Static data is passed to the child modules to render - self.static_data = { - 'max_score': self._max_score, - 'max_attempts': self.max_attempts, - 'prompt': definition['prompt'], - 'rubric': definition['rubric'], - 'display_name': self.display_name, - 'accept_file_upload': self.accept_file_upload, - 'close_date': self.timeinfo.close_date, - 's3_interface': self.system.s3_interface, - 'skip_basic_checks': self.skip_basic_checks, - 'control': { - 'required_peer_grading': self.required_peer_grading, - 'peer_grader_count': self.peer_grader_count, - 'min_to_calibrate': self.min_to_calibrate, - 'max_to_calibrate': self.max_to_calibrate, - 'peer_grade_finished_submissions_when_none_pending': ( - self.peer_grade_finished_submissions_when_none_pending - ), - } - } - - self.task_xml = definition['task_xml'] - self.location = location - self.fix_invalid_state() - self.setup_next_task() - - def validate_task_states(self, tasks_xml, task_states): - """ - Check whether the provided task_states are valid for the supplied task_xml. - - Returns a list of messages indicating what is invalid about the state. - If the list is empty, then the state is valid - """ - msgs = [] - #Loop through each task state and make sure it matches the xml definition - for task_xml, task_state in zip(tasks_xml, task_states): - tag_name = self.get_tag_name(task_xml) - children = self.child_modules() - task_descriptor = children['descriptors'][tag_name](self.system) - task_parsed_xml = task_descriptor.definition_from_xml(etree.fromstring(task_xml), self.system) - try: - task = children['modules'][tag_name]( - self.system, - self.location, - task_parsed_xml, - task_descriptor, - self.static_data, - instance_state=task_state, - ) - #Loop through each attempt of the task and see if it is valid. - for attempt in task.child_history: - if "post_assessment" not in attempt: - continue - post_assessment = attempt['post_assessment'] - try: - post_assessment = json.loads(post_assessment) - except ValueError: - #This is okay, the value may or may not be json encoded. - pass - if tag_name == "openended" and isinstance(post_assessment, list): - msgs.append("Type is open ended and post assessment is a list.") - break - elif tag_name == "selfassessment" and not isinstance(post_assessment, list): - msgs.append("Type is self assessment and post assessment is not a list.") - break - #See if we can properly render the task. Will go into the exception clause below if not. - task.get_html(self.system) - except Exception: - #If one task doesn't match, the state is invalid. - msgs.append("Could not parse task with xml {xml!r} and states {state!r}: {err}".format( - xml=task_xml, - state=task_state, - err=traceback.format_exc() - )) - break - return msgs - - def is_initial_child_state(self, task_child): - """ - Returns true if this is a child task in an initial configuration - """ - task_child = json.loads(task_child) - return ( - task_child['child_state'] == self.INITIAL and - task_child['child_history'] == [] - ) - - def is_reset_task_states(self, task_state): - """ - Returns True if this task_state is from something that was just reset - """ - return all(self.is_initial_child_state(child) for child in task_state) - - def states_sort_key(self, idx_task_states): - """ - Return a key for sorting a list of indexed task_states, by how far the student got - through the tasks, what their highest score was, and then the index of the submission. - """ - idx, task_states = idx_task_states - - state_values = { - OpenEndedChild.INITIAL: 0, - OpenEndedChild.ASSESSING: 1, - OpenEndedChild.POST_ASSESSMENT: 2, - OpenEndedChild.DONE: 3 - } - - if not task_states: - return (0, 0, state_values[OpenEndedChild.INITIAL], idx) - - final_task_xml = self.task_xml[-1] - final_child_state_json = task_states[-1] - final_child_state = json.loads(final_child_state_json) - - tag_name = self.get_tag_name(final_task_xml) - children = self.child_modules() - task_descriptor = children['descriptors'][tag_name](self.system) - task_parsed_xml = task_descriptor.definition_from_xml(etree.fromstring(final_task_xml), self.system) - task = children['modules'][tag_name]( - self.system, - self.location, - task_parsed_xml, - task_descriptor, - self.static_data, - instance_state=final_child_state_json, - ) - scores = task.all_scores() - if scores: - best_score = max(scores) - else: - best_score = 0 - return ( - len(task_states), - best_score, - state_values.get(final_child_state.get('child_state', OpenEndedChild.INITIAL), 0), - idx - ) - - def fix_invalid_state(self): - """ - Sometimes a teacher will change the xml definition of a problem in Studio. - This means that the state passed to the module is invalid. - If that is the case, moved it to old_task_states and delete task_states. - """ - - # If we are on a task that is greater than the number of available tasks, - # it is an invalid state. If the current task number is greater than the number of tasks - # we have in the definition, our state is invalid. - if self.current_task_number > len(self.task_states) or self.current_task_number > len(self.task_xml): - self.current_task_number = max(min(len(self.task_states), len(self.task_xml)) - 1, 0) - #If the length of the task xml is less than the length of the task states, state is invalid - if len(self.task_xml) < len(self.task_states): - self.current_task_number = len(self.task_xml) - 1 - self.task_states = self.task_states[:len(self.task_xml)] - - if not self.old_task_states and not self.task_states: - # No validation needed when a student first looks at the problem - return - - # Pick out of self.task_states and self.old_task_states the state that is - # a) valid for the current task definition - # b) not the result of a reset due to not having a valid task state - # c) has the highest total score - # d) is the most recent (if the other two conditions are met) - - valid_states = [ - task_states - for task_states - in self.old_task_states + [self.task_states] - if ( - len(self.validate_task_states(self.task_xml, task_states)) == 0 and - not self.is_reset_task_states(task_states) - ) - ] - - # If there are no valid states, don't try and use an old state - if len(valid_states) == 0: - # If this isn't an initial task state, then reset to an initial state - if not self.is_reset_task_states(self.task_states): - self.reset_task_state('\n'.join(self.validate_task_states(self.task_xml, self.task_states))) - - return - - sorted_states = sorted(enumerate(valid_states), key=self.states_sort_key, reverse=True) - idx, best_task_states = sorted_states[0] - - if best_task_states == self.task_states: - return - - log.warning( - "Updating current task state for %s to %r for student with anonymous id %r", - self.system.location, - best_task_states, - self.system.anonymous_student_id - ) - - self.old_task_states.remove(best_task_states) - self.old_task_states.append(self.task_states) - self.task_states = best_task_states - - # The state is ASSESSING unless all of the children are done, or all - # of the children haven't been started yet - children = [json.loads(child) for child in best_task_states] - if all(child['child_state'] == self.DONE for child in children): - self.state = self.DONE - elif all(child['child_state'] == self.INITIAL for child in children): - self.state = self.INITIAL - else: - self.state = self.ASSESSING - - # The current task number is the index of the last completed child + 1, - # limited by the number of tasks - last_completed_child = next((i for i, child in reversed(list(enumerate(children))) if child['child_state'] == self.DONE), 0) - self.current_task_number = min(last_completed_child + 1, len(best_task_states) - 1) - - def create_task(self, task_state, task_xml): - """Create task object for given task state and task xml.""" - - tag_name = self.get_tag_name(task_xml) - children = self.child_modules() - task_descriptor = children['descriptors'][tag_name](self.system) - task_parsed_xml = task_descriptor.definition_from_xml(etree.fromstring(task_xml), self.system) - task = children['modules'][tag_name]( - self.system, - self.location, - task_parsed_xml, - task_descriptor, - self.static_data, - instance_state=task_state, - ) - return task - - def get_task_number(self, task_number): - """Return task object at task_index.""" - - task_states_count = len(self.task_states) - if task_states_count > 0 and task_number < task_states_count: - task_state = self.task_states[task_number] - task_xml = self.task_xml[task_number] - return self.create_task(task_state, task_xml) - return None - - def reset_task_state(self, message=""): - """ - Resets the task states. Moves current task state to an old_state variable, and then makes the task number 0. - :param message: A message to put in the log. - :return: None - """ - info_message = "Combined open ended user state for user {0} in location {1} was invalid. It has been reset, and you now have a new attempt. {2}".format(self.system.anonymous_student_id, self.location.to_deprecated_string(), message) - self.current_task_number = 0 - self.student_attempts = 0 - self.old_task_states.append(self.task_states) - self.task_states = [] - log.info(info_message) - - def get_tag_name(self, xml): - """ - Gets the tag name of a given xml block. - Input: XML string - Output: The name of the root tag - """ - tag = etree.fromstring(xml).tag - return tag - - def overwrite_state(self, current_task_state): - """ - Overwrites an instance state and sets the latest response to the current response. This is used - to ensure that the student response is carried over from the first child to the rest. - Input: Task state json string - Output: Task state json string - """ - last_response_data = self.get_last_response(self.current_task_number - 1) - last_response = last_response_data['response'] - - loaded_task_state = json.loads(current_task_state) - if loaded_task_state['child_state'] == self.INITIAL: - loaded_task_state['child_state'] = self.ASSESSING - loaded_task_state['child_created'] = True - loaded_task_state['child_history'].append({'answer': last_response}) - current_task_state = json.dumps(loaded_task_state) - return current_task_state - - def child_modules(self): - """ - Returns the constructors associated with the child modules in a dictionary. This makes writing functions - simpler (saves code duplication) - Input: None - Output: A dictionary of dictionaries containing the descriptor functions and module functions - """ - child_modules = { - 'openended': open_ended_module.OpenEndedModule, - 'selfassessment': self_assessment_module.SelfAssessmentModule, - } - child_descriptors = { - 'openended': open_ended_module.OpenEndedDescriptor, - 'selfassessment': self_assessment_module.SelfAssessmentDescriptor, - } - children = { - 'modules': child_modules, - 'descriptors': child_descriptors, - } - return children - - def setup_next_task(self, reset=False): - """ - Sets up the next task for the module. Creates an instance state if none exists, carries over the answer - from the last instance state to the next if needed. - Input: A boolean indicating whether or not the reset function is calling. - Output: Boolean True (not useful right now) - """ - current_task_state = None - if len(self.task_states) > self.current_task_number: - current_task_state = self.task_states[self.current_task_number] - - self.current_task_xml = self.task_xml[self.current_task_number] - - if self.current_task_number > 0: - self.ready_to_reset = self.check_allow_reset() - if self.ready_to_reset: - self.current_task_number = self.current_task_number - 1 - - current_task_type = self.get_tag_name(self.current_task_xml) - - children = self.child_modules() - child_task_module = children['modules'][current_task_type] - - self.current_task_descriptor = children['descriptors'][current_task_type](self.system) - - # This is the xml object created from the xml definition of the current task - etree_xml = etree.fromstring(self.current_task_xml) - - # This sends the etree_xml object through the descriptor module of the current task, and - # returns the xml parsed by the descriptor - self.current_task_parsed_xml = self.current_task_descriptor.definition_from_xml(etree_xml, self.system) - if current_task_state is None and self.current_task_number == 0: - self.current_task = child_task_module(self.system, self.location, - self.current_task_parsed_xml, self.current_task_descriptor, - self.static_data) - self.task_states.append(self.current_task.get_instance_state()) - self.state = self.ASSESSING - elif current_task_state is None and self.current_task_number > 0: - last_response_data = self.get_last_response(self.current_task_number - 1) - last_response = last_response_data['response'] - current_task_state = json.dumps({ - 'child_state': self.ASSESSING, - 'version': self.STATE_VERSION, - 'max_score': self._max_score, - 'child_attempts': 0, - 'child_created': True, - 'child_history': [{'answer': last_response}], - }) - self.current_task = child_task_module(self.system, self.location, - self.current_task_parsed_xml, self.current_task_descriptor, - self.static_data, - instance_state=current_task_state) - self.task_states.append(self.current_task.get_instance_state()) - self.state = self.ASSESSING - else: - if self.current_task_number > 0 and not reset: - current_task_state = self.overwrite_state(current_task_state) - self.current_task = child_task_module(self.system, self.location, - self.current_task_parsed_xml, self.current_task_descriptor, - self.static_data, - instance_state=current_task_state) - - return True - - def check_allow_reset(self): - """ - Checks to see if the student has passed the criteria to move to the next module. If not, sets - allow_reset to true and halts the student progress through the tasks. - Input: None - Output: the allow_reset attribute of the current module. - """ - if not self.ready_to_reset: - if self.current_task_number > 0: - last_response_data = self.get_last_response(self.current_task_number - 1) - current_response_data = self.get_current_attributes(self.current_task_number) - - if current_response_data['min_score_to_attempt'] > last_response_data['score'] or\ - current_response_data['max_score_to_attempt'] < last_response_data['score']: - self.state = self.DONE - self.ready_to_reset = True - - return self.ready_to_reset - - def get_context(self): - """ - Generates a context dictionary that is used to render html. - Input: None - Output: A dictionary that can be rendered into the combined open ended template. - """ - task_html = self.get_html_base() - # set context variables and render template - ugettext = self.system.service(self, "i18n").ugettext - - context = { - 'items': [{'content': task_html}], - 'ajax_url': self.system.ajax_url, - 'allow_reset': self.ready_to_reset, - 'state': self.state, - 'task_count': len(self.task_xml), - 'task_number': self.current_task_number + 1, - 'status': ugettext(self.get_status(False)), # pylint: disable=translation-of-non-string - 'display_name': self.display_name, - 'accept_file_upload': self.accept_file_upload, - 'location': self.location, - 'legend_list': LEGEND_LIST, - 'human_state': ugettext(HUMAN_STATES.get(self.state, HUMAN_STATES["intitial"])), # pylint: disable=translation-of-non-string - 'is_staff': self.system.user_is_staff, - } - - return context - - def get_html(self): - """ - Gets HTML for rendering. - Input: None - Output: rendered html - """ - context = self.get_context() - html = self.system.render_template( - '{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context - ) - return html - - def get_html_nonsystem(self): - """ - Gets HTML for rendering via AJAX. Does not use system, because system contains some additional - html, which is not appropriate for returning via ajax calls. - Input: None - Output: HTML rendered directly via Mako - """ - context = self.get_context() - html = self.system.render_template( - '{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context - ) - return html - - def get_html_base(self): - """ - Gets the HTML associated with the current child task - Input: None - Output: Child task HTML - """ - self.update_task_states() - return self.current_task.get_html(self.system) - - def get_html_ajax(self, data): - """ - Get HTML in AJAX callback - data - Needed to preserve AJAX structure - Output: Dictionary with html attribute - """ - return {'html': self.get_html()} - - def get_current_attributes(self, task_number): - """ - Gets the min and max score to attempt attributes of the specified task. - Input: The number of the task. - Output: The minimum and maximum scores needed to move on to the specified task. - """ - task_xml = self.task_xml[task_number] - etree_xml = etree.fromstring(task_xml) - min_score_to_attempt = int(etree_xml.attrib.get('min_score_to_attempt', 0)) - max_score_to_attempt = int(etree_xml.attrib.get('max_score_to_attempt', self._max_score)) - return {'min_score_to_attempt': min_score_to_attempt, 'max_score_to_attempt': max_score_to_attempt} - - def get_last_response(self, task_number): - """ - Returns data associated with the specified task number, such as the last response, score, etc. - Input: The number of the task. - Output: A dictionary that contains information about the specified task. - """ - last_response = "" - task_state = self.task_states[task_number] - task_xml = self.task_xml[task_number] - task_type = self.get_tag_name(task_xml) - - children = self.child_modules() - - task_descriptor = children['descriptors'][task_type](self.system) - etree_xml = etree.fromstring(task_xml) - - min_score_to_attempt = int(etree_xml.attrib.get('min_score_to_attempt', 0)) - max_score_to_attempt = int(etree_xml.attrib.get('max_score_to_attempt', self._max_score)) - - task_parsed_xml = task_descriptor.definition_from_xml(etree_xml, self.system) - task = children['modules'][task_type](self.system, self.location, task_parsed_xml, task_descriptor, - self.static_data, instance_state=task_state) - last_response = task.latest_answer() - last_score = task.latest_score() - all_scores = task.all_scores() - last_post_assessment = task.latest_post_assessment(self.system) - last_post_feedback = "" - feedback_dicts = [{}] - grader_ids = [0] - submission_ids = [0] - if task_type == "openended": - last_post_assessment = task.latest_post_assessment(self.system, short_feedback=False, join_feedback=False) - if isinstance(last_post_assessment, list): - eval_list = [] - for assess in last_post_assessment: - eval_list.append(task.format_feedback_with_evaluation(self.system, assess)) - last_post_evaluation = "".join(eval_list) - else: - last_post_evaluation = task.format_feedback_with_evaluation(self.system, last_post_assessment) - last_post_assessment = last_post_evaluation - try: - rubric_data = task._parse_score_msg(task.child_history[-1].get('post_assessment', "{}"), self.system) - except Exception: - log.debug("Could not parse rubric data from child history. " - "Likely we have not yet initialized a previous step, so this is perfectly fine.") - rubric_data = {} - rubric_scores = rubric_data.get('rubric_scores') - grader_types = rubric_data.get('grader_types') - feedback_items = rubric_data.get('feedback_items') - feedback_dicts = rubric_data.get('feedback_dicts') - grader_ids = rubric_data.get('grader_ids') - submission_ids = rubric_data.get('submission_ids') - elif task_type == "selfassessment": - rubric_scores = last_post_assessment - grader_types = ['SA'] - feedback_items = [''] - last_post_assessment = "" - last_correctness = task.is_last_response_correct() - max_score = task.max_score() - state = task.child_state - if task_type in HUMAN_TASK_TYPE: - human_task_name = HUMAN_TASK_TYPE[task_type] - else: - human_task_name = task_type - - if state in task.HUMAN_NAMES: - human_state = task.HUMAN_NAMES[state] - else: - human_state = state - if grader_types is not None and len(grader_types) > 0: - grader_type = grader_types[0] - else: - grader_type = "IN" - grader_types = ["IN"] - - if grader_type in HUMAN_GRADER_TYPE: - human_grader_name = HUMAN_GRADER_TYPE[grader_type] - else: - human_grader_name = grader_type - - last_response_dict = { - 'response': last_response, - 'score': last_score, - 'all_scores': all_scores, - 'post_assessment': last_post_assessment, - 'type': task_type, - 'max_score': max_score, - 'state': state, - 'human_state': human_state, - 'human_task': human_task_name, - 'correct': last_correctness, - 'min_score_to_attempt': min_score_to_attempt, - 'max_score_to_attempt': max_score_to_attempt, - 'rubric_scores': rubric_scores, - 'grader_types': grader_types, - 'feedback_items': feedback_items, - 'grader_type': grader_type, - 'human_grader_type': human_grader_name, - 'feedback_dicts': feedback_dicts, - 'grader_ids': grader_ids, - 'submission_ids': submission_ids, - 'success': True - } - return last_response_dict - - def extract_human_name_from_task(self, task_xml): - """ - Given the xml for a task, pull out the human name for it. - Input: xml string - Output: a human readable task name (ie Self Assessment) - """ - tree = etree.fromstring(task_xml) - payload = tree.xpath("/openended/openendedparam/grader_payload") - if len(payload) == 0: - task_name = "selfassessment" - else: - inner_payload = json.loads(payload[0].text) - task_name = inner_payload['grader_settings'] - - human_task = HUMAN_TASK_TYPE[task_name] - return human_task - - def update_task_states(self): - """ - Updates the task state of the combined open ended module with the task state of the current child module. - Input: None - Output: boolean indicating whether or not the task state changed. - """ - changed = False - if not self.ready_to_reset: - self.task_states[self.current_task_number] = self.current_task.get_instance_state() - current_task_state = json.loads(self.task_states[self.current_task_number]) - if current_task_state['child_state'] == self.DONE: - self.current_task_number += 1 - if self.current_task_number >= (len(self.task_xml)): - self.state = self.DONE - self.current_task_number = len(self.task_xml) - 1 - else: - self.state = self.INITIAL - changed = True - self.setup_next_task() - return changed - - def update_task_states_ajax(self, return_html): - """ - Runs the update task states function for ajax calls. Currently the same as update_task_states - Input: The html returned by the handle_ajax function of the child - Output: New html that should be rendered - """ - changed = self.update_task_states() - if changed: - pass - return return_html - - def check_if_student_has_done_needed_grading(self): - """ - Checks with the ORA server to see if the student has completed the needed peer grading to be shown their grade. - For example, if a student submits one response, and three peers grade their response, the student - cannot see their grades and feedback unless they reciprocate. - Output: - success - boolean indicator of success - allowed_to_submit - boolean indicator of whether student has done their needed grading or not - error_message - If not success, explains why - """ - student_id = self.system.anonymous_student_id - success = False - allowed_to_submit = True - try: - response = self.peer_gs.get_data_for_location(self.location, student_id) - count_graded = response['count_graded'] - count_required = response['count_required'] - student_sub_count = response['student_sub_count'] - count_available = response['count_available'] - success = True - except GradingServiceError: - # This is a dev_facing_error - log.error("Could not contact external open ended graders for location {0} and student {1}".format( - self.location, student_id)) - # This is a student_facing_error - error_message = "Could not contact the graders. Please notify course staff." - return success, allowed_to_submit, error_message - except KeyError: - log.error("Invalid response from grading server for location {0} and student {1}".format(self.location, student_id)) - error_message = "Received invalid response from the graders. Please notify course staff." - return success, allowed_to_submit, error_message - if count_graded >= count_required or count_available == 0: - error_message = "" - return success, allowed_to_submit, error_message - else: - allowed_to_submit = False - # This is a student_facing_error - error_string = ("You need to peer grade {0} more submissions in order to see your feedback.
" - "You have graded responses from {1} students, and {2} students have graded your submissions.
" - "You have made {3} submissions.
") - error_message = error_string.format(count_required - count_graded, count_graded, count_required, - student_sub_count) - return success, allowed_to_submit, error_message - - def get_rubric(self, _data): - """ - Gets the results of a given grader via ajax. - Input: AJAX data dictionary - Output: Dictionary to be rendered via ajax that contains the result html. - """ - ugettext = self.system.service(self, "i18n").ugettext - all_responses = [] - success, can_see_rubric, error = self.check_if_student_has_done_needed_grading() - if not can_see_rubric: - return { - 'html': self.system.render_template( - '{0}/combined_open_ended_hidden_results.html'.format(self.TEMPLATE_DIR), - {'error': error}), - 'success': True, - 'hide_reset': True - } - - contexts = [] - rubric_number = self.current_task_number - if self.ready_to_reset: - rubric_number += 1 - response = self.get_last_response(rubric_number) - score_length = len(response['grader_types']) - for z in xrange(score_length): - if response['grader_types'][z] in HUMAN_GRADER_TYPE: - try: - feedback = response['feedback_dicts'][z].get('feedback', '') - except TypeError: - return {'success': False} - rubric_scores = [[response['rubric_scores'][z]]] - grader_types = [[response['grader_types'][z]]] - feedback_items = [[response['feedback_items'][z]]] - rubric_html = self.rubric_renderer.render_combined_rubric( - stringify_children(self.static_data['rubric']), - rubric_scores, - grader_types, - feedback_items - ) - contexts.append({ - 'result': rubric_html, - # Translators: "Scored rubric" appears to a user as part of a longer - # string that looks something like: "Scored rubric from grader 1". - # "Scored" is an adjective that modifies the noun "rubric". - # That longer string appears when a user is viewing a graded rubric - # returned from one of the graders of their openended response problem. - 'task_name': ugettext('Scored rubric'), - 'feedback': feedback - }) - - context = { - 'results': contexts, - } - html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context) - return {'html': html, 'success': True, 'hide_reset': False} - - def get_legend(self, _data): - """ - Gets the results of a given grader via ajax. - Input: AJAX data dictionary - Output: Dictionary to be rendered via ajax that contains the result html. - """ - context = { - 'legend_list': LEGEND_LIST, - } - html = self.system.render_template('{0}/combined_open_ended_legend.html'.format(self.TEMPLATE_DIR), context) - return {'html': html, 'success': True} - - def handle_ajax(self, dispatch, data): - """ - This is called by courseware.module_render, to handle an AJAX call. - "data" is request.POST. - - Returns a json dictionary: - { 'progress_changed' : True/False, - 'progress': 'none'/'in_progress'/'done', -{0}'.format(self.answer)
- return {self.answer_id: anshtml}
-
- def get_initial_display(self):
- """
- Gets and shows the initial display for the input box.
- @return: Initial display html
- """
- return {self.answer_id: self.initial_display}
-
- def _convert_longform_feedback_to_html(self, response_items):
- """
- Take in a dictionary, and return html strings for display to student.
- Input:
- response_items: Dictionary with keys success, feedback.
- if success is True, feedback should be a dictionary, with keys for
- types of feedback, and the corresponding feedback values.
- if success is False, feedback is actually an error string.
-
- NOTE: this will need to change when we integrate peer grading, because
- that will have more complex feedback.
-
- Output:
- String -- html that can be displayincorrect-icon.pnged to the student.
- """
-
- # We want to display available feedback in a particular order.
- # This dictionary specifies which goes first--lower first.
- priorities = {
- # These go at the start of the feedback
- 'spelling': 0,
- 'grammar': 1,
- # needs to be after all the other feedback
- 'markup_text': 3
- }
- do_not_render = ['topicality', 'prompt-overlap']
-
- default_priority = 2
-
- def get_priority(elt):
- """
- Args:
- elt: a tuple of feedback-type, feedback
- Returns:
- the priority for this feedback type
- """
- return priorities.get(elt[0], default_priority)
-
- def encode_values(feedback_type, value):
- feedback_type = str(feedback_type).encode('ascii', 'ignore')
- if not isinstance(value, basestring):
- value = str(value)
- value = value.encode('ascii', 'ignore')
- return feedback_type, value
-
- def format_feedback(feedback_type, value):
- feedback_type, value = encode_values(feedback_type, value)
- feedback = u"""
- ', '', html))
- return re.sub("\n", "
", retv)
-
- def new_history_entry(self, answer):
- """
- Adds a new entry to the history dictionary
- @param answer: The student supplied answer
- @return: None
- """
- answer = OpenEndedChild.sanitize_html(answer)
- self.child_history.append({'answer': answer})
- self.stored_answer = None
-
- def record_latest_score(self, score):
- """Assumes that state is right, so we're adding a score to the latest
- history element"""
- self.child_history[-1]['score'] = score
-
- def record_latest_post_assessment(self, post_assessment):
- """Assumes that state is right, so we're adding a score to the latest
- history element"""
- self.child_history[-1]['post_assessment'] = post_assessment
-
- def change_state(self, new_state):
- """
- A centralized place for state changes--allows for hooks. If the
- current state matches the old state, don't run any hooks.
- """
- if self.child_state == new_state:
- return
-
- self.child_state = new_state
-
- if self.child_state == self.DONE:
- self.child_attempts += 1
-
- def get_instance_state(self):
- """
- Get the current score and state
- """
-
- state = {
- 'version': self.STATE_VERSION,
- 'child_history': self.child_history,
- 'child_state': self.child_state,
- 'max_score': self._max_score,
- 'child_attempts': self.child_attempts,
- 'child_created': self.child_created,
- 'stored_answer': self.stored_answer,
- }
- return json.dumps(state)
-
- def _allow_reset(self):
- """Can the module be reset?"""
- return self.child_state == self.DONE and self.child_attempts < self.max_attempts
-
- def max_score(self):
- """
- Return max_score
- """
- return self._max_score
-
- def get_score(self):
- """
- Returns the last score in the list
- """
- score = self.latest_score()
- return {'score': score if score is not None else 0,
- 'total': self._max_score}
-
- def reset(self, system):
- """
- If resetting is allowed, reset the state.
-
- Returns {'success': bool, 'error': msg}
- (error only present if not success)
- """
- self.change_state(self.INITIAL)
- return {'success': True}
-
- def get_display_answer(self):
- latest = self.latest_answer()
- if self.child_state == self.INITIAL:
- if self.stored_answer is not None:
- previous_answer = self.stored_answer
- elif latest is not None and len(latest) > 0:
- previous_answer = latest
- else:
- previous_answer = ""
- previous_answer = previous_answer.replace("
", "\n").replace("
", "\n")
- else:
- if latest is not None and len(latest) > 0:
- previous_answer = latest
- else:
- previous_answer = ""
- previous_answer = previous_answer.replace("\n", "
")
-
- return previous_answer
-
- def store_answer(self, data, system):
- if self.child_state != self.INITIAL:
- # We can only store an answer if the problem has not moved into the assessment phase.
- return self.out_of_sync_error(data)
-
- self.stored_answer = data['student_answer']
- return {'success': True}
-
- def get_progress(self):
- '''
- For now, just return last score / max_score
- '''
- if self._max_score > 0:
- try:
- return Progress(int(self.get_score()['score']), int(self._max_score))
- except Exception as err:
- # This is a dev_facing_error
- log.exception("Got bad progress from open ended child module. Max Score: {0}".format(self._max_score))
- return None
- return None
-
- def out_of_sync_error(self, data, msg=''):
- """
- return dict out-of-sync error message, and also log.
- """
- # This is a dev_facing_error
- log.warning("Open ended child state out sync. state: %r, data: %r. %s",
- self.child_state, data, msg)
- # This is a student_facing_error
- return {'success': False,
- 'error': 'The problem state got out-of-sync. Please try reloading the page.'}
-
- def get_html(self):
- """
- Needs to be implemented by inheritors. Renders the HTML that students see.
- @return:
- """
- pass
-
- def handle_ajax(self):
- """
- Needs to be implemented by child modules. Handles AJAX events.
- @return:
- """
- pass
-
- def is_submission_correct(self, score):
- """
- Checks to see if a given score makes the answer correct. Very naive right now (>66% is correct)
- @param score: Numeric score.
- @return: Boolean correct.
- """
- correct = False
- if isinstance(score, (int, long, float, complex)):
- score_ratio = int(score) / float(self.max_score())
- correct = (score_ratio >= 0.66)
- return correct
-
- def is_last_response_correct(self):
- """
- Checks to see if the last response in the module is correct.
- @return: 'correct' if correct, otherwise 'incorrect'
- """
- score = self.get_score()['score']
- correctness = 'correct' if self.is_submission_correct(score) else 'incorrect'
- return correctness
-
- def upload_file_to_s3(self, file_data):
- """
- Uploads a file to S3.
- file_data: InMemoryUploadedFileObject that responds to read() and seek().
- @return: A URL corresponding to the uploaded object.
- """
-
- file_key = file_data.name + datetime.now(UTC).strftime(
- xqueue_interface.dateformat
- )
-
- file_data.seek(0)
- s3_public_url = upload_to_s3(
- file_data, file_key, self.s3_interface
- )
-
- return s3_public_url
-
- def check_for_file_and_upload(self, data):
- """
- Checks to see if a file was passed back by the student. If so, it will be uploaded to S3.
- @param data: AJAX post dictionary containing keys student_file and valid_files_attached.
- @return: has_file_to_upload, whether or not a file was in the data dictionary,
- and image_tag, the html needed to create a link to the uploaded file.
- """
- has_file_to_upload = False
- image_tag = ""
-
- # Ensure that a valid file was uploaded.
- if 'valid_files_attached' in data and \
- data['valid_files_attached'] in ['true', '1', True] and \
- data['student_file'] is not None and \
- len(data['student_file']) > 0:
- has_file_to_upload = True
- student_file = data['student_file'][0]
-
- # Upload the file to S3 and generate html to embed a link.
- s3_public_url = self.upload_file_to_s3(student_file)
- image_tag = self.generate_file_link_html_from_url(s3_public_url, student_file.name)
-
- return has_file_to_upload, image_tag
-
- def generate_file_link_html_from_url(self, s3_public_url, file_name):
- """
- Create an html link to a given URL.
- @param s3_public_url: URL of the file.
- @param file_name: Name of the file.
- @return: Boolean success, updated AJAX data.
- """
- image_link = """
- {1}
- """.format(s3_public_url, file_name)
- return image_link
-
- def append_file_link_to_student_answer(self, data):
- """
- Adds a file to a student answer after uploading it to S3.
- @param data: AJAX data containing keys student_answer, valid_files_attached, and student_file.
- @return: Boolean success, and updated AJAX data dictionary.
- """
-
- _ = self.system.service(self, "i18n").ugettext
-
- error_message = ""
-
- if not self.accept_file_upload:
- # If the question does not accept file uploads, do not do anything
- return True, error_message, data
-
- try:
- # Try to upload the file to S3.
- has_file_to_upload, image_tag = self.check_for_file_and_upload(data)
- data['student_answer'] += image_tag
- success = True
- if not has_file_to_upload:
- # If there is no file to upload, probably the student has embedded the link in the answer text
- success, data['student_answer'] = self.check_for_url_in_text(data['student_answer'])
-
- # If success is False, we have not found a link, and no file was attached.
- # Show error to student.
- if success is False:
- error_message = _(
- "We could not find a file in your submission. "
- "Please try choosing a file or pasting a URL to your "
- "file into the answer box."
- )
-
- except Exception:
- # In this case, an image was submitted by the student, but the image could not be uploaded to S3. Likely
- # a config issue (development vs deployment).
- log.exception("Student AJAX post to combined open ended xmodule indicated that it contained a file, "
- "but the image was not able to be uploaded to S3. This could indicate a configuration "
- "issue with this deployment and the S3_INTERFACE setting.")
- success = False
- error_message = _(
- "We are having trouble saving your file. Please try another "
- "file or paste a URL to your file into the answer box."
- )
-
- return success, error_message, data
-
- def check_for_url_in_text(self, string):
- """
- Checks for urls in a string.
- @param string: Arbitrary string.
- @return: Boolean success, and the edited string.
- """
- has_link = False
-
- # Find all links in the string.
- links = re.findall(r'(https?://\S+)', string)
- if len(links) > 0:
- has_link = True
-
- # Autolink by wrapping links in anchor tags.
- for link in links:
- string = re.sub(link, self.generate_file_link_html_from_url(link, link), string)
-
- return has_link, string
-
- def get_eta(self):
- if self.controller_qs:
- response = self.controller_qs.check_for_eta(self.location_string)
- else:
- return ""
-
- success = response['success']
- if isinstance(success, basestring):
- success = (success.lower() == "true")
-
- if success:
- eta = controller_query_service.convert_seconds_to_human_readable(response['eta'])
- eta_string = "Please check back for your response in at most {0}.".format(eta)
- else:
- eta_string = ""
-
- return eta_string
-
- @classmethod
- def service_declaration(cls, service_name):
- """
- This classmethod is copied from XBlock's service_declaration.
- It is included to make this class act enough like an XBlock
- to get i18n working on it.
-
- This is currently only used for i18n, and will return "need"
- in that case.
-
- Arguments:
- service_name (string): the name of the service requested.
-
- Returns:
- One of "need", "want", or None.
-
- """
- declaration = cls._combined_services.get(service_name)
- return declaration
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
deleted file mode 100644
index 1ad247be74..0000000000
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
+++ /dev/null
@@ -1,168 +0,0 @@
-import logging
-import dogstats_wrapper as dog_stats_api
-
-from .grading_service_module import GradingService
-from opaque_keys.edx.keys import UsageKey
-
-log = logging.getLogger(__name__)
-
-
-class PeerGradingService(GradingService):
- """
- Interface with the grading controller for peer grading
- """
-
- METRIC_NAME = 'edxapp.open_ended_grading.peer_grading_service'
-
- def __init__(self, config, render_template):
- config['render_template'] = render_template
- super(PeerGradingService, self).__init__(config)
- self.url = config['url'] + config['peer_grading']
- self.login_url = self.url + '/login/'
- self.get_next_submission_url = self.url + '/get_next_submission/'
- self.save_grade_url = self.url + '/save_grade/'
- self.is_student_calibrated_url = self.url + '/is_student_calibrated/'
- self.show_calibration_essay_url = self.url + '/show_calibration_essay/'
- self.save_calibration_essay_url = self.url + '/save_calibration_essay/'
- self.get_problem_list_url = self.url + '/get_problem_list/'
- self.get_notifications_url = self.url + '/get_notifications/'
- self.get_data_for_location_url = self.url + '/get_data_for_location/'
-
- def get_data_for_location(self, problem_location, student_id):
- if isinstance(problem_location, UsageKey):
- problem_location = problem_location.to_deprecated_string()
- params = {'location': problem_location, 'student_id': student_id}
- result = self.get(self.get_data_for_location_url, params)
- self._record_result('get_data_for_location', result)
- for key in result.keys():
- if key in ('success', 'error', 'version'):
- continue
-
- dog_stats_api.histogram(
- self._metric_name('get_data_for_location.{}'.format(key)),
- result[key],
- )
- return result
-
- def get_next_submission(self, problem_location, grader_id):
- if isinstance(problem_location, UsageKey):
- problem_location = problem_location.to_deprecated_string()
- result = self._render_rubric(self.get(
- self.get_next_submission_url,
- {
- 'location': problem_location,
- 'grader_id': grader_id
- }
- ))
- self._record_result('get_next_submission', result)
- return result
-
- def save_grade(self, **kwargs):
- data = kwargs
- data.update({'rubric_scores_complete': True})
- result = self.post(self.save_grade_url, data)
- self._record_result('save_grade', result)
- return result
-
- def is_student_calibrated(self, problem_location, grader_id):
- if isinstance(problem_location, UsageKey):
- problem_location = problem_location.to_deprecated_string()
- params = {'problem_id': problem_location, 'student_id': grader_id}
- result = self.get(self.is_student_calibrated_url, params)
- self._record_result(
- 'is_student_calibrated',
- result,
- tags=['calibrated:{}'.format(result.get('calibrated'))]
- )
- return result
-
- def show_calibration_essay(self, problem_location, grader_id):
- if isinstance(problem_location, UsageKey):
- problem_location = problem_location.to_deprecated_string()
- params = {'problem_id': problem_location, 'student_id': grader_id}
- result = self._render_rubric(self.get(self.show_calibration_essay_url, params))
- self._record_result('show_calibration_essay', result)
- return result
-
- def save_calibration_essay(self, **kwargs):
- data = kwargs
- data.update({'rubric_scores_complete': True})
- result = self.post(self.save_calibration_essay_url, data)
- self._record_result('show_calibration_essay', result)
- return result
-
- def get_problem_list(self, course_id, grader_id):
- params = {'course_id': course_id.to_deprecated_string(), 'student_id': grader_id}
- result = self.get(self.get_problem_list_url, params)
-
- if 'problem_list' in result:
- for problem in result['problem_list']:
- problem['location'] = course_id.make_usage_key_from_deprecated_string(problem['location'])
-
- self._record_result('get_problem_list', result)
- dog_stats_api.histogram(
- self._metric_name('get_problem_list.result.length'),
- len(result.get('problem_list', [])),
- )
- return result
-
- def get_notifications(self, course_id, grader_id):
- params = {'course_id': course_id.to_deprecated_string(), 'student_id': grader_id}
- result = self.get(self.get_notifications_url, params)
- self._record_result(
- 'get_notifications',
- result,
- tags=['needs_to_peer_grade:{}'.format(result.get('student_needs_to_peer_grade'))]
- )
- return result
-
-
-class MockPeerGradingService(object):
- """
- This is a mock peer grading service that can be used for unit tests
- without making actual service calls to the grading controller
- """
-
- def get_next_submission(self, problem_location, grader_id):
- return {
- 'success': True,
- 'submission_id': 1,
- 'submission_key': "",
- 'student_response': 'Sample student response.',
- 'prompt': 'Sample submission prompt.',
- 'rubric': 'Placeholder text for the full rubric.',
- 'max_score': 4
- }
-
- def save_grade(self, **kwargs):
- return {'success': True}
-
- def is_student_calibrated(self, problem_location, grader_id):
- return {'success': True, 'calibrated': True}
-
- def show_calibration_essay(self, problem_location, grader_id):
- return {'success': True,
- 'submission_id': 1,
- 'submission_key': '',
- 'student_response': 'Sample student response.',
- 'prompt': 'Sample submission prompt.',
- 'rubric': 'Placeholder text for the full rubric.',
- 'max_score': 4}
-
- def save_calibration_essay(self, **kwargs):
- return {'success': True, 'actual_score': 2}
-
- def get_problem_list(self, course_id, grader_id):
- return {'success': True,
- 'problem_list': [
- ]}
-
- def get_data_for_location(self, problem_location, student_id):
- return {
- "version": 1,
- "count_graded": 3,
- "count_required": 3,
- "success": True,
- "student_sub_count": 1,
- 'submissions_available': 0,
- }
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
deleted file mode 100644
index fb82c54271..0000000000
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
+++ /dev/null
@@ -1,339 +0,0 @@
-import json
-import logging
-from lxml import etree
-
-from xmodule.capa_module import ComplexEncoder
-from xmodule.progress import Progress
-from xmodule.stringify import stringify_children
-import openendedchild
-
-from .combined_open_ended_rubric import CombinedOpenEndedRubric
-
-log = logging.getLogger("edx.courseware")
-
-
-class SelfAssessmentModule(openendedchild.OpenEndedChild):
- """
- A Self Assessment module that allows students to write open-ended responses,
- submit, then see a rubric and rate themselves. Persists student supplied
- hints, answers, and assessment judgment (currently only correct/incorrect).
- Parses xml definition file--see below for exact format.
-
- Sample XML format:
-
"),
- }
-
- def save_assessment(self, data, _system):
- """
- Save the assessment. If the student said they're right, don't ask for a
- hint, and go straight to the done state. Otherwise, do ask for a hint.
-
- Returns a dict { 'success': bool, 'state': state,
-
- 'hint_html': hint_html OR 'message_html': html and 'allow_reset',
-
- 'error': error-msg},
-
- with 'error' only present if 'success' is False, and 'hint_html' or
- 'message_html' only if success is true
-
- :param data: A `webob.multidict.MultiDict` containing the keys
- asasssment: The sum of assessment scores
- score_list[]: A multivalue key containing all the individual scores
- """
-
- closed, msg = self.check_if_closed()
- if closed:
- return msg
-
- if self.child_state != self.ASSESSING:
- return self.out_of_sync_error(data)
-
- try:
- score = int(data.get('assessment'))
- score_list = [int(x) for x in data.getall('score_list[]')]
- except (ValueError, TypeError):
- # This is a dev_facing_error
- log.error("Non-integer score value passed to save_assessment, or no score list present.")
- # This is a student_facing_error
- _ = self.system.service(self, "i18n").ugettext
- return {
- 'success': False,
- 'error': _("Error saving your score. Please notify course staff.")
- }
-
- # Record score as assessment and rubric scores as post assessment
- self.record_latest_score(score)
- self.record_latest_post_assessment(json.dumps(score_list))
-
- d = {'success': True, }
-
- self.change_state(self.DONE)
- d['allow_reset'] = self._allow_reset()
-
- d['state'] = self.child_state
- return d
-
- def save_hint(self, data, _system):
- '''
- Not used currently, as hints have been removed from the system.
- Save the hint.
- Returns a dict { 'success': bool,
- 'message_html': message_html,
- 'error': error-msg,
- 'allow_reset': bool},
- with the error key only present if success is False and message_html
- only if True.
- '''
- if self.child_state != self.POST_ASSESSMENT:
- # Note: because we only ask for hints on wrong answers, may not have
- # the same number of hints and answers.
- return self.out_of_sync_error(data)
-
- self.record_latest_post_assessment(data['hint'])
- self.change_state(self.DONE)
-
- return {
- 'success': True,
- 'message_html': '',
- 'allow_reset': self._allow_reset(),
- }
-
- def latest_post_assessment(self, system):
- latest_post_assessment = super(SelfAssessmentModule, self).latest_post_assessment(system)
- try:
- rubric_scores = json.loads(latest_post_assessment)
- except:
- rubric_scores = []
- return [rubric_scores]
-
-
-class SelfAssessmentDescriptor(object):
- """
- Module for adding self assessment questions to courses
- """
- mako_template = "widgets/html-edit.html"
- module_class = SelfAssessmentModule
- filename_extension = "xml"
-
- has_score = True
-
- def __init__(self, system):
- self.system = system
-
- @classmethod
- def definition_from_xml(cls, xml_object, system):
- """
- Pull out the rubric, prompt, and submitmessage into a dictionary.
-
- Returns:
- {
- 'submitmessage': 'some-html'
- 'hintprompt': 'some-html'
- }
- """
- expected_children = []
- for child in expected_children:
- if len(xml_object.xpath(child)) != 1:
- # This is a staff_facing_error
- raise ValueError(
- u"Self assessment definition must include exactly one '{0}' tag. Contact the learning sciences group for assistance.".format(
- child))
-
- def parse(k):
- """Assumes that xml_object has child k"""
- return stringify_children(xml_object.xpath(k)[0])
-
- return {}
-
- def definition_to_xml(self, resource_fs):
- '''Return an xml element representing this definition.'''
- elt = etree.Element('selfassessment')
-
- def add_child(k):
- child_str = u'<{tag}>{body}{tag}>'.format(tag=k, body=getattr(self, k))
- child_node = etree.fromstring(child_str)
- elt.append(child_node)
-
- for child in []:
- add_child(child)
-
- return elt
diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py
deleted file mode 100644
index 1c246aac71..0000000000
--- a/common/lib/xmodule/xmodule/peer_grading_module.py
+++ /dev/null
@@ -1,744 +0,0 @@
-"""
-ORA1. Deprecated.
-"""
-import json
-import logging
-
-from datetime import datetime
-
-from django.utils.timezone import UTC
-from lxml import etree
-from pkg_resources import resource_string
-
-from xblock.fields import Dict, String, Scope, Boolean, Float, Reference
-
-from xmodule.capa_module import ComplexEncoder
-from xmodule.fields import Date, Timedelta
-from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
-from xmodule.raw_module import RawDescriptor
-from xmodule.timeinfo import TimeInfo
-from xmodule.x_module import XModule, module_attr
-from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, MockPeerGradingService
-from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
-from xmodule.validation import StudioValidation, StudioValidationMessage
-
-from open_ended_grading_classes import combined_open_ended_rubric
-
-log = logging.getLogger(__name__)
-
-# Make '_' a no-op so we can scrape strings. Using lambda instead of
-# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file
-_ = lambda text: text
-
-EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please notify course staff."
-MAX_ALLOWED_FEEDBACK_LENGTH = 5000
-
-
-class PeerGradingFields(object):
- use_for_single_location = Boolean(
- display_name=_("Show Single Problem"),
- help=_('When True, only the single problem specified by "Link to Problem Location" is shown. '
- 'When False, a panel is displayed with all problems available for peer grading.'),
- default=False,
- scope=Scope.settings
- )
- link_to_location = Reference(
- display_name=_("Link to Problem Location"),
- help=_('The location of the problem being graded. Only used when "Show Single Problem" is True.'),
- default="",
- scope=Scope.settings
- )
- graded = Boolean(
- display_name=_("Graded"),
- help=_('Defines whether the student gets credit for grading this problem. Only used when "Show Single Problem" is True.'),
- default=False,
- scope=Scope.settings
- )
- due = Date(
- help=_("Due date that should be displayed."),
- scope=Scope.settings)
- graceperiod = Timedelta(
- help=_("Amount of grace to give on the due date."),
- scope=Scope.settings
- )
- student_data_for_location = Dict(
- help=_("Student data for a given peer grading problem."),
- scope=Scope.user_state
- )
- weight = Float(
- display_name=_("Problem Weight"),
- help=_("Defines the number of points each problem is worth. If the value is not set, each problem is worth one point."),
- scope=Scope.settings, values={"min": 0, "step": ".1"},
- default=1
- )
- display_name = String(
- display_name=_("Display Name"),
- help=_("Display name for this module"),
- scope=Scope.settings,
- default=_("Peer Grading Interface")
- )
- data = String(
- help=_("Html contents to display for this module"),
- default='
'
- img_clean = u'
'
- embed_dirty = u''
- embed_clean = u'