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

15 create users within a specific tenant #20

Merged
merged 16 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

## 2.2.0
### Added
- [#15](https://github.com/ibleducation/ibl-edx-lti-1p3-provider-app/issues/15): Adds a `LtiToolOrg` model to associate Tools with an Organization (multi-tenant)

### Fixed
- [#15](https://github.com/ibleducation/ibl-edx-lti-1p3-provider-app/issues/15): Log statment error when launching with no launch gate


## 2.1.0
### Added
- [#16](https://github.com/ibleducation/ibl-edx-lti-1p3-provider-app/issues/16): Adds a `LaunchGate` model where we can define whether a tool can launch a specific `UsageKey`
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

setup(
name="ibl-lti-1p3-provider",
version="2.1.0",
version="2.2.0",
packages=find_packages("src"),
include_package_data=True,
package_dir={"": "src"},
Expand Down
43 changes: 40 additions & 3 deletions src/lti_1p3_provider/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,28 @@

@admin.register(models.LaunchGate)
class LaunchGateAdmin(admin.ModelAdmin):
list_display = ("tool_name", "has_allowed_keys", "allowed_orgs")
list_display = (
"tool_name",
"tool_issuer",
"tool_client_id",
"has_allowed_keys",
"allowed_orgs",
)

def has_allowed_keys(self, obj) -> bool:
return bool(obj.allowed_keys)

def tool_name(self, obj):
return f"{obj.tool.title} ({obj.tool.client_id})"
def tool_name(self, obj) -> str:
return f"{obj.tool.title}"

def tool_issuer(self, obj) -> str:
return obj.tool.issuer

def tool_client_id(self, obj) -> str:
return obj.tool.client_id

def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""Shows human readable name for tool selection"""
formfield = super().formfield_for_foreignkey(db_field, request, **kwargs)
if db_field.name == "tool":
formfield.label_from_instance = lambda obj: f"{obj.title} ({obj.client_id})"
Expand All @@ -30,3 +43,27 @@ class LtiProfileAdmin(admin.ModelAdmin):
@admin.register(models.LtiGradedResource)
class LtiGradedResourceAdmin(admin.ModelAdmin):
pass


@admin.register(models.LtiToolOrg)
class LtiTooLOrgAdmin(admin.ModelAdmin):
list_display = ("tool_name", "tool_issuer", "tool_client_id", "edx_org_name")

def tool_name(self, obj) -> str:
return f"{obj.tool.title}"

def tool_issuer(self, obj) -> str:
return obj.tool.issuer

def tool_client_id(self, obj) -> str:
return obj.tool.client_id

def edx_org_name(self, obj) -> str:
return obj.org.short_name

def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""Shows human readable name for tool selection"""
formfield = super().formfield_for_foreignkey(db_field, request, **kwargs)
if db_field.name == "tool":
formfield.label_from_instance = lambda obj: f"{obj.title} ({obj.client_id})"
return formfield
34 changes: 34 additions & 0 deletions src/lti_1p3_provider/migrations/0003_auto_20240808_2032.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 3.2.20 on 2024-08-08 20:32

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('organizations', '0003_historicalorganizationcourse'),
('lti1p3_tool_config', '0001_initial'),
('lti_1p3_provider', '0002_launchgate'),
]

operations = [
migrations.AlterField(
model_name='launchgate',
name='allowed_keys',
field=models.JSONField(blank=True, default=list, help_text='Allows tool to access these specific UsageKeys'),
),
migrations.AlterField(
model_name='launchgate',
name='allowed_orgs',
field=models.JSONField(blank=True, default=list, help_text='Allows tools to access any content in these orgs'),
),
migrations.CreateModel(
name='LtiToolOrg',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('org', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='organizations.organization')),
('tool', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='tool_org', to='lti1p3_tool_config.ltitool')),
],
),
]
18 changes: 18 additions & 0 deletions src/lti_1p3_provider/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
from django.utils.translation import gettext_lazy as _
from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField
from opaque_keys.edx.keys import UsageKey
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
from organizations.models import Organization
from pylti1p3.contrib.django import DjangoDbToolConf, DjangoMessageLaunch
from pylti1p3.contrib.django.lti1p3_tool_config.models import LtiTool
from pylti1p3.grade import Grade
Expand Down Expand Up @@ -379,3 +381,19 @@ def can_access_key(self, usage_key: UsageKey) -> bool:
allowed_orgs = usage_key.course_key.org in self.allowed_orgs

