Skip to content

Commit 779d72c

Browse files
committed
Merge branch 'develop' of https://github.com/PythonFreeCourse/calendar into feature/corona-time-meetings
2 parents ee6d669 + ab45a02 commit 779d72c

36 files changed

+1119
-267
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ ipython_config.py
8888
# pyenv
8989
.python-version
9090

91+
# pycharm
92+
.idea/
93+
9194
# pipenv
9295
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
9396
# However, in case of collaboration, if having platform-specific dependencies or dependencies

app/config.py.example

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import os
33
from fastapi_mail import ConnectionConfig
44
# flake8: noqa
55

6+
# general
7+
DOMAIN = 'Our-Domain'
68

79
# DATABASE
810
DEVELOPMENT_DATABASE_STRING = "sqlite:///./dev.db"
@@ -12,10 +14,15 @@ MEDIA_DIRECTORY = 'media'
1214
PICTURE_EXTENSION = '.png'
1315
AVATAR_SIZE = (120, 120)
1416

17+
# export
18+
ICAL_VERSION = '2.0'
19+
PRODUCT_ID = '-//Our product id//'
20+
21+
# email
1522
email_conf = ConnectionConfig(
1623
MAIL_USERNAME=os.getenv("MAIL_USERNAME") or "user",
1724
MAIL_PASSWORD=os.getenv("MAIL_PASSWORD") or "password",
18-
MAIL_FROM=os.getenv("MAIL_FROM") or "a@a.com",
25+
MAIL_FROM=os.getenv("MAIL_FROM") or "a@a.com",
1926
MAIL_PORT=587,
2027
MAIL_SERVER="smtp.gmail.com",
2128
MAIL_TLS=True,

app/database/database.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from app import config
88

9-
109
SQLALCHEMY_DATABASE_URL = os.getenv(
1110
"DATABASE_CONNECTION_STRING", config.DEVELOPMENT_DATABASE_STRING)
1211

app/database/models.py

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,76 @@
1+
from datetime import datetime
2+
13
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String
24
from sqlalchemy.orm import relationship
35

4-
from .database import Base
6+
from app.database.database import Base
7+
8+
9+
class UserEvent(Base):
10+
__tablename__ = "user_event"
11+
12+
id = Column(Integer, primary_key=True, index=True)
13+
user_id = Column('user_id', Integer, ForeignKey('users.id'))
14+
event_id = Column('event_id', Integer, ForeignKey('events.id'))
15+
16+
events = relationship("Event", back_populates="participants")
17+
participants = relationship("User", back_populates="events")
18+
19+
def __repr__(self):
20+
return f'<UserEvent ({self.participants}, {self.events})>'
521

622

723
class User(Base):
824
__tablename__ = "users"
925

1026
id = Column(Integer, primary_key=True, index=True)
11-
username = Column(String, unique=True)
12-
email = Column(String, unique=True)
13-
password = Column(String)
27+
username = Column(String, unique=True, nullable=False)
28+
email = Column(String, unique=True, nullable=False)
29+
password = Column(String, nullable=False)
1430
full_name = Column(String)
1531
description = Column(String, default="Happy new user!")
1632
avatar = Column(String, default="profile.png")
33+
is_active = Column(Boolean, default=False)
1734

18-
is_active = Column(Boolean, default=True)
35+
events = relationship("UserEvent", back_populates="participants")
1936

20-
events = relationship(
21-
"Event", cascade="all, delete", back_populates="owner")
37+
def __repr__(self):
38+
return f'<User {self.id}>'
2239

2340

2441
class Event(Base):
2542
__tablename__ = "events"
2643

2744
id = Column(Integer, primary_key=True, index=True)
28-
title = Column(String)
29-
content = Column(String)
45+
title = Column(String, nullable=False)
3046
start = Column(DateTime, nullable=False)
3147
end = Column(DateTime, nullable=False)
48+
content = Column(String)
49+
location = Column(String)
50+
51+
owner = relationship("User")
3252
owner_id = Column(Integer, ForeignKey("users.id"))
53+
participants = relationship("UserEvent", back_populates="events")
54+
55+
def __repr__(self):
56+
return f'<Event {self.id}>'
57+
58+
59+
class Invitation(Base):
60+
__tablename__ = "invitations"
61+
62+
id = Column(Integer, primary_key=True, index=True)
63+
status = Column(String, nullable=False, default="unread")
64+
recipient_id = Column(Integer, ForeignKey("users.id"))
65+
event_id = Column(Integer, ForeignKey("events.id"))
66+
creation = Column(DateTime, default=datetime.now)
67+
68+
recipient = relationship("User")
69+
event = relationship("Event")
3370

34-
owner = relationship("User", back_populates="events")
71+
def __repr__(self):
72+
return (
73+
f'<Invitation '
74+
f'({self.event.owner}'
75+
f'to {self.recipient})>'
76+
)

app/internal/agenda_events.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,35 @@
11
from datetime import date, timedelta
2-
from typing import List, Optional
2+
from typing import List, Optional, Union, Iterator
33

4-
from app.database.models import Event
5-
from app.database.database import SessionLocal
64
import arrow
7-
from sqlalchemy.exc import SQLAlchemyError
5+
from sqlalchemy.orm import Session
6+
7+
from app.database.models import Event
8+
from app.routers.event import sort_by_date
9+
from app.routers.user import get_all_user_events
810

911

1012
def get_events_per_dates(
11-
session: SessionLocal,
13+
session: Session,
1214
user_id: int,
1315
start: Optional[date],
1416
end: Optional[date]
15-
) -> List[Event]:
16-
"""Read from the db. Return a list of all the user events between
17-
the relevant dates."""
17+
) -> Union[Iterator[Event], list]:
18+
"""Read from the db. Return a list of all
19+
the user events between the relevant dates."""
20+
1821
if start > end:
1922
return []
20-
try:
21-
events = (
22-
session.query(Event).filter(Event.owner_id == user_id)
23-
.filter(Event.start.between(start, end + timedelta(days=1)))
24-
.order_by(Event.start).all()
25-
)
26-
except SQLAlchemyError:
27-
return []
28-
else:
29-
return events
23+
24+
return (
25+
filter_dates(
26+
sort_by_date(
27+
get_all_user_events(session, user_id)
28+
),
29+
start,
30+
end,
31+
)
32+
)
3033

