diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 8df09f4fe7db..df587b84d0b1 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -201,7 +201,15 @@ def fetch_all(cls, block, filter_fields=None): 'display_name': _(field.display_name), # pylint: disable=translation-of-non-string 'help': field_help, 'deprecated': field.runtime_options.get('deprecated', False), - 'hide_on_enabled_publisher': field.runtime_options.get('hide_on_enabled_publisher', False) + 'hide_on_enabled_publisher': field.runtime_options.get('hide_on_enabled_publisher', False), + # The field's class name (e.g. "String", "Boolean", "Integer", "List", "Dict") lets the + # frontend pick the right input type without inferring it from the value's shape. + 'type': field.__class__.__name__, + # The field's declared choices, when it has any. For enum-like settings this is a list of + # {"display_name", "value"} dicts; for numeric ranges it may be a {"min"/"max"} dict; for + # free-form settings it is None. Exposing it here keeps the valid options as a single source + # of truth in the backend instead of being hardcoded in the frontend. + 'options': field.values, } return result diff --git a/xmodule/course_block.py b/xmodule/course_block.py index aee832e51659..6476d084fd5f 100644 --- a/xmodule/course_block.py +++ b/xmodule/course_block.py @@ -28,6 +28,7 @@ from openedx.core.lib.teams_config import TeamsConfig # pylint: disable=unused-import from xmodule import course_metadata_utils from xmodule.course_metadata_utils import DEFAULT_GRADING_POLICY, DEFAULT_START_DATE +from xmodule.course_settings_field_options import CERTIFICATES_DISPLAY_BEHAVIOR_FIELD_OPTIONS from xmodule.data import CertificatesDisplayBehaviors from xmodule.graders import grader_from_conf from xmodule.seq_block import SequenceBlock @@ -605,6 +606,7 @@ class CourseFields: # pylint: disable=missing-class-docstring ), scope=Scope.settings, default=CertificatesDisplayBehaviors.END.value, + values=CERTIFICATES_DISPLAY_BEHAVIOR_FIELD_OPTIONS, ) course_image = String( display_name=_("Course About Page Image"), diff --git a/xmodule/course_settings_field_options.py b/xmodule/course_settings_field_options.py new file mode 100644 index 000000000000..0384ab9fefd6 --- /dev/null +++ b/xmodule/course_settings_field_options.py @@ -0,0 +1,60 @@ +""" +Canonical option lists ("values") for course-level Advanced Settings fields +that should be presented as dropdowns in Studio. + +These describe the valid choices for enum-like course settings so the Advanced +Settings API can expose them to the frontend, instead of the frontend +hardcoding them. Each list follows the XBlock ``values`` format: +``[{"display_name": ..., "value": ...}, ...]``. + +``display_name`` values are plain strings (not wrapped in gettext) for two +reasons: this module is imported by ``xmodule/modulestore/inheritance.py``, +which explicitly forbids importing Django, and the existing enum ``values`` in +``xmodule/course_block.py`` already use plain-string labels. User-facing +translation of these labels is handled by the frontend. + +NOTE: ``showanswer`` / ``rerandomize`` / ``show_correctness`` also exist as +problem-level fields in ``xmodule/capa_block.py`` with their own inline +``values``. Those should eventually be migrated to reference these constants so +there is a single source of truth. They are mirrored here (matching the +SHOWANSWER / RANDOMIZATION / ShowCorrectness constants) to keep this module +dependency-light and avoid import cycles. +""" + +# Mirrors SHOWANSWER in xmodule/capa_block.py +SHOWANSWER_FIELD_OPTIONS = [ + {"display_name": "Always", "value": "always"}, + {"display_name": "Answered", "value": "answered"}, + {"display_name": "Attempted or Past Due", "value": "attempted"}, + {"display_name": "Closed", "value": "closed"}, + {"display_name": "Finished", "value": "finished"}, + {"display_name": "Correct or Past Due", "value": "correct_or_past_due"}, + {"display_name": "Past Due", "value": "past_due"}, + {"display_name": "Never", "value": "never"}, + {"display_name": "After Some Number of Attempts", "value": "after_attempts"}, + {"display_name": "After All Attempts", "value": "after_all_attempts"}, + {"display_name": "After All Attempts or Correct", "value": "after_all_attempts_or_correct"}, + {"display_name": "Attempted", "value": "attempted_no_past_due"}, +] + +# Mirrors RANDOMIZATION in xmodule/capa_block.py +RERANDOMIZE_FIELD_OPTIONS = [ + {"display_name": "Always", "value": "always"}, + {"display_name": "On Reset", "value": "onreset"}, + {"display_name": "Never", "value": "never"}, + {"display_name": "Per Student", "value": "per_student"}, +] + +# Mirrors ShowCorrectness in the xblock.scorable library +SHOW_CORRECTNESS_FIELD_OPTIONS = [ + {"display_name": "Always", "value": "always"}, + {"display_name": "Never", "value": "never"}, + {"display_name": "Past Due", "value": "past_due"}, +] + +# Mirrors CertificatesDisplayBehaviors in xmodule/data.py +CERTIFICATES_DISPLAY_BEHAVIOR_FIELD_OPTIONS = [ + {"display_name": "End of course", "value": "end"}, + {"display_name": "End of course, with date", "value": "end_with_date"}, + {"display_name": "Immediately upon earning", "value": "early_no_info"}, +] diff --git a/xmodule/modulestore/inheritance.py b/xmodule/modulestore/inheritance.py index 3968972d1888..9778bd2e40f2 100644 --- a/xmodule/modulestore/inheritance.py +++ b/xmodule/modulestore/inheritance.py @@ -10,6 +10,11 @@ from xblock.fields import Boolean, Date, Dict, Float, Integer, List, Scope, String, Timedelta from xblock.runtime import KeyValueStore, KvsFieldData +from xmodule.course_settings_field_options import ( + RERANDOMIZE_FIELD_OPTIONS, + SHOW_CORRECTNESS_FIELD_OPTIONS, + SHOWANSWER_FIELD_OPTIONS, +) from xmodule.error_block import ErrorBlock from xmodule.partitions.partitions import UserPartition @@ -97,6 +102,7 @@ class InheritanceMixin(XBlockMixin): ), scope=Scope.settings, default="finished", + values=SHOWANSWER_FIELD_OPTIONS, ) show_correctness = String( @@ -109,6 +115,7 @@ class InheritanceMixin(XBlockMixin): ), scope=Scope.settings, default="always", + values=SHOW_CORRECTNESS_FIELD_OPTIONS, ) rerandomize = String( @@ -123,6 +130,7 @@ class InheritanceMixin(XBlockMixin): ), scope=Scope.settings, default="never", + values=RERANDOMIZE_FIELD_OPTIONS, ) days_early_for_beta = Float( display_name=_("Days Early for Beta Users"),