From d0203663746d1f139e0e6df3c627544efb46469c Mon Sep 17 00:00:00 2001 From: Amir Pollak Date: Tue, 9 Feb 2021 01:48:08 +0200 Subject: [PATCH 1/6] Squash --- app/database/models.py | 3 +- app/internal/event.py | 63 +++++++++- app/routers/event.py | 85 ++++++++----- app/templates/event/eventview.html | 5 + .../partials/edit_event_details_tab.html | 5 + tests/test_event.py | 116 +++++++++++++++++- 6 files changed, 238 insertions(+), 39 deletions(-) diff --git a/app/database/models.py b/app/database/models.py index 94f702d8..47344466 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -58,9 +58,10 @@ class Event(Base): end = Column(DateTime, nullable=False) content = Column(String) location = Column(String) - color = Column(String, nullable=True) + invitees = Column(String) owner_id = Column(Integer, ForeignKey("users.id")) + color = Column(String, nullable=True) category_id = Column(Integer, ForeignKey("categories.id")) owner = relationship("User") diff --git a/app/internal/event.py b/app/internal/event.py index 52b031e0..7a50d1ad 100644 --- a/app/internal/event.py +++ b/app/internal/event.py @@ -1,12 +1,73 @@ +import logging import re +from typing import List +from email_validator import validate_email, EmailSyntaxError from fastapi import HTTPException +from sqlalchemy.orm import Session + from starlette.status import HTTP_400_BAD_REQUEST +from app.database.models import Event + ZOOM_REGEX = re.compile(r'https://.*?\.zoom.us/[a-z]/.[^.,\b\s]+') -def validate_zoom_link(location): +def raise_if_zoom_link_invalid(location): if ZOOM_REGEX.search(location) is None: raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail="VC type with no valid zoom link") + + +def get_invited_emails(invited_from_form: str): + invited_emails = [] + for invited_email in invited_from_form.split(','): + invited_email = invited_email.strip() + try: + validate_email(invited_email, check_deliverability=False) + invited_emails.append(invited_email) + except EmailSyntaxError: + logging.error(f'{invited_email} is not a valid email address') + + return invited_emails + + +def get_uninvited_regular_emails(session: Session, + owner_id: int, + title: str, + invited_emails: List[str]): + invitees_query = session.query(Event).with_entities(Event.invitees) + similar_events_invitees = invitees_query.filter(Event.owner_id == owner_id, + Event.title == title).all() + all_regular_invitees_concatenated = '' + for record in similar_events_invitees: + if record: + all_regular_invitees_concatenated += ',' + record[0] + + regular_invitees = set(all_regular_invitees_concatenated.split(',')) + + return regular_invitees - set(invited_emails) + + +def check_diffs(checked_event: Event, + all_events: List[Event]): + """Returns the repeated events and the week difference""" + diffs = [] + for event in all_events: + start_delta = checked_event.start - event.start + end_delta = checked_event.end - event.end + + # The current event is before the new event and they take the same time + if start_delta.total_seconds() > 0 and start_delta == end_delta: + # Difference is in multiple of 7 days + if start_delta.seconds == 0 and start_delta.days % 7 == 0: + diffs.append(int(start_delta.days / 7)) + + return diffs + + +def find_pattern(session, event): + all_events_with_same_name = session.query(Event).filter( + Event.owner_id == event.owner_id, Event.title == event.title).all() + + return check_diffs(event, all_events_with_same_name) diff --git a/app/routers/event.py b/app/routers/event.py index c4b2c44b..701735d7 100644 --- a/app/routers/event.py +++ b/app/routers/event.py @@ -1,9 +1,8 @@ -from datetime import datetime +from datetime import datetime as dt from operator import attrgetter from typing import Any, Dict, List, Optional from fastapi import APIRouter, Depends, HTTPException, Request -from loguru import logger from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound @@ -12,11 +11,25 @@ from app.database.database import get_db from app.database.models import Event, User, UserEvent +from app.dependencies import logger from app.dependencies import templates -from app.internal.event import validate_zoom_link +from app.internal.event import (raise_if_zoom_link_invalid, get_invited_emails, + get_uninvited_regular_emails, find_pattern) from app.internal.utils import create_model from app.routers.user import create_user +TIME_FORMAT = '%Y-%m-%d %H:%M' + +UPDATE_EVENTS_FIELDS = { + 'title': str, + 'start': dt, + 'end': dt, + 'content': (str, type(None)), + 'location': (str, type(None)), + 'category_id': (int, type(None)) +} + + router = APIRouter( prefix="/event", tags=["event"], @@ -35,10 +48,10 @@ async def create_new_event(request: Request, session=Depends(get_db)): data = await request.form() title = data['title'] content = data['description'] - start = datetime.strptime(data['start_date'] + ' ' + data['start_time'], - '%Y-%m-%d %H:%M') - end = datetime.strptime(data['end_date'] + ' ' + data['end_time'], - '%Y-%m-%d %H:%M') + start = dt.strptime(data['start_date'] + ' ' + data['start_time'], + TIME_FORMAT) + end = dt.strptime(data['end_date'] + ' ' + data['end_time'], + TIME_FORMAT) user = session.query(User).filter_by(id=1).first() user = user if user else create_user(username="u", password="p", @@ -52,13 +65,27 @@ async def create_new_event(request: Request, session=Depends(get_db)): location = data['location'] category_id = data.get('category_id') + invited_emails = get_invited_emails(data['invited']) + uninvited_contacts = get_uninvited_regular_emails(session, owner_id, + title, invited_emails) + if is_zoom: - validate_zoom_link(location) + raise_if_zoom_link_invalid(location) event = create_event(session, title, start, end, owner_id, content, - location, category_id=category_id) - return RedirectResponse(router.url_path_for('eventview', - event_id=event.id), + location, invited_emails, category_id=category_id) + messages = [] + if uninvited_contacts: + messages.append(f'Forgot to invite ' + f'{", ".join(uninvited_contacts)} maybe?') + + pattern = find_pattern(session, event) + for weeks_diff in pattern: + messages.append(f'Same event happened {weeks_diff} weeks before too. ' + f'Want to create another one {weeks_diff} after too?') + + return RedirectResponse(router.url_path_for('eventview', event_id=event.id) + + f'messages={"---".join(messages)}', status_code=status.HTTP_302_FOUND) @@ -69,20 +96,12 @@ async def eventview(request: Request, event_id: int, start_format = '%A, %d/%m/%Y %H:%M' end_format = ('%H:%M' if event.start.date() == event.end.date() else start_format) + messages = request.query_params.get('messages', '').split("---") return templates.TemplateResponse("event/eventview.html", {"request": request, "event": event, "start_format": start_format, - "end_format": end_format}) - - -UPDATE_EVENTS_FIELDS = { - 'title': str, - 'start': datetime, - 'end': datetime, - 'content': (str, type(None)), - 'location': (str, type(None)), - 'category_id': (int, type(None)) -} + "end_format": end_format, + "messages": messages}) def by_id(db: Session, event_id: int) -> Event: @@ -115,10 +134,8 @@ def by_id(db: Session, event_id: int) -> Event: return event -def is_end_date_before_start_date( - start_date: datetime, end_date: datetime) -> bool: +def is_end_date_before_start_date(start_date: dt, end_date: dt) -> bool: """Check if the start date is earlier than the end date""" - return start_date > end_date @@ -190,9 +207,12 @@ def update_event(event_id: int, event: Dict, db: Session def create_event(db: Session, title: str, start, end, owner_id: int, content: str = None, location: str = None, + invitees: List[str] = None, category_id: int = None): """Creates an event and an association.""" + invitees_concatenated = ','.join(invitees or []) + event = create_model( db, Event, title=title, @@ -201,6 +221,7 @@ def create_event(db: Session, title: str, start, end, owner_id: int, content=content, owner_id=owner_id, location=location, + invitees=invitees_concatenated, category_id=category_id, ) create_model( @@ -221,13 +242,11 @@ def sort_by_date(events: List[Event]) -> List[Event]: def get_participants_emails_by_event(db: Session, event_id: int) -> List[str]: """Returns a list of all the email address of the event invited users, by event id.""" - - return [email[0] for email in db.query(User.email). - select_from(Event). - join(UserEvent, UserEvent.event_id == Event.id). - join(User, User.id == UserEvent.user_id). - filter(Event.id == event_id). - all()] + return [email[0] for email in + db.query(User.email).select_from(Event).join( + UserEvent, UserEvent.event_id == Event.id).join( + User, User.id == UserEvent.user_id).filter( + Event.id == event_id).all()] def _delete_event(db: Session, event: Event): @@ -254,7 +273,7 @@ def delete_event(event_id: int, event = by_id(db, event_id) participants = get_participants_emails_by_event(db, event_id) _delete_event(db, event) - if participants and event.start > datetime.now(): + if participants and event.start > dt.now(): pass # TODO: Send them a cancellation notice # if the deletion is successful diff --git a/app/templates/event/eventview.html b/app/templates/event/eventview.html index dc343a01..d211bdef 100644 --- a/app/templates/event/eventview.html +++ b/app/templates/event/eventview.html @@ -8,6 +8,11 @@
+
+ {% for message in messages %} +

