Improve normalizers, use less None; Add author search box.
It was getting difficult to remember which of the normalizers use None and which don't. So let's try to be a little more consistent and just use empty sets, etc, so the caller can rely on receiving a set instead of having to check for None. Also renamed search parameter authors->author to be more in line with the singular form of extension.
This commit is contained in:
		
							parent
							
								
									088a79ffff
								
							
						
					
					
						commit
						0e3ae11610
					
				
					 5 changed files with 132 additions and 78 deletions
				
			
		|  | @ -366,11 +366,10 @@ def sql_listify(items): | ||||||
| 
 | 
 | ||||||
| def truthystring(s): | def truthystring(s): | ||||||
|     ''' |     ''' | ||||||
|     Convert strings to True, False, or None based on the options presented |     If s is already a boolean, int, or None, return a boolean or None. | ||||||
|  |     If s is a string, return True, False, or None based on the options presented | ||||||
|     in constants.TRUTHYSTRING_TRUE, constants.TRUTHYSTRING_NONE, or False |     in constants.TRUTHYSTRING_TRUE, constants.TRUTHYSTRING_NONE, or False | ||||||
|     for all else. |     for all else. Case insensitive. | ||||||
| 
 |  | ||||||
|     Case insensitive. |  | ||||||
|     ''' |     ''' | ||||||
|     if s is None: |     if s is None: | ||||||
|         return None |         return None | ||||||
|  | @ -378,6 +377,9 @@ def truthystring(s): | ||||||
|     if isinstance(s, (bool, int)): |     if isinstance(s, (bool, int)): | ||||||
|         return bool(s) |         return bool(s) | ||||||
| 
 | 
 | ||||||
|  |     if not isinstance(s, str): | ||||||
|  |         raise TypeError('Unsupported type %s' % type(s)) | ||||||
|  | 
 | ||||||
|     s = s.lower() |     s = s.lower() | ||||||
|     if s in constants.TRUTHYSTRING_TRUE: |     if s in constants.TRUTHYSTRING_TRUE: | ||||||
|         return True |         return True | ||||||
|  |  | ||||||
|  | @ -314,7 +314,7 @@ class PDBPhotoMixin: | ||||||
|             bytes=None, |             bytes=None, | ||||||
|             duration=None, |             duration=None, | ||||||
| 
 | 
 | ||||||
|             authors=None, |             author=None, | ||||||
|             created=None, |             created=None, | ||||||
|             extension=None, |             extension=None, | ||||||
|             extension_not=None, |             extension_not=None, | ||||||
|  | @ -341,7 +341,7 @@ class PDBPhotoMixin: | ||||||
|             for lower bound. |             for lower bound. | ||||||
| 
 | 
 | ||||||
|         TAGS AND FILTERS |         TAGS AND FILTERS | ||||||
|         authors: |         author: | ||||||
|             A list of User objects, or usernames, or user ids. |             A list of User objects, or usernames, or user ids. | ||||||
| 
 | 
 | ||||||
|         created: |         created: | ||||||
|  | @ -381,6 +381,8 @@ class PDBPhotoMixin: | ||||||
|         mimetype: |         mimetype: | ||||||
|             A string or list of strings of acceptable mimetypes. |             A string or list of strings of acceptable mimetypes. | ||||||
|             'image', 'video', ... |             'image', 'video', ... | ||||||
|  |             Note we are only interested in the simple "video", "audio" etc. | ||||||
|  |             For exact mimetypes you might as well use an extension search. | ||||||
| 
 | 
 | ||||||
|         tag_musts: |         tag_musts: | ||||||
|             A list of tag names or Tag objects. |             A list of tag names or Tag objects. | ||||||
|  | @ -436,14 +438,14 @@ class PDBPhotoMixin: | ||||||
|         searchhelpers.minmax('bytes', bytes, minimums, maximums, warning_bag=warning_bag) |         searchhelpers.minmax('bytes', bytes, minimums, maximums, warning_bag=warning_bag) | ||||||
|         searchhelpers.minmax('duration', duration, minimums, maximums, warning_bag=warning_bag) |         searchhelpers.minmax('duration', duration, minimums, maximums, warning_bag=warning_bag) | ||||||
| 
 | 
 | ||||||
