From 2aee0c316cc180e7efd67656ca046ca73fa0b77c Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Fri, 3 Nov 2017 10:57:51 -0700 Subject: [PATCH] Add team subscriptions --- db/schema.v0.sql | 13 ++ e2e/conftest.py | 2 +- e2e/test_events.py | 4 +- e2e/test_subscription.py | 161 ++++++++++++++++++++++++ src/oncall/api/v0/__init__.py | 4 + src/oncall/api/v0/events.py | 39 +++++- src/oncall/api/v0/service_oncall.py | 49 +++++--- src/oncall/api/v0/team_oncall.py | 11 +- src/oncall/api/v0/team_subscription.py | 22 ++++ src/oncall/api/v0/team_subscriptions.py | 58 +++++++++ src/oncall/api/v0/team_summary.py | 62 ++++++--- src/oncall/api/v0/upcoming_shifts.py | 36 ++++-- 12 files changed, 404 insertions(+), 57 deletions(-) create mode 100644 e2e/test_subscription.py create mode 100644 src/oncall/api/v0/team_subscription.py create mode 100644 src/oncall/api/v0/team_subscriptions.py diff --git a/db/schema.v0.sql b/db/schema.v0.sql index bb78e19..2fad857 100644 --- a/db/schema.v0.sql +++ b/db/schema.v0.sql @@ -417,6 +417,19 @@ CREATE TABLE IF NOT EXISTS `application` ( PRIMARY KEY (`id`) ); +CREATE TABLE IF NOT EXISTS `team_subscription` ( + `team_id` BIGINT(20) UNSIGNED NOT NULL, + `subscription_id` BIGINT(20) UNSIGNED NOT NULL, + `role_id` INT UNSIGNED NOT NULL, + PRIMARY KEY (`team_id`, `subscription_id`, `role_id`), + INDEX `team_subscription_team_id_idx` (`team_id` ASC), + CONSTRAINT `team_subscription_team_id_fk` FOREIGN KEY (`team_id`) REFERENCES `team` (`id`) + ON DELETE CASCADE, + CONSTRAINT `team_subscription_subscription_id_fk` FOREIGN KEY (`subscription_id`) REFERENCES `team` (`id`) + ON DELETE CASCADE, + INDEX `team_subscription_team_id_fk_idx` (`team_id` ASC) +); + INSERT INTO `scheduler` ( `name`, `description`) VALUES ('default', 'Default scheduling algorithm'), diff --git a/e2e/conftest.py b/e2e/conftest.py index 2a5b8e9..3df9341 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -179,7 +179,7 @@ def event(team, role): for ev in self.created: requests.delete(api_v0('events/%d' % ev)) for t in self.teams: - re = requests.get(api_v0('events?team=' + t)) + re = requests.get(api_v0('events?include_subscribed=false'), params={'team': t}) for ev in re.json(): requests.delete(api_v0('events/%d' % ev['id'])) diff --git a/e2e/test_events.py b/e2e/test_events.py index d00c50f..0338224 100644 --- a/e2e/test_events.py +++ b/e2e/test_events.py @@ -14,7 +14,7 @@ def test_invalid_events(): @prefix('test_events') -def test_events(team, user, role): +def test_events(event, team, user, role): team_name = team.create() team_name_2 = team.create() user_name = user.create() @@ -24,6 +24,8 @@ def test_events(team, user, role): user.add_to_team(user_name, team_name) user.add_to_team(user_name_2, team_name) user.add_to_team(user_name_2, team_name_2) + event.teams.add(team_name) + event.teams.add(team_name_2) start, end = int(time.time()) + 100, int(time.time() + 36000) diff --git a/e2e/test_subscription.py b/e2e/test_subscription.py new file mode 100644 index 0000000..3ea90cd --- /dev/null +++ b/e2e/test_subscription.py @@ -0,0 +1,161 @@ +import time +import requests +from testutils import prefix, api_v0 + + +@prefix('test_v0_sub') +def test_api_v0_team_subscription(team, role): + team_name = team.create() + team_name_2 = team.create() + team_name_3 = team.create() + role_name = role.create() + + re = requests.post(api_v0('teams/%s/subscriptions' % team_name), json={'role': role_name, 'subscription': team_name_2}) + assert re.status_code == 201 + re = requests.post(api_v0('teams/%s/subscriptions' % team_name), json={'role': role_name, 'subscription': team_name_3}) + assert re.status_code == 201 + + re = requests.get(api_v0('teams/%s/subscriptions' % team_name)) + assert re.status_code == 200 + data = re.json() + assert team_name_2 in data + assert team_name_3 in data + assert len(data) == 2 + + re = requests.delete(api_v0('teams/%s/subscriptions/%s/%s' % (team_name, team_name_3, role_name))) + assert re.status_code == 200 + + re = requests.get(api_v0('teams/%s/subscriptions' % team_name)) + assert re.status_code == 200 + data = re.json() + assert team_name_2 in data + assert len(data) == 1 + + +@prefix('test_v0_sub_events') +def test_api_v0_subscription_events(user, role, team, event): + team_name = team.create() + team_name_2 = team.create() + user_name = user.create() + role_name = role.create() + user.add_to_team(user_name, team_name) + user.add_to_team(user_name, team_name_2) + start = int(time.time()) + 1000 + ev1 = event.create({'start': start, + 'end': start + 1000, + 'user': user_name, + 'team': team_name, + 'role': role_name}) + ev2 = event.create({'start': start + 1000, + 'end': start + 2000, + 'user': user_name, + 'team': team_name_2, + 'role': role_name}) + re = requests.post(api_v0('teams/%s/subscriptions' % team_name), json={'role': role_name, 'subscription': team_name_2}) + assert re.status_code == 201 + re = requests.get(api_v0('events?team__eq=%s' % team_name)) + ev_ids = [ev['id'] for ev in re.json()] + assert ev1 in ev_ids + assert ev2 in ev_ids + + re = requests.get(api_v0('events?team__eq=%s&include_subscribed=False' % team_name)) + ev_ids = [ev['id'] for ev in re.json()] + assert ev1 in ev_ids + assert len(ev_ids) == 1 + + +@prefix('test_v0_sub_oncall') +def test_v0_subscription_oncall(user, role, team, service, event): + team_name = team.create() + team_name_2 = team.create() + service_name = service.create() + service.associate_team(service_name, team_name) + user_name = user.create() + user_name_2 = user.create() + role_name = role.create() + user.add_to_team(user_name, team_name) + user.add_to_team(user_name_2, team_name_2) + re = requests.post(api_v0('teams/%s/subscriptions' % team_name), json={'role': role_name, 'subscription': team_name_2}) + assert re.status_code == 201 + start = int(time.time()) + + ev1 = event.create({'start': start, + 'end': start + 1000, + 'user': user_name, + 'team': team_name, + 'role': role_name}) + ev2 = event.create({'start': start, + 'end': start + 1000, + 'user': user_name_2, + 'team': team_name_2, + 'role': role_name}) + + re = requests.get(api_v0('services/%s/oncall/%s' % (service_name, role_name))) + assert re.status_code == 200 + results = re.json() + users = [ev['user'] for ev in results] + assert user_name in users + assert user_name_2 in users + assert len(results) == 2 + + +@prefix('test_v0_sub_summary') +def test_v0_subscription_summary(user, role, team, event): + team_name = team.create() + team_name_2 = team.create() + user_name = user.create() + user_name_2 = user.create() + role_name = role.create() + role_name_2 = role.create() + user.add_to_team(user_name, team_name) + user.add_to_team(user_name_2, team_name_2) + + start, end = int(time.time()), int(time.time()+36000) + + event_data_1 = {'start': start, + 'end': end, + 'user': user_name, + 'team': team_name, + 'role': role_name} + event_data_2 = {'start': start - 5, + 'end': end - 5, + 'user': user_name_2, + 'team': team_name_2, + 'role': role_name_2} + event_data_3 = {'start': start + 50000, + 'end': end + 50000, + 'user': user_name, + 'team': team_name, + 'role': role_name} + event_data_4 = {'start': start + 50005, + 'end': end + 50005, + 'user': user_name_2, + 'team': team_name_2, + 'role': role_name_2} + event_data_5 = {'start': start + 50001, + 'end': end + 50001, + 'user': user_name, + 'team': team_name, + 'role': role_name} + + # Create current events + event.create(event_data_1) + event.create(event_data_2) + # Create next events + event.create(event_data_3) + event.create(event_data_4) + # Create extra future event that isn't the next event + event.create(event_data_5) + + re = requests.post(api_v0('teams/%s/subscriptions' % team_name), json={'role': role_name_2, 'subscription': team_name_2}) + assert re.status_code == 201 + + re = requests.get(api_v0('teams/%s/summary' % team_name)) + assert re.status_code == 200 + results = re.json() + keys = ['start', 'end', 'role'] + + assert all(results['current'][role_name][0][key] == event_data_1[key] for key in keys) + assert all(results['current'][role_name_2][0][key] == event_data_2[key] for key in keys) + assert all(results['next'][role_name][0][key] == event_data_3[key] for key in keys) + assert all(results['next'][role_name_2][0][key] == event_data_4[key] for key in keys) diff --git a/src/oncall/api/v0/__init__.py b/src/oncall/api/v0/__init__.py index bff19d8..5b8dcb3 100644 --- a/src/oncall/api/v0/__init__.py +++ b/src/oncall/api/v0/__init__.py @@ -82,6 +82,10 @@ def init(application, config): from . import timezones application.add_route('/api/v0/timezones', timezones) + from . import team_subscription, team_subscriptions + application.add_route('/api/v0/teams/{team}/subscriptions', team_subscriptions) + application.add_route('/api/v0/teams/{team}/subscriptions/{subscription}/{role}', team_subscription) + # Optional Iris integration from . import iris_settings application.add_route('/api/v0/iris_settings', iris_settings) diff --git a/src/oncall/api/v0/events.py b/src/oncall/api/v0/events.py index f99bd55..307b425 100644 --- a/src/oncall/api/v0/events.py +++ b/src/oncall/api/v0/events.py @@ -66,6 +66,8 @@ constraints = { 'user__endswith': '`user`.`name` LIKE CONCAT("%%", %s)' } +TEAM_PARAMS = {'team', 'team__eq', 'team__contains', 'team__startswith', 'team_endswith', 'team_id'} + def on_get(req, resp): """ @@ -144,6 +146,10 @@ def on_get(req, resp): """ fields = req.get_param_as_list('fields', transform=columns.__getitem__) req.params.pop('fields', None) + include_sub = req.get_param_as_bool('include_subscribed') + if include_sub is None: + include_sub = True + req.params.pop('include_subscribed', None) cols = ', '.join(fields) if fields else all_columns if any(key not in constraints for key in req.params): raise HTTPBadRequest('Bad constraint param') @@ -154,17 +160,42 @@ def on_get(req, resp): where_params = [] where_vals = [] - for key in req.params: + connection = db.connect() + cursor = connection.cursor(db.DictCursor) + + # Build where clause. If including subscriptions, deal with team parameters later + params = req.params.viewkeys() - TEAM_PARAMS if include_sub else req.params + for key in params: val = req.get_param(key) if key in constraints: where_params.append(constraints[key]) where_vals.append(val) + + # Deal with team subscriptions and team parameters + team_where = [] + subs_vals = [] + team_params = req.params.viewkeys() & TEAM_PARAMS + if include_sub and team_params: + + for key in team_params: + val = req.get_param(key) + team_where.append(constraints[key]) + subs_vals.append(val) + subs_and = ' AND '.join(team_where) + cursor.execute('''SELECT `subscription_id`, `role_id` FROM `team_subscription` + JOIN `team` ON `team_id` = `team`.`id` + WHERE %s''' % subs_and, + subs_vals) + if cursor.rowcount != 0: + # Check conditions are true for either team OR subscriber + subs_and = '(%s OR (%s))' % (subs_and, ' OR '.join(['`team`.`id` = %s AND `role`.`id` = %s' % + (row['subscription_id'], row['role_id']) for row in cursor])) + where_params.append(subs_and) + where_vals += subs_vals + where_query = ' AND '.join(where_params) if where_query: query = '%s WHERE %s' % (query, where_query) - - connection = db.connect() - cursor = connection.cursor(db.DictCursor) cursor.execute(query, where_vals) data = cursor.fetchall() cursor.close() diff --git a/src/oncall/api/v0/service_oncall.py b/src/oncall/api/v0/service_oncall.py index ddae086..a3ca7ca 100644 --- a/src/oncall/api/v0/service_oncall.py +++ b/src/oncall/api/v0/service_oncall.py @@ -40,24 +40,43 @@ def on_get(req, resp, service, role=None): ] ''' - get_oncall_query = '''SELECT `user`.`full_name` AS `full_name`, `event`.`start`, `event`.`end`, - `contact_mode`.`name` AS `mode`, `user_contact`.`destination`, `role`.`name` AS `role`, - `team`.`name` AS `team`, `user`.`name` AS `user` - FROM `service` JOIN `team_service` ON `service`.`id` = `team_service`.`service_id` - JOIN `event` ON `event`.`team_id` = `team_service`.`team_id` - JOIN `user` ON `user`.`id` = `event`.`user_id` - JOIN `role` ON `role`.`id` = `event`.`role_id` - JOIN `team` ON `team`.`id` = `event`.`team_id` - LEFT JOIN `user_contact` ON `user`.`id` = `user_contact`.`user_id` - LEFT JOIN `contact_mode` ON `contact_mode`.`id` = `user_contact`.`mode_id` - WHERE UNIX_TIMESTAMP() BETWEEN `event`.`start` AND `event`.`end` - AND `service`.`name` = %s ''' - query_params = [service] + get_oncall_query = ''' + SELECT `user`.`full_name` AS `full_name`, + `event`.`start`, `event`.`end`, + `contact_mode`.`name` AS `mode`, + `user_contact`.`destination`, + `user`.`name` AS `user`, + `team`.`name` AS `team`, + `role`.`name` AS `role` + FROM `event` + JOIN `user` ON `event`.`user_id` = `user`.`id` + JOIN `team` ON `event`.`team_id` = `team`.`id` + JOIN `role` ON `role`.`id` = `event`.`role_id` + LEFT JOIN `team_subscription` ON `subscription_id` = `team`.`id` + AND `team_subscription`.`role_id` = `role`.`id` + LEFT JOIN `user_contact` ON `user`.`id` = `user_contact`.`user_id` + LEFT JOIN `contact_mode` ON `contact_mode`.`id` = `user_contact`.`mode_id` + WHERE UNIX_TIMESTAMP() BETWEEN `event`.`start` AND `event`.`end` + AND (`team`.`id` IN %s OR `team_subscription`.`team_id` IN %s)''' + + query_params = [] + connection = db.connect() + cursor = connection.cursor(db.DictCursor) + # Get subscription teams for teams owning the service, along with the teams that own the service + cursor.execute('''SELECT `team_id` FROM `team_service` + JOIN `service` ON `service`.`id` = `team_service`.`service_id` + WHERE `service`.`name` = %s''', + service) + team_ids = [row['team_id'] for row in cursor] + if not team_ids: + resp.body = json_dumps([]) + cursor.close() + connection.close() + return + query_params += [team_ids, team_ids] if role is not None: get_oncall_query += ' AND `role`.`name` = %s' query_params.append(role) - connection = db.connect() - cursor = connection.cursor(db.DictCursor) cursor.execute(get_oncall_query, query_params) data = cursor.fetchall() ret = {} diff --git a/src/oncall/api/v0/team_oncall.py b/src/oncall/api/v0/team_oncall.py index 9fc8dd4..2f4b9a5 100644 --- a/src/oncall/api/v0/team_oncall.py +++ b/src/oncall/api/v0/team_oncall.py @@ -64,15 +64,18 @@ def on_get(req, resp, team, role=None): JOIN `user` ON `event`.`user_id` = `user`.`id` JOIN `team` ON `event`.`team_id` = `team`.`id` JOIN `role` ON `role`.`id` = `event`.`role_id` + LEFT JOIN `team_subscription` ON `subscription_id` = `team`.`id` + AND `team_subscription`.`role_id` = `role`.`id` + LEFT JOIN `team` `subscriber` ON `subscriber`.`id` = `team_subscription`.`team_id` LEFT JOIN `user_contact` ON `user`.`id` = `user_contact`.`user_id` LEFT JOIN `contact_mode` ON `contact_mode`.`id` = `user_contact`.`mode_id` WHERE UNIX_TIMESTAMP() BETWEEN `event`.`start` AND `event`.`end` - AND `team`.`name` = %s''' - - query_params = [team] + AND (`team`.`name` = %s OR `subscriber`.`name` = %s)''' + query_params = [team, team] if role is not None: - get_oncall_query += 'AND `role`.`name` = %s' + get_oncall_query += ' AND `role`.`name` = %s' query_params.append(role) + connection = db.connect() cursor = connection.cursor(db.DictCursor) cursor.execute(get_oncall_query, query_params) diff --git a/src/oncall/api/v0/team_subscription.py b/src/oncall/api/v0/team_subscription.py new file mode 100644 index 0000000..af8a1b1 --- /dev/null +++ b/src/oncall/api/v0/team_subscription.py @@ -0,0 +1,22 @@ +from ... import db +from ...auth import login_required, check_team_auth +from falcon import HTTPNotFound + + +@login_required +def on_delete(req, resp, team, subscription, role): + check_team_auth(team, req) + connection = db.connect() + cursor = connection.cursor() + + cursor.execute('''DELETE FROM `team_subscription` + WHERE team_id = (SELECT `id` FROM `team` WHERE `name` = %s) + AND `subscription_id` = (SELECT `id` FROM `team` WHERE `name` = %s)\ + AND `role_id` = (SELECT `id` FROM `role` WHERE `name` = %s)''', + (team, subscription, role)) + deleted = cursor.rowcount + connection.commit() + cursor.close() + connection.close() + if deleted == 0: + raise HTTPNotFound() diff --git a/src/oncall/api/v0/team_subscriptions.py b/src/oncall/api/v0/team_subscriptions.py new file mode 100644 index 0000000..a9b409c --- /dev/null +++ b/src/oncall/api/v0/team_subscriptions.py @@ -0,0 +1,58 @@ +from ... import db +from ujson import dumps as json_dumps +from falcon import HTTPError, HTTPBadRequest, HTTP_201 +from ...utils import load_json_body +from ...auth import login_required, check_team_auth +import logging + +logger = logging.getLogger('oncall-api') + + +def on_get(req, resp, team): + connection = db.connect() + cursor = connection.cursor() + cursor.execute('''SELECT `subscription`.`name`, `role`.`name` FROM `team` + JOIN `team_subscription` ON `team`.`id` = `team_subscription`.`team_id` + JOIN `team` `subscription` ON `subscription`.`id` = `team_subscription`.`subscription_id` + JOIN `role` ON `role`.`id` = `team_subscription`.`role_id` + WHERE `team`.`name` = %s''', + team) + data = [row[0] for row in cursor] + cursor.close() + connection.close() + resp.body = json_dumps(data) + + +@login_required +def on_post(req, resp, team): + data = load_json_body(req) + check_team_auth(team, req) + sub_name = data.get('subscription') + role_name = data.get('role') + if not sub_name or not role_name: + raise HTTPBadRequest('Invalid subscription', 'Missing subscription name or role name') + connection = db.connect() + cursor = connection.cursor() + try: + cursor.execute('''INSERT INTO `team_subscription` (`team_id`, `subscription_id`, `role_id`) VALUES + ((SELECT `id` FROM `team` WHERE `name` = %s), + (SELECT `id` FROM `team` WHERE `name` = %s), + (SELECT `id` FROM `role` WHERE `name` = %s))''', + (team, sub_name, role_name)) + except db.IntegrityError as e: + err_msg = str(e.args[1]) + if err_msg == 'Column \'team_id\' cannot be null': + err_msg = 'team "%s" not found' % team + elif err_msg == 'Column \'role_id\' cannot be null': + err_msg = 'role "%s" not found' % role_name + elif err_msg == 'Column \'subscription_id\' cannot be null': + err_msg = 'team "%s" not found' % sub_name + logger.exception('Unknown integrity error in team_subscriptions') + raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg) + else: + connection.commit() + finally: + cursor.close() + connection.close() + + resp.status = HTTP_201 \ No newline at end of file diff --git a/src/oncall/api/v0/team_summary.py b/src/oncall/api/v0/team_summary.py index eb3f1b4..a325fc8 100644 --- a/src/oncall/api/v0/team_summary.py +++ b/src/oncall/api/v0/team_summary.py @@ -114,17 +114,32 @@ def on_get(req, resp, team): if cursor.rowcount < 1: raise HTTPNotFound() team_id = cursor.fetchone()['id'] - current_query = ''' - SELECT `role`.`name` AS `role`, `user`.`full_name` AS `full_name`, - `event`.`start`, `event`.`end`, `user`.`photo_url`, `event`.`user_id` + SELECT `user`.`full_name` AS `full_name`, + `user`.`photo_url`, + `event`.`start`, `event`.`end`, + `event`.`user_id`, + `user`.`name` AS `user`, + `team`.`name` AS `team`, + `role`.`name` AS `role` FROM `event` - JOIN `role` ON `event`.`role_id` = `role`.`id` - JOIN `user` ON `event`.`user_id` = `user`.`id` - WHERE `event`.`team_id` = %s - AND UNIX_TIMESTAMP() >= `event`.`start` - AND UNIX_TIMESTAMP() < `event`.`end`''' - cursor.execute(current_query, team_id) + JOIN `user` ON `event`.`user_id` = `user`.`id` + JOIN `team` ON `event`.`team_id` = `team`.`id` + JOIN `role` ON `role`.`id` = `event`.`role_id` + WHERE UNIX_TIMESTAMP() BETWEEN `event`.`start` AND `event`.`end`''' + team_where = '`team`.`id` = %s' + cursor.execute('''SELECT `subscription_id`, `role_id` FROM `team_subscription` + JOIN `team` ON `team_id` = `team`.`id` + WHERE %s''' % team_where, + team_id) + + if cursor.rowcount != 0: + # Check conditions are true for either team OR subscriber + team_where = '(%s OR (%s))' % (team_where, ' OR '.join(['`event`.`team_id` = %s AND `event`.`role_id` = %s' % + (row['subscription_id'], row['role_id']) for row in cursor])) + + + cursor.execute(' AND '.join((current_query, team_where)), team_id) payload = {} users = set([]) payload['current'] = defaultdict(list) @@ -133,18 +148,25 @@ def on_get(req, resp, team): users.add(event['user_id']) next_query = ''' - SELECT `role`.`name` AS `role`, `user`.`full_name` AS `full_name`, - `event`.`start`, `event`.`end`, `user`.`photo_url`, `event`.`user_id` + SELECT `role`.`name` AS `role`, + `user`.`full_name` AS `full_name`, + `event`.`start`, + `event`.`end`, + `user`.`photo_url`, + `event`.`user_id`, + `event`.`role_id`, + `event`.`team_id` FROM `event` - JOIN `role` ON `event`.`role_id` = `role`.`id` - JOIN `user` ON `event`.`user_id` = `user`.`id` - JOIN (SELECT `role_id`, `team_id`, MIN(`start` - UNIX_TIMESTAMP()) AS dist - FROM `event` - WHERE `start` > UNIX_TIMESTAMP() AND `event`.`team_id` = %s - GROUP BY role_id) AS t1 - ON `event`.`role_id` = `t1`.`role_id` - AND `event`.`start` - UNIX_TIMESTAMP() = `t1`.dist - AND `event`.`team_id` = `t1`.`team_id`''' + JOIN `role` ON `event`.`role_id` = `role`.`id` + JOIN `user` ON `event`.`user_id` = `user`.`id` + + JOIN (SELECT `event`.`role_id`, `event`.`team_id`, MIN(`event`.`start` - UNIX_TIMESTAMP()) AS dist + FROM `event` JOIN `team` ON `team`.`id` = `event`.`team_id` + WHERE `start` > UNIX_TIMESTAMP() AND %s + GROUP BY `event`.`role_id`, `event`.`team_id`) AS t1 + ON `event`.`role_id` = `t1`.`role_id` + AND `event`.`start` - UNIX_TIMESTAMP() = `t1`.dist + AND `event`.`team_id` = `t1`.`team_id`''' % team_where cursor.execute(next_query, team_id) payload['next'] = defaultdict(list) for event in cursor: diff --git a/src/oncall/api/v0/upcoming_shifts.py b/src/oncall/api/v0/upcoming_shifts.py index e45da33..71156f2 100644 --- a/src/oncall/api/v0/upcoming_shifts.py +++ b/src/oncall/api/v0/upcoming_shifts.py @@ -4,6 +4,8 @@ from ... import db from .events import all_columns from ujson import dumps as json_dumps +from collections import defaultdict +import operator def on_get(req, resp, user_name): @@ -47,29 +49,39 @@ def on_get(req, resp, user_name): ''' - connection = db.connect() - cursor = connection.cursor(db.DictCursor) role = req.get_param('role', None) - limit = req.get_param_as_int('limit', None) + limit = req.get_param_as_int('limit') query_end = ' ORDER BY `event`.`start` ASC' - query = '''SELECT %s, (SELECT COUNT(*) FROM `event` `counter` - WHERE `counter`.`link_id` = `event`.`link_id`) AS num_events + query = '''SELECT %s FROM `event` JOIN `user` ON `user`.`id` = `event`.`user_id` JOIN `team` ON `team`.`id` = `event`.`team_id` JOIN `role` ON `role`.`id` = `event`.`role_id` - LEFT JOIN `event` `e2` ON `event`.link_id = `e2`.`link_id` AND `e2`.`start` < `event`.`start` WHERE `user`.`id` = (SELECT `id` FROM `user` WHERE `name` = %%s) - AND `event`.`start` > UNIX_TIMESTAMP() - AND `e2`.`start` IS NULL''' % all_columns + AND `event`.`start` > UNIX_TIMESTAMP()''' % all_columns query_params = [user_name] if role: query_end = ' AND `role`.`name` = %s' + query_end query_params.append(role) - if limit: - query_end += ' LIMIT %s' - query_params.append(limit) + connection = db.connect() + cursor = connection.cursor(db.DictCursor) cursor.execute(query + query_end, query_params) data = cursor.fetchall() - resp.body = json_dumps(data) + cursor.close() + connection.close() + links = defaultdict(list) + formatted = [] + for event in data: + if event['link_id'] is None: + formatted.append(event) + else: + links[event['link_id']].append(event) + for events in links.itervalues(): + first_event = min(events, key=operator.itemgetter('start')) + first_event['num_events'] = len(events) + formatted.append(first_event) + formatted = sorted(formatted, key=operator.itemgetter('start')) + if limit is not None: + formatted = formatted[:limit] + resp.body = json_dumps(formatted) \ No newline at end of file