-
Notifications
You must be signed in to change notification settings - Fork 52
Feature/shareable event #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
8928b17
79874a7
bc53da7
8f4e690
25d1b80
8fb1035
149b571
be96e5d
9b2a861
aa654ac
5227c11
56877c6
eda3c31
c24830a
69f81e1
0dc1ae6
868ba44
7c2edd0
3883d5e
e39c43d
6add784
cccd7b7
5faf0ba
bdd4d8b
d629a25
046162d
0bcbff8
9462602
053b156
7684719
8b97c29
ba09ff0
44f30a0
3400fe9
e0c75f5
3ec5f37
b9198d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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() | ||
|
||
| 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) | ||
|
||
|
|
||
| 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})>' | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| from typing import List, Dict, Union | ||
|
||
|
|
||
| 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 | ||
yammesicka marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| and unregistered users.""" | ||
|
|
||
| emails_dict = {'registered': [], 'unregistered': []} # type: ignore | ||
|
||
| 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. | ||
yammesicka marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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) | ||
| 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): | ||
|
|
@@ -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 | ||
|
||
| }) | ||
| 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 %} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| ICS_VERSION = '2.0' | ||
yammesicka marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| PRODUCT_ID = '-//Our product id//' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| from datetime import datetime | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the difference between files under
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
yammesicka marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| - 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() | ||
| 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() | ||
yammesicka marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.