Skip to content

Commit

Permalink
Merge pull request #1 from open-craft/tecoholic/BB-5559-centrally-sto…
Browse files Browse the repository at this point in the history
…re-lti-keys

[BB-5559] Create the plugin to store LTI configurations
  • Loading branch information
tecoholic authored May 9, 2022
2 parents f1d441f + d7a5df1 commit 3c45570
Show file tree
Hide file tree
Showing 24 changed files with 650 additions and 1 deletion.
12 changes: 12 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[run]
omit =
*apps.py,
*migrations/*.py,
*settings*.py,
*tests/*.py,
*urls.py,
*wsgi.py,
*asgi.py,
*test.py,
manage.py,
setup.py,
54 changes: 54 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
*.py[cod]
__pycache__
.pytest_cache

# C extensions
*.so

# Packages
*.egg
*.egg-info
/dist
/build
/eggs
/parts
/bin
/var
/sdist
/develop-eggs
/.installed.cfg
/lib
/lib64

# Installer logs
pip-log.txt

# Unit test / coverage reports
.cache/
.pytest_cache/
.coverage
.coverage.*
.tox
coverage.xml
htmlcov/



# The Silver Searcher
.agignore

# OS X artifacts
*.DS_Store

# Logging
log/
logs/
chromedriver.log
ghostdriver.log

# Complexity
output/*.html
output/*/index.html


*.sqlite
Empty file added CHANGELOG.md
Empty file.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
lint:
black lti_store

test:
pytest --cov-report term-missing lti_store
67 changes: 66 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,66 @@
# Open edX LTI Store (openedx-ltistore)
# Openedx LTI Store

A plugin for openedx-platform to store LTI configurations centrally. This allows course creators to add [LTI components](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/lti_component.html) without having to enter the details like secrets, keys and URLs everytime the component block is created.

## Development

The development instructions are written with the [Open edX Devstack](https://edx.readthedocs.io/projects/open-edx-devstack/en/latest/index.html) as reference.

1. Clone the repostiory to `<devstack_root>/src/` directory
```sh
cd <devstack_root>/src/
git clone [email protected]:open-craft/openedx-ltistore.git
```
2. Install the plugin inside the Studio container and run migrations
```sh
cd <devstack_root>/devstack/
make studio-shell
pip install -e /edx/src/openedx-ltistore
python manage.py cms migrate lti_store
exit
```
3. Install the plugin inside the LMS Container
```sh
make lms-shell
pip install -e /edx/src/openedx-ltistore
exit
```
4. The LTI Consumer XBlock can fetch configurations to LTI Tools using `openedx-filters` mechanism. It calls the filter `org.openedx.xblock.lti_consumer.configuration.listed.v1` whenever it wants to fetch the configurations from external tools like plugins. In order for **LTI Store** to send available LTI Tools, add the following to any existing `openedx-filters` configurations for both LMS (`edx-platform/lms/envs/devstack.py` or `private.py`) and studio (`edx-platform/cms/envs/devstack.py`):
```py
OPEN_EDX_FILTERS_CONFIG = {
"org.openedx.xblock.lti_consumer.configuration.listed.v1": {
"fail_silently": False,
"pipeline": [
"lti_store.pipelines.GetLtiConfigurations"
]
}
}
```
5. Restart the LMS & Studio for the latest config to take effect
```sh
make lms-restart
make studio-restart
```

Now any changes made to the source code should reflect in the application

## Adding LTI Tools to the store

1. Go to `https://localhost:18010/admin`
2. Look for `LTI_STORE` and add **LTI Configurations** by clicking `+ Add` button

## Linting

The project uses [Black](https://black.readthedocs.io/en/stable/) for linting. To lint the code

```
make lint
```

## Testing

Unit tests can be run with

```
make test
```
1 change: 1 addition & 0 deletions lti_store/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.0.1"
16 changes: 16 additions & 0 deletions lti_store/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.contrib import admin

from .models import ExternalLtiConfiguration
from .apps import LtiStoreConfig as App


class LtiConfigurationAdmin(admin.ModelAdmin):
list_display = ("id", "name", "version", "filter_key")
list_filter = ("version",)
prepopulated_fields = {"slug": ("name",)}

def filter_key(self, obj):
return f"{App.name}:{obj.slug}"


admin.site.register(ExternalLtiConfiguration, LtiConfigurationAdmin)
7 changes: 7 additions & 0 deletions lti_store/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.apps import AppConfig


class LtiStoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "lti_store"
plugin_app = {}
62 changes: 62 additions & 0 deletions lti_store/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Generated by Django 3.2.12 on 2022-04-08 20:30

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="ExternalLtiConfiguration",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=80, unique=True)),
("slug", models.SlugField(max_length=80, unique=True)),
("description", models.TextField(blank=True, default="")),
(
"version",
models.CharField(
choices=[("lti_1p1", "LTI 1.1"),],
default="lti_1p1",
max_length=10,
),
),
(
"lti_1p1_launch_url",
models.CharField(
blank=True,
help_text="The URL of the external tool that initiates the launch.",
max_length=255,
),
),
(
"lti_1p1_client_key",
models.CharField(
blank=True,
help_text="Client key provided by the LTI tool provider.",
max_length=255,
),
),
(
"lti_1p1_client_secret",
models.CharField(
blank=True,
help_text="Client secret provided by the LTI tool provider.",
max_length=255,
),
),
],
),
]
Empty file.
38 changes: 38 additions & 0 deletions lti_store/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django.db import models
from django.utils.translation import gettext_lazy as _


