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:
		
							parent
							
								
									d88db08693
								
							
						
					
					
						commit
						5f6d21fdee
					
				
					 8 changed files with 135 additions and 7 deletions
				
			
		|  | @ -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, | ||||||
|  |  | ||||||
|  | @ -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()] | ||||||
|  |  | ||||||
|  | @ -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): | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
| 
 | 
 | ||||||
|     query = ['SELECT * FROM photos'] |     if wheres is None: | ||||||
|         wheres = set() |         wheres = set() | ||||||
|  |     else: | ||||||
|  |         wheres = set(wheres) | ||||||
|  | 
 | ||||||
|  |     query = ['SELECT * FROM photos'] | ||||||
| 
 | 
 | ||||||
|     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 | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue