Skip to content

Commit

Permalink
Merge pull request #26 from iblai/add-course-to-launch-gate-and-api
Browse files Browse the repository at this point in the history
Add course to launch gate and api
  • Loading branch information
geoff-va authored Aug 27, 2024
2 parents 9e62367 + fb7dad8 commit 14e258a
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 37 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## Unreleased

## 2.3.0
### Adds
- [#25](https://github.com/ibleducation/ibl-edx-lti-1p3-provider-app/issues/25): Adds `allowed_courses` to `LaunchGate`
- [#21](https://github.com/ibleducation/ibl-edx-lti-1p3-provider-app/issues/21): Adds API to manage `LtiToolKey`'s at a tenant level
- [#22](https://github.com/ibleducation/ibl-edx-lti-1p3-provider-app/issues/22): Adds API to manage `LtiTool`'s at a tenant level

Expand Down
7 changes: 6 additions & 1 deletion src/lti_1p3_provider/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class LtiGradedResourceAdmin(admin.ModelAdmin):


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

def tool_name(self, obj) -> str:
Expand All @@ -67,3 +67,8 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "tool":
formfield.label_from_instance = lambda obj: f"{obj.title} ({obj.client_id})"
return formfield


@admin.register(models.LtiKeyOrg)
class LtiKeyOrgAdmin(admin.ModelAdmin):
list_display = ("key", "org")
151 changes: 144 additions & 7 deletions src/lti_1p3_provider/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
from __future__ import annotations

import json

from django.db import IntegrityError
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx.core.lib.api.serializers import CourseKeyField, UsageKeyField
from organizations.models import Organization
from pylti1p3.contrib.django.lti1p3_tool_config.models import LtiTool, LtiToolKey
from rest_framework import serializers

from ..models import LtiKeyOrg, LtiToolOrg
from ..models import LaunchGate, LtiKeyOrg, LtiToolOrg
from . import ssl_services


class StringListField(serializers.ListField):
child = serializers.CharField()


class TextBackedListField(StringListField):
"""A ListField backed by a Char-Type field in the db
- Writes as a JSON String
- Reads object from a JSON string
"""

def to_representation(self, data):
if isinstance(data, str):
try:
data = json.loads(data)
except ValueError:
data = []
return super().to_representation(data)

def to_internal_value(self, data):
if data:
return json.dumps(data)
return "[]"


class LtiToolKeySerializer(serializers.ModelSerializer):
class Meta:
model = LtiToolKey
Expand Down Expand Up @@ -41,7 +67,10 @@ def to_representation(self, instance):
def update(self, instance, validated_data):
name = validated_data["name"]
validated_data["name"] = f"{self.context['org_short_name']}-{name}"
return super().update(instance, validated_data)
try:
return super().update(instance, validated_data)
except IntegrityError:
raise serializers.ValidationError(f"Key name: '{name}' already exists")

def create(self, validated_data):
"""Autogenerate private/public key pairs"""
Expand All @@ -59,16 +88,100 @@ def create(self, validated_data):
tool_key = LtiToolKey.objects.create(**validated_data)
LtiKeyOrg.objects.create(key=tool_key, org=lti_org)
except IntegrityError:
raise serializers.ValidationError(f"Tool name: '{name}' already exists")
raise serializers.ValidationError(f"Key name: '{name}' already exists")
return tool_key


class LaunchGateSerializer(serializers.ModelSerializer):
class Meta:
model = LaunchGate
fields = ["allowed_keys", "allowed_courses", "allow_all_within_org"]

allowed_keys = StringListField(
allow_empty=True,
default=lambda: [],
)
allowed_courses = StringListField(
allow_empty=True,
default=lambda: [],
)
allow_all_within_org = serializers.BooleanField(
default=False,
help_text="If True, a target_link_uri will work with any content within this org",
)

def validate(self, attrs):
"""Ensure at least one of allow* is set"""
if not (
attrs["allowed_keys"]
or attrs["allowed_courses"]
or attrs["allow_all_within_org"]
):
raise serializers.ValidationError(
"Set either allow_all_within_org or one or more of allowed_courses/allowed_keys"
)
return attrs

def to_representation(self, instance):
rep = super().to_representation(instance)
rep["allow_all_within_org"] = instance.allowed_orgs == [
self.context["org_short_name"]
]
return rep

def validate_allowed_courses(self, value):
for key in value:
try:
key = CourseKey.from_string(key)
org_short_name = self.context["org_short_name"]
if key.org != org_short_name:
raise serializers.ValidationError(
f"Course Key must be within org: {org_short_name}"
)
except InvalidKeyError:
raise serializers.ValidationError(
"Invalid Course Key. Format is: course-v1:<org>+<course>+<run>"
)
return value

def validate_allowed_keys(self, value):
for key in value:
try:
key = UsageKey.from_string(key)
org_short_name = self.context["org_short_name"]
if key.course_key.org != org_short_name:
raise serializers.ValidationError(
f"Usage Key must be within org: {org_short_name}"
)
except InvalidKeyError:
raise serializers.ValidationError(
"Invalid Usage Key. Format is: "
"block-v1:<org>+<course>+<run>+type@<block_type>+block@<hex_uuid>"
)
return value


class LtiToolSerializer(serializers.ModelSerializer):
class Meta:
model = LtiTool
fields = "__all__"

deployment_ids = StringListField()
fields = [
"id",
"title",
"issuer",
"is_active",
"client_id",
"auth_login_url",
"auth_token_url",
"auth_audience",
"key_set_url",
"key_set",
"tool_key",
"deployment_ids",
"launch_gate",
]

deployment_ids = TextBackedListField()
launch_gate = LaunchGateSerializer()

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand All @@ -79,6 +192,12 @@ def __init__(self, *args, **kwargs):

def validate(self, attrs):
short_name = self.context["org_short_name"]
# Since this endpoint is for an org, allowed_orgs must be [] or their org only
allow_all_within_org = attrs["launch_gate"].pop("allow_all_within_org")
attrs["launch_gate"]["allowed_orgs"] = (
[short_name] if allow_all_within_org else []
)

try:
# Since we're validating it we may as well store it
attrs["org"] = Organization.objects.get(short_name=short_name)
Expand All @@ -94,8 +213,26 @@ def validate(self, attrs):

return attrs

def update(self, instance, validated_data):
"""Update object and launch gate, creating launch gate if necessary"""
# Update LtiTool object
launch_gate_data = validated_data.pop("launch_gate")
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()

# Update LaunchGate
launch_gate = instance.launch_gate
for attr, value in launch_gate_data.items():
setattr(launch_gate, attr, value)
launch_gate.save()

return instance

def create(self, validated_data):
lti_org = validated_data.pop("org")
tool = super().create(validated_data)
launch_gate_data = validated_data.pop("launch_gate")
tool = LtiTool.objects.create(**validated_data)
LtiToolOrg.objects.create(tool=tool, org=lti_org)
LaunchGate.objects.create(tool=tool, **launch_gate_data)
return tool
Loading

0 comments on commit 14e258a

Please sign in to comment.