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.
master
voussoir 2018-03-22 21:09:21 -07:00
parent 088a79ffff
commit 0e3ae11610
5 changed files with 132 additions and 78 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'])

View File

@ -244,8 +244,12 @@ form
name="extension" placeholder="Extension(s)"> name="extension" placeholder="Extension(s)">
<input type="text" class="basic_param" <input type="text" class="basic_param"
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] %}