From 5f6d21fdeed643a2bcfd4a063b279cf7c3e4c000 Mon Sep 17 00:00:00 2001 From: Ethan Dalool Date: Fri, 9 Mar 2018 17:10:27 -0800 Subject: [PATCH] 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. --- etiquette/constants.py | 6 ++- etiquette/jsonify.py | 1 + etiquette/objects.py | 16 ++++++ etiquette/photodb.py | 17 ++++++ etiquette/searchhelpers.py | 10 +++- .../endpoints/photo_endpoints.py | 28 ++++++++++ .../etiquette_flask/templates/clipboard.html | 52 ++++++++++++++++++- utilities/database_upgrader.py | 12 ++++- 8 files changed, 135 insertions(+), 7 deletions(-) diff --git a/etiquette/constants.py b/etiquette/constants.py index 5157d48..8e129b7 100644 --- a/etiquette/constants.py +++ b/etiquette/constants.py @@ -21,7 +21,7 @@ FILENAME_BADCHARS = '\\/:*?<>|"' # 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 # overwriting it. -DATABASE_VERSION = 8 +DATABASE_VERSION = 9 DB_INIT = ''' PRAGMA count_changes = OFF; PRAGMA cache_size = 10000; @@ -93,7 +93,8 @@ CREATE TABLE IF NOT EXISTS photos( created INT, thumbnail TEXT, 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_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_extension on photos(extension); 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( parentid TEXT, diff --git a/etiquette/jsonify.py b/etiquette/jsonify.py index 9046622..a70e5f7 100644 --- a/etiquette/jsonify.py +++ b/etiquette/jsonify.py @@ -53,6 +53,7 @@ def photo(p, include_albums=True, include_tags=True): 'created': p.created, 'filename': p.basename, 'mimetype': p.mimetype, + 'searchhidden': bool(p.searchhidden), } if include_albums: j['albums'] = [album(a, minimal=True) for a in p.get_containing_albums()] diff --git a/etiquette/objects.py b/etiquette/objects.py index dedfa36..aebd119 100644 --- a/etiquette/objects.py +++ b/etiquette/objects.py @@ -554,6 +554,7 @@ class Photo(ObjectBase): self.height = db_row['height'] self.ratio = db_row['ratio'] self.thumbnail = db_row['thumbnail'] + self.searchhidden = db_row['searchhidden'] if self.duration and self.bytes is not None: self.bitrate = (self.bytes / 128) / self.duration @@ -1023,6 +1024,21 @@ class Photo(ObjectBase): 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.transaction def set_override_filename(self, new_filename, *, commit=True): diff --git a/etiquette/photodb.py b/etiquette/photodb.py index 01d5c50..e7f519e 100644 --- a/etiquette/photodb.py +++ b/etiquette/photodb.py @@ -274,6 +274,7 @@ class PDBPhotoMixin: 'created': created, 'tagged_at': None, 'author_id': author_id, + 'searchhidden': False, # These will be filled in during the metadata stage. 'bytes': None, 'width': None, @@ -349,6 +350,7 @@ class PDBPhotoMixin: filename=None, has_tags=None, has_thumbnail=None, + is_searchhidden=False, mimetype=None, tag_musts=None, tag_mays=None, @@ -398,6 +400,13 @@ class PDBPhotoMixin: Require a thumbnail? 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: A string or list of strings of acceptable mimetypes. 'image', 'video', ... @@ -487,6 +496,7 @@ class PDBPhotoMixin: limit = searchhelpers.normalize_limit(limit, warning_bag=warning_bag) has_thumbnail = searchhelpers.normalize_has_thumbnail(has_thumbnail) + is_searchhidden = searchhelpers.normalize_is_searchhidden(is_searchhidden) offset = searchhelpers.normalize_offset(offset) if offset is None: @@ -506,6 +516,7 @@ class PDBPhotoMixin: notnulls = set() yesnulls = set() + wheres = [] if extension or mimetype: notnulls.add('extension') if width or height or ratio or area: @@ -520,6 +531,11 @@ class PDBPhotoMixin: elif has_thumbnail is False: 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: giveback_orderby = None else: @@ -611,6 +627,7 @@ class PDBPhotoMixin: notnulls=notnulls, yesnulls=yesnulls, orderby=orderby, + wheres=wheres, ) print(query[:200]) generator = helpers.select_generator(self.sql, query) diff --git a/etiquette/searchhelpers.py b/etiquette/searchhelpers.py index 9e34448..8c836d4 100644 --- a/etiquette/searchhelpers.py +++ b/etiquette/searchhelpers.py @@ -20,6 +20,7 @@ def build_query( notnulls=None, yesnulls=None, orderby=None, + wheres=None, ): if notnulls is None: @@ -28,8 +29,12 @@ def build_query( if yesnulls is None: yesnulls = set() + if wheres is None: + wheres = set() + else: + wheres = set(wheres) + query = ['SELECT * FROM photos'] - wheres = set() if author_ids: notnulls.add('author_id') @@ -275,6 +280,9 @@ def normalize_has_tags(has_tags): def normalize_has_thumbnail(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): if not limit and limit != 0: return None diff --git a/frontends/etiquette_flask/etiquette_flask/endpoints/photo_endpoints.py b/frontends/etiquette_flask/etiquette_flask/endpoints/photo_endpoints.py index 5bc4740..17f0e3b 100644 --- a/frontends/etiquette_flask/etiquette_flask/endpoints/photo_endpoints.py +++ b/frontends/etiquette_flask/etiquette_flask/endpoints/photo_endpoints.py @@ -165,6 +165,32 @@ def post_batch_photos_refresh_metadata(): response = post_photo_refresh_metadata_core(photo_ids=request.form['photo_ids']) 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 ######################################################################################## @site.route('/clipboard') @@ -214,6 +240,7 @@ def get_search_core(): extension = request.args.get('extension') extension_not = request.args.get('extension_not') mimetype = request.args.get('mimetype') + is_searchhidden = request.args.get('is_searchhidden', False) limit = request.args.get('limit') # This is being pre-processed because the site enforces a maximum value @@ -255,6 +282,7 @@ def get_search_core(): 'filename': filename_terms, 'has_tags': has_tags, 'has_thumbnail': has_thumbnail, + 'is_searchhidden': is_searchhidden, 'mimetype': mimetype, 'tag_musts': tag_musts, 'tag_mays': tag_mays, diff --git a/frontends/etiquette_flask/templates/clipboard.html b/frontends/etiquette_flask/templates/clipboard.html index 6d89f3e..4d1b241 100644 --- a/frontends/etiquette_flask/templates/clipboard.html +++ b/frontends/etiquette_flask/templates/clipboard.html @@ -41,11 +41,12 @@ body grid-area: right; display: grid; - grid-template-rows: 1fr 1fr 1fr 1fr; + grid-template-rows: 75px 75px 75px 75px auto; grid-template-areas: "add_tag_area" "remove_tag_area" "refresh_metadata_area" + "searchhidden_area" "message_area"; background-color: rgba(0, 0, 0, 0.1); @@ -65,11 +66,15 @@ body grid-area: refresh_metadata_area; margin: auto; } +#searchhidden_area +{ + grid-area: searchhidden_area; + margin: auto; +} #message_area { grid-area: message_area; margin: 8px; - max-height: 300px; } @@ -88,13 +93,23 @@ body +
+
+ +
+ + + + +
+
@@ -276,5 +291,38 @@ function refresh_metadata_callback(response) 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); +} diff --git a/utilities/database_upgrader.py b/utilities/database_upgrader.py index ff821e5..7c2e501 100644 --- a/utilities/database_upgrader.py +++ b/utilities/database_upgrader.py @@ -3,7 +3,7 @@ import os import sqlite3 import sys -import etiquette.photodb +import etiquette def upgrade_1_to_2(sql): ''' @@ -146,6 +146,14 @@ def upgrade_7_to_8(sql): cur = sql.cursor() 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): ''' @@ -160,7 +168,7 @@ def upgrade_all(database_filename): cur.execute('PRAGMA user_version') current_version = cur.fetchone()[0] - needed_version = etiquette.photodb.DATABASE_VERSION + needed_version = etiquette.constants.DATABASE_VERSION if current_version == needed_version: print('Already up-to-date with version %d.' % needed_version)