Repository: apluslms/a-plus

Issue:
The response dialog of a submit exercise shows another submission form with delayed feedback
When the ``reveal-submission-feedback`` is used and a student submits a programming exercise, A+ shows another submission form in the response dialog.

```
.. submit:: class_y_programming A600
  :config: exercises/programming/exam-y/config.yaml
  :radar_tokenizer: python
  :title: Extending the program code
  :submissions: 5
  :reveal-submission-feedback: deadline +75min
```

![delayed-feedback-bug](https://user-images.githubusercontent.com/3034945/145585249-7d0d93a1-60c3-4cb2-b1e1-291b5e8be486.png)

I think this is more than a cosmetic bug, because the student can resubmit their file using the *submission form inside the response dialog*. Thus the student can submit the same file twice by mistake.

I encountered this when testing A+ locally (containers *apluslms/run-aplus-front:1.11* and *apluslms/run-mooc-grader:1.11*).



Discussion:
One workaround is to have a run.sh grading script that notifies the student that the exercise will be graded later.

```
#!/bin/bash

# run.sh during exam

title -c "text-primary" "Submission received"

capture echo "<p>Vastauksesi on tallennettu. Tämä tehtävä arvostellaan " \
"tentin jälkeen.</p>" \
"<p>Your answer has been saved. This exercise will be graded after the " \
"examination.</p>"

points 0 10

# Always exit with code 0 to show all error output
exit 0
```

Then, after the deadline has passed, modify the run.sh script to run the actual exercise grader and resubmit students' submissions manually in A+.

Since the feedback is delayed, A+ renders the exercise description form in place of the feedback in modals that would normally show the feedback. Rendering the original exercise form is especially important for questionnaires since it is the best way to show the contents of the existing submission (to remind the student what has been submitted). In file upload exercises, the form is maybe more confusing and the submitted file is anyway separately linked in the top of the "feedback" modal.

So, you would basically want to remove the exercise form from the (delayed) feedback modal dialogs when the exercise accepts only file submissions. Is that correct?
Yes, that's correct.
preliminarily scheduled for autumn 2022 - could you @atilante still specify the schedule of the need! E.g. when the course needing this will run for the next time.
I would say **the end of August 2022**. Ari Korhonen is having another exam for CS-A1141 & CS-A1143 at 10th September as an A+ course ([similar to this one](https://plus.cs.aalto.fi/cs-a1141-and-cs-a1143-exam/2021feb/)).
I and EDIT team discussed today that this issue will be fixed in November for the possibility that CS-A1141 & CS-A1143 Data Structures and Algorithms Y exams 16.12.2022 are at A+.

Resolving patch:
diff --git a/exercise/protocol/exercise_page.py b/exercise/protocol/exercise_page.py
--- a/exercise/protocol/exercise_page.py
+++ b/exercise/protocol/exercise_page.py
@@ -43,26 +43,34 @@ def populate_form(
             field_values: Optional[Dict[str, List[str]]] = None,
             data_values: Optional[Dict[str, str]] = None,
             allow_submit: bool = True,
+            feedback_revealed: bool = True,
         ) -> None:
         """
         Populates the provided values into the exercise form by manipulating
         its HTML. `field_values` are inserted into the form input fields.
         `data_values` are inserted into `data` attributes in the form element.
-        If `allow_submit` is `False`, the submit button is hidden.
+        If `allow_submit` is `False`, the submit button is removed.
+        If `feedback_revealed` is False, file input fields in the form
+        are marked as disabled and the submit button is removed.
         """
         soup = BeautifulSoup(self.content, 'html5lib')
 
         exercise_element = self._find_exercise_element(soup)
         if exercise_element is None:
             return
+        if field_values is None:
+            field_values = {}
 
         # Most likely there is only one form
         for form_element in exercise_element.find_all('form'):
-            if field_values is not None:
-                self._populate_fields(form_element, field_values)
+            force_disable_submit = self._populate_fields(
+                form_element,
+                field_values,
+                feedback_revealed,
+            )
             if data_values is not None:
                 self._populate_data(form_element, data_values)
-            if not allow_submit:
+            if not allow_submit or force_disable_submit:
                 self._remove_submit_button(form_element)
         self.content = str(exercise_element)
 
@@ -81,17 +89,31 @@ def _find_exercise_element(self, soup: BeautifulSoup) -> Optional[Tag]:
 
         return exercise_element
 
-    def _populate_fields(self, form_element: Tag, field_values: Dict[str, List[str]]) -> None:
+    def _populate_fields(
+            self,
+            form_element: Tag,
+            field_values: Dict[str, List[str]],
+            feedback_revealed: bool,
+        ) -> bool:
         """
         Inserts the provided values into the form input elements inside the
         exercise element.
         """
+        force_disable_submit = False
         # Find all form elements on the exercise page and fill in the values
         field_elements = form_element.find_all(['input', 'select', 'textarea'])
         for field_element in field_elements:
             field_element = cast(Tag, field_element)
             field_name = cast(str, field_element.get('name'))
             if field_name not in field_values:
+                disable_file_input_field = (
+                    feedback_revealed is False
+                    and field_element.name == 'input'
+                    and field_element.get('type') == 'file'
+                )
+                if disable_file_input_field:
+                    field_element['disabled'] = ''
+                    force_disable_submit = True
                 continue
             if field_element.name == 'input':
                 if field_element.get('type') in ('radio', 'checkbox'):
@@ -112,6 +134,8 @@ def _populate_fields(self, form_element: Tag, field_values: Dict[str, List[str]]
                 string_content = NavigableString(field_values[field_name][0])
                 field_element.contents = [string_content]
 
+        return force_disable_submit
+
     def _populate_data(self, form_element: Tag, data_values: Dict[str, str]) -> None:
         """
         Inserts the provided values as `data` attributes in the exercise
diff --git a/exercise/submission_models.py b/exercise/submission_models.py
--- a/exercise/submission_models.py
+++ b/exercise/submission_models.py
@@ -439,7 +439,12 @@ def add_files(self, files):
                     param_name=key,
                 )
 
-    def load(self, request: HttpRequest, allow_submit: bool = True) -> ExercisePage:
+    def load(
+            self,
+            request: HttpRequest,
+            allow_submit: bool = True,
+            feedback_revealed: bool = True,
+        ) -> ExercisePage:
         """
         Loads the submission page, i.e. the exercise form with the submitted
         answers filled in. Not the same as the graded form, which is stored in
@@ -447,6 +452,8 @@ def load(self, request: HttpRequest, allow_submit: bool = True) -> ExercisePage:
 
         The `allow_submit` argument determines if the submit button will be
         shown on the page.
+        The `feedback_revealed` argument controls whether file inputs
+        in the exercise form are disabled.
         """
         # Load the exercise page and parse its contents
         submitters = list(self.submitters.all())
@@ -456,9 +463,12 @@ def load(self, request: HttpRequest, allow_submit: bool = True) -> ExercisePage:
             url_name='exercise',
             ordinal=self.ordinal_number(),
         )
-        if self.submission_data:
-            data = pairs_to_dict(self.submission_data)
-            page.populate_form(field_values=data, allow_submit=allow_submit)
+        data = pairs_to_dict(self.submission_data) if self.submission_data else None
+        page.populate_form(
+            field_values=data,
+            allow_submit=allow_submit,
+            feedback_revealed=feedback_revealed,
+        )
 
         return page
 
diff --git a/exercise/views.py b/exercise/views.py
--- a/exercise/views.py
+++ b/exercise/views.py
@@ -209,7 +209,7 @@ def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
                         request,
                         f"{base_message} {self.feedback_hidden_description}",
                     )
-                    page = new_submission.load(request)
+                    page = new_submission.load(request, feedback_revealed=False)
 
             # Redirect non AJAX content page request back.
             if not request.is_ajax() and "__r" in request.GET:
@@ -281,7 +281,7 @@ def get_page(self, request: HttpRequest, students: List[UserProfile]) -> Exercis
                         return page
                     else:
                         # If feedback is not revealed, return the submission page without feedback
-                        return submission.load(request)
+                        return submission.load(request, feedback_revealed=False)
 
         # In every other case, load a blank exercise page
         return self.exercise.load(
@@ -455,7 +455,11 @@ def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
             # When feedback is delayed, show a filled but ungraded form.
             # False is passed in the allow_submit parameter to hide the submit
             # button on the submission page.
-            submission_page = self.submission.load(request, False)
+            submission_page = self.submission.load(
+                request,
+                allow_submit=False,
+                feedback_revealed=False,
+            )
             kwargs.update({'submission_page': submission_page})
         return super().get(request, *args, **kwargs)
 


Test patch: