checkpoint

master
voussoir 2016-09-18 01:33:46 -07:00
commit 6c5580c1bc
30 changed files with 4958 additions and 0 deletions

48
.gitignore vendored Normal file
View File

@ -0,0 +1,48 @@
*.key
*.csr
*.crt
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# =========================
# Operating System Files
# =========================
# OSX
# =========================
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

114
etiquette/bad_search.txt Normal file
View File

@ -0,0 +1,114 @@
start_time = time.time()
# Raise for cases where the minimum > maximum
for (maxkey, maxval) in maximums.items():
if maxkey not in minimums:
continue
minval = minimums[maxkey]
if minval > maxval:
raise ValueError('Impossible min-max for %s' % maxkey)
conditions = []
minmaxers = {'<=': maximums, '>=': minimums}
# Convert the min-max parameters into query strings
print('Writing minmaxers')
for (comparator, minmaxer) in minmaxers.items():
for (field, value) in minmaxer.items():
if field not in Photo.int_properties:
raise ValueError('Unknown Photo property: %s' % field)
value = str(value)
query = min_max_query_builder(field, comparator, value)
conditions.append(query)
print(conditions)
print('Writing extension rule')
if extension is not None:
if isinstance(extension, str):
extension = [extension]
# Normalize to prevent injections
extension = [normalize_tagname(e) for e in extension]
extension = ['extension == "%s"' % e for e in extension]
extension = ' OR '.join(extension)
extension = '(%s)' % extension
conditions.append(extension)
def setify(l):
if l is None:
return set()
else:
return set(self.get_tag_by_name(t) for t in l)
tag_musts = setify(tag_musts)
tag_mays = setify(tag_mays)
tag_forbids = setify(tag_forbids)
base = '''
{negator} EXISTS(
SELECT 1 FROM photo_tag_rel
WHERE photo_tag_rel.photoid == photos.id
AND photo_tag_rel.tagid {operator} {value}
)'''
print('Writing musts')
for tag in tag_musts:
# tagid == must
query = base.format(
negator='',
operator='==',
value='"%s"' % tag.id,
)
conditions.append(query)
print('Writing mays')
if len(tag_mays) > 0:
# not any(tagid not in mays)
acceptable = tag_mays.union(tag_musts)
acceptable = ['"%s"' % t.id for t in acceptable]
acceptable = ', '.join(acceptable)
query = base.format(
negator='',
operator='IN',
value='(%s)' % acceptable,
)
conditions.append(query)
print('Writing forbids')
if len(tag_forbids) > 0:
# not any(tagid in forbids)
forbids = ['"%s"' % t.id for t in tag_forbids]
forbids = ', '.join(forbids)
query = base.format(
negator='NOT',
operator='IN',
value='(%s)' % forbids
)
conditions.append(query)
if len(conditions) == 0:
raise ValueError('No search query provided')
conditions = [query for query in conditions if query is not None]
conditions = ['(%s)' % c for c in conditions]
conditions = ' AND '.join(conditions)
conditions = 'WHERE %s' % conditions
query = 'SELECT * FROM photos %s' % conditions
query = query.replace('\n', ' ')
while ' ' in query:
query = query.replace(' ', ' ')
print(query)
temp_cur = self.sql.cursor()
temp_cur.execute(query)
for fetch in fetch_generator(temp_cur):
photo = Photo(self, fetch)
yield photo
end_time = time.time()
print(end_time - start_time)

465
etiquette/etiquette.py Normal file
View File

@ -0,0 +1,465 @@
import distutils.util
import flask
from flask import request
import json
import mimetypes
import os
import random
import re
import requests
import warnings
site = flask.Flask(__name__)
site.config.update(
SEND_FILE_MAX_AGE_DEFAULT=180,
TEMPLATES_AUTO_RELOAD=True,
)
print(os.getcwd())
import phototagger
P = phototagger.PhotoDB()
FILE_READ_CHUNK = 2 ** 20
MOTD_STRINGS = [
'Good morning, Paul. What will your first sequence of the day be?',
#'Buckle up, it\'s time to:',
]
ERROR_INVALID_ACTION = 'Invalid action'
ERROR_NO_TAG_GIVEN = 'No tag name supplied'
ERROR_TAG_TOO_SHORT = 'Not enough valid chars'
ERROR_SYNONYM_ITSELF = 'Cant apply synonym to itself'
ERROR_NO_SUCH_TAG = 'Doesn\'t exist'
def edit_params(original, modifications):
new_params = original.to_dict()
new_params.update(modifications)
if not new_params:
return ''
keep_params = {}
new_params = ['%s=%s' % (k, v) for (k, v) in new_params.items() if v]
new_params = '&'.join(new_params)
new_params = '?' + new_params
return new_params
def P_album(albumid):
try:
return P.get_album(albumid)
except phototagger.NoSuchAlbum:
flask.abort(404, 'That tag doesnt exist')
def P_photo(photoid):
try:
return P.get_photo(photoid)
except phototagger.NoSuchPhoto:
flask.abort(404, 'That photo doesnt exist')
def P_tag(tagname):
try:
return P.get_tag(tagname)
except phototagger.NoSuchTag as e:
flask.abort(404, 'That tag doesnt exist: %s' % e)
def truthystring(s):
if isinstance(s, (bool, int)) or s is None:
return s
s = s.lower()
if s in {'1', 'true', 't', 'yes', 'y', 'on'}:
return True
if s in {'null', 'none'}:
return None
return False
def read_filebytes(filepath, range_min, range_max):
range_span = range_max - range_min
#print('read span', range_min, range_max, range_span)
f = open(filepath, 'rb')
f.seek(range_min)
sent_amount = 0
with f:
while sent_amount < range_span:
chunk = f.read(FILE_READ_CHUNK)
if len(chunk) == 0:
break
yield chunk
sent_amount += len(chunk)
def send_file(filepath):
'''
Range-enabled file sending.
'''
outgoing_headers = {}
mimetype = mimetypes.guess_type(filepath)[0]
if mimetype is not None:
if 'text/' in mimetype:
mimetype += '; charset=utf-8'
outgoing_headers['Content-Type'] = mimetype
if 'range' not in request.headers:
response = flask.make_response(flask.send_file(filepath))
for (k, v) in outgoing_headers.items():
response.headers[k] = v
return response
try:
file_size = os.path.getsize(filepath)
except FileNotFoundError:
flask.abort(404)
desired_range = request.headers['range'].lower()
desired_range = desired_range.split('bytes=')[-1]
inthelper = lambda x: int(x) if x.isdigit() else None
if '-' in desired_range:
(desired_min, desired_max) = desired_range.split('-')
range_min = inthelper(desired_min)
range_max = inthelper(desired_max)
else:
range_min = inthelper(desired_range)
if range_min is None:
range_min = 0
if range_max is None:
range_max = file_size
# because ranges are 0-indexed
range_max = min(range_max, file_size - 1)
range_min = max(range_min, 0)
range_header = 'bytes {min}-{max}/{outof}'.format(
min=range_min,
max=range_max,
outof=file_size,
)
outgoing_headers['Content-Range'] = range_header
outgoing_headers['Accept-Ranges'] = 'bytes'
outgoing_headers['Content-Length'] = (range_max - range_min) + 1
outgoing_data = read_filebytes(filepath, range_min=range_min, range_max=range_max)
response = flask.Response(
outgoing_data,
status=206,
headers=outgoing_headers,
)
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):
notes = P.easybake(easybake_string)
notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes]
return notes
def delete_tag(tag):
tag = tag.split('.')[-1].split('+')[0]
tag = P.get_tag(tag)
tag.delete()
return {'action': 'delete_tag', 'tagname': tag.name, 'tagid': tag.id}
def delete_synonym(synonym):
synonym = synonym.split('+')[-1].split('.')[-1]
synonym = phototagger.normalize_tagname(synonym)
try:
master_tag = P.get_tag(synonym)
except phototagger.NoSuchTag:
flask.abort(404, 'That synonym doesnt exist')
if synonym not in master_tag.synonyms():
flask.abort(400, 'That name is not a synonym')
master_tag.remove_synonym(synonym)
return {'action':'delete_synonym', 'synonym': synonym}
@site.route('/search')
def search():
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_string = request.args.get('extension', '')
extension_not_string = request.args.get('extension_not', '')
mimetype_string = request.args.get('mimetype', '')
extension_list = comma_split_helper(extension_string)
extension_not_list = comma_split_helper(extension_not_string)
mimetype_list = comma_split_helper(mimetype_string)
# LIMIT
limit = request.args.get('limit', '')
if limit.isdigit():
limit = int(limit)
limit = min(100, limit)
else:
limit = 50
# OFFSET
offset = request.args.get('offset', None)
if offset:
offset = int(offset)
else:
offset = None
# MUSTS, MAYS, FORBIDS
qualname_map = P.export_tags(exporter=phototagger.tag_export_qualname_map)
tag_musts = request.args.get('tag_musts', '').split(',')
tag_mays = request.args.get('tag_mays', '').split(',')
tag_forbids = request.args.get('tag_forbids', '').split(',')
tag_expression = request.args.get('tag_expression', None)
tag_musts = [qualname_map.get(tag, tag) for tag in tag_musts if tag != '']
tag_mays = [qualname_map.get(tag, tag) for tag in tag_mays if tag != '']
tag_forbids = [qualname_map.get(tag, tag) for tag in tag_forbids if tag != '']
# ORDERBY
orderby = request.args.get('orderby', None)
if orderby:
orderby = orderby.replace('-', ' ')
orderby = orderby.replace('_', ' ')
orderby = orderby.split(',')
else:
orderby = None
# HAS_TAGS
has_tags = request.args.get('has_tags', '')
if has_tags == '':
has_tags = None
else:
has_tags = truthystring(has_tags)
# MINMAXERS
area = request.args.get('area', None)
width = request.args.get('width', None)
height = request.args.get('height', None)
ratio = request.args.get('ratio', None)
bytes = request.args.get('bytes', None)
length = request.args.get('length', None)
created = request.args.get('created', None)
# These are in a dictionary so I can pass them to the page template.
search_kwargs = {
'area': area,
'width': width,
'height': height,
'ratio': ratio,
'bytes': bytes,
'length': length,
'created': created,
'extension': extension_list,
'extension_not': extension_not_list,
'has_tags': has_tags,
'mimetype': mimetype_list,
'tag_musts': tag_musts,
'tag_mays': tag_mays,
'tag_forbids': tag_forbids,
'tag_expression': tag_expression,
'limit': limit,
'offset': offset,
'orderby': orderby,
'warn_bad_tags': True,
}
print(search_kwargs)
with warnings.catch_warnings(record=True) as catcher:
photos = list(P.search(**search_kwargs))
warns = [str(warning.message) for warning in catcher]
print(warns)
total_tags = set()
for photo in photos:
total_tags.update(photo.tags())
for tag in total_tags:
tag._cached_qualname = qualname_map[tag.name]
total_tags = sorted(total_tags, key=lambda x: x._cached_qualname)
# PREV-NEXT PAGE URLS
offset = offset or 0
if len(photos) == limit:
next_params = edit_params(request.args, {'offset': offset + limit})
next_page_url = '/search' + next_params
else:
next_page_url = None
if offset > 0:
prev_params = edit_params(request.args, {'offset': max(0, offset - limit)})
prev_page_url = '/search' + prev_params
else:
prev_page_url = None
search_kwargs['extension'] = extension_string
search_kwargs['extension_not'] = extension_not_string
search_kwargs['mimetype'] = mimetype_string
response = flask.render_template(
'search.html',
photos=photos,
search_kwargs=search_kwargs,
total_tags=total_tags,
prev_page_url=prev_page_url,
next_page_url=next_page_url,
qualname_map=json.dumps(qualname_map),
warns=warns,
)
return response
@site.route('/static/<filename>')
def get_resource(filename):
print(filename)
return flask.send_file('.\\static\\%s' % filename)
if __name__ == '__main__':
site.run(threaded=True)

View File

@ -0,0 +1,25 @@
# Use with
# py -i etiquette_easy.py
import phototagger
P = phototagger.PhotoDB()
import traceback
def easytagger():
while True:
i = input('> ')
if i.startswith('?'):
i = i.split('?')[1] or None
try:
P.export_tags(specific_tag=i)
except:
traceback.print_exc()
continue
P.easybake(i)
def photag(photoid):
photo = P.get_photo_by_id(photoid)
print(photo.tags())
while True:
photo.add_tag(input('> '))
get=P.get_tag

View File

@ -0,0 +1,25 @@
from gevent import monkey
monkey.patch_all()
import etiquette
import gevent.pywsgi
import gevent.wsgi
import sys
if len(sys.argv) == 2:
port = int(sys.argv[1])
else:
port = 5000
if port == 443:
http = gevent.pywsgi.WSGIServer(
('', port),
etiquette.site,
keyfile='etiquette.key',
certfile='etiquette.crt',
)
else:
http = gevent.wsgi.WSGIServer(('', port), etiquette.site)
print('Starting server')
http.serve_forever()

1955
etiquette/phototagger.py Normal file

File diff suppressed because it is too large Load Diff

16
etiquette/reider.py Normal file
View File

@ -0,0 +1,16 @@
for p in P.get_photos():
g=P.cur.execute('UPDATE photos SET id==? WHERE id==?', [p.id[-12:], p.id])
g=P.cur.execute('UPDATE photo_tag_rel SET photoid==? WHERE photoid==?', [p.id[-12:], p.id])
g=P.cur.execute('UPDATE album_photo_rel SET photoid==? WHERE photoid==?', [p.id[-12:], p.id])
for t in P.get_tags():
g=P.cur.execute('UPDATE tags SET id==? WHERE id==?', [t.id[-12:], t.id])
g=P.cur.execute('UPDATE photo_tag_rel SET tagid==? WHERE tagid==?', [t.id[-12:], t.id])
g=P.cur.execute('UPDATE tag_group_rel SET parentid==? WHERE parentid==?', [t.id[-12:], t.id])
g=P.cur.execute('UPDATE tag_group_rel SET memberid==? WHERE memberid==?', [t.id[-12:], t.id])
for a in P.get_albums():
g=P.cur.execute('UPDATE albums SET id==? WHERE id==?', [a.id[-12:], a.id])
g=P.cur.execute('UPDATE tag_group_rel SET parentid==? WHERE parentid==?', [a.id[-12:], a.id])
g=P.cur.execute('UPDATE tag_group_rel SET memberid==? WHERE memberid==?', [a.id[-12:], a.id])
g=P.cur.execute('UPDATE album_photo_rel SET albumid==? WHERE albumid==?', [a.id[-12:], a.id])

BIN
etiquette/samples/bolts.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 KiB

BIN
etiquette/samples/train.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="400"
height="400"
viewBox="0 0 400.00002 400"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="audio.svg"
inkscape:export-filename="C:\Git\else\Etiquette\static\basic_thumbnails\audio.png"
inkscape:export-xdpi="144"
inkscape:export-ydpi="144">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient4723">
<stop
style="stop-color:#157a05;stop-opacity:1"
offset="0"
id="stop4725" />
<stop
style="stop-color:#8cec27;stop-opacity:1"
offset="1"
id="stop4727" />
</linearGradient>
<clipPath
id="clipPath5600"
clipPathUnits="userSpaceOnUse">
<path
inkscape:connector-curvature="0"
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"
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>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4723"
id="linearGradient4739"
x1="196.13852"
y1="903.96155"
x2="177.04013"
y2="792.60211"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,-530.7894,361.49036)" />
</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
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
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-652.36221)">
<path
cx="356.42856"
cy="515.93359"
r="190.61275"
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"
d=""
inkscape:connector-curvature="0" />
<path
cx="356.42856"
cy="515.93359"
r="190.61275"
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"
d=""
inkscape:connector-curvature="0" />
<g
id="g4759"
transform="matrix(2.0529613,0,0,2.0529613,-208.02761,-862.66247)">
<path
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"
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
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"
id="path4179"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csc" />
<ellipse
transform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,0,0)"
ry="71.356659"
rx="39.683559"
cy="753.57629"
cx="-400.81314"
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" />
<path
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccc"
inkscape:connector-curvature="0"
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"
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" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="400"
height="400"
viewBox="0 0 400.00002 400"
id="svg2"
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
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient4361">
<stop
style="stop-color:#4d4d4d;stop-opacity:1"
offset="0"
id="stop4363" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop4365" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4361"
id="linearGradient4367"
x1="315.47162"
y1="1037.2178"
x2="204.01379"
y2="699.85175"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.8225722,0,0,1.3309496,35.485561,-282.08894)" />
</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
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
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-652.36221)">
<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"
id="rect4359"
width="251.81471"
height="251.8147"
x="74.092659"
y="726.45483" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="400"
height="400"
viewBox="0 0 400.00002 400"
id="svg2"
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
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient4160">
<stop
style="stop-color:#414141;stop-opacity:1"
offset="0"
id="stop4162" />
<stop
id="stop4172"
offset="0.44465271"
style="stop-color:#a5a5a5;stop-opacity:1" />
<stop
id="stop4168"
offset="0.51481694"
style="stop-color:#cccccc;stop-opacity:1" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop4164" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4160"
id="linearGradient4166"
x1="218.28458"
y1="950.68616"
x2="213.38019"
y2="742.12012"
gradientUnits="userSpaceOnUse" />
</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
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 />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-652.36221)">
<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"
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"
id="path4157"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cczcc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

171
etiquette/static/common.css Normal file
View File

@ -0,0 +1,171 @@
body
{
display: flex;
flex-direction: column;
background-color:#00d8f4;
margin: 8px;
}
li
{
position:relative;
}
#header
{
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
margin-bottom: 4px;
}
.header_element
{
display: flex;
justify-content: center;
flex: 1;
background-color: rgba(0, 0, 0, 0.1);
}
.header_element:hover
{
background-color: #ffffd4;
}
#content_body
{
flex: 0 0 auto;
display: flex;
flex-direction: row;
}
.add_tag_button, #search_go_button
{
border-top: 2px solid #c2ffc3;
border-left: 2px solid #c2ffc3;
border-right: 2px solid #259427;
border-bottom: 2px solid #259427;
background-color: #6df16f;
}
.add_tag_button:active, #search_go_button:active
{
border-top: 2px solid #259427;
border-left: 2px solid #259427;
border-right: 2px solid #c2ffc3;
border-bottom: 2px solid #c2ffc3;
}
.remove_tag_button,
.remove_tag_button_perm
{
position: absolute;
top: 3px;
width: 18px;
height: 14px;
padding: 0;
vertical-align: middle;
background-color: #ff4949;
border-top: 2px solid #ffacac;
border-left: 2px solid #ffacac;
border-right: 2px solid #bd1b1b;
border-bottom: 2px solid #bd1b1b;
}
.remove_tag_button
{
display: none;
}
.tag_object:hover + .remove_tag_button,
.remove_tag_button:hover,
.remove_tag_button_perm:hover
{
display:inline;
}
.remove_tag_button:active,
.remove_tag_button_perm:active
{
border-top: 2px solid #bd1b1b;
border-left: 2px solid #bd1b1b;
border-right: 2px solid #ffacac;
border-bottom: 2px solid #ffacac;
}
.photo_galleryview
{
vertical-align: middle;
position: relative;
box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25);
display: inline-block;
min-width: 150px;
max-width: 300px;
height: 200px;
background-color: #ffffd4;
padding: 8px;
margin: 8px;
border-radius: 8px;
}
.photo_galleryview_thumb
{
display:flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 150px;
}
.photo_galleryview_thumb a
{
display:flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 150px;
}
.photo_galleryview_thumb img
{
max-width: 100%;
max-height: 100%;
height: auto;
}
.photo_galleryview_info
{
position: absolute;
top: 160px;
bottom: 0px;
left: 8px;
right: 8px;
font-size: 0.8em;
}
.photo_galleryview_info a
{
position: absolute;
max-height: 30px;
overflow: hidden;
}
.photo_galleryview_info a:hover
{
max-height: 100%;
}
.photo_galleryview_info span
{
position: absolute;
bottom: 0;
right: 0;
}
.tag_object
{
font-size: 0.9em;
background-color: #fff;
border-radius: 2px;
text-decoration: none;
font-family: monospace;
line-height: 1.3;
}
.callback_message_positive, .callback_message_negative
{
width: 80%;
margin: 4px;
}
.callback_message_positive
{
background-color: #afa;
}
.callback_message_negative
{
background-color: #faa;
}

View File

@ -0,0 +1,84 @@
function create_message_bubble(message_positivity, message_text, lifespan)
{
if (lifespan === undefined)
{
lifespan = 8000;
}
var message = document.createElement("div");
message.className = message_positivity;
var span = document.createElement("span");
span.innerHTML = message_text;
message.appendChild(span);
message_area.appendChild(message);
setTimeout(function(){message_area.removeChild(message);}, lifespan);
}
function add_photo_tag(photoid, tagname, callback)
{
if (tagname === ""){return}
var url = "/photo/" + photoid;
data = new FormData();
data.append("add_tag", tagname);
return post(url, data, callback);
}
function remove_photo_tag(photoid, tagname, callback)
{
if (tagname === ""){return}
var url = "/photo/" + photoid;
data = new FormData();
data.append("remove_tag", tagname);
return post(url, data, callback);
}
function edit_tags(action, name, callback)
{
if (name === ""){return}
var url = "/tags";
data = new FormData();
data.append(action, name);
return post(url, data, callback);
}
function delete_tag_synonym(name, callback)
{
return edit_tags("delete_tag_synonym", name, callback);
}
function delete_tag(name, callback)
{
return edit_tags("delete_tag", name, callback);
}
function create_tag(name, callback)
{
return edit_tags("create_tag", name, callback);
}
function post(url, data, callback)
{
var request = new XMLHttpRequest();
request.answer = null;
request.onreadystatechange = function()
{
if (request.readyState == 4)
{
var text = request.responseText;
if (callback != null)
{
console.log(text);
callback(JSON.parse(text));
}
}
};
var asynchronous = true;
request.open("POST", url, asynchronous);
request.send(data);
}
function bind_box_to_button(box, button)
{
box.onkeydown=function()
{
if (event.keyCode == 13)
{
button.click();
}
};
}

View File

@ -0,0 +1,403 @@
/**
* complete.ly 1.0.0
* MIT Licensing
* Copyright (c) 2013 Lorenzo Puccetti
*
* This Software shall be used for doing good things, not bad things.
*
**/
function completely(container, config) {
config = config || {};
config.fontSize = config.fontSize || '16px';
config.fontFamily = config.fontFamily || 'sans-serif';
config.promptInnerHTML = config.promptInnerHTML || '';
config.color = config.color || '#333';
config.hintColor = config.hintColor || '#aaa';
config.backgroundColor = config.backgroundColor || '#fff';
config.dropDownBorderColor = config.dropDownBorderColor || '#aaa';
config.dropDownZIndex = config.dropDownZIndex || '100'; // to ensure we are in front of everybody
config.dropDownOnHoverBackgroundColor = config.dropDownOnHoverBackgroundColor || '#ddd';
var txtInput = document.createElement('input');
txtInput.type ='text';
txtInput.spellcheck = false;
txtInput.style.fontSize = config.fontSize;
txtInput.style.fontFamily = config.fontFamily;
txtInput.style.color = config.color;
txtInput.style.backgroundColor = config.backgroundColor;
txtInput.style.width = '100%';
txtInput.style.outline = '0';
txtInput.style.border = '0';
txtInput.style.margin = '0';
txtInput.style.padding = '0';
var txtHint = txtInput.cloneNode();
txtHint.disabled='';
txtHint.style.position = 'absolute';
txtHint.style.top = '0';
txtHint.style.left = '0';
txtHint.style.borderColor = 'transparent';
txtHint.style.boxShadow = 'none';
txtHint.style.color = config.hintColor;
txtInput.style.backgroundColor ='transparent';
txtInput.style.verticalAlign = 'top';
txtInput.style.position = 'relative';
var wrapper = document.createElement('div');
wrapper.style.position = 'relative';
wrapper.style.outline = '0';
wrapper.style.border = '0';
wrapper.style.margin = '0';
wrapper.style.padding = '0';
var prompt = document.createElement('div');
prompt.style.position = 'absolute';
prompt.style.outline = '0';
prompt.style.margin = '0';
prompt.style.padding = '0';
prompt.style.border = '0';
prompt.style.fontSize = config.fontSize;
prompt.style.fontFamily = config.fontFamily;
prompt.style.color = config.color;
prompt.style.backgroundColor = config.backgroundColor;
prompt.style.top = '0';
prompt.style.left = '0';
prompt.style.overflow = 'hidden';
prompt.innerHTML = config.promptInnerHTML;
prompt.style.background = 'transparent';
if (document.body === undefined) {
throw 'document.body is undefined. The library was wired up incorrectly.';
}
document.body.appendChild(prompt);
var w = prompt.getBoundingClientRect().right; // works out the width of the prompt.
wrapper.appendChild(prompt);
prompt.style.visibility = 'visible';
prompt.style.left = '-'+w+'px';
wrapper.style.marginLeft= w+'px';
wrapper.appendChild(txtHint);
wrapper.appendChild(txtInput);
var dropDown = document.createElement('div');
dropDown.style.position = 'absolute';
dropDown.style.visibility = 'hidden';
dropDown.style.outline = '0';
dropDown.style.margin = '0';
dropDown.style.padding = '0';
dropDown.style.textAlign = 'left';
dropDown.style.fontSize = config.fontSize;
dropDown.style.fontFamily = config.fontFamily;
dropDown.style.backgroundColor = config.backgroundColor;
dropDown.style.zIndex = config.dropDownZIndex;
dropDown.style.cursor = 'default';
dropDown.style.borderStyle = 'solid';
dropDown.style.borderWidth = '1px';
dropDown.style.borderColor = config.dropDownBorderColor;
dropDown.style.overflowX= 'hidden';
dropDown.style.whiteSpace = 'pre';
dropDown.style.overflowY = 'scroll'; // note: this might be ugly when the scrollbar is not required. however in this way the width of the dropDown takes into account
var createDropDownController = function(elem) {
var rows = [];
var ix = 0;
var oldIndex = -1;
var onMouseOver = function() { this.style.outline = '1px solid #ddd'; }
var onMouseOut = function() { this.style.outline = '0'; }
var onMouseDown = function() { p.hide(); p.onmouseselection(this.__hint); }
var p = {
hide : function() { elem.style.visibility = 'hidden'; },
refresh : function(token, array) {
elem.style.visibility = 'hidden';
ix = 0;
elem.innerHTML ='';
var vph = (window.innerHeight || document.documentElement.clientHeight);
var rect = elem.parentNode.getBoundingClientRect();
var distanceToTop = rect.top - 6; // heuristic give 6px
var distanceToBottom = vph - rect.bottom -6; // distance from the browser border.
rows = [];
for (var i=0;i<array.length;i++) {
if (array[i].indexOf(token)!==0) { continue; }
var divRow =document.createElement('div');
divRow.style.color = config.color;
divRow.onmouseover = onMouseOver;
divRow.onmouseout = onMouseOut;
divRow.onmousedown = onMouseDown;
divRow.__hint = array[i];
divRow.innerHTML = token+'<b>'+array[i].substring(token.length)+'</b>';
rows.push(divRow);
elem.appendChild(divRow);
}
if (rows.length===0) {
return; // nothing to show.
}
if (rows.length===1 && token === rows[0].__hint) {
return; // do not show the dropDown if it has only one element which matches what we have just displayed.
}
if (rows.length<2) return;
p.highlight(0);
if (distanceToTop > distanceToBottom*3) { // Heuristic (only when the distance to the to top is 4 times more than distance to the bottom
elem.style.maxHeight = distanceToTop+'px'; // we display the dropDown on the top of the input text
elem.style.top ='';
elem.style.bottom ='100%';
} else {
elem.style.top = '100%';
elem.style.bottom = '';
elem.style.maxHeight = distanceToBottom+'px';
}
elem.style.visibility = 'visible';
},
highlight : function(index) {
if (oldIndex !=-1 && rows[oldIndex]) {
rows[oldIndex].style.backgroundColor = config.backgroundColor;
}
rows[index].style.backgroundColor = config.dropDownOnHoverBackgroundColor; // <-- should be config
oldIndex = index;
},
move : function(step) { // moves the selection either up or down (unless it's not possible) step is either +1 or -1.
if (elem.style.visibility === 'hidden') return ''; // nothing to move if there is no dropDown. (this happens if the user hits escape and then down or up)
if (ix+step === -1 || ix+step === rows.length) return rows[ix].__hint; // NO CIRCULAR SCROLLING.
ix+=step;
p.highlight(ix);
return rows[ix].__hint;//txtShadow.value = uRows[uIndex].__hint ;
},
onmouseselection : function() {} // it will be overwritten.
};
return p;
}
var dropDownController = createDropDownController(dropDown);
dropDownController.onmouseselection = function(text) {
txtInput.value = txtHint.value = leftSide+text;
rs.onChange(txtInput.value); // <-- forcing it.
registerOnTextChangeOldValue = txtInput.value; // <-- ensure that mouse down will not show the dropDown now.
setTimeout(function() { txtInput.focus(); },0); // <-- I need to do this for IE
}
wrapper.appendChild(dropDown);
container.appendChild(wrapper);
var spacer;
var leftSide; // <-- it will contain the leftSide part of the textfield (the bit that was already autocompleted)
function calculateWidthForText(text) {
if (spacer === undefined) { // on first call only.
spacer = document.createElement('span');
spacer.style.visibility = 'hidden';
spacer.style.position = 'fixed';
spacer.style.outline = '0';
spacer.style.margin = '0';
spacer.style.padding = '0';
spacer.style.border = '0';
spacer.style.left = '0';
spacer.style.whiteSpace = 'pre';
spacer.style.fontSize = config.fontSize;
spacer.style.fontFamily = config.fontFamily;
spacer.style.fontWeight = 'normal';
document.body.appendChild(spacer);
}
// Used to encode an HTML string into a plain text.
// taken from http://stackoverflow.com/questions/1219860/javascript-jquery-html-encoding
spacer.innerHTML = String(text).replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return spacer.getBoundingClientRect().right;
}
var rs = {
onArrowDown : function() {}, // defaults to no action.
onArrowUp : function() {}, // defaults to no action.
onEnter : function() {}, // defaults to no action.
onTab : function() {}, // defaults to no action.
onChange: function() { rs.repaint() }, // defaults to repainting.
startFrom: 0,
options: [],
wrapper : wrapper, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations)
input : txtInput, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations)
hint : txtHint, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations)
dropDown : dropDown, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations)
prompt : prompt,
setText : function(text) {
txtHint.value = text;
txtInput.value = text;
},
getText : function() {
return txtInput.value;
},
hideDropDown : function() {
dropDownController.hide();
},
repaint : function() {
var text = txtInput.value;
var startFrom = rs.startFrom;
var options = rs.options;
var optionsLength = options.length;
// breaking text in leftSide and token.
var token = text.substring(startFrom);
leftSide = text.substring(0,startFrom);
// updating the hint.
txtHint.value ='';
for (var i=0;i<optionsLength;i++) {
var opt = options[i];
if (opt.indexOf(token)===0) { // <-- how about upperCase vs. lowercase
txtHint.value = leftSide +opt;
break;
}
}
// moving the dropDown and refreshing it.
dropDown.style.left = calculateWidthForText(leftSide)+'px';
dropDownController.refresh(token, rs.options);
}
};
var registerOnTextChangeOldValue;
/**
* Register a callback function to detect changes to the content of the input-type-text.
* Those changes are typically followed by user's action: a key-stroke event but sometimes it might be a mouse click.
**/
var registerOnTextChange = function(txt, callback) {
registerOnTextChangeOldValue = txt.value;
var handler = function() {
var value = txt.value;
if (registerOnTextChangeOldValue !== value) {
registerOnTextChangeOldValue = value;
callback(value);
}
};
//
// For user's actions, we listen to both input events and key up events
// It appears that input events are not enough so we defensively listen to key up events too.
// source: http://help.dottoro.com/ljhxklln.php
//
// The cost of listening to three sources should be negligible as the handler will invoke callback function
// only if the text.value was effectively changed.
//
//
if (txt.addEventListener) {
txt.addEventListener("input", handler, false);
txt.addEventListener('keyup', handler, false);
txt.addEventListener('change', handler, false);
} else { // is this a fair assumption: that attachEvent will exist ?
txt.attachEvent('oninput', handler); // IE<9
txt.attachEvent('onkeyup', handler); // IE<9
txt.attachEvent('onchange',handler); // IE<9
}
};
registerOnTextChange(txtInput,function(text) { // note the function needs to be wrapped as API-users will define their onChange
rs.onChange(text);
});
var keyDownHandler = function(e) {
e = e || window.event;
var keyCode = e.keyCode;
if (keyCode == 33) { return; } // page up (do nothing)
if (keyCode == 34) { return; } // page down (do nothing);
if (keyCode == 27) { //escape
dropDownController.hide();
txtHint.value = txtInput.value; // ensure that no hint is left.
txtInput.focus();
return;
}
if (keyCode == 39 || keyCode == 35 || keyCode == 9) { // right, end, tab (autocomplete triggered)
if (keyCode == 9) { // for tabs we need to ensure that we override the default behaviour: move to the next focusable HTML-element
e.preventDefault();
e.stopPropagation();
if (txtHint.value.length == 0) {
rs.onTab(); // tab was called with no action.
// users might want to re-enable its default behaviour or handle the call somehow.
}
}
if (txtHint.value.length > 0) { // if there is a hint
dropDownController.hide();
txtInput.value = txtHint.value;
var hasTextChanged = registerOnTextChangeOldValue != txtInput.value
registerOnTextChangeOldValue = txtInput.value; // <-- to avoid dropDown to appear again.
// for example imagine the array contains the following words: bee, beef, beetroot
// user has hit enter to get 'bee' it would be prompted with the dropDown again (as beef and beetroot also match)
if (hasTextChanged) {
rs.onChange(txtInput.value); // <-- forcing it.
}
}
return;
}
if (keyCode == 13) { // enter (autocomplete triggered)
if (txtHint.value.length == 0) { // if there is a hint
rs.onEnter();
} else {
var wasDropDownHidden = (dropDown.style.visibility == 'hidden');
dropDownController.hide();
if (wasDropDownHidden) {
txtHint.value = txtInput.value; // ensure that no hint is left.
txtInput.focus();
rs.onEnter();
return;
}
txtInput.value = txtHint.value;
var hasTextChanged = registerOnTextChangeOldValue != txtInput.value
registerOnTextChangeOldValue = txtInput.value; // <-- to avoid dropDown to appear again.
// for example imagine the array contains the following words: bee, beef, beetroot
// user has hit enter to get 'bee' it would be prompted with the dropDown again (as beef and beetroot also match)
if (hasTextChanged) {
rs.onChange(txtInput.value); // <-- forcing it.
}
}
return;
}
if (keyCode == 40) { // down
var m = dropDownController.move(+1);
if (m == '') { rs.onArrowDown(); }
txtHint.value = leftSide+m;
return;
}
if (keyCode == 38 ) { // up
var m = dropDownController.move(-1);
if (m == '') { rs.onArrowUp(); }
txtHint.value = leftSide+m;
e.preventDefault();
e.stopPropagation();
return;
}
// it's important to reset the txtHint on key down.
// think: user presses a letter (e.g. 'x') and never releases... you get (xxxxxxxxxxxxxxxxx)
// and you would see still the hint
txtHint.value =''; // resets the txtHint. (it might be updated onKeyUp)
};
if (txtInput.addEventListener) {
txtInput.addEventListener("keydown", keyDownHandler, false);
} else { // is this a fair assumption: that attachEvent will exist ?
txtInput.attachEvent('onkeydown', keyDownHandler); // IE<9
}
return rs;
}

View File

@ -0,0 +1,54 @@
<!DOCTYPE html5>
<html>
<head>
{% import "photo_object.html" as photo_object %}
{% import "header.html" as header %}
<title>Album {{album.title}}</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/common.css">
</head>
<style>
#content_body
{
/* overriding common.css here */
display: block;
}
</style>
<body>
{{header.make_header()}}
<div id="content_body">
<h2>{{album.title}}</h2>
{% set parent=album.parent() %}
{% if parent %}
<h3>Parent: <a href="/album/{{parent.id}}">{{parent.title}}</a></h3>
{% endif %}
{% if child_albums %}
<h3>Sub-albums</h3>
<ul>
{% for album in child_albums %}
<li><a href="/album/{{album.id}}">
{% if album.title %}
{{album.title}}
{% else %}
{{album.id}}
{% endif %}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% if photos %}
<h3>Photos</h3>
<ul>
{% for photo in photos %}
{{photo_object.galleryview(photo)}}
{% endfor %}
</ul>
{% endif %}
</div>
</body>
</html>
<script type="text/javascript">
</script>

View File

@ -0,0 +1,33 @@
<!DOCTYPE html5>
<html>
<head>
{% import "header.html" as header %}
<title>Albums</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/common.css">
</head>
<style>
#content_body
{
flex-direction: column;
}
</style>
<body>
{{header.make_header()}}
<div id="content_body">
{% for album in albums %}
{% if album.title %}
{% set title=album.id + " " + album.title %}
{% else %}
{% set title=album.id %}
{% endif %}
<a href="/album/{{album.id}}">{{title}}</a>
{% endfor %}
</div>
</body>
</html>
<script type="text/javascript">
</script>

View File

@ -0,0 +1,7 @@
{% macro make_header() %}
<div id="header">
<a class="header_element" href="/">Etiquette</a>
<a class="header_element" href="/search">Search</a>
<a class="header_element" href="/tags">Tags</a>
</div>
{% endmacro %}

162
etiquette/templates/pbackup Normal file
View File

@ -0,0 +1,162 @@
<!DOCTYPE html5>
<html>
<style>
#left
{
display: flex;
flex-direction: column;
justify-content: flex-start;
background-color: rgba(0, 0, 0, 0.1);
width: 300px;
}
#right
{
display: flex;
flex: 1;
}
#editor_area
{
flex: 3;
}
#message_area
{
overflow-y: auto;
flex: 1;
background-color: rgba(0, 0, 0, 0.1);
}
.photo_object
{
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
align-items: center;
}
.photo_object img
{
display: flex;
max-width: 100%;
max-height: 100%;
height: auto;
}
.photo_object audio
{
width: 100%;
}
.photo_object video
{
max-width: 100%;
max-height: 100%;
width: 100%;
}
</style>
<head>
{% import "header.html" as header %}
<title>Photo</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/common.css">
<script src="/static/common.js"></script>
</head>
<body>
{{header.make_header()}}
<div id="content_body">
<div id="left">
<div id="editor_area">
<!-- TAG INFO -->
<h4><a href="/tags">Tags</a></h4>
<ul id="this_tags">
{% for tag in tags %}
<li>
<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>
</li>
{% endfor %}
<li>
<input id="add_tag_textbox" type="text" autofocus>
<button id="add_tag_button" class="add_tag_button" onclick="submit_tag(receive_callback);">add</button>
</li>
</ul>
<!-- METADATA & DOWNLOAD -->
<h4>File info</h4>
<ul id="metadata">
{% if photo.width %}
<li>{{photo.width}}x{{photo.height}} px</li>
<li>{{photo.ratio}} aspect ratio</li>
<li>{{photo.bytestring()}}</li>
{% 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.basename}}"</a></li>
</ul>
<!-- CONTAINING ALBUMS -->
{% set albums=photo.albums() %}
{% if albums %}
<h4>Albums containing this photo</h4>
<ul id="containing albums">
{% for album in albums %}
<li><a href="/album/{{album.id}}">{{album.title}}</a></li>
{% endfor %}
{% endif %}
</ul>
</div>
<div id="message_area">
</div>
</div>
<div id="right">
<div class="photo_object">
{% set filename = photo.id + "." + photo.extension %}
{% set link = "/file/" + filename %}
{% set mimetype=photo.mimetype() %}
{% if mimetype == "image" %}
<img src="{{link}}">
{% elif mimetype == "video" %}
<video src="{{link}}" controls preload=none {%if photo.thumbnail%}poster="/thumbnail/{{photo.id}}.jpg"{%endif%}></video>
{% elif mimetype == "audio" %}
<audio src="{{link}}" controls></audio>
{% else %}
<a href="{{link}}">View {{filename}}</a>
{% endif %}
</div>
</div>
</div>
</body>
</html>
<script type="text/javascript">
var box = document.getElementById('add_tag_textbox');
var button = document.getElementById('add_tag_button');
var message_area = document.getElementById('message_area');
bind_box_to_button(box, button);
function receive_callback(response)
{
var tagname = response["tagname"];
if ("error" in response)
{
message_positivity = "callback_message_negative";
message_text = '"' + tagname + '" ' + response["error"];
}
else if ("action" in response)
{
var action = response["action"];
message_positivity = "callback_message_positive";
if (action == "add_tag")
{
message_text = "Added tag " + tagname;
}
else if (action == "remove_tag")
{
message_text = "Removed tag " + tagname;
}
}
create_message_bubble(message_positivity, message_text, 8000);
}
function submit_tag(callback)
{
add_photo_tag('{{photo.id}}', box.value, callback);
box.value='';
}
</script>

View File

