Improve separation between front & back with etiquette_flask package

Move flask-specific operations out of etiquette's files and into new etiquette_flask. In etiquette_site.py, etiquette calls are fully qualified.
This commit is contained in:
voussoir 2017-05-01 21:23:16 -07:00
parent 83b9adbd61
commit a9c7ad6993
8 changed files with 151 additions and 137 deletions

View file

@ -1 +1,8 @@
pass
from . import constants
from . import decorators
from . import exceptions
from . import helpers
from . import jsonify
from . import objects
from . import photodb
from . import searchhelpers

View file

@ -1,11 +1,7 @@
import flask
from flask import request
import functools
import time
import warnings
from . import jsonify
def required_feature(features):
'''
@ -31,35 +27,6 @@ def required_feature(features):
return wrapped
return wrapper
def required_fields(fields, forbid_whitespace=False):
'''
Declare that the endpoint requires certain POST body fields. Without them,
we respond with 400 and a message.
forbid_whitespace:
If True, then providing the field is not good enough. It must also
contain at least some non-whitespace characters.
'''
def wrapper(function):
@functools.wraps(function)
def wrapped(*args, **kwargs):
for requirement in fields:
missing = (
requirement not in request.form or
(forbid_whitespace and request.form[requirement].strip() == '')
)
if missing:
response = {
'error_type': 'MISSING_FIELDS',
'error_message': 'Required fields: %s' % ', '.join(fields),
}
response = jsonify.make_json_response(response, status=400)
return response
return function(*args, **kwargs)
return wrapped
return wrapper
def not_implemented(function):
'''
Decorator to remember what needs doing.

View file

@ -1,11 +1,5 @@
import flask
import json
def make_json_response(j, *args, **kwargs):
dumped = json.dumps(j)
response = flask.Response(dumped, *args, **kwargs)
response.headers['Content-Type'] = 'application/json;charset=utf-8'
return response
def album(a, minimal=False):
j = {

View file

@ -0,0 +1,3 @@
from . import decorators
from . import jsonify
from . import sessions

View file

@ -0,0 +1,35 @@
import flask
from flask import request
import functools
from etiquette import jsonify
def required_fields(fields, forbid_whitespace=False):
'''
Declare that the endpoint requires certain POST body fields. Without them,
we respond with 400 and a message.
forbid_whitespace:
If True, then providing the field is not good enough. It must also
contain at least some non-whitespace characters.
'''
def wrapper(function):
@functools.wraps(function)
def wrapped(*args, **kwargs):
for requirement in fields:
missing = (
requirement not in request.form or
(forbid_whitespace and request.form[requirement].strip() == '')
)
if missing:
response = {
'error_type': 'MISSING_FIELDS',
'error_message': 'Required fields: %s' % ', '.join(fields),
}
response = jsonify.make_json_response(response, status=400)
return response
return function(*args, **kwargs)
return wrapped
return wrapper

View file

@ -0,0 +1,9 @@
import flask
import json
def make_json_response(j, *args, **kwargs):
dumped = json.dumps(j)
response = flask.Response(dumped, *args, **kwargs)
response.headers['Content-Type'] = 'application/json;charset=utf-8'
return response

View file

@ -3,7 +3,7 @@ from flask import request
import functools
import uuid
from . import helpers
from etiquette import helpers
def _generate_token():
token = str(uuid.uuid4())
@ -14,6 +14,7 @@ def _normalize_token(token):
if isinstance(token, flask.Request):
token = token.cookies.get('etiquette_session', None)
class SessionManager:
def __init__(self):
self.sessions = {}

View file

@ -9,24 +9,22 @@ import urllib.parse
import warnings
import zipstream
from etiquette import constants
from etiquette import decorators
from etiquette import exceptions
from etiquette import helpers
from etiquette import jsonify
from etiquette import objects
from etiquette import photodb
from etiquette import searchhelpers
from etiquette import sessions
import etiquette
import etiquette_flask
from voussoirkit import pathclass
TEMPLATE_DIR = 'C:\\git\\Etiquette\\templates'
STATIC_DIR = 'C:\\git\\Etiquette\\static'
root_dir = pathclass.Path(__file__).parent
TEMPLATE_DIR = root_dir.with_child('templates')
STATIC_DIR = root_dir.with_child('static')
FAVICON_PATH = STATIC_DIR.with_child('favicon.png')
site = flask.Flask(
__name__,
template_folder=TEMPLATE_DIR,
static_folder=STATIC_DIR,
template_folder=TEMPLATE_DIR.absolute_path,
static_folder=STATIC_DIR.absolute_path,
)
site.config.update(
SEND_FILE_MAX_AGE_DEFAULT=180,
@ -37,9 +35,9 @@ site.jinja_env.trim_blocks = True
site.jinja_env.lstrip_blocks = True
site.debug = True
P = photodb.PhotoDB()
P = etiquette.photodb.PhotoDB()
session_manager = sessions.SessionManager()
session_manager = etiquette_flask.sessions.SessionManager()
####################################################################################################
####################################################################################################
@ -67,8 +65,8 @@ def delete_synonym(synonym):
try:
master_tag = P.get_tag(synonym)
except exceptions.NoSuchTag as e:
raise exceptions.NoSuchSynonym(*e.given_args, **e.given_kwargs)
except etiquette.exceptions.NoSuchTag as e:
raise etiquette.exceptions.NoSuchSynonym(*e.given_args, **e.given_kwargs)
master_tag.remove_synonym(synonym)
return {'action':'delete_synonym', 'synonym': synonym}
@ -78,8 +76,8 @@ def P_wrapper(function):
try:
return function(thingid)
except exceptions.EtiquetteException as e:
if isinstance(e, exceptions.NoSuch):
except etiquette.exceptions.EtiquetteException as e:
if isinstance(e, etiquette.exceptions.NoSuch):
status = 404
else:
status = 400
@ -87,8 +85,8 @@ def P_wrapper(function):
if response_type == 'html':
flask.abort(status, e.error_message)
else:
response = jsonify.exception(e)
response = jsonify.make_json_response(response, status=status)
response = etiquette.jsonify.exception(e)
response = etiquette_flask.jsonify.make_json_response(response, status=status)
flask.abort(response)
except Exception as e:
@ -96,7 +94,7 @@ def P_wrapper(function):
if response_type == 'html':
flask.abort(500)
else:
flask.abort(jsonify.make_response({}, status=500))
flask.abort(etiquette.jsonify.make_json_response({}, status=500))
return P_wrapped
@ -175,7 +173,7 @@ def send_file(filepath, override_mimetype=None):
if request.method == 'HEAD':
outgoing_data = bytes()
else:
outgoing_data = helpers.read_filebytes(
outgoing_data = etiquette.helpers.read_filebytes(
filepath,
range_min=range_min,
range_max=range_max,
@ -215,12 +213,12 @@ def get_register():
@site.route('/login', methods=['POST'])
@session_manager.give_token
@decorators.required_fields(['username', 'password'])
@etiquette_flask.decorators.required_fields(['username', 'password'])
def post_login():
if session_manager.get(request):
e = exceptions.AlreadySignedIn()
response = jsonify.exception(e)
return jsonify.make_json_response(response, status=403)
e = etiquette.exceptions.AlreadySignedIn()
response = etiquette.jsonify.exception(e)
return etiquette_flask.jsonify.make_json_response(response, status=403)
username = request.form['username']
password = request.form['password']
@ -231,22 +229,22 @@ def post_login():
# page 404s anyway.
user = P.get_user(username=username)
user = P.login(user.id, password)
except (exceptions.NoSuchUser, exceptions.WrongLogin):
e = exceptions.WrongLogin()
response = jsonify.exception(e)
return jsonify.make_json_response(response, status=422)
session = sessions.Session(request, user)
except (etiquette.exceptions.NoSuchUser, etiquette.exceptions.WrongLogin):
e = etiquette.exceptions.WrongLogin()
response = etiquette.jsonify.exception(e)
return etiquette_flask.jsonify.make_json_response(response, status=422)
session = etiquette_flask.sessions.Session(request, user)
session_manager.add(session)
return jsonify.make_json_response({})
return etiquette_flask.jsonify.make_json_response({})
@site.route('/register', methods=['POST'])
@session_manager.give_token
@decorators.required_fields(['username', 'password_1', 'password_2'])
@etiquette_flask.decorators.required_fields(['username', 'password_1', 'password_2'])
def post_register():
if session_manager.get(request):
e = exceptions.AlreadySignedIn()
response = jsonify.exception(e)
return jsonify.make_json_response(response, status=403)
e = etiquette.exceptions.AlreadySignedIn()
response = etiquette.jsonify.exception(e)
return etiquette_flask.jsonify.make_json_response(response, status=403)
username = request.form['username']
password_1 = request.form['password_1']
@ -257,17 +255,17 @@ def post_register():
'error_type': 'PASSWORDS_DONT_MATCH',
'error_message': 'Passwords do not match.',
}
return jsonify.make_json_response(response, status=422)
return etiquette_flask.jsonify.make_json_response(response, status=422)
try:
user = P.register_user(username, password_1)
except exceptions.EtiquetteException as e:
response = jsonify.exception(e)
return jsonify.make_json_response(response, status=400)
except etiquette.exceptions.EtiquetteException as e:
response = etiquette.jsonify.exception(e)
return etiquette_flask.jsonify.make_json_response(response, status=400)
session = sessions.Session(request, user)
session = etiquette_flask.sessions.Session(request, user)
session_manager.add(session)
return jsonify.make_json_response({})
return etiquette_flask.jsonify.make_json_response({})
@site.route('/logout', methods=['GET', 'POST'])
@session_manager.give_token
@ -280,8 +278,7 @@ def logout():
@site.route('/favicon.ico')
@site.route('/favicon.png')
def favicon():
filename = os.path.join(STATIC_DIR, 'favicon.png')
return flask.send_file(filename)
return flask.send_file(FAVICON_PATH.absolute_path)
def get_album_core(albumid):
@ -305,11 +302,11 @@ def get_album_html(albumid):
@session_manager.give_token
def get_album_json(albumid):
album = get_album_core(albumid)
album = jsonify.album(album)
album = etiquette.jsonify.album(album)
album['sub_albums'] = [P_album(x) for x in album['sub_albums']]
album['sub_albums'].sort(key=lambda x: (x.title or x.id).lower())
album['sub_albums'] = [jsonify.album(x, minimal=True) for x in album['sub_albums']]
return jsonify.make_json_response(album)
album['sub_albums'] = [etiquette.jsonify.album(x, minimal=True) for x in album['sub_albums']]
return etiquette_flask.jsonify.make_json_response(album)
@site.route('/album/<albumid>.zip')
@ -317,16 +314,16 @@ def get_album_zip(albumid):
album = P_album(albumid)
recursive = request.args.get('recursive', True)
recursive = helpers.truthystring(recursive)
recursive = etiquette.helpers.truthystring(recursive)
arcnames = helpers.album_zip_filenames(album, recursive=recursive)
arcnames = etiquette.helpers.album_zip_filenames(album, recursive=recursive)
streamed_zip = zipstream.ZipFile()
for (real_filepath, arcname) in arcnames.items():
streamed_zip.write(real_filepath, arcname=arcname)
# Add the album metadata as an {id}.txt file within each directory.
directories = helpers.album_zip_directories(album, recursive=recursive)
directories = etiquette.helpers.album_zip_directories(album, recursive=recursive)
for (inner_album, directory) in directories.items():
text = []
if inner_album.title:
@ -346,7 +343,7 @@ def get_album_zip(albumid):
else:
download_as = 'album %s.zip' % album.id
download_as = helpers.normalize_filepath(download_as)
download_as = etiquette.helpers.normalize_filepath(download_as)
download_as = urllib.parse.quote(download_as)
outgoing_headers = {
'Content-Type': 'application/octet-stream',
@ -372,8 +369,8 @@ def get_albums_html():
@session_manager.give_token
def get_albums_json():
albums = get_albums_core()
albums = [jsonify.album(album, minimal=True) for album in albums]
return jsonify.make_json_response(albums)
albums = [etiquette.jsonify.album(album, minimal=True) for album in albums]
return etiquette_flask.jsonify.make_json_response(albums)
@site.route('/bookmarks')
@ -390,10 +387,10 @@ def get_file(photoid):
photo = P.get_photo(photoid)
do_download = request.args.get('download', False)
do_download = helpers.truthystring(do_download)
do_download = etiquette.helpers.truthystring(do_download)
use_original_filename = request.args.get('original_filename', False)
use_original_filename = helpers.truthystring(use_original_filename)
use_original_filename = etiquette.helpers.truthystring(use_original_filename)
if do_download:
if use_original_filename:
@ -401,7 +398,7 @@ def get_file(photoid):
else:
download_as = photo.id + photo.dot_extension
download_as = helpers.normalize_filepath(download_as)
download_as = etiquette.helpers.normalize_filepath(download_as)
download_as = urllib.parse.quote(download_as)
response = flask.make_response(send_file(photo.real_filepath))
response.headers['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % download_as
@ -421,12 +418,12 @@ def get_photo_html(photoid):
@session_manager.give_token
def get_photo_json(photoid):
photo = P_photo(photoid, response_type='json')
photo = jsonify.photo(photo)
photo = jsonify.make_json_response(photo)
photo = etiquette.jsonify.photo(photo)
photo = etiquette_flask.jsonify.make_json_response(photo)
return photo
def get_search_core():
warning_bag = objects.WarningBag()
warning_bag = etiquette.objects.WarningBag()
has_tags = request.args.get('has_tags')
tag_musts = request.args.get('tag_musts')
@ -442,7 +439,7 @@ def get_search_core():
limit = request.args.get('limit')
# This is being pre-processed because the site enforces a maximum value
# which the PhotoDB api does not.
limit = searchhelpers.normalize_limit(limit, warning_bag=warning_bag)
limit = etiquette.searchhelpers.normalize_limit(limit, warning_bag=warning_bag)
if limit is None:
limit = 50
@ -515,7 +512,7 @@ def get_search_core():
warnings = set()
photos = []
for item in search_results:
if isinstance(item, objects.WarningBag):
if isinstance(item, etiquette.objects.WarningBag):
warnings.update(item.warnings)
else:
photos.append(item)
@ -534,7 +531,7 @@ def get_search_core():
if len(photos) == limit:
next_params = original_params.copy()
next_params['offset'] = offset + limit
next_params = helpers.dict_to_params(next_params)
next_params = etiquette.helpers.dict_to_params(next_params)
next_page_url = '/search' + next_params
else:
next_page_url = None
@ -542,7 +539,7 @@ def get_search_core():
if offset > 0:
prev_params = original_params.copy()
prev_params['offset'] = max(0, offset - limit)
prev_params = helpers.dict_to_params(prev_params)
prev_params = etiquette.helpers.dict_to_params(prev_params)
prev_page_url = '/search' + prev_params
else:
prev_page_url = None
@ -565,7 +562,7 @@ def get_search_core():
def get_search_html():
search_results = get_search_core()
search_kwargs = search_results['search_kwargs']
qualname_map = P.export_tags(exporter=photodb.tag_export_qualname_map)
qualname_map = P.export_tags(exporter=etiquette.photodb.tag_export_qualname_map)
session = session_manager.get(request)
response = flask.render_template(
'search.html',
@ -585,9 +582,9 @@ def get_search_html():
def get_search_json():
search_results = get_search_core()
search_results['photos'] = [
jsonify.photo(photo, include_albums=False) for photo in search_results['photos']
etiquette.jsonify.photo(photo, include_albums=False) for photo in search_results['photos']
]
return jsonify.make_json_response(search_results)
return etiquette_flask.jsonify.make_json_response(search_results)
def get_tags_core(specific_tag=None):
@ -608,7 +605,7 @@ def get_tags_html(specific_tag=None):
tags = get_tags_core(specific_tag)
session = session_manager.get(request)
include_synonyms = request.args.get('synonyms')
include_synonyms = include_synonyms is None or helpers.truthystring(include_synonyms)
include_synonyms = include_synonyms is None or etiquette.helpers.truthystring(include_synonyms)
response = flask.render_template(
'tags.html',
include_synonyms=include_synonyms,
@ -625,9 +622,9 @@ def get_tags_json(specific_tag=None):
specific_tag = P_tag(specific_tag, response_type='json')
tags = get_tags_core(specific_tag)
include_synonyms = request.args.get('synonyms')
include_synonyms = include_synonyms is None or helpers.truthystring(include_synonyms)
tags = [jsonify.tag(tag, include_synonyms=include_synonyms) for tag in tags]
return jsonify.make_json_response(tags)
include_synonyms = include_synonyms is None or etiquette.helpers.truthystring(include_synonyms)
tags = [etiquette.jsonify.tag(tag, include_synonyms=include_synonyms) for tag in tags]
return etiquette_flask.jsonify.make_json_response(tags)
@site.route('/thumbnail/<photoid>')
@ -656,8 +653,8 @@ def get_user_html(username):
@session_manager.give_token
def get_user_json(username):
user = get_user_core(username)
user = jsonify.user(user)
user = jsonify.make_json_response(user)
user = etiquette.jsonify.user(user)
user = etiquette_flask.jsonify.make_json_response(user)
return user
@ -673,15 +670,15 @@ def post_album_add_tag(albumid):
tag = request.form['tagname'].strip()
try:
tag = P_tag(tag)
except exceptions.NoSuchTag as e:
response = jsonify.exception(e)
return jsonify.make_json_response(response, status=404)
except etiquette.exceptions.NoSuchTag as e:
response = etiquette.jsonify.exception(e)
return etiquette_flask.jsonify.make_json_response(response, status=404)
recursive = request.form.get('recursive', False)
recursive = helpers.truthystring(recursive)
recursive = etiquette.helpers.truthystring(recursive)
album.add_tag_to_all(tag, nested_children=recursive)
response['action'] = 'add_tag'
response['tagname'] = tag.name
return jsonify.make_json_response(response)
return etiquette_flask.jsonify.make_json_response(response)
@site.route('/album/<albumid>/edit', methods=['POST'])
@ -696,7 +693,7 @@ def post_album_edit(albumid):
description = request.form.get('description', None)
album.edit(title=title, description=description)
response = {'title': album.title, 'description': album.description}
return jsonify.make_json_response(response)
return etiquette_flask.jsonify.make_json_response(response)
def post_photo_add_remove_tag_core(photoid, tagname, add_or_remove):
@ -708,16 +705,16 @@ def post_photo_add_remove_tag_core(photoid, tagname, add_or_remove):
photo.add_tag(tag)
elif add_or_remove == 'remove':
photo.remove_tag(tag)
except exceptions.EtiquetteException as e:
response = jsonify.exception(e)
response = jsonify.make_json_response(response, status=400)
except etiquette.exceptions.EtiquetteException as e:
response = etiquette.jsonify.exception(e)
response = etiquette_flask.jsonify.make_json_response(response, status=400)
flask.abort(response)
response = {'tagname': tag.name}
return jsonify.make_json_response(response)
return etiquette_flask.jsonify.make_json_response(response)
@site.route('/photo/<photoid>/add_tag', methods=['POST'])
@decorators.required_fields(['tagname'], forbid_whitespace=True)
@etiquette_flask.decorators.required_fields(['tagname'], forbid_whitespace=True)
def post_photo_add_tag(photoid):
'''
Add a tag to this photo.
@ -725,7 +722,7 @@ def post_photo_add_tag(photoid):
return post_photo_add_remove_tag_core(photoid, request.form['tagname'], 'add')
@site.route('/photo/<photoid>/remove_tag', methods=['POST'])
@decorators.required_fields(['tagname'], forbid_whitespace=True)
@etiquette_flask.decorators.required_fields(['tagname'], forbid_whitespace=True)
def post_photo_remove_tag(photoid):
'''
Remove a tag from this photo.
@ -737,29 +734,30 @@ def post_photo_refresh_metadata(photoid):
'''
Refresh the file metadata.
'''
P.caches['photo'].remove(photoid)
photo = P_photo(photoid, response_type='json')
try:
photo.reload_metadata()
except exceptions.EtiquetteException as e:
response = jsonify.exception(e)
response = jsonify.make_json_response(response, status=400)
except etiquette.exceptions.EtiquetteException as e:
response = etiquette.jsonify.exception(e)
response = etiquette_flask.jsonify.make_json_response(response, status=400)
flask.abort(response)
return jsonify.make_json_response({})
return etiquette_flask.jsonify.make_json_response({})
def post_tag_create_delete_core(tagname, function):
try:
response = function(tagname)
status = 200
except exceptions.EtiquetteException as e:
response = jsonify.exception(e)
except etiquette.exceptions.EtiquetteException as e:
response = etiquette.jsonify.exception(e)
status = 400
#print(response)
return jsonify.make_json_response(response, status=status)
return etiquette_flask.jsonify.make_json_response(response, status=status)
@site.route('/tags/create_tag', methods=['POST'])
@decorators.required_fields(['tagname'], forbid_whitespace=True)
@etiquette_flask.decorators.required_fields(['tagname'], forbid_whitespace=True)
def post_tag_create():
'''
Create a tag.
@ -767,7 +765,7 @@ def post_tag_create():
return post_tag_create_delete_core(request.form['tagname'], create_tag)
@site.route('/tags/delete_tag', methods=['POST'])
@decorators.required_fields(['tagname'], forbid_whitespace=True)
@etiquette_flask.decorators.required_fields(['tagname'], forbid_whitespace=True)
def post_tag_delete():
'''
Delete a tag.
@ -775,7 +773,7 @@ def post_tag_delete():
return post_tag_create_delete_core(request.form['tagname'], delete_tag)
@site.route('/tags/delete_synonym', methods=['POST'])
@decorators.required_fields(['tagname'], forbid_whitespace=True)
@etiquette_flask.decorators.required_fields(['tagname'], forbid_whitespace=True)
def post_tag_delete_synonym():
'''
Delete a synonym.