diff --git a/.gitignore b/.gitignore index 1a77a3e..8c8bf6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .idea __pycache__/ app.db +pb/static/.webassets-cache* +pb/static/*.css static/.webassets-cache* static/*.css diff --git a/Dockerfile b/Dockerfile index 8d2d3b0..a303842 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,8 +23,8 @@ WORKDIR /app ENV FLASK_APP=/app/pb/__init__.py # Don't run gunicorn until the DC/OS container actually starts. -# Otherwise, environment variables will not be availabele. +# Otherwise, environment variables will not be available. #CMD ["pipenv", "run", "gunicorn", \ # "--bind", "0.0.0.0:8000", \ # "-e", "SCRIPT_NAME=/api", \ -# "crc:app"] +# "wsgi:app"] diff --git a/Pipfile b/Pipfile index 66b20b6..b9cb12f 100644 --- a/Pipfile +++ b/Pipfile @@ -23,6 +23,7 @@ wtforms-alchemy = "*" psycopg2-binary = "*" pyscss = "*" gunicorn = "*" +werkzeug = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 16083ea..ddfeccf 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "22303ab4362e0b95f21f7c949b2d43a4ec58b42add20699c159d2a7cc3eaf0be" + "sha256": "42147b649c5838de2eba97ebf9360b65d4bdbb8b3ae94ba23c813660a9490ed3" }, "pipfile-spec": 6, "requires": { @@ -518,6 +518,7 @@ "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" ], + "index": "pypi", "version": "==1.0.1" }, "wtforms": { diff --git a/docker_run.sh b/docker_run.sh index 18ea7a1..4239672 100755 --- a/docker_run.sh +++ b/docker_run.sh @@ -18,4 +18,4 @@ if [ "$RESET_DB" = "true" ]; then pipenv run flask load-example-data fi -pipenv run gunicorn -e SCRIPT_NAME="$APPLICATION_ROOT" --bind 0.0.0.0:$PORT0 run:app +pipenv run gunicorn -e SCRIPT_NAME="$APPLICATION_ROOT" --bind 0.0.0.0:$PORT0 wsgi:app diff --git a/pb/__init__.py b/pb/__init__.py index 8056a2c..9ba0701 100644 --- a/pb/__init__.py +++ b/pb/__init__.py @@ -39,8 +39,7 @@ def get_study_details(studyid): def get_form(id, requirement_code): return -conn = connexion.App('Protocol Builder', specification_dir='pb') - +conn = connexion.FlaskApp('Protocol Builder', specification_dir='pb') app = conn.app app.config.from_object('config.default') @@ -53,13 +52,43 @@ else: app.config.root_path = app.instance_path app.config.from_pyfile('config.py', silent=True) -conn.add_api('api.yml', base_path='/pb') +conn.add_api('api.yml', base_path='/v2.0') db = SQLAlchemy(app) migrate = Migrate(app, db) ma = Marshmallow(app) + +# Set the path of the static directory +APP_ROOT = os.path.dirname(os.path.abspath(__file__)) +APP_STATIC = os.path.join(APP_ROOT, 'static') +BASE_HREF = app.config['APPLICATION_ROOT'].strip('/') +app.static_folder = APP_STATIC +app.static_url_path = app.config['APPLICATION_ROOT'] + 'static' + +print('app.static_folder', app.static_folder) +print('app.static_url_path', app.static_url_path) + +# remove old static map +url_map = app.url_map +try: + for rule in url_map.iter_rules('static'): + url_map._rules.remove(rule) +except ValueError: + # no static view was created yet + pass + +# register new; the same view function is used +app.add_url_rule( + app.static_url_path + '/', + endpoint='static', view_func=app.send_static_file) + assets = Environment(app) +assets.init_app(app) assets.url = app.static_url_path -scss = Bundle('scss/app.scss', filters='pyscss', output='app.css') +scss = Bundle( + 'scss/app.scss', + filters='pyscss', + output='app.css' +) assets.register('app_scss', scss) # Loads all the descriptions from the API so we can display them in the editor. @@ -88,7 +117,7 @@ def site_map(): # Filter out rules we can't navigate to in a browser # and rules that require parameters if "GET" in rule.methods and has_no_empty_params(rule): - url = url_for(rule.endpoint, **(rule.defaults or {})) + url = app.confg['APPLICATION_ROOT'].strip('/') + url_for(rule.endpoint, **(rule.defaults or {})) links.append((url, rule.endpoint)) return json.dumps({"links": links}) @@ -106,25 +135,33 @@ def index(): # display results studies = db.session.query(Study).order_by(Study.DATE_MODIFIED.desc()).all() table = StudyTable(studies) - return render_template('index.html', table=table, APPLICATION_ROOT=app.config['APPLICATION_ROOT']) + return render_template( + 'index.html', + table=table, + base_href=BASE_HREF + ) @app.route('/new_study', methods=['GET', 'POST']) def new_study(): form = StudyForm(request.form) - action = "/new_study" + action = BASE_HREF + "/new_study" title = "New Study" if request.method == 'POST': study = Study() study.study_details = StudyDetails() _update_study(study, form) flash('Study created successfully!') - return redirect('/') + return redirect_home() - return render_template('form.html', form=form, - action=action, - title=title, - description_map=description_map) + return render_template( + 'form.html', + form=form, + action=action, + title=title, + description_map=description_map, + base_href=BASE_HREF + ) @app.route('/study/', methods=['GET', 'POST']) @@ -132,7 +169,7 @@ def edit_study(study_id): study = db.session.query(Study).filter(Study.STUDYID == study_id).first() form = StudyForm(request.form, obj=study) if request.method == 'GET': - action = "/study/" + study_id + action = BASE_HREF + "/study/" + study_id title = "Edit Study #" + study_id if study.requirements: form.requirements.data = list(map(lambda r: r.AUXDOCID, list(study.requirements))) @@ -141,17 +178,21 @@ def edit_study(study_id): if request.method == 'POST': _update_study(study, form) flash('Study updated successfully!') - return redirect('/') - return render_template('form.html', form=form, - action=action, - title=title, - description_map={}) + return redirect_home() + return render_template( + 'form.html', + form=form, + action=action, + title=title, + description_map={}, + base_href=BASE_HREF + ) @app.route('/investigator/', methods=['GET', 'POST']) def new_investigator(study_id): form = InvestigatorForm(request.form) - action = "/investigator/" + study_id + action = BASE_HREF + "/investigator/" + study_id title = "Add Investigator to Study " + study_id if request.method == 'POST': investigator = Investigator(STUDYID=study_id) @@ -160,19 +201,23 @@ def new_investigator(study_id): db.session.add(investigator) db.session.commit() flash('Investigator created successfully!') - return redirect('/') + return redirect_home() - return render_template('form.html', form=form, - action=action, - title=title, - description_map={}) + return render_template( + 'form.html', + form=form, + action=action, + title=title, + description_map={}, + base_href=BASE_HREF + ) @app.route('/del_investigator/', methods=['GET']) def del_investigator(inv_id): db.session.query(Investigator).filter(Investigator.id == inv_id).delete() db.session.commit() - return redirect('/') + return redirect_home() @app.route('/del_study/', methods=['GET']) @@ -182,7 +227,7 @@ def del_study(study_id): db.session.query(StudyDetails).filter(StudyDetails.STUDYID == study_id).delete() db.session.query(Study).filter(Study.STUDYID == study_id).delete() db.session.commit() - return redirect('/') + return redirect_home() def _update_study(study, form): @@ -217,7 +262,7 @@ def study_details(study_id): study_details = StudyDetails(STUDYID=study_id) form = StudyDetailsForm(request.form, obj=study_details) if request.method == 'GET': - action = "/study_details/" + study_id + action = BASE_HREF + "/study_details/" + study_id title = "Edit Study Details for Study #" + study_id details = "Numeric fields can be 1 for true, 0 or false, or Null if not applicable." if request.method == 'POST': @@ -225,12 +270,20 @@ def study_details(study_id): db.session.add(study_details) db.session.commit() flash('Study updated successfully!') - return redirect('/') - return render_template('form.html', form=form, - action=action, - title=title, - details=details, - description_map=description_map) + return redirect_home() + return render_template( + 'form.html', + form=form, + action=action, + title=title, + details=details, + description_map=description_map, + base_href=BASE_HREF + ) + + +def redirect_home(): + return redirect('/' + BASE_HREF) if __name__ == '__main__': diff --git a/static/favicon.ico b/pb/static/favicon.ico similarity index 100% rename from static/favicon.ico rename to pb/static/favicon.ico diff --git a/static/scss/app.scss b/pb/static/scss/app.scss similarity index 100% rename from static/scss/app.scss rename to pb/static/scss/app.scss diff --git a/static/app.css b/static/app.css deleted file mode 100644 index aaa9fcd..0000000 --- a/static/app.css +++ /dev/null @@ -1,133 +0,0 @@ -.mat-icon { - font-family: 'Material Icons', sans-serif; - font-size: 24px; } - - .text-center { - text-align: center; } - - html, body { - padding: 1em; - margin: 0; - font-family: Arial, sans-serif; - font-size: 16px; } - - table { - border: 1px solid #cacaca; - background-color: white; - width: 100%; - text-align: left; - border-collapse: collapse; } - table th, table td { - padding: 0.5em; } - table td, table.blueTable th { - border: 1px solid #cacaca; } - table tbody td { - font-size: 14px; } - table tr:nth-child(even) { - background: #ededed; } - table thead { - background-color: #495e9d; } - table thead th { - font-size: 16px; - font-weight: bold; - color: white; - border-left: 1px solid #cacaca; } - table thead th:first-child { - border-left: none; } - table tfoot { - font-size: 16px; - font-weight: bold; - color: white; - background-color: #cacaca; } - table tfoot td { - font-size: 16px; } - table tfoot .links { - text-align: right; } - table tfoot .links a { - display: inline-block; - background: #495e9d; - color: white; - padding: 2px 8px; - border-radius: 5px; } - -.btn { - font-size: 16px; - padding: 0.5em 1em; - border-radius: 5px; - text-decoration: none; - color: white; - white-space: nowrap; - border: none; } - .btn:hover { - text-decoration: none; } - .btn.btn-icon { - font-family: 'Material Icons', sans-serif; - font-size: 24px; - border: none; } - .btn.btn-icon.btn-default { - color: #4e4e4e; - background-color: transparent; - border: none; } - .btn.btn-icon.btn-default:hover { - color: #373737; - background-color: transparent; } - .btn.btn-icon.btn-primary { - color: #232D4B; - background-color: transparent; } - .btn.btn-icon.btn-primary:hover { - color: #191f34; - background-color: transparent; } - .btn.btn-icon.btn-accent { - color: #E57200; - background-color: transparent; } - .btn.btn-icon.btn-accent:hover { - color: #a05000; - background-color: transparent; } - .btn.btn-icon.btn-warn { - color: #DF1E43; - background-color: transparent; } - .btn.btn-icon.btn-warn:hover { - color: #9c152f; - background-color: transparent; } - .btn.btn-default { - color: #373737; - background-color: white; - border: 1px solid #cacaca; } - .btn.btn-default:hover { - background-color: #ededed; } - .btn.btn-primary { - background-color: #232D4B; } - .btn.btn-primary:hover { - background-color: #191f34; } - .btn.btn-warn { - background-color: #DF1E43; } - .btn.btn-warn:hover { - background-color: #9c152f; } - .btn.btn-accent { - background-color: #E57200; } - .btn.btn-accent:hover { - background-color: #a05000; } - -select.multi { - height: 600px; } - - .form-field { - display: flex; - width: 100%; - margin-bottom: 40px; - padding: 2em; } - .form-field:nth-child(even) { - background-color: #ededed; } - .form-field .form-field-label, .form-field .form-field-help, - .form-field .form-field-input { - width: 30%; - text-align: left; - margin-right: 40px; } - .form-field .form-field-label { - font-weight: bold; } - .form-field .form-field-input input { - width: 100%; } - .form-field .form-field-help { - font-style: italic; } - .form-field .form-field-error { - color: #DF1E43; } diff --git a/templates/form.html b/templates/form.html index d1dff9a..0d0f195 100644 --- a/templates/form.html +++ b/templates/form.html @@ -3,11 +3,13 @@ Protocol Builder Mock Configuration + {% assets 'app_scss' %} - + {% endassets %} +

{{ title }}

@@ -27,7 +29,7 @@ {% endfor %} - Cancel + Cancel diff --git a/templates/index.html b/templates/index.html index 6760d61..a5e2264 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,13 +2,13 @@ Protocol Builder Mock - + {% assets 'app_scss' %} - + {% endassets %} - +

Protocol Builder Mock

diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..d0d6780 --- /dev/null +++ b/wsgi.py @@ -0,0 +1,20 @@ +from werkzeug.exceptions import NotFound +from werkzeug.middleware.dispatcher import DispatcherMiddleware +from werkzeug.middleware.proxy_fix import ProxyFix + +from pb import app + +if __name__ == "__main__": + def no_app(environ, start_response): + return NotFound()(environ, start_response) + + + # Remove trailing slash, but add leading slash + base_url = '/' + app.config['APPLICATION_ROOT'].strip('/') + + app.wsgi_app = DispatcherMiddleware(no_app, {app.config['APPLICATION_ROOT']: app.wsgi_app}) + app.wsgi_app = ProxyFix(app.wsgi_app) + + flask_port = app.config['FLASK_PORT'] + + app.run(host='0.0.0.0', port=flask_port)