diff --git a/etiquette/helpers.py b/etiquette/helpers.py
index b10651d..7829300 100644
--- a/etiquette/helpers.py
+++ b/etiquette/helpers.py
@@ -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
diff --git a/etiquette/photodb.py b/etiquette/photodb.py
index 668a269..eec50cf 100644
--- a/etiquette/photodb.py
+++ b/etiquette/photodb.py
@@ -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
diff --git a/etiquette/searchhelpers.py b/etiquette/searchhelpers.py
index d3b1aa3..a124a5c 100644
--- a/etiquette/searchhelpers.py
+++ b/etiquette/searchhelpers.py
@@ -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
diff --git a/frontends/etiquette_flask/etiquette_flask/endpoints/photo_endpoints.py b/frontends/etiquette_flask/etiquette_flask/endpoints/photo_endpoints.py
index ecad9bf..8981314 100644
--- a/frontends/etiquette_flask/etiquette_flask/endpoints/photo_endpoints.py
+++ b/frontends/etiquette_flask/etiquette_flask/endpoints/photo_endpoints.py
@@ -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'])
diff --git a/frontends/etiquette_flask/templates/search.html b/frontends/etiquette_flask/templates/search.html
index 530a754..4156585 100644
--- a/frontends/etiquette_flask/templates/search.html
+++ b/frontends/etiquette_flask/templates/search.html
@@ -244,8 +244,12 @@ form
name="extension" placeholder="Extension(s)">
+
+