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): | ||||
|     ''' | ||||
|     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 | ||||
|     for all else. | ||||
| 
 | ||||
|     Case insensitive. | ||||
|     for all else. Case insensitive. | ||||
|     ''' | ||||
|     if s is None: | ||||
|         return None | ||||
|  | @ -378,6 +377,9 @@ def truthystring(s): | |||
|     if isinstance(s, (bool, int)): | ||||
|         return bool(s) | ||||
| 
 | ||||
|     if not isinstance(s, str): | ||||
|         raise TypeError('Unsupported type %s' % type(s)) | ||||
| 
 | ||||
|     s = s.lower() | ||||
|     if s in constants.TRUTHYSTRING_TRUE: | ||||
|         return True | ||||
|  |  | |||
|  | @ -314,7 +314,7 @@ class PDBPhotoMixin: | |||
|             bytes=None, | ||||
|             duration=None, | ||||
| 
 | ||||
|             authors=None, | ||||
|             author=None, | ||||
|             created=None, | ||||
|             extension=None, | ||||
|             extension_not=None, | ||||
|  | @ -341,7 +341,7 @@ class PDBPhotoMixin: | |||
|             for lower bound. | ||||
| 
 | ||||
|         TAGS AND FILTERS | ||||
|         authors: | ||||
|         author: | ||||
|             A list of User objects, or usernames, or user ids. | ||||
| 
 | ||||
|         created: | ||||
|  | @ -381,6 +381,8 @@ class PDBPhotoMixin: | |||
|         mimetype: | ||||
|             A string or list of strings of acceptable mimetypes. | ||||
|             '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: | ||||
|             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('duration', duration, minimums, maximums, warning_bag=warning_bag) | ||||
| 
 | ||||
|         authors = searchhelpers.normalize_authors(authors, photodb=self, warning_bag=warning_bag) | ||||
|         extension = searchhelpers.normalize_extensions(extension) | ||||
|         extension_not = searchhelpers.normalize_extensions(extension_not) | ||||
|         author = searchhelpers.normalize_author(author, photodb=self, warning_bag=warning_bag) | ||||
|         extension = searchhelpers.normalize_extension(extension) | ||||
|         extension_not = searchhelpers.normalize_extension(extension_not) | ||||
|         filename = searchhelpers.normalize_filename(filename) | ||||
|         has_tags = searchhelpers.normalize_has_tags(has_tags) | ||||
|         has_thumbnail = searchhelpers.normalize_has_thumbnail(has_thumbnail) | ||||
|         is_searchhidden = searchhelpers.normalize_is_searchhidden(is_searchhidden) | ||||
|         mimetype = searchhelpers.normalize_extensions(mimetype) | ||||
|         mimetype = searchhelpers.normalize_extension(mimetype) | ||||
| 
 | ||||
|         if has_tags is False: | ||||
|             tag_musts = None | ||||
|  | @ -494,8 +496,8 @@ class PDBPhotoMixin: | |||
|             # has_tags check is redundant then, so disable it. | ||||
|             has_tags = None | ||||
| 
 | ||||
|         limit = searchhelpers.normalize_positive_integer(limit, warning_bag=warning_bag) | ||||
|         offset = searchhelpers.normalize_positive_integer(offset, warning_bag=warning_bag) | ||||
|         limit = searchhelpers.normalize_limit(limit, warning_bag=warning_bag) | ||||
|         offset = searchhelpers.normalize_offset(offset, warning_bag=warning_bag) | ||||
|         orderby = searchhelpers.normalize_orderby(orderby, warning_bag=warning_bag) | ||||
| 
 | ||||
