checkpoint
48
.gitignore
vendored
Normal 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
|
@ -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
|
@ -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)
|
25
etiquette/etiquette_easy.py
Normal 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
|
25
etiquette/etiquette_launch.py
Normal 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
16
etiquette/reider.py
Normal 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
After Width: | Height: | Size: 467 KiB |
BIN
etiquette/samples/reddit.png
Normal file
After Width: | Height: | Size: 900 KiB |
BIN
etiquette/samples/train.jpg
Normal file
After Width: | Height: | Size: 321 KiB |
BIN
etiquette/static/basic_thumbnails/audio.png
Normal file
After Width: | Height: | Size: 63 KiB |
141
etiquette/static/basic_thumbnails/audio.svg
Normal 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 |
BIN
etiquette/static/basic_thumbnails/other.png
Normal file
After Width: | Height: | Size: 19 KiB |
92
etiquette/static/basic_thumbnails/other.svg
Normal 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 |
BIN
etiquette/static/basic_thumbnails/video.png
Normal file
After Width: | Height: | Size: 21 KiB |
100
etiquette/static/basic_thumbnails/video.svg
Normal 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
|
@ -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;
|
||||
}
|
84
etiquette/static/common.js
Normal 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();
|
||||
}
|
||||
};
|
||||
}
|
403
etiquette/static/complete.ly.1.0.1.js
Normal 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, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
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;
|
||||
}
|
54
etiquette/templates/album.html
Normal 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>
|
33
etiquette/templates/albums.html
Normal 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>
|
7
etiquette/templates/header.html
Normal 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
|
@ -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>
|
176
etiquette/templates/photo.html
Normal 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>
|
30
etiquette/templates/photo_object.html
Normal 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 %}
|
40
etiquette/templates/root.html
Normal 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>
|
463
etiquette/templates/search.html
Normal 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>
|
142
etiquette/templates/tags.html
Normal 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>
|
4
etiquette/templates/test.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<form method="POST">
|
||||
<input type="text" name="phone_number">
|
||||
<input type="submit">
|
||||
</form>
|
208
etiquette/test_phototagger.py
Normal 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()
|