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:
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):
'''
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

View file

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

View file

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

View file

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

View file

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