init
Some checks failed
CI/CD / Backend & Frontend Checks (push) Has been cancelled
CI/CD / Deploy to Production (push) Has been cancelled

This commit is contained in:
2026-05-18 11:34:07 +03:30
commit 7a8ddeabed
279 changed files with 37390 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
"""Aggregate exports for API schemas and shared response payloads."""
from typing import Optional
from ninja import Schema
from api.schemas.auth import *
from api.schemas.blog import *
from api.schemas.gallery import *
from api.schemas.events import *
from api.schemas.communications import *
from api.schemas.certificates import *
class MessageSchema(Schema):
"""Basic success response containing a message."""
message: str
class ErrorSchema(Schema):
"""Standard error payload with optional details."""
error: str
details: Optional[str] = None
def rebuild_comment_schema() -> None:
"""Ensure the self-referential CommentSchema is fully initialized."""
CommentSchema.model_rebuild()
rebuild_comment_schema()

129
backend/api/schemas/auth.py Normal file
View File

@@ -0,0 +1,129 @@
"""Authentication-related API schemas."""
from ninja import Schema, ModelSchema
from typing import Optional
from users.models import User
class UserRegistrationSchema(Schema):
username: str
email: str
password: str
first_name: Optional[str] = None
last_name: Optional[str] = None
university: Optional[str] = None
student_id: Optional[str] = None
year_of_study: Optional[int] = None
major: Optional[str] = None
class UserLoginSchema(Schema):
email: str
password: str
class UserProfileSchema(ModelSchema):
profile_picture: Optional[str] = None
student_id: Optional[str] = None
major: Optional[str] = None
university: Optional[str] = None
class Meta:
model = User
fields = [
'id',
'username',
'email',
'first_name',
'last_name',
'student_id',
'year_of_study',
'major',
'university',
'bio',
'date_joined',
'is_email_verified',
'is_active',
'is_staff',
'is_superuser',
'is_deleted',
'deleted_at',
]
@staticmethod
def resolve_major(obj):
return obj.get_major_display()
@staticmethod
def resolve_university(obj):
return obj.get_university_display()
@staticmethod
def resolve_profile_picture(obj, context):
"""
Resolves the absolute URL for the profile picture.
`context` contains the request object, which is needed for build_absolute_uri.
"""
request = context['request']
if obj.profile_picture and hasattr(obj.profile_picture, 'url'):
return request.build_absolute_uri(obj.profile_picture.url)
return None
class UserListSchema(ModelSchema):
major: Optional[str] = None
university: Optional[str] = None
class Meta:
model = User
fields = [
'id',
'username',
'email',
'first_name',
'last_name',
'is_active',
'is_staff',
'is_superuser',
'date_joined',
'major',
'university',
]
@staticmethod
def resolve_full_name(obj):
return obj.get_full_name()
@staticmethod
def resolve_major(obj):
return obj.get_major_display()
@staticmethod
def resolve_university(obj):
return obj.get_university_display()
class UserUpdateSchema(Schema):
first_name: Optional[str] = None
last_name: Optional[str] = None
bio: Optional[str] = None
year_of_study: Optional[int] = None
major: Optional[str] = None
university: Optional[str] = None
student_id: Optional[str] = None
class TokenSchema(Schema):
access_token: str
refresh_token: str
token_type: str = "bearer"
class TokenRefreshIn(Schema):
refresh_token: str
class PasswordResetRequestSchema(Schema):
email: str
class PasswordResetConfirmSchema(Schema):
token: str
new_password: str
class UsernameCheckSchema(Schema):
exists: bool

View File