|         authors = searchhelpers.normalize_authors(authors, photodb=self, warning_bag=warning_bag) |         author = searchhelpers.normalize_author(author, photodb=self, warning_bag=warning_bag) | ||||||
|         extension = searchhelpers.normalize_extensions(extension) |         extension = searchhelpers.normalize_extension(extension) | ||||||
|         extension_not = searchhelpers.normalize_extensions(extension_not) |         extension_not = searchhelpers.normalize_extension(extension_not) | ||||||
|         filename = searchhelpers.normalize_filename(filename) |         filename = searchhelpers.normalize_filename(filename) | ||||||
|         has_tags = searchhelpers.normalize_has_tags(has_tags) |         has_tags = searchhelpers.normalize_has_tags(has_tags) | ||||||
|         has_thumbnail = searchhelpers.normalize_has_thumbnail(has_thumbnail) |         has_thumbnail = searchhelpers.normalize_has_thumbnail(has_thumbnail) | ||||||
|         is_searchhidden = searchhelpers.normalize_is_searchhidden(is_searchhidden) |         is_searchhidden = searchhelpers.normalize_is_searchhidden(is_searchhidden) | ||||||
|         mimetype = searchhelpers.normalize_extensions(mimetype) |         mimetype = searchhelpers.normalize_extension(mimetype) | ||||||
| 
 | 
 | ||||||
|         if has_tags is False: |         if has_tags is False: | ||||||
|             tag_musts = None |             tag_musts = None | ||||||
|  | @ -494,8 +496,8 @@ class PDBPhotoMixin: | ||||||
|             # has_tags check is redundant then, so disable it. |             # has_tags check is redundant then, so disable it. | ||||||
|             has_tags = None |             has_tags = None | ||||||
| 
 | 
 | ||||||
|         limit = searchhelpers.normalize_positive_integer(limit, warning_bag=warning_bag) |         limit = searchhelpers.normalize_limit(limit, warning_bag=warning_bag) | ||||||
|         offset = searchhelpers.normalize_positive_integer(offset, warning_bag=warning_bag) |         offset = searchhelpers.normalize_offset(offset, warning_bag=warning_bag) | ||||||
|         orderby = searchhelpers.normalize_orderby(orderby, warning_bag=warning_bag) |         orderby = searchhelpers.normalize_orderby(orderby, warning_bag=warning_bag) | ||||||
| 
 | 
 | ||||||
|         if filename: |         if filename: | ||||||
|  | @ -523,14 +525,14 @@ class PDBPhotoMixin: | ||||||
|                 'ratio': ratio, |                 'ratio': ratio, | ||||||
|                 'bytes': bytes, |                 'bytes': bytes, | ||||||
|                 'duration': duration, |                 'duration': duration, | ||||||
|                 'authors': authors, |                 'author': list(author) or None, | ||||||
|                 'created': created, |                 'created': created, | ||||||
|                 'extension': extension or None, |                 'extension': list(extension) or None, | ||||||
|                 'extension_not': extension_not or None, |                 'extension_not': list(extension_not) or None, | ||||||
|                 'filename': filename or None, |                 'filename': filename or None, | ||||||
|                 'has_tags': has_tags, |                 'has_tags': has_tags, | ||||||
|                 'has_thumbnail': has_thumbnail, |                 'has_thumbnail': has_thumbnail, | ||||||
|                 'mimetype': mimetype or None, |                 'mimetype': list(mimetype) or None, | ||||||
|                 'tag_musts': tag_musts or None, |                 'tag_musts': tag_musts or None, | ||||||
|                 'tag_mays': tag_mays or None, |                 'tag_mays': tag_mays or None, | ||||||
|                 'tag_forbids': tag_forbids or None, |                 'tag_forbids': tag_forbids or None, | ||||||
|  | @ -552,8 +554,9 @@ class PDBPhotoMixin: | ||||||
|         wheres = [] |         wheres = [] | ||||||
|         bindings = [] |         bindings = [] | ||||||
| 
 | 
 | ||||||
|         if authors: |         if author: | ||||||
|             wheres.append('author_id IN %s' % helpers.sql_listify(authors)) |             author_ids = [user.id for user in author] | ||||||
|  |             wheres.append('author_id IN %s' % helpers.sql_listify(author_ids)) | ||||||
| 
 | 
 | ||||||
|         if extension: |         if extension: | ||||||
|             if '*' in extension: |             if '*' in extension: | ||||||
|  | @ -654,7 +657,7 @@ class PDBPhotoMixin: | ||||||
|                 if not success: |                 if not success: | ||||||
|                     continue |                     continue | ||||||
| 
 | 
 | ||||||
|             if offset is not None and offset > 0: |             if offset > 0: | ||||||
|                 offset -= 1 |                 offset -= 1 | ||||||
|                 continue |                 continue | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -122,24 +122,23 @@ def minmax(key, value, minimums, maximums, warning_bag=None): | ||||||
|     if high is not None: |     if high is not None: | ||||||
|         maximums[key] = high |         maximums[key] = high | ||||||
| 
 | 
 | ||||||
| def normalize_authors(authors, photodb, warning_bag=None): | def normalize_author(authors, photodb, warning_bag=None): | ||||||
|     ''' |     ''' | ||||||
|     Either: |     Either: | ||||||
|     - A string, where the usernames are separated by commas |     - A string, where the usernames are separated by commas | ||||||
|     - An iterable containing |     - An iterable containing | ||||||
|         - Usernames |         - Usernames as strings | ||||||
|         - User IDs |  | ||||||
|         - User objects |         - User objects | ||||||
| 
 | 
 | ||||||
|     Returns: A set of user IDs. |     Returns: A set of user IDs. | ||||||
|     ''' |     ''' | ||||||
|     if not authors: |     if authors is None: | ||||||
|         return None |         authors = [] | ||||||
| 
 | 
 | ||||||
|     if isinstance(authors, str): |     if isinstance(authors, str): | ||||||
|         authors = helpers.comma_space_split(authors) |         authors = helpers.comma_space_split(authors) | ||||||
| 
 | 
 | ||||||
|     user_ids = set() |     users = set() | ||||||
|     for requested_author in authors: |     for requested_author in authors: | ||||||
|         if isinstance(requested_author, objects.User): |         if isinstance(requested_author, objects.User): | ||||||
|             if requested_author.photodb == photodb: |             if requested_author.photodb == photodb: | ||||||
|  | @ -155,14 +154,18 @@ def normalize_authors(authors, photodb, warning_bag=None): | ||||||
|             else: |             else: | ||||||
|                 raise |                 raise | ||||||
|         else: |         else: | ||||||
|             user_ids.add(user.id) |             users.add(user) | ||||||
| 
 | 
 | ||||||
|     if len(user_ids) == 0: |     return users | ||||||
|         return None |  | ||||||
| 
 | 
 | ||||||
|     return user_ids | def normalize_extension(extensions): | ||||||
|  |     ''' | ||||||
|  |     Either: | ||||||
|  |     - A string, where extensions are separated by commas or spaces. | ||||||
|  |     - An iterable containing extensions as strings. | ||||||
| 
 | 
 | ||||||
| def normalize_extensions(extensions): |     Returns: A set of strings with no leading dots. | ||||||
|  |     ''' | ||||||
|     if extensions is None: |     if extensions is None: | ||||||
|         extensions = set() |         extensions = set() | ||||||
| 
 | 
 | ||||||
|  | @ -175,44 +178,97 @@ def normalize_extensions(extensions): | ||||||
|     return extensions |     return extensions | ||||||
| 
 | 
 | ||||||
