Skip to content

Commit

Permalink
feat: introduce linked calendars
Browse files Browse the repository at this point in the history
  • Loading branch information
dantetemplar committed Oct 18, 2023
1 parent 871113c commit b5c11ec
Show file tree
Hide file tree
Showing 12 changed files with 341 additions and 20 deletions.
31 changes: 31 additions & 0 deletions alembic/versions/2db361ce1d3d_add_color_for_linked_calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Add color for linked calendar
Revision ID: 2db361ce1d3d
Revises: a9f09ae0208d
Create Date: 2023-10-16 23:07:37.504254
"""
# ruff: noqa: E501
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "2db361ce1d3d"
down_revision: Union[str, None] = "a9f09ae0208d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("linked_calendars", sa.Column("color", sa.String(length=255), nullable=True))
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("linked_calendars", "color")
# ### end Alembic commands ###
100 changes: 100 additions & 0 deletions alembic/versions/85bf4b0af84d_add_linked_calendars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Add linked calendars
Revision ID: 85bf4b0af84d
Revises: 0f714fc8defc
Create Date: 2023-10-16 22:34:45.234130
"""
# ruff: noqa: E501
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision: str = "85bf4b0af84d"
down_revision: Union[str, None] = "0f714fc8defc"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"linked_calendars",
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("alias", sa.String(length=255), nullable=False),
sa.Column("url", sa.String(), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=255), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.drop_constraint("workshops_x_ownerships_object_id_fkey", "workshops_x_ownerships")
op.drop_table("workshop_checkins")
op.drop_table("timeslots")
op.drop_table("workshops")
op.drop_table("workshops_x_ownerships")
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"workshops_x_ownerships",
sa.Column("object_id", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("user_id", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("role_alias", sa.VARCHAR(length=255), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(["object_id"], ["workshops.id"], name="workshops_x_ownerships_object_id_fkey"),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], name="workshops_x_ownerships_user_id_fkey"),
sa.PrimaryKeyConstraint("object_id", "user_id", name="workshops_x_ownerships_pkey"),
)
op.create_table(
"workshops",
sa.Column("alias", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("date", sa.DATE(), autoincrement=False, nullable=False),
sa.Column("speaker", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("capacity", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("comment", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("location", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column(
"id",
sa.INTEGER(),
server_default=sa.text("nextval('workshops_id_seq'::regclass)"),
autoincrement=True,
nullable=False,
),
sa.Column("name", sa.VARCHAR(length=255), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint("id", name="workshops_pkey"),
sa.UniqueConstraint("alias", name="workshops_alias_key"),
postgresql_ignore_search_path=False,
)
op.create_table(
"timeslots",
sa.Column("workshop_id", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("sequence", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column("start", postgresql.TIME(), autoincrement=False, nullable=False),
sa.Column("end", postgresql.TIME(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(["workshop_id"], ["workshops.id"], name="timeslots_workshop_id_fkey"),
sa.PrimaryKeyConstraint("sequence", name="timeslots_pkey"),
)
op.create_table(
"workshop_checkins",
sa.Column("workshop_id", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("user_id", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("timeslot_sequence", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column(
"dtstamp", postgresql.TIMESTAMP(), server_default=sa.text("now()"), autoincrement=False, nullable=False
),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], name="workshop_checkins_user_id_fkey"),
sa.ForeignKeyConstraint(["workshop_id"], ["workshops.id"], name="workshop_checkins_workshop_id_fkey"),
sa.PrimaryKeyConstraint("workshop_id", "user_id", "timeslot_sequence", name="workshop_checkins_pkey"),
)
op.drop_table("linked_calendars")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Add constraint for linked calendars
Revision ID: a9f09ae0208d
Revises: 85bf4b0af84d
Create Date: 2023-10-16 22:53:08.602036
"""
# ruff: noqa: E501
from typing import Sequence, Union

from alembic import op


# revision identifiers, used by Alembic.
revision: str = "a9f09ae0208d"
down_revision: Union[str, None] = "85bf4b0af84d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint("unique_user_id_alias", "linked_calendars", ["user_id", "alias"])
op.drop_constraint("tags_alias_type_key", "tags", type_="unique")
op.create_unique_constraint("unique_alias", "tags", ["alias", "type"])
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("unique_alias", "tags", type_="unique")
op.create_unique_constraint("tags_alias_type_key", "tags", ["alias", "type"])
op.drop_constraint("unique_user_id_alias", "linked_calendars", type_="unique")
# ### end Alembic commands ###
27 changes: 27 additions & 0 deletions src/app/users/routes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from fastapi import HTTPException
from sqlalchemy.exc import IntegrityError

from src.app.dependencies import (
EVENT_GROUP_REPOSITORY_DEPENDENCY,
USER_REPOSITORY_DEPENDENCY,
Expand All @@ -12,6 +15,7 @@
EventGroupNotFoundException,
)
from src.schemas import ViewUser
from src.schemas.linked import LinkedCalendarView, LinkedCalendarCreate


@router.get(
Expand Down Expand Up @@ -105,3 +109,26 @@ async def hide_favorite(
updated_user = await user_repository.set_hidden(user_id=user_id, group_id=group_id, hide=hide)
updated_user: ViewUser
return updated_user


@router.post(
"/me/linked",
responses={
200: {"description": "Linked calendar added successfully"},
**IncorrectCredentialsException.responses,
**NoCredentialsException.responses,
},
)
async def link_calendar(
linked_calendar: LinkedCalendarCreate,
user_id: CURRENT_USER_ID_DEPENDENCY,
user_repository: USER_REPOSITORY_DEPENDENCY,
) -> LinkedCalendarView:
"""
Add linked calendar to current user
"""
try:
calendar = await user_repository.link_calendar(user_id, linked_calendar)
return calendar
except IntegrityError:
raise HTTPException(status_code=409, detail="Calendar with this alias already exists")
7 changes: 6 additions & 1 deletion src/repositories/users/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING


if TYPE_CHECKING:
from src.schemas import CreateUser, ViewUser
from src.schemas import CreateUser, ViewUser, LinkedCalendarCreate, LinkedCalendarView


class AbstractUserRepository(metaclass=ABCMeta):
Expand Down Expand Up @@ -46,3 +47,7 @@ async def remove_favorite(self, user_id: int, favorite_id: int) -> "ViewUser":
@abstractmethod
async def set_hidden(self, user_id: int, group_id: int, hide: bool = True) -> "ViewUser":
...

@abstractmethod
async def link_calendar(self, user_id: int, calendar: "LinkedCalendarCreate") -> "LinkedCalendarView":
...
47 changes: 32 additions & 15 deletions src/repositories/users/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
from sqlalchemy.sql.expression import exists

from src.exceptions import DBEventGroupDoesNotExistInDb
from src.repositories.crud import CRUDFactory
from src.repositories.crud import CRUDFactory, AbstractCRUDRepository
from src.repositories.users.abc import AbstractUserRepository
from src.schemas import LinkedCalendarView
from src.schemas.linked import LinkedCalendarCreate
from src.schemas.users import CreateUser, ViewUser, UpdateUser
from src.storages.sql.models import User, EventGroup, UserXFavoriteEventGroup
from src.storages.sql.models import User, EventGroup, UserXFavoriteEventGroup, LinkedCalendar
from src.storages.sql.storage import AbstractSQLAlchemyStorage


Expand All @@ -26,7 +28,17 @@ def SELECT_USER_BY_ID(id_: int):
)


CRUD = CRUDFactory(User, CreateUser, ViewUser, UpdateUser, get_options=(selectinload(User.favorites_association),))
_get_options = (
selectinload(User.favorites_association),
selectinload(User.linked_calendars),
)


CRUD: AbstractCRUDRepository[
CreateUser,
ViewUser,
UpdateUser,
] = CRUDFactory(User, CreateUser, ViewUser, UpdateUser, get_options=_get_options)

MIN_USER_ID = 100_000
MAX_USER_ID = 999_999
Expand Down Expand Up @@ -71,9 +83,7 @@ async def batch_create_or_read(self, users: list[CreateUser]) -> list[ViewUser]:
q = (
q.on_conflict_do_update(index_elements=[User.email], set_={"id": User.id})
.returning(User)
.options(
selectinload(User.favorites_association),
)
.options(*_get_options)
)
db_users = await session.scalars(q)
await session.commit()
Expand All @@ -86,9 +96,7 @@ async def create_or_update(self, user: CreateUser) -> ViewUser:
q = (
q.on_conflict_do_update(index_elements=[User.email], set_={**q.excluded, "id": User.id})
.returning(User)
.options(
selectinload(User.favorites_association),
)
.options(*_get_options)
)
user = await session.scalar(q)
await session.commit()
Expand Down Expand Up @@ -165,13 +173,22 @@ async def set_hidden(self, user_id: int, group_id: int, hide: bool = True) -> "V
event_group.hidden = hide

# from table
q = select(User).where(User.id == user_id).options(*_get_options)
user = await session.scalar(q)
await session.commit()
return ViewUser.from_orm(user)

async def link_calendar(self, user_id: int, calendar: "LinkedCalendarCreate") -> "LinkedCalendarView":
async with self._create_session() as session:
q = (
select(User)
.where(User.id == user_id)
.options(
selectinload(User.favorites_association),
insert(LinkedCalendar)
.values(
user_id=user_id,
**calendar.dict(),
)
.returning(LinkedCalendar)
)
user = await session.scalar(q)

calendar = await session.scalar(q)
await session.commit()
return ViewUser.from_orm(user)
return LinkedCalendarView.from_orm(calendar)
2 changes: 2 additions & 0 deletions src/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"Ownership", "OwnershipEnum",
# TODO: Implement worshops
# "CreateWorkshop", "ViewWorkshop", "CheckIn", "Timeslot", "CreateTimeslot",
"LinkedCalendarCreate", "LinkedCalendarView", "LinkedCalendarUpdate"
]
# fmt: on

Expand All @@ -24,6 +25,7 @@
)
from src.schemas.events import CreateEvent, ViewEvent, UpdateEvent, AddEventPatch, ViewEventPatch, UpdateEventPatch
from src.schemas.users import CreateUser, ViewUser, UpdateUser
from src.schemas.linked import LinkedCalendarCreate, LinkedCalendarView, LinkedCalendarUpdate
from src.schemas.tags import CreateTag, ViewTag, UpdateTag
from src.schemas.ownership import Ownership, OwnershipEnum

Expand Down
51 changes: 51 additions & 0 deletions src/schemas/linked.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
__all__ = ["LinkedCalendarView", "LinkedCalendarCreate", "LinkedCalendarUpdate"]


from typing import Optional


from pydantic import BaseModel


class LinkedCalendarView(BaseModel):
"""
Represents a linked calendar instance from the database excluding sensitive information.
"""

id: int
user_id: int
alias: str
url: str
name: Optional[str] = None
description: Optional[str] = None
color: Optional[str] = None
is_active: bool = True

class Config:
orm_mode = True


class LinkedCalendarCreate(BaseModel):
"""
Represents a linked calendar instance to be created.
"""

alias: str
url: str
name: Optional[str] = None
description: Optional[str] = None
color: Optional[str] = None
is_active: bool = True


class LinkedCalendarUpdate(BaseModel):
"""
Represents a linked calendar instance to be updated.
"""

alias: Optional[str] = None
url: Optional[str] = None
name: Optional[str] = None
description: Optional[str] = None
color: Optional[str] = None
is_active: Optional[bool] = None
Loading

1 comment on commit b5c11ec

@github-actions
Copy link

Choose a reason for hiding this comment

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

Coverage

Coverage Report
FileStmtsMissCoverMissing
src
   exceptions.py35586%11, 42, 56, 84, 98
   main.py27967%46, 52–55, 62–67
src/app
   dependencies.py47198%29
src/app/auth
   common.py281739%12–21, 25–32, 36–44, 49–50
   dependencies.py19668%33–38, 46
   jwt.py512159%26–28, 32–36, 57–70, 79–81
src/app/event_groups
   routes.py703747%39–45, 65–76, 116–120, 140, 188–224
src/app/ics
   routes.py876525%36–45, 67–90, 115–128, 135–163, 167–187
src/app/root
   routes.py13192%48
src/app/users
   routes.py351849%36–38, 58–63, 82–84, 106–111, 130–134
src/app/workshops
   __init__.py550%1–9
   routes.py16160%1–63
src/repositories
   crud.py941089%89, 105, 108, 125, 142, 156–157, 174–180
   ownership.py13746%13–29
src/repositories/event_groups
   repository.py65986%52–55, 70–71, 88–90
src/repositories/predefined
   repository.py1433377%27, 53, 55, 62, 67–68, 147–148, 155, 162–168, 172–202
src/repositories/tags
   repository.py76396%81–83
src/repositories/users
   repository.py1041189%144–158, 182–194
src/repositories/workshops
   __init__.py330%1–4
   abc.py440%1–10
   repository.py34340%1–68
src/schemas
   events.py71692%48, 56, 79, 98–100
   ownership.py16194%21
   tags.py39197%36
   users.py32197%42
   workshops.py58580%1–85
src/storages/sql/models
   workshops.py31310%1–53
TOTAL173441376% 

Tests Skipped Failures Errors Time
53 2 💤 2 ❌ 0 🔥 17.562s ⏱️

Please sign in to comment.