class LTIVersion(models.TextChoices):
LTI_1P1 = "lti_1p1", _("LTI 1.1")


class ExternalLtiConfiguration(models.Model):

name = models.CharField(max_length=80, unique=True)
slug = models.SlugField(max_length=80, unique=True)
description = models.TextField(blank=True, default="")

version = models.CharField(
max_length=10, choices=LTIVersion.choices, default=LTIVersion.LTI_1P1
)

# LTI 1.1 Related variables
lti_1p1_launch_url = models.CharField(
max_length=255,
blank=True,
help_text=_("The URL of the external tool that initiates the launch."),
)
lti_1p1_client_key = models.CharField(
max_length=255,
blank=True,
help_text=_("Client key provided by the LTI tool provider."),
)

lti_1p1_client_secret = models.CharField(
max_length=255,
blank=True,
help_text=_("Client secret provided by the LTI tool provider."),
)

def __str__(self):
return f"<ExternalLtiConfiguration #{self.id}: {self.slug}>"
57 changes: 57 additions & 0 deletions lti_store/pipelines.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from typing import Dict

from django.forms.models import model_to_dict

from openedx_filters import PipelineStep

from lti_store.models import ExternalLtiConfiguration
from lti_store.apps import LtiStoreConfig


class GetLtiConfigurations(PipelineStep):
"""
Get all available LTI configurations
Example usage:
Add the following configurations to your configuration file:
OPEN_EDX_FILTERS_CONFIG = {
"org.openedx.xblock.lti_consumer.configuration.listed.v1": {
"fail_silently": false,
"pipeline": [
"lti_store.pipelines.GetLtiConfigurations"
]
}
}
"""

PLUGIN_PREFIX = LtiStoreConfig.name

def run_filter(
self, context: Dict, config_id: str, configurations: Dict, *args, **kwargs
): # pylint: disable=arguments-differ, unused-argument
config = {}
if config_id:
_slug = config_id.split(":")[1]
try:
config_object = ExternalLtiConfiguration.objects.get(slug=_slug)
config = {
f"{self.PLUGIN_PREFIX}:{config_object.slug}": model_to_dict(
config_object
)
}
except ExternalLtiConfiguration.DoesNotExist:
config = {}
else:
config_objs = ExternalLtiConfiguration.objects.all()
config = {
f"{self.PLUGIN_PREFIX}:{c.slug}": model_to_dict(c) for c in config_objs
}

configurations.update(config)
return {
"configurations": configurations,
"config_id": config_id,
"context": context,
}
Empty file added lti_store/tests/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions lti_store/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.test import TestCase
from lti_store.models import ExternalLtiConfiguration, LTIVersion


class LTIConfigurationTestCase(TestCase):
def test_string_representation_of_model(self):
cfg = ExternalLtiConfiguration.objects.create(
name="Test Config", slug="test-config"
)
self.assertEqual(str(cfg), "<ExternalLtiConfiguration #1: test-config>")
cfg.delete()
Loading

0 comments on commit 3c45570

Please sign in to comment.