Skip to content

Commit 0f18ea2

Browse files
SantiagoSuHeclaude
andcommitted
feat: expose field type and options in advanced settings API
Add `type` and `options` to each setting returned by the Advanced Settings API (CourseMetadata.fetch/fetch_all) so clients can render type-specific inputs and enum dropdowns without hardcoding that metadata in the frontend. - course_metadata.py: include the field's class name as `type` and the field's `values` as `options` in the serialized output. - Add course-level `values` to enum settings that lacked them (showanswer, rerandomize, show_correctness, certificates_display_behavior) via a new shared xmodule/course_settings_field_options module, so their valid choices live in the backend as a single source of truth. The problem-level definitions in capa_block.py are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d5111c7 commit 0f18ea2

4 files changed

Lines changed: 79 additions & 1 deletion

File tree

cms/djangoapps/models/settings/course_metadata.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,15 @@ def fetch_all(cls, block, filter_fields=None):
201201
'display_name': _(field.display_name), # pylint: disable=translation-of-non-string
202202
'help': field_help,
203203
'deprecated': field.runtime_options.get('deprecated', False),
204-
'hide_on_enabled_publisher': field.runtime_options.get('hide_on_enabled_publisher', False)
204+
'hide_on_enabled_publisher': field.runtime_options.get('hide_on_enabled_publisher', False),
205+
# The field's class name (e.g. "String", "Boolean", "Integer", "List", "Dict") lets the
206+
# frontend pick the right input type without inferring it from the value's shape.
207+
'type': field.__class__.__name__,
208+
# The field's declared choices, when it has any. For enum-like settings this is a list of
209+
# {"display_name", "value"} dicts; for numeric ranges it may be a {"min"/"max"} dict; for
210+
# free-form settings it is None. Exposing it here keeps the valid options as a single source
211+
# of truth in the backend instead of being hardcoded in the frontend.
212+
'options': field.values,
205213
}
206214
return result
207215

