We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
LTI: remove request.host from LTI parameters
Remove `request.get_host()` from all LTI parameter calculations. Use `settings.BASE_URL` instead. `request.get_host` comes from the browser, thus user can use it to manipulate the data send as part of the post.

These related issues can be fixed in connection or should be created as another issues:

- [x] `tool_consumer_instance_guid` this field doesn't seem to include relevant information. it should be created from settings.BASE_URL
- [x] `launch_presentation_return_url` this should include link to the page in A+, which will render the launch button (basically to the page this link is rendered at).
- [ ] Interesting second part is to include or keep query parameters, so we can request reauthentication by the tool provider (e.g. koodisäilö), which would be sending browser to `{launch_presentation_return_url}?tc_return_url=<ulr>` or such. TODO: raphendyr needs to write down better explanation (i.e. create an issue) - **Moved to an issue #431**

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `external_services/lti.py`
Content:
```
1 from hashlib import md5
2 from django.conf import settings
3 from django.core.exceptions import PermissionDenied
4 from django.utils.translation import get_language
5 from rest_framework.reverse import reverse
6 from rest_framework.settings import api_settings
7 from oauthlib.common import urldecode
8 from oauthlib.oauth1 import Client, SIGNATURE_HMAC, SIGNATURE_TYPE_BODY, \
9     SIGNATURE_TYPE_QUERY
10 import json
11 
12 from aplus.api import api_reverse
13 from lib.helpers import update_url_params
14 from course.models import Enrollment
15 
16 
17 class LTIRequest(object):
18 
19     def __init__(self, service, user, instance, request, title, context_id=None, link_id=None, add=None, exercise=None):
20         self.service = service
21         course = instance.course
22         # Context and resource parameters.
23         context_id = context_id or (request.get_host() + instance.get_absolute_url())
24         link_id = link_id or "aplus{:d}".format(service.pk)
25         title = title or link_id
26 
27         # Gather user information
28         user_id, given_name, family_name, full_name, email = self.user_info(instance, user)
29 
30         # Determine user role.
31         role = "Learner,Student"
32         # Student is not a standard role name, but it has been used here before
33         if course.is_teacher(user):
34             role = "Instructor"
35         elif instance.is_assistant(user):
36             role = "TA,TeachingAssistant" # "TA" is not a standard role
37 
38         self.parameters = add or {}
39         self.parameters.update({
40 
41             "lti_version": "LTI-1p0",
42             "lti_message_type": "basic-lti-launch-request",
43 
44             "resource_link_id": link_id,
45             "resource_link_title": title,
46 
47             # User.
48             "user_id": user_id,
49             "roles": role,
50             "lis_person_name_full": full_name,
51             "lis_person_name_given": given_name,
52             "lis_person_name_family": family_name,
53             "lis_person_contact_email_primary": email,
54 
55             # Selected course.
56             "context_id": context_id,
57             "context_title": course.name,
58             "context_label": course.code,
59 
60             "launch_presentation_locale": get_language(),
61             "launch_presentation_document_target":
62                 "iframe" if exercise and exercise.open_in_iframe else "window",
63             "launch_presentation_return_url": request.scheme + '://' + request.get_host() + instance.get_absolute_url(),
64 
65             "tool_consumer_instance_guid": request.get_host() + "/aplus",
66             "tool_consumer_instance_name": "A+ LMS",
67         })
68 
69         if service.api_access:
70             self.parameters.update({
71                 'custom_context_api': settings.BASE_URL + api_reverse("course-detail", kwargs={'course_id': instance.id}),
72                 'custom_context_api_id': str(instance.id),
73                 'custom_user_api_token': user.userprofile.api_token,
74             })
75 
76         if exercise:
77             # LTI 1.1 Tool Provider may return grades to A+ (Tool Consumer)
78             self.parameters.update({
79                 # Outcome Service requests from the LTI Tool Provider include the
80                 # sourcedid from the launch request. It is used to create new submissions
81                 # for storing the points of the user.
82                 "lis_result_sourcedid": "{}-{}".format(exercise.pk, user_id),
83                 # The LTI Tool Provider posts Outcome Service requests to this URL (i.e., points for a submission)
84                 "lis_outcome_service_url": reverse('lti-outcomes', request=request,
85                                                    kwargs={'version': api_settings.DEFAULT_VERSION}),
86             })
87 
88     def user_info(self, course_instance, user):
89         if self.service.is_anonymous:
90             # Anonymize user information
91             enrollment = Enrollment.objects.filter(course_instance=course_instance, user_profile=user.userprofile).first()
92             if not enrollment:
93                 raise PermissionDenied()
94             # Creates anon name and id for pre-pseudonymisation Enrollments
95             if not (enrollment.anon_name or enrollment.anon_id):
96                 # the model's post_save functions take care of the creation
97                 enrollment.save()
98             user_id = "a" + enrollment.anon_id # a for anonymous
99             full_name = enrollment.anon_name
100             given_name, sep, family_name = full_name.rpartition(" ")
101             if not given_name:
102                 given_name = "Anonymous"
103             email = "anonymous-{}@aplus.invalid".format(enrollment.anon_id)
104         else:
105             user_id = "i" + str(user.pk) # i for internal
106             full_name = "{} {}".format(user.first_name, user.last_name)
107             given_name = user.first_name
108             family_name = user.last_name
109             email = user.email
110         return user_id, given_name, family_name, full_name, email
111 
112     def get_checksum_of_parameters(self, only_user_and_course_level_params=False):
113         if only_user_and_course_level_params:
114             # do not include parameters that change between different exercises for the same LTI service
115             included_keys = (
116                 "lti_version",
117                 "lti_message_type",
118                 "user_id",
119                 "lis_person_name_full",
120                 "lis_person_contact_email_primary",
121                 "context_id",
122                 "context_label",
123                 "tool_consumer_instance_guid",
124             )
125             params = [(key, value) for key, value in self.parameters.items() if key in included_keys]
126         else:
127             params = self.parameters.items()
128         sum = md5()
129         for key, value in sorted(params):
130             sum.update("{}={};".format(key, value).encode('utf-8'))
131         return sum.hexdigest()
132 
133     def sign_post_parameters(self, url=None):
134         client = Client(self.service.consumer_key,
135             client_secret=self.service.consumer_secret,
136             signature_method=SIGNATURE_HMAC,
137             signature_type=SIGNATURE_TYPE_BODY)
138         uri, headers, body = client.sign(self._get_url(url),
139             http_method="POST",
140             body=self.parameters,
141             headers={"Content-Type": "application/x-www-form-urlencoded"})
142         return urldecode(body)
143 
144     def sign_get_query(self, url=None):
145         client = Client(self.service.consumer_key,
146             client_secret=self.service.consumer_secret,
147             signature_method=SIGNATURE_HMAC,
148             signature_type=SIGNATURE_TYPE_QUERY)
149         uri = update_url_params(self._get_url(url), self.parameters)
150         try:
151             query, headers, body = client.sign(uri, http_method="GET")
152         except ValueError as e:
153             raise ValueError("Invalid url %r for %r: %s" % (uri, self.service, e))
154         return query
155 
156     def _get_url(self, url=None):
157         if url and url.startswith('//') or '://' in url:
158             return url
159         return self.service.get_final_url(url)
160 
161 
162 class CustomStudentInfoLTIRequest(LTIRequest):
163 
164     def __init__(self, service, user, profiles, instance, request, title, context_id=None, link_id=None, add=None, exercise=None):
165         self.service = service
166         self.course_instance = instance
167         parameters = add or {}
168         if not service.is_anonymous:
169             parameters['custom_student_id'] = self.true_student_id(user.userprofile)
170         if len(profiles) > 1:
171             parameters['custom_group_members'] = self.group_json(profiles)
172         super().__init__(service, user, instance, request, title, context_id, link_id, parameters, exercise)
173 
174     def true_student_id(self, profile):
175         return profile.student_id or "A{:d}".format(profile.id)
176 
177     def group_json(self, profiles):
178         data = []
179         for profile in profiles:
180             user_id, given_name, family_name, full_name, email = self.user_info(self.course_instance, profile.user)
181             d = {
182                 'user': user_id,
183                 'given_name': given_name,
184                 'family_name': family_name,
185                 'full_name': full_name,
186                 'email': email,
187             }
188             if not self.service.is_anonymous:
189                 d['student_id'] = self.true_student_id(profile)
190             data.append(d)
191         return json.dumps(data)
192
```
--- 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/external_services/lti.py b/external_services/lti.py
--- a/external_services/lti.py
+++ b/external_services/lti.py
@@ -1,4 +1,6 @@
 from hashlib import md5