|         if filename: | ||||
|  | @ -523,14 +525,14 @@ class PDBPhotoMixin: | |||
|                 'ratio': ratio, | ||||
|                 'bytes': bytes, | ||||
|                 'duration': duration, | ||||
|                 'authors': authors, | ||||
|                 'author': list(author) or None, | ||||
|                 'created': created, | ||||
|                 'extension': extension or None, | ||||
|                 'extension_not': extension_not or None, | ||||
|                 'extension': list(extension) or None, | ||||
|                 'extension_not': list(extension_not) or None, | ||||
|                 'filename': filename or None, | ||||
|                 'has_tags': has_tags, | ||||
|                 'has_thumbnail': has_thumbnail, | ||||
|                 'mimetype': mimetype or None, | ||||
|                 'mimetype': list(mimetype) or None, | ||||
|                 'tag_musts': tag_musts or None, | ||||
|                 'tag_mays': tag_mays or None, | ||||
|                 'tag_forbids': tag_forbids or None, | ||||
|  | @ -552,8 +554,9 @@ class PDBPhotoMixin: | |||
|         wheres = [] | ||||
|         bindings = [] | ||||
| 
 | ||||
|         if authors: | ||||
|             wheres.append('author_id IN %s' % helpers.sql_listify(authors)) | ||||
|         if author: | ||||
|             author_ids = [user.id for user in author] | ||||
|             wheres.append('author_id IN %s' % helpers.sql_listify(author_ids)) | ||||
| 
 | ||||
|         if extension: | ||||
|             if '*' in extension: | ||||
|  | @ -654,7 +657,7 @@ class PDBPhotoMixin: | |||
|                 if not success: | ||||
|                     continue | ||||
| 
 | ||||
|             if offset is not None and offset > 0: | ||||
|             if offset > 0: | ||||
|                 offset -= 1 | ||||
|                 continue | ||||
| 
 | ||||
|  |  | |||
|  | @ -122,24 +122,23 @@ def minmax(key, value, minimums, maximums, warning_bag=None): | |||
|     if high is not None: | ||||
|         maximums[key] = high | ||||
| 
 | ||||
| def normalize_authors(authors, photodb, warning_bag=None): | ||||
| def normalize_author(authors, photodb, warning_bag=None): | ||||
|     ''' | ||||
|     Either: | ||||
|     - A string, where the usernames are separated by commas | ||||
|     - An iterable containing | ||||
|         - Usernames | ||||
|         - User IDs | ||||
|         - Usernames as strings | ||||
|         - User objects | ||||
| 
 | ||||
|     Returns: A set of user IDs. | ||||
|     ''' | ||||
|     if not authors: | ||||
|         return None | ||||
|     if authors is None: | ||||
|         authors = [] | ||||
| 
 | ||||
|     if isinstance(authors, str): | ||||
|         authors = helpers.comma_space_split(authors) | ||||
| 
 | ||||
|     user_ids = set() | ||||
|     users = set() | ||||
|     for requested_author in authors: | ||||
|         if isinstance(requested_author, objects.User): | ||||
|             if requested_author.photodb == photodb: | ||||
|  | @ -155,14 +154,18 @@ def normalize_authors(authors, photodb, warning_bag=None): | |||
|             else: | ||||
|                 raise | ||||
|         else: | ||||
|             user_ids.add(user.id) | ||||
|             users.add(user) | ||||
| 
 | ||||
|     if len(user_ids) == 0: | ||||
|         return None | ||||
|     return users | ||||
| 
 | ||||
|     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: | ||||
|         extensions = set() | ||||
| 
 | ||||
|  | @ -175,44 +178,97 @@ def normalize_extensions(extensions): | |||
|     return extensions | ||||
| 
 | ||||
| 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): | ||||
|         filename_terms = ' '.join(filename_terms) | ||||
| 
 | ||||
|     filename_terms = filename_terms.strip() | ||||
| 
 | ||||
|     if not filename_terms: | ||||
|         return None | ||||
| 
 | ||||
|     return filename_terms | ||||
| 
 | ||||
| def normalize_has_tags(has_tags): | ||||
|     if not has_tags: | ||||
|         return None | ||||
| 
 | ||||
|     if isinstance(has_tags, str): | ||||
|         return helpers.truthystring(has_tags) | ||||
| 
 | ||||
|     if isinstance(has_tags, int): | ||||
|         return bool(has_tags) | ||||
| 
 | ||||
|     return None | ||||
|     ''' | ||||
|     See etiquette.helpers.truthystring. | ||||
|     ''' | ||||
|     return helpers.truthystring(has_tags) | ||||
| 
 | ||||
