Repository: apluslms/a-plus

Issue:
LTI: points from ACOS assignments are not delivered to LTI Platform
When an ACOS exercise is submitted and A+ operates as an LTI tool, the points are not sent back to LTI platform (typically MyCourses in Aalto). [send_lti_points in lti_tool/utils.py](https://github.com/apluslms/a-plus/blob/master/lti_tool/utils.py#L77) fails to find the launch data with needed information, and LtiException is raised. The difference to other kinds of exercises that do work with LTI is, that with ACOS exercises the submission metadata does not contain 'lti-session-id', but only 'lti-launch-id', which does not seem to be sufficient in this case. The session-id should be set [here in create_from_post](https://github.com/apluslms/a-plus/blob/8f856141f3af955668df001942a72c696f9a9722/exercise/submission_models.py#L259-L263) method.

A possible (but perhaps not ideal) solution could be to have the ACOS server echo the original initiating lti-session-id in the similar way than it echoes the lti-launch-id, as implemented in earlier [a-plus](https://github.com/apluslms/a-plus/commit/7f1d846c1a0163fa7f9fd212b9131b66d9fed7d9) and [acos-aplus](https://github.com/acos-server/acos-aplus/commit/e51c945642a97737907d71cc617d2dd4c4e8e9f7) commits.

Fixing this should be quite high priority, because there are ongoing courses depending on this feature, that is intended to replace the functionality of the Moodle-Astra plugin that will be rolled out soon.


Discussion:
I understood that @PasiSa is working on this so I assigned the issue to him.
Having problems setting up a proper environment for locally testing and developing this, I ran out of time, and therefore unassigning myself.

Resolving patch:
diff --git a/exercise/async_views.py b/exercise/async_views.py
--- a/exercise/async_views.py
+++ b/exercise/async_views.py
@@ -61,6 +61,9 @@ def _post_async_submission(request, exercise, submission, errors=None):
         if (form.cleaned_data["lti_launch_id"]
                 and submission.meta_data.get("lti-launch-id") is None):
             submission.meta_data["lti-launch-id"] = form.cleaned_data["lti_launch_id"]
+        if (form.cleaned_data["lti_session_id"]
+                and submission.meta_data.get("lti-session-id") is None):
+            submission.meta_data["lti-session-id"] = form.cleaned_data["lti_session_id"]
 
         if form.cleaned_data["error"]:
             submission.set_error()
diff --git a/exercise/exercise_models.py b/exercise/exercise_models.py
--- a/exercise/exercise_models.py
+++ b/exercise/exercise_models.py
@@ -977,7 +977,8 @@ def get_load_url( # pylint: disable=too-many-arguments
             )
             return self._build_service_url(
                 language, students,
-                ordinal, url_name, submission_url, lti_launch_id=request.session.get("lti-launch-id")
+                ordinal, url_name, submission_url, lti_launch_id=request.session.get("lti-launch-id"),
+                lti_session_id=request.COOKIES.get("lti1p3-session-id")
             )
         return super().get_load_url(language, request, students, url_name, ordinal)
 
@@ -1016,7 +1017,8 @@ def modify_post_parameters(self, data, files, user, students, request, url): # p
         """
 
     # pylint: disable-next=too-many-arguments
-    def _build_service_url(self, language, students, ordinal_number, url_name, submission_url, lti_launch_id=None):
+    def _build_service_url(self, language, students, ordinal_number, url_name, submission_url,
+                           lti_launch_id=None, lti_session_id=None):
         """
         Generates complete URL with added parameters to the exercise service.
         """
@@ -1032,6 +1034,8 @@ def _build_service_url(self, language, students, ordinal_number, url_name, submi
         }
         if lti_launch_id:
             params["lti_launch_id"] = lti_launch_id
+        if lti_session_id:
+            params["lti_session_id"] = lti_session_id
         return update_url_params(self.get_service_url(language), params)
 
     @property
diff --git a/exercise/forms.py b/exercise/forms.py
--- a/exercise/forms.py
+++ b/exercise/forms.py
@@ -21,6 +21,7 @@ class SubmissionCallbackForm(forms.Form):
     notify = forms.CharField(required=False)
     grading_payload = forms.CharField(required=False)
     lti_launch_id = forms.CharField(required=False)
+    lti_session_id = forms.CharField(required=False)
     error = forms.BooleanField(required=False)
 
     def clean(self):


Test patch: