We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Users should not be able to edit content hierarchy
Typically the A+ course content structure is configured from a JSON file generated by gitmanager. However, the teachers have possibility to edit the attributes also in the Edit course / Content view, including the parent learning object. By editing the parent selection it is possible to create a circular reference loop between two learning objects pointing to each other as a parent, that leads to excessive number of database operations, and as a result, leading the system to become unusable due to heavy database load.

Easy approach would be to just disable the possibility to modify the parent selection. Later, it might be useful to think more thoroughly how important it is allow editing the content structure in this view, while the main form of course configuration should be through the JSON configuration.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `edit_course/exercise_forms.py`
Content:
```
1 import logging
2 from typing import Any, Dict, List
3 
4 from django import forms
5 from django.utils.translation import gettext_lazy as _
6 
7 from course.models import CourseModule, LearningObjectCategory
8 from exercise.models import LearningObject, CourseChapter, BaseExercise, \
9     LTIExercise, StaticExercise, ExerciseWithAttachment, RevealRule, \
10     LTI1p3Exercise
11 from lib.widgets import DateTimeLocalInput
12 from .course_forms import FieldsetModelForm
13 
14 from exercise.exercisecollection_models import ExerciseCollection
15 
16 logger = logging.getLogger("aplus.exercise")
17 
18 COMMON_FIELDS = [
19     'status',
20     'audience',
21     'category',
22     'course_module',
23     'parent',
24     'order',
25     'url',
26 ]
27 SERVICE_FIELDS = [
28     'service_url',
29     'name',
30     'description',
31 ]
32 EXERCISE_FIELDS = [
33     'max_submissions',
34     'max_points',
35     'difficulty',
36     'points_to_pass',
37     'allow_assistant_viewing',
38     'allow_assistant_grading',
39     'min_group_size',
40     'max_group_size',
41     'model_answers',
42     'templates',
43     'grading_mode',
44 ]
45 
46 
47 class LearningObjectMixin:
48 
49     def init_fields(self, **kwargs):
50         self.lobject = kwargs.get('instance')
51         self.fields["category"].queryset = LearningObjectCategory.objects.filter(
52             course_instance=self.lobject.course_instance)
53         self.fields["course_module"].queryset = CourseModule.objects.filter(
54             course_instance=self.lobject.course_instance)
55         self.fields["parent"].queryset = LearningObject.objects\
56             .exclude(id=self.lobject.id)\
57             .filter(course_module=self.lobject.course_module)
58 
59     @property
60     def remote_service_head(self):
61         return True
62 
63     def get_hierarchy_fieldset(self):
64         return { 'legend':_('HIERARCHY'), 'fields':self.get_fields('status',
65             'audience', 'category','course_module','parent','order','url') }
66 
67     def get_content_fieldset(self, *add):
68         return { 'legend':_('CONTENT'), 'fields':self.get_fields('name',
69             'description', *add) }
70 
71 
72 class CourseChapterForm(LearningObjectMixin, FieldsetModelForm):
73 
74     class Meta:
75         model = CourseChapter
76         fields = COMMON_FIELDS + SERVICE_FIELDS + [
77             'use_wide_column',
78             'generate_table_of_contents'
79         ]
80 
81     def __init__(self, *args, **kwargs):
82         super().__init__(*args, **kwargs)
83         self.init_fields(**kwargs)
84 
85     def get_fieldsets(self):
86         return [
87             self.get_hierarchy_fieldset(),
88             self.get_content_fieldset(
89                 'use_wide_column', 'generate_table_of_contents'),
90         ]
91 
92 
93 class RevealRuleForm(FieldsetModelForm):
94     # This form is only used internally by BaseExerciseForm.
95 
96     class Meta:
97         model = RevealRule
98         fields = ['trigger', 'delay_minutes', 'time', 'currently_revealed']
99         widgets = {'time': DateTimeLocalInput}
100 
101     def __init__(self, *args: Any, **kwargs: Any) -> None:
102         super().__init__(*args, **kwargs)
103         self.fields['trigger'].widget.attrs['data-trigger'] = True
104         # Visibility rules for the form fields. Each of the following fields is
105         # only visible when one of their specified values is selected from the
106         # trigger dropdown. See edit_model.html.
107         self.fields['currently_revealed'].widget.attrs['data-visible-triggers'] = [
108             RevealRule.TRIGGER.MANUAL.value,
109         ]
110         self.fields['time'].widget.attrs['data-visible-triggers'] = [
111             RevealRule.TRIGGER.TIME.value,
112         ]
113         self.fields['delay_minutes'].widget.attrs['data-visible-triggers'] = [
114             RevealRule.TRIGGER.DEADLINE.value,
115             RevealRule.TRIGGER.DEADLINE_ALL.value,
116             RevealRule.TRIGGER.DEADLINE_OR_FULL_POINTS.value,
117         ]
118 
119     def clean(self) -> Dict[str, Any]:
120         result = super().clean()
121         errors = {}
122         trigger = self.cleaned_data.get('trigger')
123         if trigger == RevealRule.TRIGGER.TIME:
124             time = self.cleaned_data.get('time')
125             if time is None:
126                 errors['time'] = _(
127                     'ERROR_REQUIRED_WITH_SELECTED_TRIGGER'
128                 )
129         if errors:
130             raise forms.ValidationError(errors)
131         return result
132 
133 
134 class BaseExerciseForm(LearningObjectMixin, FieldsetModelForm):
135 
136     class Meta:
137         model = BaseExercise
138         fields = COMMON_FIELDS + SERVICE_FIELDS + EXERCISE_FIELDS
139 
140     def __init__(self, *args: Any, **kwargs: Any) -> None:
141         super().__init__(*args, **kwargs)
142         self.init_fields(**kwargs)
143 
144         # This form contains two embedded RevealRuleForms.
145         self.submission_feedback_form = RevealRuleForm(
146             data=kwargs.get('data'),
147             instance=self.instance.active_submission_feedback_reveal_rule,
148             prefix='submission_feedback',
149         )
150         self.model_solutions_form = RevealRuleForm(
151             data=kwargs.get('data'),
152             instance=self.instance.active_model_solutions_reveal_rule,
153             prefix='model_solutions',
154         )
155 
156     def get_fieldsets(self) -> List[Dict[str, Any]]:
157         return [
158             self.get_hierarchy_fieldset(),
159             self.get_content_fieldset('model_answers', 'templates'),
160             { 'legend':_('GRADING'), 'fields':self.get_fields('max_submissions',
161                 'max_points','points_to_pass', 'difficulty',
162                 'allow_assistant_viewing','allow_assistant_grading','grading_mode') },
163             { 'legend':_('GROUPS'), 'fields':self.get_fields('min_group_size',
164                 'max_group_size') },
165             { 'legend':_('REVEAL_SUBMISSION_FEEDBACK'), 'fields':self.submission_feedback_form },
166             { 'legend':_('REVEAL_MODEL_SOLUTIONS'), 'fields':self.model_solutions_form },
167         ]
168 
169     def is_valid(self) -> bool:
170         return (
171             super().is_valid()
172             and self.submission_feedback_form.is_valid()
173             and self.model_solutions_form.is_valid()
174         )
175 
176     def save(self, *args: Any, **kwargs: Any) -> Any:
177         # Save the reveal rules only if they have been changed.
178         # If they were not changed, we can keep using the default rule and
179         # there's no need to save a new RevealRule.
180         if self.submission_feedback_form.has_changed():
181             self.instance.submission_feedback_reveal_rule = (
182                 self.submission_feedback_form.save(*args, **kwargs)
183             )
184         if self.model_solutions_form.has_changed():
185             self.instance.model_solutions_reveal_rule = (
186                 self.model_solutions_form.save(*args, **kwargs)
187             )
188         return super().save(*args, **kwargs)
189 
190 
191 class LTIExerciseForm(BaseExerciseForm):
192 
193     class Meta:
194         model = LTIExercise
195         fields = COMMON_FIELDS + SERVICE_FIELDS + EXERCISE_FIELDS + [
196             'lti_service',
197             'context_id',
198             'resource_link_id',
199             'resource_link_title',
200             'aplus_get_and_post',
201             'open_in_iframe',
202         ]
203 
204     @property
205     def remote_service_head(self):
206         return False
207 
208     def get_content_fieldset(self, *add):
209         return super().get_content_fieldset('lti_service','context_id',
210             'resource_link_id','resource_link_title',
211             'aplus_get_and_post','open_in_iframe','service_url')
212 
213 
214 class LTI1p3ExerciseForm(BaseExerciseForm):
215 
216     class Meta:
217         model = LTI1p3Exercise
218         fields = COMMON_FIELDS + SERVICE_FIELDS + EXERCISE_FIELDS + [
219             'lti_service',
220             'custom',
221             'open_in_iframe',
222         ]
223 
224     @property
225     def remote_service_head(self) -> bool:
226         return False
227 
228     def get_content_fieldset(self, *add) -> Dict[str, Any]:
229         return super().get_content_fieldset('lti_service', 'custom', 'open_in_iframe')
230 
231 
232 class ExerciseWithAttachmentForm(BaseExerciseForm):
233     multipart = True
234 
235     class Meta:
236         model = ExerciseWithAttachment
237         fields = COMMON_FIELDS + SERVICE_FIELDS + EXERCISE_FIELDS + [
238             'content',
239             'files_to_submit',
240             'attachment',
241         ]
242 
243     def get_content_fieldset(self, *add):
244         return super().get_content_fieldset(
245             'content', 'files_to_submit', 'attachment')
246 
247 
248 class StaticExerciseForm(BaseExerciseForm):
249 
250     class Meta:
251         model = StaticExercise
252         fields = COMMON_FIELDS + EXERCISE_FIELDS + [
253             'name',
254             'description',
255             'exercise_page_content',
256             'submission_page_content',
257         ]
258 
259     @property
260     def remote_service_head(self):
261         return False
262 
263     def get_content_fieldset(self, *add):
264         return super().get_content_fieldset(
265             'exercise_page_content', 'submission_page_content')
266 
267 class ExerciseCollectionExerciseForm(BaseExerciseForm):
268 
269     class Meta:
270         model = ExerciseCollection
271         fields = COMMON_FIELDS + EXERCISE_FIELDS + SERVICE_FIELDS + \
272                  ['target_category']
273 
274     def get_content_fieldset(self, *add):
275         return super().get_content_fieldset('target_category')
276
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/edit_course/exercise_forms.py b/edit_course/exercise_forms.py
--- a/edit_course/exercise_forms.py
+++ b/edit_course/exercise_forms.py
@@ -55,6 +55,8 @@
         self.fields["parent"].queryset = LearningObject.objects\
             .exclude(id=self.lobject.id)\
             .filter(course_module=self.lobject.course_module)
+        self.fields['parent'].widget.attrs.update(
+            {'readonly': True, 'disabled': True})
 
     @property
     def remote_service_head(self):

{"golden_diff": "diff --git a/edit_course/exercise_forms.py b/edit_course/exercise_forms.py\n--- a/edit_course/exercise_forms.py\n+++ b/edit_course/exercise_forms.py\n@@ -55,6 +55,8 @@\n         self.fields[\"parent\"].queryset = LearningObject.objects\\\n             .exclude(id=self.lobject.id)\\\n             .filter(course_module=self.lobject.course_module)\n+        self.fields['parent'].widget.attrs.update(\n+            {'readonly': True, 'disabled': True})\n \n     @property\n     def remote_service_head(self):\n", "issue": "Users should not be able to edit content hierarchy\nTypically the A+ course content structure is configured from a JSON file generated by gitmanager. However, the teachers have possibility to edit the attributes also in the Edit course / Content view, including the parent learning object. By editing the parent selection it is possible to create a circular reference loop between two learning objects pointing to each other as a parent, that leads to excessive number of database operations, and as a result, leading the system to become unusable due to heavy database load.\r\n\r\nEasy approach would be to just disable the possibility to modify the parent selection. Later, it might be useful to think more thoroughly how important it is allow editing the content structure in this view, while the main form of course configuration should be through the JSON configuration.\n", "before_files": [{"content": "import logging\nfrom typing import Any, Dict, List\n\nfrom django import forms\nfrom django.utils.translation import gettext_lazy as _\n\nfrom course.models import CourseModule, LearningObjectCategory\nfrom exercise.models import LearningObject, CourseChapter, BaseExercise, \\\n    LTIExercise, StaticExercise, ExerciseWithAttachment, RevealRule, \\\n    LTI1p3Exercise\nfrom lib.widgets import DateTimeLocalInput\nfrom .course_forms import FieldsetModelForm\n\nfrom exercise.exercisecollection_models import ExerciseCollection\n\nlogger = logging.getLogger(\"aplus.exercise\")\n\nCOMMON_FIELDS = [\n    'status',\n    'audience',\n    'category',\n    'course_module',\n    'parent',\n    'order',\n    'url',\n]\nSERVICE_FIELDS = [\n    'service_url',\n    'name',\n    'description',\n]\nEXERCISE_FIELDS = [\n    'max_submissions',\n    'max_points',\n    'difficulty',\n    'points_to_pass',\n    'allow_assistant_viewing',\n    'allow_assistant_grading',\n    'min_group_size',\n    'max_group_size',\n    'model_answers',\n    'templates',\n    'grading_mode',\n]\n\n\nclass LearningObjectMixin:\n\n    def init_fields(self, **kwargs):\n        self.lobject = kwargs.get('instance')\n        self.fields[\"category\"].queryset = LearningObjectCategory.objects.filter(\n            course_instance=self.lobject.course_instance)\n        self.fields[\"course_module\"].queryset = CourseModule.objects.filter(\n            course_instance=self.lobject.course_instance)\n        self.fields[\"parent\"].queryset = LearningObject.objects\\\n            .exclude(id=self.lobject.id)\\\n            .filter(course_module=self.lobject.course_module)\n\n    @property\n    def remote_service_head(self):\n        return True\n\n    def get_hierarchy_fieldset(self):\n        return { 'legend':_('HIERARCHY'), 'fields':self.get_fields('status',\n            'audience', 'category','course_module','parent','order','url') }\n\n    def get_content_fieldset(self, *add):\n        return { 'legend':_('CONTENT'), 'fields':self.get_fields('name',\n            'description', *add) }\n\n\nclass CourseChapterForm(LearningObjectMixin, FieldsetModelForm):\n\n    class Meta:\n        model = CourseChapter\n        fields = COMMON_FIELDS + SERVICE_FIELDS + [\n            'use_wide_column',\n            'generate_table_of_contents'\n        ]\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.init_fields(**kwargs)\n\n    def get_fieldsets(self):\n        return [\n            self.get_hierarchy_fieldset(),\n            self.get_content_fieldset(\n                'use_wide_column', 'generate_table_of_contents'),\n        ]\n\n\nclass RevealRuleForm(FieldsetModelForm):\n    # This form is only used internally by BaseExerciseForm.\n\n    class Meta:\n        model = RevealRule\n        fields = ['trigger', 'delay_minutes', 'time', 'currently_revealed']\n        widgets = {'time': DateTimeLocalInput}\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.fields['trigger'].widget.attrs['data-trigger'] = True\n        # Visibility rules for the form fields. Each of the following fields is\n        # only visible when one of their specified values is selected from the\n        # trigger dropdown. See edit_model.html.\n        self.fields['currently_revealed'].widget.attrs['data-visible-triggers'] = [\n            RevealRule.TRIGGER.MANUAL.value,\n        ]\n        self.fields['time'].widget.attrs['data-visible-triggers'] = [\n            RevealRule.TRIGGER.TIME.value,\n        ]\n        self.fields['delay_minutes'].widget.attrs['data-visible-triggers'] = [\n            RevealRule.TRIGGER.DEADLINE.value,\n            RevealRule.TRIGGER.DEADLINE_ALL.value,\n            RevealRule.TRIGGER.DEADLINE_OR_FULL_POINTS.value,\n        ]\n\n    def clean(self) -> Dict[str, Any]:\n        result = super().clean()\n        errors = {}\n        trigger = self.cleaned_data.get('trigger')\n        if trigger == RevealRule.TRIGGER.TIME:\n            time = self.cleaned_data.get('time')\n            if time is None:\n                errors['time'] = _(\n                    'ERROR_REQUIRED_WITH_SELECTED_TRIGGER'\n                )\n        if errors:\n            raise forms.ValidationError(errors)\n        return result\n\n\nclass BaseExerciseForm(LearningObjectMixin, FieldsetModelForm):\n\n    class Meta:\n        model = BaseExercise\n        fields = COMMON_FIELDS + SERVICE_FIELDS + EXERCISE_FIELDS\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.init_fields(**kwargs)\n\n        # This form contains two embedded RevealRuleForms.\n        self.submission_feedback_form = RevealRuleForm(\n            data=kwargs.get('data'),\n            instance=self.instance.active_submission_feedback_reveal_rule,\n            prefix='submission_feedback',\n        )\n        self.model_solutions_form = RevealRuleForm(\n            data=kwargs.get('data'),\n            instance=self.instance.active_model_solutions_reveal_rule,\n            prefix='model_solutions',\n        )\n\n    def get_fieldsets(self) -> List[Dict[str, Any]]:\n        return [\n            self.get_hierarchy_fieldset(),\n            self.get_content_fieldset('model_answers', 'templates'),\n            { 'legend':_('GRADING'), 'fields':self.get_fields('max_submissions',\n                'max_points','points_to_pass', 'difficulty',\n                'allow_assistant_viewing','allow_assistant_grading','grading_mode') },\n            { 'legend':_('GROUPS'), 'fields':self.get_fields('min_group_size',\n                'max_group_size') },\n            { 'legend':_('REVEAL_SUBMISSION_FEEDBACK'), 'fields':self.submission_feedback_form },\n            { 'legend':_('REVEAL_MODEL_SOLUTIONS'), 'fields':self.model_solutions_form },\n        ]\n\n    def is_valid(self) -> bool:\n        return (\n            super().is_valid()\n            and self.submission_feedback_form.is_valid()\n            and self.model_solutions_form.is_valid()\n        )\n\n    def save(self, *args: Any, **kwargs: Any) -> Any:\n        # Save the reveal rules only if they have been changed.\n        # If they were not changed, we can keep using the default rule and\n        # there's no need to save a new RevealRule.\n        if self.submission_feedback_form.has_changed():\n            self.instance.submission_feedback_reveal_rule = (\n                self.submission_feedback_form.save(*args, **kwargs)\n            )\n        if self.model_solutions_form.has_changed():\n            self.instance.model_solutions_reveal_rule = (\n                self.model_solutions_form.save(*args, **kwargs)\n            )\n        return super().save(*args, **kwargs)\n\n\nclass LTIExerciseForm(BaseExerciseForm):\n\n    class Meta:\n        model = LTIExercise\n        fields = COMMON_FIELDS + SERVICE_FIELDS + EXERCISE_FIELDS + [\n            'lti_service',\n            'context_id',\n            'resource_link_id',\n            'resource_link_title',\n            'aplus_get_and_post',\n            'open_in_iframe',\n        ]\n\n    @property\n    def remote_service_head(self):\n        return False\n\n    def get_content_fieldset(self, *add):\n        return super().get_content_fieldset('lti_service','context_id',\n            'resource_link_id','resource_link_title',\n            'aplus_get_and_post','open_in_iframe','service_url')\n\n\nclass LTI1p3ExerciseForm(BaseExerciseForm):\n\n    class Meta:\n        model = LTI1p3Exercise\n        fields = COMMON_FIELDS + SERVICE_FIELDS + EXERCISE_FIELDS + [\n            'lti_service',\n            'custom',\n            'open_in_iframe',\n        ]\n\n    @property\n    def remote_service_head(self) -> bool:\n        return False\n\n    def get_content_fieldset(self, *add) -> Dict[str, Any]:\n        return super().get_content_fieldset('lti_service', 'custom', 'open_in_iframe')\n\n\nclass ExerciseWithAttachmentForm(BaseExerciseForm):\n    multipart = True\n\n    class Meta:\n        model = ExerciseWithAttachment\n        fields = COMMON_FIELDS + SERVICE_FIELDS + EXERCISE_FIELDS + [\n            'content',\n            'files_to_submit',\n            'attachment',\n        ]\n\n    def get_content_fieldset(self, *add):\n        return super().get_content_fieldset(\n            'content', 'files_to_submit', 'attachment')\n\n\nclass StaticExerciseForm(BaseExerciseForm):\n\n    class Meta:\n        model = StaticExercise\n        fields = COMMON_FIELDS + EXERCISE_FIELDS + [\n            'name',\n            'description',\n            'exercise_page_content',\n            'submission_page_content',\n        ]\n\n    @property\n    def remote_service_head(self):\n        return False\n\n    def get_content_fieldset(self, *add):\n        return super().get_content_fieldset(\n            'exercise_page_content', 'submission_page_content')\n\nclass ExerciseCollectionExerciseForm(BaseExerciseForm):\n\n    class Meta:\n        model = ExerciseCollection\n        fields = COMMON_FIELDS + EXERCISE_FIELDS + SERVICE_FIELDS + \\\n                 ['target_category']\n\n    def get_content_fieldset(self, *add):\n        return super().get_content_fieldset('target_category')\n", "path": "edit_course/exercise_forms.py"}], "after_files": [{"content": "import logging\nfrom typing import Any, Dict, List\n\nfrom django import forms\nfrom django.utils.translation import gettext_lazy as _\n\nfrom course.models import CourseModule, LearningObjectCategory\nfrom exercise.models import LearningObject, CourseChapter, BaseExercise, \\\n    LTIExercise, StaticExercise, ExerciseWithAttachment, RevealRule, \\\n    LTI1p3Exercise\nfrom lib.widgets import DateTimeLocalInput\nfrom .course_forms import FieldsetModelForm\n\nfrom exercise.exercisecollection_models import ExerciseCollection\n\nlogger = logging.getLogger(\"aplus.exercise\")\n\nCOMMON_FIELDS = [\n    'status',\n    'audience',\n    'category',\n    'course_module',\n    'parent',\n    'order',\n    'url',\n]\nSERVICE_FIELDS = [\n    'service_url',\n    'name',\n    'description',\n]\nEXERCISE_FIELDS = [\n    'max_submissions',\n    'max_points',\n    'difficulty',\n    'points_to_pass',\n    'allow_assistant_viewing',\n    'allow_assistant_grading',\n    'min_group_size',\n    'max_group_size',\n    'model_answers',\n    'templates',\n    'grading_mode',\n]\n\n\nclass LearningObjectMixin:\n\n    def init_fields(self, **kwargs):\n        self.lobject = kwargs.get('instance')\n        self.fields[\"category\"].queryset = LearningObjectCategory.objects.filter(\n            course_instance=self.lobject.course_instance)\n        self.fields[\"course_module\"].queryset = CourseModule.objects.filter(\n            course_instance=self.lobject.course_instance)\n        self.fields[\"parent\"].queryset = LearningObject.objects\\\n            .exclude(id=self.lobject.id)\\\n            .filter(course_module=self.lobject.course_module)\n        self.fields['parent'].widget.attrs.update(\n            {'readonly': True, 'disabled': True})\n\n    @property\n    def remote_service_head(self):\n        return True\n\n    def get_hierarchy_fieldset(self):\n        return { 'legend':_('HIERARCHY'), 'fields':self.get_fields('status',\n            'audience', 'category','course_module','parent','order','url') }\n\n    def get_content_fieldset(self, *add):\n        return { 'legend':_('CONTENT'), 'fields':self.get_fields('name',\n            'description', *add) }\n\n\nclass CourseChapterForm(LearningObjectMixin, FieldsetModelForm):\n\n    class Meta:\n        model = CourseChapter\n        fields = COMMON_FIELDS + SERVICE_FIELDS + [\n            'use_wide_column',\n            'generate_table_of_contents'\n        ]\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.init_fields(**kwargs)\n\n    def get_fieldsets(self):\n        return [\n            self.get_hierarchy_fieldset(),\n            self.get_content_fieldset(\n                'use_wide_column', 'generate_table_of_contents'),\n        ]\n\n\nclass RevealRuleForm(FieldsetModelForm):\n    # This form is only used internally by BaseExerciseForm.\n\n    class Meta:\n        model = RevealRule\n        fields = ['trigger', 'delay_minutes', 'time', 'currently_revealed']\n        widgets = {'time': DateTimeLocalInput}\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.fields['trigger'].widget.attrs['data-trigger'] = True\n        # Visibility rules for the form fields. Each of the following fields is\n        # only visible when one of their specified values is selected from the\n        # trigger dropdown. See edit_model.html.\n        self.fields['currently_revealed'].widget.attrs['data-visible-triggers'] = [\n            RevealRule.TRIGGER.MANUAL.value,\n        ]\n        self.fields['time'].widget.attrs['data-visible-triggers'] = [\n            RevealRule.TRIGGER.TIME.value,\n        ]\n        self.fields['delay_minutes'].widget.attrs['data-visible-triggers'] = [\n            RevealRule.TRIGGER.DEADLINE.value,\n            RevealRule.TRIGGER.DEADLINE_ALL.value,\n            RevealRule.TRIGGER.DEADLINE_OR_FULL_POINTS.value,\n        ]\n\n    def clean(self) -> Dict[str, Any]:\n        result = super().clean()\n        errors = {}\n        trigger = self.cleaned_data.get('trigger')\n        if trigger == RevealRule.TRIGGER.TIME:\n            time = self.cleaned_data.get('time')\n            if time is None:\n                errors['time'] = _(\n                    'ERROR_REQUIRED_WITH_SELECTED_TRIGGER'\n                )\n        if errors:\n            raise forms.ValidationError(errors)\n        return result\n\n\nclass BaseExerciseForm(LearningObjectMixin, FieldsetModelForm):\n\n    class Meta:\n        model = BaseExercise\n        fields = COMMON_FIELDS + SERVICE_FIELDS + EXERCISE_FIELDS\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.init_fields(**kwargs)\n\n        # This form contains two embedded RevealRuleForms.\n        self.submission_feedback_form = RevealRuleForm(\n            data=kwargs.get('data'),\n            instance=self.instance.active_submission_feedback_reveal_rule,\n            prefix='submission_feedback',\n        )\n        self.model_solutions_form = RevealRuleForm(\n            data=kwargs.get('data'),\n            instance=self.instance.active_model_solutions_reveal_rule,\n            prefix='model_solutions',\n        )\n\n    def get_fieldsets(self) -> List[Dict[str, Any]]:\n        return [\n            self.get_hierarchy_fieldset(),\n            self.get_content_fieldset('model_answers', 'templates'),\n            { 'legend':_('GRADING'), 'fields':self.get_fields('max_submissions',\n                'max_points','points_to_pass', 'difficulty',\n                'allow_assistant_viewing','allow_assistant_grading','grading_mode') },\n            { 'legend':_('GROUPS'), 'fields':self.get_fields('min_group_size',\n                'max_group_size') },\n            { 'legend':_('REVEAL_SUBMISSION_FEEDBACK'), 'fields':self.submission_feedback_form },\n            { 'legend':_('REVEAL_MODEL_SOLUTIONS'), 'fields':self.model_solutions_form },\n        ]\n\n    def is_valid(self) -> bool:\n        return (\n            super().is_valid()\n            and self.submission_feedback_form.is_valid()\n            and self.model_solutions_form.is_valid()\n        )\n\n    def save(self, *args: Any, **kwargs: Any) -> Any:\n        # Save the reveal rules only if they have been changed.\n        # If they were not changed, we can keep using the default rule and\n        # there's no need to save a new RevealRule.\n        if self.submission_feedback_form.has_changed():\n            self.instance.submission_feedback_reveal_rule = (\n                self.submission_feedback_form.save(*args, **kwargs)\n            )\n        if self.model_solutions_form.has_changed():\n            self.instance.model_solutions_reveal_rule = (\n                self.model_solutions_form.save(*args, **kwargs)\n            )\n        return super().save(*args, **kwargs)\n\n\nclass LTIExerciseForm(BaseExerciseForm):\n\n    class Meta:\n        model = LTIExercise\n        fields = COMMON_FIELDS + SERVICE_FIELDS + EXERCISE_FIELDS + [\n            'lti_service',\n            'context_id',\n            'resource_link_id',\n            'resource_link_title',\n            'aplus_get_and_post',\n            'open_in_iframe',\n        ]\n\n    @property\n    def remote_service_head(self):\n        return False\n\n    def get_content_fieldset(self, *add):\n        return super().get_content_fieldset('lti_service','context_id',\n            'resource_link_id','resource_link_title',\n            'aplus_get_and_post','open_in_iframe','service_url')\n\n\nclass LTI1p3ExerciseForm(BaseExerciseForm):\n\n    class Meta:\n        model = LTI1p3Exercise\n        fields = COMMON_FIELDS + SERVICE_FIELDS + EXERCISE_FIELDS + [\n            'lti_service',\n            'custom',\n            'open_in_iframe',\n        ]\n\n    @property\n    def remote_service_head(self) -> bool:\n        return False\n\n    def get_content_fieldset(self, *add) -> Dict[str, Any]:\n        return super().get_content_fieldset('lti_service', 'custom', 'open_in_iframe')\n\n\nclass ExerciseWithAttachmentForm(BaseExerciseForm):\n    multipart = True\n\n    class Meta:\n        model = ExerciseWithAttachment\n        fields = COMMON_FIELDS + SERVICE_FIELDS + EXERCISE_FIELDS + [\n            'content',\n            'files_to_submit',\n            'attachment',\n        ]\n\n    def get_content_fieldset(self, *add):\n        return super().get_content_fieldset(\n            'content', 'files_to_submit', 'attachment')\n\n\nclass StaticExerciseForm(BaseExerciseForm):\n\n    class Meta:\n        model = StaticExercise\n        fields = COMMON_FIELDS + EXERCISE_FIELDS + [\n            'name',\n            'description',\n            'exercise_page_content',\n            'submission_page_content',\n        ]\n\n    @property\n    def remote_service_head(self):\n        return False\n\n    def get_content_fieldset(self, *add):\n        return super().get_content_fieldset(\n            'exercise_page_content', 'submission_page_content')\n\nclass ExerciseCollectionExerciseForm(BaseExerciseForm):\n\n    class Meta:\n        model = ExerciseCollection\n        fields = COMMON_FIELDS + EXERCISE_FIELDS + SERVICE_FIELDS + \\\n                 ['target_category']\n\n    def get_content_fieldset(self, *add):\n        return super().get_content_fieldset('target_category')\n", "path": "edit_course/exercise_forms.py"}]}