Replace etiquette_flask.py with endpoints package.
Split the object types' endpoints into separate files and group them better. Should be much easier to navigate and expand.
This commit is contained in:
		
							parent
							
								
									178a7df0b3
								
							
						
					
					
						commit
						c049b97bc6
					
				
					 9 changed files with 999 additions and 927 deletions
				
			
		|  | @ -1,6 +1,6 @@ | |||
| from . import decorators | ||||
| from . import etiquette_flask | ||||
| from . import endpoints | ||||
| from . import jsonify | ||||
| from . import sessions | ||||
| 
 | ||||
| site = etiquette_flask.site | ||||
| site = endpoints.site | ||||
|  |  | |||
|  | @ -0,0 +1,35 @@ | |||
| import flask; from flask import request | ||||
| import random | ||||
| 
 | ||||
| from . import album_endpoints | ||||
| from . import bookmark_endpoints | ||||
| from . import common | ||||
| from . import photo_endpoints | ||||
| from . import tag_endpoints | ||||
| from . import user_endpoints | ||||
| 
 | ||||
| site = common.site | ||||
| session_manager = common.session_manager | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/') | ||||
| @session_manager.give_token | ||||
| def root(): | ||||
|     motd = random.choice(common.P.config['motd_strings']) | ||||
|     session = session_manager.get(request) | ||||
|     return flask.render_template('root.html', motd=motd, session=session) | ||||
| 
 | ||||
| @site.route('/favicon.ico') | ||||
| @site.route('/favicon.png') | ||||
| def favicon(): | ||||
|     return flask.send_file(common.FAVICON_PATH.absolute_path) | ||||
| 
 | ||||
| @site.route('/apitest') | ||||
| @session_manager.give_token | ||||
| def apitest(): | ||||
|     response = flask.Response('testing') | ||||
|     return response | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     #site.run(threaded=True) | ||||
|     pass | ||||
|  | @ -0,0 +1,200 @@ | |||
| import flask; from flask import request | ||||
| import os | ||||
| import urllib.parse | ||||
| import zipstream | ||||
| 
 | ||||
| import etiquette | ||||
| 
 | ||||
| from .. import decorators | ||||
| from .. import jsonify | ||||
| from . import common | ||||
| 
 | ||||
| site = common.site | ||||
| session_manager = common.session_manager | ||||
| 
 | ||||
| 
 | ||||
| # Individual albums ################################################################################ | ||||
| 
 | ||||
| @site.route('/album/<album_id>') | ||||
| @session_manager.give_token | ||||
| def get_album_html(album_id): | ||||
|     album = common.P_album(album_id) | ||||
|     session = session_manager.get(request) | ||||
|     response = flask.render_template( | ||||
|         'album.html', | ||||
|         album=album, | ||||
|         session=session, | ||||
|         view=request.args.get('view', 'grid'), | ||||
|     ) | ||||
|     return response | ||||
| 
 | ||||
| @site.route('/album/<album_id>.json') | ||||
| @session_manager.give_token | ||||
| def get_album_json(album_id): | ||||
|     album = common.P_album(album_id) | ||||
|     album = etiquette.jsonify.album(album) | ||||
|     album['sub_albums'] = [common.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'] = [etiquette.jsonify.album(x, minimal=True) for x in album['sub_albums']] | ||||
|     return jsonify.make_json_response(album) | ||||
| 
 | ||||
| @site.route('/album/<album_id>.zip') | ||||
| def get_album_zip(album_id): | ||||
|     album = common.P_album(album_id) | ||||
| 
 | ||||
|     recursive = request.args.get('recursive', True) | ||||
|     recursive = etiquette.helpers.truthystring(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 = etiquette.helpers.album_zip_directories(album, recursive=recursive) | ||||
|     for (inner_album, directory) in directories.items(): | ||||
|         text = [] | ||||
|         if inner_album.title: | ||||
|             text.append('Title: ' + inner_album.title) | ||||
|         if inner_album.description: | ||||
|             text.append('Description: ' + inner_album.description) | ||||
|         if not text: | ||||
|             continue | ||||
|         text = '\r\n\r\n'.join(text) | ||||
|         streamed_zip.writestr( | ||||
|             arcname=os.path.join(directory, 'album %s.txt' % inner_album.id), | ||||
|             data=text.encode('utf-8'), | ||||
|         ) | ||||
| 
 | ||||
|     if album.title: | ||||
|         download_as = 'album %s - %s.zip' % (album.id, album.title) | ||||
|     else: | ||||
|         download_as = 'album %s.zip' % album.id | ||||
| 
 | ||||
|     download_as = etiquette.helpers.remove_path_badchars(download_as) | ||||
|     download_as = urllib.parse.quote(download_as) | ||||
|     outgoing_headers = { | ||||
|         'Content-Type': 'application/octet-stream', | ||||
|         'Content-Disposition': 'attachment; filename*=UTF-8\'\'%s' % download_as, | ||||
| 
 | ||||
|     } | ||||
|     return flask.Response(streamed_zip, headers=outgoing_headers) | ||||
| 
 | ||||
| # Album photo operations ########################################################################### | ||||
| 
 | ||||
| @site.route('/album/<album_id>/add_photo', methods=['POST']) | ||||
| @session_manager.give_token | ||||
| @decorators.catch_etiquette_exception | ||||
| @decorators.required_fields(['photo_id'], forbid_whitespace=True) | ||||
| def post_album_add_photo(album_id): | ||||
|     ''' | ||||
|     Add a photo or photos to this album. | ||||
|     ''' | ||||
|     response = {} | ||||
|     album = common.P_album(album_id) | ||||
| 
 | ||||
|     photo_ids = etiquette.helpers.comma_space_split(request.form['photo_id']) | ||||
|     photos = [common.P_photo(photo_id) for photo_id in photo_ids] | ||||
|     for photo in photos: | ||||
|         album.add_photo(photo, commit=False) | ||||
|     common.P.commit() | ||||
|     return jsonify.make_json_response(response) | ||||
| 
 | ||||
| @site.route('/album/<album_id>/remove_photo', methods=['POST']) | ||||
| @session_manager.give_token | ||||
| @decorators.catch_etiquette_exception | ||||
| @decorators.required_fields(['photo_id'], forbid_whitespace=True) | ||||
| def post_album_remove_photo(album_id): | ||||
|     ''' | ||||
|     Remove a photo or photos from this album. | ||||
|     ''' | ||||
|     response = {} | ||||
|     album = common.P_album(album_id) | ||||
| 
 | ||||
|     photo_ids = etiquette.helpers.comma_space_split(request.form['photo_id']) | ||||
|     photos = [common.P_photo(photo_id) for photo_id in photo_ids] | ||||
|     for photo in photos: | ||||
|         album.remove_photo(photo, commit=False) | ||||
|     common.P.commit() | ||||
|     return jsonify.make_json_response(response) | ||||
| 
 | ||||
| # Album tag operations ############################################################################# | ||||
| 
 | ||||
| @site.route('/album/<album_id>/add_tag', methods=['POST']) | ||||
| @decorators.catch_etiquette_exception | ||||
| @session_manager.give_token | ||||
| def post_album_add_tag(album_id): | ||||
|     ''' | ||||
|     Apply a tag to every photo in the album. | ||||
|     ''' | ||||
|     response = {} | ||||
|     album = common.P_album(album_id) | ||||
| 
 | ||||
|     tag = request.form['tagname'].strip() | ||||
|     try: | ||||
|         tag = common.P_tag(tag) | ||||
|     except etiquette.exceptions.NoSuchTag as exc: | ||||
|         response = etiquette.jsonify.exception(exc) | ||||
|         return jsonify.make_json_response(response, status=404) | ||||
|     recursive = request.form.get('recursive', False) | ||||
|     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) | ||||
| 
 | ||||
| # Album metadata operations ######################################################################## | ||||
| 
 | ||||
| @site.route('/album/<album_id>/edit', methods=['POST']) | ||||
| @session_manager.give_token | ||||
| @decorators.catch_etiquette_exception | ||||
| def post_album_edit(album_id): | ||||
|     ''' | ||||
|     Edit the title / description. | ||||
|     ''' | ||||
|     album = common.P_album(album_id) | ||||
| 
 | ||||
|     title = request.form.get('title', None) | ||||
|     description = request.form.get('description', None) | ||||
|     album.edit(title=title, description=description) | ||||
|     response = etiquette.jsonify.album(album, minimal=True) | ||||
|     return jsonify.make_json_response(response) | ||||
| 
 | ||||
| # Album listings ################################################################################### | ||||
| 
 | ||||
| def get_albums_core(): | ||||
|     albums = list(common.P.get_root_albums()) | ||||
|     albums.sort(key=lambda x: x.display_name.lower()) | ||||
|     return albums | ||||
| 
 | ||||
| @site.route('/albums') | ||||
| @session_manager.give_token | ||||
| def get_albums_html(): | ||||
|     albums = get_albums_core() | ||||
|     session = session_manager.get(request) | ||||
|     return flask.render_template('albums.html', albums=albums, session=session) | ||||
| 
 | ||||
| @site.route('/albums.json') | ||||
| @session_manager.give_token | ||||
| def get_albums_json(): | ||||
|     albums = get_albums_core() | ||||
|     albums = [etiquette.jsonify.album(album, minimal=True) for album in albums] | ||||
|     return jsonify.make_json_response(albums) | ||||
| 
 | ||||
| # Album create and delete ########################################################################## | ||||
| 
 | ||||
| @site.route('/albums/create_album', methods=['POST']) | ||||
| @decorators.catch_etiquette_exception | ||||
| def post_albums_create(): | ||||
|     title = request.form.get('title', None) | ||||
|     description = request.form.get('description', None) | ||||
|     parent = request.form.get('parent', None) | ||||
|     if parent is not None: | ||||
|         parent = common.P_album(parent) | ||||
| 
 | ||||
|     album = common.P.new_album(title=title, description=description) | ||||
|     if parent is not None: | ||||
|         parent.add_child(album) | ||||
|     response = etiquette.jsonify.album(album, minimal=False) | ||||
|     return jsonify.make_json_response(response) | ||||
|  | @ -0,0 +1,64 @@ | |||
| import flask; from flask import request | ||||
| 
 | ||||
| import etiquette | ||||
| 
 | ||||
| from .. import decorators | ||||
| from .. import jsonify | ||||
| from . import common | ||||
| 
 | ||||
| site = common.site | ||||
| session_manager = common.session_manager | ||||
| 
 | ||||
| 
 | ||||
| # Individual bookmarks ############################################################################# | ||||
| 
 | ||||
| @site.route('/bookmark/<bookmarkid>.json') | ||||
| @session_manager.give_token | ||||
| def get_bookmark_json(bookmarkid): | ||||
|     bookmark = common.P_bookmark(bookmarkid) | ||||
|     response = etiquette.jsonify.bookmark(bookmark) | ||||
|     return jsonify.make_json_response(response) | ||||
| 
 | ||||
| # Bookmark metadata operations ##################################################################### | ||||
| 
 | ||||
| @site.route('/bookmark/<bookmarkid>/edit', methods=['POST']) | ||||
| @session_manager.give_token | ||||
| @decorators.catch_etiquette_exception | ||||
| def post_bookmark_edit(bookmarkid): | ||||
|     bookmark = common.P_bookmark(bookmarkid) | ||||
|     # Emptystring is okay for titles, but not for URL. | ||||
|     title = request.form.get('title', None) | ||||
|     url = request.form.get('url', None) or None | ||||
|     bookmark.edit(title=title, url=url) | ||||
| 
 | ||||
|     response = etiquette.jsonify.bookmark(bookmark) | ||||
|     response = jsonify.make_json_response(response) | ||||
|     return response | ||||
| 
 | ||||
| # Bookmark listings ################################################################################ | ||||
| 
 | ||||
| @site.route('/bookmarks') | ||||
| @session_manager.give_token | ||||
| def get_bookmarks_html(): | ||||
|     session = session_manager.get(request) | ||||
|     bookmarks = list(common.P.get_bookmarks()) | ||||
|     return flask.render_template('bookmarks.html', bookmarks=bookmarks, session=session) | ||||
| 
 | ||||
| @site.route('/bookmarks.json') | ||||
| @session_manager.give_token | ||||
| def get_bookmarks_json(): | ||||
|     bookmarks = [etiquette.jsonify.bookmark(b) for b in common.P.get_bookmarks()] | ||||
|     return jsonify.make_json_response(bookmarks) | ||||
| 
 | ||||
| # Bookmark create and delete ####################################################################### | ||||
| 
 | ||||
| @site.route('/bookmarks/create_bookmark', methods=['POST']) | ||||
| @decorators.catch_etiquette_exception | ||||
| @decorators.required_fields(['url'], forbid_whitespace=True) | ||||
| def post_bookmarks_create(): | ||||
|     url = request.form['url'] | ||||
|     title = request.form.get('title', None) | ||||
|     bookmark = common.P.new_bookmark(url=url, title=title) | ||||
|     response = etiquette.jsonify.bookmark(bookmark) | ||||
|     response = jsonify.make_json_response(response) | ||||
|     return response | ||||
							
								
								
									
										165
									
								
								frontends/etiquette_flask/etiquette_flask/endpoints/common.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								frontends/etiquette_flask/etiquette_flask/endpoints/common.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,165 @@ | |||
| import flask; from flask import request | ||||
| import os | ||||
| import mimetypes | ||||
| import traceback | ||||
| 
 | ||||
| import etiquette | ||||
| 
 | ||||
| from voussoirkit import pathclass | ||||
| 
 | ||||
| from .. import jsonify | ||||
| from .. import sessions | ||||
| 
 | ||||
| 
 | ||||
| root_dir = pathclass.Path(__file__).parent.parent.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.absolute_path, | ||||
|     static_folder=STATIC_DIR.absolute_path, | ||||
| ) | ||||
| site.config.update( | ||||
|     SEND_FILE_MAX_AGE_DEFAULT=180, | ||||
|     TEMPLATES_AUTO_RELOAD=True, | ||||
| ) | ||||
| site.jinja_env.add_extension('jinja2.ext.do') | ||||
| site.jinja_env.trim_blocks = True | ||||
| site.jinja_env.lstrip_blocks = True | ||||
| site.debug = True | ||||
| 
 | ||||
| P = etiquette.photodb.PhotoDB() | ||||
| 
 | ||||
| session_manager = sessions.SessionManager() | ||||
| 
 | ||||
| 
 | ||||
| def P_wrapper(function): | ||||
|     def P_wrapped(thingid, response_type='html'): | ||||
|         try: | ||||
|             return function(thingid) | ||||
| 
 | ||||
|         except etiquette.exceptions.EtiquetteException as exc: | ||||
|             if isinstance(exc, etiquette.exceptions.NoSuch): | ||||
|                 status = 404 | ||||
|             else: | ||||
|                 status = 400 | ||||
| 
 | ||||
|             if response_type == 'html': | ||||
|                 flask.abort(status, exc.error_message) | ||||
|             else: | ||||
|                 response = etiquette.jsonify.exception(exc) | ||||
|                 response = jsonify.make_json_response(response, status=status) | ||||
|                 flask.abort(response) | ||||
| 
 | ||||
|         except Exception as exc: | ||||
|             traceback.print_exc() | ||||
|             if response_type == 'html': | ||||
|                 flask.abort(500) | ||||
|             else: | ||||
|                 flask.abort(jsonify.make_json_response({}, status=500)) | ||||
| 
 | ||||
|     return P_wrapped | ||||
| 
 | ||||
| @P_wrapper | ||||
| def P_album(album_id): | ||||
|     return P.get_album(album_id) | ||||
| 
 | ||||
| @P_wrapper | ||||
| def P_bookmark(bookmarkid): | ||||
|     return P.get_bookmark(bookmarkid) | ||||
| 
 | ||||
| @P_wrapper | ||||
| def P_photo(photo_id): | ||||
|     return P.get_photo(photo_id) | ||||
| 
 | ||||
| @P_wrapper | ||||
| def P_tag(tagname): | ||||
|     return P.get_tag(tagname) | ||||
| 
 | ||||
| @P_wrapper | ||||
| def P_user(username): | ||||
|     return P.get_user(username=username) | ||||
| 
 | ||||
| @P_wrapper | ||||
| def P_user_id(user_id): | ||||
|     return P.get_user(id=user_id) | ||||
| 
 | ||||
| 
 | ||||
| def back_url(): | ||||
|     return request.args.get('goto') or request.referrer or '/' | ||||
| 
 | ||||
| def send_file(filepath, override_mimetype=None): | ||||
|     ''' | ||||
|     Range-enabled file sending. | ||||
|     ''' | ||||
|     try: | ||||
|         file_size = os.path.getsize(filepath) | ||||
|     except FileNotFoundError: | ||||
|         flask.abort(404) | ||||
| 
 | ||||
|     outgoing_headers = {} | ||||
|     if override_mimetype is not None: | ||||
|         mimetype = override_mimetype | ||||
|     else: | ||||
|         mimetype = mimetypes.guess_type(filepath)[0] | ||||
| 
 | ||||
|     if mimetype is not None: | ||||
|         if 'text/' in mimetype: | ||||
|             mimetype += '; charset=utf-8' | ||||
|         outgoing_headers['Content-Type'] = mimetype | ||||
| 
 | ||||
|     if 'range' in request.headers: | ||||
|         desired_range = request.headers['range'].lower() | ||||
|         desired_range = desired_range.split('bytes=')[-1] | ||||
| 
 | ||||
|         int_helper = lambda x: int(x) if x.isdigit() else None | ||||
|         if '-' in desired_range: | ||||
|             (desired_min, desired_max) = desired_range.split('-') | ||||
|             range_min = int_helper(desired_min) | ||||
|             range_max = int_helper(desired_max) | ||||
|         else: | ||||
|             range_min = int_helper(desired_range) | ||||
| 
 | ||||
|         if range_min is None: | ||||
|             range_min = 0 | ||||
|         if range_max is None: | ||||
|             range_max = file_size | ||||
| 
 | ||||
|         # because ranges are 0-indexed | ||||
|         range_max = min(range_max, file_size - 1) | ||||
|         range_min = max(range_min, 0) | ||||
| 
 | ||||
|         range_header = 'bytes {min}-{max}/{outof}'.format( | ||||
|             min=range_min, | ||||
|             max=range_max, | ||||
|             outof=file_size, | ||||
|         ) | ||||
|         outgoing_headers['Content-Range'] = range_header | ||||
|         status = 206 | ||||
|     else: | ||||
|         range_max = file_size - 1 | ||||
|         range_min = 0 | ||||
|         status = 200 | ||||
| 
 | ||||
|     outgoing_headers['Accept-Ranges'] = 'bytes' | ||||
|     outgoing_headers['Content-Length'] = (range_max - range_min) + 1 | ||||
| 
 | ||||
|     if request.method == 'HEAD': | ||||
|         outgoing_data = bytes() | ||||
|     else: | ||||
|         outgoing_data = etiquette.helpers.read_filebytes( | ||||
|             filepath, | ||||
|             range_min=range_min, | ||||
|             range_max=range_max, | ||||
|             chunk_size=P.config['file_read_chunk'], | ||||
|         ) | ||||
| 
 | ||||
|     response = flask.Response( | ||||
|         outgoing_data, | ||||
|         status=status, | ||||
|         headers=outgoing_headers, | ||||
|     ) | ||||
|     return response | ||||
|  | @ -0,0 +1,278 @@ | |||
| import flask; from flask import request | ||||
| import json | ||||
| import traceback | ||||
| import urllib.parse | ||||
| 
 | ||||
| import etiquette | ||||
| 
 | ||||
| from .. import decorators | ||||
| from .. import jsonify | ||||
| from . import common | ||||
| 
 | ||||
| site = common.site | ||||
| session_manager = common.session_manager | ||||
| 
 | ||||
| 
 | ||||
| # Individual photos ################################################################################ | ||||
| 
 | ||||
| @site.route('/photo/<photo_id>', methods=['GET']) | ||||
| @session_manager.give_token | ||||
| def get_photo_html(photo_id): | ||||
|     photo = common.P_photo(photo_id, response_type='html') | ||||
|     session = session_manager.get(request) | ||||
|     return flask.render_template('photo.html', photo=photo, session=session) | ||||
| 
 | ||||
| @site.route('/photo/<photo_id>.json', methods=['GET']) | ||||
| @session_manager.give_token | ||||
| def get_photo_json(photo_id): | ||||
|     photo = common.P_photo(photo_id, response_type='json') | ||||
|     photo = etiquette.jsonify.photo(photo) | ||||
|     photo = jsonify.make_json_response(photo) | ||||
|     return photo | ||||
| 
 | ||||
| @site.route('/file/<photo_id>') | ||||
| def get_file(photo_id): | ||||
|     photo_id = photo_id.split('.')[0] | ||||
|     photo = common.P.get_photo(photo_id) | ||||
| 
 | ||||
|     do_download = request.args.get('download', False) | ||||
|     do_download = etiquette.helpers.truthystring(do_download) | ||||
| 
 | ||||
|     use_original_filename = request.args.get('original_filename', False) | ||||
|     use_original_filename = etiquette.helpers.truthystring(use_original_filename) | ||||
| 
 | ||||
|     if do_download: | ||||
|         if use_original_filename: | ||||
|             download_as = photo.basename | ||||
|         else: | ||||
|             download_as = photo.id + photo.dot_extension | ||||
| 
 | ||||
|         download_as = etiquette.helpers.remove_path_badchars(download_as) | ||||
|         download_as = urllib.parse.quote(download_as) | ||||
|         response = flask.make_response(common.send_file(photo.real_filepath)) | ||||
|         response.headers['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % download_as | ||||
|         return response | ||||
|     else: | ||||
|         return common.send_file(photo.real_filepath, override_mimetype=photo.mimetype) | ||||
| 
 | ||||
| @site.route('/thumbnail/<photo_id>') | ||||
| def get_thumbnail(photo_id): | ||||
|     photo_id = photo_id.split('.')[0] | ||||
|     photo = common.P_photo(photo_id) | ||||
|     if photo.thumbnail: | ||||
|         path = photo.thumbnail | ||||
|     else: | ||||
|         flask.abort(404, 'That file doesnt have a thumbnail') | ||||
|     return common.send_file(path) | ||||
| 
 | ||||
| # Photo tag operations ############################################################################# | ||||
| 
 | ||||
| @decorators.catch_etiquette_exception | ||||
| def post_photo_add_remove_tag_core(photo_id, tagname, add_or_remove): | ||||
|     photo = common.P_photo(photo_id, response_type='json') | ||||
|     tag = common.P_tag(tagname, response_type='json') | ||||
| 
 | ||||
|     if add_or_remove == 'add': | ||||
|         photo.add_tag(tag) | ||||
|     elif add_or_remove == 'remove': | ||||
|         photo.remove_tag(tag) | ||||
| 
 | ||||
|     response = {'tagname': tag.name} | ||||
|     return jsonify.make_json_response(response) | ||||
| 
 | ||||
| @site.route('/photo/<photo_id>/add_tag', methods=['POST']) | ||||
| @decorators.required_fields(['tagname'], forbid_whitespace=True) | ||||
| def post_photo_add_tag(photo_id): | ||||
|     ''' | ||||
|     Add a tag to this photo. | ||||
|     ''' | ||||
|     return post_photo_add_remove_tag_core(photo_id, request.form['tagname'], 'add') | ||||
| 
 | ||||
| @site.route('/photo/<photo_id>/remove_tag', methods=['POST']) | ||||
| @decorators.required_fields(['tagname'], forbid_whitespace=True) | ||||
| def post_photo_remove_tag(photo_id): | ||||
|     ''' | ||||
|     Remove a tag from this photo. | ||||
|     ''' | ||||
|     return post_photo_add_remove_tag_core(photo_id, request.form['tagname'], 'remove') | ||||
| 
 | ||||
| # Photo metadata operations ######################################################################## | ||||
| 
 | ||||
| @site.route('/photo/<photo_id>/refresh_metadata', methods=['POST']) | ||||
| @decorators.catch_etiquette_exception | ||||
| def post_photo_refresh_metadata(photo_id): | ||||
|     ''' | ||||
|     Refresh the file metadata. | ||||
|     ''' | ||||
|     common.P.caches['photo'].remove(photo_id) | ||||
|     photo = common.P_photo(photo_id, response_type='json') | ||||
|     photo.reload_metadata() | ||||
|     if photo.thumbnail is None: | ||||
|         try: | ||||
|             photo.generate_thumbnail() | ||||
|         except Exception: | ||||
|             traceback.print_exc() | ||||
| 
 | ||||
|     return jsonify.make_json_response({}) | ||||
| 
 | ||||
| # Search ########################################################################################### | ||||
| 
 | ||||
| def get_search_core(): | ||||
|     warning_bag = etiquette.objects.WarningBag() | ||||
| 
 | ||||
|     has_tags = request.args.get('has_tags') | ||||
|     tag_musts = request.args.get('tag_musts') | ||||
|     tag_mays = request.args.get('tag_mays') | ||||
|     tag_forbids = request.args.get('tag_forbids') | ||||
|     tag_expression = request.args.get('tag_expression') | ||||
| 
 | ||||
|     filename_terms = request.args.get('filename') | ||||
|     extension = request.args.get('extension') | ||||
|     extension_not = request.args.get('extension_not') | ||||
|     mimetype = request.args.get('mimetype') | ||||
| 
 | ||||
|     limit = request.args.get('limit') | ||||
|     # This is being pre-processed because the site enforces a maximum value | ||||
|     # which the PhotoDB api does not. | ||||
|     limit = etiquette.searchhelpers.normalize_limit(limit, warning_bag=warning_bag) | ||||
| 
 | ||||
|     if limit is None: | ||||
|         limit = 50 | ||||
|     else: | ||||
|         limit = min(limit, 100) | ||||
| 
 | ||||
|     offset = request.args.get('offset') | ||||
| 
 | ||||
|     authors = request.args.get('author') | ||||
| 
 | ||||
|     orderby = request.args.get('orderby') | ||||
|     area = request.args.get('area') | ||||
|     width = request.args.get('width') | ||||
|     height = request.args.get('height') | ||||
|     ratio = request.args.get('ratio') | ||||
|     bytes = request.args.get('bytes') | ||||
|     duration = request.args.get('duration') | ||||
|     created = request.args.get('created') | ||||
| 
 | ||||
|     # These are in a dictionary so I can pass them to the page template. | ||||
|     search_kwargs = { | ||||
|         'area': area, | ||||
|         'width': width, | ||||
|         'height': height, | ||||
|         'ratio': ratio, | ||||
|         'bytes': bytes, | ||||
|         'duration': duration, | ||||
| 
 | ||||
|         'authors': authors, | ||||
|         'created': created, | ||||
|         'extension': extension, | ||||
|         'extension_not': extension_not, | ||||
|         'filename': filename_terms, | ||||
|         'has_tags': has_tags, | ||||
|         'mimetype': mimetype, | ||||
|         'tag_musts': tag_musts, | ||||
|         'tag_mays': tag_mays, | ||||
|         'tag_forbids': tag_forbids, | ||||
|         'tag_expression': tag_expression, | ||||
| 
 | ||||
|         'limit': limit, | ||||
|         'offset': offset, | ||||
|         'orderby': orderby, | ||||
| 
 | ||||
|         'warning_bag': warning_bag, | ||||
|         'give_back_parameters': True | ||||
|     } | ||||
|     #print(search_kwargs) | ||||
|     search_generator = common.P.search(**search_kwargs) | ||||
|     # Because of the giveback, first element is cleaned up kwargs | ||||
|     search_kwargs = next(search_generator) | ||||
| 
 | ||||
|     # The search has converted many arguments into sets or other types. | ||||
|     # Convert them back into something that will display nicely on the search form. | ||||
|     join_helper = lambda x: ', '.join(x) if x else None | ||||
|     search_kwargs['extension'] = join_helper(search_kwargs['extension']) | ||||
|     search_kwargs['extension_not'] = join_helper(search_kwargs['extension_not']) | ||||
|     search_kwargs['mimetype'] = join_helper(search_kwargs['mimetype']) | ||||
| 
 | ||||
|     tagname_helper = lambda tags: [tag.qualified_name() for tag in tags] if tags else None | ||||
|     search_kwargs['tag_musts'] = tagname_helper(search_kwargs['tag_musts']) | ||||
|     search_kwargs['tag_mays'] = tagname_helper(search_kwargs['tag_mays']) | ||||
|     search_kwargs['tag_forbids'] = tagname_helper(search_kwargs['tag_forbids']) | ||||
| 
 | ||||
|     search_results = list(search_generator) | ||||
|     warnings = set() | ||||
|     photos = [] | ||||
|     for item in search_results: | ||||
|         if isinstance(item, etiquette.objects.WarningBag): | ||||
|             warnings.update(item.warnings) | ||||
|         else: | ||||
|             photos.append(item) | ||||
| 
 | ||||
|     # TAGS ON THIS PAGE | ||||
|     total_tags = set() | ||||
|     for photo in photos: | ||||
|         for tag in photo.tags(): | ||||
|             total_tags.add(tag) | ||||
|     total_tags = sorted(total_tags, key=lambda t: t.qualified_name()) | ||||
| 
 | ||||
|     # PREV-NEXT PAGE URLS | ||||
|     offset = search_kwargs['offset'] or 0 | ||||
|     original_params = request.args.to_dict() | ||||
|     original_params['limit'] = limit | ||||
|     if len(photos) == limit: | ||||
|         next_params = original_params.copy() | ||||
|         next_params['offset'] = offset + limit | ||||
|         next_params = etiquette.helpers.dict_to_params(next_params) | ||||
|         next_page_url = '/search' + next_params | ||||
|     else: | ||||
|         next_page_url = None | ||||
| 
 | ||||
|     if offset > 0: | ||||
|         prev_params = original_params.copy() | ||||
|         prev_params['offset'] = max(0, offset - limit) | ||||
|         prev_params = etiquette.helpers.dict_to_params(prev_params) | ||||
|         prev_page_url = '/search' + prev_params | ||||
|     else: | ||||
|         prev_page_url = None | ||||
| 
 | ||||
|     view = request.args.get('view', 'grid') | ||||
|     search_kwargs['view'] = view | ||||
| 
 | ||||
|     final_results = { | ||||
|         'next_page_url': next_page_url, | ||||
|         'prev_page_url': prev_page_url, | ||||
|         'photos': photos, | ||||
|         'total_tags': total_tags, | ||||
|         'warnings': list(warnings), | ||||
|         'search_kwargs': search_kwargs, | ||||
|     } | ||||
|     return final_results | ||||
| 
 | ||||
| @site.route('/search') | ||||
| @session_manager.give_token | ||||
| def get_search_html(): | ||||
|     search_results = get_search_core() | ||||
|     search_kwargs = search_results['search_kwargs'] | ||||
|     qualname_map = etiquette.tag_export.qualified_names(common.P.get_tags()) | ||||
|     session = session_manager.get(request) | ||||
|     response = flask.render_template( | ||||
|         'search.html', | ||||
|         next_page_url=search_results['next_page_url'], | ||||
|         prev_page_url=search_results['prev_page_url'], | ||||
|         photos=search_results['photos'], | ||||
|         qualname_map=json.dumps(qualname_map), | ||||
|         search_kwargs=search_kwargs, | ||||
|         session=session, | ||||
|         total_tags=search_results['total_tags'], | ||||
|         warnings=search_results['warnings'], | ||||
|     ) | ||||
|     return response | ||||
| 
 | ||||
| @site.route('/search.json') | ||||
| @session_manager.give_token | ||||
| def get_search_json(): | ||||
|     search_results = get_search_core() | ||||
|     search_results['photos'] = [ | ||||
|         etiquette.jsonify.photo(photo, include_albums=False) for photo in search_results['photos'] | ||||
|     ] | ||||
|     return jsonify.make_json_response(search_results) | ||||
|  | @ -0,0 +1,137 @@ | |||
| import flask; from flask import request | ||||
| 
 | ||||
| import etiquette | ||||
| 
 | ||||
| from .. import decorators | ||||
| from .. import jsonify | ||||
| from . import common | ||||
| 
 | ||||
| site = common.site | ||||
| session_manager = common.session_manager | ||||
| 
 | ||||
| 
 | ||||
| # Individual tags ################################################################################## | ||||
| 
 | ||||
| @site.route('/tags/<specific_tag>') | ||||
| @site.route('/tags/<specific_tag>.json') | ||||
| def get_tags_specific_redirect(specific_tag): | ||||
|     return flask.redirect(request.url.replace('/tags/', '/tag/')) | ||||
| 
 | ||||
| # Tag metadata operations ########################################################################## | ||||
| 
 | ||||
| @site.route('/tag/<specific_tag>/edit', methods=['POST']) | ||||
| @decorators.catch_etiquette_exception | ||||
| def post_tag_edit(specific_tag): | ||||
|     tag = common.P_tag(specific_tag) | ||||
|     name = request.form.get('name', '').strip() | ||||
|     if name: | ||||
|         tag.rename(name, commit=False) | ||||
| 
 | ||||
|     description = request.form.get('description', None) | ||||
|     tag.edit(description=description) | ||||
| 
 | ||||
|     response = etiquette.jsonify.tag(tag) | ||||
|     response = jsonify.make_json_response(response) | ||||
|     return response | ||||
| 
 | ||||
| # Tag listings ##################################################################################### | ||||
| 
 | ||||
| def get_tags_core(specific_tag=None): | ||||
|     if specific_tag is None: | ||||
|         tags = common.P.get_tags() | ||||
|     else: | ||||
|         tags = specific_tag.walk_children() | ||||
|     tags = list(tags) | ||||
|     tags.sort(key=lambda x: x.qualified_name()) | ||||
|     return tags | ||||
| 
 | ||||
| @site.route('/tag/<specific_tag_name>') | ||||
| @site.route('/tags') | ||||
| @session_manager.give_token | ||||
| def get_tags_html(specific_tag_name=None): | ||||
|     if specific_tag_name is None: | ||||
|         specific_tag = None | ||||
|     else: | ||||
|         specific_tag = common.P_tag(specific_tag_name, response_type='html') | ||||
|         if specific_tag.name != specific_tag_name: | ||||
|             new_url = request.url.replace('/tag/' + specific_tag_name, '/tag/' + specific_tag.name) | ||||
|             response = flask.redirect(new_url) | ||||
|             return response | ||||
|     tags = get_tags_core(specific_tag) | ||||
|     session = session_manager.get(request) | ||||
|     include_synonyms = request.args.get('synonyms') | ||||
|     include_synonyms = include_synonyms is None or etiquette.helpers.truthystring(include_synonyms) | ||||
|     response = flask.render_template( | ||||
|         'tags.html', | ||||
|         include_synonyms=include_synonyms, | ||||
|         session=session, | ||||
|         specific_tag=specific_tag, | ||||
|         tags=tags, | ||||
|     ) | ||||
|     return response | ||||
| 
 | ||||
| @site.route('/tag/<specific_tag_name>.json') | ||||
| @site.route('/tags.json') | ||||
| @session_manager.give_token | ||||
| def get_tags_json(specific_tag_name=None): | ||||
|     if specific_tag_name is None: | ||||
|         specific_tag = None | ||||
|     else: | ||||
|         specific_tag = common.P_tag(specific_tag_name, response_type='json') | ||||
|         if specific_tag.name != specific_tag_name: | ||||
|             new_url = request.url.replace('/tag/' + specific_tag_name, '/tag/' + specific_tag.name) | ||||
|             return flask.redirect(new_url) | ||||
|     tags = get_tags_core(specific_tag=specific_tag) | ||||
|     include_synonyms = request.args.get('synonyms') | ||||
|     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 jsonify.make_json_response(tags) | ||||
| 
 | ||||
| # Tag create and delete ############################################################################ | ||||
| 
 | ||||
| @site.route('/tags/create_tag', methods=['POST']) | ||||
| @decorators.catch_etiquette_exception | ||||
| @decorators.required_fields(['tagname'], forbid_whitespace=True) | ||||
| def post_tag_create(): | ||||
|     ''' | ||||
|     Create a tag. | ||||
|     ''' | ||||
|     easybake_string = request.form['tagname'] | ||||
|     notes = common.P.easybake(easybake_string) | ||||
|     notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes] | ||||
|     return jsonify.make_json_response(notes) | ||||
| 
 | ||||
| @site.route('/tags/delete_synonym', methods=['POST']) | ||||
| @decorators.catch_etiquette_exception | ||||
| @decorators.required_fields(['tagname'], forbid_whitespace=True) | ||||
| def post_tag_delete_synonym(): | ||||
|     ''' | ||||
|     Delete a synonym. | ||||
|     ''' | ||||
|     synonym = request.form['tagname'] | ||||
|     synonym = synonym.split('+')[-1].split('.')[-1] | ||||
| 
 | ||||
|     try: | ||||
|         master_tag = common.P_tag(synonym, response_type='json') | ||||
|     except etiquette.exceptions.NoSuchTag as exc: | ||||
|         raise etiquette.exceptions.NoSuchSynonym(*exc.given_args, **exc.given_kwargs) | ||||
|     else: | ||||
|         master_tag.remove_synonym(synonym) | ||||
| 
 | ||||
|     response = {'action':'delete_synonym', 'synonym': synonym} | ||||
|     return jsonify.make_json_response(response) | ||||
| 
 | ||||
| @site.route('/tags/delete_tag', methods=['POST']) | ||||
| @decorators.catch_etiquette_exception | ||||
| @decorators.required_fields(['tagname'], forbid_whitespace=True) | ||||
| def post_tag_delete(): | ||||
|     ''' | ||||
|     Delete a tag. | ||||
|     ''' | ||||
|     tagname = request.form['tagname'] | ||||
|     tagname = tagname.split('.')[-1].split('+')[0] | ||||
|     tag = common.P.get_tag(tagname) | ||||
| 
 | ||||
|     tag.delete() | ||||
|     response = {'action': 'delete_tag', 'tagname': tag.name} | ||||
|     return jsonify.make_json_response(response) | ||||
|  | @ -0,0 +1,118 @@ | |||
| import flask; from flask import request | ||||
| 
 | ||||
| import etiquette | ||||
| 
 | ||||
| from .. import decorators | ||||
| from .. import jsonify | ||||
| from .. import sessions | ||||
| from . import common | ||||
| 
 | ||||
| site = common.site | ||||
| session_manager = common.session_manager | ||||
| 
 | ||||
| 
 | ||||
| # Individual users ################################################################################# | ||||
| 
 | ||||
| @site.route('/user/<username>', methods=['GET']) | ||||
| @session_manager.give_token | ||||
| def get_user_html(username): | ||||
|     user = common.P_user(username, response_type='html') | ||||
|     session = session_manager.get(request) | ||||
|     return flask.render_template('user.html', user=user, session=session) | ||||
| 
 | ||||
| @site.route('/user/<username>.json', methods=['GET']) | ||||
| @session_manager.give_token | ||||
| def get_user_json(username): | ||||
|     user = common.P_user(username, response_type='json') | ||||
|     user = etiquette.jsonify.user(user) | ||||
|     return jsonify.make_json_response(user) | ||||
| 
 | ||||
| @site.route('/userid/<user_id>') | ||||
| @site.route('/userid/<user_id>.json') | ||||
| def get_user_id_redirect(user_id): | ||||
|     if request.url.endswith('.json'): | ||||
|         user = common.P_user_id(user_id, response_type='json') | ||||
|     else: | ||||
|         user = common.P_user_id(user_id, response_type='html') | ||||
|     url_from = '/userid/' + user_id | ||||
|     url_to = '/user/' + user.username | ||||
|     url = request.url.replace(url_from, url_to) | ||||
|     return flask.redirect(url) | ||||
| 
 | ||||
| # Login and logout ################################################################################# | ||||
| 
 | ||||
| @site.route('/login', methods=['GET']) | ||||
| @session_manager.give_token | ||||
| def get_login(): | ||||
|     session = session_manager.get(request) | ||||
|     return flask.render_template('login.html', session=session) | ||||
| 
 | ||||
| @site.route('/login', methods=['POST']) | ||||
| @session_manager.give_token | ||||
| @decorators.required_fields(['username', 'password']) | ||||
| def post_login(): | ||||
|     if session_manager.get(request): | ||||
|         exc = etiquette.exceptions.AlreadySignedIn() | ||||
|         response = etiquette.jsonify.exception(exc) | ||||
|         return jsonify.make_json_response(response, status=403) | ||||
| 
 | ||||
|     username = request.form['username'] | ||||
|     password = request.form['password'] | ||||
|     try: | ||||
|         # Consideration: Should the server hash the password to discourage | ||||
|         # information (user exists) leak via response time? | ||||
|         # Currently I think not, because they can check if the account | ||||
|         # page 404s anyway. | ||||
|         user = common.P.get_user(username=username) | ||||
|         user = common.P.login(user.id, password) | ||||
|     except (etiquette.exceptions.NoSuchUser, etiquette.exceptions.WrongLogin): | ||||
|         exc = etiquette.exceptions.WrongLogin() | ||||
|         response = etiquette.jsonify.exception(exc) | ||||
|         return jsonify.make_json_response(response, status=422) | ||||
|     except etiquette.exceptions.FeatureDisabled as exc: | ||||
|         response = etiquette.jsonify.exception(exc) | ||||
|         return jsonify.make_json_response(response, status=400) | ||||
|     session = sessions.Session(request, user) | ||||
|     session_manager.add(session) | ||||
|     return jsonify.make_json_response({}) | ||||
| 
 | ||||
| @site.route('/logout', methods=['GET', 'POST']) | ||||
| @session_manager.give_token | ||||
| def logout(): | ||||
|     session_manager.remove(request) | ||||
|     response = flask.Response('redirect', status=302, headers={'Location': common.back_url()}) | ||||
|     return response | ||||
| 
 | ||||
| # User registration ################################################################################ | ||||
| 
 | ||||
| @site.route('/register', methods=['GET']) | ||||
| def get_register(): | ||||
|     return flask.redirect('/login') | ||||
| 
 | ||||
| @site.route('/register', methods=['POST']) | ||||
| @session_manager.give_token | ||||
| @decorators.catch_etiquette_exception | ||||
| @decorators.required_fields(['username', 'password_1', 'password_2']) | ||||
| def post_register(): | ||||
|     if session_manager.get(request): | ||||
|         exc = etiquette.exceptions.AlreadySignedIn() | ||||
|         response = etiquette.jsonify.exception(exc) | ||||
|         return jsonify.make_json_response(response, status=403) | ||||
| 
 | ||||
|     username = request.form['username'] | ||||
|     password_1 = request.form['password_1'] | ||||
|     password_2 = request.form['password_2'] | ||||
| 
 | ||||
|     if password_1 != password_2: | ||||
|         response = { | ||||
|             'error_type': 'PASSWORDS_DONT_MATCH', | ||||
|             'error_message': 'Passwords do not match.', | ||||
|         } | ||||
|         return jsonify.make_json_response(response, status=422) | ||||
| 
 | ||||
|     user = common.P.register_user(username, password_1) | ||||
| 
 | ||||
|     session = sessions.Session(request, user) | ||||
|     session_manager.add(session) | ||||
|     return jsonify.make_json_response({}) | ||||
| 
 | ||||
|  | @ -1,925 +0,0 @@ | |||
| import flask | ||||
| from flask import request | ||||
| import json | ||||
| import mimetypes | ||||
| import os | ||||
| import random | ||||
| import traceback | ||||
| import urllib.parse | ||||
| import warnings | ||||
| import zipstream | ||||
| 
 | ||||
| import etiquette | ||||
| 
 | ||||
| from . import decorators | ||||
| from . import jsonify | ||||
| from . import sessions | ||||
| 
 | ||||
| from voussoirkit import pathclass | ||||
| 
 | ||||
| 
 | ||||
| root_dir = pathclass.Path(__file__).parent.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.absolute_path, | ||||
|     static_folder=STATIC_DIR.absolute_path, | ||||
| ) | ||||
| site.config.update( | ||||
|     SEND_FILE_MAX_AGE_DEFAULT=180, | ||||
|     TEMPLATES_AUTO_RELOAD=True, | ||||
| ) | ||||
| site.jinja_env.add_extension('jinja2.ext.do') | ||||
| site.jinja_env.trim_blocks = True | ||||
| site.jinja_env.lstrip_blocks = True | ||||
| site.debug = True | ||||
| 
 | ||||
| P = etiquette.photodb.PhotoDB() | ||||
| 
 | ||||
| session_manager = sessions.SessionManager() | ||||
| 
 | ||||
| #################################################################################################### | ||||
| #################################################################################################### | ||||
| #################################################################################################### | ||||
| #################################################################################################### | ||||
| 
 | ||||
| 
 | ||||
| def back_url(): | ||||
|     return request.args.get('goto') or request.referrer or '/' | ||||
| 
 | ||||
| def create_tag(easybake_string): | ||||
|     notes = P.easybake(easybake_string) | ||||
|     notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes] | ||||
|     return notes | ||||
| 
 | ||||
| def delete_tag(tag): | ||||
|     tag = tag.split('.')[-1].split('+')[0] | ||||
|     tag = P.get_tag(tag) | ||||
| 
 | ||||
|     tag.delete() | ||||
|     return {'action': 'delete_tag', 'tagname': tag.name} | ||||
| 
 | ||||
| def delete_synonym(synonym): | ||||
|     synonym = synonym.split('+')[-1].split('.')[-1] | ||||
| 
 | ||||
|     try: | ||||
|         master_tag = P.get_tag(synonym) | ||||
|     except etiquette.exceptions.NoSuchTag as e: | ||||
|         raise etiquette.exceptions.NoSuchSynonym(*e.given_args, **e.given_kwargs) | ||||
|     else: | ||||
|         master_tag.remove_synonym(synonym) | ||||
| 
 | ||||
|     return {'action':'delete_synonym', 'synonym': synonym} | ||||
| 
 | ||||
| def P_wrapper(function): | ||||
|     def P_wrapped(thingid, response_type='html'): | ||||
|         try: | ||||
|             return function(thingid) | ||||
| 
 | ||||
|         except etiquette.exceptions.EtiquetteException as e: | ||||
|             if isinstance(e, etiquette.exceptions.NoSuch): | ||||
|                 status = 404 | ||||
|             else: | ||||
|                 status = 400 | ||||
| 
 | ||||
|             if response_type == 'html': | ||||
|                 flask.abort(status, e.error_message) | ||||
|             else: | ||||
|                 response = etiquette.jsonify.exception(e) | ||||
|                 response = jsonify.make_json_response(response, status=status) | ||||
|                 flask.abort(response) | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             traceback.print_exc() | ||||
|             if response_type == 'html': | ||||
|                 flask.abort(500) | ||||
|             else: | ||||
|                 flask.abort(etiquette.jsonify.make_json_response({}, status=500)) | ||||
| 
 | ||||
|     return P_wrapped | ||||
| 
 | ||||
| @P_wrapper | ||||
| def P_album(album_id): | ||||
|     return P.get_album(album_id) | ||||
| 
 | ||||
| @P_wrapper | ||||
| def P_bookmark(bookmarkid): | ||||
|     return P.get_bookmark(bookmarkid) | ||||
| 
 | ||||
| @P_wrapper | ||||
| def P_photo(photo_id): | ||||
|     return P.get_photo(photo_id) | ||||
| 
 | ||||
| @P_wrapper | ||||
| def P_tag(tagname): | ||||
|     return P.get_tag(tagname) | ||||
| 
 | ||||
| @P_wrapper | ||||
| def P_user(username): | ||||
|     return P.get_user(username=username) | ||||
| 
 | ||||
| @P_wrapper | ||||
| def P_user_id(user_id): | ||||
|     return P.get_user(id=user_id) | ||||
| 
 | ||||
| def send_file(filepath, override_mimetype=None): | ||||
|     ''' | ||||
|     Range-enabled file sending. | ||||
|     ''' | ||||
|     try: | ||||
|         file_size = os.path.getsize(filepath) | ||||
|     except FileNotFoundError: | ||||
|         flask.abort(404) | ||||
| 
 | ||||
|     outgoing_headers = {} | ||||
|     if override_mimetype is not None: | ||||
|         mimetype = override_mimetype | ||||
|     else: | ||||
|         mimetype = mimetypes.guess_type(filepath)[0] | ||||
| 
 | ||||
|     if mimetype is not None: | ||||
|         if 'text/' in mimetype: | ||||
|             mimetype += '; charset=utf-8' | ||||
|         outgoing_headers['Content-Type'] = mimetype | ||||
| 
 | ||||
|     if 'range' in request.headers: | ||||
|         desired_range = request.headers['range'].lower() | ||||
|         desired_range = desired_range.split('bytes=')[-1] | ||||
| 
 | ||||
|         int_helper = lambda x: int(x) if x.isdigit() else None | ||||
|         if '-' in desired_range: | ||||
|             (desired_min, desired_max) = desired_range.split('-') | ||||
|             range_min = int_helper(desired_min) | ||||
|             range_max = int_helper(desired_max) | ||||
|         else: | ||||
|             range_min = int_helper(desired_range) | ||||
| 
 | ||||
|         if range_min is None: | ||||
|             range_min = 0 | ||||
|         if range_max is None: | ||||
|             range_max = file_size | ||||
| 
 | ||||
|         # because ranges are 0-indexed | ||||
|         range_max = min(range_max, file_size - 1) | ||||
|         range_min = max(range_min, 0) | ||||
| 
 | ||||
|         range_header = 'bytes {min}-{max}/{outof}'.format( | ||||
|             min=range_min, | ||||
|             max=range_max, | ||||
|             outof=file_size, | ||||
|         ) | ||||
|         outgoing_headers['Content-Range'] = range_header | ||||
|         status = 206 | ||||
|     else: | ||||
|         range_max = file_size - 1 | ||||
|         range_min = 0 | ||||
|         status = 200 | ||||
| 
 | ||||
|     outgoing_headers['Accept-Ranges'] = 'bytes' | ||||
|     outgoing_headers['Content-Length'] = (range_max - range_min) + 1 | ||||
| 
 | ||||
|     if request.method == 'HEAD': | ||||
|         outgoing_data = bytes() | ||||
|     else: | ||||
|         outgoing_data = etiquette.helpers.read_filebytes( | ||||
|             filepath, | ||||
|             range_min=range_min, | ||||
|             range_max=range_max, | ||||
|             chunk_size=P.config['file_read_chunk'], | ||||
|         ) | ||||
| 
 | ||||
|     response = flask.Response( | ||||
|         outgoing_data, | ||||
|         status=status, | ||||
|         headers=outgoing_headers, | ||||
|     ) | ||||
|     return response | ||||
| 
 | ||||
| 
 | ||||
| #################################################################################################### | ||||
| #################################################################################################### | ||||
| #################################################################################################### | ||||
| #################################################################################################### | ||||
| 
 | ||||
| 
 | ||||
| def get_album_core(album_id): | ||||
|     album = P_album(album_id) | ||||
|     return album | ||||
| 
 | ||||
| def get_albums_core(): | ||||
|     albums = list(P.get_root_albums()) | ||||
|     albums.sort(key=lambda x: x.display_name.lower()) | ||||
|     return albums | ||||
| 
 | ||||
| def get_search_core(): | ||||
|     warning_bag = etiquette.objects.WarningBag() | ||||
| 
 | ||||
|     has_tags = request.args.get('has_tags') | ||||
|     tag_musts = request.args.get('tag_musts') | ||||
|     tag_mays = request.args.get('tag_mays') | ||||
|     tag_forbids = request.args.get('tag_forbids') | ||||
|     tag_expression = request.args.get('tag_expression') | ||||
| 
 | ||||
|     filename_terms = request.args.get('filename') | ||||
|     extension = request.args.get('extension') | ||||
|     extension_not = request.args.get('extension_not') | ||||
|     mimetype = request.args.get('mimetype') | ||||
| 
 | ||||
|     limit = request.args.get('limit') | ||||
|     # This is being pre-processed because the site enforces a maximum value | ||||
|     # which the PhotoDB api does not. | ||||
|     limit = etiquette.searchhelpers.normalize_limit(limit, warning_bag=warning_bag) | ||||
| 
 | ||||
|     if limit is None: | ||||
|         limit = 50 | ||||
|     else: | ||||
|         limit = min(limit, 100) | ||||
| 
 | ||||
|     offset = request.args.get('offset') | ||||
| 
 | ||||
|     authors = request.args.get('author') | ||||
| 
 | ||||
|     orderby = request.args.get('orderby') | ||||
|     area = request.args.get('area') | ||||
|     width = request.args.get('width') | ||||
|     height = request.args.get('height') | ||||
|     ratio = request.args.get('ratio') | ||||
|     bytes = request.args.get('bytes') | ||||
|     duration = request.args.get('duration') | ||||
|     created = request.args.get('created') | ||||
| 
 | ||||
|     # These are in a dictionary so I can pass them to the page template. | ||||
|     search_kwargs = { | ||||
|         'area': area, | ||||
|         'width': width, | ||||
|         'height': height, | ||||
|         'ratio': ratio, | ||||
|         'bytes': bytes, | ||||
|         'duration': duration, | ||||
| 
 | ||||
|         'authors': authors, | ||||
|         'created': created, | ||||
|         'extension': extension, | ||||
|         'extension_not': extension_not, | ||||
|         'filename': filename_terms, | ||||
|         'has_tags': has_tags, | ||||
|         'mimetype': mimetype, | ||||
|         'tag_musts': tag_musts, | ||||
|         'tag_mays': tag_mays, | ||||
|         'tag_forbids': tag_forbids, | ||||
|         'tag_expression': tag_expression, | ||||
| 
 | ||||
|         'limit': limit, | ||||
|         'offset': offset, | ||||
|         'orderby': orderby, | ||||
| 
 | ||||
|         'warning_bag': warning_bag, | ||||
|         'give_back_parameters': True | ||||
|     } | ||||
|     #print(search_kwargs) | ||||
|     search_generator = P.search(**search_kwargs) | ||||
|     # Because of the giveback, first element is cleaned up kwargs | ||||
|     search_kwargs = next(search_generator) | ||||
| 
 | ||||
|     # The search has converted many arguments into sets or other types. | ||||
|     # Convert them back into something that will display nicely on the search form. | ||||
|     join_helper = lambda x: ', '.join(x) if x else None | ||||
|     search_kwargs['extension'] = join_helper(search_kwargs['extension']) | ||||
|     search_kwargs['extension_not'] = join_helper(search_kwargs['extension_not']) | ||||
|     search_kwargs['mimetype'] = join_helper(search_kwargs['mimetype']) | ||||
| 
 | ||||
|     tagname_helper = lambda tags: [tag.qualified_name() for tag in tags] if tags else None | ||||
|     search_kwargs['tag_musts'] = tagname_helper(search_kwargs['tag_musts']) | ||||
|     search_kwargs['tag_mays'] = tagname_helper(search_kwargs['tag_mays']) | ||||
|     search_kwargs['tag_forbids'] = tagname_helper(search_kwargs['tag_forbids']) | ||||
| 
 | ||||
|     search_results = list(search_generator) | ||||
|     warnings = set() | ||||
|     photos = [] | ||||
|     for item in search_results: | ||||
|         if isinstance(item, etiquette.objects.WarningBag): | ||||
|             warnings.update(item.warnings) | ||||
|         else: | ||||
|             photos.append(item) | ||||
| 
 | ||||
|     # TAGS ON THIS PAGE | ||||
|     total_tags = set() | ||||
|     for photo in photos: | ||||
|         for tag in photo.tags(): | ||||
|             total_tags.add(tag) | ||||
|     total_tags = sorted(total_tags, key=lambda t: t.qualified_name()) | ||||
| 
 | ||||
|     # PREV-NEXT PAGE URLS | ||||
|     offset = search_kwargs['offset'] or 0 | ||||
|     original_params = request.args.to_dict() | ||||
|     original_params['limit'] = limit | ||||
|     if len(photos) == limit: | ||||
|         next_params = original_params.copy() | ||||
|         next_params['offset'] = offset + limit | ||||
|         next_params = etiquette.helpers.dict_to_params(next_params) | ||||
|         next_page_url = '/search' + next_params | ||||
|     else: | ||||
|         next_page_url = None | ||||
| 
 | ||||
|     if offset > 0: | ||||
|         prev_params = original_params.copy() | ||||
|         prev_params['offset'] = max(0, offset - limit) | ||||
|         prev_params = etiquette.helpers.dict_to_params(prev_params) | ||||
|         prev_page_url = '/search' + prev_params | ||||
|     else: | ||||
|         prev_page_url = None | ||||
| 
 | ||||
|     view = request.args.get('view', 'grid') | ||||
|     search_kwargs['view'] = view | ||||
| 
 | ||||
|     final_results = { | ||||
|         'next_page_url': next_page_url, | ||||
|         'prev_page_url': prev_page_url, | ||||
|         'photos': photos, | ||||
|         'total_tags': total_tags, | ||||
|         'warnings': list(warnings), | ||||
|         'search_kwargs': search_kwargs, | ||||
|     } | ||||
|     return final_results | ||||
| 
 | ||||
| def get_tags_core(specific_tag=None): | ||||
|     if specific_tag is None: | ||||
|         tags = P.get_tags() | ||||
|     else: | ||||
|         tags = specific_tag.walk_children() | ||||
|     tags = list(tags) | ||||
|     tags.sort(key=lambda x: x.qualified_name()) | ||||
|     return tags | ||||
| 
 | ||||
| @decorators.catch_etiquette_exception | ||||
| def post_photo_add_remove_tag_core(photo_id, tagname, add_or_remove): | ||||
|     photo = P_photo(photo_id, response_type='json') | ||||
|     tag = P_tag(tagname, response_type='json') | ||||
| 
 | ||||
|     if add_or_remove == 'add': | ||||
|         photo.add_tag(tag) | ||||
|     elif add_or_remove == 'remove': | ||||
|         photo.remove_tag(tag) | ||||
| 
 | ||||
|     response = {'tagname': tag.name} | ||||
|     return jsonify.make_json_response(response) | ||||
| 
 | ||||
| @decorators.catch_etiquette_exception | ||||
| def post_tag_create_delete_core(tagname, function): | ||||
|     return jsonify.make_json_response(function(tagname)) | ||||
| 
 | ||||
| 
 | ||||
| #################################################################################################### | ||||
| #################################################################################################### | ||||
| #################################################################################################### | ||||
| #################################################################################################### | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/') | ||||
| @session_manager.give_token | ||||
| def root(): | ||||
|     motd = random.choice(P.config['motd_strings']) | ||||
|     session = session_manager.get(request) | ||||
|     return flask.render_template('root.html', motd=motd, session=session) | ||||
| 
 | ||||
| @site.route('/album/<album_id>') | ||||
| @session_manager.give_token | ||||
| def get_album_html(album_id): | ||||
|     album = get_album_core(album_id) | ||||
|     session = session_manager.get(request) | ||||
|     response = flask.render_template( | ||||
|         'album.html', | ||||
|         album=album, | ||||
|         session=session, | ||||
|         view=request.args.get('view', 'grid'), | ||||
|     ) | ||||
|     return response | ||||
| 
 | ||||
| @site.route('/album/<album_id>.json') | ||||
| @session_manager.give_token | ||||
| def get_album_json(album_id): | ||||
|     album = get_album_core(album_id) | ||||
|     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'] = [etiquette.jsonify.album(x, minimal=True) for x in album['sub_albums']] | ||||
|     return jsonify.make_json_response(album) | ||||
| 
 | ||||
| @site.route('/album/<album_id>.zip') | ||||
| def get_album_zip(album_id): | ||||
|     album = P_album(album_id) | ||||
| 
 | ||||
|     recursive = request.args.get('recursive', True) | ||||
|     recursive = etiquette.helpers.truthystring(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 = etiquette.helpers.album_zip_directories(album, recursive=recursive) | ||||
|     for (inner_album, directory) in directories.items(): | ||||
|         text = [] | ||||
|         if inner_album.title: | ||||
|             text.append('Title: ' + inner_album.title) | ||||
|         if inner_album.description: | ||||
|             text.append('Description: ' + inner_album.description) | ||||
|         if not text: | ||||
|             continue | ||||
|         text = '\r\n\r\n'.join(text) | ||||
|         streamed_zip.writestr( | ||||
|             arcname=os.path.join(directory, 'album %s.txt' % inner_album.id), | ||||
|             data=text.encode('utf-8'), | ||||
|         ) | ||||
| 
 | ||||
|     if album.title: | ||||
|         download_as = 'album %s - %s.zip' % (album.id, album.title) | ||||
|     else: | ||||
|         download_as = 'album %s.zip' % album.id | ||||
| 
 | ||||
|     download_as = etiquette.helpers.remove_path_badchars(download_as) | ||||
|     download_as = urllib.parse.quote(download_as) | ||||
|     outgoing_headers = { | ||||
|         'Content-Type': 'application/octet-stream', | ||||
|         'Content-Disposition': 'attachment; filename*=UTF-8\'\'%s' % download_as, | ||||
| 
 | ||||
|     } | ||||
|     return flask.Response(streamed_zip, headers=outgoing_headers) | ||||
| 
 | ||||
| @site.route('/album/<album_id>/add_tag', methods=['POST']) | ||||
| @decorators.catch_etiquette_exception | ||||
| @session_manager.give_token | ||||
| def post_album_add_tag(album_id): | ||||
|     ''' | ||||
|     Apply a tag to every photo in the album. | ||||
|     ''' | ||||
|     response = {} | ||||
|     album = P_album(album_id) | ||||
| 
 | ||||
|     tag = request.form['tagname'].strip() | ||||
|     try: | ||||
|         tag = P_tag(tag) | ||||
|     except etiquette.exceptions.NoSuchTag as e: | ||||
|         response = etiquette.jsonify.exception(e) | ||||
|         return jsonify.make_json_response(response, status=404) | ||||
|     recursive = request.form.get('recursive', False) | ||||
|     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) | ||||
| 
 | ||||
| @site.route('/album/<album_id>/add_photo', methods=['POST']) | ||||
| @session_manager.give_token | ||||
| @decorators.catch_etiquette_exception | ||||
| @decorators.required_fields(['photo_id'], forbid_whitespace=True) | ||||
| def post_album_add_photo(album_id): | ||||
|     ''' | ||||
|     Add a photo or photos to this album. | ||||
|     ''' | ||||
|     response = {} | ||||
|     album = P_album(album_id) | ||||
| 
 | ||||
|     photo_ids = etiquette.helpers.comma_space_split(request.form['photo_id']) | ||||
|     photos = [P_photo(photo_id) for photo_id in photo_ids] | ||||
|     for photo in photos: | ||||
|         album.add_photo(photo, commit=False) | ||||
|     P.commit() | ||||
|     return jsonify.make_json_response(response) | ||||
| 
 | ||||
| @site.route('/album/<album_id>/remove_photo', methods=['POST']) | ||||
| @session_manager.give_token | ||||
| @decorators.catch_etiquette_exception | ||||
| @decorators.required_fields(['photo_id'], forbid_whitespace=True) | ||||
| def post_album_remove_photo(album_id): | ||||
|     ''' | ||||
|     Remove a photo or photos from this album. | ||||
|     ''' | ||||
|     response = {} | ||||
|     album = P_album(album_id) | ||||
| 
 | ||||
|     photo_ids = etiquette.helpers.comma_space_split(request.form['photo_id']) | ||||
|     photos = [P_photo(photo_id) for photo_id in photo_ids] | ||||
|     for photo in photos: | ||||
|         album.remove_photo(photo, commit=False) | ||||
|     P.commit() | ||||
|     return jsonify.make_json_response(response) | ||||
| 
 | ||||
| @site.route('/album/<album_id>/edit', methods=['POST']) | ||||
| @session_manager.give_token | ||||
| @decorators.catch_etiquette_exception | ||||
| def post_album_edit(album_id): | ||||
|     ''' | ||||
|     Edit the title / description. | ||||
|     ''' | ||||
|     album = P_album(album_id) | ||||
| 
 | ||||
|     title = request.form.get('title', None) | ||||
|     description = request.form.get('description', None) | ||||
|     album.edit(title=title, description=description) | ||||
|     response = etiquette.jsonify.album(album, minimal=True) | ||||
|     return jsonify.make_json_response(response) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/albums') | ||||
| @session_manager.give_token | ||||
| def get_albums_html(): | ||||
|     albums = get_albums_core() | ||||
|     session = session_manager.get(request) | ||||
|     return flask.render_template('albums.html', albums=albums, session=session) | ||||
| 
 | ||||
| @site.route('/albums.json') | ||||
| @session_manager.give_token | ||||
| def get_albums_json(): | ||||
|     albums = get_albums_core() | ||||
|     albums = [etiquette.jsonify.album(album, minimal=True) for album in albums] | ||||
|     return jsonify.make_json_response(albums) | ||||
| 
 | ||||
| @site.route('/albums/create_album', methods=['POST']) | ||||
| @decorators.catch_etiquette_exception | ||||
| def post_albums_create(): | ||||
|     title = request.form.get('title', None) | ||||
|     description = request.form.get('description', None) | ||||
|     parent = request.form.get('parent', None) | ||||
|     if parent is not None: | ||||
|         parent = P_album(parent) | ||||
| 
 | ||||
|     album = P.new_album(title=title, description=description) | ||||
|     if parent is not None: | ||||
|         parent.add_child(album) | ||||
|     response = etiquette.jsonify.album(album, minimal=False) | ||||
|     return jsonify.make_json_response(response) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/bookmark/<bookmarkid>.json') | ||||
| @session_manager.give_token | ||||
| def get_bookmark_json(bookmarkid): | ||||
|     bookmark = P_bookmark(bookmarkid) | ||||
|     response = etiquette.jsonify.bookmark(bookmark) | ||||
|     return jsonify.make_json_response(response) | ||||
| 
 | ||||
| @site.route('/bookmark/<bookmarkid>/edit', methods=['POST']) | ||||
| @session_manager.give_token | ||||
| @decorators.catch_etiquette_exception | ||||
| def post_bookmark_edit(bookmarkid): | ||||
|     bookmark = P_bookmark(bookmarkid) | ||||
|     # Emptystring is okay for titles, but not for URL. | ||||
|     title = request.form.get('title', None) | ||||
|     url = request.form.get('url', None) or None | ||||
|     bookmark.edit(title=title, url=url) | ||||
| 
 | ||||
|     response = etiquette.jsonify.bookmark(bookmark) | ||||
|     response = jsonify.make_json_response(response) | ||||
|     return response | ||||
| 
 | ||||
| @site.route('/bookmarks') | ||||
| @session_manager.give_token | ||||
| def get_bookmarks_html(): | ||||
|     session = session_manager.get(request) | ||||
|     bookmarks = list(P.get_bookmarks()) | ||||
|     return flask.render_template('bookmarks.html', bookmarks=bookmarks, session=session) | ||||
| 
 | ||||
| @site.route('/bookmarks.json') | ||||
| @session_manager.give_token | ||||
| def get_bookmarks_json(): | ||||
|     bookmarks = [etiquette.jsonify.bookmark(b) for b in P.get_bookmarks()] | ||||
|     return jsonify.make_json_response(bookmarks) | ||||
| 
 | ||||
| @site.route('/bookmarks/create_bookmark', methods=['POST']) | ||||
| @decorators.catch_etiquette_exception | ||||
| @decorators.required_fields(['url'], forbid_whitespace=True) | ||||
| def post_bookmarks_create(): | ||||
|     url = request.form['url'] | ||||
|     title = request.form.get('title', None) | ||||
|     bookmark = P.new_bookmark(url=url, title=title) | ||||
|     response = etiquette.jsonify.bookmark(bookmark) | ||||
|     response = jsonify.make_json_response(response) | ||||
|     return response | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/favicon.ico') | ||||
| @site.route('/favicon.png') | ||||
| def favicon(): | ||||
|     return flask.send_file(FAVICON_PATH.absolute_path) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/file/<photo_id>') | ||||
| def get_file(photo_id): | ||||
|     photo_id = photo_id.split('.')[0] | ||||
|     photo = P.get_photo(photo_id) | ||||
| 
 | ||||
|     do_download = request.args.get('download', False) | ||||
|     do_download = etiquette.helpers.truthystring(do_download) | ||||
| 
 | ||||
|     use_original_filename = request.args.get('original_filename', False) | ||||
|     use_original_filename = etiquette.helpers.truthystring(use_original_filename) | ||||
| 
 | ||||
|     if do_download: | ||||
|         if use_original_filename: | ||||
|             download_as = photo.basename | ||||
|         else: | ||||
|             download_as = photo.id + photo.dot_extension | ||||
| 
 | ||||
|         download_as = etiquette.helpers.remove_path_badchars(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 | ||||
|         return response | ||||
|     else: | ||||
|         return send_file(photo.real_filepath, override_mimetype=photo.mimetype) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/login', methods=['GET']) | ||||
| @session_manager.give_token | ||||
| def get_login(): | ||||
|     session = session_manager.get(request) | ||||
|     return flask.render_template('login.html', session=session) | ||||
| 
 | ||||
| @site.route('/login', methods=['POST']) | ||||
| @session_manager.give_token | ||||
| @decorators.required_fields(['username', 'password']) | ||||
| def post_login(): | ||||
|     if session_manager.get(request): | ||||
|         e = etiquette.exceptions.AlreadySignedIn() | ||||
|         response = etiquette.jsonify.exception(e) | ||||
|         return jsonify.make_json_response(response, status=403) | ||||
| 
 | ||||
|     username = request.form['username'] | ||||
|     password = request.form['password'] | ||||
|     try: | ||||
|         # Consideration: Should the server hash the password to discourage | ||||
|         # information (user exists) leak via response time? | ||||
|         # Currently I think not, because they can check if the account | ||||
|         # page 404s anyway. | ||||
|         user = P.get_user(username=username) | ||||
|         user = P.login(user.id, password) | ||||
|     except (etiquette.exceptions.NoSuchUser, etiquette.exceptions.WrongLogin): | ||||
|         e = etiquette.exceptions.WrongLogin() | ||||
|         response = etiquette.jsonify.exception(e) | ||||
|         return jsonify.make_json_response(response, status=422) | ||||
|     except etiquette.exceptions.FeatureDisabled as e: | ||||
|         response = etiquette.jsonify.exception(e) | ||||
|         return jsonify.make_json_response(response, status=400) | ||||
|     session = sessions.Session(request, user) | ||||
|     session_manager.add(session) | ||||
|     return jsonify.make_json_response({}) | ||||
| 
 | ||||
| @site.route('/logout', methods=['GET', 'POST']) | ||||
| @session_manager.give_token | ||||
| def logout(): | ||||
|     session_manager.remove(request) | ||||
|     response = flask.Response('redirect', status=302, headers={'Location': back_url()}) | ||||
|     return response | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/photo/<photo_id>', methods=['GET']) | ||||
| @session_manager.give_token | ||||
| def get_photo_html(photo_id): | ||||
|     photo = P_photo(photo_id, response_type='html') | ||||
|     session = session_manager.get(request) | ||||
|     return flask.render_template('photo.html', photo=photo, session=session) | ||||
| 
 | ||||
| @site.route('/photo/<photo_id>.json', methods=['GET']) | ||||
| @session_manager.give_token | ||||
| def get_photo_json(photo_id): | ||||
|     photo = P_photo(photo_id, response_type='json') | ||||
|     photo = etiquette.jsonify.photo(photo) | ||||
|     photo = jsonify.make_json_response(photo) | ||||
|     return photo | ||||
| 
 | ||||
| @site.route('/photo/<photo_id>/add_tag', methods=['POST']) | ||||
| @decorators.required_fields(['tagname'], forbid_whitespace=True) | ||||
| def post_photo_add_tag(photo_id): | ||||
|     ''' | ||||
|     Add a tag to this photo. | ||||
|     ''' | ||||
|     return post_photo_add_remove_tag_core(photo_id, request.form['tagname'], 'add') | ||||
| 
 | ||||
| @site.route('/photo/<photo_id>/refresh_metadata', methods=['POST']) | ||||
| @decorators.catch_etiquette_exception | ||||
| def post_photo_refresh_metadata(photo_id): | ||||
|     ''' | ||||
|     Refresh the file metadata. | ||||
|     ''' | ||||
|     P.caches['photo'].remove(photo_id) | ||||
|     photo = P_photo(photo_id, response_type='json') | ||||
|     photo.reload_metadata() | ||||
|     if photo.thumbnail is None: | ||||
|         try: | ||||
|             photo.generate_thumbnail() | ||||
|         except Exception: | ||||
|             traceback.print_exc() | ||||
| 
 | ||||
|     return jsonify.make_json_response({}) | ||||
| 
 | ||||
| @site.route('/photo/<photo_id>/remove_tag', methods=['POST']) | ||||
| @decorators.required_fields(['tagname'], forbid_whitespace=True) | ||||
| def post_photo_remove_tag(photo_id): | ||||
|     ''' | ||||
|     Remove a tag from this photo. | ||||
|     ''' | ||||
|     return post_photo_add_remove_tag_core(photo_id, request.form['tagname'], 'remove') | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/register', methods=['GET']) | ||||
| def get_register(): | ||||
|     return flask.redirect('/login') | ||||
| 
 | ||||
| @site.route('/register', methods=['POST']) | ||||
| @session_manager.give_token | ||||
| @decorators.catch_etiquette_exception | ||||
| @decorators.required_fields(['username', 'password_1', 'password_2']) | ||||
| def post_register(): | ||||
|     if session_manager.get(request): | ||||
|         e = etiquette.exceptions.AlreadySignedIn() | ||||
|         response = etiquette.jsonify.exception(e) | ||||
|         return jsonify.make_json_response(response, status=403) | ||||
| 
 | ||||
|     username = request.form['username'] | ||||
|     password_1 = request.form['password_1'] | ||||
|     password_2 = request.form['password_2'] | ||||
| 
 | ||||
|     if password_1 != password_2: | ||||
|         response = { | ||||
|             'error_type': 'PASSWORDS_DONT_MATCH', | ||||
|             'error_message': 'Passwords do not match.', | ||||
|         } | ||||
|         return jsonify.make_json_response(response, status=422) | ||||
| 
 | ||||
|     user = P.register_user(username, password_1) | ||||
| 
 | ||||
|     session = sessions.Session(request, user) | ||||
|     session_manager.add(session) | ||||
|     return jsonify.make_json_response({}) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/search') | ||||
| @session_manager.give_token | ||||
| def get_search_html(): | ||||
|     search_results = get_search_core() | ||||
|     search_kwargs = search_results['search_kwargs'] | ||||
|     qualname_map = etiquette.tag_export.qualified_names(P.get_tags()) | ||||
|     session = session_manager.get(request) | ||||
|     response = flask.render_template( | ||||
|         'search.html', | ||||
|         next_page_url=search_results['next_page_url'], | ||||
|         prev_page_url=search_results['prev_page_url'], | ||||
|         photos=search_results['photos'], | ||||
|         qualname_map=json.dumps(qualname_map), | ||||
|         search_kwargs=search_kwargs, | ||||
|         session=session, | ||||
|         total_tags=search_results['total_tags'], | ||||
|         warnings=search_results['warnings'], | ||||
|     ) | ||||
|     return response | ||||
| 
 | ||||
| @site.route('/search.json') | ||||
| @session_manager.give_token | ||||
| def get_search_json(): | ||||
|     search_results = get_search_core() | ||||
|     search_results['photos'] = [ | ||||
|         etiquette.jsonify.photo(photo, include_albums=False) for photo in search_results['photos'] | ||||
|     ] | ||||
|     return jsonify.make_json_response(search_results) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/tag/<specific_tag_name>') | ||||
| @site.route('/tags') | ||||
| @session_manager.give_token | ||||
| def get_tags_html(specific_tag_name=None): | ||||
|     if specific_tag_name is None: | ||||
|         specific_tag = None | ||||
|     else: | ||||
|         specific_tag = P_tag(specific_tag_name, response_type='html') | ||||
|         if specific_tag.name != specific_tag_name: | ||||
|             new_url = request.url.replace('/tag/' + specific_tag_name, '/tag/' + specific_tag.name) | ||||
|             response = flask.redirect(new_url) | ||||
|             return response | ||||
|     tags = get_tags_core(specific_tag) | ||||
|     session = session_manager.get(request) | ||||
|     include_synonyms = request.args.get('synonyms') | ||||
|     include_synonyms = include_synonyms is None or etiquette.helpers.truthystring(include_synonyms) | ||||
|     response = flask.render_template( | ||||
|         'tags.html', | ||||
|         include_synonyms=include_synonyms, | ||||
|         session=session, | ||||
|         specific_tag=specific_tag, | ||||
|         tags=tags, | ||||
|     ) | ||||
|     return response | ||||
| 
 | ||||
| @site.route('/tag/<specific_tag_name>.json') | ||||
| @site.route('/tags.json') | ||||
| @session_manager.give_token | ||||
| def get_tags_json(specific_tag_name=None): | ||||
|     if specific_tag_name is None: | ||||
|         specific_tag = None | ||||
|     else: | ||||
|         specific_tag = P_tag(specific_tag_name, response_type='json') | ||||
|         if specific_tag.name != specific_tag_name: | ||||
|             new_url = request.url.replace('/tag/' + specific_tag_name, '/tag/' + specific_tag.name) | ||||
|             return flask.redirect(new_url) | ||||
|     tags = get_tags_core(specific_tag=specific_tag) | ||||
|     include_synonyms = request.args.get('synonyms') | ||||
|     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 jsonify.make_json_response(tags) | ||||
| 
 | ||||
| @site.route('/tag/<specific_tag>/edit', methods=['POST']) | ||||
| @decorators.catch_etiquette_exception | ||||
| def post_tag_edit(specific_tag): | ||||
|     tag = P_tag(specific_tag) | ||||
|     name = request.form.get('name', '').strip() | ||||
|     if name: | ||||
|         tag.rename(name, commit=False) | ||||
| 
 | ||||
|     description = request.form.get('description', None) | ||||
|     tag.edit(description=description) | ||||
| 
 | ||||
|     response = etiquette.jsonify.tag(tag) | ||||
|     response = jsonify.make_json_response(response) | ||||
|     return response | ||||
| 
 | ||||
| @site.route('/tags/<specific_tag>') | ||||
| @site.route('/tags/<specific_tag>.json') | ||||
| def get_tags_specific_redirect(specific_tag): | ||||
|     return flask.redirect(request.url.replace('/tags/', '/tag/')) | ||||
| 
 | ||||
| @site.route('/tags/create_tag', methods=['POST']) | ||||
| @decorators.required_fields(['tagname'], forbid_whitespace=True) | ||||
| def post_tag_create(): | ||||
|     ''' | ||||
|     Create a tag. | ||||
|     ''' | ||||
|     return post_tag_create_delete_core(request.form['tagname'], create_tag) | ||||
| 
 | ||||
| @site.route('/tags/delete_synonym', methods=['POST']) | ||||
| @decorators.required_fields(['tagname'], forbid_whitespace=True) | ||||
| def post_tag_delete_synonym(): | ||||
|     ''' | ||||
|     Delete a synonym. | ||||
|     ''' | ||||
|     return post_tag_create_delete_core(request.form['tagname'], delete_synonym) | ||||
| 
 | ||||
| @site.route('/tags/delete_tag', methods=['POST']) | ||||
| @decorators.required_fields(['tagname'], forbid_whitespace=True) | ||||
| def post_tag_delete(): | ||||
|     ''' | ||||
|     Delete a tag. | ||||
|     ''' | ||||
|     return post_tag_create_delete_core(request.form['tagname'], delete_tag) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/thumbnail/<photo_id>') | ||||
| def get_thumbnail(photo_id): | ||||
|     photo_id = photo_id.split('.')[0] | ||||
|     photo = P_photo(photo_id) | ||||
|     if photo.thumbnail: | ||||
|         path = photo.thumbnail | ||||
|     else: | ||||
|         flask.abort(404, 'That file doesnt have a thumbnail') | ||||
|     return send_file(path) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/user/<username>', methods=['GET']) | ||||
| @session_manager.give_token | ||||
| def get_user_html(username): | ||||
|     user = P_user(username, response_type='html') | ||||
|     session = session_manager.get(request) | ||||
|     return flask.render_template('user.html', user=user, session=session) | ||||
| 
 | ||||
| @site.route('/user/<username>.json', methods=['GET']) | ||||
| @session_manager.give_token | ||||
| def get_user_json(username): | ||||
|     user = P_user(username, response_type='json') | ||||
|     user = etiquette.jsonify.user(user) | ||||
|     return jsonify.make_json_response(user) | ||||
| 
 | ||||
| @site.route('/userid/<user_id>') | ||||
| @site.route('/userid/<user_id>.json') | ||||
| def get_user_id_redirect(user_id): | ||||
|     if request.url.endswith('.json'): | ||||
|         user = P_user_id(user_id, response_type='json') | ||||
|     else: | ||||
|         user = P_user_id(user_id, response_type='html') | ||||
|     url_from = '/userid/' + user_id | ||||
|     url_to = '/user/' + user.username | ||||
|     url = request.url.replace(url_from, url_to) | ||||
|     return flask.redirect(url) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/apitest') | ||||
| @session_manager.give_token | ||||
| def apitest(): | ||||
|     response = flask.Response('testing') | ||||
|     return response | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     #site.run(threaded=True) | ||||
|     pass | ||||
		Loading…
	
		Reference in a new issue