checkpoint
|  | @ -1,22 +1,29 @@ | ||||||
| import distutils.util | import distutils.util | ||||||
| import flask | import flask | ||||||
| from flask import request | from flask import request | ||||||
|  | import functools | ||||||
| import json | import json | ||||||
|  | import math | ||||||
| import mimetypes | import mimetypes | ||||||
| import os | import os | ||||||
| import random | import random | ||||||
| import re | import re | ||||||
| import requests | import requests | ||||||
|  | import sys | ||||||
|  | import time | ||||||
|  | import uuid | ||||||
| import warnings | import warnings | ||||||
| 
 | 
 | ||||||
|  | import phototagger | ||||||
|  | sys.path.append('C:\\git\\else\\Bytestring'); import bytestring | ||||||
|  | 
 | ||||||
| site = flask.Flask(__name__) | site = flask.Flask(__name__) | ||||||
| site.config.update( | site.config.update( | ||||||
|     SEND_FILE_MAX_AGE_DEFAULT=180, |     SEND_FILE_MAX_AGE_DEFAULT=180, | ||||||
|     TEMPLATES_AUTO_RELOAD=True, |     TEMPLATES_AUTO_RELOAD=True, | ||||||
| ) | ) | ||||||
|  | site.jinja_env.add_extension('jinja2.ext.do') | ||||||
| 
 | 
 | ||||||
| print(os.getcwd()) |  | ||||||
| import phototagger |  | ||||||
| P = phototagger.PhotoDB() | P = phototagger.PhotoDB() | ||||||
| 
 | 
 | ||||||
| FILE_READ_CHUNK = 2 ** 20 | FILE_READ_CHUNK = 2 ** 20 | ||||||
|  | @ -32,17 +39,60 @@ ERROR_TAG_TOO_SHORT = 'Not enough valid chars' | ||||||
| ERROR_SYNONYM_ITSELF = 'Cant apply synonym to itself' | ERROR_SYNONYM_ITSELF = 'Cant apply synonym to itself' | ||||||
| ERROR_NO_SUCH_TAG = 'Doesn\'t exist' | ERROR_NO_SUCH_TAG = 'Doesn\'t exist' | ||||||
| 
 | 
 | ||||||
|  | #################################################################################################### | ||||||
|  | #################################################################################################### | ||||||
|  | #################################################################################################### | ||||||
|  | #################################################################################################### | ||||||
|  | 
 | ||||||
|  | def give_session_token(function): | ||||||
|  |     @functools.wraps(function) | ||||||
|  |     def wrapped(*args, **kwargs): | ||||||
|  |         # Inject new token so the function doesn't know the difference | ||||||
|  |         token = request.cookies.get('etiquette_session', None) | ||||||
|  |         if not token: | ||||||
|  |             token = generate_session_token() | ||||||
|  |             request.cookies = dict(request.cookies) | ||||||
|  |             request.cookies['etiquette_session'] = token | ||||||
|  | 
 | ||||||
|  |         ret = function(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         # Send the token back to the client | ||||||
|  |         if not isinstance(ret, flask.Response): | ||||||
|  |             ret = flask.Response(ret) | ||||||
|  |         ret.set_cookie('etiquette_session', value=token, max_age=60) | ||||||
|  | 
 | ||||||
|  |         return ret | ||||||
|  |     return wrapped | ||||||
|  | 
 | ||||||
|  | def _helper_comma_split(s): | ||||||
|  |     if s is None: | ||||||
|  |         return s | ||||||
|  |     s = s.replace(' ', ',') | ||||||
|  |     s = [x.strip() for x in s.split(',')] | ||||||
|  |     s = [x for x in s if x] | ||||||
|  |     return s | ||||||
|  | 
 | ||||||
| def edit_params(original, modifications): | def edit_params(original, modifications): | ||||||
|     new_params = original.to_dict() |     new_params = original.to_dict() | ||||||
|     new_params.update(modifications) |     new_params.update(modifications) | ||||||
|     if not new_params: |     if not new_params: | ||||||
|         return '' |         return '' | ||||||
|     keep_params = {} |  | ||||||
|     new_params = ['%s=%s' % (k, v) for (k, v) in new_params.items() if v] |     new_params = ['%s=%s' % (k, v) for (k, v) in new_params.items() if v] | ||||||
|     new_params = '&'.join(new_params) |     new_params = '&'.join(new_params) | ||||||
|     new_params = '?' + new_params |     new_params = '?' + new_params | ||||||
|     return new_params |     return new_params | ||||||
| 
 | 
 | ||||||
|  | def generate_session_token(): | ||||||
|  |     token = str(uuid.uuid4()) | ||||||
|  |     #print('MAKE SESSION', token) | ||||||
|  |     return token | ||||||
|  | 
 | ||||||
|  | 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 P_album(albumid): | def P_album(albumid): | ||||||
|     try: |     try: | ||||||
|         return P.get_album(albumid) |         return P.get_album(albumid) | ||||||
|  | @ -80,6 +130,7 @@ def read_filebytes(filepath, range_min, range_max): | ||||||
|     sent_amount = 0 |     sent_amount = 0 | ||||||
|     with f: |     with f: | ||||||
|         while sent_amount < range_span: |         while sent_amount < range_span: | ||||||
|  |             print(sent_amount) | ||||||
|             chunk = f.read(FILE_READ_CHUNK) |             chunk = f.read(FILE_READ_CHUNK) | ||||||
|             if len(chunk) == 0: |             if len(chunk) == 0: | ||||||
|                 break |                 break | ||||||
|  | @ -91,6 +142,11 @@ def send_file(filepath): | ||||||
|     ''' |     ''' | ||||||
|     Range-enabled file sending. |     Range-enabled file sending. | ||||||
|     ''' |     ''' | ||||||
|  |     try: | ||||||
|  |         file_size = os.path.getsize(filepath) | ||||||
|  |     except FileNotFoundError: | ||||||
|  |         flask.abort(404) | ||||||
|  | 
 | ||||||
|     outgoing_headers = {} |     outgoing_headers = {} | ||||||
|     mimetype = mimetypes.guess_type(filepath)[0] |     mimetype = mimetypes.guess_type(filepath)[0] | ||||||
|     if mimetype is not None: |     if mimetype is not None: | ||||||
|  | @ -98,27 +154,17 @@ def send_file(filepath): | ||||||
|             mimetype += '; charset=utf-8' |             mimetype += '; charset=utf-8' | ||||||
|         outgoing_headers['Content-Type'] = mimetype |         outgoing_headers['Content-Type'] = mimetype | ||||||
| 
 | 
 | ||||||
|     if 'range' not in request.headers: |     if 'range' in request.headers: | ||||||
|         response = flask.make_response(flask.send_file(filepath)) |  | ||||||
|         for (k, v) in outgoing_headers.items(): |  | ||||||
|             response.headers[k] = v |  | ||||||
|         return response |  | ||||||
| 
 |  | ||||||
|     try: |  | ||||||
|         file_size = os.path.getsize(filepath) |  | ||||||
|     except FileNotFoundError: |  | ||||||
|         flask.abort(404) |  | ||||||
| 
 |  | ||||||
|         desired_range = request.headers['range'].lower() |         desired_range = request.headers['range'].lower() | ||||||
|         desired_range = desired_range.split('bytes=')[-1] |         desired_range = desired_range.split('bytes=')[-1] | ||||||
| 
 | 
 | ||||||
|     inthelper = lambda x: int(x) if x.isdigit() else None |         int_helper = lambda x: int(x) if x.isdigit() else None | ||||||
|         if '-' in desired_range: |         if '-' in desired_range: | ||||||
|             (desired_min, desired_max) = desired_range.split('-') |             (desired_min, desired_max) = desired_range.split('-') | ||||||
|         range_min = inthelper(desired_min) |             range_min = int_helper(desired_min) | ||||||
|         range_max = inthelper(desired_max) |             range_max = int_helper(desired_max) | ||||||
|         else: |         else: | ||||||
|         range_min = inthelper(desired_range) |             range_min = int_helper(desired_range) | ||||||
| 
 | 
 | ||||||
|         if range_min is None: |         if range_min is None: | ||||||
|             range_min = 0 |             range_min = 0 | ||||||
|  | @ -135,165 +181,27 @@ def send_file(filepath): | ||||||
|             outof=file_size, |             outof=file_size, | ||||||
|         ) |         ) | ||||||
|         outgoing_headers['Content-Range'] = range_header |         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['Accept-Ranges'] = 'bytes' | ||||||
|     outgoing_headers['Content-Length'] = (range_max - range_min) + 1 |     outgoing_headers['Content-Length'] = (range_max - range_min) + 1 | ||||||
| 
 | 
 | ||||||
|  |     if request.method == 'HEAD': | ||||||
|  |         outgoing_data = bytes() | ||||||
|  |     else: | ||||||
|         outgoing_data = read_filebytes(filepath, range_min=range_min, range_max=range_max) |         outgoing_data = read_filebytes(filepath, range_min=range_min, range_max=range_max) | ||||||
|  | 
 | ||||||
|     response = flask.Response( |     response = flask.Response( | ||||||
|         outgoing_data, |         outgoing_data, | ||||||
|         status=206, |         status=status, | ||||||
|         headers=outgoing_headers, |         headers=outgoing_headers, | ||||||
|     ) |     ) | ||||||
|     return response |     return response | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| @site.route('/') |  | ||||||
| def root(): |  | ||||||
|     motd = random.choice(MOTD_STRINGS) |  | ||||||
|     return flask.render_template('root.html', motd=motd) |  | ||||||
| 
 |  | ||||||
| @site.route('/album/<albumid>') |  | ||||||
| def get_album(albumid): |  | ||||||
|     album = P_album(albumid) |  | ||||||
|     response = flask.render_template( |  | ||||||
|         'album.html', |  | ||||||
|         album=album, |  | ||||||
|         child_albums=album.children(), |  | ||||||
|         photos=album.photos() |  | ||||||
|     ) |  | ||||||
|     return response |  | ||||||
| 
 |  | ||||||
| @site.route('/file/<photoid>') |  | ||||||
| def get_file(photoid): |  | ||||||
|     requested_photoid = photoid |  | ||||||
|     photoid = photoid.split('.')[0] |  | ||||||
|     photo = P.get_photo(photoid) |  | ||||||
| 
 |  | ||||||
|     do_download = request.args.get('download', False) |  | ||||||
|     do_download = truthystring(do_download) |  | ||||||
| 
 |  | ||||||
|     use_original_filename = request.args.get('original_filename', False) |  | ||||||
|     use_original_filename = truthystring(use_original_filename) |  | ||||||
| 
 |  | ||||||
|     if do_download: |  | ||||||
|         if use_original_filename: |  | ||||||
|             download_as = photo.basename |  | ||||||
|         else: |  | ||||||
|             download_as = photo.id + '.' + photo.extension |  | ||||||
| 
 |  | ||||||
|         # Sorry, but otherwise the attachment filename gets terminated |  | ||||||
|         #download_as = download_as.replace(';', '-') |  | ||||||
|         download_as = download_as.replace('"', '\\"') |  | ||||||
|         response = flask.make_response(send_file(photo.real_filepath)) |  | ||||||
|         response.headers['Content-Disposition'] = 'attachment; filename="%s"' % download_as |  | ||||||
|         return response |  | ||||||
|     else: |  | ||||||
|         return send_file(photo.real_filepath) |  | ||||||
| 
 |  | ||||||
| @site.route('/albums') |  | ||||||
| def get_albums(): |  | ||||||
|     albums = P.get_albums() |  | ||||||
|     albums = [a for a in albums if a.parent() is None] |  | ||||||
|     return flask.render_template('albums.html', albums=albums) |  | ||||||
| 
 |  | ||||||
| @site.route('/photo/<photoid>', methods=['GET']) |  | ||||||
| def get_photo(photoid): |  | ||||||
|     photo = P_photo(photoid) |  | ||||||
|     tags = photo.tags() |  | ||||||
|     tags.sort(key=lambda x: x.qualified_name()) |  | ||||||
|     return flask.render_template('photo.html', photo=photo, tags=tags) |  | ||||||
| 
 |  | ||||||
| @site.route('/tags') |  | ||||||
| @site.route('/tags/<specific_tag>') |  | ||||||
| def get_tags(specific_tag=None): |  | ||||||
|     try: |  | ||||||
|         tags = P.export_tags(phototagger.tag_export_easybake, specific_tag=specific_tag) |  | ||||||
|     except phototagger.NoSuchTag: |  | ||||||
|         flask.abort(404, 'That tag doesnt exist') |  | ||||||
| 
 |  | ||||||
|     tags = tags.split('\n') |  | ||||||
|     tags = [t for t in tags if t != ''] |  | ||||||
|     tags = [(t, t.split('.')[-1].split('+')[0]) for t in tags] |  | ||||||
|     return flask.render_template('tags.html', tags=tags) |  | ||||||
| 
 |  | ||||||
| @site.route('/thumbnail/<photoid>') |  | ||||||
| def get_thumbnail(photoid): |  | ||||||
|     photoid = photoid.split('.')[0] |  | ||||||
|     photo = P_photo(photoid) |  | ||||||
|     if photo.thumbnail: |  | ||||||
|         path = photo.thumbnail |  | ||||||
|     else: |  | ||||||
|         flask.abort(404, 'That file doesnt have a thumbnail') |  | ||||||
|     return send_file(path) |  | ||||||
| 
 |  | ||||||
| @site.route('/photo/<photoid>', methods=['POST']) |  | ||||||
| def edit_photo(photoid): |  | ||||||
|     print(request.form) |  | ||||||
|     response = {} |  | ||||||
|     photo = P_photo(photoid) |  | ||||||
| 
 |  | ||||||
|     if 'add_tag' in request.form: |  | ||||||
|         action = 'add_tag' |  | ||||||
|         method = photo.add_tag |  | ||||||
|     elif 'remove_tag' in request.form: |  | ||||||
|         action = 'remove_tag' |  | ||||||
|         method = photo.remove_tag |  | ||||||
|     else: |  | ||||||
|         flask.abort(400, 'Invalid action') |  | ||||||
| 
 |  | ||||||
|     tag = request.form[action].strip() |  | ||||||
|     if tag == '': |  | ||||||
|         flask.abort(400, 'No tag supplied') |  | ||||||
| 
 |  | ||||||
|     try: |  | ||||||
|         tag = P.get_tag(tag) |  | ||||||
|     except phototagger.NoSuchTag: |  | ||||||
|         return flask.Response('{"error": "That tag doesnt exist", "tagname":"%s"}'%tag, status=404) |  | ||||||
| 
 |  | ||||||
|     method(tag) |  | ||||||
|     response['action'] = action |  | ||||||
|     response['tagid'] = tag.id |  | ||||||
|     response['tagname'] = tag.name |  | ||||||
|     return json.dumps(response) |  | ||||||
| 
 |  | ||||||
| @site.route('/tags', methods=['POST']) |  | ||||||
| def edit_tags(): |  | ||||||
|     print(request.form) |  | ||||||
|     status = 200 |  | ||||||
|     if 'create_tag' in request.form: |  | ||||||
|         action = 'create_tag' |  | ||||||
|         method = create_tag |  | ||||||
|     elif 'delete_tag_synonym' in request.form: |  | ||||||
|         action = 'delete_tag_synonym' |  | ||||||
|         method = delete_synonym |  | ||||||
|     elif 'delete_tag' in request.form: |  | ||||||
|         action = 'delete_tag' |  | ||||||
|         method = delete_tag |  | ||||||
|     else: |  | ||||||
|         response = {'error': ERROR_INVALID_ACTION} |  | ||||||
| 
 |  | ||||||
|     if status == 200: |  | ||||||
|         status = 400 |  | ||||||
|         tag = request.form[action].strip() |  | ||||||
|         if tag == '': |  | ||||||
|             response = {'error': ERROR_NO_TAG_GIVEN} |  | ||||||
|         try: |  | ||||||
|             response = method(tag) |  | ||||||
|         except phototagger.TagTooShort: |  | ||||||
|             response = {'error': ERROR_TAG_TOO_SHORT, 'tagname': tag} |  | ||||||
|         except phototagger.CantSynonymSelf: |  | ||||||
|             response = {'error': ERROR_SYNONYM_ITSELF, 'tagname': tag} |  | ||||||
|         except phototagger.NoSuchTag as e: |  | ||||||
|             response = {'error': ERROR_NO_SUCH_TAG, 'tagname': tag} |  | ||||||
|         except ValueError as e: |  | ||||||
|             response = {'error': e.args[0], 'tagname': tag} |  | ||||||
|         else: |  | ||||||
|             status = 200 |  | ||||||
| 
 |  | ||||||
|     response = json.dumps(response) |  | ||||||
|     response = flask.Response(response, status=status) |  | ||||||
|     return response |  | ||||||
| 
 |  | ||||||
| def create_tag(easybake_string): | def create_tag(easybake_string): | ||||||
|     notes = P.easybake(easybake_string) |     notes = P.easybake(easybake_string) | ||||||
|     notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes] |     notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes] | ||||||
|  | @ -320,24 +228,170 @@ def delete_synonym(synonym): | ||||||
|     master_tag.remove_synonym(synonym) |     master_tag.remove_synonym(synonym) | ||||||
|     return {'action':'delete_synonym', 'synonym': synonym} |     return {'action':'delete_synonym', 'synonym': synonym} | ||||||
| 
 | 
 | ||||||
|  | #################################################################################################### | ||||||
|  | #################################################################################################### | ||||||
|  | #################################################################################################### | ||||||
|  | #################################################################################################### | ||||||
| 
 | 
 | ||||||
| @site.route('/search') | def jsonify_album(album, minimal=False): | ||||||
| def search(): |     j = { | ||||||
|  |         'id': album.id, | ||||||
|  |         'description': album.description, | ||||||
|  |         'title': album.title, | ||||||
|  |     } | ||||||
|  |     if minimal is False: | ||||||
|  |         j['photos'] = [jsonify_photo(photo) for photo in album.photos()] | ||||||
|  |         j['parent'] = album.parent() | ||||||
|  |         j['sub_albums'] = [child.id for child in album.children()] | ||||||
|  | 
 | ||||||
|  |     return j | ||||||
|  | 
 | ||||||
|  | def seconds_to_hms(seconds): | ||||||
|  |     seconds = math.ceil(seconds) | ||||||
|  |     (minutes, seconds) = divmod(seconds, 60) | ||||||
|  |     (hours, minutes) = divmod(minutes, 60) | ||||||
|  |     parts = [] | ||||||
|  |     if hours: parts.append(hours) | ||||||
|  |     if minutes: parts.append(minutes) | ||||||
|  |     parts.append(seconds) | ||||||
|  |     hms = ':'.join('%02d' % part for part in parts) | ||||||
|  |     return hms | ||||||
|  | 
 | ||||||
|  | def jsonify_photo(photo): | ||||||
|  |     tags = photo.tags() | ||||||
|  |     tags.sort(key=lambda x: x.name) | ||||||
|  |     j = { | ||||||
|  |         'id': photo.id, | ||||||
|  |         'extension': photo.extension, | ||||||
|  |         'width': photo.width, | ||||||
|  |         'height': photo.height, | ||||||
|  |         'ratio': photo.ratio, | ||||||
|  |         'area': photo.area, | ||||||
|  |         'bytes': photo.bytes, | ||||||
|  |         'duration': seconds_to_hms(photo.duration) if photo.duration else None, | ||||||
|  |         'duration_int': photo.duration, | ||||||
|  |         'bytestring': photo.bytestring(), | ||||||
|  |         'has_thumbnail': bool(photo.thumbnail), | ||||||
|  |         'created': photo.created, | ||||||
|  |         'filename': photo.basename, | ||||||
|  |         'mimetype': photo.mimetype(), | ||||||
|  |         'albums': [jsonify_album(album, minimal=True) for album in photo.albums()], | ||||||
|  |         'tags': [jsonify_tag(tag) for tag in tags], | ||||||
|  |     } | ||||||
|  |     return j | ||||||
|  | 
 | ||||||
|  | def jsonify_tag(tag): | ||||||
|  |     j = { | ||||||
|  |         'id': tag.id, | ||||||
|  |         'name': tag.name, | ||||||
|  |         'qualified_name': tag.qualified_name(), | ||||||
|  |     } | ||||||
|  |     return j | ||||||
|  | 
 | ||||||
|  | #################################################################################################### | ||||||
|  | #################################################################################################### | ||||||
|  | #################################################################################################### | ||||||
|  | #################################################################################################### | ||||||
|  | 
 | ||||||
|  | @site.route('/') | ||||||
|  | @give_session_token | ||||||
|  | def root(): | ||||||
|  |     motd = random.choice(MOTD_STRINGS) | ||||||
|  |     return flask.render_template('root.html', motd=motd) | ||||||
|  | 
 | ||||||
|  | @site.route('/favicon.ico') | ||||||
|  | @site.route('/favicon.png') | ||||||
|  | def favicon(): | ||||||
|  |     filename = os.path.join('static', 'favicon.png') | ||||||
|  |     return flask.send_file(filename) | ||||||
|  | 
 | ||||||
|  | def get_album_core(albumid): | ||||||
|  |     album = P_album(albumid) | ||||||
|  |     album = jsonify_album(album) | ||||||
|  |     return album | ||||||
|  | 
 | ||||||
|  | @site.route('/album/<albumid>') | ||||||
|  | @give_session_token | ||||||
|  | def get_album_html(albumid): | ||||||
|  |     album = get_album_core(albumid) | ||||||
|  |     response = flask.render_template( | ||||||
|  |         'album.html', | ||||||
|  |         album=album, | ||||||
|  |         child_albums=album['sub_albums'], | ||||||
|  |         photos=album['photos'], | ||||||
|  |     ) | ||||||
|  |     return response | ||||||
|  | 
 | ||||||
|  | @site.route('/album/<albumid>') | ||||||
|  | @give_session_token | ||||||
|  | def get_album_json(albumid): | ||||||
|  |     album = get_album_core(albumid) | ||||||
|  |     return make_json_response(album) | ||||||
|  | 
 | ||||||
|  | @site.route('/albums') | ||||||
|  | @give_session_token | ||||||
|  | def get_albums(): | ||||||
|  |     albums = P.get_albums() | ||||||
|  |     albums = [a for a in albums if a.parent() is None] | ||||||
|  |     return flask.render_template('albums.html', albums=albums) | ||||||
|  | 
 | ||||||
|  | @site.route('/file/<photoid>') | ||||||
|  | def get_file(photoid): | ||||||
|  |     requested_photoid = photoid | ||||||
|  |     photoid = photoid.split('.')[0] | ||||||
|  |     photo = P.get_photo(photoid) | ||||||
|  | 
 | ||||||
|  |     do_download = request.args.get('download', False) | ||||||
|  |     do_download = truthystring(do_download) | ||||||
|  | 
 | ||||||
|  |     use_original_filename = request.args.get('original_filename', False) | ||||||
|  |     use_original_filename = truthystring(use_original_filename) | ||||||
|  | 
 | ||||||
|  |     if do_download: | ||||||
|  |         if use_original_filename: | ||||||
|  |             download_as = photo.basename | ||||||
|  |         else: | ||||||
|  |             download_as = photo.id + '.' + photo.extension | ||||||
|  | 
 | ||||||
|  |         ## Sorry, but otherwise the attachment filename gets terminated | ||||||
|  |         #download_as = download_as.replace(';', '-') | ||||||
|  |         download_as = download_as.replace('"', '\\"') | ||||||
|  |         response = flask.make_response(send_file(photo.real_filepath)) | ||||||
|  |         response.headers['Content-Disposition'] = 'attachment; filename="%s"' % download_as | ||||||
|  |         return response | ||||||
|  |     else: | ||||||
|  |         return send_file(photo.real_filepath) | ||||||
|  | 
 | ||||||
|  | def get_photo_core(photoid): | ||||||
|  |     photo = P_photo(photoid) | ||||||
|  |     photo = jsonify_photo(photo) | ||||||
|  |     return photo | ||||||
|  | 
 | ||||||
|  | @site.route('/photo/<photoid>', methods=['GET']) | ||||||
|  | @give_session_token | ||||||
|  | def get_photo_html(photoid): | ||||||
|  |     photo = get_photo_core(photoid) | ||||||
|  |     photo['tags'].sort(key=lambda x: x['qualified_name']) | ||||||
|  |     return flask.render_template('photo.html', photo=photo) | ||||||
|  | 
 | ||||||
|  | @site.route('/photo/<photoid>.json', methods=['GET']) | ||||||
|  | @give_session_token | ||||||
|  | def get_photo_json(photoid): | ||||||
|  |     photo = get_photo_core(photoid) | ||||||
|  |     photo = make_json_response(photo) | ||||||
|  |     return photo | ||||||
|  | 
 | ||||||
|  | def get_search_core(): | ||||||
|     print(request.args) |     print(request.args) | ||||||
| 
 | 
 | ||||||
|     def comma_split_helper(s): |  | ||||||
|         s = s.replace(' ', ',') |  | ||||||
|         s = [x.strip() for x in s.split(',')] |  | ||||||
|         s = [x for x in s if x] |  | ||||||
|         return s |  | ||||||
|     # EXTENSION |     # EXTENSION | ||||||
|     extension_string = request.args.get('extension', '') |     extension_string = request.args.get('extension', None) | ||||||
|     extension_not_string = request.args.get('extension_not', '') |     extension_not_string = request.args.get('extension_not', None) | ||||||
|     mimetype_string = request.args.get('mimetype', '') |     mimetype_string = request.args.get('mimetype', None) | ||||||
| 
 | 
 | ||||||
|     extension_list = comma_split_helper(extension_string) |     extension_list = _helper_comma_split(extension_string) | ||||||
|     extension_not_list = comma_split_helper(extension_not_string) |     extension_not_list = _helper_comma_split(extension_not_string) | ||||||
|     mimetype_list = comma_split_helper(mimetype_string) |     mimetype_list = _helper_comma_split(mimetype_string) | ||||||
| 
 | 
 | ||||||
|     # LIMIT |     # LIMIT | ||||||
|     limit = request.args.get('limit', '') |     limit = request.args.get('limit', '') | ||||||
|  | @ -387,7 +441,7 @@ def search(): | ||||||
|     height = request.args.get('height', None) |     height = request.args.get('height', None) | ||||||
|     ratio = request.args.get('ratio', None) |     ratio = request.args.get('ratio', None) | ||||||
|     bytes = request.args.get('bytes', None) |     bytes = request.args.get('bytes', None) | ||||||
|     length = request.args.get('length', None) |     duration = request.args.get('duration', None) | ||||||
|     created = request.args.get('created', None) |     created = request.args.get('created', None) | ||||||
| 
 | 
 | ||||||
|     # These are in a dictionary so I can pass them to the page template. |     # These are in a dictionary so I can pass them to the page template. | ||||||
|  | @ -397,7 +451,7 @@ def search(): | ||||||
|         'height': height, |         'height': height, | ||||||
|         'ratio': ratio, |         'ratio': ratio, | ||||||
|         'bytes': bytes, |         'bytes': bytes, | ||||||
|         'length': length, |         'duration': duration, | ||||||
| 
 | 
 | ||||||
|         'created': created, |         'created': created, | ||||||
|         'extension': extension_list, |         'extension': extension_list, | ||||||
|  | @ -418,14 +472,16 @@ def search(): | ||||||
|     print(search_kwargs) |     print(search_kwargs) | ||||||
|     with warnings.catch_warnings(record=True) as catcher: |     with warnings.catch_warnings(record=True) as catcher: | ||||||
|         photos = list(P.search(**search_kwargs)) |         photos = list(P.search(**search_kwargs)) | ||||||
|  |         photos = [jsonify_photo(photo) for photo in photos] | ||||||
|         warns = [str(warning.message) for warning in catcher] |         warns = [str(warning.message) for warning in catcher] | ||||||
|     print(warns) |     print(warns) | ||||||
|  | 
 | ||||||
|  |     # TAGS ON THIS PAGE | ||||||
|     total_tags = set() |     total_tags = set() | ||||||
|     for photo in photos: |     for photo in photos: | ||||||
|         total_tags.update(photo.tags()) |         for tag in photo['tags']: | ||||||
|     for tag in total_tags: |             total_tags.add(tag['qualified_name']) | ||||||
|         tag._cached_qualname = qualname_map[tag.name] |     total_tags = sorted(total_tags) | ||||||
|     total_tags = sorted(total_tags, key=lambda x: x._cached_qualname) |  | ||||||
| 
 | 
 | ||||||
|     # PREV-NEXT PAGE URLS |     # PREV-NEXT PAGE URLS | ||||||
|     offset = offset or 0 |     offset = offset or 0 | ||||||
|  | @ -443,22 +499,148 @@ def search(): | ||||||
|     search_kwargs['extension'] = extension_string |     search_kwargs['extension'] = extension_string | ||||||
|     search_kwargs['extension_not'] = extension_not_string |     search_kwargs['extension_not'] = extension_not_string | ||||||
|     search_kwargs['mimetype'] = mimetype_string |     search_kwargs['mimetype'] = mimetype_string | ||||||
|  | 
 | ||||||
|  |     final_results = { | ||||||
|  |         'next_page_url': next_page_url, | ||||||
|  |         'prev_page_url': prev_page_url, | ||||||
|  |         'photos': photos, | ||||||
|  |         'total_tags': total_tags, | ||||||
|  |         'warns': warns, | ||||||
|  |         'search_kwargs': search_kwargs, | ||||||
|  |         'qualname_map': qualname_map, | ||||||
|  |     } | ||||||
|  |     return final_results | ||||||
|  | 
 | ||||||
|  | @site.route('/search') | ||||||
|  | @give_session_token | ||||||
|  | def get_search_html(): | ||||||
|  |     search_results = get_search_core() | ||||||
|  |     search_kwargs = search_results['search_kwargs'] | ||||||
|  |     qualname_map = search_results['qualname_map'] | ||||||
|     response = flask.render_template( |     response = flask.render_template( | ||||||
|         'search.html', |         'search.html', | ||||||
|         photos=photos, |         next_page_url=search_results['next_page_url'], | ||||||
|         search_kwargs=search_kwargs, |         prev_page_url=search_results['prev_page_url'], | ||||||
|         total_tags=total_tags, |         photos=search_results['photos'], | ||||||
|         prev_page_url=prev_page_url, |  | ||||||
|         next_page_url=next_page_url, |  | ||||||
|         qualname_map=json.dumps(qualname_map), |         qualname_map=json.dumps(qualname_map), | ||||||
|         warns=warns, |         search_kwargs=search_kwargs, | ||||||
|  |         total_tags=search_results['total_tags'], | ||||||
|  |         warns=search_results['warns'], | ||||||
|     ) |     ) | ||||||
|     return response |     return response | ||||||
| 
 | 
 | ||||||
|  | @site.route('/search.json') | ||||||
|  | @give_session_token | ||||||
|  | def get_search_json(): | ||||||
|  |     search_results = get_search_core() | ||||||
|  |     search_kwargs = search_results['search_kwargs'] | ||||||
|  |     qualname_map = search_results['qualname_map'] | ||||||
|  |     include_qualname_map = request.args.get('include_map', False) | ||||||
|  |     include_qualname_map = truthystring(include_qualname_map) | ||||||
|  |     if not include_qualname_map: | ||||||
|  |         search_results.pop('qualname_map') | ||||||
|  |     return make_json_response(j) | ||||||
|  | 
 | ||||||
| @site.route('/static/<filename>') | @site.route('/static/<filename>') | ||||||
| def get_resource(filename): | def get_static(filename): | ||||||
|     print(filename) |     filename = filename.replace('\\', os.sep) | ||||||
|     return flask.send_file('.\\static\\%s' % filename) |     filename = filename.replace('/', os.sep) | ||||||
|  |     filename = os.path.join('static', filename) | ||||||
|  |     return flask.send_file(filename) | ||||||
|  | 
 | ||||||
|  | @site.route('/tags') | ||||||
|  | @site.route('/tags/<specific_tag>') | ||||||
|  | @give_session_token | ||||||
|  | def get_tags(specific_tag=None): | ||||||
|  |     try: | ||||||
|  |         tags = P.export_tags(phototagger.tag_export_easybake, specific_tag=specific_tag) | ||||||
|  |     except phototagger.NoSuchTag: | ||||||
|  |         flask.abort(404, 'That tag doesnt exist') | ||||||
|  | 
 | ||||||
|  |     tags = tags.split('\n') | ||||||
|  |     tags = [t for t in tags if t != ''] | ||||||
|  |     tags = [(t, t.split('.')[-1].split('+')[0]) for t in tags] | ||||||
|  |     return flask.render_template('tags.html', tags=tags) | ||||||
|  | 
 | ||||||
|  | @site.route('/thumbnail/<photoid>') | ||||||
|  | def get_thumbnail(photoid): | ||||||
|  |     photoid = photoid.split('.')[0] | ||||||
|  |     photo = P_photo(photoid) | ||||||
|  |     if photo.thumbnail: | ||||||
|  |         path = photo.thumbnail | ||||||
|  |     else: | ||||||
|  |         flask.abort(404, 'That file doesnt have a thumbnail') | ||||||
|  |     return send_file(path) | ||||||
|  | 
 | ||||||
|  | @site.route('/photo/<photoid>', methods=['POST']) | ||||||
|  | @give_session_token | ||||||
|  | def post_edit_photo(photoid): | ||||||
|  |     print(request.form) | ||||||
|  |     response = {} | ||||||
|  |     photo = P_photo(photoid) | ||||||
|  | 
 | ||||||
|  |     if 'add_tag' in request.form: | ||||||
|  |         action = 'add_tag' | ||||||
|  |         method = photo.add_tag | ||||||
|  |     elif 'remove_tag' in request.form: | ||||||
|  |         action = 'remove_tag' | ||||||
|  |         method = photo.remove_tag | ||||||
|  |     else: | ||||||
|  |         flask.abort(400, 'Invalid action') | ||||||
|  | 
 | ||||||
|  |     tag = request.form[action].strip() | ||||||
|  |     if tag == '': | ||||||
|  |         flask.abort(400, 'No tag supplied') | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         tag = P.get_tag(tag) | ||||||
|  |     except phototagger.NoSuchTag: | ||||||
|  |         return flask.Response('{"error": "That tag doesnt exist", "tagname":"%s"}'%tag, status=404) | ||||||
|  | 
 | ||||||
|  |     method(tag) | ||||||
|  |     response['action'] = action | ||||||
|  |     response['tagid'] = tag.id | ||||||
|  |     response['tagname'] = tag.name | ||||||
|  |     return json.dumps(response) | ||||||
|  | 
 | ||||||
|  | @site.route('/tags', methods=['POST']) | ||||||
|  | @give_session_token | ||||||
|  | def post_edit_tags(): | ||||||
|  |     print(request.form) | ||||||
|  |     status = 200 | ||||||
|  |     if 'create_tag' in request.form: | ||||||
|  |         action = 'create_tag' | ||||||
|  |         method = create_tag | ||||||
|  |     elif 'delete_tag_synonym' in request.form: | ||||||
|  |         action = 'delete_tag_synonym' | ||||||
|  |         method = delete_synonym | ||||||
|  |     elif 'delete_tag' in request.form: | ||||||
|  |         action = 'delete_tag' | ||||||
|  |         method = delete_tag | ||||||
|  |     else: | ||||||
|  |         response = {'error': ERROR_INVALID_ACTION} | ||||||
|  | 
 | ||||||
|  |     if status == 200: | ||||||
|  |         status = 400 | ||||||
|  |         tag = request.form[action].strip() | ||||||
|  |         if tag == '': | ||||||
|  |             response = {'error': ERROR_NO_TAG_GIVEN} | ||||||
|  |         try: | ||||||
|  |             response = method(tag) | ||||||
|  |         except phototagger.TagTooShort: | ||||||
|  |             response = {'error': ERROR_TAG_TOO_SHORT, 'tagname': tag} | ||||||
|  |         except phototagger.CantSynonymSelf: | ||||||
|  |             response = {'error': ERROR_SYNONYM_ITSELF, 'tagname': tag} | ||||||
|  |         except phototagger.NoSuchTag as e: | ||||||
|  |             response = {'error': ERROR_NO_SUCH_TAG, 'tagname': tag} | ||||||
|  |         except ValueError as e: | ||||||
|  |             response = {'error': e.args[0], 'tagname': tag} | ||||||
|  |         else: | ||||||
|  |             status = 200 | ||||||
|  | 
 | ||||||
|  |     response = json.dumps(response) | ||||||
|  |     response = flask.Response(response, status=status) | ||||||
|  |     return response | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|  |  | ||||||
|  | @ -27,10 +27,13 @@ DEFAULT_THUMBDIR = '_site_thumbnails' | ||||||
| THUMBNAIL_WIDTH = 400 | THUMBNAIL_WIDTH = 400 | ||||||
| THUMBNAIL_HEIGHT = 400 | THUMBNAIL_HEIGHT = 400 | ||||||
| 
 | 
 | ||||||
| ffmpeg = converter.Converter( | try: | ||||||
|  |     ffmpeg = converter.Converter( | ||||||
|         ffmpeg_path='C:\\software\\ffmpeg\\bin\\ffmpeg.exe', |         ffmpeg_path='C:\\software\\ffmpeg\\bin\\ffmpeg.exe', | ||||||
|         ffprobe_path='C:\\software\\ffmpeg\\bin\\ffprobe.exe', |         ffprobe_path='C:\\software\\ffmpeg\\bin\\ffprobe.exe', | ||||||
| ) |     ) | ||||||
|  | except converter.ffmpeg.FFMpegError: | ||||||
|  |     ffmpeg = None | ||||||
| 
 | 
 | ||||||
| logging.basicConfig(level=logging.DEBUG) | logging.basicConfig(level=logging.DEBUG) | ||||||
| log = logging.getLogger(__name__) | log = logging.getLogger(__name__) | ||||||
|  | @ -40,8 +43,8 @@ ADDITIONAL_MIMETYPES = { | ||||||
|     'srt': 'text', |     'srt': 'text', | ||||||
|     'mkv': 'video', |     'mkv': 'video', | ||||||
| } | } | ||||||
| WARNING_MINMAX_INVALID = 'Field {field}: "{value}" is not a valid request. Ignored.' | WARNING_MINMAX_INVALID = 'Field "{field}": "{value}" is not a valid request. Ignored.' | ||||||
| WARNING_MINMAX_OOO = 'Field {field}: minimum "{min}" maximum "{max}" are out of order. Ignored.' | WARNING_MINMAX_OOO = 'Field "{field}": minimum "{min}" maximum "{max}" are out of order. Ignored.' | ||||||
| WARNING_NO_SUCH_TAG = 'Tag "{tag}" does not exist. Ignored.' | WARNING_NO_SUCH_TAG = 'Tag "{tag}" does not exist. Ignored.' | ||||||
| WARNING_ORDERBY_BADCOL = '"{column}" is not a sorting option. Ignored.' | WARNING_ORDERBY_BADCOL = '"{column}" is not a sorting option. Ignored.' | ||||||
| WARNING_ORDERBY_BADSORTER = 'You can\'t order "{column}" by "{sorter}". Defaulting to descending.' | WARNING_ORDERBY_BADSORTER = 'You can\'t order "{column}" by "{sorter}". Defaulting to descending.' | ||||||
|  | @ -66,7 +69,7 @@ SQL_PHOTO_COLUMNS = [ | ||||||
|     'height', |     'height', | ||||||
|     'ratio', |     'ratio', | ||||||
|     'area', |     'area', | ||||||
|     'length', |     'duration', | ||||||
|     'bytes', |     'bytes', | ||||||
|     'created', |     'created', | ||||||
|     'thumbnail', |     'thumbnail', | ||||||
|  | @ -79,7 +82,6 @@ SQL_SYN_COLUMNS = [ | ||||||
|     'name', |     'name', | ||||||
|     'master', |     'master', | ||||||
| ] | ] | ||||||
| 
 |  | ||||||
| SQL_ALBUMPHOTO_COLUMNS = [ | SQL_ALBUMPHOTO_COLUMNS = [ | ||||||
|     'albumid', |     'albumid', | ||||||
|     'photoid', |     'photoid', | ||||||
|  | @ -119,7 +121,7 @@ CREATE TABLE IF NOT EXISTS photos( | ||||||
|     height INT, |     height INT, | ||||||
|     ratio REAL, |     ratio REAL, | ||||||
|     area INT, |     area INT, | ||||||
|     length INT, |     duration INT, | ||||||
|     bytes INT, |     bytes INT, | ||||||
|     created INT, |     created INT, | ||||||
|     thumbnail TEXT |     thumbnail TEXT | ||||||
|  | @ -176,8 +178,30 @@ CREATE INDEX IF NOT EXISTS index_grouprel_parentid on tag_group_rel(parentid); | ||||||
| CREATE INDEX IF NOT EXISTS index_grouprel_memberid on tag_group_rel(memberid); | CREATE INDEX IF NOT EXISTS index_grouprel_memberid on tag_group_rel(memberid); | ||||||
| ''' | ''' | ||||||
| 
 | 
 | ||||||
|  | def not_implemented(function): | ||||||
|  |     ''' | ||||||
|  |     Decorator to remember what needs doing. | ||||||
|  |     ''' | ||||||
|  |     warnings.warn('%s is not implemented' % function.__name__) | ||||||
|  |     return function | ||||||
|  | 
 | ||||||
|  | def time_me(function): | ||||||
|  |     ''' | ||||||
|  |     Decorator. After the function is run, print the elapsed time. | ||||||
|  |     ''' | ||||||
|  |     @functools.wraps(function) | ||||||
|  |     def timed_function(*args, **kwargs): | ||||||
|  |         start = time.time() | ||||||
|  |         result = function(*args, **kwargs) | ||||||
|  |         end = time.time() | ||||||
|  |         print('%s: %0.8f' % (function.__name__, end-start)) | ||||||
|  |         return result | ||||||
|  |     return timed_function | ||||||
| 
 | 
 | ||||||
| def _helper_extension(ext): | def _helper_extension(ext): | ||||||
|  |     ''' | ||||||
|  |     When searching, this function normalizes the list of permissible extensions. | ||||||
|  |     ''' | ||||||
|     if isinstance(ext, str): |     if isinstance(ext, str): | ||||||
|         ext = [ext] |         ext = [ext] | ||||||
|     if ext is None: |     if ext is None: | ||||||
|  | @ -188,6 +212,10 @@ def _helper_extension(ext): | ||||||
|     return ext |     return ext | ||||||
| 
 | 
 | ||||||
| def _helper_minmax(key, value, minimums, maximums): | def _helper_minmax(key, value, minimums, maximums): | ||||||
|  |     ''' | ||||||
|  |     When searching, this function dissects a hyphenated range string | ||||||
|  |     and inserts the correct k:v pair into both minimums and maximums. | ||||||
|  |     ''' | ||||||
|     if value is None: |     if value is None: | ||||||
|         return |         return | ||||||
|     if isinstance(value, (int, float)): |     if isinstance(value, (int, float)): | ||||||
|  | @ -207,6 +235,10 @@ def _helper_minmax(key, value, minimums, maximums): | ||||||
|         maximums[key] = high |         maximums[key] = high | ||||||
| 
 | 
 | ||||||
| def _helper_orderby(orderby): | def _helper_orderby(orderby): | ||||||
|  |     ''' | ||||||
|  |     When searching, this function ensures that the user has entered a valid orderby | ||||||
|  |     query, and normalizes the query text. | ||||||
|  |     ''' | ||||||
|     orderby = orderby.lower().strip() |     orderby = orderby.lower().strip() | ||||||
|     if orderby == '': |     if orderby == '': | ||||||
|         return None |         return None | ||||||
|  | @ -227,7 +259,7 @@ def _helper_orderby(orderby): | ||||||
|         'height', |         'height', | ||||||
|         'ratio', |         'ratio', | ||||||
|         'area', |         'area', | ||||||
|         'length', |         'duration', | ||||||
|         'bytes', |         'bytes', | ||||||
|         'created', |         'created', | ||||||
|         'random', |         'random', | ||||||
|  | @ -244,6 +276,11 @@ def _helper_orderby(orderby): | ||||||
|     return (column, sorter) |     return (column, sorter) | ||||||
| 
 | 
 | ||||||
| def _helper_setify(photodb, l, warn_bad_tags=False): | def _helper_setify(photodb, l, warn_bad_tags=False): | ||||||
|  |     ''' | ||||||
|  |     When searching, this function converts the list of tag strings that the user | ||||||
|  |     requested into Tag objects. If a tag doesn't exist we'll either raise an exception | ||||||
|  |     or just issue a warning. | ||||||
|  |     ''' | ||||||
|     if l is None: |     if l is None: | ||||||
|         return set() |         return set() | ||||||
| 
 | 
 | ||||||
|  | @ -263,6 +300,18 @@ def _helper_setify(photodb, l, warn_bad_tags=False): | ||||||
|             s.add(tag) |             s.add(tag) | ||||||
|     return s |     return s | ||||||
| 
 | 
 | ||||||
|  | def _helper_unitconvert(value): | ||||||
|  |     ''' | ||||||
|  |     When parsing hyphenated ranges, this function is used to convert | ||||||
|  |     strings like "1k" to 1024 and "1:00" to 60. | ||||||
|  |     ''' | ||||||
|  |     if value is None: | ||||||
|  |         return None | ||||||
|  |     if ':' in value: | ||||||
|  |         return hms_to_seconds(value) | ||||||
|  |     else: | ||||||
|  |         return bytestring.parsebytes(value) | ||||||
|  | 
 | ||||||
| def chunk_sequence(sequence, chunk_length, allow_incomplete=True): | def chunk_sequence(sequence, chunk_length, allow_incomplete=True): | ||||||
|     ''' |     ''' | ||||||
|     Given a sequence, divide it into sequences of length `chunk_length`. |     Given a sequence, divide it into sequences of length `chunk_length`. | ||||||
|  | @ -286,25 +335,45 @@ def chunk_sequence(sequence, chunk_length, allow_incomplete=True): | ||||||
| 
 | 
 | ||||||
|     return chunks |     return chunks | ||||||
| 
 | 
 | ||||||
|  | def hms_to_seconds(hms): | ||||||
|  |     ''' | ||||||
|  |     Convert hh:mm:ss string to an integer seconds. | ||||||
|  |     ''' | ||||||
|  |     hms = hms.split(':') | ||||||
|  |     seconds = 0 | ||||||
|  |     if len(hms) == 3: | ||||||
|  |         seconds += int(hms[0])*3600 | ||||||
|  |         hms.pop(0) | ||||||
|  |     if len(hms) == 2: | ||||||
|  |         seconds += int(hms[0])*60 | ||||||
|  |         hms.pop(0) | ||||||
|  |     if len(hms) == 1: | ||||||
|  |         seconds += int(hms[0]) | ||||||
|  |     return seconds | ||||||
|  | 
 | ||||||
| def hyphen_range(s): | def hyphen_range(s): | ||||||
|     ''' |     ''' | ||||||
|     Given a string like '1-3', return floats (1.0, 3.0) representing lower |     Given a string like '1-3', return ints (1, 3) representing lower | ||||||
|     and upper bounds. |     and upper bounds. | ||||||
|  | 
 | ||||||
|  |     Supports bytestring.parsebytes and hh:mm:ss format. | ||||||
|     ''' |     ''' | ||||||
|     s = s.strip() |     s = s.strip() | ||||||
|  |     s = s.replace(' ', '') | ||||||
|     if not s: |     if not s: | ||||||
|         return (None, None) |         return (None, None) | ||||||
|     pattern = r'^(\d*\.?\d*)-(\d*\.?\d*)$' |     parts = s.split('-') | ||||||
|     try: |     parts = [part.strip() or None for part in parts] | ||||||
|         match = re.search(pattern, s) |     if len(parts) == 1: | ||||||
|         low = match.group(1) |         low = parts[0] | ||||||
|         high = match.group(2) |  | ||||||
|     except AttributeError: |  | ||||||
|         low = float(s) |  | ||||||
|         high = None |         high = None | ||||||
|  |     elif len(parts) == 2: | ||||||
|  |         (low, high) = parts | ||||||
|     else: |     else: | ||||||
|         low = float(low) if low.strip() else None |         raise ValueError('Too many hyphens') | ||||||
|         high = float(high) if high.strip() else None | 
 | ||||||
|  |     low = _helper_unitconvert(low) | ||||||
|  |     high = _helper_unitconvert(high) | ||||||
|     if low is not None and high is not None and low > high: |     if low is not None and high is not None and low > high: | ||||||
|         raise OutOfOrder(s, low, high) |         raise OutOfOrder(s, low, high) | ||||||
|     return low, high |     return low, high | ||||||
|  | @ -349,7 +418,7 @@ def is_xor(*args): | ||||||
| 
 | 
 | ||||||
| def normalize_filepath(filepath): | def normalize_filepath(filepath): | ||||||
|     ''' |     ''' | ||||||
|     Remove some bad characters |     Remove some bad characters. | ||||||
|     ''' |     ''' | ||||||
|     filepath = filepath.replace('<', '') |     filepath = filepath.replace('<', '') | ||||||
|     filepath = filepath.replace('>', '') |     filepath = filepath.replace('>', '') | ||||||
|  | @ -375,13 +444,6 @@ def normalize_tagname(tagname): | ||||||
| 
 | 
 | ||||||
|     return tagname |     return tagname | ||||||
| 
 | 
 | ||||||
| def not_implemented(function): |  | ||||||
|     ''' |  | ||||||
|     Decorator for keeping track of which functions still need to be filled out. |  | ||||||
|     ''' |  | ||||||
|     warnings.warn('%s is not implemented' % function.__name__) |  | ||||||
|     return function |  | ||||||
| 
 |  | ||||||
| def operate(operand_stack, operator_stack): | def operate(operand_stack, operator_stack): | ||||||
|     #print('before:', operand_stack, operator_stack) |     #print('before:', operand_stack, operator_stack) | ||||||
|     operator = operator_stack.pop() |     operator = operator_stack.pop() | ||||||
|  | @ -488,7 +550,8 @@ def searchfilter_must_may_forbid(photo_tags, tag_musts, tag_mays, tag_forbids, f | ||||||
| 
 | 
 | ||||||
|     return True |     return True | ||||||
| 
 | 
 | ||||||
| def select(sql, query, bindings=[]): | def select(sql, query, bindings=None): | ||||||
|  |     bindings = bindings or [] | ||||||
|     cursor = sql.cursor() |     cursor = sql.cursor() | ||||||
|     cursor.execute(query, bindings) |     cursor.execute(query, bindings) | ||||||
|     while True: |     while True: | ||||||
|  | @ -556,6 +619,7 @@ def tag_export_stdout(tags, depth=0): | ||||||
|         if tag.parent() is None: |         if tag.parent() is None: | ||||||
|             print() |             print() | ||||||
| 
 | 
 | ||||||
|  | @time_me | ||||||
| def tag_export_totally_flat(tags): | def tag_export_totally_flat(tags): | ||||||
|     result = {} |     result = {} | ||||||
|     for tag in tags: |     for tag in tags: | ||||||
|  | @ -566,18 +630,6 @@ def tag_export_totally_flat(tags): | ||||||
|                 result[synonym] = children |                 result[synonym] = children | ||||||
|     return result |     return result | ||||||
| 
 | 
 | ||||||
| def time_me(function): |  | ||||||
|     @functools.wraps(function) |  | ||||||
|     def timed_function(*args, **kwargs): |  | ||||||
|         start = time.time() |  | ||||||
|         result = function(*args, **kwargs) |  | ||||||
|         end = time.time() |  | ||||||
|         print('%s: %0.8f' % (function.__name__, end-start)) |  | ||||||
|         return result |  | ||||||
|     return timed_function |  | ||||||
| 
 |  | ||||||
| tag_export_totally_flat = time_me(tag_export_totally_flat) |  | ||||||
| 
 |  | ||||||
| #################################################################################################### | #################################################################################################### | ||||||
| #################################################################################################### | #################################################################################################### | ||||||
| 
 | 
 | ||||||
|  | @ -765,7 +817,7 @@ class PDBPhotoMixin: | ||||||
|         data[SQL_PHOTO['height']] = None |         data[SQL_PHOTO['height']] = None | ||||||
|         data[SQL_PHOTO['area']] = None |         data[SQL_PHOTO['area']] = None | ||||||
|         data[SQL_PHOTO['ratio']] = None |         data[SQL_PHOTO['ratio']] = None | ||||||
|         data[SQL_PHOTO['length']] = None |         data[SQL_PHOTO['duration']] = None | ||||||
|         data[SQL_PHOTO['thumbnail']] = None |         data[SQL_PHOTO['thumbnail']] = None | ||||||
| 
 | 
 | ||||||
|         self.cur.execute('INSERT INTO photos VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', data) |         self.cur.execute('INSERT INTO photos VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', data) | ||||||
|  | @ -782,7 +834,6 @@ class PDBPhotoMixin: | ||||||
| 
 | 
 | ||||||
|         if commit: |         if commit: | ||||||
|             log.debug('Commiting - new_photo') |             log.debug('Commiting - new_photo') | ||||||
|             self.sql.commit() |  | ||||||
|         return photo |         return photo | ||||||
| 
 | 
 | ||||||
|     def search( |     def search( | ||||||
|  | @ -792,7 +843,7 @@ class PDBPhotoMixin: | ||||||
|             height=None, |             height=None, | ||||||
|             ratio=None, |             ratio=None, | ||||||
|             bytes=None, |             bytes=None, | ||||||
|             length=None, |             duration=None, | ||||||
| 
 | 
 | ||||||
|             created=None, |             created=None, | ||||||
|             extension=None, |             extension=None, | ||||||
|  | @ -811,12 +862,12 @@ class PDBPhotoMixin: | ||||||
|         ): |         ): | ||||||
|         ''' |         ''' | ||||||
|         PHOTO PROPERTISE |         PHOTO PROPERTISE | ||||||
|         area, width, height, ratio, bytes, length: |         area, width, height, ratio, bytes, duration: | ||||||
|             A hyphen_range string representing min and max. Or just a number for lower bound. |             A hyphen_range string representing min and max. Or just a number for lower bound. | ||||||
| 
 | 
 | ||||||
|         TAGS AND FILTERS |         TAGS AND FILTERS | ||||||
|         created: |         created: | ||||||
|             A hyphen_range string respresenting min and max. Or jjust a number for lower bound. |             A hyphen_range string respresenting min and max. Or just a number for lower bound. | ||||||
| 
 | 
 | ||||||
|         extension: |         extension: | ||||||
|             A string or list of strings of acceptable file extensions. |             A string or list of strings of acceptable file extensions. | ||||||
|  | @ -868,11 +919,12 @@ class PDBPhotoMixin: | ||||||
|         maximums = {} |         maximums = {} | ||||||
|         minimums = {} |         minimums = {} | ||||||
|         _helper_minmax('area', area, minimums, maximums) |         _helper_minmax('area', area, minimums, maximums) | ||||||
|  |         _helper_minmax('created', created, minimums, maximums) | ||||||
|         _helper_minmax('width', width, minimums, maximums) |         _helper_minmax('width', width, minimums, maximums) | ||||||
|         _helper_minmax('height', height, minimums, maximums) |         _helper_minmax('height', height, minimums, maximums) | ||||||
|         _helper_minmax('ratio', ratio, minimums, maximums) |         _helper_minmax('ratio', ratio, minimums, maximums) | ||||||
|         _helper_minmax('bytes', bytes, minimums, maximums) |         _helper_minmax('bytes', bytes, minimums, maximums) | ||||||
|         _helper_minmax('length', length, minimums, maximums) |         _helper_minmax('duration', duration, minimums, maximums) | ||||||
|         orderby = orderby or [] |         orderby = orderby or [] | ||||||
| 
 | 
 | ||||||
|         extension = _helper_extension(extension) |         extension = _helper_extension(extension) | ||||||
|  | @ -907,7 +959,14 @@ class PDBPhotoMixin: | ||||||
|         # EVERY tag in the db is a key, and the value is a list of ALL ITS NESTED CHILDREN. |         # EVERY tag in the db is a key, and the value is a list of ALL ITS NESTED CHILDREN. | ||||||
|         # This representation is memory inefficient, but it is faster than repeated |         # This representation is memory inefficient, but it is faster than repeated | ||||||
|         # database lookups |         # database lookups | ||||||
|  |         is_must_may_forbid = bool(tag_musts or tag_mays or tag_forbids) | ||||||
|  |         is_tagsearch = is_must_may_forbid or tag_expression | ||||||
|  |         if is_tagsearch: | ||||||
|  |             if self._cached_frozen_children: | ||||||
|  |                 frozen_children = self._cached_frozen_children | ||||||
|  |             else: | ||||||
|                 frozen_children = self.export_tags(tag_export_totally_flat) |                 frozen_children = self.export_tags(tag_export_totally_flat) | ||||||
|  |                 self._cached_frozen_children = frozen_children | ||||||
|         photos_received = 0 |         photos_received = 0 | ||||||
| 
 | 
 | ||||||
|         for fetch in generator: |         for fetch in generator: | ||||||
|  | @ -932,7 +991,7 @@ class PDBPhotoMixin: | ||||||
|                 #print('Failed minimums') |                 #print('Failed minimums') | ||||||
|                 continue |                 continue | ||||||
| 
 | 
 | ||||||
|             if (has_tags is not None) or tag_musts or tag_mays or tag_forbids or tag_expression: |             if (has_tags is not None) or is_tagsearch: | ||||||
|                 photo_tags = photo.tags() |                 photo_tags = photo.tags() | ||||||
| 
 | 
 | ||||||
|                 if has_tags is False and len(photo_tags) > 0: |                 if has_tags is False and len(photo_tags) > 0: | ||||||
|  | @ -946,7 +1005,7 @@ class PDBPhotoMixin: | ||||||
|                 if tag_expression: |                 if tag_expression: | ||||||
|                     if not searchfilter_expression(photo_tags, tag_expression, frozen_children, warn_bad_tags): |                     if not searchfilter_expression(photo_tags, tag_expression, frozen_children, warn_bad_tags): | ||||||
|                         continue |                         continue | ||||||
|                 else: |                 elif is_must_may_forbid: | ||||||
|                     if not searchfilter_must_may_forbid(photo_tags, tag_musts, tag_mays, tag_forbids, frozen_children): |                     if not searchfilter_must_may_forbid(photo_tags, tag_musts, tag_mays, tag_forbids, frozen_children): | ||||||
|                         continue |                         continue | ||||||
| 
 | 
 | ||||||
|  | @ -1031,7 +1090,9 @@ class PDBTagMixin: | ||||||
|             pass |             pass | ||||||
|         else: |         else: | ||||||
|             raise TagExists(tagname) |             raise TagExists(tagname) | ||||||
|  | 
 | ||||||
|         tagid = self.generate_id('tags') |         tagid = self.generate_id('tags') | ||||||
|  |         self._cached_frozen_children = None | ||||||
|         self.cur.execute('INSERT INTO tags VALUES(?, ?)', [tagid, tagname]) |         self.cur.execute('INSERT INTO tags VALUES(?, ?)', [tagid, tagname]) | ||||||
|         if commit: |         if commit: | ||||||
|             log.debug('Commiting - new_tag') |             log.debug('Commiting - new_tag') | ||||||
|  | @ -1088,9 +1149,14 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | ||||||
|         for statement in statements: |         for statement in statements: | ||||||
|             self.cur.execute(statement) |             self.cur.execute(statement) | ||||||
| 
 | 
 | ||||||
|  |         self._cached_frozen_children = None | ||||||
|  | 
 | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return 'PhotoDB(databasename={dbname})'.format(dbname=repr(self.databasename)) |         return 'PhotoDB(databasename={dbname})'.format(dbname=repr(self.databasename)) | ||||||
| 
 | 
 | ||||||
|  |     def _uncache(self): | ||||||
|  |         self._cached_frozen_children = None | ||||||
|  | 
 | ||||||
|     def digest_directory(self, directory, exclude_directories=None, exclude_filenames=None, commit=True): |     def digest_directory(self, directory, exclude_directories=None, exclude_filenames=None, commit=True): | ||||||
|         ''' |         ''' | ||||||
|         Create an album, and add the directory's contents to it. |         Create an album, and add the directory's contents to it. | ||||||
|  | @ -1138,7 +1204,14 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | ||||||
|             self.sql.commit() |             self.sql.commit() | ||||||
|         return album |         return album | ||||||
| 
 | 
 | ||||||
|     def digest_new_files(self, directory, exclude_directories=None, exclude_filenames=None, commit=True): |     def digest_new_files( | ||||||
|  |             self, | ||||||
|  |             directory, | ||||||
|  |             exclude_directories=None, | ||||||
|  |             exclude_filenames=None, | ||||||
|  |             recurse=True, | ||||||
|  |             commit=True | ||||||
|  |         ): | ||||||
|         ''' |         ''' | ||||||
|         Walk the directory and add new files as Photos. |         Walk the directory and add new files as Photos. | ||||||
|         Does NOT create or modify any albums like `digest_directory` does. |         Does NOT create or modify any albums like `digest_directory` does. | ||||||
|  | @ -1161,6 +1234,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | ||||||
|             directory, |             directory, | ||||||
|             exclude_directories=exclude_directories, |             exclude_directories=exclude_directories, | ||||||
|             exclude_filenames=exclude_filenames, |             exclude_filenames=exclude_filenames, | ||||||
|  |             recurse=recurse, | ||||||
|             yield_style='flat', |             yield_style='flat', | ||||||
|         ) |         ) | ||||||
|         for filepath in generator: |         for filepath in generator: | ||||||
|  | @ -1171,7 +1245,10 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | ||||||
|                 pass |                 pass | ||||||
|             else: |             else: | ||||||
|                 continue |                 continue | ||||||
|             photo = self.new_photo(filepath) |             photo = self.new_photo(filepath, commit=False) | ||||||
|  |         if commit: | ||||||
|  |             log.debug('Committing - digest_new_files') | ||||||
|  |             self.sql.commit() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def easybake(self, string): |     def easybake(self, string): | ||||||
|  | @ -1229,7 +1306,6 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | ||||||
|             output_notes.append(note) |             output_notes.append(note) | ||||||
|         else: |         else: | ||||||
|             tag_parts = tag.split('.') |             tag_parts = tag.split('.') | ||||||
|             print('wtf') |  | ||||||
|             tags = [create_or_get(t) for t in tag_parts] |             tags = [create_or_get(t) for t in tag_parts] | ||||||
|             for (higher, lower) in zip(tags, tags[1:]): |             for (higher, lower) in zip(tags, tags[1:]): | ||||||
|                 try: |                 try: | ||||||
|  | @ -1367,6 +1443,7 @@ class GroupableMixin: | ||||||
|                 that_group = self.group_getter(id=fetch[SQL_TAGGROUP['parentid']]) |                 that_group = self.group_getter(id=fetch[SQL_TAGGROUP['parentid']]) | ||||||
|             raise GroupExists('%s already in group %s' % (member.name, that_group.name)) |             raise GroupExists('%s already in group %s' % (member.name, that_group.name)) | ||||||
| 
 | 
 | ||||||
|  |         self.photodb._cached_frozen_children = None | ||||||
|         self.photodb.cur.execute('INSERT INTO tag_group_rel VALUES(?, ?)', [self.id, member.id]) |         self.photodb.cur.execute('INSERT INTO tag_group_rel VALUES(?, ?)', [self.id, member.id]) | ||||||
|         if commit: |         if commit: | ||||||
|             log.debug('Commiting - add to group') |             log.debug('Commiting - add to group') | ||||||
|  | @ -1394,6 +1471,7 @@ class GroupableMixin: | ||||||
|             If True, all children will be deleted. |             If True, all children will be deleted. | ||||||
|             Otherwise they'll just be raised up one level. |             Otherwise they'll just be raised up one level. | ||||||
|         ''' |         ''' | ||||||
|  |         self.photodb._cached_frozen_children = None | ||||||
|         if delete_children: |         if delete_children: | ||||||
|             for child in self.children(): |             for child in self.children(): | ||||||
|                 child.delete(delete_children=delete_children, commit=False) |                 child.delete(delete_children=delete_children, commit=False) | ||||||
|  | @ -1412,6 +1490,7 @@ class GroupableMixin: | ||||||
|         # Note that this part comes after the deletion of children to prevent issues of recursion. |         # Note that this part comes after the deletion of children to prevent issues of recursion. | ||||||
|         self.photodb.cur.execute('DELETE FROM tag_group_rel WHERE memberid == ?', [self.id]) |         self.photodb.cur.execute('DELETE FROM tag_group_rel WHERE memberid == ?', [self.id]) | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - delete tag') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     def parent(self): |     def parent(self): | ||||||
|  | @ -1435,7 +1514,6 @@ class GroupableMixin: | ||||||
|         if not isinstance(group, type(self)): |         if not isinstance(group, type(self)): | ||||||
|             raise TypeError('Group must also be %s' % type(self)) |             raise TypeError('Group must also be %s' % type(self)) | ||||||
| 
 | 
 | ||||||
|         print('what') |  | ||||||
|         if self == group: |         if self == group: | ||||||
|             raise ValueError('Cant join self') |             raise ValueError('Cant join self') | ||||||
| 
 | 
 | ||||||
|  | @ -1446,8 +1524,10 @@ class GroupableMixin: | ||||||
|         ''' |         ''' | ||||||
|         Leave the current group and become independent. |         Leave the current group and become independent. | ||||||
|         ''' |         ''' | ||||||
|  |         self.photodb._cached_frozen_children = None | ||||||
|         self.photodb.cur.execute('DELETE FROM tag_group_rel WHERE memberid == ?', [self.id]) |         self.photodb.cur.execute('DELETE FROM tag_group_rel WHERE memberid == ?', [self.id]) | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - leave group') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     def walk_children(self): |     def walk_children(self): | ||||||
|  | @ -1478,6 +1558,7 @@ class Album(ObjectBase, GroupableMixin): | ||||||
|             return |             return | ||||||
|         self.photodb.cur.execute('INSERT INTO album_photo_rel VALUES(?, ?)', [self.id, photo.id]) |         self.photodb.cur.execute('INSERT INTO album_photo_rel VALUES(?, ?)', [self.id, photo.id]) | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - add photo to album') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     def add_tag_to_all(self, tag, nested_children=True): |     def add_tag_to_all(self, tag, nested_children=True): | ||||||
|  | @ -1495,6 +1576,7 @@ class Album(ObjectBase, GroupableMixin): | ||||||
|         self.photodb.cur.execute('DELETE FROM albums WHERE id == ?', [self.id]) |         self.photodb.cur.execute('DELETE FROM albums WHERE id == ?', [self.id]) | ||||||
|         self.photodb.cur.execute('DELETE FROM album_photo_rel WHERE albumid == ?', [self.id]) |         self.photodb.cur.execute('DELETE FROM album_photo_rel WHERE albumid == ?', [self.id]) | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - delete album') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     def edit(self, title=None, description=None, commit=True): |     def edit(self, title=None, description=None, commit=True): | ||||||
|  | @ -1509,6 +1591,7 @@ class Album(ObjectBase, GroupableMixin): | ||||||
|         self.title = title |         self.title = title | ||||||
|         self.description = description |         self.description = description | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - edit album') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     def has_photo(self, photo): |     def has_photo(self, photo): | ||||||
|  | @ -1568,6 +1651,7 @@ class Photo(ObjectBase): | ||||||
|         self.ratio = row_tuple[SQL_PHOTO['ratio']] |         self.ratio = row_tuple[SQL_PHOTO['ratio']] | ||||||
|         self.area = row_tuple[SQL_PHOTO['area']] |         self.area = row_tuple[SQL_PHOTO['area']] | ||||||
|         self.bytes = row_tuple[SQL_PHOTO['bytes']] |         self.bytes = row_tuple[SQL_PHOTO['bytes']] | ||||||
|  |         self.duration = row_tuple[SQL_PHOTO['duration']] | ||||||
|         self.created = row_tuple[SQL_PHOTO['created']] |         self.created = row_tuple[SQL_PHOTO['created']] | ||||||
|         self.thumbnail = row_tuple[SQL_PHOTO['thumbnail']] |         self.thumbnail = row_tuple[SQL_PHOTO['thumbnail']] | ||||||
|         self.basename = self.real_filepath.split(os.sep)[-1] |         self.basename = self.real_filepath.split(os.sep)[-1] | ||||||
|  | @ -1598,6 +1682,7 @@ class Photo(ObjectBase): | ||||||
|         log.debug('Applying tag {tag:s} to photo {pho:s}'.format(tag=tag, pho=self)) |         log.debug('Applying tag {tag:s} to photo {pho:s}'.format(tag=tag, pho=self)) | ||||||
|         self.photodb.cur.execute('INSERT INTO photo_tag_rel VALUES(?, ?)', [self.id, tag.id]) |         self.photodb.cur.execute('INSERT INTO photo_tag_rel VALUES(?, ?)', [self.id, tag.id]) | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - add photo tag') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     def albums(self): |     def albums(self): | ||||||
|  | @ -1625,6 +1710,7 @@ class Photo(ObjectBase): | ||||||
|         self.photodb.cur.execute('DELETE FROM photo_tag_rel WHERE photoid == ?', [self.id]) |         self.photodb.cur.execute('DELETE FROM photo_tag_rel WHERE photoid == ?', [self.id]) | ||||||
|         self.photodb.cur.execute('DELETE FROM album_photo_rel WHERE photoid == ?', [self.id]) |         self.photodb.cur.execute('DELETE FROM album_photo_rel WHERE photoid == ?', [self.id]) | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - delete photo') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     @time_me |     @time_me | ||||||
|  | @ -1659,7 +1745,7 @@ class Photo(ObjectBase): | ||||||
|                 image.save(hopeful_filepath, quality=50) |                 image.save(hopeful_filepath, quality=50) | ||||||
|                 return_filepath = hopeful_filepath |                 return_filepath = hopeful_filepath | ||||||
| 
 | 
 | ||||||
|         elif mime == 'video': |         elif mime == 'video' and ffmpeg: | ||||||
|             print('video') |             print('video') | ||||||
|             probe = ffmpeg.probe(self.real_filepath) |             probe = ffmpeg.probe(self.real_filepath) | ||||||
|             try: |             try: | ||||||
|  | @ -1691,6 +1777,7 @@ class Photo(ObjectBase): | ||||||
|             self.thumbnail = return_filepath |             self.thumbnail = return_filepath | ||||||
| 
 | 
 | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - generate thumbnail') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|         return self.thumbnail |         return self.thumbnail | ||||||
|  | @ -1729,7 +1816,7 @@ class Photo(ObjectBase): | ||||||
|         self.height = None |         self.height = None | ||||||
|         self.area = None |         self.area = None | ||||||
|         self.ratio = None |         self.ratio = None | ||||||
|         self.length = None |         self.duration = None | ||||||
| 
 | 
 | ||||||
|         mime = self.mimetype() |         mime = self.mimetype() | ||||||
|         if mime == 'image': |         if mime == 'image': | ||||||
|  | @ -1742,11 +1829,11 @@ class Photo(ObjectBase): | ||||||
|                 image.close() |                 image.close() | ||||||
|                 log.debug('Loaded image data for {photo:r}'.format(photo=self)) |                 log.debug('Loaded image data for {photo:r}'.format(photo=self)) | ||||||
| 
 | 
 | ||||||
|         elif mime == 'video': |         elif mime == 'video' and ffmpeg: | ||||||
|             try: |             try: | ||||||
|                 probe = ffmpeg.probe(self.real_filepath) |                 probe = ffmpeg.probe(self.real_filepath) | ||||||
|                 if probe and probe.video: |                 if probe and probe.video: | ||||||
|                     self.length = probe.video.duration |                     self.duration = probe.video.duration | ||||||
|                     self.width = probe.video.video_width |                     self.width = probe.video.video_width | ||||||
|                     self.height = probe.video.video_height |                     self.height = probe.video.video_height | ||||||
|             except: |             except: | ||||||
|  | @ -1756,7 +1843,7 @@ class Photo(ObjectBase): | ||||||
|             try: |             try: | ||||||
|                 probe = ffmpeg.probe(self.real_filepath) |                 probe = ffmpeg.probe(self.real_filepath) | ||||||
|                 if probe and probe.audio: |                 if probe and probe.audio: | ||||||
|                     self.length = probe.audio.duration |                     self.duration = probe.audio.duration | ||||||
|             except: |             except: | ||||||
|                 traceback.print_exc() |                 traceback.print_exc() | ||||||
| 
 | 
 | ||||||
|  | @ -1764,10 +1851,11 @@ class Photo(ObjectBase): | ||||||
|             self.area = self.width * self.height |             self.area = self.width * self.height | ||||||
|             self.ratio = round(self.width / self.height, 2) |             self.ratio = round(self.width / self.height, 2) | ||||||
| 
 | 
 | ||||||
|         self.photodb.cur.execute('UPDATE photos SET width=?, height=?, area=?, ratio=?, length=?, bytes=? WHERE id==?', |         self.photodb.cur.execute('UPDATE photos SET width=?, height=?, area=?, ratio=?, duration=?, bytes=? WHERE id==?', | ||||||
|             [self.width, self.height, self.area, self.ratio, self.length, self.bytes, self.id] |             [self.width, self.height, self.area, self.ratio, self.duration, self.bytes, self.id], | ||||||
|         ) |         ) | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - reload metadata') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     def remove_tag(self, tag, commit=True): |     def remove_tag(self, tag, commit=True): | ||||||
|  | @ -1781,6 +1869,7 @@ class Photo(ObjectBase): | ||||||
|                 [self.id, tag.id] |                 [self.id, tag.id] | ||||||
|             ) |             ) | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - remove photo tag') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     def tags(self): |     def tags(self): | ||||||
|  | @ -1809,12 +1898,13 @@ class Tag(ObjectBase, GroupableMixin): | ||||||
|         self.id = row_tuple[SQL_TAG['id']] |         self.id = row_tuple[SQL_TAG['id']] | ||||||
|         self.name = row_tuple[SQL_TAG['name']] |         self.name = row_tuple[SQL_TAG['name']] | ||||||
|         self.group_getter = self.photodb.get_tag |         self.group_getter = self.photodb.get_tag | ||||||
|  |         self._cached_qualified_name = None | ||||||
| 
 | 
 | ||||||
|     def __eq__(self, other): |     def __eq__(self, other): | ||||||
|         if isinstance(other, str): |         if isinstance(other, str): | ||||||
|             return self.name == other |             return self.name == other | ||||||
|         elif isinstance(other, Tag): |         elif isinstance(other, Tag): | ||||||
|             return self.id == other.id |             return self.id == other.id and self.name == other.name | ||||||
|         else: |         else: | ||||||
|             return False |             return False | ||||||
| 
 | 
 | ||||||
|  | @ -1842,9 +1932,11 @@ class Tag(ObjectBase, GroupableMixin): | ||||||
|         else: |         else: | ||||||
|             raise TagExists(synname) |             raise TagExists(synname) | ||||||
| 
 | 
 | ||||||
|  |         self.photodb._cached_frozen_children = None | ||||||
|         self.photodb.cur.execute('INSERT INTO tag_synonyms VALUES(?, ?)', [synname, self.name]) |         self.photodb.cur.execute('INSERT INTO tag_synonyms VALUES(?, ?)', [synname, self.name]) | ||||||
| 
 | 
 | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - add synonym') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     def convert_to_synonym(self, mastertag, commit=True): |     def convert_to_synonym(self, mastertag, commit=True): | ||||||
|  | @ -1860,6 +1952,7 @@ class Tag(ObjectBase, GroupableMixin): | ||||||
| 
 | 
 | ||||||
|         # Migrate the old tag's synonyms to the new one |         # Migrate the old tag's synonyms to the new one | ||||||
|         # UPDATE is safe for this operation because there is no chance of duplicates. |         # UPDATE is safe for this operation because there is no chance of duplicates. | ||||||
|  |         self.photodb._cached_frozen_children = None | ||||||
|         self.photodb.cur.execute( |         self.photodb.cur.execute( | ||||||
|             'UPDATE tag_synonyms SET mastername = ? WHERE mastername == ?', |             'UPDATE tag_synonyms SET mastername = ? WHERE mastername == ?', | ||||||
|             [mastertag.name, self.name] |             [mastertag.name, self.name] | ||||||
|  | @ -1880,24 +1973,30 @@ class Tag(ObjectBase, GroupableMixin): | ||||||
|         # Enjoy your new life as a monk. |         # Enjoy your new life as a monk. | ||||||
|         mastertag.add_synonym(self.name, commit=False) |         mastertag.add_synonym(self.name, commit=False) | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - convert to synonym') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     def delete(self, delete_children=False, commit=True): |     def delete(self, delete_children=False, commit=True): | ||||||
|         log.debug('Deleting tag {tag:r}'.format(tag=self)) |         log.debug('Deleting tag {tag:r}'.format(tag=self)) | ||||||
|  |         self.photodb._cached_frozen_children = None | ||||||
|         GroupableMixin.delete(self, delete_children=delete_children, commit=False) |         GroupableMixin.delete(self, delete_children=delete_children, commit=False) | ||||||
|         self.photodb.cur.execute('DELETE FROM tags WHERE id == ?', [self.id]) |         self.photodb.cur.execute('DELETE FROM tags WHERE id == ?', [self.id]) | ||||||
|         self.photodb.cur.execute('DELETE FROM photo_tag_rel WHERE tagid == ?', [self.id]) |         self.photodb.cur.execute('DELETE FROM photo_tag_rel WHERE tagid == ?', [self.id]) | ||||||
|         self.photodb.cur.execute('DELETE FROM tag_synonyms WHERE mastername == ?', [self.name]) |         self.photodb.cur.execute('DELETE FROM tag_synonyms WHERE mastername == ?', [self.name]) | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - delete tag') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     def qualified_name(self): |     def qualified_name(self): | ||||||
|         ''' |         ''' | ||||||
|         Return the 'group1.group2.tag' string for this tag. |         Return the 'group1.group2.tag' string for this tag. | ||||||
|         ''' |         ''' | ||||||
|  |         if self._cached_qualified_name: | ||||||
|  |             return self._cached_qualified_name | ||||||
|         string = self.name |         string = self.name | ||||||
|         for parent in self.walk_parents(): |         for parent in self.walk_parents(): | ||||||
|             string = parent.name + '.' + string |             string = parent.name + '.' + string | ||||||
|  |         self._cached_qualified_name = string | ||||||
|         return string |         return string | ||||||
| 
 | 
 | ||||||
|     def remove_synonym(self, synname, commit=True): |     def remove_synonym(self, synname, commit=True): | ||||||
|  | @ -1912,8 +2011,10 @@ class Tag(ObjectBase, GroupableMixin): | ||||||
|         if fetch is None: |         if fetch is None: | ||||||
|             raise NoSuchSynonym(synname) |             raise NoSuchSynonym(synname) | ||||||
| 
 | 
 | ||||||
|  |         self.photodb._cached_frozen_children = None | ||||||
|         self.photodb.cur.execute('DELETE FROM tag_synonyms WHERE name == ?', [synname]) |         self.photodb.cur.execute('DELETE FROM tag_synonyms WHERE name == ?', [synname]) | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - remove synonym') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     def rename(self, new_name, apply_to_synonyms=True, commit=True): |     def rename(self, new_name, apply_to_synonyms=True, commit=True): | ||||||
|  | @ -1931,6 +2032,8 @@ class Tag(ObjectBase, GroupableMixin): | ||||||
|         else: |         else: | ||||||
|             raise TagExists(new_name) |             raise TagExists(new_name) | ||||||
| 
 | 
 | ||||||
|  |         self._cached_qualified_name = None | ||||||
|  |         self.photodb._cached_frozen_children = None | ||||||
|         self.photodb.cur.execute('UPDATE tags SET name = ? WHERE id == ?', [new_name, self.id]) |         self.photodb.cur.execute('UPDATE tags SET name = ? WHERE id == ?', [new_name, self.id]) | ||||||
|         if apply_to_synonyms: |         if apply_to_synonyms: | ||||||
|             self.photodb.cur.execute( |             self.photodb.cur.execute( | ||||||
|  | @ -1940,6 +2043,7 @@ class Tag(ObjectBase, GroupableMixin): | ||||||
| 
 | 
 | ||||||
|         self.name = new_name |         self.name = new_name | ||||||
|         if commit: |         if commit: | ||||||
|  |             log.debug('Committing - rename tag') | ||||||
|             self.photodb.sql.commit() |             self.photodb.sql.commit() | ||||||
| 
 | 
 | ||||||
|     def synonyms(self): |     def synonyms(self): | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								etiquette/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,3 @@ | ||||||
|  | flask | ||||||
|  | gevent | ||||||
|  | pillow | ||||||
|  | @ -1,6 +1,4 @@ | ||||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> |  | ||||||
| 
 |  | ||||||
| <svg | <svg | ||||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" |    xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||||
|    xmlns:cc="http://creativecommons.org/ns#" |    xmlns:cc="http://creativecommons.org/ns#" | ||||||
|  | @ -10,20 +8,36 @@ | ||||||
|    xmlns:xlink="http://www.w3.org/1999/xlink" |    xmlns:xlink="http://www.w3.org/1999/xlink" | ||||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||||
|    width="400" |    width="640" | ||||||
|    height="400" |    height="640" | ||||||
|    viewBox="0 0 400.00002 400" |    viewBox="0 0 640.00003 640" | ||||||
|    id="svg2" |    id="svg2" | ||||||
|    version="1.1" |    version="1.1" | ||||||
|    inkscape:version="0.91 r13725" |    inkscape:version="0.91 r13725" | ||||||
|    sodipodi:docname="audio.svg" |    sodipodi:docname="audio.svg"> | ||||||
|    inkscape:export-filename="C:\Git\else\Etiquette\static\basic_thumbnails\audio.png" |   <sodipodi:namedview | ||||||
|    inkscape:export-xdpi="144" |      pagecolor="#ffffff" | ||||||
|    inkscape:export-ydpi="144"> |      bordercolor="#666666" | ||||||
|  |      borderopacity="1" | ||||||
|  |      objecttolerance="10" | ||||||
|  |      gridtolerance="10" | ||||||
|  |      guidetolerance="10" | ||||||
|  |      inkscape:pageopacity="0" | ||||||
|  |      inkscape:pageshadow="2" | ||||||
|  |      inkscape:window-width="686" | ||||||
|  |      inkscape:window-height="480" | ||||||
|  |      id="namedview19" | ||||||
|  |      showgrid="false" | ||||||
|  |      inkscape:zoom="0.36875" | ||||||
|  |      inkscape:cx="320" | ||||||
|  |      inkscape:cy="320" | ||||||
|  |      inkscape:window-x="0" | ||||||
|  |      inkscape:window-y="0" | ||||||
|  |      inkscape:window-maximized="0" | ||||||
|  |      inkscape:current-layer="svg2" /> | ||||||
|   <defs |   <defs | ||||||
|      id="defs4"> |      id="defs4"> | ||||||
|     <linearGradient |     <linearGradient | ||||||
|        inkscape:collect="always" |  | ||||||
|        id="linearGradient4723"> |        id="linearGradient4723"> | ||||||
|       <stop |       <stop | ||||||
|          style="stop-color:#157a05;stop-opacity:1" |          style="stop-color:#157a05;stop-opacity:1" | ||||||
|  | @ -38,13 +52,11 @@ | ||||||
|        id="clipPath5600" |        id="clipPath5600" | ||||||
|        clipPathUnits="userSpaceOnUse"> |        clipPathUnits="userSpaceOnUse"> | ||||||
|       <path |       <path | ||||||
|          inkscape:connector-curvature="0" |  | ||||||
|          id="path5602" |          id="path5602" | ||||||
|          d="m 416.04743,209.57467 a 210.71427,210.71427 0 0 0 -201.63871,156.16785 210.71427,210.71427 0 0 0 148.9961,258.07232 210.71427,210.71427 0 0 0 258.07031,-148.99795 210.71427,210.71427 0 0 0 -148.9961,-258.07035 210.71427,210.71427 0 0 0 -56.4316,-7.17187 z m 0.088,10 a 200.71428,200.71428 0 0 1 53.75188,6.83 A 200.71428,200.71428 0 0 1 611.81495,472.22889 200.71428,200.71428 0 0 1 365.99079,614.15457 200.71428,200.71428 0 0 1 224.0669,368.3325 200.71428,200.71428 0 0 1 416.13517,219.57467 Z m 0.67771,10.66015 c -28.9322,-0.0158 -57.57612,6.77648 -84.07229,20.47847 -5.58051,2.89034 -7.71121,5.24233 -5.71092,8.93562 l 84.62499,146.57421 c 3.9199,6.67208 6.92071,6.5608 8.37693,-0.1625 L 463.9225,242.25987 c 1.2404,-4.75141 0.77432,-5.32621 -7.65239,-7.81065 -13.09518,-2.80222 -26.30598,-4.21013 -39.45697,-4.21472 z m 55.19328,8.75394 c -2.49601,-0.0656 -4.01546,0.93883 -4.7636,3.45495 l -43.80467,163.4825 c -1.94613,7.48962 0.25549,9.53469 6.03899,5.81055 l 146.85941,-84.78907 c 4.23688,-2.48265 4.31319,-3.22025 0.11141,-10.93556 -23.2904,-35.97209 -57.50302,-62.6885 -98.8477,-75.84771 -2.24622,-0.71332 -4.09611,-1.13676 -5.59382,-1.17566 z m -150.93155,19.66206 c -1.45873,-0.0237 -3.57203,1.00841 -7.42973,3.10929 -35.97206,23.29037 -62.69049,57.50296 -75.8496,98.84772 -1.90228,5.98981 -1.74657,9.15834 2.2793,10.35536 l 163.48439,43.80474 c 7.48959,1.94628 9.5327,-0.25337 5.8086,-6.03706 l -84.78918,-146.8593 c -1.2413,-2.11844 -2.04519,-3.19729 -3.50392,-3.22075 z m 260.03112,70.01171 c -0.7849,-0.0158 -1.62359,0.23625 -2.54681,0.73629 l -146.57616,84.62511 c -6.67213,3.91977 -6.56093,6.92076 0.16224,8.37677 l 163.8008,43.89074 c 4.75141,1.24035 5.32608,0.77437 7.8105,-7.65244 8.96741,-41.90478 3.66661,-84.98932 -16.2636,-123.52924 -2.1678,-4.18535 -4.0319,-6.43032 -6.38678,-6.44723 z m -344.04481,45.09563 c -2.28245,0 -3.07026,1.84403 -4.93359,8.16404 -8.96736,41.90477 -3.66848,84.9895 16.2617,123.52942 2.89032,5.58046 5.2424,7.71121 8.93556,5.71094 l 146.57419,-84.62495 c 6.67208,-3.91978 6.5628,-6.92092 -0.16013,-8.37709 L 239.93603,374.26994 c -1.18786,-0.31005 -2.1142,-0.51275 -2.87501,-0.51178 z m 190.67183,51.3438 c -3.55996,0.069 -4.01188,2.39073 -1.21878,6.72855 l 84.79113,146.86128 c 2.48259,4.23704 3.2182,4.31319 10.93359,0.11167 35.97198,-23.29037 62.6885,-57.50509 75.8476,-98.8497 1.90228,-5.98979 1.7466,-9.1585 -2.27929,-10.35552 L 432.3247,425.79345 c -1.87239,-0.4865 -3.4051,-0.7143 -4.5918,-0.69133 z m -16.97067,1.91211 c -1.09041,0.0477 -2.54811,0.64684 -4.35552,1.81055 l -146.8593,84.79104 c -4.23692,2.48265 -4.31319,3.21828 -0.11142,10.93359 23.2903,35.97209 57.50296,62.6885 98.84765,75.84755 5.98981,1.90226 9.15839,1.74668 10.35542,-2.27929 l 43.80668,-163.48234 c 1.33799,-5.14927 0.7152,-7.72466 -1.68359,-7.6211 z m 8.35348,2.38285 c -1.42229,0.0345 -2.53748,1.74194 -3.26558,5.1035 L 371.9615,598.30117 c -1.2405,4.75143 -0.7744,5.32621 7.65242,7.8105 41.90477,8.96746 84.98929,3.66653 123.52929,-16.26358 5.58051,-2.89034 7.7112,-5.24251 5.71091,-8.93562 l -84.625,-146.57421 c -1.95992,-3.33596 -3.69103,-4.97547 -5.11333,-4.94134 z" |          d="m 416.04743,209.57467 a 210.71427,210.71427 0 0 0 -201.63871,156.16785 210.71427,210.71427 0 0 0 148.9961,258.07232 210.71427,210.71427 0 0 0 258.07031,-148.99795 210.71427,210.71427 0 0 0 -148.9961,-258.07035 210.71427,210.71427 0 0 0 -56.4316,-7.17187 z m 0.088,10 a 200.71428,200.71428 0 0 1 53.75188,6.83 A 200.71428,200.71428 0 0 1 611.81495,472.22889 200.71428,200.71428 0 0 1 365.99079,614.15457 200.71428,200.71428 0 0 1 224.0669,368.3325 200.71428,200.71428 0 0 1 416.13517,219.57467 Z m 0.67771,10.66015 c -28.9322,-0.0158 -57.57612,6.77648 -84.07229,20.47847 -5.58051,2.89034 -7.71121,5.24233 -5.71092,8.93562 l 84.62499,146.57421 c 3.9199,6.67208 6.92071,6.5608 8.37693,-0.1625 L 463.9225,242.25987 c 1.2404,-4.75141 0.77432,-5.32621 -7.65239,-7.81065 -13.09518,-2.80222 -26.30598,-4.21013 -39.45697,-4.21472 z m 55.19328,8.75394 c -2.49601,-0.0656 -4.01546,0.93883 -4.7636,3.45495 l -43.80467,163.4825 c -1.94613,7.48962 0.25549,9.53469 6.03899,5.81055 l 146.85941,-84.78907 c 4.23688,-2.48265 4.31319,-3.22025 0.11141,-10.93556 -23.2904,-35.97209 -57.50302,-62.6885 -98.8477,-75.84771 -2.24622,-0.71332 -4.09611,-1.13676 -5.59382,-1.17566 z m -150.93155,19.66206 c -1.45873,-0.0237 -3.57203,1.00841 -7.42973,3.10929 -35.97206,23.29037 -62.69049,57.50296 -75.8496,98.84772 -1.90228,5.98981 -1.74657,9.15834 2.2793,10.35536 l 163.48439,43.80474 c 7.48959,1.94628 9.5327,-0.25337 5.8086,-6.03706 l -84.78918,-146.8593 c -1.2413,-2.11844 -2.04519,-3.19729 -3.50392,-3.22075 z m 260.03112,70.01171 c -0.7849,-0.0158 -1.62359,0.23625 -2.54681,0.73629 l -146.57616,84.62511 c -6.67213,3.91977 -6.56093,6.92076 0.16224,8.37677 l 163.8008,43.89074 c 4.75141,1.24035 5.32608,0.77437 7.8105,-7.65244 8.96741,-41.90478 3.66661,-84.98932 -16.2636,-123.52924 -2.1678,-4.18535 -4.0319,-6.43032 -6.38678,-6.44723 z m -344.04481,45.09563 c -2.28245,0 -3.07026,1.84403 -4.93359,8.16404 -8.96736,41.90477 -3.66848,84.9895 16.2617,123.52942 2.89032,5.58046 5.2424,7.71121 8.93556,5.71094 l 146.57419,-84.62495 c 6.67208,-3.91978 6.5628,-6.92092 -0.16013,-8.37709 L 239.93603,374.26994 c -1.18786,-0.31005 -2.1142,-0.51275 -2.87501,-0.51178 z m 190.67183,51.3438 c -3.55996,0.069 -4.01188,2.39073 -1.21878,6.72855 l 84.79113,146.86128 c 2.48259,4.23704 3.2182,4.31319 10.93359,0.11167 35.97198,-23.29037 62.6885,-57.50509 75.8476,-98.8497 1.90228,-5.98979 1.7466,-9.1585 -2.27929,-10.35552 L 432.3247,425.79345 c -1.87239,-0.4865 -3.4051,-0.7143 -4.5918,-0.69133 z m -16.97067,1.91211 c -1.09041,0.0477 -2.54811,0.64684 -4.35552,1.81055 l -146.8593,84.79104 c -4.23692,2.48265 -4.31319,3.21828 -0.11142,10.93359 23.2903,35.97209 57.50296,62.6885 98.84765,75.84755 5.98981,1.90226 9.15839,1.74668 10.35542,-2.27929 l 43.80668,-163.48234 c 1.33799,-5.14927 0.7152,-7.72466 -1.68359,-7.6211 z m 8.35348,2.38285 c -1.42229,0.0345 -2.53748,1.74194 -3.26558,5.1035 L 371.9615,598.30117 c -1.2405,4.75143 -0.7744,5.32621 7.65242,7.8105 41.90477,8.96746 84.98929,3.66653 123.52929,-16.26358 5.58051,-2.89034 7.7112,-5.24251 5.71091,-8.93562 l -84.625,-146.57421 c -1.95992,-3.33596 -3.69103,-4.97547 -5.11333,-4.94134 z" | ||||||
|          style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> |          style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|     </clipPath> |     </clipPath> | ||||||
|     <linearGradient |     <linearGradient | ||||||
|        inkscape:collect="always" |  | ||||||
|        xlink:href="#linearGradient4723" |        xlink:href="#linearGradient4723" | ||||||
|        id="linearGradient4739" |        id="linearGradient4739" | ||||||
|        x1="196.13852" |        x1="196.13852" | ||||||
|  | @ -54,26 +66,6 @@ | ||||||
|        gradientUnits="userSpaceOnUse" |        gradientUnits="userSpaceOnUse" | ||||||
|        gradientTransform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,-530.7894,361.49036)" /> |        gradientTransform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,-530.7894,361.49036)" /> | ||||||
|   </defs> |   </defs> | ||||||
|   <sodipodi:namedview |  | ||||||
|      id="base" |  | ||||||
|      pagecolor="#ffffff" |  | ||||||
|      bordercolor="#666666" |  | ||||||
|      borderopacity="1.0" |  | ||||||
|      inkscape:pageopacity="0.0" |  | ||||||
|      inkscape:pageshadow="2" |  | ||||||
|      inkscape:zoom="0.67124999" |  | ||||||
|      inkscape:cx="199.31367" |  | ||||||
|      inkscape:cy="266.33033" |  | ||||||
|      inkscape:document-units="px" |  | ||||||
|      inkscape:current-layer="layer1" |  | ||||||
|      showgrid="false" |  | ||||||
|      inkscape:window-width="1366" |  | ||||||
|      inkscape:window-height="706" |  | ||||||
|      inkscape:window-x="1432" |  | ||||||
|      inkscape:window-y="204" |  | ||||||
|      inkscape:window-maximized="1" |  | ||||||
|      units="px" |  | ||||||
|      showguides="false" /> |  | ||||||
|   <metadata |   <metadata | ||||||
|      id="metadata7"> |      id="metadata7"> | ||||||
|     <rdf:RDF> |     <rdf:RDF> | ||||||
|  | @ -87,41 +79,33 @@ | ||||||
|     </rdf:RDF> |     </rdf:RDF> | ||||||
|   </metadata> |   </metadata> | ||||||
|   <g |   <g | ||||||
|      inkscape:label="Layer 1" |  | ||||||
|      inkscape:groupmode="layer" |  | ||||||
|      id="layer1" |      id="layer1" | ||||||
|      transform="translate(0,-652.36221)"> |      transform="matrix(1.5999999,0,0,1.5999999,-4.414061e-6,-1043.7794)"> | ||||||
|     <path |     <path | ||||||
|        cx="356.42856" |        cx="356.42856" | ||||||
|        cy="515.93359" |        cy="515.93359" | ||||||
|        r="190.61275" |        r="190.61275" | ||||||
|        id="path4146" |        id="path4146" | ||||||
|        style="opacity:1;fill:#f2dc39;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" |        style="opacity:1;fill:#f2dc39;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | ||||||
|        d="" |        d="" /> | ||||||
|        inkscape:connector-curvature="0" /> |  | ||||||
|     <path |     <path | ||||||
|        cx="356.42856" |        cx="356.42856" | ||||||
|        cy="515.93359" |        cy="515.93359" | ||||||
|        r="190.61275" |        r="190.61275" | ||||||
|        id="path4148" |        id="path4148" | ||||||
|        style="opacity:1;fill:#f2dc39;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" |        style="opacity:1;fill:#f2dc39;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | ||||||
|        d="" |        d="" /> | ||||||
|        inkscape:connector-curvature="0" /> |  | ||||||
|     <g |     <g | ||||||
|        id="g4759" |        id="g4759" | ||||||
|        transform="matrix(2.0529613,0,0,2.0529613,-208.02761,-862.66247)"> |        transform="matrix(2.0529613,0,0,2.0529613,-208.02761,-862.66247)"> | ||||||
|       <path |       <path | ||||||
|          id="path4176" |          id="path4176" | ||||||
|          d="m 160.49771,775.78875 c -17.18761,15.61221 -30.85175,51.16826 -26.54481,80.55638 -5.68961,15.27438 -2.23888,26.77908 5.86857,35.70464 8.41129,9.26005 20.42276,13.46722 36.74655,8.76774 28.38226,5.82869 63.31581,-4.19178 79.95892,-19.3094 z" |          d="m 160.49771,775.78875 c -17.18761,15.61221 -30.85175,51.16826 -26.54481,80.55638 -5.68961,15.27438 -2.23888,26.77908 5.86857,35.70464 8.41129,9.26005 20.42276,13.46722 36.74655,8.76774 28.38226,5.82869 63.31581,-4.19178 79.95892,-19.3094 z" | ||||||
|          style="opacity:1;fill:url(#linearGradient4739);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" |          style="opacity:1;fill:url(#linearGradient4739);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|          inkscape:connector-curvature="0" |  | ||||||
|          sodipodi:nodetypes="ccsccc" /> |  | ||||||
|       <path |       <path | ||||||
|          style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" |          style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | ||||||
|          d="m 187.71478,901.36194 c -15.6087,-1.19621 -30.19953,-6.46109 -40.11858,-17.38106 -3.59314,-3.95572 -6.38887,-8.45054 -8.4789,-13.31879" |          d="m 187.71478,901.36194 c -15.6087,-1.19621 -30.19953,-6.46109 -40.11858,-17.38106 -3.59314,-3.95572 -6.38887,-8.45054 -8.4789,-13.31879" | ||||||
|          id="path4179" |          id="path4179" /> | ||||||
|          inkscape:connector-curvature="0" |  | ||||||
|          sodipodi:nodetypes="csc" /> |  | ||||||
|       <ellipse |       <ellipse | ||||||
|          transform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,0,0)" |          transform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,0,0)" | ||||||
|          ry="71.356659" |          ry="71.356659" | ||||||
|  | @ -131,8 +115,6 @@ | ||||||
|          id="path4189" |          id="path4189" | ||||||
|          style="opacity:1;fill:#bdff7f;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.10868788;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> |          style="opacity:1;fill:#bdff7f;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.10868788;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|       <path |       <path | ||||||
|          sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccc" |  | ||||||
|          inkscape:connector-curvature="0" |  | ||||||
|          id="circle5376" |          id="circle5376" | ||||||
|          d="m 167.97402,779.73201 c 12.95394,14.14301 25.67999,28.49473 38.76045,42.52164 1.09188,1.09288 1.57468,2.03532 1.12908,0.24144 -3.81326,-15.37536 -7.34553,-30.80883 -11.73933,-46.02909 -9.33962,-3.59468 -21.01947,-4.53564 -29.35443,1.94027 z m -3.40652,1.08631 c -6.35037,7.84741 -6.53271,18.97849 -3.33194,28.11951 14.61665,5.15311 29.16348,10.48467 43.81316,15.53604 2.17968,0.89623 1.61044,0.35664 0.43632,-0.95963 -13.16593,-14.74917 -26.57361,-29.28008 -39.82726,-43.95011 l -1.09034,1.25425 z m 34.52343,-0.80156 10.96907,43.65806 c 0.3333,2.09058 0.60304,1.61585 1.77113,0.51394 7.58595,-7.11871 15.71168,-13.70923 22.85068,-21.25393 -10.11646,-11.06159 -22.40233,-20.30395 -36.34759,-25.93267 z m -36.91782,33.66852 c 4.32187,13.48593 12.1718,25.57007 21.57681,36.03286 l 22.88728,-20.78947 c 1.94371,-1.55853 0.90464,-1.6299 -0.63476,-2.19321 L 161.23898,810.7949 Z m 52.1086,14.12822 44.66065,15.90394 c -4.32392,-14.54689 -12.41384,-27.7685 -22.56898,-38.9523 l -23.01225,20.90297 c -1.67223,1.35326 -1.17245,1.5415 0.92058,2.14539 z m -28.6948,23.78756 c 9.97733,11.13211 22.30826,20.25112 36.19297,25.86994 l -11.81394,-47.01872 c -0.24809,-1.18978 -0.36986,-1.35104 -1.49167,-0.33205 l -23.22275,21.09417 0.32766,0.37768 0.008,0.009 z m 27.58834,-15.54751 c 3.45815,13.76858 6.91919,27.53657 10.37846,41.30493 9.14559,4.6969 21.26842,5.13014 29.7965,-1.27057 -13.33458,-14.60073 -26.5137,-29.34346 -39.93482,-43.86507 -0.84001,-1.17611 -1.91288,-2.39494 -1.22768,-0.34375 z m 3.75391,-2.44675 37.61326,41.40874 c 7.1511,-7.87583 7.83178,-19.8138 4.31773,-29.47142 -14.6668,-5.17311 -29.26398,-10.5241 -43.96637,-15.58822 -2.34003,-1.03127 -1.12256,0.0763 -0.0843,1.25149 0.7066,0.79977 1.41314,1.59962 2.11974,2.39938 z" |          d="m 167.97402,779.73201 c 12.95394,14.14301 25.67999,28.49473 38.76045,42.52164 1.09188,1.09288 1.57468,2.03532 1.12908,0.24144 -3.81326,-15.37536 -7.34553,-30.80883 -11.73933,-46.02909 -9.33962,-3.59468 -21.01947,-4.53564 -29.35443,1.94027 z m -3.40652,1.08631 c -6.35037,7.84741 -6.53271,18.97849 -3.33194,28.11951 14.61665,5.15311 29.16348,10.48467 43.81316,15.53604 2.17968,0.89623 1.61044,0.35664 0.43632,-0.95963 -13.16593,-14.74917 -26.57361,-29.28008 -39.82726,-43.95011 l -1.09034,1.25425 z m 34.52343,-0.80156 10.96907,43.65806 c 0.3333,2.09058 0.60304,1.61585 1.77113,0.51394 7.58595,-7.11871 15.71168,-13.70923 22.85068,-21.25393 -10.11646,-11.06159 -22.40233,-20.30395 -36.34759,-25.93267 z m -36.91782,33.66852 c 4.32187,13.48593 12.1718,25.57007 21.57681,36.03286 l 22.88728,-20.78947 c 1.94371,-1.55853 0.90464,-1.6299 -0.63476,-2.19321 L 161.23898,810.7949 Z m 52.1086,14.12822 44.66065,15.90394 c -4.32392,-14.54689 -12.41384,-27.7685 -22.56898,-38.9523 l -23.01225,20.90297 c -1.67223,1.35326 -1.17245,1.5415 0.92058,2.14539 z m -28.6948,23.78756 c 9.97733,11.13211 22.30826,20.25112 36.19297,25.86994 l -11.81394,-47.01872 c -0.24809,-1.18978 -0.36986,-1.35104 -1.49167,-0.33205 l -23.22275,21.09417 0.32766,0.37768 0.008,0.009 z m 27.58834,-15.54751 c 3.45815,13.76858 6.91919,27.53657 10.37846,41.30493 9.14559,4.6969 21.26842,5.13014 29.7965,-1.27057 -13.33458,-14.60073 -26.5137,-29.34346 -39.93482,-43.86507 -0.84001,-1.17611 -1.91288,-2.39494 -1.22768,-0.34375 z m 3.75391,-2.44675 37.61326,41.40874 c 7.1511,-7.87583 7.83178,-19.8138 4.31773,-29.47142 -14.6668,-5.17311 -29.26398,-10.5241 -43.96637,-15.58822 -2.34003,-1.03127 -1.12256,0.0763 -0.0843,1.25149 0.7066,0.79977 1.41314,1.59962 2.11974,2.39938 z" | ||||||
|          style="opacity:1;fill:#6bd625;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> |          style="opacity:1;fill:#6bd625;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|  |  | ||||||
| Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.4 KiB | 
|  | @ -1,6 +1,4 @@ | ||||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> |  | ||||||
| 
 |  | ||||||
| <svg | <svg | ||||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" |    xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||||
|    xmlns:cc="http://creativecommons.org/ns#" |    xmlns:cc="http://creativecommons.org/ns#" | ||||||
|  | @ -8,22 +6,14 @@ | ||||||
|    xmlns:svg="http://www.w3.org/2000/svg" |    xmlns:svg="http://www.w3.org/2000/svg" | ||||||
|    xmlns="http://www.w3.org/2000/svg" |    xmlns="http://www.w3.org/2000/svg" | ||||||
|    xmlns:xlink="http://www.w3.org/1999/xlink" |    xmlns:xlink="http://www.w3.org/1999/xlink" | ||||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |    width="640" | ||||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |    height="640" | ||||||
|    width="400" |    viewBox="0 0 640.00003 640" | ||||||
|    height="400" |  | ||||||
|    viewBox="0 0 400.00002 400" |  | ||||||
|    id="svg2" |    id="svg2" | ||||||
|    version="1.1" |    version="1.1"> | ||||||
|    inkscape:version="0.91 r13725" |  | ||||||
|    sodipodi:docname="other.svg" |  | ||||||
|    inkscape:export-filename="C:\Git\else\Etiquette\static\basic_thumbnails\other.png" |  | ||||||
|    inkscape:export-xdpi="185.87027" |  | ||||||
|    inkscape:export-ydpi="185.87027"> |  | ||||||
|   <defs |   <defs | ||||||
|      id="defs4"> |      id="defs4"> | ||||||
|     <linearGradient |     <linearGradient | ||||||
|        inkscape:collect="always" |  | ||||||
|        id="linearGradient4361"> |        id="linearGradient4361"> | ||||||
|       <stop |       <stop | ||||||
|          style="stop-color:#4d4d4d;stop-opacity:1" |          style="stop-color:#4d4d4d;stop-opacity:1" | ||||||
|  | @ -35,7 +25,6 @@ | ||||||
|          id="stop4365" /> |          id="stop4365" /> | ||||||
|     </linearGradient> |     </linearGradient> | ||||||
|     <linearGradient |     <linearGradient | ||||||
|        inkscape:collect="always" |  | ||||||
|        xlink:href="#linearGradient4361" |        xlink:href="#linearGradient4361" | ||||||
|        id="linearGradient4367" |        id="linearGradient4367" | ||||||
|        x1="315.47162" |        x1="315.47162" | ||||||
|  | @ -45,25 +34,6 @@ | ||||||
|        gradientUnits="userSpaceOnUse" |        gradientUnits="userSpaceOnUse" | ||||||
|        gradientTransform="matrix(0.8225722,0,0,1.3309496,35.485561,-282.08894)" /> |        gradientTransform="matrix(0.8225722,0,0,1.3309496,35.485561,-282.08894)" /> | ||||||
|   </defs> |   </defs> | ||||||
|   <sodipodi:namedview |  | ||||||
|      id="base" |  | ||||||
|      pagecolor="#ffffff" |  | ||||||
|      bordercolor="#666666" |  | ||||||
|      borderopacity="1.0" |  | ||||||
|      inkscape:pageopacity="0.0" |  | ||||||
|      inkscape:pageshadow="2" |  | ||||||
|      inkscape:zoom="0.94929084" |  | ||||||
|      inkscape:cx="51.384844" |  | ||||||
|      inkscape:cy="169.83481" |  | ||||||
|      inkscape:document-units="px" |  | ||||||
|      inkscape:current-layer="layer1" |  | ||||||
|      showgrid="false" |  | ||||||
|      inkscape:window-width="1366" |  | ||||||
|      inkscape:window-height="706" |  | ||||||
|      inkscape:window-x="1432" |  | ||||||
|      inkscape:window-y="204" |  | ||||||
|      inkscape:window-maximized="1" |  | ||||||
|      units="px" /> |  | ||||||
|   <metadata |   <metadata | ||||||
|      id="metadata7"> |      id="metadata7"> | ||||||
|     <rdf:RDF> |     <rdf:RDF> | ||||||
|  | @ -77,10 +47,8 @@ | ||||||
|     </rdf:RDF> |     </rdf:RDF> | ||||||
|   </metadata> |   </metadata> | ||||||
|   <g |   <g | ||||||
|      inkscape:label="Layer 1" |  | ||||||
|      inkscape:groupmode="layer" |  | ||||||
|      id="layer1" |      id="layer1" | ||||||
|      transform="translate(0,-652.36221)"> |      transform="matrix(1.6,0,0,1.6,-2.4414063e-5,-1043.7795)"> | ||||||
|     <rect |     <rect | ||||||
|        style="opacity:1;fill:url(#linearGradient4367);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.18531179;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" |        style="opacity:1;fill:url(#linearGradient4367);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.18531179;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | ||||||
|        id="rect4359" |        id="rect4359" | ||||||
|  |  | ||||||
| Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								etiquette/static/basic_thumbnails/txt.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.3 KiB | 
							
								
								
									
										208
									
								
								etiquette/static/basic_thumbnails/txt.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,208 @@ | ||||||
|  | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||||
|  | <svg | ||||||
|  |    xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||||
|  |    xmlns:cc="http://creativecommons.org/ns#" | ||||||
|  |    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||||||
|  |    xmlns:svg="http://www.w3.org/2000/svg" | ||||||
|  |    xmlns="http://www.w3.org/2000/svg" | ||||||
|  |    xmlns:xlink="http://www.w3.org/1999/xlink" | ||||||
|  |    width="640" | ||||||
|  |    height="640" | ||||||
|  |    viewBox="0 0 640.00003 640" | ||||||
|  |    id="svg2" | ||||||
|  |    version="1.1"> | ||||||
|  |   <defs | ||||||
|  |      id="defs4"> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4160"> | ||||||
|  |       <stop | ||||||
|  |          style="stop-color:#414141;stop-opacity:1" | ||||||
|  |          offset="0" | ||||||
|  |          id="stop4195" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4197" | ||||||
|  |          offset="0.44465271" | ||||||
|  |          style="stop-color:#a5a5a5;stop-opacity:1" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4199" | ||||||
|  |          offset="0.51481694" | ||||||
|  |          style="stop-color:#cccccc;stop-opacity:1" /> | ||||||
|  |       <stop | ||||||
|  |          style="stop-color:#ffffff;stop-opacity:1" | ||||||
|  |          offset="1" | ||||||
|  |          id="stop4201" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4203"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4179" | ||||||
|  |          offset="0" | ||||||
|  |          style="stop-color:#414141;stop-opacity:1" /> | ||||||
|  |       <stop | ||||||
|  |          style="stop-color:#a5a5a5;stop-opacity:1" | ||||||
|  |          offset="0.44465271" | ||||||
|  |          id="stop4181" /> | ||||||
|  |       <stop | ||||||
|  |          style="stop-color:#cccccc;stop-opacity:1" | ||||||
|  |          offset="0.51481694" | ||||||
|  |          id="stop4183" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4185" | ||||||
|  |          offset="1" | ||||||
|  |          style="stop-color:#ffffff;stop-opacity:1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4187"> | ||||||
|  |       <stop | ||||||
|  |          style="stop-color:#f5f5f5;stop-opacity:1" | ||||||
|  |          offset="0" | ||||||
|  |          id="stop4162" /> | ||||||
|  |       <stop | ||||||
|  |          style="stop-color:#ffffff;stop-opacity:1" | ||||||
|  |          offset="1" | ||||||
|  |          id="stop4164" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        xlink:href="#linearGradient4160" | ||||||
|  |        id="linearGradient4166" | ||||||
|  |        x1="218.28458" | ||||||
|  |        y1="950.68616" | ||||||
|  |        x2="213.38019" | ||||||
|  |        y2="742.12012" | ||||||
|  |        gradientUnits="userSpaceOnUse" /> | ||||||
|  |     <linearGradient | ||||||
|  |        xlink:href="#linearGradient4187" | ||||||
|  |        id="linearGradient4191" | ||||||
|  |        x1="185.59323" | ||||||
|  |        y1="394.32205" | ||||||
|  |        x2="185.59323" | ||||||
|  |        y2="123.97182" | ||||||
|  |        gradientUnits="userSpaceOnUse" /> | ||||||
|  |   </defs> | ||||||
|  |   <metadata | ||||||
|  |      id="metadata7"> | ||||||
|  |     <rdf:RDF> | ||||||
|  |       <cc:Work | ||||||
|  |          rdf:about=""> | ||||||
|  |         <dc:format>image/svg+xml</dc:format> | ||||||
|  |         <dc:type | ||||||
|  |            rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | ||||||
|  |         <dc:title></dc:title> | ||||||
|  |       </cc:Work> | ||||||
|  |     </rdf:RDF> | ||||||
|  |   </metadata> | ||||||
|  |   <g | ||||||
|  |      transform="matrix(1.6,0,0,1.6,151.99998,-84.47952)" | ||||||
|  |      id="g4352"> | ||||||
|  |     <g | ||||||
|  |        id="g4324"> | ||||||
|  |       <g | ||||||
|  |          id="g4280"> | ||||||
|  |         <path | ||||||
|  |            style="opacity:1;fill:url(#linearGradient4191);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | ||||||
|  |            d="m 0,119.99998 210.00002,0 0,280.00003 -210.00002,0 z" | ||||||
|  |            id="rect4141" /> | ||||||
|  |         <g | ||||||
|  |            id="g4254"> | ||||||
|  |           <path | ||||||
|  |              id="path4143" | ||||||
|  |              d="m 0,372 0,8 210,0 0,-8 -210,0 z" | ||||||
|  |              style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> | ||||||
|  |           <path | ||||||
|  |              id="path4145" | ||||||
|  |              d="m 0,352 0,8 210,0 0,-8 -210,0 z" | ||||||
|  |              style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> | ||||||
|  |           <path | ||||||
|  |              id="path4147" | ||||||
|  |              d="m 0,332 0,8 210,0 0,-8 -210,0 z" | ||||||
|  |              style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> | ||||||
|  |           <path | ||||||
|  |              id="path4149" | ||||||
|  |              d="m 0,312 0,8 210,0 0,-8 -210,0 z" | ||||||
|  |              style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> | ||||||
|  |           <path | ||||||
|  |              id="path4151" | ||||||
|  |              d="m 0,292 0,8 210,0 0,-8 -210,0 z" | ||||||
|  |              style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> | ||||||
|  |           <path | ||||||
|  |              id="path4153" | ||||||
|  |              d="m 0,272 0,8 210,0 0,-8 -210,0 z" | ||||||
|  |              style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> | ||||||
|  |           <path | ||||||
|  |              id="path4155" | ||||||
|  |              d="m 0,252 0,8 210,0 0,-8 -210,0 z" | ||||||
|  |              style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> | ||||||
|  |           <path | ||||||
|  |              id="path4157" | ||||||
|  |              d="m 0,232 0,8 210,0 0,-8 -210,0 z" | ||||||
|  |              style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> | ||||||
|  |           <path | ||||||
|  |              id="path4159" | ||||||
|  |              d="m 0,212 0,8 210,0 0,-8 -210,0 z" | ||||||
|  |              style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> | ||||||
|  |           <path | ||||||
|  |              id="path4161" | ||||||
|  |              d="m 0,192 0,8 210,0 0,-8 -210,0 z" | ||||||
|  |              style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> | ||||||
|  |           <path | ||||||
|  |              id="path4163" | ||||||
|  |              d="m 0,172 0,8 210,0 0,-8 -210,0 z" | ||||||
|  |              style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> | ||||||
|  |           <path | ||||||
|  |              style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#da6a8d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" | ||||||
|  |              d="m 20.000001,400.00001 8,0 0,-280.00002 -8,0 0,280.00002 z" | ||||||
|  |              id="path4226" /> | ||||||
|  |         </g> | ||||||
|  |       </g> | ||||||
|  |       <g | ||||||
|  |          id="g4268"> | ||||||
|  |         <path | ||||||
|  |            id="rect4239" | ||||||
|  |            d="m 37.453492,122.77542 135.093038,0 c 1.35923,0 2.45349,1.09426 2.45349,2.45349 l 0,17.3176 c 0,1.35923 -1.09426,2.45349 -2.45349,2.45349 l -135.093038,0 c -1.359233,0 -2.453488,-1.09426 -2.453488,-2.45349 l 0,-17.3176 c 0,-1.35923 1.094255,-2.45349 2.453488,-2.45349 z" | ||||||
|  |            style="opacity:1;fill:#d1d1d1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|  |         <g | ||||||
|  |            id="g4244"> | ||||||
|  |           <path | ||||||
|  |              id="path4171" | ||||||
|  |              d="m 105.00001,134.83513 0,-30.47246" | ||||||
|  |              style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:9.39278793;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> | ||||||
|  |           <path | ||||||
|  |              id="rect4190" | ||||||
|  |              d="m 104.41582,99.999977 1.16838,0 c 2.44636,0 4.41581,1.969453 4.41581,4.415813 l 0,31.16838 c 0,2.44636 -1.96945,4.41581 -4.41581,4.41581 l -1.16838,0 c -2.44636,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.96945,-4.415813 4.41581,-4.415813 z" | ||||||
|  |              style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|  |           <path | ||||||
|  |              id="rect4192" | ||||||
|  |              d="m 124.41582,99.999977 1.16838,0 c 2.44636,0 4.41581,1.969453 4.41581,4.415813 l 0,31.16838 c 0,2.44636 -1.96945,4.41581 -4.41581,4.41581 l -1.16838,0 c -2.44636,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.96945,-4.415813 4.41581,-4.415813 z" | ||||||
|  |              style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|  |           <path | ||||||
|  |              id="rect4194" | ||||||
|  |              d="m 144.41583,99.999977 1.16838,0 c 2.44635,0 4.41581,1.969453 4.41581,4.415813 l 0,31.16838 c 0,2.44636 -1.96946,4.41581 -4.41581,4.41581 l -1.16838,0 c -2.44636,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.96945,-4.415813 4.41581,-4.415813 z" | ||||||
|  |              style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|  |           <path | ||||||
|  |              id="rect4196" | ||||||
|  |              d="m 84.415818,99.999977 1.16838,0 c 2.446359,0 4.415811,1.969453 4.415811,4.415813 l 0,31.16838 c 0,2.44636 -1.969452,4.41581 -4.415811,4.41581 l -1.16838,0 c -2.446359,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.969451,-4.415813 4.41581,-4.415813 z" | ||||||
|  |              style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|  |           <path | ||||||
|  |              id="rect4198" | ||||||
|  |              d="m 64.415814,99.999977 1.16838,0 c 2.446359,0 4.415811,1.969453 4.415811,4.415813 l 0,31.16838 c 0,2.44636 -1.969452,4.41581 -4.415811,4.41581 l -1.16838,0 c -2.446359,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.969451,-4.415813 4.41581,-4.415813 z" | ||||||
|  |              style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|  |           <path | ||||||
|  |              id="rect4200" | ||||||
|  |              d="m 164.41583,99.999977 1.16838,0 c 2.44635,0 4.41581,1.969453 4.41581,4.415813 l 0,31.16838 c 0,2.44636 -1.96946,4.41581 -4.41581,4.41581 l -1.16838,0 c -2.44636,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.96945,-4.415813 4.41581,-4.415813 z" | ||||||
|  |              style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|  |           <path | ||||||
|  |              id="rect4202" | ||||||
|  |              d="m 44.415814,99.999977 1.16838,0 c 2.446359,0 4.415811,1.969453 4.415811,4.415813 l 0,31.16838 c 0,2.44636 -1.969452,4.41581 -4.415811,4.41581 l -1.16838,0 c -2.446359,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.969451,-4.415813 4.41581,-4.415813 z" | ||||||
|  |              style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|  |         </g> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |     <rect | ||||||
|  |        y="119.99998" | ||||||
|  |        x="0" | ||||||
|  |        height="280.00003" | ||||||
|  |        width="210.00002" | ||||||
|  |        id="rect4169" | ||||||
|  |        style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:12.00000095;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 21 KiB | 
|  | @ -1,6 +1,4 @@ | ||||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> |  | ||||||
| 
 |  | ||||||
| <svg | <svg | ||||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" |    xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||||
|    xmlns:cc="http://creativecommons.org/ns#" |    xmlns:cc="http://creativecommons.org/ns#" | ||||||
|  | @ -8,22 +6,14 @@ | ||||||
|    xmlns:svg="http://www.w3.org/2000/svg" |    xmlns:svg="http://www.w3.org/2000/svg" | ||||||
|    xmlns="http://www.w3.org/2000/svg" |    xmlns="http://www.w3.org/2000/svg" | ||||||
|    xmlns:xlink="http://www.w3.org/1999/xlink" |    xmlns:xlink="http://www.w3.org/1999/xlink" | ||||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |    width="640" | ||||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |    height="640" | ||||||
|    width="400" |    viewBox="0 0 640.00003 640" | ||||||
|    height="400" |  | ||||||
|    viewBox="0 0 400.00002 400" |  | ||||||
|    id="svg2" |    id="svg2" | ||||||
|    version="1.1" |    version="1.1"> | ||||||
|    inkscape:version="0.91 r13725" |  | ||||||
|    sodipodi:docname="video.svg" |  | ||||||
|    inkscape:export-filename="C:\Git\else\Etiquette\static\basic_thumbnails\video.png" |  | ||||||
|    inkscape:export-xdpi="143.99998" |  | ||||||
|    inkscape:export-ydpi="143.99998"> |  | ||||||
|   <defs |   <defs | ||||||
|      id="defs4"> |      id="defs4"> | ||||||
|     <linearGradient |     <linearGradient | ||||||
|        inkscape:collect="always" |  | ||||||
|        id="linearGradient4160"> |        id="linearGradient4160"> | ||||||
|       <stop |       <stop | ||||||
|          style="stop-color:#414141;stop-opacity:1" |          style="stop-color:#414141;stop-opacity:1" | ||||||
|  | @ -43,7 +33,6 @@ | ||||||
|          id="stop4164" /> |          id="stop4164" /> | ||||||
|     </linearGradient> |     </linearGradient> | ||||||
|     <linearGradient |     <linearGradient | ||||||
|        inkscape:collect="always" |  | ||||||
|        xlink:href="#linearGradient4160" |        xlink:href="#linearGradient4160" | ||||||
|        id="linearGradient4166" |        id="linearGradient4166" | ||||||
|        x1="218.28458" |        x1="218.28458" | ||||||
|  | @ -52,25 +41,6 @@ | ||||||
|        y2="742.12012" |        y2="742.12012" | ||||||
|        gradientUnits="userSpaceOnUse" /> |        gradientUnits="userSpaceOnUse" /> | ||||||
|   </defs> |   </defs> | ||||||
|   <sodipodi:namedview |  | ||||||
|      id="base" |  | ||||||
|      pagecolor="#ffffff" |  | ||||||
|      bordercolor="#666666" |  | ||||||
|      borderopacity="1.0" |  | ||||||
|      inkscape:pageopacity="0.0" |  | ||||||
|      inkscape:pageshadow="2" |  | ||||||
|      inkscape:zoom="0.335625" |  | ||||||
|      inkscape:cx="833.60008" |  | ||||||
|      inkscape:cy="34.45227" |  | ||||||
|      inkscape:document-units="px" |  | ||||||
|      inkscape:current-layer="layer1" |  | ||||||
|      showgrid="false" |  | ||||||
|      inkscape:window-width="1366" |  | ||||||
|      inkscape:window-height="706" |  | ||||||
|      inkscape:window-x="1432" |  | ||||||
|      inkscape:window-y="204" |  | ||||||
|      inkscape:window-maximized="1" |  | ||||||
|      units="px" /> |  | ||||||
|   <metadata |   <metadata | ||||||
|      id="metadata7"> |      id="metadata7"> | ||||||
|     <rdf:RDF> |     <rdf:RDF> | ||||||
|  | @ -79,22 +49,16 @@ | ||||||
|         <dc:format>image/svg+xml</dc:format> |         <dc:format>image/svg+xml</dc:format> | ||||||
|         <dc:type |         <dc:type | ||||||
|            rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |            rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | ||||||
|         <dc:title /> |         <dc:title></dc:title> | ||||||
|       </cc:Work> |       </cc:Work> | ||||||
|     </rdf:RDF> |     </rdf:RDF> | ||||||
|   </metadata> |   </metadata> | ||||||
|   <g |   <g | ||||||
|      inkscape:label="Layer 1" |  | ||||||
|      inkscape:groupmode="layer" |  | ||||||
|      id="layer1" |      id="layer1" | ||||||
|      transform="translate(0,-652.36221)"> |      transform="matrix(1.6,0,0,1.6,-2.4414063e-5,-1043.7795)"> | ||||||
|     <path |     <path | ||||||
|        style="opacity:1;fill:url(#linearGradient4166);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" |        style="opacity:1;fill:url(#linearGradient4166);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.00000048;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | ||||||
|        inkscape:transform-center-x="-38.11878" |  | ||||||
|        inkscape:transform-center-y="-2.5060727e-005" |  | ||||||
|        d="M 317.15321,852.36221 88.440457,984.40954 c 0,0 40.204773,-53.51798 40.204773,-132.04733 0,-78.52935 -40.204773,-132.0474 -40.204773,-132.0474 z" |        d="M 317.15321,852.36221 88.440457,984.40954 c 0,0 40.204773,-53.51798 40.204773,-132.04733 0,-78.52935 -40.204773,-132.0474 -40.204773,-132.0474 z" | ||||||
|        id="path4157" |        id="path4157" /> | ||||||
|        inkscape:connector-curvature="0" |  | ||||||
|        sodipodi:nodetypes="cczcc" /> |  | ||||||
|   </g> |   </g> | ||||||
| </svg> | </svg> | ||||||
|  |  | ||||||
| Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2 KiB | 
|  | @ -141,12 +141,18 @@ li | ||||||
| { | { | ||||||
|     max-height: 100%; |     max-height: 100%; | ||||||
| } | } | ||||||
| .photo_galleryview_info span | .photo_galleryview_file_metadata | ||||||
| { | { | ||||||
|     position: absolute; |     position: absolute; | ||||||
|     bottom: 0; |     bottom: 0; | ||||||
|     right: 0; |     right: 0; | ||||||
| } | } | ||||||
|  | .photo_galleryview_tags | ||||||
|  | { | ||||||
|  |     position: absolute; | ||||||
|  |     bottom: 0; | ||||||
|  |     left: 0; | ||||||
|  | } | ||||||
| .tag_object | .tag_object | ||||||
| { | { | ||||||
|     font-size: 0.9em; |     font-size: 0.9em; | ||||||
|  |  | ||||||
|  | @ -82,3 +82,42 @@ function bind_box_to_button(box, button) | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  | function entry_with_history_hook(box, button) | ||||||
|  | { | ||||||
|  |     //console.log(event.keyCode);
 | ||||||
|  |     if (box.entry_history === undefined) | ||||||
|  |     {box.entry_history = [];} | ||||||
|  |     if (box.entry_history_pos === undefined) | ||||||
|  |     {box.entry_history_pos = -1;} | ||||||
|  |     if (event.keyCode == 13) | ||||||
|  |     { | ||||||
|  |         /* Enter */ | ||||||
|  |         box.entry_history.push(box.value); | ||||||
|  |         button.click(); | ||||||
|  |         box.value = ""; | ||||||
|  |     } | ||||||
|  |     else if (event.keyCode == 38) | ||||||
|  |     { | ||||||
|  | 
 | ||||||
|  |         /* Up arrow */ | ||||||
|  |         if (box.entry_history.length == 0) | ||||||
|  |         {return} | ||||||
|  |         if (box.entry_history_pos == -1) | ||||||
|  |         { | ||||||
|  |             box.entry_history_pos = box.entry_history.length - 1; | ||||||
|  |         } | ||||||
|  |         else if (box.entry_history_pos > 0) | ||||||
|  |         { | ||||||
|  |             box.entry_history_pos -= 1; | ||||||
|  |         } | ||||||
|  |         box.value = box.entry_history[box.entry_history_pos]; | ||||||
|  |     } | ||||||
|  |     else if (event.keyCode == 27) | ||||||
|  |     { | ||||||
|  |         box.value = ""; | ||||||
|  |     } | ||||||
|  |     else | ||||||
|  |     { | ||||||
|  |         box.entry_history_pos = -1; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								etiquette/static/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										173
									
								
								etiquette/static/favicon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,173 @@ | ||||||
|  | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||||
|  | <svg | ||||||
|  |    xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||||
|  |    xmlns:cc="http://creativecommons.org/ns#" | ||||||
|  |    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||||||
|  |    xmlns:svg="http://www.w3.org/2000/svg" | ||||||
|  |    xmlns="http://www.w3.org/2000/svg" | ||||||
|  |    xmlns:xlink="http://www.w3.org/1999/xlink" | ||||||
|  |    width="640" | ||||||
|  |    height="640" | ||||||
|  |    viewBox="0 0 640.00004 639.99999" | ||||||
|  |    id="svg2" | ||||||
|  |    version="1.1"> | ||||||
|  |   <defs | ||||||
|  |      id="defs4"> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4499"> | ||||||
|  |       <stop | ||||||
|  |          style="stop-color:#f31010;stop-opacity:1" | ||||||
|  |          offset="0" | ||||||
|  |          id="stop4501" /> | ||||||
|  |       <stop | ||||||
|  |          style="stop-color:#ef4646;stop-opacity:1" | ||||||
|  |          offset="1" | ||||||
|  |          id="stop4503" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4489"> | ||||||
|  |       <stop | ||||||
|  |          style="stop-color:#1d8f0e;stop-opacity:1" | ||||||
|  |          offset="0" | ||||||
|  |          id="stop4491" /> | ||||||
|  |       <stop | ||||||
|  |          style="stop-color:#2fee15;stop-opacity:1" | ||||||
|  |          offset="1" | ||||||
|  |          id="stop4493" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <marker | ||||||
|  |        orient="auto" | ||||||
|  |        refY="0" | ||||||
|  |        refX="0" | ||||||
|  |        id="Tail" | ||||||
|  |        style="overflow:visible"> | ||||||
|  |       <g | ||||||
|  |          id="g4180" | ||||||
|  |          transform="scale(-1.2,-1.2)" | ||||||
|  |          style="fill:#74440c;fill-opacity:1;stroke:#000000;stroke-opacity:1"> | ||||||
|  |         <path | ||||||
|  |            id="path4182" | ||||||
|  |            d="M -3.8048674,-3.9585227 0.54352094,0" | ||||||
|  |            style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" /> | ||||||
|  |         <path | ||||||
|  |            id="path4184" | ||||||
|  |            d="M -1.2866832,-3.9585227 3.0617053,0" | ||||||
|  |            style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" /> | ||||||
|  |         <path | ||||||
|  |            id="path4186" | ||||||
|  |            d="M 1.3053582,-3.9585227 5.6537466,0" | ||||||
|  |            style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" /> | ||||||
|  |         <path | ||||||
|  |            id="path4188" | ||||||
|  |            d="M -3.8048674,4.1775838 0.54352094,0.21974226" | ||||||
|  |            style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" /> | ||||||
|  |         <path | ||||||
|  |            id="path4190" | ||||||
|  |            d="M -1.2866832,4.1775838 3.0617053,0.21974226" | ||||||
|  |            style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" /> | ||||||
|  |         <path | ||||||
|  |            id="path4192" | ||||||
|  |            d="M 1.3053582,4.1775838 5.6537466,0.21974226" | ||||||
|  |            style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" /> | ||||||
|  |       </g> | ||||||
|  |     </marker> | ||||||
|  |     <linearGradient | ||||||
|  |        xlink:href="#linearGradient4489" | ||||||
|  |        id="linearGradient4495" | ||||||
|  |        x1="280.82242" | ||||||
|  |        y1="1056.675" | ||||||
|  |        x2="36.084953" | ||||||
|  |        y2="669.78662" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(1.3511769,0,0,1.3511769,73.997383,-786.40284)" /> | ||||||
|  |     <linearGradient | ||||||
|  |        xlink:href="#linearGradient4499" | ||||||
|  |        id="linearGradient4505" | ||||||
|  |        x1="336.3808" | ||||||
|  |        y1="1003.8561" | ||||||
|  |        x2="105.84174" | ||||||
|  |        y2="647.27222" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(1.3511769,0,0,1.3511769,73.996343,-786.40298)" /> | ||||||
|  |     <linearGradient | ||||||
|  |        id="linearGradient4361"> | ||||||
|  |       <stop | ||||||
|  |          id="stop4363" | ||||||
|  |          offset="0" | ||||||
|  |          style="stop-color:#4d4d4d;stop-opacity:1" /> | ||||||
|  |       <stop | ||||||
|  |          id="stop4365" | ||||||
|  |          offset="1" | ||||||
|  |          style="stop-color:#000000;stop-opacity:0;" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <linearGradient | ||||||
|  |        gradientTransform="matrix(0.8225722,0,0,1.3309496,35.485561,-282.08894)" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        y2="699.85175" | ||||||
|  |        x2="204.01379" | ||||||
|  |        y1="1037.2178" | ||||||
|  |        x1="315.47162" | ||||||
|  |        id="linearGradient4367" | ||||||
|  |        xlink:href="#linearGradient4361" /> | ||||||
|  |     <linearGradient | ||||||
|  |        xlink:href="#linearGradient4499" | ||||||
|  |        id="linearGradient4505-3" | ||||||
|  |        x1="336.3808" | ||||||
|  |        y1="1003.8561" | ||||||
|  |        x2="105.84174" | ||||||
|  |        y2="647.27222" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(1.3511769,0,0,1.3511769,73.996343,-786.40298)" /> | ||||||
|  |     <linearGradient | ||||||
|  |        xlink:href="#linearGradient4489" | ||||||
|  |        id="linearGradient4495-0" | ||||||
|  |        x1="280.82242" | ||||||
|  |        y1="1056.675" | ||||||
|  |        x2="36.084953" | ||||||
|  |        y2="669.78662" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(1.3511769,0,0,1.3511769,73.997383,-786.40284)" /> | ||||||
|  |     <linearGradient | ||||||
|  |        xlink:href="#linearGradient4499" | ||||||
|  |        id="linearGradient4505-3-9" | ||||||
|  |        x1="336.3808" | ||||||
|  |        y1="1003.8561" | ||||||
|  |        x2="105.84174" | ||||||
|  |        y2="647.27222" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(1.220263,0,0,1.220263,66.826923,-710.20939)" /> | ||||||
|  |     <linearGradient | ||||||
|  |        xlink:href="#linearGradient4489" | ||||||
|  |        id="linearGradient4495-0-1" | ||||||
|  |        x1="280.82242" | ||||||
|  |        y1="1056.675" | ||||||
|  |        x2="36.084953" | ||||||
|  |        y2="669.78662" | ||||||
|  |        gradientUnits="userSpaceOnUse" | ||||||
|  |        gradientTransform="matrix(1.220263,0,0,1.220263,66.827863,-710.20926)" /> | ||||||
|  |   </defs> | ||||||
|  |   <metadata | ||||||
|  |      id="metadata7"> | ||||||
|  |     <rdf:RDF> | ||||||
|  |       <cc:Work | ||||||
|  |          rdf:about=""> | ||||||
|  |         <dc:format>image/svg+xml</dc:format> | ||||||
|  |         <dc:type | ||||||
|  |            rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | ||||||
|  |         <dc:title></dc:title> | ||||||
|  |       </cc:Work> | ||||||
|  |     </rdf:RDF> | ||||||
|  |   </metadata> | ||||||
|  |   <path | ||||||
|  |      id="path4480" | ||||||
|  |      d="m 573.17315,444.37019 c -48.25919,77.78171 -134.42937,129.57907 -232.69753,129.57907 -151.13202,0 -273.648697,-122.51669 -273.648697,-273.64869 0,-90.63158 44.059897,-170.97205 111.928527,-220.771487 6.51438,-4.779996 13.24791,-9.279034 20.18309,-13.478366 z" | ||||||
|  |      style="opacity:1;fill:url(#linearGradient4505-3-9);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:19.16129684;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|  |   <path | ||||||
|  |      id="circle4482" | ||||||
|  |      d="M 178.24792,78.766505 C 107.30648,130.14717 66.452241,211.94323 66.319393,299.53686 66.319375,450.66888 189.34458,573.94828 340.47661,573.94828 428.2425,573.84456 511.5918,532.18966 562.97034,461.03416 c -46.64082,33.96568 -103.78292,51.78066 -161.48062,51.90097 -151.13198,0 -274.15719,-123.27939 -274.15718,-274.41142 0.095,-57.55121 17.12911,-113.1671 50.91538,-159.757205 z" | ||||||
|  |      style="opacity:1;fill:url(#linearGradient4495-0-1);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:19.16129684;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> | ||||||
|  |   <path | ||||||
|  |      style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:16.25600243;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | ||||||
|  |      d="m 573.17315,444.37019 c -48.25919,77.78171 -134.42937,129.57907 -232.69753,129.57907 -151.13202,0 -273.648697,-122.51669 -273.648697,-273.64869 0,-90.63158 44.059897,-170.97205 111.928527,-220.771487 6.51438,-4.779996 13.24791,-9.279034 20.18309,-13.478366 z" | ||||||
|  |      id="path4252" /> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 7 KiB | 
|  | @ -3,7 +3,7 @@ | ||||||
| <head> | <head> | ||||||
|     {% import "photo_object.html" as photo_object %} |     {% import "photo_object.html" as photo_object %} | ||||||
|     {% import "header.html" as header %} |     {% import "header.html" as header %} | ||||||
|     <title>Album {{album.title}}</title> |     <title>Album {{album["title"]}}</title> | ||||||
|     <meta charset="UTF-8"> |     <meta charset="UTF-8"> | ||||||
|     <link rel="stylesheet" href="/static/common.css"> |     <link rel="stylesheet" href="/static/common.css"> | ||||||
| </head> | </head> | ||||||
|  | @ -18,20 +18,20 @@ | ||||||
| <body> | <body> | ||||||
| {{header.make_header()}} | {{header.make_header()}} | ||||||
| <div id="content_body"> | <div id="content_body"> | ||||||
|     <h2>{{album.title}}</h2> |     <h2>{{album["title"]}}</h2> | ||||||
|     {% set parent=album.parent() %} |     {% set parent=album["parent"] %} | ||||||
|     {% if parent %} |     {% if parent %} | ||||||
|     <h3>Parent: <a href="/album/{{parent.id}}">{{parent.title}}</a></h3> |     <h3>Parent: <a href="/album/{{parent["id"]}}">{{parent.title}}</a></h3> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     {% if child_albums %} |     {% if child_albums %} | ||||||
|     <h3>Sub-albums</h3> |     <h3>Sub-albums</h3> | ||||||
|     <ul> |     <ul> | ||||||
|         {% for album in child_albums %} |         {% for album in child_albums %} | ||||||
|         <li><a href="/album/{{album.id}}"> |         <li><a href="/album/{{album["id"]}}"> | ||||||
|             {% if album.title %} |             {% if album["title"] %} | ||||||
|             {{album.title}} |             {{album["title"]}} | ||||||
|             {% else %} |             {% else %} | ||||||
|             {{album.id}} |             {{album["id"]}} | ||||||
|             {% endif %}</a> |             {% endif %}</a> | ||||||
|         </li> |         </li> | ||||||
|         {% endfor %} |         {% endfor %} | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ | ||||||
|     /* Override common.css */ |     /* Override common.css */ | ||||||
|     flex: 1; |     flex: 1; | ||||||
|     height: 100%; |     height: 100%; | ||||||
|  |     width: 100%; | ||||||
| } | } | ||||||
| #left | #left | ||||||
| { | { | ||||||
|  | @ -45,20 +46,23 @@ | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|     justify-content: center; |     justify-content: center; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|  |     max-height: 100%; | ||||||
|  |     max-width: 100%; | ||||||
|     height: 100%; |     height: 100%; | ||||||
|  |     width: 100%; | ||||||
| } | } | ||||||
| .photo_object a | .photo_object a | ||||||
| { | { | ||||||
|  |     max-height: 100%; | ||||||
|  |     max-width: 100%; | ||||||
|     display: flex; |     display: flex; | ||||||
|     justify-content: center; |     justify-content: center; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     max-width: 100%; |  | ||||||
|     max-height: 100%; |  | ||||||
| } | } | ||||||
| .photo_object img | .photo_object img | ||||||
| { | { | ||||||
|     height: auto; |     max-height: 100%; | ||||||
|     height: 100%; |     max-width: 100%; | ||||||
| } | } | ||||||
| .photo_object audio | .photo_object audio | ||||||
| { | { | ||||||
|  | @ -80,10 +84,10 @@ | ||||||
|         <!-- TAG INFO --> |         <!-- TAG INFO --> | ||||||
|         <h4>Tags</h4> |         <h4>Tags</h4> | ||||||
|         <ul id="this_tags"> |         <ul id="this_tags"> | ||||||
|             {% for tag in tags %} |             {% for tag in photo['tags'] %} | ||||||
|             <li> |             <li> | ||||||
|                 <a class="tag_object" href="/search?tag_musts={{tag.name}}">{{tag.qualified_name()}}</a> |                 <a class="tag_object" href="/search?tag_musts={{tag["name"]}}">{{tag["qualified_name"]}}</a> | ||||||
|                 <button class="remove_tag_button" onclick="remove_photo_tag('{{photo.id}}', '{{tag.name}}', receive_callback);"></button> |                 <button class="remove_tag_button" onclick="remove_photo_tag('{{photo["id"]}}', '{{tag["name"]}}', receive_callback);"></button> | ||||||
|             </li> |             </li> | ||||||
|             {% endfor %} |             {% endfor %} | ||||||
|             <li> |             <li> | ||||||
|  | @ -95,22 +99,24 @@ | ||||||
|         <!-- METADATA & DOWNLOAD --> |         <!-- METADATA & DOWNLOAD --> | ||||||
|         <h4>File info</h4> |         <h4>File info</h4> | ||||||
|         <ul id="metadata"> |         <ul id="metadata"> | ||||||
|         {% if photo.width %} |         {% if photo["width"] %} | ||||||
|             <li>{{photo.width}}x{{photo.height}} px</li> |             <li>{{photo["width"]}}x{{photo["height"]}} px</li> | ||||||
|             <li>{{photo.ratio}} aspect ratio</li> |             <li>{{photo["ratio"]}} aspect ratio</li> | ||||||
|             <li>{{photo.bytestring()}}</li> |             <li>{{photo["bytestring"]}}</li> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|             <li><a href="/file/{{photo.id}}.{{photo.extension}}?download=1">Download as {{photo.id}}.{{photo.extension}}</a></li> |             <li>{{photo["duration"]}}</li> | ||||||
|             <li><a href="/file/{{photo.id}}.{{photo.extension}}?download=1&original_filename=1">Download as "{{photo.basename}}"</a></li> |         {% if photo["duration"] %} | ||||||
|  |         {% endif %} | ||||||
|  |             <li><a href="/file/{{photo["id"]}}.{{photo["extension"]}}?download=1">Download as {{photo["id"]}}.{{photo["extension"]}}</a></li> | ||||||
|  |             <li><a href="/file/{{photo["id"]}}.{{photo["extension"]}}?download=1&original_filename=1">Download as "{{photo["filename"]}}"</a></li> | ||||||
|         </ul> |         </ul> | ||||||
| 
 | 
 | ||||||
|         <!-- CONTAINING ALBUMS --> |         <!-- CONTAINING ALBUMS --> | ||||||
|         {% set albums=photo.albums() %} |         {% if photo["albums"] %} | ||||||
|         {% if albums %} |  | ||||||
|         <h4>Albums containing this photo</h4> |         <h4>Albums containing this photo</h4> | ||||||
|         <ul id="containing albums"> |         <ul id="containing albums"> | ||||||
|             {% for album in albums %} |             {% for album in photo["albums"] %} | ||||||
|             <li><a href="/album/{{album.id}}">{{album.title}}</a></li> |             <li><a href="/album/{{album["id"]}}">{{album["title"]}}</a></li> | ||||||
|             {% endfor %} |             {% endfor %} | ||||||
|         {% endif %} |         {% endif %} | ||||||
|         </ul> |         </ul> | ||||||
|  | @ -121,13 +127,14 @@ | ||||||
| 
 | 
 | ||||||
| <div id="right"> | <div id="right"> | ||||||
|     <div class="photo_object"> |     <div class="photo_object"> | ||||||
|         {% set filename = photo.id + "." + photo.extension %} |         {% set filename = photo["id"] + "." + photo["extension"] %} | ||||||
|         {% set link = "/file/" + filename %} |         {% set link = "/file/" + filename %} | ||||||
|         {% set mimetype=photo.mimetype() %} |         {% set mimetype=photo["mimetype"] %} | ||||||
|         {% if mimetype == "image" %} |         {% if mimetype == "image" %} | ||||||
|         <a target="_blank" href="{{link}}"><img src="{{link}}"></a> |         <!-- <a target="_blank" href="{{link}}"><img src="{{link}}"></a> --> | ||||||
|  |         <img src="{{link}}"> | ||||||
|         {% elif mimetype == "video" %} |         {% elif mimetype == "video" %} | ||||||
|         <video src="{{link}}" controls preload=none {%if photo.thumbnail%}poster="/thumbnail/{{photo.id}}.jpg"{%endif%}></video> |         <video src="{{link}}" controls preload=none {%if photo["has_thumbnail"]%}poster="/thumbnail/{{photo["id"]}}.jpg"{%endif%}></video> | ||||||
|         {% elif mimetype == "audio" %} |         {% elif mimetype == "audio" %} | ||||||
|         <audio src="{{link}}" controls></audio> |         <audio src="{{link}}" controls></audio> | ||||||
|         {% else %} |         {% else %} | ||||||
|  | @ -140,10 +147,10 @@ | ||||||
| </html> | </html> | ||||||
| 
 | 
 | ||||||
| <script type="text/javascript"> | <script type="text/javascript"> | ||||||
| var box = document.getElementById('add_tag_textbox'); | var add_tag_box = document.getElementById('add_tag_textbox'); | ||||||
| var button = document.getElementById('add_tag_button'); | var add_tag_button = document.getElementById('add_tag_button'); | ||||||
| var message_area = document.getElementById('message_area'); | var message_area = document.getElementById('message_area'); | ||||||
| bind_box_to_button(box, button); | add_tag_box.onkeydown = function(){entry_with_history_hook(add_tag_box, add_tag_button)}; | ||||||
| 
 | 
 | ||||||
| function receive_callback(response) | function receive_callback(response) | ||||||
| { | { | ||||||
|  | @ -170,7 +177,7 @@ function receive_callback(response) | ||||||
| } | } | ||||||
| function submit_tag(callback) | function submit_tag(callback) | ||||||
| { | { | ||||||
|     add_photo_tag('{{photo.id}}', box.value, callback); |     add_photo_tag('{{photo["id"]}}', add_tag_box.value, callback); | ||||||
|     box.value=''; |     add_tag_box.value = ""; | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  | @ -1,29 +1,47 @@ | ||||||
|  | {% set basics = | ||||||
|  |     { | ||||||
|  |         "audio": "audio", | ||||||
|  |         "txt": "txt", | ||||||
|  |         "video": "video", | ||||||
|  |     } | ||||||
|  | %} | ||||||
| {% macro galleryview(photo) %} | {% macro galleryview(photo) %} | ||||||
| <div class="photo_galleryview"> | <div class="photo_galleryview"> | ||||||
|     <div class="photo_galleryview_thumb"> |     <div class="photo_galleryview_thumb"> | ||||||
|         <a target="_blank" href="/photo/{{photo.id}}"> |         <a target="_blank" href="/photo/{{photo["id"]}}"> | ||||||
|             <img height="150" |             <img height="150" | ||||||
|         {% if photo.thumbnail %} |         {% if photo["has_thumbnail"] %} | ||||||
|             src="/thumbnail/{{photo.id}}.jpg" |             src="/thumbnail/{{photo["id"]}}.jpg" | ||||||
|         {% else %} |         {% else %} | ||||||
|             {% set mimetype = photo.mimetype() %} |             {% set choice = | ||||||
|             {% if mimetype == 'video' %} |                 photo['extension'] if photo['extension'] in basics else | ||||||
|                 src="/static/basic_thumbnails/video.png" |                 photo['mimetype'] if photo['mimetype'] in basics else | ||||||
|             {% elif mimetype == 'audio' %} |                 'other' | ||||||
|                 src="/static/basic_thumbnails/audio.png" |             %} | ||||||
|             {% else %} |             src="/static/basic_thumbnails/{{choice}}.png" | ||||||
|                 src="/static/basic_thumbnails/other.png" |              | ||||||
|             {% endif %} |  | ||||||
|         {% endif %} |         {% endif %} | ||||||
|         </a> |         </a> | ||||||
|     </div> |     </div> | ||||||
|     <div class="photo_galleryview_info"> |     <div class="photo_galleryview_info"> | ||||||
|         <a target="_blank" href="/photo/{{photo.id}}">{{photo.basename}}</a> |         <a target="_blank" href="/photo/{{photo["id"]}}">{{photo["filename"]}}</a> | ||||||
|         <span> |         <span class="photo_galleryview_file_metadata"> | ||||||
|         {% if photo.width %} |         {% if photo["width"] %} | ||||||
|             {{photo.width}}x{{photo.height}} |             {{photo["width"]}}x{{photo["height"]}}, | ||||||
|  |         {% endif %} | ||||||
|  |         {% if photo["duration"] %} | ||||||
|  |             {{photo["duration"]}}, | ||||||
|  |         {% endif %} | ||||||
|  |         {{photo["bytestring"]}} | ||||||
|  |         </span> | ||||||
|  |         <span class="photo_galleryview_tags"> | ||||||
|  |             {% if photo["tags"] %} | ||||||
|  |             {% set tags=[] %} | ||||||
|  |             {% for tag in photo["tags"] %} | ||||||
|  |             {% do tags.append(tag["name"]) %} | ||||||
|  |             {% endfor %} | ||||||
|  |             <span title="{{", ".join(tags)}}">T</span> | ||||||
|             {% endif %} |             {% endif %} | ||||||
|         {{photo.bytestring()}} |  | ||||||
|         </span> |         </span> | ||||||
|     </div> |     </div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | @ -13,9 +13,13 @@ a | ||||||
| { | { | ||||||
|     width: 50%; |     width: 50%; | ||||||
|     height: 40px; |     height: 40px; | ||||||
|     background-color: #ffffd4; |     background-color: rgba(0, 0, 0, 0.1); | ||||||
|     margin: 8px; |     margin: 8px; | ||||||
| } | } | ||||||
|  | a:hover | ||||||
|  | { | ||||||
|  |     background-color: #ffffd4; | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
| 
 | 
 | ||||||
| <head> | <head> | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ form | ||||||
|         <option value="height"   {%if selected_column=="height"%}selected{%endif%}   >Height</option> |         <option value="height"   {%if selected_column=="height"%}selected{%endif%}   >Height</option> | ||||||
|         <option value="ratio"    {%if selected_column=="ratio"%}selected{%endif%}    >Aspect Ratio</option> |         <option value="ratio"    {%if selected_column=="ratio"%}selected{%endif%}    >Aspect Ratio</option> | ||||||
|         <option value="bytes"    {%if selected_column=="bytes"%}selected{%endif%}    >File size</option> |         <option value="bytes"    {%if selected_column=="bytes"%}selected{%endif%}    >File size</option> | ||||||
|         <option value="length"  {%if selected_column=="length"%}selected{%endif%}  >Duration</option> |         <option value="duration" {%if selected_column=="duration"%}selected{%endif%} >Duration</option> | ||||||
|         <option value="random"   {%if selected_column=="random"%}selected{%endif%}   >Random</option> |         <option value="random"   {%if selected_column=="random"%}selected{%endif%}   >Random</option> | ||||||
|     </select> |     </select> | ||||||
|     <select> |     <select> | ||||||
|  | @ -149,8 +149,7 @@ form | ||||||
|             <ul id="search_builder_orderby_ul"> |             <ul id="search_builder_orderby_ul"> | ||||||
|                 {% if "orderby" in search_kwargs and search_kwargs["orderby"] %} |                 {% if "orderby" in search_kwargs and search_kwargs["orderby"] %} | ||||||
|                 {% for orderby in search_kwargs["orderby"] %} |                 {% for orderby in search_kwargs["orderby"] %} | ||||||
|                     {% set column=orderby.split(" ")[0] %} |                     {% set column, sorter=orderby.split(" ") %} | ||||||
|                     {% set sorter=orderby.split(" ")[1] %} |  | ||||||
|                     {{ create_orderby_li(selected_column=column, selected_sorter=sorter) }} |                     {{ create_orderby_li(selected_column=column, selected_sorter=sorter) }} | ||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|                 {% else %} |                 {% else %} | ||||||
|  | @ -161,24 +160,53 @@ form | ||||||
|         </div> |         </div> | ||||||
|         <br> |         <br> | ||||||
|         <form id="search_builder_form" action="" onsubmit="return submit_search();"> |         <form id="search_builder_form" action="" onsubmit="return submit_search();"> | ||||||
|             <input type="text" | 
 | ||||||
|  |             <span>Min-max values</span> | ||||||
|  |             <input type="text" class="basic_param" | ||||||
|  |             value="{%if search_kwargs['area']%}{{search_kwargs['area']}}{%endif%}" | ||||||
|  |             name="area" placeholder="Area: 1m-2m"> | ||||||
|  | 
 | ||||||
|  |             <input type="text" class="basic_param" | ||||||
|  |             value="{%if search_kwargs['width']%}{{search_kwargs['width']}}{%endif%}" | ||||||
|  |             name="width" placeholder="Width: 1k-2k"> | ||||||
|  | 
 | ||||||
|  |             <input type="text" class="basic_param" | ||||||
|  |             value="{%if search_kwargs['height']%}{{search_kwargs['height']}}{%endif%}" | ||||||
|  |             name="height" placeholder="Height: 1k-2k"> | ||||||
|  | 
 | ||||||
|  |             <input type="text" class="basic_param" | ||||||
|  |             value="{%if search_kwargs['ratio']%}{{search_kwargs['ratio']}}{%endif%}" | ||||||
|  |             name="ratio" placeholder="Aspect Ratio: 1.7-2"> | ||||||
|  | 
 | ||||||
|  |             <input type="text" class="basic_param" | ||||||
|  |             value="{%if search_kwargs['bytes']%}{{search_kwargs['bytes']}}{%endif%}" | ||||||
|  |             name="bytes" placeholder="File Size: 1mb-2mb"> | ||||||
|  | 
 | ||||||
|  |             <input type="text" class="basic_param" | ||||||
|  |             value="{%if search_kwargs['duration']%}{{search_kwargs['duration']}}{%endif%}" | ||||||
|  |             name="duration" placeholder="Duration: 10:00-20:00"> | ||||||
|  | 
 | ||||||
|  |             <br> | ||||||
|  | 
 | ||||||
|  |             <span>Other filters</span> | ||||||
|  |             <input type="text" class="basic_param" | ||||||
|             value="{%if search_kwargs['mimetype']%}{{search_kwargs['mimetype']}}{%endif%}" |             value="{%if search_kwargs['mimetype']%}{{search_kwargs['mimetype']}}{%endif%}" | ||||||
|             name="mimetype" placeholder="Mimetype(s)"> |             name="mimetype" placeholder="Mimetype(s)"> | ||||||
| 
 | 
 | ||||||
|             <input type="text" |             <input type="text" class="basic_param" | ||||||
|             value="{%if search_kwargs['extension']%}{{search_kwargs['extension']}}{%endif%}" |             value="{%if search_kwargs['extension']%}{{search_kwargs['extension']}}{%endif%}" | ||||||
|             name="extension" placeholder="Extension(s)"> |             name="extension" placeholder="Extension(s)"> | ||||||
| 
 | 
 | ||||||
|             <input type="text" |             <input type="text" class="basic_param" | ||||||
|             value="{%if search_kwargs['extension_not']%}{{search_kwargs['extension_not']}}{%endif%}"  |             value="{%if search_kwargs['extension_not']%}{{search_kwargs['extension_not']}}{%endif%}"  | ||||||
|             name="extension_not" placeholder="Forbid extension(s)"> |             name="extension_not" placeholder="Forbid extension(s)"> | ||||||
|              |              | ||||||
|             <select name="limit"> |             <select name="limit" class="basic_param"> | ||||||
|                 <option value="20"  {%if search_kwargs['limit'] == 20%}selected{%endif%}>20 items</option> |                 <option value="20"  {%if search_kwargs['limit'] == 20%}selected{%endif%}>20 items</option> | ||||||
|                 <option value="50"  {%if search_kwargs['limit'] == 50%}selected{%endif%}>50 items</option> |                 <option value="50"  {%if search_kwargs['limit'] == 50%}selected{%endif%}>50 items</option> | ||||||
|                 <option value="100" {%if search_kwargs['limit'] == 100%}selected{%endif%}>100 items</option> |                 <option value="100" {%if search_kwargs['limit'] == 100%}selected{%endif%}>100 items</option> | ||||||
|             </select> |             </select> | ||||||
|             <select name="has_tags"> |             <select name="has_tags" class="basic_param"> | ||||||
|                 <option value=""   {%if search_kwargs['has_tags'] == None %}selected{%endif%}>Tagged and untagged</option> |                 <option value=""   {%if search_kwargs['has_tags'] == None %}selected{%endif%}>Tagged and untagged</option> | ||||||
|                 <option value="yes"{%if search_kwargs['has_tags'] == True %}selected{%endif%}>Tagged only</option> |                 <option value="yes"{%if search_kwargs['has_tags'] == True %}selected{%endif%}>Tagged only</option> | ||||||
|                 <option value="no" {%if search_kwargs['has_tags'] == False %}selected{%endif%}>Untagged only</option> |                 <option value="no" {%if search_kwargs['has_tags'] == False %}selected{%endif%}>Untagged only</option> | ||||||
|  | @ -189,7 +217,7 @@ form | ||||||
|         <span>Tags on this page (click to join query):</span> |         <span>Tags on this page (click to join query):</span> | ||||||
|         <ul> |         <ul> | ||||||
|             {% for tag in total_tags %} |             {% for tag in total_tags %} | ||||||
|             <li><a href="" class="tag_object">{{tag._cached_qualname}}</a></li> |             <li><a href="" class="tag_object">{{tag}}</a></li> | ||||||
|             {% endfor %} |             {% endfor %} | ||||||
|         </ul> |         </ul> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|  | @ -314,6 +342,18 @@ function orderby_remove_hook(button) | ||||||
|         ul.removeChild(li); |         ul.removeChild(li); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | function simplify_tagnames(tags) | ||||||
|  | { | ||||||
|  |     var new_tags = []; | ||||||
|  |     for (var index = 0; index < tags.length; index += 1) | ||||||
|  |     { | ||||||
|  |         var tag = tags[index]; | ||||||
|  |         tag = tag.split("."); | ||||||
|  |         tag = tag[tag.length - 1]; | ||||||
|  |         new_tags.push(tag); | ||||||
|  |     } | ||||||
|  |     return new_tags; | ||||||
|  | } | ||||||
| function submit_search() | function submit_search() | ||||||
| { | { | ||||||
|     /* |     /* | ||||||
|  | @ -322,13 +362,13 @@ function submit_search() | ||||||
|     var url = window.location.origin + "/search"; |     var url = window.location.origin + "/search"; | ||||||
|     var parameters = []; |     var parameters = []; | ||||||
|     var has_tag_params = false; |     var has_tag_params = false; | ||||||
|     var musts = inputted_musts.join(","); |     var musts = simplify_tagnames(inputted_musts).join(","); | ||||||
|     if (musts) {parameters.push("tag_musts=" + musts); has_tag_params=true;} |     if (musts) {parameters.push("tag_musts=" + musts); has_tag_params=true;} | ||||||
| 
 | 
 | ||||||
|     var mays = inputted_mays.join(","); |     var mays = simplify_tagnames(inputted_mays).join(","); | ||||||
|     if (mays) {parameters.push("tag_mays=" + mays); has_tag_params=true;} |     if (mays) {parameters.push("tag_mays=" + mays); has_tag_params=true;} | ||||||
| 
 | 
 | ||||||
|     var forbids = inputted_forbids.join(","); |     var forbids = simplify_tagnames(inputted_forbids).join(","); | ||||||
|     if (forbids) {parameters.push("tag_forbids=" + forbids); has_tag_params=true;} |     if (forbids) {parameters.push("tag_forbids=" + forbids); has_tag_params=true;} | ||||||
| 
 | 
 | ||||||
|     var expression = document.getElementsByName("tag_expression")[0].value; |     var expression = document.getElementsByName("tag_expression")[0].value; | ||||||
|  | @ -339,10 +379,10 @@ function submit_search() | ||||||
|         has_tag_params=true; |         has_tag_params=true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var basic_inputs = ["mimetype", "extension", "extension_not", "limit", "has_tags"]; |     var basic_inputs = document.getElementsByClassName("basic_param"); | ||||||
|     for (var index = 0; index < basic_inputs.length; index += 1) |     for (var index = 0; index < basic_inputs.length; index += 1) | ||||||
|     { |     { | ||||||
|         var boxname = basic_inputs[index]; |         var boxname = basic_inputs[index].name; | ||||||
|         var box = document.getElementsByName(boxname)[0]; |         var box = document.getElementsByName(boxname)[0]; | ||||||
|         var value = box.value; |         var value = box.value; | ||||||
|         if (boxname == "has_tags" && has_tag_params && value == "no") |         if (boxname == "has_tags" && has_tag_params && value == "no") | ||||||
|  | @ -414,8 +454,8 @@ function tag_input_hook(box, inputted_list, li_class) | ||||||
|     value = value.split("."); |     value = value.split("."); | ||||||
|     value = value[value.length-1]; |     value = value[value.length-1]; | ||||||
|     value = value.split("+")[0]; |     value = value.split("+")[0]; | ||||||
|     value = value.replace(" ", "_"); |     value = value.replace(new RegExp(" ", 'g'), "_"); | ||||||
|     value = value.replace("-", "_"); |     value = value.replace(new RegExp("-", 'g'), "_"); | ||||||
|     if (!(value in QUALNAME_MAP)) |     if (!(value in QUALNAME_MAP)) | ||||||
|     { |     { | ||||||
|         return; |         return; | ||||||
|  |  | ||||||
|  | @ -91,7 +91,7 @@ body | ||||||
| var box = document.getElementById('add_tag_textbox'); | var box = document.getElementById('add_tag_textbox'); | ||||||
| var button = document.getElementById('add_tag_button'); | var button = document.getElementById('add_tag_button'); | ||||||
| var message_area = document.getElementById('message_area'); | var message_area = document.getElementById('message_area'); | ||||||
| bind_box_to_button(box, button); | box.onkeydown = function(){entry_with_history_hook(box, button)}; | ||||||
| 
 | 
 | ||||||
| function receive_callback(responses) | function receive_callback(responses) | ||||||
| { | { | ||||||
|  | @ -115,6 +115,9 @@ function receive_callback(responses) | ||||||
|             if (action == "new_tag") |             if (action == "new_tag") | ||||||
|             {message_text = "Created tag " + tagname;} |             {message_text = "Created tag " + tagname;} | ||||||
| 
 | 
 | ||||||
|  |             else if (action == "new_synonym") | ||||||
|  |             {message_text = "New synonym " + tagname;} | ||||||
|  | 
 | ||||||
|             else if (action == "existing_tag") |             else if (action == "existing_tag") | ||||||
|             {message_text = "Existing tag " + tagname;} |             {message_text = "Existing tag " + tagname;} | ||||||
| 
 | 
 | ||||||
|  | @ -137,6 +140,6 @@ function receive_callback(responses) | ||||||
| function submit_tag(callback) | function submit_tag(callback) | ||||||
| { | { | ||||||
|     create_tag(box.value, callback); |     create_tag(box.value, callback); | ||||||
|     box.value=''; |     box.value = ""; | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  | @ -1,12 +1,13 @@ | ||||||
| import os | import os | ||||||
| import phototagger | import phototagger | ||||||
| import unittest | import unittest | ||||||
| 
 | import random | ||||||
| 
 | 
 | ||||||
| class PhotoDBTest(unittest.TestCase): | class PhotoDBTest(unittest.TestCase): | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         self.P = phototagger.PhotoDB(':memory:') |         self.P = phototagger.PhotoDB(':memory:') | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class AlbumTest(PhotoDBTest): | class AlbumTest(PhotoDBTest): | ||||||
|     ''' |     ''' | ||||||
|     Test the creation and properties of albums |     Test the creation and properties of albums | ||||||
|  | @ -55,6 +56,7 @@ class PhotoTest(PhotoDBTest): | ||||||
|     def test_reload_metadata(self): |     def test_reload_metadata(self): | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class TagTest(PhotoDBTest): | class TagTest(PhotoDBTest): | ||||||
|     ''' |     ''' | ||||||
|     Test the creation and properties of tags |     Test the creation and properties of tags | ||||||
|  | @ -89,6 +91,7 @@ class TagTest(PhotoDBTest): | ||||||
|         self.assertRaises(phototagger.TagTooShort, tag.rename, '??') |         self.assertRaises(phototagger.TagTooShort, tag.rename, '??') | ||||||
|         tag.rename(tag.name)  # does nothing |         tag.rename(tag.name)  # does nothing | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class SearchTest(PhotoDBTest): | class SearchTest(PhotoDBTest): | ||||||
|     def search_extension(self): |     def search_extension(self): | ||||||
|         pass |         pass | ||||||
|  | @ -99,6 +102,7 @@ class SearchTest(PhotoDBTest): | ||||||
|     def search_tags(self): |     def search_tags(self): | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class SynonymTest(PhotoDBTest): | class SynonymTest(PhotoDBTest): | ||||||
|     ''' |     ''' | ||||||
|     Test the creation and management of synonyms |     Test the creation and management of synonyms | ||||||
|  | @ -136,6 +140,7 @@ class SynonymTest(PhotoDBTest): | ||||||
|         tag.add_synonym('test get syns3') |         tag.add_synonym('test get syns3') | ||||||
|         self.assertEqual(len(tag.synonyms()), 3) |         self.assertEqual(len(tag.synonyms()), 3) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class AlbumGroupTest(PhotoDBTest): | class AlbumGroupTest(PhotoDBTest): | ||||||
|     ''' |     ''' | ||||||
|     Test the relationships between albums as they form and leave groups |     Test the relationships between albums as they form and leave groups | ||||||
|  | @ -155,6 +160,7 @@ class AlbumGroupTest(PhotoDBTest): | ||||||
|     def test_album_parents(self): |     def test_album_parents(self): | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class TagGroupTest(PhotoDBTest): | class TagGroupTest(PhotoDBTest): | ||||||
|     ''' |     ''' | ||||||
|     Test the relationships between tags as they form and leave groups |     Test the relationships between tags as they form and leave groups | ||||||
|  | @ -188,6 +194,7 @@ class AlbumPhotoTest(PhotoDBTest): | ||||||
|     def test_remove_photo(self): |     def test_remove_photo(self): | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class PhotoTagTest(PhotoDBTest): | class PhotoTagTest(PhotoDBTest): | ||||||
|     ''' |     ''' | ||||||
|     Test the relationships between photos and tags |     Test the relationships between photos and tags | ||||||
|  |  | ||||||