return allowed_keys or allowed_orgs


class LtiToolOrg(models.Model):
"""Association between a Tool and an Organization

The short_name of an org is immutable, so we'll have to get our mutable version
from the SiteConfiguration.site_values['platform_key']
"""

tool = models.OneToOneField(
LtiTool, on_delete=models.CASCADE, related_name="tool_org"
)
org = models.ForeignKey(Organization, on_delete=models.CASCADE)

def __str__(self):
return f"{self.tool.title} - {self.org}"
27 changes: 22 additions & 5 deletions src/lti_1p3_provider/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@
from django.urls import reverse
from django.utils import timezone
from opaque_keys.edx.locator import CourseLocator
from pylti1p3.contrib.django.lti1p3_tool_config.models import LtiTool, LtiToolKey
from organizations.tests.factories import OrganizationFactory
from pylti1p3.contrib.django.lti1p3_tool_config.models import (
LtiTool,
LtiToolKey,
)
from pylti1p3.registration import Registration

from lti_1p3_provider.models import LaunchGate, LtiGradedResource, LtiProfile
from lti_1p3_provider.models import (
LaunchGate,
LtiGradedResource,
LtiProfile,
LtiToolOrg,
)

COURSE_KEY = CourseLocator(org="Org1", course="Course1", run="Run1")
USAGE_KEY = COURSE_KEY.make_usage_key("vertical", "some-html-id")
Expand Down Expand Up @@ -223,9 +232,9 @@ class IdTokenFactory(factory.DictFactory):
@classmethod
def _create(cls, model_class, *args, **kwargs):
obj = super()._create(model_class, *args, **kwargs)
obj[
"https://purl.imsglobal.org/spec/lti/claim/message_type"
] = "LtiResourceLinkRequest"
obj["https://purl.imsglobal.org/spec/lti/claim/message_type"] = (
"LtiResourceLinkRequest"
)
obj["https://purl.imsglobal.org/spec/lti/claim/version"] = "1.3.0"
obj["https://purl.imsglobal.org/spec/lti/claim/deployment_id"] = obj.pop(
"deployment_id"
Expand Down Expand Up @@ -272,3 +281,11 @@ class Meta:
model = LaunchGate

tool = factory.SubFactory(LtiToolFactory)


class LtiToolOrgFactory(factory.django.DjangoModelFactory):
class Meta:
model = LtiToolOrg

tool = factory.SubFactory(LtiToolFactory)
org = factory.SubFactory(OrganizationFactory)
15 changes: 5 additions & 10 deletions src/lti_1p3_provider/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
"""
=======================
Content Libraries Views
=======================

This module contains the REST APIs for blockstore-based content libraries, and
LTI 1.3 views.
"""
"""LTI 1.3 Provider Views"""

from __future__ import annotations

Expand All @@ -26,7 +19,6 @@
from django.urls import Resolver404, resolve, reverse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import View
Expand Down Expand Up @@ -285,6 +277,7 @@ def post(self, request):
log.info("LTI 1.3: Launch message body: %s", json.dumps(self.launch_data))

edx_user = self._authenticate_and_login()

if not edx_user:
return self._bad_request_response()

Expand Down Expand Up @@ -387,7 +380,9 @@ def _check_launch_gate(
return tool.launch_gate.can_access_key(target_usage_key)
except LaunchGate.DoesNotExist:
log.info(
"Tool (iss=%s, client_id=%s) has no launch gate; proceeding", tool.id
"Tool (iss=%s, client_id=%s) has no launch gate; proceeding",
tool.issuer,
tool.client_id,
)

return True
Expand Down