refactor page loader
This commit is contained in:
parent
8fd28cf8b1
commit
6afab2a15c
|
|
@ -11,7 +11,12 @@ from tinydb import where
|
||||||
from tinydb.storages import MemoryStorage
|
from tinydb.storages import MemoryStorage
|
||||||
|
|
||||||
from ttfrog import schema
|
from ttfrog import schema
|
||||||
from ttfrog.exceptions import ApplicationNotInitializedError
|
from ttfrog.exceptions import (
|
||||||
|
ApplicationNotInitializedError,
|
||||||
|
MalformedRequestError,
|
||||||
|
RecordNotFoundError,
|
||||||
|
UnauthorizedError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContext:
|
class ApplicationContext:
|
||||||
|
|
@ -46,6 +51,7 @@ ADMIN_EMAIL=admin@telisar
|
||||||
THEME=default
|
THEME=default
|
||||||
|
|
||||||
VIEW_URI=/
|
VIEW_URI=/
|
||||||
|
API_URI=/_/v1/
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -108,6 +114,8 @@ VIEW_URI=/
|
||||||
|
|
||||||
Session(self.web)
|
Session(self.web)
|
||||||
|
|
||||||
|
self.log = self.web.logger
|
||||||
|
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
def check_state(self) -> None:
|
def check_state(self) -> None:
|
||||||
|
|
@ -119,16 +127,16 @@ VIEW_URI=/
|
||||||
Returns the User record matching the given username and password
|
Returns the User record matching the given username and password
|
||||||
"""
|
"""
|
||||||
if not (username and password):
|
if not (username and password):
|
||||||
self.web.logger.debug("Need both username and password to login")
|
self.log.debug("Need both username and password to login")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
user = self.db.User.get(where("name") == username)
|
user = self.db.User.get(where("name") == username)
|
||||||
if not user:
|
if not user:
|
||||||
self.web.logger.debug(f"No user matching {username}")
|
self.log.debug(f"No user matching {username}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not user.check_credentials(username, password):
|
if not user.check_credentials(username, password):
|
||||||
self.web.logger.debug(f"Invalid credentials for {username}")
|
self.log.debug(f"Invalid credentials for {username}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
@ -136,5 +144,97 @@ VIEW_URI=/
|
||||||
def authorize(self, user, record, requested):
|
def authorize(self, user, record, requested):
|
||||||
return user.has_permission(record, requested)
|
return user.has_permission(record, requested)
|
||||||
|
|
||||||
|
def _get_or_create_page_by_uri(self, user, table, uri):
|
||||||
|
"""
|
||||||
|
Get a page by URI. If it doesn't exist, create a new one if and only if the user has permission
|
||||||
|
to write on its parent.
|
||||||
|
"""
|
||||||
|
uri = uri.replace(" ", "").strip("/")
|
||||||
|
if uri.startswith(self.config.VIEW_URI):
|
||||||
|
uri = uri.replace(self.config.VIEW_URI, "", 1)
|
||||||
|
|
||||||
|
parent_uri = ''
|
||||||
|
search_uri = '/'
|
||||||
|
page_name = '/'
|
||||||
|
|
||||||
|
if "/" in uri:
|
||||||
|
(parent_uri, page_name) = uri.rsplit("/", 1)
|
||||||
|
if parent_uri == 'Page':
|
||||||
|
parent_uri = '/'
|
||||||
|
search_uri = page_name
|
||||||
|
else:
|
||||||
|
search_uri = uri
|
||||||
|
elif uri:
|
||||||
|
parent_uri = "/"
|
||||||
|
search_uri = uri
|
||||||
|
page_name = uri
|
||||||
|
|
||||||
|
self.log.debug(f"Searching for page in {table = } with {search_uri = }; its parent is {parent_uri=}")
|
||||||
|
# self.log.debug("\n".join([f"{p.doc_id}: {p.uri}" for p in table.all()]))
|
||||||
|
page = table.get(where("uri") == search_uri, recurse=False)
|
||||||
|
if not page:
|
||||||
|
|
||||||
|
# load the parent to check for write permissions
|
||||||
|
self.log.debug(f"Page at {search_uri} does not exist, looking for parent at {parent_uri=}")
|
||||||
|
parent_table = table if "/" in parent_uri else self.db.Page
|
||||||
|
parent = None
|
||||||
|
try:
|
||||||
|
self.log.debug(f"Loading parent with {parent_uri}")
|
||||||
|
parent = self.get_page(user, parent_table.name, uri=parent_uri)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.debug(f"Error loading parent: {e}")
|
||||||
|
|
||||||
|
if not parent:
|
||||||
|
raise MalformedRequestError("Page does not exist and neither does its parent.")
|
||||||
|
if not self.authorize(user, parent, schema.Permissions.WRITE):
|
||||||
|
raise UnauthorizedError(f"User {user.doc_id} does not have permission to create under {parent_uri}.")
|
||||||
|
page = getattr(schema, table.name)(
|
||||||
|
name=page_name, body=f"# {page_name}\nThis page does not exist", parent=parent
|
||||||
|
)
|
||||||
|
self.log.debug(f"Returning {page.doc_id}: {page.uri}")
|
||||||
|
return page
|
||||||
|
|
||||||
|
def get_page(self, user, table_name, doc_id=None, uri=None):
|
||||||
|
"""
|
||||||
|
Get a page by doc_id or by URI, if and only if the user is allowed to read it. A new Record
|
||||||
|
instance will be returned if the requested page does not exist but the user has permission
|
||||||
|
to create it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not user.doc_id:
|
||||||
|
self.log.error(f"Invalid user: {user}")
|
||||||
|
raise MalformedRequestError("User does not exist.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
table = self.db.table(table_name)
|
||||||
|
except RuntimeError:
|
||||||
|
table = self.db.Page
|
||||||
|
self.log.error(f"Invalid table_name: {table_name}, will use Page")
|
||||||
|
# raise MalformedRequestError(f"{table_name} table does not exist.")
|
||||||
|
|
||||||
|
if doc_id:
|
||||||
|
page = table.get(doc_id=doc_id)
|
||||||
|
if not page:
|
||||||
|
raise RecordNotFoundError(f"No record with {doc_id=} was found.")
|
||||||
|
elif uri:
|
||||||
|
page = self._get_or_create_page_by_uri(user, table, uri)
|
||||||
|
else:
|
||||||
|
self.log.error("No doc_id or uri.")
|
||||||
|
raise MalformedRequestError("Either a doc_id or a uri must be specified.")
|
||||||
|
|
||||||
|
if not self.authorize(user, page, schema.Permissions.READ):
|
||||||
|
self.log.error(f"No permission for {user.name} on {page}")
|
||||||
|
raise UnauthorizedError(f"User {user.doc_id} does not have permission to read {table_name} {page.doc_id}.")
|
||||||
|
|
||||||
|
# resolve the pointers to subpages so we can render things like nav elements.
|
||||||
|
if hasattr(page, "members"):
|
||||||
|
subpages = []
|
||||||
|
for pointer in page.members:
|
||||||
|
table, pkey, pval = pointer.split("::")
|
||||||
|
subpages += self.db.table(table).search(where(pkey) == pval, recurse=False)
|
||||||
|
page.members = subpages
|
||||||
|
|
||||||
|
return page
|
||||||
|
|
||||||
|
|
||||||
sys.modules[__name__] = ApplicationContext()
|
sys.modules[__name__] = ApplicationContext()
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,21 @@ class ApplicationNotInitializedError(Exception):
|
||||||
Thrown when attempting to access methods on the
|
Thrown when attempting to access methods on the
|
||||||
ApplicationContext before it has been initialized.
|
ApplicationContext before it has been initialized.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MalformedRequestError(Exception):
|
||||||
|
"""
|
||||||
|
Thrown when a request cannnot be completed due to bad arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class RecordNotFoundError(Exception):
|
||||||
|
"""
|
||||||
|
Thrown when the specified record could not be loaded by doc_id.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthorizedError(Exception):
|
||||||
|
"""
|
||||||
|
Thrown when a user does not have permissino to do the requested action.
|
||||||
|
"""
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ from dataclasses import dataclass, field
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from flask import g
|
from flask import g
|
||||||
from grung.types import BackReference, Collection, Pointer, Record
|
from grung.types import BackReference, Collection, Pointer, Record, Timestamp
|
||||||
|
|
||||||
from ttfrog import schema
|
from ttfrog import schema
|
||||||
|
|
||||||
READ_ONLY_FIELD_TYPES = [Collection, Pointer, BackReference]
|
READ_ONLY_FIELD_TYPES = [Collection, Pointer, BackReference, Timestamp]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -22,7 +22,7 @@ class Form:
|
||||||
def read_only(self) -> set:
|
def read_only(self) -> set:
|
||||||
return [
|
return [
|
||||||
name for (name, attr) in self.record._metadata.fields.items() if type(attr) in READ_ONLY_FIELD_TYPES
|
name for (name, attr) in self.record._metadata.fields.items() if type(attr) in READ_ONLY_FIELD_TYPES
|
||||||
] + ["uid"]
|
] + ["uid", "acl"]
|
||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
for key, value in self.data.items():
|
for key, value in self.data.items():
|
||||||
|
|
@ -43,9 +43,14 @@ class Page(Form):
|
||||||
|
|
||||||
record: schema.Page
|
record: schema.Page
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def read_only(self) -> set:
|
@dataclass
|
||||||
return set(list(super().read_only) + ["stub"])
|
class Wiki(Form):
|
||||||
|
"""
|
||||||
|
A form for creating and updating Wiki records.
|
||||||
|
"""
|
||||||
|
|
||||||
|
record: schema.Page
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,27 @@ var pageContent = null;
|
||||||
var saveButton = null;
|
var saveButton = null;
|
||||||
var editorUI = null;
|
var editorUI = null;
|
||||||
|
|
||||||
|
APIv1 = {
|
||||||
|
put: function(data, callback) {
|
||||||
|
(async () => {
|
||||||
|
const raw = await fetch('/_/v1/put/' + window.location.pathname, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
'body': data
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const res = await raw.json();
|
||||||
|
if (res['code'] != 200) {
|
||||||
|
console.error("APIv1 error: ", res)
|
||||||
|
}
|
||||||
|
callback(res);
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
isReadOnly = function() {
|
isReadOnly = function() {
|
||||||
if (editor) {
|
if (editor) {
|
||||||
|
|
@ -53,7 +74,17 @@ makeSaveButton = function() {
|
||||||
button.className = 'actions';
|
button.className = 'actions';
|
||||||
button.innerHTML = 'save';
|
button.innerHTML = 'save';
|
||||||
button.id = 'saveButton';
|
button.id = 'saveButton';
|
||||||
|
button.style.border = "1px solid black";
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
|
APIv1.put({
|
||||||
|
'body': editorUI.getMarkdown()
|
||||||
|
}, (res) => {
|
||||||
|
if (res['code'] == 200) {
|
||||||
|
button.style.border = "1px solid green";
|
||||||
|
} else {
|
||||||
|
button.style.border = "1px solid red";
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
saveButton = button;
|
saveButton = button;
|
||||||
return button;
|
return button;
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,49 @@
|
||||||
from flask import Response, g, redirect, render_template, request, session, url_for
|
import json
|
||||||
from tinydb import where
|
|
||||||
|
from flask import Response, g, jsonify, redirect, render_template, request, session, url_for
|
||||||
|
|
||||||
from ttfrog import app, forms, schema
|
from ttfrog import app, forms, schema
|
||||||
|
from ttfrog.exceptions import MalformedRequestError, RecordNotFoundError, UnauthorizedError
|
||||||
|
|
||||||
|
|
||||||
def relative_uri(path: str = ""):
|
def get_page(
|
||||||
"""
|
path: str, table: str = "Page", doc_id: int = None, create_okay: bool = False
|
||||||
The request's URI relative to the VIEW_URI without the leading '/'.
|
) -> (schema.Record | None, Exception | None):
|
||||||
"""
|
|
||||||
|
|
||||||
return (path or request.path).replace(app.config.VIEW_URI, "", 1).strip("/") or "/"
|
|
||||||
|
|
||||||
|
|
||||||
def get_parent(table: str, uri: str):
|
|
||||||
try:
|
|
||||||
parent_uri = uri.strip("/").rsplit("/", 1)[0]
|
|
||||||
except IndexError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return get_page(parent_uri, table=table if "/" in parent_uri else "Page", create_okay=False)
|
|
||||||
|
|
||||||
|
|
||||||
def get_page(path: str, table: str = "Page", create_okay: bool = False):
|
|
||||||
"""
|
"""
|
||||||
Get one page, including its members, but not recursively.
|
Get one page, including its members, but not recursively.
|
||||||
"""
|
"""
|
||||||
uri = relative_uri(path)
|
try:
|
||||||
|
page = app.get_page(g.user, table, doc_id=doc_id, uri=path)
|
||||||
if table not in app.db.tables():
|
except (UnauthorizedError, MalformedRequestError) as e:
|
||||||
app.web.logger.debug(f"Table {table} does not exist in {app.db.tables()}.")
|
return None, e
|
||||||
return None
|
except RecordNotFoundError as e:
|
||||||
|
|
||||||
page = app.db.table(table).get(where("uri") == uri, recurse=False)
|
|
||||||
|
|
||||||
if not page:
|
|
||||||
app.web.logger.debug("Page does not exist.")
|
|
||||||
if not create_okay:
|
if not create_okay:
|
||||||
app.web.logger.debug("Page does not exist and creating is not okay.")
|
return None, e
|
||||||
return None
|
return page, None
|
||||||
parent = get_parent(table, uri)
|
|
||||||
if not app.authorize(g.user, parent, schema.Permissions.WRITE):
|
|
||||||
app.web.logger.debug(f"User {g.user} is not authorized to write {parent}")
|
|
||||||
return None
|
|
||||||
return getattr(schema, table)(name=uri.split("/")[-1], body="This page does not exist", parent=parent)
|
|
||||||
|
|
||||||
if not app.authorize(g.user, page, schema.Permissions.READ):
|
|
||||||
return None
|
|
||||||
|
|
||||||
if hasattr(page, "members"):
|
def api_response(response={}, messages=[], error=None):
|
||||||
subpages = []
|
response_code = 200
|
||||||
for pointer in page.members:
|
|
||||||
table, pkey, pval = pointer.split("::")
|
|
||||||
subpages += app.db.table(table).search(where(pkey) == pval, recurse=False)
|
|
||||||
page.members = subpages
|
|
||||||
|
|
||||||
return page
|
if error:
|
||||||
|
response_code = 500
|
||||||
|
response = {}
|
||||||
|
if isinstance(error, UnauthorizedError):
|
||||||
|
response_code = 403
|
||||||
|
elif isinstance(error, MalformedRequestError):
|
||||||
|
response_code = 4000
|
||||||
|
elif isinstance(error, RecordNotFoundError):
|
||||||
|
response_code = 404
|
||||||
|
|
||||||
|
return jsonify({"messages": messages, "response": response, "code": response_code}), response_code
|
||||||
|
|
||||||
|
|
||||||
def rendered(page: schema.Record, template: str = "page.html"):
|
def rendered(page: schema.Record, template: str = "page.html"):
|
||||||
if not page:
|
if not page:
|
||||||
return Response("Page not found", status=404)
|
return Response("Page not found", status=404)
|
||||||
|
|
||||||
return render_template(template, page=page, app=app, breadcrumbs=breadcrumbs(), root=g.root, user=g.user, g=g)
|
root = page if page.uri == app.config.VIEW_URI else get_page(app.config.VIEW_URI)[0]
|
||||||
|
return render_template(template, page=page, app=app, breadcrumbs=breadcrumbs(), root=root, user=g.user, g=g)
|
||||||
|
|
||||||
|
|
||||||
def breadcrumbs():
|
def breadcrumbs():
|
||||||
|
|
@ -69,14 +51,36 @@ def breadcrumbs():
|
||||||
Return (uri, name) pairs for the parents leading from the VIEW_URI to the current request.
|
Return (uri, name) pairs for the parents leading from the VIEW_URI to the current request.
|
||||||
"""
|
"""
|
||||||
uri = ""
|
uri = ""
|
||||||
for name in relative_uri().split("/"):
|
names = (request.path.replace(app.config.VIEW_URI, "", 1).strip("/") or "/").split("/")
|
||||||
|
for name in names:
|
||||||
uri = "/".join([uri, name])
|
uri = "/".join([uri, name])
|
||||||
yield (uri, name)
|
yield (uri, name)
|
||||||
|
|
||||||
|
|
||||||
|
@app.web.before_request
|
||||||
|
def before_request():
|
||||||
|
g.messages = []
|
||||||
|
if not request.path.startswith("/static"):
|
||||||
|
user_id = session.get("user_id", 1)
|
||||||
|
g.user = app.db.User.get(doc_id=user_id)
|
||||||
|
session["user_id"] = user_id
|
||||||
|
session["user"] = dict(g.user.serialize())
|
||||||
|
|
||||||
|
|
||||||
|
@app.web.after_request
|
||||||
|
def add_header(r):
|
||||||
|
r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, public, max-age=0"
|
||||||
|
r.headers["Pragma"] = "no-cache"
|
||||||
|
r.headers["Expires"] = "0"
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
@app.web.route(app.config.VIEW_URI)
|
@app.web.route(app.config.VIEW_URI)
|
||||||
def index():
|
def index():
|
||||||
return rendered(get_page(app.config.VIEW_URI, create_okay=False))
|
page, error = get_page(app.config.VIEW_URI)
|
||||||
|
if error:
|
||||||
|
g.messages.append(str(error))
|
||||||
|
return rendered(page)
|
||||||
|
|
||||||
|
|
||||||
@app.web.route("/login", methods=["GET", "POST"])
|
@app.web.route("/login", methods=["GET", "POST"])
|
||||||
|
|
@ -92,7 +96,7 @@ def login():
|
||||||
session["user"] = dict(g.user.serialize())
|
session["user"] = dict(g.user.serialize())
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
g.messages.append(f"Invalid login for {username}")
|
g.messages.append(f"Invalid login for {username}")
|
||||||
return rendered(schema.Page(name="Login", title="Please enter your login details"), "login.html")
|
return rendered(schema.Page(name="Login"), "login.html")
|
||||||
|
|
||||||
|
|
||||||
@app.web.route("/logout")
|
@app.web.route("/logout")
|
||||||
|
|
@ -106,51 +110,31 @@ def logout():
|
||||||
@app.web.route(f"{app.config.VIEW_URI}/<path:table>/<path:path>", methods=["GET"])
|
@app.web.route(f"{app.config.VIEW_URI}/<path:table>/<path:path>", methods=["GET"])
|
||||||
@app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["GET"], defaults={"table": "Page"})
|
@app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["GET"], defaults={"table": "Page"})
|
||||||
def view(table, path):
|
def view(table, path):
|
||||||
parent = get_parent(table, relative_uri())
|
page, error = get_page(request.path, table=table, create_okay=True)
|
||||||
if table not in app.db.tables():
|
if error:
|
||||||
table = parent.__class__.__name__ if parent else "Page"
|
g.messages.append(str(error))
|
||||||
page = get_page(request.path, table=table, create_okay=(parent and parent.doc_id is not None))
|
|
||||||
return rendered(page)
|
return rendered(page)
|
||||||
|
|
||||||
|
|
||||||
@app.web.route(f"{app.config.VIEW_URI}/<path:table>/<path:path>", methods=["POST"])
|
@app.web.route(f"{app.config.API_URI}/put/<path:table>/<path:path>", methods=["POST"])
|
||||||
@app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["POST"], defaults={"table": "Page"})
|
@app.web.route(f"{app.config.API_URI}/put/<path:path>", methods=["POST"], defaults={"table": "Page"})
|
||||||
def edit(table, path):
|
def put(table, path):
|
||||||
uri = relative_uri()
|
app.log.debug(f"Checking for page at {table}/{path} in {table} space")
|
||||||
parent = get_parent(table, uri)
|
page, error = get_page("/".join([table, path]), table=table, create_okay=True)
|
||||||
if not parent:
|
app.log.debug(f"Found {page.doc_id}")
|
||||||
return Response("You cannot create a page at this location.", status=403)
|
if error:
|
||||||
|
return api_response(error=error)
|
||||||
|
|
||||||
# get or create the docoument at this uri
|
params = json.loads(request.data.decode())["body"]
|
||||||
page = get_page(uri, table=table, create_okay=True)
|
save_data = getattr(forms, table)(page, params).prepare()
|
||||||
if not app.authorize(g.user, page, schema.Permissions.WRITE):
|
|
||||||
return Response("Permission denied.", status=403)
|
|
||||||
save_data = getattr(forms, table)(page, request.form).prepare()
|
|
||||||
|
|
||||||
# editing existing document
|
doc = app.db.save(save_data) if page.doc_id else page.parent.add_member(save_data)
|
||||||
if page.doc_id:
|
app.log.debug(f"Saved: {dict(doc)=}")
|
||||||
if page.uid != request.form["uid"]:
|
return api_response(response=dict(doc))
|
||||||
return Response("Invalid UID.", status=403)
|
|
||||||
return rendered(app.db.save(save_data))
|
|
||||||
|
|
||||||
# saving a new document
|
|
||||||
return rendered(parent.add_member(save_data))
|
|
||||||
|
|
||||||
|
|
||||||
@app.web.before_request
|
@app.web.route(f"{app.config.API_URI}/get/<path:table>/<int:doc_id>", methods=["GET"])
|
||||||
def before_request():
|
def get(table, doc_id):
|
||||||
g.messages = []
|
app.log.debug(f"API: getting {table}({doc_id})")
|
||||||
if not request.path.startswith("/static"):
|
page, error = get_page(g.user, table=table, doc_id=doc_id)
|
||||||
user_id = session.get("user_id", 1)
|
return api_response(response=dict(page), error=error)
|
||||||
g.user = app.db.User.get(doc_id=user_id)
|
|
||||||
session["user_id"] = user_id
|
|
||||||
session["user"] = dict(g.user.serialize())
|
|
||||||
g.root = get_page(app.config.VIEW_URI)
|
|
||||||
|
|
||||||
|
|
||||||
@app.web.after_request
|
|
||||||
def add_header(r):
|
|
||||||
r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, public, max-age=0"
|
|
||||||
r.headers["Pragma"] = "no-cache"
|
|
||||||
r.headers["Expires"] = "0"
|
|
||||||
return r
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user