| def normalize_filename(filename_terms): | def normalize_filename(filename_terms): | ||||||
|     if not filename_terms: |     ''' | ||||||
|         return None |     Either: | ||||||
|  |     - A string. | ||||||
|  |     - An iterable containing search terms as strings. | ||||||
|  | 
 | ||||||
|  |     Returns: A string where terms are separated by spaces. | ||||||
|  |     ''' | ||||||
|  |     if filename_terms is None: | ||||||
|  |         filename_terms = '' | ||||||
| 
 | 
 | ||||||
|     if not isinstance(filename_terms, str): |     if not isinstance(filename_terms, str): | ||||||
|         filename_terms = ' '.join(filename_terms) |         filename_terms = ' '.join(filename_terms) | ||||||
| 
 | 
 | ||||||
|     filename_terms = filename_terms.strip() |     filename_terms = filename_terms.strip() | ||||||
| 
 | 
 | ||||||
|     if not filename_terms: |  | ||||||
|         return None |  | ||||||
| 
 |  | ||||||
|     return filename_terms |     return filename_terms | ||||||
| 
 | 
 | ||||||
| def normalize_has_tags(has_tags): | def normalize_has_tags(has_tags): | ||||||
|     if not has_tags: |     ''' | ||||||
|         return None |     See etiquette.helpers.truthystring. | ||||||
| 
 |     ''' | ||||||
|     if isinstance(has_tags, str): |     return helpers.truthystring(has_tags) | ||||||
|         return helpers.truthystring(has_tags) |  | ||||||
| 
 |  | ||||||
|     if isinstance(has_tags, int): |  | ||||||
|         return bool(has_tags) |  | ||||||
| 
 |  | ||||||
|     return None |  | ||||||
| 
 | 
 | ||||||
| def normalize_has_thumbnail(has_thumbnail): | def normalize_has_thumbnail(has_thumbnail): | ||||||
|  |     ''' | ||||||
|  |     See etiquette.helpers.truthystring. | ||||||
|  |     ''' | ||||||
|     return helpers.truthystring(has_thumbnail) |     return helpers.truthystring(has_thumbnail) | ||||||
| 
 | 
 | ||||||
| def normalize_is_searchhidden(is_searchhidden): | def normalize_is_searchhidden(is_searchhidden): | ||||||
|  |     ''' | ||||||
|  |     See etiquette.helpers.truthystring. | ||||||
|  |     ''' | ||||||
|     return helpers.truthystring(is_searchhidden) |     return helpers.truthystring(is_searchhidden) | ||||||
| 
 | 
 | ||||||
|  | def _limit_offset(number, warning_bag): | ||||||
|  |     if number is None: | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         number = normalize_positive_integer(number) | ||||||
|  |     except ValueError as exc: | ||||||
|  |         if warning_bag: | ||||||
|  |             warning_bag.add(exc) | ||||||
|  |         number = 0 | ||||||
|  |     return number | ||||||
|  | 
 | ||||||
| def normalize_limit(limit, warning_bag=None): | def normalize_limit(limit, warning_bag=None): | ||||||
|     return normalize_positive_integer(limit, warning_bag) |     ''' | ||||||
|  |     Either: | ||||||
|  |     - None to indicate unlimited. | ||||||
|  |     - A non-negative number as an int, float, or string. | ||||||
|  | 
 | ||||||
|  |     Returns: None or a positive integer. | ||||||
|  |     ''' | ||||||
|  |     return _limit_offset(limit, warning_bag) | ||||||
|  | 
 | ||||||
|  | def normalize_mimetype(mimetype, warning_bag=None): | ||||||
|  |     ''' | ||||||
|  |     Either: | ||||||
|  |     - A string, where mimetypes are separated by commas or spaces. | ||||||
|  |     - An iterable containing mimetypes as strings. | ||||||
|  | 
 | ||||||
|  |     Returns: A set of strings. | ||||||
|  |     ''' | ||||||
|  |     return normalize_extensions(mimetype, warning_bag) | ||||||
| 
 | 
 | ||||||
| def normalize_offset(offset, warning_bag=None): | def normalize_offset(offset, warning_bag=None): | ||||||
|     return normalize_positive_integer(limit, warning_bag) |     ''' | ||||||
|  |     Either: | ||||||
|  |     - None. | ||||||
|  |     - A non-negative number as an int, float, or string. | ||||||
|  | 
 | ||||||
|  |     Returns: None or a positive integer. | ||||||
|  |     ''' | ||||||
|  |     if offset is None: | ||||||
|  |         return 0 | ||||||
|  |     return _limit_offset(offset, warning_bag) | ||||||
| 
 | 
 | ||||||
| def normalize_orderby(orderby, warning_bag=None): | def normalize_orderby(orderby, warning_bag=None): | ||||||
|  |     ''' | ||||||
|  |     Either: | ||||||
|  |     - A string of orderbys separated by commas, where a single orderby consists | ||||||
|  |     of 'column' or 'column-direction' or 'column direction'. | ||||||
|  |     - A list of such orderby strings. | ||||||
|  |     - A list of tuples of (column, direction) | ||||||
|  | 
 | ||||||
|  |     With no direction, direction is implied desc. | ||||||
|  | 
 | ||||||
|  |     Returns: A list of tuples of (column, direction) | ||||||
|  |     ''' | ||||||
|     if orderby is None: |     if orderby is None: | ||||||
|         orderby = [] |         orderby = [] | ||||||
| 
 | 
 | ||||||
|  | @ -222,11 +278,14 @@ def normalize_orderby(orderby, warning_bag=None): | ||||||
| 
 | 
 | ||||||
|     final_orderby = [] |     final_orderby = [] | ||||||
|     for requested_order in orderby: |     for requested_order in orderby: | ||||||
|         requested_order = requested_order.lower().strip() |         if isinstance(requested_order, str): | ||||||
|         if not requested_order: |             requested_order = requested_order.strip().lower() | ||||||
|             continue |             if not requested_order: | ||||||
|  |                 continue | ||||||
|  |             split_order = requested_order.split() | ||||||
|  |         else: | ||||||
|  |             split_order = tuple(x.strip().lower() for x in requested_order) | ||||||
| 
 | 
 | ||||||
|         split_order = requested_order.split(' ') |  | ||||||
|         if len(split_order) == 2: |         if len(split_order) == 2: | ||||||
|             (column, direction) = split_order |             (column, direction) = split_order | ||||||
| 
 | 
 | ||||||
|  | @ -235,7 +294,7 @@ def normalize_orderby(orderby, warning_bag=None): | ||||||
|             direction = 'desc' |             direction = 'desc' | ||||||
| 
 | 
 | ||||||
|         else: |         else: | ||||||
|             message = constants.WARNING_ORDERBY_INVALID.format(requested=requested_order) |             message = constants.WARNING_ORDERBY_INVALID.format(request=requested_order) | ||||||
|             if warning_bag: |             if warning_bag: | ||||||
|                 warning_bag.add(message) |                 warning_bag.add(message) | ||||||
|             else: |             else: | ||||||
|  | @ -269,36 +328,19 @@ def normalize_orderby(orderby, warning_bag=None): | ||||||
| 
 | 
 | ||||||
|     return final_orderby |     return final_orderby | ||||||
| 
 | 
 | ||||||
| def normalize_positive_integer(number, warning_bag=None): | def normalize_positive_integer(number): | ||||||
|     if number is None: |     if number is None: | ||||||
|         return None |  | ||||||
| 
 |  | ||||||
|     if not number: |  | ||||||
|         number = 0 |         number = 0 | ||||||
| 
 | 
 | ||||||
|     elif isinstance(number, str): |     elif isinstance(number, str): | ||||||
|         number = number.strip() |         # Convert to float, then int, just in case they type '-4.5' | ||||||
|         try: |         # because int('-4.5') does not work. | ||||||
|             number = int(number) |         number = float(number) | ||||||
|         except ValueError as exc: |  | ||||||
|             if warning_bag: |  | ||||||
|                 warning_bag.add(exc) |  | ||||||
|             else: |  | ||||||
|                 raise |  | ||||||
| 
 | 
 | ||||||
|     elif isinstance(number, float): |     number = int(number) | ||||||
|         number = int(number) |  | ||||||
| 
 |  | ||||||
