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 +