From 092953410ff36e462a131f2f7bbf0c26c9edef5a Mon Sep 17 00:00:00 2001 From: juan Date: Fri, 4 Oct 2024 15:16:04 +0200 Subject: [PATCH] feat: adds a new custom content jsonfield to ccx model --- lms/djangoapps/ccx/api/v0/serializers.py | 9 +++++ lms/djangoapps/ccx/api/v0/views.py | 34 ++++++++++++++++++- .../0007_customcourseforedx_custom_content.py | 18 ++++++++++ lms/djangoapps/ccx/models.py | 2 ++ 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 lms/djangoapps/ccx/migrations/0007_customcourseforedx_custom_content.py diff --git a/lms/djangoapps/ccx/api/v0/serializers.py b/lms/djangoapps/ccx/api/v0/serializers.py index c94a458f49a1..33d632e1abfb 100644 --- a/lms/djangoapps/ccx/api/v0/serializers.py +++ b/lms/djangoapps/ccx/api/v0/serializers.py @@ -19,6 +19,7 @@ class CCXCourseSerializer(serializers.ModelSerializer): due = serializers.CharField(allow_blank=True) max_students_allowed = serializers.IntegerField(source='max_student_enrollments_allowed') course_modules = serializers.SerializerMethodField() + custom_content = serializers.SerializerMethodField() class Meta: model = CustomCourseForEdX @@ -31,6 +32,7 @@ class Meta: "due", "max_students_allowed", "course_modules", + "custom_content", ) read_only_fields = ( "ccx_course_id", @@ -52,3 +54,10 @@ def get_course_modules(obj): Getter for the Course Modules. The list is stored in a compressed field. """ return obj.structure or [] + + @staticmethod + def get_custom_content(obj): + """ + Getter for the Custom Content. The dictionary is stored in a compressed field. + """ + return obj.custom_content or {} diff --git a/lms/djangoapps/ccx/api/v0/views.py b/lms/djangoapps/ccx/api/v0/views.py index b3e345a77022..faf2c62dca83 100644 --- a/lms/djangoapps/ccx/api/v0/views.py +++ b/lms/djangoapps/ccx/api/v0/views.py @@ -152,6 +152,16 @@ def get_valid_input(request_data, ignore_missing=False): elif 'max_students_allowed' in request_data: field_errors['max_students_allowed'] = {'error_code': 'null_field_max_students_allowed'} + custom_content = request_data.get('custom_content') + if custom_content is not None: + if isinstance(custom_content, dict): + valid_input['custom_content'] = custom_content + else: + field_errors['custom_content'] = {'error_code': 'invalid_custom_content_type'} + elif 'custom_content' in request_data: + # case if the user actually passed null as input + valid_input['custom_content'] = None + course_modules = request_data.get('course_modules') if course_modules is not None: if isinstance(course_modules, list): @@ -221,6 +231,7 @@ class CCXListView(GenericAPIView): "display_name": "CCX example title", "coach_email": "john@example.com", "max_students_allowed": 123, + "custom_content": {"custom_field": "CCX custom content example"}, "course_modules" : [ "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week1", "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week4", @@ -255,6 +266,8 @@ class CCXListView(GenericAPIView): * max_students_allowed: An integer representing he maximum number of students that can be enrolled in the CCX Course. + * custom_content: Optional. A dictionary representation of the custom content. + * course_modules: Optional. A list of course modules id keys. **GET Response Values** @@ -279,6 +292,8 @@ class CCXListView(GenericAPIView): * max_students_allowed: An integer representing he maximum number of students that can be enrolled in the CCX Course. + * custom_content: A dictionary with the custom content for the CCX Course. + * course_modules: A list of course modules id keys. * count: An integer representing the total number of records that matched the request parameters. @@ -303,6 +318,7 @@ class CCXListView(GenericAPIView): "start": "2019-01-01", "due": "2019-06-01", "max_students_allowed": 123, + "custom_content": {"custom_field": "CCX custom content example"}, "course_modules" : [ "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week1", "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week4", @@ -333,6 +349,8 @@ class CCXListView(GenericAPIView): * max_students_allowed: An integer representing he maximum number of students that can be enrolled in the CCX Course. + * custom_content: A dictionary with the custom content for the CCX Course. + * course_modules: A list of course modules id keys. **Example POST Response** @@ -344,6 +362,7 @@ class CCXListView(GenericAPIView): "start": "2019-01-01", "due": "2019-06-01", "max_students_allowed": 123, + "custom_content": {"custom_field": "CCX custom content example"}, "course_modules" : [ "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week1", "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week4", @@ -455,12 +474,16 @@ def post(self, request): # prepare the course_modules to be stored in a json stringified field course_modules_json = json.dumps(valid_input.get('course_modules')) + # Include the json array to add/storage custom content, if it exist. + custom_content_json = valid_input.get('custom_content') + with transaction.atomic(): ccx_course_object = CustomCourseForEdX( course_id=master_course_object.id, coach=coach, display_name=valid_input['display_name'], - structure_json=course_modules_json + structure_json=course_modules_json, + custom_content=custom_content_json, ) ccx_course_object.save() @@ -552,6 +575,7 @@ class CCXDetailView(GenericAPIView): "display_name": "CCX example title modified", "coach_email": "joe@example.com", "max_students_allowed": 111, + "custom_content": {"custom_field": "CCX custom content example"}, "course_modules" : [ "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week1", "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week4", @@ -580,6 +604,8 @@ class CCXDetailView(GenericAPIView): * max_students_allowed: Optional. An integer representing he maximum number of students that can be enrolled in the CCX Course. + * custom_content: Optional. A dictionary representation of the custom content. + * course_modules: Optional. A list of course modules id keys. **GET Response Values** @@ -602,6 +628,8 @@ class CCXDetailView(GenericAPIView): * max_students_allowed: An integer representing he maximum number of students that can be enrolled in the CCX Course. + * custom_content: A dictionary with the custom content for the CCX Course. + * course_modules: A list of course modules id keys. **PATCH and DELETE Response Values** @@ -733,6 +761,10 @@ def patch(self, request, ccx_course_id=None): if ccx_course_object.coach.id != coach.id: old_coach = ccx_course_object.coach ccx_course_object.coach = coach + if 'custom_content' in valid_input: + existing_content = ccx_course_object.custom_content or {} + existing_content.update(valid_input.get('custom_content')) + ccx_course_object.custom_content = existing_content if 'course_modules' in valid_input: if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): diff --git a/lms/djangoapps/ccx/migrations/0007_customcourseforedx_custom_content.py b/lms/djangoapps/ccx/migrations/0007_customcourseforedx_custom_content.py new file mode 100644 index 000000000000..e7bdf94cf13a --- /dev/null +++ b/lms/djangoapps/ccx/migrations/0007_customcourseforedx_custom_content.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.17 on 2024-10-03 10:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ccx', '0006_set_display_name_as_override'), + ] + + operations = [ + migrations.AddField( + model_name='customcourseforedx', + name='custom_content', + field=models.JSONField(blank=True, default=dict, null=True, verbose_name='Custom Json Content'), + ), + ] diff --git a/lms/djangoapps/ccx/models.py b/lms/djangoapps/ccx/models.py index 46400b53579e..212750e3ed19 100644 --- a/lms/djangoapps/ccx/models.py +++ b/lms/djangoapps/ccx/models.py @@ -32,6 +32,8 @@ class CustomCourseForEdX(models.Model): # if not empty, this field contains a json serialized list of # the master course modules structure_json = models.TextField(verbose_name='Structure JSON', blank=True, null=True) + # Custom Json field to add any additional information to the CCX model + custom_content = models.JSONField(default=dict, blank=True, null=True, verbose_name="Custom Json Content") class Meta: app_label = 'ccx'