Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BB-5559] Decouple LTI 1.3 from LTI Consumer XBlock functionality #254

Merged

Conversation

tecoholic
Copy link
Contributor

@tecoholic tecoholic commented Jun 3, 2022

Description

The goal of the PR is to decouple the LTI 1.3 functionality from the XBlock and move the functionality to the Django Plugin, while keeping backward compatibility with the XBlock Handlers. The main consideration is that, this does not break any existing integrations. So all the endpoints and XBlock handlers are made backward compatible.

Changes introduced by the refactor

  1. The functionality of LTI 1.3 Launch Handler is moved from the XBlock to the Django plugin
  2. The functionality of the Access Token endpoint is moved from XBlock to the Django plugin
  3. A new URL format using the LtiConfiguration ID is introduced for the Access Token endpoint and is used when a LTI Consumer is configured without a location allowing LTI integrations to be created without the XBlock context.
  4. A new URL format using the LtiConfiguration ID is introduced for the Keyset Endpoint and is used with the location of the XBlock is not available in the configuration.

Testing instructions

  1. Setup an LTI Consumer block (using test tools like Saltire or IMS Global Reference Tool)
  2. Verify the LTI launches are working as expected.
  3. Install the PR branch in the test system pip install git+https:/open-craft/xblock-lti-consumer.git@tecoholic/BB-5559-lti1p3-refactor. If you are using devstack, install it in both lms and studio containers.
  4. Restart the services as necessary.
  5. Ensure that all the LTI Blocks are working as before.

@openedx-webhooks openedx-webhooks added needs triage open-source-contribution PR author is not from Axim or 2U labels Jun 3, 2022
@openedx-webhooks
Copy link

openedx-webhooks commented Jun 3, 2022

Thanks for the pull request, @tecoholic! Please note that it may take us up to several weeks or months to complete a review and merge your PR.

Feel free to add as much of the following information to the ticket as you can:

  • supporting documentation
  • Open edX discussion forum threads
  • timeline information ("this must be merged by XX date", and why that is)
  • partner information ("this is a course on edx.org")
  • any other information that can help Product understand the context for the PR

All technical communication about the code itself will be done via the GitHub pull request interface. As a reminder, our process documentation is here.

Please let us know once your PR is ready for our review and all tests are green.

@tecoholic tecoholic marked this pull request as draft June 3, 2022 05:51
@tecoholic tecoholic force-pushed the tecoholic/BB-5559-lti1p3-refactor branch from 198068d to 0e7c50c Compare June 3, 2022 08:48
@codecov
Copy link

codecov bot commented Jun 3, 2022

Codecov Report

Merging #254 (5dda795) into master (06c08bd) will decrease coverage by 0.46%.
The diff coverage is 94.37%.

@@            Coverage Diff             @@
##           master     #254      +/-   ##
==========================================
- Coverage   97.01%   96.55%   -0.47%     
==========================================
  Files          71       71              
  Lines        5162     5252      +90     
==========================================
+ Hits         5008     5071      +63     
- Misses        154      181      +27     
Flag Coverage Δ
unittests 96.55% <94.37%> (-0.47%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
lti_consumer/utils.py 90.90% <ø> (ø)
lti_consumer/plugin/compat.py 40.21% <28.57%> (-0.96%) ⬇️
lti_consumer/models.py 90.74% <61.53%> (-1.57%) ⬇️
lti_consumer/tests/unit/test_lti_xblock.py 98.93% <68.96%> (-1.07%) ⬇️
lti_consumer/__init__.py 100.00% <100.00%> (ø)
lti_consumer/api.py 96.92% <100.00%> (-1.52%) ⬇️
lti_consumer/lti_xblock.py 93.82% <100.00%> (-1.40%) ⬇️
lti_consumer/plugin/views.py 97.89% <100.00%> (+0.79%) ⬆️
lti_consumer/tests/unit/plugin/test_views.py 100.00% <100.00%> (ø)
lti_consumer/tests/unit/test_api.py 100.00% <100.00%> (ø)
... and 4 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

@tecoholic tecoholic force-pushed the tecoholic/BB-5559-lti1p3-refactor branch 2 times, most recently from 61c2444 to ae42e75 Compare June 3, 2022 09:37
@natabene
Copy link
Contributor

natabene commented Jun 6, 2022

@tecoholic Thank you for the contribution, please let me know once this is ready for our review.

@giovannicimolin
Copy link
Contributor

@tecoholic

@giovannicimolin Once we move the Access Token logic to the Django view, do we need to keep this XBlock Handler here? I do not find any internal reference to this handler except from the Django view. As that is no longer required, we can completely get rid of this handler here right?
I wanted to confirm this with you before I started rewriting the related unittests. Kindly let me know if there is any reason to keep this.
Don't mind the redirect stub I have put in here - its just a placeholder.

Unfortunately, we can't, this is used by external tools - and there's already a lot of them configured on edX.org.
We need to keep backwards capability here (just as a passthrough view, minimal code).

Studio should only display the new URL from now onwards.

Copy link
Contributor

@giovannicimolin giovannicimolin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tecoholic Woah, great progress here!

You'll also need to update the URLs on https:/openedx/xblock-lti-consumer/blob/master/lti_consumer/utils.py#L56 to point to the new endpoints and update references to those.

lti_consumer/lti_xblock.py Outdated Show resolved Hide resolved
lti_consumer/plugin/views.py Outdated Show resolved Hide resolved
lti_consumer/tests/unit/test_lti_xblock.py Show resolved Hide resolved
lti_consumer/plugin/compat.py Outdated Show resolved Hide resolved
@tecoholic tecoholic force-pushed the tecoholic/BB-5559-lti1p3-refactor branch 2 times, most recently from 310a2e2 to 60a24e6 Compare June 22, 2022 13:57
@tecoholic tecoholic changed the title LTI 1.3 Refactoring [BB-5559] Decouple LTI 1.3 from LTI Consumer XBlock functionality Jun 23, 2022
@tecoholic tecoholic marked this pull request as ready for review June 23, 2022 05:43
lti_consumer/lti_xblock.py Outdated Show resolved Hide resolved
lti_consumer/lti_xblock.py Outdated Show resolved Hide resolved
lti_consumer/lti_xblock.py Outdated Show resolved Hide resolved
lti_consumer/lti_xblock.py Outdated Show resolved Hide resolved
lti_consumer/lti_xblock.py Outdated Show resolved Hide resolved
lti_consumer/plugin/views.py Outdated Show resolved Hide resolved
lti_consumer/plugin/views.py Outdated Show resolved Hide resolved
lti_consumer/plugin/views.py Outdated Show resolved Hide resolved
lti_consumer/plugin/views.py Outdated Show resolved Hide resolved
lti_consumer/lti_xblock.py Outdated Show resolved Hide resolved
@tecoholic
Copy link
Contributor Author

NOTE: The lti_config_id in the URLs are supposed to be UUID but it got mis-implemented here, I think. I guess this wasn't caught early on because the regex ([-\w]+) applies to both UUID and integer ids. Maybe this was intentional. Anyway, this means, the new URLs can use UUIDs and prevent primary key from leaking as originally intended.

@Zacharis278
Copy link
Contributor

Hi @tecoholic. This is great, we also have some very similar work queued up to start doing LTI1.3 launches outside of the XBlock. (including #260) I believe @giovannicimolin is out for the better part of the month but I'd like to sync up so we aren't duplicating effort. Who is the best person to reach out to?

@tecoholic tecoholic force-pushed the tecoholic/BB-5559-lti1p3-refactor branch from d4ff48b to c7a8545 Compare July 6, 2022 15:15
@tecoholic
Copy link
Contributor Author

@Zacharis278 Hi! Looks like #260 is a very similar to this. 👀 You are right, @giovannicimolin is going to be unavailable for the month. @Agrendalath is handling the reviews in the meantime. Maybe he can help.

# set to CONFIG_ON_XBLOCK
if config_changed or conf.config_store == conf.CONFIG_ON_XBLOCK:
conf.lti_config.update(block_config)
conf.config_store = conf.CONFIG_ON_DB
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also need to set config_type to 'external'? I see that _get_lti_config is checking this, not _get_lti_config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. I have not considered the external case. For now we can simply return the config without any changes in the get_or_update_lti_config (Line 91). Adding

    if block.config_type == "external":
        return conf

lti_consumer/lti_xblock.py Outdated Show resolved Hide resolved
lti_consumer/plugin/views.py Outdated Show resolved Hide resolved
lti_consumer/tests/unit/test_api.py Show resolved Hide resolved
@tecoholic tecoholic force-pushed the tecoholic/BB-5559-lti1p3-refactor branch from 2f7e77b to 3976c2b Compare July 14, 2022 10:49
@tecoholic tecoholic force-pushed the tecoholic/BB-5559-lti1p3-refactor branch 2 times, most recently from b85f2da to 718f18f Compare July 20, 2022 13:49
@tecoholic
Copy link
Contributor Author

tecoholic commented Jul 21, 2022

Original content removed

Never mind. I figured out the solution literally 5 mins after posting the comment. I think articulating the design challenge helped see the problem clearly. :) Sorry for the noise.

@tecoholic tecoholic force-pushed the tecoholic/BB-5559-lti1p3-refactor branch from ed4e4b3 to 9c2e441 Compare August 4, 2022 07:31
@tecoholic tecoholic force-pushed the tecoholic/BB-5559-lti1p3-refactor branch from c4adc1e to 1684785 Compare August 4, 2022 11:54
@tecoholic
Copy link
Contributor Author

@giovannicimolin I have removed the overengineered parts, incorporated your feedback about the logging, rebased the code to master. Kindly give this another go.

Copy link
Contributor

@MichaelRoytman MichaelRoytman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be building off this, so I'm reviewing to better understand your work. I left a few small thoughts.

@@ -710,6 +700,14 @@ def editable_fields(self):
if not is_external_config_filter_enabled:
noneditable_fields.append('external_config')

if self.lti_version == 'lti_1p3' and is_database_config_enabled and self.config_type == 'database':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had something very similar in #260. I removed it in #266, because I believe that the Javascript should be responsible for showing or hiding fields. If this method doesn't return the LTI 1.3 fields when the config_type is database, then the Javascript cannot show the LTI 1.3 fields when the config_type is changed to XBlock in the modal.

Is this leftover code from #260 that should be removed? Or is it necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MichaelRoytman I believe this would be leftover code from #260. I am not sure why rebasing hasn't updated it. Maybe a conflict line. Let me update it to the latest code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tecoholic This is still showing up for me :)

)
# Asserting that the consumer can be created. This makes sure that the LtiConfiguration
# object exists before calling the Django View
assert self._get_lti_consumer()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need exception handling similar to what you have in launch_gate_endpoint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original code doesn't have any error handling around this line. So I assume it is safe to call. We can add one here if it is required.