{{ message }}

+ {% endfor %} +
+
+ + +
+
diff --git a/tests/test_event.py b/tests/test_event.py index be9adb9d..a176bc24 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -12,15 +12,16 @@ CORRECT_EVENT_FORM_DATA = { 'title': 'test title', 'start_date': '2021-01-28', - 'start_time': '15:59', - 'end_date': '2021-01-27', + 'start_time': '12:59', + 'end_date': '2021-01-28', 'end_time': '15:01', 'location_type': 'vc_url', 'location': 'https://us02web.zoom.us/j/875384596', 'description': 'content', 'color': 'red', 'availability': 'busy', - 'privacy': 'public' + 'privacy': 'public', + 'invited': 'a@a.com,b@b.com' } WRONG_EVENT_FORM_DATA = { @@ -34,7 +35,53 @@ 'description': 'content', 'color': 'red', 'availability': 'busy', - 'privacy': 'public' + 'privacy': 'public', + 'invited': 'a@a.com,b@b.com' +} + +BAD_EMAILS_FORM_DATA = { + 'title': 'test title', + 'start_date': '2021-01-28', + 'start_time': '15:59', + 'end_date': '2021-01-27', + 'end_time': '15:01', + 'location_type': 'vc_url', + 'location': 'https://us02web.zoom.us/j/875384596', + 'description': 'content', + 'color': 'red', + 'availability': 'busy', + 'privacy': 'public', + 'invited': 'a@a.com,b@b.com,ccc' +} + +WEEK_LATER_EVENT_FORM_DATA = { + 'title': 'test title', + 'start_date': '2021-02-04', + 'start_time': '12:59', + 'end_date': '2021-02-04', + 'end_time': '15:01', + 'location_type': 'vc_url', + 'location': 'https://us02web.zoom.us/j/875384596', + 'description': 'content', + 'color': 'red', + 'availability': 'busy', + 'privacy': 'public', + 'invited': 'a@a.com,b@b.com' +} + +TWO_WEEKS_LATER_EVENT_FORM_DATA = { + 'title': 'test title', + 'start_date': '2021-02-11', + 'start_time': '12:59', + 'end_date': '2021-02-11', + 'end_time': '15:01', + 'location_type': 'vc_url', + 'location': 'https://us02web.zoom.us/j/875384596', + 'description': 'content', + 'color': 'red', + 'availability': 'busy', + 'privacy': 'public', + 'invited': 'a@a.com,b@b.com' } NONE_UPDATE_OPTIONS = [ @@ -66,6 +113,44 @@ def test_eventview_with_id(event_test_client, session, event): f'{event_detail} not in view event page' +def test_eventview_without_id(client): + response = client.get("/event/view") + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + +def test_eventedit_missing_old_invites(client, user): + response = client.post(client.app.url_path_for('create_new_event'), + data=CORRECT_EVENT_FORM_DATA) + assert response.ok + assert response.status_code == status.HTTP_302_FOUND + + different_invitees_event = CORRECT_EVENT_FORM_DATA.copy() + different_invitees_event['invited'] = 'c@c.com,d@d.com' + response = client.post(client.app.url_path_for('create_new_event'), + data=different_invitees_event) + assert response.ok + assert response.status_code == status.HTTP_302_FOUND + for invitee in CORRECT_EVENT_FORM_DATA["invited"].split(","): + assert invitee in response.headers['location'] + + +def test_eventedit_bad_emails(client, user): + response = client.post(client.app.url_path_for('create_new_event'), + data=BAD_EMAILS_FORM_DATA) + assert response.ok + assert response.status_code == status.HTTP_302_FOUND + + different_invitees_event = CORRECT_EVENT_FORM_DATA.copy() + different_invitees_event['invited'] = 'c@c.com,d@d.com' + response = client.post(client.app.url_path_for('create_new_event'), + data=different_invitees_event) + assert response.ok + assert response.status_code == status.HTTP_302_FOUND + for invitee in CORRECT_EVENT_FORM_DATA["invited"].split(","): + assert invitee in response.headers['location'] + assert 'ccc' not in response.headers['location'] + + def test_eventedit_post_correct(client, user): """ Test create new event successfully. @@ -100,6 +185,29 @@ def test_eventedit_post_wrong(client, user): assert response.json()['detail'] == 'VC type with no valid zoom link' +def test_eventedit_with_pattern(client, user): + response = client.post(client.app.url_path_for('create_new_event'), + data=CORRECT_EVENT_FORM_DATA) + assert response.ok + assert response.status_code == status.HTTP_302_FOUND + + response = client.post(client.app.url_path_for('create_new_event'), + data=WEEK_LATER_EVENT_FORM_DATA) + assert response.ok + assert response.status_code == status.HTTP_302_FOUND + assert ('Same event happened 1 weeks before too. ' in + response.headers['location'].replace('+', ' ')) + + response = client.post(client.app.url_path_for('create_new_event'), + data=TWO_WEEKS_LATER_EVENT_FORM_DATA) + assert response.ok + assert response.status_code == status.HTTP_302_FOUND + assert ('Same event happened 1 weeks before too. ' in + response.headers['location'].replace('+', ' ')) + assert ('Same event happened 2 weeks before too. ' in + response.headers['location'].replace('+', ' ')) + + @pytest.mark.parametrize("data", NONE_UPDATE_OPTIONS) def test_invalid_update(event, data, session): """ From 7320c06681f7d350b5587d2ea606c96cc521efa9 Mon Sep 17 00:00:00 2001 From: Amir Pollak Date: Wed, 10 Feb 2021 12:08:06 +0200 Subject: [PATCH 2/6] CR fix --- app/internal/event.py | 5 +++-- app/templates/event/eventview.html | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/internal/event.py b/app/internal/event.py index 7a50d1ad..f2bfe14b 100644 --- a/app/internal/event.py +++ b/app/internal/event.py @@ -25,9 +25,10 @@ def get_invited_emails(invited_from_form: str): invited_email = invited_email.strip() try: validate_email(invited_email, check_deliverability=False) - invited_emails.append(invited_email) except EmailSyntaxError: - logging.error(f'{invited_email} is not a valid email address') + logging.exception(f'{invited_email} is not a valid email address') + continue + invited_emails.append(invited_email) return invited_emails diff --git a/app/templates/event/eventview.html b/app/templates/event/eventview.html index d211bdef..f545d781 100644 --- a/app/templates/event/eventview.html +++ b/app/templates/event/eventview.html @@ -10,7 +10,7 @@
{% for message in messages %} -

{{ message }}

+

{{ message }}

{% endfor %}