From 361de946934ec78ead6892911baca9260773151b Mon Sep 17 00:00:00 2001 From: Matt Prahl Date: Wed, 17 Aug 2016 10:37:24 -0400 Subject: [PATCH] Add filtering to the module-builds route --- rida/errors.py | 25 +++++++++++++++++++++ rida/utils.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ rida/views.py | 14 +++++++----- 3 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 rida/errors.py diff --git a/rida/errors.py b/rida/errors.py new file mode 100644 index 00000000..f44c4d99 --- /dev/null +++ b/rida/errors.py @@ -0,0 +1,25 @@ +# Copyright (c) 2016 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Written by Matt Prahl +""" Defines custom exceptions and error handling functions """ + +class ValidationError(ValueError): + pass diff --git a/rida/utils.py b/rida/utils.py index 3a54c216..caa02f65 100644 --- a/rida/utils.py +++ b/rida/utils.py @@ -22,9 +22,12 @@ # Matt Prahl """ Utility functions for rida. """ from flask import request, url_for +from datetime import datetime +import re import functools import time from rida import log, models +from errors import ValidationError def retry(timeout=120, interval=30, wait_on=Exception): @@ -96,3 +99,61 @@ def pagination_metadata(p_query): per_page=p_query.per_page, _external=True) return pagination_data + + +def filter_module_builds(flask_request): + """ + Returns a flask_sqlalchemy.Pagination object based on the request parameters + :param request: Flask request object + :return: flask_sqlalchemy.Pagination + """ + search_query = dict() + state = flask_request.args.get('state', None) + + if state: + if state.isdigit(): + search_query['state'] = state + else: + if state in models.BUILD_STATES: + search_query['state'] = models.BUILD_STATES[state] + else: + raise ValidationError('An invalid state was supplied') + + for key in ['name', 'owner']: + if flask_request.args.get(key, None): + search_query[key] = flask_request.args[key] + + query = models.ModuleBuild.query + + if search_query: + query = query.filter_by(**search_query) + + # This is used when filtering the date request parameters, but it is here to avoid recompiling + utc_iso_datetime_regex = re.compile(r'^(?P\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.\d+)?' + r'(?:Z|[-+]00(?::00)?)?$') + + # Filter the query based on date request parameters + for item in ('submitted', 'modified', 'completed'): + for context in ('before', 'after'): + request_arg = '%s_%s' % (item, context) # i.e. submitted_before + iso_datetime_arg = request.args.get(request_arg, None) + + if iso_datetime_arg: + iso_datetime_matches = re.match(utc_iso_datetime_regex, iso_datetime_arg) + + if not iso_datetime_matches or not iso_datetime_matches.group('datetime'): + raise ValidationError('An invalid Zulu ISO 8601 timestamp was provided for the "%s" parameter' + % request_arg) + # Converts the ISO 8601 string to a datetime object for SQLAlchemy to use to filter + item_datetime = datetime.strptime(iso_datetime_matches.group('datetime'), '%Y-%m-%dT%H:%M:%S') + # Get the database column to filter against + column = getattr(models.ModuleBuild, 'time_' + item) + + if context == 'after': + query = query.filter(column >= item_datetime) + elif context == 'before': + query = query.filter(column <= item_datetime) + + page = flask_request.args.get('page', 1, type=int) + per_page = flask_request.args.get('per_page', 10, type=int) + return query.paginate(page, per_page, False) diff --git a/rida/views.py b/rida/views.py index a032e108..0921309d 100644 --- a/rida/views.py +++ b/rida/views.py @@ -39,13 +39,13 @@ import shutil import tempfile from rida import app, conf, db, log from rida import models -from rida.utils import pagination_metadata +from rida.utils import pagination_metadata, filter_module_builds +from errors import ValidationError @app.route("/rida/module-builds/", methods=["POST"]) def submit_build(): """Handles new module build submissions.""" - # Get the time from when the build was submitted username = rida.auth.is_packager(conf.pkgdb_api_url) if not username: return "You must use your Fedora certificate when submitting a new build", 403 @@ -165,15 +165,17 @@ def submit_build(): @app.route("/rida/module-builds/", methods=["GET"]) def query_builds(): """Lists all tracked module builds.""" - page = request.args.get('page', 1, type=int) - per_page = request.args.get('per_page', 10, type=int) - p_query = models.ModuleBuild.query.paginate(page, per_page, False) - verbose_flag = request.args.get('verbose', 'false') + try: + p_query = filter_module_builds(request) + except ValidationError as e: + return e.message, 400 json_data = { 'meta': pagination_metadata(p_query) } + verbose_flag = request.args.get('verbose', 'false') + if verbose_flag.lower() == 'true' or verbose_flag == '1': json_data['items'] = [item.api_json() for item in p_query.items] else: