shri-jai commited on
Commit
fcdc4de
·
1 Parent(s): 119d5fc

feat: journaling endpoints

Browse files
alembic/versions/217db60578fa_added_journal_table.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """added journal table
2
+
3
+ Revision ID: 217db60578fa
4
+ Revises: d9b4df655a55
5
+ Create Date: 2025-12-05 14:19:31.722971
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ import sqlmodel.sql.sqltypes
13
+
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = '217db60578fa'
17
+ down_revision: Union[str, Sequence[str], None] = 'd9b4df655a55'
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ """Upgrade schema."""
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ op.create_table('journal_entries',
26
+ sa.Column('id', sa.Uuid(), nullable=False),
27
+ sa.Column('user_id', sa.UUID(), nullable=False),
28
+ sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
29
+ sa.Column('content', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
30
+ sa.Column('journal_date', sa.Date(), nullable=False),
31
+ sa.Column('created_at', sa.DateTime(), nullable=False),
32
+ sa.Column('updated_at', sa.DateTime(), nullable=False),
33
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
34
+ sa.PrimaryKeyConstraint('id'),
35
+ sa.UniqueConstraint('user_id', 'journal_date', name='unique_user_date_journal')
36
+ )
37
+ # ### end Alembic commands ###
38
+
39
+
40
+ def downgrade() -> None:
41
+ """Downgrade schema."""
42
+ # ### commands auto generated by Alembic - please adjust! ###
43
+ op.drop_table('journal_entries')
44
+ # ### end Alembic commands ###
src/auth/utils.py CHANGED
@@ -156,7 +156,7 @@ async def verify_verification_token(token: str) -> str:
156
  bearer_scheme = HTTPBearer()
157
 
158
 
159
- def get_current_user(
160
  credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
161
  ):
162
  """Decode JWT token and extract current user ID"""
@@ -169,7 +169,7 @@ def get_current_user(
169
  if user_id is None:
170
  raise HTTPException(
171
  status_code=status.HTTP_401_UNAUTHORIZED,
172
- detail="Invalid token: missing user id",
173
  )
174
  return user_id
175
 
 
156
  bearer_scheme = HTTPBearer()
157
 
158
 
159
+ def get_current_user(
160
  credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
161
  ):
162
  """Decode JWT token and extract current user ID"""
 
169
  if user_id is None:
170
  raise HTTPException(
171
  status_code=status.HTTP_401_UNAUTHORIZED,
172
+ detail="Invalid token: Missing user id",
173
  )
174
  return user_id
175
 
src/core/__init__.py CHANGED
@@ -6,3 +6,4 @@ from src.home import models as home_models
6
  from src.profile import models as profile_models
7
  from src.wellbeing import models as wellbeing_models
8
  from src.payslip import models as payslip_models
 
 
6
  from src.profile import models as profile_models
7
  from src.wellbeing import models as wellbeing_models
8
  from src.payslip import models as payslip_models
9
+ from src.journaling import models as journaling_models
src/core/models.py CHANGED
@@ -43,6 +43,7 @@ class Users(SQLModel, table=True):
43
  created_at: datetime = Field(default_factory=datetime.now)
44
  asset: List["Assets"] = Relationship(back_populates="user")
45
  water_logs: List["WaterLogs"] = Relationship(back_populates="user")
 
46
 
47
 
48
  class Teams(SQLModel, table=True):
 
43
  created_at: datetime = Field(default_factory=datetime.now)
44
  asset: List["Assets"] = Relationship(back_populates="user")
45
  water_logs: List["WaterLogs"] = Relationship(back_populates="user")
46
+ journal_entries: List["JournalEntry"] = Relationship(back_populates="user")
47
 
48
 
49
  class Teams(SQLModel, table=True):
src/journaling/__init__.py ADDED
File without changes
src/journaling/models.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from datetime import date, datetime
3
+ from typing import Optional
4
+
5
+ from sqlalchemy.dialects.postgresql import UUID
6
+ from sqlalchemy import Column, ForeignKey, UniqueConstraint
7
+ from sqlmodel import SQLModel, Field, Relationship
8
+
9
+
10
+ class JournalEntry(SQLModel, table=True):
11
+ __tablename__ = "journal_entries"
12
+ __table_args__ = (
13
+ UniqueConstraint("user_id", "journal_date", name="unique_user_date_journal"),
14
+ )
15
+ id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
16
+ user_id: uuid.UUID = Field(
17
+ sa_column=Column(
18
+ UUID(as_uuid=True),
19
+ ForeignKey("users.id", ondelete="CASCADE"),
20
+ nullable=False,
21
+ )
22
+ )
23
+ title: str = Field(nullable=False)
24
+ content: str = Field(nullable=False)
25
+ journal_date: date = Field(nullable=False)
26
+ created_at: datetime = Field(default_factory=datetime.now)
27
+ updated_at: datetime = Field(
28
+ default_factory=datetime.now,
29
+ sa_column_kwargs={"onupdate": datetime.now},
30
+ nullable=False,
31
+ )
32
+ user: Optional["Users"] = Relationship(back_populates="journal_entries")
src/journaling/router.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from fastapi import APIRouter, Depends, HTTPException
3
+ from sqlmodel.ext.asyncio.session import AsyncSession
4
+
5
+ from src.core.database import get_async_session
6
+ from src.core.schemas import BaseResponse
7
+ from src.auth.utils import get_current_user
8
+
9
+ from src.journaling.schemas import (
10
+ JournalCreate,
11
+ JournalUpdate,
12
+ JournalResponse,
13
+ )
14
+ from src.journaling.service import (
15
+ create_or_update_journal,
16
+ get_all_journals,
17
+ get_journal,
18
+ update_journal,
19
+ delete_journal,
20
+ )
21
+
22
+ router = APIRouter(prefix="/journal", tags=["Journal"])
23
+
24
+
25
+ @router.post("/", response_model=BaseResponse[JournalResponse], status_code=200)
26
+ async def create_or_update(
27
+ data: JournalCreate,
28
+ user_id: uuid.UUID = Depends(get_current_user),
29
+ session: AsyncSession = Depends(get_async_session),
30
+ ):
31
+ data.user_id = user_id
32
+
33
+ try:
34
+ record = await create_or_update_journal(data, session)
35
+ return BaseResponse(status_code=200, data=record)
36
+ except ValueError as e:
37
+ raise HTTPException(status_code=404, detail=str(e))
38
+
39
+
40
+ @router.get("/", response_model=BaseResponse[list[JournalResponse]], status_code=200)
41
+ async def list_user_journals(
42
+ user_id: uuid.UUID = Depends(get_current_user),
43
+ session: AsyncSession = Depends(get_async_session),
44
+ ):
45
+ records = await get_all_journals(user_id, session)
46
+ return BaseResponse(status_code=200, data=records)
47
+
48
+
49
+ @router.get("/entry/{journal_id}", response_model=BaseResponse[JournalResponse], status_code=200)
50
+ async def fetch_single(
51
+ journal_id: uuid.UUID,
52
+ user_id: uuid.UUID = Depends(get_current_user),
53
+ session: AsyncSession = Depends(get_async_session),
54
+ ):
55
+ try:
56
+ record = await get_journal(journal_id, user_id, session)
57
+ return BaseResponse(status_code=200, data=record)
58
+ except ValueError as e:
59
+ if str(e) == "Not authorized":
60
+ raise HTTPException(status_code=403, detail=str(e))
61
+ raise HTTPException(status_code=404, detail=str(e))
62
+
63
+
64
+ @router.put("/entry/{journal_id}", response_model=BaseResponse[JournalResponse], status_code=200)
65
+ async def update_entry(
66
+ journal_id: uuid.UUID,
67
+ data: JournalUpdate,
68
+ user_id: uuid.UUID = Depends(get_current_user),
69
+ session: AsyncSession = Depends(get_async_session),
70
+ ):
71
+ try:
72
+ record = await update_journal(journal_id, data, user_id, session)
73
+ return BaseResponse(status_code=200, data=record)
74
+ except ValueError as e:
75
+ if str(e) == "Not authorized":
76
+ raise HTTPException(status_code=403, detail=str(e))
77
+ raise HTTPException(status_code=404, detail=str(e))
78
+
79
+
80
+ @router.delete("/entry/{journal_id}", response_model=BaseResponse[str], status_code=200)
81
+ async def delete_entry(
82
+ journal_id: uuid.UUID,
83
+ user_id: uuid.UUID = Depends(get_current_user),
84
+ session: AsyncSession = Depends(get_async_session),
85
+ ):
86
+ try:
87
+ await delete_journal(journal_id, user_id, session)
88
+ return BaseResponse(status_code=200, data="Deleted")
89
+ except ValueError as e:
90
+ if str(e) == "Not authorized":
91
+ raise HTTPException(status_code=403, detail=str(e))
92
+ raise HTTPException(status_code=404, detail=str(e))
src/journaling/schemas.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from datetime import date, datetime
3
+ from typing import Optional
4
+
5
+ from sqlmodel import SQLModel
6
+
7
+
8
+ class JournalBase(SQLModel):
9
+ title: str
10
+ content: str
11
+ journal_date: date
12
+
13
+
14
+ class JournalCreate(JournalBase):
15
+ user_id: Optional[uuid.UUID] = None
16
+
17
+
18
+ class JournalUpdate(SQLModel):
19
+ title: Optional[str] = None
20
+ content: Optional[str] = None
21
+ journal_date: Optional[date] = None
22
+
23
+
24
+ class JournalResponse(SQLModel):
25
+ id: uuid.UUID
26
+ user_id: uuid.UUID
27
+ title: str
28
+ content: str
29
+ journal_date: date
30
+ created_at: datetime
31
+ updated_at: datetime
src/journaling/service.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ import uuid
3
+ from sqlmodel import select
4
+ from sqlmodel.ext.asyncio.session import AsyncSession
5
+
6
+ from src.core.models import Users
7
+ from src.journaling.models import JournalEntry
8
+ from src.journaling.schemas import JournalCreate, JournalUpdate
9
+
10
+
11
+ async def create_or_update_journal(data: JournalCreate, session: AsyncSession):
12
+ user = await session.exec(select(Users).where(Users.id == data.user_id))
13
+ if not user.first():
14
+ raise ValueError("User not found")
15
+
16
+ query = select(JournalEntry).where(
17
+ JournalEntry.user_id == data.user_id,
18
+ JournalEntry.journal_date == data.journal_date
19
+ )
20
+ existing = (await session.exec(query)).first()
21
+
22
+ if existing:
23
+ existing.title = data.title
24
+ existing.content = data.content
25
+ existing.updated_at = datetime.utcnow()
26
+ record = existing
27
+ else:
28
+ record = JournalEntry(
29
+ user_id=data.user_id,
30
+ title=data.title,
31
+ content=data.content,
32
+ journal_date=data.journal_date,
33
+ )
34
+ session.add(record)
35
+
36
+ await session.commit()
37
+ await session.refresh(record)
38
+ return record
39
+
40
+
41
+ async def get_all_journals(user_id: uuid.UUID, session: AsyncSession):
42
+ stmt = (
43
+ select(JournalEntry)
44
+ .where(JournalEntry.user_id == user_id)
45
+ .order_by(JournalEntry.journal_date.desc())
46
+ )
47
+ result = await session.exec(stmt)
48
+ return result.all()
49
+
50
+
51
+ async def get_journal(journal_id: uuid.UUID, user_id: uuid.UUID, session: AsyncSession):
52
+ stmt = select(JournalEntry).where(JournalEntry.id == journal_id)
53
+ result = await session.exec(stmt)
54
+ record = result.first()
55
+
56
+ if not record:
57
+ raise ValueError("Journal entry not found")
58
+ if str(record.user_id) != user_id:
59
+ raise ValueError("Not authorized")
60
+
61
+ return record
62
+
63
+
64
+ async def update_journal(
65
+ journal_id: uuid.UUID,
66
+ data: JournalUpdate,
67
+ user_id: uuid.UUID,
68
+ session: AsyncSession,
69
+ ):
70
+ record = await get_journal(journal_id, user_id, session)
71
+
72
+ if data.title is not None:
73
+ record.title = data.title
74
+
75
+ if data.content is not None:
76
+ record.content = data.content
77
+
78
+ if data.journal_date is not None:
79
+ record.journal_date = data.journal_date
80
+
81
+ record.updated_at = datetime.utcnow()
82
+
83
+ await session.commit()
84
+ await session.refresh(record)
85
+ return record
86
+
87
+
88
+ async def delete_journal(journal_id: uuid.UUID, user_id: uuid.UUID, session: AsyncSession):
89
+ record = await get_journal(journal_id, user_id, session)
90
+ await session.delete(record)
91
+ await session.commit()
src/main.py CHANGED
@@ -9,6 +9,7 @@ from src.home.router import router as home_router
9
  from src.notifications.router import router as notifications_router
10
  from src.payslip.router import router as payslip_router
11
  from src.profile.router import router as profile
 
12
  from fastapi.staticfiles import StaticFiles
13
 
14
 
@@ -36,6 +37,8 @@ app.include_router(notifications_router)
36
 
37
  app.include_router(payslip_router)
38
 
 
 
39
 
40
  @app.get("/")
41
  def root():
 
9
  from src.notifications.router import router as notifications_router
10
  from src.payslip.router import router as payslip_router
11
  from src.profile.router import router as profile
12
+ from src.journaling.router import router as journal
13
  from fastapi.staticfiles import StaticFiles
14
 
15
 
 
37
 
38
  app.include_router(payslip_router)
39
 
40
+ app.include_router(journal)
41
+
42
 
43
  @app.get("/")
44
  def root():