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,202 +154,54 @@ 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))
|
desired_range = request.headers['range'].lower()
|
||||||
for (k, v) in outgoing_headers.items():
|
desired_range = desired_range.split('bytes=')[-1]
|
||||||
response.headers[k] = v
|
|
||||||
return response
|
|
||||||
|
|
||||||
try:
|
int_helper = lambda x: int(x) if x.isdigit() else None
|
||||||
file_size = os.path.getsize(filepath)
|
if '-' in desired_range:
|
||||||
except FileNotFoundError:
|
(desired_min, desired_max) = desired_range.split('-')
|
||||||
flask.abort(404)
|
range_min = int_helper(desired_min)
|
||||||
|
range_max = int_helper(desired_max)
|
||||||
|
else:
|
||||||
|
range_min = int_helper(desired_range)
|
||||||
|
|
||||||
desired_range = request.headers['range'].lower()
|
if range_min is None:
|
||||||
desired_range = desired_range.split('bytes=')[-1]
|
range_min = 0
|
||||||
|
if range_max is None:
|
||||||
|
range_max = file_size
|
||||||
|
|
||||||
inthelper = lambda x: int(x) if x.isdigit() else None
|
# because ranges are 0-indexed
|
||||||
if '-' in desired_range:
|
range_max = min(range_max, file_size - 1)
|
||||||
(desired_min, desired_max) = desired_range.split('-')
|
range_min = max(range_min, 0)
|
||||||
range_min = inthelper(desired_min)
|
|
||||||
range_max = inthelper(desired_max)
|
range_header = 'bytes {min}-{max}/{outof}'.format(
|
||||||
|
min=range_min,
|
||||||
|
max=range_max,
|
||||||
|
outof=file_size,
|
||||||
|
)
|
||||||
|
outgoing_headers['Content-Range'] = range_header
|
||||||
|
status = 206
|
||||||
else:
|
else:
|
||||||
range_min = inthelper(desired_range)
|
range_max = file_size - 1
|
||||||
|
|
||||||
if range_min is None:
|
|
||||||
range_min = 0
|
range_min = 0
|
||||||
if range_max is None:
|
status = 200
|
||||||
range_max = file_size
|
|
||||||
|
|
||||||
# because ranges are 0-indexed
|
|
||||||
range_max = min(range_max, file_size - 1)
|
|
||||||
range_min = max(range_min, 0)
|
|
||||||
|
|
||||||
range_header = 'bytes {min}-{max}/{outof}'.format(
|
|
||||||
min=range_min,
|
|
||||||
max=range_max,
|
|
||||||
outof=file_size,
|
|
||||||
)
|
|
||||||
outgoing_headers['Content-Range'] = range_header
|
|
||||||
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
|
||||||
|
|
||||||
outgoing_data = read_filebytes(filepath, range_min=range_min, range_max=range_max)
|
if request.method == 'HEAD':
|
||||||
|
outgoing_data = bytes()
|
||||||
|
else:
|
||||||
|
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_path='C:\\software\\ffmpeg\\bin\\ffmpeg.exe',
|
ffmpeg = converter.Converter(
|
||||||
ffprobe_path='C:\\software\\ffmpeg\\bin\\ffprobe.exe',
|
ffmpeg_path='C:\\software\\ffmpeg\\bin\\ffmpeg.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
|
||||||
frozen_children = self.export_tags(tag_export_totally_flat)
|
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)
|
||||||
|
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 %}
|
{% endif %}
|
||||||
{{photo.bytestring()}}
|
{% 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 %}
|
||||||
</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>
|
||||||
|
|
|
@ -94,14 +94,14 @@ form
|
||||||
{% macro create_orderby_li(selected_column, selected_sorter) %}
|
{% macro create_orderby_li(selected_column, selected_sorter) %}
|
||||||
<li class="search_builder_orderby_li">
|
<li class="search_builder_orderby_li">
|
||||||
<select>
|
<select>
|
||||||
<option value="created" {%if selected_column=="created"%}selected{%endif%} >Creation date</option>
|
<option value="created" {%if selected_column=="created"%}selected{%endif%} >Creation date</option>
|
||||||
<option value="area" {%if selected_column=="area"%}selected{%endif%} >Area</option>
|
<option value="area" {%if selected_column=="area"%}selected{%endif%} >Area</option>
|
||||||
<option value="width" {%if selected_column=="width"%}selected{%endif%} >Width</option>
|
<option value="width" {%if selected_column=="width"%}selected{%endif%} >Width</option>
|
||||||
<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>
|
||||||
<option value="desc" {%if selected_sorter=="desc"%}selected{%endif%} >Descending</option>
|
<option value="desc" {%if selected_sorter=="desc"%}selected{%endif%} >Descending</option>
|
||||||
|
@ -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
|
||||||
|
|