lti_consumer/plugin/views.py Outdated Show resolved Hide resolved
Copy link
Contributor

@giovannicimolin giovannicimolin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Launches are working! @tecoholic Nice job!

I left a few comments in the PR though, we can address the remaining issues next sprint with smaller PRs.

Comment on lines +114 to +117
if usage_id:
lti_config = LtiConfiguration.objects.get(location=UsageKey.from_string(usage_id))
elif lti_config_id:
lti_config = LtiConfiguration.objects.get(config_id=lti_config_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If none of these blocks execute, you'll have a NameError: name 'lti_config' is not defined, which is unhandled by the try-except block below.

Copy link
Contributor Author

@tecoholic tecoholic Aug 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But wouldn't the Django's URL resolver take care this path and return a 404? The two paths that lead to this function are

 'lti_consumer/v1/public_keysets/<uuid:lti_config_id>'
 f'lti_consumer/v1/public_keysets/{settings.USAGE_ID_PATTERN}$'

lti_consumer/plugin/views.py Outdated Show resolved Hide resolved
"""
usage_id = request.GET.get('login_hint')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is getting too big - we'll have to change the way we give the context to the LTI consumer on a follow-up PR. Let's get these endpoints working on django with this PR first.

Comment on lines 305 to 312
if lti_config_id:
lti_config = LtiConfiguration.objects.get(config_id=lti_config_id)
else:
usage_key = UsageKey.from_string(usage_id)
lti_config = LtiConfiguration.objects.get(location=usage_key)
except LtiConfiguration.DoesNotExist as exc:
log.warning("Error getting the LTI configuration with id %r: %s", lti_config_id, exc)
raise Http404 from exc
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an extra case for the uncaught path here, and improve log output.

Suggested change
if lti_config_id:
lti_config = LtiConfiguration.objects.get(config_id=lti_config_id)
else:
usage_key = UsageKey.from_string(usage_id)
lti_config = LtiConfiguration.objects.get(location=usage_key)
except LtiConfiguration.DoesNotExist as exc:
log.warning("Error getting the LTI configuration with id %r: %s", lti_config_id, exc)
raise Http404 from exc
if lti_config_id:
lti_config = LtiConfiguration.objects.get(config_id=lti_config_id)
elif usage_id:
usage_key = UsageKey.from_string(usage_id)
lti_config = LtiConfiguration.objects.get(location=usage_key)
else:
raise ValueError("Either `lti_config` or `usage_id` need to be set for this function to work")
except (LtiConfiguration.DoesNotExist, ValueError) as exc:
log.warning("Error getting the LTI configuration with id %r: %s", lti_config_id, exc, exc_info=True)
raise Http404 from exc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as above? The Djanog's URL resolver would reject calls without either lti_config_id or usage_id.

lti_consumer/lti_xblock.py Show resolved Hide resolved
@@ -710,6 +700,14 @@ def editable_fields(self):
if not is_external_config_filter_enabled:
noneditable_fields.append('external_config')

if self.lti_version == 'lti_1p3' and is_database_config_enabled and self.config_type == 'database':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tecoholic This is still showing up for me :)

@giovannicimolin giovannicimolin added the waiting on author PR author needs to resolve review requests, answer questions, fix tests, etc. label Aug 10, 2022
@Zacharis278
Copy link
Contributor

Hey guys, wanted to check in on where we stand with this PR. We've got some work lined up that would require doing a 1.3 launch without an xblock context so this would be great to have in master since there's some big changes here, even if we haven't totally solved the context/login_hint solution in launch_gate_endpoint().

More than happy to collaborate on how to solve that in a future PR since it's key to our needs as well!

@openedx-webhooks openedx-webhooks removed the waiting on author PR author needs to resolve review requests, answer questions, fix tests, etc. label Aug 11, 2022
The LTI 1.3 Launch callback handler is removed from the XBlock as the
logic has been moved to its corresponding Django View. The associated
tests are also moved from the XBlock testcase to the Django View
TestCase.
@tecoholic
Copy link
Contributor Author

@Zacharis278 I have made the changes to address the comments from @giovannicimolin. I think it is close to being merged.

@giovannicimolin
Copy link
Contributor

giovannicimolin commented Aug 12, 2022

@Zacharis278 This is in the final stages of review.
I've started an issue (#273) so we can all discuss the launch context problem and get re-usability and standalone launches working.

@giovannicimolin
Copy link
Contributor

@tecoholic I'm almost done with the review - just wanted to test the grade passback functionality, but I don't have any tools with that capability set up locally.

Other than that, launches and DL launches are working - just want to make sure the other services work too.

Copy link
Contributor

@giovannicimolin giovannicimolin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

  1. I checked that launches are working as expected ✔️
  2. I checked that Grades are being sent back to the LMS ✔️
  3. I checked that Deep linking configuration is still working ✔️
  • I read through the code
  • I checked for accessibility issues NA
  • Includes documentation

@tecoholic Good to go, I've checked that everything keeps working with this new rework.

Before this is merged, can you add a version bump?
We should go to __version__ = '4.4.0' with this change.

@tecoholic
Copy link
Contributor Author

@giovannicimolin Great. Thank you for testing it so thoroughly. I have updated bumped the version to 4.4.0

Copy link
Contributor

@Zacharis278 Zacharis278 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also run through a basic reference tool launch with this. Looks great!

@giovannicimolin giovannicimolin merged commit ec43c30 into openedx:master Aug 17, 2022
@openedx-webhooks
Copy link

@tecoholic 🎉 Your pull request was merged! Please take a moment to answer a two question survey so we can improve your experience in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
open-source-contribution PR author is not from Axim or 2U
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

7 participants