|     if not isinstance(number, int): |  | ||||||
|         message = 'Invalid number "%s"' % number |  | ||||||
|         if warning_bag: |  | ||||||
|             warning_bag.add(message) |  | ||||||
|             number = None |  | ||||||
|         else: |  | ||||||
|             raise ValueError(message) |  | ||||||
| 
 | 
 | ||||||
|     if number < 0: |     if number < 0: | ||||||
|         raise ValueError('Invalid number %d' % number) |         raise ValueError('%d must be >= 0.' % number) | ||||||
| 
 | 
 | ||||||
|     return number |     return number | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -254,7 +254,7 @@ def get_search_core(): | ||||||
| 
 | 
 | ||||||
|     offset = request.args.get('offset') |     offset = request.args.get('offset') | ||||||
| 
 | 
 | ||||||
|     authors = request.args.get('author') |     author = request.args.get('author') | ||||||
| 
 | 
 | ||||||
|     orderby = request.args.get('orderby') |     orderby = request.args.get('orderby') | ||||||
|     area = request.args.get('area') |     area = request.args.get('area') | ||||||
|  | @ -275,7 +275,7 @@ def get_search_core(): | ||||||
|         'bytes': bytes, |         'bytes': bytes, | ||||||
|         'duration': duration, |         'duration': duration, | ||||||
| 
 | 
 | ||||||
|         'authors': authors, |         'author': author, | ||||||
|         'created': created, |         'created': created, | ||||||
|         'extension': extension, |         'extension': extension, | ||||||
|         'extension_not': extension_not, |         'extension_not': extension_not, | ||||||
|  | @ -308,6 +308,9 @@ def get_search_core(): | ||||||
|     search_kwargs['extension_not'] = join_helper(search_kwargs['extension_not']) |     search_kwargs['extension_not'] = join_helper(search_kwargs['extension_not']) | ||||||
|     search_kwargs['mimetype'] = join_helper(search_kwargs['mimetype']) |     search_kwargs['mimetype'] = join_helper(search_kwargs['mimetype']) | ||||||
| 
 | 
 | ||||||
|  |     author_helper = lambda users: ', '.join(user.username for user in users) if users else None | ||||||
|  |     search_kwargs['author'] = author_helper(search_kwargs['author']) | ||||||
|  | 
 | ||||||
|     tagname_helper = lambda tags: [tag.qualified_name() for tag in tags] if tags else None |     tagname_helper = lambda tags: [tag.qualified_name() for tag in tags] if tags else None | ||||||
|     search_kwargs['tag_musts'] = tagname_helper(search_kwargs['tag_musts']) |     search_kwargs['tag_musts'] = tagname_helper(search_kwargs['tag_musts']) | ||||||
|     search_kwargs['tag_mays'] = tagname_helper(search_kwargs['tag_mays']) |     search_kwargs['tag_mays'] = tagname_helper(search_kwargs['tag_mays']) | ||||||
|  |  | ||||||
|  | @ -247,6 +247,10 @@ form | ||||||
|             value="{%if search_kwargs['extension_not']%}{{search_kwargs['extension_not']}}{%endif%}" |             value="{%if search_kwargs['extension_not']%}{{search_kwargs['extension_not']}}{%endif%}" | ||||||
|             name="extension_not" placeholder="Forbid extension(s)"> |             name="extension_not" placeholder="Forbid extension(s)"> | ||||||
| 
 | 
 | ||||||
|  |             <input type="text" class="basic_param" | ||||||
|  |             value="{%if search_kwargs['author']%}{{search_kwargs['author']}}{%endif%}" | ||||||
|  |             name="author" placeholder="Author"> | ||||||
|  |              | ||||||
|             <select name="limit" class="basic_param"> |             <select name="limit" class="basic_param"> | ||||||
|                 {% set limit_options = [20, 50, 100] %} |                 {% set limit_options = [20, 50, 100] %} | ||||||
|                 {% if search_kwargs['limit'] not in limit_options %} |                 {% if search_kwargs['limit'] not in limit_options %} | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue