diff --git a/alembic/versions/859f31384fe2_add_organizer_email_field.py b/alembic/versions/859f31384fe2_add_organizer_email_field.py new file mode 100644 index 000000000..a83fbcc78 --- /dev/null +++ b/alembic/versions/859f31384fe2_add_organizer_email_field.py @@ -0,0 +1,25 @@ +"""add organizer email field + +Revision ID: 859f31384fe2 +Revises: c058d462a21d +Create Date: 2019-11-18 09:26:07.682060 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '859f31384fe2' +down_revision = 'c058d462a21d' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('events', sa.Column( + 'organizer_email', sa.String(), nullable=True)) + + +def downgrade(): + op.drop_column('events', 'organizer_email') diff --git a/api/events/models.py b/api/events/models.py index 9021c0a9e..722cc402b 100644 --- a/api/events/models.py +++ b/api/events/models.py @@ -29,6 +29,7 @@ class Events(Base, Utility): meeting_end_time = Column(String, nullable=True) auto_cancelled = Column(Boolean, nullable=True, default=False) app_booking = Column(Boolean, nullable=True, default=False) + organizer_email = Column(String, nullable=True) def filter_event(start_date, end_date, room_id=None): diff --git a/api/events/schema.py b/api/events/schema.py index 03955c433..f8c1a16e8 100644 --- a/api/events/schema.py +++ b/api/events/schema.py @@ -29,6 +29,7 @@ calendar_dates_format, empty_string_checker ) +from helpers.event.slack_notifier import notify_slack utc = pytz.utc @@ -41,6 +42,18 @@ class Meta: model = EventsModel +class EventArguments(graphene.ObjectType): + + """ + returns common event arguments + """ + + calendar_id = graphene.String(required=True) + event_id = graphene.String(required=True) + start_time = graphene.String(required=True) + end_time = graphene.String(required=True) + + class BookEvent(graphene.Mutation): """ Book Calendar events @@ -56,6 +69,7 @@ class Arguments: organizer = graphene.String(required=False) description = graphene.String(required=False) room = graphene.String(required=True) + room_id = graphene.Int(required=True) time_zone = graphene.String(required=True) @Auth.user_roles('Admin', 'Default User', 'Super Admin') @@ -71,9 +85,11 @@ def mutate(self, info, **kwargs): attendees: A string of emails of the event guests description: Any additional information about the event room: The meeting room where the even will be held [required] + room_id: The id of the meeting room where the even will + be held [required] time_zone: The timezone of the event location eg. 'Africa/Kigali' - organizer: The email of the co-organizer, the converge email is - the default + organizer: The email of the organizer. it should be gotten + from the user token. Returns: A string that communicates a successfully created event. @@ -86,12 +102,14 @@ def mutate(self, info, **kwargs): organizer = kwargs.get('organizer', None) event_title = kwargs['event_title'] + room_id = kwargs['room_id'] empty_string_checker(event_title) empty_string_checker(room) + empty_string_checker(room_id) empty_string_checker(time_zone) start_date, end_date = calendar_dates_format( - kwargs['start_date'], kwargs['start_time'], duration) + kwargs['start_date'], kwargs['start_time'], duration, time_zone) attendees = attendees.replace(" ", "").split(",") guests = [] @@ -117,8 +135,23 @@ def mutate(self, info, **kwargs): } } service = credentials.set_api_credentials() - service.events().insert(calendarId='primary', body=event, - sendNotifications=True).execute() + event_created = service.events() \ + .insert(calendarId='primary', + body=event, + sendNotifications=True).execute() + new_event = EventsModel( + event_id=event_created['id'], + event_title=kwargs['event_title'], + start_time=start_date, + end_time=end_date, + number_of_participants=len(guests), + app_booking=True, + checked_in=False, + room_id=room_id, + cancelled=False, + organizer_email=organizer + ) + new_event.save() return BookEvent(response='Event created successfully') @@ -127,11 +160,11 @@ class EventCheckin(graphene.Mutation): Returns the eventcheckin payload """ class Arguments: - calendar_id = graphene.String(required=True) - event_id = graphene.String(required=True) + calendar_id = EventArguments.calendar_id + event_id = EventArguments.event_id event_title = graphene.String(required=True) - start_time = graphene.String(required=True) - end_time = graphene.String(required=True) + start_time = EventArguments.start_time + end_time = EventArguments.end_time number_of_participants = graphene.Int(required=True) check_in_time = graphene.String(required=False) event = graphene.Field(Events) @@ -160,11 +193,11 @@ class CancelEvent(graphene.Mutation): Returns the payload on event cancelation """ class Arguments: - calendar_id = graphene.String(required=True) - event_id = graphene.String(required=True) + calendar_id = EventArguments.calendar_id + event_id = EventArguments.event_id event_title = graphene.String(required=True) - start_time = graphene.String(required=True) - end_time = graphene.String(required=True) + start_time = EventArguments.start_time + end_time = EventArguments.end_time number_of_participants = graphene.Int() event = graphene.Field(Events) @@ -210,14 +243,32 @@ class EndEvent(graphene.Mutation): Returns event payload on ending the event """ class Arguments: - calendar_id = graphene.String(required=True) - event_id = graphene.String(required=True) - start_time = graphene.String(required=True) - end_time = graphene.String(required=True) + calendar_id = EventArguments.calendar_id + event_id = EventArguments.event_id + organizer_email = graphene.String(required=True) + room_id = graphene.Int(required=True) + start_time = EventArguments.start_time + end_time = EventArguments.end_time meeting_end_time = graphene.String(required=True) event = graphene.Field(Events) def mutate(self, info, **kwargs): + calendar_id = kwargs['calendar_id'] + event_id = kwargs['event_id'] + organizer_email = kwargs['organizer_email'] + room_id = kwargs['room_id'] + start_time = kwargs['start_time'] + end_time = kwargs['end_time'] + meeting_end_time = kwargs['meeting_end_time'] + + empty_string_checker(calendar_id) + empty_string_checker(event_id) + empty_string_checker(organizer_email) + empty_string_checker(room_id) + empty_string_checker(start_time) + empty_string_checker(end_time) + empty_string_checker(meeting_end_time) + room_id, event = check_event_in_db(self, info, "ended", **kwargs) if kwargs.get('meeting_end_time'): update_device_last_activity( @@ -229,6 +280,7 @@ def mutate(self, info, **kwargs): ) event.save() + notify_slack.delay(event_id, organizer_email, room_id) return EndEvent(event=event) diff --git a/config.py b/config.py index 65e9c4901..88941b4d7 100644 --- a/config.py +++ b/config.py @@ -27,6 +27,9 @@ class Config: }, } + # slack bot url + NOTIFY_URL = os.getenv('SLACK_NOTIFICATION_URL') + @staticmethod def init_app(app): pass diff --git a/fixtures/events/book_event_fixtures.py b/fixtures/events/book_event_fixtures.py index 979d17527..0d288467f 100644 --- a/fixtures/events/book_event_fixtures.py +++ b/fixtures/events/book_event_fixtures.py @@ -6,7 +6,8 @@ duration:60 attendees: "adafia.samuel@gmail.com, qanda8@gmail.com", timeZone: "Africa/Kigali", - room:"Yankara" + room:"Yankara", + roomId:1 ){ response } @@ -21,7 +22,8 @@ duration:60 attendees: "adafia.samuel@gmail.com, qanda8@gmail.com", timeZone: "Africa/Kigali", - room:"Yankara" + room:"Yankara", + roomId:1 ){ response } @@ -36,7 +38,8 @@ duration:60 attendees: "adafia.samuel@gmail.com, qanda8@gmail.com", timeZone: "Africa/Kigali", - room:"" + room:"", + roomId:1 ){ response } @@ -51,7 +54,8 @@ duration:60 attendees: "adafia.samuel@gmail.com, qanda8@gmail.com", timeZone: "Africa/Kigali", - room:"Yankara" + room:"Yankara", + roomId:1 ){ response } @@ -66,7 +70,8 @@ duration:60 attendees: "adafia.samuel@gmail.com, qanda8@gmail.com", timeZone: "Africa/Kigali", - room:"Yankara" + room:"Yankara", + roomId:1 ){ response } @@ -81,7 +86,8 @@ duration:60 attendees: "adafia.samuel@gmail.com, qanda8@gmail.com", timeZone: "", - room:"Yankara" + room:"Yankara", + roomId:1 ){ response } diff --git a/fixtures/events/end_event_fixtures.py b/fixtures/events/end_event_fixtures.py index ef48ee520..c1bc076c0 100644 --- a/fixtures/events/end_event_fixtures.py +++ b/fixtures/events/end_event_fixtures.py @@ -1,6 +1,8 @@ end_event_mutation = '''mutation { endEvent(calendarId:"andela.com_3630363835303531343031@resource.calendar.google.com", eventId:"test_id5", + organizerEmail:"converge.notification@andela.com", + roomId:1, startTime:"2018-07-10T09:00:00Z", endTime:"2018-07-10T09:45:00Z", meetingEndTime: "2018-07-10T09:45:00Z"){ @@ -43,6 +45,8 @@ endEvent(calendarId:"andela.com_3630363835303531343031@resource.calendar.google.com", eventId:"test_id5", startTime:"2018-07-11T09:00:00Z", + organizerEmail:"converge.notification@andela.com", + roomId:1, endTime:"2018-07-11T09:45:00Z", meetingEndTime: "2018-07-11T09:45:00Z"){ event{ @@ -69,6 +73,8 @@ endEvent(calendarId:"invalid_calendar_id", eventId:"test_id5", startTime:"2018-08-11T09:00:00Z", + organizerEmail:"converge.notification@andela.com", + roomId:1, endTime:"2018-08-11T09:45:00Z", meetingEndTime: "2018-08-11T09:45:00Z"){ event{ diff --git a/helpers/calendar/events.py b/helpers/calendar/events.py index 022264f6e..7cf494fa4 100644 --- a/helpers/calendar/events.py +++ b/helpers/calendar/events.py @@ -189,6 +189,7 @@ def sync_single_room_events(self, room): recurring_event_id=event.get("recurringEventId"), room_id=room.id, event_title=event.get("summary"), + organizer_email=organizer.get('email'), start_time=event["start"].get( "dateTime") or event["start"].get("date"), end_time=event["end"].get( diff --git a/helpers/event/__init__.py b/helpers/event/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/helpers/event/slack_notifier.py b/helpers/event/slack_notifier.py new file mode 100644 index 000000000..e2fc4928e --- /dev/null +++ b/helpers/event/slack_notifier.py @@ -0,0 +1,18 @@ +import requests +import celery +from config import Config + + +@celery.task(name="notify-slack") +def notify_slack(event_id, organizer_email, room_id): + """ + This function calls the slack BOT URL + and notifies the organizer. + """ + params_data = { + "event_id": event_id, + "organizer_email": organizer_email, + "room_id": room_id + } + response = requests.get(url=Config.NOTIFY_URL, params=params_data) + return(response.status_code) diff --git a/helpers/events_filter/events_filter.py b/helpers/events_filter/events_filter.py index 7fa019b22..4ef07557b 100644 --- a/helpers/events_filter/events_filter.py +++ b/helpers/events_filter/events_filter.py @@ -6,6 +6,7 @@ utc = pytz.utc +timezone = pytz.timezone def validate_date_input(start_date, end_date): @@ -97,7 +98,7 @@ def date_time_format_validator(date_text, time_text): raise ValueError("start time should be in this format: 'HH:MM'") -def calendar_dates_format(start_date, start_time, duration): +def calendar_dates_format(start_date, start_time, duration, time_zone): """Converts user date, time and duration input into start_date and end_date format that is acceptable by the Google Calendar API @@ -105,6 +106,7 @@ def calendar_dates_format(start_date, start_time, duration): start_date: Date string of the day of the event start_time: Time sting of the time of the event duration: A float of the duration of the event + time_zone: The timezone of the event location eg. 'Africa/Kigali' Returns: start_date and end_date in this format "%Y-%m-%dT%H:%M:%S". @@ -121,7 +123,7 @@ def calendar_dates_format(start_date, start_time, duration): end_date = start_date + timedelta(minutes=duration) - start_date = start_date.strftime('%Y-%m-%dT%H:%M:%S') - end_date = end_date.strftime('%Y-%m-%dT%H:%M:%S') + start_date = start_date.replace(tzinfo=timezone(time_zone)).isoformat() + end_date = end_date.replace(tzinfo=timezone(time_zone)).isoformat() return (start_date, end_date) diff --git a/requirements.txt b/requirements.txt index 11be5f617..0bd4e3df1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,3 +39,5 @@ flake8==3.5.0 coveralls==1.8.2 validators==0.12.4 graphql-core==2.2.1 +requests==2.22.0 +graphql-core==2.2.1 \ No newline at end of file diff --git a/tests/base.py b/tests/base.py index d44febaff..ef6121cff 100644 --- a/tests/base.py +++ b/tests/base.py @@ -142,6 +142,7 @@ def setUp(self, mock_verify_calendar_id): event_id="test_id5", room_id=1, event_title="Onboarding", + organizer_email="converge.notification@andela.com", start_time="2018-07-11T09:00:00Z", end_time="2018-07-11T09:45:00Z", number_of_participants=4, @@ -211,18 +212,18 @@ def setUp(self, mock_verify_calendar_id): ) structure.save() parent_node = OfficeStructure( - id='C56A4180-65AA-42EC-A945-5FD21DEC0518', - name='Epic Tower', - tag='Lagos Building', - location_id=1 + id='C56A4180-65AA-42EC-A945-5FD21DEC0518', + name='Epic Tower', + tag='Lagos Building', + location_id=1 ) parent_node.save() child_node = OfficeStructure( - id='C56A4180-65AA-42EC-A945-5FD21DEC0519', - name='Gold Coast', - tag='First Floor', - parent_id='C56A4180-65AA-42EC-A945-5FD21DEC0518', - location_id=1 + id='C56A4180-65AA-42EC-A945-5FD21DEC0519', + name='Gold Coast', + tag='First Floor', + parent_id='C56A4180-65AA-42EC-A945-5FD21DEC0518', + location_id=1 ) child_node.save() db_session.commit() diff --git a/tests/test_events/test_end_event.py b/tests/test_events/test_end_event.py index 3c5db77d0..37da06e0b 100644 --- a/tests/test_events/test_end_event.py +++ b/tests/test_events/test_end_event.py @@ -1,4 +1,5 @@ from tests.base import BaseTestCase, CommonTestCases +from unittest.mock import patch from fixtures.events.end_event_fixtures import ( end_event_mutation, @@ -16,10 +17,12 @@ class TestEndEvent(BaseTestCase): - def test_end_event(self): + @patch('api.events.schema.notify_slack.delay') + def test_end_event(self, mock_notify_slack): """ Test user can end an event """ + mock_notify_slack.return_value = True CommonTestCases.user_token_assert_equal( self, event_checkin_mutation, @@ -41,10 +44,12 @@ def test_end_unchecked_in_event(self): end_unchecked_in_event_mutation_response ) - def test_end_event_twice(self): + @patch('api.events.schema.notify_slack.delay') + def test_end_event_twice(self, mock_notify_slack): """ Test user cannot end an event twice """ + mock_notify_slack.return_value = True CommonTestCases.user_token_assert_equal( self, event_checkin_mutation, diff --git a/tests/test_events/test_slack_notifier.py b/tests/test_events/test_slack_notifier.py new file mode 100644 index 000000000..ec2aa243e --- /dev/null +++ b/tests/test_events/test_slack_notifier.py @@ -0,0 +1,15 @@ +import unittest +from unittest.mock import patch +from helpers.event.slack_notifier import notify_slack + + +class TestSlackNotifier(unittest.TestCase): + + @patch('helpers.event.slack_notifier.requests.get') + def test_slack_notifier(self, mock_get_request): + """ + Test to verify that a request is made when + an event ends + """ + notify_slack('event_id', 'andela@andela.com', '18') + mock_get_request.assert_called_once()