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

LTI 1.3 #1023

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft

LTI 1.3 #1023

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
2 changes: 1 addition & 1 deletion doc/dev_doc/extensions_doc/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The following code adds a new page displaying ``This is a simple demo plugin`` o

def init(plugin_manager, taskset_factory, client, plugin_config):
""" Init the plugin """
plugin_manager.add_page("/<cookieless:sessionid>plugindemo", DemoPage.as_view('demopage'))
plugin_manager.add_page("/plugindemo", DemoPage.as_view('demopage'))


The plugin is initialized by the plugin manager, which is the frontend-extended hook manager, by calling method ``init``.
Expand Down
83 changes: 43 additions & 40 deletions inginious/frontend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from inginious.frontend.taskset_factory import create_factories
from inginious.common.entrypoints import filesystem_from_config_dict
from inginious.common.filesystems.local import LocalFSProvider
from inginious.frontend.lti_outcome_manager import LTIOutcomeManager
from inginious.frontend.lti_grade_manager import LTIGradeManager
from inginious.frontend.task_problems import get_default_displayable_problem_types
from inginious.frontend.task_dispensers.toc import TableOfContents
from inginious.frontend.task_dispensers.combinatory_test import CombinatoryTest
Expand Down Expand Up @@ -100,20 +100,20 @@ def _put_configuration_defaults(config):

return config

def get_homepath():
""" Returns the URL root. """
return flask.request.url_root[:-1]

def get_homepath(ignore_session=False, force_cookieless=False):
def get_path(*path_parts):
"""
:param ignore_session: Ignore the cookieless session_id that should be put in the URL
:param force_cookieless: Force the cookieless session; the link will include the session_creator if needed.
:param path_parts: List of elements in the path to be separated by slashes
"""
session = flask.session
request = flask.request
if not ignore_session and session.sid is not None and session.cookieless:
return request.url_root[:-1] + "/@" + session.sid + "@"
elif not ignore_session and force_cookieless:
return request.url_root[:-1] + "/@@"
else:
return request.url_root[:-1]
lti_session_id = flask.request.args.get('session_id', flask.g.get('lti_session_id'))
path_parts = (get_homepath(), ) + path_parts
if lti_session_id:
query_delimiter = '&' if path_parts and '?' in path_parts[-1] else '?'
return "/".join(path_parts) + f"{query_delimiter}session_id={lti_session_id}"
return "/".join(path_parts)


def _close_app(mongo_client, client):
Expand Down Expand Up @@ -161,33 +161,10 @@ def get_app(config):
"sessions", config.get('SESSION_USE_SIGNER', False), True # config.get('SESSION_PERMANENT', True)
)

# Init gettext
available_translations = {
"de": "Deutsch",
"el": "ελληνικά",
"es": "Español",
"fr": "Français",
"he": "עִבְרִית",
"nl": "Nederlands",
"nb_NO": "Norsk (bokmål)",
"pt": "Português",
"vi": "Tiếng Việt"
}

available_languages = {"en": "English"}
available_languages.update(available_translations)

l10n_manager = L10nManager()

l10n_manager.translations["en"] = gettext.NullTranslations() # English does not need translation ;-)
for lang in available_translations.keys():
l10n_manager.translations[lang] = gettext.translation('messages', get_root_path() + '/frontend/i18n', [lang])

builtins.__dict__['_'] = l10n_manager.gettext

if config.get("maintenance", False):
template_helper = TemplateHelper(PluginManager(), None, config.get('use_minified_js', True))
template_helper.add_to_template_globals("get_homepath", get_homepath)
template_helper.add_to_template_globals("get_path", get_path)
template_helper.add_to_template_globals("pkg_version", __version__)
template_helper.add_to_template_globals("available_languages", available_languages)
template_helper.add_to_template_globals("_", _)
Expand Down Expand Up @@ -222,20 +199,45 @@ def get_app(config):
taskset_factory, course_factory, task_factory = create_factories(fs_provider, default_task_dispensers, default_problem_types, plugin_manager, database)

user_manager = UserManager(database, config.get('superadmins', []))
flask.request_finished.connect(UserManager._lti_session_save, flask_app)

update_pending_jobs(database)

client = create_arch(config, fs_provider, zmq_context, taskset_factory)

lti_outcome_manager = LTIOutcomeManager(database, user_manager, course_factory)
lti_grade_manager = LTIGradeManager(database, flask_app)

submission_manager = WebAppSubmissionManager(client, user_manager, database, gridfs, plugin_manager, lti_outcome_manager)
submission_manager = WebAppSubmissionManager(client, user_manager, database, gridfs, plugin_manager, lti_grade_manager)
template_helper = TemplateHelper(plugin_manager, user_manager, config.get('use_minified_js', True))

register_utils(database, user_manager, template_helper)

is_tos_defined = config.get("privacy_page", "") and config.get("terms_page", "")

# Init gettext
available_translations = {
"de": "Deutsch",
"el": "ελληνικά",
"es": "Español",
"fr": "Français",
"he": "עִבְרִית",
"nl": "Nederlands",
"nb_NO": "Norsk (bokmål)",
"pt": "Português",
"vi": "Tiếng Việt"
}

available_languages = {"en": "English"}
available_languages.update(available_translations)

