Introduce an Events API
* Meld the following disparate endpoints: * `/projects/:id/events` * `/events` * `/users/:id/events` + Add result filtering to the above endpoints: * action * target_type * before and after dates
This commit is contained in:
parent
e34e576104
commit
ad3e180ed3
15 changed files with 712 additions and 443 deletions
62
app/finders/events_finder.rb
Normal file
62
app/finders/events_finder.rb
Normal file
|
@ -0,0 +1,62 @@
|
|||
class EventsFinder
|
||||
attr_reader :source, :params, :current_user
|
||||
|
||||
# Used to filter Events
|
||||
#
|
||||
# Arguments:
|
||||
# source - which user or project to looks for events on
|
||||
# current_user - only return events for projects visible to this user
|
||||
# params:
|
||||
# action: string
|
||||
# target_type: string
|
||||
# before: datetime
|
||||
# after: datetime
|
||||
#
|
||||
def initialize(params = {})
|
||||
@source = params.delete(:source)
|
||||
@current_user = params.delete(:current_user)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
events = source.events
|
||||
|
||||
events = by_current_user_access(events)
|
||||
events = by_action(events)
|
||||
events = by_target_type(events)
|
||||
events = by_created_at_before(events)
|
||||
events = by_created_at_after(events)
|
||||
|
||||
events
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def by_current_user_access(events)
|
||||
events.merge(ProjectsFinder.new(current_user: current_user).execute).references(:project)
|
||||
end
|
||||
|
||||
def by_action(events)
|
||||
return events unless Event::ACTIONS[params[:action]]
|
||||
|
||||
events.where(action: Event::ACTIONS[params[:action]])
|
||||
end
|
||||
|
||||
def by_target_type(events)
|
||||
return events unless Event::TARGET_TYPES[params[:target_type]]
|
||||
|
||||
events.where(target_type: Event::TARGET_TYPES[params[:target_type]])
|
||||
end
|
||||
|
||||
def by_created_at_before(events)
|
||||
return events unless params[:before]
|
||||
|
||||
events.where('events.created_at < ?', params[:before].beginning_of_day)
|
||||
end
|
||||
|
||||
def by_created_at_after(events)
|
||||
return events unless params[:after]
|
||||
|
||||
events.where('events.created_at > ?', params[:after].end_of_day)
|
||||
end
|
||||
end
|
|
@ -14,6 +14,30 @@ class Event < ActiveRecord::Base
|
|||
DESTROYED = 10
|
||||
EXPIRED = 11 # User left project due to expiry
|
||||
|
||||
ACTIONS = HashWithIndifferentAccess.new(
|
||||
created: CREATED,
|
||||
updated: UPDATED,
|
||||
closed: CLOSED,
|
||||
reopened: REOPENED,
|
||||
pushed: PUSHED,
|
||||
commented: COMMENTED,
|
||||
merged: MERGED,
|
||||
joined: JOINED,
|
||||
left: LEFT,
|
||||
destroyed: DESTROYED,
|
||||
expired: EXPIRED
|
||||
).freeze
|
||||
|
||||
TARGET_TYPES = HashWithIndifferentAccess.new(
|
||||
issue: Issue,
|
||||
milestone: Milestone,
|
||||
merge_request: MergeRequest,
|
||||
note: Note,
|
||||
project: Project,
|
||||
snippet: Snippet,
|
||||
user: User
|
||||
).freeze
|
||||
|
||||
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
|
||||
|
||||
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
|
||||
|
@ -55,6 +79,14 @@ class Event < ActiveRecord::Base
|
|||
def limit_recent(limit = 20, offset = nil)
|
||||
recent.limit(limit).offset(offset)
|
||||
end
|
||||
|
||||
def actions
|
||||
ACTIONS.keys
|
||||
end
|
||||
|
||||
def target_types
|
||||
TARGET_TYPES.keys
|
||||
end
|
||||
end
|
||||
|
||||
def visible_to_user?(user = nil)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Introduce an Events API
|
||||
merge_request: 11755
|
||||
author:
|
|
@ -16,6 +16,7 @@ following locations:
|
|||
- [Deployments](deployments.md)
|
||||
- [Deploy Keys](deploy_keys.md)
|
||||
- [Environments](environments.md)
|
||||
- [Events](events.md)
|
||||
- [Gitignores templates](templates/gitignores.md)
|
||||
- [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
|
||||
- [Groups](groups.md)
|
||||
|
|
347
doc/api/events.md
Normal file
347
doc/api/events.md
Normal file
|
@ -0,0 +1,347 @@
|
|||
# Events
|
||||
|
||||
## Filter parameters
|
||||
|
||||
### Action Types
|
||||
|
||||
Available action types for the `action` parameter are:
|
||||
|
||||
- `created`
|
||||
- `updated`
|
||||
- `closed`
|
||||
- `reopened`
|
||||
- `pushed`
|
||||
- `commented`
|
||||
- `merged`
|
||||
- `joined`
|
||||
- `left`
|
||||
- `destroyed`
|
||||
- `expired`
|
||||
|
||||
Note that these options are downcased.
|
||||
|
||||
### Target Types
|
||||
|
||||
Available target types for the `target_type` parameter are:
|
||||
|
||||
- `issue`
|
||||
- `milestone`
|
||||
- `merge_request`
|
||||
- `note`
|
||||
- `project`
|
||||
- `snippet`
|
||||
- `user`
|
||||
|
||||
Note that these options are downcased.
|
||||
|
||||
### Date formatting
|
||||
|
||||
Dates for the `before` and `after` parameters should be supplied in the following format:
|
||||
|
||||
```
|
||||
YYYY-MM-DD
|
||||
```
|
||||
|
||||
## List currently authenticated user's events
|
||||
|
||||
>**Note:** This endpoint was introduced in GitLab 9.3.
|
||||
|
||||
Get a list of events for the authenticated user.
|
||||
|
||||
```
|
||||
GET /events
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `action` | string | no | Include only events of a particular [action type][action-types] |
|
||||
| `target_type` | string | no | Include only events of a particular [target type][target-types] |
|
||||
| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
|
||||
| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
|
||||
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
|
||||
|
||||
Example request:
|
||||
|
||||
```
|
||||
curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title":null,
|
||||
"project_id":1,
|
||||
"action_name":"opened",
|
||||
"target_id":160,
|
||||
"target_type":"Issue",
|
||||
"author_id":25,
|
||||
"data":null,
|
||||
"target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
|
||||
"created_at":"2017-02-09T10:43:19.667Z",
|
||||
"author":{
|
||||
"name":"User 3",
|
||||
"username":"user3",
|
||||
"id":25,
|
||||
"state":"active",
|
||||
"avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon",
|
||||
"web_url":"https://gitlab.example.com/user3"
|
||||
},
|
||||
"author_username":"user3"
|
||||
},
|
||||
{
|
||||
"title":null,
|
||||
"project_id":1,
|
||||
"action_name":"opened",
|
||||
"target_id":159,
|
||||
"target_type":"Issue",
|
||||
"author_id":21,
|
||||
"data":null,
|
||||
"target_title":"Nostrum enim non et sed optio illo deleniti non.",
|
||||
"created_at":"2017-02-09T10:43:19.426Z",
|
||||
"author":{
|
||||
"name":"Test User",
|
||||
"username":"ted",
|
||||
"id":21,
|
||||
"state":"active",
|
||||
"avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon",
|
||||
"web_url":"https://gitlab.example.com/ted"
|
||||
},
|
||||
"author_username":"ted"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Get user contribution events
|
||||
|
||||
>**Note:** Documentation was formerly located in the [Users API pages][users-api].
|
||||
|
||||
Get the contribution events for the specified user, sorted from newest to oldest.
|
||||
|
||||
```
|
||||
GET /users/:id/events
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of the user |
|
||||
| `action` | string | no | Include only events of a particular [action type][action-types] |
|
||||
| `target_type` | string | no | Include only events of a particular [target type][target-types] |
|
||||
| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
|
||||
| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
|
||||
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "closed",
|
||||
"target_id": 830,
|
||||
"target_type": "Issue",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": "Public project search field",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "opened",
|
||||
"target_id": null,
|
||||
"target_type": null,
|
||||
"author_id": 1,
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "john",
|
||||
"data": {
|
||||
"before": "50d4420237a9de7be1304607147aec22e4a14af7",
|
||||
"after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"ref": "refs/heads/master",
|
||||
"user_id": 1,
|
||||
"user_name": "Dmitriy Zaporozhets",
|
||||
"repository": {
|
||||
"name": "gitlabhq",
|
||||
"url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
|
||||
"description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
|
||||
"homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
|
||||
},
|
||||
"commits": [
|
||||
{
|
||||
"id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"message": "Add simple search to projects in public area",
|
||||
"timestamp": "2013-05-13T18:18:08+00:00",
|
||||
"url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"email": "dmitriy.zaporozhets@gmail.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"total_commits_count": 1
|
||||
},
|
||||
"target_title": null
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "closed",
|
||||
"target_id": 840,
|
||||
"target_type": "Issue",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": "Finish & merge Code search PR",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "commented on",
|
||||
"target_id": 1312,
|
||||
"target_type": "Note",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": null,
|
||||
"created_at": "2015-12-04T10:33:58.089Z",
|
||||
"note": {
|
||||
"id": 1312,
|
||||
"body": "What an awesome day!",
|
||||
"attachment": null,
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"created_at": "2015-12-04T10:33:56.698Z",
|
||||
"system": false,
|
||||
"noteable_id": 377,
|
||||
"noteable_type": "Issue"
|
||||
},
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## List a Project's visible events
|
||||
|
||||
>**Note:** This endpoint has been around longer than the others. Documentation was formerly located in the [Projects API pages][projects-api].
|
||||
|
||||
Get a list of visible events for a particular project.
|
||||
|
||||
```
|
||||
GET /:project_id/events
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
|
||||
| `action` | string | no | Include only events of a particular [action type][action-types] |
|
||||
| `target_type` | string | no | Include only events of a particular [target type][target-types] |
|
||||
| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
|
||||
| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
|
||||
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
|
||||
|
||||
Example request:
|
||||
|
||||
```
|
||||
curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:project_id/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title":null,
|
||||
"project_id":1,
|
||||
"action_name":"opened",
|
||||
"target_id":160,
|
||||
"target_type":"Issue",
|
||||
"author_id":25,
|
||||
"data":null,
|
||||
"target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
|
||||
"created_at":"2017-02-09T10:43:19.667Z",
|
||||
"author":{
|
||||
"name":"User 3",
|
||||
"username":"user3",
|
||||
"id":25,
|
||||
"state":"active",
|
||||
"avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon",
|
||||
"web_url":"https://gitlab.example.com/user3"
|
||||
},
|
||||
"author_username":"user3"
|
||||
},
|
||||
{
|
||||
"title":null,
|
||||
"project_id":1,
|
||||
"action_name":"opened",
|
||||
"target_id":159,
|
||||
"target_type":"Issue",
|
||||
"author_id":21,
|
||||
"data":null,
|
||||
"target_title":"Nostrum enim non et sed optio illo deleniti non.",
|
||||
"created_at":"2017-02-09T10:43:19.426Z",
|
||||
"author":{
|
||||
"name":"Test User",
|
||||
"username":"ted",
|
||||
"id":21,
|
||||
"state":"active",
|
||||
"avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon",
|
||||
"web_url":"https://gitlab.example.com/ted"
|
||||
},
|
||||
"author_username":"ted"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
[target-types]: #target-types "Target Type parameter"
|
||||
[action-types]: #action-types "Action Type parameter"
|
||||
[date-formatting]: #date-formatting "Date Formatting guidance"
|
||||
[projects-api]: projects.md "Projects API pages"
|
||||
[users-api]: users.md "Users API pages"
|
|
@ -310,143 +310,7 @@ GET /projects/:id/users
|
|||
|
||||
### Get project events
|
||||
|
||||
Get the events for the specified project sorted from newest to oldest. This
|
||||
endpoint can be accessed without authentication if the project is publicly
|
||||
accessible.
|
||||
|
||||
```
|
||||
GET /projects/:id/events
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "closed",
|
||||
"target_id": 830,
|
||||
"target_type": "Issue",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": "Public project search field",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "opened",
|
||||
"target_id": null,
|
||||
"target_type": null,
|
||||
"author_id": 1,
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "john",
|
||||
"data": {
|
||||
"before": "50d4420237a9de7be1304607147aec22e4a14af7",
|
||||
"after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"ref": "refs/heads/master",
|
||||
"user_id": 1,
|
||||
"user_name": "Dmitriy Zaporozhets",
|
||||
"repository": {
|
||||
"name": "gitlabhq",
|
||||
"url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
|
||||
"description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
|
||||
"homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
|
||||
},
|
||||
"commits": [
|
||||
{
|
||||
"id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"message": "Add simple search to projects in public area",
|
||||
"timestamp": "2013-05-13T18:18:08+00:00",
|
||||
"url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"email": "dmitriy.zaporozhets@gmail.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"total_commits_count": 1
|
||||
},
|
||||
"target_title": null
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "closed",
|
||||
"target_id": 840,
|
||||
"target_type": "Issue",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": "Finish & merge Code search PR",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "commented on",
|
||||
"target_id": 1312,
|
||||
"target_type": "Note",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": null,
|
||||
"created_at": "2015-12-04T10:33:58.089Z",
|
||||
"note": {
|
||||
"id": 1312,
|
||||
"body": "What an awesome day!",
|
||||
"attachment": null,
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"created_at": "2015-12-04T10:33:56.698Z",
|
||||
"system": false,
|
||||
"noteable_id": 377,
|
||||
"noteable_type": "Issue"
|
||||
},
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
}
|
||||
]
|
||||
```
|
||||
Please refer to the [Events API documentation](events.md#list-a-projects-visible-events)
|
||||
|
||||
### Create project
|
||||
|
||||
|
|
141
doc/api/users.md
141
doc/api/users.md
|
@ -701,147 +701,8 @@ Will return `201 OK` on success, `404 User Not Found` is user cannot be found or
|
|||
|
||||
### Get user contribution events
|
||||
|
||||
Get the contribution events for the specified user, sorted from newest to oldest.
|
||||
Please refer to the [Events API documentation](events.md#get-user-contribution-events)
|
||||
|
||||
```
|
||||
GET /users/:id/events
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of the user |
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "closed",
|
||||
"target_id": 830,
|
||||
"target_type": "Issue",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": "Public project search field",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "opened",
|
||||
"target_id": null,
|
||||
"target_type": null,
|
||||
"author_id": 1,
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "john",
|
||||
"data": {
|
||||
"before": "50d4420237a9de7be1304607147aec22e4a14af7",
|
||||
"after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"ref": "refs/heads/master",
|
||||
"user_id": 1,
|
||||
"user_name": "Dmitriy Zaporozhets",
|
||||
"repository": {
|
||||
"name": "gitlabhq",
|
||||
"url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
|
||||
"description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
|
||||
"homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
|
||||
},
|
||||
"commits": [
|
||||
{
|
||||
"id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"message": "Add simple search to projects in public area",
|
||||
"timestamp": "2013-05-13T18:18:08+00:00",
|
||||
"url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"email": "dmitriy.zaporozhets@gmail.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"total_commits_count": 1
|
||||
},
|
||||
"target_title": null
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "closed",
|
||||
"target_id": 840,
|
||||
"target_type": "Issue",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": "Finish & merge Code search PR",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "commented on",
|
||||
"target_id": 1312,
|
||||
"target_type": "Note",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": null,
|
||||
"created_at": "2015-12-04T10:33:58.089Z",
|
||||
"note": {
|
||||
"id": 1312,
|
||||
"body": "What an awesome day!",
|
||||
"attachment": null,
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"created_at": "2015-12-04T10:33:56.698Z",
|
||||
"system": false,
|
||||
"noteable_id": 377,
|
||||
"noteable_type": "Issue"
|
||||
},
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Get all impersonation tokens of a user
|
||||
|
||||
|
|
|
@ -94,6 +94,7 @@ module API
|
|||
mount ::API::DeployKeys
|
||||
mount ::API::Deployments
|
||||
mount ::API::Environments
|
||||
mount ::API::Events
|
||||
mount ::API::Features
|
||||
mount ::API::Files
|
||||
mount ::API::Groups
|
||||
|
|
86
lib/api/events.rb
Normal file
86
lib/api/events.rb
Normal file
|
@ -0,0 +1,86 @@
|
|||
module API
|
||||
class Events < Grape::API
|
||||
include PaginationParams
|
||||
|
||||
helpers do
|
||||
params :event_filter_params do
|
||||
optional :action, type: String, values: Event.actions, desc: 'Event action to filter on'
|
||||
optional :target_type, type: String, values: Event.target_types, desc: 'Event target type to filter on'
|
||||
optional :before, type: Date, desc: 'Include only events created before this date'
|
||||
optional :after, type: Date, desc: 'Include only events created after this date'
|
||||
end
|
||||
|
||||
params :sort_params do
|
||||
optional :sort, type: String, values: %w[asc desc], default: 'desc',
|
||||
desc: 'Return events sorted in ascending and descending order'
|
||||
end
|
||||
|
||||
def present_events(events)
|
||||
events = events.reorder(created_at: params[:sort])
|
||||
|
||||
present paginate(events), with: Entities::Event
|
||||
end
|
||||
end
|
||||
|
||||
resource :events do
|
||||
desc "List currently authenticated user's events" do
|
||||
detail 'This feature was introduced in GitLab 9.3.'
|
||||
success Entities::Event
|
||||
end
|
||||
params do
|
||||
use :pagination
|
||||
use :event_filter_params
|
||||
use :sort_params
|
||||
end
|
||||
get do
|
||||
authenticate!
|
||||
|
||||
events = EventsFinder.new(params.merge(source: current_user, current_user: current_user)).execute.preload(:author, :target)
|
||||
|
||||
present_events(events)
|
||||
end
|
||||
end
|
||||
|
||||
params do
|
||||
requires :id, type: Integer, desc: 'The ID of the user'
|
||||
end
|
||||
resource :users do
|
||||
desc 'Get the contribution events of a specified user' do
|
||||
detail 'This feature was introduced in GitLab 8.13.'
|
||||
success Entities::Event
|
||||
end
|
||||
params do
|
||||
use :pagination
|
||||
use :event_filter_params
|
||||
use :sort_params
|
||||
end
|
||||
get ':id/events' do
|
||||
user = User.find_by(id: params[:id])
|
||||
not_found!('User') unless user
|
||||
|
||||
events = EventsFinder.new(params.merge(source: user, current_user: current_user)).execute.preload(:author, :target)
|
||||
|
||||
present_events(events)
|
||||
end
|
||||
end
|
||||
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID of a project'
|
||||
end
|
||||
resource :projects, requirements: { id: %r{[^/]+} } do
|
||||
desc "List a Project's visible events" do
|
||||
success Entities::Event
|
||||
end
|
||||
params do
|
||||
use :pagination
|
||||
use :event_filter_params
|
||||
use :sort_params
|
||||
end
|
||||
get ":id/events" do
|
||||
events = EventsFinder.new(params.merge(source: user_project, current_user: current_user)).execute.preload(:author, :target)
|
||||
|
||||
present_events(events)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -167,16 +167,6 @@ module API
|
|||
user_can_admin_project: can?(current_user, :admin_project, user_project), statistics: params[:statistics]
|
||||
end
|
||||
|
||||
desc 'Get events for a single project' do
|
||||
success Entities::Event
|
||||
end
|
||||
params do
|
||||
use :pagination
|
||||
end
|
||||
get ":id/events" do
|
||||
present paginate(user_project.events.recent), with: Entities::Event
|
||||
end
|
||||
|
||||
desc 'Fork new project for the current user or provided namespace.' do
|
||||
success Entities::Project
|
||||
end
|
||||
|
|
|
@ -328,27 +328,6 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
desc 'Get the contribution events of a specified user' do
|
||||
detail 'This feature was introduced in GitLab 8.13.'
|
||||
success Entities::Event
|
||||
end
|
||||
params do
|
||||
requires :id, type: Integer, desc: 'The ID of the user'
|
||||
use :pagination
|
||||
end
|
||||
get ':id/events' do
|
||||
user = User.find_by(id: params[:id])
|
||||
not_found!('User') unless user
|
||||
|
||||
events = user.events.
|
||||
merge(ProjectsFinder.new(current_user: current_user).execute).
|
||||
references(:project).
|
||||
with_associations.
|
||||
recent
|
||||
|
||||
present paginate(events), with: Entities::Event
|
||||
end
|
||||
|
||||
params do
|
||||
requires :user_id, type: Integer, desc: 'The ID of the user'
|
||||
end
|
||||
|
|
44
spec/finders/events_finder_spec.rb
Normal file
44
spec/finders/events_finder_spec.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe EventsFinder do
|
||||
let(:user) { create(:user) }
|
||||
let(:other_user) { create(:user) }
|
||||
let(:project1) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
|
||||
let(:project2) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
|
||||
let(:closed_issue) { create(:closed_issue, project: project1, author: user) }
|
||||
let(:opened_merge_request) { create(:merge_request, source_project: project2, author: user) }
|
||||
let!(:closed_issue_event) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) }
|
||||
let!(:opened_merge_request_event) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 1, 31)) }
|
||||
let(:closed_issue2) { create(:closed_issue, project: project1, author: user) }
|
||||
let(:opened_merge_request2) { create(:merge_request, source_project: project2, author: user) }
|
||||
let!(:closed_issue_event2) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 2, 2)) }
|
||||
let!(:opened_merge_request_event2) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 2, 2)) }
|
||||
|
||||
context 'when targeting a user' do
|
||||
it 'returns events between specified dates filtered on action and type' do
|
||||
events = described_class.new(source: user, current_user: user, action: 'created', target_type: 'merge_request', after: Date.new(2017, 1, 1), before: Date.new(2017, 2, 1)).execute
|
||||
|
||||
expect(events).to eq([opened_merge_request_event])
|
||||
end
|
||||
|
||||
it 'does not return events the current_user does not have access to' do
|
||||
events = described_class.new(source: user, current_user: other_user).execute
|
||||
|
||||
expect(events).not_to include(opened_merge_request_event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when targeting a project' do
|
||||
it 'returns project events between specified dates filtered on action and type' do
|
||||
events = described_class.new(source: project1, current_user: user, action: 'closed', target_type: 'issue', after: Date.new(2016, 12, 1), before: Date.new(2017, 1, 1)).execute
|
||||
|
||||
expect(events).to eq([closed_issue_event])
|
||||
end
|
||||
|
||||
it 'does not return events the current_user does not have access to' do
|
||||
events = described_class.new(source: project2, current_user: other_user).execute
|
||||
|
||||
expect(events).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
133
spec/requests/api/events_spec.rb
Normal file
133
spec/requests/api/events_spec.rb
Normal file
|
@ -0,0 +1,133 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe API::Events, api: true do
|
||||
include ApiHelpers
|
||||
let(:user) { create(:user) }
|
||||
let(:non_member) { create(:user) }
|
||||
let(:other_user) { create(:user, username: 'otheruser') }
|
||||
let(:private_project) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
|
||||
let(:closed_issue) { create(:closed_issue, project: private_project, author: user) }
|
||||
let!(:closed_issue_event) { create(:event, project: private_project, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) }
|
||||
|
||||
describe 'GET /events' do
|
||||
context 'when unauthenticated' do
|
||||
it 'returns authentication error' do
|
||||
get api('/events')
|
||||
|
||||
expect(response).to have_http_status(401)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated' do
|
||||
it 'returns users events' do
|
||||
get api('/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31', user)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.size).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /users/:id/events' do
|
||||
context "as a user that cannot see the event's project" do
|
||||
it 'returns no events' do
|
||||
get api("/users/#{user.id}/events", other_user)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "as a user that can see the event's project" do
|
||||
it 'returns the events' do
|
||||
get api("/users/#{user.id}/events", user)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.size).to eq(1)
|
||||
end
|
||||
|
||||
context 'when there are multiple events from different projects' do
|
||||
let(:second_note) { create(:note_on_issue, project: create(:empty_project)) }
|
||||
|
||||
before do
|
||||
second_note.project.add_user(user, :developer)
|
||||
|
||||
[second_note].each do |note|
|
||||
EventCreateService.new.leave_note(note, user)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns events in the correct order (from newest to oldest)' do
|
||||
get api("/users/#{user.id}/events", user)
|
||||
|
||||
comment_events = json_response.select { |e| e['action_name'] == 'commented on' }
|
||||
close_events = json_response.select { |e| e['action_name'] == 'closed' }
|
||||
|
||||
expect(comment_events[0]['target_id']).to eq(second_note.id)
|
||||
expect(close_events[0]['target_id']).to eq(closed_issue.id)
|
||||
end
|
||||
|
||||
it 'accepts filter parameters' do
|
||||
get api("/users/#{user.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", user)
|
||||
|
||||
expect(json_response.size).to eq(1)
|
||||
expect(json_response[0]['target_id']).to eq(closed_issue.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns a 404 error if not found' do
|
||||
get api('/users/42/events', user)
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
expect(json_response['message']).to eq('404 User Not Found')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/events' do
|
||||
context 'when unauthenticated ' do
|
||||
it 'returns 404 for private project' do
|
||||
get api("/projects/#{private_project.id}/events")
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
|
||||
it 'returns 200 status for a public project' do
|
||||
public_project = create(:empty_project, :public)
|
||||
|
||||
get api("/projects/#{public_project.id}/events")
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not permitted to read' do
|
||||
it 'returns 404' do
|
||||
get api("/projects/#{private_project.id}/events", non_member)
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated' do
|
||||
it 'returns project events' do
|
||||
get api("/projects/#{private_project.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", user)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.size).to eq(1)
|
||||
end
|
||||
|
||||
it 'returns 404 if project does not exist' do
|
||||
get api("/projects/1234/events", user)
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -762,64 +762,6 @@ describe API::Projects do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/events' do
|
||||
shared_examples_for 'project events response' do
|
||||
it 'returns the project events' do
|
||||
member = create(:user)
|
||||
create(:project_member, :developer, user: member, project: project)
|
||||
note = create(:note_on_issue, note: 'What an awesome day!', project: project)
|
||||
EventCreateService.new.leave_note(note, note.author)
|
||||
|
||||
get api("/projects/#{project.id}/events", current_user)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
|
||||
first_event = json_response.first
|
||||
expect(first_event['action_name']).to eq('commented on')
|
||||
expect(first_event['note']['body']).to eq('What an awesome day!')
|
||||
|
||||
last_event = json_response.last
|
||||
|
||||
expect(last_event['action_name']).to eq('joined')
|
||||
expect(last_event['project_id'].to_i).to eq(project.id)
|
||||
expect(last_event['author_username']).to eq(member.username)
|
||||
expect(last_event['author']['name']).to eq(member.name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it_behaves_like 'project events response' do
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
let(:current_user) { nil }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated' do
|
||||
context 'valid request' do
|
||||
it_behaves_like 'project events response' do
|
||||
let(:current_user) { user }
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns a 404 error if not found' do
|
||||
get api('/projects/42/events', user)
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
expect(json_response['message']).to eq('404 Project Not Found')
|
||||
end
|
||||
|
||||
it 'returns a 404 error if user is not a member' do
|
||||
other_user = create(:user)
|
||||
|
||||
get api("/projects/#{project.id}/events", other_user)
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/users' do
|
||||
shared_examples_for 'project users response' do
|
||||
it 'returns the project users' do
|
||||
|
|
|
@ -1130,83 +1130,6 @@ describe API::Users do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET /users/:id/events' do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:empty_project) }
|
||||
let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) }
|
||||
|
||||
before do
|
||||
project.add_user(user, :developer)
|
||||
EventCreateService.new.leave_note(note, user)
|
||||
end
|
||||
|
||||
context "as a user than cannot see the event's project" do
|
||||
it 'returns no events' do
|
||||
other_user = create(:user)
|
||||
|
||||
get api("/users/#{user.id}/events", other_user)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "as a user than can see the event's project" do
|
||||
context 'joined event' do
|
||||
it 'returns the "joined" event' do
|
||||
get api("/users/#{user.id}/events", user)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
|
||||
comment_event = json_response.find { |e| e['action_name'] == 'commented on' }
|
||||
|
||||
expect(comment_event['project_id'].to_i).to eq(project.id)
|
||||
expect(comment_event['author_username']).to eq(user.username)
|
||||
expect(comment_event['note']['id']).to eq(note.id)
|
||||
expect(comment_event['note']['body']).to eq('What an awesome day!')
|
||||
|
||||
joined_event = json_response.find { |e| e['action_name'] == 'joined' }
|
||||
|
||||
expect(joined_event['project_id'].to_i).to eq(project.id)
|
||||
expect(joined_event['author_username']).to eq(user.username)
|
||||
expect(joined_event['author']['name']).to eq(user.name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are multiple events from different projects' do
|
||||
let(:second_note) { create(:note_on_issue, project: create(:empty_project)) }
|
||||
let(:third_note) { create(:note_on_issue, project: project) }
|
||||
|
||||
before do
|
||||
second_note.project.add_user(user, :developer)
|
||||
|
||||
[second_note, third_note].each do |note|
|
||||
EventCreateService.new.leave_note(note, user)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns events in the correct order (from newest to oldest)' do
|
||||
get api("/users/#{user.id}/events", user)
|
||||
|
||||
comment_events = json_response.select { |e| e['action_name'] == 'commented on' }
|
||||
|
||||
expect(comment_events[0]['target_id']).to eq(third_note.id)
|
||||
expect(comment_events[1]['target_id']).to eq(second_note.id)
|
||||
expect(comment_events[2]['target_id']).to eq(note.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns a 404 error if not found' do
|
||||
get api('/users/42/events', user)
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
expect(json_response['message']).to eq('404 User Not Found')
|
||||
end
|
||||
end
|
||||
|
||||
context "user activities", :redis do
|
||||
let!(:old_active_user) { create(:user, last_activity_on: Time.utc(2000, 1, 1)) }
|
||||
let!(:newly_active_user) { create(:user, last_activity_on: 2.days.ago.midday) }
|
||||
|
|
Loading…
Reference in a new issue