You've already forked oncall
mirror of
https://github.com/linkedin/oncall.git
synced 2025-11-28 23:20:23 +02:00
Document everything
This commit is contained in:
committed by
Qingping Hou
parent
9777768235
commit
6558d1f388
@@ -137,7 +137,7 @@ CREATE TABLE IF NOT EXISTS `schedule` (
|
||||
ON UPDATE CASCADE);
|
||||
|
||||
-- -----------------------------------------------------
|
||||
-- Table `schedule_events`
|
||||
-- Table `schedule_event`
|
||||
-- -----------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `schedule_event` (
|
||||
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
@@ -67,7 +67,7 @@ def on_get(req, resp):
|
||||
"""
|
||||
http:get:: /api/v0/events
|
||||
|
||||
Search for events.
|
||||
Search for events. Allows filtering based on a number of parameters, detailed below.
|
||||
|
||||
**Example request**:
|
||||
|
||||
@@ -80,33 +80,33 @@ def on_get(req, resp):
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"start": 1488441600,
|
||||
"end": 1489132800,
|
||||
"team": "foo-sre",
|
||||
"link_id": null,
|
||||
"schedule_id": null,
|
||||
"role": "primary",
|
||||
"user": "foo",
|
||||
"full_name": "Foo Icecream",
|
||||
"id": 187795
|
||||
},
|
||||
{
|
||||
"start": 1488441600,
|
||||
"end": 1489132800,
|
||||
"team": "foo-sre",
|
||||
"link_id": "8a8ae77b8c52448db60c8a701e7bffc2",
|
||||
"schedule_id": 123,
|
||||
"role": "primary",
|
||||
"user": "bar",
|
||||
"full_name": "Bar Apple",
|
||||
"id": 187795
|
||||
}
|
||||
]
|
||||
[
|
||||
{
|
||||
"start": 1488441600,
|
||||
"end": 1489132800,
|
||||
"team": "foo-sre",
|
||||
"link_id": null,
|
||||
"schedule_id": null,
|
||||
"role": "primary",
|
||||
"user": "foo",
|
||||
"full_name": "Foo Icecream",
|
||||
"id": 187795
|
||||
},
|
||||
{
|
||||
"start": 1488441600,
|
||||
"end": 1489132800,
|
||||
"team": "foo-sre",
|
||||
"link_id": "8a8ae77b8c52448db60c8a701e7bffc2",
|
||||
"schedule_id": 123,
|
||||
"role": "primary",
|
||||
"user": "bar",
|
||||
"full_name": "Bar Apple",
|
||||
"id": 187795
|
||||
}
|
||||
]
|
||||
|
||||
:query team: team name
|
||||
:query user: user name
|
||||
@@ -120,6 +120,21 @@ def on_get(req, resp):
|
||||
:query end__ge: end time (unix timestamp) greater than or equal
|
||||
:query end__lt: end time (unix timestamp) less than
|
||||
:query end__le: end time (unix timestamp) less than or equal
|
||||
:query role: role name
|
||||
:query role__eq: role name
|
||||
:query role__contains: role name contains param
|
||||
:query role__startswith: role name starts with param
|
||||
:query role__endswith: role name ends with param
|
||||
:query team: team name
|
||||
:query team__eq: team name
|
||||
:query team__contains: team name contains param
|
||||
:query team__startswith: team name starts with param
|
||||
:query team__endswith: team name ends with param
|
||||
:query user: user name
|
||||
:query user__eq: user name
|
||||
:query user__contains: user name contains param
|
||||
:query user__startswith: user name starts with param
|
||||
:query user__endswith: user name ends with param
|
||||
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: bad request
|
||||
@@ -157,13 +172,22 @@ def on_get(req, resp):
|
||||
@login_required
|
||||
def on_post(req, resp):
|
||||
"""
|
||||
Endpoint for creating event. Responds with event id for created event
|
||||
Endpoint for creating event. Responds with event id for created event. Events must
|
||||
specify the following parameters:
|
||||
|
||||
- start: Unix timestamp for the event start time (seconds)
|
||||
- end: Unix timestamp for the event end time (seconds)
|
||||
- user: Username for the event's user
|
||||
- team: Name for the event's team
|
||||
- role: Name for the event's role
|
||||
|
||||
All of these parameters are required.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /v0/events HTTP/1.1
|
||||
POST api/v0/events HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ from pytz import timezone, utc
|
||||
def on_post(req, resp, schedule_id):
|
||||
"""
|
||||
Run the scheduler on demand from a given point in time. Deletes existing schedule events if applicable.
|
||||
Given the `start` param, this will find the first schedule start time after `start`, then populate out
|
||||
Given the ``start`` param, this will find the first schedule start time after ``start``, then populate out
|
||||
to the schedule's auto_populate_threshold. It will also clear the calendar of any events associated
|
||||
with the chosen schedule from the start of the first event it created onward. For example, if `start`
|
||||
is Monday, May 1 and the chosen schedule starts on Wednesday, this will create events starting from
|
||||
|
||||
@@ -14,7 +14,25 @@ from ...constants import ROSTER_USER_ADDED
|
||||
|
||||
def on_get(req, resp, team, roster):
|
||||
"""
|
||||
Get all users for a team roster
|
||||
http:get:: /api/v0/teams/team-foo/rosters/roster-foo/users
|
||||
|
||||
Get all users for a team's roster
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/teams/team-foo/rosters/roster-foo/users HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
["jdoe", "asmith"]
|
||||
"""
|
||||
team, roster = unquote(team), unquote(roster)
|
||||
connection = db.connect()
|
||||
@@ -36,7 +54,47 @@ def on_get(req, resp, team, roster):
|
||||
@login_required
|
||||
def on_post(req, resp, team, roster):
|
||||
"""
|
||||
Add user to a roster for a team
|
||||
Add user to a roster for a team. On successful creation, returns that user's information.
|
||||
This includes id, contacts, etc, similar to the /api/v0/users GET endpoint.
|
||||
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /v0/teams/team-foo/rosters/roster-foo/users HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "jdoe"
|
||||
}
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"active": 1,
|
||||
"contacts": {
|
||||
"email": "jdoe@example.com",
|
||||
"im": "jdoe",
|
||||
"sms": "+1 111-111-1111",
|
||||
"call": "+1 111-111-1111"
|
||||
},
|
||||
"full_name": "John Doe",
|
||||
"id": 1,
|
||||
"name": "jdoe",
|
||||
"photo_url": "example.image.com",
|
||||
"time_zone": "US/Pacific"
|
||||
}
|
||||
|
||||
:statuscode 201: Roster user added
|
||||
:statuscode 400: Missing "name" parameter
|
||||
:statuscode 422: Invalid team/user or user is already in roster.
|
||||
|
||||
"""
|
||||
team, roster = unquote(team), unquote(roster)
|
||||
data = load_json_body(req)
|
||||
|
||||
@@ -60,7 +60,63 @@ def get_roster_by_team_id(cursor, team_id, params=None):
|
||||
|
||||
def on_get(req, resp, team):
|
||||
"""
|
||||
Get roster info(including schedules) for a team
|
||||
Get roster info for a team. Returns a JSON object with roster names
|
||||
as keys, and info as values. This info includes the roster id, any
|
||||
schedules associated with the rosters, and roster users (along
|
||||
with their status as in/out of rotation).
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/teams/team-foo/rosters HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"roster-foo": {
|
||||
"id": 2923,
|
||||
"schedules": [
|
||||
{
|
||||
"advanced_mode": 0,
|
||||
"auto_populate_threshold": 30,
|
||||
"events": [
|
||||
{
|
||||
"duration": 604800,
|
||||
"start": 266400
|
||||
}
|
||||
],
|
||||
"id": 1788,
|
||||
"role": "primary",
|
||||
"role_id": 1,
|
||||
"roster": "roster-foo",
|
||||
"roster_id": 2923,
|
||||
"team": "team-foo",
|
||||
"team_id": 2122,
|
||||
"timezone": "US/Pacific"
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"in_rotation": true,
|
||||
"name": "jdoe"
|
||||
},
|
||||
{
|
||||
"in_rotation": true,
|
||||
"name": "asmith"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
:statuscode 422: Invalid team
|
||||
|
||||
"""
|
||||
team = unquote(team)
|
||||
connection = db.connect()
|
||||
@@ -84,6 +140,28 @@ def on_get(req, resp, team):
|
||||
def on_post(req, resp, team):
|
||||
"""
|
||||
Create a roster for a team
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /v0/teams/team-foo/rosters HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "roster-foo",
|
||||
}
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
:statuscode 201: Succesful roster creation
|
||||
:statuscode 422: Invalid character in roster name/Duplicate roster name
|
||||
"""
|
||||
team = unquote(team)
|
||||
data = load_json_body(req)
|
||||
|
||||
@@ -37,13 +37,77 @@ def verify_auth(req, schedule_id, connection, cursor):
|
||||
|
||||
|
||||
def on_get(req, resp, schedule_id):
|
||||
"""
|
||||
Get schedule information. Detailed information on schedule parameters is provided in the
|
||||
POST method for /api/v0/team/{team_name}/rosters/{roster_name}/schedules.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/schedules/1234 HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"advanced_mode": 1,
|
||||
"auto_populate_threshold": 30,
|
||||
"events": [
|
||||
{
|
||||
"duration": 259200,
|
||||
"start": 0
|
||||
}
|
||||
],
|
||||
"id": 1234,
|
||||
"role": "primary",
|
||||
"role_id": 1,
|
||||
"roster": "roster-foo",
|
||||
"roster_id": 2922,
|
||||
"team": "asdf",
|
||||
"team_id": 2121,
|
||||
"timezone": "US/Pacific"
|
||||
}
|
||||
"""
|
||||
|
||||
resp.body = json_dumps(get_schedules({'schedule_id': schedule_id}, fields=req.get_param_as_list('fields'))[0])
|
||||
|
||||
|
||||
@login_required
|
||||
def on_put(req, resp, schedule_id):
|
||||
"""
|
||||
Update a schedule
|
||||
Update a schedule. Allows editing of role, team, roster, auto_populate_threshold,
|
||||
events, and advanced_mode. Only allowed for team admins. Note that simple mode
|
||||
schedules must conform to simple schedule restrictions (described in documentation
|
||||
for the /api/v0/team/{team_name}/rosters/{roster_name}/schedules GET endpoint).
|
||||
This is checked on both "events" and "advanced_mode" edits.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /api/v0/schedules/1234 HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"role": "primary",
|
||||
"team": "team-bar",
|
||||
"roster": "roster-bar",
|
||||
"auto_populate_threshold": 28,
|
||||
"events":
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"duration": 100
|
||||
}
|
||||
]
|
||||
"advanced_mode": 1
|
||||
}
|
||||
"""
|
||||
data = load_json_body(req)
|
||||
|
||||
@@ -90,7 +154,16 @@ def on_put(req, resp, schedule_id):
|
||||
@login_required
|
||||
def on_delete(req, resp, schedule_id):
|
||||
"""
|
||||
Delete a schedule
|
||||
Delete a schedule by id. Only allowed for team admins.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v0/schedules/1234 HTTP/1.1
|
||||
|
||||
:statuscode 200: Successful delete
|
||||
:statuscode 404: Schedule not found
|
||||
"""
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor()
|
||||
|
||||
@@ -71,7 +71,16 @@ def validate_simple_schedule(events):
|
||||
|
||||
def get_schedules(filter_params, dbinfo=None, fields=None):
|
||||
"""
|
||||
Get schedule data for a request
|
||||
Helper function to get schedule data for a request.
|
||||
|
||||
:param filter_params: dict mapping constraint keys with values. Valid constraints are
|
||||
defined in the global ``constraints`` dict.
|
||||
:param dbinfo: optional. If provided, defines (connection, cursor) to use in DB queries.
|
||||
Otherwise, this creates its own connection/cursor.
|
||||
:param fields: optional. If provided, defines which schedule fields to return. Valid
|
||||
fields are defined in the global ``columns`` dict. Defaults to all fields. Invalid
|
||||
fields raise a 400 Bad Request.
|
||||
:return:
|
||||
"""
|
||||
events = False
|
||||
from_clause = ['`schedule`']
|
||||
@@ -133,9 +142,13 @@ def get_schedules(filter_params, dbinfo=None, fields=None):
|
||||
|
||||
|
||||
def insert_schedule_events(schedule_id, events, cursor):
|
||||
"""
|
||||
Helper to insert schedule events for a schedule
|
||||
"""
|
||||
insert_events = '''INSERT INTO `schedule_event` (`schedule_id`, `start`, `duration`)
|
||||
VALUES (%(schedule)s, %(start)s, %(duration)s)'''
|
||||
# Merge consecutive events for db storage
|
||||
# Merge consecutive events for db storage. This creates an equivalent, simpler
|
||||
# form of the schedule for the scheduler.
|
||||
raw_events = sorted(events, key=lambda e: e['start'])
|
||||
new_events = []
|
||||
for e in raw_events:
|
||||
@@ -149,6 +162,71 @@ def insert_schedule_events(schedule_id, events, cursor):
|
||||
|
||||
|
||||
def on_get(req, resp, team, roster):
|
||||
"""
|
||||
Get schedules for a given roster. Information on schedule attributes is detailed
|
||||
in the schedules POST endpoint documentation. Schedules can be filtered with
|
||||
the following parameters passed in the query string:
|
||||
|
||||
:query id: id of the schedule
|
||||
:query id__eq: id of the schedule
|
||||
:query id__gt: id greater than
|
||||
:query id__ge: id greater than or equal
|
||||
:query id__lt: id less than
|
||||
:query id__le: id less than or equal
|
||||
:query name: schedule name
|
||||
:query name__eq: schedule name
|
||||
:query name__contains: schedule name contains param
|
||||
:query name__startswith: schedule name starts with param
|
||||
:query name__endswith: schedule name ends with param
|
||||
:query role: schedule role name
|
||||
:query role__eq: schedule role name
|
||||
:query role__contains: schedule role name contains param
|
||||
:query role__startswith: schedule role name starts with param
|
||||
:query role__endswith: schedule role name ends with param
|
||||
:query team: schedule team name
|
||||
:query team__eq: schedule team name
|
||||
:query team__contains: schedule team name contains param
|
||||
:query team__startswith: schedule team name starts with param
|
||||
:query team__endswith: schedule team name ends with param
|
||||
:query team_id: id of the schedule's team
|
||||
:query roster_id: id of the schedule's roster
|
||||
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/teams/team-foo/rosters/roster-foo/schedules HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"advanced_mode": 1,
|
||||
"auto_populate_threshold": 30,
|
||||
"events": [
|
||||
{
|
||||
"duration": 259200,
|
||||
"start": 0
|
||||
}
|
||||
],
|
||||
"id": 2065,
|
||||
"role": "primary",
|
||||
"role_id": 1,
|
||||
"roster": "roster-foo",
|
||||
"roster_id": 2922,
|
||||
"team": "team-foo",
|
||||
"team_id": 2121,
|
||||
"timezone": "US/Pacific"
|
||||
}
|
||||
]
|
||||
"""
|
||||
team = unquote(team)
|
||||
roster = unquote(roster)
|
||||
fields = req.get_param_as_list('fields')
|
||||
@@ -169,8 +247,38 @@ required_params = frozenset(['events', 'role', 'advanced_mode'])
|
||||
@login_required
|
||||
def on_post(req, resp, team, roster):
|
||||
'''
|
||||
Schedule create endpoint. Schedules are templates for the auto-scheduler to follow that define
|
||||
how it should populate a certain period of time. This template is followed repeatedly to
|
||||
populate events on a team's calendar. Schedules are associated with a roster, which defines
|
||||
the pool of users that the scheduler selects from. Similarly, the schedule's role indicates
|
||||
the role that the populated events shoud have. The ``auto_populate_threshold`` parameter
|
||||
defines how far into the future the scheduler populates.
|
||||
|
||||
Finally, each schedule has a list of events, each defining ``start`` and ``duration``. ``start``
|
||||
represents an offset from Sunday at 00:00 in the team's scheduling timezone, in seconds. For
|
||||
example, denote DAY and HOUR as the number of seconds in a day/hour, respectively. An
|
||||
event with ``start`` of (DAY + 9 * HOUR) starts on Monday, at 9:00 am. Duration is also given
|
||||
in seconds.
|
||||
|
||||
The scheduler will start at Sunday 00:00 in the team's scheduling timezone, choose a user,
|
||||
and populate events on the calendar according to the offsets defined in the events list.
|
||||
It then repeats this process, moving to the next Sunday 00:00 after the events it has
|
||||
created.
|
||||
|
||||
``advanced_mode`` acts as a hint to the frontend on how the schedule should be displayed,
|
||||
defining whether the advanced mode toggle on the schedule edit action should be set on or off.
|
||||
Because of how the frontend displays simple schedules, a schedule can only have advanced_mode = 0
|
||||
if its events have one of 4 formats:
|
||||
|
||||
1. One event that is one week long
|
||||
2. One event that is two weeks long
|
||||
3. Seven events that are 12 hours long
|
||||
4. Fourteen events that are 12 hours long
|
||||
|
||||
See below for sample JSON requests.
|
||||
|
||||
Assume these schedules' team defines US/Pacific as its scheduling timezone.
|
||||
|
||||
Weekly 7*24 shift that starts at Monday 6PM PST:
|
||||
|
||||
.. code-block:: javascript
|
||||
@@ -199,6 +307,40 @@ def on_post(req, resp, team, roster):
|
||||
],
|
||||
'advanced_mode': 1
|
||||
}
|
||||
|
||||
**Example Request**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /v0/teams/team-foo/rosters/roster-foo/schedules HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"advanced_mode": 0,
|
||||
"auto_populate_threshold": "21",
|
||||
"events": [
|
||||
{
|
||||
"duration": 604800,
|
||||
"start": 129600
|
||||
}
|
||||
],
|
||||
"role": "primary",
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 2221
|
||||
}
|
||||
|
||||
:statuscode 201: Successful schedule create. Response contains created schedule's id.
|
||||
:statuscode 400: Missing required parameters
|
||||
:statuscode 422: Invalid roster specified
|
||||
'''
|
||||
data = load_json_body(req)
|
||||
data['team'] = unquote(team)
|
||||
|
||||
@@ -6,6 +6,70 @@ from ujson import dumps
|
||||
|
||||
|
||||
def on_get(req, resp):
|
||||
'''
|
||||
Endpoint for searching for teams, services, users, and team users by keyword. Used for
|
||||
typeaheads in the frontend. Team/service search is done using substring matching, while
|
||||
user and team_user search is done with prefix matching. If no fields are provided, the
|
||||
endpoint defaults to ['teams', 'services']. A keyword parameter must be passed in the
|
||||
query string.
|
||||
|
||||
**Example request**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/search?keyword=key HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"service-key": "team-foo"
|
||||
},
|
||||
{
|
||||
"service-key2": "team-bar"
|
||||
}
|
||||
]
|
||||
"teams": [
|
||||
"team-key",
|
||||
"team-key2"
|
||||
]
|
||||
}
|
||||
|
||||
An example for user search:
|
||||
**Example request**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/search?keyword=key&fields=users HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"users":
|
||||
[
|
||||
{
|
||||
"full_name": "John Doe",
|
||||
"name": "jdoe"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
'''
|
||||
keyword = req.get_param('keyword', required=True)
|
||||
fields = req.get_param_as_list('fields')
|
||||
if not fields:
|
||||
|
||||
@@ -12,6 +12,27 @@ from ...auth import debug_only
|
||||
def on_get(req, resp, service):
|
||||
"""
|
||||
Get service id by name
|
||||
|
||||
**Example request**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/services/service-foo HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1234,
|
||||
"name": "service-foo"
|
||||
}
|
||||
|
||||
"""
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor(db.DictCursor)
|
||||
@@ -28,7 +49,7 @@ def on_get(req, resp, service):
|
||||
@debug_only
|
||||
def on_put(req, resp, service):
|
||||
"""
|
||||
Change name for a service
|
||||
Change name for a service. Currently unused/debug only.
|
||||
"""
|
||||
data = load_json_body(req)
|
||||
connection = db.connect()
|
||||
@@ -43,7 +64,7 @@ def on_put(req, resp, service):
|
||||
@debug_only
|
||||
def on_delete(req, resp, service):
|
||||
"""
|
||||
Delete a service
|
||||
Delete a service. Currently unused/debug only.
|
||||
"""
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor()
|
||||
|
||||
@@ -6,6 +6,40 @@ from ... import db
|
||||
|
||||
|
||||
def on_get(req, resp, service, role):
|
||||
'''
|
||||
Get the current user on-call for a given service/role. Returns event start/end, contact info,
|
||||
and user name.
|
||||
|
||||
**Example request**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/services/service-foo/oncall/primary HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"contacts": {
|
||||
"call": "+1 111-111-1111",
|
||||
"email": "jdoe@example.com",
|
||||
"im": "jdoe",
|
||||
"sms": "+1 111-111-1111"
|
||||
},
|
||||
"end": 1495695600,
|
||||
"start": 1495263600,
|
||||
"user": "John Doe"
|
||||
}
|
||||
]
|
||||
|
||||
'''
|
||||
get_oncall_query = '''SELECT `user`.`full_name` AS `user`, `event`.`start`, `event`.`end`,
|
||||
`contact_mode`.`name` AS `mode`, `user_contact`.`destination`
|
||||
FROM `service` JOIN `team_service` ON `service`.`id` = `team_service`.`service_id`
|
||||
|
||||
@@ -8,6 +8,25 @@ from ... import db
|
||||
def on_get(req, resp, service):
|
||||
"""
|
||||
Get list of team mapped to a service
|
||||
|
||||
**Example request**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/services/service-foo/teams HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
"team-foo"
|
||||
]
|
||||
"""
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor()
|
||||
|
||||
@@ -26,7 +26,38 @@ constraints = {
|
||||
|
||||
def on_get(req, resp):
|
||||
"""
|
||||
Search for services
|
||||
Find services, filtered by params
|
||||
|
||||
:query id: id of the service
|
||||
:query id__eq: id of the service
|
||||
:query id__gt: id greater than
|
||||
:query id__ge: id greater than or equal
|
||||
:query id__lt: id less than
|
||||
:query id__le: id less than or equal
|
||||
:query name: service name
|
||||
:query name__eq: service name
|
||||
:query name__contains: service name contains param
|
||||
:query name__startswith: service name starts with param
|
||||
:query name__endswith: service name ends with param
|
||||
|
||||
**Example request**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/services?name__startswith=service HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
"service-foo"
|
||||
]
|
||||
"""
|
||||
query = 'SELECT `name` FROM `service`'
|
||||
|
||||
|
||||
@@ -54,6 +54,91 @@ populate_map = {
|
||||
|
||||
|
||||
def on_get(req, resp, team):
|
||||
'''
|
||||
Get team info by name. By default, only finds active teams. Allows selection of
|
||||
fields, including: users, admins, services, and rosters. If no ``fields`` is
|
||||
specified in the query string, it defaults to all fields.
|
||||
|
||||
**Example request**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/teams/team-foo HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"admins": [
|
||||
{
|
||||
"name": "jdoe"
|
||||
}
|
||||
],
|
||||
"email": "foo@example.com",
|
||||
"id": 5501,
|
||||
"iris_plan": null,
|
||||
"name": "team-foo",
|
||||
"rosters": {
|
||||
"roster-foo": {
|
||||
"id": 4186,
|
||||
"schedules": [
|
||||
{
|
||||
"advanced_mode": 0,
|
||||
"auto_populate_threshold": 21,
|
||||
"events": [
|
||||
{
|
||||
"duration": 604800,
|
||||
"start": 7200
|
||||
}
|
||||
],
|
||||
"id": 2222,
|
||||
"role": "primary",
|
||||
"role_id": 1,
|
||||
"roster": "roster-foo",
|
||||
"roster_id": 4186,
|
||||
"team": "team-foo",
|
||||
"team_id": 5501,
|
||||
"timezone": "US/Pacific"
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"in_rotation": true,
|
||||
"name": "jdoe"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"scheduling_timezone": "US/Pacific",
|
||||
"services": [
|
||||
"service-foo"
|
||||
],
|
||||
"slack_channel": "#foo",
|
||||
"users": {
|
||||
"jdoe": {
|
||||
"active": 1,
|
||||
"contacts": {
|
||||
"call": "+1 111-111-1111",
|
||||
"email": "jdoe@example.com",
|
||||
"im": "jdoe",
|
||||
"sms": "+1 111-111-1111"
|
||||
},
|
||||
"full_name": "John Doe",
|
||||
"id": 1234,
|
||||
"name": "jdoe",
|
||||
"photo_url": "image.example.com",
|
||||
"time_zone": "US/Pacific"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
'''
|
||||
team = unquote(team)
|
||||
fields = req.get_param_as_list('fields')
|
||||
active = req.get_param('active', default=True)
|
||||
@@ -83,6 +168,27 @@ def on_get(req, resp, team):
|
||||
|
||||
@login_required
|
||||
def on_put(req, resp, team):
|
||||
'''
|
||||
Edit a team's information. Allows edit of: name, slack_channel, email, scheduling_timezone, iris_plan.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /api/v0/teams/team-foo HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "team-bar",
|
||||
"slack_channel": "roster-bar",
|
||||
"email": 28,
|
||||
"scheduling_timezone": "US/Central"
|
||||
}
|
||||
|
||||
:statuscode 200: Successful edit
|
||||
:statuscode 400: Invalid team name/iris escalation plan
|
||||
:statuscode 422: Duplicate team name
|
||||
'''
|
||||
team = unquote(team)
|
||||
check_team_auth(team, req)
|
||||
data = load_json_body(req)
|
||||
@@ -122,6 +228,20 @@ def on_put(req, resp, team):
|
||||
|
||||
@login_required
|
||||
def on_delete(req, resp, team):
|
||||
'''
|
||||
Soft delete for teams. Does not remove data from the database, but sets the team's active
|
||||
param to false. Note that this means deleted teams' names remain in the namespace, so new
|
||||
teams cannot be created with the same name a sa deleted team.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v0/teams/team-foo HTTP/1.1
|
||||
|
||||
:statuscode 200: Successful delete
|
||||
:statuscode 404: Team not found
|
||||
'''
|
||||
team = unquote(team)
|
||||
check_team_auth(team, req)
|
||||
connection = db.connect()
|
||||
|
||||
@@ -12,7 +12,16 @@ from ...constants import ADMIN_DELETED
|
||||
@login_required
|
||||
def on_delete(req, resp, team, user):
|
||||
"""
|
||||
Delete team admin user
|
||||
Delete team admin user. Removes admin from the team if he/she is not a member of any roster.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v0/teams/team-foo/admins/jdoe HTTP/1.1
|
||||
|
||||
:statuscode 200: Successful delete
|
||||
:statuscode 404: Team admin not found
|
||||
"""
|
||||
check_team_auth(team, req)
|
||||
connection = db.connect()
|
||||
|
||||
@@ -13,7 +13,27 @@ from ...constants import ADMIN_CREATED
|
||||
|
||||
def on_get(req, resp, team):
|
||||
"""
|
||||
Get list of admin users for a team
|
||||
Get list of admin usernames for a team
|
||||
|
||||
**Example request**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/teams/team-foo/admins HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
"jdoe",
|
||||
"asmith"
|
||||
]
|
||||
"""
|
||||
team = unquote(team)
|
||||
connection = db.connect()
|
||||
@@ -32,7 +52,43 @@ def on_get(req, resp, team):
|
||||
@login_required
|
||||
def on_post(req, resp, team):
|
||||
"""
|
||||
Add user as team admin
|
||||
Add user as a team admin. Responds with that user's info (similar to user GET).
|
||||
Subscribes this user to default notifications for the team, and adds the user
|
||||
to the team (if needed).
|
||||
|
||||
**Example request**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v0/teams/team-foo/admins HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"active": 1,
|
||||
"contacts": {
|
||||
"call": "+1 111-111-1111",
|
||||
"email": "jdoe@example.com",
|
||||
"im": "jdoe",
|
||||
"sms": "+1 111-111-1111"
|
||||
},
|
||||
"full_name": "John Doe",
|
||||
"id": 9535,
|
||||
"name": "jdoe",
|
||||
"photo_url": "image.example.com",
|
||||
"time_zone": "US/Pacific"
|
||||
}
|
||||
|
||||
:statuscode 201: Successful admin added
|
||||
:statuscode 400: Missing name attribute in request
|
||||
:statuscode 422: Invalid team/user, or user is already a team admin
|
||||
"""
|
||||
team = unquote(team)
|
||||
check_team_auth(team, req)
|
||||
@@ -46,7 +102,7 @@ def on_post(req, resp, team):
|
||||
cursor = connection.cursor()
|
||||
|
||||
cursor.execute('''(SELECT `id` FROM `team` WHERE `name`=%s)
|
||||
UNION
|
||||
UNION ALL
|
||||
(SELECT `id` FROM `user` WHERE `name`=%s)''', (team, user_name))
|
||||
results = [r[0] for r in cursor]
|
||||
if len(results) < 2:
|
||||
|
||||
@@ -9,7 +9,23 @@ from requests import ConnectionError
|
||||
def on_post(req, resp, team):
|
||||
'''
|
||||
Escalate to a team using the team's configured Iris plan. Configured in the
|
||||
'iris_plan_integration' section of the configuration file.
|
||||
'iris_plan_integration' section of the configuration file. If iris plan integration
|
||||
is not activated, this endpoint will be disabled.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /v0/events HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"description": "Something bad happened!",
|
||||
}
|
||||
|
||||
:statuscode 200: Incident created
|
||||
:statuscode 400: Escalation failed, missing description/No escalation plan specified
|
||||
for team/Iris client error.
|
||||
'''
|
||||
data = load_json_body(req)
|
||||
|
||||
|
||||
@@ -12,7 +12,16 @@ from ... import db
|
||||
@login_required
|
||||
def on_delete(req, resp, team, service):
|
||||
"""
|
||||
Delete service team mapping
|
||||
Delete service team mapping. Only allowed for team admins.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v0/teams/team-foo/services/service-foo HTTP/1.1
|
||||
|
||||
:statuscode 200: Successful delete
|
||||
:statuscode 404: Team-service mapping not found
|
||||
"""
|
||||
team = unquote(team)
|
||||
check_team_auth(team, req)
|
||||
|
||||
@@ -13,6 +13,25 @@ from ... import db
|
||||
def on_get(req, resp, team):
|
||||
"""
|
||||
Get list of services mapped to a team
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/teams/team-foo/services HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
"service-foo",
|
||||
"service-bar"
|
||||
]
|
||||
"""
|
||||
team = unquote(team)
|
||||
connection = db.connect()
|
||||
@@ -31,7 +50,31 @@ def on_get(req, resp, team):
|
||||
@login_required
|
||||
def on_post(req, resp, team):
|
||||
"""
|
||||
Create team to service mapping
|
||||
Create team to service mapping. Takes an object defining "name", then maps
|
||||
that service to the team specified in the URL. Note that this endpoint does
|
||||
not create a service; it expects this service to already exist.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST api/v0/teams/team-foo/services HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "service-foo",
|
||||
}
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json
|
||||
|
||||
:statuscode 201: Successful create
|
||||
:statuscode 422: Mapping creation failed; Possible errors: Invalid service/team name,
|
||||
service already mapped to the team, service mapped to another team
|
||||
"""
|
||||
team = unquote(team)
|
||||
check_team_auth(team, req)
|
||||
@@ -50,7 +93,7 @@ def on_post(req, resp, team):
|
||||
if claimed_team:
|
||||
raise HTTPError('422 Unprocessable Entity',
|
||||
'IntegrityError',
|
||||
'service "%s" alread claimed by team "%s"' % (service, claimed_team[0]))
|
||||
'service "%s" already claimed by team "%s"' % (service, claimed_team[0]))
|
||||
|
||||
cursor.execute('''INSERT INTO `team_service` (`team_id`, `service_id`)
|
||||
VALUES (
|
||||
|
||||
@@ -8,6 +8,105 @@ from falcon import HTTPNotFound
|
||||
|
||||
|
||||
def on_get(req, resp, team):
|
||||
'''
|
||||
Endpoint to get a summary of the team's oncall information. Returns an object
|
||||
containing the fields ``current`` and ``next``, which then contain information
|
||||
on the current and next on-call shifts for this team. ``current`` and ``next``
|
||||
are objects keyed by role (if an event of that role exists), with values of
|
||||
lists of event/user information. This list will have multiple elements if
|
||||
multiple events with the same role are currently occurring, or if multiple
|
||||
events with the same role are starting next in the future at the same time.
|
||||
|
||||
If no event with a given role exists, that role is excluded from the ``current``
|
||||
or ``next`` object. If no events exist, the ``current`` and ``next`` objects
|
||||
will be empty objects.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET api/v0/teams/team-foo/summary HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"current": {
|
||||
"manager": [
|
||||
{
|
||||
"end": 1495760400,
|
||||
"full_name": "John Doe",
|
||||
"photo_url": "example.image.com",
|
||||
"role": "manager",
|
||||
"start": 1495436400,
|
||||
"user_contacts": {
|
||||
"call": "+1 111-111-1111",
|
||||
"email": "jdoe@example.com",
|
||||
"im": "jdoe",
|
||||
"sms": "+1 111-111-1111"
|
||||
},
|
||||
"user_id": 1234
|
||||
}
|
||||
],
|
||||
"primary": [
|
||||
{
|
||||
"end": 1495760400,
|
||||
"full_name": "Adam Smith",
|
||||
"photo_url": "example.image.com",
|
||||
"role": "primary",
|
||||
"start": 1495350000,
|
||||
"user_contacts": {
|
||||
"call": "+1 222-222-2222",
|
||||
"email": "asmith@example.com",
|
||||
"im": "asmith",
|
||||
"sms": "+1 222-222-2222"
|
||||
},
|
||||
"user_id": 1235
|
||||
}
|
||||
]
|
||||
},
|
||||
"next": {
|
||||
"manager": [
|
||||
{
|
||||
"end": 1496127600,
|
||||
"full_name": "John Doe",
|
||||
"photo_url": "example.image.com",
|
||||
"role": "manager",
|
||||
"start": 1495436400,
|
||||
"user_contacts": {
|
||||
"call": "+1 111-111-1111",
|
||||
"email": "jdoe@example.com",
|
||||
"im": "jdoe",
|
||||
"sms": "+1 111-111-1111"
|
||||
},
|
||||
"user_id": 1234
|
||||
}
|
||||
],
|
||||
"primary": [
|
||||
{
|
||||
"end": 1495760400,
|
||||
"full_name": "Adam Smith",
|
||||
"photo_url": "example.image.com",
|
||||
"role": "primary",
|
||||
"start": 1495350000,
|
||||
"user_contacts": {
|
||||
"call": "+1 222-222-2222",
|
||||
"email": "asmith@example.com",
|
||||
"im": "asmith",
|
||||
"sms": "+1 222-222-2222"
|
||||
},
|
||||
"user_id": 1235
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
'''
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor(db.DictCursor)
|
||||
|
||||
|
||||
@@ -13,6 +13,15 @@ from ... import db
|
||||
def on_delete(req, resp, team, user):
|
||||
"""
|
||||
Delete user from a team
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v0/teams/team-foo/users/jdoe HTTP/1.1
|
||||
|
||||
:statuscode 200: Successful delete
|
||||
:statuscode 404: User not found in team
|
||||
"""
|
||||
team = unquote(team)
|
||||
check_team_auth(team, req)
|
||||
|
||||
@@ -13,7 +13,28 @@ constraints = {'active': '`team`.`active` = %s'}
|
||||
|
||||
def on_get(req, resp, team):
|
||||
"""
|
||||
Get list of users for a team
|
||||
Get list of usernames for all team members. A user is a member of a team when
|
||||
he/she is a team admin or a member of one of the team's rosters. Accepts an
|
||||
``active`` parameter in the query string that filters inactive (deleted) teams.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/teams/team-foo/users HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
"jdoe",
|
||||
"asmith"
|
||||
]
|
||||
"""
|
||||
query = '''SELECT `user`.`name` FROM `user`
|
||||
JOIN `team_user` ON `team_user`.`user_id`=`user`.`id`
|
||||
@@ -37,7 +58,7 @@ def on_get(req, resp, team):
|
||||
@login_required
|
||||
def on_post(req, resp, team):
|
||||
"""
|
||||
Add user to a team
|
||||
Add user to a team. Deprecated; used only for testing purposes.
|
||||
"""
|
||||
check_team_auth(team, req)
|
||||
data = load_json_body(req)
|
||||
|
||||
@@ -23,6 +23,42 @@ constraints = {
|
||||
|
||||
|
||||
def on_get(req, resp):
|
||||
'''
|
||||
http:get:: /api/v0/teams
|
||||
|
||||
Search for team names. Allows filtering based on a number of parameters, detailed below.
|
||||
Returns list of matching team names. If "active" parameter is unspecified, defaults to
|
||||
True (only displaying undeleted teams)
|
||||
|
||||
:query name: team name
|
||||
:query name__eq: team name
|
||||
:query name__contains: team name contains param
|
||||
:query name__startswith: team name starts with param
|
||||
:query name__endswith: team name ends with param
|
||||
:query id: team id
|
||||
:query id__eq: team id
|
||||
:query active: team active/deleted (1 and 0, respectively)
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/teams?name__startswith=team- HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
"team-foo",
|
||||
"team-bar"
|
||||
]
|
||||
|
||||
'''
|
||||
|
||||
query = 'SELECT `name`, `email`, `slack_channel`, `scheduling_timezone`, `iris_plan` FROM `team`'
|
||||
if 'active' not in req.params:
|
||||
@@ -50,6 +86,48 @@ def on_get(req, resp):
|
||||
|
||||
@login_required
|
||||
def on_post(req, resp):
|
||||
'''
|
||||
Endpoint for team creation. The user who creates the team is automatically added as a
|
||||
team admin. Because of this, this endpoint cannot be called using an API key, otherwise
|
||||
a team would have no admins, making many team operations impossible.
|
||||
|
||||
Teams can specify a number of attributes, detailed below:
|
||||
|
||||
- name: the team's name. Teams must have unique names.
|
||||
- email: email address for the team.
|
||||
- slack_channel: slack channel for the team. Must start with '#'
|
||||
- iris_plan: Iris escalation plan that incidents created from the Oncall UI will follow.
|
||||
|
||||
If iris plan integration is not activated, this attribute can still be set, but its
|
||||
value is not used.
|
||||
|
||||
Teams must specify ``name`` and ``scheduling_timezone``; other parameters are optional.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST api/v0/teams HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "team-foo",
|
||||
"scheduling_timezone": "US/Pacific",
|
||||
"email": "team-foo@example.com",
|
||||
"slack_channel": "#team-foo",
|
||||
}
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json
|
||||
|
||||
:statuscode 201: Successful create
|
||||
:statuscode 400: Error in creating team. Possible errors: API key auth not allowed, invalid attributes, missing required attributes
|
||||
:statuscode 422: Duplicate team name
|
||||
'''
|
||||
if 'user' not in req.context:
|
||||
# ban API auth because we don't know who to set as team admin
|
||||
raise HTTPBadRequest('invalid login', 'API key auth is not allowed for team creation')
|
||||
|
||||
@@ -7,6 +7,46 @@ from ujson import dumps as json_dumps
|
||||
|
||||
|
||||
def on_get(req, resp, user_name):
|
||||
'''
|
||||
Endpoint for retrieving a user's upcoming shifts. Groups linked events into a single
|
||||
entity, with the number of events indicated in the ``num_events`` attribute. Non-linked
|
||||
events have ``num_events = 0``. Returns a list of event information for each of that
|
||||
user's upcoming shifts. Results can be filtered with the query string params below:
|
||||
|
||||
:query limit: The number of shifts to retrieve. Default is unlimited
|
||||
:query role: Filters results to return only shifts with the provided roles.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/users/jdoe/upcoming HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"end": 1496264400,
|
||||
"full_name": "John Doe",
|
||||
"id": 169877,
|
||||
"link_id": "7b3b96279bb24de8ac3fb7dbf06e5d1e",
|
||||
"num_events": 7,
|
||||
"role": "primary",
|
||||
"schedule_id": 1788,
|
||||
"start": 1496221200,
|
||||
"team": "team-foo",
|
||||
"user": "jdoe"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
'''
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor(db.DictCursor)
|
||||
role = req.get_param('role', None)
|
||||
|
||||
@@ -11,7 +11,49 @@ from .users import columns, get_user_data
|
||||
|
||||
def on_get(req, resp, user_name):
|
||||
"""
|
||||
Get user info by name
|
||||
Get user info by name. Retrieved fields can be filtered with the ``fields``
|
||||
query parameter. Valid fields:
|
||||
|
||||
- id - user id
|
||||
- name - username
|
||||
- contacts - user contact information
|
||||
- full_name - user's full name
|
||||
- time_zone - user's preferred display timezone
|
||||
- photo_url - URL of user's thumbnail photo
|
||||
- active - bool indicating whether the user is active in Oncall. Users can
|
||||
be marked inactive after leaving the company to preserve past event information.
|
||||
|
||||
If no ``fields`` is provided, the endpoint defaults to returning all fields.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/users/jdoe HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"active": 1,
|
||||
"contacts": {
|
||||
"call": "+1 111-111-1111",
|
||||
"email": "jdoe@example.com",
|
||||
"im": "jdoe",
|
||||
"sms": "+1 111-111-1111"
|
||||
},
|
||||
"full_name": "John Doe",
|
||||
"id": 1234,
|
||||
"name": "jdoe",
|
||||
"photo_url": "image.example.com",
|
||||
"time_zone": "US/Pacific"
|
||||
}
|
||||
|
||||
"""
|
||||
# Format request to filter query on user name
|
||||
req.params['name'] = user_name
|
||||
@@ -25,6 +67,15 @@ def on_get(req, resp, user_name):
|
||||
def on_delete(req, resp, user_name):
|
||||
"""
|
||||
Delete user by name
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v0/users/jdoe HTTP/1.1
|
||||
|
||||
:statuscode 200: Successful delete
|
||||
:statuscode 404: User not found
|
||||
"""
|
||||
check_user_auth(user_name, req)
|
||||
connection = db.connect()
|
||||
@@ -38,7 +89,38 @@ def on_delete(req, resp, user_name):
|
||||
@login_required
|
||||
def on_put(req, resp, user_name):
|
||||
"""
|
||||
Update user info
|
||||
Update user info. Allows edits to:
|
||||
|
||||
- contacts
|
||||
- name
|
||||
- full_name
|
||||
- time_zone
|
||||
- photo_url
|
||||
- active
|
||||
|
||||
Takes an object specifying the new values of these attributes. ``contacts`` acts
|
||||
slightly differently, specifying an object with the contact mode as key and new
|
||||
values for that contact mode as values. Any contact mode not specified will be
|
||||
unchanged. Similarly, any field not specified in the PUT will be unchanged.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /api/v0/users/jdoe HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"contacts": {
|
||||
"call": "+1 222-222-2222",
|
||||
"email": "jdoe@example2.com"
|
||||
}
|
||||
"name": "johndoe",
|
||||
"full_name": "Johnathan Doe",
|
||||
}
|
||||
|
||||
:statuscode 204: Successful edit
|
||||
:statuscode 404: User not found
|
||||
"""
|
||||
contacts_query = '''INSERT INTO user_contact (`user_id`, `mode_id`, `destination`) VALUES
|
||||
((SELECT `id` FROM `user` WHERE `name` = %(user)s),
|
||||
|
||||
@@ -15,6 +15,18 @@ columns = {'team': '`team_id` = (SELECT `id` FROM `team` WHERE `name` = %s)',
|
||||
|
||||
@login_required
|
||||
def on_delete(req, resp, notification_id):
|
||||
'''
|
||||
Delete user notification settings by id.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v0/notifications/1234 HTTP/1.1
|
||||
|
||||
:statuscode 200: Successful delete
|
||||
:statuscode 404: Notification setting not found
|
||||
'''
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
@@ -38,6 +50,34 @@ def on_delete(req, resp, notification_id):
|
||||
|
||||
@login_required
|
||||
def on_put(req, resp, notification_id):
|
||||
'''
|
||||
Edit user notification settings. Allows editing of the following attributes:
|
||||
|
||||
- roles: list of role names
|
||||
- team: team name
|
||||
- mode: contact mode name
|
||||
- type: string defining what event to notify on. Types are detailed in notification
|
||||
POST documentation
|
||||
- time_before: in units of seconds (if reminder setting)
|
||||
- only_if_involved: boolean (if notification setting)
|
||||
|
||||
**Example request**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /api/v0/events/1234 HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"team": "team-bar",
|
||||
"mode": "call",
|
||||
"user": "asmith",
|
||||
"roles": ["secondary"]
|
||||
}
|
||||
|
||||
:statuscode 200: Successful edit
|
||||
:statuscode 400: Validation checks failed.
|
||||
'''
|
||||
data = load_json_body(req)
|
||||
params = data.keys()
|
||||
roles = data.pop('roles')
|
||||
|
||||
@@ -13,6 +13,55 @@ all_params = required_params | other_params
|
||||
|
||||
|
||||
def on_get(req, resp, user_name):
|
||||
'''
|
||||
Get all notification settings for a user by name.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/users/jdoe/notifications HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"id": 21830,
|
||||
"mode": "email",
|
||||
"only_if_involved": null,
|
||||
"roles": [
|
||||
"primary",
|
||||
"secondary",
|
||||
"shadow",
|
||||
"manager"
|
||||
],
|
||||
"team": "team-foo",
|
||||
"time_before": 86400,
|
||||
"type": "oncall_reminder"
|
||||
},
|
||||
{
|
||||
"id": 21831,
|
||||
"mode": "email",
|
||||
"only_if_involved": null,
|
||||
"roles": [
|
||||
"primary",
|
||||
"secondary",
|
||||
"shadow",
|
||||
"manager"
|
||||
],
|
||||
"team": "team-foo",
|
||||
"time_before": 604800,
|
||||
"type": "oncall_reminder"
|
||||
}
|
||||
]
|
||||
|
||||
'''
|
||||
query = '''SELECT `team`.`name` AS `team`, `role`.`name` AS `role`, `contact_mode`.`name` AS `mode`,
|
||||
`notification_type`.`name` AS `type`, `notification_setting`.`time_before`,
|
||||
`notification_setting`.`only_if_involved`, `notification_setting`.`id`
|
||||
@@ -44,6 +93,69 @@ def on_get(req, resp, user_name):
|
||||
|
||||
@login_required
|
||||
def on_post(req, resp, user_name):
|
||||
'''
|
||||
Endpoint to create notification settings for a user. Responds with an object denoting the created
|
||||
setting's id. Requests to create notification settings must define the following:
|
||||
|
||||
- team
|
||||
- roles
|
||||
- mode
|
||||
- type
|
||||
|
||||
Users will be notified via ``$mode`` if a ``$type`` action occurs on the ``$team`` calendar that
|
||||
modifies events having a role contained in ``$roles``. In addition to these parameters,
|
||||
notification settings must define one of ``time_before`` and ``only_if_involved``, depending
|
||||
on whether the notification type is a reminder or a notification. Reminders define a ``time_before``
|
||||
and reference the start/end time of an event that user is involved in. There are two reminder
|
||||
types: "oncall_reminder" and "offcall_reminder", referencing the start and end of on-call events,
|
||||
respectively. ``time_before`` is specified in seconds and denotes how far in advance the user
|
||||
should be reminded of an event.
|
||||
|
||||
Notifications are event-driven, and created when a team's calendar is modified. By default,
|
||||
the notification types are:
|
||||
|
||||
- event_created
|
||||
- event_edited
|
||||
- event_deleted
|
||||
- event_swapped
|
||||
- event_substituted
|
||||
|
||||
Non-reminder settings must define ``only_if_involved`` which determines whether the user will
|
||||
be notified on all actions of the given typ or only on ones in which they are involved. Note
|
||||
that ``time_before`` must not be specified for a non-reminder setting, and ``only_if_involved``
|
||||
must not be specified for reminder settings.
|
||||
|
||||
An authoritative list of notification types can be obtained from the /api/v0/notification_types
|
||||
GET endpoint, which also details whether the type is a reminder. This will obtain all
|
||||
notification type data from the database, and is an absolute source of truth for Oncall.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST api/v0/events HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"team": "team-foo",
|
||||
"roles": ["primary", "secondary"],
|
||||
"mode": "email",
|
||||
"type": "event_created",
|
||||
"only_if_involved": true
|
||||
}
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1234
|
||||
}
|
||||
|
||||
'''
|
||||
check_user_auth(user_name, req)
|
||||
data = load_json_body(req)
|
||||
|
||||
|
||||
@@ -8,7 +8,27 @@ from falcon import HTTPNotFound
|
||||
|
||||
def on_get(req, resp, user_name):
|
||||
"""
|
||||
Get active teams by user name
|
||||
Get active teams by user name. Note that this does not return any deleted teams that
|
||||
this user is a member of.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/users/jdoe/teams HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
"team-foo",
|
||||
"team-bar"
|
||||
]
|
||||
"""
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor()
|
||||
|
||||
@@ -111,7 +111,59 @@ def get_user_data(fields, filter_params, dbinfo=None):
|
||||
|
||||
def on_get(req, resp):
|
||||
"""
|
||||
Search users
|
||||
Get users filtered by params. Returns a list of user info objects for all users matching
|
||||
filter parameters.
|
||||
|
||||
:query id: id of the user
|
||||
:query id__eq: id of the user
|
||||
:query id__gt: id greater than
|
||||
:query id__ge: id greater than or equal
|
||||
:query id__lt: id less than
|
||||
:query id__le: id less than or equal
|
||||
:query name: username
|
||||
:query name__eq: username
|
||||
:query name__contains: username contains param
|
||||
:query name__startswith: username starts with param
|
||||
:query name__endswith: username ends with param
|
||||
:query full_name: full name
|
||||
:query full_name__eq: username
|
||||
:query full_name__contains: full name contains param
|
||||
:query full_name__startswith: full name starts with param
|
||||
:query full_name__endswith: full name ends with param
|
||||
:query active: whether user has been deactivated (deleted)
|
||||
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v0/events?team=foo-sre&end__gt=1487466146&role=primary HTTP/1.1
|
||||
Host: example.com
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"active": 1,
|
||||
"contacts": {
|
||||
"call": "+1 111-111-1111",
|
||||
"email": "jdoe@example.com",
|
||||
"im": "jdoe",
|
||||
"sms": "+1 111-111-1111"
|
||||
},
|
||||
"full_name": "John Doe",
|
||||
"id": 1234,
|
||||
"name": "jdoe",
|
||||
"photo_url": "image.example.com",
|
||||
"time_zone": "US/Pacific"
|
||||
}
|
||||
]
|
||||
|
||||
"""
|
||||
resp.body = json_dumps(get_user_data(req.get_param_as_list('fields'), req.params))
|
||||
|
||||
@@ -119,7 +171,7 @@ def on_get(req, resp):
|
||||
@auth.debug_only
|
||||
def on_post(req, resp):
|
||||
"""
|
||||
Create user
|
||||
Create user. Currently used only in debug mode.
|
||||
"""
|
||||
data = load_json_body(req)
|
||||
connection = db.connect()
|
||||
|
||||
Reference in New Issue
Block a user