From c7e0027159ffb41ce682efd1cd55a9641f88c4b0 Mon Sep 17 00:00:00 2001 From: gromdimon Date: Mon, 23 Oct 2023 16:59:03 +0200 Subject: [PATCH 01/10] feat: Add bookmarks model, schemas and api --- backend/app/api/api_v1/api.py | 3 +- backend/app/api/api_v1/endpoints/bookmarks.py | 10 +++++ backend/app/models/__init__.py | 1 + backend/app/models/bookmark.py | 36 ++++++++++++++++++ backend/app/schemas/__init__.py | 1 + backend/app/schemas/adminmsg.py | 3 +- backend/app/schemas/bookmark.py | 38 +++++++++++++++++++ 7 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 backend/app/api/api_v1/endpoints/bookmarks.py create mode 100644 backend/app/models/bookmark.py create mode 100644 backend/app/schemas/bookmark.py diff --git a/backend/app/api/api_v1/api.py b/backend/app/api/api_v1/api.py index d0d924d8..b27af071 100644 --- a/backend/app/api/api_v1/api.py +++ b/backend/app/api/api_v1/api.py @@ -5,13 +5,14 @@ from httpx_oauth.errors import GetIdEmailError from httpx_oauth.oauth2 import BaseOAuth2, OAuth2Error -from app.api.api_v1.endpoints import adminmsgs, auth +from app.api.api_v1.endpoints import adminmsgs, auth, bookmarks from app.core.auth import auth_backend_bearer, auth_backend_cookie, fastapi_users from app.core.config import settings from app.schemas.user import UserRead, UserUpdate api_router = APIRouter() api_router.include_router(adminmsgs.router, prefix="/adminmsgs", tags=["adminmsgs"]) +api_router.include_router(bookmarks.router, prefix="/bookmarks", tags=["bookmarks"]) api_router.include_router( fastapi_users.get_auth_router(auth_backend_bearer), prefix="/auth/bearer", tags=["auth"] diff --git a/backend/app/api/api_v1/endpoints/bookmarks.py b/backend/app/api/api_v1/endpoints/bookmarks.py new file mode 100644 index 00000000..978e57df --- /dev/null +++ b/backend/app/api/api_v1/endpoints/bookmarks.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from app import crud, models, schemas +from app.api import deps +from app.core import config + + +router = APIRouter() + diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index a60fac75..878c290b 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,2 +1,3 @@ from app.models.adminmsg import AdminMessage # noqa +from app.models.bookmark import Bookmark # noqa from app.models.user import OAuthAccount, User # noqa diff --git a/backend/app/models/bookmark.py b/backend/app/models/bookmark.py new file mode 100644 index 00000000..9a986225 --- /dev/null +++ b/backend/app/models/bookmark.py @@ -0,0 +1,36 @@ +"""Models for bookmarks of variants and genes.""" + +import uuid as uuid_module +from typing import TYPE_CHECKING + +from fastapi_users_db_sqlalchemy.generics import GUID # noqa +from sqlalchemy import Column, ForeignKey, String +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.session import Base +from app.schemas.bookmark import BookmarkTypes + +UUID_ID = uuid_module.UUID + + +class Bookmark(Base): + """Bookmark of a variant or gene.""" + + __tablename__ = "bookmarks" + + if TYPE_CHECKING: # pragma: no cover + id: UUID_ID + user: str + obj_type: BookmarkTypes + obj_id: str + else: + #: UUID of the bookmark. + id: Mapped[UUID_ID] = mapped_column( + GUID, primary_key=True, index=True, default=uuid_module.uuid4 + ) + #: User who created the bookmark. + user = Column(String(255), ForeignKey("users.id"), nullable=False) + #: Type of the bookmarked object. + obj_type = Column(String(255), nullable=False) + #: ID of the bookmarked object. + obj_id = Column(String(255), nullable=False) \ No newline at end of file diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 6005cb57..ef9b9c94 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -1,3 +1,4 @@ from app.schemas.adminmsg import AdminMessageCreate, AdminMessageRead, AdminMessageUpdate # noqa +from app.schemas.bookmark import BookmarkCreate, BookmarkRead, BookmarkUpdate # noqa from app.schemas.auth import OAuth2ProviderConfig, OAuth2ProviderPublic # noqa from app.schemas.user import UserCreate, UserRead, UserUpdate # noqa diff --git a/backend/app/schemas/adminmsg.py b/backend/app/schemas/adminmsg.py index 8b66ab59..521b84e8 100644 --- a/backend/app/schemas/adminmsg.py +++ b/backend/app/schemas/adminmsg.py @@ -23,8 +23,7 @@ class AdminMessageUpdate(AdminMessageBase): class AdminMessageInDbBase(AdminMessageBase): model_config = ConfigDict(from_attributes=True) - id: int - uuid: UUID + id: UUID class AdminMessageRead(AdminMessageInDbBase): diff --git a/backend/app/schemas/bookmark.py b/backend/app/schemas/bookmark.py new file mode 100644 index 00000000..d76eeed8 --- /dev/null +++ b/backend/app/schemas/bookmark.py @@ -0,0 +1,38 @@ +from enum import Enum +from uuid import UUID + +from pydantic import BaseModel, ConfigDict + + +class BookmarkTypes(Enum): + seqvar = "seqvar" + strucvar = "strucvar" + gene = "gene" + + +class BookmarkBase(BaseModel): + user: str | None = None + obj_type: BookmarkTypes | None = None + obj_id: str | None = None + + +class BookmarkCreate(BookmarkBase): + pass + + +class BookmarkUpdate(BookmarkBase): + pass + + +class BookmarkInDbBase(BookmarkBase): + model_config = ConfigDict(from_attributes=True) + + id: UUID + + +class BookmarkRead(BookmarkInDbBase): + pass + + +class BookmarkInDb(BookmarkInDbBase): + pass \ No newline at end of file From 91bb1935a3740807bcc0aa4d22baa22f60b12e77 Mon Sep 17 00:00:00 2001 From: gromdimon Date: Mon, 23 Oct 2023 17:23:32 +0200 Subject: [PATCH 02/10] wip --- backend/app/api/api_v1/endpoints/bookmarks.py | 11 ++++++++--- backend/app/crud/__init__.py | 3 +++ backend/app/models/bookmark.py | 4 ++-- backend/app/schemas/__init__.py | 2 +- backend/app/schemas/bookmark.py | 2 +- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/app/api/api_v1/endpoints/bookmarks.py b/backend/app/api/api_v1/endpoints/bookmarks.py index 978e57df..b570fe39 100644 --- a/backend/app/api/api_v1/endpoints/bookmarks.py +++ b/backend/app/api/api_v1/endpoints/bookmarks.py @@ -1,10 +1,15 @@ from fastapi import APIRouter, Depends from sqlalchemy.ext.asyncio import AsyncSession -from app import crud, models, schemas +from app import crud, schemas from app.api import deps -from app.core import config - router = APIRouter() + +@router.post("/create", response_model=schemas.BookmarkCreate) +async def create_bookmark( + bookmark: schemas.BookmarkCreate, db: AsyncSession = Depends(deps.get_db) +): + """Create a new bookmark.""" + return await crud.bookmark.create(db, obj_in=bookmark) diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py index 52d8227b..1b77fabb 100644 --- a/backend/app/crud/__init__.py +++ b/backend/app/crud/__init__.py @@ -2,6 +2,9 @@ from app.crud.base import CrudBase from app.models.adminmsg import AdminMessage +from app.models.bookmark import Bookmark from app.schemas.adminmsg import AdminMessageCreate, AdminMessageUpdate +from app.schemas.bookmark import BookmarkCreate, BookmarkUpdate adminmessage = CrudBase[AdminMessage, AdminMessageCreate, AdminMessageUpdate](AdminMessage) +bookmark = CrudBase[Bookmark, BookmarkCreate, BookmarkUpdate](Bookmark) diff --git a/backend/app/models/bookmark.py b/backend/app/models/bookmark.py index 9a986225..54a7276d 100644 --- a/backend/app/models/bookmark.py +++ b/backend/app/models/bookmark.py @@ -29,8 +29,8 @@ class Bookmark(Base): GUID, primary_key=True, index=True, default=uuid_module.uuid4 ) #: User who created the bookmark. - user = Column(String(255), ForeignKey("users.id"), nullable=False) + user = Column(String(255), ForeignKey("user.id"), nullable=False) #: Type of the bookmarked object. obj_type = Column(String(255), nullable=False) #: ID of the bookmarked object. - obj_id = Column(String(255), nullable=False) \ No newline at end of file + obj_id = Column(String(255), nullable=False) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index ef9b9c94..2d77e2b7 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -1,4 +1,4 @@ from app.schemas.adminmsg import AdminMessageCreate, AdminMessageRead, AdminMessageUpdate # noqa -from app.schemas.bookmark import BookmarkCreate, BookmarkRead, BookmarkUpdate # noqa from app.schemas.auth import OAuth2ProviderConfig, OAuth2ProviderPublic # noqa +from app.schemas.bookmark import BookmarkCreate, BookmarkRead, BookmarkUpdate # noqa from app.schemas.user import UserCreate, UserRead, UserUpdate # noqa diff --git a/backend/app/schemas/bookmark.py b/backend/app/schemas/bookmark.py index d76eeed8..75ad32a9 100644 --- a/backend/app/schemas/bookmark.py +++ b/backend/app/schemas/bookmark.py @@ -35,4 +35,4 @@ class BookmarkRead(BookmarkInDbBase): class BookmarkInDb(BookmarkInDbBase): - pass \ No newline at end of file + pass From e0deba73f4b22182d604fc557afd44e089a3e7f8 Mon Sep 17 00:00:00 2001 From: gromdimon Date: Mon, 23 Oct 2023 18:56:50 +0200 Subject: [PATCH 03/10] working! --- .../versions/435a8e31682e_init_bookmarks.py | 45 +++++++++++++++++++ backend/app/api/api_v1/api.py | 1 - backend/app/api/api_v1/endpoints/adminmsgs.py | 2 +- backend/app/api/api_v1/endpoints/auth.py | 6 +-- backend/app/api/api_v1/endpoints/bookmarks.py | 18 ++++++++ backend/app/core/auth.py | 2 +- backend/app/core/config.py | 2 +- backend/app/db/init_db.py | 1 - backend/app/db/session.py | 3 +- backend/app/models/bookmark.py | 10 +++-- backend/app/schemas/bookmark.py | 2 +- 11 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 backend/alembic/versions/435a8e31682e_init_bookmarks.py diff --git a/backend/alembic/versions/435a8e31682e_init_bookmarks.py b/backend/alembic/versions/435a8e31682e_init_bookmarks.py new file mode 100644 index 00000000..00cdc85c --- /dev/null +++ b/backend/alembic/versions/435a8e31682e_init_bookmarks.py @@ -0,0 +1,45 @@ +"""empty message + +Revision ID: 435a8e31682e +Revises: 8ccd31a4f116 +Create Date: 2023-10-23 18:44:22.900953+02:00 + +""" +import fastapi_users_db_sqlalchemy.generics # noqa +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "435a8e31682e" +down_revision = "8ccd31a4f116" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "bookmarks", + sa.Column("id", fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False), + sa.Column("user", sa.Uuid(), nullable=False), + sa.Column( + "obj_type", sa.Enum("seqvar", "strucvar", "gene", name="bookmarktypes"), nullable=False + ), + sa.Column("obj_id", sa.String(length=255), nullable=False), + sa.ForeignKeyConstraint( + ["user"], + ["user.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("user", "obj_type", "obj_id", name="uq_bookmark"), + ) + op.create_index(op.f("ix_bookmarks_id"), "bookmarks", ["id"], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_bookmarks_id"), table_name="bookmarks") + op.drop_table("bookmarks") + # ### end Alembic commands ### diff --git a/backend/app/api/api_v1/api.py b/backend/app/api/api_v1/api.py index b27af071..701edfa1 100644 --- a/backend/app/api/api_v1/api.py +++ b/backend/app/api/api_v1/api.py @@ -3,7 +3,6 @@ from fastapi import APIRouter from httpx_oauth.clients.openid import OpenID from httpx_oauth.errors import GetIdEmailError -from httpx_oauth.oauth2 import BaseOAuth2, OAuth2Error from app.api.api_v1.endpoints import adminmsgs, auth, bookmarks from app.core.auth import auth_backend_bearer, auth_backend_cookie, fastapi_users diff --git a/backend/app/api/api_v1/endpoints/adminmsgs.py b/backend/app/api/api_v1/endpoints/adminmsgs.py index 743ad10c..28b6f4fc 100644 --- a/backend/app/api/api_v1/endpoints/adminmsgs.py +++ b/backend/app/api/api_v1/endpoints/adminmsgs.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends from sqlalchemy.ext.asyncio import AsyncSession -from app import crud, models, schemas +from app import crud, schemas from app.api import deps router = APIRouter() diff --git a/backend/app/api/api_v1/endpoints/auth.py b/backend/app/api/api_v1/endpoints/auth.py index 82bfcb23..2a39b1eb 100644 --- a/backend/app/api/api_v1/endpoints/auth.py +++ b/backend/app/api/api_v1/endpoints/auth.py @@ -1,8 +1,6 @@ -from fastapi import APIRouter, Depends -from sqlalchemy.ext.asyncio import AsyncSession +from fastapi import APIRouter -from app import crud, models, schemas -from app.api import deps +from app import schemas from app.core import config router = APIRouter() diff --git a/backend/app/api/api_v1/endpoints/bookmarks.py b/backend/app/api/api_v1/endpoints/bookmarks.py index b570fe39..ec5855fd 100644 --- a/backend/app/api/api_v1/endpoints/bookmarks.py +++ b/backend/app/api/api_v1/endpoints/bookmarks.py @@ -13,3 +13,21 @@ async def create_bookmark( ): """Create a new bookmark.""" return await crud.bookmark.create(db, obj_in=bookmark) + + +@router.get("/list", response_model=list[schemas.BookmarkRead]) +async def list_bookmarks(skip: int = 0, limit: int = 100, db: AsyncSession = Depends(deps.get_db)): + """List bookmarks.""" + return await crud.bookmark.get_multi(db, skip=skip, limit=limit) + + +@router.get("/get", response_model=schemas.BookmarkRead) +async def get_bookmark(id: str, db: AsyncSession = Depends(deps.get_db)): + """Get a bookmark.""" + return await crud.bookmark.get(db, id=id) + + +@router.delete("/delete", response_model=schemas.BookmarkRead) +async def delete_bookmark(id: str, db: AsyncSession = Depends(deps.get_db)): + """Delete a bookmark.""" + return await crud.bookmark.remove(db, id=id) diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py index 51db2ee8..76375ecf 100644 --- a/backend/app/core/auth.py +++ b/backend/app/core/auth.py @@ -13,7 +13,7 @@ from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase from sqlalchemy.ext.asyncio import AsyncSession -from app.api.deps import get_async_session, get_db +from app.api.deps import get_async_session from app.core.config import settings from app.models.user import OAuthAccount, User diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 21fd1bb8..5a5b6073 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -3,7 +3,7 @@ import secrets from typing import Any -from pydantic import AnyHttpUrl, BaseModel, EmailStr, HttpUrl, PostgresDsn, field_validator +from pydantic import AnyHttpUrl, EmailStr, HttpUrl, PostgresDsn, field_validator from pydantic_core.core_schema import ValidationInfo from pydantic_settings import BaseSettings, SettingsConfigDict diff --git a/backend/app/db/init_db.py b/backend/app/db/init_db.py index 35e8285a..f89a36f5 100644 --- a/backend/app/db/init_db.py +++ b/backend/app/db/init_db.py @@ -6,7 +6,6 @@ from app.api.deps import get_async_session from app.core.auth import get_user_db, get_user_manager from app.core.config import settings -from app.db.session import SessionLocal from app.schemas import UserCreate logging.basicConfig(level=logging.INFO) diff --git a/backend/app/db/session.py b/backend/app/db/session.py index 75e5cc98..f1107d5d 100644 --- a/backend/app/db/session.py +++ b/backend/app/db/session.py @@ -1,6 +1,5 @@ -from sqlalchemy import create_engine from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine -from sqlalchemy.orm import declarative_base, sessionmaker +from sqlalchemy.orm import declarative_base from app.core.config import settings diff --git a/backend/app/models/bookmark.py b/backend/app/models/bookmark.py index 54a7276d..bdc3ee2a 100644 --- a/backend/app/models/bookmark.py +++ b/backend/app/models/bookmark.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from fastapi_users_db_sqlalchemy.generics import GUID # noqa -from sqlalchemy import Column, ForeignKey, String +from sqlalchemy import Column, Enum, ForeignKey, String, UniqueConstraint, Uuid from sqlalchemy.orm import Mapped, mapped_column from app.db.session import Base @@ -18,9 +18,11 @@ class Bookmark(Base): __tablename__ = "bookmarks" + __table_args__ = (UniqueConstraint("user", "obj_type", "obj_id", name="uq_bookmark"),) + if TYPE_CHECKING: # pragma: no cover id: UUID_ID - user: str + user: UUID_ID obj_type: BookmarkTypes obj_id: str else: @@ -29,8 +31,8 @@ class Bookmark(Base): GUID, primary_key=True, index=True, default=uuid_module.uuid4 ) #: User who created the bookmark. - user = Column(String(255), ForeignKey("user.id"), nullable=False) + user = Column(Uuid, ForeignKey("user.id"), nullable=False) #: Type of the bookmarked object. - obj_type = Column(String(255), nullable=False) + obj_type = Column(Enum(BookmarkTypes), nullable=False) #: ID of the bookmarked object. obj_id = Column(String(255), nullable=False) diff --git a/backend/app/schemas/bookmark.py b/backend/app/schemas/bookmark.py index 75ad32a9..9f33ff5b 100644 --- a/backend/app/schemas/bookmark.py +++ b/backend/app/schemas/bookmark.py @@ -11,7 +11,7 @@ class BookmarkTypes(Enum): class BookmarkBase(BaseModel): - user: str | None = None + user: UUID | None = None obj_type: BookmarkTypes | None = None obj_id: str | None = None From a7a05fbad5d747ad74e22e890c84e67bd34baf45 Mon Sep 17 00:00:00 2001 From: gromdimon Date: Wed, 25 Oct 2023 13:13:51 +0200 Subject: [PATCH 04/10] front-end --- backend/app/api/api_v1/endpoints/bookmarks.py | 143 ++++++++++++++++-- backend/app/crud/__init__.py | 5 +- backend/app/crud/bookmarks.py | 50 ++++++ frontend/src/api/bookmarks.ts | 107 +++++++++++++ frontend/src/components/BookmarkButton.vue | 60 ++++++++ frontend/src/stores/bookmarks.ts | 52 +++++++ frontend/src/views/GeneDetailView.vue | 3 + frontend/src/views/ProfileView.vue | 60 +++++++- frontend/src/views/SvDetailView.vue | 5 +- frontend/src/views/VariantDetailView.vue | 3 + 10 files changed, 469 insertions(+), 19 deletions(-) create mode 100644 backend/app/crud/bookmarks.py create mode 100644 frontend/src/api/bookmarks.ts create mode 100644 frontend/src/components/BookmarkButton.vue create mode 100644 frontend/src/stores/bookmarks.ts diff --git a/backend/app/api/api_v1/endpoints/bookmarks.py b/backend/app/api/api_v1/endpoints/bookmarks.py index ec5855fd..bfec5490 100644 --- a/backend/app/api/api_v1/endpoints/bookmarks.py +++ b/backend/app/api/api_v1/endpoints/bookmarks.py @@ -1,8 +1,9 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app import crud, schemas from app.api import deps +from app.core import auth router = APIRouter() @@ -11,23 +12,143 @@ async def create_bookmark( bookmark: schemas.BookmarkCreate, db: AsyncSession = Depends(deps.get_db) ): - """Create a new bookmark.""" + """ + Create a new bookmark. + + **Parameters** + * **obj_type**: type of object to bookmark + * **obj_id**: id of object to bookmark + * **user**: user id of user who created the bookmark + + **Response** + * **id**: id of the bookmark + * **obj_type**: type of object to bookmark + * **obj_id**: id of object to bookmark + * **user**: user id of user who created the bookmark + """ return await crud.bookmark.create(db, obj_in=bookmark) -@router.get("/list", response_model=list[schemas.BookmarkRead]) +@router.get("/list-all", response_model=list[schemas.BookmarkRead]) async def list_bookmarks(skip: int = 0, limit: int = 100, db: AsyncSession = Depends(deps.get_db)): - """List bookmarks.""" - return await crud.bookmark.get_multi(db, skip=skip, limit=limit) + """ + List bookmarks. Available only for superusers. + **Parameters** + * **skip**: number of bookmarks to skip + * **limit**: maximum number of bookmarks to return -@router.get("/get", response_model=schemas.BookmarkRead) + **Response** + * List of bookmarks + """ + user = auth.fastapi_users.current_user(active=True, superuser=True) + if user: + return await crud.bookmark.get_multi(db, skip=skip, limit=limit) + else: + raise HTTPException(status_code=403, detail="Not enough permissions") + + +@router.get("/get-by-id", response_model=schemas.BookmarkRead) async def get_bookmark(id: str, db: AsyncSession = Depends(deps.get_db)): - """Get a bookmark.""" - return await crud.bookmark.get(db, id=id) + """ + Get a bookmark by id. Available only for superusers. + **Parameters** + * **id**: id of the bookmark -@router.delete("/delete", response_model=schemas.BookmarkRead) + **Response** + * **id**: id of the bookmark + * **obj_type**: type of object to bookmark + * **obj_id**: id of object to bookmark + * **user**: user id of user who created the bookmark + """ + user = auth.fastapi_users.current_user(active=True, superuser=True) + if user: + return await crud.bookmark.get(db, id=id) + else: + raise HTTPException(status_code=403, detail="Not enough permissions") + + +@router.delete("/delete-by-id", response_model=schemas.BookmarkRead) async def delete_bookmark(id: str, db: AsyncSession = Depends(deps.get_db)): - """Delete a bookmark.""" - return await crud.bookmark.remove(db, id=id) + """ + Delete a bookmark. Available only for superusers. + + **Parameters** + * **id**: id of the bookmark + + **Response** + * **id**: id of the bookmark + * **obj_type**: type of object to bookmark + * **obj_id**: id of object to bookmark + * **user**: user id of user who created the bookmark + """ + user = auth.fastapi_users.current_user(active=True, superuser=True) + if user: + return await crud.bookmark.remove(db, id=id) + else: + raise HTTPException(status_code=403, detail="Not enough permissions") + + +@router.get("/list", response_model=list[schemas.BookmarkRead]) +async def list_bookmarks_for_user( + user_id: str, skip: int = 0, limit: int = 100, db: AsyncSession = Depends(deps.get_db) +): + """ + List bookmarks for a user. + + **Parameters** + * **user_id**: id of the user + * **skip**: number of bookmarks to skip + * **limit**: maximum number of bookmarks to return + + **Response** + * List of bookmarks for the user + """ + return await crud.bookmark.get_multi_by_user(db, user_id=user_id, skip=skip, limit=limit) + + +@router.get("/get", response_model=schemas.BookmarkRead) +async def get_bookmark_for_user( + user_id: str, obj_type: str, obj_id: str, db: AsyncSession = Depends(deps.get_db) +): + """ + Get a bookmark for a user by obj_type and obj_id. + + **Parameters** + * **user_id**: id of the user + * **obj_type**: type of object to bookmark + * **obj_id**: id of object to bookmark + + **Response** + * **id**: id of the bookmark + * **obj_type**: type of object to bookmark + * **obj_id**: id of object to bookmark + * **user**: user id of user who created the bookmark + """ + return await crud.bookmark.get_by_user_and_obj( + db, user_id=user_id, obj_type=obj_type, obj_id=obj_id + ) + + +@router.delete("/delete", response_model=schemas.BookmarkRead) +async def delete_bookmark_for_user( + user_id: str, obj_type: str, obj_id: str, db: AsyncSession = Depends(deps.get_db) +): + """ + Delete a bookmark for a user by obj_type and obj_id. + + **Parameters** + * **user_id**: id of the user + * **obj_type**: type of object to bookmark + * **obj_id**: id of object to bookmark + + **Response** + * **id**: id of the bookmark + * **obj_type**: type of object to bookmark + * **obj_id**: id of object to bookmark + * **user**: user id of user who created the bookmark + """ + return await crud.bookmark.remove_by_user_and_obj( + db, user_id=user_id, obj_type=obj_type, obj_id=obj_id + ) diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py index 1b77fabb..404afc4c 100644 --- a/backend/app/crud/__init__.py +++ b/backend/app/crud/__init__.py @@ -1,10 +1,9 @@ -from typing import Any - from app.crud.base import CrudBase +from app.crud.bookmarks import CrudBookmark from app.models.adminmsg import AdminMessage from app.models.bookmark import Bookmark from app.schemas.adminmsg import AdminMessageCreate, AdminMessageUpdate from app.schemas.bookmark import BookmarkCreate, BookmarkUpdate adminmessage = CrudBase[AdminMessage, AdminMessageCreate, AdminMessageUpdate](AdminMessage) -bookmark = CrudBase[Bookmark, BookmarkCreate, BookmarkUpdate](Bookmark) +bookmark = CrudBookmark(Bookmark) diff --git a/backend/app/crud/bookmarks.py b/backend/app/crud/bookmarks.py new file mode 100644 index 00000000..4a0c00bd --- /dev/null +++ b/backend/app/crud/bookmarks.py @@ -0,0 +1,50 @@ +from typing import Any, List + +from sqlalchemy import and_ +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.crud.base import CrudBase +from app.models.bookmark import Bookmark +from app.schemas.bookmark import BookmarkCreate, BookmarkUpdate + + +class CrudBookmark(CrudBase[Bookmark, BookmarkCreate, BookmarkUpdate]): + async def get_multi_by_user( + self, session: AsyncSession, *, user_id: str, skip: int = 0, limit: int = 100 + ) -> List[Bookmark]: + query = ( + select(self.model).filter(and_(self.model.user == user_id)).offset(skip).limit(limit) + ) + result = await session.execute(query) + return result.scalars().all() + + async def get_by_user_and_obj( + self, session: AsyncSession, *, user_id: str, obj_type: str, obj_id: str + ) -> Bookmark: + query = select(self.model).filter( + and_( + self.model.user == user_id, + self.model.obj_type == obj_type, + self.model.obj_id == obj_id, + ) + ) + result = await session.execute(query) + return result.scalars().first() + + async def remove_by_user_and_obj( + self, session: AsyncSession, *, user_id: str, obj_type: str, obj_id: str + ) -> Bookmark: + query = select(self.model).filter( + and_( + self.model.user == user_id, + self.model.obj_type == obj_type, + self.model.obj_id == obj_id, + ) + ) + result = await session.execute(query) + obj = result.scalars().first() + if obj: + await session.delete(obj) + await session.commit() + return obj diff --git a/frontend/src/api/bookmarks.ts b/frontend/src/api/bookmarks.ts new file mode 100644 index 00000000..093da985 --- /dev/null +++ b/frontend/src/api/bookmarks.ts @@ -0,0 +1,107 @@ +import { API_V1_BASE_PREFIX } from '@/api/common' +import { UsersClient } from '@/api/users' + +/** + * Access to the bookmarks part of the API. + */ +export class BookmarksClient { + private apiBaseUrl: string + private csrfToken: string | null + private currentUserId: string | null + + constructor(apiBaseUrl?: string, csrfToken?: string) { + this.apiBaseUrl = apiBaseUrl ?? API_V1_BASE_PREFIX + this.csrfToken = csrfToken ?? null + this.currentUserId = null + } + + /** + * Obtains the currently logged in user's information. + */ + async fetchCurrentUser(): Promise { + const userClient = new UsersClient() + const response = await userClient.fetchCurrentUserProfile() + this.currentUserId = response.id + } + + /** + * Obtains the currently logged in user's bookmarks. + * + * @returns bookmarks list for the current user + */ + async fetchBookmarks(): Promise { + // Obtain the current user's information + await this.fetchCurrentUser() + const url = `${this.apiBaseUrl}bookmarks/list?user_id=${this.currentUserId}` + const response = await fetch(url, { + method: 'GET', + mode: 'cors', + credentials: 'include' + }) + return await response.json() + } + + /** + * Obtains the currently logged in user's bookmark for the given object. + * + * @param obj_type object type, e.g., "seqvar" + * @param obj_id object ID, e.g., "HGNC:1100" + * @returns bookmark for the current user + */ + async fetchBookmark(obj_type: string, obj_id: string): Promise { + // Obtain the current user's information + await this.fetchCurrentUser() + const url = `${this.apiBaseUrl}bookmarks/get?user_id=${this.currentUserId}&obj_type=${obj_type}&obj_id=${obj_id}` + const response = await fetch(url, { + method: 'GET', + mode: 'cors', + credentials: 'include' + }) + return await response.json() + } + + /** + * Creates a bookmark for the current user. + * + * @param obj_type object type, e.g., "seqvar" + * @param obj_id object ID, e.g., "HGNC:1100" + * @returns created bookmark + */ + async createBookmark(obj_type: string, obj_id: string): Promise { + // Obtain the current user's information + await this.fetchCurrentUser() + if (this.currentUserId === null) { + throw new Error('User ID is null.') + } + const response = await fetch(`${this.apiBaseUrl}bookmarks/create`, { + method: 'POST', + mode: 'cors', + credentials: 'include', + headers: { + accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: `{"user": "${this.currentUserId}", "obj_type": "${obj_type}", "obj_id": "${obj_id}"}` + }) + return await response.json() + } + + /** + * Deletes a bookmark for the current user. + * + * @param obj_type object type, e.g., "seqvar" + * @param obj_id object ID, e.g., "HGNC:1100" + * @returns deleted bookmark + */ + async deleteBookmark(obj_type: string, obj_id: string): Promise { + // Obtain the current user's information + await this.fetchCurrentUser() + const url = `${this.apiBaseUrl}bookmarks/delete?user_id=${this.currentUserId}&obj_type=${obj_type}&obj_id=${obj_id}` + const response = await fetch(url, { + method: 'DELETE', + mode: 'cors', + credentials: 'include' + }) + return await response.json() + } +} diff --git a/frontend/src/components/BookmarkButton.vue b/frontend/src/components/BookmarkButton.vue new file mode 100644 index 00000000..56b28bc3 --- /dev/null +++ b/frontend/src/components/BookmarkButton.vue @@ -0,0 +1,60 @@ + + + diff --git a/frontend/src/stores/bookmarks.ts b/frontend/src/stores/bookmarks.ts new file mode 100644 index 00000000..9b28954b --- /dev/null +++ b/frontend/src/stores/bookmarks.ts @@ -0,0 +1,52 @@ +/** + * Store for information regarding the current user. + */ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +import { BookmarksClient } from '@/api/bookmarks' +import { StoreState } from '@/stores/misc' + +export interface BookmarkData { + user: string + obj_type: string + obj_id: string + id: string +} + +export const useBookmarksStore = defineStore('bookmarks', () => { + /* The current store state. */ + const storeState = ref(StoreState.Initial) + + /* The bookmarks list for current user. */ + const bookmarks = ref([]) + + const loadBookmarks = async () => { + storeState.value = StoreState.Loading + try { + const client = new BookmarksClient() + bookmarks.value = await client.fetchBookmarks() + storeState.value = StoreState.Active + } catch (e) { + storeState.value = StoreState.Error + } + } + + const deleteBookmark = async (bookmark: BookmarkData) => { + storeState.value = StoreState.Loading + try { + const client = new BookmarksClient() + await client.deleteBookmark(bookmark.obj_type, bookmark.obj_id) + await loadBookmarks() + } catch (e) { + storeState.value = StoreState.Error + } + } + + return { + storeState, + bookmarks, + loadBookmarks, + deleteBookmark + } +}) diff --git a/frontend/src/views/GeneDetailView.vue b/frontend/src/views/GeneDetailView.vue index 26388aa0..1da78ce0 100644 --- a/frontend/src/views/GeneDetailView.vue +++ b/frontend/src/views/GeneDetailView.vue @@ -2,6 +2,7 @@ import { onMounted, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' +import BookmarkButton from '@/components/BookmarkButton.vue' import AlternativeIdentifiers from '@/components/GeneDetails/AlternativeIdentifiers.vue' import ClinvarFreqPlot from '@/components/GeneDetails/ClinVarFreqPlot.vue' import ClinvarImpact from '@/components/GeneDetails/ClinvarImpact.vue' @@ -92,6 +93,8 @@ const genomeReleaseRef = ref(props.genomeRelease)
+ + Sections: +import { onMounted } from 'vue' import { useRouter } from 'vue-router' import { AuthClient } from '@/api/auth' import HeaderDefault from '@/components/HeaderDefault.vue' +import { search } from '@/lib/utils' +import { useBookmarksStore } from '@/stores/bookmarks' import { useUserStore } from '@/stores/user' +const bookmarksStore = useBookmarksStore() const userStore = useUserStore() -userStore.initialize() const router = useRouter() @@ -18,19 +21,44 @@ const logout = async () => { router.push('/') } + +/** + * Perform a search based on the bookmark id. + * + * If a route is found for the search term then redirect to that route. + * Otherwise log an error. + * + * @param geneSymbol Gene symbol to search for + */ +const performSearch = async (geneSymbol: string) => { + const routeLocation: any = await search(geneSymbol, 'grch37') + if (routeLocation) { + router.push(routeLocation) + } else { + console.error(`no route found for ${geneSymbol}`) + } +} + +const loadDataToStore = async () => { + await bookmarksStore.loadBookmarks() +} + +onMounted(() => { + userStore.initialize() + loadDataToStore() +})