Update code via agent code generation

This commit is contained in:
Automated Action 2025-07-01 12:54:48 +00:00
parent 771ee5214f
commit 75feb39820
31 changed files with 4163 additions and 2 deletions

View 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")

View File

View File

484
app/api/attendance.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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)

View File

71
app/core/upload.py Normal file
View 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

View File

View File

124
app/models/attendance.py Normal file
View 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
View 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
View 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
View 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])

View 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
View 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")

View File

@ -34,6 +34,13 @@ class User(Base):
likes = relationship("Like", back_populates="user")
event_registrations = relationship("EventRegistration", back_populates="user")
# New relationships for enhanced features
donations = relationship("Donation", back_populates="donor")
prayer_requests = relationship("PrayerRequest", back_populates="requester")
notification_settings = relationship(
"NotificationSettings", back_populates="user", uselist=False
)
class Connection(Base):
__tablename__ = "connections"

158
app/schemas/attendance.py Normal file
View 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
View 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
View 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
View 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

View 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
View 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

View 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
View File

@ -1,6 +1,19 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api import auth, users, posts, events, connections
from app.api import (
auth,
users,
posts,
events,
connections,
uploads,
notifications,
ministries,
donations,
messages,
prayers,
attendance,
)
import os
app = FastAPI(
@ -24,6 +37,15 @@ app.include_router(users.router, prefix="/users", tags=["Users"])
app.include_router(posts.router, prefix="/posts", tags=["Posts"])
app.include_router(events.router, prefix="/events", tags=["Events"])
app.include_router(connections.router, prefix="/connections", tags=["Connections"])
app.include_router(uploads.router, prefix="/uploads", tags=["File Uploads"])
app.include_router(
notifications.router, prefix="/notifications", tags=["Notifications"]
)
app.include_router(ministries.router, prefix="/ministries", tags=["Ministries"])
app.include_router(donations.router, prefix="/donations", tags=["Donations"])
app.include_router(messages.router, prefix="/messages", tags=["Messaging"])
app.include_router(prayers.router, prefix="/prayers", tags=["Prayer Requests"])
app.include_router(attendance.router, prefix="/attendance", tags=["Attendance"])
@app.get("/")

View File

@ -8,4 +8,11 @@ passlib[bcrypt]==1.7.4
python-decouple==3.8
ruff==0.1.6
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