├── game ├── game │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ ├── static │ │ ├── breakout.css │ │ └── breakout.js │ ├── templates │ │ └── game.html │ ├── settings.py │ └── views.py └── manage.py ├── .gitattributes ├── requirements.txt ├── docker-compose.yml ├── Dockerfile ├── configs ├── public.jwk.json ├── public.key ├── public2.key ├── cert_suite_private.key ├── private.key ├── private2.key └── game.json ├── LICENSE ├── .gitignore └── README.rst /game/game/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | game/game/static/* linguist-vendored 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django==4.1.2 2 | PyLTI1p3==2.0.0 3 | -------------------------------------------------------------------------------- /game/game/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "game.settings") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | game: 4 | restart: always 5 | container_name: pylti1p3-django-example 6 | build: . 7 | stdin_open: true 8 | tty: true 9 | volumes: 10 | - ./configs:/configs 11 | - ./game:/game 12 | working_dir: /game 13 | ports: 14 | - "9001:9001" 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.7-alpine3.16 2 | 3 | RUN apk add --update \ 4 | build-base libffi-dev openssl-dev \ 5 | xmlsec xmlsec-dev \ 6 | && rm -rf /var/cache/apk/* 7 | 8 | ADD requirements.txt /tmp 9 | RUN pip install --upgrade pip 10 | RUN pip install -r /tmp/requirements.txt 11 | 12 | EXPOSE 9001 13 | CMD python manage.py runserver 0.0.0.0:9001 14 | -------------------------------------------------------------------------------- /game/game/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | from .views import login, launch, get_jwks, configure, score, scoreboard 3 | 4 | urlpatterns = [ 5 | re_path(r'^login/$', login, name='game-login'), 6 | re_path(r'^launch/$', launch, name='game-launch'), 7 | re_path(r'^jwks/$', get_jwks, name='game-jwks'), 8 | re_path(r'^configure/(?P[\w-]+)/(?P[\w-]+)/$', configure, name='game-configure'), 9 | re_path(r'^api/score/(?P[\w-]+)/(?P[\w-]+)/(?P[\w-]+)/$', score, 10 | name='game-api-score'), 11 | re_path(r'^api/scoreboard/(?P[\w-]+)/$', scoreboard, name='game-api-scoreboard'), 12 | ] 13 | -------------------------------------------------------------------------------- /configs/public.jwk.json: -------------------------------------------------------------------------------- 1 | { 2 | "e": "AQAB", 3 | "n": "uvEnCaUOy1l9gk3wjW3Pib1dBc5g92-6rhvZZOsN1a77fdOqKsrjWG1lDu8kq2nL-wbAzR3DdEPVw_1WUwtr_Q1d5m-7S4ciXT63pENs1EPwWmeN33O0zkGx8I7vdiOTSVoywEyUZe6UyS-ujLfsRc2ImeLP5OHxpE1yULEDSiMLtSvgzEaMvf2AkVq5EL5nLYDWXZWXUnpiT_f7iK47Mp2iQd4KYYG7YZ7lMMPCMBuhej7SOtZQ2FwaBjvZiXDZ172sQYBCiBAmOR3ofTL6aD2-HUxYztVIPCkhyO84mQ7W4BFsOnKW4WRfEySHXd2hZkFMgcFNXY3dA6de519qlcrL0YYx8ZHpzNt0foEzUsgJd8uJMUVvzPZgExwcyIbv5jWYBg0ILgULo7ve7VXG5lMwasW_ch2zKp7tTILnDJwITMjF71h4fn4dMTun_7MWEtSl_iFiALnIL_4_YY717cr4rmcG1424LyxJGRD9L9WjO8etAbPkiRFJUd5fmfqjHkO6fPxyWsMUAu8bfYdVRH7qN_erfGHmykmVGgH8AfK9GLT_cjN4GHA29bK9jMed6SWdrkygbQmlnsCAHrw0RA-QE0t617h3uTrSEr5vkbLz-KThVEBfH84qsweqcac_unKIZ0e2iRuyVnG4cbq8HUdio8gJ62D3wZ0UvVgr4a0", 4 | "alg": "RS256", 5 | "kid": "NtQYzsKs_TWLQ0p3bLmfM7fOwY0nEBVVH3z3Q-zJ06Y", 6 | "kty": "RSA", 7 | "use": "sig" 8 | } 9 | -------------------------------------------------------------------------------- /configs/public.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuvEnCaUOy1l9gk3wjW3P 3 | ib1dBc5g92+6rhvZZOsN1a77fdOqKsrjWG1lDu8kq2nL+wbAzR3DdEPVw/1WUwtr 4 | /Q1d5m+7S4ciXT63pENs1EPwWmeN33O0zkGx8I7vdiOTSVoywEyUZe6UyS+ujLfs 5 | Rc2ImeLP5OHxpE1yULEDSiMLtSvgzEaMvf2AkVq5EL5nLYDWXZWXUnpiT/f7iK47 6 | Mp2iQd4KYYG7YZ7lMMPCMBuhej7SOtZQ2FwaBjvZiXDZ172sQYBCiBAmOR3ofTL6 7 | aD2+HUxYztVIPCkhyO84mQ7W4BFsOnKW4WRfEySHXd2hZkFMgcFNXY3dA6de519q 8 | lcrL0YYx8ZHpzNt0foEzUsgJd8uJMUVvzPZgExwcyIbv5jWYBg0ILgULo7ve7VXG 9 | 5lMwasW/ch2zKp7tTILnDJwITMjF71h4fn4dMTun/7MWEtSl/iFiALnIL/4/YY71 10 | 7cr4rmcG1424LyxJGRD9L9WjO8etAbPkiRFJUd5fmfqjHkO6fPxyWsMUAu8bfYdV 11 | RH7qN/erfGHmykmVGgH8AfK9GLT/cjN4GHA29bK9jMed6SWdrkygbQmlnsCAHrw0 12 | RA+QE0t617h3uTrSEr5vkbLz+KThVEBfH84qsweqcac/unKIZ0e2iRuyVnG4cbq8 13 | HUdio8gJ62D3wZ0UvVgr4a0CAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /configs/public2.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9m+JmFyK1vzg6XY9D/Ti 3 | URXqhXTUDquxdkQzpeuqQlrUetSMCiUk7FyJXnox1pnccsA+i8gUDRhSIpJdH+p6 4 | HuKs8a2yxTxZ4AjzXwgAzf80HCe3E1UCx4Ed2g09tuwaRqQi43dsuf2sGPairbR4 5 | OfgCSMZkyeRVZdQKzV2Uj2w7rDTSJQtCpc4spD/58biCZgTweXD+F68xZh5FGTnV 6 | qGlPk8BOBbazIWTaMFsKdG0hHS1GtfO+yzoaBlzhvoLZN63GU40/el7iZWq4VTMf 7 | iG337qfcax8cp30LVlWGxRnUuLeGWOYmKPJUDeu8pZKLdybvqbWwOrX35B2JV1/L 8 | AjzcLCD6XOnQ9P9qwyAOgbTogvq4elROKJSWAliiXEv6wyT4oEVe+mlZcqxsbiot 9 | 62ZZ3TKRFs7/25bmKhnfPoPc7d96rCTpF9lK4KrCBJOJLhhAGSsnlaZYiL55Vgl0 10 | TNCY7nk+UHWLFpqcgkjbPPjwaHxZ9PCX0L10Hore3gNxJgiNL+cNh+4Hb95K8ZW8 11 | GFgMVlGLqe7JHHetdic8c2je3IBCe/oGs33fC/aZPBdbOI0Bk8jxg12HI9eL1IHM 12 | NajexSjJ+yody3IcLafi2Fd5os0nDYGLMPpHf7URa7gnPOWTOOpwb5MZ3sSmc/rl 13 | KcDaZ66uMLATvKrJj7Eg1HcCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /game/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "game.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /game/game/static/breakout.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Roboto', sans-serif; 3 | } 4 | #scoreboard { 5 | border: solid 1px #000; 6 | border-left: none; 7 | border-top-right-radius: 20px; 8 | border-bottom-right-radius: 20px; 9 | padding-bottom: 12px; 10 | background: linear-gradient(to bottom, rgb(0, 0, 0), rgb(0, 0, 50) 500px); 11 | color: white; 12 | } 13 | th, td { 14 | color: white; 15 | } 16 | .dl-config { 17 | font-family: Verdana, Geneva, Tahoma, sans-serif; 18 | display: block; 19 | width: 400px; 20 | position: absolute; 21 | } 22 | .dl-config h1 { 23 | text-align: center; 24 | } 25 | .dl-config ul { 26 | list-style: none; 27 | padding: 0; 28 | } 29 | .dl-config ul li a { 30 | display: block; 31 | width: 200px; 32 | height:50px; 33 | text-align: center; 34 | border: black solid 1px; 35 | border-radius: 15px; 36 | margin:auto; 37 | text-decoration: none; 38 | color: black; 39 | margin-top:8px; 40 | font-size: 30px; 41 | line-height: 46px; 42 | } 43 | .dl-config ul li a:hover { 44 | background-color: #dddddd; 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dmitry Viskov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /configs/cert_suite_private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAsW3eobPIj5LsyHcMGckVSSC621uL+0zkeMoWfXfNmvTH+zt5 3 | WOeEIdz+X7fK+F+lO7ic5WdJEGmp9/cjAf0Z6SsmnvvHlHV/xsWtJm4DiuuF2MAa 4 | hRQ5QEkhaEdh5QM2vAYyc8Nfxe504vA3czuynrW9MsOdZHeVzF+zWhhEl+olC5fW 5 | A1rhTUPpdxuZ0opVIrGJtI/QYfndoN+7zTs/4CXqG6WpB+AZio8j7c6fJLC7J33c 6 | pxB1+O+64Qbh+5sxz46cEByboAB8qerYCmcfxxfBbwyySBBK5X77aNHWA01B1kpO 7 | Q2VB8YKQk+OrXsPgJobPkR9ONWa9DC9JjEdUJwIDAQABAoIBAQCAA+qutt2NIY/v 8 | 71zuudO+yHupSzsLXOY3dG+XpTnWhKhJTxb1m00Ndbqe6yfp3nCET2X8anIgAmzc 9 | +RXsGGZ6gmTCLp1IMyK3EuckJBowQFB5G9nGjNnl1R3idCZgqtnx/XKnbZ6LW8o/ 10 | 9tu7K6ZrtmrE1riXxWRyadYoufu7ssNTqtj03oh3Tvw+Ze6xvF6hpaxnbVHxJcGt 11 | xZO51L6rGOSFq5CJ81BswyBDOKB/Z2OC0o3m2t4ZF4/2Lf070sB7RoejGD7mhYVe 12 | lEOoC95C14hfcspzmDEb8I/n0MvAxlwddM4KZRilAJ+e2R0rM9M1MnyYsmYUsMNX 13 | EKWcx+/5AoGBAOLtNVbIohpY5kbX4WREJ/0INPbbx0gf68ozEZTjsOzIP7oaIzry 14 | URmxyZzSpx446QCO8s26vuxrPGm7OAteNS7UpDdunzKsaIlZScZQEpE9htp3MKKw 15 | KXaA4l7H55uWWnaUAcDqjEdybhYL6SbPKhOaK53VeHOLro900FiRnfaDAoGBAMgp 16 | O8GwAI3LbD06Fn+DT+3hj/i8wxbWilgJlI+RU+wWfQ421jMKv2dck8zbnzKGxEwA 17 | 3WPh6gGMlkavEZ95d0qZ/TOkSh+VIjJuOrjcckRcrKcycYJJUzreO7ENsFbA+8xL 18 | Qp2gNV+NntiChzSUGY5Nup3keoaT9iV13oYDSdqNAoGARDn9Z3I7CqDf2zzcz0CO 19 | pUzqX64EZHL0eX6RMqqibw5l2pYxMW/ZYlhJvZS4GiYSJ9DSv3f+Hya+qytW1lQk 20 | uUfFd8USqDGd3G2z+KPqcTCGcviS7tb4IGDvrn976xNxb2VggZgDRRfqcUZzeu+e 21 | PvaDVpjv9g1xFkCQw5BEZfECgYBcSB5jywhGV14c0FYlDd5g9xiQfj6XnewEcM5M 22 | bp05gJjBX+jbeX4LYnRGA49fFSEVRWTMsxBXDIEQL5C5bJ/iBiLllz4RV4l/pLBw 23 | IDqSaAO1xhztC29S+bidhYkiRjEQ3DXnREC3QCzW9z7sr8ckg5OhTgBrYXYfiTtB 24 | n+yB1QKBgG/J+WhkqMEtZ8CgdoiTIqYKmFsLvl07wETAVU6Nv1sEI+jnhyug0QtQ 25 | yLAlBOVyrXuJ1DZMX6hTRij4L0jvnJFSq0Sv8COuLIH90xdq/NTNQ3LAy60l/3b1 26 | ojAnnRJORDegdJjCBxJ59Fch6Qfd+e8742DVsJu8zVo2garUVMH3 27 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /game/game/templates/game.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | {{ page_title }} 7 | 8 | 9 | 15 | 16 | 17 | 18 | {% if is_deep_link_launch %} 19 |

Django example (Deep Link launch)

20 |
21 |

Pick a Difficulty

22 | 27 |
28 | {% else %} 29 |

Django example (difficulty: {{ curr_diff }})

30 |
31 |
32 |
33 |

Scoreboard

34 | 35 |
36 |
37 | Refresh 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | {% endif %} 47 | 48 | 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | venv3/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # Pycharm 133 | .idea 134 | 135 | # library symlink (for development) 136 | pylti1p3 137 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Example of usage PyLTI1p3 library within Django framework 2 | ========================================================== 3 | 4 | `PyLTI1p3`_ is a Python implementation of LTI 1.3 Advantage Tool. 5 | 6 | .. _PyLTI1p3: https://github.com/dmitry-viskov/pylti1.3 7 | 8 | First of all choose and configure test LTI 1.3 Platform. It may be: 9 | 10 | * `IMS Global test site`_ 11 | * `Canvas LMS`_ (Detailed `instruction`_ how to setup Canvas as LTI 1.3 Platform is `here`_) 12 | * `Blackboard Learn`_ (`Documentation`_) 13 | 14 | .. _IMS Global test site: https://lti-ri.imsglobal.org 15 | .. _Canvas LMS: https://github.com/instructure/canvas-lms 16 | .. _instruction: https://github.com/dmitry-viskov/pylti1.3/wiki/Configure-Canvas-as-LTI-1.3-Platform 17 | .. _here: https://github.com/dmitry-viskov/pylti1.3/wiki/Configure-Canvas-as-LTI-1.3-Platform 18 | .. _Blackboard Learn: https://github.com/blackboard 19 | .. _Documentation: https://docs.blackboard.com/lti/tutorials/py-lti-1p3 20 | 21 | The most simple way to check example is to use ``docker`` + ``docker-compose``. 22 | Change the necessary configs in the ``configs/game.json`` (`here is instruction`_ how to generate your own public + private keys): 23 | 24 | .. _here is instruction: https://github.com/dmitry-viskov/pylti1.3/wiki/How-to-generate-JWT-RS256-key-and-JWKS 25 | 26 | .. code-block:: javascript 27 | 28 | { 29 | "" : [{ // This will usually look something like 'http://example.com' 30 | "default": true, // this block will be used in case if client-id was not passed 31 | "client_id" : "", // This is the id received in the 'aud' during a launch 32 | "auth_login_url" : "", // The platform's OIDC login endpoint 33 | "auth_token_url" : "", // The platform's service authorization endpoint 34 | "auth_audience": null, // The platform's OAuth2 Audience (aud). Is used to get platform's access token, 35 | // Usually the same as "auth_token_url" but in the common case could be a different url 36 | "key_set_url" : "", // The platform's JWKS endpoint 37 | "key_set": null, // in case if platform's JWKS endpoint somehow unavailable you may paste JWKS here 38 | "private_key_file" : "", // Relative path to the tool's private key 39 | "public_key_file": "", // Relative path to the tool's public key 40 | "deployment_ids" : [""] // The deployment_id passed by the platform during launch 41 | }, { 42 | "default": false, 43 | "client_id" : "", 44 | ... 45 | }] 46 | } 47 | 48 | and execute: 49 | 50 | .. code-block:: shell 51 | 52 | $ docker-compose up --build 53 | 54 | You may use virtualenv instead of docker: 55 | 56 | .. code-block:: shell 57 | 58 | $ virtualenv venv 59 | $ source venv/bin/activate 60 | $ pip install -r requirements.txt 61 | $ cd game 62 | $ python manage.py runserver 127.0.0.1:9001 63 | 64 | Now there is game example tool you can launch into on the port 9001: 65 | 66 | .. code-block:: shell 67 | 68 | OIDC Login URL: http://127.0.0.1:9001/login/ 69 | LTI Launch URL: http://127.0.0.1:9001/launch/ 70 | JWKS URL: http://127.0.0.1:9001/jwks/ 71 | -------------------------------------------------------------------------------- /configs/private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /configs/private2.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKQIBAAKCAgEA9m+JmFyK1vzg6XY9D/TiURXqhXTUDquxdkQzpeuqQlrUetSM 3 | CiUk7FyJXnox1pnccsA+i8gUDRhSIpJdH+p6HuKs8a2yxTxZ4AjzXwgAzf80HCe3 4 | E1UCx4Ed2g09tuwaRqQi43dsuf2sGPairbR4OfgCSMZkyeRVZdQKzV2Uj2w7rDTS 5 | JQtCpc4spD/58biCZgTweXD+F68xZh5FGTnVqGlPk8BOBbazIWTaMFsKdG0hHS1G 6 | tfO+yzoaBlzhvoLZN63GU40/el7iZWq4VTMfiG337qfcax8cp30LVlWGxRnUuLeG 7 | WOYmKPJUDeu8pZKLdybvqbWwOrX35B2JV1/LAjzcLCD6XOnQ9P9qwyAOgbTogvq4 8 | elROKJSWAliiXEv6wyT4oEVe+mlZcqxsbiot62ZZ3TKRFs7/25bmKhnfPoPc7d96 9 | rCTpF9lK4KrCBJOJLhhAGSsnlaZYiL55Vgl0TNCY7nk+UHWLFpqcgkjbPPjwaHxZ 10 | 9PCX0L10Hore3gNxJgiNL+cNh+4Hb95K8ZW8GFgMVlGLqe7JHHetdic8c2je3IBC 11 | e/oGs33fC/aZPBdbOI0Bk8jxg12HI9eL1IHMNajexSjJ+yody3IcLafi2Fd5os0n 12 | DYGLMPpHf7URa7gnPOWTOOpwb5MZ3sSmc/rlKcDaZ66uMLATvKrJj7Eg1HcCAwEA 13 | AQKCAgAGxs9qUf/RZKJ3WQnZ0qVB5I/j5pq0N+geipvppUmJHz/2Nrpjxwdfu+Bd 14 | KZANni0v4fXLreSl7YlzVkIFpX2BNEDcOSdlgWuD8SBmzcIJPXTR4g0IlORe1xVF 15 | O0BQs5JkvJERtJAtVyVF3yEJv3Y05eCDiXfDuNSDrdPe4i1lJO1JJtSxMcDol/r7 16 | SJ/CFA2l6jSF/7jvrst1dZdMgnN2ePA4GGgs2QwjpXHTNZWxLj+5soEzcgXznF/o 17 | f+Jcn52DM0iftePNJNHQnHbVhPKKM7fx0fTfjztK/HHKMyThw6bpWMadfYqxbTnU 18 | KNYxrrjIvdiGUAQ4wRXq1HMZOihLaXpLQVZAPpFjUPVxHM/+GRv6j5terjpmtxoK 19 | pUhCr0LHUjeuMlsFO1KgJe2Qym5Q2ajwSpwLnYY5k62Q/TP0FSvjuNU0vOfABvV2 20 | weuS0wN54d6bmd1GZOyJCyJ9V9JbIpTQ3XDUuX+0oj4ahkGWg2aYwZHLryjCVE+h 21 | mpgw6HNexlAHUf5nzTp/QAw1Hyn50qYV2nsJ4gM/6e69crB+P8aKxZ9wuIcTSfpU 22 | MMNxTYItZvo0WKT2MHFHbsfQ/xzdNGSZ/JjeCPRSyi0N5hqpHQq56QuBSKKfjKK5 23 | W52MVzy/yw0RHZ52avt3aBBwSz0iNDzwxX5fHShjexFaZpU2NQKCAQEA+gyEehug 24 | 6a8XV93UrEijfUkK5oYeqEunqkPUQqaGdYBY8QzUwf061noip2183HQQvKQVJUHl 25 | jNSwYrz0mBBBSyduxK8lNmdc7F7RDyzGQkkvY2+3t4RMvPQC9tMW6NUSAKr6YBey 26 | 0bmr5KlXQpX0glFzzjzGBaNSuU51gRLFH4wjbv8LSnBbNnLDuw9FXPe0r2ihao// 27 | CQXHe++QT+qmb8h6MyFB/0J1PspAg2NRAeNb6nH5LhAJXImNCBt8upGwF5oOKGy0 28 | jJ7kptMy/xgcrNMbcIjQaYaTFdMryWnPwyxyxMHdw89YWJA8fXvVzd2/s6tv2MOw 29 | 5KnHGYdAAhXqrQKCAQEA/E0BdPxLDFRz2R0IRPnM638IlIayGJ64ewjkz/1O0KjI 30 | bkGtWIvUmI0LnT6S+2p0deQxF49qPBtW3+d8kMF6NXgrT2lcEFEQRMImF4vzoicg 31 | iVL0H6HT21z/nKyYPnjJjxcPsNVvBiCdgFzjJI8WdoK2cCYMkkypDDnX+lGyMNK5 32 | fQ34YiC4MUCm8kdeLs9IIWVkqFqTwnHIBNvK+tEoiMErkIDBJVcCFIUoMkY4mnp0 33 | g12UYE/ZsGGKNOYooJY31DO7r0AKt/YGL/90/YRc9e/PcB1mGmAGMTfKjl00Sj61 34 | m0Mdi5fH+M7aMck8/NYnfrufA/3HepwtqQFTyeXkMwKCAQAahklnSpb/Mvue7oEo 35 | 5WuyVpU5bvDBmYTnotpZV6DbxgmpSFspWNts9PfIGu0r1YQQ/rbfhOX80nMDhlfL 36 | 6a2Dc2Nkqc+gvcY1rLkwiuddELZeLfOnG/mn8Zp+5FWBzVhjib+Vge4OIUwCrZP8 37 | FDwjttA6CGhZIMIdthzw3DTc69i2ZYelFdYXKIVqymvpOL2J/edhjnTXWC2ZEAiW 38 | 8aVGJlYQJm8BzOLzDjFZvqdRM/UEIaL1J+5WSqETQxcwE1RCrKzjzOQ6JoDK0YyP 39 | Utd33c0DWwAAsa3YmtYCP58ybPyfspD7vHr6qhJAnWpItEUpof3zWI7jMr9UDezU 40 | S87hAoIBAQCmiUHr8VisU9aAOmyp8uVzkM+eEmbeX25QMCewrnhmvPJH7Ow6JVp4 41 | M6m16obkk3k6FBzfe1fZQwaOFuOfPUaooqCb82TElG2TpT+1jTiNERyl6G6hrpUE 42 | GBfVWAvKOLp6y/Mce1WkisTL5QQ9roFaSp+X2VW1AAsZudi5L1Habk3noOASDZzn 43 | TdCk3bzqUwI+oQnXIqqjz60Cn/4UnxkNY8yYvpQ/THZgCyDkRnu4ZBoiWBPHmw2L 44 | Imcy0bWBgoZpeJhrbm9kzG17Izka2lLuN5QKYi1yPW348OWwIQ+R7mC7koqfCNoD 45 | fq6B7F5oWgeJ3NhyrwIMCakOBIVFII0HAoIBAQCkYUxHa9ORoRGqIUKJ7Dv7jrCC 46 | 2h0rXm+vDZy7TfI8j0M4GqUG6fJRskI1m3OPDv7a+U7hDtDibXNfn9MUecJ8Cfzy 47 | zeyV1cORs3Xmxd33iTq0NvPMy6ViNSYhX1klElHcpMeiceiTIgLDqNeeCjIs8x7v 48 | Dnot/HO2AfelhXrDMDTOvRn/LhQ1Lek48rJgnSB+RWH7nt0pm5nigoFvDPW72IBI 49 | iPzxPfV/wQTEVG8WOlagAkTzRcgwWCWefNJxW72fyp2ztWiayEVOeo5ZK+E8ntxm 50 | gAR08Vmdne4GC7TvHqkohTvAKLnfvKDNsySC3w5yGw8liCAL0QK3sDxzI7VG 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /game/game/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | 6 | 7 | # Quick-start development settings - unsuitable for production 8 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 9 | 10 | # SECURITY WARNING: keep the secret key used in production secret! 11 | SECRET_KEY = 'mn*=d18zwp+n%)+in=7!zp4x)he+%y6+0$e3&!c&keg%&q!%z&' 12 | 13 | # SECURITY WARNING: don't run with debug turned on in production! 14 | DEBUG = True 15 | 16 | ALLOWED_HOSTS = ['*'] 17 | 18 | INTERNAL_IPS = ['127.0.0.1'] 19 | 20 | # Application definition 21 | 22 | INSTALLED_APPS = [ 23 | 'django.contrib.auth', 24 | 'django.contrib.contenttypes', 25 | 'django.contrib.sessions', 26 | 'django.contrib.messages', 27 | 'django.contrib.staticfiles', 28 | 'game', 29 | ] 30 | 31 | MIDDLEWARE = [ 32 | 'django.middleware.security.SecurityMiddleware', 33 | 'django.contrib.sessions.middleware.SessionMiddleware', 34 | 'django.middleware.common.CommonMiddleware', 35 | ] 36 | 37 | ROOT_URLCONF = 'game.urls' 38 | 39 | TEMPLATES = [ 40 | { 41 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 42 | 'DIRS': [ 43 | os.path.join(BASE_DIR, 'templates'), 44 | ], 45 | 'APP_DIRS': True, 46 | 'OPTIONS': { 47 | 'context_processors': [ 48 | 'django.template.context_processors.debug', 49 | 'django.template.context_processors.request', 50 | 'django.contrib.auth.context_processors.auth', 51 | 'django.contrib.messages.context_processors.messages', 52 | ], 53 | }, 54 | }, 55 | ] 56 | 57 | WSGI_APPLICATION = 'game.wsgi.application' 58 | 59 | SESSION_COOKIE_NAME = 'pylti1p3-django-app-sessionid' 60 | SESSION_COOKIE_SAMESITE = None 61 | CSRF_COOKIE_SAMESITE = None 62 | SESSION_COOKIE_SECURE = False # should be True in case of HTTPS usage (production) 63 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 64 | 65 | # Database 66 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 67 | 68 | DATABASES = {} 69 | 70 | CACHES = { 71 | 'default': { 72 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 73 | } 74 | } 75 | 76 | SESSION_ENGINE = "django.contrib.sessions.backends.cache" 77 | 78 | # Password validation 79 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 80 | 81 | AUTH_PASSWORD_VALIDATORS = [ 82 | { 83 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 84 | }, 85 | { 86 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 87 | }, 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 90 | }, 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 93 | }, 94 | ] 95 | 96 | 97 | # Internationalization 98 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 99 | 100 | LANGUAGE_CODE = 'en-us' 101 | 102 | TIME_ZONE = 'UTC' 103 | 104 | USE_I18N = True 105 | 106 | USE_L10N = True 107 | 108 | USE_TZ = True 109 | 110 | 111 | # Static files (CSS, JavaScript, Images) 112 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 113 | 114 | STATIC_URL = '/static/' 115 | -------------------------------------------------------------------------------- /configs/game.json: -------------------------------------------------------------------------------- 1 | { 2 | "http://imsglobal.org": [{ 3 | "default": true, 4 | "client_id": "pytest12345", 5 | "auth_login_url": "https://lti-ri.imsglobal.org/platforms/370/authorizations/new", 6 | "auth_token_url": "https://lti-ri.imsglobal.org/platforms/370/access_tokens", 7 | "key_set_url": "https://lti-ri.imsglobal.org/platforms/370/platform_keys/361.json", 8 | "key_set": { 9 | "keys": [ 10 | { 11 | "kty": "RSA", 12 | "e": "AQAB", 13 | "n": "r3WB5ECKptJliYft6F_XJysCy1KevoGJgKNHgdVR20lplUv1SzRH1mifzOmEzxWM0kj6blS7SRxK9GFGs6optHAmzcb6_joegKzLHSj14RRVSoI0MgyltJcAl8z6d4yZ9KobV8OvpICnMgsGO20Wih-Cq-oSUjtJT7WET3GZmzmM9MzamiGsCtC0dUWdDOW1FOMzTt8et9YA5jOfkLdJdPyZ5mdUZjBkYMlDGoD8fPRPdS9M-uczxvUeuKvyy1BVGlu5AG0xy-wN1tKjSE1iuC5Kkm39CZwQXBRpStDExWw_ApzP40SK3CKez4ls3jjkE3i4CpJSgLn1D8rT6wOpJw", 14 | "kid": "uhMfBQzVLmaJNU9c1am2X9pTzcEYhgYL2hO6hbYAvdw", 15 | "alg": "RS256", 16 | "use": "sig" 17 | } 18 | ] 19 | }, 20 | "private_key_file": "private.key", 21 | "public_key_file": "public.key", 22 | "deployment_ids": ["py1234"] 23 | }], 24 | "https://canvas.instructure.com": [{ 25 | "default": true, 26 | "client_id": "10000000000003", 27 | "auth_login_url": "http://canvas.test/api/lti/authorize_redirect", 28 | "auth_token_url": "http://canvas.test/login/oauth2/token", 29 | "key_set_url": "http://canvas.test/api/lti/security/jwks", 30 | "key_set": null, 31 | "private_key_file": "private.key", 32 | "public_key_file": "public.key", 33 | "deployment_ids": ["2:4dde05e8ca1973bcca9bffc13e1548820eee93a3"] 34 | }, { 35 | "client_id": "10000000000004", 36 | "auth_login_url": "http://canvas.test/api/lti/authorize_redirect", 37 | "auth_token_url": "http://canvas.test/login/oauth2/token", 38 | "key_set_url": "http://canvas.test/api/lti/security/jwks", 39 | "key_set": null, 40 | "private_key_file": "private.key", 41 | "public_key_file": "public.key", 42 | "deployment_ids": ["3:4dde05e8ca1973bcca9bffc13e1548820eee93a3"] 43 | }], 44 | "ltiadvantagevalidator.imsglobal.org": [{ 45 | "default": true, 46 | "client_id": "imstestuser", 47 | "auth_login_url": "https://ltiadvantagevalidator.imsglobal.org/ltitool/oidcauthurl.html", 48 | "auth_token_url": "https://oauth2server.imsglobal.org/oauth2server/authcodejwt", 49 | "key_set_url": "https://oauth2server.imsglobal.org/jwks", 50 | "key_set": null, 51 | "private_key_file": "cert_suite_private.key", 52 | "public_key_file": null, 53 | "deployment_ids": ["testdeploy"] 54 | }], 55 | "https://blackboard.com": [{ 56 | "default": true, 57 | "client_id": "client-id", 58 | "auth_login_url": "https://developer.blackboard.com/api/v1/gateway/oidcauth", 59 | "auth_token_url": "https://developer.blackboard.com/api/v1/gateway/oauth2/jwttoken", 60 | "key_set_url": "https://developer.blackboard.com/api/v1/management/applications//jwks.json", 61 | "key_set": null, 62 | "private_key_file": "private.key", 63 | "public_key_file": "public.key", 64 | "deployment_ids": ["deployment-id"] 65 | }], 66 | "https://partners.brightspace.com": [{ 67 | "default": true, 68 | "client_id": "client-id", 69 | "auth_login_url": "https://partners.brightspace.com/d2l/lti/authenticate", 70 | "auth_token_url": "https://auth.brightspace.com/core/connect/token", 71 | "auth_audience": "https://api.brightspace.com/auth/token", 72 | "key_set_url": "https://partners.brightspace.com/d2l/.well-known/jwks", 73 | "key_set": null, 74 | "private_key_file": "private2.key", 75 | "public_key_file": "public2.key", 76 | "deployment_ids": ["deployment-id"] 77 | }], 78 | "http://moodle.test": [{ 79 | "default": true, 80 | "client_id": "LyRl2z2Ai4Vxgok", 81 | "auth_login_url": "http://moodle.test/mod/lti/auth.php", 82 | "auth_token_url": "http://moodle.test/mod/lti/token.php", 83 | "auth_audience": null, 84 | "key_set_url": "http://moodle.test/mod/lti/certs.php", 85 | "key_set": null, 86 | "private_key_file": "private.key", 87 | "public_key_file": "public.key", 88 | "deployment_ids": ["1"] 89 | }] 90 | } 91 | -------------------------------------------------------------------------------- /game/game/views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import pprint 4 | 5 | from django.conf import settings 6 | from django.http import HttpResponse, HttpResponseForbidden, JsonResponse 7 | from django.shortcuts import render 8 | from django.views.decorators.http import require_POST 9 | from django.urls import reverse 10 | from pylti1p3.contrib.django import DjangoOIDCLogin, DjangoMessageLaunch, DjangoCacheDataStorage 11 | from pylti1p3.deep_link_resource import DeepLinkResource 12 | from pylti1p3.grade import Grade 13 | from pylti1p3.lineitem import LineItem 14 | from pylti1p3.tool_config import ToolConfJsonFile 15 | from pylti1p3.registration import Registration 16 | 17 | 18 | PAGE_TITLE = 'Game Example' 19 | 20 | 21 | class ExtendedDjangoMessageLaunch(DjangoMessageLaunch): 22 | 23 | def validate_nonce(self): 24 | """ 25 | Probably it is bug on "https://lti-ri.imsglobal.org": 26 | site passes invalid "nonce" value during deep links launch. 27 | Because of this in case of iss == http://imsglobal.org just skip nonce validation. 28 | 29 | """ 30 | iss = self.get_iss() 31 | deep_link_launch = self.is_deep_link_launch() 32 | if iss == "http://imsglobal.org" and deep_link_launch: 33 | return self 34 | return super().validate_nonce() 35 | 36 | 37 | def get_lti_config_path(): 38 | return os.path.join(settings.BASE_DIR, '..', 'configs', 'game.json') 39 | 40 | 41 | def get_tool_conf(): 42 | tool_conf = ToolConfJsonFile(get_lti_config_path()) 43 | return tool_conf 44 | 45 | 46 | def get_jwk_from_public_key(key_name): 47 | key_path = os.path.join(settings.BASE_DIR, '..', 'configs', key_name) 48 | f = open(key_path, 'r') 49 | key_content = f.read() 50 | jwk = Registration.get_jwk(key_content) 51 | f.close() 52 | return jwk 53 | 54 | 55 | def get_launch_data_storage(): 56 | return DjangoCacheDataStorage() 57 | 58 | 59 | def get_launch_url(request): 60 | target_link_uri = request.POST.get('target_link_uri', request.GET.get('target_link_uri')) 61 | if not target_link_uri: 62 | raise Exception('Missing "target_link_uri" param') 63 | return target_link_uri 64 | 65 | 66 | def login(request): 67 | tool_conf = get_tool_conf() 68 | launch_data_storage = get_launch_data_storage() 69 | 70 | oidc_login = DjangoOIDCLogin(request, tool_conf, launch_data_storage=launch_data_storage) 71 | target_link_uri = get_launch_url(request) 72 | return oidc_login\ 73 | .enable_check_cookies()\ 74 | .redirect(target_link_uri) 75 | 76 | 77 | @require_POST 78 | def launch(request): 79 | tool_conf = get_tool_conf() 80 | launch_data_storage = get_launch_data_storage() 81 | message_launch = ExtendedDjangoMessageLaunch(request, tool_conf, launch_data_storage=launch_data_storage) 82 | message_launch_data = message_launch.get_launch_data() 83 | pprint.pprint(message_launch_data) 84 | 85 | difficulty = message_launch_data.get('https://purl.imsglobal.org/spec/lti/claim/custom', {})\ 86 | .get('difficulty', None) 87 | if not difficulty: 88 | difficulty = request.GET.get('difficulty', 'normal') 89 | 90 | return render(request, 'game.html', { 91 | 'page_title': PAGE_TITLE, 92 | 'is_deep_link_launch': message_launch.is_deep_link_launch(), 93 | 'launch_data': message_launch.get_launch_data(), 94 | 'launch_id': message_launch.get_launch_id(), 95 | 'curr_user_name': message_launch_data.get('name', ''), 96 | 'curr_diff': difficulty 97 | }) 98 | 99 | 100 | def get_jwks(request): 101 | tool_conf = get_tool_conf() 102 | return JsonResponse(tool_conf.get_jwks(), safe=False) 103 | 104 | 105 | def configure(request, launch_id, difficulty): 106 | tool_conf = get_tool_conf() 107 | launch_data_storage = get_launch_data_storage() 108 | message_launch = ExtendedDjangoMessageLaunch.from_cache(launch_id, request, tool_conf, 109 | launch_data_storage=launch_data_storage) 110 | 111 | if not message_launch.is_deep_link_launch(): 112 | return HttpResponseForbidden('Must be a deep link!') 113 | 114 | launch_url = request.build_absolute_uri(reverse('game-launch') + '?difficulty=' + difficulty) 115 | 116 | resource = DeepLinkResource() 117 | resource.set_url(launch_url)\ 118 | .set_custom_params({'difficulty': difficulty})\ 119 | .set_title('Breakout ' + difficulty + ' mode!') 120 | 121 | html = message_launch.get_deep_link().output_response_form([resource]) 122 | return HttpResponse(html) 123 | 124 | 125 | @require_POST 126 | def score(request, launch_id, earned_score, time_spent): 127 | tool_conf = get_tool_conf() 128 | launch_data_storage = get_launch_data_storage() 129 | message_launch = ExtendedDjangoMessageLaunch.from_cache(launch_id, request, tool_conf, 130 | launch_data_storage=launch_data_storage) 131 | resource_link_id = message_launch.get_launch_data() \ 132 | .get('https://purl.imsglobal.org/spec/lti/claim/resource_link', {}).get('id') 133 | 134 | if not message_launch.has_ags(): 135 | return HttpResponseForbidden("Don't have grades!") 136 | 137 | sub = message_launch.get_launch_data().get('sub') 138 | timestamp = datetime.datetime.utcnow().isoformat() + 'Z' 139 | earned_score = int(earned_score) 140 | time_spent = int(time_spent) 141 | 142 | ags = message_launch.get_ags() 143 | 144 | if ags.can_create_lineitem(): 145 | sc = Grade() 146 | sc.set_score_given(earned_score)\ 147 | .set_score_maximum(100)\ 148 | .set_timestamp(timestamp)\ 149 | .set_activity_progress('Completed')\ 150 | .set_grading_progress('FullyGraded')\ 151 | .set_user_id(sub) 152 | 153 | sc_line_item = LineItem() 154 | sc_line_item.set_tag('score')\ 155 | .set_score_maximum(100)\ 156 | .set_label('Score') 157 | if resource_link_id: 158 | sc_line_item.set_resource_id(resource_link_id) 159 | 160 | ags.put_grade(sc, sc_line_item) 161 | 162 | tm = Grade() 163 | tm.set_score_given(time_spent)\ 164 | .set_score_maximum(999)\ 165 | .set_timestamp(timestamp)\ 166 | .set_activity_progress('Completed')\ 167 | .set_grading_progress('FullyGraded')\ 168 | .set_user_id(sub) 169 | 170 | tm_line_item = LineItem() 171 | tm_line_item.set_tag('time')\ 172 | .set_score_maximum(999)\ 173 | .set_label('Time Taken') 174 | if resource_link_id: 175 | tm_line_item.set_resource_id(resource_link_id) 176 | 177 | result = ags.put_grade(tm, tm_line_item) 178 | else: 179 | sc = Grade() 180 | sc.set_score_given(earned_score) \ 181 | .set_score_maximum(100) \ 182 | .set_timestamp(timestamp) \ 183 | .set_activity_progress('Completed') \ 184 | .set_grading_progress('FullyGraded') \ 185 | .set_user_id(sub) 186 | result = ags.put_grade(sc) 187 | 188 | return JsonResponse({'success': True, 'result': result.get('body')}) 189 | 190 | 191 | def scoreboard(request, launch_id): 192 | tool_conf = get_tool_conf() 193 | launch_data_storage = get_launch_data_storage() 194 | message_launch = ExtendedDjangoMessageLaunch.from_cache(launch_id, request, tool_conf, 195 | launch_data_storage=launch_data_storage) 196 | resource_link_id = message_launch.get_launch_data() \ 197 | .get('https://purl.imsglobal.org/spec/lti/claim/resource_link', {}).get('id') 198 | 199 | if not message_launch.has_nrps(): 200 | return HttpResponseForbidden("Don't have names and roles!") 201 | 202 | if not message_launch.has_ags(): 203 | return HttpResponseForbidden("Don't have grades!") 204 | 205 | ags = message_launch.get_ags() 206 | 207 | if ags.can_create_lineitem(): 208 | score_line_item = LineItem() 209 | score_line_item.set_tag('score') \ 210 | .set_score_maximum(100) \ 211 | .set_label('Score') 212 | if resource_link_id: 213 | score_line_item.set_resource_id(resource_link_id) 214 | 215 | score_line_item = ags.find_or_create_lineitem(score_line_item) 216 | scores = ags.get_grades(score_line_item) 217 | 218 | time_line_item = LineItem() 219 | time_line_item.set_tag('time') \ 220 | .set_score_maximum(999) \ 221 | .set_label('Time Taken') 222 | if resource_link_id: 223 | time_line_item.set_resource_id(resource_link_id) 224 | 225 | time_line_item = ags.find_or_create_lineitem(time_line_item) 226 | times = ags.get_grades(time_line_item) 227 | else: 228 | scores = ags.get_grades() 229 | times = None 230 | 231 | members = message_launch.get_nrps().get_members() 232 | scoreboard_result = [] 233 | 234 | for sc in scores: 235 | result = {'score': sc['resultScore']} 236 | if times is None: 237 | result['time'] = 'Not set' 238 | else: 239 | for tm in times: 240 | if tm['userId'] == sc['userId']: 241 | result['time'] = tm['resultScore'] 242 | break 243 | for member in members: 244 | if member['user_id'] == sc['userId']: 245 | result['name'] = member.get('name', 'Unknown') 246 | break 247 | scoreboard_result.append(result) 248 | 249 | return JsonResponse(scoreboard_result, safe=False) 250 | -------------------------------------------------------------------------------- /game/game/static/breakout.js: -------------------------------------------------------------------------------- 1 | function mainGame() { 2 | var c = document.getElementById("breakout"); 3 | var ctx = c.getContext("2d"); 4 | 5 | var bgctx = document.getElementById("breakoutbg").getContext("2d"); 6 | bgctx.beginPath(); 7 | var bggrad = ctx.createLinearGradient(0, 0, 0, c.height); 8 | bggrad.addColorStop(0, "rgb(0, 0, 0)"); 9 | bggrad.addColorStop(1, "rgb(0, 0, 50)"); 10 | bgctx.fillStyle = bggrad; 11 | bgctx.rect(0, 0, c.width, c.height); 12 | bgctx.fill(); 13 | bgctx.font = "10px Gugi"; 14 | bgctx.fillStyle = '#FFFFFF'; 15 | bgctx.textAlign = "left"; 16 | bgctx.fillText("Powered By Turnitin", 6, c.height - 6); 17 | 18 | var score = 0; 19 | 20 | var difficulty = { 21 | hard: { 22 | speed_multiplier: 1.5 23 | }, 24 | normal: { 25 | speed_multiplier: 1 26 | }, 27 | easy: { 28 | speed_multiplier: 0.7 29 | }, 30 | }; 31 | 32 | var ball = { 33 | pos: { 34 | x: c.width / 2 - 200, 35 | y: c.height / 2 - 2, 36 | }, 37 | vel: { 38 | x: 6 * difficulty[currDiff]['speed_multiplier'], 39 | y: 6 * difficulty[currDiff]['speed_multiplier'], 40 | }, 41 | r: 10, 42 | rot: 0, 43 | velr: 0, 44 | render: function () { 45 | this.pos.x += this.vel.x; 46 | this.pos.y += this.vel.y; 47 | if (this.oob(c.width, this.pos.x, this.r)) { 48 | this.vel.x = -this.vel.x; 49 | this.pos.x += this.vel.x; 50 | this.pos.y -= this.vel.y; 51 | } 52 | if (this.oob(c.height, this.pos.y, this.r)) { 53 | if (this.pos.y > c.height - this.r) { 54 | endGame(); 55 | } 56 | this.vel.y = -this.vel.y; 57 | this.pos.y += this.vel.y; 58 | this.pos.x -= this.vel.x; 59 | } 60 | 61 | ctx.save(); 62 | ctx.beginPath(); 63 | var gradient = ctx.createRadialGradient(this.pos.x, this.pos.y, 2, this.pos.x, this.pos.y, 10); 64 | ctx.fillStyle = "rgb(255, 232, 102)"; 65 | ctx.strokeStyle = "rgb(255, 232, 102)"; 66 | ctx.setLineDash([5, 5]); 67 | ctx.lineWidth = 4; 68 | ctx.translate(this.pos.x, this.pos.y); 69 | ctx.rotate(this.rot * Math.PI); 70 | ctx.arc(0, 0, this.r, 0, 2 * Math.PI); 71 | if (this.vel.x > 0) { 72 | this.velr = 0.01; 73 | } else if (this.vel.x < 0) { 74 | this.velr = -0.01; 75 | } else { 76 | this.velr = 0; 77 | } 78 | this.rot += this.velr; 79 | ctx.fill(); 80 | ctx.stroke(); 81 | ctx.restore(); 82 | 83 | }, 84 | oob: function (max, curr, offset) { 85 | if (curr < offset || curr > (max - offset)) { 86 | return true; 87 | } 88 | }, 89 | left: function () { 90 | return this.pos.x - this.r; 91 | }, 92 | right: function () { 93 | return this.pos.x + this.r; 94 | }, 95 | top: function () { 96 | return this.pos.y - this.r; 97 | }, 98 | bottom: function () { 99 | return this.pos.y + this.r; 100 | }, 101 | }; 102 | 103 | var paddle = { 104 | pos: { 105 | x: c.width / 2 + 2, 106 | y: c.height - 40, 107 | }, 108 | width: 80, 109 | height: 20, 110 | render: function () { 111 | ctx.beginPath(); 112 | var gradient = ctx.createLinearGradient(this.pos.x, this.pos.y, this.pos.x, this.pos.y + this.height); 113 | gradient.addColorStop(0, "#999999"); 114 | gradient.addColorStop(0.7, "#eeeeee"); 115 | gradient.addColorStop(1, "#999999"); 116 | ctx.fillStyle = gradient; 117 | var hh = this.height / 2; 118 | ctx.arc(this.pos.x + hh, this.pos.y + hh, hh, 0.5 * Math.PI, 1.5 * Math.PI); 119 | ctx.rect(this.pos.x + hh, this.pos.y, this.width - this.height, this.height); 120 | ctx.arc(this.pos.x + this.width - hh, this.pos.y + hh, hh, 1.5 * Math.PI, 0.5 * Math.PI); 121 | ctx.fill(); 122 | ctx.stroke(); 123 | }, 124 | left: function () { 125 | return this.pos.x; 126 | }, 127 | right: function () { 128 | return this.pos.x + this.width; 129 | }, 130 | top: function () { 131 | return this.pos.y; 132 | }, 133 | bottom: function () { 134 | return this.pos.y + this.height; 135 | }, 136 | test_hit: function () { 137 | var hitx = this.test_hit_x(); 138 | var hity = this.test_hit_y(); 139 | if (!hitx || !hity) { 140 | return 0; 141 | } 142 | if (hity) { 143 | ball.vel.y = -Math.abs(ball.vel.y); 144 | ball.pos.y += ball.vel.y; 145 | ball.pos.x -= ball.vel.x; 146 | } 147 | if (hitx) { 148 | var xdiff = ball.pos.x - (this.pos.x + (this.width / 2)); 149 | ball.vel.x = (xdiff > 0 ? Math.ceil(xdiff / 5) : Math.floor(xdiff / 5)) * difficulty[currDiff]['speed_multiplier']; 150 | ball.pos.x += ball.vel.x; 151 | } 152 | return 1; 153 | }, 154 | test_hit_x: function () { 155 | if (this.left() > ball.right()) { 156 | return 0; 157 | } 158 | if (this.right() < ball.left()) { 159 | return 0; 160 | } 161 | return 1; 162 | }, 163 | test_hit_y: function () { 164 | if (this.top() > ball.bottom()) { 165 | return 0; 166 | } 167 | if (this.bottom() < ball.top()) { 168 | return 0; 169 | } 170 | return 1; 171 | }, 172 | move: function () { 173 | if (pressLeft) { 174 | if (this.pos.x > 0) { 175 | this.pos.x -= 8; 176 | } 177 | } 178 | if (pressRight) { 179 | if (this.pos.x < c.width - this.width) { 180 | this.pos.x += 8; 181 | } 182 | } 183 | } 184 | }; 185 | 186 | function fire() { 187 | this.r = 0; 188 | this.a = 0; 189 | this.render = function () { 190 | if (this.a < 0.2) { 191 | this.reset(); 192 | } 193 | 194 | this.pos.x += this.vel.x + (Math.random() * 2) - 1; 195 | this.pos.y += this.vel.y + (Math.random() * 2) - 1; 196 | 197 | this.r *= 0.95; 198 | this.a *= 0.95; 199 | 200 | ctx.beginPath(); 201 | ctx.fillStyle = 'rgba(' + (239 - this.green) + ', ' + this.green + ', 66,' + this.a + ')'; 202 | ctx.arc(this.pos.x, this.pos.y, this.r, 0, 2 * Math.PI); 203 | ctx.fill(); 204 | 205 | if (this.green < 232) { 206 | this.green += 8; 207 | } 208 | }; 209 | this.reset = function () { 210 | this.pos = { 211 | x: ball.pos.x, 212 | y: ball.pos.y, 213 | }; 214 | this.vel = { 215 | x: (Math.random() * 4) - 2, 216 | y: (Math.random() * 4) - 2, 217 | }; 218 | this.r = (Math.random() * 5) + 1; 219 | this.a = 0.9; 220 | this.green = 62; 221 | }; 222 | } 223 | 224 | function brick() { 225 | this.id = 0; 226 | this.pos = { 227 | x: 40, 228 | y: 40, 229 | }; 230 | this.vely = 0; 231 | this.rot = 0; 232 | this.velr = 0; 233 | this.hit = false; 234 | this.last_hitx = false; 235 | this.last_hity = false; 236 | this.width = 40; 237 | this.height = 20; 238 | this.render = function () { 239 | if (this.hit) { 240 | if (this.pos.y > c.height + 60) { 241 | return; 242 | } 243 | this.vely++; 244 | this.pos.y += this.vely; 245 | ctx.save(); 246 | ctx.beginPath(); 247 | this.rot += this.velr; 248 | ctx.translate(this.pos.x + (this.width / 2), this.pos.y + (this.height / 2)); 249 | ctx.rotate(this.rot * Math.PI); 250 | var gradient = ctx.createRadialGradient(-(this.width / 2) + 10, -(this.height / 2) + 5, 0, -(this.width / 2) + 40, -(this.height / 2) + 15, 40); 251 | gradient.addColorStop(0, 'rgba(137, 211, 234, 0.2)'); 252 | gradient.addColorStop(1, 'rgba(137, 211, 234, 1)'); 253 | ctx.strokeStyle = 'rgba(254, 254, 254, 0.8)'; 254 | ctx.fillStyle = gradient; 255 | ctx.rect(-(this.width / 2), -(this.height / 2), this.width, this.height); 256 | ctx.fill(); 257 | ctx.stroke(); 258 | ctx.restore(); 259 | return; 260 | } 261 | ctx.beginPath(); 262 | var gradient = ctx.createRadialGradient(this.pos.x + 10, this.pos.y + 5, 0, this.pos.x + 40, this.pos.y + 15, 40); 263 | gradient.addColorStop(0, 'rgba(137, 211, 234, 0.2)'); 264 | gradient.addColorStop(1, 'rgba(137, 211, 234, 1)'); 265 | ctx.strokeStyle = 'rgba(254, 254, 254, 0.8)'; 266 | ctx.fillStyle = gradient; 267 | ctx.rect(this.pos.x, this.pos.y, this.width, this.height); 268 | ctx.fill(); 269 | ctx.stroke(); 270 | }; 271 | this.test_hit = function () { 272 | if (this.hit) { 273 | return 0; 274 | } 275 | var hitx = this.test_hit_x(); 276 | var hity = this.test_hit_y(); 277 | if (!hitx || !hity) { 278 | this.last_hitx = hitx; 279 | this.last_hity = hity; 280 | return 0; 281 | } 282 | if (this.last_hity) { 283 | ball.vel.y = -ball.vel.y; 284 | ball.pos.y += ball.vel.y; 285 | ball.pos.x -= ball.vel.x; 286 | } 287 | if (this.last_hitx) { 288 | ball.vel.x = -ball.vel.x; 289 | ball.pos.x += ball.vel.x; 290 | ball.pos.y -= ball.vel.y; 291 | } 292 | if (!this.last_hity && this.last_hitx) { 293 | ball.vel.x = -ball.vel.x; 294 | ball.pos.x += ball.vel.x; 295 | ball.vel.y = -ball.vel.y; 296 | ball.pos.y += ball.vel.y; 297 | } 298 | this.last_hitx = hitx; 299 | this.last_hity = hity; 300 | this.hit = true; 301 | this.velr = (Math.random() * 0.04) - 0.02; 302 | score++; 303 | return 1; 304 | }; 305 | this.test_hit_x = function () { 306 | if (this.left() > ball.right()) { 307 | return 0; 308 | } 309 | if (this.right() < ball.left()) { 310 | return 0; 311 | } 312 | return 1; 313 | }; 314 | this.test_hit_y = function () { 315 | if (this.top() > ball.bottom()) { 316 | return 0; 317 | } 318 | if (this.bottom() < ball.top()) { 319 | return 0; 320 | } 321 | return 1; 322 | }; 323 | this.left = function () { 324 | return this.pos.x; 325 | }; 326 | this.right = function () { 327 | return this.pos.x + this.width; 328 | }; 329 | this.top = function () { 330 | return this.pos.y; 331 | }; 332 | this.bottom = function () { 333 | return this.pos.y + this.height; 334 | }; 335 | } 336 | 337 | var pressLeft = false; 338 | var pressRight = false; 339 | 340 | document.addEventListener('keydown', (event) => { 341 | const keyName = event.key; 342 | if (keyName == "ArrowLeft") { 343 | pressLeft = true; 344 | } 345 | if (keyName == "ArrowRight") { 346 | pressRight = true; 347 | } 348 | }); 349 | 350 | document.addEventListener('keyup', (event) => { 351 | const keyName = event.key; 352 | if (keyName == "ArrowLeft") { 353 | pressLeft = false; 354 | } 355 | if (keyName == "ArrowRight") { 356 | pressRight = false; 357 | } 358 | if (keyName == " ") { 359 | if (pause && !gameover) { 360 | if (!startTime) { 361 | startTime = Math.floor(Date.now() / 1000); 362 | } 363 | pause = false; 364 | frame(); 365 | } else { 366 | pause = true; 367 | } 368 | } 369 | }); 370 | 371 | var bricks = []; 372 | 373 | for (var h = 0; h < 6; h++) { 374 | for (var w = 0; w < 18; w++) { 375 | var brickid = (18 * h) + w; 376 | bricks[brickid] = new brick(); 377 | bricks[brickid].pos.x = 40 + (w * 40); 378 | bricks[brickid].pos.y = 40 + (h * 20); 379 | bricks[brickid].id = brickid; 380 | } 381 | } 382 | var fires = []; 383 | 384 | for (var i = 0; i < 80; i++) { 385 | fires[i] = new fire(); 386 | } 387 | var startFireCount = 1; 388 | 389 | var pause = true; 390 | var gameover = false; 391 | 392 | var frame = function () { 393 | if (score >= bricks.length) { 394 | endGame(); 395 | } 396 | ctx.clearRect(0, 0, c.width, c.height); 397 | for (var i = 0; i < bricks.length; i++) { 398 | bricks[i].render(); 399 | } 400 | for (var i = 0; i < bricks.length; i++) { 401 | if (bricks[i].test_hit()) { 402 | break; 403 | } 404 | } 405 | for (var i = 0; i < fires.length && i < startFireCount; i++) { 406 | fires[i].render(); 407 | } 408 | if (startFireCount <= fires.length) { 409 | startFireCount++; 410 | } 411 | paddle.move(); 412 | paddle.render(); 413 | paddle.test_hit(); 414 | ball.render(); 415 | 416 | if (pause) { 417 | if (!gameover) { 418 | ctx.font = "50px Gugi"; 419 | ctx.fillStyle = '#FFFFFF'; 420 | ctx.textAlign = "center"; 421 | ctx.fillText("Ready " + currUserName, c.width / 2, c.height / 2); 422 | ctx.fillText("Press Space to Start", c.width / 2, c.height / 2 + 60); 423 | } 424 | } else { 425 | requestAnimationFrame(frame); 426 | } 427 | }; 428 | 429 | var startTime = false; 430 | document.fonts.load('50px Gugi').then(frame); 431 | 432 | var endGame = function () { 433 | pause = true; 434 | gameover = true; 435 | submitScore(); 436 | }; 437 | 438 | var refreshScoreBoard = function () { 439 | var scores = JSON.parse(this.responseText); 440 | console.log(scores); 441 | var output = 'ScoreTimeName'; 442 | for (var i = 0; i < scores.length; i++) { 443 | output += '' + scores[i].score + '' + scores[i].time + 's' + scores[i].name + ''; 444 | } 445 | document.getElementById("leadertable").innerHTML = output; 446 | }; 447 | 448 | var submitScore = function () { 449 | var time_taken = Math.floor(Date.now() / 1000) - startTime; 450 | var xhttp = new XMLHttpRequest(); 451 | xhttp.addEventListener("load", getScoreBoard); 452 | xhttp.open("POST", "/api/score/" + launchId + "/" + score + "/" + time_taken + "/", false); 453 | xhttp.send(); 454 | }; 455 | 456 | var getScoreBoard = function () { 457 | var xhttp = new XMLHttpRequest(); 458 | xhttp.addEventListener("load", refreshScoreBoard); 459 | xhttp.open("GET", "/api/scoreboard/" + launchId + "/", true); 460 | xhttp.send(); 461 | }; 462 | 463 | document.getElementById("refresh-btn").addEventListener("click", function() { 464 | getScoreBoard(); 465 | }); 466 | 467 | getScoreBoard(); 468 | } 469 | 470 | document.addEventListener("DOMContentLoaded", mainGame); 471 | --------------------------------------------------------------------------------