@ -0,0 +1,176 @@
<!DOCTYPE html5>
<html>
<head>
{% import "header.html" as header %}
<title>Photo</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/common.css">
<script src="/static/common.js"></script>
</head>
<style>
#content_body
{
/* Override common.css */
flex: 1;
height: 100%;
}
#left
{
display: flex;
flex-direction: column;
justify-content: flex-start;
background-color: rgba(0, 0, 0, 0.1);
width: 300px;
}
#right
{
display: flex;
flex: 1;
height: 100%;
}
#editor_area
{
flex: 3;
}
#message_area
{
overflow-y: auto;
flex: 1;
background-color: rgba(0, 0, 0, 0.1);
}
.photo_object
{
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
.photo_object a
{
display: flex;
justify-content: center;
align-items: center;
max-width: 100%;
max-height: 100%;
}
.photo_object img
{
height: auto;
height: 100%;
}
.photo_object audio
{
width: 100%;
}
.photo_object video
{
max-width: 100%;
max-height: 100%;
width: 100%;
}
</style>
<body>
{{header.make_header()}}
<div id="content_body">
<div id="left">
<div id="editor_area">
<!-- TAG INFO -->
<h4>Tags</h4>
<ul id="this_tags">
{% for tag in tags %}
<li>
<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>
</li>
{% endfor %}
<li>
<input id="add_tag_textbox" type="text" autofocus>
<button id="add_tag_button" class="add_tag_button" onclick="submit_tag(receive_callback);">add</button>
</li>
</ul>
<!-- METADATA & DOWNLOAD -->
<h4>File info</h4>
<ul id="metadata">
{% if photo.width %}
<li>{{photo.width}}x{{photo.height}} px</li>
<li>{{photo.ratio}} aspect ratio</li>
<li>{{photo.bytestring()}}</li>
{% 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.basename}}"</a></li>
</ul>
<!-- CONTAINING ALBUMS -->
{% set albums=photo.albums() %}
{% if albums %}
<h4>Albums containing this photo</h4>
<ul id="containing albums">
{% for album in albums %}
<li><a href="/album/{{album.id}}">{{album.title}}</a></li>
{% endfor %}
{% endif %}
</ul>
</div>
<div id="message_area">
</div>
</div>
<div id="right">
<div class="photo_object">
{% set filename = photo.id + "." + photo.extension %}
{% set link = "/file/" + filename %}
{% set mimetype=photo.mimetype() %}
{% if mimetype == "image" %}
<a target="_blank" href="{{link}}"><img src="{{link}}"></a>
{% elif mimetype == "video" %}
<video src="{{link}}" controls preload=none {%if photo.thumbnail%}poster="/thumbnail/{{photo.id}}.jpg"{%endif%}></video>
{% elif mimetype == "audio" %}
<audio src="{{link}}" controls></audio>
{% else %}
<a href="{{link}}">View {{filename}}</a>
{% endif %}
</div>
</div>
</div>
</body>
</html>
<script type="text/javascript">
var box = document.getElementById('add_tag_textbox');
var button = document.getElementById('add_tag_button');
var message_area = document.getElementById('message_area');
bind_box_to_button(box, button);
function receive_callback(response)
{
var tagname = response["tagname"];
if ("error" in response)
{
message_positivity = "callback_message_negative";
message_text = '"' + tagname + '" ' + response["error"];
}
else if ("action" in response)
{
var action = response["action"];
message_positivity = "callback_message_positive";
if (action == "add_tag")
{
message_text = "Added tag " + tagname;
}
else if (action == "remove_tag")
{
message_text = "Removed tag " + tagname;
}
}
create_message_bubble(message_positivity, message_text, 8000);
}
function submit_tag(callback)
{
add_photo_tag('{{photo.id}}', box.value, callback);
box.value='';
}
</script>

View File

@ -0,0 +1,30 @@
{% macro galleryview(photo) %}
<div class="photo_galleryview">
<div class="photo_galleryview_thumb">
<a target="_blank" href="/photo/{{photo.id}}">
<img height="150"
{% if photo.thumbnail %}
src="/thumbnail/{{photo.id}}.jpg"
{% else %}
{% set mimetype = photo.mimetype() %}
{% if mimetype == 'video' %}
src="/static/basic_thumbnails/video.png"
{% elif mimetype == 'audio' %}
src="/static/basic_thumbnails/audio.png"
{% else %}
src="/static/basic_thumbnails/other.png"
{% endif %}
{% endif %}
</a>
</div>
<div class="photo_galleryview_info">
<a target="_blank" href="/photo/{{photo.id}}">{{photo.basename}}</a>
<span>
{% if photo.width %}
{{photo.width}}x{{photo.height}}
{% endif %}
{{photo.bytestring()}}
</span>
</div>
</div>
{% endmacro %}

View File

@ -0,0 +1,40 @@
<!DOCTYPE html5>
<html>
<style>
body, a
{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
a
{
width: 50%;
height: 40px;
background-color: #ffffd4;
margin: 8px;
}
</style>
<head>
<title>Etiquette</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/common.css">
</head>
<body>
<span>{{motd}}</span>
<a href="/search">Search</a>
<a href="/tags">Browse tags</a>
<a href="/albums">Browse albums</a>
</body>
</html>
<script type="text/javascript">
</script>

View File

@ -0,0 +1,463 @@
<!DOCTYPE html5>
<html>
<head>
{% import "photo_object.html" as photo_object %}
{% import "header.html" as header %}
<title>Search</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/common.css">
<script src="/static/common.js"></script>
</head>
<style>
form
{
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
#error_message_area
{
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
}
.search_warning
{
align-self: center;
color: #fff;
background-color: #f00;
padding: 2px;
}
#left, #right
{
/* Keep the prev-next buttons from scraping the floor */
padding-bottom: 30px;
}
#left
{
background-color: rgba(0, 0, 0, 0.1);
width: 300px;
padding: 8px;
}
#right
{
flex: 1;
width: auto;
}
.prev_page
{
float: left;
}
.next_page
{
float: right;
}
.prev_page, .next_page
{
display: inline-block;
width: 48%;
height: auto;
background-color: #ffffd4;
font-size: 20;
border: 1px solid black;
}
.search_builder_tagger,
#search_builder_orderby_ul
{
margin: 0;
}
</style>
{% macro prev_next_buttons() %}
{% if prev_page_url or next_page_url %}
<div class="prev_next_holder">
<center>
{% if prev_page_url %}
<a class="prev_page" href="{{prev_page_url}}">Previous</a>
{% else %}
<a class="prev_page"><br></a>
{% endif %}
{% if next_page_url %}
<a class="next_page" href="{{next_page_url}}">Next</a>
{% else %}
<a class="next_page"><br></a>
{% endif %}
</center>
</div>
{% endif %}
{% endmacro %}
{% macro create_orderby_li(selected_column, selected_sorter) %}
<li class="search_builder_orderby_li">
<select>
<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="width" {%if selected_column=="width"%}selected{%endif%} >Width</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="bytes" {%if selected_column=="bytes"%}selected{%endif%} >File size</option>
<option value="length" {%if selected_column=="length"%}selected{%endif%} >Duration</option>
<option value="random" {%if selected_column=="random"%}selected{%endif%} >Random</option>
</select>
<select>
<option value="desc" {%if selected_sorter=="desc"%}selected{%endif%} >Descending</option>
<option value="asc" {%if selected_sorter=="asc"%}selected{%endif%} >Ascending</option>
</select>
<button class="remove_tag_button_perm" onclick="orderby_remove_hook(this);"></button>
</li>
{% endmacro %}
<body>
{{header.make_header()}}
<div id="error_message_area">
{% for warn in warns %}
<span class="search_warning">{{warn}}</span>
{% endfor %}
</div>
<div id="content_body">
<div id="left">
{% for tagtype in ["musts", "mays", "forbids"] %}
<div id="search_builder_{{tagtype}}" {% if search_kwargs["tag_expression"]%}style="display:none"{%endif%}>
<span>Tag {{tagtype}}:</span>
<ul class="search_builder_tagger">
{% set key="tag_" + tagtype %}
{% for tagname in search_kwargs[key] %}
<li class="search_builder_{{tagtype}}_inputted">
<span class="tag_object">{{tagname}}</span>
<button class="remove_tag_button"
onclick="remove_searchtag(this, '{{tagname}}', inputted_{{tagtype}});"></button>
</li>
{% endfor %}
<li><input id="search_builder_{{tagtype}}_input" type="text"></li>
</ul>
</div>
{% endfor %}
<div id="search_builder_expression" {% if not search_kwargs["tag_expression"]%}style="display:none"{%endif%}>
<span>Tag Expression:</span>
<input id="search_builder_expression_input" name="tag_expression" type="text"
{% if search_kwargs["tag_expression"] %}
value="{{search_kwargs["tag_expression"]}}"
{% endif %}
>
</div>
<div id="search_builder_orderby">
<span>Order by</span>
<ul id="search_builder_orderby_ul">
{% if "orderby" in search_kwargs and search_kwargs["orderby"] %}
{% for orderby in search_kwargs["orderby"] %}
{% set column=orderby.split(" ")[0] %}
{% set sorter=orderby.split(" ")[1] %}
{{ create_orderby_li(selected_column=column, selected_sorter=sorter) }}
{% endfor %}
{% else %}
{{ create_orderby_li(selected_column=0, selected_sorter=0) }}
{% endif %}
<li id="search_builder_orderby_newrow"><button class="add_tag_button" onclick="add_new_orderby()">+</button></li>
</ul>
</div>
<br>
<form id="search_builder_form" action="" onsubmit="return submit_search();">
<input type="text"
value="{%if search_kwargs['mimetype']%}{{search_kwargs['mimetype']}}{%endif%}"
name="mimetype" placeholder="Mimetype(s)">
<input type="text"
value="{%if search_kwargs['extension']%}{{search_kwargs['extension']}}{%endif%}"
name="extension" placeholder="Extension(s)">
<input type="text"
value="{%if search_kwargs['extension_not']%}{{search_kwargs['extension_not']}}{%endif%}"
name="extension_not" placeholder="Forbid extension(s)">
<select name="limit">
<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="100" {%if search_kwargs['limit'] == 100%}selected{%endif%}>100 items</option>
</select>
<select name="has_tags">
<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="no" {%if search_kwargs['has_tags'] == False %}selected{%endif%}>Untagged only</option>
</select>
<button type="submit" id="search_go_button" value="">Search</button>
</form>
{% if total_tags %}
<span>Tags on this page (click to join query):</span>
<ul>
{% for tag in total_tags %}
<li><a href="" class="tag_object">{{tag._cached_qualname}}</a></li>
{% endfor %}
</ul>
{% endif %}
</div>
<div id="right">
<p>You got {{photos|length}} items</p><br>
{{prev_next_buttons()}}
{% for photo in photos %}
{{photo_object.galleryview(photo)}}
{% endfor %}
{{prev_next_buttons()}}
</div>
</div>
</body>
</html>
<script type="text/javascript">
function add_searchtag(box, value, inputted_list, li_class)
{
/*
Called by hitting Enter within a must/may/forbid field. Checks whether the tag exists
and adds it to the query.
*/
console.log("adding " + value);
var already_have = false;
// We're going to be doing some in-place splicing to remove,
// so make a duplicate for iterating
existing_tags = Array.from(inputted_list);
for (var index = 0; index < existing_tags.length; index += 1)
{
existing_tag = existing_tags[index];
if (existing_tag == value)
{
already_have = true;
}
else if (existing_tag.startsWith(value + ".") || value.startsWith(existing_tag + "."))
{
remove_searchtag(box, existing_tag, inputted_list);
}
}
if (!already_have)
{
inputted_list.push(value);
var new_li = document.createElement("li");
new_li.className = li_class;
var new_span = document.createElement("span");
new_span.className = "tag_object";
new_span.innerHTML = value;
var new_delbutton = document.createElement("button")
new_delbutton.className = "remove_tag_button";
new_delbutton.onclick = function(){remove_searchtag(new_delbutton, value, inputted_list)};
new_li.appendChild(new_span);
new_li.appendChild(new_delbutton);
box_li = box.parentElement;
ul = box_li.parentElement;
ul.insertBefore(new_li, box_li);
}
}
function remove_searchtag(li_member, value, inputted_list)
{
/*
Given a member of the same tag type as the one we intend to remove,
find the tag of interest and remove it from both the DOM and the
inputted_list.
Sorry for the roundabout technique.
*/
console.log("removing " + value);
var li = li_member.parentElement;
var ul = li.parentElement;
var lis = ul.children;
//console.log(lis);
for (var index = 0; index < lis.length; index += 1)
{
li = lis[index];
var span = li.children[0];
if (span.tagName != "SPAN")
{continue}
var tagname = span.innerHTML;
if (tagname != value)
{continue}
ul.removeChild(li);
splice_at = inputted_list.indexOf(tagname);
if (splice_at == -1)
{continue}
inputted_list.splice(splice_at, 1);
}
}
function add_new_orderby()
{
/* Called by the green + button */
var ul = document.getElementById("search_builder_orderby_ul");
if (ul.children.length >= 8)
{
/* 8 because there are only 8 sortable properties */
return;
}
var li = ul.children;
li = li[li.length - 2];
var clone_children = true;
var new_li = li.cloneNode(clone_children)
var button = document.getElementById("search_builder_orderby_newrow");
ul.insertBefore(new_li, button);
}
function orderby_remove_hook(button)
{
/* Called by the red button next to orderby dropdowns */
var li = button.parentElement;
var ul = li.parentElement;
// 2 because keep 1 row and the adder button
if (ul.children.length>2)
{
/* You can't remove the only one left */
ul.removeChild(li);
}
}
function submit_search()
{
/*
Gather up all the form data and tags and compose the search URL
*/
var url = window.location.origin + "/search";
var parameters = [];
var has_tag_params = false;
var musts = inputted_musts.join(",");
if (musts) {parameters.push("tag_musts=" + musts); has_tag_params=true;}
var mays = inputted_mays.join(",");
if (mays) {parameters.push("tag_mays=" + mays); has_tag_params=true;}
var forbids = inputted_forbids.join(",");
if (forbids) {parameters.push("tag_forbids=" + forbids); has_tag_params=true;}
var expression = document.getElementsByName("tag_expression")[0].value;
if (expression)
{
expression = expression.replace(new RegExp(" ", 'g'), "-");
parameters.push("tag_expression=" + expression);
has_tag_params=true;
}
var basic_inputs = ["mimetype", "extension", "extension_not", "limit", "has_tags"];
for (var index = 0; index < basic_inputs.length; index += 1)
{
var boxname = basic_inputs[index];
var box = document.getElementsByName(boxname)[0];
var value = box.value;
if (boxname == "has_tags" && has_tag_params && value == "no")
{
/*
The user wants untagged only, but has tags in the search boxes?
Override to "tagged or untagged" and let the tag searcher handle it.
*/
value = "";
}
if (value == "")
{
continue;
}
parameters.push(boxname + "=" + box.value);
}
orderby_rows = document.getElementsByClassName("search_builder_orderby_li");
orderby_params = [];
for (var index = 0; index < orderby_rows.length; index += 1)
{
var row = orderby_rows[index];
var column = row.children[0].value;
var sorter = row.children[1].value;
orderby_params.push(column + "-" + sorter);
}
orderby_params = orderby_params.join(",");
if (orderby_params)
{
parameters.push("orderby=" + orderby_params);
}
if (parameters)
{
parameters = parameters.join("&");
parameters = "?" + parameters;
url = url + parameters;
}
window.location.href = url;
return false;
}
function tags_on_this_page_hook()
{
/*
This is hooked onto the tag objects listed under "Found on this page".
Clicking them will add it to your current search query under Musts
*/
add_searchtag(
input_musts,
this.innerHTML,
inputted_musts,
"search_builder_musts_inputted"
);
return false;
}
function tag_input_hook(box, inputted_list, li_class)
{
/*
Assigned to the input boxes for musts, mays, forbids.
Hitting Enter will add the resovled tag to the search form.
*/
if (event.keyCode != 13)
{return;}
if (!box.value)
{return;}
var value = box.value.toLocaleLowerCase();
value = value.split(".");
value = value[value.length-1];
value = value.split("+")[0];
value = value.replace(" ", "_");
value = value.replace("-", "_");
if (!(value in QUALNAME_MAP))
{
return;
}
value = QUALNAME_MAP[value];
console.log(inputted_list);
add_searchtag(box, value, inputted_list, li_class)
box.value = "";
}
QUALNAME_MAP = {{qualname_map|safe}};
var input_musts = document.getElementById("search_builder_musts_input");
var input_mays = document.getElementById("search_builder_mays_input");
var input_forbids = document.getElementById("search_builder_forbids_input");
var input_expression = document.getElementById("search_builder_expression_input");
/* Prefix the form with the parameters from last search */
var inputted_musts = [];
var inputted_mays = [];
var inputted_forbids = [];
{% for tagtype in ["musts", "mays", "forbids"] %}
{% set key="tag_" + tagtype %}
{% for tagname in search_kwargs[key] %}
inputted_{{tagtype}}.push("{{tagname}}");
{% endfor %}
{% endfor %}
/* Assign the click handler to "Tags on this page" results. */
var found_on_page = document.getElementsByClassName("tag_object");
for (var index = 0; index < found_on_page.length; index += 1)
{
var tag_object = found_on_page[index];
if (tag_object.tagName != "A")
{continue}
tag_object.onclick = tags_on_this_page_hook;
}
input_musts.onkeydown = function(){tag_input_hook(this, inputted_musts, "search_builder_musts_inputted")};
input_mays.onkeydown = function(){tag_input_hook(this, inputted_mays, "search_builder_mays_inputted")};
input_forbids.onkeydown = function(){tag_input_hook(this, inputted_forbids, "search_builder_forbids_inputted")};
bind_box_to_button(input_expression, document.getElementById("search_go_button"));
</script>

View File

@ -0,0 +1,142 @@
<!DOCTYPE html5>
<html>
<head>
{% import "header.html" as header %}
<title>Tags</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/common.css">
<script src="/static/common.js"></script>
</head>
<style>
body
{
display:flex;
}
#left
{
height:100%;
}
#left
{
flex: 1;
height: auto;
}
#right
{
background-color: rgba(0, 0, 0, 0.1);
width: 300px;
position: fixed;
right: 8px;
bottom: 8px;
top: 30px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#editor_area
{
flex: 1;
display: flex;
flex-direction: row;
justify-content center;
align-items: center;
}
#message_area
{
overflow-y: auto;
background-color: rgba(0, 0, 0, 0.1);
width: 100%;
height: 100%;
flex: 2;
display: flex;
flex-direction: column;
justify-content center;
align-items: center;
}
</style>
<body>
{{header.make_header()}}
<div id="content_body">
<div id="left">
<ul>
{% for tag in tags %}
<li>
<a class="tag_object" href="/search?tag_musts={{tag[1]}}">{{tag[0]}}</a>
{% if "+" in tag[0] %}
<button class="remove_tag_button" onclick="delete_tag_synonym('{{tag[0]}}', receive_callback);"></button>
{% else %}
<button class="remove_tag_button" onclick="delete_tag('{{tag[0]}}', receive_callback);"></button>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
<div id="right">
<div id="editor_area">
<input type="text" id="add_tag_textbox" autofocus>
<button class="add_tag_button" id="add_tag_button" onclick="submit_tag(receive_callback);">add</button>
</div>
<div id="message_area">
</div>
</div>
</div>
</body>
</html>
<script type="text/javascript">
var box = document.getElementById('add_tag_textbox');
var button = document.getElementById('add_tag_button');
var message_area = document.getElementById('message_area');
bind_box_to_button(box, button);
function receive_callback(responses)
{
if (!(responses instanceof Array))
{
responses = [responses];
}
for (var index = 0; index < responses.length; index += 1)
{
var response = responses[index];
var tagname = response["tagname"];
if ("error" in response)
{
message_positivity = "callback_message_negative";
message_text = '"' + tagname + '" ' + response["error"];
}
else if ("action" in response)
{
var action = response["action"];
message_positivity = "callback_message_positive";
if (action == "new_tag")
{message_text = "Created tag " + tagname;}
else if (action == "existing_tag")
{message_text = "Existing tag " + tagname;}
else if (action == "join_group")
{message_text = "Grouped " + tagname;}
else if (action == "rename")
{message_text = "Renamed " + tagname;}
else if (action == "delete_tag")
{message_text = "Deleted tag " + tagname;}
else if (action == "delete_synonym")
{message_text = "Deleted synonym " + response["synonym"];}
}
create_message_bubble(message_positivity, message_text, 8000);
}
}
function submit_tag(callback)
{
create_tag(box.value, callback);
box.value='';
}
</script>

View File

@ -0,0 +1,4 @@
<form method="POST">
<input type="text" name="phone_number">
<input type="submit">
</form>

View File

@ -0,0 +1,208 @@
import os
import phototagger
import unittest
class PhotoDBTest(unittest.TestCase):
def setUp(self):
self.P = phototagger.PhotoDB(':memory:')
class AlbumTest(PhotoDBTest):
'''
Test the creation and properties of albums
'''
def test_create_album(self):
album = self.P.new_album()
test = self.P.get_album(album.id)
self.assertEqual(album, test)
album = self.P.new_album(title='test1', description='test2')
self.assertEqual(album.title, 'test1')
self.assertEqual(album.description, 'test2')
def test_delete_album_nonrecursive(self):
album = self.P.new_album()
album.delete()
self.assertRaises(phototagger.NoSuchAlbum, self.P.get_album, album.id)
def test_edit_album(self):
album = self.P.new_album(title='t1', description='d1')
album.edit(title='t2')
self.assertEqual(album.title, 't2')
self.assertEqual(album.description, 'd1')
album.edit(title='t3', description='d2')
self.assertEqual(album.title, 't3')
self.assertEqual(album.description, 'd2')
album.edit(description='d3')
album = self.P.get_album(album.id)
self.assertEqual(album.title, 't3')
self.assertEqual(album.description, 'd3')
class PhotoTest(PhotoDBTest):
'''
Test the creation and properties of photos
'''
def test_create_photo(self):
photo = self.P.new_photo('samples\\bolts.jpg')
self.assertGreater(photo.area, 1)
def test_delete_photo(self):
pass
def test_reload_metadata(self):
pass
class TagTest(PhotoDBTest):
'''
Test the creation and properties of tags
'''
def test_normalize_tagname(self):
tag = self.P.new_tag('test normalize')
self.assertEqual(tag.name, 'test_normalize')
tag = self.P.new_tag('TEST!!NORMALIZE')
self.assertEqual(tag.name, 'testnormalize')
self.assertRaises(phototagger.TagTooShort, self.P.new_tag, '')
self.assertRaises(phototagger.TagTooShort, self.P.new_tag, '!??*&')
self.assertRaises(phototagger.TagTooLong, self.P.new_tag, 'a'*(phototagger.MAX_TAG_NAME_LENGTH+1))
def test_create_tag(self):
tag = self.P.new_tag('test create tag')
self.assertEqual(tag.name, 'test_create_tag')
self.assertRaises(phototagger.TagExists, self.P.new_tag, 'test create tag')
def test_delete_tag_nonrecursive(self):
tag = self.P.new_tag('test delete tag non')
tag.delete()
self.assertRaises(phototagger.NoSuchTag, self.P.get_tag, tag.name)
def test_rename_tag(self):
tag = self.P.new_tag('test rename pre')
self.assertEqual(tag.name, 'test_rename_pre')
tag.rename('test rename post')
self.assertEqual(self.P.get_tag('test rename post'), tag)
self.assertRaises(phototagger.NoSuchTag, self.P.get_tag, 'test rename pre')
self.assertRaises(phototagger.TagTooShort, tag.rename, '??')
tag.rename(tag.name) # does nothing
class SearchTest(PhotoDBTest):
def search_extension(self):
pass
def search_minmaxers(self):
pass
def search_notags(self):
pass
def search_tags(self):
pass
class SynonymTest(PhotoDBTest):
'''
Test the creation and management of synonyms
'''
def test_create_synonym(self):
tag = self.P.new_tag('test create syn')
tag2 = self.P.new_tag('getting in the way')
tag.add_synonym('test make syn')
test = self.P.get_tag('test make syn')
self.assertEqual(test, tag)
self.assertTrue('test_make_syn' in tag.synonyms())
self.assertRaises(phototagger.TagExists, tag.add_synonym, 'test make syn')
def test_delete_synonym(self):
tag = self.P.new_tag('test del syn')
tag.add_synonym('test rem syn')
tag.remove_synonym('test rem syn')
self.assertRaises(phototagger.NoSuchSynonym, tag.remove_synonym, 'test rem syn')
def test_convert_tag_to_synonym(self):
tag1 = self.P.new_tag('convert 1')
tag2 = self.P.new_tag('convert 2')
tag2.convert_to_synonym(tag1)
test = self.P.get_tag(tag2)
self.assertEqual(test, tag1)
self.assertTrue('convert_2' in tag1.synonyms())
def test_get_synonyms(self):
tag = self.P.new_tag('test get syns')
tag.add_synonym('test get syns1')
tag.add_synonym('test get syns2')
tag.add_synonym('test get syns3')
self.assertEqual(len(tag.synonyms()), 3)
class AlbumGroupTest(PhotoDBTest):
'''
Test the relationships between albums as they form and leave groups
'''
def test_delete_album_recursive(self):
pass
def test_join_album(self):
pass
def test_leave_album(self):
pass
def test_album_children(self):
pass
def test_album_parents(self):
pass
class TagGroupTest(PhotoDBTest):
'''
Test the relationships between tags as they form and leave groups
'''
def test_delete_tag_recursive(self):
pass
def test_join_tag(self):
pass
def test_leave_tag(self):
pass
def test_tag_children(self):
pass
def test_tag_parents(self):
pass
def test_tag_qualified_name(self):
pass
class AlbumPhotoTest(PhotoDBTest):
'''
Test the relationships between albums and photos
'''
def test_add_photo(self):
pass
def test_remove_photo(self):
pass
class PhotoTagTest(PhotoDBTest):
'''
Test the relationships between photos and tags
'''
def test_photo_has_tag(self):
pass
def test_add_tag(self):
pass
def test_remove_tag(self):
pass
if __name__ == '__main__':
unittest.main()