l10n_manager = L10nManager(user_manager)

l10n_manager.translations["en"] = gettext.NullTranslations() # English does not need translation ;-)
for lang in available_translations.keys():
l10n_manager.translations[lang] = gettext.translation('messages', get_root_path() + '/frontend/i18n', [lang])

builtins.__dict__['_'] = l10n_manager.gettext

# Init web mail
mail.init_app(flask_app)

Expand All @@ -244,6 +246,7 @@ def get_app(config):
template_helper.add_to_template_globals("str", str)
template_helper.add_to_template_globals("available_languages", available_languages)
template_helper.add_to_template_globals("get_homepath", get_homepath)
template_helper.add_to_template_globals("get_path", get_path)
template_helper.add_to_template_globals("pkg_version", __version__)
template_helper.add_to_template_globals("allow_registration", config.get("allow_registration", True))
template_helper.add_to_template_globals("sentry_io_url", config.get("sentry_io_url"))
Expand Down Expand Up @@ -283,7 +286,7 @@ def flask_internalerror(e):
flask_app.register_error_handler(InternalServerError, flask_internalerror)

# Insert the needed singletons into the application, to allow pages to call them
flask_app.get_homepath = get_homepath
flask_app.get_path = get_path
Copy link
Contributor Author

Choose a reason for hiding this comment

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

get_path should now be used in all instances where get_homepath was used to build an URL. Templates still have access to get_homepath when a public URL is needed, such that I didn't have to go thru all that code (esp. layout.html) by hand.

flask_app.plugin_manager = plugin_manager
flask_app.taskset_factory = taskset_factory
flask_app.course_factory = course_factory
Expand All @@ -299,7 +302,7 @@ def flask_internalerror(e):
flask_app.default_max_file_size = default_max_file_size
flask_app.backup_dir = config.get("backup_directory", './backup')
flask_app.webterm_link = config.get("webterm", None)
flask_app.lti_outcome_manager = lti_outcome_manager
flask_app.lti_grade_manager = lti_grade_manager
flask_app.allow_registration = config.get("allow_registration", True)
flask_app.allow_deletion = config.get("allow_deletion", True)
flask_app.available_languages = available_languages
Expand Down
33 changes: 27 additions & 6 deletions inginious/frontend/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

import copy
import gettext
import hashlib
import re
from typing import List
from typing import Iterable, List

from inginious.frontend.user_settings.course_user_setting import CourseUserSetting
from inginious.common.tags import Tag
Expand All @@ -18,6 +19,7 @@
from inginious.frontend.task_dispensers.toc import TableOfContents
from inginious.frontend.tasksets import _migrate_from_v_0_6

from pylti1p3.tool_config import ToolConfDict

class Course(object):
""" A course with some modification for users """
Expand Down Expand Up @@ -54,7 +56,7 @@ def __init__(self, courseid, content, taskset_factory, task_factory, plugin_mana
self._allow_preview = self._content.get('allow_preview', False)
self._is_lti = self._content.get('is_lti', False)
self._lti_url = self._content.get('lti_url', '')
self._lti_keys = self._content.get('lti_keys', {})
self._lti_config = self._content.get('lti_config', {})
self._lti_send_back_grade = self._content.get('lti_send_back_grade', False)
self._tags = {key: Tag(key, tag_dict, self.gettext) for key, tag_dict in self._content.get("tags", {}).items()}
self._course_user_setting = {key: CourseUserSetting(key,
Expand Down Expand Up @@ -82,7 +84,7 @@ def __init__(self, courseid, content, taskset_factory, task_factory, plugin_mana
self._groups_student_choice = False
self._allow_unregister = False
else:
self._lti_keys = {}
self._lti_config = {}
self._lti_url = ''
self._lti_send_back_grade = False

Expand Down Expand Up @@ -162,9 +164,28 @@ def is_lti(self):
""" True if the current course is in LTI mode """
return self._is_lti

def lti_keys(self):
""" {name: key} for the LTI customers """
return self._lti_keys if self._is_lti else {}
def lti_config(self):
""" LTI Tool config dictionary. Specs are at https://github.com/dmitry-viskov/pylti1.3/blob/master/README.rst?plain=1#L70-L98 """
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There will be documentation part of this PR on this.

return self._lti_config if self._is_lti else {}

def lti_tool(self) -> ToolConfDict:
""" LTI Tool object. """
lti_tool = ToolConfDict(self._lti_config)
for iss in self._lti_config:
for client_config in self._lti_config[iss]:
lti_tool.set_private_key(iss, client_config['private_key'], client_id=client_config['client_id'])
lti_tool.set_public_key(iss, client_config['public_key'], client_id=client_config['client_id'])
return lti_tool

def lti_platform_instances_ids(self) -> Iterable[str]:
""" Set of LTI Platform instance ids registered for this course. """
for iss in self._lti_config:
for client_config in self._lti_config[iss]:
for deployment_id in client_config['deployment_ids']:
yield '/'.join([iss, client_config['client_id'], deployment_id])

def lti_keyset_hash(self, issuer: str, client_id: str) -> str:
return hashlib.md5((issuer + client_id).encode('utf-8')).digest().hex()

def lti_url(self):
""" Returns the URL to the external platform the course is hosted on """
Expand Down
Loading
Loading