3134

3235
def build_arrow_delta_granularity(diff: timedelta) -> List[str]:
@@ -51,5 +54,16 @@ def get_time_delta_string(start: date, end: date) -> str:
5154
granularity = build_arrow_delta_granularity(diff)
5255
duration_string = arrow_end.humanize(
5356
arrow_start, only_distance=True, granularity=granularity
54-
)
57+
)
5558
return duration_string
59+
60+
61+
def filter_dates(
62+
events: List[Event], start: Optional[date],
63+
end: Optional[date]) -> Iterator[Event]:
64+
"""filter events by a time frame."""
65+
66+
yield from (
67+
event for event in events
68+
if start <= event.start.date() <= end
69+
)

app/internal/event.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import re
2+
3+
from fastapi import HTTPException
4+
5+
ZOOM_REGEX = re.compile(r'https://.*?\.zoom.us/[a-z]/.[^.,\b\t\n]+')
6+
7+
8+
def validate_zoom_link(location):
9+
if not ZOOM_REGEX.findall(location):
10+
raise HTTPException(status_code=400,
11+
detail="VC type with no valid zoom link")

app/internal/utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from sqlalchemy.orm import Session
2+
3+
from app.database.models import Base
4+
5+
6+
def save(item, session: Session) -> bool:
7+
"""Commits an instance to the db.
8+
source: app.database.database.Base"""
9+
10+
if issubclass(item.__class__, Base):
11+
session.add(item)
12+
session.commit()
13+
return True
14+
return False
15+
16+
17+
def create_model(session: Session, model_class, **kw):
18+
"""Creates and saves a db model."""
19+
20+
instance = model_class(**kw)
21+
save(instance, session)
22+
return instance

app/main.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
from app.database.database import engine
66
from app.dependencies import (
77
MEDIA_PATH, STATIC_PATH, templates)
8-
from app.routers import agenda, event, profile, email
9-
8+
from app.routers import agenda, event, profile, email, invitation
109

1110
models.Base.metadata.create_all(bind=engine)
1211

@@ -18,12 +17,12 @@
1817
app.include_router(event.router)
1918
app.include_router(agenda.router)
2019
app.include_router(email.router)
20+
app.include_router(invitation.router)
2121

2222

2323
@app.get("/")
2424
async def home(request: Request):
2525
return templates.TemplateResponse("home.html", {
2626
"request": request,
27-
"message": "Hello, World!"
28-
27+
"message": "Hello, World!",
2928
})

app/routers/agenda.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,22 @@
33
from typing import Optional, Tuple
44

55
from fastapi import APIRouter, Depends, Request
6-
from fastapi.templating import Jinja2Templates
76
from sqlalchemy.orm import Session
7+
from starlette.templating import _TemplateResponse
88

99
from app.database.database import get_db
1010
from app.dependencies import templates
1111
from app.internal import agenda_events
1212