| def normalize_has_thumbnail(has_thumbnail): | ||||
|     ''' | ||||
|     See etiquette.helpers.truthystring. | ||||
|     ''' | ||||
|     return helpers.truthystring(has_thumbnail) | ||||
| 
 | ||||
| def normalize_is_searchhidden(is_searchhidden): | ||||
|     ''' | ||||
|     See etiquette.helpers.truthystring. | ||||
|     ''' | ||||
|     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): | ||||
|     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): | ||||
|     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): | ||||
|     ''' | ||||
|     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: | ||||
|         orderby = [] | ||||
| 
 | ||||
|  | @ -222,11 +278,14 @@ def normalize_orderby(orderby, warning_bag=None): | |||
| 
 | ||||
|     final_orderby = [] | ||||
|     for requested_order in orderby: | ||||
|         requested_order = requested_order.lower().strip() | ||||
|         if not requested_order: | ||||
|             continue | ||||
|         if isinstance(requested_order, str): | ||||
|             requested_order = requested_order.strip().lower() | ||||
|             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: | ||||
|             (column, direction) = split_order | ||||
| 
 | ||||
|  | @ -235,7 +294,7 @@ def normalize_orderby(orderby, warning_bag=None): | |||
|             direction = 'desc' | ||||
| 
 | ||||
|         else: | ||||
|             message = constants.WARNING_ORDERBY_INVALID.format(requested=requested_order) | ||||
|             message = constants.WARNING_ORDERBY_INVALID.format(request=requested_order) | ||||
|             if warning_bag: | ||||
|                 warning_bag.add(message) | ||||
|             else: | ||||
|  | @ -269,36 +328,19 @@ def normalize_orderby(orderby, warning_bag=None): | |||
| 
 | ||||
|     return final_orderby | ||||
| 
 | ||||
| def normalize_positive_integer(number, warning_bag=None): | ||||
| def normalize_positive_integer(number): | ||||
|     if number is None: | ||||
|         return None | ||||
| 
 | ||||
|     if not number: | ||||
|         number = 0 | ||||
| 
 | ||||
|     elif isinstance(number, str): | ||||
|         number = number.strip() | ||||
|         try: | ||||
|             number = int(number) | ||||
|         except ValueError as exc: | ||||
|             if warning_bag: | ||||
|                 warning_bag.add(exc) | ||||
|             else: | ||||
|                 raise | ||||
|         # Convert to float, then int, just in case they type '-4.5' | ||||
|         # because int('-4.5') does not work. | ||||
|         number = float(number) | ||||
| 
 | ||||
|     elif isinstance(number, float): | ||||
|         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) | ||||
|     number = int(number) | ||||
| 
 | ||||
|     if number < 0: | ||||
|         raise ValueError('Invalid number %d' % number) | ||||
|         raise ValueError('%d must be >= 0.' % number) | ||||
| 
 | ||||
|     return number | ||||
| 
 | ||||
|  |  | |||
|  | @ -254,7 +254,7 @@ def get_search_core(): | |||
| 
 | ||||
|     offset = request.args.get('offset') | ||||
| 
 | ||||
|     authors = request.args.get('author') | ||||
|     author = request.args.get('author') | ||||
| 
 | ||||
|     orderby = request.args.get('orderby') | ||||
|     area = request.args.get('area') | ||||
|  | @ -275,7 +275,7 @@ def get_search_core(): | |||
|         'bytes': bytes, | ||||
|         'duration': duration, | ||||
| 
 | ||||
|         'authors': authors, | ||||
|         'author': author, | ||||
|         'created': created, | ||||
|         'extension': extension, | ||||
|         'extension_not': extension_not, | ||||
|  | @ -308,6 +308,9 @@ def get_search_core(): | |||
|     search_kwargs['extension_not'] = join_helper(search_kwargs['extension_not']) | ||||
|     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 | ||||
|     search_kwargs['tag_musts'] = tagname_helper(search_kwargs['tag_musts']) | ||||
|     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%}" | ||||
|             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"> | ||||
|                 {% set limit_options = [20, 50, 100] %} | ||||
|                 {% if search_kwargs['limit'] not in limit_options %} | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue