diff options
| -rw-r--r-- | README.org | 2 | ||||
| -rw-r--r-- | uploader/background_jobs.py | 114 | ||||
| -rw-r--r-- | uploader/errors.py | 3 | ||||
| -rw-r--r-- | uploader/flask_extensions.py | 40 | ||||
| -rw-r--r-- | uploader/phenotypes/views.py | 9 | ||||
| -rw-r--r-- | uploader/session.py | 14 | ||||
| -rw-r--r-- | uploader/static/css/layout-large.css | 6 | ||||
| -rw-r--r-- | uploader/static/css/theme.css | 17 | ||||
| -rw-r--r-- | uploader/templates/background-jobs/base.html | 10 | ||||
| -rw-r--r-- | uploader/templates/background-jobs/delete-job.html | 61 | ||||
| -rw-r--r-- | uploader/templates/background-jobs/job-status.html | 41 | ||||
| -rw-r--r-- | uploader/templates/background-jobs/job-summary.html | 69 | ||||
| -rw-r--r-- | uploader/templates/background-jobs/list-jobs.html | 79 | ||||
| -rw-r--r-- | uploader/templates/background-jobs/macro-display-job-details.html | 29 | ||||
| -rw-r--r-- | uploader/templates/background-jobs/stop-job.html | 61 | ||||
| -rw-r--r-- | uploader/templates/base.html | 28 | ||||
| -rw-r--r-- | uploader/ui.py | 2 |
17 files changed, 553 insertions, 32 deletions
diff --git a/README.org b/README.org index ca77653..efa837b 100644 --- a/README.org +++ b/README.org @@ -219,7 +219,7 @@ To check for correct type usage in the application, run: Run unit tests with: #+BEGIN_SRC shell $ export UPLOADER_CONF=</path/to/configuration/file.py> - $ pytest -m unit_test + $ pytest -m unit_test -n auto #+END_SRC To run ALL tests (not just unit tests): diff --git a/uploader/background_jobs.py b/uploader/background_jobs.py index 2c55272..61e57f7 100644 --- a/uploader/background_jobs.py +++ b/uploader/background_jobs.py @@ -1,29 +1,32 @@ """Generic views and utilities to handle background jobs.""" import uuid +import datetime import importlib from typing import Callable from functools import partial from werkzeug.wrappers.response import Response from flask import ( + flash, + request, redirect, Blueprint, - render_template, current_app as app) from gn_libs import jobs from gn_libs import sqlite3 from gn_libs.jobs.jobs import JobNotFound -from uploader.flask_extensions import url_for +from uploader import session from uploader.authorisation import require_login +from uploader.flask_extensions import url_for, render_template background_jobs_bp = Blueprint("background-jobs", __name__) HandlerType = Callable[[dict], Response] def __default_error_handler__(job: dict) -> Response: - return redirect(url_for("background-jobs.job_error", job_id=job["job_id"])) + return redirect(url_for("background-jobs.job_summary", job_id=job["job_id"])) def register_handlers( job_type: str, @@ -98,13 +101,15 @@ def job_status(job_id: uuid.UUID): status = job["metadata"]["status"] register_job_handlers(job) - if status == "error": + if status in ("error", "stopped"): return error_handler(job) if status == "completed": return success_handler(job) - return render_template("jobs/job-status.html", job=job) + return render_template("background-jobs/job-status.html", + job=job, + display_datetime=make_datetime_formatter()) except JobNotFound as _jnf: return render_template("jobs/job-not-found.html", job_id=job_id) @@ -119,3 +124,102 @@ def job_error(job_id: uuid.UUID): return render_template("jobs/job-error.html", job=job) except JobNotFound as _jnf: return render_template("jobs/job-not-found.html", job_id=job_id) + + +def make_datetime_formatter(dtformat: str = "") -> Callable[[str], str]: + """Make a datetime formatter with the provided `dtformat`""" + def __formatter__(val: str) -> str: + dt = datetime.datetime.fromisoformat(val) + return dt.strftime(dtformat.strip() or "%A, %d %B %Y at %H:%M %Z") + + return __formatter__ + + +@background_jobs_bp.route("/list") +@require_login +def list_jobs(): + """List background jobs.""" + with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn: + return render_template( + "background-jobs/list-jobs.html", + jobs=jobs.jobs_by_external_id( + conn, session.user_details()["user_id"]), + display_datetime=make_datetime_formatter()) + + +@background_jobs_bp.route("/summary/<uuid:job_id>") +@require_login +def job_summary(job_id: uuid.UUID): + """Provide a summary for completed jobs.""" + with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn: + try: + job = jobs.job(conn, job_id, fulldetails=True) + status = job["metadata"]["status"] + + if status in ("completed", "error", "stopped"): + return render_template("background-jobs/job-summary.html", + job=job, + display_datetime=make_datetime_formatter()) + return redirect(url_for( + "background-jobs.job_status", job_id=job["job_id"])) + except JobNotFound as _jnf: + return render_template("jobs/job-not-found.html", job_id=job_id) + + +@background_jobs_bp.route("/delete/<uuid:job_id>", methods=["GET", "POST"]) +@require_login +def delete_single(job_id: uuid.UUID): + """Delete a single job.""" + with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn: + try: + job = jobs.job(conn, job_id, fulldetails=True) + status = job["metadata"]["status"] + if status not in ("completed", "error", "stopped"): + flash("We cannot delete a running job.", "alert alert-danger") + return redirect(url_for( + "background-jobs.job_summary", job_id=job_id)) + + if request.method == "GET": + return render_template("background-jobs/delete-job.html", + job=job, + display_datetime=make_datetime_formatter()) + + if request.form["btn-confirm-delete"] == "delete": + jobs.delete_job(conn, job_id) + flash("Job was deleted successfully.", "alert alert-success") + return redirect(url_for("background-jobs.list_jobs")) + flash("Delete cancelled.", "alert alert-info") + return redirect(url_for( + "background-jobs.job_summary", job_id=job_id)) + except JobNotFound as _jnf: + return render_template("jobs/job-not-found.html", job_id=job_id) + + +@background_jobs_bp.route("/stop/<uuid:job_id>", methods=["GET", "POST"]) +@require_login +def stop_job(job_id: uuid.UUID): + """Stop a running job.""" + with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn: + try: + job = jobs.job(conn, job_id, fulldetails=True) + status = job["metadata"]["status"] + if status != "running": + flash("Cannot stop a job that is not running.", "alert alert-danger") + return redirect(url_for( + "background-jobs.job_summary", job_id=job_id)) + + if request.method == "GET": + return render_template("background-jobs/stop-job.html", + job=job, + display_datetime=make_datetime_formatter()) + + if request.form["btn-confirm-stop"] == "stop": + jobs.kill_job(conn, job_id) + flash("Job was stopped successfully.", "alert alert-success") + return redirect(url_for( + "background-jobs.job_summary", job_id=job_id)) + flash("Stop cancelled.", "alert alert-info") + return redirect(url_for( + "background-jobs.job_summary", job_id=job_id)) + except JobNotFound as _jnf: + return render_template("jobs/job-not-found.html", job_id=job_id) diff --git a/uploader/errors.py b/uploader/errors.py index 3e7c893..2ac48b8 100644 --- a/uploader/errors.py +++ b/uploader/errors.py @@ -3,7 +3,8 @@ import traceback from werkzeug.exceptions import HTTPException import MySQLdb as mdb -from flask import Flask, request, render_template, current_app as app +from flask import Flask, request, current_app as app +from uploader.flask_extensions import render_template def handle_general_exception(exc: Exception): """Handle generic exceptions.""" diff --git a/uploader/flask_extensions.py b/uploader/flask_extensions.py index 30fbad7..83d25aa 100644 --- a/uploader/flask_extensions.py +++ b/uploader/flask_extensions.py @@ -2,19 +2,16 @@ import logging from typing import Any, Optional -from flask import (request, current_app as app, url_for as flask_url_for) +from flask import ( + request, + current_app as app, + url_for as flask_url_for, + render_template as flask_render_template) logger = logging.getLogger(__name__) -def url_for( - endpoint: str, - _anchor: Optional[str] = None, - _method: Optional[str] = None, - _scheme: Optional[str] = None, - _external: Optional[bool] = None, - **values: Any) -> str: - """Extension to flask's `url_for` function.""" +def __fetch_flags__(): flags = {} for flag in app.config["FEATURE_FLAGS_HTTP"]: flag_value = (request.args.get(flag) or request.form.get(flag) or "").strip() @@ -22,12 +19,33 @@ def url_for( flags[flag] = flag_value continue continue + logger.debug("HTTP FEATURE FLAGS: %s", flags) + return flags - logger.debug("HTTP FEATURE FLAGS: %s, other variables: %s", flags, values) + +def url_for( + endpoint: str, + _anchor: Optional[str] = None, + _method: Optional[str] = None, + _scheme: Optional[str] = None, + _external: Optional[bool] = None, + **values: Any) -> str: + """Extension to flask's `url_for` function.""" + logger.debug("other variables: %s", values) return flask_url_for(endpoint=endpoint, _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external, **values, - **flags) + **__fetch_flags__()) + + +def render_template(template_name_or_list, **context: Any) -> str: + """Extend flask's `render_template` function""" + return flask_render_template( + template_name_or_list, + **{ + **context, + **__fetch_flags__() # override any flag values + }) diff --git a/uploader/phenotypes/views.py b/uploader/phenotypes/views.py index c4c6170..60d5775 100644 --- a/uploader/phenotypes/views.py +++ b/uploader/phenotypes/views.py @@ -679,7 +679,8 @@ def load_data_to_database( "success_handler": ( "uploader.phenotypes.views" ".load_phenotypes_success_handler") - }) + }, + external_id=session.logged_in_user_id()) ).then( lambda job: gnlibs_jobs.launch_job( job, @@ -1059,7 +1060,8 @@ def recompute_means(# pylint: disable=[unused-argument] "success_handler": ( "uploader.phenotypes.views." "recompute_phenotype_means_success_handler") - }), + }, + external_id=session.logged_in_user_id()), _jobs_db, Path(f"{app.config['UPLOAD_FOLDER']}/job_errors"), worker_manager="gn_libs.jobs.launcher", @@ -1138,7 +1140,8 @@ def rerun_qtlreaper(# pylint: disable=[unused-argument] "success_handler": ( "uploader.phenotypes.views." "rerun_qtlreaper_success_handler") - }), + }, + external_id=session.logged_in_user_id()), _jobs_db, Path(f"{app.config['UPLOAD_FOLDER']}/job_errors"), worker_manager="gn_libs.jobs.launcher", diff --git a/uploader/session.py b/uploader/session.py index 5af5827..9cb305b 100644 --- a/uploader/session.py +++ b/uploader/session.py @@ -1,4 +1,5 @@ """Deal with user sessions""" +import logging from uuid import UUID, uuid4 from datetime import datetime from typing import Any, Optional, TypedDict @@ -7,6 +8,8 @@ from authlib.jose import KeySet from flask import request, session from pymonad.either import Left, Right, Either +logger = logging.getLogger(__name__) + class UserDetails(TypedDict): """Session information relating specifically to the user.""" @@ -91,6 +94,17 @@ def user_details() -> UserDetails: """Retrieve user details.""" return session_info()["user"] + +def logged_in_user_id() -> Optional[UUID]: + """Get user id for logged in user. If user has not logged in, return None.""" + return user_token().then( + lambda _tok: user_details() + ).then( + lambda _user: Either(_user["user_id"], + (None, _user["email"] != "anon@ymous.user")) + ).either(lambda _err: None, lambda uid: uid) + + def user_token() -> Either: """Retrieve the user token.""" return session_info()["user"]["token"] diff --git a/uploader/static/css/layout-large.css b/uploader/static/css/layout-large.css index 8abd2dd..c40a130 100644 --- a/uploader/static/css/layout-large.css +++ b/uploader/static/css/layout-large.css @@ -6,13 +6,15 @@ } #header { + margin: -0.7em; /* Fill entire length of screen */ + /* Place it in the parent element */ grid-column-start: 1; grid-column-end: 3; /* Define layout for the children elements */ display: grid; - grid-template-columns: 8fr 2fr; + grid-template-columns: 1fr 9fr; } #header #header-text { @@ -45,6 +47,8 @@ grid-column-start: 1; grid-column-end: 3; padding: 0 3px; + + margin: -0.3em -0.7em 0 -0.7em; } #main #main-content { diff --git a/uploader/static/css/theme.css b/uploader/static/css/theme.css index cd3d103..45e5d3d 100644 --- a/uploader/static/css/theme.css +++ b/uploader/static/css/theme.css @@ -8,24 +8,27 @@ body { #header { background-color: #336699; color: #FFFFFF; - border-radius: 3px; min-height: 30px; + border-bottom: solid black 1px; } #header #header-nav .nav li a { /* Content styling */ color: #FFFFFF; - background: #4477AA; - border: solid 5px #336699; - border-radius: 5px; font-size: 0.7em; text-align: center; padding: 1px 7px; + text-decoration: none; } #main #breadcrumbs { - border-radius:3px; text-align: center; + background-color: #D5D5D5; + padding: 0 1em 0 1em; +} + +#main #breadcrumbs .breadcrumb { + padding-top: 0.5em; } #main #main-content { @@ -88,3 +91,7 @@ table.dataTable tbody tr.selected td { .breadcrumb-item { text-transform: Capitalize; } + +.breadcrumb-item a { + text-decoration: none; +} diff --git a/uploader/templates/background-jobs/base.html b/uploader/templates/background-jobs/base.html new file mode 100644 index 0000000..7201207 --- /dev/null +++ b/uploader/templates/background-jobs/base.html @@ -0,0 +1,10 @@ +{%extends "base.html"%} + +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('background-jobs.list_jobs')}}"> + background jobs + </a> +</li> +{%endblock%} diff --git a/uploader/templates/background-jobs/delete-job.html b/uploader/templates/background-jobs/delete-job.html new file mode 100644 index 0000000..242c775 --- /dev/null +++ b/uploader/templates/background-jobs/delete-job.html @@ -0,0 +1,61 @@ +{%extends "background-jobs/base.html"%} +{%from "flash_messages.html" import flash_all_messages%} +{%from "background-jobs/macro-display-job-details.html" import display_job_details%} + +{%block title%}Background Jobs{%endblock%} + +{%block pagetitle%}Background Jobs{%endblock%} + +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('background-jobs.job_summary', job_id=job.job_id)}}"> + summary + </a> +</li> +{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"> + <h2 class="heading">background jobs: delete?</h2> + + <p class="text-danger">Are you sure you want to delete the job below?</p> + + {{display_job_details(job, display_datetime)}} +</div> + +<div class="row"> + <form id="frm-delete-job" + method="POST" + action="{{url_for('background-jobs.delete_single', job_id=job.job_id)}}"> + <div class="row"> + <div class="col"> + <input type="submit" + class="btn btn-info" + value="cancel" + name="btn-confirm-delete" /> + </div> + <div class="col"> + <input type="submit" + class="btn btn-danger" + value="delete" + name="btn-confirm-delete" /> + </div> + </div> + </form> +</div> +{%endblock%} + + +{%block sidebarcontents%} +<div class="row"> + <h6 class="subheading">What is this?</h6> +</div> +<div class="row"> + <p>Confirm whether or not you want to delete job + <strong>{{job.job_id}}</strong>.</p> +</div> +{{super()}} +{%endblock%} diff --git a/uploader/templates/background-jobs/job-status.html b/uploader/templates/background-jobs/job-status.html new file mode 100644 index 0000000..50cf6e5 --- /dev/null +++ b/uploader/templates/background-jobs/job-status.html @@ -0,0 +1,41 @@ +{%extends "background-jobs/base.html"%} +{%from "background-jobs/macro-display-job-details.html" import display_job_details%} + +{%from "flash_messages.html" import flash_all_messages%} + +{%block extrameta%} +<meta http-equiv="refresh" content="5" /> +{%endblock%} + +{%block title%}Background Jobs{%endblock%} + +{%block pagetitle%}Background Jobs{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"> + <h2 class="heading">job status</h2> + + {{display_job_details(job, display_datetime)}} +</div> + +<div class="row"> + <div class="col"> + <a href="{{url_for('background-jobs.stop_job', job_id=job.job_id)}}" + title="Stop/Kill this job." + class="btn btn-danger">stop job</a> + </div> +</div> + +<div class="row"> + <h3 class="subheading">STDOUT</h3> + <pre>{{job["stdout"]}}</pre> +</div> + +<div class="row"> + <h3 class="subheading">STDERR</h3> + <pre>{{job["stderr"]}}</pre> +</div> + +{%endblock%} diff --git a/uploader/templates/background-jobs/job-summary.html b/uploader/templates/background-jobs/job-summary.html new file mode 100644 index 0000000..fe62d5d --- /dev/null +++ b/uploader/templates/background-jobs/job-summary.html @@ -0,0 +1,69 @@ +{%extends "background-jobs/base.html"%} +{%from "flash_messages.html" import flash_all_messages%} +{%from "background-jobs/macro-display-job-details.html" import display_job_details%} + +{%block title%}Background Jobs{%endblock%} + +{%block pagetitle%}Background Jobs{%endblock%} + +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('background-jobs.job_summary', job_id=job.job_id)}}"> + summary + </a> +</li> +{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"> + <h2 class="heading">background jobs: summary</h2> + + {{display_job_details(job, display_datetime)}} +</div> + +<div class="row"> + <div class="col"> + <a href="#" + class="btn btn-info not-implemented" + title="Update the expiry date and time for this job.">update expiry</a> + </div> + + <div class="col"> + <a href="{{url_for('background-jobs.delete_single', job_id=job.job_id)}}" + class="btn btn-danger" + title="Delete this job.">delete</a> + </div> + + {%if job.metadata.status in ("stopped",)%} + <div class="col"> + <a href="#" + class="btn btn-warning not-implemented" + title="Create a new copy of this job, and run the copy.">Run Copy</a> + </div> + {%endif%} +</div> + +<div class="row"> + <h3 class="subheading">Script Errors and Logging</h3> + <pre>{{job["stderr"]}}</pre> +</div> + +<div class="row"> + <h3 class="subheading">Script Output</h3> + <pre>{{job["stdout"]}}</pre> +</div> +{%endblock%} + + +{%block sidebarcontents%} +<div class="row"> + <h6 class="subheading">What is this?</h6> +</div> +<div class="row"> + <p>This page shows the results of running job '{{job.job_id}}'.</p> +</div> +{{super()}} +{%endblock%} diff --git a/uploader/templates/background-jobs/list-jobs.html b/uploader/templates/background-jobs/list-jobs.html new file mode 100644 index 0000000..c16b850 --- /dev/null +++ b/uploader/templates/background-jobs/list-jobs.html @@ -0,0 +1,79 @@ +{%extends "background-jobs/base.html"%} +{%from "flash_messages.html" import flash_all_messages%} + +{%block title%}Background Jobs{%endblock%} + +{%block pagetitle%}Background Jobs{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"><h2 class="heading">Background Jobs</h2></div> + +<div class="row"> + <div class="table-responsive"> + <table class="table"> + <thead> + <tr class="table-primary"> + <th>Type</th> + <th>Created</th> + <th title="Date and time past which the job's details will be deleted from the system."> + Expires</th> + <th>Status</th> + <th>Actions</th> + </tr> + </thead> + + <tbody> + {%for job in jobs%} + <tr> + <td>{{job.metadata["job-type"]}}</td> + <td>{{display_datetime(job.created)}}</td> + <td title="Date and time past which the job's details will be deleted from the system."> + {{display_datetime(job.expires)}} + </td> + <td {%if job.metadata.status == "completed"%} + class="fw-bold text-capitalize text-success" + {%elif job.metadata.status == "error"%} + class="fw-bold text-capitalize text-danger" + {%elif job.metadata.status == "stopped"%} + class="fw-bold text-capitalize text-warning" + {%else%} + class="fw-bold text-capitalize text-info" + {%endif%}> + <div> + {{job.metadata.status}} + </div> + </td> + <td> + <a href="{{url_for('background-jobs.job_summary', job_id=job.job_id)}}" + class="btn btn-info" + title="View more detailed information about this job."> + view summary</a> + </td> + </tr> + {%else%} + <tr> + <td colspan="5"> + You do not have any jobs you have run in the background.</td> + </tr> + {%endfor%} + </tbody> + </table> + </div> +</div> +{%endblock%} + + +{%block sidebarcontents%} +<div class="row"> + <h6 class="subheading">What is this?</h6> +</div> +<div class="row"> + <p>The table lists the jobs that are running in the background, that you + started.</p> + <p>You can use the tools provided on this page to manage the jobs, and to view + each job's details.</p> +</div> +{{super()}} +{%endblock%} diff --git a/uploader/templates/background-jobs/macro-display-job-details.html b/uploader/templates/background-jobs/macro-display-job-details.html new file mode 100644 index 0000000..82e33c0 --- /dev/null +++ b/uploader/templates/background-jobs/macro-display-job-details.html @@ -0,0 +1,29 @@ +{%macro display_job_details(job, display_datetime)%} +<table class="table"> + <thead> + </thead> + + <tbody> + <tr> + <th class="table-primary">Job ID</th> + <td>{{job.job_id}}</td> + </tr> + <tr> + <th class="table-primary">Type</th> + <td>{{job.metadata["job-type"]}}</td> + </tr> + <tr> + <th class="table-primary">Created</th> + <td>{{display_datetime(job.created)}}</td> + </tr> + <tr> + <th class="table-primary">Expires</th> + <td>{{display_datetime(job.expires)}}</td> + </tr> + <tr> + <th class="table-primary">Status</th> + <td>{{job.metadata.status}}</td> + </tr> + </tbody> +</table> +{%endmacro%} diff --git a/uploader/templates/background-jobs/stop-job.html b/uploader/templates/background-jobs/stop-job.html new file mode 100644 index 0000000..fc190ac --- /dev/null +++ b/uploader/templates/background-jobs/stop-job.html @@ -0,0 +1,61 @@ +{%extends "background-jobs/base.html"%} +{%from "flash_messages.html" import flash_all_messages%} +{%from "background-jobs/macro-display-job-details.html" import display_job_details%} + +{%block title%}Background Jobs{%endblock%} + +{%block pagetitle%}Background Jobs{%endblock%} + +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('background-jobs.job_summary', job_id=job.job_id)}}"> + summary + </a> +</li> +{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"> + <h2 class="heading">background jobs: stop?</h2> + + <p class="text-danger">Are you sure you want to stop the job below?</p> + + {{display_job_details(job, display_datetime)}} +</div> + +<div class="row"> + <form id="frm-stop-job" + method="POST" + action="{{url_for('background-jobs.stop_job', job_id=job.job_id)}}"> + <div class="row"> + <div class="col"> + <input type="submit" + class="btn btn-info" + value="cancel" + name="btn-confirm-stop" /> + </div> + <div class="col"> + <input type="submit" + class="btn btn-danger" + value="stop" + name="btn-confirm-stop" /> + </div> + </div> + </form> +</div> +{%endblock%} + + +{%block sidebarcontents%} +<div class="row"> + <h6 class="subheading">What is this?</h6> +</div> +<div class="row"> + <p>Confirm whether or not you want to stop job + <strong>{{job.job_id}}</strong>.</p> +</div> +{{super()}} +{%endblock%} diff --git a/uploader/templates/base.html b/uploader/templates/base.html index 719a646..dd097de 100644 --- a/uploader/templates/base.html +++ b/uploader/templates/base.html @@ -30,14 +30,34 @@ <header id="header"> <span id="header-text">GeneNetwork</span> <nav id="header-nav"> - <ul class="nav justify-content-end"> + <ul class="nav"> + {%if user_logged_in()%} + {%if view_under_construction%} + <li> + <a href="{{url_for('background-jobs.list_jobs')}}" + title="User's background jobs."> + <!-- https://icons.getbootstrap.com/icons/back/ --> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-back" viewBox="0 0 16 16"> + <path d="M0 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"/> + </svg> + Background jobs + </a> + </li> + {%endif%} + <li> - {%if user_logged_in()%} <a href="{{url_for('oauth2.logout')}}" title="Log out of the system"> + <!-- https://icons.getbootstrap.com/icons/file-person/ --> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-person" viewBox="0 0 16 16"> + <path d="M12 1a1 1 0 0 1 1 1v10.755S12 11 8 11s-5 1.755-5 1.755V2a1 1 0 0 1 1-1zM4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/> + <path d="M8 10a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/> + </svg> <span class="glyphicon glyphicon-user"></span> - {{user_email()}} Sign Out</a> - {%else%} + Sign Out ({{user_email()}})</a> + </li> + {%else%} + <li> <a href="{{authserver_authorise_uri()}}" title="Log in to the system">Sign In</a> {%endif%} diff --git a/uploader/ui.py b/uploader/ui.py index 1994056..41791c7 100644 --- a/uploader/ui.py +++ b/uploader/ui.py @@ -1,5 +1,5 @@ """Utilities to handle the UI""" -from flask import render_template as flask_render_template +from uploader.flask_extensions import render_template as flask_render_template def make_template_renderer(default): """Render template for species.""" |
