From 4bf12090eb23a2e471abd723d1bd2fe93787bc46 Mon Sep 17 00:00:00 2001
From: anar <anaradilovab@gmail.com>
Date: Nov 23 2017 09:50:38 +0000
Subject: [PATCH 1/5] Add API endpoint to retrieve all issues related to an user across all repos


Merges https://pagure.io/pagure/pull-request/2748

---

diff --git a/pagure/api/user.py b/pagure/api/user.py
index 0d1a982..6416a9b 100644
--- a/pagure/api/user.py
+++ b/pagure/api/user.py
@@ -132,6 +132,222 @@ def api_view_user(username):
     return jsonout
 
 
+@API.route('/user/<username>/issues')
+@api_method
+def api_view_user_issues(username):
+    """
+    List user's issues
+    ---------------------
+    List issues opened by or assigned to a specific user across all projects.
+
+    ::
+
+        GET /api/0/user/<username>/issues
+
+    Parameters
+    ^^^^^^^^^^
+
+    +---------------+---------+--------------+---------------------------+
+    | Key           | Type    | Optionality  | Description               |
+    +===============+=========+==============+===========================+
+    | ``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`` |
+    +---------------+---------+--------------+---------------------------+
+
+    Sample response
+    ^^^^^^^^^^^^^^^
+
+    ::
+
+        {
+          "args": {
+            "milestones": [],
+            "no_stones": null,
+            "order": null,
+            "order_key": null,
+            "since": null,
+            "status": null,
+            "tags": []
+          },
+          "issues_assigned": [
+            {
+              "assignee": {
+                "fullname": "Anar Adilova",
+                "name": "anar"
+              },
+              "blocks": [],
+              "close_status": null,
+              "closed_at": null,
+              "comments": [],
+              "content": "Test Issue",
+              "custom_fields": [],
+              "date_created": "1510124763",
+              "depends": [],
+              "id": 2,
+              "last_updated": "1510124763",
+              "milestone": null,
+              "priority": null,
+              "private": false,
+              "status": "Open",
+              "tags": [],
+              "title": "issue4",
+              "user": {
+                "fullname": "Anar Adilova",
+                "name": "anar"
+              }
+            }
+          ],
+          "issues_created": [
+            {
+              "assignee": {
+                "fullname": "Anar Adilova",
+                "name": "anar"
+              },
+              "blocks": [],
+              "close_status": null,
+              "closed_at": null,
+              "comments": [],
+              "content": "Test Issue",
+              "custom_fields": [],
+              "date_created": "1510124763",
+              "depends": [],
+              "id": 2,
+              "last_updated": "1510124763",
+              "milestone": null,
+              "priority": null,
+              "private": false,
+              "status": "Open",
+              "tags": [],
+              "title": "issue4",
+              "user": {
+                "fullname": "Anar Adilova",
+                "name": "anar"
+              }
+            }
+          ],
+          "total_issues_assigned": 1,
+          "total_issues_created": 1
+        }
+
+
+    """
+    assignee = flask.request.args.get('assignee', None)
+    author = username
+    milestone = flask.request.args.getlist('milestones', None)
+    no_stones = flask.request.args.get('no_stones', None)
+    if no_stones is not None:
+        if str(no_stones).lower() in ['1', 'true', 't']:
+            no_stones = True
+        else:
+            no_stones = False
+    since = flask.request.args.get('since', None)
+    order = flask.request.args.get('order', None)
+    order_key = flask.request.args.get('order_key', None)
+    status = flask.request.args.get('status', None)
+    tags = flask.request.args.getlist('tags')
+    tags = [tag.strip() for tag in tags if tag.strip()]
+    params = {
+        'session': SESSION,
+        'tags': tags,
+        'milestones': milestone,
+        'order': order,
+        'order_key': order_key,
+        'no_milestones': no_stones,
+    }
+
+    if status is not None:
+        if status.lower() == 'all':
+            params.update({'status': None})
+        elif status.lower() == 'closed':
+            params.update({'closed': True})
+        else:
+            params.update({'status': status})
+    else:
+        params.update({'status': 'Open'})
+
+    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)
+
+    params.update({'updated_after': updated_after})
+    params_created = params.copy()
+    params_assigned = params
+    params.update({"author": username})
+    issues_created = pagure.lib.search_issues(**params_created)
+    params_assigned.update({"assignee": username})
+    issues_assigned = pagure.lib.search_issues(**params_assigned)
+    jsonout = flask.jsonify({
+        'total_issues_created': len(issues_created),
+        'total_issues_assigned': len(issues_assigned),
+        'issues_created': [issue.to_json(public=True)
+                           for issue in issues_created],
+        'issues_assigned': [issue.to_json(public=True)
+                            for issue in issues_assigned],
+        'args': {
+            'milestones': milestone,
+            'no_stones': no_stones,
+            'order': order,
+            'order_key': order_key,
+            'since': since,
+            'status': status,
+            'tags': tags,
+        }
+    })
+    return jsonout
+
+
 @API.route('/user/<username>/activity/stats')
 @api_method
 def api_view_user_activity_stats(username):
diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py
index 3588415..6f4e8c3 100644
--- a/pagure/lib/__init__.py
+++ b/pagure/lib/__init__.py
@@ -2189,7 +2189,7 @@ def _get_project(session, name, user=None, namespace=None, case=False):
 
 
 def search_issues(
-        session, repo, issueid=None, issueuid=None, status=None,
+        session, repo=None, issueid=None, issueuid=None, status=None,
         closed=False, tags=None, assignee=None, author=None, private=None,
         priority=None, milestones=None, count=False, offset=None,
         limit=None, search_pattern=None, custom_search=None,
@@ -2263,10 +2263,13 @@ def search_issues(
     '''
     query = session.query(
         sqlalchemy.distinct(model.Issue.uid)
-    ).filter(
-        model.Issue.project_id == repo.id
     )
 
+    if repo is not None:
+        query = query.filter(
+            model.Issue.project_id == repo.id
+        )
+
     if updated_after:
         query = query.filter(
             model.Issue.last_updated >= updated_after
@@ -2313,9 +2316,12 @@ def search_issues(
         if ytags:
             sub_q2 = session.query(
                 sqlalchemy.distinct(model.Issue.uid)
-            ).filter(
-                model.Issue.project_id == repo.id
-            ).filter(
+            )
+            if repo is not None:
+                sub_q2 = sub_q2.filter(
+                    model.Issue.project_id == repo.id
+                )
+            sub_q2 = sub_q2.filter(
                 model.Issue.uid == model.TagIssueColored.issue_uid
             ).filter(
                 model.TagIssueColored.tag_id == model.TagColored.id
@@ -2325,9 +2331,12 @@ def search_issues(
         if notags:
             sub_q3 = session.query(
                 sqlalchemy.distinct(model.Issue.uid)
-            ).filter(
-                model.Issue.project_id == repo.id
-            ).filter(
+            )
+            if repo is not None:
+                sub_q3 = sub_q3.filter(
+                    model.Issue.project_id == repo.id
+                )
+            sub_q3 = sub_q3.filter(
                 model.Issue.uid == model.TagIssueColored.issue_uid
             ).filter(
                 model.TagIssueColored.tag_id == model.TagColored.id
@@ -2462,10 +2471,13 @@ def search_issues(
         model.Issue
     ).filter(
         model.Issue.uid.in_(query.subquery())
-    ).filter(
-        model.Issue.project_id == repo.id
     )
 
+    if repo is not None:
+        query = query.filter(
+            model.Issue.project_id == repo.id
+        )
+
     if search_pattern is not None:
         query = query.filter(
             model.Issue.title.ilike('%%%s%%' % search_pattern)

From 9c6ee5e3b746baeefb623e3bbef227f690361628 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon <pingou@pingoured.fr>
Date: Nov 23 2017 09:50:41 +0000
Subject: [PATCH 2/5] Small bug fix and test for the API endpoint listing user's issues


We were adding the `author` filter after we had copied the dict of
parameters which meant that filter wasn't being applied.
This is now fixed.

This commit also adds some tests for this API endpoint (which lead to the
discovery of the above mentioned bug).

Signed-off-by: Pierre-Yves Chibon <pingou@pingoured.fr>

---

diff --git a/pagure/api/user.py b/pagure/api/user.py
index 6416a9b..d3fce1e 100644
--- a/pagure/api/user.py
+++ b/pagure/api/user.py
@@ -322,12 +322,17 @@ def api_view_user_issues(username):
                     400, error_code=APIERROR.EDATETIME)
 
     params.update({'updated_after': updated_after})
+
+    # Issues authored by this user
     params_created = params.copy()
-    params_assigned = params
-    params.update({"author": username})
+    params_created.update({"author": username})
     issues_created = pagure.lib.search_issues(**params_created)
+
+    # Issues assigned to this user
+    params_assigned = params.copy()
     params_assigned.update({"assignee": username})
     issues_assigned = pagure.lib.search_issues(**params_assigned)
+
     jsonout = flask.jsonify({
         'total_issues_created': len(issues_created),
         'total_issues_assigned': len(issues_assigned),
diff --git a/tests/test_pagure_flask_api_issue.py b/tests/test_pagure_flask_api_issue.py
index 1a71fe1..9142544 100644
--- a/tests/test_pagure_flask_api_issue.py
+++ b/tests/test_pagure_flask_api_issue.py
@@ -297,6 +297,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
         pagure.SESSION = self.session
         pagure.api.SESSION = self.session
         pagure.api.issue.SESSION = self.session
+        pagure.api.user.SESSION = self.session
         pagure.lib.SESSION = self.session
 
         pagure.APP.config['TICKETS_FOLDER'] = None
@@ -3057,6 +3058,144 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
         for k in sorted(data['stats'].keys())[:-1]:
             self.assertEqual(data['stats'][k], 0)
 
+    def test_api_view_user_issues_pingou(self):
+        """ Test the api_view_user_issues method of the flask api for pingou.
+        """
+        self.test_api_new_issue()
+
+        # Create private issue
+        repo = pagure.get_authorized_project(self.session, 'test')
+        msg = pagure.lib.new_issue(
+            session=self.session,
+            repo=repo,
+            title='Test issue',
+            content='We should work on this',
+            user='pingou',
+            ticketfolder=None,
+            private=True,
+            status="Closed"
+        )
+        self.session.commit()
+        self.assertEqual(msg.title, 'Test issue')
+
+        output = self.app.get('/api/0/user/pingou/issues')
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.data)
+        args = {
+            "milestones": [],
+            "no_stones": None,
+            "order": None,
+            "order_key": None,
+            "since": None,
+            "status": None,
+            "tags": []
+        }
+
+        self.assertEqual(data['args'], args)
+        self.assertEqual(data['issues_assigned'], [])
+        self.assertEqual(len(data['issues_created']), 8)
+        self.assertEqual(data['total_issues_assigned'], 0)
+        self.assertEqual(data['total_issues_created'], 8)
+
+        # Restrict to a certain, fake milestone
+        output = self.app.get('/api/0/user/pingou/issues?milestones=v1.0')
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.data)
+        args = {
+            "milestones": ['v1.0'],
+            "no_stones": None,
+            "order": None,
+            "order_key": None,
+            "since": None,
+            "status": None,
+            "tags": []
+        }
+
+        self.assertEqual(data['args'], args)
+        self.assertEqual(data['issues_assigned'], [])
+        self.assertEqual(data['issues_created'], [])
+        self.assertEqual(data['total_issues_assigned'], 0)
+        self.assertEqual(data['total_issues_created'], 0)
+
+        # Restrict to a certain status
+        output = self.app.get('/api/0/user/pingou/issues?status=closed')
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.data)
+        args = {
+            "milestones": [],
+            "no_stones": None,
+            "order": None,
+            "order_key": None,
+            "since": None,
+            "status": 'closed',
+            "tags": []
+        }
+
+        self.assertEqual(data['args'], args)
+        self.assertEqual(data['issues_assigned'], [])
+        self.assertEqual(len(data['issues_created']), 1)
+        self.assertEqual(data['total_issues_assigned'], 0)
+        self.assertEqual(data['total_issues_created'], 1)
+
+        # Restrict to a certain status
+        output = self.app.get('/api/0/user/pingou/issues?status=all')
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.data)
+        args = {
+            "milestones": [],
+            "no_stones": None,
+            "order": None,
+            "order_key": None,
+            "since": None,
+            "status": 'all',
+            "tags": []
+        }
+
+        self.assertEqual(data['args'], args)
+        self.assertEqual(data['issues_assigned'], [])
+        self.assertEqual(len(data['issues_created']), 9)
+        self.assertEqual(data['total_issues_assigned'], 0)
+        self.assertEqual(data['total_issues_created'], 9)
+
+    def test_api_view_user_issues_foo(self):
+        """ Test the api_view_user_issues method of the flask api for foo.
+        """
+        self.test_api_new_issue()
+
+        # Create private issue
+        repo = pagure.get_authorized_project(self.session, 'test')
+        msg = pagure.lib.new_issue(
+            session=self.session,
+            repo=repo,
+            title='Test issue',
+            content='We should work on this',
+            user='pingou',
+            ticketfolder=None,
+            private=True,
+            status="Closed"
+        )
+        self.session.commit()
+        self.assertEqual(msg.title, 'Test issue')
+
+        output = self.app.get('/api/0/user/foo/issues')
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.data)
+        args = {
+            "milestones": [],
+            "no_stones": None,
+            "order": None,
+            "order_key": None,
+            "since": None,
+            "status": None,
+            "tags": []
+        }
+
+        self.assertEqual(data['args'], args)
+        self.assertEqual(len(data['issues_assigned']), 1)
+        self.assertEqual(data['issues_created'], [])
+        self.assertEqual(data['total_issues_assigned'], 1)
+        self.assertEqual(data['total_issues_created'], 0)
+
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)

From 5241ac49be0873601f262abf0e1aacd09844ede0 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon <pingou@pingoured.fr>
Date: Nov 23 2017 09:50:41 +0000
Subject: [PATCH 3/5] Paginate the api_view_user_issues endpoint


Signed-off-by: Pierre-Yves Chibon <pingou@pingoured.fr>

---

diff --git a/pagure/api/user.py b/pagure/api/user.py
index d3fce1e..4f0bc75 100644
--- a/pagure/api/user.py
+++ b/pagure/api/user.py
@@ -10,6 +10,7 @@
 
 import collections
 import datetime
+from math import ceil
 
 import arrow
 import flask
@@ -150,6 +151,9 @@ def api_view_user_issues(username):
     +---------------+---------+--------------+---------------------------+
     | Key           | Type    | Optionality  | Description               |
     +===============+=========+==============+===========================+
+    | ``page``      | integer | Mandatory    | | The page requested.     |
+    |               |         |              |   Defaults to 1.          |
+    +---------------+---------+--------------+---------------------------+
     | ``status``    | string  | Optional     | | Filters the status of   |
     |               |         |              |   issues. Fetches all the |
     |               |         |              |   issues if status is     |
@@ -284,6 +288,20 @@ 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()]
+    page = flask.request.args.get('page', 1)
+
+    try:
+        page = int(page)
+        if page <= 0:
+            raise ValueError()
+    except ValueError:
+        raise pagure.exceptions.APIError(
+            400, error_code=APIERROR.ENOCODE,
+            error='Invalid page requested')
+
+    offset = (page - 1) * 50
+    limit = page * 50
+
     params = {
         'session': SESSION,
         'tags': tags,
@@ -291,6 +309,8 @@ def api_view_user_issues(username):
         'order': order,
         'order_key': order_key,
         'no_milestones': no_stones,
+        'offset': offset,
+        'limit': limit,
     }
 
     if status is not None:
@@ -327,13 +347,21 @@ def api_view_user_issues(username):
     params_created = params.copy()
     params_created.update({"author": username})
     issues_created = pagure.lib.search_issues(**params_created)
+    params_created.update({"offset": None, 'limit': None, 'count': True})
+    issues_created_cnt = pagure.lib.search_issues(**params_created)
+    issues_created_pages = int(ceil(issues_created_cnt / float(50))) or 1
 
     # Issues assigned to this user
     params_assigned = params.copy()
     params_assigned.update({"assignee": username})
     issues_assigned = pagure.lib.search_issues(**params_assigned)
+    params_assigned.update({"offset": None, 'limit': None, 'count': True})
+    issues_assigned_cnt = pagure.lib.search_issues(**params_assigned)
+    issues_assigned_pages = int(ceil(issues_assigned_cnt / float(50))) or 1
 
     jsonout = flask.jsonify({
+        'total_issues_created_pages': issues_created_pages,
+        'total_issues_assigned_pages': issues_assigned_pages,
         'total_issues_created': len(issues_created),
         'total_issues_assigned': len(issues_assigned),
         'issues_created': [issue.to_json(public=True)
@@ -348,6 +376,7 @@ def api_view_user_issues(username):
             'since': since,
             'status': status,
             'tags': tags,
+            'page': page,
         }
     })
     return jsonout
diff --git a/tests/test_pagure_flask_api_issue.py b/tests/test_pagure_flask_api_issue.py
index 9142544..beb2177 100644
--- a/tests/test_pagure_flask_api_issue.py
+++ b/tests/test_pagure_flask_api_issue.py
@@ -3086,6 +3086,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
             "no_stones": None,
             "order": None,
             "order_key": None,
+            "page": 1,
             "since": None,
             "status": None,
             "tags": []
@@ -3096,6 +3097,8 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
         self.assertEqual(len(data['issues_created']), 8)
         self.assertEqual(data['total_issues_assigned'], 0)
         self.assertEqual(data['total_issues_created'], 8)
+        self.assertEqual(data['total_issues_assigned_pages'], 1)
+        self.assertEqual(data['total_issues_created_pages'], 1)
 
         # Restrict to a certain, fake milestone
         output = self.app.get('/api/0/user/pingou/issues?milestones=v1.0')
@@ -3106,6 +3109,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
             "no_stones": None,
             "order": None,
             "order_key": None,
+            "page": 1,
             "since": None,
             "status": None,
             "tags": []
@@ -3116,6 +3120,8 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
         self.assertEqual(data['issues_created'], [])
         self.assertEqual(data['total_issues_assigned'], 0)
         self.assertEqual(data['total_issues_created'], 0)
+        self.assertEqual(data['total_issues_assigned_pages'], 1)
+        self.assertEqual(data['total_issues_created_pages'], 1)
 
         # Restrict to a certain status
         output = self.app.get('/api/0/user/pingou/issues?status=closed')
@@ -3126,6 +3132,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
             "no_stones": None,
             "order": None,
             "order_key": None,
+            "page": 1,
             "since": None,
             "status": 'closed',
             "tags": []
@@ -3136,6 +3143,8 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
         self.assertEqual(len(data['issues_created']), 1)
         self.assertEqual(data['total_issues_assigned'], 0)
         self.assertEqual(data['total_issues_created'], 1)
+        self.assertEqual(data['total_issues_assigned_pages'], 1)
+        self.assertEqual(data['total_issues_created_pages'], 1)
 
         # Restrict to a certain status
         output = self.app.get('/api/0/user/pingou/issues?status=all')
@@ -3146,6 +3155,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
             "no_stones": None,
             "order": None,
             "order_key": None,
+            "page": 1,
             "since": None,
             "status": 'all',
             "tags": []
@@ -3156,6 +3166,8 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
         self.assertEqual(len(data['issues_created']), 9)
         self.assertEqual(data['total_issues_assigned'], 0)
         self.assertEqual(data['total_issues_created'], 9)
+        self.assertEqual(data['total_issues_assigned_pages'], 1)
+        self.assertEqual(data['total_issues_created_pages'], 1)
 
     def test_api_view_user_issues_foo(self):
         """ Test the api_view_user_issues method of the flask api for foo.
@@ -3185,9 +3197,10 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
             "no_stones": None,
             "order": None,
             "order_key": None,
+            "page": 1,
             "since": None,
             "status": None,
-            "tags": []
+            "tags": [],
         }
 
         self.assertEqual(data['args'], args)
@@ -3195,6 +3208,37 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
         self.assertEqual(data['issues_created'], [])
         self.assertEqual(data['total_issues_assigned'], 1)
         self.assertEqual(data['total_issues_created'], 0)
+        self.assertEqual(data['total_issues_assigned_pages'], 1)
+        self.assertEqual(data['total_issues_created_pages'], 1)
+
+    def test_api_view_user_issues_foo_invalid_page(self):
+        """ Test the api_view_user_issues method of the flask api for foo.
+        """
+        self.test_api_new_issue()
+
+        output = self.app.get('/api/0/user/foo/issues?page=0')
+        self.assertEqual(output.status_code, 400)
+        data = json.loads(output.data)
+
+        self.assertEqual(
+            data,
+            {
+                u'error': u'Invalid page requested',
+                u'error_code': u'ENOCODE'
+            }
+        )
+
+        output = self.app.get('/api/0/user/foo/issues?page=abc')
+        self.assertEqual(output.status_code, 400)
+        data = json.loads(output.data)
+
+        self.assertEqual(
+            data,
+            {
+                u'error': u'Invalid page requested',
+                u'error_code': u'ENOCODE'
+            }
+        )
 
 
 if __name__ == '__main__':

From 738a5641fabaec40e1237126ea563b8a4f38c351 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon <pingou@pingoured.fr>
Date: Nov 23 2017 09:50:41 +0000
Subject: [PATCH 4/5] Add the option to get only the issue created or assigned to an user


Instead of always returning both.
Adjust the tests accordingly.

Signed-off-by: Pierre-Yves Chibon <pingou@pingoured.fr>

---

diff --git a/pagure/api/user.py b/pagure/api/user.py
index 4f0bc75..43a1a5e 100644
--- a/pagure/api/user.py
+++ b/pagure/api/user.py
@@ -195,6 +195,16 @@ def api_view_user_issues(username):
     |               |         |              |   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   |
+    +---------------+---------+--------------+---------------------------+
 
     Sample response
     ^^^^^^^^^^^^^^^
@@ -273,8 +283,6 @@ def api_view_user_issues(username):
 
 
     """
-    assignee = flask.request.args.get('assignee', None)
-    author = username
     milestone = flask.request.args.getlist('milestones', None)
     no_stones = flask.request.args.get('no_stones', None)
     if no_stones is not None:
@@ -290,6 +298,11 @@ def api_view_user_issues(username):
     tags = [tag.strip() for tag in tags if tag.strip()]
     page = flask.request.args.get('page', 1)
 
+    assignee = flask.request.args.get('assignee', '').lower()\
+        not in ['false', '0', 'f']
+    author = flask.request.args.get('author', '').lower() \
+        not in ['false', '0', 'f']
+
     try:
         page = int(page)
         if page <= 0:
@@ -343,21 +356,29 @@ def api_view_user_issues(username):
 
     params.update({'updated_after': updated_after})
 
-    # Issues authored by this user
-    params_created = params.copy()
-    params_created.update({"author": username})
-    issues_created = pagure.lib.search_issues(**params_created)
-    params_created.update({"offset": None, 'limit': None, 'count': True})
-    issues_created_cnt = pagure.lib.search_issues(**params_created)
-    issues_created_pages = int(ceil(issues_created_cnt / float(50))) or 1
-
-    # Issues assigned to this user
-    params_assigned = params.copy()
-    params_assigned.update({"assignee": username})
-    issues_assigned = pagure.lib.search_issues(**params_assigned)
-    params_assigned.update({"offset": None, 'limit': None, 'count': True})
-    issues_assigned_cnt = pagure.lib.search_issues(**params_assigned)
-    issues_assigned_pages = int(ceil(issues_assigned_cnt / float(50))) or 1
+    issues_created = []
+    issues_created_pages = 1
+    if author:
+        # Issues authored by this user
+        params_created = params.copy()
+        params_created.update({"author": username})
+        issues_created = pagure.lib.search_issues(**params_created)
+        params_created.update({"offset": None, 'limit': None, 'count': True})
+        issues_created_cnt = pagure.lib.search_issues(**params_created)
+        issues_created_pages = int(
+            ceil(issues_created_cnt / float(50))) or 1
+
+    issues_assigned = []
+    issues_assigned_pages = 1
+    if assignee:
+        # Issues assigned to this user
+        params_assigned = params.copy()
+        params_assigned.update({"assignee": username})
+        issues_assigned = pagure.lib.search_issues(**params_assigned)
+        params_assigned.update({"offset": None, 'limit': None, 'count': True})
+        issues_assigned_cnt = pagure.lib.search_issues(**params_assigned)
+        issues_assigned_pages = int(
+            ceil(issues_assigned_cnt / float(50))) or 1
 
     jsonout = flask.jsonify({
         'total_issues_created_pages': issues_created_pages,
@@ -377,6 +398,8 @@ def api_view_user_issues(username):
             'status': status,
             'tags': tags,
             'page': page,
+            'assignee': assignee,
+            'author': author,
         }
     })
     return jsonout
diff --git a/tests/test_pagure_flask_api_issue.py b/tests/test_pagure_flask_api_issue.py
index beb2177..0d5f15a 100644
--- a/tests/test_pagure_flask_api_issue.py
+++ b/tests/test_pagure_flask_api_issue.py
@@ -284,7 +284,7 @@ LCL_ISSUES = [
 ]
 
 
-class PagureFlaskApiIssuetests(tests.Modeltests):
+class PagureFlaskApiIssuetests(tests.SimplePagureTest):
     """ Tests for the flask API of pagure for issue """
 
     maxDiff = None
@@ -3082,6 +3082,8 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
         self.assertEqual(output.status_code, 200)
         data = json.loads(output.data)
         args = {
+            u"assignee": True,
+            u"author": True,
             "milestones": [],
             "no_stones": None,
             "order": None,
@@ -3105,6 +3107,8 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
         self.assertEqual(output.status_code, 200)
         data = json.loads(output.data)
         args = {
+            u"assignee": True,
+            u"author": True,
             "milestones": ['v1.0'],
             "no_stones": None,
             "order": None,
@@ -3128,6 +3132,8 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
         self.assertEqual(output.status_code, 200)
         data = json.loads(output.data)
         args = {
+            u"assignee": True,
+            u"author": True,
             "milestones": [],
             "no_stones": None,
             "order": None,
@@ -3151,6 +3157,8 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
         self.assertEqual(output.status_code, 200)
         data = json.loads(output.data)
         args = {
+            u"assignee": True,
+            u"author": True,
             "milestones": [],
             "no_stones": None,
             "order": None,
@@ -3193,14 +3201,16 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
         self.assertEqual(output.status_code, 200)
         data = json.loads(output.data)
         args = {
-            "milestones": [],
-            "no_stones": None,
-            "order": None,
-            "order_key": None,
-            "page": 1,
-            "since": None,
-            "status": None,
-            "tags": [],
+            u"assignee": True,
+            u"author": True,
+            u"milestones": [],
+            u"no_stones": None,
+            u"order": None,
+            u"order_key": None,
+            u"page": 1,
+            u"since": None,
+            u"status": None,
+            u"tags": [],
         }
 
         self.assertEqual(data['args'], args)
@@ -3240,6 +3250,64 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
             }
         )
 
+    def test_api_view_user_issues_foo_no_assignee(self):
+        """ Test the api_view_user_issues method of the flask api for foo.
+        """
+        self.test_api_new_issue()
+
+        output = self.app.get('/api/0/user/foo/issues?assignee=0')
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.data)
+        args = {
+            u"assignee": False,
+            u"author": True,
+            u"milestones": [],
+            u"no_stones": None,
+            u"order": None,
+            u"order_key": None,
+            u"page": 1,
+            u"since": None,
+            u"status": None,
+            u"tags": [],
+        }
+
+        self.assertEqual(data['args'], args)
+        self.assertEqual(data['issues_assigned'], [])
+        self.assertEqual(data['issues_created'], [])
+        self.assertEqual(data['total_issues_assigned'], 0)
+        self.assertEqual(data['total_issues_created'], 0)
+        self.assertEqual(data['total_issues_assigned_pages'], 1)
+        self.assertEqual(data['total_issues_created_pages'], 1)
+
+    def test_api_view_user_issues_pingou_no_author(self):
+        """ Test the api_view_user_issues method of the flask api for pingou.
+        """
+        self.test_api_new_issue()
+
+        output = self.app.get('/api/0/user/pingou/issues?author=0')
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.data)
+        args = {
+            u"assignee": True,
+            u"author": False,
+            "milestones": [],
+            "no_stones": None,
+            "order": None,
+            "order_key": None,
+            "page": 1,
+            "since": None,
+            "status": None,
+            "tags": []
+        }
+
+        self.assertEqual(data['args'], args)
+        self.assertEqual(data['issues_assigned'], [])
+        self.assertEqual(data['issues_created'], [])
+        self.assertEqual(data['total_issues_assigned'], 0)
+        self.assertEqual(data['total_issues_created'], 0)
+        self.assertEqual(data['total_issues_assigned_pages'], 1)
+        self.assertEqual(data['total_issues_created_pages'], 1)
+
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)

From 36669f0129c306f54431bcfb718c9cfa0e767a19 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon <pingou@pingoured.fr>
Date: Nov 23 2017 09:50:41 +0000
Subject: [PATCH 5/5] Include the new API endpoints to the API documentation


Signed-off-by: Pierre-Yves Chibon <pingou@pingoured.fr>

---

diff --git a/pagure/api/__init__.py b/pagure/api/__init__.py
index 293f0ab..793b6f6 100644
--- a/pagure/api/__init__.py
+++ b/pagure/api/__init__.py
@@ -457,6 +457,7 @@ def api():
         issues.append(load_doc(issue.api_change_milestone_issue))
         issues.append(load_doc(issue.api_assign_issue))
         issues.append(load_doc(issue.api_subscribe_issue))
+        issues.append(load_doc(user.api_view_user_issues))
 
     ci_doc = []
     if pagure.APP.config.get('PAGURE_CI_SERVICES', True):
@@ -478,6 +479,10 @@ def api():
         user.api_view_user_activity_stats)
     api_view_user_activity_date_doc = load_doc(
         user.api_view_user_activity_date)
+    api_view_user_requests_filed_doc = load_doc(
+        user.api_view_user_requests_filed)
+    api_view_user_requests_actionable_doc = load_doc(
+        user.api_view_user_requests_actionable)
 
     api_view_group_doc = load_doc(group.api_view_group)
     api_groups_doc = load_doc(group.api_groups)
@@ -526,6 +531,8 @@ def api():
             api_view_user_doc,
             api_view_user_activity_stats_doc,
             api_view_user_activity_date_doc,
+            api_view_user_requests_filed_doc,
+            api_view_user_requests_actionable_doc,
         ],
         groups=[
             api_groups_doc,

