From b25008ac1ede869a5e0230771ac41ac9c2e6d1f0 Mon Sep 17 00:00:00 2001 From: jansenk Date: Wed, 26 Jul 2023 09:29:18 -0400 Subject: [PATCH 01/24] feat: wip datalayer --- .../xblock/data_layer/data_layer_mixin.py | 10 ++ .../xblock/data_layer/serializers.py | 152 ++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 openassessment/xblock/data_layer/data_layer_mixin.py create mode 100644 openassessment/xblock/data_layer/serializers.py diff --git a/openassessment/xblock/data_layer/data_layer_mixin.py b/openassessment/xblock/data_layer/data_layer_mixin.py new file mode 100644 index 0000000000..625ed926e5 --- /dev/null +++ b/openassessment/xblock/data_layer/data_layer_mixin.py @@ -0,0 +1,10 @@ +""" +XBlock +""" +from xblock.core import XBlock + +class DataLayerMixin: + + @XBlock.json_handler + def get_block_info(self, data, suffix=""): + \ No newline at end of file diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py new file mode 100644 index 0000000000..ef954c96e0 --- /dev/null +++ b/openassessment/xblock/data_layer/serializers.py @@ -0,0 +1,152 @@ +from rest_framework.serializers import ( + Serializer, + BooleanField, + CharField, + ListField, + URLField, + DateTimeField, + IntegerField, + SerializerMethodField +) + +class CharListField(ListField): + child = CharField() + +class ORABlockSerializer(Serializer): + title = CharField() + prompts = CharListField() + base_asset_url = SerializerMethodField(source="*") + submission_config = SubmissionConfigSerializer(source="*") + teams_config = TeamsConfigSerializer(source="*") + assessment_steps = AssessmentStepsSerializer(source="*") + rubric_config = RubricConfigSerializer(source="*") + leaderboard_config = LeaderboardConfigSerializer(source="*") + + def get_base_asset_url(self, block): + return block._get_base_url_path_for_course_assets(block.course_id) + +class SubmissionConfigSerializer(Serializer): + start = DateTimeField(source="submission_start") + due = DateTimeField(source="submission_due") + + text_response_config = TextResponseConfigSerializer(source='*') + file_response_config = FileResponseConfigSerializer(source='*') + +class TextResponseConfigSerializer(Serializer): + enabled = SerializerMethodField() + required = SerializerMethodField() + editor_type = CharField(source="text_response_editor") + allow_latex_preview = BooleanField(source="allow_latex") + + def get_enabled(self, block): + return block.text_response is not None + + def get_required(self, block): + return block.text_response == 'required' + +class FileResponseConfigSerializer(Serializer): + enabled = SerializerMethodField() + required = SerializerMethodField() + file_upload_limit = SerializerMethodField() + allowed_extensions = CharListField(source="get_allowed_file_types_or_preset") + blocked_extensions = CharListField(source="FILE_EXT_BLACK_LIST") + allowed_file_type_description = CharField(source="file_upload_type") + + def get_enabled(self, block): + return block.file_upload_response is not None + + def get_required(self, block): + return block.file_upload_response == 'required' + + def get_file_upload_limit(self, block): + if not block.allow_multiple_files: + return 1 + return block.MAX_FILES_COUNT + + +class TeamsConfigSerializer(Serializer): + enabled = BooleanField(source="is_team_assignment") + teamset_name = SerializerMethodField() + + def get_teamset_name(self, block): + if block.teamset_config is not None: + return block.teamset_config.name + +# It's so unclear to me how the block stores this stuff. +# What's the best way to handle all this? I feel like there's an +# obvious solution but I can't see it + + +# editor_assessments_order: ['student-training', 'self-assessment', 'peer-assessment', 'staff-assessment'] +# rubric_assessments: [{'examples': [{'answer': ['Replace this text with your own sample response for this assignment. Then, under Response Score to the right, select an option for each criterion. Learners practice performing peer assessments by assessing this response and comparing the options that they select in the rubric with the options that you specified.'], 'options_selected': [{'criterion': 'Ideas', 'option': 'Fair'}, {'criterion': 'Content', 'option': 'Good'}]}, {'answer': ['Replace this text with another sample response, and then specify the options that you would select for this response.'], 'options_selected': [{'criterion': 'Ideas', 'option': 'Poor'}, {'criterion': 'Content', 'option': 'Good'}]}], 'name': 'student-training', 'start': None, 'enable_flexible_grading': False, 'due': None}, {'start': '2001-01-01T00:00:00+00:00', 'due': '2029-01-01T00:00:00+00:00', 'name': 'self-assessment', 'enable_flexible_grading': False}, {'must_grade': 5, 'must_be_graded_by': 3, 'enable_flexible_grading': False, 'start': '2001-01-01T00:00:00+00:00', 'due': '2029-01-01T00:00:00+00:00', 'name': 'peer-assessment'}, {'required': True, 'name': 'staff-assessment', 'start': None, 'enable_flexible_grading': False, 'due': None}] + + +class AssessmentStepsSerializer(Serializer): + order = CharListField() + settings = AssessmentStepsSettingsSerializer(source="*") + +class AssessmentStepsSettingsSerializer(Serializer): + peer = PeerSettingsSerializer() + staff = StaffSettingsSerializer() + self = SelfSettingsSerializer() + training = TrainingSettingsSerializer() + +class RequiredMixin: + required = BooleanField() + +class StartEndMixin: + start = DateTimeField() + due = DateTimeField() + +class PeerSettingsSerializer(RequiredMixin, StartEndMixin, Serializer): + min_number_to_grade = IntegerField() + min_number_to_be_graded_by = IntegerField() + flexible_grading = BooleanField() + +class StaffSettingsSerializer(RequiredMixin, Serializer): + pass + +class SelfSettingsSerializer(RequiredMixin, StartEndMixin, Serializer): + pass + +class TrainingSettingsSerializer(RequiredMixin, Serializer): + pass + +class RubricConfigSerializer(Serializer): + show_during_response = BooleanField(source="show_rubric_during_response") + feedback_config = RubricFeedbackConfigSerializer(source="*") + criteria = RubricCriterionSerializer(many=True, source="rubric_criteria_with_labels") + +class RubricFeedbackConfigSerializer(Serializer): + description = CharField(source="rubric_feedback_prompt") #is this this field? + default_text = CharField(source="rubric_feedback_default_text") + +class RubricCriterionSerializer(Serializer): + name = CharField(source="label") + description = CharField(source="prompt") + feedback_enabled = SerializerMethodField() + feedback_required = SerializerMethodField() + options = RubricCriterionOptionSerializer(many=True, source="options") + + @staticmethod + def _feedback(criterion): + return criterion.get('feedback', 'disabled') + + def get_feedback_enabled(self, criterion): + return self._feedback(criterion) != 'disabled' + + def get_feedback_required(self, criterion): + return self._feedback(criterion) == 'required' + +class RubricCriterionOptionSerializer(Serializer): + name = CharField() + label = CharField() + points = IntegerField() + description = CharField(source="explanation") + +class LeaderboardConfigSerializer(Serializer): + enabled = SerializerMethodField() + number_to_show = IntegerField(source="leaderboard_show") + + def get_enabled(self, block): + return block.leaderboard_show > 0 \ No newline at end of file From 37a8f23b453c839ebe6bcc0ef8b78fe735b3380b Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 26 Jul 2023 15:30:46 -0400 Subject: [PATCH 02/24] feat: start ORA block info serialization Doing this field by field checking to make it work along the way instead of in one big bang. --- .../xblock/data_layer/data_layer_mixin.py | 6 +- .../xblock/data_layer/serializers.py | 141 +----------------- openassessment/xblock/openassessmentblock.py | 44 +++--- 3 files changed, 32 insertions(+), 159 deletions(-) diff --git a/openassessment/xblock/data_layer/data_layer_mixin.py b/openassessment/xblock/data_layer/data_layer_mixin.py index 625ed926e5..18b436f89c 100644 --- a/openassessment/xblock/data_layer/data_layer_mixin.py +++ b/openassessment/xblock/data_layer/data_layer_mixin.py @@ -3,8 +3,12 @@ """ from xblock.core import XBlock +from openassessment.xblock.data_layer.serializers import OraBlockInfoSerializer + class DataLayerMixin: @XBlock.json_handler def get_block_info(self, data, suffix=""): - \ No newline at end of file + context = {} + block_info = OraBlockInfoSerializer(self, context=context) + return block_info.data \ No newline at end of file diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py index ef954c96e0..777618e778 100644 --- a/openassessment/xblock/data_layer/serializers.py +++ b/openassessment/xblock/data_layer/serializers.py @@ -1,152 +1,17 @@ from rest_framework.serializers import ( Serializer, - BooleanField, CharField, ListField, - URLField, - DateTimeField, - IntegerField, SerializerMethodField ) class CharListField(ListField): child = CharField() -class ORABlockSerializer(Serializer): +class OraBlockInfoSerializer(Serializer): title = CharField() prompts = CharListField() base_asset_url = SerializerMethodField(source="*") - submission_config = SubmissionConfigSerializer(source="*") - teams_config = TeamsConfigSerializer(source="*") - assessment_steps = AssessmentStepsSerializer(source="*") - rubric_config = RubricConfigSerializer(source="*") - leaderboard_config = LeaderboardConfigSerializer(source="*") - - def get_base_asset_url(self, block): - return block._get_base_url_path_for_course_assets(block.course_id) - -class SubmissionConfigSerializer(Serializer): - start = DateTimeField(source="submission_start") - due = DateTimeField(source="submission_due") - - text_response_config = TextResponseConfigSerializer(source='*') - file_response_config = FileResponseConfigSerializer(source='*') - -class TextResponseConfigSerializer(Serializer): - enabled = SerializerMethodField() - required = SerializerMethodField() - editor_type = CharField(source="text_response_editor") - allow_latex_preview = BooleanField(source="allow_latex") - - def get_enabled(self, block): - return block.text_response is not None - - def get_required(self, block): - return block.text_response == 'required' - -class FileResponseConfigSerializer(Serializer): - enabled = SerializerMethodField() - required = SerializerMethodField() - file_upload_limit = SerializerMethodField() - allowed_extensions = CharListField(source="get_allowed_file_types_or_preset") - blocked_extensions = CharListField(source="FILE_EXT_BLACK_LIST") - allowed_file_type_description = CharField(source="file_upload_type") - - def get_enabled(self, block): - return block.file_upload_response is not None - - def get_required(self, block): - return block.file_upload_response == 'required' - - def get_file_upload_limit(self, block): - if not block.allow_multiple_files: - return 1 - return block.MAX_FILES_COUNT - - -class TeamsConfigSerializer(Serializer): - enabled = BooleanField(source="is_team_assignment") - teamset_name = SerializerMethodField() - - def get_teamset_name(self, block): - if block.teamset_config is not None: - return block.teamset_config.name - -# It's so unclear to me how the block stores this stuff. -# What's the best way to handle all this? I feel like there's an -# obvious solution but I can't see it - - -# editor_assessments_order: ['student-training', 'self-assessment', 'peer-assessment', 'staff-assessment'] -# rubric_assessments: [{'examples': [{'answer': ['Replace this text with your own sample response for this assignment. Then, under Response Score to the right, select an option for each criterion. Learners practice performing peer assessments by assessing this response and comparing the options that they select in the rubric with the options that you specified.'], 'options_selected': [{'criterion': 'Ideas', 'option': 'Fair'}, {'criterion': 'Content', 'option': 'Good'}]}, {'answer': ['Replace this text with another sample response, and then specify the options that you would select for this response.'], 'options_selected': [{'criterion': 'Ideas', 'option': 'Poor'}, {'criterion': 'Content', 'option': 'Good'}]}], 'name': 'student-training', 'start': None, 'enable_flexible_grading': False, 'due': None}, {'start': '2001-01-01T00:00:00+00:00', 'due': '2029-01-01T00:00:00+00:00', 'name': 'self-assessment', 'enable_flexible_grading': False}, {'must_grade': 5, 'must_be_graded_by': 3, 'enable_flexible_grading': False, 'start': '2001-01-01T00:00:00+00:00', 'due': '2029-01-01T00:00:00+00:00', 'name': 'peer-assessment'}, {'required': True, 'name': 'staff-assessment', 'start': None, 'enable_flexible_grading': False, 'due': None}] - - -class AssessmentStepsSerializer(Serializer): - order = CharListField() - settings = AssessmentStepsSettingsSerializer(source="*") - -class AssessmentStepsSettingsSerializer(Serializer): - peer = PeerSettingsSerializer() - staff = StaffSettingsSerializer() - self = SelfSettingsSerializer() - training = TrainingSettingsSerializer() -class RequiredMixin: - required = BooleanField() - -class StartEndMixin: - start = DateTimeField() - due = DateTimeField() - -class PeerSettingsSerializer(RequiredMixin, StartEndMixin, Serializer): - min_number_to_grade = IntegerField() - min_number_to_be_graded_by = IntegerField() - flexible_grading = BooleanField() - -class StaffSettingsSerializer(RequiredMixin, Serializer): - pass - -class SelfSettingsSerializer(RequiredMixin, StartEndMixin, Serializer): - pass - -class TrainingSettingsSerializer(RequiredMixin, Serializer): - pass - -class RubricConfigSerializer(Serializer): - show_during_response = BooleanField(source="show_rubric_during_response") - feedback_config = RubricFeedbackConfigSerializer(source="*") - criteria = RubricCriterionSerializer(many=True, source="rubric_criteria_with_labels") - -class RubricFeedbackConfigSerializer(Serializer): - description = CharField(source="rubric_feedback_prompt") #is this this field? - default_text = CharField(source="rubric_feedback_default_text") - -class RubricCriterionSerializer(Serializer): - name = CharField(source="label") - description = CharField(source="prompt") - feedback_enabled = SerializerMethodField() - feedback_required = SerializerMethodField() - options = RubricCriterionOptionSerializer(many=True, source="options") - - @staticmethod - def _feedback(criterion): - return criterion.get('feedback', 'disabled') - - def get_feedback_enabled(self, criterion): - return self._feedback(criterion) != 'disabled' - - def get_feedback_required(self, criterion): - return self._feedback(criterion) == 'required' - -class RubricCriterionOptionSerializer(Serializer): - name = CharField() - label = CharField() - points = IntegerField() - description = CharField(source="explanation") - -class LeaderboardConfigSerializer(Serializer): - enabled = SerializerMethodField() - number_to_show = IntegerField(source="leaderboard_show") - - def get_enabled(self, block): - return block.leaderboard_show > 0 \ No newline at end of file + def get_base_asset_url(self, block): + return block._get_base_url_path_for_course_assets(block.course.id) diff --git a/openassessment/xblock/openassessmentblock.py b/openassessment/xblock/openassessmentblock.py index 17a399e597..2c13ca7f29 100644 --- a/openassessment/xblock/openassessmentblock.py +++ b/openassessment/xblock/openassessmentblock.py @@ -26,6 +26,7 @@ from openassessment.workflow.errors import AssessmentWorkflowError from openassessment.xblock.course_items_listing_mixin import CourseItemsListingMixin from openassessment.xblock.data_conversion import create_prompts_list, create_rubric_dict, update_assessments_format +from openassessment.xblock.data_layer.data_layer_mixin import DataLayerMixin from openassessment.xblock.defaults import * # pylint: disable=wildcard-import, unused-wildcard-import from openassessment.xblock.grade_mixin import GradeMixin from openassessment.xblock.leaderboard_mixin import LeaderboardMixin @@ -106,26 +107,29 @@ def load(path): @XBlock.needs("user_state") @XBlock.needs("teams") @XBlock.needs("teams_configuration") -class OpenAssessmentBlock(MessageMixin, - SubmissionMixin, - PeerAssessmentMixin, - SelfAssessmentMixin, - StaffAssessmentMixin, - StudioMixin, - GradeMixin, - LeaderboardMixin, - StaffAreaMixin, - WorkflowMixin, - TeamWorkflowMixin, - StudentTrainingMixin, - LmsCompatibilityMixin, - CourseItemsListingMixin, - ConfigMixin, - TeamMixin, - OpenAssessmentTemplatesMixin, - RubricReuseMixin, - StaffGraderMixin, - XBlock): +class OpenAssessmentBlock( + MessageMixin, + SubmissionMixin, + PeerAssessmentMixin, + SelfAssessmentMixin, + StaffAssessmentMixin, + StudioMixin, + GradeMixin, + LeaderboardMixin, + StaffAreaMixin, + WorkflowMixin, + TeamWorkflowMixin, + StudentTrainingMixin, + LmsCompatibilityMixin, + CourseItemsListingMixin, + ConfigMixin, + TeamMixin, + OpenAssessmentTemplatesMixin, + RubricReuseMixin, + StaffGraderMixin, + DataLayerMixin, + XBlock + ): """Displays a prompt and provides an area where students can compose a response.""" VALID_ASSESSMENT_TYPES = [ From a1c5a9886f274e8ca191ca06f033a4af41cb7f8f Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 26 Jul 2023 15:59:39 -0400 Subject: [PATCH 03/24] feat: add submission config serializer --- .../xblock/data_layer/serializers.py | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py index 777618e778..00c82da36c 100644 --- a/openassessment/xblock/data_layer/serializers.py +++ b/openassessment/xblock/data_layer/serializers.py @@ -1,5 +1,7 @@ from rest_framework.serializers import ( - Serializer, + BooleanField, + DateTimeField, + Serializer, CharField, ListField, SerializerMethodField @@ -8,10 +10,67 @@ class CharListField(ListField): child = CharField() +class TextResponseConfigSerializer(Serializer): + enabled = SerializerMethodField() + required = SerializerMethodField() + editor_type = CharField(source="text_response_editor") + allow_latex_preview = BooleanField(source="allow_latex") + + def get_enabled(self, block): + return block.text_response is not None + + def get_required(self, block): + return block.text_response == 'required' + +class FileResponseConfigSerializer(Serializer): + enabled = SerializerMethodField() + required = SerializerMethodField() + file_upload_limit = SerializerMethodField() + allowed_extensions = CharListField(source="get_allowed_file_types_or_preset") + blocked_extensions = CharListField(source="FILE_EXT_BLACK_LIST") + allowed_file_type_description = CharField(source="file_upload_type") + + def get_enabled(self, block): + return block.file_upload_response is not None + + def get_required(self, block): + return block.file_upload_response == 'required' + + def get_file_upload_limit(self, block): + if not block.allow_multiple_files: + return 1 + return block.MAX_FILES_COUNT + +class TeamsConfigSerializer(Serializer): + enabled = BooleanField(source="is_team_assignment") + teamset_name = SerializerMethodField() + + def get_teamset_name(self, block): + if block.teamset_config is not None: + return block.teamset_config.name + +class SubmissionConfigSerializer(Serializer): + start = DateTimeField(source="submission_start") + due = DateTimeField(source="submission_due") + + text_response_config = TextResponseConfigSerializer(source='*') + file_response_config = FileResponseConfigSerializer(source='*') + + teams_config = TeamsConfigSerializer(source="*") + class OraBlockInfoSerializer(Serializer): + """ + Main serializer for statically-defined ORA Block information + """ + title = CharField() - prompts = CharListField() + prompts = SerializerMethodField(source="*") base_asset_url = SerializerMethodField(source="*") + submission_config = SubmissionConfigSerializer(source="*") + def get_base_asset_url(self, block): return block._get_base_url_path_for_course_assets(block.course.id) + + def get_prompts(self, block): + return [prompt["description"] for prompt in block.prompts] From dfb40d3121c6a3c55dcdf46c45623d7b94edcdf3 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 26 Jul 2023 16:00:07 -0400 Subject: [PATCH 04/24] style: black --- .../xblock/data_layer/data_layer_mixin.py | 4 ++-- openassessment/xblock/data_layer/serializers.py | 16 +++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/openassessment/xblock/data_layer/data_layer_mixin.py b/openassessment/xblock/data_layer/data_layer_mixin.py index 18b436f89c..01a2877908 100644 --- a/openassessment/xblock/data_layer/data_layer_mixin.py +++ b/openassessment/xblock/data_layer/data_layer_mixin.py @@ -5,10 +5,10 @@ from openassessment.xblock.data_layer.serializers import OraBlockInfoSerializer -class DataLayerMixin: +class DataLayerMixin: @XBlock.json_handler def get_block_info(self, data, suffix=""): context = {} block_info = OraBlockInfoSerializer(self, context=context) - return block_info.data \ No newline at end of file + return block_info.data diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py index 00c82da36c..e0ea0d57e3 100644 --- a/openassessment/xblock/data_layer/serializers.py +++ b/openassessment/xblock/data_layer/serializers.py @@ -4,12 +4,14 @@ Serializer, CharField, ListField, - SerializerMethodField + SerializerMethodField, ) + class CharListField(ListField): child = CharField() + class TextResponseConfigSerializer(Serializer): enabled = SerializerMethodField() required = SerializerMethodField() @@ -20,7 +22,8 @@ def get_enabled(self, block): return block.text_response is not None def get_required(self, block): - return block.text_response == 'required' + return block.text_response == "required" + class FileResponseConfigSerializer(Serializer): enabled = SerializerMethodField() @@ -34,13 +37,14 @@ def get_enabled(self, block): return block.file_upload_response is not None def get_required(self, block): - return block.file_upload_response == 'required' + return block.file_upload_response == "required" def get_file_upload_limit(self, block): if not block.allow_multiple_files: return 1 return block.MAX_FILES_COUNT + class TeamsConfigSerializer(Serializer): enabled = BooleanField(source="is_team_assignment") teamset_name = SerializerMethodField() @@ -49,15 +53,17 @@ def get_teamset_name(self, block): if block.teamset_config is not None: return block.teamset_config.name + class SubmissionConfigSerializer(Serializer): start = DateTimeField(source="submission_start") due = DateTimeField(source="submission_due") - text_response_config = TextResponseConfigSerializer(source='*') - file_response_config = FileResponseConfigSerializer(source='*') + text_response_config = TextResponseConfigSerializer(source="*") + file_response_config = FileResponseConfigSerializer(source="*") teams_config = TeamsConfigSerializer(source="*") + class OraBlockInfoSerializer(Serializer): """ Main serializer for statically-defined ORA Block information From aa7823a0196df2cf171c52e6ae2b614b5ad7622c Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Fri, 28 Jul 2023 11:54:09 -0400 Subject: [PATCH 05/24] feat: add assessment steps config --- .../xblock/data_layer/serializers.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py index e0ea0d57e3..eaecf4f199 100644 --- a/openassessment/xblock/data_layer/serializers.py +++ b/openassessment/xblock/data_layer/serializers.py @@ -1,6 +1,7 @@ from rest_framework.serializers import ( BooleanField, DateTimeField, + IntegerField, Serializer, CharField, ListField, @@ -64,6 +65,113 @@ class SubmissionConfigSerializer(Serializer): teams_config = TeamsConfigSerializer(source="*") +class RubricFeedbackConfigSerializer(Serializer): + description = CharField(source="rubric_feedback_prompt") # is this this field? + default_text = CharField(source="rubric_feedback_default_text") + + +class RubricCriterionOptionSerializer(Serializer): + name = CharField() + label = CharField() + points = IntegerField() + description = CharField(source="explanation") + + +class RubricCriterionSerializer(Serializer): + name = CharField(source="label") + description = CharField(source="prompt") + feedback_enabled = SerializerMethodField() + feedback_required = SerializerMethodField() + options = RubricCriterionOptionSerializer(many=True) + + @staticmethod + def _feedback(criterion): + return criterion.get("feedback", "disabled") + + def get_feedback_enabled(self, criterion): + return self._feedback(criterion) != "disabled" + + def get_feedback_required(self, criterion): + return self._feedback(criterion) == "required" + + +class RubricConfigSerializer(Serializer): + show_during_response = BooleanField(source="show_rubric_during_response") + feedback_config = RubricFeedbackConfigSerializer(source="*") + criteria = RubricCriterionSerializer( + many=True, source="rubric_criteria_with_labels" + ) + + +class RequiredMixin(Serializer): + required = BooleanField(default=True) + + +class StartEndMixin(Serializer): + start = DateTimeField() + due = DateTimeField() + + +class TrainingSettingsSerializer(RequiredMixin, Serializer): + pass + + +class PeerSettingsSerializer(RequiredMixin, StartEndMixin, Serializer): + min_number_to_grade = IntegerField(source="must_grade") + min_number_to_be_graded_by = IntegerField(source="must_be_graded_by") + flexible_grading = BooleanField(source="enable_flexible_grading") + + +class SelfSettingsSerializer(RequiredMixin, Serializer): + pass + + +class StaffSettingsSerializer(RequiredMixin, Serializer): + pass + + +class AssessmentStepsSettingsSerializer(Serializer): + training_step = SerializerMethodField(label="training") + peer_step = SerializerMethodField(label="peer") + self_step = SerializerMethodField(label="self") + staff_step = SerializerMethodField(label="staff") + + def _get_step(self, instance, step_name): + """Get the assessment step config for a given step_name""" + for step in instance.rubric_assessments: + if step["name"] == step_name: + return step + return None + + def get_training_step(self, instance): + """Get the training step configuration""" + training_step = self._get_step(instance, "student-training") + return TrainingSettingsSerializer(training_step).data or {} + + def get_peer_step(self, instance): + """Get the peer step configuration""" + peer_step = self._get_step(instance, "peer-assessment") + return PeerSettingsSerializer(peer_step).data or {} + + def get_self_step(self, instance): + """Get the self step configuration""" + self_step = self._get_step(instance, "self-assessment") + return SelfSettingsSerializer(self_step).data or {} + + def get_staff_step(self, instance): + """Get the staff step configuration""" + staff_step = self._get_step(instance, "staff-assessment") + return StaffSettingsSerializer(staff_step).data or {} + + +class AssessmentStepsSerializer(Serializer): + order = SerializerMethodField() + settings = AssessmentStepsSettingsSerializer(source="*") + + def get_order(self, block): + return [step["name"] for step in block.rubric_assessments] + + class OraBlockInfoSerializer(Serializer): """ Main serializer for statically-defined ORA Block information @@ -74,6 +182,8 @@ class OraBlockInfoSerializer(Serializer): base_asset_url = SerializerMethodField(source="*") submission_config = SubmissionConfigSerializer(source="*") + assessment_steps = AssessmentStepsSerializer(source="*") + rubric_config = RubricConfigSerializer(source="*") def get_base_asset_url(self, block): return block._get_base_url_path_for_course_assets(block.course.id) From 9882dcff612097b51b1ee2771ac7a9e8de315f34 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Mon, 31 Jul 2023 14:27:38 -0400 Subject: [PATCH 06/24] feat: add leaderboard config --- openassessment/xblock/data_layer/serializers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py index eaecf4f199..fea1434b91 100644 --- a/openassessment/xblock/data_layer/serializers.py +++ b/openassessment/xblock/data_layer/serializers.py @@ -171,6 +171,12 @@ class AssessmentStepsSerializer(Serializer): def get_order(self, block): return [step["name"] for step in block.rubric_assessments] +class LeaderboardConfigSerializer(Serializer): + enabled = SerializerMethodField() + number_to_show = IntegerField(source="leaderboard_show") + + def get_enabled(self, block): + return block.leaderboard_show > 0 class OraBlockInfoSerializer(Serializer): """ @@ -184,6 +190,7 @@ class OraBlockInfoSerializer(Serializer): submission_config = SubmissionConfigSerializer(source="*") assessment_steps = AssessmentStepsSerializer(source="*") rubric_config = RubricConfigSerializer(source="*") + leaderboard = LeaderboardConfigSerializer(source="*") def get_base_asset_url(self, block): return block._get_base_url_path_for_course_assets(block.course.id) From 22023ba55ede9181c47eb94452f90c3b797cf941 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Mon, 31 Jul 2023 14:52:39 -0400 Subject: [PATCH 07/24] style: fix pep8 issues --- openassessment/xblock/data_layer/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py index fea1434b91..11dfe100c8 100644 --- a/openassessment/xblock/data_layer/serializers.py +++ b/openassessment/xblock/data_layer/serializers.py @@ -171,6 +171,7 @@ class AssessmentStepsSerializer(Serializer): def get_order(self, block): return [step["name"] for step in block.rubric_assessments] + class LeaderboardConfigSerializer(Serializer): enabled = SerializerMethodField() number_to_show = IntegerField(source="leaderboard_show") @@ -178,6 +179,7 @@ class LeaderboardConfigSerializer(Serializer): def get_enabled(self, block): return block.leaderboard_show > 0 + class OraBlockInfoSerializer(Serializer): """ Main serializer for statically-defined ORA Block information From 4aa0717a66be1b857744bdcdcd8ec3a95a12de04 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Mon, 31 Jul 2023 14:52:47 -0400 Subject: [PATCH 08/24] docs: update docstring --- openassessment/xblock/data_layer/data_layer_mixin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openassessment/xblock/data_layer/data_layer_mixin.py b/openassessment/xblock/data_layer/data_layer_mixin.py index 01a2877908..1540cd3ce2 100644 --- a/openassessment/xblock/data_layer/data_layer_mixin.py +++ b/openassessment/xblock/data_layer/data_layer_mixin.py @@ -1,5 +1,7 @@ """ -XBlock +Data layer for ORA + +XBlock handlers which surface info about an ORA, instead of being tied to views. """ from xblock.core import XBlock From 2ee6f3e6e913c25c6e97fe67747ec7f837c2e5c5 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Mon, 31 Jul 2023 17:18:57 -0400 Subject: [PATCH 09/24] style: fix indent --- openassessment/xblock/openassessmentblock.py | 40 ++++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/openassessment/xblock/openassessmentblock.py b/openassessment/xblock/openassessmentblock.py index 2c13ca7f29..cc7d83e81b 100644 --- a/openassessment/xblock/openassessmentblock.py +++ b/openassessment/xblock/openassessmentblock.py @@ -108,26 +108,26 @@ def load(path): @XBlock.needs("teams") @XBlock.needs("teams_configuration") class OpenAssessmentBlock( - MessageMixin, - SubmissionMixin, - PeerAssessmentMixin, - SelfAssessmentMixin, - StaffAssessmentMixin, - StudioMixin, - GradeMixin, - LeaderboardMixin, - StaffAreaMixin, - WorkflowMixin, - TeamWorkflowMixin, - StudentTrainingMixin, - LmsCompatibilityMixin, - CourseItemsListingMixin, - ConfigMixin, - TeamMixin, - OpenAssessmentTemplatesMixin, - RubricReuseMixin, - StaffGraderMixin, - DataLayerMixin, + MessageMixin, + SubmissionMixin, + PeerAssessmentMixin, + SelfAssessmentMixin, + StaffAssessmentMixin, + StudioMixin, + GradeMixin, + LeaderboardMixin, + StaffAreaMixin, + WorkflowMixin, + TeamWorkflowMixin, + StudentTrainingMixin, + LmsCompatibilityMixin, + CourseItemsListingMixin, + ConfigMixin, + TeamMixin, + OpenAssessmentTemplatesMixin, + RubricReuseMixin, + StaffGraderMixin, + DataLayerMixin, XBlock ): """Displays a prompt and provides an area where students can compose a response.""" From ff97a253e3826e0a6ff48d743eaf8fba735b01ed Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Tue, 1 Aug 2023 15:01:38 -0400 Subject: [PATCH 10/24] fix: default flex grading field when disabled --- openassessment/xblock/data_layer/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py index 11dfe100c8..6acf26bbf1 100644 --- a/openassessment/xblock/data_layer/serializers.py +++ b/openassessment/xblock/data_layer/serializers.py @@ -119,7 +119,7 @@ class TrainingSettingsSerializer(RequiredMixin, Serializer): class PeerSettingsSerializer(RequiredMixin, StartEndMixin, Serializer): min_number_to_grade = IntegerField(source="must_grade") min_number_to_be_graded_by = IntegerField(source="must_be_graded_by") - flexible_grading = BooleanField(source="enable_flexible_grading") + flexible_grading = BooleanField(source="enable_flexible_grading", required=False) class SelfSettingsSerializer(RequiredMixin, Serializer): From 5dca62764e2ca5113c505ed7f9bed2cb3ad8c56b Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Tue, 1 Aug 2023 15:02:51 -0400 Subject: [PATCH 11/24] test: assessment and leaderboard serializer Added new test block setup for flex grading. --- .../xblock/data_layer/test_serializers.py | 187 ++++++++++++++++++ .../peer_assessment_flex_grading_scenario.xml | 49 +++++ 2 files changed, 236 insertions(+) create mode 100644 openassessment/xblock/data_layer/test_serializers.py create mode 100644 openassessment/xblock/test/data/peer_assessment_flex_grading_scenario.xml diff --git a/openassessment/xblock/data_layer/test_serializers.py b/openassessment/xblock/data_layer/test_serializers.py new file mode 100644 index 0000000000..abe4336152 --- /dev/null +++ b/openassessment/xblock/data_layer/test_serializers.py @@ -0,0 +1,187 @@ +""" +Tests for data layer of ORA XBlock +""" + +from openassessment.xblock.data_layer.serializers import ( + AssessmentStepsSerializer, + LeaderboardConfigSerializer, +) +from openassessment.xblock.test.base import XBlockHandlerTestCase, scenario + + +class TestAssessmentStepsSerializer(XBlockHandlerTestCase): + """ + Test for AssessmentStepsSerializer + """ + + @scenario("data/basic_scenario.xml") + def test_order(self, xblock): + # Given a basic setup + expected_order = ["peer-assessment", "self-assessment"] + expected_step_keys = {"training_step", "peer_step", "self_step", "staff_step"} + + # When I ask for assessment step config + steps_config = AssessmentStepsSerializer(xblock).data + + # Then I get the right ordering and step keys + self.assertListEqual(steps_config["order"], expected_order) + steps = {step for step in steps_config["settings"].keys()} + self.assertSetEqual(steps, expected_step_keys) + + +class TestPeerSettingsSerializer(XBlockHandlerTestCase): + """Tests for PeerSettingsSerializer""" + + @scenario("data/basic_scenario.xml") + def test_peer_settings(self, xblock): + # Given a basic setup + expected_must_grade = 5 + expected_grade_by = 3 + + # When I ask for peer step config + peer_config = AssessmentStepsSerializer(xblock).data["settings"]["peer_step"] + + # Then I get the right config + self.assertEqual(peer_config["min_number_to_grade"], expected_must_grade) + self.assertEqual(peer_config["min_number_to_be_graded_by"], expected_grade_by) + + @scenario("data/dates_scenario.xml") + def test_peer_dates(self, xblock): + # Given a basic setup + expected_start = "2015-01-02T00:00:00" + expected_due = "2015-04-01T00:00:00" + + # When I ask for peer step config + peer_config = AssessmentStepsSerializer(xblock).data["settings"]["peer_step"] + + # Then I get the right dates + self.assertEqual(peer_config["start"], expected_start) + self.assertEqual(peer_config["due"], expected_due) + + @scenario("data/peer_assessment_flex_grading_scenario.xml") + def test_flex_grading(self, xblock): + # Given a peer step with flex grading + + # When I ask for peer step config + peer_config = AssessmentStepsSerializer(xblock).data["settings"]["peer_step"] + + # Then I get the right steps and ordering + self.assertTrue(peer_config["flexible_grading"]) + + +class TestTrainingSettingsSerializer(XBlockHandlerTestCase): + """ + Test for TrainingSettingsSerializer + """ + + step_config_key = "training_step" + + @scenario("data/student_training.xml") + def test_enabled(self, xblock): + # Given an ORA with a training step + # When I ask for step config + step_config = AssessmentStepsSerializer(xblock).data["settings"][ + self.step_config_key + ] + + # Then I get the right config + self.assertTrue(step_config["required"]) + + @scenario("data/basic_scenario.xml") + def test_disabled(self, xblock): + # Given an ORA without a training step + # When I ask for step config + step_config = AssessmentStepsSerializer(xblock).data["settings"][ + self.step_config_key + ] + + # Then I get the right config + self.assertFalse(step_config["required"]) + + +class TestSelfSettingsSerializer(XBlockHandlerTestCase): + """ + Test for SelfSettingsSerializer + """ + + step_config_key = "self_step" + + @scenario("data/self_assessment_scenario.xml") + def test_enabled(self, xblock): + # Given an ORA with a self assessment step + # When I ask for step config + step_config = AssessmentStepsSerializer(xblock).data["settings"][ + self.step_config_key + ] + + # Then I get the right config + self.assertTrue(step_config["required"]) + + @scenario("data/peer_only_scenario.xml") + def test_disabled(self, xblock): + # Given an ORA without a self assessment step + # When I ask for step config + step_config = AssessmentStepsSerializer(xblock).data["settings"][ + self.step_config_key + ] + + # Then I get the right config + self.assertFalse(step_config["required"]) + + +class TestStaffSettingsSerializer(XBlockHandlerTestCase): + """ + Test for StaffSettingsSerializer + """ + + step_config_key = "staff_step" + + @scenario("data/staff_grade_scenario.xml") + def test_enabled(self, xblock): + # Given an ORA with a staff assessment step + # When I ask for step config + step_config = AssessmentStepsSerializer(xblock).data["settings"][ + self.step_config_key + ] + + # Then I get the right config + self.assertTrue(step_config["required"]) + + @scenario("data/peer_only_scenario.xml") + def test_disabled(self, xblock): + # Given an ORA without a staff assessment step + # When I ask for step config + step_config = AssessmentStepsSerializer(xblock).data["settings"][ + self.step_config_key + ] + + # Then I get the right config + self.assertFalse(step_config["required"]) + + +class TestLeaderboardConfigSerializer(XBlockHandlerTestCase): + """ + Test for LeaderboardConfigSerializer + """ + + @scenario("data/leaderboard_show.xml") + def test_leaderboard(self, xblock): + # Given I have a leaderboard configured + number_to_show = xblock.leaderboard_show + + # When I ask for leaderboard config + leaderboard_config = LeaderboardConfigSerializer(xblock).data + + # Then I get the expected config + self.assertTrue(leaderboard_config["enabled"]) + self.assertEqual(leaderboard_config["number_to_show"], number_to_show) + + @scenario("data/basic_scenario.xml") + def test_no_leaderboard(self, xblock): + # Given I don't have a leaderboard configured + # When I ask for leaderboard config + leaderboard_config = LeaderboardConfigSerializer(xblock).data + + # Then I get the expected config + self.assertFalse(leaderboard_config["enabled"]) + self.assertEqual(leaderboard_config["number_to_show"], 0) diff --git a/openassessment/xblock/test/data/peer_assessment_flex_grading_scenario.xml b/openassessment/xblock/test/data/peer_assessment_flex_grading_scenario.xml new file mode 100644 index 0000000000..74a8fbe966 --- /dev/null +++ b/openassessment/xblock/test/data/peer_assessment_flex_grading_scenario.xml @@ -0,0 +1,49 @@ + + Open Assessment Test + + + Given the state of the world today, what do you think should be done to combat poverty? + + + Given the state of the world today, what do you think should be done to combat pollution? + + + + + 𝓒𝓸𝓷𝓬𝓲𝓼𝓮 + How concise is it? + + + + + + Form + How well-formed is it? + + + + + + + + + + From 0a51e179c1823f5db5c7fb065b47dd3f9099b891 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 2 Aug 2023 10:54:24 -0400 Subject: [PATCH 12/24] test: add tests for rubrics --- .../xblock/data_layer/test_serializers.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/openassessment/xblock/data_layer/test_serializers.py b/openassessment/xblock/data_layer/test_serializers.py index abe4336152..46bf984647 100644 --- a/openassessment/xblock/data_layer/test_serializers.py +++ b/openassessment/xblock/data_layer/test_serializers.py @@ -2,13 +2,110 @@ Tests for data layer of ORA XBlock """ +import ddt +import pytest + +from openassessment.xblock.defaults import ( + DEFAULT_RUBRIC_FEEDBACK_PROMPT, + DEFAULT_RUBRIC_FEEDBACK_TEXT, +) + from openassessment.xblock.data_layer.serializers import ( AssessmentStepsSerializer, LeaderboardConfigSerializer, + RubricConfigSerializer, ) from openassessment.xblock.test.base import XBlockHandlerTestCase, scenario +@ddt.ddt +class TestRubricConfigSerializer(XBlockHandlerTestCase): + """ + Test for RubricConfigSerializer + """ + + @ddt.data(True, False) + @scenario("data/basic_scenario.xml") + def test_show_during_response(self, xblock, mock_show_rubric): + # Given a basic setup where I do/not have rubric shown during response + xblock.show_rubric_during_response = mock_show_rubric + + # When I ask for rubric config + rubric_config = RubricConfigSerializer(xblock).data + + # Then I get the right values + self.assertEqual(rubric_config["show_during_response"], mock_show_rubric) + + @scenario("data/feedback_only_criterion_staff.xml") + def test_overall_feedback(self, xblock): + # Given an ORA block with one criterion + + # When I ask for rubric config + rubric_config = RubricConfigSerializer(xblock).data + + # Then I get the expected defaults + criteria = rubric_config["criteria"] + criterion = criteria[0] + self.assertEqual(len(criteria), 1) + self.assertEqual(criterion["name"], "vocabulary") + self.assertEqual( + criterion["description"], + "This criterion accepts only written feedback, so it has no options", + ) + + # ... In this example, feedback is required + self.assertTrue(criterion["feedback_enabled"]) + self.assertTrue(criterion["feedback_required"]) + + @scenario("data/feedback_only_criterion_staff.xml") + def test_criterion(self, xblock): + # Given an ORA block with one criterion + + # When I ask for rubric config + rubric_config = RubricConfigSerializer(xblock).data + + # Then I get the expected defaults + criteria = rubric_config["criteria"] + criterion = criteria[0] + self.assertEqual(len(criteria), 1) + self.assertEqual(criterion["name"], "vocabulary") + self.assertEqual( + criterion["description"], + "This criterion accepts only written feedback, so it has no options", + ) + + # ... In this example, feedback is required + self.assertTrue(criterion["feedback_enabled"]) + self.assertTrue(criterion["feedback_required"]) + + @scenario("data/basic_scenario.xml") + def test_criteria(self, xblock): + # Given an ORA block with multiple criteria + expected_criteria = xblock.rubric_criteria + + # When I ask for rubric config + rubric_config = RubricConfigSerializer(xblock).data + + # Then I get the expected number of criteria + criteria = rubric_config["criteria"] + self.assertEqual(len(criteria), len(expected_criteria)) + + @scenario("data/basic_scenario.xml") + def test_feedback_config(self, xblock): + # Given an ORA block with feedback + xblock.rubric_feedback_prompt = "foo" + xblock.rubric_feedback_default_text = "bar" + + # When I ask for rubric config + feedback_config = RubricConfigSerializer(xblock).data["feedback_config"] + + # Then I get the expected defaults + self.assertEqual(feedback_config["description"], xblock.rubric_feedback_prompt) + self.assertEqual( + feedback_config["default_text"], xblock.rubric_feedback_default_text + ) + + class TestAssessmentStepsSerializer(XBlockHandlerTestCase): """ Test for AssessmentStepsSerializer From 62a43b980140fb0a54f72af4fe8438acde81a9be Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 2 Aug 2023 15:35:38 -0400 Subject: [PATCH 13/24] test: add tests for submissions --- .../xblock/data_layer/test_serializers.py | 132 +++++++++++++++++- 1 file changed, 127 insertions(+), 5 deletions(-) diff --git a/openassessment/xblock/data_layer/test_serializers.py b/openassessment/xblock/data_layer/test_serializers.py index 46bf984647..d08916e505 100644 --- a/openassessment/xblock/data_layer/test_serializers.py +++ b/openassessment/xblock/data_layer/test_serializers.py @@ -2,20 +2,142 @@ Tests for data layer of ORA XBlock """ +from unittest.mock import MagicMock + import ddt -import pytest -from openassessment.xblock.defaults import ( - DEFAULT_RUBRIC_FEEDBACK_PROMPT, - DEFAULT_RUBRIC_FEEDBACK_TEXT, -) from openassessment.xblock.data_layer.serializers import ( AssessmentStepsSerializer, LeaderboardConfigSerializer, RubricConfigSerializer, + SubmissionConfigSerializer, ) from openassessment.xblock.test.base import XBlockHandlerTestCase, scenario +from openassessment.xblock.test.test_team import ( + MockTeamsConfigurationService, + MockTeamsService, +) + + +class TestSubmissionConfigSerializer(XBlockHandlerTestCase): + """ + Test for SubmissionConfigSerializer + """ + + def _enable_team_ora(self, xblock): + """Utility function for mocking team dependencies on the passed xblock""" + xblock.is_team_assignment = MagicMock(return_value=True) + + xblock.teamset_config = MagicMock() + xblock.teamset_config.name = xblock.selected_teamset_id + + @scenario("data/submission_open.xml") + def test_dates(self, xblock): + # Given an individual (non-teams) ORA + xblock.teamset_config = MagicMock(return_value=None) + + # When I ask for the submission config + submission_config = SubmissionConfigSerializer(xblock).data + + # Then I get the expected values + expected_start = xblock.submission_start + expected_due = xblock.submission_due + self.assertEqual(submission_config["start"], expected_start) + self.assertEqual(submission_config["due"], expected_due) + + @scenario("data/basic_scenario.xml") + def test_dates_missing(self, xblock): + # Given an individual (non-teams) ORA + xblock.teamset_config = MagicMock(return_value=None) + + # When I ask for submission config + submission_config = SubmissionConfigSerializer(xblock).data + + # Then I get the expected values + self.assertIsNone(submission_config["start"]) + self.assertIsNone(submission_config["due"]) + + @scenario("data/basic_scenario.xml") + def test_text_response_config(self, xblock): + # Given an individual (non-teams) ORA with a text response + xblock.teamset_config = MagicMock(return_value=None) + + # When I ask for text response config + submission_config = SubmissionConfigSerializer(xblock).data + text_response_config = submission_config["text_response_config"] + + # Then I get the expected values + self.assertTrue(text_response_config["enabled"]) + self.assertTrue(text_response_config["required"]) + self.assertEqual(text_response_config["editor_type"], "text") + self.assertFalse(text_response_config["allow_latex_preview"]) + + @scenario("data/basic_scenario.xml") + def test_html_response_config(self, xblock): + # Given an individual (non-teams) ORA with an html response + xblock.teamset_config = MagicMock(return_value=None) + xblock.text_response_editor = "html" + + # When I ask for text response config + submission_config = SubmissionConfigSerializer(xblock).data + text_response_config = submission_config["text_response_config"] + + # Then I get the expected values + self.assertEqual(text_response_config["editor_type"], "html") + + @scenario("data/basic_scenario.xml") + def test_latex_preview(self, xblock): + # Given an individual (non-teams) ORA + xblock.teamset_config = MagicMock(return_value=None) + # ... with latex preview enabled + xblock.allow_latex = True + + # When I ask for text response config + submission_config = SubmissionConfigSerializer(xblock).data + text_response_config = submission_config["text_response_config"] + + # Then I get the expected values + self.assertTrue(text_response_config["allow_latex_preview"]) + + @scenario("data/file_upload_scenario.xml") + def test_file_response_config(self, xblock): + # Given an individual (non-teams) ORA with file upload enabled + xblock.teamset_config = MagicMock(return_value=None) + + # When I ask for file upload config + submission_config = SubmissionConfigSerializer(xblock).data + file_response_config = submission_config["file_response_config"] + + # Then I get the expected values + self.assertTrue(file_response_config["enabled"]) + self.assertEqual( + file_response_config["file_upload_limit"], xblock.MAX_FILES_COUNT + ) + self.assertEqual( + file_response_config["allowed_file_type_description"], + xblock.file_upload_type, + ) + self.assertEqual( + file_response_config["allowed_extensions"], + xblock.get_allowed_file_types_or_preset(), + ) + self.assertEqual( + file_response_config["blocked_extensions"], xblock.FILE_EXT_BLACK_LIST + ) + + @scenario("data/team_submission.xml") + def test_team_ora_config(self, xblock): + # Given a team ORA + self._enable_team_ora(xblock) + + # When I ask for teams config + submission_config = SubmissionConfigSerializer(xblock).data + teams_config = submission_config["teams_config"] + + # Then I get the expected values + self.assertTrue(teams_config["enabled"]) + self.assertEqual(teams_config["teamset_name"], xblock.selected_teamset_id) @ddt.ddt From 9ef25395688793ac1d79d1a0cd0d6fe9620600e3 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 2 Aug 2023 15:53:49 -0400 Subject: [PATCH 14/24] chore: remove context and fix pylint issue --- openassessment/xblock/data_layer/data_layer_mixin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openassessment/xblock/data_layer/data_layer_mixin.py b/openassessment/xblock/data_layer/data_layer_mixin.py index 1540cd3ce2..d4ad40920a 100644 --- a/openassessment/xblock/data_layer/data_layer_mixin.py +++ b/openassessment/xblock/data_layer/data_layer_mixin.py @@ -10,7 +10,6 @@ class DataLayerMixin: @XBlock.json_handler - def get_block_info(self, data, suffix=""): - context = {} - block_info = OraBlockInfoSerializer(self, context=context) + def get_block_info(self, data, suffix=""): # pylint: disable=unused-argument + block_info = OraBlockInfoSerializer(self) return block_info.data From 028754862f06862c456f8e691d8deb1c490d2dc6 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 2 Aug 2023 15:54:19 -0400 Subject: [PATCH 15/24] style: convert output to camelCase --- .../xblock/data_layer/serializers.py | 72 +++++++++---------- .../xblock/data_layer/test_serializers.py | 66 ++++++++--------- 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py index 6acf26bbf1..00f0db8550 100644 --- a/openassessment/xblock/data_layer/serializers.py +++ b/openassessment/xblock/data_layer/serializers.py @@ -16,8 +16,8 @@ class CharListField(ListField): class TextResponseConfigSerializer(Serializer): enabled = SerializerMethodField() required = SerializerMethodField() - editor_type = CharField(source="text_response_editor") - allow_latex_preview = BooleanField(source="allow_latex") + editorType = CharField(source="text_response_editor") + allowLatexPreview = BooleanField(source="allow_latex") def get_enabled(self, block): return block.text_response is not None @@ -29,10 +29,10 @@ def get_required(self, block): class FileResponseConfigSerializer(Serializer): enabled = SerializerMethodField() required = SerializerMethodField() - file_upload_limit = SerializerMethodField() - allowed_extensions = CharListField(source="get_allowed_file_types_or_preset") - blocked_extensions = CharListField(source="FILE_EXT_BLACK_LIST") - allowed_file_type_description = CharField(source="file_upload_type") + fileUploadLimit = SerializerMethodField() + allowedExtensions = CharListField(source="get_allowed_file_types_or_preset") + blockedExtensions = CharListField(source="FILE_EXT_BLACK_LIST") + allowedFileTypeDescription = CharField(source="file_upload_type") def get_enabled(self, block): return block.file_upload_response is not None @@ -40,7 +40,7 @@ def get_enabled(self, block): def get_required(self, block): return block.file_upload_response == "required" - def get_file_upload_limit(self, block): + def get_fileUploadLimit(self, block): if not block.allow_multiple_files: return 1 return block.MAX_FILES_COUNT @@ -48,9 +48,9 @@ def get_file_upload_limit(self, block): class TeamsConfigSerializer(Serializer): enabled = BooleanField(source="is_team_assignment") - teamset_name = SerializerMethodField() + teamsetName = SerializerMethodField() - def get_teamset_name(self, block): + def get_teamsetName(self, block): if block.teamset_config is not None: return block.teamset_config.name @@ -59,15 +59,15 @@ class SubmissionConfigSerializer(Serializer): start = DateTimeField(source="submission_start") due = DateTimeField(source="submission_due") - text_response_config = TextResponseConfigSerializer(source="*") - file_response_config = FileResponseConfigSerializer(source="*") + textResponseConfig = TextResponseConfigSerializer(source="*") + fileResponseConfig = FileResponseConfigSerializer(source="*") - teams_config = TeamsConfigSerializer(source="*") + teamsConfig = TeamsConfigSerializer(source="*") class RubricFeedbackConfigSerializer(Serializer): description = CharField(source="rubric_feedback_prompt") # is this this field? - default_text = CharField(source="rubric_feedback_default_text") + defaultText = CharField(source="rubric_feedback_default_text") class RubricCriterionOptionSerializer(Serializer): @@ -80,24 +80,24 @@ class RubricCriterionOptionSerializer(Serializer): class RubricCriterionSerializer(Serializer): name = CharField(source="label") description = CharField(source="prompt") - feedback_enabled = SerializerMethodField() - feedback_required = SerializerMethodField() + feedbackEnabled = SerializerMethodField() + feedbackRequired = SerializerMethodField() options = RubricCriterionOptionSerializer(many=True) @staticmethod def _feedback(criterion): return criterion.get("feedback", "disabled") - def get_feedback_enabled(self, criterion): + def get_feedbackEnabled(self, criterion): return self._feedback(criterion) != "disabled" - def get_feedback_required(self, criterion): + def get_feedbackRequired(self, criterion): return self._feedback(criterion) == "required" class RubricConfigSerializer(Serializer): - show_during_response = BooleanField(source="show_rubric_during_response") - feedback_config = RubricFeedbackConfigSerializer(source="*") + showDuringResponse = BooleanField(source="show_rubric_during_response") + feedbackConfig = RubricFeedbackConfigSerializer(source="*") criteria = RubricCriterionSerializer( many=True, source="rubric_criteria_with_labels" ) @@ -117,9 +117,9 @@ class TrainingSettingsSerializer(RequiredMixin, Serializer): class PeerSettingsSerializer(RequiredMixin, StartEndMixin, Serializer): - min_number_to_grade = IntegerField(source="must_grade") - min_number_to_be_graded_by = IntegerField(source="must_be_graded_by") - flexible_grading = BooleanField(source="enable_flexible_grading", required=False) + minNumberToGrade = IntegerField(source="must_grade") + minNumberToBeGradedBy = IntegerField(source="must_be_graded_by") + flexibleGrading = BooleanField(source="enable_flexible_grading", required=False) class SelfSettingsSerializer(RequiredMixin, Serializer): @@ -131,10 +131,10 @@ class StaffSettingsSerializer(RequiredMixin, Serializer): class AssessmentStepsSettingsSerializer(Serializer): - training_step = SerializerMethodField(label="training") - peer_step = SerializerMethodField(label="peer") - self_step = SerializerMethodField(label="self") - staff_step = SerializerMethodField(label="staff") + trainingStep = SerializerMethodField() + peerStep = SerializerMethodField() + selfStep = SerializerMethodField() + staffStep = SerializerMethodField() def _get_step(self, instance, step_name): """Get the assessment step config for a given step_name""" @@ -143,22 +143,22 @@ def _get_step(self, instance, step_name): return step return None - def get_training_step(self, instance): + def get_trainingStep(self, instance): """Get the training step configuration""" training_step = self._get_step(instance, "student-training") return TrainingSettingsSerializer(training_step).data or {} - def get_peer_step(self, instance): + def get_peerStep(self, instance): """Get the peer step configuration""" peer_step = self._get_step(instance, "peer-assessment") return PeerSettingsSerializer(peer_step).data or {} - def get_self_step(self, instance): + def get_selfStep(self, instance): """Get the self step configuration""" self_step = self._get_step(instance, "self-assessment") return SelfSettingsSerializer(self_step).data or {} - def get_staff_step(self, instance): + def get_staffStep(self, instance): """Get the staff step configuration""" staff_step = self._get_step(instance, "staff-assessment") return StaffSettingsSerializer(staff_step).data or {} @@ -174,7 +174,7 @@ def get_order(self, block): class LeaderboardConfigSerializer(Serializer): enabled = SerializerMethodField() - number_to_show = IntegerField(source="leaderboard_show") + numberToShow = IntegerField(source="leaderboard_show") def get_enabled(self, block): return block.leaderboard_show > 0 @@ -187,14 +187,14 @@ class OraBlockInfoSerializer(Serializer): title = CharField() prompts = SerializerMethodField(source="*") - base_asset_url = SerializerMethodField(source="*") + baseAssetUrl = SerializerMethodField(source="*") - submission_config = SubmissionConfigSerializer(source="*") - assessment_steps = AssessmentStepsSerializer(source="*") - rubric_config = RubricConfigSerializer(source="*") + submissionConfig = SubmissionConfigSerializer(source="*") + assessmentSteps = AssessmentStepsSerializer(source="*") + rubricConfig = RubricConfigSerializer(source="*") leaderboard = LeaderboardConfigSerializer(source="*") - def get_base_asset_url(self, block): + def get_baseAssetUrl(self, block): return block._get_base_url_path_for_course_assets(block.course.id) def get_prompts(self, block): diff --git a/openassessment/xblock/data_layer/test_serializers.py b/openassessment/xblock/data_layer/test_serializers.py index d08916e505..7cda5205ed 100644 --- a/openassessment/xblock/data_layer/test_serializers.py +++ b/openassessment/xblock/data_layer/test_serializers.py @@ -65,13 +65,13 @@ def test_text_response_config(self, xblock): # When I ask for text response config submission_config = SubmissionConfigSerializer(xblock).data - text_response_config = submission_config["text_response_config"] + text_response_config = submission_config["textResponseConfig"] # Then I get the expected values self.assertTrue(text_response_config["enabled"]) self.assertTrue(text_response_config["required"]) - self.assertEqual(text_response_config["editor_type"], "text") - self.assertFalse(text_response_config["allow_latex_preview"]) + self.assertEqual(text_response_config["editorType"], "text") + self.assertFalse(text_response_config["allowLatexPreview"]) @scenario("data/basic_scenario.xml") def test_html_response_config(self, xblock): @@ -81,10 +81,10 @@ def test_html_response_config(self, xblock): # When I ask for text response config submission_config = SubmissionConfigSerializer(xblock).data - text_response_config = submission_config["text_response_config"] + text_response_config = submission_config["textResponseConfig"] # Then I get the expected values - self.assertEqual(text_response_config["editor_type"], "html") + self.assertEqual(text_response_config["editorType"], "html") @scenario("data/basic_scenario.xml") def test_latex_preview(self, xblock): @@ -95,10 +95,10 @@ def test_latex_preview(self, xblock): # When I ask for text response config submission_config = SubmissionConfigSerializer(xblock).data - text_response_config = submission_config["text_response_config"] + text_response_config = submission_config["textResponseConfig"] # Then I get the expected values - self.assertTrue(text_response_config["allow_latex_preview"]) + self.assertTrue(text_response_config["allowLatexPreview"]) @scenario("data/file_upload_scenario.xml") def test_file_response_config(self, xblock): @@ -107,23 +107,23 @@ def test_file_response_config(self, xblock): # When I ask for file upload config submission_config = SubmissionConfigSerializer(xblock).data - file_response_config = submission_config["file_response_config"] + file_response_config = submission_config["fileResponseConfig"] # Then I get the expected values self.assertTrue(file_response_config["enabled"]) self.assertEqual( - file_response_config["file_upload_limit"], xblock.MAX_FILES_COUNT + file_response_config["fileUploadLimit"], xblock.MAX_FILES_COUNT ) self.assertEqual( - file_response_config["allowed_file_type_description"], + file_response_config["allowedFileTypeDescription"], xblock.file_upload_type, ) self.assertEqual( - file_response_config["allowed_extensions"], + file_response_config["allowedExtensions"], xblock.get_allowed_file_types_or_preset(), ) self.assertEqual( - file_response_config["blocked_extensions"], xblock.FILE_EXT_BLACK_LIST + file_response_config["blockedExtensions"], xblock.FILE_EXT_BLACK_LIST ) @scenario("data/team_submission.xml") @@ -133,11 +133,11 @@ def test_team_ora_config(self, xblock): # When I ask for teams config submission_config = SubmissionConfigSerializer(xblock).data - teams_config = submission_config["teams_config"] + teams_config = submission_config["teamsConfig"] # Then I get the expected values self.assertTrue(teams_config["enabled"]) - self.assertEqual(teams_config["teamset_name"], xblock.selected_teamset_id) + self.assertEqual(teams_config["teamsetName"], xblock.selected_teamset_id) @ddt.ddt @@ -156,7 +156,7 @@ def test_show_during_response(self, xblock, mock_show_rubric): rubric_config = RubricConfigSerializer(xblock).data # Then I get the right values - self.assertEqual(rubric_config["show_during_response"], mock_show_rubric) + self.assertEqual(rubric_config["showDuringResponse"], mock_show_rubric) @scenario("data/feedback_only_criterion_staff.xml") def test_overall_feedback(self, xblock): @@ -176,8 +176,8 @@ def test_overall_feedback(self, xblock): ) # ... In this example, feedback is required - self.assertTrue(criterion["feedback_enabled"]) - self.assertTrue(criterion["feedback_required"]) + self.assertTrue(criterion["feedbackEnabled"]) + self.assertTrue(criterion["feedbackRequired"]) @scenario("data/feedback_only_criterion_staff.xml") def test_criterion(self, xblock): @@ -197,8 +197,8 @@ def test_criterion(self, xblock): ) # ... In this example, feedback is required - self.assertTrue(criterion["feedback_enabled"]) - self.assertTrue(criterion["feedback_required"]) + self.assertTrue(criterion["feedbackEnabled"]) + self.assertTrue(criterion["feedbackRequired"]) @scenario("data/basic_scenario.xml") def test_criteria(self, xblock): @@ -219,12 +219,12 @@ def test_feedback_config(self, xblock): xblock.rubric_feedback_default_text = "bar" # When I ask for rubric config - feedback_config = RubricConfigSerializer(xblock).data["feedback_config"] + feedback_config = RubricConfigSerializer(xblock).data["feedbackConfig"] # Then I get the expected defaults self.assertEqual(feedback_config["description"], xblock.rubric_feedback_prompt) self.assertEqual( - feedback_config["default_text"], xblock.rubric_feedback_default_text + feedback_config["defaultText"], xblock.rubric_feedback_default_text ) @@ -237,7 +237,7 @@ class TestAssessmentStepsSerializer(XBlockHandlerTestCase): def test_order(self, xblock): # Given a basic setup expected_order = ["peer-assessment", "self-assessment"] - expected_step_keys = {"training_step", "peer_step", "self_step", "staff_step"} + expected_step_keys = {"trainingStep", "peerStep", "selfStep", "staffStep"} # When I ask for assessment step config steps_config = AssessmentStepsSerializer(xblock).data @@ -258,11 +258,11 @@ def test_peer_settings(self, xblock): expected_grade_by = 3 # When I ask for peer step config - peer_config = AssessmentStepsSerializer(xblock).data["settings"]["peer_step"] + peer_config = AssessmentStepsSerializer(xblock).data["settings"]["peerStep"] # Then I get the right config - self.assertEqual(peer_config["min_number_to_grade"], expected_must_grade) - self.assertEqual(peer_config["min_number_to_be_graded_by"], expected_grade_by) + self.assertEqual(peer_config["minNumberToGrade"], expected_must_grade) + self.assertEqual(peer_config["minNumberToBeGradedBy"], expected_grade_by) @scenario("data/dates_scenario.xml") def test_peer_dates(self, xblock): @@ -271,7 +271,7 @@ def test_peer_dates(self, xblock): expected_due = "2015-04-01T00:00:00" # When I ask for peer step config - peer_config = AssessmentStepsSerializer(xblock).data["settings"]["peer_step"] + peer_config = AssessmentStepsSerializer(xblock).data["settings"]["peerStep"] # Then I get the right dates self.assertEqual(peer_config["start"], expected_start) @@ -282,10 +282,10 @@ def test_flex_grading(self, xblock): # Given a peer step with flex grading # When I ask for peer step config - peer_config = AssessmentStepsSerializer(xblock).data["settings"]["peer_step"] + peer_config = AssessmentStepsSerializer(xblock).data["settings"]["peerStep"] # Then I get the right steps and ordering - self.assertTrue(peer_config["flexible_grading"]) + self.assertTrue(peer_config["flexibleGrading"]) class TestTrainingSettingsSerializer(XBlockHandlerTestCase): @@ -293,7 +293,7 @@ class TestTrainingSettingsSerializer(XBlockHandlerTestCase): Test for TrainingSettingsSerializer """ - step_config_key = "training_step" + step_config_key = "trainingStep" @scenario("data/student_training.xml") def test_enabled(self, xblock): @@ -323,7 +323,7 @@ class TestSelfSettingsSerializer(XBlockHandlerTestCase): Test for SelfSettingsSerializer """ - step_config_key = "self_step" + step_config_key = "selfStep" @scenario("data/self_assessment_scenario.xml") def test_enabled(self, xblock): @@ -353,7 +353,7 @@ class TestStaffSettingsSerializer(XBlockHandlerTestCase): Test for StaffSettingsSerializer """ - step_config_key = "staff_step" + step_config_key = "staffStep" @scenario("data/staff_grade_scenario.xml") def test_enabled(self, xblock): @@ -393,7 +393,7 @@ def test_leaderboard(self, xblock): # Then I get the expected config self.assertTrue(leaderboard_config["enabled"]) - self.assertEqual(leaderboard_config["number_to_show"], number_to_show) + self.assertEqual(leaderboard_config["numberToShow"], number_to_show) @scenario("data/basic_scenario.xml") def test_no_leaderboard(self, xblock): @@ -403,4 +403,4 @@ def test_no_leaderboard(self, xblock): # Then I get the expected config self.assertFalse(leaderboard_config["enabled"]) - self.assertEqual(leaderboard_config["number_to_show"], 0) + self.assertEqual(leaderboard_config["numberToShow"], 0) From a43873d7c9dbb400be98f0ec7fea492daba854e0 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 2 Aug 2023 16:06:28 -0400 Subject: [PATCH 16/24] refactor: create IsRequired field Reduce code duplication by creating a custom required field --- .../xblock/data_layer/serializers.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py index 00f0db8550..faa8b4236d 100644 --- a/openassessment/xblock/data_layer/serializers.py +++ b/openassessment/xblock/data_layer/serializers.py @@ -13,22 +13,28 @@ class CharListField(ListField): child = CharField() +class IsRequiredField(BooleanField): + """ + Utility for checking if a field is "required" to reduce repeated code. + """ + + def to_representation(self, value): + return value == "required" + + class TextResponseConfigSerializer(Serializer): enabled = SerializerMethodField() - required = SerializerMethodField() + required = IsRequiredField(source="text_response") editorType = CharField(source="text_response_editor") allowLatexPreview = BooleanField(source="allow_latex") def get_enabled(self, block): return block.text_response is not None - def get_required(self, block): - return block.text_response == "required" - class FileResponseConfigSerializer(Serializer): enabled = SerializerMethodField() - required = SerializerMethodField() + required = IsRequiredField(source="file_upload_response") fileUploadLimit = SerializerMethodField() allowedExtensions = CharListField(source="get_allowed_file_types_or_preset") blockedExtensions = CharListField(source="FILE_EXT_BLACK_LIST") @@ -37,9 +43,6 @@ class FileResponseConfigSerializer(Serializer): def get_enabled(self, block): return block.file_upload_response is not None - def get_required(self, block): - return block.file_upload_response == "required" - def get_fileUploadLimit(self, block): if not block.allow_multiple_files: return 1 @@ -81,19 +84,18 @@ class RubricCriterionSerializer(Serializer): name = CharField(source="label") description = CharField(source="prompt") feedbackEnabled = SerializerMethodField() - feedbackRequired = SerializerMethodField() + feedbackRequired = IsRequiredField(source="feedback") options = RubricCriterionOptionSerializer(many=True) @staticmethod def _feedback(criterion): + # Feedback is disabled as a default return criterion.get("feedback", "disabled") def get_feedbackEnabled(self, criterion): + # Feedback can be specified as optional or required return self._feedback(criterion) != "disabled" - def get_feedbackRequired(self, criterion): - return self._feedback(criterion) == "required" - class RubricConfigSerializer(Serializer): showDuringResponse = BooleanField(source="show_rubric_during_response") From 305ed8378ca6c41fa3bf3602c913fa69ac3beb8f Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 2 Aug 2023 16:22:16 -0400 Subject: [PATCH 17/24] test: update rubric criteria tests --- .../xblock/data_layer/test_serializers.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/openassessment/xblock/data_layer/test_serializers.py b/openassessment/xblock/data_layer/test_serializers.py index 7cda5205ed..f03a984a49 100644 --- a/openassessment/xblock/data_layer/test_serializers.py +++ b/openassessment/xblock/data_layer/test_serializers.py @@ -200,6 +200,37 @@ def test_criterion(self, xblock): self.assertTrue(criterion["feedbackEnabled"]) self.assertTrue(criterion["feedbackRequired"]) + @scenario("data/feedback_only_criterion_self.xml") + def test_criterion_disabled_required(self, xblock): + # Given an ORA block with two criterion + + # When I ask for rubric config + rubric_config = RubricConfigSerializer(xblock).data + + # Then I get the expected defaults + criteria = rubric_config["criteria"] + + # .. the first criterion has feedback disabled + self.assertFalse(criteria[0]["feedbackEnabled"]) + self.assertFalse(criteria[0]["feedbackRequired"]) + + # .. the first criterion has feedback required + self.assertTrue(criteria[1]["feedbackEnabled"]) + self.assertTrue(criteria[1]["feedbackRequired"]) + + @scenario("data/file_upload_missing_scenario.xml") + def test_criterion_optional(self, xblock): + # Given an ORA block with one criterion, feedback optional + + # When I ask for rubric config + rubric_config = RubricConfigSerializer(xblock).data + + # Then I get the feedback enabled / required values + criteria = rubric_config["criteria"] + criterion = criteria[0] + self.assertTrue(criterion["feedbackEnabled"]) + self.assertFalse(criterion["feedbackRequired"]) + @scenario("data/basic_scenario.xml") def test_criteria(self, xblock): # Given an ORA block with multiple criteria From 1c4d25c685000d89ef24e6fb8d158655c8df765f Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 2 Aug 2023 16:31:09 -0400 Subject: [PATCH 18/24] refactor: update assessment step names Previously, used a different name to work around not being able to use "self" reserved keyword. Found this workaround to allow. --- .../xblock/data_layer/serializers.py | 17 +++++++------- .../xblock/data_layer/test_serializers.py | 22 +++++++++++++------ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py index faa8b4236d..a5650aebc0 100644 --- a/openassessment/xblock/data_layer/serializers.py +++ b/openassessment/xblock/data_layer/serializers.py @@ -133,10 +133,11 @@ class StaffSettingsSerializer(RequiredMixin, Serializer): class AssessmentStepsSettingsSerializer(Serializer): - trainingStep = SerializerMethodField() - peerStep = SerializerMethodField() - selfStep = SerializerMethodField() - staffStep = SerializerMethodField() + training = SerializerMethodField() + peer = SerializerMethodField() + # Workaround to allow reserved keyword in serializer key + vars()["self"] = SerializerMethodField() + staff = SerializerMethodField() def _get_step(self, instance, step_name): """Get the assessment step config for a given step_name""" @@ -145,22 +146,22 @@ def _get_step(self, instance, step_name): return step return None - def get_trainingStep(self, instance): + def get_training(self, instance): """Get the training step configuration""" training_step = self._get_step(instance, "student-training") return TrainingSettingsSerializer(training_step).data or {} - def get_peerStep(self, instance): + def get_peer(self, instance): """Get the peer step configuration""" peer_step = self._get_step(instance, "peer-assessment") return PeerSettingsSerializer(peer_step).data or {} - def get_selfStep(self, instance): + def get_self(self, instance): """Get the self step configuration""" self_step = self._get_step(instance, "self-assessment") return SelfSettingsSerializer(self_step).data or {} - def get_staffStep(self, instance): + def get_staff(self, instance): """Get the staff step configuration""" staff_step = self._get_step(instance, "staff-assessment") return StaffSettingsSerializer(staff_step).data or {} diff --git a/openassessment/xblock/data_layer/test_serializers.py b/openassessment/xblock/data_layer/test_serializers.py index f03a984a49..11007aaff8 100644 --- a/openassessment/xblock/data_layer/test_serializers.py +++ b/openassessment/xblock/data_layer/test_serializers.py @@ -268,7 +268,7 @@ class TestAssessmentStepsSerializer(XBlockHandlerTestCase): def test_order(self, xblock): # Given a basic setup expected_order = ["peer-assessment", "self-assessment"] - expected_step_keys = {"trainingStep", "peerStep", "selfStep", "staffStep"} + expected_step_keys = {"training", "peer", "self", "staff"} # When I ask for assessment step config steps_config = AssessmentStepsSerializer(xblock).data @@ -282,6 +282,8 @@ def test_order(self, xblock): class TestPeerSettingsSerializer(XBlockHandlerTestCase): """Tests for PeerSettingsSerializer""" + step_config_key = "peer" + @scenario("data/basic_scenario.xml") def test_peer_settings(self, xblock): # Given a basic setup @@ -289,7 +291,9 @@ def test_peer_settings(self, xblock): expected_grade_by = 3 # When I ask for peer step config - peer_config = AssessmentStepsSerializer(xblock).data["settings"]["peerStep"] + peer_config = AssessmentStepsSerializer(xblock).data["settings"][ + self.step_config_key + ] # Then I get the right config self.assertEqual(peer_config["minNumberToGrade"], expected_must_grade) @@ -302,7 +306,9 @@ def test_peer_dates(self, xblock): expected_due = "2015-04-01T00:00:00" # When I ask for peer step config - peer_config = AssessmentStepsSerializer(xblock).data["settings"]["peerStep"] + peer_config = AssessmentStepsSerializer(xblock).data["settings"][ + self.step_config_key + ] # Then I get the right dates self.assertEqual(peer_config["start"], expected_start) @@ -313,7 +319,9 @@ def test_flex_grading(self, xblock): # Given a peer step with flex grading # When I ask for peer step config - peer_config = AssessmentStepsSerializer(xblock).data["settings"]["peerStep"] + peer_config = AssessmentStepsSerializer(xblock).data["settings"][ + self.step_config_key + ] # Then I get the right steps and ordering self.assertTrue(peer_config["flexibleGrading"]) @@ -324,7 +332,7 @@ class TestTrainingSettingsSerializer(XBlockHandlerTestCase): Test for TrainingSettingsSerializer """ - step_config_key = "trainingStep" + step_config_key = "training" @scenario("data/student_training.xml") def test_enabled(self, xblock): @@ -354,7 +362,7 @@ class TestSelfSettingsSerializer(XBlockHandlerTestCase): Test for SelfSettingsSerializer """ - step_config_key = "selfStep" + step_config_key = "self" @scenario("data/self_assessment_scenario.xml") def test_enabled(self, xblock): @@ -384,7 +392,7 @@ class TestStaffSettingsSerializer(XBlockHandlerTestCase): Test for StaffSettingsSerializer """ - step_config_key = "staffStep" + step_config_key = "staff" @scenario("data/staff_grade_scenario.xml") def test_enabled(self, xblock): From e6c6de221dcc11832b3deab2e6b6221cacae6fa2 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 2 Aug 2023 17:19:03 -0400 Subject: [PATCH 19/24] refactor: common assessment step serializer --- .../xblock/data_layer/serializers.py | 78 ++++++++++--------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py index a5650aebc0..c9ab4e16d6 100644 --- a/openassessment/xblock/data_layer/serializers.py +++ b/openassessment/xblock/data_layer/serializers.py @@ -114,57 +114,59 @@ class StartEndMixin(Serializer): due = DateTimeField() -class TrainingSettingsSerializer(RequiredMixin, Serializer): - pass - - class PeerSettingsSerializer(RequiredMixin, StartEndMixin, Serializer): minNumberToGrade = IntegerField(source="must_grade") minNumberToBeGradedBy = IntegerField(source="must_be_graded_by") flexibleGrading = BooleanField(source="enable_flexible_grading", required=False) -class SelfSettingsSerializer(RequiredMixin, Serializer): - pass - -class StaffSettingsSerializer(RequiredMixin, Serializer): - pass - - -class AssessmentStepsSettingsSerializer(Serializer): - training = SerializerMethodField() - peer = SerializerMethodField() - # Workaround to allow reserved keyword in serializer key - vars()["self"] = SerializerMethodField() - staff = SerializerMethodField() +class AssessmentStepSettingsSerializer(RequiredMixin, Serializer): + """ + Generic Assessments step, where we just need to know if the step is + required given the ora.rubric_assessments soruce. + """ + required = BooleanField(default=True) - def _get_step(self, instance, step_name): + def _get_step(self, rubric_assessments, step_name): """Get the assessment step config for a given step_name""" - for step in instance.rubric_assessments: + for step in rubric_assessments: if step["name"] == step_name: return step return None - def get_training(self, instance): - """Get the training step configuration""" - training_step = self._get_step(instance, "student-training") - return TrainingSettingsSerializer(training_step).data or {} - - def get_peer(self, instance): - """Get the peer step configuration""" - peer_step = self._get_step(instance, "peer-assessment") - return PeerSettingsSerializer(peer_step).data or {} - - def get_self(self, instance): - """Get the self step configuration""" - self_step = self._get_step(instance, "self-assessment") - return SelfSettingsSerializer(self_step).data or {} - - def get_staff(self, instance): - """Get the staff step configuration""" - staff_step = self._get_step(instance, "staff-assessment") - return StaffSettingsSerializer(staff_step).data or {} + def __init__(self, *args, **kwargs): + self.step_name = kwargs.pop("step_name") + return super().__init__(*args, **kwargs) + + def to_representation(self, rubric_assessments): + assessment_step = self._get_step(rubric_assessments, self.step_name) + + # Special handling for the peer step which includes extra fields + if assessment_step and self.step_name == "peer-assessment": + return PeerSettingsSerializer(assessment_step).data + + # If we didn't find a step, it is not required + if assessment_step is None: + assessment_step = {"required": False} + + return super().to_representation(assessment_step) + + +class AssessmentStepsSettingsSerializer(Serializer): + training = AssessmentStepSettingsSerializer( + step_name="student-training", source="rubric_assessments" + ) + peer = AssessmentStepSettingsSerializer( + step_name="peer-assessment", source="rubric_assessments" + ) + # Workaround to allow reserved keyword in serializer key + vars()["self"] = AssessmentStepSettingsSerializer( + step_name="self-assessment", source="rubric_assessments" + ) + staff = AssessmentStepSettingsSerializer( + step_name="staff-assessment", source="rubric_assessments" + ) class AssessmentStepsSerializer(Serializer): From 2db062db73bf19255b8236f11903e7cff9924ba2 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 2 Aug 2023 17:25:36 -0400 Subject: [PATCH 20/24] refactor: remove non-reused mixins --- openassessment/xblock/data_layer/serializers.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py index c9ab4e16d6..60067847f7 100644 --- a/openassessment/xblock/data_layer/serializers.py +++ b/openassessment/xblock/data_layer/serializers.py @@ -105,27 +105,24 @@ class RubricConfigSerializer(Serializer): ) -class RequiredMixin(Serializer): +class PeerSettingsSerializer(Serializer): required = BooleanField(default=True) - -class StartEndMixin(Serializer): start = DateTimeField() due = DateTimeField() - -class PeerSettingsSerializer(RequiredMixin, StartEndMixin, Serializer): minNumberToGrade = IntegerField(source="must_grade") minNumberToBeGradedBy = IntegerField(source="must_be_graded_by") - flexibleGrading = BooleanField(source="enable_flexible_grading", required=False) + flexibleGrading = BooleanField(source="enable_flexible_grading", required=False) -class AssessmentStepSettingsSerializer(RequiredMixin, Serializer): +class AssessmentStepSettingsSerializer(Serializer): """ Generic Assessments step, where we just need to know if the step is required given the ora.rubric_assessments soruce. """ + required = BooleanField(default=True) def _get_step(self, rubric_assessments, step_name): From 99177f6c8745e517bb5b34f83909c00c70336937 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 2 Aug 2023 17:27:21 -0400 Subject: [PATCH 21/24] style: fix indent issue --- openassessment/xblock/openassessmentblock.py | 40 ++++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/openassessment/xblock/openassessmentblock.py b/openassessment/xblock/openassessmentblock.py index cc7d83e81b..2c13ca7f29 100644 --- a/openassessment/xblock/openassessmentblock.py +++ b/openassessment/xblock/openassessmentblock.py @@ -108,26 +108,26 @@ def load(path): @XBlock.needs("teams") @XBlock.needs("teams_configuration") class OpenAssessmentBlock( - MessageMixin, - SubmissionMixin, - PeerAssessmentMixin, - SelfAssessmentMixin, - StaffAssessmentMixin, - StudioMixin, - GradeMixin, - LeaderboardMixin, - StaffAreaMixin, - WorkflowMixin, - TeamWorkflowMixin, - StudentTrainingMixin, - LmsCompatibilityMixin, - CourseItemsListingMixin, - ConfigMixin, - TeamMixin, - OpenAssessmentTemplatesMixin, - RubricReuseMixin, - StaffGraderMixin, - DataLayerMixin, + MessageMixin, + SubmissionMixin, + PeerAssessmentMixin, + SelfAssessmentMixin, + StaffAssessmentMixin, + StudioMixin, + GradeMixin, + LeaderboardMixin, + StaffAreaMixin, + WorkflowMixin, + TeamWorkflowMixin, + StudentTrainingMixin, + LmsCompatibilityMixin, + CourseItemsListingMixin, + ConfigMixin, + TeamMixin, + OpenAssessmentTemplatesMixin, + RubricReuseMixin, + StaffGraderMixin, + DataLayerMixin, XBlock ): """Displays a prompt and provides an area where students can compose a response.""" From 31cd21b809380191502296adf8149c0ed9b7198a Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 2 Aug 2023 17:36:53 -0400 Subject: [PATCH 22/24] style: fix indent issue (for real) --- openassessment/xblock/openassessmentblock.py | 44 ++++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/openassessment/xblock/openassessmentblock.py b/openassessment/xblock/openassessmentblock.py index 2c13ca7f29..30727c8673 100644 --- a/openassessment/xblock/openassessmentblock.py +++ b/openassessment/xblock/openassessmentblock.py @@ -108,28 +108,28 @@ def load(path): @XBlock.needs("teams") @XBlock.needs("teams_configuration") class OpenAssessmentBlock( - MessageMixin, - SubmissionMixin, - PeerAssessmentMixin, - SelfAssessmentMixin, - StaffAssessmentMixin, - StudioMixin, - GradeMixin, - LeaderboardMixin, - StaffAreaMixin, - WorkflowMixin, - TeamWorkflowMixin, - StudentTrainingMixin, - LmsCompatibilityMixin, - CourseItemsListingMixin, - ConfigMixin, - TeamMixin, - OpenAssessmentTemplatesMixin, - RubricReuseMixin, - StaffGraderMixin, - DataLayerMixin, - XBlock - ): + MessageMixin, + SubmissionMixin, + PeerAssessmentMixin, + SelfAssessmentMixin, + StaffAssessmentMixin, + StudioMixin, + GradeMixin, + LeaderboardMixin, + StaffAreaMixin, + WorkflowMixin, + TeamWorkflowMixin, + StudentTrainingMixin, + LmsCompatibilityMixin, + CourseItemsListingMixin, + ConfigMixin, + TeamMixin, + OpenAssessmentTemplatesMixin, + RubricReuseMixin, + StaffGraderMixin, + DataLayerMixin, + XBlock, +): """Displays a prompt and provides an area where students can compose a response.""" VALID_ASSESSMENT_TYPES = [ From 890bfe1f5cc140c1b738fd58603631edb9284f71 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Thu, 3 Aug 2023 22:09:20 -0400 Subject: [PATCH 23/24] fix: update incorrect fields to match contract --- .../xblock/data_layer/serializers.py | 27 +++++++++++++------ .../xblock/data_layer/test_serializers.py | 20 +++++++------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/openassessment/xblock/data_layer/serializers.py b/openassessment/xblock/data_layer/serializers.py index 60067847f7..e42eec908b 100644 --- a/openassessment/xblock/data_layer/serializers.py +++ b/openassessment/xblock/data_layer/serializers.py @@ -38,7 +38,7 @@ class FileResponseConfigSerializer(Serializer): fileUploadLimit = SerializerMethodField() allowedExtensions = CharListField(source="get_allowed_file_types_or_preset") blockedExtensions = CharListField(source="FILE_EXT_BLACK_LIST") - allowedFileTypeDescription = CharField(source="file_upload_type") + fileTypeDescription = CharField(source="file_upload_type") def get_enabled(self, block): return block.file_upload_response is not None @@ -59,8 +59,8 @@ def get_teamsetName(self, block): class SubmissionConfigSerializer(Serializer): - start = DateTimeField(source="submission_start") - due = DateTimeField(source="submission_due") + startDatetime = DateTimeField(source="submission_start") + endDatetime = DateTimeField(source="submission_due") textResponseConfig = TextResponseConfigSerializer(source="*") fileResponseConfig = FileResponseConfigSerializer(source="*") @@ -105,16 +105,25 @@ class RubricConfigSerializer(Serializer): ) +class SelfSettingsSerializer(Serializer): + required = BooleanField(default=True) + + startTime = DateTimeField(source="start") + endTime = DateTimeField(source="due") + + class PeerSettingsSerializer(Serializer): required = BooleanField(default=True) - start = DateTimeField() - due = DateTimeField() + startTime = DateTimeField(source="start") + endTime = DateTimeField(source="due") minNumberToGrade = IntegerField(source="must_grade") minNumberToBeGradedBy = IntegerField(source="must_be_graded_by") - flexibleGrading = BooleanField(source="enable_flexible_grading", required=False) + enableFlexibleGrading = BooleanField( + source="enable_flexible_grading", required=False + ) class AssessmentStepSettingsSerializer(Serializer): @@ -142,6 +151,8 @@ def to_representation(self, rubric_assessments): # Special handling for the peer step which includes extra fields if assessment_step and self.step_name == "peer-assessment": return PeerSettingsSerializer(assessment_step).data + elif assessment_step and self.step_name == "self-assessment": + return SelfSettingsSerializer(assessment_step).data # If we didn't find a step, it is not required if assessment_step is None: @@ -176,7 +187,7 @@ def get_order(self, block): class LeaderboardConfigSerializer(Serializer): enabled = SerializerMethodField() - numberToShow = IntegerField(source="leaderboard_show") + numberOfEntries = IntegerField(source="leaderboard_show") def get_enabled(self, block): return block.leaderboard_show > 0 @@ -194,7 +205,7 @@ class OraBlockInfoSerializer(Serializer): submissionConfig = SubmissionConfigSerializer(source="*") assessmentSteps = AssessmentStepsSerializer(source="*") rubricConfig = RubricConfigSerializer(source="*") - leaderboard = LeaderboardConfigSerializer(source="*") + leaderboardConfig = LeaderboardConfigSerializer(source="*") def get_baseAssetUrl(self, block): return block._get_base_url_path_for_course_assets(block.course.id) diff --git a/openassessment/xblock/data_layer/test_serializers.py b/openassessment/xblock/data_layer/test_serializers.py index 11007aaff8..6200f24027 100644 --- a/openassessment/xblock/data_layer/test_serializers.py +++ b/openassessment/xblock/data_layer/test_serializers.py @@ -43,8 +43,8 @@ def test_dates(self, xblock): # Then I get the expected values expected_start = xblock.submission_start expected_due = xblock.submission_due - self.assertEqual(submission_config["start"], expected_start) - self.assertEqual(submission_config["due"], expected_due) + self.assertEqual(submission_config["startDatetime"], expected_start) + self.assertEqual(submission_config["endDatetime"], expected_due) @scenario("data/basic_scenario.xml") def test_dates_missing(self, xblock): @@ -55,8 +55,8 @@ def test_dates_missing(self, xblock): submission_config = SubmissionConfigSerializer(xblock).data # Then I get the expected values - self.assertIsNone(submission_config["start"]) - self.assertIsNone(submission_config["due"]) + self.assertIsNone(submission_config["startDatetime"]) + self.assertIsNone(submission_config["endDatetime"]) @scenario("data/basic_scenario.xml") def test_text_response_config(self, xblock): @@ -115,7 +115,7 @@ def test_file_response_config(self, xblock): file_response_config["fileUploadLimit"], xblock.MAX_FILES_COUNT ) self.assertEqual( - file_response_config["allowedFileTypeDescription"], + file_response_config["fileTypeDescription"], xblock.file_upload_type, ) self.assertEqual( @@ -311,8 +311,8 @@ def test_peer_dates(self, xblock): ] # Then I get the right dates - self.assertEqual(peer_config["start"], expected_start) - self.assertEqual(peer_config["due"], expected_due) + self.assertEqual(peer_config["startTime"], expected_start) + self.assertEqual(peer_config["endTime"], expected_due) @scenario("data/peer_assessment_flex_grading_scenario.xml") def test_flex_grading(self, xblock): @@ -324,7 +324,7 @@ def test_flex_grading(self, xblock): ] # Then I get the right steps and ordering - self.assertTrue(peer_config["flexibleGrading"]) + self.assertTrue(peer_config["enableFlexibleGrading"]) class TestTrainingSettingsSerializer(XBlockHandlerTestCase): @@ -432,7 +432,7 @@ def test_leaderboard(self, xblock): # Then I get the expected config self.assertTrue(leaderboard_config["enabled"]) - self.assertEqual(leaderboard_config["numberToShow"], number_to_show) + self.assertEqual(leaderboard_config["numberOfEntries"], number_to_show) @scenario("data/basic_scenario.xml") def test_no_leaderboard(self, xblock): @@ -442,4 +442,4 @@ def test_no_leaderboard(self, xblock): # Then I get the expected config self.assertFalse(leaderboard_config["enabled"]) - self.assertEqual(leaderboard_config["numberToShow"], 0) + self.assertEqual(leaderboard_config["numberOfEntries"], 0) From 1fcd34f3229398823244989a46f5435e9d61c335 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Mon, 7 Aug 2023 10:04:59 -0400 Subject: [PATCH 24/24] chore: bump version to 5.2.2 --- openassessment/__init__.py | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openassessment/__init__.py b/openassessment/__init__.py index 6dfc8594c6..0ee3280c08 100644 --- a/openassessment/__init__.py +++ b/openassessment/__init__.py @@ -1,4 +1,4 @@ """ Initialization Information for Open Assessment Module """ -__version__ = '5.2.1' +__version__ = '5.2.2' diff --git a/package-lock.json b/package-lock.json index b2a27283eb..9d7667e032 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "edx-ora2", - "version": "5.1.0", + "version": "5.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "edx-ora2", - "version": "4.5.0", + "version": "5.2.1", "dependencies": { "@edx/frontend-build": "^6.1.1", "@edx/paragon": "^20.9.2", diff --git a/package.json b/package.json index 3b55fa754f..3edddcf451 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "edx-ora2", - "version": "5.2.1", + "version": "5.2.2", "repository": "https://github.com/openedx/edx-ora2.git", "dependencies": { "@edx/frontend-build": "^6.1.1",