add search view
This commit is contained in:
parent
6c4c1c609a
commit
39dbb2926d
@ -10,7 +10,7 @@ from photoapp.dbsession import DatabaseSession
|
||||
from photoapp.common import pwhash
|
||||
from photoapp.api import PhotosApi, LibraryManager
|
||||
from photoapp.dbutils import SAEnginePlugin, SATool, db, get_db_engine, date_format
|
||||
from photoapp.utils import auth, require_auth, photoset_auth_filter, slugify
|
||||
from photoapp.utils import auth, require_auth, photoset_auth_filter, slugify, cherryparam
|
||||
from photoapp.storage import uri_to_storage
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
from sqlalchemy import desc, func, and_, or_
|
||||
@ -26,6 +26,9 @@ def validate_password(realm, username, password):
|
||||
|
||||
|
||||
class PhotosWeb(object):
|
||||
"""
|
||||
Http root of the UI webserver
|
||||
"""
|
||||
def __init__(self, library, thumbtool, template_dir):
|
||||
self.library = library
|
||||
self.thumbtool = thumbtool
|
||||
@ -42,6 +45,7 @@ class PhotosWeb(object):
|
||||
self.date = DateView(self)
|
||||
self.tag = TagView(self)
|
||||
self.album = self.tag
|
||||
self.search = SearchView(self)
|
||||
|
||||
def render(self, template, **kwargs):
|
||||
"""
|
||||
@ -53,20 +57,18 @@ class PhotosWeb(object):
|
||||
"""
|
||||
Return a dict containing variables expected to be on every page
|
||||
"""
|
||||
# all tags / albums with photos visible under the current auth context
|
||||
tagq = db.query(Tag).join(TagItem).join(PhotoSet)
|
||||
if not auth():
|
||||
tagq = tagq.filter(PhotoSet.status == PhotoStatus.public)
|
||||
tagq = tagq.filter(Tag.is_album == False).order_by(Tag.name).all() # pragma: manual auth
|
||||
if auth():
|
||||
tagq = db.query(Tag).order_by(Tag.name).all()
|
||||
|
||||
albumq = db.query(Tag).join(TagItem).join(PhotoSet)
|
||||
if not auth():
|
||||
albumq = albumq.filter(PhotoSet.status == PhotoStatus.public)
|
||||
albumq = albumq.filter(Tag.is_album == True).order_by(Tag.name).all() # pragma: manual auth
|
||||
else:
|
||||
# for anonymous users, all tags with at least one album / photo marked visible
|
||||
tagq = db.query(Tag).join(TagItem).join(PhotoSet)
|
||||
if not auth():
|
||||
tagq = tagq.filter(PhotoSet.status == PhotoStatus.public)
|
||||
tagq = tagq.order_by(Tag.name).all() # pragma: manual auth
|
||||
|
||||
ret = {
|
||||
"all_tags": tagq,
|
||||
"all_albums": albumq,
|
||||
"path": cherrypy.request.path_info,
|
||||
"auth": auth(),
|
||||
"PhotoStatus": PhotoStatus
|
||||
@ -352,25 +354,25 @@ class TagView(object):
|
||||
def index(self, uuid, page=0):
|
||||
page = int(page)
|
||||
pgsize = 100
|
||||
if uuid == "untagged":
|
||||
numphotos = photoset_auth_filter(db.query(func.count(PhotoSet.id))). \
|
||||
filter(~PhotoSet.id.in_(db.query(TagItem.set_id))).scalar()
|
||||
photos = photoset_auth_filter(db.query(PhotoSet)).filter(~PhotoSet.id.in_(db.query(TagItem.set_id))). \
|
||||
order_by(PhotoSet.date.desc()). \
|
||||
offset(page * pgsize). \
|
||||
limit(pgsize).all()
|
||||
yield self.master.render("untagged.html", images=photos, total_items=numphotos, pgsize=pgsize, page=page)
|
||||
else:
|
||||
tag = db.query(Tag).filter(or_(Tag.uuid == uuid, Tag.slug == uuid)).first()
|
||||
numphotos = photoset_auth_filter(db.query(func.count(Tag.id)).join(TagItem).join(PhotoSet)). \
|
||||
filter(Tag.id == tag.id).scalar()
|
||||
photos = photoset_auth_filter(db.query(PhotoSet)).join(TagItem).join(Tag). \
|
||||
filter(Tag.id == tag.id). \
|
||||
order_by(PhotoSet.date.desc()). \
|
||||
offset(page * pgsize). \
|
||||
limit(pgsize).all()
|
||||
yield self.master.render("album.html", tag=tag, images=photos,
|
||||
total_items=numphotos, pgsize=pgsize, page=page)
|
||||
# if uuid == "untagged":
|
||||
# numphotos = photoset_auth_filter(db.query(func.count(PhotoSet.id))). \
|
||||
# filter(~PhotoSet.id.in_(db.query(TagItem.set_id))).scalar()
|
||||
# photos = photoset_auth_filter(db.query(PhotoSet)).filter(~PhotoSet.id.in_(db.query(TagItem.set_id))). \
|
||||
# order_by(PhotoSet.date.desc()). \
|
||||
# offset(page * pgsize). \
|
||||
# limit(pgsize).all()
|
||||
# yield self.master.render("untagged.html", images=photos, total_items=numphotos, pgsize=pgsize, page=page)
|
||||
# else:
|
||||
tag = db.query(Tag).filter(or_(Tag.uuid == uuid, Tag.slug == uuid)).first()
|
||||
numphotos = photoset_auth_filter(db.query(func.count(Tag.id)).join(TagItem).join(PhotoSet)). \
|
||||
filter(Tag.id == tag.id).scalar()
|
||||
photos = photoset_auth_filter(db.query(PhotoSet)).join(TagItem).join(Tag). \
|
||||
filter(Tag.id == tag.id). \
|
||||
order_by(PhotoSet.date.desc()). \
|
||||
offset(page * pgsize). \
|
||||
limit(pgsize).all()
|
||||
yield self.master.render("album.html", tag=tag, images=photos,
|
||||
total_items=numphotos, pgsize=pgsize, page=page)
|
||||
|
||||
@cherrypy.expose
|
||||
@require_auth
|
||||
@ -418,6 +420,158 @@ class TagView(object):
|
||||
yield self.master.render("tag_edit.html", tag=tag)
|
||||
|
||||
|
||||
FMT_TIME_SEARCH = "%Y/%m/%d %H.%M.%S"
|
||||
|
||||
|
||||
class SearchView(object):
|
||||
"""
|
||||
Search & operations view.
|
||||
Search -> View targets -> Apply actions
|
||||
"""
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
|
||||
@cherrypy.expose
|
||||
def index(
|
||||
self,
|
||||
page=0,
|
||||
pgsize=100,
|
||||
include_tags=None,
|
||||
exclude_tags=None,
|
||||
untagged=False,
|
||||
before=None,
|
||||
after=None,
|
||||
set_before=None,
|
||||
set_after=None,
|
||||
keywords_title=None,
|
||||
keywords_description=None,
|
||||
operation=None,
|
||||
add_tag=None,
|
||||
remove_tag=None,
|
||||
):
|
||||
page = int(page)
|
||||
pgsize = int(pgsize)
|
||||
|
||||
before = set_before or before
|
||||
after = set_after or after
|
||||
before = datetime.strptime(before, FMT_TIME_SEARCH) if before else None
|
||||
after = datetime.strptime(after, FMT_TIME_SEARCH) if after else None
|
||||
|
||||
include_tags = cherryparam(include_tags)
|
||||
exclude_tags = cherryparam(exclude_tags)
|
||||
add_tag = cherryparam(add_tag)
|
||||
remove_tag = cherryparam(remove_tag)
|
||||
|
||||
untagged = untagged in frozenset([True, 1, "1", "yes"])
|
||||
|
||||
include_tag_ids = []
|
||||
if include_tags:
|
||||
include_tag_ids = [i[0] for i in db.query(Tag.id).filter(Tag.uuid.in_(include_tags)).all()]
|
||||
|
||||
exclude_tag_ids = []
|
||||
if exclude_tags:
|
||||
exclude_tag_ids = [i[0] for i in db.query(Tag.id).filter(Tag.uuid.in_(exclude_tags)).all()]
|
||||
|
||||
# build image search query
|
||||
def build_query(base):
|
||||
base = photoset_auth_filter(base)
|
||||
|
||||
if before:
|
||||
base = base \
|
||||
.filter(PhotoSet.date <= before)
|
||||
|
||||
if after:
|
||||
base = base \
|
||||
.filter(PhotoSet.date >= after)
|
||||
|
||||
if include_tag_ids or exclude_tag_ids:
|
||||
base = base \
|
||||
.join(TagItem) \
|
||||
.join(Tag)
|
||||
|
||||
if include_tag_ids:
|
||||
base = base.filter(Tag.id.in_(include_tag_ids))
|
||||
|
||||
if exclude_tag_ids:
|
||||
base = base.filter(~PhotoSet.id.in_(
|
||||
db.query(PhotoSet.id)
|
||||
.join(TagItem)
|
||||
.join(Tag)
|
||||
.filter(Tag.id.in_(exclude_tag_ids))))
|
||||
|
||||
if untagged:
|
||||
base = base \
|
||||
.filter(~PhotoSet.id.in_(db.query(TagItem.set_id)))
|
||||
|
||||
if keywords_title:
|
||||
base = base \
|
||||
.filter(PhotoSet.title.like("%{}%".format(keywords_title)))
|
||||
|
||||
if keywords_description:
|
||||
base = base \
|
||||
.filter(PhotoSet.description.like("%{}%".format(keywords_description)))
|
||||
|
||||
base = base \
|
||||
.order_by(PhotoSet.date.desc())
|
||||
|
||||
return base
|
||||
|
||||
if add_tag:
|
||||
# this is extremely inefficient but i can't be arsed stepping outside of sqlalchemy
|
||||
# no replace into :-(
|
||||
tag_ids = set([r[0] for r in db.query(Tag.id).filter(Tag.uuid.in_(add_tag)).all()])
|
||||
|
||||
if tag_ids:
|
||||
for photo in build_query(db.query(PhotoSet)).all():
|
||||
# get tag ids for all tags on the photo
|
||||
has_tagids = set([
|
||||
r[0] for r in db.query(Tag.id).join(TagItem).filter(TagItem.set_id == photo.id).all()
|
||||
])
|
||||
# calculate which tags the image misses
|
||||
new_tags = tag_ids - has_tagids
|
||||
|
||||
# add each new tag to the image
|
||||
for new_tagid in new_tags:
|
||||
db.add(TagItem(tag_id=new_tagid, set_id=photo.id))
|
||||
db.commit()
|
||||
|
||||
if remove_tag:
|
||||
# this is not as bad as add_tag but could be better
|
||||
tag_ids = [r[0] for r in db.query(Tag.id).filter(Tag.uuid.in_(remove_tag)).all()]
|
||||
photo_ids = [r[0] for r in build_query(db.query(PhotoSet.id)).all()]
|
||||
|
||||
if tag_ids and photo_ids:
|
||||
db.query(TagItem) \
|
||||
.filter(TagItem.tag_id.in_(tag_ids)) \
|
||||
.filter(TagItem.set_id.in_(photo_ids)) \
|
||||
.delete(synchronize_session=False)
|
||||
db.commit()
|
||||
|
||||
# consequence of synchronize_session=False above
|
||||
# or is it Fine since we don't re-use any orm-mapped objects from above?
|
||||
db.expire_all()
|
||||
|
||||
total_sets = build_query(db.query(func.count(PhotoSet.id))).scalar()
|
||||
images = build_query(db.query(PhotoSet)).offset(pgsize * page).limit(pgsize).all()
|
||||
|
||||
yield self.master.render(
|
||||
"search.html",
|
||||
include_tags=include_tags,
|
||||
exclude_tags=exclude_tags,
|
||||
selecting_untagged=untagged,
|
||||
images=[i for i in images],
|
||||
total_sets=total_sets,
|
||||
page=page,
|
||||
pgsize=int(pgsize),
|
||||
operation=operation,
|
||||
before=before.strftime(FMT_TIME_SEARCH) if before else "",
|
||||
after=after.strftime(FMT_TIME_SEARCH) if after else "",
|
||||
keywords_title=keywords_title or "",
|
||||
keywords_description=keywords_description or "",
|
||||
now=datetime.now().strftime(FMT_TIME_SEARCH),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
import signal
|
||||
|
@ -62,3 +62,15 @@ def photoset_auth_filter(query):
|
||||
def slugify(words):
|
||||
return ''.join(letter for letter in '-'.join(words.lower().split())
|
||||
if ('a' <= letter <= 'z') or ('0' <= letter <= '9') or letter == '-')
|
||||
|
||||
|
||||
def cherryparam(v, type_=str):
|
||||
"""
|
||||
Cherrypy handles duplicate or list field names in post/get/body parameters by setting the parameter value to
|
||||
a list of strings. However, if there is just one entry the parameter value is a string. Third, if the
|
||||
field isn't provided the value is None. This function always returns a list of values.
|
||||
"""
|
||||
v = v or []
|
||||
if type(v) == type_:
|
||||
v = [v] # one entry
|
||||
return v
|
||||
|
@ -17,7 +17,7 @@ Jinja2==2.11.2
|
||||
jmespath==0.10.0
|
||||
MarkupSafe==1.1.1
|
||||
more-itertools==8.5.0
|
||||
Pillow==7.2.0
|
||||
Pillow==8.3.1
|
||||
portend==2.6
|
||||
pycparser==2.20
|
||||
PyMySQL==0.10.0
|
||||
|
@ -286,3 +286,26 @@ ul.pager {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.search-result {
|
||||
float: left;
|
||||
position: relative;
|
||||
margin: 2px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
|
||||
.inner {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 75px;
|
||||
left: 0px;
|
||||
width: 200px;
|
||||
background-color: #fff;
|
||||
z-index: 1;
|
||||
border: 1px solid #000;
|
||||
padding: 2px;
|
||||
}
|
||||
&:hover .inner {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
@ -14,20 +14,18 @@
|
||||
<div class="pure-menu">
|
||||
<ul class="pure-menu-list">
|
||||
<li class="pure-menu-item"><a href="/" class="pure-menu-link">All photos</a></li>
|
||||
<li class="pure-menu-item"><a href="/albums" class="pure-menu-link">Albums</a></li>
|
||||
<li class="pure-menu-item"><a href="/search" class="pure-menu-link">Search</a></li>
|
||||
<li class="pure-menu-item"><a href="/date" class="pure-menu-link">Dates</a></li>
|
||||
<li class="pure-menu-item"><a href="/stats" class="pure-menu-link">Stats</a></li>
|
||||
<li class="pure-menu-item"><a href="/map" class="pure-menu-link">Map</a></li>
|
||||
<li class="pure-menu-item"><a href="/tag/untagged" class="pure-menu-link">Untagged</a></li>
|
||||
<li class="pure-menu-item"><a href="/admin/trash" class="pure-menu-link">Trash</a></li>
|
||||
<li class="pure-menu-item"><a href="/stats" class="pure-menu-link">Stats</a></li>
|
||||
<li class="pure-menu-heading">Albums</li>
|
||||
{% for tag in all_albums %}
|
||||
{% for tag in all_tags %}{% if tag.is_album %}
|
||||
<li class="pure-menu-item"><a href="/album/{{ tag.slug }}" class="pure-menu-link"><span class="tag-icon tag-icon-mod-6-{{ tag.id % 6 }}"></span>{{ tag.name }}</a></li>
|
||||
{% endfor %}
|
||||
{% endif %}{% endfor %}
|
||||
<li class="pure-menu-heading">Tags</li>
|
||||
{% for tag in all_tags %}
|
||||
{% for tag in all_tags %}{% if not tag.is_album %}
|
||||
<li class="pure-menu-item"><a href="/tag/{{ tag.slug }}" class="pure-menu-link"><span class="tag-icon tag-icon-mod-6-{{ tag.id % 6 }}"></span>{{ tag.name }}</a></li>
|
||||
{% endfor %}
|
||||
{% endif %}{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="user-status">
|
||||
|
@ -94,7 +94,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="photo-tags">
|
||||
<h2>Tags{% if auth %} <a href="/create_tags?uuid={{ image.uuid }}">add</a>{% endif %}</h2>
|
||||
<h2>Tags{% if auth %} <a href="/create_tags?uuid={{ image.uuid }}">edit</a>{% endif %}</h2>
|
||||
<ul class="tags-picker">
|
||||
{% for tagi in image.tags %}
|
||||
<li>
|
||||
|
158
templates/search.html
Normal file
158
templates/search.html
Normal file
@ -0,0 +1,158 @@
|
||||
{% extends "page.html" %}
|
||||
{% block title %}Search & Bulk Operations{% endblock %}
|
||||
{% block subtitle %}Matched Images: {{ total_sets }} <a href="/search">reset</a>{% endblock %}
|
||||
{% block buttons %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<form method="post" action="/search">
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-5 ">
|
||||
<p>Include tags</p>
|
||||
<select multiple name="include_tags" style="height: 200px;width:100%;">
|
||||
{% for tag in all_tags %}
|
||||
<option value="{{ tag.uuid }}"{% if tag.uuid in include_tags %} selected{% endif %}>{{ tag.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="pure-u-1-5">
|
||||
<p>Exclude tags</p>
|
||||
<select multiple name="exclude_tags" style="height:200px;width:100%;">
|
||||
{% for tag in all_tags %}
|
||||
<option value="{{ tag.uuid }}"{% if tag.uuid in exclude_tags %} selected{% endif %}>{{ tag.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="pure-u-1-5">
|
||||
<p>Date range</p>
|
||||
<div>
|
||||
<label>
|
||||
Before:<br />
|
||||
<input type="text" name="before" value="{{ before }}" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
After:<br />
|
||||
<input type="text" name="after" value="{{ after }}" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
Now: <pre style="margin:0">{{ now }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-5">
|
||||
<p>Keywords</p>
|
||||
<div>
|
||||
<label>
|
||||
Title:<br />
|
||||
<input type="text" name="keywords_title" value="{{ keywords_title }}" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
Description:<br />
|
||||
<input type="text" name="keywords_description" value="{{ keywords_description }}" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-5">
|
||||
<p>Options</p>
|
||||
<label><input type="checkbox" name="untagged" value="1"{% if selecting_untagged %} checked="checked"{% endif %} /> Untagged</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Search">
|
||||
|
||||
<hr>
|
||||
<h2>Preview</h2>
|
||||
<div>
|
||||
{% for image in images %}
|
||||
<div class="search-result">
|
||||
<a href="/photo/{{ image.uuid }}">
|
||||
<img src="/thumb/set/small/{{ image.uuid }}.jpg" style="max-width: 100px"/>
|
||||
</a>
|
||||
<div class="inner">
|
||||
<button name="set_after" value="{{ image.date.strftime("%Y/%m/%d %H.%M.%S") }}" type="submit">After</button>
|
||||
<button name="set_before" value="{{ image.date.strftime("%Y/%m/%d %H.%M.%S") }}" type="submit">Before</button>
|
||||
{{ image.date }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<br clear="all" />
|
||||
</div>
|
||||
|
||||
|
||||
{% set total_pages = (total_sets/pgsize)|ceil %}
|
||||
<div class="pager">
|
||||
<h6>Page</h6>
|
||||
{% if page > 0 %}
|
||||
<div class="nav-prev">
|
||||
<button name="page" value="{{ page - 1 }}" type="submit">Previous</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="pages">
|
||||
<ul class="pager">
|
||||
{% for pgnum in range(0, total_pages) %}
|
||||
<li{% if pgnum == page %} class="current"{% endif %}>
|
||||
<input type="submit" name="page" value="{{ pgnum }}">
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if page + 1 < total_pages %}
|
||||
<div class="nav-next">
|
||||
<button name="page" value="{{ page + 1 }}" type="submit">Next</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<label for="pgsize">Page size: </label>
|
||||
<select name="pgsize">
|
||||
<option value="25"{% if pgsize == 25 %} selected="selected"{% endif %}>25</option>
|
||||
<option value="100"{% if pgsize == 100 %} selected="selected"{% endif %}>100</option>
|
||||
<option value="250"{% if pgsize == 250 %} selected="selected"{% endif %}>250</option>
|
||||
<option value="1000"{% if pgsize == 1000 %} selected="selected"{% endif %}>1000</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h2>Operation</h2>
|
||||
<select name="operation">
|
||||
<option value=""></option>
|
||||
<option value="add_tags"{% if operation == "add_tags" %} selected{% endif %}>Add Tags</option>
|
||||
<option value="remove_tags"{% if operation == "remove_tags" %} selected{% endif %}>Remove Tags</option>
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Go">
|
||||
|
||||
<hr />
|
||||
|
||||
{% if operation == "add_tags" %}
|
||||
<h2>Add Tag</h2>
|
||||
<div>
|
||||
<ul class="tags-picker">
|
||||
{% for tag in all_tags %}
|
||||
<li>
|
||||
<button name="add_tag" value="{{ tag.uuid }}" type="submit">{{ tag.name }}</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if operation == "remove_tags" %}
|
||||
<h2>Remove Tag</h2>
|
||||
<div>
|
||||
<ul class="tags-picker">
|
||||
{% for tag in all_tags %}
|
||||
<li>
|
||||
<button name="remove_tag" value="{{ tag.uuid }}" type="submit">{{ tag.name }}</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
Loading…
Reference in New Issue
Block a user