13-
1413
router = APIRouter()
1514

1615

1716
def calc_dates_range_for_agenda(
1817
start: Optional[date],
1918
end: Optional[date],
20-
days: Optional[int]
19+
days: Optional[int],
2120
) -> Tuple[date, date]:
22-
"""Create start and end dates eccording to the parameters in the page."""
21+
"""Create start and end dates according to the parameters in the page."""
2322
if days is not None:
2423
start = date.today()
2524
end = start + timedelta(days=days)
@@ -35,8 +34,8 @@ def agenda(
3534
db: Session = Depends(get_db),
3635
start_date: Optional[date] = None,
3736
end_date: Optional[date] = None,
38-
days: Optional[int] = None
39-
) -> Jinja2Templates:
37+
days: Optional[int] = None,
38+
) -> _TemplateResponse:
4039
"""Route for the agenda page, using dates range or exact amount of days."""
4140

4241
user_id = 1 # there is no user session yet, so I use user id- 1.
@@ -58,5 +57,5 @@ def agenda(
5857
"request": request,
5958
"events": events,
6059
"start_date": start_date,
61-
"end_date": end_date
60+
"end_date": end_date,
6261
})

app/routers/event.py

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1-
import re
21
from datetime import datetime as dt
3-
from fastapi import APIRouter, Request, Depends, HTTPException
2+
from operator import attrgetter
3+
from typing import List
4+
5+
from fastapi import APIRouter, Request
6+
from fastapi import Depends
47
from fastapi.responses import RedirectResponse
58
from starlette.status import HTTP_303_SEE_OTHER
69

710
from app.database.database import get_db
8-
from app.database.models import Event, User
11+
from app.database.models import Event
12+
from app.database.models import User
13+
from app.database.models import UserEvent
914
from app.dependencies import templates
10-
11-
ZOOM_REGEX = re.compile(r'https://.*?\.zoom.us/[a-z]/.[^.,\b\t\n]+')
15+
from app.internal.event import validate_zoom_link
16+
from app.internal.utils import create_model
17+
from app.routers.user import create_user
1218

1319
router = APIRouter(
1420
prefix="/event",
@@ -24,7 +30,7 @@ async def eventedit(request: Request):
2430

2531

2632
@router.post("/edit")
27-
async def create_event(request: Request, session=Depends(get_db)):
33+
async def create_new_event(request: Request, session=Depends(get_db)):
2834
data = await request.form()
2935
title = data['title']
3036
content = data['description']
@@ -33,18 +39,17 @@ async def create_event(request: Request, session=Depends(get_db)):
3339
end = dt.strptime(data['end_date'] + ' ' + data['end_time'],
3440
'%Y-%m-%d %H:%M')
3541
user = session.query(User).filter_by(id=1).first()
42+
if not user:
43+
user = create_user("username", "password", "email@email.com", session)
3644
owner_id = user.id
3745
location_type = data['location_type']
3846
is_zoom = location_type == 'vc_url'
3947
location = data['location']
4048

41-
if is_zoom and not ZOOM_REGEX.findall(location):
42-
raise HTTPException(status_code=400,
43-
detail="VC type with no valid zoom link")
44-
event = Event(title=title, content=content, start=start, end=end,
45-
owner_id=owner_id)
46-
session.add(event)
47-
session.commit()
49+
if is_zoom:
50+
validate_zoom_link(location)
51+
52+
event = create_event(session, title, start, end, owner_id, content, location)
4853
return RedirectResponse(f'/event/view/{event.id}',
4954
status_code=HTTP_303_SEE_OTHER)
5055

@@ -53,3 +58,30 @@ async def create_event(request: Request, session=Depends(get_db)):
5358
async def eventview(request: Request, id: int):
5459
return templates.TemplateResponse("event/eventview.html",
5560
{"request": request, "event_id": id})
61+
62+
63+
def create_event(db, title, start, end, owner_id, content=None, location=None):
64+
"""Creates an event and an association."""
65+
66+
event = create_model(
67+
db, Event,
68+
title=title,
69+
start=start,
70+
end=end,
71+
content=content,
72+
owner_id=owner_id,
73+
location=location,
74+
)
75+
create_model(
76+
db, UserEvent,
77+
user_id=owner_id,
78+
event_id=event.id
79+
)
80+
return event
81+
82+
83+
def sort_by_date(events: List[Event]) -> List[Event]:
84+
"""Sorts the events by the start of the event."""
85+
86+
temp = events.copy()
87+
return sorted(temp, key=attrgetter('start'))

0 commit comments

Comments
 (0)