@@ -0,0 +1,87 @@
"""Blog API schemas."""
from ninja import Schema, ModelSchema
from typing import Optional, List
from datetime import datetime
from blog.models import Category, Tag, Comment
class CategorySchema(ModelSchema):
class Config:
model = Category
model_fields = ['id', 'name', 'slug', 'description']
class TagSchema(ModelSchema):
class Config:
model = Tag
model_fields = ['id', 'name', 'slug']
class AuthorSchema(Schema):
id: int
username: str
first_name: str
last_name: str
profile_picture: Optional[str] = None
@staticmethod
def resolve_profile_picture(obj, context):
request = context['request']
if obj.profile_picture and hasattr(obj.profile_picture, 'url'):
return request.build_absolute_uri(obj.profile_picture.url)
return None
class PostListSchema(Schema):
id: int
title: str
slug: str
excerpt: str
author: AuthorSchema
featured_image: Optional[str] = None
status: str
published_at: Optional[datetime] = None
category: Optional[CategorySchema] = None
tags: List[TagSchema]
is_featured: bool
created_at: datetime
reading_time: int
class PostDetailSchema(PostListSchema):
content: str
content_html: str
class PostCreateSchema(Schema):
title: str
content: str
excerpt: Optional[str] = None
category_id: Optional[int] = None
tag_ids: Optional[List[int]] = []
status: str = "draft"
is_featured: bool = False
class CommentSchema(ModelSchema):
author: AuthorSchema
replies: List['CommentSchema'] = []
post_id: int
post_title: str
post_slug: str
class Config:
model = Comment
model_fields = ['id', 'content', 'created_at', 'is_approved']
@staticmethod
def resolve_post_id(obj):
return obj.post_id
@staticmethod
def resolve_post_title(obj):
return obj.post.title
@staticmethod
def resolve_post_slug(obj):
return obj.post.slug
class CommentCreateSchema(Schema):
content: str
parent_id: Optional[int] = None

View File

@@ -0,0 +1,70 @@
"""API payloads for certificate operations."""
from datetime import datetime
from typing import List, Optional
from ninja import Schema
class SkillSchema(Schema):
id: int
name: str
description: Optional[str] = None
class CertificateTemplateOut(Schema):
id: int
event_id: int
event_title: str
image_url: Optional[str]
skill_ids: List[int]
skills: List[SkillSchema]
class CertificateGenerationItem(Schema):
user_id: int
score: int
title: Optional[str] = None
description: Optional[str] = None
skill_ids: Optional[List[int]] = None
issued_at: Optional[datetime] = None
expires_at: Optional[datetime] = None
class CertificateGenerationPayload(Schema):
entries: List[CertificateGenerationItem]
default_title: Optional[str] = None
default_description: Optional[str] = None
class UserCertificateOut(Schema):
id: int
user_id: int
user_name: str
event_id: int
title: str
certificate_id: str
certificate_code: str
score: int
score_label: str
image_url: Optional[str]
class CertificateGenerationResponse(Schema):
certificates: List[UserCertificateOut]
class CertificateVerificationOut(Schema):
certificate_id: str
certificate_code: str
user_id: int
user_name: str
event_id: int
event_title: str
title: str
score: int
score_label: str
issued_at: datetime
expires_at: Optional[datetime] = None
image_url: Optional[str] = None
skills: List[str]

View File

@@ -0,0 +1,124 @@
"""Schemas for communications-related endpoints."""
from datetime import datetime
from typing import Optional, List
from ninja import Schema, ModelSchema
from api.schemas import AuthorSchema
from communications.models import (
Announcement,
NewsletterSubscription,
PushNotificationDevice
)
class AnnouncementSchema(ModelSchema):
author: AuthorSchema
content_html: str
class Config:
model = Announcement
model_fields = [
'id', 'title', 'content', 'announcement_type', 'priority',
'is_published', 'publish_date', 'send_email', 'send_push',
'target_audience', 'email_sent', 'push_sent', 'created_at', 'updated_at'
]
@staticmethod
def resolve_content_html(obj):
return obj.content_html
class AnnouncementListSchema(Schema):
id: int
title: str
content: str
announcement_type: str
priority: str
author: AuthorSchema
is_published: bool
publish_date: Optional[datetime] = None
target_audience: str
created_at: datetime
class AnnouncementCreateSchema(Schema):
title: str
content: str
announcement_type: str = "general"
priority: str = "normal"
target_audience: str = "all"
is_published: bool = False
publish_date: Optional[datetime] = None
send_email: bool = False
send_push: bool = False
class AnnouncementUpdateSchema(Schema):
title: Optional[str] = None
content: Optional[str] = None
announcement_type: Optional[str] = None
priority: Optional[str] = None
target_audience: Optional[str] = None
is_published: Optional[bool] = None
publish_date: Optional[datetime] = None
send_email: Optional[bool] = None
send_push: Optional[bool] = None
class NewsletterSubscriptionSchema(ModelSchema):
user: Optional[AuthorSchema] = None
class Config:
model = NewsletterSubscription
model_fields = [
'id', 'email', 'is_active', 'subscribed_categories',
'confirmed_at', 'created_at'
]
class NewsletterSubscribeSchema(Schema):
email: str
subscribed_categories: Optional[List[str]] = []
class NewsletterUnsubscribeSchema(Schema):
email: str
class PushDeviceSchema(ModelSchema):
user: AuthorSchema
class Config:
model = PushNotificationDevice
model_fields = [
'id', 'device_token', 'device_type', 'is_active', 'created_at'
]
class PushDeviceCreateSchema(Schema):
device_token: str
device_type: str = "web"
class PushDeviceUpdateSchema(Schema):
is_active: bool
class PushNotificationSchema(Schema):
title: str
body: str
data: Optional[dict] = None
target_audience: str = "all"
class MessageResponseSchema(Schema):
"""Simple message payload for API responses."""
message: str
success: bool = True
class AnnouncementStatsSchema(Schema):
"""Summary statistics for announcements."""
total_announcements: int
published_announcements: int
draft_announcements: int
urgent_announcements: int
email_sent_count: int
push_sent_count: int
class NewsletterStatsSchema(Schema):
"""Summary statistics for newsletter subscriptions."""
total_subscriptions: int
active_subscriptions: int
confirmed_subscriptions: int
recent_subscriptions: int

View File

@@ -0,0 +1,247 @@
"""Event and gallery API schemas."""
from uuid import UUID
from ninja import ModelSchema, Schema
from pydantic import field_validator
from typing import Literal, Optional, List
from datetime import datetime
from api.schemas.blog import AuthorSchema
from events.models import Event, Registration
from gallery.models import Gallery
from payments.models import Payment
class EventGallerySchema(ModelSchema):
"""Schema representing gallery items associated with an event."""
uploaded_by: AuthorSchema
file_size_mb: float
markdown_url: str
absolute_image_url: Optional[str] = None
class Config:
model = Gallery
model_fields = ['id', 'title', 'description', 'image', 'alt_text',
'width', 'height', 'is_public', 'created_at']
@staticmethod
def resolve_absolute_image_url(obj, context):
request = context['request']
if obj.image and hasattr(obj.image, 'url'):
return request.build_absolute_uri(obj.image.url)
return None
class EventSchema(ModelSchema):
"""Schema providing full event details for API responses."""
gallery_images: List[EventGallerySchema]
description_html: str
registration_count: int
absolute_featured_image_url: Optional[str] = None
class Config:
model = Event
model_fields = [
'id', 'title', 'slug', 'description', 'featured_image', 'event_type',
'address', 'location', 'online_link', 'start_time', 'end_time',
'registration_start_date', 'registration_end_date', 'registration_success_markdown',
'capacity', 'price', 'status', 'created_at', 'updated_at'
]
@staticmethod
def resolve_absolute_featured_image_url(obj, context):
request = context['request']
if obj.featured_image and hasattr(obj.featured_image, 'url'):
return request.build_absolute_uri(obj.featured_image.url)
return None
@staticmethod
def resolve_registration_count(obj):
return obj.registrations.filter(status__in=[Registration.StatusChoices.CONFIRMED, Registration.StatusChoices.ATTENDED]).count()
@staticmethod
def resolve_description_html(obj):
return obj.description_html
class EventListSchema(Schema):
"""Condensed event representation for list endpoints."""
id: int
title: str
slug: str
featured_image: Optional[str] = None
absolute_featured_image_url: Optional[str] = None
event_type: str
start_time: datetime
end_time: datetime
registration_start_date: Optional[datetime] = None
registration_end_date: Optional[datetime] = None
capacity: Optional[int] = None
price: Optional[float] = None
status: str
registration_count: int
created_at: datetime
@staticmethod
def resolve_absolute_featured_image_url(obj, context):
request = context['request']
if obj.featured_image and hasattr(obj.featured_image, 'url'):
return request.build_absolute_uri(obj.featured_image.url)
return None
@staticmethod
def resolve_registration_count(obj):
return obj.registrations.filter(status__in=[Registration.StatusChoices.CONFIRMED, Registration.StatusChoices.ATTENDED]).count()
class EventCreateSchema(Schema):
"""Payload for creating events via the API."""
title: str
description: str
event_type: str
address: Optional[str] = None
location: Optional[str] = None
online_link: Optional[str] = None
start_time: datetime
end_time: datetime
registration_start_date: Optional[datetime] = None
registration_end_date: Optional[datetime] = None
capacity: Optional[int] = None
price: Optional[float] = None
status: str = "draft"
gallery_image_ids: Optional[List[int]] = []
class EventUpdateSchema(Schema):
"""Payload for updating events via the API."""
title: Optional[str] = None
description: Optional[str] = None
event_type: Optional[str] = None
address: Optional[str] = None
location: Optional[str] = None
online_link: Optional[str] = None
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
registration_start_date: Optional[datetime] = None
registration_end_date: Optional[datetime] = None
capacity: Optional[int] = None
price: Optional[float] = None
status: Optional[str] = None
gallery_image_ids: Optional[List[int]] = None
class RegistrationSchema(ModelSchema):
"""Schema describing a registration entry with event context."""
user: AuthorSchema
event: EventListSchema
discount_code: str | None = None
class Config:
model = Registration
model_fields = [
'id',
'status',
'registered_at',
'ticket_id',
'discount_amount',
'final_price',
'created_at',
'updated_at',
]
@staticmethod
def resolve_discount_code(obj):
return obj.discount_code.code if obj.discount_code else None
class AdminUserSchema(Schema):
id: int
username: str
first_name: str
last_name: str
email: str
class PaymentAdminSchema(Schema):
id: int
authority: Optional[str]
ref_id: Optional[str]
status: int
status_label: str
base_amount: int
discount_amount: int
amount: int
verified_at: Optional[datetime]
created_at: datetime
discount_code: Optional[str]
user: AdminUserSchema
@field_validator("discount_code", mode="before")
def normalize_discount_code(cls, value):
if value is None:
return None
if hasattr(value, "code"):
return value.code
return str(value)
class RegistrationAdminSchema(Schema):
id: int
ticket_id: UUID
status: str
status_label: str
registered_at: datetime
final_price: Optional[int]
discount_amount: Optional[int]
user: AdminUserSchema
payments: List[PaymentAdminSchema]
class EventAdminDetailSchema(EventSchema):
registrations: List[RegistrationAdminSchema] = []
@staticmethod
def resolve_registrations(obj):
return obj.registrations.select_related("user").prefetch_related(
"payments__discount_code"
).order_by("-registered_at")
class PaginatedRegistrationSchema(Schema):
count: int
next: Optional[str] = None
previous: Optional[str] = None
results: List[RegistrationAdminSchema]
class RegistrationStatusUpdateSchema(Schema):
status: str
class RegisterationDetailSchema(Schema):
"""Detailed registration information with associated event metadata."""
event_image: Optional[str]
event_title: str
event_type: str
ticket_id: UUID
status: str
registered_at: datetime
success_markdown: Optional[str]
class EventBriefSchema(Schema):
"""Minimal event representation used for nested responses."""
id: int
title: str
slug: str
start_date: datetime
end_date: Optional[datetime] = None
location: Optional[str] = None
price: int
absolute_image_url: Optional[str] = None
class MyEventRegistrationOut(Schema):
"""Registration information as returned to authenticated users."""
id: int
created_at: datetime
status: Literal["pending", "confirmed", "cancelled", "attended"]
event: EventBriefSchema
class RegistrationStatusOut(Schema):
is_registered: bool
class RegistrationCreateSchema(Schema):
discount_code: Optional[str] = None

View File

@@ -0,0 +1,27 @@
"""Schemas for gallery resources."""
from ninja import Schema, ModelSchema
from typing import Optional
from api.schemas.blog import AuthorSchema
from gallery.models import Gallery
class GallerySchema(ModelSchema):
"""Serialized representation of a gallery image."""
uploaded_by: AuthorSchema
file_size_mb: float
markdown_url: str
class Config:
model = Gallery
model_fields = ['id', 'title', 'description', 'image', 'alt_text',
'width', 'height', 'is_public', 'created_at']
class GalleryCreateSchema(Schema):
"""Payload for creating a gallery entry."""
title: str
description: Optional[str] = None
alt_text: Optional[str] = None
is_public: bool = True

View File

@@ -0,0 +1,35 @@
from ninja import Schema
class CreatePaymentIn(Schema):
event_id: int
description: str
discount_code: str | None = None
mobile: str | None = None
email: str | None = None
class CreatePaymentOut(Schema):
start_pay_url: str | None = None
authority: str | None = None
base_amount: int
discount_amount: int
amount: int
class PaymentDetailOut(Schema):
ref_id: str | None = None
authority: str | None = None
base_amount: int
discount_amount: int
amount: int
status: str
verified_at: str | None = None
event: dict
class CouponVerifyIn(Schema):
event_id: int
code: str
class CouponVerifyOut(Schema):
discount_amount: int
final_price: int