From a2e15553ae648fa2f1b56b2336b33036088fb451 Mon Sep 17 00:00:00 2001 From: Shea Silverman Date: Mon, 25 Apr 2022 12:59:14 -0400 Subject: [PATCH 01/12] Dockerizing LTI templates --- .dockerignore | 20 +++++++ .env-template | 4 ++ .gitignore | 133 +++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 11 ++++ README.md | 45 +++++++-------- config.py | 50 +++++++++++++--- docker-compose.yml | 14 +++++ gunicorn_conf.py | 9 +++ requirements.txt | 5 +- settings.py.template | 52 ----------------- templates/index.html | 6 +- views.py | 24 ++++---- 12 files changed, 270 insertions(+), 103 deletions(-) create mode 100644 .dockerignore create mode 100644 .env-template create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 gunicorn_conf.py delete mode 100644 settings.py.template diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e0a513e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +env +pip-log.txt +pip-delete-this-directory.txt +.tox +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.log +.git +.mypy_cache +.pytest_cache +.hypothesis% \ No newline at end of file diff --git a/.env-template b/.env-template new file mode 100644 index 0000000..547f23f --- /dev/null +++ b/.env-template @@ -0,0 +1,4 @@ +SECRET_FLASK=CHANGEME +CONSUMER_KEY=CHANGEME +LTI_SECRET=CHANGEME +CONFIG=config.DevelopmentConfig \ No newline at end of file diff --git a/.gitignore b/.gitignore index eb287e4..8f6c042 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,136 @@ venv/ error.log *.DS_STORE test.db + +# config/settings +logs + +.env + + + +# system +*.DS_Store + +# database files +*.db + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +*.log.* +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +env2/ +env3/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..93984e7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.7 +ARG REQUIREMENTS + +COPY ./requirements.txt /code/requirements.txt +RUN pip install --upgrade pip +RUN pip install -r /code/$REQUIREMENTS + +WORKDIR /code +COPY ./ /code/ +EXPOSE 3104 +CMD ["gunicorn", "--conf", "gunicorn_conf.py", "--bind", "0.0.0.0:9001", "views:app"] \ No newline at end of file diff --git a/README.md b/README.md index aac3b4f..9acf8dd 100644 --- a/README.md +++ b/README.md @@ -4,46 +4,41 @@ ## Setup -### Virtual Environment -Create a virtual environment that uses Python 2: +### Docker +We are going to be creating a Docker container that uses Python 3: -``` -virtualenv venv -p /usr/bin/python2.7 -source venv/bin/activate -``` - -Install the dependencies from the requirements file. +#### Setup environment variables -``` -pip install -r requirements.txt +```bash +cp .env-template .env ``` -### Create your local settings file -Create settings.py from settings.py.template +Now you can begin editing the `.env` file. At a minimum, CONSUMER_KEY, SHARED_SECRET, and SECRET_FLASK need to be input by the developer. The SECRET_FLASK is used by Flask, but the CONSUMER_KEY and SHARED_SECRET will be used in setting up the LTI. For security purposes, it's best to have randomized keys. You can generate random keys in the command line by using os.urandom(24) and inputing the resulting values into the .env file: ``` -cp settings.py.template settings.py +import os +os.urandom(24) ``` -Note: settings.py is alreay referenced in the .gitignore and multiple python files, if you want a different settings file name be sure to update the references. - -#### Add your values to the settings file. -At a minimum, CONSUMER_KEY, SHARED_SECRET, and secret_key need to be input by the developer. The secret_key is used by Flask, but the CONSUMER_KEY and SHARED_SECRET will be used in setting up the LTI. For security purposes, it's best to have randomized keys. You can generate random keys in the command line by using os.urandom(24) and inputing the resulting values into the settings.py file: +#### Build and run Docker Container -``` -import os -os.urandom(24) +```bash +docker-compose build +docker-compose up ``` -### Run a Development Server -Here's how you run the flask app from the terminal: +You should see something like this in your console: ``` -export FLASK_APP=views.py -flask run +[INFO] Starting gunicorn 20.1.0 +[INFO] Listening at: http://0.0.0.0:9001 (1) +[INFO] Using worker: gthread +[INFO] Booting worker with pid: 11 ``` +Your server should now be and running. + ### Open in a Browser -Your running server will be visible at [http://127.0.0.1:5000](http://127.0.0.1:5000) +Your running server will be visible at [http://127.0.0.1:9001](http://127.0.0.1:9001) ## Install LTI in Canvas - Have the XML, consumer key, and secret ready. diff --git a/config.py b/config.py index 3b73470..f14180b 100644 --- a/config.py +++ b/config.py @@ -1,25 +1,57 @@ -import settings - - -class Config(object): - PYLTI_CONFIG = settings.PYLTI_CONFIG - +import os class BaseConfig(object): DEBUG = False TESTING = False - PYLTI_CONFIG = settings.PYLTI_CONFIG + + # Using a placeholder for environment variables. + # If not found in the environment, it will default to the local value, which is set in + # the second parameter. Example: + # os.environ.get("ENVIRONMENT_KEY", "LOCAL_VALUE") + # This makes it a bit easier to use one file for local and environment deployment. + + # Declare your consumer key and shared secret. If you end + # up having multiple consumers, you may want to add separate + # key/secret sets for them. + CONSUMER_KEY = os.environ.get("CONSUMER_KEY", "CHANGEME") + SHARED_SECRET = os.environ.get("SHARED_SECRET", "CHANGEME") + + # Configuration for LTI + PYLTI_CONFIG = { + 'consumers': { + CONSUMER_KEY: { + "secret": SHARED_SECRET + } + # Feel free to add more key/secret pairs for other consumers. + }, + 'roles': { + # Maps values sent in the lti launch value of "roles" to a group + # Allows you to check LTI.is_role('admin') for your user + 'admin': ['Administrator', 'urn:lti:instrole:ims/lis/Administrator'], + 'student': ['Student', 'urn:lti:instrole:ims/lis/Student'] + } + } + + # Secret key used for Flask sessions, etc. Must stay named 'secret_key'. + # Can be any randomized string, recommend generating one with os.urandom(24) + secret_key = os.environ.get("SECRET_FLASK", "CHANGEME") + SECRET_FLASK = os.environ.get("SECRET_FLASK", "CHANGEME") + # Store application wide settings here + # For example: we could store our app's api keys for canvas + # + # CANVAS_API_URL = '' + # CANVAS_API_KEY = '' + # class DevelopmentConfig(BaseConfig): DEBUG = True TESTING = True - PYLTI_CONFIG = settings.PYLTI_CONFIG class TestingConfig(BaseConfig): DEBUG = False TESTING = True - PYLTI_CONFIG = settings.PYLTI_CONFIG # DEFINE ADDITIONAL CONFIGS AS NEEDED + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..52ff158 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.1' + +services: + lti: + build: + context: . + args: + - "REQUIREMENTS=${REQUIREMENTS}" + volumes: + - .:/code + ports: + - "9001:9001" + env_file: + - .env diff --git a/gunicorn_conf.py b/gunicorn_conf.py new file mode 100644 index 0000000..481b49c --- /dev/null +++ b/gunicorn_conf.py @@ -0,0 +1,9 @@ +# Gunicorn config variables +loglevel = "info" +errorlog = "-" # stderr +accesslog = "-" # stdout +worker_tmp_dir = "/dev/shm" +graceful_timeout = 120 +timeout = 120 +keepalive = 5 +threads = 3 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0d38200..26dd0e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -Flask==1.0.2 -Flask-SQLAlchemy==2.3.2 +Flask==2.1.1 +Flask-SQLAlchemy==2.5.1 oauth==1.0.1 -e git+https://github.com/ucfcdl/pylti.git@roles#egg=PyLTI +gunicorn==20.1.0 \ No newline at end of file diff --git a/settings.py.template b/settings.py.template deleted file mode 100644 index 535d46c..0000000 --- a/settings.py.template +++ /dev/null @@ -1,52 +0,0 @@ -# COPY THIS FILE TO settings.py -import os - -# Using a placeholder for environment variables. -# If not found in the environment, it will default to the local value, which is set in -# the second parameter. Example: -# os.environ.get("ENVIRONMENT_KEY", "LOCAL_VALUE") -# This makes it a bit easier to use one file for local and environment deployment. - -# Declare your consumer key and shared secret. If you end -# up having multiple consumers, you may want to add separate -# key/secret sets for them. -CONSUMER_KEY = os.environ.get("CONSUMER_KEY", "CHANGEME") -SHARED_SECRET = os.environ.get("SHARED_SECRET", "CHANGEME") - -# Configuration for LTI -PYLTI_CONFIG = { - 'consumers': { - CONSUMER_KEY: { - "secret": SHARED_SECRET - } - # Feel free to add more key/secret pairs for other consumers. - }, - 'roles': { - # Maps values sent in the lti launch value of "roles" to a group - # Allows you to check LTI.is_role('admin') for your user - 'admin': ['Administrator', 'urn:lti:instrole:ims/lis/Administrator'], - 'student': ['Student', 'urn:lti:instrole:ims/lis/Student'] - } -} - -# Secret key used for Flask sessions, etc. Must stay named 'secret_key'. -# Can be any randomized string, recommend generating one with os.urandom(24) -secret_key = os.environ.get("SECRET_FLASK", "CHANGEME") - -# Application Logging -LOG_FILE = 'error.log' -LOG_FORMAT = '%(asctime)s [%(levelname)s] {%(filename)s:%(lineno)d} %(message)s' -LOG_LEVEL = 'INFO' -LOG_MAX_BYTES = 1024 * 1024 * 5 # 5 MB -LOG_BACKUP_COUNT = 1 - -# Config object settings -# See config.py other environments and options -configClass = 'config.DevelopmentConfig' - -# Store application wide settings here -# For example: we could store our app's api keys for canvas -# -# CANVAS_API_URL = '' -# CANVAS_API_KEY = '' -# diff --git a/templates/index.html b/templates/index.html index 5f970a5..3dcdca2 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,7 +7,7 @@

