From e6f2e0862e79f422e54d04b8a445f8620a9893de Mon Sep 17 00:00:00 2001 From: Idan Pelled Date: Mon, 8 Feb 2021 17:15:37 +0200 Subject: [PATCH 01/18] feat: create a notification page --- app/database/models.py | 16 +++ app/internal/utils.py | 15 ++- app/main.py | 4 +- app/routers/calendar_grid.py | 2 +- app/routers/event.py | 2 +- app/routers/invitation.py | 67 ------------ app/routers/notification.py | 169 +++++++++++++++++++++++++++++ app/routers/share.py | 12 +- app/static/notification.css | 44 ++++++++ app/templates/base.html | 8 +- app/templates/calendar/layout.html | 7 +- app/templates/invitations.html | 25 ----- app/templates/notification.html | 62 +++++++++++ tests/client_fixture.py | 6 +- tests/test_invitation.py | 16 +-- tests/test_share_event.py | 2 +- 16 files changed, 342 insertions(+), 115 deletions(-) delete mode 100644 app/routers/invitation.py create mode 100644 app/routers/notification.py create mode 100644 app/static/notification.css delete mode 100644 app/templates/invitations.html create mode 100644 app/templates/notification.html diff --git a/app/database/models.py b/app/database/models.py index 3e7c1782..0c067644 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -151,6 +151,22 @@ def __repr__(self): ) +class Message(Base): + __tablename__ = "messages" + + id = Column(Integer, primary_key=True, index=True) + status = Column(String, nullable=False, default="unread") + body = Column(String, nullable=False) + link = Column(String) + recipient_id = Column(Integer, ForeignKey("users.id")) + creation = Column(DateTime, default=datetime.now) + + recipient = relationship("User") + + def __repr__(self): + return f'' + + class Quote(Base): __tablename__ = "quotes" diff --git a/app/internal/utils.py b/app/internal/utils.py index 90a647b8..8d3afb39 100644 --- a/app/internal/utils.py +++ b/app/internal/utils.py @@ -1,6 +1,7 @@ -from sqlalchemy.orm import Session +from typing import Union, Type -from app.database.models import Base +from app.database.models import Base, Message, Invitation +from sqlalchemy.orm import Session def save(item, session: Session) -> bool: @@ -20,3 +21,13 @@ def create_model(session: Session, model_class, **kw): instance = model_class(**kw) save(instance, session) return instance + + +def mark_as_read( + session: Session, + message: Union[Message, Invitation] +) -> None: + """""" + print(message, '!!!') + message.status = 'accepted' + save(message, session=session) diff --git a/app/main.py b/app/main.py index 08b27769..df4b5b63 100644 --- a/app/main.py +++ b/app/main.py @@ -9,7 +9,7 @@ from app.internal import daily_quotes, json_data_loader from app.routers import ( agenda, calendar, categories, dayview, email, - event, invitation, profile, search, telegram, whatsapp + event, notification, profile, search, telegram, whatsapp ) from app.telegram.bot import telegram_bot @@ -41,7 +41,7 @@ def create_tables(engine, psql_environment): dayview.router, email.router, event.router, - invitation.router, + notification.router, profile.router, search.router, telegram.router, diff --git a/app/routers/calendar_grid.py b/app/routers/calendar_grid.py index d8ceba23..0c8d3444 100644 --- a/app/routers/calendar_grid.py +++ b/app/routers/calendar_grid.py @@ -8,7 +8,7 @@ MONTH_BLOCK: int = 6 -locale.setlocale(locale.LC_TIME, ("en", "UTF-8")) +locale.setlocale(locale.LC_ALL, "en_US.UTF-8") class Day: diff --git a/app/routers/event.py b/app/routers/event.py index fd460583..dc42a079 100644 --- a/app/routers/event.py +++ b/app/routers/event.py @@ -40,7 +40,7 @@ async def create_new_event(request: Request, session=Depends(get_db)): end = datetime.strptime(data['end_date'] + ' ' + data['end_time'], '%Y-%m-%d %H:%M') user = session.query(User).filter_by(id=1).first() - user = user if user else create_user("u", "p", "e@mail.com", session) + user = user if user else create_user("u", "p", "e@mail.com", 'en', session) owner_id = user.id location_type = data['location_type'] is_zoom = location_type == 'vc_url' diff --git a/app/routers/invitation.py b/app/routers/invitation.py deleted file mode 100644 index f92d7cd3..00000000 --- a/app/routers/invitation.py +++ /dev/null @@ -1,67 +0,0 @@ -from typing import List, Union - -from fastapi import APIRouter, Depends, Request -from fastapi.responses import RedirectResponse -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session -from starlette.status import HTTP_302_FOUND -from starlette.templating import Jinja2Templates - -from app.database.database import get_db -from app.database.models import Invitation -from app.routers.share import accept - -templates = Jinja2Templates(directory="app/templates") - -router = APIRouter( - prefix="/invitations", - tags=["invitation"], - dependencies=[Depends(get_db)] -) - - -@router.get("/") -def view_invitations(request: Request, db: Session = Depends(get_db)): - return templates.TemplateResponse("invitations.html", { - "request": request, - # TODO: create current user - # 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(session=db), - }) - - -@router.post("/") -async def accept_invitations( - request: Request, - db: Session = Depends(get_db) -): - data = await request.form() - invite_id = list(data.values())[0] - - invitation = get_invitation_by_id(invite_id, session=db) - accept(invitation, db) - - url = router.url_path_for("view_invitations") - return RedirectResponse(url=url, status_code=HTTP_302_FOUND) - - -def get_all_invitations(session: Session, **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, session: Session -) -> Union[Invitation, None]: - """Returns a invitation by an id. - if id does not exist, returns None.""" - - return session.query(Invitation).filter_by(id=invitation_id).first() diff --git a/app/routers/notification.py b/app/routers/notification.py new file mode 100644 index 00000000..0bdbf91c --- /dev/null +++ b/app/routers/notification.py @@ -0,0 +1,169 @@ +from operator import attrgetter +from typing import List, Union + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import RedirectResponse +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session +from starlette.status import HTTP_302_FOUND + +from app.database.database import get_db +from app.database.models import Invitation, Message +from app.dependencies import templates +from app.internal.utils import mark_as_read +from app.routers.share import accept, decline +from tests.utils import create_model + +NOTIFICATION_TYPE = Union[Invitation, Message] + + +router = APIRouter( + prefix="/notification", + tags=["notification"], + dependencies=[Depends(get_db)] +) + + +@router.get("/") +async def view_notifications(request: Request, db: Session = Depends(get_db)): + return templates.TemplateResponse("notification.html", { + "request": request, + "notifications": get_all_notifications(session=db, user_id=1), + }) + + +@router.post("/accept_invitations") +async def accept_invitations( + request: Request, + db: Session = Depends(get_db) +): + data = await request.form() + invite_id = list(data.values())[0] + + invitation = get_invitation_by_id(invite_id, session=db) + accept(invitation, db) + + url = router.url_path_for("view_notifications") + return RedirectResponse(url=url, status_code=HTTP_302_FOUND) + + +@router.post("/decline_invitations") +async def decline_invitations( + request: Request, + db: Session = Depends(get_db) +): + data = await request.form() + invite_id = list(data.values())[0] + + invitation = get_invitation_by_id(invite_id, session=db) + decline(invitation, db) + + url = router.url_path_for("view_notifications") + return RedirectResponse(url=url, status_code=HTTP_302_FOUND) + + +@router.post("/mark_as_read") +async def mark_message_as_read( + request: Request, + db: Session = Depends(get_db) +): + data = await request.form() + message_id = list(data.values())[0] + + message = await get_message_by_id(message_id, session=db) + mark_as_read(db, message) + + url = router.url_path_for("view_notifications") + return RedirectResponse(url=url, status_code=HTTP_302_FOUND) + + +@router.get("/mark_all_as_read") +async def mark_all_as_read( + db: Session = Depends(get_db) +): + user_id = 1 + for m in get_all_messages(db, user_id): + if m.status == 'unread': + mark_as_read(db, m) + + url = router.url_path_for("view_notifications") + return RedirectResponse(url=url, status_code=HTTP_302_FOUND) + + +async def get_message_by_id( + message_id: int, session: Session +) -> Union[Message, None]: + """Returns a invitation by an id. + if id does not exist, returns None.""" + + return session.query(Message).filter_by(id=message_id).one() + + +def get_all_notifications(session: Session, user_id: int): + """""" + + invitations: List[Invitation] = ( + get_all_invitations(session, recipient_id=user_id)) + + messages: List[Message] = ( + get_all_messages(session, user_id)) + + notifications: List[NOTIFICATION_TYPE] = invitations + messages + + return sort_notifications(notifications) + + +def sort_notifications( + notification: List[NOTIFICATION_TYPE] +) -> List[NOTIFICATION_TYPE]: + """Sorts the notifications by the creation date.""" + + temp = notification.copy() + return sorted(temp, key=attrgetter('creation'), reverse=True) + + +def create_notification( + session: Session, + msg: str, + recipient_id: int, + link=None +) -> None: + """""" + + create_model( + session, + Message, + body=msg, + recipient_id=recipient_id, + link=link + ) + + +def get_all_messages( + session: Session, + recipient_id: int +) -> List[Message]: + """""" + + condition = Message.recipient_id == recipient_id + return session.query(Message).filter(condition).all() + + +def get_all_invitations(session: Session, **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, session: Session +) -> Union[Invitation, None]: + """Returns a invitation by an id. + if id does not exist, returns None.""" + + return session.query(Invitation).filter_by(id=invitation_id).first() diff --git a/app/routers/share.py b/app/routers/share.py index 40fb8f3c..c7b5d025 100644 --- a/app/routers/share.py +++ b/app/routers/share.py @@ -3,7 +3,7 @@ from sqlalchemy.orm import Session from app.database.models import Event, Invitation, UserEvent -from app.internal.utils import save +from app.internal.utils import save, mark_as_read from app.routers.export import event_to_ical from app.routers.user import does_user_exist, get_users @@ -62,6 +62,13 @@ def send_in_app_invitation( return True +def decline(invitation: Invitation, session: Session) -> None: + """""" + + invitation.status = 'declined' + save(invitation, session=session) + + def accept(invitation: Invitation, session: Session) -> None: """Accepts an invitation by creating an UserEvent association that represents @@ -71,8 +78,7 @@ def accept(invitation: Invitation, session: Session) -> None: user_id=invitation.recipient.id, event_id=invitation.event.id ) - invitation.status = 'accepted' - save(invitation, session=session) + mark_as_read(session, invitation) save(association, session=session) diff --git a/app/static/notification.css b/app/static/notification.css new file mode 100644 index 00000000..cdf2c661 --- /dev/null +++ b/app/static/notification.css @@ -0,0 +1,44 @@ +/* general */ +#main { + width: 70%; + margin: 0 25% 0 5%; +} + + +/* buttons */ +.notification-btn { + background-color: transparent; + border: none; +} + +.notification-btn:focus { + outline: 0; +} + +.btn-read, .btn-accept { + color: green; +} + +.btn-decline { + color: red; +} + +.btn-read:focus, .btn-accept:focus { + color: black; +} + +.btn-decline:focus { + color: black; +} + + +/* form */ +.notification-form { + display: inline-block; +} + + +/* icons */ +.icon { + font-size: 1.5rem; +} diff --git a/app/templates/base.html b/app/templates/base.html index 16aaf490..009bf805 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -2,6 +2,7 @@ {% block head %} + {% endblock %} @@ -10,7 +11,7 @@ Calendar - {% endblock %} +