Commit 31b9e0c1 authored by Madi Baig's avatar Madi Baig
Browse files

Version 1.0.0

parents
Pipeline #212907 passed with stage
in 1 minute and 2 seconds
**/__pycache__/**
stages:
- tests
- deploy
testing:
image: "python:3.7"
stage: tests
before_script:
- apt-get update -y
- apt-get install -y libsasl2-dev libldap2-dev libssl-dev
- python -m pip install -r requirements.txt
script:
- python -m pytest tests
deploy:
stage: deploy
only:
- master
script:
- chmod +x ./setup_push.sh
- ./setup_push.sh ssh://dokku@cloud-vm-42-75.doc.ic.ac.uk:22/$APP_NAME master
web: gunicorn app:app --log-file=-
\ No newline at end of file
# Python3 Flask Template
A started template for Flask web apps using **Python 3.8**. This started template includes:
- Dynamic frontend templates (not React)
- Database support
- Imperial LDAP user authentication
- Multiple environments (development and production)
This template is useful if you would like to create a backend REST API, optionally coupled with a simple dynamic frontend.
## Getting started
Once you have created your new app, take a few minutes to look through the files to familiarise yourself with the project structure.
- `app.py` : entry point to the Flask app
- `templates/` : contains the frontend dynamic HTML files
- `static/` : contains the static frontend assets (images, stylesheets and scripts)
- `blueprints/` : contains all the application routes
- `models/` : contains all the database models
- `database/` : contains the database creation
- `config/` : contains the app settings for the different environments
**The first change you should make** is to set the `APP_NAME` variable in `config/config.py` to whatever you app's name is.
To start the application locally, you can just run `python3 app.py` and this will launch the app on port 5000 (by default).
You will notice a message in the console saying:
`WARNING: Could not connect to the given database URL!`
To fix this, you should set the environment variable DATABASE_URL accordingly. If you have PostgreSQL running locally, you can use that. Alternatively, you could use SQLite which is much simpler and does not require installation.
If you do not want to use a database yet, you can ignore this warning and delete any routes that interact with the database.
If you navigate to `http://localhost:5000`, you will see the response created by the route defined in `blueprints/home.py`.
You will also notice the lines `Environment: production` and `Debug mode: off` when the Flask application starts in the console. To enable debug mode, you must set the environment variable `ENV` to `dev`, ie: `export ENV=dev` (see `config/config.py` for more details on different environments).
## Tutorial 1: Adding a new route
## Tutorial 2: Adding database interaction
## Tutorial 3: Configuring a test environment
\ No newline at end of file
from .app import app
\ No newline at end of file
from flask import Flask, send_from_directory
from database.db import db
from config.config import APP_NAME, ENV, get_app_config, get_static_url
# Create and configure our Flask app
app = Flask(__name__, static_url_path=get_static_url())
app.url_map.strict_slashes = False
app.config.from_object(get_app_config())
# Setup authentication
import imperial_ldap
imperial_ldap.init_app(app)
imperial_ldap.set_auth_type(imperial_ldap.config.AuthType.SESSION, redirect_url='auth.login')
# Add integration to the Materials API
import imperial_doc_materials
imperial_doc_materials.init_app(app)
imperial_doc_materials.set_redirect_url('auth.login')
db.init_app(app)
# TODO Find a workaround for migrations/db.create_all()
with app.app_context():
from models.entity import Entity
if app.config["SQLALCHEMY_DATABASE_URI"]:
db.create_all()
# Serve all static assets for the frontend
@app.route('/static/<path:path>')
def serve_static_files(path):
return send_from_directory('static', path)
# Register all routes from the blueprints module
from blueprints.home import home_blueprint
from blueprints.auth import auth_blueprint
app.register_blueprint(home_blueprint)
app.register_blueprint(auth_blueprint)
# Setup panopto api
import imperial_panopto
imperial_panopto.set_panopto_api_client_id("YOUR_CLIENT_ID_HERE")
imperial_panopto.set_panopto_api_client_secret("YOUR_CLIENT_SECRET_HERE")
imperial_panopto.set_app_name(app)
if ENV == 'prod':
imperial_panopto.set_panopto_is_dev(False)
app.register_blueprint(imperial_panopto.panopto_api_blueprint)
# Hook any custom Jinja templating functions
from config import CUSTOM_TEMPLATE_FUNCTIONS
app.jinja_env.globals.update(CUSTOM_TEMPLATE_FUNCTIONS)
if __name__ == '__main__':
app.run()
\ No newline at end of file
from flask import Blueprint, render_template, request, redirect, flash
from imperial_ldap.auth import ldap_login, ldap_logout
from config import url_for2
from imperial_doc_materials import materials_login
auth_blueprint = Blueprint("auth", __name__, url_prefix="/auth")
@auth_blueprint.route("/login", methods=["GET", "POST"])
def login():
if request.method == "GET":
return render_template("login.html")
# Handle post request
username = request.form.get("username")
password = request.form.get("password")
if username and password:
user = ldap_login(username, password)
if user:
# We're logged in here, so now we also login into the Material API
materials_login(username, password)
return redirect(url_for2("home.dashboard"))
else:
# bad credentials
flash("Invalid login credentials.")
return redirect(url_for2("auth.login"))
return "Please provide a username and password"
@auth_blueprint.route("/logout", methods=["GET"])
def logout():
ldap_logout()
return redirect(url_for2("home.index"))
from flask import Blueprint, render_template, request, redirect
from imperial_panopto.auth import panopto_login_required
from imperial_panopto.videos import search_videos, SortField, SortOrder
from config import url_for2
from database.db import db
from models.entity import Entity
from imperial_ldap.auth import login_required, get_user_from_session
from imperial_doc_materials import using_materials
home_blueprint = Blueprint('home', __name__, url_prefix='/')
@home_blueprint.route('')
def index():
user = get_user_from_session() # If there is no user logged, this returns None
return render_template('index.html', user=user)
@home_blueprint.route('/dashboard')
@login_required # Indicate that the user must be logged in (via LDAP)
@using_materials # Indicate that we want to access the Materials API
def dashboard(materials_client, user):
my_courses = materials_client.get_courses_for_year("2122")
return render_template('dashboard.html', user=user, my_courses=my_courses)
@home_blueprint.route('/panopto_example')
@panopto_login_required
def panopto_example():
videos = search_videos("Test", sort_field=SortField.Name, sort_order=SortOrder.Asc, page_number=0)
return videos[0].build_iframe()
@home_blueprint.route('/entities', methods=['GET', 'POST'])
@login_required
def entities(user):
if request.method == "GET":
try:
rows = ""
for e in db.session.query(Entity).all():
rows += str(e) + "<br>"
return rows
except:
return "App database error"
else:
name = request.form.get('name')
age = request.form.get('age')
try:
db.session.add(Entity(name=name, age=int(age)))
db.session.commit()
return redirect(url_for2('.entities'))
except:
return "Could not add entity."
# Define any custom Jinja2 functions here
from flask import url_for
from .config import URL_PREFIX
# Get around the routing prefix issue inside the templates
def url_for2(endpoint: str, **kwargs):
return URL_PREFIX + str(url_for(endpoint, **kwargs))
CUSTOM_TEMPLATE_FUNCTIONS = {
"url": url_for2,
}
import os
# This should match exactly the name of the app you specified
APP_NAME = "MY_APP_NAME_HERE"
ENV = os.environ.get('ENV', 'prod').lower()
URL_PREFIX = f"/{APP_NAME}" if ENV == 'prod' else ""
# Get the static URL of the app (to get around the production path issue)
def get_static_url():
if ENV == 'prod':
return f'/{APP_NAME}/static'
else:
return '/static'
# Get the app configuration based on the ENV environment variable (default is prod)
def get_app_config():
if ENV == 'prod':
return ProductionConfig()
else:
return DevelopmentConfig()
# If you have created a database for this app, the connection string will be automatically
# accessible through the DATABASE_URL environment variable.
def get_db_url(testing=False):
if testing:
return 'sqlite://{0}/{1}.db'.format("tmp", "testing")
url = os.environ.get('DATABASE_URL')
if url is None:
print("WARNING: Could not connect to the given database URL!")
# For PostgreSQL databases, the conn string needs to start with "postgresql"
if url and url.startswith("postgres://"):
url = url.replace("postgres://", "postgresql://", 1)
return url
class CommonConfig:
APP_NAME = APP_NAME
URL_PREFIX = URL_PREFIX
SECRET_KEY = "my-secrefasdfasdfasdfasdt-key"
SESSION_COOKIE_SECURE = True
SQLALCHEMY_DATABASE_URI = get_db_url()
SQLALCHEMY_TRACK_MODIFICATIONS = False
# Flask App settings for production environment
class ProductionConfig(CommonConfig):
DEBUG = False
APPLICATION_ROOT = f"/{APP_NAME}"
# Flask App settings for local development enviroment
class DevelopmentConfig(CommonConfig):
DEBUG = True
# Settings for running tests
class TestConfig(CommonConfig):
TESTING = True
SQLALCHEMY_DATABASE_URI = get_db_url(testing=True)
\ No newline at end of file
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
\ No newline at end of file
from database.db import db
class Entity(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
age = db.Column(db.Integer, unique=False, nullable=False)
def __repr__(self):
return f'Entity({self.id}, {self.name}, {self.age})'
python-3.8.12
#!/bin/bash -e
url=$1
branch=$2
if [ -z "$SSH_PRIVATE_KEY" ]; then
>&2 echo "Set SSH_PRIVATE_KEY environment variable"
exit 1
fi
ssh_host=$(echo $url | sed 's/.*@//' | sed 's/[:/].*//')
if [ -z "$ssh_host" ]; then
>&2 echo "Usage: $0 <user@git.host:project | ssh://user@git.host:port/project> [<branch>]"
exit 1
fi
ssh_port=
if [[ $url =~ ^ssh://[^/]+:([0-9]+) ]]; then
ssh_port="-p ${BASH_REMATCH[1]}"
fi
# TODO: skip on multiple runs
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H $ssh_port "$ssh_host" >> ~/.ssh/known_hosts
git push $url ${CI_COMMIT_SHA:-HEAD}:refs/heads/${branch:-master} $([ -z "$DISABLE_FORCE_PUSH" ] && echo --force)
#content {
margin-left: 1%;
margin-right: 1%;
}
\ No newline at end of file
html,
body {
height: 100%;
}
body {
display: -ms-flexbox;
display: -webkit-box;
display: flex;
-ms-flex-align: center;
-ms-flex-pack: center;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-control {
position: relative;
box-sizing: border-box;
height: auto;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment