Give Photos a searchhidden property.

By default, photos with searchhidden do not appear in the search
results. This allows a small number of representative images from
a large album to appear in the results, while the rest can be
found on the album's page.

The same effect could be achieved with a tag and forbid search,
but tag searching has much higher cost and it would be more difficult
to implement as a default behavior without requiring lots of special
checks whenever listing tags etc.
This commit is contained in:
voussoir 2018-03-09 17:10:27 -08:00
parent d88db08693
commit 5f6d21fdee
8 changed files with 135 additions and 7 deletions

View file

@ -21,7 +21,7 @@ FILENAME_BADCHARS = '\\/:*?<>|"'
# Note: Setting user_version pragma in init sequence is safe because it only # Note: Setting user_version pragma in init sequence is safe because it only
# happens after the out-of-date check occurs, so no chance of accidentally # happens after the out-of-date check occurs, so no chance of accidentally
# overwriting it. # overwriting it.
DATABASE_VERSION = 8 DATABASE_VERSION = 9
DB_INIT = ''' DB_INIT = '''
PRAGMA count_changes = OFF; PRAGMA count_changes = OFF;
PRAGMA cache_size = 10000; PRAGMA cache_size = 10000;
@ -93,7 +93,8 @@ CREATE TABLE IF NOT EXISTS photos(
created INT, created INT,
thumbnail TEXT, thumbnail TEXT,
tagged_at INT, tagged_at INT,
author_id TEXT author_id TEXT,
searchhidden INT
); );
CREATE INDEX IF NOT EXISTS index_photos_id on photos(id); CREATE INDEX IF NOT EXISTS index_photos_id on photos(id);
CREATE INDEX IF NOT EXISTS index_photos_filepath on photos(filepath COLLATE NOCASE); CREATE INDEX IF NOT EXISTS index_photos_filepath on photos(filepath COLLATE NOCASE);
@ -102,6 +103,7 @@ CREATE INDEX IF NOT EXISTS index_photos_override_filename on
CREATE INDEX IF NOT EXISTS index_photos_created on photos(created); CREATE INDEX IF NOT EXISTS index_photos_created on photos(created);
CREATE INDEX IF NOT EXISTS index_photos_extension on photos(extension); CREATE INDEX IF NOT EXISTS index_photos_extension on photos(extension);
CREATE INDEX IF NOT EXISTS index_photos_author_id on photos(author_id); CREATE INDEX IF NOT EXISTS index_photos_author_id on photos(author_id);
CREATE INDEX IF NOT EXISTS index_photos_searchhidden on photos(searchhidden);
---------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tag_group_rel( CREATE TABLE IF NOT EXISTS tag_group_rel(
parentid TEXT, parentid TEXT,

View file

@ -53,6 +53,7 @@ def photo(p, include_albums=True, include_tags=True):
'created': p.created, 'created': p.created,
'filename': p.basename, 'filename': p.basename,
'mimetype': p.mimetype, 'mimetype': p.mimetype,
'searchhidden': bool(p.searchhidden),
} }
if include_albums: if include_albums:
j['albums'] = [album(a, minimal=True) for a in p.get_containing_albums()] j['albums'] = [album(a, minimal=True) for a in p.get_containing_albums()]

View file

@ -554,6 +554,7 @@ class Photo(ObjectBase):
self.height = db_row['height'] self.height = db_row['height']
self.ratio = db_row['ratio'] self.ratio = db_row['ratio']
self.thumbnail = db_row['thumbnail'] self.thumbnail = db_row['thumbnail']
self.searchhidden = db_row['searchhidden']
if self.duration and self.bytes is not None: if self.duration and self.bytes is not None:
self.bitrate = (self.bytes / 128) / self.duration self.bitrate = (self.bytes / 128) / self.duration
@ -1023,6 +1024,21 @@ class Photo(ObjectBase):
self.__reinit__() self.__reinit__()
@decorators.required_feature('photo.edit')
@decorators.transaction
def set_searchhidden(self, searchhidden, *, commit=True):
data = {
'id': self.id,
'searchhidden': bool(searchhidden),
}
self.photodb.sql_update(table='photos', pairs=data, where_key='id')
self.searchhidden = searchhidden
if commit:
self.photodb.log.debug('Committing - set override filename')
self.photodb.commit()
@decorators.required_feature('photo.edit') @decorators.required_feature('photo.edit')
@decorators.transaction @decorators.transaction
def set_override_filename(self, new_filename, *, commit=True): def set_override_filename(self, new_filename, *, commit=True):

