Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8928b17
update ORM and added export for an event
IdanPelled Jan 12, 2021
79874a7
update ORM and added export for an event
IdanPelled Jan 12, 2021
bc53da7
Merge branch 'main' of https://github.com/PythonFreeCourse/calendar i…
IdanPelled Jan 12, 2021
8f4e690
Added in app shareable events
IdanPelled Jan 13, 2021
25d1b80
upstream merge
IdanPelled Jan 13, 2021
8fb1035
docs: add type annotation and fix folder structure
IdanPelled Jan 13, 2021
149b571
docs: add type annotation and fix folder structure
IdanPelled Jan 13, 2021
be96e5d
docs: fix documentation
IdanPelled Jan 14, 2021
9b2a861
add: tests
IdanPelled Jan 15, 2021
aa654ac
add: timezone support
IdanPelled Jan 16, 2021
5227c11
add: session management
IdanPelled Jan 16, 2021
56877c6
fix bug
IdanPelled Jan 16, 2021
eda3c31
split conftest file
IdanPelled Jan 16, 2021
c24830a
split conftest file
IdanPelled Jan 16, 2021
69f81e1
move "utils" folder into "internal" folder
IdanPelled Jan 17, 2021
0dc1ae6
change file structure
IdanPelled Jan 18, 2021
868ba44
Merge branch 'develop' into feature/shareable-event
IdanPelled Jan 18, 2021
7c2edd0
Merge branch 'develop' feature/shareable-event
IdanPelled Jan 19, 2021
3883d5e
feat: enable invited users to view events
IdanPelled Jan 19, 2021
e39c43d
Merge branch 'develop into feature/shareable-event
IdanPelled Jan 19, 2021
6add784
feat: flake8 changes
IdanPelled Jan 19, 2021
cccd7b7
fix: requirements bug
IdanPelled Jan 19, 2021
5faf0ba
fix: requirements bug
IdanPelled Jan 19, 2021
bdd4d8b
feat: flake8 changes
IdanPelled Jan 19, 2021
d629a25
add: tests
IdanPelled Jan 19, 2021
046162d
feat: flake8 changes
IdanPelled Jan 19, 2021
0bcbff8
add: tests
IdanPelled Jan 20, 2021
9462602
feat: flake8 changes
IdanPelled Jan 20, 2021
053b156
edit: file structure
IdanPelled Jan 20, 2021
7684719
edit: file structure
IdanPelled Jan 20, 2021
8b97c29
feat: add route tests
IdanPelled Jan 20, 2021
ba09ff0
fix: test bug
IdanPelled Jan 21, 2021
44f30a0
Merge branch 'develop' into feature/shareable-event
IdanPelled Jan 21, 2021
3400fe9
remove: config.py
IdanPelled Jan 21, 2021
e0c75f5
fix: type annotation
IdanPelled Jan 22, 2021
3ec5f37
add: minor changes
IdanPelled Jan 22, 2021
b9198d5
feat: flake8 changes
IdanPelled Jan 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ ipython_config.py
# pyenv
.python-version

# pycharm
.idea/

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
Expand Down
4 changes: 4 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from app.database.database import SessionLocal

DOMAIN = 'Our-Domain'
session = SessionLocal()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should find a better way to manage the session (in terms of closing/opening it)

62 changes: 53 additions & 9 deletions app/database/models.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,73 @@
from datetime import datetime

from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.orm import relationship

from .database import Base
from app.database.database import Base


class UserEvent(Base):
__tablename__ = "user_event"

id = Column(Integer, primary_key=True, index=True)
user_id = Column('user_id', Integer, ForeignKey('users.id'))
event_id = Column('event_id', Integer, ForeignKey('events.id'))

events = relationship("Event", back_populates="participants")
participants = relationship("User", back_populates="events")

def __repr__(self):
return f'<UserEvent ({self.participants}, {self.events})>'


class User(Base):
__tablename__ = "users"

id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True)
email = Column(String, unique=True)
password = Column(String)
username = Column(String, unique=True, nullable=False)
email = Column(String, unique=True, nullable=False)
password = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be wiser to set it as False if its about being connected to the calendar

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree


events = relationship(
"Event", cascade="all, delete", back_populates="owner")
events = relationship("UserEvent", back_populates="participants")

def __repr__(self):
return f'<User {self.id}>'


class Event(Base):
__tablename__ = "events"

id = Column(Integer, primary_key=True, index=True)
title = Column(String)
title = Column(String, nullable=False)
start = Column(DateTime, nullable=False)
end = Column(DateTime, nullable=False)
content = Column(String)
date = Column(DateTime)
location = Column(String)

owner = relationship("User")
owner_id = Column(Integer, ForeignKey("users.id"))
participants = relationship("UserEvent", back_populates="events")

def __repr__(self):
return f'<Event {self.id}>'


class Invitation(Base):
__tablename__ = "invitations"

id = Column(Integer, primary_key=True, index=True)
status = Column(String, nullable=False, default="unread")
recipient_id = Column(Integer, ForeignKey("users.id"))
event_id = Column(Integer, ForeignKey("events.id"))
creation = Column(DateTime, default=datetime.now)

recipient = relationship("User")
event = relationship("Event")

owner = relationship("User", back_populates="events")
def __repr__(self):
return (
f'<Invitation '
f'({self.event.owner}'
f'to {self.recipient})>'
)
79 changes: 79 additions & 0 deletions app/internal/share_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from typing import List, Dict, Union
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this file should be in app/routers/share.py?

https://fastapi.tiangolo.com/tutorial/bigger-applications/


from app.config import session
from app.database.models import Event, Invitation, UserEvent
from app.utils.export import event_to_ical
from app.utils.user import dose_user_exist, get_users
from app.utils.utils import save


def sort_emails(emails: List[str]) -> Dict[str, List[str]]:
"""Sorts emails to registered
and unregistered users."""

emails_dict = {'registered': [], 'unregistered': []} # type: ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use _dict prefix. In Python, we usually try to avoid Hungarian notation

for email in emails:

if dose_user_exist(email=email):
emails_dict['registered'] += [email]
else:
emails_dict['unregistered'] += [email]

return emails_dict


def send_email_invitation(
participants: List[str],
event: Event
):
"""Sends an email with an invitation."""

ical_invitation = event_to_ical(event, participants)
for participant in participants:
# sends an email
pass


def send_in_app_invitation(
participants: List[str],
event: Event
) -> Union[bool, None]:
"""Sends an in-app invitation for registered users."""

for participant in participants:
# email is unique
recipient = get_users(email=participant)[0]

if recipient.id != event.owner.id:
session.add(Invitation(recipient=recipient, event=event))

else:
# if user tries to send to himself.
session.rollback()
return None

session.commit()
return True


def accept(invitation: Invitation) -> None:
"""Accepts an invitation by creating an
UserEvent association that represents
participantship at the event."""

association = UserEvent(
participants=invitation.recipient,
events=invitation.event
)
invitation.status = 'accepted'
save(invitation)
save(association)


def share(event: Event, participants: List[str]) -> None:
"""Sends invitations to all event participants."""

registered, unregistered = sort_emails(participants).values()

send_in_app_invitation(registered, event)
send_email_invitation(unregistered, event)
36 changes: 31 additions & 5 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
from fastapi import FastAPI, Request
from fastapi import FastAPI, Form, Request
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

from starlette.status import HTTP_302_FOUND

app = FastAPI()
from app.database.database import Base, engine
from app.internal.share_event import accept
from app.utils.invitation import get_all_invitations, get_invitation_by_id

app.mount("/static", StaticFiles(directory="static"), name="static")

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")

Base.metadata.create_all(bind=engine)


@app.get("/")
def home(request: Request):
Expand All @@ -18,15 +25,34 @@ def home(request: Request):
})


@app.get("/invitations")
def view_invitations(request: Request):
return templates.TemplateResponse("requests.html", {
"request": request,
# recipient_id should be the current user
# but because we don't have one yet,
# "get_all_invitations" returns all invitations
"invitations": get_all_invitations(),
"message": "Hello, World!"
})


@app.post("/invitations")
async def accept_invitations(invite_id: int = Form(...)):
invitation = get_invitation_by_id(invite_id)
accept(invitation)
return RedirectResponse("/invitations", status_code=HTTP_302_FOUND)


@app.get("/profile")
def profile(request: Request):

# Get relevant data from database
upcouming_events = range(5)
upcoming_events = range(5)
current_username = "Chuck Norris"

return templates.TemplateResponse("profile.html", {
"request": request,
"username": current_username,
"events": upcouming_events
"events": upcoming_events
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add trailing ,

})
25 changes: 25 additions & 0 deletions app/templates/requests.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{% extends "base.html" %}


{% block content %}

<div class="container mt-4">
<h1>{{message}}</h1>
</div>

{% if invitations %}
<div>
{% for i in invitations %}
<form action="\invitations" method="post">
<span>{{ i.event.owner.username }} - {{ i.event.title }} ({{ i.event.start }})</span>
<input type="hidden" value="{{ i.id }}" name="invite_id">
<input type="submit" value="Accept">
</form>
{% endfor %}
</div>
{% else %}
<span>You don't have any invitations.</span>
{% endif %}


{% endblock %}
Empty file added app/utils/__init__.py
Empty file.
2 changes: 2 additions & 0 deletions app/utils/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ICS_VERSION = '2.0'
PRODUCT_ID = '-//Our product id//'
Empty file added app/utils/event.py
Empty file.
81 changes: 81 additions & 0 deletions app/utils/export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from datetime import datetime
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference between files under internal/ and utils/ folders?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. Maybe should be under app/routers/export.py?

from typing import List

from icalendar import Calendar, Event, vCalAddress

from app.config import DOMAIN
from app.database.models import Event as UserEvent
from app.utils.config import ICS_VERSION, PRODUCT_ID


def generate_id(event: UserEvent) -> bytes:
"""Creates an unique id from:
- event id
- event start time
- event end time
- our domain.
"""

return (
str(event.id)
+ event.start.strftime('%Y%m%d')
+ event.end.strftime('%Y%m%d')
+ f'@{DOMAIN}'
).encode()


def create_ical_calendar():
"""Creates an ical calendar,
and adds the required information"""

cal = Calendar()
cal.add('version', ICS_VERSION)
cal.add('prodid', PRODUCT_ID)

return cal


def create_ical_event(user_event):
"""Creates an ical event,
and adds the event information"""

ievent = Event()
data = [
('organizer', vCalAddress(user_event.owner.email)),
('uid', generate_id(user_event)),
('dtstart', user_event.start),
('dtstamp', datetime.now()),
('dtend', user_event.end),
('summary', user_event.title),
]

for param in data:
ievent.add(*param)

return ievent


def add_attendees(ievent, attendees: list):
"""Adds attendees for the event."""

for attendee in attendees:
ievent.add(
'attendee',
vCalAddress(f'MAILTO:{attendee}'),
encode=0
)

return ievent


def event_to_ical(user_event: UserEvent, attendees: List[str]) -> bytes:
"""Returns an ical event,
given an "UserEvent" instance
and a list of email"""

ical = create_ical_calendar()
ievent = create_ical_event(user_event)
ievent = add_attendees(ievent, attendees)
ical.add_component(ievent)

return ical.to_ical()
23 changes: 23 additions & 0 deletions app/utils/invitation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import List

from sqlalchemy.exc import SQLAlchemyError

from app.config import session
from app.database.models import Invitation


def get_all_invitations(**param) -> List[Invitation]:
"""Returns all invitations filter by param."""

try:
invitations = list(session.query(Invitation).filter_by(**param))
except SQLAlchemyError:
return []
else:
return invitations


def get_invitation_by_id(invitation_id: int) -> Invitation:
"""Returns a invitation by an id."""

return session.query(Invitation).filter_by(id=invitation_id).first()
Loading