diff --git a/db/schema.v0.sql b/db/schema.v0.sql index 07e4f28..eb4ac88 100644 --- a/db/schema.v0.sql +++ b/db/schema.v0.sql @@ -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, diff --git a/src/oncall/api/v0/events.py b/src/oncall/api/v0/events.py index d349c4e..bd4e33e 100644 --- a/src/oncall/api/v0/events.py +++ b/src/oncall/api/v0/events.py @@ -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 { diff --git a/src/oncall/api/v0/populate.py b/src/oncall/api/v0/populate.py index b2f6dbd..f1b4dcd 100644 --- a/src/oncall/api/v0/populate.py +++ b/src/oncall/api/v0/populate.py @@ -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 diff --git a/src/oncall/api/v0/roster_users.py b/src/oncall/api/v0/roster_users.py index a5e3f1b..886cbb9 100644 --- a/src/oncall/api/v0/roster_users.py +++ b/src/oncall/api/v0/roster_users.py @@ -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) diff --git a/src/oncall/api/v0/rosters.py b/src/oncall/api/v0/rosters.py index af74e37..29dcaae 100644 --- a/src/oncall/api/v0/rosters.py +++ b/src/oncall/api/v0/rosters.py @@ -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) diff --git a/src/oncall/api/v0/schedule.py b/src/oncall/api/v0/schedule.py index f7769e8..5e8f8cc 100644 --- a/src/oncall/api/v0/schedule.py +++ b/src/oncall/api/v0/schedule.py @@ -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() diff --git a/src/oncall/api/v0/schedules.py b/src/oncall/api/v0/schedules.py index 26ecd3b..1e9c846 100644 --- a/src/oncall/api/v0/schedules.py +++ b/src/oncall/api/v0/schedules.py @@ -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) diff --git a/src/oncall/api/v0/search.py b/src/oncall/api/v0/search.py index f95fc6e..ceded05 100644 --- a/src/oncall/api/v0/search.py +++ b/src/oncall/api/v0/search.py @@ -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: diff --git a/src/oncall/api/v0/service.py b/src/oncall/api/v0/service.py index 8e260e4..7f7f7a4 100644 --- a/src/oncall/api/v0/service.py +++ b/src/oncall/api/v0/service.py @@ -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() diff --git a/src/oncall/api/v0/service_oncall.py b/src/oncall/api/v0/service_oncall.py index 45ed77c..10b0f1f 100644 --- a/src/oncall/api/v0/service_oncall.py +++ b/src/oncall/api/v0/service_oncall.py @@ -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` diff --git a/src/oncall/api/v0/service_teams.py b/src/oncall/api/v0/service_teams.py index c0b2e14..15e2b1d 100644 --- a/src/oncall/api/v0/service_teams.py +++ b/src/oncall/api/v0/service_teams.py @@ -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() diff --git a/src/oncall/api/v0/services.py b/src/oncall/api/v0/services.py index 3d67590..32d2586 100755 --- a/src/oncall/api/v0/services.py +++ b/src/oncall/api/v0/services.py @@ -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`' diff --git a/src/oncall/api/v0/team.py b/src/oncall/api/v0/team.py index 6477242..f234059 100644 --- a/src/oncall/api/v0/team.py +++ b/src/oncall/api/v0/team.py @@ -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() diff --git a/src/oncall/api/v0/team_admin.py b/src/oncall/api/v0/team_admin.py index b20c88b..e73ae3d 100644 --- a/src/oncall/api/v0/team_admin.py +++ b/src/oncall/api/v0/team_admin.py @@ -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() diff --git a/src/oncall/api/v0/team_admins.py b/src/oncall/api/v0/team_admins.py index f6e2e41..c460367 100644 --- a/src/oncall/api/v0/team_admins.py +++ b/src/oncall/api/v0/team_admins.py @@ -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: diff --git a/src/oncall/api/v0/team_iris_escalate.py b/src/oncall/api/v0/team_iris_escalate.py index a1e2881..37bc2df 100644 --- a/src/oncall/api/v0/team_iris_escalate.py +++ b/src/oncall/api/v0/team_iris_escalate.py @@ -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) diff --git a/src/oncall/api/v0/team_service.py b/src/oncall/api/v0/team_service.py index abcc3f0..6fc1140 100644 --- a/src/oncall/api/v0/team_service.py +++ b/src/oncall/api/v0/team_service.py @@ -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) diff --git a/src/oncall/api/v0/team_services.py b/src/oncall/api/v0/team_services.py index d4e980f..facf87d 100644 --- a/src/oncall/api/v0/team_services.py +++ b/src/oncall/api/v0/team_services.py @@ -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 ( diff --git a/src/oncall/api/v0/team_summary.py b/src/oncall/api/v0/team_summary.py index 3201526..eb3f1b4 100644 --- a/src/oncall/api/v0/team_summary.py +++ b/src/oncall/api/v0/team_summary.py @@ -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) diff --git a/src/oncall/api/v0/team_user.py b/src/oncall/api/v0/team_user.py index 5fd8e0f..52a43b6 100644 --- a/src/oncall/api/v0/team_user.py +++ b/src/oncall/api/v0/team_user.py @@ -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) diff --git a/src/oncall/api/v0/team_users.py b/src/oncall/api/v0/team_users.py index 02d7f6f..26116b8 100644 --- a/src/oncall/api/v0/team_users.py +++ b/src/oncall/api/v0/team_users.py @@ -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) diff --git a/src/oncall/api/v0/teams.py b/src/oncall/api/v0/teams.py index f052f25..f86db89 100755 --- a/src/oncall/api/v0/teams.py +++ b/src/oncall/api/v0/teams.py @@ -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') diff --git a/src/oncall/api/v0/upcoming_shifts.py b/src/oncall/api/v0/upcoming_shifts.py index a546f2a..e45da33 100644 --- a/src/oncall/api/v0/upcoming_shifts.py +++ b/src/oncall/api/v0/upcoming_shifts.py @@ -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) diff --git a/src/oncall/api/v0/user.py b/src/oncall/api/v0/user.py index 38f57f4..1d1486e 100644 --- a/src/oncall/api/v0/user.py +++ b/src/oncall/api/v0/user.py @@ -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), diff --git a/src/oncall/api/v0/user_notification.py b/src/oncall/api/v0/user_notification.py index 65cef38..34f4273 100644 --- a/src/oncall/api/v0/user_notification.py +++ b/src/oncall/api/v0/user_notification.py @@ -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') diff --git a/src/oncall/api/v0/user_notifications.py b/src/oncall/api/v0/user_notifications.py index ade6ac5..a113d49 100644 --- a/src/oncall/api/v0/user_notifications.py +++ b/src/oncall/api/v0/user_notifications.py @@ -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) diff --git a/src/oncall/api/v0/user_teams.py b/src/oncall/api/v0/user_teams.py index b3bf44b..16a55ce 100644 --- a/src/oncall/api/v0/user_teams.py +++ b/src/oncall/api/v0/user_teams.py @@ -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() diff --git a/src/oncall/api/v0/users.py b/src/oncall/api/v0/users.py index a73e097..06cc1ea 100644 --- a/src/oncall/api/v0/users.py +++ b/src/oncall/api/v0/users.py @@ -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()