From 0e3ae116107e4ec679316af38dda29ada41f9d16 Mon Sep 17 00:00:00 2001 From: Ethan Dalool Date: Thu, 22 Mar 2018 21:09:21 -0700 Subject: [PATCH] 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. --- etiquette/helpers.py | 10 +- etiquette/photodb.py | 33 ++-- etiquette/searchhelpers.py | 154 +++++++++++------- .../endpoints/photo_endpoints.py | 7 +- .../etiquette_flask/templates/search.html | 6 +- 5 files changed, 132 insertions(+), 78 deletions(-) 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)"> + +