From 9968ef9af54af43f75054c14ba70ece79ce78e78 Mon Sep 17 00:00:00 2001 From: Manzi Fabrice Date: Wed, 18 Dec 2019 16:28:50 +0200 Subject: [PATCH] CV3-71-story(notifications): receive notifications via bouquet - receive notifications from google - pass notifications on to subscribers that are listening. --- ...7311_subscriber_and_subscription_tables.py | 45 +++++++++++++++++++ api/v2/__init__.py | 3 +- .../bouquets/bouquets_controller.py | 34 +++++++++++++- api/v2/helpers/channels/channels_helper.py | 7 +++ api/v2/helpers/subscriber/__init__.py | 0 .../helpers/subscriber/subscriber_helper.py | 16 +++++++ api/v2/models/subscriber/__init__.py | 0 api/v2/models/subscriber/subscriber_model.py | 30 +++++++++++++ api/v2/models/subscriber/subscriber_schema.py | 9 ++++ api/v2/models/subscription_method/__init__.py | 0 .../subscription_method_model.py | 15 +++++++ .../subscription_method_schema.py | 8 ++++ tests/base.py | 19 ++++++++ tests/test_bouquets/test_bouquets.py | 33 +++++++++++++- 14 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 alembic/versions/b95e7b9d7311_subscriber_and_subscription_tables.py create mode 100644 api/v2/helpers/subscriber/__init__.py create mode 100644 api/v2/helpers/subscriber/subscriber_helper.py create mode 100644 api/v2/models/subscriber/__init__.py create mode 100644 api/v2/models/subscriber/subscriber_model.py create mode 100644 api/v2/models/subscriber/subscriber_schema.py create mode 100644 api/v2/models/subscription_method/__init__.py create mode 100644 api/v2/models/subscription_method/subscription_method_model.py create mode 100644 api/v2/models/subscription_method/subscription_method_schema.py diff --git a/alembic/versions/b95e7b9d7311_subscriber_and_subscription_tables.py b/alembic/versions/b95e7b9d7311_subscriber_and_subscription_tables.py new file mode 100644 index 0000000..dc8d84e --- /dev/null +++ b/alembic/versions/b95e7b9d7311_subscriber_and_subscription_tables.py @@ -0,0 +1,45 @@ +"""subscriber and subscription tables + +Revision ID: b95e7b9d7311 +Revises: e511d62f38b7 +Create Date: 2019-12-19 12:02:14.160467 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b95e7b9d7311' +down_revision = 'e511d62f38b7' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table('subscribers', + sa.Column('date_created', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), + sa.Column('date_updated', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('subscriber_name', sa.Text(), nullable=False), + sa.Column('username', sa.Text(), nullable=False), + sa.Column('password', sa.Text(), nullable=False), + sa.Column('notification_url', sa.Text(), nullable=False), + sa.Column('subscription_method_id', sa.Integer(), nullable=True), + sa.Column('bouquet_id', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['subscription_method_id'], ['subscriptions.id'], name='fk_subscribers_subscriptions', onupdate='CASCADE', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['bouquet_id'], ['bouquets.id'], name='fk_subscribers_bouquets', onupdate='CASCADE', ondelete='CASCADE') + ), + op.create_table('subscriptions', + sa.Column('date_created', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), + sa.Column('date_updated', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade(): + op.drop_table('subscribers'), + op.drop_table('subscriptions') diff --git a/api/v2/__init__.py b/api/v2/__init__.py index fbb9caa..c514279 100644 --- a/api/v2/__init__.py +++ b/api/v2/__init__.py @@ -2,7 +2,7 @@ from flask import Blueprint from api.v2.controllers.channels.channels_controller import Channels -from api.v2.controllers.bouquets.bouquets_controller import Bouquets +from api.v2.controllers.bouquets.bouquets_controller import Bouquets, SendNotifications from api.v2.controllers.logs.logs_controller import Logs @@ -15,4 +15,5 @@ mrm_push.add_resource(Channels, '/channels', strict_slashes=False) mrm_push.add_resource(Bouquets, '/bouquets', strict_slashes=False) +mrm_push.add_resource(SendNotifications, '/notifications', strict_slashes=False) mrm_push.add_resource(Logs, '/logs', strict_slashes=False) diff --git a/api/v2/controllers/bouquets/bouquets_controller.py b/api/v2/controllers/bouquets/bouquets_controller.py index e925513..7a15fdf 100644 --- a/api/v2/controllers/bouquets/bouquets_controller.py +++ b/api/v2/controllers/bouquets/bouquets_controller.py @@ -1,8 +1,14 @@ +import requests +import datetime + from flask import make_response, jsonify, request from flask_restful import Resource from api.v2.helpers.bouquets.bouquets_helper import query_all_bouquets, query_bouquet +from api.v2.helpers.channels.channels_helper import query_channel +from api.v2.helpers.subscriber.subscriber_helper import get_subscribers from api.v2.models.bouquets.bouquets_model import Bouquets as BouquetsModel +from api.v2.models.logs.logs_model import Logs from api.v2.utilities.validators import validate_bouquet_adding from api.v2.helpers.credentials import check_bouquet_credentials @@ -42,5 +48,31 @@ def delete(self): if not bouquet: return make_response(jsonify({'message': 'The bouquet you want to delete is not found'}), 404) BouquetsModel.delete_bouquet(BouquetsModel, bouquet_id) - return make_response(jsonify({'message': 'The bouquet was deleted successfully'}), 200) + return make_response(jsonify({'message': 'The bouquet was deleted successfully'}), 200) + +class SendNotifications(Resource): + + def post(self): + """function to receive notifications and send them to subscriber""" + resource_id = request.headers['X-Goog-Resource-ID'] + channel = query_channel(resource_id) + if not channel: + return make_response(jsonify({'message': 'Channel not found'}), 404) + + bouquet_id = channel['bouquet_id'] + subscribers = get_subscribers(bouquet_id) + if not subscribers: + return make_response(jsonify({'message': 'Subscribers not found'}), 404) + + for subscriber in subscribers: + calendar_id = channel['calendar_id'] + notification_url = subscriber['notification_url'] + results = requests.post(url=notification_url, json=calendar_id) + Logs.save_log(timestamp = datetime.datetime.now(), + calendar_id=calendar_id, + subscriber_name = subscriber['subscriber_name'], + subscription_method = subscriber['subscription'].name, + payload = results.status_code) + + return make_response(jsonify({'message': 'Notifications sent'}), 200) diff --git a/api/v2/helpers/channels/channels_helper.py b/api/v2/helpers/channels/channels_helper.py index 1bf90b1..8b93f55 100644 --- a/api/v2/helpers/channels/channels_helper.py +++ b/api/v2/helpers/channels/channels_helper.py @@ -9,3 +9,10 @@ def query_all_channels(): channels = ChannelsSchema(many=True).dump(all_channels) return channels + +def query_channel(resource_id): + channel = ChannelsModel.query.filter_by(resource_id=resource_id).first() + if channel: + return ChannelsSchema(many=False).dump(channel) + + return None diff --git a/api/v2/helpers/subscriber/__init__.py b/api/v2/helpers/subscriber/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/v2/helpers/subscriber/subscriber_helper.py b/api/v2/helpers/subscriber/subscriber_helper.py new file mode 100644 index 0000000..dfad584 --- /dev/null +++ b/api/v2/helpers/subscriber/subscriber_helper.py @@ -0,0 +1,16 @@ +from api.v2.models.subscriber.subscriber_model import Subscribers as SubscriberModel +from api.v2.models.subscriber.subscriber_schema import SubscribersSchema + +def get_subscribers(bouquet_id): + subscribers = SubscriberModel.query.filter_by(bouquet_id=bouquet_id).all() + if subscribers: + return SubscribersSchema(many=True).dump(subscribers) + + return None + +def get_subscriber(suscriber_id): + subscriber = SubscriberModel.query.filter_by(id=suscriber_id).first() + if subscriber: + return SubscribersSchema(many=False).dump(subscriber) + + return None diff --git a/api/v2/models/subscriber/__init__.py b/api/v2/models/subscriber/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/v2/models/subscriber/subscriber_model.py b/api/v2/models/subscriber/subscriber_model.py new file mode 100644 index 0000000..3a59154 --- /dev/null +++ b/api/v2/models/subscriber/subscriber_model.py @@ -0,0 +1,30 @@ +from sqlalchemy import (Column, String, Integer, Enum, ForeignKey, Text) +from sqlalchemy.schema import Sequence +from sqlalchemy.orm import relationship + +from api.v2.helpers.database import Base +from api.v2.utilities.utility import Utility, StateType + +from api.v2.models.subscription_method.subscription_method_model import Subscriptions + + +class Subscribers(Base, Utility): + __tablename__ = 'subscribers' + id = Column(Integer, Sequence('subscribers_id_seq', start=1, increment=1), primary_key=True) + subscriber_name = Column(Text, nullable=False) + username = Column(Text, nullable=False) + password = Column(Text, nullable=False) + notification_url = Column(Text, nullable=False) + subscription_method_id = Column(Integer, ForeignKey('subscriptions.id', + name='fk_subscribers_subscriptions', ondelete='CASCADE')) + bouquet_id = Column(Integer, ForeignKey('bouquets.id', + name='fk_subscribers_bouquets', ondelete='CASCADE')) + subscription = relationship('Subscriptions', backref='subscriptions') + + def __init__(self, **kwargs): + self.subscriber_name = kwargs['subscriber_name'] + self.username = kwargs['username'] + self.password = kwargs['password'] + self.notification_url = kwargs['notification_url'] + self.subscription_method_id = kwargs['subscription_method_id'] + self.bouquet_id = kwargs['bouquet_id'] diff --git a/api/v2/models/subscriber/subscriber_schema.py b/api/v2/models/subscriber/subscriber_schema.py new file mode 100644 index 0000000..36ddb5e --- /dev/null +++ b/api/v2/models/subscriber/subscriber_schema.py @@ -0,0 +1,9 @@ +from ma import ma +from api.v2.models.subscriber.subscriber_model import Subscribers + + +class SubscribersSchema(ma.Schema): + class Meta: + model = Subscribers + fields = ("id", "subscriber_name", "username", + "notification_url", "subscription_method_id", "bouquet_id", "subscription") diff --git a/api/v2/models/subscription_method/__init__.py b/api/v2/models/subscription_method/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/v2/models/subscription_method/subscription_method_model.py b/api/v2/models/subscription_method/subscription_method_model.py new file mode 100644 index 0000000..f805dca --- /dev/null +++ b/api/v2/models/subscription_method/subscription_method_model.py @@ -0,0 +1,15 @@ +from sqlalchemy import (Column, String, Integer, Enum, ForeignKey, Text) +from sqlalchemy.schema import Sequence +from sqlalchemy.orm import relationship + +from api.v2.helpers.database import Base +from api.v2.utilities.utility import Utility, StateType + + +class Subscriptions(Base, Utility): + __tablename__ = 'subscriptions' + id = Column(Integer, autoincrement=True, primary_key=True) + name = Column(Text, nullable=False) + + def __init__(self, **kwargs): + self.name = kwargs['name'] diff --git a/api/v2/models/subscription_method/subscription_method_schema.py b/api/v2/models/subscription_method/subscription_method_schema.py new file mode 100644 index 0000000..ffa6b4a --- /dev/null +++ b/api/v2/models/subscription_method/subscription_method_schema.py @@ -0,0 +1,8 @@ +from ma import ma +from api.v2.models.subscription_method.subscription_method_model import Subscriptions + + +class SubscribersSchema(ma.Schema): + class Meta: + model = Subscriptions + fields = ("id", "name") diff --git a/tests/base.py b/tests/base.py index 7deb16d..cae9220 100644 --- a/tests/base.py +++ b/tests/base.py @@ -9,6 +9,8 @@ from api.v2.helpers.database import engine, db_session, Base from api.v2.models.channels.channels_model import Channels from api.v2.models.bouquets.bouquets_model import Bouquets +from api.v2.models.subscriber.subscriber_model import Subscribers +from api.v2.models.subscription_method.subscription_method_model import Subscriptions from api.v2.models.logs.logs_model import Logs sys.path.append(os.getcwd()) @@ -144,10 +146,27 @@ def setUp(self): calendar_id="emilereas@gmail.com", resource_id="9ty4bejkkfvdw", extra_atrributes='t284nff94nf', bouquet_id=1) + + channel_two = Channels(channel_id="564dfg67jn56h7jh6n", + calendar_id="emilereasw@gmail.com", + resource_id="9ty4bejkkfvdww", + extra_atrributes='t284nff94nfn', bouquet_id=2) + + subscription = Subscriptions(name="compact") + + subscriber = Subscribers(subscriber_name="Manzi", + username="manzif6", + password="password1", + notification_url='https://notificati.com', + subscription_method_id=1, + bouquet_id=1) + subscription.save() channel.save() + channel_two.save() bouquet.save() bouquet_two.save() log.save() + subscriber.save() db_session.commit() diff --git a/tests/test_bouquets/test_bouquets.py b/tests/test_bouquets/test_bouquets.py index 5f9b636..1495439 100644 --- a/tests/test_bouquets/test_bouquets.py +++ b/tests/test_bouquets/test_bouquets.py @@ -4,7 +4,18 @@ from tests.base import BaseTestCase -from api.v2.controllers.bouquets.bouquets_controller import Bouquets +from api.v2.controllers.bouquets.bouquets_controller import Bouquets, SendNotifications + +headers = { + 'Content-Type': 'application/json', + 'Token': 'token', + 'X-Goog-Channel-ID': 123456, + 'X-Goog-Channel-Token': 'fmdpe5wgN', + 'X-Goog-Channel-Expiration': '2019-07-12T17:15:38Z', + 'X-Goog-Resource-ID': '9ty4bejkkfvdw', + 'X-Goog-Resource-URI': 'andela.com_37343037343034@resource.calendar.google.com', + 'X-Goog-Message-Number': 1 +} class TestBouquets(BaseTestCase): def test_get_all_bouquets(self): @@ -74,3 +85,23 @@ def test_delete_bouquet_with_invalid_id(self): response = self.app_test.delete("/v2/bouquets?bouquet_id=mmm") self.assertTrue(b"The bouquet id should be an integer. Try again" in response.data) self.assertEqual(response.status_code, 400) + + def test_get_notifications_with_no_channels(self): + # Should return 404 when there is no channels + headers['X-Goog-Resource-ID'] = '5CcS2uZQikVsiOG7oeB4gx0oLrcmm' + response = self.app_test.post("/v2/notifications", headers=headers) + self.assertTrue(b"Channel not found" in response.data) + self.assertEqual(response.status_code, 404) + + def test_get_notifications_with_no_subscribers(self): + # Should return 404 when there is no subscribers + headers['X-Goog-Resource-ID'] = '9ty4bejkkfvdww' + response = self.app_test.post("/v2/notifications", headers=headers) + self.assertTrue(b"Subscribers not found" in response.data) + self.assertEqual(response.status_code, 404) + + def test_get_notifications_with_channels(self): + # Should return 200 when the notification has been sent + response = self.app_test.post("/v2/notifications", headers=headers) + self.assertTrue(b"Notifications sent" in response.data) + self.assertEqual(response.status_code, 200)