+from urllib.parse import urlsplit, urljoin
+
 from django.conf import settings
 from django.core.exceptions import PermissionDenied
 from django.utils.translation import get_language
@@ -19,8 +21,9 @@
     def __init__(self, service, user, instance, request, title, context_id=None, link_id=None, add=None, exercise=None):
         self.service = service
         course = instance.course
+        base_url_parts = urlsplit(settings.BASE_URL)
         # Context and resource parameters.
-        context_id = context_id or (request.get_host() + instance.get_absolute_url())
+        context_id = context_id or (base_url_parts.netloc + instance.get_absolute_url())
         link_id = link_id or "aplus{:d}".format(service.pk)
         title = title or link_id
 
@@ -60,9 +63,9 @@
             "launch_presentation_locale": get_language(),
             "launch_presentation_document_target":
                 "iframe" if exercise and exercise.open_in_iframe else "window",
-            "launch_presentation_return_url": request.scheme + '://' + request.get_host() + instance.get_absolute_url(),
+            "launch_presentation_return_url": urljoin(settings.BASE_URL, instance.get_absolute_url()),
 
-            "tool_consumer_instance_guid": request.get_host() + "/aplus",
+            "tool_consumer_instance_guid": base_url_parts.netloc + "/aplus",
             "tool_consumer_instance_name": "A+ LMS",
         })

{"golden_diff": "diff --git a/external_services/lti.py b/external_services/lti.py\n--- a/external_services/lti.py\n+++ b/external_services/lti.py\n@@ -1,4 +1,6 @@\n from hashlib import md5\n+from urllib.parse import urlsplit, urljoin\n+\n from django.conf import settings\n from django.core.exceptions import PermissionDenied\n from django.utils.translation import get_language\n@@ -19,8 +21,9 @@\n     def __init__(self, service, user, instance, request, title, context_id=None, link_id=None, add=None, exercise=None):\n         self.service = service\n         course = instance.course\n+        base_url_parts = urlsplit(settings.BASE_URL)\n         # Context and resource parameters.\n-        context_id = context_id or (request.get_host() + instance.get_absolute_url())\n+        context_id = context_id or (base_url_parts.netloc + instance.get_absolute_url())\n         link_id = link_id or \"aplus{:d}\".format(service.pk)\n         title = title or link_id\n \n@@ -60,9 +63,9 @@\n             \"launch_presentation_locale\": get_language(),\n             \"launch_presentation_document_target\":\n                 \"iframe\" if exercise and exercise.open_in_iframe else \"window\",\n-            \"launch_presentation_return_url\": request.scheme + '://' + request.get_host() + instance.get_absolute_url(),\n+            \"launch_presentation_return_url\": urljoin(settings.BASE_URL, instance.get_absolute_url()),\n \n-            \"tool_consumer_instance_guid\": request.get_host() + \"/aplus\",\n+            \"tool_consumer_instance_guid\": base_url_parts.netloc + \"/aplus\",\n             \"tool_consumer_instance_name\": \"A+ LMS\",\n         })\n", "issue": "LTI: remove request.host from LTI parameters\nRemove `request.get_host()` from all LTI parameter calculations. Use `settings.BASE_URL` instead. `request.get_host` comes from the browser, thus user can use it to manipulate the data send as part of the post.\r\n\r\nThese related issues can be fixed in connection or should be created as another issues:\r\n\r\n- [x] `tool_consumer_instance_guid` this field doesn't seem to include relevant information. it should be created from settings.BASE_URL\r\n- [x] `launch_presentation_return_url` this should include link to the page in A+, which will render the launch button (basically to the page this link is rendered at).\r\n- [ ] Interesting second part is to include or keep query parameters, so we can request reauthentication by the tool provider (e.g. koodis\u00e4il\u00f6), which would be sending browser to `{launch_presentation_return_url}?tc_return_url=<ulr>` or such. TODO: raphendyr needs to write down better explanation (i.e. create an issue) - **Moved to an issue #431**\n", "before_files": [{"content": "from hashlib import md5\nfrom django.conf import settings\nfrom django.core.exceptions import PermissionDenied\nfrom django.utils.translation import get_language\nfrom rest_framework.reverse import reverse\nfrom rest_framework.settings import api_settings\nfrom oauthlib.common import urldecode\nfrom oauthlib.oauth1 import Client, SIGNATURE_HMAC, SIGNATURE_TYPE_BODY, \\\n    SIGNATURE_TYPE_QUERY\nimport json\n\nfrom aplus.api import api_reverse\nfrom lib.helpers import update_url_params\nfrom course.models import Enrollment\n\n\nclass LTIRequest(object):\n\n    def __init__(self, service, user, instance, request, title, context_id=None, link_id=None, add=None, exercise=None):\n        self.service = service\n        course = instance.course\n        # Context and resource parameters.\n        context_id = context_id or (request.get_host() + instance.get_absolute_url())\n        link_id = link_id or \"aplus{:d}\".format(service.pk)\n        title = title or link_id\n\n        # Gather user information\n        user_id, given_name, family_name, full_name, email = self.user_info(instance, user)\n\n        # Determine user role.\n        role = \"Learner,Student\"\n        # Student is not a standard role name, but it has been used here before\n        if course.is_teacher(user):\n            role = \"Instructor\"\n        elif instance.is_assistant(user):\n            role = \"TA,TeachingAssistant\" # \"TA\" is not a standard role\n\n        self.parameters = add or {}\n        self.parameters.update({\n\n            \"lti_version\": \"LTI-1p0\",\n            \"lti_message_type\": \"basic-lti-launch-request\",\n\n            \"resource_link_id\": link_id,\n            \"resource_link_title\": title,\n\n            # User.\n            \"user_id\": user_id,\n            \"roles\": role,\n            \"lis_person_name_full\": full_name,\n            \"lis_person_name_given\": given_name,\n            \"lis_person_name_family\": family_name,\n            \"lis_person_contact_email_primary\": email,\n\n            # Selected course.\n            \"context_id\": context_id,\n            \"context_title\": course.name,\n            \"context_label\": course.code,\n\n            \"launch_presentation_locale\": get_language(),\n            \"launch_presentation_document_target\":\n                \"iframe\" if exercise and exercise.open_in_iframe else \"window\",\n            \"launch_presentation_return_url\": request.scheme + '://' + request.get_host() + instance.get_absolute_url(),\n\n            \"tool_consumer_instance_guid\": request.get_host() + \"/aplus\",\n            \"tool_consumer_instance_name\": \"A+ LMS\",\n        })\n\n        if service.api_access:\n            self.parameters.update({\n                'custom_context_api': settings.BASE_URL + api_reverse(\"course-detail\", kwargs={'course_id': instance.id}),\n                'custom_context_api_id': str(instance.id),\n                'custom_user_api_token': user.userprofile.api_token,\n            })\n\n        if exercise:\n            # LTI 1.1 Tool Provider may return grades to A+ (Tool Consumer)\n            self.parameters.update({\n                # Outcome Service requests from the LTI Tool Provider include the\n                # sourcedid from the launch request. It is used to create new submissions\n                # for storing the points of the user.\n                \"lis_result_sourcedid\": \"{}-{}\".format(exercise.pk, user_id),\n                # The LTI Tool Provider posts Outcome Service requests to this URL (i.e., points for a submission)\n                \"lis_outcome_service_url\": reverse('lti-outcomes', request=request,\n                                                   kwargs={'version': api_settings.DEFAULT_VERSION}),\n            })\n\n    def user_info(self, course_instance, user):\n        if self.service.is_anonymous:\n            # Anonymize user information\n            enrollment = Enrollment.objects.filter(course_instance=course_instance, user_profile=user.userprofile).first()\n            if not enrollment:\n                raise PermissionDenied()\n            # Creates anon name and id for pre-pseudonymisation Enrollments\n            if not (enrollment.anon_name or enrollment.anon_id):\n                # the model's post_save functions take care of the creation\n                enrollment.save()\n            user_id = \"a\" + enrollment.anon_id # a for anonymous\n            full_name = enrollment.anon_name\n            given_name, sep, family_name = full_name.rpartition(\" \")\n            if not given_name:\n                given_name = \"Anonymous\"\n            email = \"anonymous-{}@aplus.invalid\".format(enrollment.anon_id)\n        else:\n            user_id = \"i\" + str(user.pk) # i for internal\n            full_name = \"{} {}\".format(user.first_name, user.last_name)\n            given_name = user.first_name\n            family_name = user.last_name\n            email = user.email\n        return user_id, given_name, family_name, full_name, email\n\n    def get_checksum_of_parameters(self, only_user_and_course_level_params=False):\n        if only_user_and_course_level_params:\n            # do not include parameters that change between different exercises for the same LTI service\n            included_keys = (\n                \"lti_version\",\n                \"lti_message_type\",\n                \"user_id\",\n                \"lis_person_name_full\",\n                \"lis_person_contact_email_primary\",\n                \"context_id\",\n                \"context_label\",\n                \"tool_consumer_instance_guid\",\n            )\n            params = [(key, value) for key, value in self.parameters.items() if key in included_keys]\n        else:\n            params = self.parameters.items()\n        sum = md5()\n        for key, value in sorted(params):\n            sum.update(\"{}={};\".format(key, value).encode('utf-8'))\n        return sum.hexdigest()\n\n    def sign_post_parameters(self, url=None):\n        client = Client(self.service.consumer_key,\n            client_secret=self.service.consumer_secret,\n            signature_method=SIGNATURE_HMAC,\n            signature_type=SIGNATURE_TYPE_BODY)\n        uri, headers, body = client.sign(self._get_url(url),\n            http_method=\"POST\",\n            body=self.parameters,\n            headers={\"Content-Type\": \"application/x-www-form-urlencoded\"})\n        return urldecode(body)\n\n    def sign_get_query(self, url=None):\n        client = Client(self.service.consumer_key,\n            client_secret=self.service.consumer_secret,\n            signature_method=SIGNATURE_HMAC,\n            signature_type=SIGNATURE_TYPE_QUERY)\n        uri = update_url_params(self._get_url(url), self.parameters)\n        try:\n            query, headers, body = client.sign(uri, http_method=\"GET\")\n        except ValueError as e:\n            raise ValueError(\"Invalid url %r for %r: %s\" % (uri, self.service, e))\n        return query\n\n    def _get_url(self, url=None):\n        if url and url.startswith('//') or '://' in url:\n            return url\n        return self.service.get_final_url(url)\n\n\nclass CustomStudentInfoLTIRequest(LTIRequest):\n\n    def __init__(self, service, user, profiles, instance, request, title, context_id=None, link_id=None, add=None, exercise=None):\n        self.service = service\n        self.course_instance = instance\n        parameters = add or {}\n        if not service.is_anonymous:\n            parameters['custom_student_id'] = self.true_student_id(user.userprofile)\n        if len(profiles) > 1:\n            parameters['custom_group_members'] = self.group_json(profiles)\n        super().__init__(service, user, instance, request, title, context_id, link_id, parameters, exercise)\n\n    def true_student_id(self, profile):\n        return profile.student_id or \"A{:d}\".format(profile.id)\n\n    def group_json(self, profiles):\n        data = []\n        for profile in profiles:\n            user_id, given_name, family_name, full_name, email = self.user_info(self.course_instance, profile.user)\n            d = {\n                'user': user_id,\n                'given_name': given_name,\n                'family_name': family_name,\n                'full_name': full_name,\n                'email': email,\n            }\n            if not self.service.is_anonymous:\n                d['student_id'] = self.true_student_id(profile)\n            data.append(d)\n        return json.dumps(data)\n", "path": "external_services/lti.py"}], "after_files": [{"content": "from hashlib import md5\nfrom urllib.parse import urlsplit, urljoin\n\nfrom django.conf import settings\nfrom django.core.exceptions import PermissionDenied\nfrom django.utils.translation import get_language\nfrom rest_framework.reverse import reverse\nfrom rest_framework.settings import api_settings\nfrom oauthlib.common import urldecode\nfrom oauthlib.oauth1 import Client, SIGNATURE_HMAC, SIGNATURE_TYPE_BODY, \\\n    SIGNATURE_TYPE_QUERY\nimport json\n\nfrom aplus.api import api_reverse\nfrom lib.helpers import update_url_params\nfrom course.models import Enrollment\n\n\nclass LTIRequest(object):\n\n    def __init__(self, service, user, instance, request, title, context_id=None, link_id=None, add=None, exercise=None):\n        self.service = service\n        course = instance.course\n        base_url_parts = urlsplit(settings.BASE_URL)\n        # Context and resource parameters.\n        context_id = context_id or (base_url_parts.netloc + instance.get_absolute_url())\n        link_id = link_id or \"aplus{:d}\".format(service.pk)\n        title = title or link_id\n\n        # Gather user information\n        user_id, given_name, family_name, full_name, email = self.user_info(instance, user)\n\n        # Determine user role.\n        role = \"Learner,Student\"\n        # Student is not a standard role name, but it has been used here before\n        if course.is_teacher(user):\n            role = \"Instructor\"\n        elif instance.is_assistant(user):\n            role = \"TA,TeachingAssistant\" # \"TA\" is not a standard role\n\n        self.parameters = add or {}\n        self.parameters.update({\n\n            \"lti_version\": \"LTI-1p0\",\n            \"lti_message_type\": \"basic-lti-launch-request\",\n\n            \"resource_link_id\": link_id,\n            \"resource_link_title\": title,\n\n            # User.\n            \"user_id\": user_id,\n            \"roles\": role,\n            \"lis_person_name_full\": full_name,\n            \"lis_person_name_given\": given_name,\n            \"lis_person_name_family\": family_name,\n            \"lis_person_contact_email_primary\": email,\n\n            # Selected course.\n            \"context_id\": context_id,\n            \"context_title\": course.name,\n            \"context_label\": course.code,\n\n            \"launch_presentation_locale\": get_language(),\n            \"launch_presentation_document_target\":\n                \"iframe\" if exercise and exercise.open_in_iframe else \"window\",\n            \"launch_presentation_return_url\": urljoin(settings.BASE_URL, instance.get_absolute_url()),\n\n            \"tool_consumer_instance_guid\": base_url_parts.netloc + \"/aplus\",\n            \"tool_consumer_instance_name\": \"A+ LMS\",\n        })\n\n        if service.api_access:\n            self.parameters.update({\n                'custom_context_api': settings.BASE_URL + api_reverse(\"course-detail\", kwargs={'course_id': instance.id}),\n                'custom_context_api_id': str(instance.id),\n                'custom_user_api_token': user.userprofile.api_token,\n            })\n\n        if exercise:\n            # LTI 1.1 Tool Provider may return grades to A+ (Tool Consumer)\n            self.parameters.update({\n                # Outcome Service requests from the LTI Tool Provider include the\n                # sourcedid from the launch request. It is used to create new submissions\n                # for storing the points of the user.\n                \"lis_result_sourcedid\": \"{}-{}\".format(exercise.pk, user_id),\n                # The LTI Tool Provider posts Outcome Service requests to this URL (i.e., points for a submission)\n                \"lis_outcome_service_url\": reverse('lti-outcomes', request=request,\n                                                   kwargs={'version': api_settings.DEFAULT_VERSION}),\n            })\n\n    def user_info(self, course_instance, user):\n        if self.service.is_anonymous:\n            # Anonymize user information\n            enrollment = Enrollment.objects.filter(course_instance=course_instance, user_profile=user.userprofile).first()\n            if not enrollment:\n                raise PermissionDenied()\n            # Creates anon name and id for pre-pseudonymisation Enrollments\n            if not (enrollment.anon_name or enrollment.anon_id):\n                # the model's post_save functions take care of the creation\n                enrollment.save()\n            user_id = \"a\" + enrollment.anon_id # a for anonymous\n            full_name = enrollment.anon_name\n            given_name, sep, family_name = full_name.rpartition(\" \")\n            if not given_name:\n                given_name = \"Anonymous\"\n            email = \"anonymous-{}@aplus.invalid\".format(enrollment.anon_id)\n        else:\n            user_id = \"i\" + str(user.pk) # i for internal\n            full_name = \"{} {}\".format(user.first_name, user.last_name)\n            given_name = user.first_name\n            family_name = user.last_name\n            email = user.email\n        return user_id, given_name, family_name, full_name, email\n\n    def get_checksum_of_parameters(self, only_user_and_course_level_params=False):\n        if only_user_and_course_level_params:\n            # do not include parameters that change between different exercises for the same LTI service\n            included_keys = (\n                \"lti_version\",\n                \"lti_message_type\",\n                \"user_id\",\n                \"lis_person_name_full\",\n                \"lis_person_contact_email_primary\",\n                \"context_id\",\n                \"context_label\",\n                \"tool_consumer_instance_guid\",\n            )\n            params = [(key, value) for key, value in self.parameters.items() if key in included_keys]\n        else:\n            params = self.parameters.items()\n        sum = md5()\n        for key, value in sorted(params):\n            sum.update(\"{}={};\".format(key, value).encode('utf-8'))\n        return sum.hexdigest()\n\n    def sign_post_parameters(self, url=None):\n        client = Client(self.service.consumer_key,\n            client_secret=self.service.consumer_secret,\n            signature_method=SIGNATURE_HMAC,\n            signature_type=SIGNATURE_TYPE_BODY)\n        uri, headers, body = client.sign(self._get_url(url),\n            http_method=\"POST\",\n            body=self.parameters,\n            headers={\"Content-Type\": \"application/x-www-form-urlencoded\"})\n        return urldecode(body)\n\n    def sign_get_query(self, url=None):\n        client = Client(self.service.consumer_key,\n            client_secret=self.service.consumer_secret,\n            signature_method=SIGNATURE_HMAC,\n            signature_type=SIGNATURE_TYPE_QUERY)\n        uri = update_url_params(self._get_url(url), self.parameters)\n        try:\n            query, headers, body = client.sign(uri, http_method=\"GET\")\n        except ValueError as e:\n            raise ValueError(\"Invalid url %r for %r: %s\" % (uri, self.service, e))\n        return query\n\n    def _get_url(self, url=None):\n        if url and url.startswith('//') or '://' in url:\n            return url\n        return self.service.get_final_url(url)\n\n\nclass CustomStudentInfoLTIRequest(LTIRequest):\n\n    def __init__(self, service, user, profiles, instance, request, title, context_id=None, link_id=None, add=None, exercise=None):\n        self.service = service\n        self.course_instance = instance\n        parameters = add or {}\n        if not service.is_anonymous:\n            parameters['custom_student_id'] = self.true_student_id(user.userprofile)\n        if len(profiles) > 1:\n            parameters['custom_group_members'] = self.group_json(profiles)\n        super().__init__(service, user, instance, request, title, context_id, link_id, parameters, exercise)\n\n    def true_student_id(self, profile):\n        return profile.student_id or \"A{:d}\".format(profile.id)\n\n    def group_json(self, profiles):\n        data = []\n        for profile in profiles:\n            user_id, given_name, family_name, full_name, email = self.user_info(self.course_instance, profile.user)\n            d = {\n                'user': user_id,\n                'given_name': given_name,\n                'family_name': family_name,\n                'full_name': full_name,\n                'email': email,\n            }\n            if not self.service.is_anonymous:\n                d['student_id'] = self.true_student_id(profile)\n            data.append(d)\n        return json.dumps(data)\n", "path": "external_services/lti.py"}]}