Skip to content

Commit a2a7eb8

Browse files
authored
Squash (#126)
1 parent 4df85bc commit a2a7eb8

File tree

6 files changed

+142
-37
lines changed

6 files changed

+142
-37
lines changed

app/database/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ class Event(Base):
6161
color = Column(String, nullable=True)
6262

6363
owner_id = Column(Integer, ForeignKey("users.id"))
64+
invitees = Column(String)
65+
color = Column(String, nullable=True)
6466
category_id = Column(Integer, ForeignKey("categories.id"))
6567

6668
owner = relationship("User")

app/internal/event.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,41 @@
1+
import logging
12
import re
23

4+
from email_validator import validate_email, EmailSyntaxError
35
from fastapi import HTTPException
6+
47
from starlette.status import HTTP_400_BAD_REQUEST
58

9+
from app.database.models import Event
10+
611
ZOOM_REGEX = re.compile(r'https://.*?\.zoom.us/[a-z]/.[^.,\b\s]+')
712

813

9-
def validate_zoom_link(location):
14+
def raise_if_zoom_link_invalid(location):
1015
if ZOOM_REGEX.search(location) is None:
1116
raise HTTPException(status_code=HTTP_400_BAD_REQUEST,
1217
detail="VC type with no valid zoom link")
18+
19+
20+
def get_invited_emails(invited_from_form):
21+
invited_emails = []
22+
for invited_email in invited_from_form.split(','):
23+
invited_email = invited_email.strip()
24+
try:
25+
validate_email(invited_email, check_deliverability=False)
26+
invited_emails.append(invited_email)
27+
except EmailSyntaxError:
28+
logging.error(f'{invited_email} is not a valid email address')
29+
30+
return invited_emails
31+
32+
33+
def get_uninvited_regular_emails(session, owner_id, title, invited_emails):
34+
regular_invitees = set()
35+
invitees_query = session.query(Event).with_entities(Event.invitees)
36+
similar_events_invitees = invitees_query.filter(Event.owner_id == owner_id,
37+
Event.title == title).all()
38+
for record in similar_events_invitees:
39+
regular_invitees.update(record[0].split(','))
40+
41+
return regular_invitees - set(invited_emails)

app/routers/event.py

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
from datetime import datetime
1+
from datetime import datetime as dt
22
from operator import attrgetter
33
from typing import Any, Dict, List, Optional
44

55
from fastapi import APIRouter, Depends, HTTPException, Request
6-
from loguru import logger
76
from sqlalchemy.exc import SQLAlchemyError
87
from sqlalchemy.orm import Session
98
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
@@ -12,11 +11,25 @@
1211

1312
from app.database.database import get_db
1413
from app.database.models import Event, User, UserEvent
14+
from app.dependencies import logger
1515
from app.dependencies import templates
16-
from app.internal.event import validate_zoom_link
16+
from app.internal.event import (raise_if_zoom_link_invalid, get_invited_emails,
17+
get_uninvited_regular_emails)
1718
from app.internal.utils import create_model
1819
from app.routers.user import create_user
1920

21+
TIME_FORMAT = '%Y-%m-%d %H:%M'
22+
23+
UPDATE_EVENTS_FIELDS = {
24+
'title': str,
25+
'start': dt,
26+
'end': dt,
27+
'content': (str, type(None)),
28+
'location': (str, type(None)),
29+
'category_id': (int, type(None))
30+
}
31+
32+
2033
router = APIRouter(
2134
prefix="/event",
2235
tags=["event"],
@@ -35,10 +48,10 @@ async def create_new_event(request: Request, session=Depends(get_db)):
3548
data = await request.form()
3649
title = data['title']
3750
content = data['description']
38-
start = datetime.strptime(data['start_date'] + ' ' + data['start_time'],
39-
'%Y-%m-%d %H:%M')
40-
end = datetime.strptime(data['end_date'] + ' ' + data['end_time'],
41-
'%Y-%m-%d %H:%M')
51+
start = dt.strptime(data['start_date'] + ' ' + data['start_time'],
52+
TIME_FORMAT)
53+
end = dt.strptime(data['end_date'] + ' ' + data['end_time'],
54+
TIME_FORMAT)
4255
user = session.query(User).filter_by(id=1).first()
4356
user = user if user else create_user(username="u",
4457
password="p",
@@ -52,14 +65,20 @@ async def create_new_event(request: Request, session=Depends(get_db)):
5265
location = data['location']
5366
category_id = data.get('category_id')
5467

68+
invited_emails = get_invited_emails(data['invited'])
69+
uninvited_contacts = get_uninvited_regular_emails(session, owner_id,
70+
title, invited_emails)
71+
5572
if is_zoom:
56-
validate_zoom_link(location)
73+
raise_if_zoom_link_invalid(location)
5774

5875
event = create_event(session, title, start, end, owner_id, content,
59-
location, category_id=category_id)
60-
return RedirectResponse(router.url_path_for('eventview',
61-
event_id=event.id),
62-
status_code=status.HTTP_302_FOUND)
76+
location, invited_emails, category_id=category_id)
77+
message = ''
78+
if uninvited_contacts:
79+
message = f'Forgot to invite {", ".join(uninvited_contacts)} maybe?'
80+
return RedirectResponse(router.url_path_for('eventview', event_id=event.id)
81+
+ f'?{message}', status_code=status.HTTP_302_FOUND)
6382

6483

6584
@router.get("/{event_id}")
@@ -69,20 +88,12 @@ async def eventview(request: Request, event_id: int,
6988
start_format = '%A, %d/%m/%Y %H:%M'
7089
end_format = ('%H:%M' if event.start.date() == event.end.date()
7190
else start_format)
91+
message = request.query_params.get('message', '')
7292
return templates.TemplateResponse("event/eventview.html",
7393
{"request": request, "event": event,
7494
"start_format": start_format,
75-
"end_format": end_format})
76-
77-
78-
UPDATE_EVENTS_FIELDS = {
79-
'title': str,
80-
'start': datetime,
81-
'end': datetime,
82-
'content': (str, type(None)),
83-
'location': (str, type(None)),
84-
'category_id': (int, type(None))
85-
}
95+
"end_format": end_format,
96+
"message": message})
8697

8798

8899
def by_id(db: Session, event_id: int) -> Event:
@@ -115,10 +126,8 @@ def by_id(db: Session, event_id: int) -> Event:
115126
return event
116127

117128

118-
def is_end_date_before_start_date(
119-
start_date: datetime, end_date: datetime) -> bool:
129+
def is_end_date_before_start_date(start_date: dt, end_date: dt) -> bool:
120130
"""Check if the start date is earlier than the end date"""
121-
122131
return start_date > end_date
123132

124133

@@ -190,9 +199,12 @@ def update_event(event_id: int, event: Dict, db: Session
190199
def create_event(db: Session, title: str, start, end, owner_id: int,
191200
content: str = None,
192201
location: str = None,
202+
invitees: List[str] = None,
193203
category_id: int = None):
194204
"""Creates an event and an association."""
195205

206+
invitees_concatenated = ','.join(invitees or [])
207+
196208
event = create_model(
197209
db, Event,
198210
title=title,
@@ -201,6 +213,7 @@ def create_event(db: Session, title: str, start, end, owner_id: int,
201213
content=content,
202214
owner_id=owner_id,
203215
location=location,
216+
invitees=invitees_concatenated,
204217
category_id=category_id,
205218
)
206219
create_model(
@@ -221,13 +234,11 @@ def sort_by_date(events: List[Event]) -> List[Event]:
221234
def get_participants_emails_by_event(db: Session, event_id: int) -> List[str]:
222235
"""Returns a list of all the email address of the event invited users,
223236
by event id."""
224-
225-
return [email[0] for email in db.query(User.email).
226-
select_from(Event).
227-
join(UserEvent, UserEvent.event_id == Event.id).
228-
join(User, User.id == UserEvent.user_id).
229-
filter(Event.id == event_id).
230-
all()]
237+
return [email[0] for email in
238+
db.query(User.email).select_from(Event).join(
239+
UserEvent, UserEvent.event_id == Event.id).join(
240+
User, User.id == UserEvent.user_id).filter(
241+
Event.id == event_id).all()]
231242

232243

233244
def _delete_event(db: Session, event: Event):
@@ -254,7 +265,7 @@ def delete_event(event_id: int,
254265
event = by_id(db, event_id)
255266
participants = get_participants_emails_by_event(db, event_id)
256267
_delete_event(db, event)
257-
if participants and event.start > datetime.now():
268+
if participants and event.start > dt.now():
258269
pass
259270
# TODO: Send them a cancellation notice
260271
# if the deletion is successful

app/templates/event/eventview.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
<body>
99
<div class = "event_view_wrapper">
1010
<!-- Temporary nav layout based on bootstrap -->
11+
<div class="forgot-to-invite">
12+
<h2>{{ message }}</h2>
13+
</div>
1114
<ul class="nav nav-tabs" id="event_view_nav" role="tablist">
1215
<li class="nav-item">
1316
<a class="nav-link active" id="eventdetails-tab" data-toggle="tab" href="#eventdetails" role="tab"

app/templates/event/partials/edit_event_details_tab.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
<input type="text" name="location" placeholder="VC URL/Location">
2222
</div>
2323

24+
<div class="form_row">
25+
<label for="invited">Invited emails: </label>
26+
<input type="text" id="invited" name="invited" placeholder="Invited emails, separated by commas">
27+
</div>
28+
2429
<div class="form_row textarea">
2530
<textarea name="description" placeholder="Description"></textarea>
2631
</div>

tests/test_event.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
'description': 'content',
2121
'color': 'red',
2222
'availability': 'busy',
23-
'privacy': 'public'
23+
'privacy': 'public',
24+
'invited': 'a@a.com,b@b.com'
2425
}
2526

2627
WRONG_EVENT_FORM_DATA = {
@@ -34,7 +35,23 @@
3435
'description': 'content',
3536
'color': 'red',
3637
'availability': 'busy',
37-
'privacy': 'public'
38+
'privacy': 'public',
39+
'invited': 'a@a.com,b@b.com'
40+
}
41+
42+
BAD_EMAILS_FORM_DATA = {
43+
'title': 'test title',
44+
'start_date': '2021-01-28',
45+
'start_time': '15:59',
46+
'end_date': '2021-01-27',
47+
'end_time': '15:01',
48+
'location_type': 'vc_url',
49+
'location': 'https://us02web.zoom.us/j/875384596',
50+
'description': 'content',
51+
'color': 'red',
52+
'availability': 'busy',
53+
'privacy': 'public',
54+
'invited': 'a@a.com,b@b.com,ccc'
3855
}
3956

4057
NONE_UPDATE_OPTIONS = [
@@ -66,6 +83,44 @@ def test_eventview_with_id(event_test_client, session, event):
6683
f'{event_detail} not in view event page'
6784

6885

86+
def test_eventview_without_id(client):
87+
response = client.get("/event/view")
88+
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
89+
90+
91+
def test_eventedit_missing_old_invites(client, user):
92+
response = client.post(client.app.url_path_for('create_new_event'),
93+
data=CORRECT_EVENT_FORM_DATA)
94+
assert response.ok
95+
assert response.status_code == status.HTTP_302_FOUND
96+
97+
different_invitees_event = CORRECT_EVENT_FORM_DATA.copy()
98+
different_invitees_event['invited'] = 'c@c.com,d@d.com'
99+
response = client.post(client.app.url_path_for('create_new_event'),
100+
data=different_invitees_event)
101+
assert response.ok
102+
assert response.status_code == status.HTTP_302_FOUND
103+
for invitee in CORRECT_EVENT_FORM_DATA["invited"].split(","):
104+
assert invitee in response.headers['location']
105+
106+
107+
def test_eventedit_bad_emails(client, user):
108+
response = client.post(client.app.url_path_for('create_new_event'),
109+
data=BAD_EMAILS_FORM_DATA)
110+
assert response.ok
111+
assert response.status_code == status.HTTP_302_FOUND
112+
113+
different_invitees_event = CORRECT_EVENT_FORM_DATA.copy()
114+
different_invitees_event['invited'] = 'c@c.com,d@d.com'
115+
response = client.post(client.app.url_path_for('create_new_event'),
116+
data=different_invitees_event)
117+
assert response.ok
118+
assert response.status_code == status.HTTP_302_FOUND
119+
for invitee in CORRECT_EVENT_FORM_DATA["invited"].split(","):
120+
assert invitee in response.headers['location']
121+
assert 'ccc' not in response.headers['location']
122+
123+
69124
def test_eventedit_post_correct(client, user):
70125
"""
71126
Test create new event successfully.

0 commit comments

Comments
 (0)