From 4bf12090eb23a2e471abd723d1bd2fe93787bc46 Mon Sep 17 00:00:00 2001 From: anar Date: Nov 23 2017 09:50:38 +0000 Subject: 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//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//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//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)