1515from opaque_keys .edx .django .models import CourseKeyField , UsageKeyField
1616from opaque_keys .edx .keys import CourseKey
1717from config_models .models import ConfigurationModel
18+ from django .utils .functional import cached_property
1819from django .utils .translation import gettext_lazy as _
1920from lti_consumer .filters import get_external_config_from_filter
2021
3132 get_lti_nrps_context_membership_url ,
3233 choose_lti_1p3_redirect_uris ,
3334 model_to_dict ,
35+ EXTERNAL_ID_REGEX ,
3436)
3537
3638log = logging .getLogger (__name__ )
@@ -251,6 +253,12 @@ def clean(self):
251253 raise ValidationError ({
252254 "config_store" : _ ("LTI Configuration stores on XBlock needs a block location set." ),
253255 })
256+ if self .config_store == self .CONFIG_EXTERNAL and not EXTERNAL_ID_REGEX .match (str (self .external_id )):
257+ raise ValidationError ({
258+ "config_store" : _ (
259+ 'LTI Configuration using reusable configuration needs a external ID in "x:y" format.' ,
260+ ),
261+ })
254262 if self .version == self .LTI_1P3 and self .config_store == self .CONFIG_ON_DB :
255263 if self .lti_1p3_tool_public_key == "" and self .lti_1p3_tool_keyset_url == "" :
256264 raise ValidationError ({
@@ -280,7 +288,7 @@ def sync_configurations(self):
280288 otherwise, it will try to query any children configuration and update their fields using
281289 the current configuration values.
282290 """
283- EXCLUDED_FIELDS = ['id' , 'config_id' , 'location' ]
291+ EXCLUDED_FIELDS = ['id' , 'config_id' , 'location' , 'external_config' ]
284292
285293 if isinstance (self .location , CCXBlockUsageLocator ):
286294 # Query main configuration using main location.
@@ -364,6 +372,13 @@ def lti_1p3_public_jwk(self):
364372 self ._generate_lti_1p3_keys_if_missing ()
365373 return json .loads (self .lti_1p3_internal_public_jwk )
366374
375+ @cached_property
376+ def external_config (self ):
377+ """
378+ Return the external resuable configuration.
379+ """
380+ return get_external_config_from_filter ({}, self .external_id )
381+
367382 def _get_lti_1p1_consumer (self ):
368383 """
369384 Return a class of LTI 1.1 consumer.
@@ -374,10 +389,9 @@ def _get_lti_1p1_consumer(self):
374389 key , secret = block .lti_provider_key_secret
375390 launch_url = block .launch_url
376391 elif self .config_store == self .CONFIG_EXTERNAL :
377- config = get_external_config_from_filter ({}, self .external_id )
378- key = config .get ("lti_1p1_client_key" )
379- secret = config .get ("lti_1p1_client_secret" )
380- launch_url = config .get ("lti_1p1_launch_url" )
392+ key = self .external_config .get ("lti_1p1_client_key" )
393+ secret = self .external_config .get ("lti_1p1_client_secret" )
394+ launch_url = self .external_config .get ("lti_1p1_launch_url" )
381395 else :
382396 key = self .lti_1p1_client_key
383397 secret = self .lti_1p1_client_secret
@@ -389,11 +403,10 @@ def get_lti_advantage_ags_mode(self):
389403 """
390404 Return LTI 1.3 Advantage Assignment and Grade Services mode.
391405 """
392- if self .config_store == self .CONFIG_EXTERNAL :
393- # TODO: Add support for CONFIG_EXTERNAL for LTI 1.3.
394- raise NotImplementedError
395406 if self .config_store == self .CONFIG_ON_DB :
396407 return self .lti_advantage_ags_mode
408+ elif self .config_store == self .CONFIG_EXTERNAL :
409+ return self .external_config .get ('lti_advantage_ags_mode' )
397410 else :
398411 block = compat .load_enough_xblock (self .location )
399412 return block .lti_advantage_ags_mode
@@ -402,11 +415,10 @@ def get_lti_advantage_deep_linking_enabled(self):
402415 """
403416 Return whether LTI 1.3 Advantage Deep Linking is enabled.
404417 """
405- if self .config_store == self .CONFIG_EXTERNAL :
406- # TODO: Add support for CONFIG_EXTERNAL for LTI 1.3.
407- raise NotImplementedError ("CONFIG_EXTERNAL is not supported for LTI 1.3 Advantage services: %s" )
408418 if self .config_store == self .CONFIG_ON_DB :
409419 return self .lti_advantage_deep_linking_enabled
420+ elif self .config_store == self .CONFIG_EXTERNAL :
421+ return self .external_config .get ('lti_advantage_deep_linking_enabled' )
410422 else :
411423 block = compat .load_enough_xblock (self .location )
412424 return block .lti_advantage_deep_linking_enabled
@@ -415,11 +427,10 @@ def get_lti_advantage_deep_linking_launch_url(self):
415427 """
416428 Return the LTI 1.3 Advantage Deep Linking launch URL.
417429 """
418- if self .config_store == self .CONFIG_EXTERNAL :
419- # TODO: Add support for CONFIG_EXTERNAL for LTI 1.3.
420- raise NotImplementedError ("CONFIG_EXTERNAL is not supported for LTI 1.3 Advantage services: %s" )
421430 if self .config_store == self .CONFIG_ON_DB :
422431 return self .lti_advantage_deep_linking_launch_url
432+ elif self .config_store == self .CONFIG_EXTERNAL :
433+ return self .external_config .get ('lti_advantage_deep_linking_launch_url' )
423434 else :
424435 block = compat .load_enough_xblock (self .location )
425436 return block .lti_advantage_deep_linking_launch_url
@@ -428,11 +439,10 @@ def get_lti_advantage_nrps_enabled(self):
428439 """
429440 Return whether LTI 1.3 Advantage Names and Role Provisioning Services is enabled.
430441 """
431- if self .config_store == self .CONFIG_EXTERNAL :
432- # TODO: Add support for CONFIG_EXTERNAL for LTI 1.3.
433- raise NotImplementedError ("CONFIG_EXTERNAL is not supported for LTI 1.3 Advantage services: %s" )
434442 if self .config_store == self .CONFIG_ON_DB :
435443 return self .lti_advantage_enable_nrps
444+ elif self .config_store == self .CONFIG_EXTERNAL :
445+ return self .external_config .get ('lti_advantage_enable_nrps' )
436446 else :
437447 block = compat .load_enough_xblock (self .location )
438448 return block .lti_1p3_enable_nrps
@@ -453,6 +463,7 @@ def _setup_lti_1p3_ags(self, consumer):
453463 return
454464
455465 lineitem = self .ltiagslineitem_set .first ()
466+
456467 # If using the declarative approach, we should create a LineItem if it
457468 # doesn't exist. This is because on this mode the tool is not able to create
458469 # and manage lineitems using the AGS endpoints.
@@ -572,9 +583,25 @@ def _get_lti_1p3_consumer(self):
572583 tool_key = self .lti_1p3_tool_public_key ,
573584 tool_keyset_url = self .lti_1p3_tool_keyset_url ,
574585 )
586+ elif self .config_store == self .CONFIG_EXTERNAL :
587+ consumer = consumer_class (
588+ iss = get_lti_api_base (),
589+ lti_oidc_url = self .external_config .get ('lti_1p3_oidc_url' ),
590+ lti_launch_url = self .external_config .get ('lti_1p3_launch_url' ),
591+ client_id = self .external_config .get ('lti_1p3_client_id' ),
592+ # Deployment ID hardcoded to 1 since
593+ # we're not using multi-tenancy.
594+ deployment_id = '1' ,
595+ rsa_key = self .external_config .get ('lti_1p3_private_key' ),
596+ rsa_key_id = self .external_config .get ('lti_1p3_private_key_id' ),
597+ # Registered redirect uris
598+ redirect_uris = self .get_lti_1p3_redirect_uris (),
599+ tool_key = self .external_config .get ('lti_1p3_tool_public_key' ),
600+ tool_keyset_url = self .external_config .get ('lti_1p3_tool_keyset_url' ),
601+ )
575602 else :
576- # This should not occur, but raise an error if self.config_store is not CONFIG_ON_XBLOCK
577- # or CONFIG_ON_DB.
603+ # This should not occur, but raise an error if self.config_store is not
604+ # CONFIG_ON_XBLOCK, CONFIG_ON_DB or CONFIG_EXTERNAL .
578605 raise NotImplementedError
579606
580607 if isinstance (consumer , LtiAdvantageConsumer ):
@@ -598,10 +625,10 @@ def get_lti_1p3_redirect_uris(self):
598625 Return pre-registered redirect uris or sensible defaults
599626 """
600627 if self .config_store == self .CONFIG_EXTERNAL :
601- # TODO: Add support for CONFIG_EXTERNAL for LTI 1.3.
602- raise NotImplementedError
603-
604- if self .config_store == self .CONFIG_ON_DB :
628+ redirect_uris = self . external_config . get ( 'lti_1p3_redirect_uris' )
629+ launch_url = self . external_config . get ( 'lti_1p3_launch_url' )
630+ deep_link_launch_url = self . external_config . get ( 'lti_advantage_deep_linking_launch_url' )
631+ elif self .config_store == self .CONFIG_ON_DB :
605632 redirect_uris = self .lti_1p3_redirect_uris
606633 launch_url = self .lti_1p3_launch_url
607634 deep_link_launch_url = self .lti_advantage_deep_linking_launch_url
0 commit comments