View file

@ -274,6 +274,7 @@ class PDBPhotoMixin:
'created': created, 'created': created,
'tagged_at': None, 'tagged_at': None,
'author_id': author_id, 'author_id': author_id,
'searchhidden': False,
# These will be filled in during the metadata stage. # These will be filled in during the metadata stage.
'bytes': None, 'bytes': None,
'width': None, 'width': None,
@ -349,6 +350,7 @@ class PDBPhotoMixin:
filename=None, filename=None,
has_tags=None, has_tags=None,
has_thumbnail=None, has_thumbnail=None,
is_searchhidden=False,
mimetype=None, mimetype=None,
tag_musts=None, tag_musts=None,
tag_mays=None, tag_mays=None,
@ -398,6 +400,13 @@ class PDBPhotoMixin:
Require a thumbnail? Require a thumbnail?
If None, anything is okay. If None, anything is okay.
is_searchhidden:
Find photos that are marked as searchhidden?
If True, find *only* searchhidden photos.
If False, find *only* nonhidden photos.
If None, either is okay.
Default False.
mimetype: mimetype:
A string or list of strings of acceptable mimetypes. A string or list of strings of acceptable mimetypes.
'image', 'video', ... 'image', 'video', ...
@ -487,6 +496,7 @@ class PDBPhotoMixin:
limit = searchhelpers.normalize_limit(limit, warning_bag=warning_bag) limit = searchhelpers.normalize_limit(limit, warning_bag=warning_bag)
has_thumbnail = searchhelpers.normalize_has_thumbnail(has_thumbnail) has_thumbnail = searchhelpers.normalize_has_thumbnail(has_thumbnail)
is_searchhidden = searchhelpers.normalize_is_searchhidden(is_searchhidden)
offset = searchhelpers.normalize_offset(offset) offset = searchhelpers.normalize_offset(offset)
if offset is None: if offset is None:
@ -506,6 +516,7 @@ class PDBPhotoMixin:
notnulls = set() notnulls = set()
yesnulls = set() yesnulls = set()
wheres = []
if extension or mimetype: if extension or mimetype:
notnulls.add('extension') notnulls.add('extension')
if width or height or ratio or area: if width or height or ratio or area:
@ -520,6 +531,11 @@ class PDBPhotoMixin:
elif has_thumbnail is False: elif has_thumbnail is False:
yesnulls.add('thumbnail') yesnulls.add('thumbnail')
if is_searchhidden is True:
wheres.append('searchhidden == 1')
elif is_searchhidden is False:
wheres.append('searchhidden == 0')
if orderby is None: if orderby is None:
giveback_orderby = None giveback_orderby = None
else: else:
@ -611,6 +627,7 @@ class PDBPhotoMixin:
notnulls=notnulls, notnulls=notnulls,
yesnulls=yesnulls, yesnulls=yesnulls,
orderby=orderby, orderby=orderby,
wheres=wheres,
) )
print(query[:200]) print(query[:200])
generator = helpers.select_generator(self.sql, query) generator = helpers.select_generator(self.sql, query)

View file

@ -20,6 +20,7 @@ def build_query(
notnulls=None, notnulls=None,
yesnulls=None, yesnulls=None,
orderby=None, orderby=None,
wheres=None,
): ):
if notnulls is None: if notnulls is None:
@ -28,8 +29,12 @@ def build_query(
if yesnulls is None: if yesnulls is None:
yesnulls = set() yesnulls = set()
if wheres is None:
wheres = set()
else:
wheres = set(wheres)
query = ['SELECT * FROM photos'] query = ['SELECT * FROM photos']
wheres = set()
if author_ids: if author_ids:
notnulls.add('author_id') notnulls.add('author_id')
@ -275,6 +280,9 @@ def normalize_has_tags(has_tags):
def normalize_has_thumbnail(has_thumbnail): def normalize_has_thumbnail(has_thumbnail):
return helpers.truthystring(has_thumbnail) return helpers.truthystring(has_thumbnail)
def normalize_is_searchhidden(is_searchhidden):
return helpers.truthystring(is_searchhidden)
def normalize_limit(limit, warning_bag=None): def normalize_limit(limit, warning_bag=None):
if not limit and limit != 0: if not limit and limit != 0:
return None return None

View file

@ -165,6 +165,32 @@ def post_batch_photos_refresh_metadata():
response = post_photo_refresh_metadata_core(photo_ids=request.form['photo_ids']) response = post_photo_refresh_metadata_core(photo_ids=request.form['photo_ids'])
return response return response
@decorators.catch_etiquette_exception
def post_photo_searchhidden_core(photo_ids, searchhidden):
if isinstance(photo_ids, str):
photo_ids = etiquette.helpers.comma_space_split(photo_ids)
photos = [common.P_photo(photo_id, response_type='json') for photo_id in photo_ids]
for photo in photos:
photo.set_searchhidden(searchhidden, commit=False)
common.P.commit()
return jsonify.make_json_response({})
@site.route('/batch/photos/set_searchhidden', methods=['POST'])
@decorators.required_fields(['photo_ids'], forbid_whitespace=True)
def post_batch_photos_set_searchhidden():
response = post_photo_searchhidden_core(photo_ids=request.form['photo_ids'], searchhidden=True)
return response
@site.route('/batch/photos/unset_searchhidden', methods=['POST'])
@decorators.required_fields(['photo_ids'], forbid_whitespace=True)
def post_batch_photos_unset_searchhidden():
response = post_photo_searchhidden_core(photo_ids=request.form['photo_ids'], searchhidden=False)
return response
# Clipboard ######################################################################################## # Clipboard ########################################################################################
@site.route('/clipboard') @site.route('/clipboard')
@ -214,6 +240,7 @@ def get_search_core():
extension = request.args.get('extension') extension = request.args.get('extension')
extension_not = request.args.get('extension_not') extension_not = request.args.get('extension_not')
mimetype = request.args.get('mimetype') mimetype = request.args.get('mimetype')
is_searchhidden = request.args.get('is_searchhidden', False)
limit = request.args.get('limit') limit = request.args.get('limit')
# This is being pre-processed because the site enforces a maximum value # This is being pre-processed because the site enforces a maximum value
@ -255,6 +282,7 @@ def get_search_core():
'filename': filename_terms, 'filename': filename_terms,
'has_tags': has_tags, 'has_tags': has_tags,
'has_thumbnail': has_thumbnail, 'has_thumbnail': has_thumbnail,
'is_searchhidden': is_searchhidden,
'mimetype': mimetype, 'mimetype': mimetype,
'tag_musts': tag_musts, 'tag_musts': tag_musts,
'tag_mays': tag_mays, 'tag_mays': tag_mays,

View file

@ -41,11 +41,12 @@ body
grid-area: right; grid-area: right;
display: grid; display: grid;
grid-template-rows: 1fr 1fr 1fr 1fr; grid-template-rows: 75px 75px 75px 75px auto;
grid-template-areas: grid-template-areas:
"add_tag_area" "add_tag_area"
"remove_tag_area" "remove_tag_area"
"refresh_metadata_area" "refresh_metadata_area"
"searchhidden_area"
"message_area"; "message_area";
background-color: rgba(0, 0, 0, 0.1); background-color: rgba(0, 0, 0, 0.1);
@ -65,11 +66,15 @@ body
grid-area: refresh_metadata_area; grid-area: refresh_metadata_area;
margin: auto; margin: auto;
} }
#searchhidden_area
{
grid-area: searchhidden_area;
margin: auto;
}
#message_area #message_area
{ {
grid-area: message_area; grid-area: message_area;
margin: 8px; margin: 8px;
max-height: 300px;
} }
</style> </style>
</head> </head>
@ -88,13 +93,23 @@ body
<input type="text" id="add_tag_textbox"> <input type="text" id="add_tag_textbox">
<button class="add_tag_button green_button" id="add_tag_button" onclick="submit_add_tag(add_remove_callback);">Add tag</button> <button class="add_tag_button green_button" id="add_tag_button" onclick="submit_add_tag(add_remove_callback);">Add tag</button>
</div> </div>
<div id="remove_tag_area"> <div id="remove_tag_area">
<input type="text" id="remove_tag_textbox"> <input type="text" id="remove_tag_textbox">
<button class="red_button" id="remove_tag_button" onclick="submit_remove_tag(add_remove_callback);">Remove tag</button> <button class="red_button" id="remove_tag_button" onclick="submit_remove_tag(add_remove_callback);">Remove tag</button>
</div> </div>
<div id="refresh_metadata_area"> <div id="refresh_metadata_area">
<button class="green_button" id="refresh_metadata_button" onclick="submit_refresh_metadata(refresh_metadata_callback);">Refresh metadata</button> <button class="green_button" id="refresh_metadata_button" onclick="submit_refresh_metadata(refresh_metadata_callback);">Refresh metadata</button>
</div> </div>
<div id="searchhidden_area">
<span>
<button class="yellow_button" id="set_searchhidden_button" onclick="submit_set_searchhidden(searchhidden_callback)">Searchhide</button>
<button class="yellow_button" id="unset_searchhidden_button" onclick="submit_unset_searchhidden(searchhidden_callback)">Unhide</button>
</span>
</div>
<div id="message_area"> <div id="message_area">
</div> </div>
</div> </div>
@ -276,5 +291,38 @@ function refresh_metadata_callback(response)
location.reload(); location.reload();
} }
} }
function searchhidden_callback(response)
{
response = response["data"];
var message_area = document.getElementById("message_area");
var message_positivity;
var message_text;
if ("error_type" in response)
{
message_positivity = "message_negative";
message_text = response["error_message"];
}
else
{
message_positivity = "message_positive";
message_text = "Success."
}
create_message_bubble(message_area, message_positivity, message_text, 8000);
}
function submit_set_searchhidden()
{
var url = "/batch/photos/set_searchhidden";
var data = new FormData();
data.append("photo_ids", Array.from(photo_clipboard).join(","));
post(url, data, searchhidden_callback);
}
function submit_unset_searchhidden()
{
var url = "/batch/photos/unset_searchhidden";
var data = new FormData();
data.append("photo_ids", Array.from(photo_clipboard).join(","));
post(url, data, searchhidden_callback);
}
</script> </script>
</html> </html>

