From 0ccdbacad7e33f906552e1270a54bd6053411430 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jan 08 2019 14:19:20 +0000 Subject: [PATCH 1/2] Allow filtering user's PR by time information This commit adds support for filtering the PR filed or actionable for an user using time information such as the created date, the updated date or the closed date. The format supported it since..until. If the string provided does not include a '..', it is assume that only the since value was specified. Fixes https://pagure.io/pagure/issue/4065 Signed-off-by: Pierre-Yves Chibon --- diff --git a/pagure/api/user.py b/pagure/api/user.py index d06b5f4..c9e1d9e 100644 --- a/pagure/api/user.py +++ b/pagure/api/user.py @@ -21,7 +21,7 @@ import pagure import pagure.exceptions import pagure.lib.query from pagure.api import API, api_method, APIERROR, get_page, get_per_page -from pagure.utils import is_true +from pagure.utils import is_true, validate_date, validate_date_range def _get_user(username): @@ -414,23 +414,7 @@ def api_view_user_issues(username): updated_after = None if since: - # Validate and convert the time - if since.isdigit(): - # We assume its a timestamp, so convert it to datetime - try: - updated_after = datetime.datetime.fromtimestamp(int(since)) - except ValueError: - raise pagure.exceptions.APIError( - 400, error_code=APIERROR.ETIMESTAMP - ) - else: - # We assume datetime format, so validate it - try: - updated_after = datetime.datetime.strptime(since, "%Y-%m-%d") - except ValueError: - raise pagure.exceptions.APIError( - 400, error_code=APIERROR.EDATETIME - ) + updated_after = validate_date(since) params.update({"updated_after": updated_after}) @@ -755,32 +739,65 @@ def api_view_user_requests_filed(username): Parameters ^^^^^^^^^^ - +---------------+----------+--------------+----------------------------+ - | Key | Type | Optionality | Description | - +===============+==========+==============+============================+ - | ``username`` | string | Mandatory | | The username of the user | - | | | | whose activity you are | - | | | | interested in. | - +---------------+----------+--------------+----------------------------+ - | ``status`` | string | Optional | | Filter the status of | - | | | | pull requests. Default: | - | | | | ``Open`` (open pull | - | | | | requests), can be | - | | | | ``Closed`` for closed | - | | | | requests, ``Merged`` | - | | | | for merged requests, or | - | | | | ``Open`` for open | - | | | | requests. | - | | | | ``All`` returns closed, | - | | | | merged and open requests.| - +---------------+----------+--------------+----------------------------+ - | ``page`` | integer | Mandatory | | The page requested. | - | | | | Defaults to 1. | - +---------------+----------+--------------+----------------------------+ - | ``per_page`` | int | Optional | | The number of items to | - | | | | return per page. | - | | | | The maximum is 100. | - +---------------+----------+--------------+----------------------------+ + +---------------+----------+--------------+-----------------------------+ + | Key | Type | Optionality | Description | + +===============+==========+==============+=============================+ + | ``username`` | string | Mandatory | | The username of the user | + | | | | whose activity you are | + | | | | interested in. | + +---------------+----------+--------------+-----------------------------+ + | ``status`` | string | Optional | | Filter the status of | + | | | | pull requests. Default: | + | | | | ``Open`` (open pull | + | | | | requests), can be | + | | | | ``Closed`` for closed | + | | | | requests, ``Merged`` | + | | | | for merged requests, or | + | | | | ``Open`` for open | + | | | | requests. | + | | | | ``All`` returns closed, | + | | | | merged and open requests. | + +---------------+----------+--------------+-----------------------------+ + | ``created`` | string | Optional | | Filter the pull-requests | + | | | | returned by their creation| + | | | | date. The date can be of | + | | | | specified either using | + | | | | a timestamp format or | + | | | | using the iso format for | + | | | | dates: yyyy-mm-dd. | + | | | | You can specify a start | + | | | | and a end date to this | + | | | | filter using start..end. | + +---------------+----------+--------------+-----------------------------+ + | ``updated`` | string | Optional | | Filter the pull-requests | + | | | | returned by their update | + | | | | date. The date can be of | + | | | | specified either using | + | | | | a timestamp format or | + | | | | using the iso format for | + | | | | dates: yyyy-mm-dd. | + | | | | You can specify a start | + | | | | and a end date to this | + | | | | filter using start..end. | + +---------------+----------+--------------+-----------------------------+ + | ``closed`` | string | Optional | | Filter the pull-requests | + | | | | returned by their closing | + | | | | date. The date can be of | + | | | | specified either using | + | | | | a timestamp format or | + | | | | using the iso format for | + | | | | dates: yyyy-mm-dd. | + | | | | You can specify a start | + | | | | and a end date to this | + | | | | filter using start..end. | + +---------------+----------+--------------+-----------------------------+ + | ``page`` | integer | Mandatory | | The page requested. | + | | | | Defaults to 1. | + +---------------+----------+--------------+-----------------------------+ + | ``per_page`` | int | Optional | | The number of items to | + | | | | return per page. | + | | | | The maximum is 100. | + +---------------+----------+--------------+-----------------------------+ Sample response @@ -793,6 +810,9 @@ def api_view_user_requests_filed(username): "status": "open", "username": "dudemcpants", "page": 1, + "created": null, + "updated": null, + "closed": null, }, "pagination": { "first": "http://localhost:5000/api/0/user/dudemcpants/requests/filed?per_page=1&page=1", @@ -931,6 +951,18 @@ def api_view_user_requests_filed(username): """ # noqa status = flask.request.args.get("status", "open") + created = flask.request.args.get("created") + updated = flask.request.args.get("updated") + closed = flask.request.args.get("closed") + + try: + created_since, created_until = validate_date_range(created) + updated_since, updated_until = validate_date_range(updated) + closed_since, closed_until = validate_date_range(closed) + except pagure.exceptions.InvalidTimestampException: + raise pagure.exceptions.APIError(400, error_code=APIERROR.ETIMESTAMP) + except pagure.exceptions.InvalidDateformatException: + raise pagure.exceptions.APIError(400, error_code=APIERROR.EDATETIME) page = get_page() per_page = get_per_page() @@ -948,6 +980,12 @@ def api_view_user_requests_filed(username): username=username, status=status, filed=username, + created_since=created_since, + created_until=created_until, + updated_since=updated_since, + updated_until=updated_until, + closed_since=closed_since, + closed_until=closed_until, count=True, ) pagination = pagure.lib.query.get_pagination_metadata( @@ -959,6 +997,12 @@ def api_view_user_requests_filed(username): username=username, status=status, filed=username, + created_since=created_since, + created_until=created_until, + updated_since=updated_since, + updated_until=updated_until, + closed_since=closed_since, + closed_until=closed_until, offset=offset, limit=limit, ) @@ -975,6 +1019,9 @@ def api_view_user_requests_filed(username): "username": username, "status": orig_status, "page": page, + "created": created, + "updated": updated, + "closed": closed, }, "pagination": pagination, } @@ -1002,28 +1049,61 @@ def api_view_user_requests_actionable(username): Parameters ^^^^^^^^^^ - +---------------+----------+--------------+----------------------------+ - | Key | Type | Optionality | Description | - +===============+==========+==============+============================+ - | ``username`` | string | Mandatory | | The username of the user | - | | | | whose activity you are | - | | | | interested in. | - +---------------+----------+--------------+----------------------------+ - | ``page`` | integer | Mandatory | | The page requested. | - | | | | Defaults to 1. | - +---------------+----------+--------------+----------------------------+ - | ``status`` | string | Optional | | Filter the status of | - | | | | pull requests. Default: | - | | | | ``Open`` (open pull | - | | | | requests), can be | - | | | | ``Closed`` for closed | - | | | | requests, ``Merged`` | - | | | | for merged requests, or | - | | | | ``Open`` for open | - | | | | requests. | - | | | | ``All`` returns closed, | - | | | | merged and open requests.| - +---------------+----------+--------------+----------------------------+ + +---------------+----------+--------------+-----------------------------+ + | Key | Type | Optionality | Description | + +===============+==========+==============+=============================+ + | ``username`` | string | Mandatory | | The username of the user | + | | | | whose activity you are | + | | | | interested in. | + +---------------+----------+--------------+-----------------------------+ + | ``created`` | string | Optional | | Filter the pull-requests | + | | | | returned by their creation| + | | | | date. The date can be of | + | | | | specified either using | + | | | | a timestamp format or | + | | | | using the iso format for | + | | | | dates: yyyy-mm-dd. | + | | | | You can specify a start | + | | | | and a end date to this | + | | | | filter using start..end. | + +---------------+----------+--------------+-----------------------------+ + | ``updated`` | string | Optional | | Filter the pull-requests | + | | | | returned by their update | + | | | | date. The date can be of | + | | | | specified either using | + | | | | a timestamp format or | + | | | | using the iso format for | + | | | | dates: yyyy-mm-dd. | + | | | | You can specify a start | + | | | | and a end date to this | + | | | | filter using start..end. | + +---------------+----------+--------------+-----------------------------+ + | ``closed`` | string | Optional | | Filter the pull-requests | + | | | | returned by their closing | + | | | | date. The date can be of | + | | | | specified either using | + | | | | a timestamp format or | + | | | | using the iso format for | + | | | | dates: yyyy-mm-dd. | + | | | | You can specify a start | + | | | | and a end date to this | + | | | | filter using start..end. | + +---------------+----------+--------------+-----------------------------+ + | ``page`` | integer | Mandatory | | The page requested. | + | | | | Defaults to 1. | + +---------------+----------+--------------+-----------------------------+ + | ``status`` | string | Optional | | Filter the status of | + | | | | pull requests. Default: | + | | | | ``Open`` (open pull | + | | | | requests), can be | + | | | | ``Closed`` for closed | + | | | | requests, ``Merged`` | + | | | | for merged requests, or | + | | | | ``Open`` for open | + | | | | requests. | + | | | | ``All`` returns closed, | + | | | | merged and open requests. | + +---------------+----------+--------------+-----------------------------+ Sample response ^^^^^^^^^^^^^^^ @@ -1035,6 +1115,9 @@ def api_view_user_requests_actionable(username): "status": "open", "username": "ryanlerch", "page": 1, + "created": null, + "updated": null, + "closed": null, }, "pagination": { "first": "http://localhost:5000/api/0/user/ryanlerch/requests/actionable?per_page=1&page=1", @@ -1173,6 +1256,18 @@ def api_view_user_requests_actionable(username): """ # noqa status = flask.request.args.get("status", "open") + created = flask.request.args.get("created") + updated = flask.request.args.get("updated") + closed = flask.request.args.get("closed") + + try: + created_since, created_until = validate_date_range(created) + updated_since, updated_until = validate_date_range(updated) + closed_since, closed_until = validate_date_range(closed) + except pagure.exceptions.InvalidTimestampException: + raise pagure.exceptions.APIError(400, error_code=APIERROR.ETIMESTAMP) + except pagure.exceptions.InvalidDateformatException: + raise pagure.exceptions.APIError(400, error_code=APIERROR.EDATETIME) page = get_page() per_page = get_per_page() @@ -1190,6 +1285,12 @@ def api_view_user_requests_actionable(username): username=username, status=status, actionable=username, + created_since=created_since, + created_until=created_until, + updated_since=updated_since, + updated_until=updated_until, + closed_since=closed_since, + closed_until=closed_until, count=True, ) pagination = pagure.lib.query.get_pagination_metadata( @@ -1201,6 +1302,12 @@ def api_view_user_requests_actionable(username): username=username, status=status, actionable=username, + created_since=created_since, + created_until=created_until, + updated_since=updated_since, + updated_until=updated_until, + closed_since=closed_since, + closed_until=closed_until, offset=offset, limit=limit, ) @@ -1217,6 +1324,9 @@ def api_view_user_requests_actionable(username): "username": username, "status": orig_status, "page": page, + "created": created, + "updated": updated, + "closed": closed, }, "pagination": pagination, } diff --git a/pagure/exceptions.py b/pagure/exceptions.py index 78677f4..ed2fcc4 100644 --- a/pagure/exceptions.py +++ b/pagure/exceptions.py @@ -117,3 +117,15 @@ class PagurePushDenied(PagureException): """ Exception raised if a remote hook rejected a push """ pass + + +class InvalidTimestampException(PagureException): + """ Exception raised when the hook is inactive. """ + + pass + + +class InvalidDateformatException(PagureException): + """ Exception raised when the hook is inactive. """ + + pass diff --git a/pagure/lib/query.py b/pagure/lib/query.py index 2fad9fd..8ffb8fb 100644 --- a/pagure/lib/query.py +++ b/pagure/lib/query.py @@ -4359,6 +4359,12 @@ def get_pull_request_of_user( actionable=None, offset=None, limit=None, + created_since=None, + created_until=None, + updated_since=None, + updated_until=None, + closed_since=None, + closed_until=None, count=False, ): """List the opened pull-requests of an user. @@ -4456,6 +4462,21 @@ def get_pull_request_of_user( model.User.user != actionable, ) + if created_since: + query = query.filter(model.PullRequest.date_created >= created_since) + if created_until: + query = query.filter(model.PullRequest.date_created <= created_until) + + if updated_since: + query = query.filter(model.PullRequest.updated_on <= updated_since) + if updated_until: + query = query.filter(model.PullRequest.updated_on <= updated_until) + + if closed_since: + query = query.filter(model.PullRequest.closed_at <= closed_since) + if closed_until: + query = query.filter(model.PullRequest.closed_at <= closed_until) + if offset: query = query.offset(offset) if limit: diff --git a/pagure/utils.py b/pagure/utils.py index 45c6d14..d575df8 100644 --- a/pagure/utils.py +++ b/pagure/utils.py @@ -10,6 +10,7 @@ from __future__ import unicode_literals, absolute_import +import datetime import logging import logging.config import os @@ -22,7 +23,11 @@ import pygit2 import six import werkzeug -from pagure.exceptions import PagureException +from pagure.exceptions import ( + PagureException, + InvalidTimestampException, + InvalidDateformatException, +) from pagure.config import config as pagure_config @@ -656,6 +661,49 @@ def is_true(value, trueish=("1", "true", "t", "y")): return value.strip().lower() in trueish +def validate_date(input_date, allow_empty=False): + """ Validate a given time. + The time can either be given as an unix timestamp or using the + yyyy-mm-dd format. + If either fail to parse, we raise a 400 error + """ + if allow_empty and input_date == "": + return None + # Validate and convert the time + if input_date.isdigit(): + # We assume its a timestamp, so convert it to datetime + try: + output_date = datetime.datetime.fromtimestamp(int(input_date)) + except ValueError: + raise InvalidTimestampException() + else: + # We assume datetime format, so validate it + try: + output_date = datetime.datetime.strptime(input_date, "%Y-%m-%d") + except ValueError: + raise InvalidDateformatException() + + return output_date + + +def validate_date_range(value): + """ Validate a given date range specified using the format since..until. + If .. is not present in the range, it is assumed that only since was + provided. + """ + since = until = None + if value is not None: + if ".." in value: + since, _, until = value.partition("..") + else: + since = value + if since is not None: + since = validate_date(since, allow_empty=True) + if until is not None: + until = validate_date(until, allow_empty=True) + return (since, until) + + def get_merge_options(request, merge_status): MERGE_OPTIONS = { "NO_CHANGE": { diff --git a/tests/test_pagure_flask_api_user.py b/tests/test_pagure_flask_api_user.py index dc885af..4f787d8 100644 --- a/tests/test_pagure_flask_api_user.py +++ b/tests/test_pagure_flask_api_user.py @@ -921,6 +921,129 @@ class PagureFlaskApiUsertestrequests(tests.Modeltests): self.assertEqual(data['args']['page'], 2) @patch('pagure.lib.notify.send_email') + def test_api_view_user_requests_filed_created(self, mockemail): + """ Test the api_view_user_requests_filed method of the flask user + api with the created parameter """ + + today = datetime.datetime.utcnow().date() + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&created=%s' % ( + today.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 6) + + yesterday = today - datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&created=%s' % ( + yesterday.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 6) + + tomorrow = today + datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&created=%s' % ( + tomorrow.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&created=..%s' % ( + today.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&created=..%s' % ( + yesterday.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + thedaybefore = today - datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&created=..%s' % ( + thedaybefore.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&created=..%s' % ( + tomorrow.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 6) + + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&created=%s..%s' % ( + thedaybefore.isoformat(), tomorrow.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 6) + + @patch('pagure.lib.notify.send_email') + def test_api_view_user_requests_filed_updated(self, mockemail): + """ Test the api_view_user_requests_filed method of the flask user + api with the created parameter """ + + today = datetime.datetime.utcnow().date() + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&updated=%s' % ( + today.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + yesterday = today - datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&updated=%s' % ( + yesterday.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + tomorrow = today + datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&updated=%s' % ( + tomorrow.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 6) + + @patch('pagure.lib.notify.send_email') + def test_api_view_user_requests_filed_closed(self, mockemail): + """ Test the api_view_user_requests_filed method of the flask user + api with the created parameter """ + + today = datetime.datetime.utcnow().date() + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&closed=%s' % ( + today.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + yesterday = today - datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&closed=%s' % ( + yesterday.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + tomorrow = today + datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/filed?status=all&closed=%s' % ( + tomorrow.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + @patch('pagure.lib.notify.send_email') def test_api_view_user_requests_filed_foo(self, mockemail): """ Test the api_view_user_requests_filed method of the flask user api """ @@ -1111,6 +1234,130 @@ class PagureFlaskApiUsertestrequests(tests.Modeltests): self.assertEqual(data['args']['page'], 2) + @patch('pagure.lib.notify.send_email') + def test_api_view_user_requests_actionable_created(self, mockemail): + """ Test the api_view_user_requests_filed method of the flask user + api with the created parameter """ + + today = datetime.datetime.utcnow().date() + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&created=%s' % ( + today.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 6) + + yesterday = today - datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&created=%s' % ( + yesterday.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 6) + + tomorrow = today + datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&created=%s' % ( + tomorrow.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&created=..%s' % ( + today.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&created=..%s' % ( + yesterday.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + thedaybefore = today - datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&created=..%s' % ( + thedaybefore.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&created=..%s' % ( + tomorrow.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 6) + + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&created=%s..%s' % ( + thedaybefore.isoformat(), tomorrow.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 6) + + @patch('pagure.lib.notify.send_email') + def test_api_view_user_requests_actionable_updated(self, mockemail): + """ Test the api_view_user_requests_filed method of the flask user + api with the created parameter """ + + today = datetime.datetime.utcnow().date() + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&updated=%s' % ( + today.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + yesterday = today - datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&updated=%s' % ( + yesterday.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + tomorrow = today + datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&updated=%s' % ( + tomorrow.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 6) + + @patch('pagure.lib.notify.send_email') + def test_api_view_user_requests_actionable_closed(self, mockemail): + """ Test the api_view_user_requests_filed method of the flask user + api with the created parameter """ + + today = datetime.datetime.utcnow().date() + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&closed=%s' % ( + today.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + yesterday = today - datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&closed=%s' % ( + yesterday.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + tomorrow = today + datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/requests/actionable?status=all&closed=%s' % ( + tomorrow.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(len(data['requests']), 0) + + class PagureFlaskApiUsertestissues(tests.Modeltests): """ Tests for the user issues endpoints """ From 8728985293ee727653bebe95159fc497ae3410d1 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jan 08 2019 14:19:20 +0000 Subject: [PATCH 2/2] Add the possibility to filter the user's issues by dates This commit add support for filtering the issues returned by the ``/api/0/user//issues`` API endpoint by dates either one of: creation date, last update or close date. Fixes https://pagure.io/pagure/issue/4064 Signed-off-by: Pierre-Yves Chibon --- diff --git a/pagure/api/user.py b/pagure/api/user.py index c9e1d9e..6b3d3e0 100644 --- a/pagure/api/user.py +++ b/pagure/api/user.py @@ -201,67 +201,98 @@ def api_view_user_issues(username): Parameters ^^^^^^^^^^ - +---------------+---------+--------------+---------------------------+ - | Key | Type | Optionality | Description | - +===============+=========+==============+===========================+ - | ``page`` | integer | Mandatory | | The page requested. | - | | | | Defaults to 1. | - +---------------+---------+--------------+---------------------------+ - | ``per_page`` | int | Optional | | The number of items | - | | | | to return per page. | - | | | | The maximum is 100. | - +---------------+---------+--------------+---------------------------+ - | ``status`` | string | Optional | | Filters the status of | - | | | | issues. Fetches all the | - | | | | issues if status is | - | | | | ``all``. Default: | - | | | | ``Open`` | - +---------------+---------+--------------+---------------------------+ - | ``tags`` | string | Optional | | A list of tags you | - | | | | wish to filter. If | - | | | | you want to filter | - | | | | for issues not having | - | | | | a tag, add an | - | | | | exclamation mark in | - | | | | front of it | - +---------------+---------+--------------+---------------------------+ - | ``milestones``| list of | Optional | | Filter the issues | - | | strings | | by milestone | - +---------------+---------+--------------+---------------------------+ - | ``no_stones`` | boolean | Optional | | If true returns only the| - | | | | issues having no | - | | | | milestone, if false | - | | | | returns only the issues | - | | | | having a milestone | - +---------------+---------+--------------+---------------------------+ - | ``since`` | string | Optional | | Filter the issues | - | | | | updated after this date.| - | | | | The date can either be | - | | | | provided as an unix date| - | | | | or in the format Y-M-D | - +---------------+---------+--------------+---------------------------+ - | ``order`` | string | Optional | | Set the ordering of the | - | | | | issues. This can be | - | | | | ``asc`` or ``desc``. | - | | | | Default: ``desc`` | - +---------------+---------+--------------+---------------------------+ - | ``order_key`` | string | Optional | | Set the ordering key. | - | | | | This can be ``assignee``| - | | | | , ``last_updated`` or | - | | | | name of other column. | - | | | | Default: | - | | | | ``date_created`` | - +---------------+---------+--------------+---------------------------+ - | ``assignee`` | boolean | Optional | | A boolean of whether to | - | | | | return the issues | - | | | | assigned to this user | - | | | | or not. Defaults to True| - +---------------+---------+--------------+---------------------------+ - | ``author`` | boolean | Optional | | A boolean of whether to | - | | | | return the issues | - | | | | created by this user or | - | | | | not. Defaults to True | - +---------------+---------+--------------+---------------------------+ + +---------------+---------+--------------+-----------------------------+ + | Key | Type | Optionality | Description | + +===============+=========+==============+=============================+ + | ``page`` | integer | Mandatory | | The page requested. | + | | | | Defaults to 1. | + +---------------+---------+--------------+-----------------------------+ + | ``per_page`` | int | Optional | | The number of items | + | | | | to return per page. | + | | | | The maximum is 100. | + +---------------+---------+--------------+-----------------------------+ + | ``status`` | string | Optional | | Filters the status of | + | | | | issues. Fetches all the | + | | | | issues if status is | + | | | | ``all``. Default: | + | | | | ``Open`` | + +---------------+---------+--------------+-----------------------------+ + | ``tags`` | string | Optional | | A list of tags you wish to| + | | | | filter. If you want to | + | | | | filter for issues not | + | | | | having a tag, add an | + | | | | exclamation mark in front | + | | | | of it | + +---------------+---------+--------------+-----------------------------+ + | ``milestones``| list of | Optional | | Filter the issues by | + | | strings | | milestone | + +---------------+---------+--------------+-----------------------------+ + | ``no_stones`` | boolean | Optional | | If true returns only the | + | | | | issues having no | + | | | | milestone, if false | + | | | | returns only the issues | + | | | | having a milestone | + +---------------+---------+--------------+-----------------------------+ + | ``since`` | string | Optional | | Filter the issues | + | | | | updated after this date. | + | | | | The date can either be | + | | | | provided as an unix date | + | | | | or in the format Y-M-D | + +---------------+---------+--------------+-----------------------------+ + | ``order`` | string | Optional | | Set the ordering of the | + | | | | issues. This can be | + | | | | ``asc`` or ``desc``. | + | | | | Default: ``desc`` | + +---------------+---------+--------------+-----------------------------+ + | ``order_key`` | string | Optional | | Set the ordering key. | + | | | | This can be ``assignee`` | + | | | | , ``last_updated`` or | + | | | | name of other column. | + | | | | Default: ``date_created`` | + +---------------+---------+--------------+-----------------------------+ + | ``assignee`` | boolean | Optional | | A boolean of whether to | + | | | | return the issues | + | | | | assigned to this user | + | | | | or not. Defaults to True | + +---------------+---------+--------------+-----------------------------+ + | ``author`` | boolean | Optional | | A boolean of whether to | + | | | | return the issues | + | | | | created by this user or | + | | | | not. Defaults to True | + +---------------+---------+--------------+-----------------------------+ + | ``created`` | string | Optional | | Filter the issues returned| + | | | | by their creation date | + | | | | The date can be of | + | | | | specified either using | + | | | | a timestamp format or | + | | | | using the iso format for | + | | | | dates: yyyy-mm-dd. | + | | | | You can specify a start | + | | | | and a end date to this | + | | | | filter using start..end. | + +---------------+---------+--------------+-----------------------------+ + | ``updated`` | string | Optional | | Filter the pull-requests | + | | | | returned by their update | + | | | | date. The date can be of | + | | | | specified either using | + | | | | a timestamp format or | + | | | | using the iso format for | + | | | | dates: yyyy-mm-dd. | + | | | | You can specify a start | + | | | | and a end date to this | + | | | | filter using start..end. | + +---------------+---------+--------------+-----------------------------+ + | ``closed`` | string | Optional | | Filter the pull-requests | + | | | | returned by their closing | + | | | | date. The date can be of | + | | | | specified either using | + | | | | a timestamp format or | + | | | | using the iso format for | + | | | | dates: yyyy-mm-dd. | + | | | | You can specify a start | + | | | | and a end date to this | + | | | | filter using start..end. | + +---------------+---------+--------------+-----------------------------+ Sample response ^^^^^^^^^^^^^^^ @@ -279,7 +310,10 @@ def api_view_user_issues(username): "page": 1, "since": null, "status": null, - "tags": [] + "tags": [], + "created": null, + "updated": null, + "closed": null, }, "issues_assigned": [ { @@ -373,6 +407,18 @@ def api_view_user_issues(username): status = flask.request.args.get("status", None) tags = flask.request.args.getlist("tags") tags = [tag.strip() for tag in tags if tag.strip()] + created = flask.request.args.get("created") + updated = flask.request.args.get("updated") + closed = flask.request.args.get("closed") + + try: + created_since, created_until = validate_date_range(created) + updated_since, updated_until = validate_date_range(updated) + closed_since, closed_until = validate_date_range(closed) + except pagure.exceptions.InvalidTimestampException: + raise pagure.exceptions.APIError(400, error_code=APIERROR.ETIMESTAMP) + except pagure.exceptions.InvalidDateformatException: + raise pagure.exceptions.APIError(400, error_code=APIERROR.EDATETIME) page = get_page() per_page = get_per_page() @@ -400,6 +446,12 @@ def api_view_user_issues(username): "no_milestones": no_stones, "offset": offset, "limit": limit, + "created_since": created_since, + "created_until": created_until, + "updated_since": updated_since, + "updated_until": updated_until, + "closed_since": closed_since, + "closed_until": closed_until, } if status is not None: @@ -475,6 +527,9 @@ def api_view_user_issues(username): "page": page, "assignee": assignee, "author": author, + "created": created, + "updated": updated, + "closed": closed, }, } ) diff --git a/pagure/lib/query.py b/pagure/lib/query.py index 8ffb8fb..86bda50 100644 --- a/pagure/lib/query.py +++ b/pagure/lib/query.py @@ -2775,6 +2775,12 @@ def search_issues( custom_search=None, updated_after=None, no_milestones=None, + created_since=None, + created_until=None, + updated_since=None, + updated_until=None, + closed_since=None, + closed_until=None, order="desc", order_key=None, ): @@ -2990,6 +2996,21 @@ def search_issues( # Asking for all ticket with a milestone query = query.filter(model.Issue.milestone.isnot(None)) + if created_since: + query = query.filter(model.Issue.date_created >= created_since) + if created_until: + query = query.filter(model.Issue.date_created <= created_until) + + if updated_since: + query = query.filter(model.Issue.last_updated <= updated_since) + if updated_until: + query = query.filter(model.Issue.last_updated <= updated_until) + + if closed_since: + query = query.filter(model.Issue.closed_at <= closed_since) + if closed_until: + query = query.filter(model.Issue.closed_at <= closed_until) + if custom_search: constraints = [] for key in custom_search: diff --git a/tests/test_pagure_flask_api_issue.py b/tests/test_pagure_flask_api_issue.py index 8be82a5..bc54cf2 100644 --- a/tests/test_pagure_flask_api_issue.py +++ b/tests/test_pagure_flask_api_issue.py @@ -4059,6 +4059,8 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): args = { "assignee": True, "author": True, + "closed": None, + "created": None, "milestones": [], "no_stones": None, "order": None, @@ -4066,7 +4068,8 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): "page": 1, "since": None, "status": None, - "tags": [] + "tags": [], + "updated": None, } self.assertEqual(data['args'], args) @@ -4084,6 +4087,8 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): args = { "assignee": True, "author": True, + "closed": None, + "created": None, "milestones": ['v1.0'], "no_stones": None, "order": None, @@ -4091,7 +4096,8 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): "page": 1, "since": None, "status": None, - "tags": [] + "tags": [], + "updated": None, } self.assertEqual(data['args'], args) @@ -4109,6 +4115,8 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): args = { "assignee": True, "author": True, + "closed": None, + "created": None, "milestones": [], "no_stones": None, "order": None, @@ -4116,7 +4124,8 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): "page": 1, "since": None, "status": 'closed', - "tags": [] + "tags": [], + "updated": None, } self.assertEqual(data['args'], args) @@ -4134,6 +4143,8 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): args = { "assignee": True, "author": True, + "closed": None, + "created": None, "milestones": [], "no_stones": None, "order": None, @@ -4141,7 +4152,8 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): "page": 1, "since": None, "status": 'all', - "tags": [] + "tags": [], + "updated": None, } self.assertEqual(data['args'], args) @@ -4177,6 +4189,8 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): args = { "assignee": True, "author": True, + "closed": None, + "created": None, "milestones": [], "no_stones": None, "order": None, @@ -4185,6 +4199,7 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): "since": None, "status": None, "tags": [], + "updated": None, } self.assertEqual(data['args'], args) @@ -4235,6 +4250,8 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): args = { "assignee": False, "author": True, + "closed": None, + "created": None, "milestones": [], "no_stones": None, "order": None, @@ -4243,6 +4260,7 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): "since": None, "status": None, "tags": [], + "updated": None, } self.assertEqual(data['args'], args) @@ -4264,6 +4282,8 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): args = { "assignee": True, "author": False, + "closed": None, + "created": None, "milestones": [], "no_stones": None, "order": None, @@ -4271,7 +4291,8 @@ class PagureFlaskApiIssuetests(tests.SimplePagureTest): "page": 1, "since": None, "status": None, - "tags": [] + "tags": [], + "updated": None, } self.assertEqual(data['args'], args) diff --git a/tests/test_pagure_flask_api_user.py b/tests/test_pagure_flask_api_user.py index 4f787d8..a705e81 100644 --- a/tests/test_pagure_flask_api_user.py +++ b/tests/test_pagure_flask_api_user.py @@ -1400,6 +1400,8 @@ class PagureFlaskApiUsertestissues(tests.Modeltests): "args": { "assignee": True, "author": True, + "closed": None, + "created": None, "milestones": [], "no_stones": None, "order": None, @@ -1407,7 +1409,8 @@ class PagureFlaskApiUsertestissues(tests.Modeltests): "page": 1, "since": None, "status": None, - "tags": [] + "tags": [], + "updated": None, }, "issues_assigned": [], "issues_created": [], @@ -1458,6 +1461,8 @@ class PagureFlaskApiUsertestissues(tests.Modeltests): "args": { "assignee": True, "author": True, + "closed": None, + "created": None, "milestones": [], "no_stones": None, "order": None, @@ -1465,7 +1470,8 @@ class PagureFlaskApiUsertestissues(tests.Modeltests): "page": 1, "since": None, "status": None, - "tags": [] + "tags": [], + "updated": None, }, "issues_assigned": [], "issues_created": [ @@ -1555,6 +1561,57 @@ class PagureFlaskApiUsertestissues(tests.Modeltests): } ) + def test_user_issues_created(self): + """ Return the list of issues associated with the specified user + and play with the created filter. """ + + today = datetime.datetime.utcnow().date() + output = self.app.get( + '/api/0/user/pingou/issues?created=%s' % (today.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(data["total_issues_assigned"], 0) + self.assertEqual(data["total_issues_created"], 1) + + yesterday = today - datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/issues?created=%s' % (yesterday.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(data["total_issues_assigned"], 0) + self.assertEqual(data["total_issues_created"], 1) + + tomorrow = today + datetime.timedelta(days=1) + output = self.app.get( + '/api/0/user/pingou/issues?created=%s' % (tomorrow.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(data["total_issues_assigned"], 0) + self.assertEqual(data["total_issues_created"], 0) + + output = self.app.get( + '/api/0/user/pingou/issues?created=..%s' % (yesterday.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(data["total_issues_assigned"], 0) + self.assertEqual(data["total_issues_created"], 0) + + output = self.app.get( + '/api/0/user/pingou/issues?created=%s..%s' % ( + yesterday.isoformat(), today.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(data["total_issues_assigned"], 0) + self.assertEqual(data["total_issues_created"], 0) + + output = self.app.get( + '/api/0/user/pingou/issues?created=%s..%s' % ( + yesterday.isoformat(), tomorrow.isoformat())) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(data["total_issues_assigned"], 0) + self.assertEqual(data["total_issues_created"], 1) + if __name__ == '__main__': unittest.main(verbosity=2)