Repository: apluslms/a-plus

Issue:
LTI Tool v1.3: add LTI grading support for Acos exercises (AJAX submission)
Pull request #1104 added the LTI Tool v1.3 feature to A+. However, the LTI gradebook integration does not yet include assignments that submit the final (graded) solution directly to the grader backend (via AJAX), which then forwards it to the A+ front (including the submission and grading data as well as the feedback and points). For example, Acos and JSAV. The A+ front creates the submission in the database only in the end and attaches the points at the same time. 

A+ code where Acos grading is received and the submission is created: 
https://github.com/apluslms/a-plus/blob/9b530cb00d0594d8d9f49b0a0c84c4239a039791/exercise/api/views.py#L125-L132

The API endpoint above matches the `submission_url` argument of the A+ grader protocol when A+ retrieves the assignment description from the grader (HTTP GET):
https://github.com/apluslms/a-plus/blob/6f7da8cbc52ef01da1799097f7cf737e99b05862/doc/GRADERS.md


Discussion:

Resolving patch:
diff --git a/exercise/async_views.py b/exercise/async_views.py
--- a/exercise/async_views.py
+++ b/exercise/async_views.py
@@ -52,6 +52,12 @@ def _post_async_submission(request, exercise, submission, errors=None):
         submission.feedback = form.cleaned_data["feedback"]
         submission.grading_data = post_data
 
+        # Acos-submissions initialize this as an empty string for some reason; fix here
+        if submission.meta_data == "":
+            submission.meta_data = {}
+        if form.cleaned_data["lti_launch_id"]:
+            submission.meta_data["lti-launch-id"] = form.cleaned_data["lti_launch_id"]
+
         if form.cleaned_data["error"]:
             submission.set_error()
         else:
diff --git a/exercise/exercise_models.py b/exercise/exercise_models.py
--- a/exercise/exercise_models.py
+++ b/exercise/exercise_models.py
@@ -955,7 +955,7 @@ def get_load_url(
             )
             return self._build_service_url(
                 language, students,
-                ordinal, url_name, submission_url
+                ordinal, url_name, submission_url, lti_launch_id=request.session.get("lti-launch-id")
             )
         return super().get_load_url(language, request, students, url_name, ordinal)
 
@@ -994,12 +994,12 @@ def modify_post_parameters(self, data, files, user, students, request, url):
         """
         pass
 
-    def _build_service_url(self, language, students, ordinal_number, url_name, submission_url):
+    def _build_service_url(self, language, students, ordinal_number, url_name, submission_url, lti_launch_id=None):
         """
         Generates complete URL with added parameters to the exercise service.
         """
         uid_str = '-'.join(sorted(str(profile.user.id) for profile in students)) if students else ''
-        return update_url_params(self.get_service_url(language), {
+        params = {
             "max_points": self.max_points,
             "max_submissions": self.max_submissions,
             "submission_url": build_aplus_url(submission_url),
@@ -1007,7 +1007,10 @@ def _build_service_url(self, language, students, ordinal_number, url_name, submi
             "uid": uid_str,
             "ordinal_number": ordinal_number,
             "lang": language,
-        })
+        }
+        if lti_launch_id:
+            params["lti_launch_id"] = lti_launch_id
+        return update_url_params(self.get_service_url(language), params)
 
     @property
     def can_regrade(self):
diff --git a/exercise/forms.py b/exercise/forms.py
--- a/exercise/forms.py
+++ b/exercise/forms.py
@@ -20,6 +20,7 @@ class SubmissionCallbackForm(forms.Form):
     feedback = forms.CharField(required=False)
     notify = forms.CharField(required=False)
     grading_payload = forms.CharField(required=False)
+    lti_launch_id = forms.CharField(required=False)
     error = forms.BooleanField(required=False)
 
     def clean(self):


Test patch: