user.py 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2015-2016 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. """
  7. from __future__ import unicode_literals
  8. import collections
  9. import datetime
  10. from math import ceil
  11. import arrow
  12. import flask
  13. import pagure
  14. import pagure.exceptions
  15. import pagure.lib
  16. from pagure.api import API, api_method, APIERROR
  17. def _get_user(username):
  18. """ Check user is valid or not
  19. """
  20. try:
  21. return pagure.lib.get_user(flask.g.session, username)
  22. except pagure.exceptions.PagureException:
  23. raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOUSER)
  24. @API.route('/user/<username>')
  25. @api_method
  26. def api_view_user(username):
  27. """
  28. User information
  29. ----------------
  30. Use this endpoint to retrieve information about a specific user.
  31. ::
  32. GET /api/0/user/<username>
  33. ::
  34. GET /api/0/user/ralph
  35. Sample response
  36. ^^^^^^^^^^^^^^^
  37. ::
  38. {
  39. "forks": [],
  40. "repos": [
  41. {
  42. "custom_keys": [],
  43. "description": "",
  44. "parent": null,
  45. "settings": {
  46. "issues_default_to_private": false,
  47. "Minimum_score_to_merge_pull-request": -1,
  48. "Web-hooks": None,
  49. "fedmsg_notifications": true,
  50. "always_merge": false,
  51. "project_documentation": true,
  52. "Enforce_signed-off_commits_in_pull-request": false,
  53. "pull_requests": true,
  54. "Only_assignee_can_merge_pull-request": false,
  55. "issue_tracker": true
  56. },
  57. "tags": [],
  58. "namespace": None,
  59. "priorities": {},
  60. "close_status": [
  61. "Invalid",
  62. "Insufficient data",
  63. "Fixed",
  64. "Duplicated"
  65. ],
  66. "milestones": {},
  67. "user": {
  68. "fullname": "ralph",
  69. "name": "ralph"
  70. },
  71. "date_created": "1426595173",
  72. "id": 5,
  73. "name": "pagure"
  74. }
  75. ],
  76. "user": {
  77. "fullname": "ralph",
  78. "name": "ralph"
  79. }
  80. }
  81. """
  82. httpcode = 200
  83. output = {}
  84. user = _get_user(username=username)
  85. repopage = flask.request.args.get('repopage', 1)
  86. try:
  87. repopage = int(repopage)
  88. except ValueError:
  89. repopage = 1
  90. forkpage = flask.request.args.get('forkpage', 1)
  91. try:
  92. forkpage = int(forkpage)
  93. except ValueError:
  94. forkpage = 1
  95. repos = pagure.lib.search_projects(
  96. flask.g.session,
  97. username=username,
  98. fork=False)
  99. forks = pagure.lib.search_projects(
  100. flask.g.session,
  101. username=username,
  102. fork=True)
  103. output['user'] = user.to_json(public=True)
  104. output['repos'] = [repo.to_json(public=True) for repo in repos]
  105. output['forks'] = [repo.to_json(public=True) for repo in forks]
  106. jsonout = flask.jsonify(output)
  107. jsonout.status_code = httpcode
  108. return jsonout
  109. @API.route('/user/<username>/issues')
  110. @api_method
  111. def api_view_user_issues(username):
  112. """
  113. List user's issues
  114. ---------------------
  115. List issues opened by or assigned to a specific user across all projects.
  116. ::
  117. GET /api/0/user/<username>/issues
  118. Parameters
  119. ^^^^^^^^^^
  120. +---------------+---------+--------------+---------------------------+
  121. | Key | Type | Optionality | Description |
  122. +===============+=========+==============+===========================+
  123. | ``page`` | integer | Mandatory | | The page requested. |
  124. | | | | Defaults to 1. |
  125. +---------------+---------+--------------+---------------------------+
  126. | ``status`` | string | Optional | | Filters the status of |
  127. | | | | issues. Fetches all the |
  128. | | | | issues if status is |
  129. | | | | ``all``. Default: |
  130. | | | | ``Open`` |
  131. +---------------+---------+--------------+---------------------------+
  132. | ``tags`` | string | Optional | | A list of tags you |
  133. | | | | wish to filter. If |
  134. | | | | you want to filter |
  135. | | | | for issues not having |
  136. | | | | a tag, add an |
  137. | | | | exclamation mark in |
  138. | | | | front of it |
  139. +---------------+---------+--------------+---------------------------+
  140. | ``milestones``| list of | Optional | | Filter the issues |
  141. | | strings | | by milestone |
  142. +---------------+---------+--------------+---------------------------+
  143. | ``no_stones`` | boolean | Optional | | If true returns only the|
  144. | | | | issues having no |
  145. | | | | milestone, if false |
  146. | | | | returns only the issues |
  147. | | | | having a milestone |
  148. +---------------+---------+--------------+---------------------------+
  149. | ``since`` | string | Optional | | Filter the issues |
  150. | | | | updated after this date.|
  151. | | | | The date can either be |
  152. | | | | provided as an unix date|
  153. | | | | or in the format Y-M-D |
  154. +---------------+---------+--------------+---------------------------+
  155. | ``order`` | string | Optional | | Set the ordering of the |
  156. | | | | issues. This can be |
  157. | | | | ``asc`` or ``desc``. |
  158. | | | | Default: ``desc`` |
  159. +---------------+---------+--------------+---------------------------+
  160. | ``order_key`` | string | Optional | | Set the ordering key. |
  161. | | | | This can be ``assignee``|
  162. | | | | , ``last_updated`` or |
  163. | | | | name of other column. |
  164. | | | | Default: |
  165. | | | | ``date_created`` |
  166. +---------------+---------+--------------+---------------------------+
  167. | ``assignee`` | boolean | Optional | | A boolean of whether to |
  168. | | | | return the issues |
  169. | | | | assigned to this user |
  170. | | | | or not. Defaults to True|
  171. +---------------+---------+--------------+---------------------------+
  172. | ``author`` | boolean | Optional | | A boolean of whether to |
  173. | | | | return the issues |
  174. | | | | created by this user or |
  175. | | | | not. Defaults to True |
  176. +---------------+---------+--------------+---------------------------+
  177. Sample response
  178. ^^^^^^^^^^^^^^^
  179. ::
  180. {
  181. "args": {
  182. "milestones": [],
  183. "no_stones": null,
  184. "order": null,
  185. "order_key": null,
  186. "since": null,
  187. "status": null,
  188. "tags": []
  189. },
  190. "issues_assigned": [
  191. {
  192. "assignee": {
  193. "fullname": "Anar Adilova",
  194. "name": "anar"
  195. },
  196. "blocks": [],
  197. "close_status": null,
  198. "closed_at": null,
  199. "comments": [],
  200. "content": "Test Issue",
  201. "custom_fields": [],
  202. "date_created": "1510124763",
  203. "depends": [],
  204. "id": 2,
  205. "last_updated": "1510124763",
  206. "milestone": null,
  207. "priority": null,
  208. "private": false,
  209. "status": "Open",
  210. "tags": [],
  211. "title": "issue4",
  212. "user": {
  213. "fullname": "Anar Adilova",
  214. "name": "anar"
  215. }
  216. }
  217. ],
  218. "issues_created": [
  219. {
  220. "assignee": {
  221. "fullname": "Anar Adilova",
  222. "name": "anar"
  223. },
  224. "blocks": [],
  225. "close_status": null,
  226. "closed_at": null,
  227. "comments": [],
  228. "content": "Test Issue",
  229. "custom_fields": [],
  230. "date_created": "1510124763",
  231. "depends": [],
  232. "id": 2,
  233. "last_updated": "1510124763",
  234. "milestone": null,
  235. "priority": null,
  236. "private": false,
  237. "status": "Open",
  238. "tags": [],
  239. "title": "issue4",
  240. "user": {
  241. "fullname": "Anar Adilova",
  242. "name": "anar"
  243. }
  244. }
  245. ],
  246. "total_issues_assigned": 1,
  247. "total_issues_created": 1
  248. }
  249. """
  250. milestone = flask.request.args.getlist('milestones', None)
  251. no_stones = flask.request.args.get('no_stones', None)
  252. if no_stones is not None:
  253. if str(no_stones).lower() in ['1', 'true', 't']:
  254. no_stones = True
  255. else:
  256. no_stones = False
  257. since = flask.request.args.get('since', None)
  258. order = flask.request.args.get('order', None)
  259. order_key = flask.request.args.get('order_key', None)
  260. status = flask.request.args.get('status', None)
  261. tags = flask.request.args.getlist('tags')
  262. tags = [tag.strip() for tag in tags if tag.strip()]
  263. page = flask.request.args.get('page', 1)
  264. assignee = flask.request.args.get('assignee', '').lower()\
  265. not in ['false', '0', 'f']
  266. author = flask.request.args.get('author', '').lower() \
  267. not in ['false', '0', 'f']
  268. try:
  269. page = int(page)
  270. if page <= 0:
  271. raise ValueError()
  272. except ValueError:
  273. raise pagure.exceptions.APIError(
  274. 400, error_code=APIERROR.ENOCODE,
  275. error='Invalid page requested')
  276. offset = (page - 1) * 50
  277. limit = page * 50
  278. params = {
  279. 'session': flask.g.session,
  280. 'tags': tags,
  281. 'milestones': milestone,
  282. 'order': order,
  283. 'order_key': order_key,
  284. 'no_milestones': no_stones,
  285. 'offset': offset,
  286. 'limit': limit,
  287. }
  288. if status is not None:
  289. if status.lower() == 'all':
  290. params.update({'status': None})
  291. elif status.lower() == 'closed':
  292. params.update({'closed': True})
  293. else:
  294. params.update({'status': status})
  295. else:
  296. params.update({'status': 'Open'})
  297. updated_after = None
  298. if since:
  299. # Validate and convert the time
  300. if since.isdigit():
  301. # We assume its a timestamp, so convert it to datetime
  302. try:
  303. updated_after = datetime.datetime.fromtimestamp(int(since))
  304. except ValueError:
  305. raise pagure.exceptions.APIError(
  306. 400, error_code=APIERROR.ETIMESTAMP)
  307. else:
  308. # We assume datetime format, so validate it
  309. try:
  310. updated_after = datetime.datetime.strptime(since, '%Y-%m-%d')
  311. except ValueError:
  312. raise pagure.exceptions.APIError(
  313. 400, error_code=APIERROR.EDATETIME)
  314. params.update({'updated_after': updated_after})
  315. issues_created = []
  316. issues_created_pages = 1
  317. if author:
  318. # Issues authored by this user
  319. params_created = params.copy()
  320. params_created.update({"author": username})
  321. issues_created = pagure.lib.search_issues(**params_created)
  322. params_created.update({"offset": None, 'limit': None, 'count': True})
  323. issues_created_cnt = pagure.lib.search_issues(**params_created)
  324. issues_created_pages = int(
  325. ceil(issues_created_cnt / float(50))) or 1
  326. issues_assigned = []
  327. issues_assigned_pages = 1
  328. if assignee:
  329. # Issues assigned to this user
  330. params_assigned = params.copy()
  331. params_assigned.update({"assignee": username})
  332. issues_assigned = pagure.lib.search_issues(**params_assigned)
  333. params_assigned.update({"offset": None, 'limit': None, 'count': True})
  334. issues_assigned_cnt = pagure.lib.search_issues(**params_assigned)
  335. issues_assigned_pages = int(
  336. ceil(issues_assigned_cnt / float(50))) or 1
  337. jsonout = flask.jsonify({
  338. 'total_issues_created_pages': issues_created_pages,
  339. 'total_issues_assigned_pages': issues_assigned_pages,
  340. 'total_issues_created': len(issues_created),
  341. 'total_issues_assigned': len(issues_assigned),
  342. 'issues_created': [issue.to_json(public=True, with_project=True)
  343. for issue in issues_created],
  344. 'issues_assigned': [issue.to_json(public=True, with_project=True)
  345. for issue in issues_assigned],
  346. 'args': {
  347. 'milestones': milestone,
  348. 'no_stones': no_stones,
  349. 'order': order,
  350. 'order_key': order_key,
  351. 'since': since,
  352. 'status': status,
  353. 'tags': tags,
  354. 'page': page,
  355. 'assignee': assignee,
  356. 'author': author,
  357. }
  358. })
  359. return jsonout
  360. @API.route('/user/<username>/activity/stats')
  361. @api_method
  362. def api_view_user_activity_stats(username):
  363. """
  364. User activity stats
  365. -------------------
  366. Use this endpoint to retrieve activity stats about a specific user over
  367. the last year.
  368. ::
  369. GET /api/0/user/<username>/activity/stats
  370. ::
  371. GET /api/0/user/ralph/activity/stats
  372. GET /api/0/user/ralph/activity/stats?format=timestamp
  373. Parameters
  374. ^^^^^^^^^^
  375. +---------------+----------+--------------+----------------------------+
  376. | Key | Type | Optionality | Description |
  377. +===============+==========+==============+============================+
  378. | ``username`` | string | Mandatory | | The username of the user |
  379. | | | | whose activity you are |
  380. | | | | interested in. |
  381. +---------------+----------+--------------+----------------------------+
  382. | ``format`` | string | Optional | | Allows changing the |
  383. | | | | of the date/time returned|
  384. | | | | from iso format to unix |
  385. | | | | timestamp |
  386. | | | | Can be: `timestamp` |
  387. | | | | or `isoformat` |
  388. +---------------+----------+--------------+----------------------------+
  389. Sample response
  390. ^^^^^^^^^^^^^^^
  391. ::
  392. {
  393. "2015-11-04": 9,
  394. "2015-11-06": 3,
  395. "2015-11-09": 6,
  396. "2015-11-13": 4,
  397. "2015-11-15": 3,
  398. "2015-11-18": 15,
  399. "2015-11-19": 3,
  400. "2015-11-20": 15,
  401. "2015-11-26": 18,
  402. "2015-11-30": 116,
  403. "2015-12-02": 12,
  404. "2015-12-03": 2
  405. }
  406. or::
  407. {
  408. "1446591600": 9,
  409. "1446764400": 3,
  410. "1447023600": 6,
  411. "1447369200": 4,
  412. "1447542000": 3,
  413. "1447801200": 15,
  414. "1447887600": 3,
  415. "1447974000": 15,
  416. "1448492400": 18,
  417. "1448838000": 116,
  418. "1449010800": 12,
  419. "1449097200": 2
  420. }
  421. """
  422. date_format = flask.request.args.get('format', 'isoformat')
  423. tz = flask.request.args.get('tz', 'UTC')
  424. user = _get_user(username=username)
  425. stats = pagure.lib.get_yearly_stats_user(
  426. flask.g.session,
  427. user,
  428. datetime.datetime.utcnow().date() + datetime.timedelta(days=1),
  429. tz=tz
  430. )
  431. def format_date(d, tz):
  432. if date_format == 'timestamp':
  433. # the reason we have this at all is the cal-heatmap js lib
  434. # wants times as timestamps. We're trying to feed it a
  435. # timestamp it will count as having happened on date 'd'.
  436. # However, cal-heatmap always uses the browser timezone,
  437. # so we have to be careful to produce a timestamp which
  438. # falls on the correct date *in the browser timezone*. We
  439. # aim for noon on the desired date.
  440. try:
  441. return arrow.get(d, tz).replace(hour=12).timestamp
  442. except arrow.parser.ParserError:
  443. # if tz is invalid for some reason, just go with UTC
  444. return arrow.get(d).replace(hour=12).timestamp
  445. else:
  446. d = d.isoformat()
  447. return d
  448. stats = {format_date(d[0], tz): d[1] for d in stats}
  449. jsonout = flask.jsonify(stats)
  450. return jsonout
  451. @API.route('/user/<username>/activity/<date>')
  452. @api_method
  453. def api_view_user_activity_date(username, date):
  454. """
  455. User activity on a specific date
  456. --------------------------------
  457. Use this endpoint to retrieve activity information about a specific user
  458. on the specified date.
  459. ::
  460. GET /api/0/user/<username>/activity/<date>
  461. ::
  462. GET /api/0/user/ralph/activity/2016-01-02
  463. GET /api/0/user/ralph/activity/2016-01-02?grouped=true
  464. Parameters
  465. ^^^^^^^^^^
  466. +---------------+----------+--------------+----------------------------+
  467. | Key | Type | Optionality | Description |
  468. +===============+==========+==============+============================+
  469. | ``username`` | string | Mandatory | | The username of the user |
  470. | | | | whose activity you are |
  471. | | | | interested in. |
  472. +---------------+----------+--------------+----------------------------+
  473. | ``date`` | string | Mandatory | | The date of interest, |
  474. | | | | best provided in ISO |
  475. | | | | format: YYYY-MM-DD |
  476. +---------------+----------+--------------+----------------------------+
  477. | ``grouped`` | boolean | Optional | | Whether or not to group |
  478. | | | | the commits |
  479. +---------------+----------+--------------+----------------------------+
  480. Sample response
  481. ^^^^^^^^^^^^^^^
  482. ::
  483. {
  484. "activities": [
  485. {
  486. "date": "2016-02-24",
  487. "date_created": "1456305852",
  488. "description": "pingou created PR test#44",
  489. "description_mk": "<p>pingou created PR <a href=\"/test/pull-request/44\" title=\"Update test_foo\">test#44</a></p>",
  490. "id": 4067,
  491. "user": {
  492. "fullname": "Pierre-YvesC",
  493. "name": "pingou"
  494. }
  495. },
  496. {
  497. "date": "2016-02-24",
  498. "date_created": "1456305887",
  499. "description": "pingou commented on PR test#44",
  500. "description_mk": "<p>pingou commented on PR <a href=\"/test/pull-request/44\" title=\"Update test_foo\">test#44</a></p>",
  501. "id": 4112,
  502. "user": {
  503. "fullname": "Pierre-YvesC",
  504. "name": "pingou"
  505. }
  506. }
  507. ]
  508. }
  509. """ # noqa
  510. grouped = str(flask.request.args.get('grouped')).lower() in ['1', 'true']
  511. tz = flask.request.args.get('tz', 'UTC')
  512. try:
  513. date = arrow.get(date)
  514. date = date.strftime('%Y-%m-%d')
  515. except arrow.parser.ParserError as err:
  516. raise pagure.exceptions.APIError(
  517. 400, error_code=APIERROR.ENOCODE, error=str(err))
  518. user = _get_user(username=username)
  519. activities = pagure.lib.get_user_activity_day(
  520. flask.g.session, user, date, tz=tz
  521. )
  522. js_act = []
  523. if grouped:
  524. commits = collections.defaultdict(list)
  525. acts = []
  526. for activity in activities:
  527. if activity.log_type == 'committed':
  528. commits[activity.project.fullname].append(activity)
  529. else:
  530. acts.append(activity)
  531. for project in commits:
  532. if len(commits[project]) == 1:
  533. tmp = dict(
  534. description_mk=pagure.lib.text2markdown(
  535. str(commits[project][0]))
  536. )
  537. else:
  538. tmp = dict(
  539. description_mk=pagure.lib.text2markdown(
  540. '@%s pushed %s commits to %s' % (
  541. username, len(commits[project]), project
  542. )
  543. )
  544. )
  545. js_act.append(tmp)
  546. activities = acts
  547. for act in activities:
  548. activity = act.to_json(public=True)
  549. activity['description_mk'] = pagure.lib.text2markdown(str(act))
  550. js_act.append(activity)
  551. jsonout = flask.jsonify(
  552. dict(
  553. activities=js_act,
  554. date=date,
  555. )
  556. )
  557. return jsonout
  558. @API.route('/user/<username>/requests/filed')
  559. @api_method
  560. def api_view_user_requests_filed(username):
  561. """
  562. List pull-requests filled by user
  563. ---------------------------------
  564. Use this endpoint to retrieve a list of open pull requests a user has
  565. filed over the entire pagure instance.
  566. ::
  567. GET /api/0/user/<username>/requests/filed
  568. ::
  569. GET /api/0/user/dudemcpants/requests/filed
  570. Parameters
  571. ^^^^^^^^^^
  572. +---------------+----------+--------------+----------------------------+
  573. | Key | Type | Optionality | Description |
  574. +===============+==========+==============+============================+
  575. | ``username`` | string | Mandatory | | The username of the user |
  576. | | | | whose activity you are |
  577. | | | | interested in. |
  578. +---------------+----------+--------------+----------------------------+
  579. | ``page`` | integer | Mandatory | | The page requested. |
  580. | | | | Defaults to 1. |
  581. +---------------+----------+--------------+----------------------------+
  582. | ``status`` | string | Optional | | Filter the status of |
  583. | | | | pull requests. Default: |
  584. | | | | ``Open`` (open pull |
  585. | | | | requests), can be |
  586. | | | | ``Closed`` for closed |
  587. | | | | requests, ``Merged`` |
  588. | | | | for merged requests, or |
  589. | | | | ``Open`` for open |
  590. | | | | requests. |
  591. | | | | ``All`` returns closed, |
  592. | | | | merged and open requests.|
  593. +---------------+----------+--------------+----------------------------+
  594. Sample response
  595. ^^^^^^^^^^^^^^^
  596. ::
  597. {
  598. "args": {
  599. "status": "open",
  600. "username": "dudemcpants",
  601. "page": 1,
  602. },
  603. "requests": [
  604. {
  605. "assignee": null,
  606. "branch": "master",
  607. "branch_from": "master",
  608. "closed_at": null,
  609. "closed_by": null,
  610. "comments": [],
  611. "commit_start": "3973fae98fc485783ca14f5c3612d85832185065",
  612. "commit_stop": "3973fae98fc485783ca14f5c3612d85832185065",
  613. "date_created": "1510227832",
  614. "id": 2,
  615. "initial_comment": null,
  616. "last_updated": "1510227833",
  617. "project": {
  618. "access_groups": {
  619. "admin": [],
  620. "commit": [],
  621. "ticket": []
  622. },
  623. "access_users": {
  624. "admin": [],
  625. "commit": [],
  626. "owner": [
  627. "ryanlerch"
  628. ],
  629. "ticket": []
  630. },
  631. "close_status": [],
  632. "custom_keys": [],
  633. "date_created": "1510227638",
  634. "date_modified": "1510227638",
  635. "description": "this is a quick project",
  636. "fullname": "aquickproject",
  637. "id": 1,
  638. "milestones": {},
  639. "name": "aquickproject",
  640. "namespace": null,
  641. "parent": null,
  642. "priorities": {},
  643. "tags": [],
  644. "url_path": "aquickproject",
  645. "user": {
  646. "fullname": "ryanlerch",
  647. "name": "ryanlerch"
  648. }
  649. },
  650. "remote_git": null,
  651. "repo_from": {
  652. "access_groups": {
  653. "admin": [],
  654. "commit": [],
  655. "ticket": []
  656. },
  657. "access_users": {
  658. "admin": [],
  659. "commit": [],
  660. "owner": [
  661. "dudemcpants"
  662. ],
  663. "ticket": []
  664. },
  665. "close_status": [],
  666. "custom_keys": [],
  667. "date_created": "1510227729",
  668. "date_modified": "1510227729",
  669. "description": "this is a quick project",
  670. "fullname": "forks/dudemcpants/aquickproject",
  671. "id": 2,
  672. "milestones": {},
  673. "name": "aquickproject",
  674. "namespace": null,
  675. "parent": {
  676. "access_groups": {
  677. "admin": [],
  678. "commit": [],
  679. "ticket": []
  680. },
  681. "access_users": {
  682. "admin": [],
  683. "commit": [],
  684. "owner": [
  685. "ryanlerch"
  686. ],
  687. "ticket": []
  688. },
  689. "close_status": [],
  690. "custom_keys": [],
  691. "date_created": "1510227638",
  692. "date_modified": "1510227638",
  693. "description": "this is a quick project",
  694. "fullname": "aquickproject",
  695. "id": 1,
  696. "milestones": {},
  697. "name": "aquickproject",
  698. "namespace": null,
  699. "parent": null,
  700. "priorities": {},
  701. "tags": [],
  702. "url_path": "aquickproject",
  703. "user": {
  704. "fullname": "ryanlerch",
  705. "name": "ryanlerch"
  706. }
  707. },
  708. "priorities": {},
  709. "tags": [],
  710. "url_path": "fork/dudemcpants/aquickproject",
  711. "user": {
  712. "fullname": "Dude McPants",
  713. "name": "dudemcpants"
  714. }
  715. },
  716. "status": "Open",
  717. "title": "Update README.md",
  718. "uid": "819e0b1c449e414fa291c914f28d73ec",
  719. "updated_on": "1510227832",
  720. "user": {
  721. "fullname": "Dude McPants",
  722. "name": "dudemcpants"
  723. }
  724. }
  725. ],
  726. "total_requests": 1
  727. }
  728. """
  729. status = flask.request.args.get('status', 'open')
  730. page = flask.request.args.get('page', 1)
  731. try:
  732. page = int(page)
  733. if page <= 0:
  734. raise ValueError()
  735. except ValueError:
  736. raise pagure.exceptions.APIError(
  737. 400, error_code=APIERROR.ENOCODE,
  738. error='Invalid page requested')
  739. offset = (page - 1) * 50
  740. limit = page * 50
  741. orig_status = status
  742. if status.lower() == 'all':
  743. status = None
  744. else:
  745. status = status.capitalize()
  746. pullrequests = pagure.lib.get_pull_request_of_user(
  747. flask.g.session,
  748. username=username,
  749. status=status,
  750. offset=offset,
  751. limit=limit,
  752. )
  753. pullrequestslist = [
  754. pr.to_json(public=True, api=True)
  755. for pr in pullrequests
  756. if pr.user.username == username
  757. ]
  758. return flask.jsonify({
  759. 'total_requests': len(pullrequestslist),
  760. 'requests': pullrequestslist,
  761. 'args': {
  762. 'username': username,
  763. 'status': orig_status,
  764. 'page': page,
  765. }
  766. })
  767. @API.route('/user/<username>/requests/actionable')
  768. @api_method
  769. def api_view_user_requests_actionable(username):
  770. """
  771. List PRs actionable by user
  772. ---------------------------
  773. Use this endpoint to retrieve a list of open pull requests a user is
  774. able to action (e.g. merge) over the entire pagure instance.
  775. ::
  776. GET /api/0/user/<username>/requests/actionable
  777. ::
  778. GET /api/0/user/dudemcpants/requests/actionable
  779. Parameters
  780. ^^^^^^^^^^
  781. +---------------+----------+--------------+----------------------------+
  782. | Key | Type | Optionality | Description |
  783. +===============+==========+==============+============================+
  784. | ``username`` | string | Mandatory | | The username of the user |
  785. | | | | whose activity you are |
  786. | | | | interested in. |
  787. +---------------+----------+--------------+----------------------------+
  788. | ``page`` | integer | Mandatory | | The page requested. |
  789. | | | | Defaults to 1. |
  790. +---------------+----------+--------------+----------------------------+
  791. | ``status`` | string | Optional | | Filter the status of |
  792. | | | | pull requests. Default: |
  793. | | | | ``Open`` (open pull |
  794. | | | | requests), can be |
  795. | | | | ``Closed`` for closed |
  796. | | | | requests, ``Merged`` |
  797. | | | | for merged requests, or |
  798. | | | | ``Open`` for open |
  799. | | | | requests. |
  800. | | | | ``All`` returns closed, |
  801. | | | | merged and open requests.|
  802. +---------------+----------+--------------+----------------------------+
  803. Sample response
  804. ^^^^^^^^^^^^^^^
  805. ::
  806. {
  807. "args": {
  808. "status": "open",
  809. "username": "ryanlerch",
  810. "page": 1,
  811. },
  812. "requests": [
  813. {
  814. "assignee": null,
  815. "branch": "master",
  816. "branch_from": "master",
  817. "closed_at": null,
  818. "closed_by": null,
  819. "comments": [],
  820. "commit_start": "3973fae98fc485783ca14f5c3612d85832185065",
  821. "commit_stop": "3973fae98fc485783ca14f5c3612d85832185065",
  822. "date_created": "1510227832",
  823. "id": 2,
  824. "initial_comment": null,
  825. "last_updated": "1510227833",
  826. "project": {
  827. "access_groups": {
  828. "admin": [],
  829. "commit": [],
  830. "ticket": []
  831. },
  832. "access_users": {
  833. "admin": [],
  834. "commit": [],
  835. "owner": [
  836. "ryanlerch"
  837. ],
  838. "ticket": []
  839. },
  840. "close_status": [],
  841. "custom_keys": [],
  842. "date_created": "1510227638",
  843. "date_modified": "1510227638",
  844. "description": "this is a quick project",
  845. "fullname": "aquickproject",
  846. "id": 1,
  847. "milestones": {},
  848. "name": "aquickproject",
  849. "namespace": null,
  850. "parent": null,
  851. "priorities": {},
  852. "tags": [],
  853. "url_path": "aquickproject",
  854. "user": {
  855. "fullname": "ryanlerch",
  856. "name": "ryanlerch"
  857. }
  858. },
  859. "remote_git": null,
  860. "repo_from": {
  861. "access_groups": {
  862. "admin": [],
  863. "commit": [],
  864. "ticket": []
  865. },
  866. "access_users": {
  867. "admin": [],
  868. "commit": [],
  869. "owner": [
  870. "dudemcpants"
  871. ],
  872. "ticket": []
  873. },
  874. "close_status": [],
  875. "custom_keys": [],
  876. "date_created": "1510227729",
  877. "date_modified": "1510227729",
  878. "description": "this is a quick project",
  879. "fullname": "forks/dudemcpants/aquickproject",
  880. "id": 2,
  881. "milestones": {},
  882. "name": "aquickproject",
  883. "namespace": null,
  884. "parent": {
  885. "access_groups": {
  886. "admin": [],
  887. "commit": [],
  888. "ticket": []
  889. },
  890. "access_users": {
  891. "admin": [],
  892. "commit": [],
  893. "owner": [
  894. "ryanlerch"
  895. ],
  896. "ticket": []
  897. },
  898. "close_status": [],
  899. "custom_keys": [],
  900. "date_created": "1510227638",
  901. "date_modified": "1510227638",
  902. "description": "this is a quick project",
  903. "fullname": "aquickproject",
  904. "id": 1,
  905. "milestones": {},
  906. "name": "aquickproject",
  907. "namespace": null,
  908. "parent": null,
  909. "priorities": {},
  910. "tags": [],
  911. "url_path": "aquickproject",
  912. "user": {
  913. "fullname": "ryanlerch",
  914. "name": "ryanlerch"
  915. }
  916. },
  917. "priorities": {},
  918. "tags": [],
  919. "url_path": "fork/dudemcpants/aquickproject",
  920. "user": {
  921. "fullname": "Dude McPants",
  922. "name": "dudemcpants"
  923. }
  924. },
  925. "status": "Open",
  926. "title": "Update README.md",
  927. "uid": "819e0b1c449e414fa291c914f28d73ec",
  928. "updated_on": "1510227832",
  929. "user": {
  930. "fullname": "Dude McPants",
  931. "name": "dudemcpants"
  932. }
  933. }
  934. ],
  935. "total_requests": 1
  936. }
  937. """
  938. status = flask.request.args.get('status', 'open')
  939. page = flask.request.args.get('page', 1)
  940. try:
  941. page = int(page)
  942. if page <= 0:
  943. raise ValueError()
  944. except ValueError:
  945. raise pagure.exceptions.APIError(
  946. 400, error_code=APIERROR.ENOCODE,
  947. error='Invalid page requested')
  948. offset = (page - 1) * 50
  949. limit = page * 50
  950. orig_status = status
  951. if status.lower() == 'all':
  952. status = None
  953. else:
  954. status = status.capitalize()
  955. pullrequests = pagure.lib.get_pull_request_of_user(
  956. flask.g.session,
  957. username=username,
  958. status=status,
  959. offset=offset,
  960. limit=limit,
  961. )
  962. pullrequestslist = [
  963. pr.to_json(public=True, api=True)
  964. for pr in pullrequests
  965. if pr.user.username != username
  966. ]
  967. return flask.jsonify({
  968. 'total_requests': len(pullrequestslist),
  969. 'requests': pullrequestslist,
  970. 'args': {
  971. 'username': username,
  972. 'status': orig_status,
  973. 'page': page,
  974. }
  975. })