View file

@ -3,7 +3,7 @@ import os
import sqlite3 import sqlite3
import sys import sys
import etiquette.photodb import etiquette
def upgrade_1_to_2(sql): def upgrade_1_to_2(sql):
''' '''
@ -146,6 +146,14 @@ def upgrade_7_to_8(sql):
cur = sql.cursor() cur = sql.cursor()
cur.execute('ALTER TABLE tags ADD COLUMN description TEXT') cur.execute('ALTER TABLE tags ADD COLUMN description TEXT')
def upgrade_8_to_9(sql):
'''
Give the Photos table a searchhidden field.
'''
cur = sql.cursor()
cur.execute('ALTER TABLE photos ADD COLUMN searchhidden INT')
cur.execute('UPDATE photos SET searchhidden = 0')
cur.execute('CREATE INDEX index_photos_searchhidden on photos(searchhidden)')
def upgrade_all(database_filename): def upgrade_all(database_filename):
''' '''
@ -160,7 +168,7 @@ def upgrade_all(database_filename):
cur.execute('PRAGMA user_version') cur.execute('PRAGMA user_version')
current_version = cur.fetchone()[0] current_version = cur.fetchone()[0]
needed_version = etiquette.photodb.DATABASE_VERSION needed_version = etiquette.constants.DATABASE_VERSION
if current_version == needed_version: if current_version == needed_version:
print('Already up-to-date with version %d.' % needed_version) print('Already up-to-date with version %d.' % needed_version)