From cb6591029d9d23a9b73bc9bc879c4ca3c501cad8 Mon Sep 17 00:00:00 2001 From: Shea Silverman Date: Tue, 30 Mar 2021 17:12:00 -0400 Subject: [PATCH 01/52] Dockerized Faculty Tools --- .dockerignore | 22 +++++++++++++++ .env.template | 15 ++++++++++ .gitignore | 2 +- Dockerfile | 10 +++++++ README.md | 68 ++++++++++++++++++---------------------------- config.py | 18 ++++++++---- docker-compose.yml | 35 ++++++++++++++++++++++++ lti.py => main.py | 0 requirements.txt | 4 +-- settings.py | 55 +++++++++++++++++++++++++++++++++++++ 10 files changed, 180 insertions(+), 49 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.template create mode 100644 Dockerfile create mode 100644 docker-compose.yml rename lti.py => main.py (100%) create mode 100644 settings.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..20d7ebb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +.dockerignore +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +env +venv* +pip-log.txt +pip-delete-this-directory.txt +.tox +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +*.log +.git +*.template +.env* +fake-s3 diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..aef2480 --- /dev/null +++ b/.env.template @@ -0,0 +1,15 @@ +TOOL_TITLE=Faculty Tools +THEME_DIR= +API_URL=https://example.com/ +SECRET_KEY=CHANGEME +LTI_KEY=CHANGEME +LTI_SECRET=CHANGEME +OAUTH2_URI=https://127.0.0.1:9001/oauthlogin +OAUTH2_ID=CHANGEME +OAUTH2_KEY=CHANGEME +GOOGLE_ANALYTICS=GA-000000 +CONFIG=config.DevelopmentConfig +DATABASE_URI=mysql://root:secret@db/faculty_tools + +REQUIREMENTS=test_requirements.txt + diff --git a/.gitignore b/.gitignore index 0e980ad..86319a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # config/settings logs -settings.py whitelist.json +.env # local theming themes/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..95af5ae --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM tiangolo/uwsgi-nginx-flask:python3.7 +ARG REQUIREMENTS +RUN apt-get update && apt-get -y install libffi-dev gcc python3-dev libffi-dev libssl-dev libxml2-dev libxmlsec1-dev libxmlsec1-openssl +RUN apt-get -y install ca-certificates +WORKDIR /app +COPY requirements.txt /app/ +COPY test_requirements.txt /app/ +RUN echo $REQUIREMENTS +RUN pip install -r $REQUIREMENTS +COPY ./ /app/ diff --git a/README.md b/README.md index 529383f..a946475 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,19 @@ # Documentation for Faculty Tools -## Settings +## Setting up Faculty Tools with Docker & Docker-Compose -Create a new `settings.py` file from the template +First clone and setup the repo. ```sh -cp settings.py.template settings.py +git clone git@github.com:ucfopen/faculty-tools.git +cp whitelist.json.template whitelist.json +cp .env.template .env +mkdir logs +touch logs/faculty-tools.log ``` -Edit `settings.py` to configure the application. All fields are required, +Edit `.env` to configure the application. All fields are required, unless specifically noted. ## Developer Key @@ -57,32 +61,13 @@ Add the tools you want instructors and faculty to see to `whitelist.json`. ] ``` -## Virtual Environment - -Create a new virtual environment. - -```sh -virtualenv env -``` - -Activate the environment. - -```sh -source env/bin/activate -``` - -Install everything: - -```sh -pip install -r requirements.txt -``` - ## Create DB -Change directory into the project folder. Create the database in python shell: +We need to generate the database and tables for faculty tools to run properly. The MySQL docker image automatically creates the user, password, and database name set in the `docker-compose.yml` file. ```sh -from lti import db +docker-compose run lti python +from main import db db.create_all() ``` @@ -90,34 +75,35 @@ If you want to look at your users table in the future, you can do so in the python shell: ```python -from lti import Users +docker-compose run lti python +from main import Users Users.query.all() ``` -## Environment Variables -Set the flask app to `lti.py` and debug to true. +## Run the App + +It's time to use docker-compose to bring up the application. ```sh -export FLASK_APP=lti.py -export FLASK_DEBUG=1 +docker-compose up -d ``` -Alternatively, you can run the setup script to simultaneously setup environment -variables and the virtual environment. +Go to the /xml page, [http://127.0.0.1:9001/xml](http://127.0.0.1:9001/xml) by default -```sh -source setup.sh -``` +Copy the xml, install it into a course. -## Run the App +## View the Logs -Run the lti script while your virtual environment is active. +To view the logs while the application is running use this docker command: ```sh -flask run +docker-compose logs -f ``` -Go to the /xml page, [http://0.0.0.0:5000/xml](http://0.0.0.0:5000/xml) by default +## Stopping the App -Copy the xml, install it into a course. +To shutdown Faculty Tools +```sh +docker-compose down +``` \ No newline at end of file diff --git a/config.py b/config.py index c360a34..41c8e96 100644 --- a/config.py +++ b/config.py @@ -1,10 +1,9 @@ import settings - +import os class Config(object): # make the warning shut up until Flask-SQLAlchemy v3 comes out SQLALCHEMY_TRACK_MODIFICATIONS = True - SQLALCHEMY_DATABASE_URI = settings.select_db("Config") PYLTI_CONFIG = settings.PYLTI_CONFIG @@ -14,6 +13,9 @@ class Config(object): SESSION_COOKIE_SECURE = True SESSION_COOKIE_SAMESITE = "None" + SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI") + + class BaseConfig(object): DEBUG = False @@ -21,7 +23,6 @@ class BaseConfig(object): # make the warning shut up until Flask-SQLAlchemy v3 comes out SQLALCHEMY_TRACK_MODIFICATIONS = True - SQLALCHEMY_DATABASE_URI = settings.select_db("BaseConfig") PYLTI_CONFIG = settings.PYLTI_CONFIG @@ -31,16 +32,23 @@ class BaseConfig(object): SESSION_COOKIE_SECURE = True SESSION_COOKIE_SAMESITE = "None" + SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI") + + class DevelopmentConfig(BaseConfig): DEBUG = True TESTING = True - SQLALCHEMY_DATABASE_URI = settings.select_db("DevelopmentConfig") + SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI") + + class TestingConfig(BaseConfig): DEBUG = False TESTING = True - SQLALCHEMY_DATABASE_URI = settings.select_db("TestingConfig") + SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI") + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..751f9ca --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +version: '3.1' + +services: + lti: + build: + context: . + args: + - "REQUIREMENTS=${REQUIREMENTS}" + ports: + - "9001:80" + env_file: + - .env + environment: + - MODULE_NAME=app + depends_on: + - db + + db: + image: mysql:5.7 + volumes: + - ft_dbdata:/var/lib/mysql + restart: always + environment: + MYSQL_ROOT_PASSWORD: secret + MYSQL_DATABASE: faculty_tools + MYSQL_USER: root + MYSQL_PASSWORD: secret + ports: + - "33061:3306" +volumes: + ft_dbdata: {} + + + + diff --git a/lti.py b/main.py similarity index 100% rename from lti.py rename to main.py diff --git a/requirements.txt b/requirements.txt index b4f768f..38b85d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ canvasapi==0.15.0 Flask==1.1.1 -Flask-SQLAlchemy==2.4.1 +Flask-SQLAlchemy==2.5.1 mysqlclient --e git+https://github.com/ucfcdl/pylti.git@roles#egg=PyLTI +git+https://github.com/ucfcdl/pylti.git@roles#egg=PyLTI requests==2.22.0 Werkzeug>=1.0.1 # Chrome 80 SameSite fix diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..a67aca3 --- /dev/null +++ b/settings.py @@ -0,0 +1,55 @@ +import os +# Title of the tool. Appears in the element, headers, and configuration XML +TOOL_TITLE = os.environ.get("TOOL_TITLE", "Faculty Tools") + +# Which theme directory to use. Leave blank for default. +THEME_DIR = os.environ.get("THEME_DIR", "") + +# Canvas instance URL. ex: https://example.instructure.com/ +BASE_URL = os.environ.get("API_URL") +API_URL = BASE_URL + "api/v1/" + +# Secret key to sign Flask sessions with. KEEP THIS SECRET! +secret_key = os.environ.get("SECRET_KEY") + +# LTI consumer key and shared secret +CONSUMER_KEY = os.environ.get("LTI_KEY") +SHARED_SECRET = os.environ.get("LTI_SECRET") + +# Configuration for pylti library. Uses the above key and secret +PYLTI_CONFIG = { + "consumers": { + CONSUMER_KEY: { + "secret": SHARED_SECRET + } + }, + # Custom configurable roles + "roles": { + "staff": [ + "urn:lti:instrole:ims/lis/Administrator", + "Instructor", + "ContentDeveloper", + "urn:lti:role:ims/lis/TeachingAssistant", + ] + }, +} + +# The "Oauth2 Redirect URI" that you provided to Instructure. +oauth2_uri = os.environ.get("OAUTH2_URI") # ex. 'https://localhost:5000/oauthlogin' +# The Client_ID Instructure gave you +oauth2_id = os.environ.get("OAUTH2_ID") +# The Secret Instructure gave you +oauth2_key = os.environ.get("OAUTH2_KEY") + +# Logging configuration +LOG_MAX_BYTES = 1024 * 1024 * 5 # 5 MB +LOG_BACKUP_COUNT = 2 +ERROR_LOG = "logs/faculty-tools.log" + +whitelist = "whitelist.json" + +# Google Analytics Tracking ID (optional) +GOOGLE_ANALYTICS = os.environ.get("GOOGLE_ANALYTICS", "GA-") + + +configClass = os.environ.get("CONFIG", "config.DevelopmentConfig") \ No newline at end of file From b0031a679afab985f312b5d598f6efdcb415762f Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Tue, 6 Apr 2021 16:14:51 -0400 Subject: [PATCH 02/52] Removing settings.py.template --- settings.py.template | 63 -------------------------------------------- 1 file changed, 63 deletions(-) delete mode 100644 settings.py.template diff --git a/settings.py.template b/settings.py.template deleted file mode 100644 index ce34aac..0000000 --- a/settings.py.template +++ /dev/null @@ -1,63 +0,0 @@ -# Title of the tool. Appears in the <title> element, headers, and configuration XML -TOOL_TITLE = "Faculty Tools" - -# Which theme directory to use. Leave blank for default. -THEME_DIR = "" - -# Canvas instance URL. ex: https://example.instructure.com/ -BASE_URL = "https://example.instructure.com/" -API_URL = BASE_URL + "api/v1/" - -# Secret key to sign Flask sessions with. KEEP THIS SECRET! -secret_key = "" - -# LTI consumer key and shared secret -CONSUMER_KEY = "key" -SHARED_SECRET = "secret" - -# Configuration for pylti library. Uses the above key and secret -PYLTI_CONFIG = { - "consumers": { - CONSUMER_KEY: { - "secret": SHARED_SECRET - } - }, - # Custom configurable roles - "roles": { - "staff": [ - "urn:lti:instrole:ims/lis/Administrator", - "Instructor", - "ContentDeveloper", - "urn:lti:role:ims/lis/TeachingAssistant", - ] - }, -} - -# The "Oauth2 Redirect URI" that you provided to Instructure. -oauth2_uri = "" # ex. 'https://localhost:5000/oauthlogin' -# The Client_ID Instructure gave you -oauth2_id = "" -# The Secret Instructure gave you -oauth2_key = "" - -# Logging configuration -LOG_MAX_BYTES = 1024 * 1024 * 5 # 5 MB -LOG_BACKUP_COUNT = 2 -ERROR_LOG = "logs/faculty-tools.log" - -whitelist = "whitelist.json" - -# Google Analytics Tracking ID (optional) -GOOGLE_ANALYTICS = "" - - -def select_db(x): - return { - "DevelopmentConfig": "sqlite:///test.db", - "Config": "sqlite:///test.db", - "BaseConfig": "sqlite:///test.db", - "TestingConfig": "sqlite:///test.db", - }.get(x, "sqlite:///test2.db") - - -configClass = "config.DevelopmentConfig" From ea9774733b6dbc03db57f2f5842ecd71eeb955c6 Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Tue, 6 Apr 2021 16:17:42 -0400 Subject: [PATCH 03/52] Fixes naming confusion --- .env.template | 2 +- settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.template b/.env.template index aef2480..9277768 100644 --- a/.env.template +++ b/.env.template @@ -1,6 +1,6 @@ TOOL_TITLE=Faculty Tools THEME_DIR= -API_URL=https://example.com/ +BASE_CANVAS_SERVER_URL=https://example.com/ SECRET_KEY=CHANGEME LTI_KEY=CHANGEME LTI_SECRET=CHANGEME diff --git a/settings.py b/settings.py index a67aca3..10b7f49 100644 --- a/settings.py +++ b/settings.py @@ -6,7 +6,7 @@ THEME_DIR = os.environ.get("THEME_DIR", "") # Canvas instance URL. ex: https://example.instructure.com/ -BASE_URL = os.environ.get("API_URL") +BASE_URL = os.environ.get("BASE_CANVAS_SERVER_URL") API_URL = BASE_URL + "api/v1/" # Secret key to sign Flask sessions with. KEEP THIS SECRET! From c5bc16e6e2cd69a14473184d922e4053df704bcf Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Tue, 6 Apr 2021 16:26:53 -0400 Subject: [PATCH 04/52] CI test --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 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..17375f0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: Run Python Tests +on: + push: + branches: + - issue/21-dockerize + pull_request: + branches: + - issue/21-dockerize + +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 text_requirements.txt + pip install coveralls + gem install mdl + - name: Setup Repo + run: | + cp settings.py.template settings.py; cp whitelist.json.template whitelist.json + mkdir logs; touch logs/faculty-tools.log + - name: Run flake8 + run: flake8 \ No newline at end of file From 1a1532fd9746b03fb8a720181bcb75a183dc0b32 Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Tue, 6 Apr 2021 16:27:44 -0400 Subject: [PATCH 05/52] CI test --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17375f0..ddd5716 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r text_requirements.txt + pip install -r test_requirements.txt pip install coveralls gem install mdl - name: Setup Repo From 32300df6bdec4a36dfb030a9483b5cb01e31f652 Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Tue, 6 Apr 2021 16:29:03 -0400 Subject: [PATCH 06/52] CI test --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddd5716..271b397 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: python -m pip install --upgrade pip pip install -r test_requirements.txt pip install coveralls - gem install mdl + # gem install mdl - name: Setup Repo run: | cp settings.py.template settings.py; cp whitelist.json.template whitelist.json From 5dd2415b7878e8bbd71de729f3f4a0ac18da8383 Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Tue, 6 Apr 2021 16:34:37 -0400 Subject: [PATCH 07/52] CI test --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 271b397..819eded 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: # gem install mdl - name: Setup Repo run: | - cp settings.py.template settings.py; cp whitelist.json.template whitelist.json + cp whitelist.json.template whitelist.json mkdir logs; touch logs/faculty-tools.log - name: Run flake8 run: flake8 \ No newline at end of file From 9c1cb3e58801948949a12a246a7664b1e67601b5 Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Tue, 6 Apr 2021 16:36:37 -0400 Subject: [PATCH 08/52] flake8 --- config.py | 7 +------ settings.py | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/config.py b/config.py index 41c8e96..059501c 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,7 @@ import settings import os + class Config(object): # make the warning shut up until Flask-SQLAlchemy v3 comes out SQLALCHEMY_TRACK_MODIFICATIONS = True @@ -16,7 +17,6 @@ class Config(object): SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI") - class BaseConfig(object): DEBUG = False TESTING = False @@ -35,7 +35,6 @@ class BaseConfig(object): SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI") - class DevelopmentConfig(BaseConfig): DEBUG = True TESTING = True @@ -43,12 +42,8 @@ class DevelopmentConfig(BaseConfig): SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI") - - class TestingConfig(BaseConfig): DEBUG = False TESTING = True SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI") - - diff --git a/settings.py b/settings.py index 10b7f49..36dbc14 100644 --- a/settings.py +++ b/settings.py @@ -52,4 +52,4 @@ GOOGLE_ANALYTICS = os.environ.get("GOOGLE_ANALYTICS", "GA-") -configClass = os.environ.get("CONFIG", "config.DevelopmentConfig") \ No newline at end of file +configClass = os.environ.get("CONFIG", "config.DevelopmentConfig") From 2601e5d58ed53b6d9042e905a151178f50d58d30 Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Tue, 6 Apr 2021 16:38:00 -0400 Subject: [PATCH 09/52] black --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 819eded..fa6bc5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,4 +27,6 @@ jobs: cp whitelist.json.template whitelist.json mkdir logs; touch logs/faculty-tools.log - name: Run flake8 - run: flake8 \ No newline at end of file + run: flake8 + - name: Run black + run: black --check . \ No newline at end of file From 0721d3416e35dc50da31acddf6f8335509d29669 Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Tue, 6 Apr 2021 16:53:42 -0400 Subject: [PATCH 10/52] CI/CD + black --- .github/workflows/ci.yml | 6 +++++- settings.py | 8 ++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa6bc5a..952e7d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,4 +29,8 @@ jobs: - name: Run flake8 run: flake8 - name: Run black - run: black --check . \ No newline at end of file + run: black --check . + - name: Lint markdown files + uses: bewuethr/mdl-action@v1 + - name: Run unittests + run: coverage run -m unittest discover diff --git a/settings.py b/settings.py index 36dbc14..2c32b39 100644 --- a/settings.py +++ b/settings.py @@ -1,4 +1,5 @@ import os + # Title of the tool. Appears in the <title> element, headers, and configuration XML TOOL_TITLE = os.environ.get("TOOL_TITLE", "Faculty Tools") @@ -18,11 +19,7 @@ # Configuration for pylti library. Uses the above key and secret PYLTI_CONFIG = { - "consumers": { - CONSUMER_KEY: { - "secret": SHARED_SECRET - } - }, + "consumers": {CONSUMER_KEY: {"secret": SHARED_SECRET}}, # Custom configurable roles "roles": { "staff": [ @@ -51,5 +48,4 @@ # Google Analytics Tracking ID (optional) GOOGLE_ANALYTICS = os.environ.get("GOOGLE_ANALYTICS", "GA-") - configClass = os.environ.get("CONFIG", "config.DevelopmentConfig") From 3d1cee538d061feb25df4f891cf9cd4b892192c0 Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Tue, 6 Apr 2021 21:29:57 -0400 Subject: [PATCH 11/52] README MDL --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a946475..525492f 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,9 @@ Add the tools you want instructors and faculty to see to `whitelist.json`. ## Create DB -We need to generate the database and tables for faculty tools to run properly. The MySQL docker image automatically creates the user, password, and database name set in the `docker-compose.yml` file. +We need to generate the database and tables for faculty tools to run properly. +The MySQL docker image automatically creates the user, password, and database +name set in the `docker-compose.yml` file. ```sh docker-compose run lti python @@ -80,7 +82,6 @@ from main import Users Users.query.all() ``` - ## Run the App It's time to use docker-compose to bring up the application. @@ -104,6 +105,7 @@ docker-compose logs -f ## Stopping the App To shutdown Faculty Tools + ```sh docker-compose down -``` \ No newline at end of file +``` From 98ee7b9ea30bcb8d60489a442ac7b8c8f5413ce4 Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Wed, 7 Apr 2021 12:29:48 -0400 Subject: [PATCH 12/52] Trying with nginx and uwsgi files --- Dockerfile | 2 ++ README.md | 5 +++-- devops/nginx.conf | 44 ++++++++++++++++++++++++++++++++++++++++++++ devops/uwsgi.ini | 4 ++++ main.py => lti.py | 0 5 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 devops/nginx.conf create mode 100644 devops/uwsgi.ini rename main.py => lti.py (100%) diff --git a/Dockerfile b/Dockerfile index 95af5ae..227b4da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,8 @@ RUN apt-get -y install ca-certificates WORKDIR /app COPY requirements.txt /app/ COPY test_requirements.txt /app/ +COPY devops/nginx.conf /app +COPY devops/uwsgi.ini /app RUN echo $REQUIREMENTS RUN pip install -r $REQUIREMENTS COPY ./ /app/ diff --git a/README.md b/README.md index 525492f..bd065da 100644 --- a/README.md +++ b/README.md @@ -90,9 +90,10 @@ It's time to use docker-compose to bring up the application. docker-compose up -d ``` -Go to the /xml page, [http://127.0.0.1:9001/xml](http://127.0.0.1:9001/xml) by default +Go to the /xml page, [http://127.0.0.1:9001/xml](http://127.0.0.1:9001/facultytools/xml) +by default. -Copy the xml, install it into a course. +Copy the xml, and install it into a course (Course->Settings->Apps). ## View the Logs diff --git a/devops/nginx.conf b/devops/nginx.conf new file mode 100644 index 0000000..8733dd1 --- /dev/null +++ b/devops/nginx.conf @@ -0,0 +1,44 @@ +user nginx; +worker_processes 1; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; +events { + worker_connections 1024; +} +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; + sendfile on; + keepalive_timeout 65; + + server { + listen 80; + location /facultytools/ { + try_files $uri @app; + } + + location @app { + include uwsgi_params; + uwsgi_pass unix:///tmp/uwsgi.sock; + uwsgi_param SCRIPT_NAME /facultytools/; + uwsgi_modifier1 30; + uwsgi_read_timeout 300; + uwsgi_connect_timeout 300; + uwsgi_send_timeout 300; + proxy_redirect off; + + } + + location /facultytools/static { + alias /app/static; + } + } + +} +daemon off; + + diff --git a/devops/uwsgi.ini b/devops/uwsgi.ini new file mode 100644 index 0000000..b0a0bd4 --- /dev/null +++ b/devops/uwsgi.ini @@ -0,0 +1,4 @@ +[uwsgi] +module = lti +callable = app +route-run = fixpathinfo: diff --git a/main.py b/lti.py similarity index 100% rename from main.py rename to lti.py From a5d3d5f303cf8afbd1e57c7604264311e96bbebc Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Wed, 7 Apr 2021 14:31:37 -0400 Subject: [PATCH 13/52] README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index bd065da..283d707 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,7 @@ It's time to use docker-compose to bring up the application. docker-compose up -d ``` -Go to the /xml page, [http://127.0.0.1:9001/xml](http://127.0.0.1:9001/facultytools/xml) -by default. +Go to the /xml page, [http://127.0.0.1:9001/xml](http://127.0.0.1:9001/facultytools/xml) by default. Copy the xml, and install it into a course (Course->Settings->Apps). From 71e6278dc18d96a523d6a4b3c32656e71b47c43f Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Wed, 7 Apr 2021 15:14:03 -0400 Subject: [PATCH 14/52] README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 283d707..1bd6034 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ It's time to use docker-compose to bring up the application. docker-compose up -d ``` -Go to the /xml page, [http://127.0.0.1:9001/xml](http://127.0.0.1:9001/facultytools/xml) by default. +Go to the /xml page, <http://127.0.0.1:9001/facultytools/xml> by default. Copy the xml, and install it into a course (Course->Settings->Apps). From 2ec1f22ba834a8be27119f56d89705ed1348e900 Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Wed, 7 Apr 2021 15:26:12 -0400 Subject: [PATCH 15/52] Load ENV? --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 952e7d1..80b5ebb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,7 @@ jobs: - name: Setup Repo run: | cp whitelist.json.template whitelist.json + cp .env.template .env mkdir logs; touch logs/faculty-tools.log - name: Run flake8 run: flake8 @@ -32,5 +33,7 @@ jobs: run: black --check . - name: Lint markdown files uses: bewuethr/mdl-action@v1 + - name: Load dotenv + uses: falti/dotenv-action@v0.2.5 - name: Run unittests run: coverage run -m unittest discover From 58952fd9adf9f3370616c8ce4580371e690c932e Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Wed, 7 Apr 2021 15:46:38 -0400 Subject: [PATCH 16/52] .env --- .github/workflows/ci.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80b5ebb..4bdd071 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,6 @@ jobs: python -m pip install --upgrade pip pip install -r test_requirements.txt pip install coveralls - # gem install mdl - name: Setup Repo run: | cp whitelist.json.template whitelist.json @@ -33,7 +32,12 @@ jobs: run: black --check . - name: Lint markdown files uses: bewuethr/mdl-action@v1 - - name: Load dotenv - uses: falti/dotenv-action@v0.2.5 + # - name: Load dotenv + # uses: falti/dotenv-action@v0.2.5 + - name: Environment Variables from Dotenv + uses: c-py/action-dotenv-to-setenv@v3 + - name: Print Repo + run: | + env - name: Run unittests run: coverage run -m unittest discover From 8d8cf3150b6bd3711329b0e06484d9d3a294e8e5 Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Tue, 20 Apr 2021 13:27:19 -0400 Subject: [PATCH 17/52] Fixing status test --- .travis.yml | 23 ----------------------- lti.py | 3 ++- 2 files changed, 2 insertions(+), 24 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 802f960..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: python -matrix: - include: - - python: 3.7 - - python: 3.8 -install: -- pip install -r test_requirements.txt -- pip install coveralls -- gem install mdl -before_script: -- cp settings.py.template settings.py; cp whitelist.json.template whitelist.json -- mkdir logs; touch logs/faculty-tools.log -script: -- flake8 -- black --check . -- mdl . -- coverage run -m unittest discover -after_success: -- coveralls -notifications: - slack: - rooms: - secure: F3YANiuNHZjtbgiJC8M1JOKBKhct2EyDEkCitW4FMwrauV0G5XcWKqtqLyRzSchI1LUALn+Dnj032ElGEx7D7cJrXlqC700UjQ4wXv938GQObVnRfTVCVrsktpr5gf077dIXvwcYJYt9ZSu4uIyk+HqyNnHLX4qIL0zhFR8HytoOeXVdik35SQoLJLvorgf4EGfqU8Yo25LUJArp2AB7RceAiMg3QXmi+nDHumFFczURexaYXIDBrRYTyZgfpYP245HOUmEf/LD6G53e6FU+8hiISYr7nE0hTkNkj3U4WNga25//9VIdpjWW8VWd+G7vf8CuhzHYuWEtredoVqNnJDwKE/MLhixleA+1lAEUypAEp+0k3+zMfTk748gdl1buJ/kINjoNqjhLv6MtDH/YTw/eQKEXA+V+odoudDiHUCztHQbCaIXYmIDFnebzO9u/Gz7QJ7PfpBlmUASQru5qTFPL0tmHP/w6/zrog0n07+uwl4qo2d6qxslgtmw4+K0VxGXl1Z8STArEgD6a8KoTZ1N8XDFF0E7KXE3kGYEtLHNoV7Z9OaohB3AFwJaKPTytYyIQ+OPvzYmpyUwRGIWBfRIP7t9qemyOExbYoinw0rF7jCMm7T3gLxkwxHNOCt+Gc1kwfLvuDy8QhQR2iHspMM8NeuDQKVzK4L3FeLx7bTo= diff --git a/lti.py b/lti.py index 3b98987..1b317d6 100644 --- a/lti.py +++ b/lti.py @@ -16,6 +16,7 @@ send_from_directory, ) from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import text import jinja2 from pylti.flask import lti import requests @@ -286,7 +287,7 @@ def status(): # Check DB connection try: - db.session.query("1").all() + db.session.query(text("1")).all() status["checks"]["db"] = True except Exception: app.logger.exception("DB connection failed.") From 0851770adfeb5637f3dd37f80de469ab35ce7df4 Mon Sep 17 00:00:00 2001 From: Shea Silverman <shea.silverman@ucf.edu> Date: Tue, 20 Apr 2021 15:19:42 -0400 Subject: [PATCH 18/52] Key / Secret env template --- .env.template | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.template b/.env.template index 9277768..8e2e4e5 100644 --- a/.env.template +++ b/.env.template @@ -2,8 +2,8 @@ TOOL_TITLE=Faculty Tools THEME_DIR= BASE_CANVAS_SERVER_URL=https://example.com/ SECRET_KEY=CHANGEME -LTI_KEY=CHANGEME -LTI_SECRET=CHANGEME +LTI_KEY=key +LTI_SECRET=secret OAUTH2_URI=https://127.0.0.1:9001/oauthlogin OAUTH2_ID=CHANGEME OAUTH2_KEY=CHANGEME From d916489dfeeafb91e24d48638544ceaac65205e2 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Tue, 29 Mar 2022 08:52:51 -0400 Subject: [PATCH 19/52] Better dockeriziation --- Dockerfile | 13 +++++-------- devops/nginx.conf | 44 -------------------------------------------- devops/uwsgi.ini | 4 ---- docker-compose.yml | 5 ++--- gunicorn_conf.py | 9 +++++++++ requirements.txt | 14 +++++++------- settings.py | 2 +- 7 files changed, 24 insertions(+), 67 deletions(-) delete mode 100644 devops/nginx.conf delete mode 100644 devops/uwsgi.ini create mode 100644 gunicorn_conf.py diff --git a/Dockerfile b/Dockerfile index 227b4da..cbfdd11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,9 @@ -FROM tiangolo/uwsgi-nginx-flask:python3.7 +FROM python:3.7 as base ARG REQUIREMENTS -RUN apt-get update && apt-get -y install libffi-dev gcc python3-dev libffi-dev libssl-dev libxml2-dev libxmlsec1-dev libxmlsec1-openssl -RUN apt-get -y install ca-certificates -WORKDIR /app COPY requirements.txt /app/ COPY test_requirements.txt /app/ -COPY devops/nginx.conf /app -COPY devops/uwsgi.ini /app -RUN echo $REQUIREMENTS -RUN pip install -r $REQUIREMENTS +RUN pip install -r /app/$REQUIREMENTS +WORKDIR /app COPY ./ /app/ +EXPOSE 9001 +CMD ["gunicorn", "--conf", "gunicorn_conf.py", "--bind", "0.0.0.0:9001", "lti:app"] \ No newline at end of file diff --git a/devops/nginx.conf b/devops/nginx.conf deleted file mode 100644 index 8733dd1..0000000 --- a/devops/nginx.conf +++ /dev/null @@ -1,44 +0,0 @@ -user nginx; -worker_processes 1; -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; -events { - worker_connections 1024; -} -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - access_log /var/log/nginx/access.log main; - sendfile on; - keepalive_timeout 65; - - server { - listen 80; - location /facultytools/ { - try_files $uri @app; - } - - location @app { - include uwsgi_params; - uwsgi_pass unix:///tmp/uwsgi.sock; - uwsgi_param SCRIPT_NAME /facultytools/; - uwsgi_modifier1 30; - uwsgi_read_timeout 300; - uwsgi_connect_timeout 300; - uwsgi_send_timeout 300; - proxy_redirect off; - - } - - location /facultytools/static { - alias /app/static; - } - } - -} -daemon off; - - diff --git a/devops/uwsgi.ini b/devops/uwsgi.ini deleted file mode 100644 index b0a0bd4..0000000 --- a/devops/uwsgi.ini +++ /dev/null @@ -1,4 +0,0 @@ -[uwsgi] -module = lti -callable = app -route-run = fixpathinfo: diff --git a/docker-compose.yml b/docker-compose.yml index 751f9ca..e5b3ca2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,16 +7,15 @@ services: args: - "REQUIREMENTS=${REQUIREMENTS}" ports: - - "9001:80" + - "9001:9001" env_file: - .env - environment: - - MODULE_NAME=app depends_on: - db db: image: mysql:5.7 + platform: linux/amd64 volumes: - ft_dbdata:/var/lib/mysql restart: always diff --git a/gunicorn_conf.py b/gunicorn_conf.py new file mode 100644 index 0000000..cad3455 --- /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 diff --git a/requirements.txt b/requirements.txt index 38b85d3..f189721 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -canvasapi==0.15.0 -Flask==1.1.1 -Flask-SQLAlchemy==2.5.1 -mysqlclient -git+https://github.com/ucfcdl/pylti.git@roles#egg=PyLTI -requests==2.22.0 -Werkzeug>=1.0.1 # Chrome 80 SameSite fix +Flask==2.0.3 +-e git+https://github.com/ucfcdl/pylti.git@roles#egg=PyLTI +requests==2.23.0 +gunicorn==20.1.0 +mysqlclient==2.1.0 +Flask-SQLAlchemy +canvasapi==2.2.0 \ No newline at end of file diff --git a/settings.py b/settings.py index 2c32b39..53112a1 100644 --- a/settings.py +++ b/settings.py @@ -8,7 +8,7 @@ # Canvas instance URL. ex: https://example.instructure.com/ BASE_URL = os.environ.get("BASE_CANVAS_SERVER_URL") -API_URL = BASE_URL + "api/v1/" +API_URL = BASE_URL # Secret key to sign Flask sessions with. KEEP THIS SECRET! secret_key = os.environ.get("SECRET_KEY") From db4b2d474b43ba432000f2bd74fe28097669a134 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Wed, 30 Mar 2022 17:10:29 -0400 Subject: [PATCH 20/52] Fixed linting --- requirements.txt | 12 ++++++++---- settings.py | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index f189721..246acc5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,11 @@ -Flask==2.0.3 +Flask==2.0.1 -e git+https://github.com/ucfcdl/pylti.git@roles#egg=PyLTI -requests==2.23.0 +requests==2.27.1 gunicorn==20.1.0 mysqlclient==2.1.0 -Flask-SQLAlchemy -canvasapi==2.2.0 \ No newline at end of file +Flask-SQLAlchemy==2.5.1 +canvasapi==2.2.0 +Werkzeug==2.0.2 +itsdangerous==2.0.1 +Jinja2==3.0.2 +MarkupSafe==2.0.1 \ No newline at end of file diff --git a/settings.py b/settings.py index 53112a1..e94b246 100644 --- a/settings.py +++ b/settings.py @@ -7,8 +7,8 @@ THEME_DIR = os.environ.get("THEME_DIR", "") # Canvas instance URL. ex: https://example.instructure.com/ -BASE_URL = os.environ.get("BASE_CANVAS_SERVER_URL") -API_URL = BASE_URL +BASE_URL = os.environ.get("BASE_CANVAS_SERVER_URL", "https://example.com") +API_URL = BASE_URL + "api/v1/" # Secret key to sign Flask sessions with. KEEP THIS SECRET! secret_key = os.environ.get("SECRET_KEY") From 30b4620a24cbc1502bd29457f502a3e13b01a159 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Thu, 31 Mar 2022 15:26:49 -0400 Subject: [PATCH 21/52] testing github docker image push --- .github/workflows/public_image.yml | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/public_image.yml diff --git a/.github/workflows/public_image.yml b/.github/workflows/public_image.yml new file mode 100644 index 0000000..b14f923 --- /dev/null +++ b/.github/workflows/public_image.yml @@ -0,0 +1,46 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Create and publish a Docker image + +on: + push: + branches: ['issue/21-dockerize'] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - 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 }} \ No newline at end of file From 9c523bb5201ac12de38863afcaa4fabf362370e6 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Thu, 31 Mar 2022 15:32:09 -0400 Subject: [PATCH 22/52] req + on needs --- .github/workflows/public_image.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/public_image.yml b/.github/workflows/public_image.yml index b14f923..9f70097 100644 --- a/.github/workflows/public_image.yml +++ b/.github/workflows/public_image.yml @@ -5,13 +5,18 @@ name: Create and publish a Docker image + +# Only trigger, when the build workflow succeeded on: - push: - branches: ['issue/21-dockerize'] + workflow_run: + workflows: ["Run Python Tests"] + types: + - completed env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} + REQUIREMENTS: requirements.txt jobs: build-and-push-image: From 1b56e91a3a82d7c68a7abeb66f3acfad9ed1a0fb Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Thu, 31 Mar 2022 15:57:56 -0400 Subject: [PATCH 23/52] CICD --- .github/workflows/ci.yml | 34 +++++++++++++++++++- .github/workflows/public_image.yml | 51 ------------------------------ 2 files changed, 33 insertions(+), 52 deletions(-) delete mode 100644 .github/workflows/public_image.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bdd071..040cfb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,5 @@ -name: Run Python Tests +name: Run Python Tests and Build Image + on: push: branches: @@ -7,6 +8,14 @@ on: branches: - issue/21-dockerize +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + REQUIREMENTS: requirements.txt + +: + + jobs: build: runs-on: ubuntu-latest @@ -41,3 +50,26 @@ jobs: env - 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 }} + diff --git a/.github/workflows/public_image.yml b/.github/workflows/public_image.yml deleted file mode 100644 index 9f70097..0000000 --- a/.github/workflows/public_image.yml +++ /dev/null @@ -1,51 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Create and publish a Docker image - - -# Only trigger, when the build workflow succeeded -on: - workflow_run: - workflows: ["Run Python Tests"] - types: - - completed - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - REQUIREMENTS: requirements.txt - -jobs: - build-and-push-image: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - 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 }} \ No newline at end of file From 5c8096a1fd3a91f01c5999a5692715c7ad885f51 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Thu, 31 Mar 2022 16:00:25 -0400 Subject: [PATCH 24/52] cicd --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 040cfb6..a1cc571 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,9 +13,6 @@ env: IMAGE_NAME: ${{ github.repository }} REQUIREMENTS: requirements.txt -: - - jobs: build: runs-on: ubuntu-latest From 912864c3a1df6ef9e2a1b353d85db54bc6d478ed Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Thu, 31 Mar 2022 16:06:05 -0400 Subject: [PATCH 25/52] requirements? --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1cc571..5efb216 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,4 +69,5 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + REQUIREMENTS: ${{ env.REQUIREMENTS }} From 52da8103c36fb7300695e1e51d1fdef3c08bdfb6 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Fri, 1 Apr 2022 15:52:02 -0400 Subject: [PATCH 26/52] Env? --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5efb216..76fd847 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,5 +69,5 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - REQUIREMENTS: ${{ env.REQUIREMENTS }} + REQUIREMENTS: requirements.txt From 10629f9cbeaa8340c5fad0e1fd292932b02bfd24 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Fri, 1 Apr 2022 15:59:34 -0400 Subject: [PATCH 27/52] Env? --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76fd847..12721ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,5 +69,9 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + env: REQUIREMENTS: requirements.txt + arg: + REQUIREMENTS: requirements.txt + From aaef3987ff0792306f7fd5ad8fcff55065062bae Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Fri, 1 Apr 2022 16:00:51 -0400 Subject: [PATCH 28/52] Env? --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12721ec..b3d21f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,7 +71,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} env: REQUIREMENTS: requirements.txt - arg: - REQUIREMENTS: requirements.txt + # arg: + # REQUIREMENTS: requirements.txt From b4902aa16cfe9c46fc2c5e3b21b98b42ea31c67e Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Tue, 5 Apr 2022 16:11:37 -0400 Subject: [PATCH 29/52] build args --- .github/workflows/ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3d21f0..8593ef3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,9 +69,7 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - env: - REQUIREMENTS: requirements.txt - # arg: - # REQUIREMENTS: requirements.txt + build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} + From d87cfa67f4a537e7a525df8a11d503c8d3a886cd Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Tue, 12 Apr 2022 09:48:40 -0400 Subject: [PATCH 30/52] adding git hash to ci --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8593ef3..1b1d028 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,7 +68,8 @@ jobs: context: . push: true tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + # labels: ${{ steps.meta.outputs.labels }} + labels: ${GITHUB_SHA} build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} From 4af0af2e67115bd61708095a73cb5c95c5f6045f Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Tue, 12 Apr 2022 09:55:33 -0400 Subject: [PATCH 31/52] sha --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b1d028..1036e0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,9 +67,8 @@ jobs: with: context: . push: true - tags: ${{ steps.meta.outputs.tags }} - # labels: ${{ steps.meta.outputs.labels }} - labels: ${GITHUB_SHA} + tags: sha + labels: ${{ steps.meta.outputs.labels }} build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} From e3e186551ab74b0bba7219e230eebc9f7bc03d74 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Tue, 12 Apr 2022 10:04:01 -0400 Subject: [PATCH 32/52] sha --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1036e0a..f30c7a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: with: context: . push: true - tags: sha + tags: ${{ steps.meta.outputs.tags }}, ${{ GITHUB_SHA }} labels: ${{ steps.meta.outputs.labels }} build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} From f7750fe60c3b4a3f1c15fd305266c2fde87a4da5 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Tue, 12 Apr 2022 10:07:31 -0400 Subject: [PATCH 33/52] sha --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f30c7a9..44cecdb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: with: context: . push: true - tags: ${{ steps.meta.outputs.tags }}, ${{ GITHUB_SHA }} + tags: ${{ steps.meta.outputs.tags }}, ${{ github.sha }} labels: ${{ steps.meta.outputs.labels }} build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} From 11ae1047fdc639eefb14dd0cee046d731495d8f9 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Tue, 12 Apr 2022 10:15:03 -0400 Subject: [PATCH 34/52] sha --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44cecdb..87fdf97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,13 +61,16 @@ jobs: uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # minimal (short sha) + type=sha - name: Build and push Docker image uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc with: context: . push: true - tags: ${{ steps.meta.outputs.tags }}, ${{ github.sha }} + tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} From b6ecd9f2ab49c5dd5ff2ac7621f73be39dc49f77 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Tue, 12 Apr 2022 10:29:11 -0400 Subject: [PATCH 35/52] sha --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87fdf97..7a61b31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,6 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - # minimal (short sha) type=sha - name: Build and push Docker image From 3c3f49d2c9d3e13ea3a6d475394dc6fd4b181d07 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Wed, 13 Apr 2022 15:16:41 -0400 Subject: [PATCH 36/52] using tags based on branch --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a61b31..8593ef3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,8 +61,6 @@ jobs: uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=sha - name: Build and push Docker image uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc From e39ee497a0a3d79f5f4083a159381492e9b29676 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Wed, 13 Apr 2022 15:26:59 -0400 Subject: [PATCH 37/52] Log handler --- .github/workflows/ci.yml | 4 ++++ lti.py | 11 +++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8593ef3..3840610 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,13 @@ on: push: branches: - issue/21-dockerize + - develop + - master pull_request: branches: - issue/21-dockerize + - develop + - master env: REGISTRY: ghcr.io diff --git a/lti.py b/lti.py index 1b317d6..c756d4f 100644 --- a/lti.py +++ b/lti.py @@ -1,7 +1,8 @@ +import logging from logging import Formatter, INFO -from logging.handlers import RotatingFileHandler import json import os +import sys import time from canvasapi.exceptions import CanvasException @@ -45,11 +46,7 @@ def select_theme_dirs(): app.jinja_loader = jinja2.ChoiceLoader([jinja2.FileSystemLoader(theme_dirs)]) # Logging -handler = RotatingFileHandler( - settings.ERROR_LOG, - maxBytes=settings.LOG_MAX_BYTES, - backupCount=settings.LOG_BACKUP_COUNT, -) +handler = logging.StreamHandler(sys.stdout) handler.setLevel(INFO) handler.setFormatter( Formatter( @@ -60,6 +57,8 @@ def select_theme_dirs(): app.logger.addHandler(handler) + + # DB Model class Users(db.Model): id = db.Column(db.Integer, primary_key=True) From be52aeb303f69476084891150c85d6f5b4cc6c75 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Wed, 13 Apr 2022 16:00:59 -0400 Subject: [PATCH 38/52] flake8 linting --- lti.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lti.py b/lti.py index c756d4f..594987c 100644 --- a/lti.py +++ b/lti.py @@ -57,8 +57,6 @@ def select_theme_dirs(): app.logger.addHandler(handler) - - # DB Model class Users(db.Model): id = db.Column(db.Integer, primary_key=True) From fa302998dd97e050eba5fd73b54e5b10e6335a3d Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Thu, 14 Apr 2022 12:43:02 -0400 Subject: [PATCH 39/52] changed env to remove https for demo --- .env.template | 2 +- .github/workflows/ci.yml | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.env.template b/.env.template index 8e2e4e5..ce374f0 100644 --- a/.env.template +++ b/.env.template @@ -4,7 +4,7 @@ BASE_CANVAS_SERVER_URL=https://example.com/ SECRET_KEY=CHANGEME LTI_KEY=key LTI_SECRET=secret -OAUTH2_URI=https://127.0.0.1:9001/oauthlogin +OAUTH2_URI=http://127.0.0.1:9001/oauthlogin OAUTH2_ID=CHANGEME OAUTH2_KEY=CHANGEME GOOGLE_ANALYTICS=GA-000000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3840610..9d11d4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,11 +6,6 @@ on: - issue/21-dockerize - develop - master - pull_request: - branches: - - issue/21-dockerize - - develop - - master env: REGISTRY: ghcr.io From 6c2fd0c54c04f815d2435b7cf4083046b7f8cb0b Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Mon, 18 Apr 2022 13:00:15 -0400 Subject: [PATCH 40/52] Passing tests --- .env.template | 1 + .github/workflows/ci.yml | 1 - config.py | 64 +++++++++++++++++++++++++-- lti.py | 82 +++++++++++++++++----------------- settings.py | 51 --------------------- tests.py | 95 ++++++++++++++++++++++++++++------------ utils.py | 6 +-- 7 files changed, 171 insertions(+), 129 deletions(-) delete mode 100644 settings.py diff --git a/.env.template b/.env.template index ce374f0..e452872 100644 --- a/.env.template +++ b/.env.template @@ -13,3 +13,4 @@ DATABASE_URI=mysql://root:secret@db/faculty_tools REQUIREMENTS=test_requirements.txt +WHITELIST_JSON=whitelist.json \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d11d4a..f401391 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,6 @@ jobs: run: | cp whitelist.json.template whitelist.json cp .env.template .env - mkdir logs; touch logs/faculty-tools.log - name: Run flake8 run: flake8 - name: Run black diff --git a/config.py b/config.py index 059501c..50443b8 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,3 @@ -import settings import os @@ -6,7 +5,23 @@ class Config(object): # make the warning shut up until Flask-SQLAlchemy v3 comes out SQLALCHEMY_TRACK_MODIFICATIONS = True - PYLTI_CONFIG = settings.PYLTI_CONFIG + # LTI consumer key and shared secret + CONSUMER_KEY = os.environ.get("LTI_KEY") + SHARED_SECRET = os.environ.get("LTI_SECRET") + + # Configuration for pylti library. Uses the above key and secret + PYLTI_CONFIG = { + "consumers": {CONSUMER_KEY: {"secret": SHARED_SECRET}}, + # Custom configurable roles + "roles": { + "staff": [ + "urn:lti:instrole:ims/lis/Administrator", + "Instructor", + "ContentDeveloper", + "urn:lti:role:ims/lis/TeachingAssistant", + ] + }, + } SESSION_COOKIE_NAME = "ft_session" @@ -24,8 +39,6 @@ class BaseConfig(object): # make the warning shut up until Flask-SQLAlchemy v3 comes out SQLALCHEMY_TRACK_MODIFICATIONS = True - PYLTI_CONFIG = settings.PYLTI_CONFIG - SESSION_COOKIE_NAME = "ft_session" # Chrome 80 SameSite=None; Secure fix @@ -34,6 +47,49 @@ class BaseConfig(object): SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI") + # Title of the tool. Appears in the <title> element, headers, and configuration XML + TOOL_TITLE = os.environ.get("TOOL_TITLE", "Faculty Tools") + + # Which theme directory to use. Leave blank for default. + THEME_DIR = os.environ.get("THEME_DIR", "") + + # Canvas instance URL. ex: https://example.instructure.com/ + BASE_URL = os.environ.get("BASE_CANVAS_SERVER_URL", "https://example.com") + API_URL = BASE_URL + "api/v1/" + + # Secret key to sign Flask sessions with. KEEP THIS SECRET! + SECRET_KEY = os.environ.get("SECRET_KEY") + + # LTI consumer key and shared secret + CONSUMER_KEY = os.environ.get("LTI_KEY") + SHARED_SECRET = os.environ.get("LTI_SECRET") + + # Configuration for pylti library. Uses the above key and secret + PYLTI_CONFIG = { + "consumers": {CONSUMER_KEY: {"secret": SHARED_SECRET}}, + # Custom configurable roles + "roles": { + "staff": [ + "urn:lti:instrole:ims/lis/Administrator", + "Instructor", + "ContentDeveloper", + "urn:lti:role:ims/lis/TeachingAssistant", + ] + }, + } + + # The "Oauth2 Redirect URI" that you provided to Instructure. + OAUTH2_URI = os.environ.get("OAUTH2_URI") # ex. 'https://localhost:5000/oauthlogin' + # The Client_ID Instructure gave you + OAUTH2_ID = os.environ.get("OAUTH2_ID") + # The Secret Instructure gave you + OAUTH2_KEY = os.environ.get("OAUTH2_KEY") + + WHITELIST = os.environ.get("WHITELIST_JSON") + + # Google Analytics Tracking ID (optional) + GOOGLE_ANALYTICS = os.environ.get("GOOGLE_ANALYTICS", "GA-") + class DevelopmentConfig(BaseConfig): DEBUG = True diff --git a/lti.py b/lti.py index 594987c..72bb902 100644 --- a/lti.py +++ b/lti.py @@ -24,11 +24,11 @@ from requests.exceptions import HTTPError from utils import filter_tool_list, slugify -import settings app = Flask(__name__) -app.config.from_object(settings.configClass) -app.secret_key = settings.secret_key +app.config.from_object(os.environ.get("CONFIG", "config.DevelopmentConfig")) +app.secret_key = app.config['SECRET_KEY'] + db = SQLAlchemy(app) @@ -36,8 +36,8 @@ def select_theme_dirs(): """ Load theme templates, if applicable """ - if settings.THEME_DIR: - return ["themes/" + settings.THEME_DIR + "/templates", "templates"] + if app.config['THEME_DIR']: + return ["themes/" + app.config['THEME_DIR'] + "/templates", "templates"] else: return ["templates"] @@ -77,7 +77,7 @@ def __repr__(self): @app.context_processor def ga_utility_processor(): def google_analytics(): - return settings.GOOGLE_ANALYTICS + return app.config['GOOGLE_ANALYTICS'] return dict(google_analytics=google_analytics()) @@ -85,7 +85,7 @@ def google_analytics(): @app.context_processor def title_utility_processor(): def title(): - return settings.TOOL_TITLE + return app.config['TOOL_TITLE'] return dict(title=title()) @@ -93,13 +93,13 @@ def title(): @app.context_processor def theme_static_files_processor(): def theme_static_files(folder): - if not settings.THEME_DIR: + if not app.config['THEME_DIR']: return list() try: all_files = os.listdir( "themes/{theme_dir}/static/{folder}".format( - theme_dir=settings.THEME_DIR, folder=folder + theme_dir=app.config['THEME_DIR'], folder=folder ) ) @@ -142,7 +142,7 @@ def error(exception=None): @app.route("/themes/static/<path:filename>") def theme_static(filename): # pragma: nocover - static_dir = "themes/{theme_dir}/static".format(theme_dir=settings.THEME_DIR) + static_dir = "themes/{theme_dir}/static".format(theme_dir=app.config['THEME_DIR']) return send_from_directory(static_dir, filename) @@ -165,7 +165,7 @@ def index(lti=lti): # Test API key to see if they need to reauthenticate auth_header = {"Authorization": "Bearer " + session["api_key"]} - r = requests.get(settings.API_URL + "users/self", headers=auth_header) + r = requests.get(app.config['API_URL'] + "users/self", headers=auth_header) if "WWW-Authenticate" in r.headers: # reroll oauth app.logger.info( @@ -176,11 +176,11 @@ def index(lti=lti): ) return redirect( - settings.BASE_URL + app.config['BASE_URL'] + "login/oauth2/auth?client_id=" - + settings.oauth2_id + + app.config['OAUTH2_ID'] + "&response_type=code&redirect_uri=" - + settings.oauth2_uri + + app.config['OAUTH2_URI'] ) if "WWW-Authenticate" not in r.headers and r.status_code == 401: @@ -208,15 +208,15 @@ def index(lti=lti): ) ) return redirect( - settings.BASE_URL + app.config['BASE_URL'] + "login/oauth2/auth?client_id=" - + settings.oauth2_id + + app.config['OAUTH2_ID'] + "&response_type=code&redirect_uri=" - + settings.oauth2_uri + + app.config['OAUTH2_URI'] ) r = requests.get( - settings.API_URL + app.config['API_URL'] + "courses/{0}/external_tools?include_parents=true&per_page=100".format( session["course_id"] ), @@ -259,7 +259,7 @@ def status(): "checks": {"index": False, "xml": False, "db": False, "dev_key": False}, "url": url_for("index", _external=True), "xml_url": url_for("xml", _external=True), - "base_url": settings.BASE_URL, + "base_url": app.config['BASE_URL'], "debug": app.debug, } @@ -267,7 +267,7 @@ def status(): try: response = requests.get(url_for("index", _external=True), verify=False) index_check = ( - response.status_code == 200 and settings.TOOL_TITLE in response.text + response.status_code == 200 and app.config['TOOL_TITLE'] in response.text ) status["checks"]["index"] = index_check except Exception: @@ -293,7 +293,7 @@ def status(): try: response = requests.get( "{}login/oauth2/auth?client_id={}&response_type=code&redirect_uri={}".format( - settings.BASE_URL, settings.oauth2_id, settings.oauth2_uri + app.config['BASE_URL'], app.config['OAUTH2_ID'], app.config['OAUTH2_URI'] ) ) status["checks"]["dev_key"] = response.status_code == 200 @@ -335,12 +335,12 @@ def oauth_login(lti=lti): payload = { "grant_type": "authorization_code", - "client_id": settings.oauth2_id, - "redirect_uri": settings.oauth2_uri, - "client_secret": settings.oauth2_key, + "client_id": app.config['OAUTH2_ID'], + "redirect_uri": app.config['OAUTH2_URI'], + "client_secret": app.config['OAUTH2_KEY'], "code": code, } - r = requests.post(settings.BASE_URL + "login/oauth2/token", data=payload) + r = requests.post(app.config['BASE_URL'] + "login/oauth2/token", data=payload) try: r.raise_for_status() @@ -440,12 +440,12 @@ def refresh_access_token(user): payload = { "grant_type": "refresh_token", - "client_id": settings.oauth2_id, - "redirect_uri": settings.oauth2_uri, - "client_secret": settings.oauth2_key, + "client_id": app.config['OAUTH2_ID'], + "redirect_uri": app.config['OAUTH2_URI'], + "client_secret": app.config['OAUTH2_KEY'], "refresh_token": refresh_token, } - response = requests.post(settings.BASE_URL + "login/oauth2/token", data=payload) + response = requests.post(app.config['BASE_URL'] + "login/oauth2/token", data=payload) try: response.raise_for_status() @@ -533,11 +533,11 @@ def auth(lti=lti): ) ) return redirect( - settings.BASE_URL + app.config['BASE_URL'] + "login/oauth2/auth?client_id=" - + settings.oauth2_id + + app.config['OAUTH2_ID'] + "&response_type=code&redirect_uri=" - + settings.oauth2_uri + + app.config['OAUTH2_URI'] ) # Get the expiration date @@ -565,17 +565,17 @@ def auth(lti=lti): # Refresh didn't work. Reauthenticate. app.logger.info("Reauthenticating:\nSession: {}".format(session)) return redirect( - settings.BASE_URL + app.config['BASE_URL'] + "login/oauth2/auth?client_id=" - + settings.oauth2_id + + app.config['OAUTH2_ID'] + "&response_type=code&redirect_uri=" - + settings.oauth2_uri + + app.config['OAUTH2_URI'] ) else: # API key that shouldn't be expired. Test it. auth_header = {"Authorization": "Bearer " + session["api_key"]} r = requests.get( - settings.API_URL + "users/%s/profile" % (session["canvas_user_id"]), + app.config['API_URL'] + "users/%s/profile" % (session["canvas_user_id"]), headers=auth_header, ) # check for WWW-Authenticate @@ -593,11 +593,11 @@ def auth(lti=lti): # Refresh didn't work. Reauthenticate. app.logger.info("Reauthenticating\nSession: {}".format(session)) return redirect( - settings.BASE_URL + app.config['BASE_URL'] + "login/oauth2/auth?client_id=" - + settings.oauth2_id + + app.config['OAUTH2_ID'] + "&response_type=code&redirect_uri=" - + settings.oauth2_uri + + app.config['OAUTH2_URI'] ) @@ -614,7 +614,7 @@ def get_sessionless_url(lti_id, is_course_nav, lti=lti): "&launch_type=course_navigation" ) r = requests.get( - url.format(settings.API_URL, session["course_id"], lti_id), + url.format(app.config['API_URL'], session["course_id"], lti_id), headers=auth_header, ) if r.status_code >= 400: @@ -638,7 +638,7 @@ def get_sessionless_url(lti_id, is_course_nav, lti=lti): auth_header = {"Authorization": "Bearer " + session["api_key"]} # get sessionless launch url r = requests.get( - settings.API_URL + app.config['API_URL'] + "courses/{0}/external_tools/sessionless_launch?id={1}".format( session["course_id"], lti_id ), diff --git a/settings.py b/settings.py deleted file mode 100644 index e94b246..0000000 --- a/settings.py +++ /dev/null @@ -1,51 +0,0 @@ -import os - -# Title of the tool. Appears in the <title> element, headers, and configuration XML -TOOL_TITLE = os.environ.get("TOOL_TITLE", "Faculty Tools") - -# Which theme directory to use. Leave blank for default. -THEME_DIR = os.environ.get("THEME_DIR", "") - -# Canvas instance URL. ex: https://example.instructure.com/ -BASE_URL = os.environ.get("BASE_CANVAS_SERVER_URL", "https://example.com") -API_URL = BASE_URL + "api/v1/" - -# Secret key to sign Flask sessions with. KEEP THIS SECRET! -secret_key = os.environ.get("SECRET_KEY") - -# LTI consumer key and shared secret -CONSUMER_KEY = os.environ.get("LTI_KEY") -SHARED_SECRET = os.environ.get("LTI_SECRET") - -# Configuration for pylti library. Uses the above key and secret -PYLTI_CONFIG = { - "consumers": {CONSUMER_KEY: {"secret": SHARED_SECRET}}, - # Custom configurable roles - "roles": { - "staff": [ - "urn:lti:instrole:ims/lis/Administrator", - "Instructor", - "ContentDeveloper", - "urn:lti:role:ims/lis/TeachingAssistant", - ] - }, -} - -# The "Oauth2 Redirect URI" that you provided to Instructure. -oauth2_uri = os.environ.get("OAUTH2_URI") # ex. 'https://localhost:5000/oauthlogin' -# The Client_ID Instructure gave you -oauth2_id = os.environ.get("OAUTH2_ID") -# The Secret Instructure gave you -oauth2_key = os.environ.get("OAUTH2_KEY") - -# Logging configuration -LOG_MAX_BYTES = 1024 * 1024 * 5 # 5 MB -LOG_BACKUP_COUNT = 2 -ERROR_LOG = "logs/faculty-tools.log" - -whitelist = "whitelist.json" - -# Google Analytics Tracking ID (optional) -GOOGLE_ANALYTICS = os.environ.get("GOOGLE_ANALYTICS", "GA-") - -configClass = os.environ.get("CONFIG", "config.DevelopmentConfig") diff --git a/tests.py b/tests.py index c7c09e4..11d5027 100644 --- a/tests.py +++ b/tests.py @@ -6,7 +6,7 @@ import canvasapi import oauthlib.oauth1 import flask -from flask import url_for +from flask import Flask, url_for import flask_testing import requests_mock from pylti.common import LTI_SESSION_KEY @@ -14,7 +14,6 @@ from mock import patch, mock_open import lti -import settings import utils @@ -34,11 +33,13 @@ def create_app(self): @classmethod def setUpClass(cls): logging.disable(logging.CRITICAL) - settings.BASE_URL = "https://example.edu/" - settings.oauth2_id = "10000000000001" - settings.oauth2_uri = "oauthlogin" - settings.GOOGLE_ANALYTICS = "123abc" - settings.THEME_DIR = "test_theme" + app = lti.app + app.config["BASE_URL"] = "https://example.edu/" + app.config["OAUTH2_ID"] = "10000000000001" + app.config["OAUTH2_URI"] = "oauthlogin" + app.config["GOOGLE_ANALYTICS"] = "123abc" + app.config["THEME_DIR"] = "test_theme" + def setUp(self): with self.app.test_request_context(): @@ -94,8 +95,10 @@ def test_select_theme_dirs(self, m): self.assertEqual(theme_dirs[0], "themes/test_theme/templates") self.assertEqual(theme_dirs[1], "templates") - @patch("settings.THEME_DIR", "") + # @patch('self.app.config["BASE_URL"]', "") def test_select_theme_dirs_no_theme(self, m): + self.app.config["BASE_URL"] = "" + self.app.config["THEME_DIR"] = "" theme_dirs = lti.select_theme_dirs() self.assertIsInstance(theme_dirs, list) @@ -115,6 +118,7 @@ def test__slugify_empty(self, m): @patch("os.listdir") def test_theme_static_files_processor(self, m, mocked_listdir): + self.app.config["THEME_DIR"] = "test_theme" mocked_listdir.return_value = ["file1.css", "file2.js"] files = lti.theme_static_files_processor() @@ -144,8 +148,9 @@ def test_theme_static_files_processor_oserror(self, m, mocked_listdir): self.assertIsInstance(files["theme_static_js"], list) self.assertEqual(len(files["theme_static_js"]), 0) - @patch("settings.THEME_DIR", "") - def test_heme_static_files_processor_no_theme(self, m): + # @patch('app.config["THEME_DIR"]', "") + def test_theme_static_files_processor_no_theme(self, m): + self.app.config["THEME_DIR"] = "" files = lti.theme_static_files_processor() self.assertIsInstance(files, dict) @@ -181,7 +186,7 @@ def test_ga_utility_processor(self, m): self.assertIsInstance(ga, dict) self.assertIn("google_analytics", ga) - self.assertEqual(ga["google_analytics"], settings.GOOGLE_ANALYTICS) + self.assertEqual(ga["google_analytics"], self.app.config["GOOGLE_ANALYTICS"]) # title_utility_processor def test_title_utility_processor(self, m): @@ -189,7 +194,7 @@ def test_title_utility_processor(self, m): self.assertIsInstance(title, dict) self.assertIn("title", title) - self.assertEqual(title["title"], settings.TOOL_TITLE) + self.assertEqual(title["title"], self.app.config["TOOL_TITLE"]) # return_error def test_return_error(self, m): @@ -260,7 +265,9 @@ def test_index_api_key_expired(self, m): self.assert_redirects( response, redirect_url.format( - settings.BASE_URL, settings.oauth2_id, settings.oauth2_uri + self.app.config["BASE_URL"], + self.app.config["OAUTH2_ID"], + self.app.config["OAUTH2_URI"] ), ) @@ -300,7 +307,9 @@ def test_index_api_key_404(self, m): self.assert_redirects( response, redirect_url.format( - settings.BASE_URL, settings.oauth2_id, settings.oauth2_uri + self.app.config["BASE_URL"], + self.app.config["OAUTH2_ID"], + self.app.config["OAUTH2_URI"] ), ) @@ -349,7 +358,7 @@ def test_index_whitelist_error(self, m, filter_tool_list): ], headers={ "Link": '<{}api/v1/courses/1/external_tools?page=2>; rel="next"'.format( - settings.BASE_URL + self.app.config["BASE_URL"] ) }, status_code=200, @@ -394,7 +403,7 @@ def test_index_canvas_error(self, m, filter_tool_list): ], headers={ "Link": '<{}api/v1/courses/1/external_tools?page=2>; rel="next"'.format( - settings.BASE_URL + self.app.config["BASE_URL"] ) }, status_code=200, @@ -432,7 +441,7 @@ def test_index(self, m): ], headers={ "Link": '<{}api/v1/courses/1/external_tools?page=2>; rel="next"'.format( - settings.BASE_URL + self.app.config["BASE_URL"] ) }, status_code=200, @@ -452,8 +461,10 @@ def test_index(self, m): # status def test_status_healthy(self, m): + self.app.config["BASE_URL"] = "https://example.edu/" + m.register_uri( - "GET", "http://localhost/", status_code=200, text=settings.TOOL_TITLE + "GET", "http://localhost/", status_code=200, text=self.app.config["TOOL_TITLE"] ) m.register_uri( "GET", @@ -908,7 +919,9 @@ def test_auth_no_user(self, m): self.assert_redirects( response, redirect_url.format( - settings.BASE_URL, settings.oauth2_id, settings.oauth2_uri + self.app.config["BASE_URL"], + self.app.config["OAUTH2_ID"], + self.app.config["OAUTH2_URI"] ), ) @@ -990,7 +1003,9 @@ def test_auth_no_api_key_refresh_fail(self, m, mock_refresh_access_token): self.assert_redirects( response, redirect_url.format( - settings.BASE_URL, settings.oauth2_id, settings.oauth2_uri + self.app.config["BASE_URL"], + self.app.config["OAUTH2_ID"], + self.app.config["OAUTH2_URI"] ), ) @@ -1088,7 +1103,9 @@ def test_auth_invalid_api_key_refresh_fail(self, m, mock_refresh_access_token): self.assert_redirects( response, redirect_url.format( - settings.BASE_URL, settings.oauth2_id, settings.oauth2_uri + self.app.config["BASE_URL"], + self.app.config["OAUTH2_ID"], + self.app.config["OAUTH2_URI"] ), ) @@ -1238,20 +1255,40 @@ def test_get_sessionless_url_not_course_nav_succeed(self, m): class UtilsTests(unittest.TestCase): + app = Flask('test') + app.config["WHITELIST"] = "whitelist.json" + app.config["BASE_URL"] = "https://example.edu/" + + # def create_app(self): + # # app = lti.app + # app.config["WHITELIST"] = "whitelist.json" + # app.config["BASE_URL"] = "https://example.edu/" + # return app + @classmethod def setUpClass(cls): - settings.BASE_URL = "https://example.edu/" - settings.whitelist = "whitelist.json" + app = lti.app + app.config["BASE_URL"] = "https://example.edu/" + app.config["WHITELIST"] = "whitelist.json" + return app + + # def setUp(self): + # app = lti.app + # app.config["BASE_URL"] = "https://example.edu/" + # app.config["WHITELIST"] = "whitelist.json" + # return app def test_filter_tool_list_empty_file(self): - with self.assertRaises(JSONDecodeError): - with patch("builtins.open", mock_open(read_data="")): - utils.filter_tool_list(1, "password") + with self.app.app_context(): + with self.assertRaises(JSONDecodeError): + with patch("builtins.open", mock_open(read_data="")): + utils.filter_tool_list(1, "password") def test_filter_tool_list_empty_data(self): - with self.assertRaisesRegex(ValueError, r"whitelist\.json is empty"): - with patch("builtins.open", mock_open(read_data="{}")): - utils.filter_tool_list(1, "password") + with self.app.app_context(): + with self.assertRaisesRegex(ValueError, r"whitelist\.json is empty"): + with patch("builtins.open", mock_open(read_data="{}")): + utils.filter_tool_list(1, "password") @patch("canvasapi.canvas.Canvas.get_course") @patch("canvasapi.course.Course.get_external_tools") diff --git a/utils.py b/utils.py index aca6e28..a2b8ff1 100644 --- a/utils.py +++ b/utils.py @@ -4,7 +4,7 @@ from canvasapi import Canvas -import settings +from flask import current_app def get_tool_info(whitelist, tool_name): @@ -37,13 +37,13 @@ def filter_tool_list(course_id, access_token): The values are a list of all installed external tools that are in that category and on the whitelist. """ - with open(settings.whitelist, "r") as wl_file: + with open(current_app.config['WHITELIST'], "r") as wl_file: whitelist = json.loads(wl_file.read()) if not whitelist: raise ValueError("whitelist.json is empty") - canvas = Canvas(settings.BASE_URL, access_token) + canvas = Canvas(current_app.config['BASE_URL'], access_token) course = canvas.get_course(course_id) installed_tools = course.get_external_tools(include_parents=True) From 6d80d5d23044ab7006913124a29ee2cbc4f2b411 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Mon, 18 Apr 2022 13:02:31 -0400 Subject: [PATCH 41/52] Tests passing --- tests.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests.py b/tests.py index 11d5027..c4d5e06 100644 --- a/tests.py +++ b/tests.py @@ -40,7 +40,6 @@ def setUpClass(cls): app.config["GOOGLE_ANALYTICS"] = "123abc" app.config["THEME_DIR"] = "test_theme" - def setUp(self): with self.app.test_request_context(): lti.db.create_all() @@ -1258,12 +1257,6 @@ class UtilsTests(unittest.TestCase): app = Flask('test') app.config["WHITELIST"] = "whitelist.json" app.config["BASE_URL"] = "https://example.edu/" - - # def create_app(self): - # # app = lti.app - # app.config["WHITELIST"] = "whitelist.json" - # app.config["BASE_URL"] = "https://example.edu/" - # return app @classmethod def setUpClass(cls): @@ -1271,12 +1264,6 @@ def setUpClass(cls): app.config["BASE_URL"] = "https://example.edu/" app.config["WHITELIST"] = "whitelist.json" return app - - # def setUp(self): - # app = lti.app - # app.config["BASE_URL"] = "https://example.edu/" - # app.config["WHITELIST"] = "whitelist.json" - # return app def test_filter_tool_list_empty_file(self): with self.app.app_context(): From 06d3a240744106ca5892f672822a22ff509aa14b Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Mon, 18 Apr 2022 13:20:25 -0400 Subject: [PATCH 42/52] Fixed formatting --- lti.py | 82 +++++++++++++++++++++++++++++--------------------------- tests.py | 17 +++++++----- utils.py | 4 +-- 3 files changed, 55 insertions(+), 48 deletions(-) diff --git a/lti.py b/lti.py index 72bb902..073ba0c 100644 --- a/lti.py +++ b/lti.py @@ -27,7 +27,7 @@ app = Flask(__name__) app.config.from_object(os.environ.get("CONFIG", "config.DevelopmentConfig")) -app.secret_key = app.config['SECRET_KEY'] +app.secret_key = app.config["SECRET_KEY"] db = SQLAlchemy(app) @@ -36,8 +36,8 @@ def select_theme_dirs(): """ Load theme templates, if applicable """ - if app.config['THEME_DIR']: - return ["themes/" + app.config['THEME_DIR'] + "/templates", "templates"] + if app.config["THEME_DIR"]: + return ["themes/" + app.config["THEME_DIR"] + "/templates", "templates"] else: return ["templates"] @@ -77,7 +77,7 @@ def __repr__(self): @app.context_processor def ga_utility_processor(): def google_analytics(): - return app.config['GOOGLE_ANALYTICS'] + return app.config["GOOGLE_ANALYTICS"] return dict(google_analytics=google_analytics()) @@ -85,7 +85,7 @@ def google_analytics(): @app.context_processor def title_utility_processor(): def title(): - return app.config['TOOL_TITLE'] + return app.config["TOOL_TITLE"] return dict(title=title()) @@ -93,13 +93,13 @@ def title(): @app.context_processor def theme_static_files_processor(): def theme_static_files(folder): - if not app.config['THEME_DIR']: + if not app.config["THEME_DIR"]: return list() try: all_files = os.listdir( "themes/{theme_dir}/static/{folder}".format( - theme_dir=app.config['THEME_DIR'], folder=folder + theme_dir=app.config["THEME_DIR"], folder=folder ) ) @@ -142,7 +142,7 @@ def error(exception=None): @app.route("/themes/static/<path:filename>") def theme_static(filename): # pragma: nocover - static_dir = "themes/{theme_dir}/static".format(theme_dir=app.config['THEME_DIR']) + static_dir = "themes/{theme_dir}/static".format(theme_dir=app.config["THEME_DIR"]) return send_from_directory(static_dir, filename) @@ -165,7 +165,7 @@ def index(lti=lti): # Test API key to see if they need to reauthenticate auth_header = {"Authorization": "Bearer " + session["api_key"]} - r = requests.get(app.config['API_URL'] + "users/self", headers=auth_header) + r = requests.get(app.config["API_URL"] + "users/self", headers=auth_header) if "WWW-Authenticate" in r.headers: # reroll oauth app.logger.info( @@ -176,11 +176,11 @@ def index(lti=lti): ) return redirect( - app.config['BASE_URL'] + app.config["BASE_URL"] + "login/oauth2/auth?client_id=" - + app.config['OAUTH2_ID'] + + app.config["OAUTH2_ID"] + "&response_type=code&redirect_uri=" - + app.config['OAUTH2_URI'] + + app.config["OAUTH2_URI"] ) if "WWW-Authenticate" not in r.headers and r.status_code == 401: @@ -208,15 +208,15 @@ def index(lti=lti): ) ) return redirect( - app.config['BASE_URL'] + app.config["BASE_URL"] + "login/oauth2/auth?client_id=" - + app.config['OAUTH2_ID'] + + app.config["OAUTH2_ID"] + "&response_type=code&redirect_uri=" - + app.config['OAUTH2_URI'] + + app.config["OAUTH2_URI"] ) r = requests.get( - app.config['API_URL'] + app.config["API_URL"] + "courses/{0}/external_tools?include_parents=true&per_page=100".format( session["course_id"] ), @@ -259,7 +259,7 @@ def status(): "checks": {"index": False, "xml": False, "db": False, "dev_key": False}, "url": url_for("index", _external=True), "xml_url": url_for("xml", _external=True), - "base_url": app.config['BASE_URL'], + "base_url": app.config["BASE_URL"], "debug": app.debug, } @@ -267,7 +267,7 @@ def status(): try: response = requests.get(url_for("index", _external=True), verify=False) index_check = ( - response.status_code == 200 and app.config['TOOL_TITLE'] in response.text + response.status_code == 200 and app.config["TOOL_TITLE"] in response.text ) status["checks"]["index"] = index_check except Exception: @@ -293,7 +293,9 @@ def status(): try: response = requests.get( "{}login/oauth2/auth?client_id={}&response_type=code&redirect_uri={}".format( - app.config['BASE_URL'], app.config['OAUTH2_ID'], app.config['OAUTH2_URI'] + app.config["BASE_URL"], + app.config["OAUTH2_ID"], + app.config["OAUTH2_URI"], ) ) status["checks"]["dev_key"] = response.status_code == 200 @@ -335,12 +337,12 @@ def oauth_login(lti=lti): payload = { "grant_type": "authorization_code", - "client_id": app.config['OAUTH2_ID'], - "redirect_uri": app.config['OAUTH2_URI'], - "client_secret": app.config['OAUTH2_KEY'], + "client_id": app.config["OAUTH2_ID"], + "redirect_uri": app.config["OAUTH2_URI"], + "client_secret": app.config["OAUTH2_KEY"], "code": code, } - r = requests.post(app.config['BASE_URL'] + "login/oauth2/token", data=payload) + r = requests.post(app.config["BASE_URL"] + "login/oauth2/token", data=payload) try: r.raise_for_status() @@ -440,12 +442,14 @@ def refresh_access_token(user): payload = { "grant_type": "refresh_token", - "client_id": app.config['OAUTH2_ID'], - "redirect_uri": app.config['OAUTH2_URI'], - "client_secret": app.config['OAUTH2_KEY'], + "client_id": app.config["OAUTH2_ID"], + "redirect_uri": app.config["OAUTH2_URI"], + "client_secret": app.config["OAUTH2_KEY"], "refresh_token": refresh_token, } - response = requests.post(app.config['BASE_URL'] + "login/oauth2/token", data=payload) + response = requests.post( + app.config["BASE_URL"] + "login/oauth2/token", data=payload + ) try: response.raise_for_status() @@ -533,11 +537,11 @@ def auth(lti=lti): ) ) return redirect( - app.config['BASE_URL'] + app.config["BASE_URL"] + "login/oauth2/auth?client_id=" - + app.config['OAUTH2_ID'] + + app.config["OAUTH2_ID"] + "&response_type=code&redirect_uri=" - + app.config['OAUTH2_URI'] + + app.config["OAUTH2_URI"] ) # Get the expiration date @@ -565,17 +569,17 @@ def auth(lti=lti): # Refresh didn't work. Reauthenticate. app.logger.info("Reauthenticating:\nSession: {}".format(session)) return redirect( - app.config['BASE_URL'] + app.config["BASE_URL"] + "login/oauth2/auth?client_id=" - + app.config['OAUTH2_ID'] + + app.config["OAUTH2_ID"] + "&response_type=code&redirect_uri=" - + app.config['OAUTH2_URI'] + + app.config["OAUTH2_URI"] ) else: # API key that shouldn't be expired. Test it. auth_header = {"Authorization": "Bearer " + session["api_key"]} r = requests.get( - app.config['API_URL'] + "users/%s/profile" % (session["canvas_user_id"]), + app.config["API_URL"] + "users/%s/profile" % (session["canvas_user_id"]), headers=auth_header, ) # check for WWW-Authenticate @@ -593,11 +597,11 @@ def auth(lti=lti): # Refresh didn't work. Reauthenticate. app.logger.info("Reauthenticating\nSession: {}".format(session)) return redirect( - app.config['BASE_URL'] + app.config["BASE_URL"] + "login/oauth2/auth?client_id=" - + app.config['OAUTH2_ID'] + + app.config["OAUTH2_ID"] + "&response_type=code&redirect_uri=" - + app.config['OAUTH2_URI'] + + app.config["OAUTH2_URI"] ) @@ -614,7 +618,7 @@ def get_sessionless_url(lti_id, is_course_nav, lti=lti): "&launch_type=course_navigation" ) r = requests.get( - url.format(app.config['API_URL'], session["course_id"], lti_id), + url.format(app.config["API_URL"], session["course_id"], lti_id), headers=auth_header, ) if r.status_code >= 400: @@ -638,7 +642,7 @@ def get_sessionless_url(lti_id, is_course_nav, lti=lti): auth_header = {"Authorization": "Bearer " + session["api_key"]} # get sessionless launch url r = requests.get( - app.config['API_URL'] + app.config["API_URL"] + "courses/{0}/external_tools/sessionless_launch?id={1}".format( session["course_id"], lti_id ), diff --git a/tests.py b/tests.py index c4d5e06..b6d989d 100644 --- a/tests.py +++ b/tests.py @@ -266,7 +266,7 @@ def test_index_api_key_expired(self, m): redirect_url.format( self.app.config["BASE_URL"], self.app.config["OAUTH2_ID"], - self.app.config["OAUTH2_URI"] + self.app.config["OAUTH2_URI"], ), ) @@ -308,7 +308,7 @@ def test_index_api_key_404(self, m): redirect_url.format( self.app.config["BASE_URL"], self.app.config["OAUTH2_ID"], - self.app.config["OAUTH2_URI"] + self.app.config["OAUTH2_URI"], ), ) @@ -463,7 +463,10 @@ def test_status_healthy(self, m): self.app.config["BASE_URL"] = "https://example.edu/" m.register_uri( - "GET", "http://localhost/", status_code=200, text=self.app.config["TOOL_TITLE"] + "GET", + "http://localhost/", + status_code=200, + text=self.app.config["TOOL_TITLE"], ) m.register_uri( "GET", @@ -920,7 +923,7 @@ def test_auth_no_user(self, m): redirect_url.format( self.app.config["BASE_URL"], self.app.config["OAUTH2_ID"], - self.app.config["OAUTH2_URI"] + self.app.config["OAUTH2_URI"], ), ) @@ -1004,7 +1007,7 @@ def test_auth_no_api_key_refresh_fail(self, m, mock_refresh_access_token): redirect_url.format( self.app.config["BASE_URL"], self.app.config["OAUTH2_ID"], - self.app.config["OAUTH2_URI"] + self.app.config["OAUTH2_URI"], ), ) @@ -1104,7 +1107,7 @@ def test_auth_invalid_api_key_refresh_fail(self, m, mock_refresh_access_token): redirect_url.format( self.app.config["BASE_URL"], self.app.config["OAUTH2_ID"], - self.app.config["OAUTH2_URI"] + self.app.config["OAUTH2_URI"], ), ) @@ -1254,7 +1257,7 @@ def test_get_sessionless_url_not_course_nav_succeed(self, m): class UtilsTests(unittest.TestCase): - app = Flask('test') + app = Flask("test") app.config["WHITELIST"] = "whitelist.json" app.config["BASE_URL"] = "https://example.edu/" diff --git a/utils.py b/utils.py index a2b8ff1..f6adfac 100644 --- a/utils.py +++ b/utils.py @@ -37,13 +37,13 @@ def filter_tool_list(course_id, access_token): The values are a list of all installed external tools that are in that category and on the whitelist. """ - with open(current_app.config['WHITELIST'], "r") as wl_file: + with open(current_app.config["WHITELIST"], "r") as wl_file: whitelist = json.loads(wl_file.read()) if not whitelist: raise ValueError("whitelist.json is empty") - canvas = Canvas(current_app.config['BASE_URL'], access_token) + canvas = Canvas(current_app.config["BASE_URL"], access_token) course = canvas.get_course(course_id) installed_tools = course.get_external_tools(include_parents=True) From 6dc0cd371385b202ec59517165d2161c6310cf8b Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Mon, 25 Apr 2022 16:27:46 -0400 Subject: [PATCH 43/52] First fixes --- README.md | 7 +++---- docker-compose.yml | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1bd6034..b81b52d 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,9 @@ First clone and setup the repo. ```sh git clone git@github.com:ucfopen/faculty-tools.git +cd faculty-tools cp whitelist.json.template whitelist.json cp .env.template .env -mkdir logs -touch logs/faculty-tools.log ``` Edit `.env` to configure the application. All fields are required, @@ -69,7 +68,7 @@ name set in the `docker-compose.yml` file. ```sh docker-compose run lti python -from main import db +from lti import db db.create_all() ``` @@ -90,7 +89,7 @@ It's time to use docker-compose to bring up the application. docker-compose up -d ``` -Go to the /xml page, <http://127.0.0.1:9001/facultytools/xml> by default. +Go to the /xml page, <http://127.0.0.1:9001/xml> by default. Copy the xml, and install it into a course (Course->Settings->Apps). diff --git a/docker-compose.yml b/docker-compose.yml index e5b3ca2..921263f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,6 @@ services: environment: MYSQL_ROOT_PASSWORD: secret MYSQL_DATABASE: faculty_tools - MYSQL_USER: root MYSQL_PASSWORD: secret ports: - "33061:3306" From 8fd80eec6cd1142be311a3fe525832941efb4065 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Tue, 26 Apr 2022 09:20:24 -0400 Subject: [PATCH 44/52] Linted --- README.md | 39 ++++++++++++++++++++++++++++++++++++--- config.py | 41 ++++------------------------------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index b81b52d..8ce9384 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,43 @@ cp whitelist.json.template whitelist.json cp .env.template .env ``` +### Environment Variables + Edit `.env` to configure the application. All fields are required, unless specifically noted. -## Developer Key +To create a good secure secret key, run this command: + +```sh +docker-compose run --rm lti python -c "import os, binascii; print(binascii.b2a_base64(os.urandom(24)).decode('ascii'))" +``` + +```sh +TOOL_TITLE=Faculty Tools # Window Title of Page +THEME_DIR= # Keep blank unless building out your own theme directory +BASE_CANVAS_SERVER_URL=https://example.com/ # the URL for your canvas server +SECRET_KEY=CHANGEME # Random key used to secure portions of Flask - Follow instructions above +LTI_KEY=key # Random key - This is public - Used to install LTI. +LTI_SECRET=secret # Random secret key - Used to install LTI. Do not share! +OAUTH2_URI=http://127.0.0.1:9001/oauthlogin # URL of faculty tools oauthlogin page +OAUTH2_ID=CHANGEME # ID given by LMS Admins / Developer Key (API Key) page in Canvas +OAUTH2_KEY=CHANGEME # ID given by LMS Admins / Developer Key (API Key) page in Canvas +GOOGLE_ANALYTICS=GA-000000 # Your Google Analytics id. +DATABASE_URI=mysql://root:secret@db/faculty_tools # Your mysql connection string. + +# config.py configuration settings +# config.Development for Dev, config.BaseConfig for production +CONFIG=config.DevelopmentConfig + + +# test_requirements for development / requirements.txt for production. +REQUIREMENTS=test_requirements.txt + +WHITELIST_JSON=whitelist.json # See below + +``` + +## Developer Key -> API Key You will need a developer key for the OAuth2 flow. Check out the [Canvas documentation for creating a new developer key](https://community.canvaslms.com/docs/DOC-12657-4214441833) @@ -67,7 +100,7 @@ The MySQL docker image automatically creates the user, password, and database name set in the `docker-compose.yml` file. ```sh -docker-compose run lti python +docker-compose run --rm lti python from lti import db db.create_all() ``` @@ -77,7 +110,7 @@ python shell: ```python docker-compose run lti python -from main import Users +from lti import Users Users.query.all() ``` diff --git a/config.py b/config.py index 50443b8..4ddc9ff 100644 --- a/config.py +++ b/config.py @@ -1,37 +1,6 @@ import os -class Config(object): - # make the warning shut up until Flask-SQLAlchemy v3 comes out - SQLALCHEMY_TRACK_MODIFICATIONS = True - - # LTI consumer key and shared secret - CONSUMER_KEY = os.environ.get("LTI_KEY") - SHARED_SECRET = os.environ.get("LTI_SECRET") - - # Configuration for pylti library. Uses the above key and secret - PYLTI_CONFIG = { - "consumers": {CONSUMER_KEY: {"secret": SHARED_SECRET}}, - # Custom configurable roles - "roles": { - "staff": [ - "urn:lti:instrole:ims/lis/Administrator", - "Instructor", - "ContentDeveloper", - "urn:lti:role:ims/lis/TeachingAssistant", - ] - }, - } - - SESSION_COOKIE_NAME = "ft_session" - - # Chrome 80 SameSite=None; Secure fix - SESSION_COOKIE_SECURE = True - SESSION_COOKIE_SAMESITE = "None" - - SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI") - - class BaseConfig(object): DEBUG = False TESTING = False @@ -54,10 +23,11 @@ class BaseConfig(object): THEME_DIR = os.environ.get("THEME_DIR", "") # Canvas instance URL. ex: https://example.instructure.com/ - BASE_URL = os.environ.get("BASE_CANVAS_SERVER_URL", "https://example.com") + BASE_URL = os.environ.get("BASE_CANVAS_SERVER_URL", "https://example.com/") API_URL = BASE_URL + "api/v1/" # Secret key to sign Flask sessions with. KEEP THIS SECRET! + # Set this in .env file. SECRET_KEY = os.environ.get("SECRET_KEY") # LTI consumer key and shared secret @@ -79,7 +49,8 @@ class BaseConfig(object): } # The "Oauth2 Redirect URI" that you provided to Instructure. - OAUTH2_URI = os.environ.get("OAUTH2_URI") # ex. 'https://localhost:5000/oauthlogin' + # Set in .env file + OAUTH2_URI = os.environ.get("OAUTH2_URI") # ex. 'https://localhost:9001/oauthlogin' # The Client_ID Instructure gave you OAUTH2_ID = os.environ.get("OAUTH2_ID") # The Secret Instructure gave you @@ -95,11 +66,7 @@ class DevelopmentConfig(BaseConfig): DEBUG = True TESTING = True - SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI") - class TestingConfig(BaseConfig): DEBUG = False TESTING = True - - SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI") From e60651bd6162b35af280962e295bfb469636a003 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Tue, 26 Apr 2022 09:27:26 -0400 Subject: [PATCH 45/52] MDL --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ce9384..06fefc9 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ unless specifically noted. To create a good secure secret key, run this command: ```sh -docker-compose run --rm lti python -c "import os, binascii; print(binascii.b2a_base64(os.urandom(24)).decode('ascii'))" +docker-compose run --rm lti python -c "import os, binascii; print(binascii.b2a_base64(os.urandom(24)).decode('ascii'))" ``` ```sh @@ -45,7 +45,7 @@ CONFIG=config.DevelopmentConfig # test_requirements for development / requirements.txt for production. -REQUIREMENTS=test_requirements.txt +REQUIREMENTS=test_requirements.txt WHITELIST_JSON=whitelist.json # See below From dcd0c27c4e9614224daf5f3141a039cbe6e0ddee Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Tue, 26 Apr 2022 09:45:47 -0400 Subject: [PATCH 46/52] config comment change --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 4ddc9ff..473e348 100644 --- a/config.py +++ b/config.py @@ -50,7 +50,7 @@ class BaseConfig(object): # The "Oauth2 Redirect URI" that you provided to Instructure. # Set in .env file - OAUTH2_URI = os.environ.get("OAUTH2_URI") # ex. 'https://localhost:9001/oauthlogin' + OAUTH2_URI = os.environ.get("OAUTH2_URI") # ex. 'http://localhost:9001/oauthlogin' # The Client_ID Instructure gave you OAUTH2_ID = os.environ.get("OAUTH2_ID") # The Secret Instructure gave you From af2863a87a88b4e8ff0d48e1f2ec0ccb1040d517 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Mon, 16 May 2022 08:39:39 -0400 Subject: [PATCH 47/52] Testing ARM --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f401391..8fc1c4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,6 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} - + platform: linux/amd64,linux/arm64,linux/arm/v7 From cd8784d1a0cae783d5c7567dc09461f985bb3e35 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Mon, 16 May 2022 08:43:21 -0400 Subject: [PATCH 48/52] Testing ARM platforms --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fc1c4b..3a9afd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,6 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} - platform: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64,linux/arm/v7 From e43eb0fc077554528a5faac3649648d7855a2d5c Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Mon, 16 May 2022 08:45:59 -0400 Subject: [PATCH 49/52] Testing ARM platforms --- .github/workflows/ci.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a9afd6..0f69daa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,15 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64 + - name: Build and push ARM 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 }} + platforms: linux/amd64,linux/arm/v7 From 6649bed7778fb0a9a0ae9bfcf7498f2cf1812d5e Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Mon, 16 May 2022 09:31:56 -0400 Subject: [PATCH 50/52] Testing ARM platforms --- .github/workflows/ci.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f69daa..a96e635 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,23 +60,23 @@ jobs: 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 }} - platforms: linux/amd64 + # - 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 }} + # platforms: linux/amd64 - name: Build and push ARM Docker image - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + uses: docker/build-push-action@v3 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} - platforms: linux/amd64,linux/arm/v7 + platforms: linux/amd64,linux/arm64,linux/arm/v7 From ef627525655320f5a68eb0b0b4b0cf2e5dfddec1 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Mon, 16 May 2022 09:35:34 -0400 Subject: [PATCH 51/52] Testing ARM platforms --- .github/workflows/ci.yml | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a96e635..68708a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,17 +60,27 @@ jobs: 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 }} - # platforms: linux/amd64 + - 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 }} + platforms: linux/amd64 + + - name: Build and push ARM64 Docker image + uses: docker/build-push-action@v3 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} + platforms: linux/arm64 - - name: Build and push ARM Docker image + - name: Build and push ARMv7 Docker image uses: docker/build-push-action@v3 with: context: . @@ -78,5 +88,5 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: REQUIREMENTS=${{ env.REQUIREMENTS }} - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/arm/v7 From b2f2f112fe7c271c8a5cfe767c8663ae2ebf1286 Mon Sep 17 00:00:00 2001 From: Shea Silverman <s@ucf.edu> Date: Mon, 16 May 2022 09:56:30 -0400 Subject: [PATCH 52/52] Testing ARM platforms --- .github/workflows/ci.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68708a7..796eb2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,9 +40,9 @@ jobs: # uses: falti/dotenv-action@v0.2.5 - name: Environment Variables from Dotenv uses: c-py/action-dotenv-to-setenv@v3 - - name: Print Repo - run: | - env + # - name: Print Repo + # run: | + # env - name: Run unittests run: coverage run -m unittest discover @@ -60,6 +60,10 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + - name: Print Buildx + run: | + docker buildx ls + - name: Build and push Docker image uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc with: