From 0820432853b1756c52c4739346c655dad4117a81 Mon Sep 17 00:00:00 2001 From: "[Arusey]" <[kevinkibetw7@gmail.com]> Date: Thu, 20 Jun 2019 12:53:22 +0300 Subject: [PATCH 1/8] CON-72-story(admin-notifications): setup admin notifications - setup folder structure for implementing notifications structure - implement notification when admin has not been seen in a while [Delivers CON-72] --- app.py | 2 + notifications/__init__.py | 0 notifications/helpers/__init__.py | 0 requirements.txt | 75 +++++++++++++++++++++++++------ 4 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 notifications/__init__.py create mode 100644 notifications/helpers/__init__.py diff --git a/app.py b/app.py index f6ef5faea..9f8498055 100644 --- a/app.py +++ b/app.py @@ -3,6 +3,7 @@ from flask_graphql import GraphQLView from flask_cors import CORS from flask_json import FlaskJSON +from flask_socketio import SocketIO from flask_mail import Mail from config import config @@ -19,6 +20,7 @@ def create_app(config_name): app = Flask(__name__) CORS(app) FlaskJSON(app) + SocketIO(app) app.config.from_object(config[config_name]) config[config_name].init_app(app) mail.init_app(app) diff --git a/notifications/__init__.py b/notifications/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/notifications/helpers/__init__.py b/notifications/helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/requirements.txt b/requirements.txt index e27578af6..e510aad0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,39 +1,86 @@ alembic==0.9.8 -bugsnag==3.4.2 +amqp==1.4.9 +aniso8601==3.0.2 +anyjson==0.3.3 +astroid==2.2.5 +attrs==19.1.0 +autopep8==1.4.4 +billiard==3.3.0.23 blinker==1.4 +bugsnag==3.4.2 celery==3.1.17 +certifi==2019.3.9 +chardet==3.0.4 +Click==7.0 coverage==4.5.1 +coveralls==1.8.0 +decorator==4.4.0 +docopt==0.6.2 +flake8==3.5.0 Flask==0.12.2 Flask-Cors==3.0.4 -Flask-JSON==0.3.2 -Flask-Script==2.0.6 Flask-GraphQL==1.4.1 +Flask-JSON==0.3.2 Flask-Mail==0.9.1 +Flask-Script==2.0.6 +Flask-SocketIO==4.1.0 +Flask-Testing==0.7.1 google-api-python-client==1.6.7 -graphene-sqlalchemy==2.0.0 graphene==2.1 +graphene-sqlalchemy==2.0.0 +graphql-core==2.2 +graphql-relay==0.4.5 +httplib2==0.13.0 +idna==2.8 imgkit==1.0.1 +iso8601==0.1.12 +isort==4.3.20 +itsdangerous==1.1.0 +Jinja2==2.10.1 +kombu==3.0.37 +lazy-object-proxy==1.4.1 +Mako==1.0.12 MarkupSafe==1.0 +mccabe==0.6.1 more-itertools==4.1.0 +nose==1.3.7 numpy==1.15.2 +oauth2client==4.1.3 opencv-python==3.4.3.18 pandas==0.23.4 -Pillow==5.3.0 pdfkit==0.6.1 +Pillow==5.3.0 +pluggy==0.6.0 +promise==2.2.1 psycopg2-binary==2.7.4 py==1.5.3 +pyasn1==0.4.5 +pyasn1-modules==0.2.5 +pycodestyle==2.3.1 +pyflakes==1.6.0 +PyJWT==1.6.4 +pylint==2.3.1 pytest==3.5.0 python-dateutil==2.7.0 -PyJWT==1.6.4 -redis==2.10.3 -nose==1.3.7 python-editor==1.0.3 +python-engineio==3.8.1 +python-socketio==4.1.0 +pytz==2019.1 +redis==2.10.3 +requests==2.22.0 +rsa==4.0 +Rx==1.6.1 +singledispatch==3.4.0.3 +six==1.12.0 SQLAlchemy==1.2.6 -tox==3.0.0 SQLAlchemy-Utils==0.33.2 -virtualenv==15.2.0 -Flask-Testing==0.7.1 +tox==3.0.0 +typed-ast==1.4.0 typing==3.6.4 -flake8==3.5.0 -coveralls -validators==0.12.4 \ No newline at end of file +uritemplate==3.0.0 +urllib3==1.25.3 +validators==0.12.4 +virtualenv==15.2.0 +WebOb==1.8.5 +Werkzeug==0.15.4 +wrapt==1.11.1 From 1a6ce5e03628567e8147f4ac7cd29210835a6b80 Mon Sep 17 00:00:00 2001 From: leksyib Date: Thu, 20 Jun 2019 16:07:23 +0100 Subject: [PATCH 2/8] feat(notifications): setup notific ations - add new model for adminnotifications [Delivers CON-71] --- .coveragerc | 17 --------- .../__init__.py | 0 .../helpers/__init__.py | 0 .../helpers/create_notification.py | 24 ++++++++++++ admin_notifications/models.py | 19 ++++++++++ admin_notifications/socket_handler.py | 19 ++++++++++ alembic/env.py | 2 +- ...718cd4ed8_add_admin_notifications_table.py | 38 +++++++++++++++++++ app.py | 3 -- manage.py | 11 +++++- requirements.txt | 3 +- setup.cfg | 7 ---- utilities/utility.py | 5 +++ 13 files changed, 117 insertions(+), 31 deletions(-) delete mode 100644 .coveragerc rename {notifications => admin_notifications}/__init__.py (100%) rename {notifications => admin_notifications}/helpers/__init__.py (100%) create mode 100644 admin_notifications/helpers/create_notification.py create mode 100644 admin_notifications/models.py create mode 100644 admin_notifications/socket_handler.py create mode 100644 alembic/versions/c65718cd4ed8_add_admin_notifications_table.py delete mode 100644 setup.cfg diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 3d0cca121..000000000 --- a/.coveragerc +++ /dev/null @@ -1,17 +0,0 @@ -# .coveragerc -[run] -omit = - *__init__* - tests/* - fixtures/* - -[report] -show_missing = True -omit = - */python?.?/* - */alembic/* - -[html] -directory=html_coverage_report - - \ No newline at end of file diff --git a/notifications/__init__.py b/admin_notifications/__init__.py similarity index 100% rename from notifications/__init__.py rename to admin_notifications/__init__.py diff --git a/notifications/helpers/__init__.py b/admin_notifications/helpers/__init__.py similarity index 100% rename from notifications/helpers/__init__.py rename to admin_notifications/helpers/__init__.py diff --git a/admin_notifications/helpers/create_notification.py b/admin_notifications/helpers/create_notification.py new file mode 100644 index 000000000..0ecddd8eb --- /dev/null +++ b/admin_notifications/helpers/create_notification.py @@ -0,0 +1,24 @@ +from flask_socketio import send +from admin_notifications.models import AdminNotification +from manage import socketio + +def create_notification(title, message, location_id): + notification = AdminNotification( + title=title, + message=message, + location_id=location_id, + status="unread" + ) + notification.save() + + @socketio.on('notification') + def send_notification(): + notification = { "title": title, "message": message } + return send(notification, broadcast=True) + + + + + +def create_notif_lol(): + return create_notification(title="twre3r", message="rerf3ef3rf3rf", location_id=1) diff --git a/admin_notifications/models.py b/admin_notifications/models.py new file mode 100644 index 000000000..8485377fa --- /dev/null +++ b/admin_notifications/models.py @@ -0,0 +1,19 @@ +from sqlalchemy import (Column, String, Enum, Integer, ForeignKey) +from helpers.database import Base +from utilities.utility import Utility, StatusType + + +class AdminNotification(Base, Utility): + __tablename__ = 'admin_notifications' + + id = Column(Integer, primary_key=True) # noqa + title = Column(String, nullable=True) + message = Column(String, nullable=True) + date_received = Column(String, nullable=True) + date_read = Column(String, nullable=True) + status = Column(Enum(StatusType), default="unread") + location_id = Column( + Integer, + ForeignKey('locations.id', ondelete="CASCADE"), + nullable=True + ) diff --git a/admin_notifications/socket_handler.py b/admin_notifications/socket_handler.py new file mode 100644 index 000000000..25d07d8c3 --- /dev/null +++ b/admin_notifications/socket_handler.py @@ -0,0 +1,19 @@ +from flask_socketio import send +from admin_notifications.models import AdminNotification +from flask import jsonify + +def serialize_message(notification): + return { + "title": notification.title, + "message": notification.message, + } + +def send_notifications(): + query = AdminNotification.query + notifications = query.filter_by(status="unread").all() + notifications = [serialize_message(notification) for notification in notifications] + return send(notifications, broadcast=True) + +def send_single_notification(title, message): + notification = { "title": title, "message": message } + return send(notification, broadcast=True) diff --git a/alembic/env.py b/alembic/env.py index 01ce12db5..5a0334069 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -45,7 +45,7 @@ from api.response.models import Response from api.tag.models import Tag from api.structure.models import Structure - +from admin_notifications.models import AdminNotification target_metadata = Base.metadata diff --git a/alembic/versions/c65718cd4ed8_add_admin_notifications_table.py b/alembic/versions/c65718cd4ed8_add_admin_notifications_table.py new file mode 100644 index 000000000..51bc9944c --- /dev/null +++ b/alembic/versions/c65718cd4ed8_add_admin_notifications_table.py @@ -0,0 +1,38 @@ +"""add admin notifications table + +Revision ID: c65718cd4ed8 +Revises: a5a4841351d7 +Create Date: 2019-06-21 08:38:07.110402 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c65718cd4ed8' +down_revision = 'a5a4841351d7' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('admin_notifications', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(), nullable=True), + sa.Column('message', sa.String(), nullable=True), + sa.Column('date_received', sa.String(), nullable=True), + sa.Column('date_read', sa.String(), nullable=True), + sa.Column('status', sa.Enum('read', 'unread', name='statustype'), nullable=True), + sa.Column('location_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['location_id'], ['locations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('admin_notifications') + # ### end Alembic commands ### diff --git a/app.py b/app.py index 9f8498055..6ace0096c 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,6 @@ from flask_graphql import GraphQLView from flask_cors import CORS from flask_json import FlaskJSON -from flask_socketio import SocketIO from flask_mail import Mail from config import config @@ -12,7 +11,6 @@ from healthcheck_schema import healthcheck_schema from helpers.auth.authentication import Auth from api.analytics.analytics_request import AnalyticsRequest - mail = Mail() @@ -20,7 +18,6 @@ def create_app(config_name): app = Flask(__name__) CORS(app) FlaskJSON(app) - SocketIO(app) app.config.from_object(config[config_name]) config[config_name].init_app(app) mail.init_app(app) diff --git a/manage.py b/manage.py index a9e7943d6..39891de9f 100644 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import bugsnag from flask_script import Manager, Shell from bugsnag.flask import handle_exceptions - +from flask_socketio import SocketIO # Configure bugnsag bugsnag.configure( @@ -18,16 +18,23 @@ app = create_app(os.getenv('APP_SETTINGS') or 'default') handle_exceptions(app) manager = Manager(app) +socketio = SocketIO(app) def make_shell_context(): return dict(app=app) +@socketio.on('message') +def hanldleMessage(msg): + from admin_notifications.socket_handler import send_notifications + return send_notifications() + + manager.add_command( "shell", Shell( make_context=make_shell_context)) - if __name__ == '__main__': + socketio.run(app, debug=True) manager.run() diff --git a/requirements.txt b/requirements.txt index e510aad0d..8ade2b2f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,7 @@ coverage==4.5.1 coveralls==1.8.0 decorator==4.4.0 docopt==0.6.2 +eventlet==0.25.0 flake8==3.5.0 Flask==0.12.2 Flask-Cors==3.0.4 @@ -23,7 +24,7 @@ Flask-GraphQL==1.4.1 Flask-JSON==0.3.2 Flask-Mail==0.9.1 Flask-Script==2.0.6 -Flask-SocketIO==4.1.0 +flask-socketio==4.1.0 Flask-Testing==0.7.1 google-api-python-client==1.6.7 graphene==2.1 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 877d1b6ed..000000000 --- a/setup.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[tool:pytest] -testpaths = tests - -[coverage:run] -branch = True -source = - mrm_api/ \ No newline at end of file diff --git a/utilities/utility.py b/utilities/utility.py index 1064f65cc..19f014faf 100644 --- a/utilities/utility.py +++ b/utilities/utility.py @@ -86,3 +86,8 @@ class QuestionType(enum.Enum): rate = "rate" check = "check" textarea = "textarea" + missingitem = "missing_items" + +class StatusType(enum.Enum): + read = "read" + unread = "unread" From 599bd44ba1fa980c500930d54ce970f3be97fee3 Mon Sep 17 00:00:00 2001 From: leksyib Date: Thu, 20 Jun 2019 16:07:23 +0100 Subject: [PATCH 3/8] feat(notifications): setup notific ations - add new model for adminnotifications [Delivers CON-71] --- admin_notifications/models.py | 3 +++ utilities/utility.py | 1 + 2 files changed, 4 insertions(+) diff --git a/admin_notifications/models.py b/admin_notifications/models.py index 8485377fa..e96940516 100644 --- a/admin_notifications/models.py +++ b/admin_notifications/models.py @@ -1,4 +1,6 @@ from sqlalchemy import (Column, String, Enum, Integer, ForeignKey) +from sqlalchemy.schema import Sequence +from utilities.validations import validate_empty_fields from helpers.database import Base from utilities.utility import Utility, StatusType @@ -17,3 +19,4 @@ class AdminNotification(Base, Utility): ForeignKey('locations.id', ondelete="CASCADE"), nullable=True ) + \ No newline at end of file diff --git a/utilities/utility.py b/utilities/utility.py index 19f014faf..a5b51013d 100644 --- a/utilities/utility.py +++ b/utilities/utility.py @@ -88,6 +88,7 @@ class QuestionType(enum.Enum): textarea = "textarea" missingitem = "missing_items" + class StatusType(enum.Enum): read = "read" unread = "unread" From 8980de400ec87c32f8f1da8822bddce83bc4bebe Mon Sep 17 00:00:00 2001 From: "[Arusey]" <[kevinkibetw7@gmail.com]> Date: Fri, 21 Jun 2019 15:08:31 +0300 Subject: [PATCH 4/8] CON-72-story(notifications): admin receives notification when device is not seen in a while - setup queue manager for when notifications are sent - implement notifications when device is not seen in a while [Delivers CON-72] --- .../helpers/device_last_seen.py | 19 ++++++ admin_notifications/helpers/queue_manager.py | 7 ++ admin_notifications/models.py | 6 +- ...cf_add_activity_column_to_devices_table.py | 34 ++++++++++ api/devices/models.py | 7 +- cworker.py | 18 ++++- helpers/email/email_setup.py | 2 +- requirements.txt | 67 ++++--------------- utilities/utility.py | 6 ++ 9 files changed, 99 insertions(+), 67 deletions(-) create mode 100644 admin_notifications/helpers/device_last_seen.py create mode 100644 admin_notifications/helpers/queue_manager.py create mode 100644 alembic/versions/1b53553ddacf_add_activity_column_to_devices_table.py diff --git a/admin_notifications/helpers/device_last_seen.py b/admin_notifications/helpers/device_last_seen.py new file mode 100644 index 000000000..af951e7ee --- /dev/null +++ b/admin_notifications/helpers/device_last_seen.py @@ -0,0 +1,19 @@ +from datetime import datetime +from api.devices.models import Devices as DevicesModel +from utilities.utility import update_entity_fields +import celery + + +@celery.task(name='check-device-last-seen') +def notify_when_device_is_offline(**kwargs): + query = DevicesModel.query + active_devices = query.filter(DevicesModel.activity == "online").all() + for device in active_devices: + device_last_seen = device.last_seen + current_time = datetime.now() + duration_offline = current_time - device_last_seen + + if duration_offline.days > 1: + update_entity_fields(device, activity="offline") + device.save() + return active_devices diff --git a/admin_notifications/helpers/queue_manager.py b/admin_notifications/helpers/queue_manager.py new file mode 100644 index 000000000..e9397e5ef --- /dev/null +++ b/admin_notifications/helpers/queue_manager.py @@ -0,0 +1,7 @@ +from datetime import timedelta +beat_schedule = { + 'run-check-device-last-seen-hourly': { + 'task': 'check-device-last-seen', + 'schedule': timedelta(seconds=5) + } +} diff --git a/admin_notifications/models.py b/admin_notifications/models.py index e96940516..71e5eef86 100644 --- a/admin_notifications/models.py +++ b/admin_notifications/models.py @@ -1,6 +1,4 @@ from sqlalchemy import (Column, String, Enum, Integer, ForeignKey) -from sqlalchemy.schema import Sequence -from utilities.validations import validate_empty_fields from helpers.database import Base from utilities.utility import Utility, StatusType @@ -17,6 +15,4 @@ class AdminNotification(Base, Utility): location_id = Column( Integer, ForeignKey('locations.id', ondelete="CASCADE"), - nullable=True - ) - \ No newline at end of file + nullable=True) diff --git a/alembic/versions/1b53553ddacf_add_activity_column_to_devices_table.py b/alembic/versions/1b53553ddacf_add_activity_column_to_devices_table.py new file mode 100644 index 000000000..f99895fc3 --- /dev/null +++ b/alembic/versions/1b53553ddacf_add_activity_column_to_devices_table.py @@ -0,0 +1,34 @@ +"""add activity column to devices table + +Revision ID: 1b53553ddacf +Revises: c65718cd4ed8 +Create Date: 2019-06-22 12:59:24.591339 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision = '1b53553ddacf' +down_revision = 'c65718cd4ed8' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + activitytype = postgresql.ENUM('online', 'offline', name='activitytype') + activitytype.create(op.get_bind()) + op.add_column('devices', sa.Column('activity', sa.Enum( + 'online', 'offline', name='activitytype'), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('devices', 'activity') + op.execute("DROP TYPE activitytype;") + # ### end Alembic commands ### diff --git a/api/devices/models.py b/api/devices/models.py index 78a225e9f..84bd892b1 100644 --- a/api/devices/models.py +++ b/api/devices/models.py @@ -1,15 +1,15 @@ -from sqlalchemy import (Column, String, Integer, DateTime, ForeignKey) +from sqlalchemy import (Column, String, Integer, DateTime, ForeignKey, Enum) from sqlalchemy.schema import Sequence from sqlalchemy.orm import relationship from helpers.database import Base from utilities.validations import validate_empty_fields -from utilities.utility import Utility +from utilities.utility import Utility, ActivityType class Devices(Base, Utility): __tablename__ = 'devices' - id = Column(Integer, Sequence('devices_id_seq', start=1, increment=1), primary_key=True) # noqa + id = Column(Integer, Sequence('devices_id_seq', start=1, increment=1), primary_key=True) # noqa name = Column(String, nullable=False) device_type = Column(String, nullable=False) date_added = Column(DateTime, nullable=False) @@ -17,6 +17,7 @@ class Devices(Base, Utility): location = Column(String, nullable=False) room_id = Column(Integer, ForeignKey('rooms.id', ondelete="CASCADE")) room = relationship('Room') + activity = Column(Enum(ActivityType), default="active") def __init__(self, **kwargs): validate_empty_fields(**kwargs) diff --git a/cworker.py b/cworker.py index ad6695d1a..200ad389c 100644 --- a/cworker.py +++ b/cworker.py @@ -1,16 +1,26 @@ import os - from celery import Celery from app import create_app - +from admin_notifications.helpers.queue_manager import beat_schedule app = create_app(os.getenv('APP_SETTINGS') or 'default') app.app_context().push() +app.config.update( + CELERY_BROKER_URL='redis://localhost:6379', + CELERY_RESULT_BACKEND='redis://localhost:6379', + CELERY_ACCEPT_CONTENT=['pickle'], + CELERYBEAT_SCHEDULE=beat_schedule +) + + def make_celery(app): - celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL']) + celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'], include=['admin_notifications.helpers.device_last_seen'], # noqa 501 + backend=app.config['CELERY_BROKER_URL']) + celery.conf.update(app.config) + celery.conf.enable_utc = False TaskBase = celery.Task class ContextTask(TaskBase): @@ -24,3 +34,5 @@ def __call__(self, *args, **kwargs): celery = make_celery(app) +celery_scheduler = Celery(app.name, broker=app.config['CELERY_BROKER_URL']) +celery_scheduler.conf.update(app.config) diff --git a/helpers/email/email_setup.py b/helpers/email/email_setup.py index 236499f23..82ff25ad1 100644 --- a/helpers/email/email_setup.py +++ b/helpers/email/email_setup.py @@ -30,7 +30,7 @@ def __init__( html=self.template, sender=self.sender) - @celery.task + @celery.task(name='asynchronous-email-notifications') def send_async_email(msg_dict): mail = Mail() msg = Message() diff --git a/requirements.txt b/requirements.txt index 8ade2b2f4..61e49d488 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,7 @@ alembic==0.9.8 -amqp==1.4.9 -aniso8601==3.0.2 -anyjson==0.3.3 -astroid==2.2.5 -attrs==19.1.0 -autopep8==1.4.4 -billiard==3.3.0.23 -blinker==1.4 bugsnag==3.4.2 +blinker==1.4 celery==3.1.17 -certifi==2019.3.9 -chardet==3.0.4 -Click==7.0 coverage==4.5.1 coveralls==1.8.0 decorator==4.4.0 @@ -20,68 +10,35 @@ eventlet==0.25.0 flake8==3.5.0 Flask==0.12.2 Flask-Cors==3.0.4 -Flask-GraphQL==1.4.1 Flask-JSON==0.3.2 -Flask-Mail==0.9.1 Flask-Script==2.0.6 flask-socketio==4.1.0 Flask-Testing==0.7.1 google-api-python-client==1.6.7 -graphene==2.1 graphene-sqlalchemy==2.0.0 -graphql-core==2.2 -graphql-relay==0.4.5 -httplib2==0.13.0 -idna==2.8 +graphene==2.1 imgkit==1.0.1 -iso8601==0.1.12 -isort==4.3.20 -itsdangerous==1.1.0 -Jinja2==2.10.1 -kombu==3.0.37 -lazy-object-proxy==1.4.1 -Mako==1.0.12 MarkupSafe==1.0 -mccabe==0.6.1 more-itertools==4.1.0 -nose==1.3.7 numpy==1.15.2 -oauth2client==4.1.3 opencv-python==3.4.3.18 pandas==0.23.4 -pdfkit==0.6.1 Pillow==5.3.0 -pluggy==0.6.0 -promise==2.2.1 +pdfkit==0.6.1 psycopg2-binary==2.7.4 py==1.5.3 -pyasn1==0.4.5 -pyasn1-modules==0.2.5 -pycodestyle==2.3.1 -pyflakes==1.6.0 -PyJWT==1.6.4 -pylint==2.3.1 pytest==3.5.0 python-dateutil==2.7.0 -python-editor==1.0.3 -python-engineio==3.8.1 -python-socketio==4.1.0 -pytz==2019.1 +PyJWT==1.6.4 redis==2.10.3 -requests==2.22.0 -rsa==4.0 -Rx==1.6.1 -singledispatch==3.4.0.3 -six==1.12.0 +nose==1.3.7 +python-editor==1.0.3 SQLAlchemy==1.2.6 -SQLAlchemy-Utils==0.33.2 tox==3.0.0 -typed-ast==1.4.0 -typing==3.6.4 -uritemplate==3.0.0 -urllib3==1.25.3 -validators==0.12.4 +SQLAlchemy-Utils==0.33.2 virtualenv==15.2.0 -WebOb==1.8.5 -Werkzeug==0.15.4 -wrapt==1.11.1 +Flask-Testing==0.7.1 +typing==3.6.4 +flake8==3.5.0 +coveralls +validators==0.12.4 \ No newline at end of file diff --git a/utilities/utility.py b/utilities/utility.py index a5b51013d..99fbd3455 100644 --- a/utilities/utility.py +++ b/utilities/utility.py @@ -89,6 +89,12 @@ class QuestionType(enum.Enum): missingitem = "missing_items" + class StatusType(enum.Enum): read = "read" unread = "unread" + + +class ActivityType(enum.Enum): + online = "online" + offline = "offline" From c23c551ec36611c9c3e3448c3e5b8210afb71005 Mon Sep 17 00:00:00 2001 From: leksyib Date: Thu, 20 Jun 2019 16:07:23 +0100 Subject: [PATCH 5/8] feat(notifications): setup notific ations - add new model for adminnotifications [Delivers CON-71] --- admin_notifications/models.py | 22 ++++++++++++++++++++++ utilities/utility.py | 5 +++++ 2 files changed, 27 insertions(+) create mode 100644 admin_notifications/models.py diff --git a/admin_notifications/models.py b/admin_notifications/models.py new file mode 100644 index 000000000..e96940516 --- /dev/null +++ b/admin_notifications/models.py @@ -0,0 +1,22 @@ +from sqlalchemy import (Column, String, Enum, Integer, ForeignKey) +from sqlalchemy.schema import Sequence +from utilities.validations import validate_empty_fields +from helpers.database import Base +from utilities.utility import Utility, StatusType + + +class AdminNotification(Base, Utility): + __tablename__ = 'admin_notifications' + + id = Column(Integer, primary_key=True) # noqa + title = Column(String, nullable=True) + message = Column(String, nullable=True) + date_received = Column(String, nullable=True) + date_read = Column(String, nullable=True) + status = Column(Enum(StatusType), default="unread") + location_id = Column( + Integer, + ForeignKey('locations.id', ondelete="CASCADE"), + nullable=True + ) + \ No newline at end of file diff --git a/utilities/utility.py b/utilities/utility.py index 6fa044428..a5b51013d 100644 --- a/utilities/utility.py +++ b/utilities/utility.py @@ -87,3 +87,8 @@ class QuestionType(enum.Enum): check = "check" textarea = "textarea" missingitem = "missing_items" + + +class StatusType(enum.Enum): + read = "read" + unread = "unread" From 1bb2408b3684f6807e64df77d1f1c0835be4e161 Mon Sep 17 00:00:00 2001 From: "[Arusey]" <[kevinkibetw7@gmail.com]> Date: Fri, 21 Jun 2019 15:08:31 +0300 Subject: [PATCH 6/8] CON-72-story(notifications): admin receives notification when device is not seen in a while - setup queue manager for when notifications are sent - implement notifications when device is not seen in a while [Delivers CON-72] --- .../helpers/create_notification.py | 17 ++++++++++ .../helpers/device_last_seen.py | 26 ++++++++++++++ admin_notifications/helpers/queue_manager.py | 7 ++++ admin_notifications/models.py | 6 +--- ...cf_add_activity_column_to_devices_table.py | 34 +++++++++++++++++++ api/devices/models.py | 7 ++-- cworker.py | 18 ++++++++-- helpers/email/email_setup.py | 2 +- utilities/utility.py | 6 ++++ 9 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 admin_notifications/helpers/create_notification.py create mode 100644 admin_notifications/helpers/device_last_seen.py create mode 100644 admin_notifications/helpers/queue_manager.py create mode 100644 alembic/versions/1b53553ddacf_add_activity_column_to_devices_table.py diff --git a/admin_notifications/helpers/create_notification.py b/admin_notifications/helpers/create_notification.py new file mode 100644 index 000000000..e7e64af2b --- /dev/null +++ b/admin_notifications/helpers/create_notification.py @@ -0,0 +1,17 @@ +from flask_socketio import send, emit +from admin_notifications.models import AdminNotification +from manage import socketio +import celery + + +@celery.task(name="create-notification") +def create_notification(title, message, location_id): + notification = AdminNotification( + title=title, + message=message, + location_id=location_id, + status="unread" + ) + notification.save() + new_notification = {"title": title, "message": message} + return socketio.emit('notification', {'notification': new_notification}, broadcast=True) diff --git a/admin_notifications/helpers/device_last_seen.py b/admin_notifications/helpers/device_last_seen.py new file mode 100644 index 000000000..ccaff0bad --- /dev/null +++ b/admin_notifications/helpers/device_last_seen.py @@ -0,0 +1,26 @@ +from datetime import datetime +from api.devices.models import Devices as DevicesModel +from utilities.utility import update_entity_fields +import celery + + +@celery.task(name='check-device-last-seen') +def notify_when_device_is_offline(**kwargs): + query = DevicesModel.query + online_devices = query.filter(DevicesModel.activity == "online").all() + for device in online_devices: + device_last_seen = device.last_seen + current_time = datetime.now() + duration_offline = current_time - device_last_seen + + if duration_offline.days > 1: + update_entity_fields(device, activity="offline") + device.save() + return online_devices + + +def notify(**kwargs): + query = DevicesModel.query + offline_device = query.filter(DevicesModel.activity == "offline").first() + if offline_device: + pass \ No newline at end of file diff --git a/admin_notifications/helpers/queue_manager.py b/admin_notifications/helpers/queue_manager.py new file mode 100644 index 000000000..e9397e5ef --- /dev/null +++ b/admin_notifications/helpers/queue_manager.py @@ -0,0 +1,7 @@ +from datetime import timedelta +beat_schedule = { + 'run-check-device-last-seen-hourly': { + 'task': 'check-device-last-seen', + 'schedule': timedelta(seconds=5) + } +} diff --git a/admin_notifications/models.py b/admin_notifications/models.py index e96940516..71e5eef86 100644 --- a/admin_notifications/models.py +++ b/admin_notifications/models.py @@ -1,6 +1,4 @@ from sqlalchemy import (Column, String, Enum, Integer, ForeignKey) -from sqlalchemy.schema import Sequence -from utilities.validations import validate_empty_fields from helpers.database import Base from utilities.utility import Utility, StatusType @@ -17,6 +15,4 @@ class AdminNotification(Base, Utility): location_id = Column( Integer, ForeignKey('locations.id', ondelete="CASCADE"), - nullable=True - ) - \ No newline at end of file + nullable=True) diff --git a/alembic/versions/1b53553ddacf_add_activity_column_to_devices_table.py b/alembic/versions/1b53553ddacf_add_activity_column_to_devices_table.py new file mode 100644 index 000000000..f99895fc3 --- /dev/null +++ b/alembic/versions/1b53553ddacf_add_activity_column_to_devices_table.py @@ -0,0 +1,34 @@ +"""add activity column to devices table + +Revision ID: 1b53553ddacf +Revises: c65718cd4ed8 +Create Date: 2019-06-22 12:59:24.591339 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision = '1b53553ddacf' +down_revision = 'c65718cd4ed8' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + activitytype = postgresql.ENUM('online', 'offline', name='activitytype') + activitytype.create(op.get_bind()) + op.add_column('devices', sa.Column('activity', sa.Enum( + 'online', 'offline', name='activitytype'), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('devices', 'activity') + op.execute("DROP TYPE activitytype;") + # ### end Alembic commands ### diff --git a/api/devices/models.py b/api/devices/models.py index 78a225e9f..84bd892b1 100644 --- a/api/devices/models.py +++ b/api/devices/models.py @@ -1,15 +1,15 @@ -from sqlalchemy import (Column, String, Integer, DateTime, ForeignKey) +from sqlalchemy import (Column, String, Integer, DateTime, ForeignKey, Enum) from sqlalchemy.schema import Sequence from sqlalchemy.orm import relationship from helpers.database import Base from utilities.validations import validate_empty_fields -from utilities.utility import Utility +from utilities.utility import Utility, ActivityType class Devices(Base, Utility): __tablename__ = 'devices' - id = Column(Integer, Sequence('devices_id_seq', start=1, increment=1), primary_key=True) # noqa + id = Column(Integer, Sequence('devices_id_seq', start=1, increment=1), primary_key=True) # noqa name = Column(String, nullable=False) device_type = Column(String, nullable=False) date_added = Column(DateTime, nullable=False) @@ -17,6 +17,7 @@ class Devices(Base, Utility): location = Column(String, nullable=False) room_id = Column(Integer, ForeignKey('rooms.id', ondelete="CASCADE")) room = relationship('Room') + activity = Column(Enum(ActivityType), default="active") def __init__(self, **kwargs): validate_empty_fields(**kwargs) diff --git a/cworker.py b/cworker.py index ad6695d1a..c6dab9e1f 100644 --- a/cworker.py +++ b/cworker.py @@ -1,16 +1,26 @@ import os - from celery import Celery from app import create_app - +from admin_notifications.helpers.queue_manager import beat_schedule app = create_app(os.getenv('APP_SETTINGS') or 'default') app.app_context().push() +app.config.update( + CELERY_BROKER_URL='redis://localhost:6379', + CELERY_RESULT_BACKEND='redis://localhost:6379', + CELERY_ACCEPT_CONTENT=['pickle'], + CELERYBEAT_SCHEDULE=beat_schedule +) + + def make_celery(app): - celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL']) + celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'], include=['admin_notifications.helpers.device_last_seen', 'admin_notifications.helpers.create_notification'], # noqa 501 + backend=app.config['CELERY_BROKER_URL']) + celery.conf.update(app.config) + celery.conf.enable_utc = False TaskBase = celery.Task class ContextTask(TaskBase): @@ -24,3 +34,5 @@ def __call__(self, *args, **kwargs): celery = make_celery(app) +celery_scheduler = Celery(app.name, broker=app.config['CELERY_BROKER_URL']) +celery_scheduler.conf.update(app.config) diff --git a/helpers/email/email_setup.py b/helpers/email/email_setup.py index 236499f23..82ff25ad1 100644 --- a/helpers/email/email_setup.py +++ b/helpers/email/email_setup.py @@ -30,7 +30,7 @@ def __init__( html=self.template, sender=self.sender) - @celery.task + @celery.task(name='asynchronous-email-notifications') def send_async_email(msg_dict): mail = Mail() msg = Message() diff --git a/utilities/utility.py b/utilities/utility.py index a5b51013d..99fbd3455 100644 --- a/utilities/utility.py +++ b/utilities/utility.py @@ -89,6 +89,12 @@ class QuestionType(enum.Enum): missingitem = "missing_items" + class StatusType(enum.Enum): read = "read" unread = "unread" + + +class ActivityType(enum.Enum): + online = "online" + offline = "offline" From e5de19ada1fd9d780e8914d297381bcc543cb7cd Mon Sep 17 00:00:00 2001 From: "[Arusey]" <[kevinkibetw7@gmail.com]> Date: Thu, 20 Jun 2019 12:53:22 +0300 Subject: [PATCH 7/8] CON-72-story(admin-notifications): setup admin notifications - setup folder structure for implementing notifications structure - implement notification when admin has not been seen in a while [Delivers CON-72] --- app.py | 2 + notifications/__init__.py | 0 notifications/helpers/__init__.py | 0 requirements.txt | 75 +++++++++++++++++++++++++------ 4 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 notifications/__init__.py create mode 100644 notifications/helpers/__init__.py diff --git a/app.py b/app.py index f6ef5faea..9f8498055 100644 --- a/app.py +++ b/app.py @@ -3,6 +3,7 @@ from flask_graphql import GraphQLView from flask_cors import CORS from flask_json import FlaskJSON +from flask_socketio import SocketIO from flask_mail import Mail from config import config @@ -19,6 +20,7 @@ def create_app(config_name): app = Flask(__name__) CORS(app) FlaskJSON(app) + SocketIO(app) app.config.from_object(config[config_name]) config[config_name].init_app(app) mail.init_app(app) diff --git a/notifications/__init__.py b/notifications/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/notifications/helpers/__init__.py b/notifications/helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/requirements.txt b/requirements.txt index e27578af6..e510aad0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,39 +1,86 @@ alembic==0.9.8 -bugsnag==3.4.2 +amqp==1.4.9 +aniso8601==3.0.2 +anyjson==0.3.3 +astroid==2.2.5 +attrs==19.1.0 +autopep8==1.4.4 +billiard==3.3.0.23 blinker==1.4 +bugsnag==3.4.2 celery==3.1.17 +certifi==2019.3.9 +chardet==3.0.4 +Click==7.0 coverage==4.5.1 +coveralls==1.8.0 +decorator==4.4.0 +docopt==0.6.2 +flake8==3.5.0 Flask==0.12.2 Flask-Cors==3.0.4 -Flask-JSON==0.3.2 -Flask-Script==2.0.6 Flask-GraphQL==1.4.1 +Flask-JSON==0.3.2 Flask-Mail==0.9.1 +Flask-Script==2.0.6 +Flask-SocketIO==4.1.0 +Flask-Testing==0.7.1 google-api-python-client==1.6.7 -graphene-sqlalchemy==2.0.0 graphene==2.1 +graphene-sqlalchemy==2.0.0 +graphql-core==2.2 +graphql-relay==0.4.5 +httplib2==0.13.0 +idna==2.8 imgkit==1.0.1 +iso8601==0.1.12 +isort==4.3.20 +itsdangerous==1.1.0 +Jinja2==2.10.1 +kombu==3.0.37 +lazy-object-proxy==1.4.1 +Mako==1.0.12 MarkupSafe==1.0 +mccabe==0.6.1 more-itertools==4.1.0 +nose==1.3.7 numpy==1.15.2 +oauth2client==4.1.3 opencv-python==3.4.3.18 pandas==0.23.4 -Pillow==5.3.0 pdfkit==0.6.1 +Pillow==5.3.0 +pluggy==0.6.0 +promise==2.2.1 psycopg2-binary==2.7.4 py==1.5.3 +pyasn1==0.4.5 +pyasn1-modules==0.2.5 +pycodestyle==2.3.1 +pyflakes==1.6.0 +PyJWT==1.6.4 +pylint==2.3.1 pytest==3.5.0 python-dateutil==2.7.0 -PyJWT==1.6.4 -redis==2.10.3 -nose==1.3.7 python-editor==1.0.3 +python-engineio==3.8.1 +python-socketio==4.1.0 +pytz==2019.1 +redis==2.10.3 +requests==2.22.0 +rsa==4.0 +Rx==1.6.1 +singledispatch==3.4.0.3 +six==1.12.0 SQLAlchemy==1.2.6 -tox==3.0.0 SQLAlchemy-Utils==0.33.2 -virtualenv==15.2.0 -Flask-Testing==0.7.1 +tox==3.0.0 +typed-ast==1.4.0 typing==3.6.4 -flake8==3.5.0 -coveralls -validators==0.12.4 \ No newline at end of file +uritemplate==3.0.0 +urllib3==1.25.3 +validators==0.12.4 +virtualenv==15.2.0 +WebOb==1.8.5 +Werkzeug==0.15.4 +wrapt==1.11.1 From eb9b082b115d77fa06c4e776e444491bafca58c8 Mon Sep 17 00:00:00 2001 From: leksyib Date: Thu, 20 Jun 2019 16:07:23 +0100 Subject: [PATCH 8/8] feat(notifications): send notifications to admins when an event is autocancelled - add new model for adminnotifications - setup socketio for real time notifications - add query to get all notifications - add mutation to change the status of notification [Delivers CON-71] --- .circleci/config.yml | 2 +- .codeclimate.yml | 5 +- .coveragerc | 17 ----- .../__init__.py | 0 .../helpers/__init__.py | 0 .../helpers/create_notification.py | 25 +++++-- .../helpers/device_last_seen.py | 26 ------- .../helpers/notification_templates.py | 6 ++ admin_notifications/helpers/queue_manager.py | 7 -- admin_notifications/models.py | 3 +- admin_notifications/schema.py | 67 +++++++++++++++++++ alembic/env.py | 2 +- ...cf_add_activity_column_to_devices_table.py | 34 ---------- ...73fb8edb3_add_admin_notifications_table.py | 39 +++++++++++ api/devices/models.py | 5 +- api/events/schema.py | 12 ++++ app.py | 3 - cworker.py | 7 +- fixtures/admin_notification/__init__.py | 0 .../admin_notification_fixtures.py | 53 +++++++++++++++ manage.py | 5 +- requirements.txt | 3 +- schema.py | 3 + setup.cfg | 7 -- tests/test_admin_notification/__init__.py | 0 .../test_admin_notification.py | 23 +++++++ utilities/utility.py | 6 -- 27 files changed, 242 insertions(+), 118 deletions(-) delete mode 100644 .coveragerc rename {notifications => admin_notifications}/__init__.py (100%) rename {notifications => admin_notifications}/helpers/__init__.py (100%) delete mode 100644 admin_notifications/helpers/device_last_seen.py create mode 100644 admin_notifications/helpers/notification_templates.py delete mode 100644 admin_notifications/helpers/queue_manager.py create mode 100644 admin_notifications/schema.py delete mode 100644 alembic/versions/1b53553ddacf_add_activity_column_to_devices_table.py create mode 100644 alembic/versions/f0873fb8edb3_add_admin_notifications_table.py create mode 100644 fixtures/admin_notification/__init__.py create mode 100644 fixtures/admin_notification/admin_notification_fixtures.py delete mode 100644 setup.cfg create mode 100644 tests/test_admin_notification/__init__.py create mode 100644 tests/test_admin_notification/test_admin_notification.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 0e4a4b37a..77b938523 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -191,7 +191,7 @@ jobs: ./cc-test-reporter before-build . venv/bin/activate coverage combine parallel-coverage/ - coverage xml + coverage xml -i coverage report ./cc-test-reporter format-coverage -o ./.coverage -t coverage.py ./cc-test-reporter upload-coverage -i .coverage diff --git a/.codeclimate.yml b/.codeclimate.yml index d0ded27fc..7ae6e6674 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -2,4 +2,7 @@ version: "2" exclude_patterns: - "helpers/auth/authentication.py" - "helpers/calendar/events.py" - - "alembic/" \ No newline at end of file + - "alembic/" + - "admin_notifications/__init__.py" + - "admin_notifications/helpers/__init__.py" + - "admin_notifications/helpers/notification_templates.py" diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 3d0cca121..000000000 --- a/.coveragerc +++ /dev/null @@ -1,17 +0,0 @@ -# .coveragerc -[run] -omit = - *__init__* - tests/* - fixtures/* - -[report] -show_missing = True -omit = - */python?.?/* - */alembic/* - -[html] -directory=html_coverage_report - - \ No newline at end of file diff --git a/notifications/__init__.py b/admin_notifications/__init__.py similarity index 100% rename from notifications/__init__.py rename to admin_notifications/__init__.py diff --git a/notifications/helpers/__init__.py b/admin_notifications/helpers/__init__.py similarity index 100% rename from notifications/helpers/__init__.py rename to admin_notifications/helpers/__init__.py diff --git a/admin_notifications/helpers/create_notification.py b/admin_notifications/helpers/create_notification.py index e7e64af2b..67a965eaf 100644 --- a/admin_notifications/helpers/create_notification.py +++ b/admin_notifications/helpers/create_notification.py @@ -1,11 +1,23 @@ -from flask_socketio import send, emit from admin_notifications.models import AdminNotification -from manage import socketio +from api.location.models import Location +from datetime import datetime import celery -@celery.task(name="create-notification") +def update_notification(notification_id): + notification = AdminNotification.query.filter_by(id=notification_id).first() + notification.date_received = datetime.now() + notification.save() + + +@celery.task def create_notification(title, message, location_id): + """ + Create notifications in the database and emit them to the client + """ + from manage import socketio + location = Location.query.filter_by(id=location_id).first() + location_name = location.name notification = AdminNotification( title=title, message=message, @@ -14,4 +26,9 @@ def create_notification(title, message, location_id): ) notification.save() new_notification = {"title": title, "message": message} - return socketio.emit('notification', {'notification': new_notification}, broadcast=True) + return socketio.emit( + f"notifications-{location_name}", + {'notification': new_notification}, + broadcast=True, + callback=update_notification(notification.id) + ) diff --git a/admin_notifications/helpers/device_last_seen.py b/admin_notifications/helpers/device_last_seen.py deleted file mode 100644 index ccaff0bad..000000000 --- a/admin_notifications/helpers/device_last_seen.py +++ /dev/null @@ -1,26 +0,0 @@ -from datetime import datetime -from api.devices.models import Devices as DevicesModel -from utilities.utility import update_entity_fields -import celery - - -@celery.task(name='check-device-last-seen') -def notify_when_device_is_offline(**kwargs): - query = DevicesModel.query - online_devices = query.filter(DevicesModel.activity == "online").all() - for device in online_devices: - device_last_seen = device.last_seen - current_time = datetime.now() - duration_offline = current_time - device_last_seen - - if duration_offline.days > 1: - update_entity_fields(device, activity="offline") - device.save() - return online_devices - - -def notify(**kwargs): - query = DevicesModel.query - offline_device = query.filter(DevicesModel.activity == "offline").first() - if offline_device: - pass \ No newline at end of file diff --git a/admin_notifications/helpers/notification_templates.py b/admin_notifications/helpers/notification_templates.py new file mode 100644 index 000000000..624a01160 --- /dev/null +++ b/admin_notifications/helpers/notification_templates.py @@ -0,0 +1,6 @@ +def event_auto_cancelled_notification(event_name, room_name): + return { + "title": "Event Auto cancelled.", + "message": f"An event {event_name} in {room_name} \ +has been auto cancelled." + } diff --git a/admin_notifications/helpers/queue_manager.py b/admin_notifications/helpers/queue_manager.py deleted file mode 100644 index e9397e5ef..000000000 --- a/admin_notifications/helpers/queue_manager.py +++ /dev/null @@ -1,7 +0,0 @@ -from datetime import timedelta -beat_schedule = { - 'run-check-device-last-seen-hourly': { - 'task': 'check-device-last-seen', - 'schedule': timedelta(seconds=5) - } -} diff --git a/admin_notifications/models.py b/admin_notifications/models.py index 71e5eef86..8485377fa 100644 --- a/admin_notifications/models.py +++ b/admin_notifications/models.py @@ -15,4 +15,5 @@ class AdminNotification(Base, Utility): location_id = Column( Integer, ForeignKey('locations.id', ondelete="CASCADE"), - nullable=True) + nullable=True + ) diff --git a/admin_notifications/schema.py b/admin_notifications/schema.py new file mode 100644 index 000000000..4debc06e2 --- /dev/null +++ b/admin_notifications/schema.py @@ -0,0 +1,67 @@ +import graphene +from graphene_sqlalchemy import SQLAlchemyObjectType +from graphql import GraphQLError +from helpers.auth.authentication import Auth +from datetime import datetime +from admin_notifications.models import AdminNotification as \ + AdminNotificationModel + + +class AdminNotifications(SQLAlchemyObjectType): + """ + Returns the admin_notificationspayload + """ + class Meta: + model = AdminNotificationModel + + +class NotificationsList(graphene.ObjectType): + """ + Class to return the types for Admin notifications + \n- rooms: The rooms data + """ + notifications = graphene.List(AdminNotifications) + + +class Query(graphene.ObjectType): + all_unread_notifications = graphene.Field( + NotificationsList, + description="Returns a list of admin notifications" + ) + + @Auth.user_roles('Admin') + def resolve_all_unread_notifications(self, info): + query = AdminNotifications.get_query(info) + notifications = query.filter( + AdminNotificationModel.status == "unread").all() + return NotificationsList(notifications=notifications) + + +class UpdateNotificationStatus(graphene.Mutation): + """ + Class to update the status of a notification + """ + + class Arguments: + notification_id = graphene.Int(required=True) + notification = graphene.Field(AdminNotifications) + + @Auth.user_roles('Admin') + def mutate(self, info, notification_id): + query = AdminNotifications.get_query(info) + unread_notifications = query.filter( + AdminNotificationModel.status == "unread") + notification = unread_notifications.filter( + AdminNotificationModel.id == notification_id).first() + if not notification: + raise GraphQLError("Notification is already read or not found.") + notification.status = "read" + notification.date_read = datetime.now() + notification.save() + return UpdateNotificationStatus(notification=notification) + + +class Mutation(graphene.ObjectType): + update_notification_status = UpdateNotificationStatus.Field( + description="Updates the status od a notification and takes the argument\ + \n- notification_id: The name of the room[required]") diff --git a/alembic/env.py b/alembic/env.py index 01ce12db5..5a0334069 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -45,7 +45,7 @@ from api.response.models import Response from api.tag.models import Tag from api.structure.models import Structure - +from admin_notifications.models import AdminNotification target_metadata = Base.metadata diff --git a/alembic/versions/1b53553ddacf_add_activity_column_to_devices_table.py b/alembic/versions/1b53553ddacf_add_activity_column_to_devices_table.py deleted file mode 100644 index f99895fc3..000000000 --- a/alembic/versions/1b53553ddacf_add_activity_column_to_devices_table.py +++ /dev/null @@ -1,34 +0,0 @@ -"""add activity column to devices table - -Revision ID: 1b53553ddacf -Revises: c65718cd4ed8 -Create Date: 2019-06-22 12:59:24.591339 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - - -# revision identifiers, used by Alembic. -revision = '1b53553ddacf' -down_revision = 'c65718cd4ed8' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - activitytype = postgresql.ENUM('online', 'offline', name='activitytype') - activitytype.create(op.get_bind()) - op.add_column('devices', sa.Column('activity', sa.Enum( - 'online', 'offline', name='activitytype'), nullable=True)) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('devices', 'activity') - op.execute("DROP TYPE activitytype;") - # ### end Alembic commands ### diff --git a/alembic/versions/f0873fb8edb3_add_admin_notifications_table.py b/alembic/versions/f0873fb8edb3_add_admin_notifications_table.py new file mode 100644 index 000000000..c593c4156 --- /dev/null +++ b/alembic/versions/f0873fb8edb3_add_admin_notifications_table.py @@ -0,0 +1,39 @@ +"""add admin notifications table + +Revision ID: f0873fb8edb3 +Revises: 50173cb0491f +Create Date: 2019-06-27 13:31:19.694650 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f0873fb8edb3' +down_revision = '50173cb0491f' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.execute('DROP TYPE statustype;') + op.create_table('admin_notifications', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(), nullable=True), + sa.Column('message', sa.String(), nullable=True), + sa.Column('date_received', sa.String(), nullable=True), + sa.Column('date_read', sa.String(), nullable=True), + sa.Column('status', sa.Enum('read', 'unread', name='statustype'), nullable=True), + sa.Column('location_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['location_id'], ['locations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('admin_notifications') + # ### end Alembic commands ### diff --git a/api/devices/models.py b/api/devices/models.py index 84bd892b1..8bf4b1f82 100644 --- a/api/devices/models.py +++ b/api/devices/models.py @@ -1,10 +1,10 @@ -from sqlalchemy import (Column, String, Integer, DateTime, ForeignKey, Enum) +from sqlalchemy import (Column, String, Integer, DateTime, ForeignKey) from sqlalchemy.schema import Sequence from sqlalchemy.orm import relationship from helpers.database import Base from utilities.validations import validate_empty_fields -from utilities.utility import Utility, ActivityType +from utilities.utility import Utility class Devices(Base, Utility): @@ -17,7 +17,6 @@ class Devices(Base, Utility): location = Column(String, nullable=False) room_id = Column(Integer, ForeignKey('rooms.id', ondelete="CASCADE")) room = relationship('Room') - activity = Column(Enum(ActivityType), default="active") def __init__(self, **kwargs): validate_empty_fields(**kwargs) diff --git a/api/events/schema.py b/api/events/schema.py index d8d15b039..341eef1e6 100644 --- a/api/events/schema.py +++ b/api/events/schema.py @@ -15,6 +15,9 @@ import pytz from dateutil import parser from datetime import datetime, timedelta +from admin_notifications.helpers.create_notification import create_notification +from admin_notifications.helpers.notification_templates import ( + event_auto_cancelled_notification) class Events(SQLAlchemyObjectType): @@ -104,6 +107,15 @@ def mutate(self, info, **kwargs): kwargs['event_id'] ) event_reject_reason = 'after 10 minutes' + notification_payload = event_auto_cancelled_notification( + event_name=event.event_title, + room_name=event.room.name + ) + create_notification( + title=notification_payload['title'], + message=notification_payload['message'], + location_id=event.room.location_id + ) if not notification.event_cancellation_notification( calendar_event, room_id, diff --git a/app.py b/app.py index 9f8498055..6ace0096c 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,6 @@ from flask_graphql import GraphQLView from flask_cors import CORS from flask_json import FlaskJSON -from flask_socketio import SocketIO from flask_mail import Mail from config import config @@ -12,7 +11,6 @@ from healthcheck_schema import healthcheck_schema from helpers.auth.authentication import Auth from api.analytics.analytics_request import AnalyticsRequest - mail = Mail() @@ -20,7 +18,6 @@ def create_app(config_name): app = Flask(__name__) CORS(app) FlaskJSON(app) - SocketIO(app) app.config.from_object(config[config_name]) config[config_name].init_app(app) mail.init_app(app) diff --git a/cworker.py b/cworker.py index c6dab9e1f..2b7f2f866 100644 --- a/cworker.py +++ b/cworker.py @@ -1,7 +1,7 @@ import os from celery import Celery from app import create_app -from admin_notifications.helpers.queue_manager import beat_schedule + app = create_app(os.getenv('APP_SETTINGS') or 'default') app.app_context().push() @@ -10,13 +10,12 @@ app.config.update( CELERY_BROKER_URL='redis://localhost:6379', CELERY_RESULT_BACKEND='redis://localhost:6379', - CELERY_ACCEPT_CONTENT=['pickle'], - CELERYBEAT_SCHEDULE=beat_schedule + CELERY_ACCEPT_CONTENT=['pickle'] ) def make_celery(app): - celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'], include=['admin_notifications.helpers.device_last_seen', 'admin_notifications.helpers.create_notification'], # noqa 501 + celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'], include=['admin_notifications.helpers.create_notification'], # noqa 501 backend=app.config['CELERY_BROKER_URL']) celery.conf.update(app.config) diff --git a/fixtures/admin_notification/__init__.py b/fixtures/admin_notification/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fixtures/admin_notification/admin_notification_fixtures.py b/fixtures/admin_notification/admin_notification_fixtures.py new file mode 100644 index 000000000..ec7e0bd11 --- /dev/null +++ b/fixtures/admin_notification/admin_notification_fixtures.py @@ -0,0 +1,53 @@ +null = None + +get_all_unread_notifications = ''' +query { + allUnreadNotifications{ + notifications { + title + message + id + } + } +} +''' + +get_all_unread_notifications_response = { + "data": { + "allUnreadNotifications": { + "notifications": [] + } + } +} + +change_notification_status_unexistent_id = ''' +mutation { + updateNotificationStatus(notificationId: 260){ + notification { + id + title + message + } + } +} +''' + +change_notification_status_unexistent_id_response = { + "errors": [ + { + "message": "Notification is already read or not found.", + "locations": [ + { + "line": 3, + "column": 3 + } + ], + "path": [ + "updateNotificationStatus" + ] + } + ], + "data": { + "updateNotificationStatus": null + } +} diff --git a/manage.py b/manage.py index a9e7943d6..fdfd88a28 100644 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import bugsnag from flask_script import Manager, Shell from bugsnag.flask import handle_exceptions - +from flask_socketio import SocketIO # Configure bugnsag bugsnag.configure( @@ -18,6 +18,7 @@ app = create_app(os.getenv('APP_SETTINGS') or 'default') handle_exceptions(app) manager = Manager(app) +socketio = SocketIO(app) def make_shell_context(): @@ -28,6 +29,6 @@ def make_shell_context(): "shell", Shell( make_context=make_shell_context)) - if __name__ == '__main__': + socketio.run(app, debug=True) manager.run() diff --git a/requirements.txt b/requirements.txt index e510aad0d..8ade2b2f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,7 @@ coverage==4.5.1 coveralls==1.8.0 decorator==4.4.0 docopt==0.6.2 +eventlet==0.25.0 flake8==3.5.0 Flask==0.12.2 Flask-Cors==3.0.4 @@ -23,7 +24,7 @@ Flask-GraphQL==1.4.1 Flask-JSON==0.3.2 Flask-Mail==0.9.1 Flask-Script==2.0.6 -Flask-SocketIO==4.1.0 +flask-socketio==4.1.0 Flask-Testing==0.7.1 google-api-python-client==1.6.7 graphene==2.1 diff --git a/schema.py b/schema.py index 8d28e261e..3488084d0 100644 --- a/schema.py +++ b/schema.py @@ -16,6 +16,7 @@ import api.response.schema_query import api.structure.schema import api.analytics.all_analytics_query +import admin_notifications.schema class Query( @@ -33,6 +34,7 @@ class Query( api.structure.schema.Query, api.analytics.all_analytics_query.Query, api.events.schema.Query, + admin_notifications.schema.Query ): """Root for converge Graphql queries""" pass @@ -51,6 +53,7 @@ class Mutation( api.response.schema.Mutation, api.tag.schema.Mutation, api.structure.schema.Mutation, + admin_notifications.schema.Mutation ): """The root query for implementing GraphQL mutations.""" pass diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 877d1b6ed..000000000 --- a/setup.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[tool:pytest] -testpaths = tests - -[coverage:run] -branch = True -source = - mrm_api/ \ No newline at end of file diff --git a/tests/test_admin_notification/__init__.py b/tests/test_admin_notification/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_admin_notification/test_admin_notification.py b/tests/test_admin_notification/test_admin_notification.py new file mode 100644 index 000000000..6ec151e61 --- /dev/null +++ b/tests/test_admin_notification/test_admin_notification.py @@ -0,0 +1,23 @@ +from tests.base import BaseTestCase, CommonTestCases +from fixtures.admin_notification.admin_notification_fixtures import ( + get_all_unread_notifications, + get_all_unread_notifications_response, + change_notification_status_unexistent_id, + change_notification_status_unexistent_id_response +) + + +class TestDeleteTag(BaseTestCase): + def test_get_all_notifications(self): + CommonTestCases.admin_token_assert_equal( + self, + get_all_unread_notifications, + get_all_unread_notifications_response + ) + + def test_change_notification_status(self): + CommonTestCases.admin_token_assert_equal( + self, + change_notification_status_unexistent_id, + change_notification_status_unexistent_id_response + ) diff --git a/utilities/utility.py b/utilities/utility.py index 99fbd3455..a5b51013d 100644 --- a/utilities/utility.py +++ b/utilities/utility.py @@ -89,12 +89,6 @@ class QuestionType(enum.Enum): missingitem = "missing_items" - class StatusType(enum.Enum): read = "read" unread = "unread" - - -class ActivityType(enum.Enum): - online = "online" - offline = "offline"