Update code via agent code generation
This commit is contained in:
parent
771ee5214f
commit
75feb39820
451
alembic/versions/002_enhanced_features.py
Normal file
451
alembic/versions/002_enhanced_features.py
Normal file
@ -0,0 +1,451 @@
|
||||
"""Enhanced features migration
|
||||
|
||||
Revision ID: 002
|
||||
Revises: 001
|
||||
Create Date: 2025-07-01 14:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "002"
|
||||
down_revision: Union[str, None] = "001"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create notifications table
|
||||
op.create_table(
|
||||
"notifications",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("recipient_id", sa.Integer(), nullable=False),
|
||||
sa.Column("sender_id", sa.Integer(), nullable=True),
|
||||
sa.Column(
|
||||
"type",
|
||||
sa.Enum(
|
||||
"CONNECTION_REQUEST",
|
||||
"CONNECTION_ACCEPTED",
|
||||
"POST_LIKE",
|
||||
"POST_COMMENT",
|
||||
"EVENT_INVITATION",
|
||||
"EVENT_REMINDER",
|
||||
"PRAYER_REQUEST",
|
||||
"MINISTRY_ASSIGNMENT",
|
||||
"DONATION_REMINDER",
|
||||
"BIRTHDAY_REMINDER",
|
||||
"ANNOUNCEMENT",
|
||||
name="notificationtype",
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("message", sa.Text(), nullable=False),
|
||||
sa.Column("data", sa.Text(), nullable=True),
|
||||
sa.Column("is_read", sa.Boolean(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("read_at", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["recipient_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["sender_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_notifications_id"), "notifications", ["id"], unique=False)
|
||||
|
||||
# Create notification_settings table
|
||||
op.create_table(
|
||||
"notification_settings",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.Column("email_notifications", sa.Boolean(), nullable=True),
|
||||
sa.Column("push_notifications", sa.Boolean(), nullable=True),
|
||||
sa.Column("connection_requests", sa.Boolean(), nullable=True),
|
||||
sa.Column("post_interactions", sa.Boolean(), nullable=True),
|
||||
sa.Column("event_reminders", sa.Boolean(), nullable=True),
|
||||
sa.Column("prayer_requests", sa.Boolean(), nullable=True),
|
||||
sa.Column("ministry_updates", sa.Boolean(), nullable=True),
|
||||
sa.Column("donation_reminders", sa.Boolean(), nullable=True),
|
||||
sa.Column("birthday_reminders", sa.Boolean(), nullable=True),
|
||||
sa.Column("announcements", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_notification_settings_id"),
|
||||
"notification_settings",
|
||||
["id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# Create ministries table
|
||||
op.create_table(
|
||||
"ministries",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("name", sa.String(), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("vision", sa.Text(), nullable=True),
|
||||
sa.Column("mission", sa.Text(), nullable=True),
|
||||
sa.Column("meeting_day", sa.String(), nullable=True),
|
||||
sa.Column("meeting_time", sa.String(), nullable=True),
|
||||
sa.Column("meeting_location", sa.String(), nullable=True),
|
||||
sa.Column("leader_id", sa.Integer(), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["leader_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_ministries_id"), "ministries", ["id"], unique=False)
|
||||
|
||||
# Create ministry_members table
|
||||
op.create_table(
|
||||
"ministry_members",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("ministry_id", sa.Integer(), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.Column(
|
||||
"role",
|
||||
sa.Enum("LEADER", "CO_LEADER", "MEMBER", "VOLUNTEER", name="ministryrole"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("joined_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["ministry_id"],
|
||||
["ministries.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_ministry_members_id"), "ministry_members", ["id"], unique=False
|
||||
)
|
||||
|
||||
# Create ministry_activities table
|
||||
op.create_table(
|
||||
"ministry_activities",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("ministry_id", sa.Integer(), nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("activity_date", sa.DateTime(), nullable=False),
|
||||
sa.Column("location", sa.String(), nullable=True),
|
||||
sa.Column("created_by", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["created_by"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["ministry_id"],
|
||||
["ministries.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_ministry_activities_id"), "ministry_activities", ["id"], unique=False
|
||||
)
|
||||
|
||||
# Create donations table
|
||||
op.create_table(
|
||||
"donations",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("donor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("amount", sa.Numeric(precision=10, scale=2), nullable=False),
|
||||
sa.Column("currency", sa.String(), nullable=True),
|
||||
sa.Column(
|
||||
"donation_type",
|
||||
sa.Enum(
|
||||
"TITHE",
|
||||
"OFFERING",
|
||||
"SPECIAL_OFFERING",
|
||||
"BUILDING_FUND",
|
||||
"MISSIONS",
|
||||
"CHARITY",
|
||||
"OTHER",
|
||||
name="donationtype",
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"payment_method",
|
||||
sa.Enum(
|
||||
"CASH",
|
||||
"CHECK",
|
||||
"CARD",
|
||||
"BANK_TRANSFER",
|
||||
"MOBILE_MONEY",
|
||||
"ONLINE",
|
||||
name="paymentmethod",
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("reference_number", sa.String(), nullable=True),
|
||||
sa.Column("notes", sa.Text(), nullable=True),
|
||||
sa.Column("is_anonymous", sa.Boolean(), nullable=True),
|
||||
sa.Column("is_recurring", sa.Boolean(), nullable=True),
|
||||
sa.Column("recurring_frequency", sa.String(), nullable=True),
|
||||
sa.Column("next_due_date", sa.DateTime(), nullable=True),
|
||||
sa.Column("donation_date", sa.DateTime(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["donor_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_donations_id"), "donations", ["id"], unique=False)
|
||||
|
||||
# Create conversations table
|
||||
op.create_table(
|
||||
"conversations",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("participant_1_id", sa.Integer(), nullable=False),
|
||||
sa.Column("participant_2_id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["participant_1_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["participant_2_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_conversations_id"), "conversations", ["id"], unique=False)
|
||||
|
||||
# Create messages table
|
||||
op.create_table(
|
||||
"messages",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("conversation_id", sa.Integer(), nullable=False),
|
||||
sa.Column("sender_id", sa.Integer(), nullable=False),
|
||||
sa.Column("receiver_id", sa.Integer(), nullable=False),
|
||||
sa.Column(
|
||||
"message_type",
|
||||
sa.Enum("TEXT", "IMAGE", "FILE", name="messagetype"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("file_url", sa.String(), nullable=True),
|
||||
sa.Column("is_read", sa.Boolean(), nullable=True),
|
||||
sa.Column("read_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["conversation_id"],
|
||||
["conversations.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["receiver_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["sender_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_messages_id"), "messages", ["id"], unique=False)
|
||||
|
||||
# Create prayer_requests table
|
||||
op.create_table(
|
||||
"prayer_requests",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("requester_id", sa.Integer(), nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
"category",
|
||||
sa.Enum(
|
||||
"HEALTH",
|
||||
"FAMILY",
|
||||
"FINANCES",
|
||||
"RELATIONSHIPS",
|
||||
"WORK",
|
||||
"SPIRITUAL",
|
||||
"CHURCH",
|
||||
"WORLD",
|
||||
"OTHER",
|
||||
name="prayercategory",
|
||||
),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"privacy_level",
|
||||
sa.Enum(
|
||||
"PUBLIC",
|
||||
"CONNECTIONS_ONLY",
|
||||
"MINISTRY_ONLY",
|
||||
"PRIVATE",
|
||||
name="prayerprivacylevel",
|
||||
),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum("ACTIVE", "ANSWERED", "CLOSED", name="prayerstatus"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("ministry_id", sa.Integer(), nullable=True),
|
||||
sa.Column("is_anonymous", sa.Boolean(), nullable=True),
|
||||
sa.Column("answer_description", sa.Text(), nullable=True),
|
||||
sa.Column("answered_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["ministry_id"],
|
||||
["ministries.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["requester_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_prayer_requests_id"), "prayer_requests", ["id"], unique=False
|
||||
)
|
||||
|
||||
# Create prayers table
|
||||
op.create_table(
|
||||
"prayers",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("prayer_request_id", sa.Integer(), nullable=False),
|
||||
sa.Column("prayer_warrior_id", sa.Integer(), nullable=False),
|
||||
sa.Column("message", sa.Text(), nullable=True),
|
||||
sa.Column("prayed_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["prayer_request_id"],
|
||||
["prayer_requests.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["prayer_warrior_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_prayers_id"), "prayers", ["id"], unique=False)
|
||||
|
||||
# Create services table
|
||||
op.create_table(
|
||||
"services",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column(
|
||||
"service_type",
|
||||
sa.Enum(
|
||||
"SUNDAY_SERVICE",
|
||||
"BIBLE_STUDY",
|
||||
"PRAYER_MEETING",
|
||||
"YOUTH_SERVICE",
|
||||
"WOMEN_MEETING",
|
||||
"MEN_MEETING",
|
||||
"SPECIAL_SERVICE",
|
||||
"OTHER",
|
||||
name="servicetype",
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("service_date", sa.Date(), nullable=False),
|
||||
sa.Column("start_time", sa.DateTime(), nullable=False),
|
||||
sa.Column("end_time", sa.DateTime(), nullable=True),
|
||||
sa.Column("location", sa.String(), nullable=True),
|
||||
sa.Column("minister_id", sa.Integer(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=True),
|
||||
sa.Column("created_by", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["created_by"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["minister_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_services_id"), "services", ["id"], unique=False)
|
||||
|
||||
# Create attendance_records table
|
||||
op.create_table(
|
||||
"attendance_records",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("service_id", sa.Integer(), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum("PRESENT", "ABSENT", "LATE", "EXCUSED", name="attendancestatus"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("check_in_time", sa.DateTime(), nullable=True),
|
||||
sa.Column("check_out_time", sa.DateTime(), nullable=True),
|
||||
sa.Column("notes", sa.Text(), nullable=True),
|
||||
sa.Column("recorded_by", sa.Integer(), nullable=False),
|
||||
sa.Column("recorded_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["recorded_by"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["service_id"],
|
||||
["services.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_attendance_records_id"), "attendance_records", ["id"], unique=False
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(op.f("ix_attendance_records_id"), table_name="attendance_records")
|
||||
op.drop_table("attendance_records")
|
||||
op.drop_index(op.f("ix_services_id"), table_name="services")
|
||||
op.drop_table("services")
|
||||
op.drop_index(op.f("ix_prayers_id"), table_name="prayers")
|
||||
op.drop_table("prayers")
|
||||
op.drop_index(op.f("ix_prayer_requests_id"), table_name="prayer_requests")
|
||||
op.drop_table("prayer_requests")
|
||||
op.drop_index(op.f("ix_messages_id"), table_name="messages")
|
||||
op.drop_table("messages")
|
||||
op.drop_index(op.f("ix_conversations_id"), table_name="conversations")
|
||||
op.drop_table("conversations")
|
||||
op.drop_index(op.f("ix_donations_id"), table_name="donations")
|
||||
op.drop_table("donations")
|
||||
op.drop_index(op.f("ix_ministry_activities_id"), table_name="ministry_activities")
|
||||
op.drop_table("ministry_activities")
|
||||
op.drop_index(op.f("ix_ministry_members_id"), table_name="ministry_members")
|
||||
op.drop_table("ministry_members")
|
||||
op.drop_index(op.f("ix_ministries_id"), table_name="ministries")
|
||||
op.drop_table("ministries")
|
||||
op.drop_index(
|
||||
op.f("ix_notification_settings_id"), table_name="notification_settings"
|
||||
)
|
||||
op.drop_table("notification_settings")
|
||||
op.drop_index(op.f("ix_notifications_id"), table_name="notifications")
|
||||
op.drop_table("notifications")
|
484
app/api/attendance.py
Normal file
484
app/api/attendance.py
Normal file
@ -0,0 +1,484 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, date
|
||||
from app.db.base import get_db
|
||||
from app.models.user import User
|
||||
from app.models.attendance import (
|
||||
Service,
|
||||
AttendanceRecord,
|
||||
AttendanceGoal,
|
||||
ServiceType,
|
||||
AttendanceStatus,
|
||||
)
|
||||
from app.schemas.attendance import (
|
||||
ServiceCreate,
|
||||
ServiceResponse,
|
||||
AttendanceRecordCreate,
|
||||
AttendanceRecordUpdate,
|
||||
AttendanceRecordResponse,
|
||||
AttendanceGoalCreate,
|
||||
AttendanceGoalResponse,
|
||||
AttendanceStats,
|
||||
)
|
||||
from app.api.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Services
|
||||
|
||||
|
||||
@router.post("/services", response_model=ServiceResponse)
|
||||
def create_service(
|
||||
service: ServiceCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new service (admin only)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
db_service = Service(**service.dict(), created_by=current_user.id)
|
||||
db.add(db_service)
|
||||
db.commit()
|
||||
db.refresh(db_service)
|
||||
|
||||
db_service.creator_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
if db_service.minister:
|
||||
db_service.minister_name = (
|
||||
f"{db_service.minister.first_name} {db_service.minister.last_name}"
|
||||
)
|
||||
db_service.attendance_count = 0
|
||||
|
||||
return db_service
|
||||
|
||||
|
||||
@router.get("/services", response_model=List[ServiceResponse])
|
||||
def get_services(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
service_type: Optional[ServiceType] = None,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get services"""
|
||||
query = db.query(Service).filter(Service.is_active)
|
||||
|
||||
if service_type:
|
||||
query = query.filter(Service.service_type == service_type)
|
||||
if start_date:
|
||||
query = query.filter(Service.service_date >= start_date)
|
||||
if end_date:
|
||||
query = query.filter(Service.service_date <= end_date)
|
||||
|
||||
services = (
|
||||
query.order_by(Service.service_date.desc()).offset(skip).limit(limit).all()
|
||||
)
|
||||
|
||||
for service in services:
|
||||
service.creator_name = (
|
||||
f"{service.creator.first_name} {service.creator.last_name}"
|
||||
)
|
||||
if service.minister:
|
||||
service.minister_name = (
|
||||
f"{service.minister.first_name} {service.minister.last_name}"
|
||||
)
|
||||
|
||||
service.attendance_count = (
|
||||
db.query(AttendanceRecord)
|
||||
.filter(
|
||||
AttendanceRecord.service_id == service.id,
|
||||
AttendanceRecord.status.in_(
|
||||
[AttendanceStatus.PRESENT, AttendanceStatus.LATE]
|
||||
),
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
return services
|
||||
|
||||
|
||||
@router.get("/services/{service_id}", response_model=ServiceResponse)
|
||||
def get_service(
|
||||
service_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get specific service"""
|
||||
service = db.query(Service).filter(Service.id == service_id).first()
|
||||
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
|
||||
service.creator_name = f"{service.creator.first_name} {service.creator.last_name}"
|
||||
if service.minister:
|
||||
service.minister_name = (
|
||||
f"{service.minister.first_name} {service.minister.last_name}"
|
||||
)
|
||||
|
||||
service.attendance_count = (
|
||||
db.query(AttendanceRecord)
|
||||
.filter(
|
||||
AttendanceRecord.service_id == service.id,
|
||||
AttendanceRecord.status.in_(
|
||||
[AttendanceStatus.PRESENT, AttendanceStatus.LATE]
|
||||
),
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
return service
|
||||
|
||||
|
||||
# Attendance Records
|
||||
|
||||
|
||||
@router.post(
|
||||
"/services/{service_id}/attendance", response_model=AttendanceRecordResponse
|
||||
)
|
||||
def record_attendance(
|
||||
service_id: int,
|
||||
attendance: AttendanceRecordCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Record attendance for a service (admin only)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
service = db.query(Service).filter(Service.id == service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
|
||||
# Check if attendance already recorded
|
||||
existing_record = (
|
||||
db.query(AttendanceRecord)
|
||||
.filter(
|
||||
AttendanceRecord.service_id == service_id,
|
||||
AttendanceRecord.user_id == attendance.user_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing_record:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Attendance already recorded for this user"
|
||||
)
|
||||
|
||||
db_record = AttendanceRecord(
|
||||
**attendance.dict(), service_id=service_id, recorded_by=current_user.id
|
||||
)
|
||||
|
||||
db.add(db_record)
|
||||
db.commit()
|
||||
db.refresh(db_record)
|
||||
|
||||
db_record.user_name = f"{db_record.user.first_name} {db_record.user.last_name}"
|
||||
db_record.recorder_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
|
||||
return db_record
|
||||
|
||||
|
||||
@router.get(
|
||||
"/services/{service_id}/attendance", response_model=List[AttendanceRecordResponse]
|
||||
)
|
||||
def get_service_attendance(
|
||||
service_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get attendance records for a service"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
records = (
|
||||
db.query(AttendanceRecord)
|
||||
.filter(AttendanceRecord.service_id == service_id)
|
||||
.all()
|
||||
)
|
||||
|
||||
for record in records:
|
||||
record.user_name = f"{record.user.first_name} {record.user.last_name}"
|
||||
record.recorder_name = (
|
||||
f"{record.recorder.first_name} {record.recorder.last_name}"
|
||||
)
|
||||
|
||||
return records
|
||||
|
||||
|
||||
@router.put("/attendance/{record_id}", response_model=AttendanceRecordResponse)
|
||||
def update_attendance_record(
|
||||
record_id: int,
|
||||
attendance_update: AttendanceRecordUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update attendance record (admin only)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
record = db.query(AttendanceRecord).filter(AttendanceRecord.id == record_id).first()
|
||||
|
||||
if not record:
|
||||
raise HTTPException(status_code=404, detail="Attendance record not found")
|
||||
|
||||
for field, value in attendance_update.dict(exclude_unset=True).items():
|
||||
setattr(record, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(record)
|
||||
|
||||
return record
|
||||
|
||||
|
||||
# Personal Attendance
|
||||
|
||||
|
||||
@router.get("/my-attendance", response_model=List[AttendanceRecordResponse])
|
||||
def get_my_attendance(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
service_type: Optional[ServiceType] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get current user's attendance records"""
|
||||
query = db.query(AttendanceRecord).filter(
|
||||
AttendanceRecord.user_id == current_user.id
|
||||
)
|
||||
|
||||
if service_type:
|
||||
query = query.join(Service).filter(Service.service_type == service_type)
|
||||
|
||||
records = (
|
||||
query.order_by(AttendanceRecord.recorded_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
for record in records:
|
||||
record.user_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
record.recorder_name = (
|
||||
f"{record.recorder.first_name} {record.recorder.last_name}"
|
||||
)
|
||||
|
||||
return records
|
||||
|
||||
|
||||
@router.get("/my-stats", response_model=AttendanceStats)
|
||||
def get_my_attendance_stats(
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
service_type: Optional[ServiceType] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get personal attendance statistics"""
|
||||
|
||||
# Default to current year if no dates provided
|
||||
if not start_date:
|
||||
start_date = date(datetime.now().year, 1, 1)
|
||||
if not end_date:
|
||||
end_date = date.today()
|
||||
|
||||
# Base queries
|
||||
services_query = db.query(Service).filter(
|
||||
Service.service_date >= start_date,
|
||||
Service.service_date <= end_date,
|
||||
Service.is_active,
|
||||
)
|
||||
|
||||
attendance_query = (
|
||||
db.query(AttendanceRecord)
|
||||
.filter(AttendanceRecord.user_id == current_user.id)
|
||||
.join(Service)
|
||||
.filter(Service.service_date >= start_date, Service.service_date <= end_date)
|
||||
)
|
||||
|
||||
if service_type:
|
||||
services_query = services_query.filter(Service.service_type == service_type)
|
||||
attendance_query = attendance_query.filter(Service.service_type == service_type)
|
||||
|
||||
total_services = services_query.count()
|
||||
|
||||
attendance_records = attendance_query.all()
|
||||
services_attended = len(
|
||||
[
|
||||
r
|
||||
for r in attendance_records
|
||||
if r.status in [AttendanceStatus.PRESENT, AttendanceStatus.LATE]
|
||||
]
|
||||
)
|
||||
|
||||
attendance_percentage = (
|
||||
(services_attended / total_services * 100) if total_services > 0 else 0
|
||||
)
|
||||
|
||||
# Find most attended service type
|
||||
if not service_type:
|
||||
service_type_counts = (
|
||||
db.query(
|
||||
Service.service_type, func.count(AttendanceRecord.id).label("count")
|
||||
)
|
||||
.join(AttendanceRecord)
|
||||
.filter(
|
||||
AttendanceRecord.user_id == current_user.id,
|
||||
AttendanceRecord.status.in_(
|
||||
[AttendanceStatus.PRESENT, AttendanceStatus.LATE]
|
||||
),
|
||||
Service.service_date >= start_date,
|
||||
Service.service_date <= end_date,
|
||||
)
|
||||
.group_by(Service.service_type)
|
||||
.order_by(func.count(AttendanceRecord.id).desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
most_attended_service_type = (
|
||||
service_type_counts.service_type if service_type_counts else None
|
||||
)
|
||||
else:
|
||||
most_attended_service_type = service_type
|
||||
|
||||
# Calculate streaks (simplified)
|
||||
recent_services = (
|
||||
db.query(Service)
|
||||
.filter(Service.service_date <= date.today())
|
||||
.order_by(Service.service_date.desc())
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
|
||||
current_streak = 0
|
||||
for service in recent_services:
|
||||
record = (
|
||||
db.query(AttendanceRecord)
|
||||
.filter(
|
||||
AttendanceRecord.service_id == service.id,
|
||||
AttendanceRecord.user_id == current_user.id,
|
||||
AttendanceRecord.status.in_(
|
||||
[AttendanceStatus.PRESENT, AttendanceStatus.LATE]
|
||||
),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if record:
|
||||
current_streak += 1
|
||||
else:
|
||||
break
|
||||
|
||||
return AttendanceStats(
|
||||
total_services=total_services,
|
||||
services_attended=services_attended,
|
||||
attendance_percentage=attendance_percentage,
|
||||
most_attended_service_type=most_attended_service_type,
|
||||
current_streak=current_streak,
|
||||
longest_streak=current_streak, # Simplified - would need more complex logic for actual longest
|
||||
)
|
||||
|
||||
|
||||
# Attendance Goals
|
||||
|
||||
|
||||
@router.post("/goals", response_model=AttendanceGoalResponse)
|
||||
def create_attendance_goal(
|
||||
goal: AttendanceGoalCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create attendance goal"""
|
||||
|
||||
# Deactivate existing goals for the same service type
|
||||
db.query(AttendanceGoal).filter(
|
||||
AttendanceGoal.user_id == current_user.id,
|
||||
AttendanceGoal.service_type == goal.service_type,
|
||||
AttendanceGoal.is_active,
|
||||
).update({"is_active": False})
|
||||
|
||||
db_goal = AttendanceGoal(**goal.dict(), user_id=current_user.id)
|
||||
db.add(db_goal)
|
||||
db.commit()
|
||||
db.refresh(db_goal)
|
||||
|
||||
# Calculate current percentage
|
||||
services_count = (
|
||||
db.query(Service)
|
||||
.filter(
|
||||
Service.service_type == goal.service_type,
|
||||
Service.service_date >= goal.start_date,
|
||||
Service.service_date <= min(goal.end_date, date.today()),
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
attended_count = (
|
||||
db.query(AttendanceRecord)
|
||||
.join(Service)
|
||||
.filter(
|
||||
Service.service_type == goal.service_type,
|
||||
Service.service_date >= goal.start_date,
|
||||
Service.service_date <= min(goal.end_date, date.today()),
|
||||
AttendanceRecord.user_id == current_user.id,
|
||||
AttendanceRecord.status.in_(
|
||||
[AttendanceStatus.PRESENT, AttendanceStatus.LATE]
|
||||
),
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
db_goal.current_percentage = (
|
||||
(attended_count / services_count * 100) if services_count > 0 else 0
|
||||
)
|
||||
|
||||
return db_goal
|
||||
|
||||
|
||||
@router.get("/goals", response_model=List[AttendanceGoalResponse])
|
||||
def get_my_attendance_goals(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get user's attendance goals"""
|
||||
|
||||
goals = (
|
||||
db.query(AttendanceGoal)
|
||||
.filter(AttendanceGoal.user_id == current_user.id, AttendanceGoal.is_active)
|
||||
.all()
|
||||
)
|
||||
|
||||
for goal in goals:
|
||||
# Calculate current percentage
|
||||
services_count = (
|
||||
db.query(Service)
|
||||
.filter(
|
||||
Service.service_type == goal.service_type,
|
||||
Service.service_date >= goal.start_date,
|
||||
Service.service_date <= min(goal.end_date, date.today()),
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
attended_count = (
|
||||
db.query(AttendanceRecord)
|
||||
.join(Service)
|
||||
.filter(
|
||||
Service.service_type == goal.service_type,
|
||||
Service.service_date >= goal.start_date,
|
||||
Service.service_date <= min(goal.end_date, date.today()),
|
||||
AttendanceRecord.user_id == current_user.id,
|
||||
AttendanceRecord.status.in_(
|
||||
[AttendanceStatus.PRESENT, AttendanceStatus.LATE]
|
||||
),
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
goal.current_percentage = (
|
||||
(attended_count / services_count * 100) if services_count > 0 else 0
|
||||
)
|
||||
|
||||
return goals
|
311
app/api/donations.py
Normal file
311
app/api/donations.py
Normal file
@ -0,0 +1,311 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, extract
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from app.db.base import get_db
|
||||
from app.models.user import User
|
||||
from app.models.donation import Donation, DonationGoal, TithePledge, DonationType
|
||||
from app.schemas.donation import (
|
||||
DonationCreate,
|
||||
DonationResponse,
|
||||
DonationGoalCreate,
|
||||
DonationGoalResponse,
|
||||
TithePledgeCreate,
|
||||
TithePledgeResponse,
|
||||
DonationStats,
|
||||
MonthlyDonationSummary,
|
||||
)
|
||||
from app.api.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/", response_model=DonationResponse)
|
||||
def create_donation(
|
||||
donation: DonationCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new donation"""
|
||||
donation_data = donation.dict()
|
||||
if not donation_data.get("donation_date"):
|
||||
donation_data["donation_date"] = datetime.utcnow()
|
||||
|
||||
# Calculate next due date for recurring donations
|
||||
if donation.is_recurring and donation.recurring_frequency:
|
||||
if donation.recurring_frequency == "weekly":
|
||||
donation_data["next_due_date"] = donation_data["donation_date"] + timedelta(
|
||||
weeks=1
|
||||
)
|
||||
elif donation.recurring_frequency == "monthly":
|
||||
donation_data["next_due_date"] = donation_data["donation_date"] + timedelta(
|
||||
days=30
|
||||
)
|
||||
elif donation.recurring_frequency == "yearly":
|
||||
donation_data["next_due_date"] = donation_data["donation_date"] + timedelta(
|
||||
days=365
|
||||
)
|
||||
|
||||
db_donation = Donation(**donation_data, donor_id=current_user.id)
|
||||
db.add(db_donation)
|
||||
db.commit()
|
||||
db.refresh(db_donation)
|
||||
|
||||
db_donation.donor_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
return db_donation
|
||||
|
||||
|
||||
@router.get("/", response_model=List[DonationResponse])
|
||||
def get_my_donations(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
donation_type: Optional[DonationType] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get current user's donations"""
|
||||
query = db.query(Donation).filter(Donation.donor_id == current_user.id)
|
||||
|
||||
if donation_type:
|
||||
query = query.filter(Donation.donation_type == donation_type)
|
||||
|
||||
donations = (
|
||||
query.order_by(Donation.donation_date.desc()).offset(skip).limit(limit).all()
|
||||
)
|
||||
|
||||
for donation in donations:
|
||||
donation.donor_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
|
||||
return donations
|
||||
|
||||
|
||||
@router.get("/all", response_model=List[DonationResponse])
|
||||
def get_all_donations(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
donation_type: Optional[DonationType] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get all donations (admin only)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
query = db.query(Donation)
|
||||
|
||||
if donation_type:
|
||||
query = query.filter(Donation.donation_type == donation_type)
|
||||
|
||||
donations = (
|
||||
query.order_by(Donation.donation_date.desc()).offset(skip).limit(limit).all()
|
||||
)
|
||||
|
||||
for donation in donations:
|
||||
if not donation.is_anonymous:
|
||||
donation.donor_name = (
|
||||
f"{donation.donor.first_name} {donation.donor.last_name}"
|
||||
)
|
||||
else:
|
||||
donation.donor_name = "Anonymous"
|
||||
|
||||
return donations
|
||||
|
||||
|
||||
@router.get("/stats", response_model=DonationStats)
|
||||
def get_donation_stats(
|
||||
year: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get donation statistics"""
|
||||
query = db.query(Donation)
|
||||
|
||||
if not current_user.is_admin:
|
||||
query = query.filter(Donation.donor_id == current_user.id)
|
||||
|
||||
if year:
|
||||
query = query.filter(extract("year", Donation.donation_date) == year)
|
||||
|
||||
donations = query.all()
|
||||
|
||||
total_donations = sum(d.amount for d in donations)
|
||||
total_tithes = sum(
|
||||
d.amount for d in donations if d.donation_type == DonationType.TITHE
|
||||
)
|
||||
total_offerings = sum(
|
||||
d.amount for d in donations if d.donation_type == DonationType.OFFERING
|
||||
)
|
||||
donation_count = len(donations)
|
||||
average_donation = (
|
||||
total_donations / donation_count if donation_count > 0 else Decimal("0")
|
||||
)
|
||||
|
||||
return DonationStats(
|
||||
total_donations=total_donations,
|
||||
total_tithes=total_tithes,
|
||||
total_offerings=total_offerings,
|
||||
donation_count=donation_count,
|
||||
average_donation=average_donation,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/monthly-summary", response_model=List[MonthlyDonationSummary])
|
||||
def get_monthly_donation_summary(
|
||||
year: int = datetime.now().year,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get monthly donation summary"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
results = (
|
||||
db.query(
|
||||
extract("month", Donation.donation_date).label("month"),
|
||||
extract("year", Donation.donation_date).label("year"),
|
||||
func.sum(Donation.amount).label("total_amount"),
|
||||
func.count(Donation.id).label("donation_count"),
|
||||
func.sum(
|
||||
func.case(
|
||||
(Donation.donation_type == DonationType.TITHE, Donation.amount),
|
||||
else_=0,
|
||||
)
|
||||
).label("tithe_amount"),
|
||||
func.sum(
|
||||
func.case(
|
||||
(Donation.donation_type == DonationType.OFFERING, Donation.amount),
|
||||
else_=0,
|
||||
)
|
||||
).label("offering_amount"),
|
||||
)
|
||||
.filter(extract("year", Donation.donation_date) == year)
|
||||
.group_by(
|
||||
extract("month", Donation.donation_date),
|
||||
extract("year", Donation.donation_date),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
months = [
|
||||
"",
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
]
|
||||
|
||||
summary = []
|
||||
for result in results:
|
||||
summary.append(
|
||||
MonthlyDonationSummary(
|
||||
month=months[int(result.month)],
|
||||
year=int(result.year),
|
||||
total_amount=result.total_amount or Decimal("0"),
|
||||
donation_count=result.donation_count,
|
||||
tithe_amount=result.tithe_amount or Decimal("0"),
|
||||
offering_amount=result.offering_amount or Decimal("0"),
|
||||
)
|
||||
)
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
# Donation Goals
|
||||
|
||||
|
||||
@router.post("/goals", response_model=DonationGoalResponse)
|
||||
def create_donation_goal(
|
||||
goal: DonationGoalCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a donation goal (admin only)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
db_goal = DonationGoal(**goal.dict(), created_by=current_user.id)
|
||||
db.add(db_goal)
|
||||
db.commit()
|
||||
db.refresh(db_goal)
|
||||
|
||||
db_goal.creator_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
db_goal.progress_percentage = 0.0
|
||||
db_goal.days_remaining = (db_goal.end_date - datetime.utcnow()).days
|
||||
|
||||
return db_goal
|
||||
|
||||
|
||||
@router.get("/goals", response_model=List[DonationGoalResponse])
|
||||
def get_donation_goals(
|
||||
active_only: bool = True,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get donation goals"""
|
||||
query = db.query(DonationGoal)
|
||||
|
||||
if active_only:
|
||||
query = query.filter(DonationGoal.is_active)
|
||||
|
||||
goals = query.order_by(DonationGoal.created_at.desc()).all()
|
||||
|
||||
for goal in goals:
|
||||
goal.creator_name = f"{goal.creator.first_name} {goal.creator.last_name}"
|
||||
goal.progress_percentage = (
|
||||
(float(goal.current_amount) / float(goal.target_amount)) * 100
|
||||
if goal.target_amount > 0
|
||||
else 0
|
||||
)
|
||||
goal.days_remaining = max(0, (goal.end_date - datetime.utcnow()).days)
|
||||
|
||||
return goals
|
||||
|
||||
|
||||
# Tithe Pledges
|
||||
|
||||
|
||||
@router.post("/tithe-pledge", response_model=TithePledgeResponse)
|
||||
def create_tithe_pledge(
|
||||
pledge: TithePledgeCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create or update tithe pledge"""
|
||||
# Deactivate existing pledges
|
||||
db.query(TithePledge).filter(
|
||||
TithePledge.user_id == current_user.id, TithePledge.is_active
|
||||
).update({"is_active": False})
|
||||
|
||||
db_pledge = TithePledge(**pledge.dict(), user_id=current_user.id)
|
||||
db.add(db_pledge)
|
||||
db.commit()
|
||||
db.refresh(db_pledge)
|
||||
|
||||
return db_pledge
|
||||
|
||||
|
||||
@router.get("/tithe-pledge", response_model=TithePledgeResponse)
|
||||
def get_my_tithe_pledge(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get current user's active tithe pledge"""
|
||||
pledge = (
|
||||
db.query(TithePledge)
|
||||
.filter(TithePledge.user_id == current_user.id, TithePledge.is_active)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not pledge:
|
||||
raise HTTPException(status_code=404, detail="No active tithe pledge found")
|
||||
|
||||
return pledge
|
413
app/api/messages.py
Normal file
413
app/api/messages.py
Normal file
@ -0,0 +1,413 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_, and_, func
|
||||
from typing import List
|
||||
from app.db.base import get_db
|
||||
from app.models.user import User, Connection
|
||||
from app.models.message import (
|
||||
Conversation,
|
||||
Message,
|
||||
GroupChat,
|
||||
GroupChatMember,
|
||||
GroupMessage,
|
||||
)
|
||||
from app.schemas.message import (
|
||||
MessageCreate,
|
||||
MessageResponse,
|
||||
ConversationResponse,
|
||||
GroupChatCreate,
|
||||
GroupChatResponse,
|
||||
GroupMessageCreate,
|
||||
GroupMessageResponse,
|
||||
)
|
||||
from app.api.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Direct Messages
|
||||
|
||||
|
||||
@router.get("/conversations", response_model=List[ConversationResponse])
|
||||
def get_conversations(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get user's conversations"""
|
||||
conversations = (
|
||||
db.query(Conversation)
|
||||
.filter(
|
||||
or_(
|
||||
Conversation.participant_1_id == current_user.id,
|
||||
Conversation.participant_2_id == current_user.id,
|
||||
)
|
||||
)
|
||||
.order_by(Conversation.updated_at.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for conv in conversations:
|
||||
# Determine other participant
|
||||
other_participant = (
|
||||
conv.participant_2
|
||||
if conv.participant_1_id == current_user.id
|
||||
else conv.participant_1
|
||||
)
|
||||
|
||||
# Get last message
|
||||
last_message = (
|
||||
db.query(Message)
|
||||
.filter(Message.conversation_id == conv.id)
|
||||
.order_by(Message.created_at.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
# Get unread count
|
||||
unread_count = (
|
||||
db.query(Message)
|
||||
.filter(
|
||||
Message.conversation_id == conv.id,
|
||||
Message.receiver_id == current_user.id,
|
||||
Message.is_read is False,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
conv_response = ConversationResponse(
|
||||
id=conv.id,
|
||||
participant_1_id=conv.participant_1_id,
|
||||
participant_2_id=conv.participant_2_id,
|
||||
created_at=conv.created_at,
|
||||
updated_at=conv.updated_at,
|
||||
other_participant_name=f"{other_participant.first_name} {other_participant.last_name}",
|
||||
other_participant_id=other_participant.id,
|
||||
last_message=last_message.content if last_message else None,
|
||||
unread_count=unread_count,
|
||||
)
|
||||
result.append(conv_response)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/conversations/{user_id}/messages", response_model=List[MessageResponse])
|
||||
def get_conversation_messages(
|
||||
user_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get messages in a conversation with specific user"""
|
||||
|
||||
# Check if users are connected
|
||||
connection = (
|
||||
db.query(Connection)
|
||||
.filter(
|
||||
or_(
|
||||
and_(
|
||||
Connection.sender_id == current_user.id,
|
||||
Connection.receiver_id == user_id,
|
||||
),
|
||||
and_(
|
||||
Connection.sender_id == user_id,
|
||||
Connection.receiver_id == current_user.id,
|
||||
),
|
||||
),
|
||||
Connection.status == "accepted",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not connection:
|
||||
raise HTTPException(status_code=403, detail="Can only message connections")
|
||||
|
||||
# Get or create conversation
|
||||
conversation = (
|
||||
db.query(Conversation)
|
||||
.filter(
|
||||
or_(
|
||||
and_(
|
||||
Conversation.participant_1_id == current_user.id,
|
||||
Conversation.participant_2_id == user_id,
|
||||
),
|
||||
and_(
|
||||
Conversation.participant_1_id == user_id,
|
||||
Conversation.participant_2_id == current_user.id,
|
||||
),
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not conversation:
|
||||
conversation = Conversation(
|
||||
participant_1_id=min(current_user.id, user_id),
|
||||
participant_2_id=max(current_user.id, user_id),
|
||||
)
|
||||
db.add(conversation)
|
||||
db.commit()
|
||||
db.refresh(conversation)
|
||||
|
||||
# Get messages
|
||||
messages = (
|
||||
db.query(Message)
|
||||
.filter(Message.conversation_id == conversation.id)
|
||||
.order_by(Message.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Mark messages as read
|
||||
db.query(Message).filter(
|
||||
Message.conversation_id == conversation.id,
|
||||
Message.receiver_id == current_user.id,
|
||||
Message.is_read is False,
|
||||
).update({"is_read": True, "read_at": func.now()})
|
||||
db.commit()
|
||||
|
||||
# Add sender names
|
||||
for message in messages:
|
||||
message.sender_name = f"{message.sender.first_name} {message.sender.last_name}"
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
@router.post("/send", response_model=MessageResponse)
|
||||
def send_message(
|
||||
message: MessageCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Send a direct message"""
|
||||
|
||||
# Check if users are connected
|
||||
connection = (
|
||||
db.query(Connection)
|
||||
.filter(
|
||||
or_(
|
||||
and_(
|
||||
Connection.sender_id == current_user.id,
|
||||
Connection.receiver_id == message.receiver_id,
|
||||
),
|
||||
and_(
|
||||
Connection.sender_id == message.receiver_id,
|
||||
Connection.receiver_id == current_user.id,
|
||||
),
|
||||
),
|
||||
Connection.status == "accepted",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not connection:
|
||||
raise HTTPException(status_code=403, detail="Can only message connections")
|
||||
|
||||
# Get or create conversation
|
||||
conversation = (
|
||||
db.query(Conversation)
|
||||
.filter(
|
||||
or_(
|
||||
and_(
|
||||
Conversation.participant_1_id == current_user.id,
|
||||
Conversation.participant_2_id == message.receiver_id,
|
||||
),
|
||||
and_(
|
||||
Conversation.participant_1_id == message.receiver_id,
|
||||
Conversation.participant_2_id == current_user.id,
|
||||
),
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not conversation:
|
||||
conversation = Conversation(
|
||||
participant_1_id=min(current_user.id, message.receiver_id),
|
||||
participant_2_id=max(current_user.id, message.receiver_id),
|
||||
)
|
||||
db.add(conversation)
|
||||
db.commit()
|
||||
db.refresh(conversation)
|
||||
|
||||
# Create message
|
||||
db_message = Message(
|
||||
conversation_id=conversation.id,
|
||||
sender_id=current_user.id,
|
||||
receiver_id=message.receiver_id,
|
||||
content=message.content,
|
||||
message_type=message.message_type,
|
||||
file_url=message.file_url,
|
||||
)
|
||||
|
||||
db.add(db_message)
|
||||
|
||||
# Update conversation timestamp
|
||||
conversation.updated_at = func.now()
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_message)
|
||||
|
||||
db_message.sender_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
return db_message
|
||||
|
||||
|
||||
# Group Chats
|
||||
|
||||
|
||||
@router.post("/groups", response_model=GroupChatResponse)
|
||||
def create_group_chat(
|
||||
group_data: GroupChatCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a group chat"""
|
||||
|
||||
# Create group chat
|
||||
group_chat = GroupChat(
|
||||
name=group_data.name,
|
||||
description=group_data.description,
|
||||
created_by=current_user.id,
|
||||
)
|
||||
|
||||
db.add(group_chat)
|
||||
db.commit()
|
||||
db.refresh(group_chat)
|
||||
|
||||
# Add creator as admin member
|
||||
creator_member = GroupChatMember(
|
||||
group_chat_id=group_chat.id, user_id=current_user.id, is_admin=True
|
||||
)
|
||||
db.add(creator_member)
|
||||
|
||||
# Add other members
|
||||
for member_id in group_data.member_ids:
|
||||
if member_id != current_user.id: # Don't add creator twice
|
||||
member = GroupChatMember(group_chat_id=group_chat.id, user_id=member_id)
|
||||
db.add(member)
|
||||
|
||||
db.commit()
|
||||
|
||||
group_chat.creator_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
group_chat.member_count = len(group_data.member_ids) + 1 # Include creator
|
||||
|
||||
return group_chat
|
||||
|
||||
|
||||
@router.get("/groups", response_model=List[GroupChatResponse])
|
||||
def get_my_group_chats(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get user's group chats"""
|
||||
|
||||
# Get groups where user is a member
|
||||
memberships = (
|
||||
db.query(GroupChatMember)
|
||||
.filter(GroupChatMember.user_id == current_user.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
groups = []
|
||||
for membership in memberships:
|
||||
group = membership.group_chat
|
||||
if group.is_active:
|
||||
# Get member count
|
||||
member_count = (
|
||||
db.query(GroupChatMember)
|
||||
.filter(GroupChatMember.group_chat_id == group.id)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Get last message
|
||||
last_message = (
|
||||
db.query(GroupMessage)
|
||||
.filter(GroupMessage.group_chat_id == group.id)
|
||||
.order_by(GroupMessage.created_at.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
group.creator_name = f"{group.creator.first_name} {group.creator.last_name}"
|
||||
group.member_count = member_count
|
||||
group.last_message = last_message.content if last_message else None
|
||||
|
||||
groups.append(group)
|
||||
|
||||
return groups
|
||||
|
||||
|
||||
@router.get("/groups/{group_id}/messages", response_model=List[GroupMessageResponse])
|
||||
def get_group_messages(
|
||||
group_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get group chat messages"""
|
||||
|
||||
# Check if user is a member
|
||||
membership = (
|
||||
db.query(GroupChatMember)
|
||||
.filter(
|
||||
GroupChatMember.group_chat_id == group_id,
|
||||
GroupChatMember.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not membership:
|
||||
raise HTTPException(status_code=403, detail="Not a member of this group")
|
||||
|
||||
# Get messages
|
||||
messages = (
|
||||
db.query(GroupMessage)
|
||||
.filter(GroupMessage.group_chat_id == group_id)
|
||||
.order_by(GroupMessage.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Add sender names
|
||||
for message in messages:
|
||||
message.sender_name = f"{message.sender.first_name} {message.sender.last_name}"
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
@router.post("/groups/{group_id}/send", response_model=GroupMessageResponse)
|
||||
def send_group_message(
|
||||
group_id: int,
|
||||
message: GroupMessageCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Send a group message"""
|
||||
|
||||
# Check if user is a member
|
||||
membership = (
|
||||
db.query(GroupChatMember)
|
||||
.filter(
|
||||
GroupChatMember.group_chat_id == group_id,
|
||||
GroupChatMember.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not membership:
|
||||
raise HTTPException(status_code=403, detail="Not a member of this group")
|
||||
|
||||
# Create message
|
||||
db_message = GroupMessage(
|
||||
group_chat_id=group_id,
|
||||
sender_id=current_user.id,
|
||||
content=message.content,
|
||||
message_type=message.message_type,
|
||||
file_url=message.file_url,
|
||||
)
|
||||
|
||||
db.add(db_message)
|
||||
db.commit()
|
||||
db.refresh(db_message)
|
||||
|
||||
db_message.sender_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
return db_message
|
325
app/api/ministries.py
Normal file
325
app/api/ministries.py
Normal file
@ -0,0 +1,325 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.db.base import get_db
|
||||
from app.models.user import User
|
||||
from app.models.ministry import (
|
||||
Ministry,
|
||||
MinistryMember,
|
||||
MinistryActivity,
|
||||
MinistryRequest,
|
||||
MinistryRole,
|
||||
)
|
||||
from app.schemas.ministry import (
|
||||
MinistryCreate,
|
||||
MinistryUpdate,
|
||||
MinistryResponse,
|
||||
MinistryMemberResponse,
|
||||
MinistryActivityCreate,
|
||||
MinistryActivityResponse,
|
||||
MinistryJoinRequest,
|
||||
MinistryRequestResponse,
|
||||
)
|
||||
from app.api.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/", response_model=MinistryResponse)
|
||||
def create_ministry(
|
||||
ministry: MinistryCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new ministry"""
|
||||
db_ministry = Ministry(**ministry.dict(), leader_id=current_user.id)
|
||||
db.add(db_ministry)
|
||||
db.commit()
|
||||
db.refresh(db_ministry)
|
||||
|
||||
# Add creator as leader
|
||||
member = MinistryMember(
|
||||
ministry_id=db_ministry.id, user_id=current_user.id, role=MinistryRole.LEADER
|
||||
)
|
||||
db.add(member)
|
||||
db.commit()
|
||||
|
||||
db_ministry.leader_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
db_ministry.member_count = 1
|
||||
db_ministry.is_member = True
|
||||
db_ministry.user_role = MinistryRole.LEADER
|
||||
|
||||
return db_ministry
|
||||
|
||||
|
||||
@router.get("/", response_model=List[MinistryResponse])
|
||||
def get_ministries(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get all ministries"""
|
||||
ministries = (
|
||||
db.query(Ministry).filter(Ministry.is_active).offset(skip).limit(limit).all()
|
||||
)
|
||||
|
||||
for ministry in ministries:
|
||||
ministry.leader_name = (
|
||||
f"{ministry.leader.first_name} {ministry.leader.last_name}"
|
||||
)
|
||||
ministry.member_count = (
|
||||
db.query(MinistryMember)
|
||||
.filter(MinistryMember.ministry_id == ministry.id, MinistryMember.is_active)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Check if current user is a member and their role
|
||||
member = (
|
||||
db.query(MinistryMember)
|
||||
.filter(
|
||||
MinistryMember.ministry_id == ministry.id,
|
||||
MinistryMember.user_id == current_user.id,
|
||||
MinistryMember.is_active,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
ministry.is_member = member is not None
|
||||
ministry.user_role = member.role if member else None
|
||||
|
||||
return ministries
|
||||
|
||||
|
||||
@router.get("/my-ministries", response_model=List[MinistryResponse])
|
||||
def get_my_ministries(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get ministries where current user is a member"""
|
||||
memberships = (
|
||||
db.query(MinistryMember)
|
||||
.filter(MinistryMember.user_id == current_user.id, MinistryMember.is_active)
|
||||
.all()
|
||||
)
|
||||
|
||||
ministries = []
|
||||
for membership in memberships:
|
||||
ministry = membership.ministry
|
||||
ministry.leader_name = (
|
||||
f"{ministry.leader.first_name} {ministry.leader.last_name}"
|
||||
)
|
||||
ministry.member_count = (
|
||||
db.query(MinistryMember)
|
||||
.filter(MinistryMember.ministry_id == ministry.id, MinistryMember.is_active)
|
||||
.count()
|
||||
)
|
||||
ministry.is_member = True
|
||||
ministry.user_role = membership.role
|
||||
ministries.append(ministry)
|
||||
|
||||
return ministries
|
||||
|
||||
|
||||
@router.get("/{ministry_id}", response_model=MinistryResponse)
|
||||
def get_ministry(
|
||||
ministry_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get specific ministry"""
|
||||
ministry = db.query(Ministry).filter(Ministry.id == ministry_id).first()
|
||||
if not ministry:
|
||||
raise HTTPException(status_code=404, detail="Ministry not found")
|
||||
|
||||
ministry.leader_name = f"{ministry.leader.first_name} {ministry.leader.last_name}"
|
||||
ministry.member_count = (
|
||||
db.query(MinistryMember)
|
||||
.filter(MinistryMember.ministry_id == ministry.id, MinistryMember.is_active)
|
||||
.count()
|
||||
)
|
||||
|
||||
member = (
|
||||
db.query(MinistryMember)
|
||||
.filter(
|
||||
MinistryMember.ministry_id == ministry.id,
|
||||
MinistryMember.user_id == current_user.id,
|
||||
MinistryMember.is_active,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
ministry.is_member = member is not None
|
||||
ministry.user_role = member.role if member else None
|
||||
|
||||
return ministry
|
||||
|
||||
|
||||
@router.put("/{ministry_id}", response_model=MinistryResponse)
|
||||
def update_ministry(
|
||||
ministry_id: int,
|
||||
ministry_update: MinistryUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update ministry (leaders only)"""
|
||||
ministry = db.query(Ministry).filter(Ministry.id == ministry_id).first()
|
||||
if not ministry:
|
||||
raise HTTPException(status_code=404, detail="Ministry not found")
|
||||
|
||||
# Check if user is leader or co-leader
|
||||
member = (
|
||||
db.query(MinistryMember)
|
||||
.filter(
|
||||
MinistryMember.ministry_id == ministry_id,
|
||||
MinistryMember.user_id == current_user.id,
|
||||
MinistryMember.is_active,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not member or member.role not in [MinistryRole.LEADER, MinistryRole.CO_LEADER]:
|
||||
raise HTTPException(status_code=403, detail="Only leaders can update ministry")
|
||||
|
||||
for field, value in ministry_update.dict(exclude_unset=True).items():
|
||||
setattr(ministry, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(ministry)
|
||||
return ministry
|
||||
|
||||
|
||||
@router.post("/{ministry_id}/join", response_model=MinistryRequestResponse)
|
||||
def request_to_join_ministry(
|
||||
ministry_id: int,
|
||||
join_request: MinistryJoinRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Request to join a ministry"""
|
||||
ministry = db.query(Ministry).filter(Ministry.id == ministry_id).first()
|
||||
if not ministry:
|
||||
raise HTTPException(status_code=404, detail="Ministry not found")
|
||||
|
||||
# Check if already a member
|
||||
existing_member = (
|
||||
db.query(MinistryMember)
|
||||
.filter(
|
||||
MinistryMember.ministry_id == ministry_id,
|
||||
MinistryMember.user_id == current_user.id,
|
||||
MinistryMember.is_active,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing_member:
|
||||
raise HTTPException(status_code=400, detail="Already a member of this ministry")
|
||||
|
||||
# Check if already has pending request
|
||||
existing_request = (
|
||||
db.query(MinistryRequest)
|
||||
.filter(
|
||||
MinistryRequest.ministry_id == ministry_id,
|
||||
MinistryRequest.user_id == current_user.id,
|
||||
MinistryRequest.status == "pending",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing_request:
|
||||
raise HTTPException(status_code=400, detail="Already have a pending request")
|
||||
|
||||
request = MinistryRequest(
|
||||
ministry_id=ministry_id, user_id=current_user.id, message=join_request.message
|
||||
)
|
||||
|
||||
db.add(request)
|
||||
db.commit()
|
||||
db.refresh(request)
|
||||
|
||||
request.user_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
request.ministry_name = ministry.name
|
||||
|
||||
return request
|
||||
|
||||
|
||||
@router.get("/{ministry_id}/members", response_model=List[MinistryMemberResponse])
|
||||
def get_ministry_members(
|
||||
ministry_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get ministry members"""
|
||||
ministry = db.query(Ministry).filter(Ministry.id == ministry_id).first()
|
||||
if not ministry:
|
||||
raise HTTPException(status_code=404, detail="Ministry not found")
|
||||
|
||||
members = (
|
||||
db.query(MinistryMember)
|
||||
.filter(MinistryMember.ministry_id == ministry_id, MinistryMember.is_active)
|
||||
.all()
|
||||
)
|
||||
|
||||
for member in members:
|
||||
member.user_name = f"{member.user.first_name} {member.user.last_name}"
|
||||
member.ministry_name = ministry.name
|
||||
|
||||
return members
|
||||
|
||||
|
||||
@router.get("/{ministry_id}/activities", response_model=List[MinistryActivityResponse])
|
||||
def get_ministry_activities(
|
||||
ministry_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get ministry activities"""
|
||||
activities = (
|
||||
db.query(MinistryActivity)
|
||||
.filter(MinistryActivity.ministry_id == ministry_id)
|
||||
.order_by(MinistryActivity.activity_date.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
for activity in activities:
|
||||
activity.creator_name = (
|
||||
f"{activity.creator.first_name} {activity.creator.last_name}"
|
||||
)
|
||||
|
||||
return activities
|
||||
|
||||
|
||||
@router.post("/{ministry_id}/activities", response_model=MinistryActivityResponse)
|
||||
def create_ministry_activity(
|
||||
ministry_id: int,
|
||||
activity: MinistryActivityCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create ministry activity (members only)"""
|
||||
# Check if user is a member
|
||||
member = (
|
||||
db.query(MinistryMember)
|
||||
.filter(
|
||||
MinistryMember.ministry_id == ministry_id,
|
||||
MinistryMember.user_id == current_user.id,
|
||||
MinistryMember.is_active,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not member:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Only ministry members can create activities"
|
||||
)
|
||||
|
||||
db_activity = MinistryActivity(
|
||||
**activity.dict(), ministry_id=ministry_id, created_by=current_user.id
|
||||
)
|
||||
|
||||
db.add(db_activity)
|
||||
db.commit()
|
||||
db.refresh(db_activity)
|
||||
|
||||
db_activity.creator_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
|
||||
return db_activity
|
147
app/api/notifications.py
Normal file
147
app/api/notifications.py
Normal file
@ -0,0 +1,147 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.db.base import get_db
|
||||
from app.models.user import User
|
||||
from app.models.notification import Notification, NotificationSettings
|
||||
from app.schemas.notification import (
|
||||
NotificationResponse,
|
||||
NotificationSettingsResponse,
|
||||
NotificationSettingsUpdate,
|
||||
)
|
||||
from app.api.auth import get_current_user
|
||||
from app.services.notification_service import NotificationService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[NotificationResponse])
|
||||
def get_notifications(
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
unread_only: bool = False,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get user's notifications"""
|
||||
query = db.query(Notification).filter(Notification.recipient_id == current_user.id)
|
||||
|
||||
if unread_only:
|
||||
query = query.filter(Notification.is_read is False)
|
||||
|
||||
notifications = (
|
||||
query.order_by(Notification.created_at.desc()).offset(skip).limit(limit).all()
|
||||
)
|
||||
|
||||
# Add sender names
|
||||
for notification in notifications:
|
||||
if notification.sender:
|
||||
notification.sender_name = (
|
||||
f"{notification.sender.first_name} {notification.sender.last_name}"
|
||||
)
|
||||
|
||||
return notifications
|
||||
|
||||
|
||||
@router.get("/unread-count")
|
||||
def get_unread_count(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get count of unread notifications"""
|
||||
count = NotificationService.get_unread_count(db, current_user.id)
|
||||
return {"unread_count": count}
|
||||
|
||||
|
||||
@router.put("/{notification_id}/read")
|
||||
def mark_notification_read(
|
||||
notification_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Mark a notification as read"""
|
||||
success = NotificationService.mark_as_read(db, notification_id, current_user.id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Notification not found")
|
||||
return {"message": "Notification marked as read"}
|
||||
|
||||
|
||||
@router.put("/mark-all-read")
|
||||
def mark_all_notifications_read(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Mark all notifications as read"""
|
||||
count = NotificationService.mark_all_as_read(db, current_user.id)
|
||||
return {"message": f"Marked {count} notifications as read"}
|
||||
|
||||
|
||||
@router.delete("/{notification_id}")
|
||||
def delete_notification(
|
||||
notification_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Delete a notification"""
|
||||
notification = (
|
||||
db.query(Notification)
|
||||
.filter(
|
||||
Notification.id == notification_id,
|
||||
Notification.recipient_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not notification:
|
||||
raise HTTPException(status_code=404, detail="Notification not found")
|
||||
|
||||
db.delete(notification)
|
||||
db.commit()
|
||||
return {"message": "Notification deleted"}
|
||||
|
||||
|
||||
@router.get("/settings", response_model=NotificationSettingsResponse)
|
||||
def get_notification_settings(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get user's notification settings"""
|
||||
settings = (
|
||||
db.query(NotificationSettings)
|
||||
.filter(NotificationSettings.user_id == current_user.id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not settings:
|
||||
# Create default settings
|
||||
settings = NotificationSettings(user_id=current_user.id)
|
||||
db.add(settings)
|
||||
db.commit()
|
||||
db.refresh(settings)
|
||||
|
||||
return settings
|
||||
|
||||
|
||||
@router.put("/settings", response_model=NotificationSettingsResponse)
|
||||
def update_notification_settings(
|
||||
settings_update: NotificationSettingsUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update user's notification settings"""
|
||||
settings = (
|
||||
db.query(NotificationSettings)
|
||||
.filter(NotificationSettings.user_id == current_user.id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not settings:
|
||||
settings = NotificationSettings(user_id=current_user.id)
|
||||
db.add(settings)
|
||||
db.commit()
|
||||
db.refresh(settings)
|
||||
|
||||
# Update settings
|
||||
for field, value in settings_update.dict(exclude_unset=True).items():
|
||||
setattr(settings, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(settings)
|
||||
return settings
|
427
app/api/prayers.py
Normal file
427
app/api/prayers.py
Normal file
@ -0,0 +1,427 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_, and_
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from app.db.base import get_db
|
||||
from app.models.user import User, Connection
|
||||
from app.models.ministry import MinistryMember
|
||||
from app.models.prayer import (
|
||||
PrayerRequest,
|
||||
Prayer,
|
||||
PrayerPrivacyLevel,
|
||||
PrayerStatus,
|
||||
PrayerCategory,
|
||||
)
|
||||
from app.schemas.prayer import (
|
||||
PrayerRequestCreate,
|
||||
PrayerRequestUpdate,
|
||||
PrayerRequestResponse,
|
||||
PrayerCreate,
|
||||
PrayerResponse,
|
||||
PrayerStats,
|
||||
)
|
||||
from app.api.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def can_view_prayer_request(
|
||||
prayer_request: PrayerRequest, current_user: User, db: Session
|
||||
) -> bool:
|
||||
"""Check if user can view a prayer request based on privacy level"""
|
||||
|
||||
if prayer_request.requester_id == current_user.id:
|
||||
return True
|
||||
|
||||
if prayer_request.privacy_level == PrayerPrivacyLevel.PUBLIC:
|
||||
return True
|
||||
|
||||
if prayer_request.privacy_level == PrayerPrivacyLevel.PRIVATE:
|
||||
return False
|
||||
|
||||
if prayer_request.privacy_level == PrayerPrivacyLevel.CONNECTIONS_ONLY:
|
||||
# Check if users are connected
|
||||
connection = (
|
||||
db.query(Connection)
|
||||
.filter(
|
||||
or_(
|
||||
and_(
|
||||
Connection.sender_id == current_user.id,
|
||||
Connection.receiver_id == prayer_request.requester_id,
|
||||
),
|
||||
and_(
|
||||
Connection.sender_id == prayer_request.requester_id,
|
||||
Connection.receiver_id == current_user.id,
|
||||
),
|
||||
),
|
||||
Connection.status == "accepted",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
return connection is not None
|
||||
|
||||
if (
|
||||
prayer_request.privacy_level == PrayerPrivacyLevel.MINISTRY_ONLY
|
||||
and prayer_request.ministry_id
|
||||
):
|
||||
# Check if user is in the same ministry
|
||||
membership = (
|
||||
db.query(MinistryMember)
|
||||
.filter(
|
||||
MinistryMember.ministry_id == prayer_request.ministry_id,
|
||||
MinistryMember.user_id == current_user.id,
|
||||
MinistryMember.is_active,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
return membership is not None
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@router.post("/", response_model=PrayerRequestResponse)
|
||||
def create_prayer_request(
|
||||
prayer_request: PrayerRequestCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new prayer request"""
|
||||
|
||||
# Validate ministry access if ministry_only privacy
|
||||
if (
|
||||
prayer_request.privacy_level == PrayerPrivacyLevel.MINISTRY_ONLY
|
||||
and prayer_request.ministry_id
|
||||
):
|
||||
membership = (
|
||||
db.query(MinistryMember)
|
||||
.filter(
|
||||
MinistryMember.ministry_id == prayer_request.ministry_id,
|
||||
MinistryMember.user_id == current_user.id,
|
||||
MinistryMember.is_active,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not membership:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Must be a ministry member to create ministry-only requests",
|
||||
)
|
||||
|
||||
db_request = PrayerRequest(**prayer_request.dict(), requester_id=current_user.id)
|
||||
db.add(db_request)
|
||||
db.commit()
|
||||
db.refresh(db_request)
|
||||
|
||||
# Add response metadata
|
||||
db_request.requester_name = (
|
||||
f"{current_user.first_name} {current_user.last_name}"
|
||||
if not db_request.is_anonymous
|
||||
else "Anonymous"
|
||||
)
|
||||
db_request.prayer_count = 0
|
||||
db_request.has_prayed = False
|
||||
|
||||
if db_request.ministry_id:
|
||||
db_request.ministry_name = db_request.ministry.name
|
||||
|
||||
return db_request
|
||||
|
||||
|
||||
@router.get("/", response_model=List[PrayerRequestResponse])
|
||||
def get_prayer_requests(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
category: Optional[PrayerCategory] = None,
|
||||
status: Optional[PrayerStatus] = None,
|
||||
privacy_level: Optional[PrayerPrivacyLevel] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get prayer requests based on user's access level"""
|
||||
|
||||
query = db.query(PrayerRequest)
|
||||
|
||||
# Apply filters
|
||||
if category:
|
||||
query = query.filter(PrayerRequest.category == category)
|
||||
if status:
|
||||
query = query.filter(PrayerRequest.status == status)
|
||||
if privacy_level:
|
||||
query = query.filter(PrayerRequest.privacy_level == privacy_level)
|
||||
|
||||
prayer_requests = (
|
||||
query.order_by(PrayerRequest.created_at.desc()).offset(skip).limit(limit).all()
|
||||
)
|
||||
|
||||
# Filter based on privacy and add metadata
|
||||
accessible_requests = []
|
||||
for request in prayer_requests:
|
||||
if can_view_prayer_request(request, current_user, db):
|
||||
# Add metadata
|
||||
request.requester_name = (
|
||||
f"{request.requester.first_name} {request.requester.last_name}"
|
||||
if not request.is_anonymous
|
||||
else "Anonymous"
|
||||
)
|
||||
|
||||
request.prayer_count = (
|
||||
db.query(Prayer).filter(Prayer.prayer_request_id == request.id).count()
|
||||
)
|
||||
|
||||
request.has_prayed = (
|
||||
db.query(Prayer)
|
||||
.filter(
|
||||
Prayer.prayer_request_id == request.id,
|
||||
Prayer.prayer_warrior_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
|
||||
if request.ministry_id:
|
||||
request.ministry_name = request.ministry.name
|
||||
|
||||
accessible_requests.append(request)
|
||||
|
||||
return accessible_requests
|
||||
|
||||
|
||||
@router.get("/my-requests", response_model=List[PrayerRequestResponse])
|
||||
def get_my_prayer_requests(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get current user's prayer requests"""
|
||||
|
||||
requests = (
|
||||
db.query(PrayerRequest)
|
||||
.filter(PrayerRequest.requester_id == current_user.id)
|
||||
.order_by(PrayerRequest.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
for request in requests:
|
||||
request.requester_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
request.prayer_count = (
|
||||
db.query(Prayer).filter(Prayer.prayer_request_id == request.id).count()
|
||||
)
|
||||
request.has_prayed = False # Can't pray for own request
|
||||
|
||||
if request.ministry_id:
|
||||
request.ministry_name = request.ministry.name
|
||||
|
||||
return requests
|
||||
|
||||
|
||||
@router.get("/{request_id}", response_model=PrayerRequestResponse)
|
||||
def get_prayer_request(
|
||||
request_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get specific prayer request"""
|
||||
|
||||
prayer_request = (
|
||||
db.query(PrayerRequest).filter(PrayerRequest.id == request_id).first()
|
||||
)
|
||||
|
||||
if not prayer_request:
|
||||
raise HTTPException(status_code=404, detail="Prayer request not found")
|
||||
|
||||
if not can_view_prayer_request(prayer_request, current_user, db):
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied to this prayer request"
|
||||
)
|
||||
|
||||
# Add metadata
|
||||
prayer_request.requester_name = (
|
||||
f"{prayer_request.requester.first_name} {prayer_request.requester.last_name}"
|
||||
if not prayer_request.is_anonymous
|
||||
else "Anonymous"
|
||||
)
|
||||
|
||||
prayer_request.prayer_count = (
|
||||
db.query(Prayer).filter(Prayer.prayer_request_id == prayer_request.id).count()
|
||||
)
|
||||
|
||||
prayer_request.has_prayed = (
|
||||
db.query(Prayer)
|
||||
.filter(
|
||||
Prayer.prayer_request_id == prayer_request.id,
|
||||
Prayer.prayer_warrior_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
|
||||
if prayer_request.ministry_id:
|
||||
prayer_request.ministry_name = prayer_request.ministry.name
|
||||
|
||||
return prayer_request
|
||||
|
||||
|
||||
@router.put("/{request_id}", response_model=PrayerRequestResponse)
|
||||
def update_prayer_request(
|
||||
request_id: int,
|
||||
prayer_update: PrayerRequestUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update prayer request (owner only)"""
|
||||
|
||||
prayer_request = (
|
||||
db.query(PrayerRequest).filter(PrayerRequest.id == request_id).first()
|
||||
)
|
||||
|
||||
if not prayer_request:
|
||||
raise HTTPException(status_code=404, detail="Prayer request not found")
|
||||
|
||||
if prayer_request.requester_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Can only update own prayer requests"
|
||||
)
|
||||
|
||||
# Handle status changes
|
||||
update_data = prayer_update.dict(exclude_unset=True)
|
||||
if "status" in update_data and update_data["status"] == PrayerStatus.ANSWERED:
|
||||
if not update_data.get("answer_description"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Answer description required when marking as answered",
|
||||
)
|
||||
update_data["answered_at"] = datetime.utcnow()
|
||||
|
||||
# Update fields
|
||||
for field, value in update_data.items():
|
||||
setattr(prayer_request, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(prayer_request)
|
||||
|
||||
return prayer_request
|
||||
|
||||
|
||||
@router.post("/{request_id}/pray", response_model=PrayerResponse)
|
||||
def pray_for_request(
|
||||
request_id: int,
|
||||
prayer: PrayerCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Pray for a prayer request"""
|
||||
|
||||
prayer_request = (
|
||||
db.query(PrayerRequest).filter(PrayerRequest.id == request_id).first()
|
||||
)
|
||||
|
||||
if not prayer_request:
|
||||
raise HTTPException(status_code=404, detail="Prayer request not found")
|
||||
|
||||
if not can_view_prayer_request(prayer_request, current_user, db):
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied to this prayer request"
|
||||
)
|
||||
|
||||
if prayer_request.requester_id == current_user.id:
|
||||
raise HTTPException(status_code=400, detail="Cannot pray for your own request")
|
||||
|
||||
# Check if already prayed
|
||||
existing_prayer = (
|
||||
db.query(Prayer)
|
||||
.filter(
|
||||
Prayer.prayer_request_id == request_id,
|
||||
Prayer.prayer_warrior_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing_prayer:
|
||||
raise HTTPException(status_code=400, detail="Already prayed for this request")
|
||||
|
||||
db_prayer = Prayer(
|
||||
prayer_request_id=request_id,
|
||||
prayer_warrior_id=current_user.id,
|
||||
message=prayer.message,
|
||||
)
|
||||
|
||||
db.add(db_prayer)
|
||||
db.commit()
|
||||
db.refresh(db_prayer)
|
||||
|
||||
db_prayer.prayer_warrior_name = (
|
||||
f"{current_user.first_name} {current_user.last_name}"
|
||||
)
|
||||
|
||||
return db_prayer
|
||||
|
||||
|
||||
@router.get("/{request_id}/prayers", response_model=List[PrayerResponse])
|
||||
def get_prayers_for_request(
|
||||
request_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get prayers for a specific request"""
|
||||
|
||||
prayer_request = (
|
||||
db.query(PrayerRequest).filter(PrayerRequest.id == request_id).first()
|
||||
)
|
||||
|
||||
if not prayer_request:
|
||||
raise HTTPException(status_code=404, detail="Prayer request not found")
|
||||
|
||||
if not can_view_prayer_request(prayer_request, current_user, db):
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied to this prayer request"
|
||||
)
|
||||
|
||||
prayers = (
|
||||
db.query(Prayer)
|
||||
.filter(Prayer.prayer_request_id == request_id)
|
||||
.order_by(Prayer.prayed_at.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
for prayer in prayers:
|
||||
prayer.prayer_warrior_name = (
|
||||
f"{prayer.prayer_warrior.first_name} {prayer.prayer_warrior.last_name}"
|
||||
)
|
||||
|
||||
return prayers
|
||||
|
||||
|
||||
@router.get("/stats/personal", response_model=PrayerStats)
|
||||
def get_personal_prayer_stats(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get personal prayer statistics"""
|
||||
|
||||
total_requests = db.query(PrayerRequest).count()
|
||||
active_requests = (
|
||||
db.query(PrayerRequest)
|
||||
.filter(PrayerRequest.status == PrayerStatus.ACTIVE)
|
||||
.count()
|
||||
)
|
||||
answered_requests = (
|
||||
db.query(PrayerRequest)
|
||||
.filter(PrayerRequest.status == PrayerStatus.ANSWERED)
|
||||
.count()
|
||||
)
|
||||
|
||||
prayers_offered = (
|
||||
db.query(Prayer).filter(Prayer.prayer_warrior_id == current_user.id).count()
|
||||
)
|
||||
requests_created = (
|
||||
db.query(PrayerRequest)
|
||||
.filter(PrayerRequest.requester_id == current_user.id)
|
||||
.count()
|
||||
)
|
||||
|
||||
return PrayerStats(
|
||||
total_requests=total_requests,
|
||||
active_requests=active_requests,
|
||||
answered_requests=answered_requests,
|
||||
prayers_offered=prayers_offered,
|
||||
requests_created=requests_created,
|
||||
)
|
68
app/api/uploads.py
Normal file
68
app/api/uploads.py
Normal file
@ -0,0 +1,68 @@
|
||||
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from pathlib import Path
|
||||
from app.models.user import User
|
||||
from app.api.auth import get_current_user
|
||||
from app.core.upload import save_upload_file, delete_file
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/profile-picture")
|
||||
async def upload_profile_picture(
|
||||
file: UploadFile = File(...), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Upload a profile picture"""
|
||||
try:
|
||||
file_path = await save_upload_file(file, "profile_pictures")
|
||||
|
||||
# Delete old profile picture if exists
|
||||
if current_user.profile_picture:
|
||||
delete_file(current_user.profile_picture)
|
||||
|
||||
# Update user's profile picture in database would happen here
|
||||
# For now, just return the path
|
||||
return {
|
||||
"file_path": file_path,
|
||||
"message": "Profile picture uploaded successfully",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/event-image")
|
||||
async def upload_event_image(
|
||||
file: UploadFile = File(...), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Upload an event image"""
|
||||
try:
|
||||
file_path = await save_upload_file(file, "event_images")
|
||||
return {"file_path": file_path, "message": "Event image uploaded successfully"}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/post-image")
|
||||
async def upload_post_image(
|
||||
file: UploadFile = File(...), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Upload a post image"""
|
||||
try:
|
||||
file_path = await save_upload_file(file, "post_images")
|
||||
return {"file_path": file_path, "message": "Post image uploaded successfully"}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/serve/{folder}/{filename}")
|
||||
async def serve_file(folder: str, filename: str):
|
||||
"""Serve uploaded files"""
|
||||
file_path = Path(f"/app/storage/uploads/{folder}/{filename}")
|
||||
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
return FileResponse(file_path)
|
71
app/core/upload.py
Normal file
71
app/core/upload.py
Normal file
@ -0,0 +1,71 @@
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from fastapi import UploadFile, HTTPException
|
||||
from PIL import Image
|
||||
import aiofiles
|
||||
|
||||
UPLOAD_DIR = Path("/app/storage/uploads")
|
||||
ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
|
||||
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
|
||||
|
||||
|
||||
async def save_upload_file(upload_file: UploadFile, subfolder: str = "general") -> str:
|
||||
"""Save uploaded file and return the file path"""
|
||||
|
||||
if upload_file.size > MAX_FILE_SIZE:
|
||||
raise HTTPException(status_code=413, detail="File too large")
|
||||
|
||||
if upload_file.content_type not in ALLOWED_IMAGE_TYPES:
|
||||
raise HTTPException(status_code=400, detail="Invalid file type")
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
upload_path = UPLOAD_DIR / subfolder
|
||||
upload_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Generate unique filename
|
||||
file_extension = upload_file.filename.split(".")[-1]
|
||||
unique_filename = f"{uuid.uuid4()}.{file_extension}"
|
||||
file_path = upload_path / unique_filename
|
||||
|
||||
# Save file
|
||||
async with aiofiles.open(file_path, "wb") as f:
|
||||
content = await upload_file.read()
|
||||
await f.write(content)
|
||||
|
||||
# Optimize image if it's an image
|
||||
if upload_file.content_type.startswith("image/"):
|
||||
optimize_image(str(file_path))
|
||||
|
||||
return f"/uploads/{subfolder}/{unique_filename}"
|
||||
|
||||
|
||||
def optimize_image(
|
||||
file_path: str, max_width: int = 1024, max_height: int = 1024, quality: int = 85
|
||||
):
|
||||
"""Optimize image size and quality"""
|
||||
try:
|
||||
with Image.open(file_path) as img:
|
||||
# Convert to RGB if needed
|
||||
if img.mode in ("RGBA", "P"):
|
||||
img = img.convert("RGB")
|
||||
|
||||
# Resize if too large
|
||||
if img.width > max_width or img.height > max_height:
|
||||
img.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# Save with optimization
|
||||
img.save(file_path, optimize=True, quality=quality)
|
||||
except Exception as e:
|
||||
print(f"Image optimization failed: {e}")
|
||||
|
||||
|
||||
def delete_file(file_path: str) -> bool:
|
||||
"""Delete a file from storage"""
|
||||
try:
|
||||
full_path = Path("/app/storage") / file_path.lstrip("/")
|
||||
if full_path.exists():
|
||||
full_path.unlink()
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
124
app/models/attendance.py
Normal file
124
app/models/attendance.py
Normal file
@ -0,0 +1,124 @@
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
Integer,
|
||||
String,
|
||||
DateTime,
|
||||
Text,
|
||||
ForeignKey,
|
||||
Enum,
|
||||
Date,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
import enum
|
||||
|
||||
|
||||
class ServiceType(enum.Enum):
|
||||
SUNDAY_SERVICE = "sunday_service"
|
||||
BIBLE_STUDY = "bible_study"
|
||||
PRAYER_MEETING = "prayer_meeting"
|
||||
YOUTH_SERVICE = "youth_service"
|
||||
WOMEN_MEETING = "women_meeting"
|
||||
MEN_MEETING = "men_meeting"
|
||||
SPECIAL_SERVICE = "special_service"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class AttendanceStatus(enum.Enum):
|
||||
PRESENT = "present"
|
||||
ABSENT = "absent"
|
||||
LATE = "late"
|
||||
EXCUSED = "excused"
|
||||
|
||||
|
||||
class Service(Base):
|
||||
__tablename__ = "services"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
service_type = Column(Enum(ServiceType), nullable=False)
|
||||
service_date = Column(Date, nullable=False)
|
||||
start_time = Column(DateTime, nullable=False)
|
||||
end_time = Column(DateTime, nullable=True)
|
||||
location = Column(String, nullable=True)
|
||||
minister_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
minister = relationship("User", foreign_keys=[minister_id])
|
||||
creator = relationship("User", foreign_keys=[created_by])
|
||||
attendance_records = relationship(
|
||||
"AttendanceRecord", back_populates="service", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class AttendanceRecord(Base):
|
||||
__tablename__ = "attendance_records"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
service_id = Column(Integer, ForeignKey("services.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
status = Column(Enum(AttendanceStatus), default=AttendanceStatus.PRESENT)
|
||||
check_in_time = Column(DateTime, nullable=True)
|
||||
check_out_time = Column(DateTime, nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
recorded_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
recorded_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
service = relationship("Service", back_populates="attendance_records")
|
||||
user = relationship("User", foreign_keys=[user_id])
|
||||
recorder = relationship("User", foreign_keys=[recorded_by])
|
||||
|
||||
|
||||
class AttendanceGoal(Base):
|
||||
__tablename__ = "attendance_goals"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
service_type = Column(Enum(ServiceType), nullable=False)
|
||||
target_percentage = Column(Integer, default=80) # Target attendance percentage
|
||||
start_date = Column(Date, nullable=False)
|
||||
end_date = Column(Date, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
class FamilyGroup(Base):
|
||||
__tablename__ = "family_groups"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
head_of_family_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
head_of_family = relationship("User")
|
||||
members = relationship(
|
||||
"FamilyMember", back_populates="family_group", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class FamilyMember(Base):
|
||||
__tablename__ = "family_members"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
family_group_id = Column(Integer, ForeignKey("family_groups.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
name = Column(String, nullable=False) # For non-registered family members
|
||||
relationship = Column(String, nullable=True) # spouse, child, parent, etc.
|
||||
date_of_birth = Column(Date, nullable=True)
|
||||
is_registered_user = Column(Boolean, default=False)
|
||||
|
||||
# Relationships
|
||||
family_group = relationship("FamilyGroup", back_populates="members")
|
||||
user = relationship("User")
|
106
app/models/donation.py
Normal file
106
app/models/donation.py
Normal file
@ -0,0 +1,106 @@
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
Integer,
|
||||
String,
|
||||
DateTime,
|
||||
Text,
|
||||
ForeignKey,
|
||||
Enum,
|
||||
Numeric,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
import enum
|
||||
|
||||
|
||||
class DonationType(enum.Enum):
|
||||
TITHE = "tithe"
|
||||
OFFERING = "offering"
|
||||
SPECIAL_OFFERING = "special_offering"
|
||||
BUILDING_FUND = "building_fund"
|
||||
MISSIONS = "missions"
|
||||
CHARITY = "charity"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class PaymentMethod(enum.Enum):
|
||||
CASH = "cash"
|
||||
CHECK = "check"
|
||||
CARD = "card"
|
||||
BANK_TRANSFER = "bank_transfer"
|
||||
MOBILE_MONEY = "mobile_money"
|
||||
ONLINE = "online"
|
||||
|
||||
|
||||
class Donation(Base):
|
||||
__tablename__ = "donations"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
donor_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
amount = Column(Numeric(10, 2), nullable=False)
|
||||
currency = Column(String, default="USD")
|
||||
donation_type = Column(Enum(DonationType), nullable=False)
|
||||
payment_method = Column(Enum(PaymentMethod), nullable=False)
|
||||
reference_number = Column(String, nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
is_anonymous = Column(Boolean, default=False)
|
||||
is_recurring = Column(Boolean, default=False)
|
||||
recurring_frequency = Column(String, nullable=True) # weekly, monthly, yearly
|
||||
next_due_date = Column(DateTime, nullable=True)
|
||||
donation_date = Column(DateTime, nullable=False, default=func.now())
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
donor = relationship("User", back_populates="donations")
|
||||
|
||||
|
||||
class DonationGoal(Base):
|
||||
__tablename__ = "donation_goals"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
target_amount = Column(Numeric(10, 2), nullable=False)
|
||||
current_amount = Column(Numeric(10, 2), default=0)
|
||||
currency = Column(String, default="USD")
|
||||
start_date = Column(DateTime, nullable=False)
|
||||
end_date = Column(DateTime, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
creator = relationship("User")
|
||||
donations = relationship("GoalDonation", back_populates="goal")
|
||||
|
||||
|
||||
class GoalDonation(Base):
|
||||
__tablename__ = "goal_donations"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
goal_id = Column(Integer, ForeignKey("donation_goals.id"), nullable=False)
|
||||
donation_id = Column(Integer, ForeignKey("donations.id"), nullable=False)
|
||||
amount = Column(Numeric(10, 2), nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
goal = relationship("DonationGoal", back_populates="donations")
|
||||
donation = relationship("Donation")
|
||||
|
||||
|
||||
class TithePledge(Base):
|
||||
__tablename__ = "tithe_pledges"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
monthly_amount = Column(Numeric(10, 2), nullable=False)
|
||||
currency = Column(String, default="USD")
|
||||
start_date = Column(DateTime, nullable=False)
|
||||
end_date = Column(DateTime, nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
user = relationship("User")
|
109
app/models/message.py
Normal file
109
app/models/message.py
Normal file
@ -0,0 +1,109 @@
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
Integer,
|
||||
String,
|
||||
DateTime,
|
||||
Text,
|
||||
ForeignKey,
|
||||
Enum,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
import enum
|
||||
|
||||
|
||||
class MessageType(enum.Enum):
|
||||
TEXT = "text"
|
||||
IMAGE = "image"
|
||||
FILE = "file"
|
||||
|
||||
|
||||
class Conversation(Base):
|
||||
__tablename__ = "conversations"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
participant_1_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
participant_2_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
updated_at = Column(
|
||||
DateTime, nullable=False, default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
participant_1 = relationship("User", foreign_keys=[participant_1_id])
|
||||
participant_2 = relationship("User", foreign_keys=[participant_2_id])
|
||||
messages = relationship(
|
||||
"Message", back_populates="conversation", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class Message(Base):
|
||||
__tablename__ = "messages"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
conversation_id = Column(Integer, ForeignKey("conversations.id"), nullable=False)
|
||||
sender_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
receiver_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
message_type = Column(Enum(MessageType), default=MessageType.TEXT)
|
||||
content = Column(Text, nullable=False)
|
||||
file_url = Column(String, nullable=True)
|
||||
is_read = Column(Boolean, default=False)
|
||||
read_at = Column(DateTime, nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
conversation = relationship("Conversation", back_populates="messages")
|
||||
sender = relationship("User", foreign_keys=[sender_id])
|
||||
receiver = relationship("User", foreign_keys=[receiver_id])
|
||||
|
||||
|
||||
class GroupChat(Base):
|
||||
__tablename__ = "group_chats"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
creator = relationship("User")
|
||||
members = relationship(
|
||||
"GroupChatMember", back_populates="group_chat", cascade="all, delete-orphan"
|
||||
)
|
||||
messages = relationship(
|
||||
"GroupMessage", back_populates="group_chat", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class GroupChatMember(Base):
|
||||
__tablename__ = "group_chat_members"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
group_chat_id = Column(Integer, ForeignKey("group_chats.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
joined_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
group_chat = relationship("GroupChat", back_populates="members")
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
class GroupMessage(Base):
|
||||
__tablename__ = "group_messages"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
group_chat_id = Column(Integer, ForeignKey("group_chats.id"), nullable=False)
|
||||
sender_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
message_type = Column(Enum(MessageType), default=MessageType.TEXT)
|
||||
content = Column(Text, nullable=False)
|
||||
file_url = Column(String, nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
group_chat = relationship("GroupChat", back_populates="messages")
|
||||
sender = relationship("User")
|
99
app/models/ministry.py
Normal file
99
app/models/ministry.py
Normal file
@ -0,0 +1,99 @@
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
Integer,
|
||||
String,
|
||||
DateTime,
|
||||
Text,
|
||||
ForeignKey,
|
||||
Enum,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
import enum
|
||||
|
||||
|
||||
class MinistryRole(enum.Enum):
|
||||
LEADER = "leader"
|
||||
CO_LEADER = "co_leader"
|
||||
MEMBER = "member"
|
||||
VOLUNTEER = "volunteer"
|
||||
|
||||
|
||||
class Ministry(Base):
|
||||
__tablename__ = "ministries"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
vision = Column(Text, nullable=True)
|
||||
mission = Column(Text, nullable=True)
|
||||
meeting_day = Column(String, nullable=True)
|
||||
meeting_time = Column(String, nullable=True)
|
||||
meeting_location = Column(String, nullable=True)
|
||||
leader_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
updated_at = Column(
|
||||
DateTime, nullable=False, default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
leader = relationship("User")
|
||||
members = relationship(
|
||||
"MinistryMember", back_populates="ministry", cascade="all, delete-orphan"
|
||||
)
|
||||
activities = relationship(
|
||||
"MinistryActivity", back_populates="ministry", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class MinistryMember(Base):
|
||||
__tablename__ = "ministry_members"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
ministry_id = Column(Integer, ForeignKey("ministries.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
role = Column(Enum(MinistryRole), default=MinistryRole.MEMBER)
|
||||
joined_at = Column(DateTime, nullable=False, default=func.now())
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Relationships
|
||||
ministry = relationship("Ministry", back_populates="members")
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
class MinistryActivity(Base):
|
||||
__tablename__ = "ministry_activities"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
ministry_id = Column(Integer, ForeignKey("ministries.id"), nullable=False)
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
activity_date = Column(DateTime, nullable=False)
|
||||
location = Column(String, nullable=True)
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
ministry = relationship("Ministry", back_populates="activities")
|
||||
creator = relationship("User")
|
||||
|
||||
|
||||
class MinistryRequest(Base):
|
||||
__tablename__ = "ministry_requests"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
ministry_id = Column(Integer, ForeignKey("ministries.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
message = Column(Text, nullable=True)
|
||||
status = Column(String, default="pending") # pending, approved, rejected
|
||||
requested_at = Column(DateTime, nullable=False, default=func.now())
|
||||
responded_at = Column(DateTime, nullable=True)
|
||||
responded_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# Relationships
|
||||
ministry = relationship("Ministry")
|
||||
user = relationship("User", foreign_keys=[user_id])
|
||||
responder = relationship("User", foreign_keys=[responded_by])
|
67
app/models/notification.py
Normal file
67
app/models/notification.py
Normal file
@ -0,0 +1,67 @@
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
Integer,
|
||||
String,
|
||||
DateTime,
|
||||
Text,
|
||||
ForeignKey,
|
||||
Enum,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
import enum
|
||||
|
||||
|
||||
class NotificationType(enum.Enum):
|
||||
CONNECTION_REQUEST = "connection_request"
|
||||
CONNECTION_ACCEPTED = "connection_accepted"
|
||||
POST_LIKE = "post_like"
|
||||
POST_COMMENT = "post_comment"
|
||||
EVENT_INVITATION = "event_invitation"
|
||||
EVENT_REMINDER = "event_reminder"
|
||||
PRAYER_REQUEST = "prayer_request"
|
||||
MINISTRY_ASSIGNMENT = "ministry_assignment"
|
||||
DONATION_REMINDER = "donation_reminder"
|
||||
BIRTHDAY_REMINDER = "birthday_reminder"
|
||||
ANNOUNCEMENT = "announcement"
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
__tablename__ = "notifications"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
recipient_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
sender_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
type = Column(Enum(NotificationType), nullable=False)
|
||||
title = Column(String, nullable=False)
|
||||
message = Column(Text, nullable=False)
|
||||
data = Column(Text, nullable=True) # JSON data for additional context
|
||||
is_read = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
read_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
recipient = relationship("User", foreign_keys=[recipient_id])
|
||||
sender = relationship("User", foreign_keys=[sender_id])
|
||||
|
||||
|
||||
class NotificationSettings(Base):
|
||||
__tablename__ = "notification_settings"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
email_notifications = Column(Boolean, default=True)
|
||||
push_notifications = Column(Boolean, default=True)
|
||||
connection_requests = Column(Boolean, default=True)
|
||||
post_interactions = Column(Boolean, default=True)
|
||||
event_reminders = Column(Boolean, default=True)
|
||||
prayer_requests = Column(Boolean, default=True)
|
||||
ministry_updates = Column(Boolean, default=True)
|
||||
donation_reminders = Column(Boolean, default=True)
|
||||
birthday_reminders = Column(Boolean, default=True)
|
||||
announcements = Column(Boolean, default=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="notification_settings")
|
112
app/models/prayer.py
Normal file
112
app/models/prayer.py
Normal file
@ -0,0 +1,112 @@
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
Integer,
|
||||
String,
|
||||
DateTime,
|
||||
Text,
|
||||
ForeignKey,
|
||||
Enum,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
import enum
|
||||
|
||||
|
||||
class PrayerPrivacyLevel(enum.Enum):
|
||||
PUBLIC = "public"
|
||||
CONNECTIONS_ONLY = "connections_only"
|
||||
MINISTRY_ONLY = "ministry_only"
|
||||
PRIVATE = "private"
|
||||
|
||||
|
||||
class PrayerStatus(enum.Enum):
|
||||
ACTIVE = "active"
|
||||
ANSWERED = "answered"
|
||||
CLOSED = "closed"
|
||||
|
||||
|
||||
class PrayerCategory(enum.Enum):
|
||||
HEALTH = "health"
|
||||
FAMILY = "family"
|
||||
FINANCES = "finances"
|
||||
RELATIONSHIPS = "relationships"
|
||||
WORK = "work"
|
||||
SPIRITUAL = "spiritual"
|
||||
CHURCH = "church"
|
||||
WORLD = "world"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class PrayerRequest(Base):
|
||||
__tablename__ = "prayer_requests"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
requester_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(Text, nullable=False)
|
||||
category = Column(Enum(PrayerCategory), default=PrayerCategory.OTHER)
|
||||
privacy_level = Column(Enum(PrayerPrivacyLevel), default=PrayerPrivacyLevel.PUBLIC)
|
||||
status = Column(Enum(PrayerStatus), default=PrayerStatus.ACTIVE)
|
||||
ministry_id = Column(Integer, ForeignKey("ministries.id"), nullable=True)
|
||||
is_anonymous = Column(Boolean, default=False)
|
||||
answer_description = Column(Text, nullable=True)
|
||||
answered_at = Column(DateTime, nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
updated_at = Column(
|
||||
DateTime, nullable=False, default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
requester = relationship("User", back_populates="prayer_requests")
|
||||
ministry = relationship("Ministry")
|
||||
prayers = relationship(
|
||||
"Prayer", back_populates="prayer_request", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class Prayer(Base):
|
||||
__tablename__ = "prayers"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
prayer_request_id = Column(
|
||||
Integer, ForeignKey("prayer_requests.id"), nullable=False
|
||||
)
|
||||
prayer_warrior_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
message = Column(Text, nullable=True)
|
||||
prayed_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
prayer_request = relationship("PrayerRequest", back_populates="prayers")
|
||||
prayer_warrior = relationship("User")
|
||||
|
||||
|
||||
class PrayerGroup(Base):
|
||||
__tablename__ = "prayer_groups"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
leader_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
leader = relationship("User")
|
||||
members = relationship(
|
||||
"PrayerGroupMember", back_populates="prayer_group", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class PrayerGroupMember(Base):
|
||||
__tablename__ = "prayer_group_members"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
prayer_group_id = Column(Integer, ForeignKey("prayer_groups.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
joined_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
prayer_group = relationship("PrayerGroup", back_populates="members")
|
||||
user = relationship("User")
|
@ -34,6 +34,13 @@ class User(Base):
|
||||
likes = relationship("Like", back_populates="user")
|
||||
event_registrations = relationship("EventRegistration", back_populates="user")
|
||||
|
||||
# New relationships for enhanced features
|
||||
donations = relationship("Donation", back_populates="donor")
|
||||
prayer_requests = relationship("PrayerRequest", back_populates="requester")
|
||||
notification_settings = relationship(
|
||||
"NotificationSettings", back_populates="user", uselist=False
|
||||
)
|
||||
|
||||
|
||||
class Connection(Base):
|
||||
__tablename__ = "connections"
|
||||
|
158
app/schemas/attendance.py
Normal file
158
app/schemas/attendance.py
Normal file
@ -0,0 +1,158 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime, date
|
||||
from typing import Optional
|
||||
from app.models.attendance import ServiceType, AttendanceStatus
|
||||
|
||||
|
||||
class ServiceBase(BaseModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
service_type: ServiceType
|
||||
service_date: date
|
||||
start_time: datetime
|
||||
end_time: Optional[datetime] = None
|
||||
location: Optional[str] = None
|
||||
minister_id: Optional[int] = None
|
||||
|
||||
|
||||
class ServiceCreate(ServiceBase):
|
||||
pass
|
||||
|
||||
|
||||
class ServiceUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
service_type: Optional[ServiceType] = None
|
||||
service_date: Optional[date] = None
|
||||
start_time: Optional[datetime] = None
|
||||
end_time: Optional[datetime] = None
|
||||
location: Optional[str] = None
|
||||
minister_id: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class ServiceResponse(ServiceBase):
|
||||
id: int
|
||||
is_active: bool
|
||||
created_by: int
|
||||
created_at: datetime
|
||||
creator_name: Optional[str] = None
|
||||
minister_name: Optional[str] = None
|
||||
attendance_count: int = 0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AttendanceRecordBase(BaseModel):
|
||||
user_id: int
|
||||
status: AttendanceStatus = AttendanceStatus.PRESENT
|
||||
check_in_time: Optional[datetime] = None
|
||||
check_out_time: Optional[datetime] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class AttendanceRecordCreate(AttendanceRecordBase):
|
||||
pass
|
||||
|
||||
|
||||
class AttendanceRecordUpdate(BaseModel):
|
||||
status: Optional[AttendanceStatus] = None
|
||||
check_in_time: Optional[datetime] = None
|
||||
check_out_time: Optional[datetime] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class AttendanceRecordResponse(AttendanceRecordBase):
|
||||
id: int
|
||||
service_id: int
|
||||
recorded_by: int
|
||||
recorded_at: datetime
|
||||
user_name: Optional[str] = None
|
||||
recorder_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AttendanceGoalBase(BaseModel):
|
||||
service_type: ServiceType
|
||||
target_percentage: int = 80
|
||||
start_date: date
|
||||
end_date: date
|
||||
|
||||
|
||||
class AttendanceGoalCreate(AttendanceGoalBase):
|
||||
pass
|
||||
|
||||
|
||||
class AttendanceGoalResponse(AttendanceGoalBase):
|
||||
id: int
|
||||
user_id: int
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
current_percentage: float = 0.0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class FamilyGroupBase(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class FamilyGroupCreate(FamilyGroupBase):
|
||||
pass
|
||||
|
||||
|
||||
class FamilyGroupResponse(FamilyGroupBase):
|
||||
id: int
|
||||
head_of_family_id: int
|
||||
created_at: datetime
|
||||
head_of_family_name: Optional[str] = None
|
||||
member_count: int = 0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class FamilyMemberBase(BaseModel):
|
||||
name: str
|
||||
relationship: Optional[str] = None
|
||||
date_of_birth: Optional[date] = None
|
||||
user_id: Optional[int] = None
|
||||
|
||||
|
||||
class FamilyMemberCreate(FamilyMemberBase):
|
||||
pass
|
||||
|
||||
|
||||
class FamilyMemberResponse(FamilyMemberBase):
|
||||
id: int
|
||||
family_group_id: int
|
||||
is_registered_user: bool
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AttendanceStats(BaseModel):
|
||||
total_services: int
|
||||
services_attended: int
|
||||
attendance_percentage: float
|
||||
most_attended_service_type: Optional[ServiceType] = None
|
||||
current_streak: int = 0
|
||||
longest_streak: int = 0
|
||||
|
||||
|
||||
class ServiceAttendanceSummary(BaseModel):
|
||||
service_id: int
|
||||
service_title: str
|
||||
service_date: date
|
||||
service_type: ServiceType
|
||||
total_attendees: int
|
||||
present_count: int
|
||||
absent_count: int
|
||||
late_count: int
|
||||
attendance_rate: float
|
137
app/schemas/donation.py
Normal file
137
app/schemas/donation.py
Normal file
@ -0,0 +1,137 @@
|
||||
from pydantic import BaseModel, validator
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
from app.models.donation import DonationType, PaymentMethod
|
||||
|
||||
|
||||
class DonationBase(BaseModel):
|
||||
amount: Decimal
|
||||
currency: str = "USD"
|
||||
donation_type: DonationType
|
||||
payment_method: PaymentMethod
|
||||
reference_number: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
is_anonymous: bool = False
|
||||
is_recurring: bool = False
|
||||
recurring_frequency: Optional[str] = None
|
||||
donation_date: Optional[datetime] = None
|
||||
|
||||
@validator("amount")
|
||||
def amount_must_be_positive(cls, v):
|
||||
if v <= 0:
|
||||
raise ValueError("Amount must be positive")
|
||||
return v
|
||||
|
||||
|
||||
class DonationCreate(DonationBase):
|
||||
pass
|
||||
|
||||
|
||||
class DonationResponse(DonationBase):
|
||||
id: int
|
||||
donor_id: int
|
||||
next_due_date: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
donor_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class DonationGoalBase(BaseModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
target_amount: Decimal
|
||||
currency: str = "USD"
|
||||
start_date: datetime
|
||||
end_date: datetime
|
||||
|
||||
@validator("target_amount")
|
||||
def target_amount_must_be_positive(cls, v):
|
||||
if v <= 0:
|
||||
raise ValueError("Target amount must be positive")
|
||||
return v
|
||||
|
||||
@validator("end_date")
|
||||
def end_date_must_be_after_start_date(cls, v, values):
|
||||
if "start_date" in values and v <= values["start_date"]:
|
||||
raise ValueError("End date must be after start date")
|
||||
return v
|
||||
|
||||
|
||||
class DonationGoalCreate(DonationGoalBase):
|
||||
pass
|
||||
|
||||
|
||||
class DonationGoalUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
target_amount: Optional[Decimal] = None
|
||||
end_date: Optional[datetime] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class DonationGoalResponse(DonationGoalBase):
|
||||
id: int
|
||||
current_amount: Decimal
|
||||
is_active: bool
|
||||
created_by: int
|
||||
created_at: datetime
|
||||
creator_name: Optional[str] = None
|
||||
progress_percentage: float = 0.0
|
||||
days_remaining: int = 0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TithePledgeBase(BaseModel):
|
||||
monthly_amount: Decimal
|
||||
currency: str = "USD"
|
||||
start_date: datetime
|
||||
end_date: Optional[datetime] = None
|
||||
|
||||
@validator("monthly_amount")
|
||||
def monthly_amount_must_be_positive(cls, v):
|
||||
if v <= 0:
|
||||
raise ValueError("Monthly amount must be positive")
|
||||
return v
|
||||
|
||||
|
||||
class TithePledgeCreate(TithePledgeBase):
|
||||
pass
|
||||
|
||||
|
||||
class TithePledgeUpdate(BaseModel):
|
||||
monthly_amount: Optional[Decimal] = None
|
||||
end_date: Optional[datetime] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class TithePledgeResponse(TithePledgeBase):
|
||||
id: int
|
||||
user_id: int
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class DonationStats(BaseModel):
|
||||
total_donations: Decimal
|
||||
total_tithes: Decimal
|
||||
total_offerings: Decimal
|
||||
donation_count: int
|
||||
average_donation: Decimal
|
||||
currency: str = "USD"
|
||||
|
||||
|
||||
class MonthlyDonationSummary(BaseModel):
|
||||
month: str
|
||||
year: int
|
||||
total_amount: Decimal
|
||||
donation_count: int
|
||||
tithe_amount: Decimal
|
||||
offering_amount: Decimal
|
98
app/schemas/message.py
Normal file
98
app/schemas/message.py
Normal file
@ -0,0 +1,98 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from app.models.message import MessageType
|
||||
|
||||
|
||||
class MessageBase(BaseModel):
|
||||
content: str
|
||||
message_type: MessageType = MessageType.TEXT
|
||||
file_url: Optional[str] = None
|
||||
|
||||
|
||||
class MessageCreate(MessageBase):
|
||||
receiver_id: int
|
||||
|
||||
|
||||
class MessageResponse(MessageBase):
|
||||
id: int
|
||||
conversation_id: int
|
||||
sender_id: int
|
||||
receiver_id: int
|
||||
is_read: bool
|
||||
read_at: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
sender_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ConversationResponse(BaseModel):
|
||||
id: int
|
||||
participant_1_id: int
|
||||
participant_2_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
other_participant_name: Optional[str] = None
|
||||
other_participant_id: Optional[int] = None
|
||||
last_message: Optional[str] = None
|
||||
unread_count: int = 0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class GroupChatBase(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class GroupChatCreate(GroupChatBase):
|
||||
member_ids: List[int] = []
|
||||
|
||||
|
||||
class GroupChatResponse(GroupChatBase):
|
||||
id: int
|
||||
created_by: int
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
creator_name: Optional[str] = None
|
||||
member_count: int = 0
|
||||
last_message: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class GroupChatMemberResponse(BaseModel):
|
||||
id: int
|
||||
group_chat_id: int
|
||||
user_id: int
|
||||
is_admin: bool
|
||||
joined_at: datetime
|
||||
user_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class GroupMessageBase(BaseModel):
|
||||
content: str
|
||||
message_type: MessageType = MessageType.TEXT
|
||||
file_url: Optional[str] = None
|
||||
|
||||
|
||||
class GroupMessageCreate(GroupMessageBase):
|
||||
pass
|
||||
|
||||
|
||||
class GroupMessageResponse(GroupMessageBase):
|
||||
id: int
|
||||
group_chat_id: int
|
||||
sender_id: int
|
||||
created_at: datetime
|
||||
sender_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
99
app/schemas/ministry.py
Normal file
99
app/schemas/ministry.py
Normal file
@ -0,0 +1,99 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from app.models.ministry import MinistryRole
|
||||
|
||||
|
||||
class MinistryBase(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
vision: Optional[str] = None
|
||||
mission: Optional[str] = None
|
||||
meeting_day: Optional[str] = None
|
||||
meeting_time: Optional[str] = None
|
||||
meeting_location: Optional[str] = None
|
||||
|
||||
|
||||
class MinistryCreate(MinistryBase):
|
||||
pass
|
||||
|
||||
|
||||
class MinistryUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
vision: Optional[str] = None
|
||||
mission: Optional[str] = None
|
||||
meeting_day: Optional[str] = None
|
||||
meeting_time: Optional[str] = None
|
||||
meeting_location: Optional[str] = None
|
||||
|
||||
|
||||
class MinistryResponse(MinistryBase):
|
||||
id: int
|
||||
leader_id: int
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
leader_name: Optional[str] = None
|
||||
member_count: int = 0
|
||||
is_member: bool = False
|
||||
user_role: Optional[MinistryRole] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class MinistryMemberResponse(BaseModel):
|
||||
id: int
|
||||
ministry_id: int
|
||||
user_id: int
|
||||
role: MinistryRole
|
||||
joined_at: datetime
|
||||
is_active: bool
|
||||
user_name: Optional[str] = None
|
||||
ministry_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class MinistryActivityBase(BaseModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
activity_date: datetime
|
||||
location: Optional[str] = None
|
||||
|
||||
|
||||
class MinistryActivityCreate(MinistryActivityBase):
|
||||
pass
|
||||
|
||||
|
||||
class MinistryActivityResponse(MinistryActivityBase):
|
||||
id: int
|
||||
ministry_id: int
|
||||
created_by: int
|
||||
created_at: datetime
|
||||
creator_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class MinistryJoinRequest(BaseModel):
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
class MinistryRequestResponse(BaseModel):
|
||||
id: int
|
||||
ministry_id: int
|
||||
user_id: int
|
||||
message: Optional[str] = None
|
||||
status: str
|
||||
requested_at: datetime
|
||||
responded_at: Optional[datetime] = None
|
||||
responded_by: Optional[int] = None
|
||||
user_name: Optional[str] = None
|
||||
ministry_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
52
app/schemas/notification.py
Normal file
52
app/schemas/notification.py
Normal file
@ -0,0 +1,52 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from app.models.notification import NotificationType
|
||||
|
||||
|
||||
class NotificationResponse(BaseModel):
|
||||
id: int
|
||||
recipient_id: int
|
||||
sender_id: Optional[int] = None
|
||||
type: NotificationType
|
||||
title: str
|
||||
message: str
|
||||
data: Optional[str] = None
|
||||
is_read: bool
|
||||
created_at: datetime
|
||||
read_at: Optional[datetime] = None
|
||||
sender_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class NotificationSettingsResponse(BaseModel):
|
||||
id: int
|
||||
user_id: int
|
||||
email_notifications: bool
|
||||
push_notifications: bool
|
||||
connection_requests: bool
|
||||
post_interactions: bool
|
||||
event_reminders: bool
|
||||
prayer_requests: bool
|
||||
ministry_updates: bool
|
||||
donation_reminders: bool
|
||||
birthday_reminders: bool
|
||||
announcements: bool
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class NotificationSettingsUpdate(BaseModel):
|
||||
email_notifications: Optional[bool] = None
|
||||
push_notifications: Optional[bool] = None
|
||||
connection_requests: Optional[bool] = None
|
||||
post_interactions: Optional[bool] = None
|
||||
event_reminders: Optional[bool] = None
|
||||
prayer_requests: Optional[bool] = None
|
||||
ministry_updates: Optional[bool] = None
|
||||
donation_reminders: Optional[bool] = None
|
||||
birthday_reminders: Optional[bool] = None
|
||||
announcements: Optional[bool] = None
|
103
app/schemas/prayer.py
Normal file
103
app/schemas/prayer.py
Normal file
@ -0,0 +1,103 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from app.models.prayer import PrayerPrivacyLevel, PrayerStatus, PrayerCategory
|
||||
|
||||
|
||||
class PrayerRequestBase(BaseModel):
|
||||
title: str
|
||||
description: str
|
||||
category: PrayerCategory = PrayerCategory.OTHER
|
||||
privacy_level: PrayerPrivacyLevel = PrayerPrivacyLevel.PUBLIC
|
||||
ministry_id: Optional[int] = None
|
||||
is_anonymous: bool = False
|
||||
|
||||
|
||||
class PrayerRequestCreate(PrayerRequestBase):
|
||||
pass
|
||||
|
||||
|
||||
class PrayerRequestUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
category: Optional[PrayerCategory] = None
|
||||
privacy_level: Optional[PrayerPrivacyLevel] = None
|
||||
status: Optional[PrayerStatus] = None
|
||||
answer_description: Optional[str] = None
|
||||
|
||||
|
||||
class PrayerRequestResponse(PrayerRequestBase):
|
||||
id: int
|
||||
requester_id: int
|
||||
status: PrayerStatus
|
||||
answer_description: Optional[str] = None
|
||||
answered_at: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
requester_name: Optional[str] = None
|
||||
ministry_name: Optional[str] = None
|
||||
prayer_count: int = 0
|
||||
has_prayed: bool = False
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PrayerBase(BaseModel):
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
class PrayerCreate(PrayerBase):
|
||||
pass
|
||||
|
||||
|
||||
class PrayerResponse(PrayerBase):
|
||||
id: int
|
||||
prayer_request_id: int
|
||||
prayer_warrior_id: int
|
||||
prayed_at: datetime
|
||||
prayer_warrior_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PrayerGroupBase(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class PrayerGroupCreate(PrayerGroupBase):
|
||||
member_ids: List[int] = []
|
||||
|
||||
|
||||
class PrayerGroupResponse(PrayerGroupBase):
|
||||
id: int
|
||||
leader_id: int
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
leader_name: Optional[str] = None
|
||||
member_count: int = 0
|
||||
is_member: bool = False
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PrayerGroupMemberResponse(BaseModel):
|
||||
id: int
|
||||
prayer_group_id: int
|
||||
user_id: int
|
||||
joined_at: datetime
|
||||
user_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PrayerStats(BaseModel):
|
||||
total_requests: int
|
||||
active_requests: int
|
||||
answered_requests: int
|
||||
prayers_offered: int
|
||||
requests_created: int
|
164
app/services/notification_service.py
Normal file
164
app/services/notification_service.py
Normal file
@ -0,0 +1,164 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional, Dict, Any
|
||||
import json
|
||||
from app.models.notification import Notification, NotificationType, NotificationSettings
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class NotificationService:
|
||||
@staticmethod
|
||||
def create_notification(
|
||||
db: Session,
|
||||
recipient_id: int,
|
||||
notification_type: NotificationType,
|
||||
title: str,
|
||||
message: str,
|
||||
sender_id: Optional[int] = None,
|
||||
data: Optional[Dict[Any, Any]] = None,
|
||||
) -> Notification:
|
||||
"""Create a new notification"""
|
||||
|
||||
# Check if user wants this type of notification
|
||||
settings = (
|
||||
db.query(NotificationSettings)
|
||||
.filter(NotificationSettings.user_id == recipient_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if settings and not NotificationService._should_send_notification(
|
||||
settings, notification_type
|
||||
):
|
||||
return None
|
||||
|
||||
notification = Notification(
|
||||
recipient_id=recipient_id,
|
||||
sender_id=sender_id,
|
||||
type=notification_type,
|
||||
title=title,
|
||||
message=message,
|
||||
data=json.dumps(data) if data else None,
|
||||
)
|
||||
|
||||
db.add(notification)
|
||||
db.commit()
|
||||
db.refresh(notification)
|
||||
return notification
|
||||
|
||||
@staticmethod
|
||||
def _should_send_notification(
|
||||
settings: NotificationSettings, notification_type: NotificationType
|
||||
) -> bool:
|
||||
"""Check if user wants to receive this type of notification"""
|
||||
type_mapping = {
|
||||
NotificationType.CONNECTION_REQUEST: settings.connection_requests,
|
||||
NotificationType.CONNECTION_ACCEPTED: settings.connection_requests,
|
||||
NotificationType.POST_LIKE: settings.post_interactions,
|
||||
NotificationType.POST_COMMENT: settings.post_interactions,
|
||||
NotificationType.EVENT_INVITATION: settings.event_reminders,
|
||||
NotificationType.EVENT_REMINDER: settings.event_reminders,
|
||||
NotificationType.PRAYER_REQUEST: settings.prayer_requests,
|
||||
NotificationType.MINISTRY_ASSIGNMENT: settings.ministry_updates,
|
||||
NotificationType.DONATION_REMINDER: settings.donation_reminders,
|
||||
NotificationType.BIRTHDAY_REMINDER: settings.birthday_reminders,
|
||||
NotificationType.ANNOUNCEMENT: settings.announcements,
|
||||
}
|
||||
return type_mapping.get(notification_type, True)
|
||||
|
||||
@staticmethod
|
||||
def mark_as_read(db: Session, notification_id: int, user_id: int) -> bool:
|
||||
"""Mark notification as read"""
|
||||
notification = (
|
||||
db.query(Notification)
|
||||
.filter(
|
||||
Notification.id == notification_id, Notification.recipient_id == user_id
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if notification:
|
||||
notification.is_read = True
|
||||
from datetime import datetime
|
||||
|
||||
notification.read_at = datetime.utcnow()
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def mark_all_as_read(db: Session, user_id: int) -> int:
|
||||
"""Mark all notifications as read for a user"""
|
||||
from datetime import datetime
|
||||
|
||||
count = (
|
||||
db.query(Notification)
|
||||
.filter(Notification.recipient_id == user_id, Notification.is_read is False)
|
||||
.update({"is_read": True, "read_at": datetime.utcnow()})
|
||||
)
|
||||
db.commit()
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
def get_unread_count(db: Session, user_id: int) -> int:
|
||||
"""Get count of unread notifications"""
|
||||
return (
|
||||
db.query(Notification)
|
||||
.filter(Notification.recipient_id == user_id, Notification.is_read is False)
|
||||
.count()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_connection_request_notification(
|
||||
db: Session, sender: User, recipient_id: int
|
||||
):
|
||||
"""Create notification for connection request"""
|
||||
return NotificationService.create_notification(
|
||||
db=db,
|
||||
recipient_id=recipient_id,
|
||||
sender_id=sender.id,
|
||||
notification_type=NotificationType.CONNECTION_REQUEST,
|
||||
title="New Connection Request",
|
||||
message=f"{sender.first_name} {sender.last_name} wants to connect with you",
|
||||
data={"sender_name": f"{sender.first_name} {sender.last_name}"},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_post_like_notification(
|
||||
db: Session, liker: User, post_author_id: int, post_id: int
|
||||
):
|
||||
"""Create notification for post like"""
|
||||
if liker.id == post_author_id: # Don't notify self
|
||||
return None
|
||||
|
||||
return NotificationService.create_notification(
|
||||
db=db,
|
||||
recipient_id=post_author_id,
|
||||
sender_id=liker.id,
|
||||
notification_type=NotificationType.POST_LIKE,
|
||||
title="Post Liked",
|
||||
message=f"{liker.first_name} {liker.last_name} liked your post",
|
||||
data={
|
||||
"post_id": post_id,
|
||||
"liker_name": f"{liker.first_name} {liker.last_name}",
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_post_comment_notification(
|
||||
db: Session, commenter: User, post_author_id: int, post_id: int
|
||||
):
|
||||
"""Create notification for post comment"""
|
||||
if commenter.id == post_author_id: # Don't notify self
|
||||
return None
|
||||
|
||||
return NotificationService.create_notification(
|
||||
db=db,
|
||||
recipient_id=post_author_id,
|
||||
sender_id=commenter.id,
|
||||
notification_type=NotificationType.POST_COMMENT,
|
||||
title="New Comment",
|
||||
message=f"{commenter.first_name} {commenter.last_name} commented on your post",
|
||||
data={
|
||||
"post_id": post_id,
|
||||
"commenter_name": f"{commenter.first_name} {commenter.last_name}",
|
||||
},
|
||||
)
|
24
main.py
24
main.py
@ -1,6 +1,19 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.api import auth, users, posts, events, connections
|
||||
from app.api import (
|
||||
auth,
|
||||
users,
|
||||
posts,
|
||||
events,
|
||||
connections,
|
||||
uploads,
|
||||
notifications,
|
||||
ministries,
|
||||
donations,
|
||||
messages,
|
||||
prayers,
|
||||
attendance,
|
||||
)
|
||||
import os
|
||||
|
||||
app = FastAPI(
|
||||
@ -24,6 +37,15 @@ app.include_router(users.router, prefix="/users", tags=["Users"])
|
||||
app.include_router(posts.router, prefix="/posts", tags=["Posts"])
|
||||
app.include_router(events.router, prefix="/events", tags=["Events"])
|
||||
app.include_router(connections.router, prefix="/connections", tags=["Connections"])
|
||||
app.include_router(uploads.router, prefix="/uploads", tags=["File Uploads"])
|
||||
app.include_router(
|
||||
notifications.router, prefix="/notifications", tags=["Notifications"]
|
||||
)
|
||||
app.include_router(ministries.router, prefix="/ministries", tags=["Ministries"])
|
||||
app.include_router(donations.router, prefix="/donations", tags=["Donations"])
|
||||
app.include_router(messages.router, prefix="/messages", tags=["Messaging"])
|
||||
app.include_router(prayers.router, prefix="/prayers", tags=["Prayer Requests"])
|
||||
app.include_router(attendance.router, prefix="/attendance", tags=["Attendance"])
|
||||
|
||||
|
||||
@app.get("/")
|
||||
|
@ -9,3 +9,10 @@ python-decouple==3.8
|
||||
ruff==0.1.6
|
||||
pydantic==2.5.0
|
||||
email-validator==2.1.0
|
||||
pillow==10.1.0
|
||||
aiofiles==23.2.1
|
||||
python-dateutil==2.8.2
|
||||
celery==5.3.4
|
||||
redis==5.0.1
|
||||
emails==0.6.0
|
||||
jinja2==3.1.2
|
Loading…
x
Reference in New Issue
Block a user