#4177 Add support for querying user's PR and issues in a timeframe
Merged 9 months ago by pingou. Opened 9 months ago by pingou.

file modified
+293 -128

@@ -21,7 +21,7 @@ 

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

@@ -201,67 +201,98 @@ 

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

              "page": 1,

              "since": null,

              "status": null,

-             "tags": []

+             "tags": [],

+             "created": null,

+             "updated": null,

+             "closed": null,

            },

            "issues_assigned": [

              {

@@ -373,6 +407,18 @@ 

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

          "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:

@@ -414,23 +466,7 @@ 

  

      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})

  

@@ -491,6 +527,9 @@ 

                  "page": page,

                  "assignee": assignee,

                  "author": author,

+                 "created": created,

+                 "updated": updated,

+                 "closed": closed,

              },

          }

      )

@@ -755,32 +794,65 @@ 

      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 +865,9 @@ 

              "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 +1006,18 @@ 

  

      """  # 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 +1035,12 @@ 

          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 +1052,12 @@ 

          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 +1074,9 @@ 

                  "username": username,

                  "status": orig_status,

                  "page": page,

+                 "created": created,

+                 "updated": updated,

+                 "closed": closed,

              },

              "pagination": pagination,

          }

@@ -1002,28 +1104,61 @@ 

      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 +1170,9 @@ 

              "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 +1311,18 @@ 

  

      """  # 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 +1340,12 @@ 

          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 +1357,12 @@ 

          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 +1379,9 @@ 

                  "username": username,

                  "status": orig_status,

                  "page": page,

+                 "created": created,

+                 "updated": updated,

+                 "closed": closed,

              },

              "pagination": pagination,

          }

file modified
+12

@@ -117,3 +117,15 @@ 

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

file modified
+42

@@ -2775,6 +2775,12 @@ 

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

          # 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:

@@ -4359,6 +4380,12 @@ 

      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 +4483,21 @@ 

              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:

file modified
+49 -1

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

      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": {

@@ -4059,6 +4059,8 @@ 

          args = {

              "assignee": True,

              "author": True,

+             "closed": None,

+             "created": None,

              "milestones": [],

              "no_stones": None,

              "order": None,

@@ -4066,7 +4068,8 @@ 

              "page": 1,

              "since": None,

              "status": None,

-             "tags": []

+             "tags": [],

+             "updated": None,

          }

  

          self.assertEqual(data['args'], args)

@@ -4084,6 +4087,8 @@ 

          args = {

              "assignee": True,

              "author": True,

+             "closed": None,

+             "created": None,

              "milestones": ['v1.0'],

              "no_stones": None,

              "order": None,

@@ -4091,7 +4096,8 @@ 

              "page": 1,

              "since": None,

              "status": None,

-             "tags": []

+             "tags": [],

+             "updated": None,

          }

  

          self.assertEqual(data['args'], args)

@@ -4109,6 +4115,8 @@ 

          args = {

              "assignee": True,

              "author": True,

+             "closed": None,

+             "created": None,

              "milestones": [],

              "no_stones": None,

              "order": None,

@@ -4116,7 +4124,8 @@ 

              "page": 1,

              "since": None,

              "status": 'closed',

-             "tags": []

+             "tags": [],

+             "updated": None,

          }

  

          self.assertEqual(data['args'], args)

@@ -4134,6 +4143,8 @@ 

          args = {

              "assignee": True,

              "author": True,

+             "closed": None,

+             "created": None,

              "milestones": [],

              "no_stones": None,

              "order": None,

@@ -4141,7 +4152,8 @@ 

              "page": 1,

              "since": None,

              "status": 'all',

-             "tags": []

+             "tags": [],

+             "updated": None,

          }

  

          self.assertEqual(data['args'], args)

@@ -4177,6 +4189,8 @@ 

          args = {

              "assignee": True,

              "author": True,

+             "closed": None,

+             "created": None,

              "milestones": [],

              "no_stones": None,

              "order": None,

@@ -4185,6 +4199,7 @@ 

              "since": None,

              "status": None,

              "tags": [],

+             "updated": None,

          }

  

          self.assertEqual(data['args'], args)

@@ -4235,6 +4250,8 @@ 

          args = {

              "assignee": False,

              "author": True,

+             "closed": None,

+             "created": None,

              "milestones": [],

              "no_stones": None,

              "order": None,

@@ -4243,6 +4260,7 @@ 

              "since": None,

              "status": None,

              "tags": [],

+             "updated": None,

          }

  

          self.assertEqual(data['args'], args)

@@ -4264,6 +4282,8 @@ 

          args = {

              "assignee": True,

              "author": False,

+             "closed": None,

+             "created": None,

              "milestones": [],

              "no_stones": None,

              "order": None,

@@ -4271,7 +4291,8 @@ 

              "page": 1,

              "since": None,

              "status": None,

-             "tags": []

+             "tags": [],

+             "updated": None,

          }

  

          self.assertEqual(data['args'], args)

@@ -921,6 +921,129 @@ 

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

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

  

@@ -1153,6 +1400,8 @@ 

                "args": {

                  "assignee": True,

                  "author": True,

+                 "closed": None,

+                 "created": None,

                  "milestones": [],

                  "no_stones": None,

                  "order": None,

@@ -1160,7 +1409,8 @@ 

                  "page": 1,

                  "since": None,

                  "status": None,

-                 "tags": []

+                 "tags": [],

+                 "updated": None,

                },

                "issues_assigned": [],

                "issues_created": [],

@@ -1211,6 +1461,8 @@ 

                "args": {

                  "assignee": True,

                  "author": True,

+                 "closed": None,

+                 "created": None,

                  "milestones": [],

                  "no_stones": None,

                  "order": None,

@@ -1218,7 +1470,8 @@ 

                  "page": 1,

                  "since": None,

                  "status": None,

-                 "tags": []

+                 "tags": [],

+                 "updated": None,

                },

                "issues_assigned": [],

                "issues_created": [

@@ -1308,6 +1561,57 @@ 

              }

          )

  

+     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)