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")
|
likes = relationship("Like", back_populates="user")
|
||||||
event_registrations = relationship("EventRegistration", 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):
|
class Connection(Base):
|
||||||
__tablename__ = "connections"
|
__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 import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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
|
import os
|
||||||
|
|
||||||
app = FastAPI(
|
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(posts.router, prefix="/posts", tags=["Posts"])
|
||||||
app.include_router(events.router, prefix="/events", tags=["Events"])
|
app.include_router(events.router, prefix="/events", tags=["Events"])
|
||||||
app.include_router(connections.router, prefix="/connections", tags=["Connections"])
|
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("/")
|
@app.get("/")
|
||||||
|
@ -8,4 +8,11 @@ passlib[bcrypt]==1.7.4
|
|||||||
python-decouple==3.8
|
python-decouple==3.8
|
||||||
ruff==0.1.6
|
ruff==0.1.6
|
||||||
pydantic==2.5.0
|
pydantic==2.5.0
|
||||||
email-validator==2.1.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