xmodule/course_block.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from openedx.core.lib.teams_config import TeamsConfig # pylint: disable=unused-import
2929
from xmodule import course_metadata_utils
3030
from xmodule.course_metadata_utils import DEFAULT_GRADING_POLICY, DEFAULT_START_DATE
31+
from xmodule.course_settings_field_options import CERTIFICATES_DISPLAY_BEHAVIOR_FIELD_OPTIONS
3132
from xmodule.data import CertificatesDisplayBehaviors
3233
from xmodule.graders import grader_from_conf
3334
from xmodule.seq_block import SequenceBlock
@@ -605,6 +606,7 @@ class CourseFields: # pylint: disable=missing-class-docstring
605606
),
606607
scope=Scope.settings,
607608
default=CertificatesDisplayBehaviors.END.value,
609+
values=CERTIFICATES_DISPLAY_BEHAVIOR_FIELD_OPTIONS,
608610
)
609611
course_image = String(
610612
display_name=_("Course About Page Image"),
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
Canonical option lists ("values") for course-level Advanced Settings fields
3+
that should be presented as dropdowns in Studio.
4+
5+
These describe the valid choices for enum-like course settings so the Advanced
6+
Settings API can expose them to the frontend, instead of the frontend
7+
hardcoding them. Each list follows the XBlock ``values`` format:
8+
``[{"display_name": ..., "value": ...}, ...]``.
9+
10+
``display_name`` values are plain strings (not wrapped in gettext) for two
11+
reasons: this module is imported by ``xmodule/modulestore/inheritance.py``,
12+
which explicitly forbids importing Django, and the existing enum ``values`` in
13+
``xmodule/course_block.py`` already use plain-string labels. User-facing
14+
translation of these labels is handled by the frontend.
15+
16+
NOTE: ``showanswer`` / ``rerandomize`` / ``show_correctness`` also exist as
17+
problem-level fields in ``xmodule/capa_block.py`` with their own inline
18+
``values``. Those should eventually be migrated to reference these constants so
19+
there is a single source of truth. They are mirrored here (matching the
20+
SHOWANSWER / RANDOMIZATION / ShowCorrectness constants) to keep this module
21+
dependency-light and avoid import cycles.
22+
"""
23+
24+
# Mirrors SHOWANSWER in xmodule/capa_block.py
25+
SHOWANSWER_FIELD_OPTIONS = [
26+
{"display_name": "Always", "value": "always"},
27+
{"display_name": "Answered", "value": "answered"},
28+
{"display_name": "Attempted or Past Due", "value": "attempted"},
29+
{"display_name": "Closed", "value": "closed"},
30+
{"display_name": "Finished", "value": "finished"},
31+
{"display_name": "Correct or Past Due", "value": "correct_or_past_due"},
32+
{"display_name": "Past Due", "value": "past_due"},
33+
{"display_name": "Never", "value": "never"},
34+
{"display_name": "After Some Number of Attempts", "value": "after_attempts"},
35+
{"display_name": "After All Attempts", "value": "after_all_attempts"},
36+
{"display_name": "After All Attempts or Correct", "value": "after_all_attempts_or_correct"},
37+
{"display_name": "Attempted", "value": "attempted_no_past_due"},
38+
]
39+
40+
# Mirrors RANDOMIZATION in xmodule/capa_block.py
41+
RERANDOMIZE_FIELD_OPTIONS = [
42+
{"display_name": "Always", "value": "always"},
43+
{"display_name": "On Reset", "value": "onreset"},
44+
{"display_name": "Never", "value": "never"},
45+
{"display_name": "Per Student", "value": "per_student"},
46+
]
47+
48+
# Mirrors ShowCorrectness in the xblock.scorable library
49+
SHOW_CORRECTNESS_FIELD_OPTIONS = [
50+
{"display_name": "Always", "value": "always"},
51+
{"display_name": "Never", "value": "never"},
52+
{"display_name": "Past Due", "value": "past_due"},
53+
]
54+
55+
# Mirrors CertificatesDisplayBehaviors in xmodule/data.py
56+
CERTIFICATES_DISPLAY_BEHAVIOR_FIELD_OPTIONS = [
57+
{"display_name": "End of course", "value": "end"},
58+
{"display_name": "End of course, with date", "value": "end_with_date"},
59+
{"display_name": "Immediately upon earning", "value": "early_no_info"},
60+
]

xmodule/modulestore/inheritance.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
from xblock.fields import Boolean, Date, Dict, Float, Integer, List, Scope, String, Timedelta
1111
from xblock.runtime import KeyValueStore, KvsFieldData
1212

13+
from xmodule.course_settings_field_options import (
14+
RERANDOMIZE_FIELD_OPTIONS,
15+
SHOW_CORRECTNESS_FIELD_OPTIONS,
16+
SHOWANSWER_FIELD_OPTIONS,
17+
)
1318
from xmodule.error_block import ErrorBlock
1419
from xmodule.partitions.partitions import UserPartition
1520

@@ -97,6 +102,7 @@ class InheritanceMixin(XBlockMixin):
97102
),
98103
scope=Scope.settings,
99104
default="finished",
105+
values=SHOWANSWER_FIELD_OPTIONS,
100106
)
101107

102108
show_correctness = String(
@@ -109,6 +115,7 @@ class InheritanceMixin(XBlockMixin):
109115
),
110116
scope=Scope.settings,
111117
default="always",
118+
values=SHOW_CORRECTNESS_FIELD_OPTIONS,
112119
)
113120

114121
rerandomize = String(
@@ -123,6 +130,7 @@ class InheritanceMixin(XBlockMixin):
123130
),
124131
scope=Scope.settings,
125132
default="never",
133+
values=RERANDOMIZE_FIELD_OPTIONS,
126134
)
127135
days_early_for_beta = Float(
128136
display_name=_("Days Early for Beta Users"),

0 commit comments

Comments
 (0)