Configuration XML

View the LTI configuration xml here

-

This configuration is used to install your LTI tool into an LSM. This xml can be referenced by url or copied and pasted durring the install process.

+

This configuration is used to install your LTI tool into an LMS. This xml can be referenced by url or copied and pasted durring the install process.

LTI Launch

@@ -20,8 +20,8 @@

Testing LTI Launch

  1. Visit the IMS LTI Tool Consumer Emulator
  2. Set Launch URL to "{{ url_for('launch', _external=True) }}"
  3. -
  4. Set Consmer key to the value defined in "settings.py"
  5. -
  6. Set Shared secret to the value defined in "settings.py"
  7. +
  8. Set Consmer key to the value defined in your ".env"
  9. +
  10. Set Shared secret to the value defined in your ".env"
{% endblock content %} diff --git a/views.py b/views.py index 639950f..a5aa06f 100644 --- a/views.py +++ b/views.py @@ -1,30 +1,30 @@ +import os +import sys from flask import Flask, render_template, session, request, Response from pylti.flask import lti -import settings import logging import json -from logging.handlers import RotatingFileHandler +from logging import Formatter, INFO app = Flask(__name__) -app.secret_key = settings.secret_key -app.config.from_object(settings.configClass) +app.config.from_object(os.environ.get("CONFIG", "config.DevelopmentConfig")) +app.secret_key = app.config["SECRET_FLASK"] # ============================================ # Logging # ============================================ -formatter = logging.Formatter(settings.LOG_FORMAT) -handler = RotatingFileHandler( - settings.LOG_FILE, - maxBytes=settings.LOG_MAX_BYTES, - backupCount=settings.LOG_BACKUP_COUNT +handler = logging.StreamHandler(sys.stdout) +handler.setLevel(INFO) +handler.setFormatter( + Formatter( + "%(asctime)s %(levelname)s: %(message)s " + "[in %(pathname)s: %(lineno)d of %(funcName)s]" + ) ) -handler.setLevel(logging.getLevelName(settings.LOG_LEVEL)) -handler.setFormatter(formatter) app.logger.addHandler(handler) - # ============================================ # Utility Functions # ============================================ From 8598fe9e3f23a0f1c78b7e5069fb50194e557fc4 Mon Sep 17 00:00:00 2001 From: Shea Silverman Date: Tue, 26 Apr 2022 14:56:47 -0400 Subject: [PATCH 02/12] test suites --- .env-template | 3 +- README.md | 19 ++++++- config.py | 17 +++--- gunicorn_conf.py | 2 +- requirements.txt | 22 ++++++-- setup.cfg | 19 +++++++ tests.py | 138 +++++++++++++++++++++++++++++++++++++++++++++++ views.py | 34 ++++++------ 8 files changed, 220 insertions(+), 34 deletions(-) create mode 100644 setup.cfg create mode 100644 tests.py diff --git a/.env-template b/.env-template index 547f23f..5a9c02a 100644 --- a/.env-template +++ b/.env-template @@ -1,4 +1,5 @@ SECRET_FLASK=CHANGEME CONSUMER_KEY=CHANGEME LTI_SECRET=CHANGEME -CONFIG=config.DevelopmentConfig \ No newline at end of file +CONFIG=config.DevelopmentConfig +REQUIREMENTS=requirements.txt \ No newline at end of file diff --git a/README.md b/README.md index 9acf8dd..b531333 100644 --- a/README.md +++ b/README.md @@ -52,4 +52,21 @@ Your running server will be visible at [http://127.0.0.1:9001](http://127.0.0.1: - Account Navigation (account-level navigation) - User Navigation (user profile) -**Note**: If you're using Canvas, your version might be finicky about SSL certificates. Keep HTTP/HTTPS in mind when creating your XML and while developing your project. Some browsers will disable non-SSL LTI content until you enable it through clicking a shield in the browser bar or something similar. \ No newline at end of file +**Note**: If you're using Canvas, your version might be finicky about SSL certificates. Keep HTTP/HTTPS in mind when creating your XML and while developing your project. Some browsers will disable non-SSL LTI content until you enable it through clicking a shield in the browser bar or something similar. + +## Testing + +The LTI Template comes with a pre written test suite. In order to run it make sure your .env key and secret match the tests.py key and secret. + +### Run the test suite + +```bash +docker-compose run lti --rm coverage run -m unittest discover +``` + +### Generate reports + +```bash +docker-compose run lti --rm coverage report +docker-compose run lti --rm coverage html +``` diff --git a/config.py b/config.py index f14180b..312a546 100644 --- a/config.py +++ b/config.py @@ -1,5 +1,6 @@ import os + class BaseConfig(object): DEBUG = False TESTING = False @@ -18,18 +19,16 @@ class BaseConfig(object): # Configuration for LTI PYLTI_CONFIG = { - 'consumers': { - CONSUMER_KEY: { - "secret": SHARED_SECRET - } + "consumers": { + CONSUMER_KEY: {"secret": SHARED_SECRET} # Feel free to add more key/secret pairs for other consumers. }, - 'roles': { + "roles": { # Maps values sent in the lti launch value of "roles" to a group # Allows you to check LTI.is_role('admin') for your user - 'admin': ['Administrator', 'urn:lti:instrole:ims/lis/Administrator'], - 'student': ['Student', 'urn:lti:instrole:ims/lis/Student'] - } + "admin": ["Administrator", "urn:lti:instrole:ims/lis/Administrator"], + "student": ["Student", "urn:lti:instrole:ims/lis/Student"], + }, } # Secret key used for Flask sessions, etc. Must stay named 'secret_key'. @@ -53,5 +52,5 @@ class TestingConfig(BaseConfig): DEBUG = False TESTING = True -# DEFINE ADDITIONAL CONFIGS AS NEEDED +# DEFINE ADDITIONAL CONFIGS AS NEEDED diff --git a/gunicorn_conf.py b/gunicorn_conf.py index 481b49c..cad3455 100644 --- a/gunicorn_conf.py +++ b/gunicorn_conf.py @@ -6,4 +6,4 @@ graceful_timeout = 120 timeout = 120 keepalive = 5 -threads = 3 \ No newline at end of file +threads = 3 diff --git a/requirements.txt b/requirements.txt index 26dd0e7..70316e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,19 @@ -Flask==2.1.1 -Flask-SQLAlchemy==2.5.1 -oauth==1.0.1 +Flask==2.0.1 -e git+https://github.com/ucfcdl/pylti.git@roles#egg=PyLTI -gunicorn==20.1.0 \ No newline at end of file +gunicorn==20.1.0 +Werkzeug==2.0.2 +itsdangerous==2.0.1 +Jinja2==3.0.2 +MarkupSafe==2.0.1 + +# Testing +black +blinker +coverage +flake8 +Flask-Testing>=0.8.0 +mock +oauthlib +requests-mock + + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3a166fe --- /dev/null +++ b/setup.cfg @@ -0,0 +1,19 @@ +[flake8] +exclude= + venv*/* + env/* + env2/* + env3/* + src/* +max_line_length=99 +ignore = W503, E203 + +[coverage:run] +source = . +omit= + venv*/* + env/* + env2/* + env3/* + src/* + gunicorn_conf.py diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..482f8b7 --- /dev/null +++ b/tests.py @@ -0,0 +1,138 @@ +from json.decoder import JSONDecodeError +import logging +import unittest +from urllib.parse import urlencode + +import oauthlib.oauth1 +import flask +from flask import Flask, url_for +import flask_testing +import requests_mock +from pylti.common import LTI_SESSION_KEY +import time + +from mock import patch, mock_open +import views + + +@requests_mock.Mocker() +class LTITests(flask_testing.TestCase): + def create_app(self): + app = views.app + app.config["PRESERVE_CONTEXT_ON_EXCEPTION"] = False + app.config["SECRET_KEY"] = "S3cr3tK3y" + app.config["SESSION_COOKIE_DOMAIN"] = None + + return app + + @classmethod + def setUpClass(cls): + logging.disable(logging.CRITICAL) + app = views.app + app.config["BASE_URL"] = "https://example.edu/" + app.config["GOOGLE_ANALYTICS"] = "123abc" + + def setUp(self): + with self.app.test_request_context(): + pass + + @classmethod + def tearDownClass(cls): + logging.disable(logging.NOTSET) + + def tearDown(self): + pass + + @staticmethod + def generate_launch_request( + url, + body=None, + http_method="GET", + base_url="http://localhost", + roles="Instructor", + headers=None, + ): + params = {} + + if roles is not None: + params["roles"] = roles + + urlparams = urlencode(params) + + client = oauthlib.oauth1.Client( + "CHANGEME", + client_secret="CHANGEME", + signature_method=oauthlib.oauth1.SIGNATURE_HMAC, + signature_type=oauthlib.oauth1.SIGNATURE_TYPE_QUERY, + ) + signature = client.sign( + "{}{}?{}".format(base_url, url, urlparams), + body=body, + http_method=http_method, + headers=headers, + ) + signed_url = signature[0] + new_url = signed_url[len(base_url) :] + return new_url + + + # index + def test_index(self, m): + response = self.client.get(url_for("index")) + + self.assert_200(response) + self.assert_template_used("index.html") + + self.assertIn( + b"LTI Python/Flask Template", response.data + ) + + # xml + def test_xml(self, m): + response = self.client.get(url_for("xml")) + + self.assert_200(response) + self.assert_template_used("lti.xml") + self.assertEqual(response.mimetype, "application/xml") + + + # launch + def test_launch(self, m): + with self.client.session_transaction() as sess: + sess[LTI_SESSION_KEY] = True + sess["oauth_consumer_key"] = "key" + sess["roles"] = "Instructor" + sess["canvas_user_id"] = 1 + sess["user_id"] = 1 + + payload = { + "launch_presentation_return_url": "http://localhost/", + "custom_canvas_user_id": "1", + "custom_canvas_course_id": "1", + "user_id": "1", + "lis_person_name_full": "Tester", + } + + signed_url = self.generate_launch_request( + "/launch", + http_method="POST", + body=payload, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + response = self.client.post( + signed_url, + data=payload, + ) + + self.assertIn(b"Tester", response.data) + self.assert_200(response) + self.assert_template_used("launch.html") + + # error launch + def test_error(self, m): + response = self.client.get(url_for("launch")) + + self.assert_200(response) + self.assert_template_used("error.html") + diff --git a/views.py b/views.py index a5aa06f..f6d1896 100644 --- a/views.py +++ b/views.py @@ -29,15 +29,18 @@ # Utility Functions # ============================================ + def return_error(msg): - return render_template('error.html', msg=msg) + return render_template("error.html", msg=msg) def error(exception=None): app.logger.error("PyLTI error: {}".format(exception)) - return return_error('''Authentication error, + return return_error( + """Authentication error, please refresh and try again. If this error persists, - please contact support.''') + please contact support.""" + ) # ============================================ @@ -45,8 +48,8 @@ def error(exception=None): # ============================================ # LTI Launch -@app.route('/launch', methods=['POST', 'GET']) -@lti(error=error, request='initial', role='any', app=app) +@app.route("/launch", methods=["POST", "GET"]) +@lti(error=error, request="initial", role="any", app=app) def launch(lti=lti): """ Returns the launch page @@ -55,32 +58,27 @@ def launch(lti=lti): # example of getting lti data from the request # let's just store it in our session - session['lis_person_name_full'] = request.form.get('lis_person_name_full') + session["lis_person_name_full"] = request.form.get("lis_person_name_full") # Write the lti params to the console app.logger.info(json.dumps(request.form, indent=2)) - return render_template('launch.html', lis_person_name_full=session['lis_person_name_full']) + return render_template( + "launch.html", lis_person_name_full=session["lis_person_name_full"] + ) # Home page -@app.route('/', methods=['GET']) +@app.route("/", methods=["GET"]) def index(lti=lti): - return render_template('index.html') + return render_template("index.html") # LTI XML Configuration -@app.route("/xml/", methods=['GET']) +@app.route("/xml/", methods=["GET"]) def xml(): """ Returns the lti.xml file for the app. XML can be built at https://www.eduappcenter.com/ """ - try: - return Response(render_template( - 'lti.xml'), mimetype='application/xml' - ) - except: - app.logger.error("Error with XML.") - return return_error('''Error with XML. Please refresh and try again. If this error persists, - please contact support.''') + return Response(render_template("lti.xml"), mimetype="application/xml") From 6a9ef074dc3127038d9f1c2da1fdbc74b499a0d9 Mon Sep 17 00:00:00 2001 From: Shea Silverman Date: Tue, 26 Apr 2022 17:00:22 -0400 Subject: [PATCH 03/12] ci/cd --- .github/workflows/ci.yml | 70 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3d40062 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,70 @@ +name: Run Python Tests and Build Image + +on: + push: + branches: + - issue/15-Dockerize + - develop + - master + +env: + REQUIREMENTS: requirements.txt + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install Python 3 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install coveralls + - name: Setup Repo + run: | + cp .env.template .env + - name: Run flake8 + run: flake8 + - name: Run black + run: black --check . + - name: Lint markdown files + uses: bewuethr/mdl-action@v1 + + - name: Load dotenv + uses: falti/dotenv-action@v0.2.5 + + - name: Environment Variables from Dotenv + uses: c-py/action-dotenv-to-setenv@v3 + + - name: Run unittests + run: coverage run -m unittest discover + + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} + + + From 4ea2bdfdfc24bc47eaad90481e74e1d7f699e33a Mon Sep 17 00:00:00 2001 From: Shea Silverman Date: Tue, 26 Apr 2022 17:11:26 -0400 Subject: [PATCH 04/12] env name --- .github/workflows/ci.yml | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d40062..05004c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: pip install coveralls - name: Setup Repo run: | - cp .env.template .env + cp .env-template .env - name: Run flake8 run: flake8 - name: Run black @@ -44,27 +44,5 @@ jobs: run: coverage run -m unittest discover - - name: Log in to the Container registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - - name: Build and push Docker image - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} - From 79fe2d9e194d5b124f38059c848252850cd1b2cd Mon Sep 17 00:00:00 2001 From: Shea Silverman Date: Tue, 26 Apr 2022 17:20:20 -0400 Subject: [PATCH 05/12] linted --- tests.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/tests.py b/tests.py index 482f8b7..de37c9d 100644 --- a/tests.py +++ b/tests.py @@ -1,17 +1,13 @@ -from json.decoder import JSONDecodeError import logging -import unittest from urllib.parse import urlencode import oauthlib.oauth1 -import flask -from flask import Flask, url_for + +from flask import url_for import flask_testing import requests_mock from pylti.common import LTI_SESSION_KEY -import time -from mock import patch, mock_open import views @@ -75,7 +71,6 @@ def generate_launch_request( new_url = signed_url[len(base_url) :] return new_url - # index def test_index(self, m): response = self.client.get(url_for("index")) @@ -83,9 +78,7 @@ def test_index(self, m): self.assert_200(response) self.assert_template_used("index.html") - self.assertIn( - b"LTI Python/Flask Template", response.data - ) + self.assertIn(b"LTI Python/Flask Template", response.data) # xml def test_xml(self, m): @@ -95,7 +88,6 @@ def test_xml(self, m): self.assert_template_used("lti.xml") self.assertEqual(response.mimetype, "application/xml") - # launch def test_launch(self, m): with self.client.session_transaction() as sess: @@ -135,4 +127,3 @@ def test_error(self, m): self.assert_200(response) self.assert_template_used("error.html") - From 624d8eadac4e30b1e4ff6e6d1b47693ccdceb826 Mon Sep 17 00:00:00 2001 From: Shea Silverman Date: Tue, 26 Apr 2022 17:27:48 -0400 Subject: [PATCH 06/12] Linted README test --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b531333..fec24a7 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ ## Setup ### Docker + We are going to be creating a Docker container that uses Python 3: #### Setup environment variables @@ -15,7 +16,7 @@ cp .env-template .env Now you can begin editing the `.env` file. At a minimum, CONSUMER_KEY, SHARED_SECRET, and SECRET_FLASK need to be input by the developer. The SECRET_FLASK is used by Flask, but the CONSUMER_KEY and SHARED_SECRET will be used in setting up the LTI. For security purposes, it's best to have randomized keys. You can generate random keys in the command line by using os.urandom(24) and inputing the resulting values into the .env file: -``` +```bash import os os.urandom(24) ``` @@ -28,7 +29,8 @@ docker-compose up ``` You should see something like this in your console: -``` + +```bash [INFO] Starting gunicorn 20.1.0 [INFO] Listening at: http://0.0.0.0:9001 (1) [INFO] Using worker: gthread @@ -38,9 +40,11 @@ You should see something like this in your console: Your server should now be and running. ### Open in a Browser + Your running server will be visible at [http://127.0.0.1:9001](http://127.0.0.1:9001) ## Install LTI in Canvas + - Have the XML, consumer key, and secret ready. - You can use the [XML Config Builder](https://www.edu-apps.org/build_xml.html) to build XML. - Navigate to the course that you would like the LTI to be added to. Click Settings in the course navigation bar. Then, select the Apps tab. Near the tabs on the right side, click 'View App Configurations'. It should lead to a page that lists what LTIs are inside the course. Click the button near the tabs that reads '+ App'. From b301c5880c1f710d22954c91ed562f2a07b97ffe Mon Sep 17 00:00:00 2001 From: Shea Silverman Date: Wed, 27 Apr 2022 09:47:35 -0400 Subject: [PATCH 07/12] MD lint --- CONTRIBUTING.md | 9 ++++++--- README.md | 12 ++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4539f5e..7599224 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,18 +3,21 @@ Thanks for being interested! Here's our guidelines to contributing to the templates. ## Get Set Up + - Check the issues to see if your problem has already been brought up. - Submit a ticket if it hasn't. Be sure to be clear and give instructions on how to reproduce the issue you're having. ## Getting Your Hands Dirty + - Fork the repository. - Please try not to work on master. Instead, create a new branch named after your issue and its number: - - `git checkout -b issue/number-brief-issue-description` + - `git checkout -b issue/number-brief-issue-description` + +## Submit -## Submit! - Push your changes. - Submit a pull request. - We will review your changes and try to respond in a timely manner. -That's it! \ No newline at end of file +That's it! diff --git a/README.md b/README.md index fec24a7..c7c56c0 100644 --- a/README.md +++ b/README.md @@ -46,15 +46,15 @@ Your running server will be visible at [http://127.0.0.1:9001](http://127.0.0.1: ## Install LTI in Canvas - Have the XML, consumer key, and secret ready. - - You can use the [XML Config Builder](https://www.edu-apps.org/build_xml.html) to build XML. + - You can use the [XML Config Builder](https://www.edu-apps.org/build_xml.html) to build XML. - Navigate to the course that you would like the LTI to be added to. Click Settings in the course navigation bar. Then, select the Apps tab. Near the tabs on the right side, click 'View App Configurations'. It should lead to a page that lists what LTIs are inside the course. Click the button near the tabs that reads '+ App'. - A modal should come up that allows you to customize how the app gets added. Change the configuration in the Configuration Type dropdown menu to 'By URL' or 'Paste XML' depending on how you have your LTI configured. If your LTI is publicly accessible, 'By URL' is recommended. From there, fill out the Name and Consumer Keys, and the Config URL or XML Configuration. Click Submit. - Your LTI will appear depending on specifications in the XML. Currently, they get specified in the **options** tag within the **extensions** tag. Extensions can include these options: - - Editor Button (visible from within any wiki page editor in Canvas) - - Homework Submission (when a student is submitting content for an assignment) - - Course Navigation (link on the lefthand nav) - - Account Navigation (account-level navigation) - - User Navigation (user profile) + - Editor Button (visible from within any wiki page editor in Canvas) + - Homework Submission (when a student is submitting content for an assignment) + - Course Navigation (link on the lefthand nav) + - Account Navigation (account-level navigation) + - User Navigation (user profile) **Note**: If you're using Canvas, your version might be finicky about SSL certificates. Keep HTTP/HTTPS in mind when creating your XML and while developing your project. Some browsers will disable non-SSL LTI content until you enable it through clicking a shield in the browser bar or something similar. From 986ca7e086388340685556af7869658ed905fcbb Mon Sep 17 00:00:00 2001 From: Shea Silverman Date: Wed, 27 Apr 2022 09:52:20 -0400 Subject: [PATCH 08/12] Style rules --- README.md | 12 ++++++------ markdown-style.rb | 9 +++++++++ mdlrc.txt | 1 + 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 markdown-style.rb create mode 100644 mdlrc.txt diff --git a/README.md b/README.md index c7c56c0..fec24a7 100644 --- a/README.md +++ b/README.md @@ -46,15 +46,15 @@ Your running server will be visible at [http://127.0.0.1:9001](http://127.0.0.1: ## Install LTI in Canvas - Have the XML, consumer key, and secret ready. - - You can use the [XML Config Builder](https://www.edu-apps.org/build_xml.html) to build XML. + - You can use the [XML Config Builder](https://www.edu-apps.org/build_xml.html) to build XML. - Navigate to the course that you would like the LTI to be added to. Click Settings in the course navigation bar. Then, select the Apps tab. Near the tabs on the right side, click 'View App Configurations'. It should lead to a page that lists what LTIs are inside the course. Click the button near the tabs that reads '+ App'. - A modal should come up that allows you to customize how the app gets added. Change the configuration in the Configuration Type dropdown menu to 'By URL' or 'Paste XML' depending on how you have your LTI configured. If your LTI is publicly accessible, 'By URL' is recommended. From there, fill out the Name and Consumer Keys, and the Config URL or XML Configuration. Click Submit. - Your LTI will appear depending on specifications in the XML. Currently, they get specified in the **options** tag within the **extensions** tag. Extensions can include these options: - - Editor Button (visible from within any wiki page editor in Canvas) - - Homework Submission (when a student is submitting content for an assignment) - - Course Navigation (link on the lefthand nav) - - Account Navigation (account-level navigation) - - User Navigation (user profile) + - Editor Button (visible from within any wiki page editor in Canvas) + - Homework Submission (when a student is submitting content for an assignment) + - Course Navigation (link on the lefthand nav) + - Account Navigation (account-level navigation) + - User Navigation (user profile) **Note**: If you're using Canvas, your version might be finicky about SSL certificates. Keep HTTP/HTTPS in mind when creating your XML and while developing your project. Some browsers will disable non-SSL LTI content until you enable it through clicking a shield in the browser bar or something similar. diff --git a/markdown-style.rb b/markdown-style.rb new file mode 100644 index 0000000..defe4ef --- /dev/null +++ b/markdown-style.rb @@ -0,0 +1,9 @@ +all + +exclude_rule 'first-line-h1' +exclude_rule 'line-length' +exclude_rule 'no-duplicate-header' + +rule 'no-trailing-punctuation', :punctuation => '.,;:!' +rule 'ol-prefix', :style => 'ordered' +rule 'ul-indent', :indent => 4 diff --git a/mdlrc.txt b/mdlrc.txt new file mode 100644 index 0000000..bdaf13d --- /dev/null +++ b/mdlrc.txt @@ -0,0 +1 @@ +style "markdown-style.rb" From 15259e18d61f8767a67309b02ae061847f38f47f Mon Sep 17 00:00:00 2001 From: Shea Silverman Date: Wed, 27 Apr 2022 09:54:49 -0400 Subject: [PATCH 09/12] .mdlrc --- .mdlrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .mdlrc diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 0000000..bdaf13d --- /dev/null +++ b/.mdlrc @@ -0,0 +1 @@ +style "markdown-style.rb" From 79dd0c50aa6d75e714bbe569c57e072064a8e6de Mon Sep 17 00:00:00 2001 From: Shea Silverman Date: Wed, 27 Apr 2022 09:57:03 -0400 Subject: [PATCH 10/12] md linting --- CONTRIBUTING.md | 2 +- README.md | 2 +- mdlrc.txt | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 mdlrc.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7599224..6192170 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Thanks for being interested! Here's our guidelines to contributing to the templa - Fork the repository. - Please try not to work on master. Instead, create a new branch named after your issue and its number: - - `git checkout -b issue/number-brief-issue-description` + - `git checkout -b issue/number-brief-issue-description` ## Submit diff --git a/README.md b/README.md index fec24a7..e464a1e 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,6 @@ docker-compose run lti --rm coverage run -m unittest discover ### Generate reports ```bash -docker-compose run lti --rm coverage report +docker-compose run lti --rm coverage report docker-compose run lti --rm coverage html ``` diff --git a/mdlrc.txt b/mdlrc.txt deleted file mode 100644 index bdaf13d..0000000 --- a/mdlrc.txt +++ /dev/null @@ -1 +0,0 @@ -style "markdown-style.rb" From 054d4c84c9b3f068152b9df32b2aaf9bef63b138 Mon Sep 17 00:00:00 2001 From: Shea Silverman Date: Tue, 7 Jun 2022 09:27:29 -0400 Subject: [PATCH 11/12] Updated variable names, added Same Site --- .env-template | 2 +- config.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.env-template b/.env-template index 5a9c02a..d75d812 100644 --- a/.env-template +++ b/.env-template @@ -1,5 +1,5 @@ SECRET_FLASK=CHANGEME -CONSUMER_KEY=CHANGEME +LTI_KEY=CHANGEME LTI_SECRET=CHANGEME CONFIG=config.DevelopmentConfig REQUIREMENTS=requirements.txt \ No newline at end of file diff --git a/config.py b/config.py index 312a546..d48a3a6 100644 --- a/config.py +++ b/config.py @@ -14,13 +14,13 @@ class BaseConfig(object): # Declare your consumer key and shared secret. If you end # up having multiple consumers, you may want to add separate # key/secret sets for them. - CONSUMER_KEY = os.environ.get("CONSUMER_KEY", "CHANGEME") - SHARED_SECRET = os.environ.get("SHARED_SECRET", "CHANGEME") + LTI_KEY = os.environ.get("LTI_KEY", "CHANGEME") + LTI_SECRET = os.environ.get("LTI_SECRET", "CHANGEME") # Configuration for LTI PYLTI_CONFIG = { "consumers": { - CONSUMER_KEY: {"secret": SHARED_SECRET} + LTI_KEY: {"secret": LTI_SECRET} # Feel free to add more key/secret pairs for other consumers. }, "roles": { @@ -41,7 +41,9 @@ class BaseConfig(object): # CANVAS_API_URL = '' # CANVAS_API_KEY = '' # - + # Chrome 80 SameSite=None; Secure fix + SESSION_COOKIE_SECURE = True + SESSION_COOKIE_SAMESITE = "None" class DevelopmentConfig(BaseConfig): DEBUG = True From 163b5941be44115396d1f98b3e5babd3c044aabe Mon Sep 17 00:00:00 2001 From: Shea Silverman Date: Tue, 7 Jun 2022 09:29:59 -0400 Subject: [PATCH 12/12] Lint --- config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/config.py b/config.py index d48a3a6..f3a1146 100644 --- a/config.py +++ b/config.py @@ -45,6 +45,7 @@ class BaseConfig(object): SESSION_COOKIE_SECURE = True SESSION_COOKIE_SAMESITE = "None" + class DevelopmentConfig(BaseConfig): DEBUG = True TESTING = True