Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion backend/app/base/exceptions.py
Original file line number Diff line number Diff line change
@@ -1 +1,50 @@
class BaseQueryException(Exception): ...
"""Base exceptions for the application."""


class BaseQueryException(Exception):
"""Base exception for query layer errors."""

pass


class BaseServiceException(Exception):
"""Base exception for service layer errors."""

def __init__(self, message: str):
self.message = message
super().__init__(self.message)


class EntityNotFound(BaseServiceException):
"""Raised when an entity is not found."""

def __init__(self, entity_name: str, entity_id: int | str | None = None):
if entity_id is None:
# entity_name is actually the full message
message = entity_name
elif isinstance(entity_id, int):
message = f"{entity_name} with id {entity_id} not found"
else:
message = f"{entity_name} '{entity_id}' not found"
super().__init__(message)


class UnauthorizedAccess(BaseServiceException):
"""Raised when a user is not authorized to perform an action."""

def __init__(self, message: str = "Unauthorized access"):
super().__init__(message)


class ValidationError(BaseServiceException):
"""Raised when validation fails."""

def __init__(self, message: str):
super().__init__(message)


class BusinessLogicError(BaseServiceException):
"""Raised when business logic validation fails."""

def __init__(self, message: str):
super().__init__(message)
1 change: 1 addition & 0 deletions backend/app/documents/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ def update_record(
repeated_record.target = data.target
repeated_record.approved = data.approved

# this should be better put on the service level, not here
if data.approved is True:
bound_tm = None
for memory in record.document.memory_associations:
Expand Down
98 changes: 0 additions & 98 deletions backend/app/glossary/controllers.py

This file was deleted.

4 changes: 4 additions & 0 deletions backend/app/glossary/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ def create_glossary_record(
record: GlossaryRecordCreate,
glossary_id: int,
) -> GlossaryRecord:
# Check if glossary exists first
if not self.db.query(Glossary).filter(Glossary.id == glossary_id).first():
raise NotFoundGlossaryExc()

glossary_record = GlossaryRecord(
glossary_id=glossary_id,
created_by=user_id,
Expand Down
68 changes: 13 additions & 55 deletions backend/app/glossary/tasks.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,19 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, Self
"""Background tasks for glossary processing."""

from sqlalchemy.orm import Session

from app.glossary.models import GlossaryRecord
from app.glossary.query import GlossaryQuery
from app.linguistic.utils import postprocess_stemmed_segment, stem_sentence


@dataclass
class GlossaryRowRecord:
comment: Optional[str]
created_at: datetime
author: str
updated_at: datetime
source: str
target: str

@classmethod
def from_tuple(cls, data_tuple: tuple[str, ...]) -> Self:
comment, created_at, author, updated_at, _, source, target = data_tuple
created_at = datetime.strptime(created_at, "%m/%d/%Y %H:%M:%S")
updated_at = datetime.strptime(updated_at, "%m/%d/%Y %H:%M:%S")
return cls(comment, created_at, author, updated_at, source, target)
from app.services import GlossaryService


def create_glossary_from_file_tasks(user_id: int, sheet, db: Session, glossary_id: int):
record_for_save = extract_from_xlsx(user_id, sheet, glossary_id)
bulk_save_glossaries_update_processing_status(
db=db, record_for_save=record_for_save, glossary_id=glossary_id
)


def extract_from_xlsx(user_id: int, sheet, glossary_id: int) -> list[GlossaryRecord]:
record_for_save = []
for cells in sheet.iter_rows(min_row=2, values_only=True):
parsed_record = GlossaryRowRecord.from_tuple(cells)
record = GlossaryRecord(
created_by=user_id,
created_at=parsed_record.created_at,
updated_at=parsed_record.updated_at,
comment=parsed_record.comment,
source=parsed_record.source,
target=parsed_record.target,
glossary_id=glossary_id,
stemmed_source=" ".join(
postprocess_stemmed_segment(stem_sentence(parsed_record.source))
),
)
record_for_save.append(record)
return record_for_save


def bulk_save_glossaries_update_processing_status(
db: Session, record_for_save: list[GlossaryRecord], glossary_id: int
):
glossary_query = GlossaryQuery(db)
glossary_query.bulk_create_glossary_record(record_for_save)
glossary_query.update_glossary_processing_status(glossary_id)
"""
Background task to process glossary file and create records.

Args:
user_id: ID of user who uploaded the file
sheet: XLSX sheet object
db: Database session
glossary_id: Glossary ID to add records to
"""
service = GlossaryService(db)
service.process_glossary_file(sheet, user_id, glossary_id)
48 changes: 23 additions & 25 deletions backend/app/routers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

from fastapi import APIRouter, Depends, HTTPException, Response, status
from itsdangerous import URLSafeTimedSerializer
from sqlalchemy.orm import Session

from app import models, schema
from app import models
from app.base.exceptions import BusinessLogicError, UnauthorizedAccess
from app.db import get_db
from app.security import verify_password
from app.services import AuthService
from app.settings import settings
from app.user.depends import has_user_role

Expand All @@ -17,32 +17,30 @@
def login(
data: models.AuthFields,
response: Response,
db: Session = Depends(get_db),
db=Depends(get_db),
) -> models.StatusMessage:
if not data.password:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)

user = db.query(schema.User).filter_by(email=data.email).first()

if not user or not verify_password(data.password, user.password):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)

if user.disabled:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)

serializer = URLSafeTimedSerializer(secret_key=settings.secret_key)
response.set_cookie(
"session",
serializer.dumps({"user_id": user.id}),
secure=bool(settings.domain_name),
domain=settings.domain_name,
httponly=True,
expires=datetime.now(UTC) + timedelta(days=21) if data.remember else None,
)
return models.StatusMessage(message="Logged in")
service = AuthService(db)
try:
user = service.login(data)
# Set session cookie in router (HTTP-specific concern)
serializer = URLSafeTimedSerializer(secret_key=settings.secret_key)
response.set_cookie(
"session",
serializer.dumps({"user_id": user.id}),
secure=bool(settings.domain_name),
domain=settings.domain_name,
httponly=True,
expires=datetime.now(UTC) + timedelta(days=21) if data.remember else None,
)
return models.StatusMessage(message="Logged in")
except UnauthorizedAccess as e:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
except BusinessLogicError as e:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))


@router.post("/logout", dependencies=[Depends(has_user_role)])
def logout(response: Response) -> models.StatusMessage:
# Logout doesn't need database access, just clear the cookie
response.delete_cookie("session")
return models.StatusMessage(message="Logged out")
56 changes: 20 additions & 36 deletions backend/app/routers/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,18 @@
from sqlalchemy.orm import Session

from app import schema
from app.comments.query import CommentsQuery
from app.base.exceptions import EntityNotFound, UnauthorizedAccess
from app.comments.schema import CommentResponse, CommentUpdate
from app.db import get_db
from app.models import StatusMessage
from app.services import CommentService
from app.user.depends import get_current_user_id, has_user_role

router = APIRouter(
prefix="/comments", tags=["comments"], dependencies=[Depends(has_user_role)]
)


def get_comment_by_id(db: Session, comment_id: int):
"""Helper function to get comment by ID"""
comment = CommentsQuery(db).get_comment(comment_id)
if not comment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Comment not found"
)
return comment


def check_comment_authorship(comment, current_user_id: int, is_admin: bool = False):
"""Check if user can modify/delete comment"""
if not is_admin and comment.created_by != current_user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You can only modify your own comments",
)


@router.put("/{comment_id}")
def update_comment(
comment_id: int,
Expand All @@ -42,15 +24,16 @@ def update_comment(
current_user: Annotated[int, Depends(get_current_user_id)],
) -> CommentResponse:
"""Update an existing comment"""
comment = get_comment_by_id(db, comment_id)

# Check if user can modify this comment
service = CommentService(db)
user = db.query(schema.User).filter_by(id=current_user).one()
is_admin = user.role == "admin"
check_comment_authorship(comment, current_user, is_admin)

updated_comment = CommentsQuery(db).update_comment(comment_id, comment_data)
return CommentResponse.model_validate(updated_comment)
try:
return service.update_comment(
comment_id, comment_data, current_user, force=user.role == "admin"
)
except EntityNotFound as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except UnauthorizedAccess as e:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))


@router.delete("/{comment_id}")
Expand All @@ -60,12 +43,13 @@ def delete_comment(
current_user: Annotated[int, Depends(get_current_user_id)],
) -> StatusMessage:
"""Delete a comment"""
comment = get_comment_by_id(db, comment_id)

# Check if user can delete this comment
service = CommentService(db)
user = db.query(schema.User).filter_by(id=current_user).one()
is_admin = user.role == "admin"
check_comment_authorship(comment, current_user, is_admin)

CommentsQuery(db).delete_comment(comment_id)
return StatusMessage(message="Comment deleted successfully")
try:
return service.delete_comment(
comment_id, current_user, force=user.role == "admin"
)
except EntityNotFound as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except UnauthorizedAccess as e:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))
Loading