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
2 changes: 2 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ CELERY_BROKER_URL=redis://localhost:6379/1

CELERY_RESULT_BACKEND=redis://localhost:6379/2

CELERY_TASK_ALWAYS_EAGER=false

# ----------------------------------------------------------
# WebSocket
# ----------------------------------------------------------
Expand Down
24 changes: 24 additions & 0 deletions backend/app/core/celery_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from __future__ import annotations

from celery import Celery

from app.core.config import settings

celery_app = Celery(
"devlink",
broker=settings.CELERY_BROKER_URL,
backend=settings.CELERY_RESULT_BACKEND,
include=["app.tasks.notification_tasks"],
)

celery_app.conf.update(
task_serializer="json",
result_serializer="json",
accept_content=["json"],
timezone="UTC",
enable_utc=True,
task_track_started=True,
task_time_limit=60,
task_soft_time_limit=45,
task_always_eager=settings.CELERY_TASK_ALWAYS_EAGER,
)
2 changes: 2 additions & 0 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ class Settings(BaseSettings):
CELERY_BROKER_URL: str = "redis://localhost:6379/1"
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/2"

CELERY_TASK_ALWAYS_EAGER: bool = False

# ==========================================================
# WebSocket
# ==========================================================
Expand Down
64 changes: 61 additions & 3 deletions backend/app/routers/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
ApplicationUpdate,
)
from app.services.application_service import ApplicationService
from app.models.notification import NotificationType
from app.models.project import Project
from app.services.notification_service import NotificationService

router = APIRouter(
prefix="/applications",
Expand All @@ -32,14 +35,33 @@ def create_application(
current_user: User = Depends(get_current_user),
):

return ApplicationService.create_application(
created = ApplicationService.create_application(
db=db,
applicant_id=current_user.id,
project_id=application.project_id,
flare_id=application.flare_id,
application=application,
)

try:
project = db.get(Project, created.project_id)
if project is not None:
NotificationService.enqueue(
db,
recipient_id=project.owner_id,
sender_id=current_user.id,
type=NotificationType.APPLICATION,
title="New application",
message=f"{current_user.username} applied to your project.",
project_id=created.project_id,
application_id=created.id,
action_url=f"/applications/{created.id}",
)
except Exception:
db.rollback()

return created


@router.get(
"/{application_id}",
Expand Down Expand Up @@ -129,6 +151,7 @@ def update_application(
def accept_application(
application_id: uuid.UUID,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):

db_application = ApplicationService.get_application(
Expand All @@ -142,11 +165,28 @@ def accept_application(
detail="Application not found",
)

return ApplicationService.accept_application(
accepted = ApplicationService.accept_application(
db,
db_application,
)

try:
NotificationService.enqueue(
db,
recipient_id=db_application.applicant_id,
sender_id=current_user.id,
type=NotificationType.APPLICATION_ACCEPTED,
title="Application accepted",
message="Your application was accepted. 🎉",
project_id=db_application.project_id,
application_id=db_application.id,
action_url=f"/applications/{db_application.id}",
)
except Exception:
db.rollback()

return accepted


@router.patch(
"/{application_id}/reject",
Expand All @@ -155,6 +195,7 @@ def accept_application(
def reject_application(
application_id: uuid.UUID,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):

db_application = ApplicationService.get_application(
Expand All @@ -168,11 +209,28 @@ def reject_application(
detail="Application not found",
)

return ApplicationService.reject_application(
rejected = ApplicationService.reject_application(
db,
db_application,
)

try:
NotificationService.enqueue(
db,
recipient_id=db_application.applicant_id,
sender_id=current_user.id,
type=NotificationType.APPLICATION_REJECTED,
title="Application rejected",
message="Your application was not accepted this time.",
project_id=db_application.project_id,
application_id=db_application.id,
action_url=f"/applications/{db_application.id}",
)
except Exception:
db.rollback()

return rejected


@router.patch(
"/{application_id}/withdraw",
Expand Down
19 changes: 18 additions & 1 deletion backend/app/routers/followers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from app.models.user import User
from app.schemas.follower import FollowerResponse
from app.services.follower_service import FollowerService
from app.models.notification import NotificationType
from app.services.notification_service import NotificationService

router = APIRouter(
prefix="/followers",
Expand Down Expand Up @@ -46,12 +48,27 @@ def follow_user(
detail="Already following this user",
)

return FollowerService.follow_user(
follow = FollowerService.follow_user(
db,
current_user.id,
user_id,
)

try:
NotificationService.enqueue(
db,
recipient_id=user_id,
sender_id=current_user.id,
type=NotificationType.FOLLOW,
title="New follower",
message=f"{current_user.username} started following you.",
action_url=f"/users/{current_user.id}",
)
except Exception:
db.rollback()

return follow


@router.delete(
"/{user_id}",
Expand Down
32 changes: 31 additions & 1 deletion backend/app/routers/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
MessageUpdate,
)
from app.services.message_service import MessageService
from sqlalchemy import select

from app.models.conversation_member import ConversationMember
from app.models.notification import NotificationType
from app.services.notification_service import NotificationService

router = APIRouter(
prefix="/messages",
Expand All @@ -32,13 +37,38 @@ def send_message(
current_user: User = Depends(get_current_user),
):

return MessageService.send_message(
sent = MessageService.send_message(
db=db,
conversation_id=message.conversation_id,
sender_id=current_user.id,
message=message,
)

try:
recipient_ids = db.scalars(
select(ConversationMember.user_id).where(
ConversationMember.conversation_id == message.conversation_id,
ConversationMember.user_id != current_user.id,
)
).all()

for recipient_id in recipient_ids:
NotificationService.enqueue(
db,
recipient_id=recipient_id,
sender_id=current_user.id,
type=NotificationType.MESSAGE,
title="New message",
message=f"{current_user.username} sent you a message.",
conversation_id=message.conversation_id,
message_id=sent.id,
action_url=f"/conversations/{message.conversation_id}",
)
except Exception:
db.rollback()

return sent


@router.get(
"/{message_id}",
Expand Down
69 changes: 69 additions & 0 deletions backend/app/schemas/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from __future__ import annotations

import uuid
from datetime import datetime
from typing import Optional

from pydantic import BaseModel, ConfigDict

from app.models.notification import NotificationType


# ==========================================================
# Base
# ==========================================================


class NotificationBase(BaseModel):
type: NotificationType
title: str
message: str

action_url: Optional[str] = None
image_url: Optional[str] = None

project_id: Optional[uuid.UUID] = None
conversation_id: Optional[uuid.UUID] = None
message_id: Optional[uuid.UUID] = None
application_id: Optional[uuid.UUID] = None


# ==========================================================
# Create (the existing test endpoint reads notification.recipient_id)
# ==========================================================


class NotificationCreate(NotificationBase):
recipient_id: uuid.UUID


# ==========================================================
# Update
# ==========================================================


class NotificationUpdate(BaseModel):
is_read: Optional[bool] = None
title: Optional[str] = None
message: Optional[str] = None
action_url: Optional[str] = None
image_url: Optional[str] = None


# ==========================================================
# Response
# ==========================================================


class NotificationResponse(NotificationBase):
model_config = ConfigDict(from_attributes=True)

id: uuid.UUID
recipient_id: uuid.UUID
sender_id: Optional[uuid.UUID] = None

is_read: bool
read_at: Optional[datetime] = None

created_at: datetime
updated_at: datetime
Loading
Loading