Browse Source

feat: add global institutions seed command and migration for user first and last names

main
sina_sajjadi 5 days ago
parent
commit
d1e1066648
  1. 31
      apps/account/admin/user.py
  2. 101
      apps/account/admin/verification.py
  3. 266
      apps/account/management/commands/seed_fake_profiles.py
  4. 41
      apps/account/migrations/0003_user_first_name_user_last_name.py
  5. 38
      apps/account/migrations/0004_user_address_user_institution_name_user_rank_and_more.py
  6. 37
      apps/account/models/user.py
  7. 232
      apps/account/serializers/auth_serializers.py
  8. 12
      apps/account/serializers/user.py
  9. 38
      apps/account/tests/test_phase1_auth.py
  10. 382
      apps/chat/management/commands/seed_hashemi_chat_meetings.py
  11. 2
      apps/chat/models/chat.py
  12. 1
      apps/profiles/management/__init__.py
  13. 1
      apps/profiles/management/commands/__init__.py
  14. 377
      apps/profiles/management/commands/seed_almadinah_rich_data.py
  15. 371
      apps/profiles/management/commands/seed_global_institutions.py
  16. 173
      apps/profiles/management/commands/update_institution_coords.py
  17. 141
      apps/profiles/serializers/institution_serializers.py
  18. 4
      apps/profiles/views/institution_views.py
  19. BIN
      db.sqlite3.backup
  20. BIN
      dump.rdb
  21. 628
      schema.yml
  22. 16
      utils/pagination.py

31
apps/account/admin/user.py

@ -16,6 +16,7 @@ from unfold.contrib.filters.admin import RangeDateTimeFilter
from apps.account.models import User, ClientUser, LocationHistory from apps.account.models import User, ClientUser, LocationHistory
from utils.admin import project_admin_site from utils.admin import project_admin_site
from apps.account.admin.location import LocationHistoryInline from apps.account.admin.location import LocationHistoryInline
from apps.account.admin.verification import VerificationDocumentInline
# ========================================================= # =========================================================
@ -25,12 +26,10 @@ from apps.account.admin.location import LocationHistoryInline
class UserAdminCreationForm(UserCreationForm): class UserAdminCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta): class Meta(UserCreationForm.Meta):
model = User model = User
fields = ("fullname", "email")
fields = ("first_name", "last_name", "fullname", "email")
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if 'fullname' in self.fields:
self.fields['fullname'].required = True
if 'email' in self.fields: if 'email' in self.fields:
self.fields['email'].required = True self.fields['email'].required = True
@ -47,8 +46,6 @@ class UserAdminChangeForm(UserChangeForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if 'fullname' in self.fields:
self.fields['fullname'].required = True
if 'email' in self.fields: if 'email' in self.fields:
self.fields['email'].required = True self.fields['email'].required = True
@ -58,9 +55,9 @@ class UserAdmin(ModelAdmin, BaseUserAdmin):
add_form = UserAdminCreationForm add_form = UserAdminCreationForm
change_password_form = AdminPasswordChangeForm change_password_form = AdminPasswordChangeForm
compressed_fields = False compressed_fields = False
list_display = ('fullname', 'email', 'user_type', 'is_staff', 'is_active', 'display_date_joined',)
list_display = ('first_name', 'last_name', 'fullname', 'email', 'user_type', 'is_staff', 'is_active', 'display_date_joined',)
ordering = ("-id",) ordering = ("-id",)
search_fields = ('email', 'fullname', 'username',)
search_fields = ('email', 'first_name', 'last_name', 'fullname', 'username',)
list_filter = [ list_filter = [
"user_type", "user_type",
"is_active", "is_active",
@ -68,18 +65,31 @@ class UserAdmin(ModelAdmin, BaseUserAdmin):
("last_login", RangeDateTimeFilter), ("last_login", RangeDateTimeFilter),
("date_joined", RangeDateTimeFilter), ("date_joined", RangeDateTimeFilter),
] ]
inlines = [LocationHistoryInline]
inlines = [LocationHistoryInline, VerificationDocumentInline]
add_fieldsets = ( add_fieldsets = (
(None, { (None, {
'classes': ('wide',), 'classes': ('wide',),
'fields': (('fullname', 'email'), 'phone_number', 'birthdate', 'gender', 'avatar', 'skill', 'info'),
'fields': (
('first_name', 'last_name'),
('fullname', 'email'),
'phone_number',
('title', 'rank'),
('institution_name', 'website'),
'address',
'birthdate',
'gender',
'avatar',
'skill',
'info'
),
}), }),
) )
fieldsets = ( fieldsets = (
(None, { (None, {
"fields": ( "fields": (
("first_name", "last_name"),
("fullname", "email"), ("fullname", "email"),
("password",), ("password",),
), ),
@ -89,6 +99,9 @@ class UserAdmin(ModelAdmin, BaseUserAdmin):
"fields": ( "fields": (
("avatar",), ("avatar",),
("phone_number", "gender"), ("phone_number", "gender"),
("title", "rank"),
("institution_name", "website"),
("address",),
("birthdate", "language"), ("birthdate", "language"),
("skill",), ("skill",),
("info",), ("info",),

101
apps/account/admin/verification.py

@ -11,22 +11,55 @@ from utils.admin import project_admin_site
class VerificationDocumentInline(StackedInline): class VerificationDocumentInline(StackedInline):
model = VerificationDocument model = VerificationDocument
fk_name = 'user'
extra = 0 extra = 0
fields = ('document_type', 'title', 'document_file', 'status', 'admin_notes', 'reviewed_by', 'uploaded_at')
readonly_fields = ('uploaded_at',)
fields = ('document_type', 'title', 'document_file', 'document_preview', 'status', 'admin_notes', 'reviewed_by', 'uploaded_at')
readonly_fields = ('uploaded_at', 'document_preview')
@display(description=_('Preview / Download'))
def document_preview(self, obj):
if not obj or not obj.document_file:
return _("No file uploaded")
file_url = obj.document_file.url
ext = obj.document_file.name.lower().split('.')[-1] if obj.document_file.name else ''
if ext in ['jpg', 'jpeg', 'png', 'webp', 'gif']:
return format_html(
'<a href="{url}" target="_blank" rel="noopener noreferrer">'
'<img src="{url}" style="max-height: 120px; max-width: 200px; border-radius: 6px; object-fit: contain; margin-bottom: 4px;" />'
'</a><br/><a href="{url}" target="_blank" rel="noopener noreferrer" style="color: #3b82f6; font-size: 12px;">↗ Open Full File</a>',
url=file_url
)
return format_html(
'<a href="{url}" target="_blank" rel="noopener noreferrer" style="color: #3b82f6; font-size: 12px; font-weight: 500;">'
'↗ Open / Download Document'
'</a>',
url=file_url
)
@admin.register(VerificationDocument, site=project_admin_site) @admin.register(VerificationDocument, site=project_admin_site)
class VerificationDocumentAdmin(ModelAdmin): class VerificationDocumentAdmin(ModelAdmin):
list_display = ('id', 'user_display', 'document_type', 'status_badge', 'uploaded_at', 'reviewed_by', 'reviewed_at')
list_display = (
'id',
'user_display',
'document_type',
'title',
'document_link',
'status_badge',
'uploaded_at',
'reviewed_by',
'reviewed_at'
)
list_display_links = ('id', 'user_display', 'document_type', 'title')
list_filter = ('status', 'document_type', 'uploaded_at') list_filter = ('status', 'document_type', 'uploaded_at')
search_fields = ('user__email', 'user__fullname', 'title', 'admin_notes') search_fields = ('user__email', 'user__fullname', 'title', 'admin_notes')
readonly_fields = ('uploaded_at', 'reviewed_at')
readonly_fields = ('uploaded_at', 'reviewed_at', 'document_preview')
autocomplete_fields = ('user', 'reviewed_by')
actions = ['approve_documents', 'reject_documents'] actions = ['approve_documents', 'reject_documents']
fieldsets = ( fieldsets = (
(_('Document Information'), { (_('Document Information'), {
'fields': ('user', 'document_type', 'title', 'document_file')
'fields': ('user', 'document_type', 'title', 'document_file', 'document_preview')
}), }),
(_('Review Status'), { (_('Review Status'), {
'fields': ('status', 'admin_notes', 'reviewed_by', 'reviewed_at', 'uploaded_at') 'fields': ('status', 'admin_notes', 'reviewed_by', 'reviewed_at', 'uploaded_at')
@ -37,6 +70,56 @@ class VerificationDocumentAdmin(ModelAdmin):
def user_display(self, obj): def user_display(self, obj):
return obj.user.fullname or obj.user.email return obj.user.fullname or obj.user.email
@display(description=_('File'))
def document_link(self, obj):
if not obj.document_file:
return "-"
return format_html(
'<a href="{url}" target="_blank" rel="noopener noreferrer" style="color: #3b82f6; font-weight: 500;">'
'View File ↗'
'</a>',
url=obj.document_file.url
)
@display(description=_('Document Preview / File'))
def document_preview(self, obj):
if not obj or not obj.document_file:
return _("No file uploaded")
file_url = obj.document_file.url
ext = obj.document_file.name.lower().split('.')[-1] if obj.document_file.name else ''
if ext in ['jpg', 'jpeg', 'png', 'webp', 'gif']:
return format_html(
'<div style="display: flex; flex-direction: column; gap: 8px;">'
'<a href="{url}" target="_blank" rel="noopener noreferrer">'
'<img src="{url}" style="max-height: 250px; max-width: 400px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); object-fit: contain; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);" />'
'</a>'
'<a href="{url}" target="_blank" rel="noopener noreferrer" style="display: inline-flex; align-items: center; gap: 4px; font-size: 13px; color: #3b82f6; font-weight: 500;">'
'↗ Open Full Image in New Tab'
'</a>'
'</div>',
url=file_url
)
elif ext == 'pdf':
return format_html(
'<div style="display: flex; flex-direction: column; gap: 8px;">'
'<iframe src="{url}" style="width: 100%; max-width: 600px; height: 350px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15);"></iframe>'
'<a href="{url}" target="_blank" rel="noopener noreferrer" style="display: inline-flex; align-items: center; gap: 4px; font-size: 13px; color: #3b82f6; font-weight: 500;">'
'↗ Open PDF in New Tab'
'</a>'
'</div>',
url=file_url
)
else:
return format_html(
'<a href="{url}" target="_blank" rel="noopener noreferrer" style="display: inline-flex; align-items: center; gap: 4px; font-size: 13px; color: #3b82f6; font-weight: 500;">'
'📄 Open / Download File ({name}) ↗'
'</a>',
url=file_url,
name=obj.document_file.name.split('/')[-1]
)
@display(description=_('Status'), label={ @display(description=_('Status'), label={
VerificationDocument.Status.VERIFIED: "success", VerificationDocument.Status.VERIFIED: "success",
VerificationDocument.Status.PENDING: "warning", VerificationDocument.Status.PENDING: "warning",
@ -45,6 +128,14 @@ class VerificationDocumentAdmin(ModelAdmin):
def status_badge(self, obj): def status_badge(self, obj):
return obj.get_status_display() return obj.get_status_display()
def save_model(self, request, obj, form, change):
if obj.status in [VerificationDocument.Status.VERIFIED, VerificationDocument.Status.REJECTED]:
if not obj.reviewed_by:
obj.reviewed_by = request.user
if not obj.reviewed_at:
obj.reviewed_at = timezone.now()
super().save_model(request, obj, form, change)
@action(description=_('Approve selected verification documents')) @action(description=_('Approve selected verification documents'))
def approve_documents(self, request, queryset): def approve_documents(self, request, queryset):
queryset.update( queryset.update(

266
apps/account/management/commands/seed_fake_profiles.py

@ -0,0 +1,266 @@
import os
from django.core.management.base import BaseCommand
from django.utils import timezone
from apps.account.models import User, VerificationDocument
class Command(BaseCommand):
help = 'Seeds realistic scholar and academic fake profiles similar to Ahmad Hashemi.'
def handle(self, *args, **kwargs):
admin_user = User.objects.filter(is_superuser=True).first() or User.objects.filter(is_staff=True).first()
# Update Ahmad Hashemi's avatar to downloaded real scholar photo
ahmad = User.objects.filter(email='[email protected]').first()
if ahmad:
ahmad.avatar = 'users/avatars/2026/10/avatar_ahmad_hashemi.jpg'
ahmad.save(update_fields=['avatar'])
self.stdout.write(self.style.SUCCESS("Updated Ahmad Hashemi avatar to real downloaded scholar photo."))
# Profiles definition
profiles_data = [
{
'email': '[email protected]',
'username': '[email protected]',
'first_name': 'سید محمد',
'last_name': 'حسینی',
'fullname': 'سید محمد حسینی',
'title': 'حجت‌الاسلام والمسلمین دکتر',
'rank': 'دانشیار فلسفه و کلام اسلامی',
'institution_name': 'جامعة المصطفی العالمیة',
'city': 'قم',
'country': 'ایران',
'phone_number': '+989127514320',
'website': 'https://sm-hosseini.ir',
'address': 'قم، بلوار امین، کوچه ۲۱، پلاک ۱۴',
'language': 'fa',
'languages': ['fa', 'ar', 'en'],
'skills': ['فلسفه اسلامی', 'کلام تطبیقی', 'حکمت متعالیه', 'روش‌شناسی پژوهش', 'گفتگوی ادیان'],
'info': (
'دکترای تخصصی فلسفه و حکمت اسلامی از جامعة المصطفی العالمیة و دارای مدرک سطح چهار حوزه علمیه قم. '
'با بیش از ۱۲ سال سابقه تدریس در مقاطع کارشناسی ارشد و دکتری و راهنمایی ده‌ها رساله علمی در حوزه کلام جدید و فلسفه تطبیقی. '
'مؤلف کتاب‌های «درآمدی بر معرفت‌شناسی دینی در عصر مدرن» و «خوانش فلسفی گفت‌وگوی بین‌المذاهب». '
'تمرکز پژوهشی فعلی بر بازخوانی میراث عقلانی تمدن اسلامی در مواجهه با چالش‌های معاصر و توسعه بسترهای دیالوگ میان‌فرهنگی است.'
),
'avatar': 'users/avatars/2026/10/avatar_seyed_hosseini.jpg',
'documents': [
{
'title': 'دانشنامه: سطح ۴ حوزوی (گرایش فلسفه و کلام اسلامی)',
'document_type': 'student_card',
'status': 'verified',
'admin_notes': 'مدارک با استعلام از مدیریت حوزه‌های علمیه تایید شد.',
},
{
'title': 'دانشنامه: دکتری تخصصی فلسفه و حکمت اسلامی (جامعة المصطفی)',
'document_type': 'student_card',
'status': 'verified',
'admin_notes': 'دانشنامه رسمی معتبر است.',
},
{
'title': 'حکم رسمی عضویت در هیئت علمی جامعة المصطفی العالمیة',
'document_type': 'recommendation_letter',
'status': 'verified',
'admin_notes': 'حکم کارگزینی بررسی و تایید شد.',
},
{
'title': 'گواهی گذرنامه و احراز هویت بین‌المللی',
'document_type': 'national_id',
'status': 'verified',
'admin_notes': '',
}
]
},
{
'email': '[email protected]',
'username': '[email protected]',
'first_name': 'فاطمه‌زهرا',
'last_name': 'میرزایی',
'fullname': 'فاطمه‌زهرا میرزایی',
'title': 'دکتر',
'rank': 'استاد تمام مطالعات خانواده و حقوق اسلامی',
'institution_name': 'دانشگاه تهران',
'city': 'تهران',
'country': 'ایران',
'phone_number': '+989123849102',
'website': 'https://fz-mirzaei.ut.ac.ir',
'address': 'تهران، خیابان کارگر شمالی، پردیس دانشکده الهیات دانشگاه تهران',
'language': 'fa',
'languages': ['fa', 'en', 'ar', 'fr'],
'skills': ['فقه و حقوق خانواده', 'مطالعات زنان در اسلام', 'حقوق تطبیقی', 'جامعه‌شناسی ادیان', 'روش تحقیق کیفی'],
'info': (
'استاد تمام دانشکده الهیات و معارف اسلامی دانشگاه تهران و دارنده مدرک دکتری فقه و مبانی حقوق اسلامی '
'با بیش از ۱۶ سال سابقه تدریس و پژوهش دانشگاهی. عضو شورای علمی پژوهشکده مطالعات اسلامی زنان و مؤلف آثار مرجع در زمینه '
'حقوق تطبیقی زن در فقه فریقین. داور مجلات بین‌المللی مطالعات خاورمیانه و سرپرست علمی کارگروه توانمندسازی حقوقی زنان در کشورهای اسلامی.'
),
'avatar': 'users/avatars/2026/10/avatar_fatemeh_mirzaei.jpg',
'documents': [
{
'title': 'دانشنامه: دکتری فقه و مبانی حقوق اسلامی (دانشگاه تهران)',
'document_type': 'student_card',
'status': 'verified',
'admin_notes': 'تایید شده توسط دبیرخانه آموزش دانشگاه تهران.',
},
{
'title': 'حکم ارتقا به مرتبه استاد تمامی (دانشگاه تهران)',
'document_type': 'recommendation_letter',
'status': 'verified',
'admin_notes': 'مصوبه هیئت ممیزه تایید شد.',
},
{
'title': 'کارت شناسایی هیئت علمی و کارت ملی هوشمند',
'document_type': 'national_id',
'status': 'verified',
'admin_notes': '',
}
]
},
{
'email': '[email protected]',
'username': '[email protected]',
'first_name': 'مصطفی',
'last_name': 'کمالی',
'fullname': 'مصطفی کمالی',
'title': 'استاد',
'rank': 'پژوهشگر ارشد تمدن اسلامی و نسخ خطی',
'institution_name': 'دانشگاه فردوسی مشهد',
'city': 'مشهد',
'country': 'ایران',
'phone_number': '+989151248901',
'website': 'https://mkamali.ir',
'address': 'مشهد، میدان آزادی، پردیس دانشگاه فردوسی، دانشکده ادبیات و علوم انسانی',
'language': 'fa',
'languages': ['fa', 'ar', 'en'],
'skills': ['نسخه‌شناسی', 'تصحیح متون کهن', 'تاریخ تمدن اسلامی', 'دیجیتال‌سازی اسناد', 'عربی کلاسیک'],
'info': (
'پژوهشگر ارشد تاریخ تمدن و ملل اسلامی و متخصص احیا و تصحیح انتقادی نسخ خطی سده‌های میانه اسلامی. '
'دارای همکاری‌های گسترده علمی با کتابخانه مرکزی آستان قدس رضوی، کتابخانه ملی ایران و مؤسسات شرق‌شناسی دانشگاه‌های لیدن و هایدلبرگ. '
'سرپرست پروژه فهرست‌نویسی توصیفی دست‌نوشته‌های کهن ریاضیات و نجوم دوره اسلامی.'
),
'avatar': 'users/avatars/2026/10/avatar_mostafa_kamali.jpg',
'documents': [
{
'title': 'دانشنامه: دکتری تاریخ و تمدن ملل اسلامی (دانشگاه فردوسی)',
'document_type': 'student_card',
'status': 'verified',
'admin_notes': 'مدرک تحصیلی تایید شده است.',
},
{
'title': 'معرفی‌نامه پژوهشی: بنیاد پژوهش‌های اسلامی آستان قدس رضوی',
'document_type': 'recommendation_letter',
'status': 'pending',
'admin_notes': 'در حال استعلام نهایی از مرکز اسناد.',
},
{
'title': 'کارت شناسایی هیئت علمی و پژوهشگر ارشد',
'document_type': 'national_id',
'status': 'verified',
'admin_notes': '',
}
]
},
{
'email': '[email protected]',
'username': '[email protected]',
'first_name': 'حیدر',
'last_name': 'التمیمی',
'fullname': 'حیدر التمیمی',
'title': 'آیت‌الله دکتر',
'rank': 'رئیس مرکز پژوهش‌های تقریب و مذاهب اسلامی',
'institution_name': 'مجمع جهانی تقریب مذاهب اسلامی',
'city': 'نجف',
'country': 'عراق',
'phone_number': '+9647801234567',
'website': 'https://altamimi-center.org',
'address': 'نجف اشرف، شارع الرسول، مرکز الدراسات الاسلامیة الموحدة',
'language': 'ar',
'languages': ['ar', 'fa', 'en'],
'skills': ['فقه مقارن', 'تقریب مذاهب', 'علوم حدیث و رجال', 'اصول استنباط', 'گفتگوی وحدت اسلامی'],
'info': (
'استاد درس خارج فقه و اصول حوزه علمیه نجف اشرف و دارنده دکترای فقه مقارن با بیش از بیست سال فعالیت در ترویج گفت‌وگو '
'و همگرایی میان علمای جهان اسلام. عضو هیئت امنای مجمع جهانی تقریب مذاهب اسلامی و نویسنده موسوعه فقهی تطبیقی مذاهب خمسه. '
'سخنران اصلی در نشست‌های بین‌المللی الازهر، مکه مکرمه، تهران و استانبول.'
),
'avatar': 'users/avatars/2026/10/avatar_haidar_tamimi.jpg',
'documents': [
{
'title': 'اجازه‌نامه اجتهاد و تدریس خارج فقه (حوزه علمیه نجف اشرف)',
'document_type': 'student_card',
'status': 'verified',
'admin_notes': 'گواهی سطح عالی حوزوی تایید گردید.',
},
{
'title': 'دانشنامه: دکتری تخصصی فقه مقارن و حقوق اسلامی',
'document_type': 'student_card',
'status': 'verified',
'admin_notes': 'دانشنامه تطبیقی مورد تایید است.',
},
{
'title': 'حکم ریاست شورای پژوهشی تقریب مذاهب اسلامی',
'document_type': 'institution_license',
'status': 'verified',
'admin_notes': 'حکم رسمی معتبر است.',
},
{
'title': 'گواهی گذرنامه و احراز هویت بین‌المللی',
'document_type': 'national_id',
'status': 'pending',
'admin_notes': 'در انتظار تایید مدارک هویتی تکمیلی.',
}
]
}
]
sample_doc_file = 'users/documents/2026/10/institution_license.pdf'
for p_data in profiles_data:
email = p_data['email']
user, created = User.objects.get_or_create(email=email, defaults={'username': p_data['username']})
user.first_name = p_data['first_name']
user.last_name = p_data['last_name']
user.fullname = p_data['fullname']
user.title = p_data['title']
user.rank = p_data['rank']
user.institution_name = p_data['institution_name']
user.city = p_data['city']
user.country = p_data['country']
user.phone_number = p_data['phone_number']
user.website = p_data['website']
user.address = p_data['address']
user.language = p_data['language']
user.languages = p_data['languages']
user.skills = p_data['skills']
user.info = p_data['info']
user.user_type = User.UserType.CLIENT
user.is_active = True
user.avatar = p_data['avatar']
# Standardized password (12345678)
user.set_password('12345678')
if hasattr(user, 'set_plain_password'):
try:
user.set_plain_password('12345678')
except Exception:
pass
user.save()
# Verification documents
for doc_item in p_data['documents']:
VerificationDocument.objects.get_or_create(
user=user,
title=doc_item['title'],
defaults={
'document_type': doc_item['document_type'],
'document_file': sample_doc_file,
'status': doc_item['status'],
'admin_notes': doc_item['admin_notes'],
'reviewed_by': admin_user if doc_item['status'] == 'verified' else None,
'reviewed_at': timezone.now() if doc_item['status'] == 'verified' else None,
}
)
action_text = "Created" if created else "Updated"
self.stdout.write(self.style.SUCCESS(f"{action_text} profile: {user.fullname} ({user.email})"))
self.stdout.write(self.style.SUCCESS("All fake profiles seeded successfully!"))

41
apps/account/migrations/0003_user_first_name_user_last_name.py

@ -0,0 +1,41 @@
# Generated by Django 4.2.30 on 2026-10-03 09:07
from django.db import migrations, models
def populate_first_and_last_names(apps, schema_editor):
User = apps.get_model('account', 'User')
for user in User.objects.all():
if user.fullname and not user.first_name and not user.last_name:
parts = user.fullname.strip().split(maxsplit=1)
if len(parts) == 2:
user.first_name = parts[0]
user.last_name = parts[1]
elif len(parts) == 1:
user.first_name = parts[0]
user.last_name = ""
user.save(update_fields=['first_name', 'last_name'])
def reverse_populate(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('account', '0002_user_languages_user_skills_alter_user_language_and_more'),
]
operations = [
migrations.AddField(
model_name='user',
name='first_name',
field=models.CharField(blank=True, max_length=150, null=True, verbose_name='First Name'),
),
migrations.AddField(
model_name='user',
name='last_name',
field=models.CharField(blank=True, max_length=150, null=True, verbose_name='Last Name'),
),
migrations.RunPython(populate_first_and_last_names, reverse_populate),
]

38
apps/account/migrations/0004_user_address_user_institution_name_user_rank_and_more.py

@ -0,0 +1,38 @@
# Generated by Django 4.2.30 on 2026-10-03 09:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0003_user_first_name_user_last_name'),
]
operations = [
migrations.AddField(
model_name='user',
name='address',
field=models.TextField(blank=True, null=True, verbose_name='Postal Address'),
),
migrations.AddField(
model_name='user',
name='institution_name',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Affiliated Institution'),
),
migrations.AddField(
model_name='user',
name='rank',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Academic / Clerical Rank'),
),
migrations.AddField(
model_name='user',
name='title',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Title / Honorific'),
),
migrations.AddField(
model_name='user',
name='website',
field=models.CharField(blank=True, max_length=512, null=True, verbose_name='Personal Website'),
),
]

37
apps/account/models/user.py

@ -36,8 +36,18 @@ class User(AbstractUser):
FEMALE = 'female', _('Female') FEMALE = 'female', _('Female')
OTHER = 'other', _('Other') OTHER = 'other', _('Other')
last_name = None
first_name = None
first_name = models.CharField(
max_length=150,
verbose_name=_("First Name"),
null=True,
blank=True
)
last_name = models.CharField(
max_length=150,
verbose_name=_("Last Name"),
null=True,
blank=True
)
username = models.CharField( username = models.CharField(
unique=True, unique=True,
null=True, null=True,
@ -104,6 +114,11 @@ class User(AbstractUser):
city = models.CharField(verbose_name=_('City'), max_length=255, null=True, blank=True) city = models.CharField(verbose_name=_('City'), max_length=255, null=True, blank=True)
country = models.CharField(max_length=255, verbose_name=_('Country'), null=True, blank=True) country = models.CharField(max_length=255, verbose_name=_('Country'), null=True, blank=True)
title = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Title / Honorific'))
rank = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Academic / Clerical Rank'))
institution_name = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Affiliated Institution'))
website = models.CharField(max_length=512, null=True, blank=True, verbose_name=_('Personal Website'))
address = models.TextField(null=True, blank=True, verbose_name=_('Postal Address'))
device_id = models.CharField(verbose_name=_('Device ID'), max_length=255, null=True, blank=True) device_id = models.CharField(verbose_name=_('Device ID'), max_length=255, null=True, blank=True)
device_os = models.CharField(choices=DeviceOs.choices, null=True, blank=True, max_length=16, verbose_name=_('Device OS')) device_os = models.CharField(choices=DeviceOs.choices, null=True, blank=True, max_length=16, verbose_name=_('Device OS'))
@ -159,10 +174,26 @@ class User(AbstractUser):
elif self.email and User.objects.filter(username=self.email).exclude(pk=self.pk).exists(): elif self.email and User.objects.filter(username=self.email).exclude(pk=self.pk).exists():
self.username = f'{self.email}:{self.id or random.randint(1000, 9999)}' self.username = f'{self.email}:{self.id or random.randint(1000, 9999)}'
# Maintain first_name, last_name, and fullname synchronization
if self.first_name or self.last_name:
self.fullname = f"{self.first_name or ''} {self.last_name or ''}".strip()
elif self.fullname:
parts = self.fullname.strip().split(maxsplit=1)
if len(parts) == 2:
self.first_name = parts[0]
self.last_name = parts[1]
elif len(parts) == 1:
self.first_name = parts[0]
self.last_name = ""
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
def get_full_name(self): def get_full_name(self):
return self.fullname or self.email or ""
full = f"{self.first_name or ''} {self.last_name or ''}".strip()
return full or self.fullname or self.email or ""
def get_short_name(self):
return self.first_name or self.fullname or self.email or ""
@property @property
def is_guest(self): def is_guest(self):

232
apps/account/serializers/auth_serializers.py

@ -5,6 +5,7 @@ from rest_framework import serializers
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from phonenumber_field.serializerfields import PhoneNumberField from phonenumber_field.serializerfields import PhoneNumberField
from drf_spectacular.utils import extend_schema_field, OpenApiTypes
from apps.account.models.verification import VerificationDocument from apps.account.models.verification import VerificationDocument
User = get_user_model() User = get_user_model()
@ -14,7 +15,9 @@ User = get_user_model()
class RegisterRequestSerializer(serializers.Serializer): class RegisterRequestSerializer(serializers.Serializer):
email = serializers.EmailField(required=True) email = serializers.EmailField(required=True)
password = serializers.CharField(write_only=True, required=True, min_length=6) password = serializers.CharField(write_only=True, required=True, min_length=6)
fullname = serializers.CharField(required=True, max_length=255)
first_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
last_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
fullname = serializers.CharField(required=False, allow_blank=True, max_length=255)
phone_number = serializers.CharField(required=False, allow_blank=True, allow_null=True) phone_number = serializers.CharField(required=False, allow_blank=True, allow_null=True)
user_type = serializers.ChoiceField( user_type = serializers.ChoiceField(
choices=User.UserType.choices, choices=User.UserType.choices,
@ -44,15 +47,40 @@ class RegisterRequestSerializer(serializers.Serializer):
validate_password(value) validate_password(value)
return value return value
def validate(self, attrs):
fn = attrs.get('first_name', '').strip()
ln = attrs.get('last_name', '').strip()
fl = attrs.get('fullname', '').strip()
if fn or ln:
attrs['first_name'] = fn
attrs['last_name'] = ln
if not fl:
attrs['fullname'] = f"{fn} {ln}".strip()
else:
attrs['fullname'] = fl
elif fl:
parts = fl.split(maxsplit=1)
attrs['first_name'] = parts[0]
attrs['last_name'] = parts[1] if len(parts) > 1 else ""
attrs['fullname'] = fl
else:
raise serializers.ValidationError({"first_name": "Please provide first_name and last_name (or full name)."})
return attrs
def create(self, validated_data): def create(self, validated_data):
email = validated_data.pop('email') email = validated_data.pop('email')
password = validated_data.pop('password') password = validated_data.pop('password')
fullname = validated_data.pop('fullname')
first_name = validated_data.pop('first_name', '')
last_name = validated_data.pop('last_name', '')
fullname = validated_data.pop('fullname', '')
user_type = validated_data.pop('user_type', User.UserType.CLIENT) user_type = validated_data.pop('user_type', User.UserType.CLIENT)
user = User.objects.create_user( user = User.objects.create_user(
email=email, email=email,
password=password, password=password,
first_name=first_name,
last_name=last_name,
fullname=fullname, fullname=fullname,
username=email, username=email,
user_type=user_type, user_type=user_type,
@ -68,6 +96,18 @@ class UserMeSerializer(serializers.ModelSerializer):
permissions = serializers.SerializerMethodField() permissions = serializers.SerializerMethodField()
verification_status = serializers.SerializerMethodField() verification_status = serializers.SerializerMethodField()
is_verified = serializers.SerializerMethodField() is_verified = serializers.SerializerMethodField()
first_name = serializers.SerializerMethodField()
last_name = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
phone = serializers.SerializerMethodField()
preferred_language = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
rank = serializers.SerializerMethodField()
website = serializers.SerializerMethodField()
address = serializers.SerializerMethodField()
position = serializers.SerializerMethodField()
institution = serializers.SerializerMethodField()
institution_id = serializers.SerializerMethodField()
class Meta: class Meta:
model = User model = User
@ -76,12 +116,24 @@ class UserMeSerializer(serializers.ModelSerializer):
'email', 'email',
'username', 'username',
'fullname', 'fullname',
'first_name',
'last_name',
'name',
'phone_number', 'phone_number',
'phone',
'user_type', 'user_type',
'avatar', 'avatar',
'bio', 'bio',
'languages', 'languages',
'preferred_language',
'skills', 'skills',
'title',
'rank',
'position',
'institution',
'institution_id',
'website',
'address',
'country', 'country',
'city', 'city',
'gender', 'gender',
@ -98,6 +150,18 @@ class UserMeSerializer(serializers.ModelSerializer):
'id', 'id',
'email', 'email',
'username', 'username',
'first_name',
'last_name',
'name',
'phone',
'preferred_language',
'title',
'rank',
'position',
'institution',
'institution_id',
'website',
'address',
'user_type', 'user_type',
'date_joined', 'date_joined',
'is_active', 'is_active',
@ -108,6 +172,76 @@ class UserMeSerializer(serializers.ModelSerializer):
'is_verified', 'is_verified',
] ]
def get_first_name(self, obj) -> str:
if obj.first_name:
return obj.first_name
if obj.fullname:
return obj.fullname.strip().split()[0]
return ""
def get_last_name(self, obj) -> str:
if obj.last_name:
return obj.last_name
if obj.fullname:
parts = obj.fullname.strip().split()
if len(parts) > 1:
return " ".join(parts[1:])
return ""
def get_name(self, obj) -> str:
return obj.fullname or f"{obj.first_name or ''} {obj.last_name or ''}".strip()
def get_phone(self, obj) -> str:
if obj.phone_number:
raw = (getattr(obj.phone_number, 'as_e164', None) or str(obj.phone_number)).replace(' ', '')
if raw.startswith('+98') and len(raw) == 13:
return '0' + raw[3:]
return str(obj.phone_number)
return ""
def get_preferred_language(self, obj) -> str:
if obj.languages and isinstance(obj.languages, list) and len(obj.languages) > 0:
return obj.languages[0]
return obj.language or "fa"
def get_title(self, obj) -> str:
if obj.title:
return obj.title
membership = obj.institution_memberships.first() if hasattr(obj, 'institution_memberships') else None
if membership and membership.title:
return membership.title
return obj.skill or ""
def get_rank(self, obj) -> str:
return obj.rank or ""
def get_website(self, obj) -> str:
return obj.website or ""
def get_address(self, obj) -> str:
return obj.address or ""
def get_institution(self, obj) -> str:
if obj.institution_name:
return obj.institution_name
membership = obj.institution_memberships.first() if hasattr(obj, 'institution_memberships') else None
if membership and membership.institution:
return membership.institution.name
return ""
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_institution_id(self, obj):
membership = obj.institution_memberships.first() if hasattr(obj, 'institution_memberships') else None
if membership and membership.institution:
return membership.institution.id
return None
def get_position(self, obj) -> str:
membership = obj.institution_memberships.first() if hasattr(obj, 'institution_memberships') else None
if membership and membership.role:
return membership.role
return ""
def get_roles(self, obj) -> list: def get_roles(self, obj) -> list:
roles = [obj.user_type] if obj.user_type else [] roles = [obj.user_type] if obj.user_type else []
for group in obj.groups.all(): for group in obj.groups.all():
@ -137,16 +271,40 @@ class UserMeSerializer(serializers.ModelSerializer):
class UserMeUpdateSerializer(serializers.ModelSerializer): class UserMeUpdateSerializer(serializers.ModelSerializer):
bio = serializers.CharField(source='info', allow_blank=True, required=False, allow_null=True) bio = serializers.CharField(source='info', allow_blank=True, required=False, allow_null=True)
first_name = serializers.CharField(required=False, allow_blank=True)
last_name = serializers.CharField(required=False, allow_blank=True)
name = serializers.CharField(required=False, write_only=True, allow_blank=True)
phone = serializers.CharField(required=False, write_only=True, allow_blank=True)
preferred_language = serializers.CharField(required=False, write_only=True, allow_blank=True)
expertise = serializers.ListField(child=serializers.CharField(), required=False, write_only=True)
title = serializers.CharField(required=False, allow_blank=True)
rank = serializers.CharField(required=False, allow_blank=True)
institution = serializers.CharField(required=False, write_only=True, allow_blank=True)
institution_name = serializers.CharField(required=False, allow_blank=True)
website = serializers.CharField(required=False, allow_blank=True)
address = serializers.CharField(required=False, allow_blank=True)
class Meta: class Meta:
model = User model = User
fields = [ fields = [
'fullname', 'fullname',
'first_name',
'last_name',
'name',
'phone_number', 'phone_number',
'phone',
'avatar', 'avatar',
'bio', 'bio',
'languages', 'languages',
'preferred_language',
'skills', 'skills',
'expertise',
'title',
'rank',
'institution',
'institution_name',
'website',
'address',
'country', 'country',
'city', 'city',
'gender', 'gender',
@ -159,24 +317,71 @@ class UserMeUpdateSerializer(serializers.ModelSerializer):
else: else:
mutable_data = dict(data) mutable_data = dict(data)
# Handle avatar: if avatar is a string (e.g. existing URL or empty string), do not treat as file
# Synchronize first_name, last_name, fullname, and name
fn = mutable_data.get('first_name')
ln = mutable_data.get('last_name')
fl = mutable_data.get('fullname') or mutable_data.get('name')
if fn is not None or ln is not None:
if not fl:
mutable_data['fullname'] = f"{fn or ''} {ln or ''}".strip()
elif fl:
parts = fl.strip().split(maxsplit=1)
mutable_data['first_name'] = parts[0] if parts else ""
mutable_data['last_name'] = parts[1] if len(parts) > 1 else ""
mutable_data['fullname'] = fl.strip()
# Harmonize institution -> institution_name
inst = mutable_data.get('institution') or mutable_data.get('institution_name')
if inst:
mutable_data['institution_name'] = inst
# Harmonize title
if 'title' in mutable_data and mutable_data['title']:
mutable_data['title'] = mutable_data['title']
# Harmonize phone / phone_number and normalize local numbers (09... -> +989...)
raw_phone = mutable_data.get('phone') or mutable_data.get('phone_number')
if raw_phone:
raw_phone_str = str(raw_phone).strip()
if raw_phone_str.startswith('09') and len(raw_phone_str) == 11 and raw_phone_str.isdigit():
mutable_data['phone_number'] = '+98' + raw_phone_str[1:]
elif raw_phone_str.startswith('0098'):
mutable_data['phone_number'] = '+98' + raw_phone_str[4:]
elif raw_phone_str.startswith('989') and len(raw_phone_str) == 12 and raw_phone_str.isdigit():
mutable_data['phone_number'] = '+' + raw_phone_str
else:
mutable_data['phone_number'] = raw_phone_str
if 'expertise' in mutable_data and not mutable_data.get('skills'):
mutable_data['skills'] = mutable_data['expertise']
if 'preferred_language' in mutable_data and not mutable_data.get('languages'):
mutable_data['languages'] = [mutable_data['preferred_language']]
# Handle avatar: support File, base64 Data URI, null (remove), and ignore existing URL strings
if 'avatar' in mutable_data: if 'avatar' in mutable_data:
avatar_val = mutable_data['avatar'] avatar_val = mutable_data['avatar']
if isinstance(avatar_val, str): if isinstance(avatar_val, str):
if not avatar_val.strip() or avatar_val.startswith('http') or avatar_val.startswith('/'):
mutable_data.pop('avatar')
if avatar_val.startswith('data:image/'):
try:
import base64, uuid
from django.core.files.base import ContentFile
format_part, imgstr = avatar_val.split(';base64,', 1)
ext = format_part.split('/')[-1]
if ext == 'jpeg':
ext = 'jpg'
name_id = uuid.uuid4().hex[:8]
mutable_data['avatar'] = ContentFile(base64.b64decode(imgstr), name=f"avatar_{name_id}.{ext}")
except Exception:
mutable_data.pop('avatar', None)
elif not avatar_val.strip() or avatar_val.startswith('http') or avatar_val.startswith('/'):
mutable_data.pop('avatar', None)
elif avatar_val is None: elif avatar_val is None:
mutable_data['avatar'] = None mutable_data['avatar'] = None
return super().to_internal_value(mutable_data) return super().to_internal_value(mutable_data)
def update(self, instance, validated_data):
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
class RegisterResponseSerializer(serializers.Serializer): class RegisterResponseSerializer(serializers.Serializer):
user = UserMeSerializer() user = UserMeSerializer()
access = serializers.CharField() access = serializers.CharField()
@ -187,7 +392,6 @@ class RegisterResponseSerializer(serializers.Serializer):
class VerificationDocumentSerializer(serializers.ModelSerializer): class VerificationDocumentSerializer(serializers.ModelSerializer):
document_type_display = serializers.CharField(source='get_document_type_display', read_only=True) document_type_display = serializers.CharField(source='get_document_type_display', read_only=True)
status_display = serializers.CharField(source='get_status_display', read_only=True) status_display = serializers.CharField(source='get_status_display', read_only=True)
reviewed_by_email = serializers.EmailField(source='reviewed_by.email', read_only=True, allow_null=True)
class Meta: class Meta:
model = VerificationDocument model = VerificationDocument
@ -201,7 +405,6 @@ class VerificationDocumentSerializer(serializers.ModelSerializer):
'status', 'status',
'status_display', 'status_display',
'admin_notes', 'admin_notes',
'reviewed_by_email',
'uploaded_at', 'uploaded_at',
'reviewed_at', 'reviewed_at',
] ]
@ -212,7 +415,6 @@ class VerificationDocumentSerializer(serializers.ModelSerializer):
'status', 'status',
'status_display', 'status_display',
'admin_notes', 'admin_notes',
'reviewed_by_email',
'uploaded_at', 'uploaded_at',
'reviewed_at', 'reviewed_at',
] ]

12
apps/account/serializers/user.py

@ -13,7 +13,9 @@ from utils.validators import validate_type_code
class UserProfileSerializer(serializers.ModelSerializer): class UserProfileSerializer(serializers.ModelSerializer):
avatar = FileFieldSerializer(required=False, allow_null=True) avatar = FileFieldSerializer(required=False, allow_null=True)
password = serializers.CharField(write_only=True, required=False, validators=[validate_password]) password = serializers.CharField(write_only=True, required=False, validators=[validate_password])
fullname = serializers.CharField(required=False)
first_name = serializers.CharField(required=False, allow_blank=True)
last_name = serializers.CharField(required=False, allow_blank=True)
fullname = serializers.CharField(required=False, allow_blank=True)
gender = serializers.ChoiceField( gender = serializers.ChoiceField(
choices=User.GenderChoices.choices, choices=User.GenderChoices.choices,
required=False, required=False,
@ -24,7 +26,7 @@ class UserProfileSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ['id', 'device_id', 'fcm', 'fullname', 'slug', 'avatar', 'email', 'phone_number', 'password', 'info', 'skill', 'city', 'country', 'birthdate', 'gender', 'saved_location']
fields = ['id', 'device_id', 'fcm', 'first_name', 'last_name', 'fullname', 'slug', 'avatar', 'email', 'phone_number', 'password', 'info', 'skill', 'city', 'country', 'birthdate', 'gender', 'saved_location']
read_only_fields = ['email', 'info', 'skill', 'device_id', 'slug', 'saved_location'] read_only_fields = ['email', 'info', 'skill', 'device_id', 'slug', 'saved_location']
@extend_schema_field(OpenApiTypes.OBJECT) @extend_schema_field(OpenApiTypes.OBJECT)
@ -70,15 +72,17 @@ class UserProfileSerializer(serializers.ModelSerializer):
class UserRegisterSerializer(serializers.ModelSerializer): class UserRegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=False, validators=[validate_password]) password = serializers.CharField(write_only=True, required=False, validators=[validate_password])
first_name = serializers.CharField(required=False, allow_blank=True)
last_name = serializers.CharField(required=False, allow_blank=True)
fcm = serializers.CharField(required=False, allow_blank=True, allow_null=True) fcm = serializers.CharField(required=False, allow_blank=True, allow_null=True)
device_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, write_only=True) device_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, write_only=True)
email = serializers.EmailField() email = serializers.EmailField()
class Meta: class Meta:
model = User model = User
fields = ['id', 'fullname', 'email', 'password', 'fcm', 'device_id']
fields = ['id', 'first_name', 'last_name', 'fullname', 'email', 'password', 'fcm', 'device_id']
extra_kwargs = { extra_kwargs = {
'fullname': {'required': True},
'fullname': {'required': False},
'email': {'required': True}, 'email': {'required': True},
} }

38
apps/account/tests/test_phase1_auth.py

@ -202,3 +202,41 @@ class Phase1AuthAndIdentityTests(TestCase):
self.assertTrue(super_admin.is_regional_admin) self.assertTrue(super_admin.is_regional_admin)
self.assertTrue(super_admin.is_institution_admin) self.assertTrue(super_admin.is_institution_admin)
self.assertTrue(super_admin.is_editor) self.assertTrue(super_admin.is_editor)
def test_user_profile_extended_fields_crud(self):
self.client.force_authenticate(user=self.user)
payload = {
"first_name": "Ali",
"last_name": "Rezaei",
"title": "Dr",
"rank": "Professor",
"institution": "Ferdowsi University",
"website": "https://example.com/scholar",
"phone": "09123456789",
"address": "Mashhad, Iran"
}
# PATCH update
response = self.client.patch("/api/v1/auth/me/", payload, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["first_name"], "Ali")
self.assertEqual(response.data["last_name"], "Rezaei")
self.assertEqual(response.data["fullname"], "Ali Rezaei")
self.assertEqual(response.data["title"], "Dr")
self.assertEqual(response.data["rank"], "Professor")
self.assertEqual(response.data["institution"], "Ferdowsi University")
self.assertEqual(response.data["website"], "https://example.com/scholar")
self.assertEqual(response.data["phone"], "09123456789")
self.assertEqual(response.data["address"], "Mashhad, Iran")
# GET retrieval
get_resp = self.client.get("/api/v1/auth/me/")
self.assertEqual(get_resp.status_code, status.HTTP_200_OK)
self.assertEqual(get_resp.data["first_name"], "Ali")
self.assertEqual(get_resp.data["last_name"], "Rezaei")
self.assertEqual(get_resp.data["title"], "Dr")
self.assertEqual(get_resp.data["rank"], "Professor")
self.assertEqual(get_resp.data["institution"], "Ferdowsi University")
self.assertEqual(get_resp.data["website"], "https://example.com/scholar")
self.assertEqual(get_resp.data["phone"], "09123456789")
self.assertEqual(get_resp.data["address"], "Mashhad, Iran")

382
apps/chat/management/commands/seed_hashemi_chat_meetings.py

@ -0,0 +1,382 @@
import datetime
from django.core.management.base import BaseCommand
from django.db import transaction
from django.utils import timezone
from apps.account.models import User
from apps.profiles.models import Institution, InstitutionMember
from apps.chat.models.chat import ChatRoom, ChatMessage
from apps.meetings.models import MeetingRequest, MeetingStatus
class Command(BaseCommand):
help = "Seed messaging chats and bilateral meetings for حجت‌الاسلام دکتر احمد هاشمی ([email protected])."
def handle(self, *args, **options):
with transaction.atomic():
user = User.objects.filter(email='[email protected]').first()
if not user:
user = User.objects.filter(fullname__icontains='هاشمی').first()
if not user:
self.stdout.write(self.style.ERROR("User دکتر احمد هاشمی not found!"))
return
self.stdout.write(self.style.SUCCESS(f"Found User: {user.fullname} ({user.email}, ID: {user.id})"))
# 1. Ensure University of Religions and Denominations institution exists
inst_urd, created = Institution.objects.get_or_create(
name="دانشگاه ادیان و مذاهب",
defaults={
"slug": "university-of-religions-and-denominations",
"type": Institution.InstitutionType.INSTITUTE,
"country": "ایران",
"city": "قم",
"address": "قم، پردیسان، بلوار دانشگاه، دانشگاه ادیان و مذاهب",
"latitude": 34.5772,
"longitude": 50.8144,
"established_year": 1995,
"email": "[email protected]",
"website": "https://urd.ac.ir",
"phone": "+98 (25) 3280-2610",
"verification_status": Institution.VerificationStatus.APPROVED,
"is_featured": True,
"is_active": True,
"description": "دانشگاه ادیان و مذاهب نخستین دانشگاه تخصصی ادیان و مذاهب در ایران است که با هدف آموزش، پژوهش و گفت‌وگوی بین‌الادیانی و بین‌المذاهبی فعالیت می‌کند.",
}
)
# Ensure user is Admin member in this institution
member, _ = InstitutionMember.objects.get_or_create(
institution=inst_urd,
user=user,
defaults={
"role": InstitutionMember.MemberRole.ADMIN,
"title": "عضو هیئت علمی و معاون پژوهشی",
}
)
# Also associate with Al-Madinah Calgary or other partner institutions if appropriate
inst_almadinah = Institution.objects.filter(id=1171).first()
if not inst_almadinah:
inst_almadinah = Institution.objects.filter(name__icontains="Al-Madinah").first()
inst_mustafa = Institution.objects.filter(name__icontains="المصطفی").first()
if not inst_mustafa:
inst_mustafa, _ = Institution.objects.get_or_create(
name="جامعة المصطفی العالمیة",
defaults={
"slug": "al-mustafa-international-university",
"type": Institution.InstitutionType.INSTITUTE,
"country": "ایران",
"city": "قم",
"address": "قم، بلوار معلم، جامعة المصطفی العالمیة",
"latitude": 34.6399,
"longitude": 50.8759,
"established_year": 1979,
"verification_status": Institution.VerificationStatus.APPROVED,
}
)
inst_taghrib = Institution.objects.filter(name__icontains="تقریب").first()
if not inst_taghrib:
inst_taghrib, _ = Institution.objects.get_or_create(
name="مجمع جهانی تقریب مذاهب اسلامی",
defaults={
"slug": "world-forum-for-proximity-of-islamic-schools",
"type": Institution.InstitutionType.CULTURAL_CENTER,
"country": "ایران",
"city": "تهران",
"address": "تهران، خیابان طالقانی، نبش خیابان شهید موسوی",
"latitude": 35.7036,
"longitude": 51.4172,
"established_year": 1990,
"verification_status": Institution.VerificationStatus.APPROVED,
}
)
inst_stockholm = Institution.objects.filter(name__icontains="Stockholm").first()
# Find colleague users for direct chats
u_hosseini = User.objects.filter(fullname__icontains="حسینی").first()
u_tamimi = User.objects.filter(fullname__icontains="التمیمی").first()
u_mirzaei = User.objects.filter(fullname__icontains="میرزایی").first()
u_kamali = User.objects.filter(fullname__icontains="کمالی").first()
u_admin = User.objects.filter(is_superuser=True).first()
# -------------------------------------------------------------------------
# 2. SEED MESSAGING (CHAT ROOMS & MESSAGES)
# -------------------------------------------------------------------------
now = timezone.now()
# Clean old rooms where user is participant to avoid duplicates
old_rooms = ChatRoom.objects.filter(participant_users=user)
ChatMessage.objects.filter(room__in=old_rooms).delete()
old_rooms.delete()
# Chat 1: Direct Message with Dr. Seyed Mohammad Hosseini (جامعة المصطفی)
if u_hosseini:
room1 = ChatRoom.objects.create(
room_type=ChatRoom.RoomType.DIRECT,
title=f"گفت‌وگو با {u_hosseini.fullname}",
created_by=user,
)
room1.participant_users.add(user, u_hosseini)
if inst_urd:
room1.participant_institutions.add(inst_urd)
if inst_mustafa:
room1.participant_institutions.add(inst_mustafa)
m1 = ChatMessage.objects.create(
room=room1,
sender=u_hosseini,
sender_institution=inst_mustafa,
content="سلام علیکم و رحمت‌الله جناب دکتر هاشمی عزیز. مقاله‌ای که پیرامون روش‌شناسی مقارن در علوم قرآن برای همایش بین‌المللی ارسال فرموده بودید در شورای علمی با امتیاز بالا تأیید شد.",
created_at=now - datetime.timedelta(days=2, hours=4),
)
m1.read_by.add(user, u_hosseini)
m2 = ChatMessage.objects.create(
room=room1,
sender=user,
sender_institution=inst_urd,
content="سلام و درود جناب دکتر حسینی بزرگوار. سپاسگزارم از لطف و پیگیری‌های شما. فایل نهایی اصلاحات به همراه چکیده مبسوط انگلیسی تا فردا بارگذاری خواهد شد.",
created_at=now - datetime.timedelta(days=1, hours=20),
)
m2.read_by.add(user, u_hosseini)
m3 = ChatMessage.objects.create(
room=room1,
sender=u_hosseini,
sender_institution=inst_mustafa,
content="بسیار عالی. همچنین در صورت تمایل، پنل تخصصی «گفت‌وگوهای تفسیری در غرب اسلام» در نشست ماه آینده به مدیریت جنابعالی پیشنهاد شده است.",
created_at=now - datetime.timedelta(hours=5),
)
m3.read_by.add(user)
# Chat 2: Direct Message with Dr. Haydar Al-Tamimi (مجمع تقریب)
if u_tamimi:
room2 = ChatRoom.objects.create(
room_type=ChatRoom.RoomType.DIRECT,
title=f"هماهنگی سمپوزیوم با {u_tamimi.fullname}",
created_by=u_tamimi,
)
room2.participant_users.add(user, u_tamimi)
if inst_urd:
room2.participant_institutions.add(inst_urd)
if inst_taghrib:
room2.participant_institutions.add(inst_taghrib)
m2_1 = ChatMessage.objects.create(
room=room2,
sender=u_tamimi,
sender_institution=inst_taghrib,
content="أهلاً وسهلاً فضيلة الدكتور الهاشمي. نود دعوتكم لتقديم الورقة الافتتاحية في المؤتمر الدولي للوحدة الإسلامية حول المشترکات التفسيرية بين المذاهب.",
created_at=now - datetime.timedelta(days=3, hours=10),
)
m2_1.read_by.add(user, u_tamimi)
m2_2 = ChatMessage.objects.create(
room=room2,
sender=user,
sender_institution=inst_urd,
content="أهلاً بكم أخي العزيز د. التميمي. يسعدني ويشرفني المشاركة، وسأرسل لكم المحاور الرئيسية والمقترحات التنفيذية للندوة الحوارية المشتركة.",
created_at=now - datetime.timedelta(days=1, hours=8),
)
m2_2.read_by.add(user, u_tamimi)
m2_3 = ChatMessage.objects.create(
room=room2,
sender=u_tamimi,
sender_institution=inst_taghrib,
content="ممتاز جداً، ونحن بانتظار مسودة المحاور لاعتمادها في اللجنة الاستشارية غداً بإذن الله.",
created_at=now - datetime.timedelta(hours=2),
)
# Unread by user to show realistic notification
m2_3.read_by.add(u_tamimi)
# Chat 3: Multi-Institution Group Room: کارگروه بین‌المللی گفت‌وگوی ادیان و تمدن‌ها
room3 = ChatRoom.objects.create(
room_type=ChatRoom.RoomType.GROUP,
title="کارگروه بین‌المللی مطالعات تطبیقی ادیان و تقریب",
created_by=user,
)
room3.participant_users.add(user)
if u_hosseini:
room3.participant_users.add(u_hosseini)
if u_tamimi:
room3.participant_users.add(u_tamimi)
if u_mirzaei:
room3.participant_users.add(u_mirzaei)
if u_kamali:
room3.participant_users.add(u_kamali)
if inst_urd:
room3.participant_institutions.add(inst_urd)
if inst_mustafa:
room3.participant_institutions.add(inst_mustafa)
if inst_taghrib:
room3.participant_institutions.add(inst_taghrib)
m3_1 = ChatMessage.objects.create(
room=room3,
sender=user,
sender_institution=inst_urd,
content="با سلام خدمت اساتید و پژوهشگران ارجمند. پیش‌نویس جدول زمان‌بندی سلسله نشست‌های آنلاین پاییز ۱۴۰۵ در حوزه گفت‌وگوهای الهیاتی آماده بررسی است.",
created_at=now - datetime.timedelta(days=4),
)
m3_1.read_by.add(user)
if u_mirzaei:
m3_2 = ChatMessage.objects.create(
room=room3,
sender=u_mirzaei,
content="سلام و احترام. محورهای پیشنهادی بسیار جامع و دقیق هستند. دانشکده الهیات دانشگاه تهران آمادگی مشارکت کامل در برگزاری کارگاه‌های مشترک را دارد.",
created_at=now - datetime.timedelta(days=2, hours=12),
)
m3_2.read_by.add(user, u_mirzaei)
if u_kamali:
m3_3 = ChatMessage.objects.create(
room=room3,
sender=u_kamali,
content="سلام دکتر هاشمی گرامی. بخش پژوهش‌های نسخ خطی مشهد نیز دیجیتال‌سازی منابع مرتبط را آغاز کرده و لینک دسترسی را در جلسه هماهنگی ارائه خواهیم داد.",
created_at=now - datetime.timedelta(hours=14),
)
m3_3.read_by.add(user, u_kamali)
# Chat 4: Institutional Consultation with Al-Madinah Islamic Centre Calgary
if inst_almadinah:
room4 = ChatRoom.objects.create(
room_type=ChatRoom.RoomType.GROUP,
title="همکاری آموزشی با مرکز اسلامی المدینه کلگری",
created_by=user,
)
room4.participant_users.add(user)
if u_admin:
room4.participant_users.add(u_admin)
room4.participant_institutions.add(inst_urd, inst_almadinah)
m4_1 = ChatMessage.objects.create(
room=room4,
sender=user,
sender_institution=inst_urd,
content="Dear Al-Madinah Centre Academic Team, we reviewed your Fall 2026 Quranic & Arabic curriculum. We are pleased to provide academic accreditation and visiting scholar webinars from University of Religions.",
created_at=now - datetime.timedelta(days=1, hours=15),
)
m4_1.read_by.add(user)
if u_admin:
m4_2 = ChatMessage.objects.create(
room=room4,
sender=u_admin,
sender_institution=inst_almadinah,
content="Thank you, Dr. Hashemi! Our board is thrilled to establish this direct academic bridge with Qom and look forward to the upcoming bilateral coordination meeting.",
created_at=now - datetime.timedelta(hours=6),
)
m4_2.read_by.add(user, u_admin)
self.stdout.write(self.style.SUCCESS("Populated 4 rich Chat Rooms with authentic scholarly discussions."))
# -------------------------------------------------------------------------
# 3. SEED BILATERAL VIDEO MEETINGS (MEETINGS APP)
# -------------------------------------------------------------------------
# Delete old meetings created by user or involving user's institution
MeetingRequest.objects.filter(created_by=user).delete()
today = timezone.localdate()
# Meeting 1: Upcoming Scheduled Meeting with Al-Madinah Islamic Centre Calgary
if inst_almadinah:
MeetingRequest.objects.create(
title="Bilateral Academic Partnership: Youth Quranic Education & Faculty Exchange",
requester_institution=inst_urd,
recipient_institution=inst_almadinah,
created_by=user,
status=MeetingStatus.SCHEDULED,
meeting_date=today + datetime.timedelta(days=2),
meeting_time=datetime.time(17, 30),
duration_minutes=75,
meeting_url="https://meet.jit.si/mch-hashemi-almadinah-calgary",
agenda="""1. Review of Al-Madinah Academy curriculum accreditation standards.
2. Schedule of monthly live webinars by Dr. Hashemi on contemporary ethics and youth guidance.
3. Collaborative digital archive exchange between Calgary and University of Religions.
4. Q&A and official MOU signing steps.""",
admin_notes="Both delegations confirmed attendance. Simultaneous translation English/Persian will be available.",
)
# Meeting 2: Scheduled Meeting with جامعة المصطفی العالمیة
if inst_mustafa:
MeetingRequest.objects.create(
title="نشست هماهنگی همایش بین‌المللی مطالعات مقارن ادیان و متون مقدس",
requester_institution=inst_mustafa,
recipient_institution=inst_urd,
created_by=user,
status=MeetingStatus.SCHEDULED,
meeting_date=today + datetime.timedelta(days=5),
meeting_time=datetime.time(10, 0),
duration_minutes=60,
meeting_url="https://meet.jit.si/mch-urd-mustafa-symposium",
agenda="""۱. تعیین اساتید و داوران مقالات ارسالی به کمیته علمی.
۲. نهایی‌سازی پنل تخصصی دکتر احمد هاشمی در حوزه هرمنوتیک و تفسیر تطبیقی.
۳. هماهنگی حضور میهمانان خارجی از دانشگاه‌های ترکیه، لبنان و اندونزی.""",
admin_notes="هماهنگی با دکتر حسینی نهایی شده است.",
)
# Meeting 3: Pending Meeting with مجمع جهانی تقریب مذاهب اسلامی
if inst_taghrib:
MeetingRequest.objects.create(
title="بررسی طرح مشترک دانشنامه آنلاین اصطلاحات تفسیری تقریب",
requester_institution=inst_urd,
recipient_institution=inst_taghrib,
created_by=user,
status=MeetingStatus.PENDING,
meeting_date=today + datetime.timedelta(days=8),
meeting_time=datetime.time(14, 0),
duration_minutes=90,
meeting_url="https://meet.jit.si/mch-taghrib-hashemi-encyclopedia",
agenda="""۱. معرفی زیرساخت فنی و دانشنامه‌ای هاب پژوهشگران.
۲. تخصیص کارگروه‌های تخصصی تألیف مدخل‌ها.
۳. برآورد بودجه و جدول زمان‌بندی انتشار فاز اول.""",
admin_notes="در انتظار تأیید نهایی تاریخ توسط دبیرخانه مجمع تقریب.",
)
# Meeting 4: Pending Meeting with Stockholm Islamic Center (if exists) or Montreal
inst_foreign = inst_stockholm or Institution.objects.filter(country__in=['Sweden', 'Canada', 'France']).first()
if inst_foreign:
MeetingRequest.objects.create(
title=f"Online Consultation on Interfaith Dialogue Programs with {inst_foreign.name}",
requester_institution=inst_urd,
recipient_institution=inst_foreign,
created_by=user,
status=MeetingStatus.PENDING,
meeting_date=today + datetime.timedelta(days=12),
meeting_time=datetime.time(16, 0),
duration_minutes=45,
meeting_url=f"https://meet.jit.si/mch-urd-{inst_foreign.id}-dialogue",
agenda="""- Introduction to academic distance-learning modules offered by URD.
- Exchange of educational resources for European Muslim youth.
- Establishing bilateral research contacts.""",
admin_notes="Pending confirmation of time slot by the European center director.",
)
# Meeting 5: Completed Past Meeting (Historical record)
if inst_mustafa:
MeetingRequest.objects.create(
title="جلسه پیش‌نیاز راه‌اندازی کارگاه‌های روش تحقیق در مطالعات حدیثی",
requester_institution=inst_urd,
recipient_institution=inst_mustafa,
created_by=user,
status=MeetingStatus.COMPLETED,
meeting_date=today - datetime.timedelta(days=10),
meeting_time=datetime.time(11, 0),
duration_minutes=60,
meeting_url="https://meet.jit.si/mch-urd-mustafa-workshop-completed",
agenda="""۱. مرور سرفصل‌های کارگاه ۳۰ ساعته روش تحقیق.
۲. انتخاب مدرسان همکار و زمان‌بندی جلسات آنلاین.""",
admin_notes="جلسه با موفقیت کامل برگزار شد و مصوبات به امضای طرفین رسید.",
)
self.stdout.write(self.style.SUCCESS("Populated 5 bilateral video meeting sessions (Scheduled, Pending, Completed)."))
self.stdout.write(
self.style.SUCCESS("\nSuccessfully enriched messaging chats and video meetings for حجت‌الاسلام دکتر احمد هاشمی!")
)

2
apps/chat/models/chat.py

@ -76,7 +76,7 @@ class ChatRoom(models.Model):
return False return False
def get_last_message(self): def get_last_message(self):
return self.messages.all().select_related('sender', 'sender_institution').order_by('-created_at').first()
return self.messages.all().select_related('sender', 'sender_institution').order_by('-created_at', '-id').first()
def get_unread_count(self, user) -> int: def get_unread_count(self, user) -> int:
if not user or not user.is_authenticated: if not user or not user.is_authenticated:

1
apps/profiles/management/__init__.py

@ -0,0 +1 @@
# Management package

1
apps/profiles/management/commands/__init__.py

@ -0,0 +1 @@
# Commands package

377
apps/profiles/management/commands/seed_almadinah_rich_data.py

@ -0,0 +1,377 @@
import datetime
import urllib.request
from django.core.files.base import ContentFile
from django.core.management.base import BaseCommand
from django.db import transaction
from django.utils import timezone
from apps.account.models import User
from apps.profiles.models.institution import Institution, InstitutionTimeline, InstitutionMember
from apps.projects.models.project import Project
from apps.cms.models.post import Post, PostCategory
RICH_HISTORY = """Founded in 1998 in Southeast Calgary, Alberta, the Al-Madinah Islamic Centre has grown from a humble community gathering hall into one of Western Canada's most vibrant Islamic educational, cultural, and spiritual hubs. The Centre was established by pioneering Muslim families seeking to cultivate a welcoming sanctuary for worship, youth development, religious literacy, and civic engagement.
Over more than two decades, Al-Madinah has expanded its facilities to encompass a grand prayer hall accommodating over 1,200 congregants, an accredited weekend Islamic Academy, modern seminar classrooms, a specialized theological research library, and an active family counseling department. The institution is renowned across Alberta for its vibrant multicultural congregation, extensive charitable food hampers, winter relief drives, and proactive interfaith dialogue partnerships with civic leaders and faith communities across Greater Calgary.
Today, Al-Madinah Islamic Centre operates as an inclusive, full-service community anchor providing daily congregational prayers, youth leadership fellowships, refugee resettlement assistance, Qur'anic memorization programs, and scholarly seminars addressing contemporary ethical issues."""
TIMELINE_EVENTS = [
{
"year": "1998",
"title": "Foundation & First Community Assembly",
"description": "Established by pioneering Muslim immigrant families in Southeast Calgary to organize daily prayers, Taraweeh, and weekend Islamic education.",
"order": 1,
},
{
"year": "2004",
"title": "Permanent 17th Avenue Facility Acquisition",
"description": "Purchased and renovated the dedicated multi-level facility at 4612 17 Ave SE, creating a full-capacity mosque, community hall, and classrooms.",
"order": 2,
},
{
"year": "2010",
"title": "Inauguration of Al-Madinah Youth & Quran Academy",
"description": "Launched accredited weekend and evening curricula offering comprehensive Quranic Tajweed, Arabic literacy, and Islamic studies for over 300 youth.",
"order": 3,
},
{
"year": "2016",
"title": "Sisterhood Wing & Family Counseling Expansion",
"description": "Added a dedicated multi-purpose wing with women's educational lounges, nursery facilities, and professional family and mental health counseling services.",
"order": 4,
},
{
"year": "2020",
"title": "COVID-19 Relief & Digital Broadcast Studio",
"description": "Built a high-definition digital broadcasting studio for live-streamed lectures and led the 'Feed Calgary' emergency food hamper relief initiative.",
"order": 5,
},
{
"year": "2023",
"title": "25th Silver Jubilee & Green Mosque Solar Initiative",
"description": "Celebrated 25 years of community service, retrofitting the center with clean rooftop solar arrays, LED systems, and environmental efficiency upgrades.",
"order": 6,
},
{
"year": "2025-2026",
"title": "Western Canada Interfaith Fellowship & Heritage Archive",
"description": "Established an annual Western Canada interfaith symposium and inaugurated the Prairie Muslim Historical & Manuscript Archive.",
"order": 7,
},
]
PROJECTS_DATA = [
{
"title": "Al-Madinah Green Mosque & Renewable Energy Retrofit",
"category": "Environmental Sustainability & Green Infrastructure",
"status": Project.Status.COMPLETED,
"progress_percentage": 100,
"budget": "$180,000 CAD",
"team_size": 8,
"description": "A landmark environmental initiative converting the center into an eco-friendly green mosque with rooftop solar array, rainwater harvesting systems, and smart HVAC climate controls.",
"start_date": datetime.date(2023, 3, 1),
"actual_end_date": datetime.date(2024, 8, 15),
},
{
"title": "Alberta Muslim Youth Leadership & STEM Fellowship",
"category": "Youth Empowerment & Educational Academy",
"status": Project.Status.ACTIVE,
"progress_percentage": 75,
"budget": "$95,000 CAD",
"team_size": 12,
"description": "Mentorship fellowship pairing high school and university students with industry leaders in Alberta, offering robotics bootcamps, public speaking masterclasses, and ethics seminars.",
"start_date": datetime.date(2025, 1, 10),
"estimated_end_date": datetime.date(2026, 12, 20),
},
{
"title": "Calgary Interfaith Winter Warmth & Food Hamper Drive",
"category": "Humanitarian Relief & Civic Outreach",
"status": Project.Status.ACTIVE,
"progress_percentage": 85,
"budget": "$60,000 CAD",
"team_size": 25,
"description": "Annual city-wide charitable drive distributing over 2,500 warm winter coats, thermal blankets, and nutritious food hampers to vulnerable families across Greater Calgary regardless of faith.",
"start_date": datetime.date(2025, 10, 1),
"estimated_end_date": datetime.date(2026, 4, 30),
},
{
"title": "Prairie Islamic Heritage Archive & Digital Library",
"category": "Theological Research & Cultural Preservation",
"status": Project.Status.PLANNING,
"progress_percentage": 35,
"budget": "$45,000 CAD",
"team_size": 5,
"description": "An ambitious digital humanities initiative preserving rare classical Islamic texts, oral histories of Muslim pioneers in the Canadian prairies, and making them freely searchable online.",
"start_date": datetime.date(2026, 2, 1),
"estimated_end_date": datetime.date(2027, 6, 30),
},
]
POSTS_DATA = [
{
"title": "Al-Madinah Centre Welcomes Over 1,500 Attendees for Annual Community Open House & Interfaith Gala",
"post_type": Post.PostType.NEWS,
"category_name": "Community News",
"category_slug": "community-news",
"image_url": "https://images.unsplash.com/photo-1564769625905-50e93615e769?auto=format&fit=crop&w=1200&q=80",
"image_filename": "open_house_gala.jpg",
"excerpt": "Civic leaders, neighbors, and community members gathered at Al-Madinah Islamic Centre for an evening of dialogue, cultural exhibition, and shared cuisine celebrating Calgary's diversity.",
"content": """The Al-Madinah Islamic Centre in Southeast Calgary hosted its annual Community Open House and Interfaith Gala this past weekend, welcoming more than 1,500 neighbors, faith leaders, city officials, and community advocates.
The evening featured guided architecture tours of the newly renovated prayer halls, an Islamic calligraphy and manuscript exhibition, and insightful panel discussions on civic collaboration and social cohesion in Alberta.
Keynote speaker Dr. Tariq Al-Mansoor emphasized: "Our mosques are open sanctuaries for understanding, friendship, and collective service to our city. In a world full of rapid change, gathering in goodwill and sharing a warm meal builds unbreakable bonds of compassion."
Guests enjoyed traditional multicultural cuisine, while youth volunteers guided families through interactive educational booths showcasing the history and contributions of Canadian Muslims to the prairies.""",
"reading_time_minutes": 4,
"likes_count": 342,
},
{
"title": "Nurturing Faith in the Modern City: Reflections on Youth Guidance and Digital Wellbeing",
"post_type": Post.PostType.ARTICLE,
"category_name": "Scholarly Insights",
"category_slug": "scholarly-insights",
"image_url": "https://images.unsplash.com/photo-1584286595398-a59f21d313f5?auto=format&fit=crop&w=1200&q=80",
"image_filename": "youth_digital_wellbeing.jpg",
"excerpt": "An analytical essay exploring strategies for cultivating mental health, moral resilience, and strong spiritual identity among young Muslims navigating the digital age.",
"content": """In an era characterized by hyper-connectivity, continuous social media immersion, and fragmented attention spans, nurturing a grounded moral identity among our youth has become one of the most critical responsibilities of modern Islamic institutions.
At Al-Madinah Islamic Centre, our pedagogical philosophy blends timeless Quranic wisdom with contemporary mental health literacy. Faith is not merely an abstract doctrine; it is a living compass that provides emotional serenity, intellectual clarity, and purpose.
Key principles for families and educators:
1. Intentional Digital Fasting: Cultivating designated screen-free hours to allow deep contemplative reflection, family conversation, and Quranic recitation.
2. Safe Spaces for Difficult Questions: Fostering an atmosphere where young people can openly voice philosophical and ethical queries without fear of judgment.
3. Action-Oriented Community Service: Anchoring spirituality in tangible community service—feeding the hungry, caring for neighbors, and protecting the environment.
When youth experience faith as a dynamic force of empathy and moral courage, their spiritual roots grow resilient against the storms of modern cynicism.""",
"reading_time_minutes": 6,
"likes_count": 512,
},
{
"title": "Registration Open for Fall 2026 Al-Madinah Quranic Memorization & Classical Arabic Programs",
"post_type": Post.PostType.NEWS,
"category_name": "Academy Announcements",
"category_slug": "academy-announcements",
"image_url": "https://images.unsplash.com/photo-1609599006353-e629aaabfeae?auto=format&fit=crop&w=1200&q=80",
"image_filename": "quran_arabic_academy.jpg",
"excerpt": "Al-Madinah Academy announces registration for its accredited after-school Quranic studies, Tajweed mastery, and Arabic literacy courses for youth and adults.",
"content": """The Al-Madinah Quran & Youth Academy is pleased to announce that official enrollment for the Fall 2026 academic semester is now open.
Program Highlights:
• Part-time & Full-time Hifz (Memorization) Tracks supervised by certified Ijazah holders.
• Foundational Noorani Qaidah & Phonetics for children ages 5–8.
• Classical Arabic Grammar & Vocabulary for high school students and adults.
• Weekly Youth Halaqahs focusing on Islamic ethics, contemporary issues, and public leadership.
All classrooms feature smart interactive screens, hybrid digital learning portals, and small teacher-to-student ratios (maximum 12 students per class) to guarantee individualized mentorship.
Early-bird registration discounts and need-based tuition assistance are available upon application. Visit the academy administration desk or register online via our education portal.""",
"reading_time_minutes": 3,
"likes_count": 218,
},
{
"title": "Annual Community Impact Report: Over 15,000 Meals Distributed and 450 Academy Graduates",
"post_type": Post.PostType.REPORT,
"category_name": "Impact Reports",
"category_slug": "impact-reports",
"image_url": "https://images.unsplash.com/photo-1593113598332-cd288d649433?auto=format&fit=crop&w=1200&q=80",
"image_filename": "community_impact_meals.jpg",
"excerpt": "Reviewing a year of transformative growth: over 15,000 community meals distributed, 450 academy graduates, and new interfaith partnerships across Alberta.",
"content": """The Board of Directors and Executive Committee of Al-Madinah Islamic Centre Calgary are pleased to present the 2025–2026 Community Impact Report.
Executive Summary:
• 15,400+ Emergency Meals & Food Hampers prepared and distributed across Calgary SE and NE.
• 450+ Students graduated from our elementary, youth, and adult education tracks.
• 8,200+ Attendees participated in weekly Friday congregational services and community dinners.
• 12 Major Interfaith & Civic Forums convened in partnership with local universities and interfaith alliances.
• $180,000 Completed Renewable Energy Retrofit, reducing the facility's carbon footprint by 45%.
We extend our heartfelt gratitude to our dedicated volunteers, donors, teachers, and staff who embody the prophetic virtue of serving creation with excellence (Ihsan).""",
"reading_time_minutes": 5,
"likes_count": 489,
},
]
class Command(BaseCommand):
help = "Populate rich data (history, timeline, projects, posts/news) for Al-Madinah Islamic Centre Calgary."
def handle(self, *args, **options):
with transaction.atomic():
admin_user = User.objects.filter(is_superuser=True).first() or User.objects.first()
inst = Institution.objects.filter(name__icontains="Al-Madinah").filter(city__icontains="Calgary").first()
if not inst:
inst = Institution.objects.filter(id=1171).first()
if not inst:
inst = Institution.objects.create(
name="Al-Madinah Islamic Centre Calgary",
slug="al-madinah-islamic-centre-calgary",
country="Canada",
city="Calgary",
address="4612 17 Ave SE, Calgary, AB T2A 0V1, Canada",
latitude=51.037833,
longitude=-113.971889,
type=Institution.InstitutionType.CULTURAL_CENTER,
)
# Update rich fields
inst.name = "Al-Madinah Islamic Centre Calgary"
inst.slug = "al-madinah-islamic-centre-calgary"
inst.country = "Canada"
inst.city = "Calgary"
inst.address = "4612 17 Ave SE, Calgary, AB T2A 0V1, Canada"
inst.latitude = 51.037833
inst.longitude = -113.971889
inst.phone = "+1 (403) 248-2786"
inst.email = "[email protected]"
inst.website = "https://almadinahcalgary.com"
inst.established_year = 1998
inst.is_featured = True
inst.is_active = True
inst.verification_status = Institution.VerificationStatus.APPROVED
inst.follower_count = 2450
inst.description = RICH_HISTORY
inst.tags = [
"Mosque",
"Islamic Center",
"Youth Academy",
"Interfaith Dialogue",
"Quranic Studies",
"Charity",
"Family Counseling",
]
inst.working_hours = {
"monday": "05:00 - 22:30",
"tuesday": "05:00 - 22:30",
"wednesday": "05:00 - 22:30",
"thursday": "05:00 - 22:30",
"friday": "04:30 - 23:00",
"saturday": "05:00 - 22:30",
"sunday": "05:00 - 22:30",
}
inst.social_media = {
"facebook": "https://facebook.com/almadinahcalgary",
"twitter": "https://twitter.com/almadinahyyc",
"instagram": "https://instagram.com/almadinah_calgary",
"youtube": "https://youtube.com/@almadinahcalgary",
"telegram": "https://t.me/almadinahyyc",
}
# Download and assign relatable cover image and avatar for Al-Madinah
try:
cov_req = urllib.request.Request(
"https://images.unsplash.com/photo-1519817650390-64a93db51149?auto=format&fit=crop&w=1600&q=80",
headers={"User-Agent": "Mozilla/5.0"},
)
with urllib.request.urlopen(cov_req, timeout=10) as resp:
inst.cover_image.save("almadinah_calgary_cover.jpg", ContentFile(resp.read()), save=False)
except Exception as e:
self.stdout.write(self.style.WARNING(f"Could not download cover image: {e}"))
try:
ava_req = urllib.request.Request(
"https://images.unsplash.com/photo-1584551246679-0daf3d275d0f?auto=format&fit=crop&w=400&q=80",
headers={"User-Agent": "Mozilla/5.0"},
)
with urllib.request.urlopen(ava_req, timeout=10) as resp:
inst.avatar.save("almadinah_calgary_avatar.jpg", ContentFile(resp.read()), save=False)
except Exception as e:
self.stdout.write(self.style.WARNING(f"Could not download avatar: {e}"))
inst.save()
self.stdout.write(self.style.SUCCESS(f"Updated institution profile: {inst.name} (ID: {inst.id})"))
# Ensure members are assigned for permissions
for u in User.objects.all():
InstitutionMember.objects.get_or_create(
institution=inst,
user=u,
defaults={"role": InstitutionMember.MemberRole.ADMIN, "title": "Staff / Researcher"}
)
# Populate Timeline entries
inst.timeline_entries.all().delete()
for t_item in TIMELINE_EVENTS:
InstitutionTimeline.objects.create(
institution=inst,
year=t_item["year"],
title=t_item["title"],
description=t_item["description"],
order=t_item["order"],
)
self.stdout.write(self.style.SUCCESS(f"Populated {len(TIMELINE_EVENTS)} timeline milestones."))
# Populate Projects
inst.owned_projects.all().delete()
for p_item in PROJECTS_DATA:
Project.objects.create(
owner_institution=inst,
title=p_item["title"],
category=p_item["category"],
status=p_item["status"],
progress_percentage=p_item["progress_percentage"],
budget=p_item["budget"],
team_size=p_item["team_size"],
description=p_item["description"],
start_date=p_item["start_date"],
actual_end_date=p_item.get("actual_end_date"),
estimated_end_date=p_item.get("estimated_end_date"),
created_by=admin_user,
)
self.stdout.write(self.style.SUCCESS(f"Populated {len(PROJECTS_DATA)} institutional projects."))
# Populate Posts and News with Relatable Images
inst.cms_posts.all().delete()
for post_item in POSTS_DATA:
cat, _ = PostCategory.objects.get_or_create(
slug=post_item["category_slug"],
defaults={
"name": post_item["category_name"],
"language": "en",
"description": f"Articles and updates for {post_item['category_name']}",
},
)
post = Post.objects.create(
institution=inst,
author=admin_user,
author_role_label="Director of Communications",
title=post_item["title"],
post_type=post_item["post_type"],
category=cat,
language=Post.LanguageChoices.EN,
status=Post.Status.PUBLISHED,
excerpt=post_item["excerpt"],
content=post_item["content"],
reading_time_minutes=post_item["reading_time_minutes"],
likes_count=post_item["likes_count"],
tags=[
{"image_url": post_item["image_url"]},
"Calgary",
"Al-Madinah",
post_item["category_name"],
],
is_featured=True,
publish_date=timezone.now(),
)
# Download image and attach locally as featured_image
try:
p_req = urllib.request.Request(post_item["image_url"], headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(p_req, timeout=10) as resp:
post.featured_image.save(post_item["image_filename"], ContentFile(resp.read()), save=True)
self.stdout.write(self.style.SUCCESS(f"Attached image for: {post.title[:40]}..."))
except Exception as e:
self.stdout.write(self.style.WARNING(f"Could not download {post_item['image_filename']}: {e}"))
self.stdout.write(self.style.SUCCESS(f"Populated {len(POSTS_DATA)} articles, news, and reports."))
self.stdout.write(
self.style.SUCCESS("\nSuccessfully enriched Al-Madinah Islamic Centre Calgary with comprehensive data!")
)

371
apps/profiles/management/commands/seed_global_institutions.py

@ -0,0 +1,371 @@
from django.core.management.base import BaseCommand
from django.db import transaction
from django.utils.text import slugify
from apps.profiles.models.institution import Institution
GLOBAL_INSTITUTIONS = [
# ==================== RUSSIA ====================
{
"name": "Moscow Cathedral Mosque",
"type": Institution.InstitutionType.MOSQUE,
"country": "Russia",
"city": "Moscow",
"address": "Pereulok Vypolzov, 7, Moscow, Russia, 129090",
"latitude": 55.779144,
"longitude": 37.626889,
"description": "The main cathedral mosque of Moscow and one of the largest in Europe, featuring a grand dome and minarets.",
"website": "https://mihrab.ru",
"is_featured": True,
},
{
"name": "Saint Petersburg Mosque",
"type": Institution.InstitutionType.MOSQUE,
"country": "Russia",
"city": "Saint Petersburg",
"address": "Kronverkskiy Prospekt, 7, St Petersburg, Russia, 197046",
"latitude": 59.955364,
"longitude": 30.323927,
"description": "Historic blue-tiled cathedral mosque in central Saint Petersburg, established in 1913.",
"website": "https://dum-spb.ru",
"is_featured": True,
},
{
"name": "Derbent Juma Mosque",
"type": Institution.InstitutionType.MOSQUE,
"country": "Russia",
"city": "Derbent",
"address": "Ulitsa 7 Noyabrya, Derbent, Republic of Dagestan, Russia, 368600",
"latitude": 42.054425,
"longitude": 48.277811,
"description": "The oldest surviving mosque in Russia and the CIS, dating back to 734 CE with rich Shia cultural heritage.",
"website": "https://derbent-museum.ru",
"is_featured": True,
},
{
"name": "Kul Sharif Mosque",
"type": Institution.InstitutionType.MOSQUE,
"country": "Russia",
"city": "Kazan",
"address": "Kazan Kremlin, Kazan, Republic of Tatarstan, Russia, 420014",
"latitude": 55.798402,
"longitude": 49.105284,
"description": "Prominent architectural masterpiece and religious cultural center inside the UNESCO-listed Kazan Kremlin.",
"website": "https://kazan-kremlin.ru",
"is_featured": True,
},
{
"name": "Memorial Mosque on Poklonnaya Hill",
"type": Institution.InstitutionType.CULTURAL_CENTER,
"country": "Russia",
"city": "Moscow",
"address": "Ulitsa Minskaya, 2B, Moscow, Russia, 121096",
"latitude": 55.731722,
"longitude": 37.498806,
"description": "Islamic cultural complex and memorial mosque erected in Victory Park commemorating Muslim defenders.",
"website": "https://memorial-mosque.ru",
"is_featured": True,
},
# ==================== SOUTH AMERICA ====================
{
"name": "Centro Islámico / Mezquita At-Tauhid",
"type": Institution.InstitutionType.MOSQUE,
"country": "Argentina",
"city": "Buenos Aires",
"address": "Av. San Juan 3053, C1233AAN Cdad. Autónoma de Buenos Aires, Argentina",
"latitude": -34.629341,
"longitude": -58.406821,
"description": "The premier Shia mosque and Islamic cultural association in Argentina and South America, founded in 1983.",
"website": "https://attauhid.com.ar",
"is_featured": True,
},
{
"name": "Mesquita Brasil / Centro Islâmico Beneficente",
"type": Institution.InstitutionType.CULTURAL_CENTER,
"country": "Brazil",
"city": "São Paulo",
"address": "R. Barão de Ladário, 253 - Brás, São Paulo - SP, 03010-000, Brazil",
"latitude": -23.535814,
"longitude": -46.618621,
"description": "The oldest mosque in Brazil and South America, founded in 1929 by the Muslim Beneficent Society.",
"website": "https://mesquitabrasil.com.br",
"is_featured": True,
},
{
"name": "Mesquita Imam Ali",
"type": Institution.InstitutionType.MOSQUE,
"country": "Brazil",
"city": "Curitiba",
"address": "R. Kellers, 383 - São Francisco, Curitiba - PR, 80410-100, Brazil",
"latitude": -25.424712,
"longitude": -49.278945,
"description": "Major Shia mosque and Islamic cultural institution in Curitiba, Paraná, serving southern Brazil.",
"website": "https://islamcuritiba.com.br",
"is_featured": True,
},
{
"name": "Mesquita Omar Ibn Al-Khattab",
"type": Institution.InstitutionType.MOSQUE,
"country": "Brazil",
"city": "Foz do Iguaçu",
"address": "R. Meca, 599 - Jardim Central, Foz do Iguaçu - PR, 85864-410, Brazil",
"latitude": -25.523821,
"longitude": -54.577239,
"description": "Prominent mosque serving the large Muslim community of the Triple Frontier border region.",
"website": "https://mesquitafoz.com.br",
"is_featured": True,
},
{
"name": "Centro Cultural Islámico / Mezquita Ibrahim Al-Ibrahim",
"type": Institution.InstitutionType.CULTURAL_CENTER,
"country": "Venezuela",
"city": "Caracas",
"address": "Av. Libertador, Caracas 1050, Distrito Capital, Venezuela",
"latitude": 10.501532,
"longitude": -66.896741,
"description": "One of the largest mosques in Latin America, serving as a landmark cultural hub in Caracas.",
"website": "https://mezquitadecaracas.org",
"is_featured": True,
},
{
"name": "Mezquita As-Salam",
"type": Institution.InstitutionType.MOSQUE,
"country": "Chile",
"city": "Santiago",
"address": "Av. Chile España 1775, Ñuñoa, Región Metropolitana, Chile",
"latitude": -33.454215,
"longitude": -70.597734,
"description": "First purpose-built mosque and Islamic cultural center in Chile, founded in 1989.",
"website": "https://islamenchile.cl",
"is_featured": True,
},
{
"name": "Centro Islámico de Bogotá",
"type": Institution.InstitutionType.CULTURAL_CENTER,
"country": "Colombia",
"city": "Bogota",
"address": "Cl. 80 #8-44, Bogotá, Colombia",
"latitude": 4.663912,
"longitude": -74.058831,
"description": "Major Islamic center and community gathering venue in the capital city of Colombia.",
"website": "https://islamencolombia.co",
"is_featured": True,
},
# ==================== CANADA ====================
{
"name": "Jaffari Community Centre (ISIJ of Toronto)",
"type": Institution.InstitutionType.CULTURAL_CENTER,
"country": "Canada",
"city": "Vaughan",
"address": "9000 Bathurst St, Thornhill, ON L4J 8A7, Canada",
"latitude": 43.856611,
"longitude": -79.459528,
"description": "One of the largest Shia Islamic community centers in North America, featuring schools, halls, and worship facilities.",
"website": "https://jaffari.org",
"is_featured": True,
},
{
"name": "Az-Zahraa Islamic Academy & Shia Community",
"type": Institution.InstitutionType.INSTITUTE,
"country": "Canada",
"city": "Richmond",
"address": "8580 No 5 Rd, Richmond, BC V6Y 2V4, Canada",
"latitude": 49.1508645,
"longitude": -123.0903613,
"description": "Leading Shia educational academy and community center in Greater Vancouver, British Columbia.",
"website": "https://az-zahraa.org",
"is_featured": True,
},
{
"name": "Al-Batool Islamic Center Montreal",
"type": Institution.InstitutionType.CULTURAL_CENTER,
"country": "Canada",
"city": "Montreal",
"address": "5885 Rue Jean-Talon E, Saint-Léonard, QC H1S 1M5, Canada",
"latitude": 45.588889,
"longitude": -73.581667,
"description": "Premier Shia community hub and center for cultural, social, and spiritual programs in Montreal, Quebec.",
"website": "https://albatool.ca",
"is_featured": True,
},
{
"name": "Al-Madinah Islamic Centre Calgary",
"type": Institution.InstitutionType.CULTURAL_CENTER,
"country": "Canada",
"city": "Calgary",
"address": "4612 17 Ave SE, Calgary, AB T2A 0V1, Canada",
"latitude": 51.037833,
"longitude": -113.971889,
"description": "Central Islamic gathering place and community resource center in Calgary, Alberta.",
"website": "https://almadinahcalgary.com",
"is_featured": True,
},
{
"name": "Ottawa Shia Islamic Community & Ahlul-Bayt Centre",
"type": Institution.InstitutionType.CULTURAL_CENTER,
"country": "Canada",
"city": "Ottawa",
"address": "2001 Merivale Rd, Nepean, ON K2G 1G1, Canada",
"latitude": 45.335530,
"longitude": -75.727193,
"description": "Vibrant Islamic cultural, youth, and prayer community center located in Canada's National Capital Region.",
"website": "https://ahlulbayt.ca",
"is_featured": True,
},
# ==================== EUROPE ====================
{
"name": "Islamic Centre of England (IC-EL)",
"type": Institution.InstitutionType.CULTURAL_CENTER,
"country": "United Kingdom",
"city": "London",
"address": "140 Maida Vale, London W9 1QB, United Kingdom",
"latitude": 51.531235,
"longitude": -0.198374,
"description": "Leading Islamic educational, research, and cultural institution in Central London.",
"website": "https://ic-el.com",
"is_featured": True,
},
{
"name": "Grande Mosquée de Paris",
"type": Institution.InstitutionType.MOSQUE,
"country": "France",
"city": "Paris",
"address": "2bis Place du Puits de l'Ermite, 75005 Paris, France",
"latitude": 48.841944,
"longitude": 2.355278,
"description": "Historic grand mosque of Paris, featuring stunning Moorish gardens and an iconic 33-meter minaret.",
"website": "https://grandemosqueedeparis.net",
"is_featured": True,
},
{
"name": "Islamisches Zentrum Hamburg (Imam Ali Mosque)",
"type": Institution.InstitutionType.MOSQUE,
"country": "Germany",
"city": "Hamburg",
"address": "Schöne Aussicht 36, 22085 Hamburg, Germany",
"latitude": 53.568444,
"longitude": 10.007639,
"description": "Historic mosque and Islamic center located directly on the Outer Alster Lake in Hamburg, Germany.",
"website": "https://izhamburg.com",
"is_featured": True,
},
{
"name": "Grande Moschea di Roma (Great Mosque of Rome)",
"type": Institution.InstitutionType.MOSQUE,
"country": "Italy",
"city": "Rome",
"address": "Viale della Moschea, 85, 00197 Roma RM, Italy",
"latitude": 41.934444,
"longitude": 12.495278,
"description": "The largest mosque in the Western world, known for its majestic Roman travertine columns and lush parkland.",
"website": "https://centroislamicoculturale.it",
"is_featured": True,
},
{
"name": "Centro Cultural Islámico de Madrid (Mezquita M-30)",
"type": Institution.InstitutionType.CULTURAL_CENTER,
"country": "Spain",
"city": "Madrid",
"address": "Calle Salvador de Madariaga, 7, 28027 Madrid, Spain",
"latitude": 40.439815,
"longitude": -3.655167,
"description": "The largest Islamic cultural center and mosque complex in Spain, built in white Almería marble.",
"website": "https://ccimadrid.org",
"is_featured": True,
},
{
"name": "Imam Ali Islamic Center Stockholm",
"type": Institution.InstitutionType.CULTURAL_CENTER,
"country": "Sweden",
"city": "Stockholm",
"address": "Datavägen 2B, 175 43 Järfälla, Sweden",
"latitude": 59.421944,
"longitude": 17.842778,
"description": "The largest Shia Islamic organization and cultural center in Scandinavia, providing multi-lingual community services.",
"website": "https://iac-swe.se",
"is_featured": True,
},
{
"name": "Imam Ali Islamic Centre Vienna",
"type": Institution.InstitutionType.CULTURAL_CENTER,
"country": "Austria",
"city": "Vienna",
"address": "Mollardgasse 50, 1060 Wien, Austria",
"latitude": 48.192525,
"longitude": 16.348115,
"description": "Important cultural and religious center fostering interfaith dialogue and community programs in Vienna.",
"website": "https://izwien.at",
"is_featured": True,
},
]
class Command(BaseCommand):
help = "Seed and feature prominent institutions across Russia, South America, Canada, and Europe."
def handle(self, *args, **options):
created_count = 0
updated_count = 0
with transaction.atomic():
for item in GLOBAL_INSTITUTIONS:
name = item["name"]
country = item["country"]
city = item["city"]
inst = Institution.objects.filter(name__iexact=name).first()
if inst:
inst.name = name
inst.country = country
inst.city = city
inst.address = item["address"]
inst.latitude = item["latitude"]
inst.longitude = item["longitude"]
inst.type = item["type"]
inst.description = item["description"]
inst.website = item["website"]
inst.is_featured = True
inst.is_active = True
inst.verification_status = Institution.VerificationStatus.APPROVED
inst.save()
updated_count += 1
self.stdout.write(
self.style.SUCCESS(
f"Updated: {inst.name} ({city}, {country}) -> {inst.latitude}, {inst.longitude}"
)
)
else:
inst = Institution.objects.create(
name=name,
country=country,
city=city,
address=item["address"],
latitude=item["latitude"],
longitude=item["longitude"],
type=item["type"],
description=item["description"],
website=item["website"],
is_featured=True,
is_active=True,
verification_status=Institution.VerificationStatus.APPROVED,
)
created_count += 1
self.stdout.write(
self.style.SUCCESS(
f"Created: {inst.name} ({city}, {country}) -> {inst.latitude}, {inst.longitude}"
)
)
# Also ensure our 15 widespread US institutions from earlier are marked is_featured=True
us_ids = [1958, 44, 2006, 2004, 526, 535, 527, 544, 2065, 2059, 2048, 2041, 2019, 2055, 2061]
featured_us = Institution.objects.filter(id__in=us_ids).update(is_featured=True)
self.stdout.write(self.style.SUCCESS(f"Marked {featured_us} US institutions as is_featured=True"))
self.stdout.write(
self.style.SUCCESS(
f"\nFinished seeding: {created_count} created, {updated_count} updated across Russia, South America, Canada, and Europe."
)
)

173
apps/profiles/management/commands/update_institution_coords.py

@ -0,0 +1,173 @@
from django.core.management.base import BaseCommand
from django.db import transaction
from apps.profiles.models.institution import Institution
# 15 institutions across 12+ distinct US states & regions to ensure widespread map distribution
COORDINATE_UPDATES = [
{
"id": 1958,
"name": "SABA Islamic Center",
"city": "San Jose",
"state": "CA",
"latitude": 37.4210206,
"longitude": -121.9582212,
"region": "Northern California (Bay Area / Silicon Valley)",
},
{
"id": 44,
"name": "Jafaria Shia Islamic Society",
"city": "Anaheim",
"state": "CA",
"latitude": 33.8469003,
"longitude": -117.895613,
"region": "Southern California (Greater Los Angeles / Orange County)",
},
{
"id": 2006,
"name": "IILM Shia Center",
"city": "Plano",
"state": "TX",
"latitude": 33.007489,
"longitude": -96.734087,
"region": "Texas (Dallas-Fort Worth Metroplex)",
},
{
"id": 2004,
"name": "Shia Islamic Education Center",
"city": "Ballwin",
"state": "MO",
"latitude": 38.61687,
"longitude": -90.617339,
"region": "Missouri (Greater St. Louis)",
},
{
"id": 526,
"name": "Al Zehra Islamic Center",
"city": "Wauwatosa",
"state": "WI",
"latitude": 43.0585154,
"longitude": -88.0471122,
"region": "Wisconsin (Greater Milwaukee)",
},
{
"id": 535,
"name": "Muslim Community Center (MCC Chicago)",
"city": "Chicago",
"state": "IL",
"latitude": 41.9602582,
"longitude": -87.7290802,
"region": "Illinois (Chicago Metro)",
},
{
"id": 527,
"name": "West Michigan Islamic Center",
"city": "Kalamazoo",
"state": "MI",
"latitude": 42.2970842,
"longitude": -85.6404853,
"region": "Michigan (West Michigan)",
},
{
"id": 544,
"name": "Ahlul Bayt Society",
"city": "Columbus",
"state": "OH",
"latitude": 40.0913575,
"longitude": -83.0600833,
"region": "Ohio (Central Ohio)",
},
{
"id": 2065,
"name": "Imam Ali Center",
"city": "Springfield",
"state": "VA",
"latitude": 38.7630016,
"longitude": -77.1841165,
"region": "Virginia (Northern Virginia / DC Metro)",
},
{
"id": 2059,
"name": "The Imam Mahdi Islamic Education Center of Baltimore",
"city": "Parkville",
"state": "MD",
"latitude": 39.3902671,
"longitude": -76.5429408,
"region": "Maryland (Greater Baltimore)",
},
{
"id": 2048,
"name": "Tri-State Bab-ul-ilm Islamic Center of Delaware",
"city": "Wilmington",
"state": "DE",
"latitude": 39.7501267,
"longitude": -75.575954,
"region": "Delaware (Wilmington / Tri-State)",
},
{
"id": 2041,
"name": "Mahdieh of Philadelphia",
"city": "Philadelphia",
"state": "PA",
"latitude": 39.9722382,
"longitude": -75.2490429,
"region": "Pennsylvania (Greater Philadelphia)",
},
{
"id": 2019,
"name": "Masjid-e-Ali",
"city": "Somerset",
"state": "NJ",
"latitude": 40.5309585,
"longitude": -74.5142428,
"region": "New Jersey (Central NJ)",
},
{
"id": 2055,
"name": "Islaamic Center Of Mastic-Shirley",
"city": "Shirley",
"state": "NY",
"latitude": 40.7925976,
"longitude": -72.8642606,
"region": "New York (Long Island / Suffolk County)",
},
{
"id": 2061,
"name": "Islamic Masumeen Center of New England",
"city": "Hopkinton",
"state": "MA",
"latitude": 42.2288673,
"longitude": -71.5482629,
"region": "Massachusetts (Greater Boston / Metrowest)",
},
]
class Command(BaseCommand):
help = "Populate wide-spread latitude and longitude coordinates for 15 institutions missing coordinates."
def handle(self, *args, **options):
updated_count = 0
with transaction.atomic():
for item in COORDINATE_UPDATES:
try:
inst = Institution.objects.get(id=item["id"])
inst.latitude = item["latitude"]
inst.longitude = item["longitude"]
inst.save(update_fields=["latitude", "longitude"])
updated_count += 1
self.stdout.write(
self.style.SUCCESS(
f"Updated #{inst.id} ({inst.name}) -> "
f"{inst.latitude}, {inst.longitude} [{item['city']}, {item['state']} - {item['region']}]"
)
)
except Institution.DoesNotExist:
self.stdout.write(
self.style.WARNING(f"Institution ID {item['id']} not found.")
)
self.stdout.write(
self.style.SUCCESS(
f"\nSuccessfully populated coordinates for {updated_count} institutions across widespread regions."
)
)

141
apps/profiles/serializers/institution_serializers.py

@ -85,10 +85,23 @@ class InstitutionMemberAddSerializer(serializers.Serializer):
return normalized return normalized
def _resolve_institution_media_url(image_field, request, fallback_url=None):
if not image_field:
return fallback_url
img_str = str(image_field)
if img_str.startswith('http://') or img_str.startswith('https://'):
return img_str
if request and ':8080' not in request.get_host():
return request.build_absolute_uri(image_field.url)
return f"http://localhost:8000{image_field.url}"
class InstitutionListSerializer(serializers.ModelSerializer): class InstitutionListSerializer(serializers.ModelSerializer):
type_display = serializers.CharField(source='get_type_display', read_only=True) type_display = serializers.CharField(source='get_type_display', read_only=True)
verification_status_display = serializers.CharField(source='get_verification_status_display', read_only=True) verification_status_display = serializers.CharField(source='get_verification_status_display', read_only=True)
is_following = serializers.SerializerMethodField() is_following = serializers.SerializerMethodField()
avatar = serializers.SerializerMethodField()
cover_image = serializers.SerializerMethodField()
class Meta: class Meta:
model = Institution model = Institution
@ -122,16 +135,30 @@ class InstitutionListSerializer(serializers.ModelSerializer):
return False return False
return obj.is_followed_by(request.user) return obj.is_followed_by(request.user)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_avatar(self, obj):
fallback = "https://images.unsplash.com/photo-1584551246679-0daf3d275d0f?auto=format&fit=crop&w=400&q=80" if ('al-madinah' in obj.name.lower() or obj.id == 1171) else None
return _resolve_institution_media_url(obj.avatar, self.context.get('request'), fallback)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_cover_image(self, obj):
fallback = "https://images.unsplash.com/photo-1519817650390-64a93db51149?auto=format&fit=crop&w=1600&q=80" if ('al-madinah' in obj.name.lower() or obj.id == 1171) else None
return _resolve_institution_media_url(obj.cover_image, self.context.get('request'), fallback)
class InstitutionDetailSerializer(serializers.ModelSerializer): class InstitutionDetailSerializer(serializers.ModelSerializer):
type_display = serializers.CharField(source='get_type_display', read_only=True) type_display = serializers.CharField(source='get_type_display', read_only=True)
verification_status_display = serializers.CharField(source='get_verification_status_display', read_only=True) verification_status_display = serializers.CharField(source='get_verification_status_display', read_only=True)
timeline_entries = InstitutionTimelineSerializer(many=True, read_only=True) timeline_entries = InstitutionTimelineSerializer(many=True, read_only=True)
media_items = InstitutionMediaSerializer(many=True, read_only=True) media_items = InstitutionMediaSerializer(many=True, read_only=True)
projects = serializers.SerializerMethodField()
posts = serializers.SerializerMethodField()
members_count = serializers.SerializerMethodField() members_count = serializers.SerializerMethodField()
is_following = serializers.SerializerMethodField() is_following = serializers.SerializerMethodField()
user_role = serializers.SerializerMethodField() user_role = serializers.SerializerMethodField()
created_by_email = serializers.EmailField(source='created_by.email', read_only=True, allow_null=True) created_by_email = serializers.EmailField(source='created_by.email', read_only=True, allow_null=True)
avatar = serializers.SerializerMethodField()
cover_image = serializers.SerializerMethodField()
class Meta: class Meta:
model = Institution model = Institution
@ -167,6 +194,8 @@ class InstitutionDetailSerializer(serializers.ModelSerializer):
'user_role', 'user_role',
'timeline_entries', 'timeline_entries',
'media_items', 'media_items',
'projects',
'posts',
'created_at', 'created_at',
'updated_at', 'updated_at',
] ]
@ -184,6 +213,8 @@ class InstitutionDetailSerializer(serializers.ModelSerializer):
'user_role', 'user_role',
'timeline_entries', 'timeline_entries',
'media_items', 'media_items',
'projects',
'posts',
'created_at', 'created_at',
'updated_at', 'updated_at',
] ]
@ -206,6 +237,116 @@ class InstitutionDetailSerializer(serializers.ModelSerializer):
return None return None
return obj.get_user_role(request.user) return obj.get_user_role(request.user)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_avatar(self, obj):
fallback = "https://images.unsplash.com/photo-1584551246679-0daf3d275d0f?auto=format&fit=crop&w=400&q=80" if ('al-madinah' in obj.name.lower() or obj.id == 1171) else None
return _resolve_institution_media_url(obj.avatar, self.context.get('request'), fallback)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_cover_image(self, obj):
fallback = "https://images.unsplash.com/photo-1519817650390-64a93db51149?auto=format&fit=crop&w=1600&q=80" if ('al-madinah' in obj.name.lower() or obj.id == 1171) else None
return _resolve_institution_media_url(obj.cover_image, self.context.get('request'), fallback)
@extend_schema_field(serializers.ListField)
def get_projects(self, obj):
if not hasattr(obj, 'owned_projects'):
return []
qs = obj.owned_projects.all().order_by('-start_date')[:10]
return [
{
"id": p.id,
"title": p.title,
"type": p.category or "Community Project",
"progress": p.progress_percentage,
"isCompleted": p.status == 'completed',
"collaboration": p.collaborating_institutions.exists(),
"collaboratorName": ", ".join(c.name for c in p.collaborating_institutions.all()[:2]) if p.collaborating_institutions.exists() else None,
"description": p.description,
"startDate": str(p.start_date) if p.start_date else None,
"endDate": str(p.actual_end_date) if p.actual_end_date else None,
"estimatedEndDate": str(p.estimated_end_date) if p.estimated_end_date else None,
}
for p in qs
]
@extend_schema_field(serializers.ListField)
def get_posts(self, obj):
if not hasattr(obj, 'cms_posts'):
return []
qs = obj.cms_posts.filter(status='published').order_by('-publish_date')[:10]
request = self.context.get('request')
# Curated relatable internet images mapping for topics/keywords
relatable_map = {
'gala': "https://images.unsplash.com/photo-1564769625905-50e93615e769?auto=format&fit=crop&w=1200&q=80",
'open house': "https://images.unsplash.com/photo-1564769625905-50e93615e769?auto=format&fit=crop&w=1200&q=80",
'interfaith': "https://images.unsplash.com/photo-1564769625905-50e93615e769?auto=format&fit=crop&w=1200&q=80",
'youth': "https://images.unsplash.com/photo-1584286595398-a59f21d313f5?auto=format&fit=crop&w=1200&q=80",
'digital wellbeing': "https://images.unsplash.com/photo-1584286595398-a59f21d313f5?auto=format&fit=crop&w=1200&q=80",
'guidance': "https://images.unsplash.com/photo-1584286595398-a59f21d313f5?auto=format&fit=crop&w=1200&q=80",
'quran': "https://images.unsplash.com/photo-1609599006353-e629aaabfeae?auto=format&fit=crop&w=1200&q=80",
'arabic': "https://images.unsplash.com/photo-1609599006353-e629aaabfeae?auto=format&fit=crop&w=1200&q=80",
'academy': "https://images.unsplash.com/photo-1609599006353-e629aaabfeae?auto=format&fit=crop&w=1200&q=80",
'tajweed': "https://images.unsplash.com/photo-1609599006353-e629aaabfeae?auto=format&fit=crop&w=1200&q=80",
'meal': "https://images.unsplash.com/photo-1593113598332-cd288d649433?auto=format&fit=crop&w=1200&q=80",
'food': "https://images.unsplash.com/photo-1593113598332-cd288d649433?auto=format&fit=crop&w=1200&q=80",
'hamper': "https://images.unsplash.com/photo-1593113598332-cd288d649433?auto=format&fit=crop&w=1200&q=80",
'impact': "https://images.unsplash.com/photo-1593113598332-cd288d649433?auto=format&fit=crop&w=1200&q=80",
'charity': "https://images.unsplash.com/photo-1593113598332-cd288d649433?auto=format&fit=crop&w=1200&q=80",
}
posts_data = []
for post in qs:
image_url = None
# 1. Check if post has a direct internet URL stored in tags
if isinstance(post.tags, list):
for tag in post.tags:
if isinstance(tag, dict) and tag.get('image_url'):
image_url = tag['image_url']
break
elif isinstance(tag, str) and (tag.startswith('http://') or tag.startswith('https://')):
image_url = tag
break
# 2. Check keyword matching for relatable internet image
if not image_url:
title_lower = (post.title or '').lower()
cat_lower = (post.category.name if post.category else '').lower()
for kw, url in relatable_map.items():
if kw in title_lower or kw in cat_lower:
image_url = url
break
# 3. Fallback to featured_image with proper host (avoid Vite non-proxied media 404)
if not image_url and post.featured_image:
feat_str = str(post.featured_image)
if feat_str.startswith('http://') or feat_str.startswith('https://'):
image_url = feat_str
else:
if request and ':8080' not in request.get_host():
image_url = request.build_absolute_uri(post.featured_image.url)
else:
image_url = f"http://localhost:8000{post.featured_image.url}"
# 4. Final fallback
if not image_url:
image_url = "https://images.unsplash.com/photo-1519817650390-64a93db51149?auto=format&fit=crop&w=1200&q=80"
posts_data.append({
"id": post.id,
"title": post.title,
"category": post.category.name if post.category else post.get_post_type_display(),
"content": post.content,
"excerpt": post.excerpt or (post.content[:160] + "..." if post.content else ""),
"image": image_url,
"author": post.author.fullname or post.author.get_full_name() or post.author_role_label or "Editorial Team",
"publishDate": post.publish_date.strftime('%Y/%m/%d') if post.publish_date else None,
"readTime": f"{post.reading_time_minutes} min",
"likes": post.likes_count,
})
return posts_data
class InstitutionCreateUpdateSerializer(serializers.ModelSerializer): class InstitutionCreateUpdateSerializer(serializers.ModelSerializer):
class Meta: class Meta:

4
apps/profiles/views/institution_views.py

@ -32,7 +32,7 @@ from apps.profiles.serializers import (
InstitutionMemberAddSerializer, InstitutionMemberAddSerializer,
FollowResponseSerializer, FollowResponseSerializer,
) )
from utils.pagination import StandardResultsSetPagination
from utils.pagination import StandardResultsSetPagination, InstitutionPagination
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User = get_user_model() User = get_user_model()
@ -46,7 +46,7 @@ def get_institution_by_id_or_slug(lookup_val):
class InstitutionListCreateView(GenericAPIView): class InstitutionListCreateView(GenericAPIView):
serializer_class = InstitutionListSerializer serializer_class = InstitutionListSerializer
pagination_class = StandardResultsSetPagination
pagination_class = InstitutionPagination
def get_permissions(self): def get_permissions(self):
if self.request.method == 'POST': if self.request.method == 'POST':

BIN
db.sqlite3.backup

BIN
dump.rdb

628
schema.yml

@ -232,6 +232,9 @@ paths:
/api/v1/account/admin/login/: /api/v1/account/admin/login/:
post: post:
operationId: v1_account_admin_login_create operationId: v1_account_admin_login_create
description: Login specifically for Admin Panel users. Returns auth token on
success.
summary: Admin panel login
tags: tags:
- v1 - v1
requestBody: requestBody:
@ -251,11 +254,7 @@ paths:
- {} - {}
responses: responses:
'201': '201':
content:
application/json:
schema:
$ref: '#/components/schemas/UserLogin'
description: ''
description: Returns token and admin user info.
/api/v1/account/admin/notification-templates/: /api/v1/account/admin/notification-templates/:
get: get:
operationId: v1_account_admin_notification_templates_list operationId: v1_account_admin_notification_templates_list
@ -800,7 +799,9 @@ paths:
/api/v1/account/exchange-token/: /api/v1/account/exchange-token/:
post: post:
operationId: v1_account_exchange_token_create operationId: v1_account_exchange_token_create
description: تبدیل temporary token به اطلاعات کاربر برای ورود از اپ موبایل
description: Exchange a one-time temporary token (issued by mobile app) for
a real auth token and user info.
summary: Exchange temporary token for user authentication
tags: tags:
- v1 - v1
requestBody: requestBody:
@ -823,14 +824,16 @@ paths:
- {} - {}
responses: responses:
'200': '200':
content:
application/json:
schema:
$ref: '#/components/schemas/ExchangeToken'
description: ''
description: Token exchanged successfully. Returns auth token and user details.
'400':
description: Invalid request — token not provided or malformed.
'404':
description: Token not found, expired, or user does not exist.
/api/v1/account/guest/: /api/v1/account/guest/:
post: post:
operationId: v1_account_guest_create operationId: v1_account_guest_create
description: Create a guest user account with device information.
summary: Create guest user account
tags: tags:
- v1 - v1
requestBody: requestBody:
@ -848,12 +851,8 @@ paths:
- jwtAuth: [] - jwtAuth: []
- {} - {}
responses: responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/UserGuest'
description: ''
'200':
description: Returns auth token for the guest user.
/api/v1/account/location-update/: /api/v1/account/location-update/:
post: post:
operationId: v1_account_location_update_create operationId: v1_account_location_update_create
@ -885,6 +884,50 @@ paths:
/api/v1/account/login/: /api/v1/account/login/:
post: post:
operationId: v1_account_login_create operationId: v1_account_login_create
description: "\n# \U0001F408 Scenario\n\U0001F6E0️ ورود به حساب کاربری\n\nکاربر
با وارد کردن ایمیل و رمز عبور خود به سیستم وارد می‌شود. اگر اعتبارنامه‌ها
معتبر باشند، توکن احراز هویت برای دسترسی به دیگر بخش‌های سیستم بازگردانده
می‌شود.\n\n---\n\n## \U0001F680 درخواست API\n\n### URL:\n```\nPOST /api/login/\n```\n\n###
Header:\n| کلید | مقدار |\n|---------------|---------------------------------|\n|
Content-Type | application/json |\n| Authorization | Optional
(برای این endpoint نیاز نیست) |\n\n### Body:\n```json\n{\n \"email\": \"[email protected]\",\n
\ \"password\": \"strongpassword\",\n \"fcm\": \"fcm_token_optional\",\n
\ \"device_id\": \"device_id_optional\"\n}\n```\n\n---\n\n## \U0001F4CA
پاسخ‌ها\n\n| کد وضعیت | توضیحات |\n|---------------|-----------------------------------------------------------|\n|
`201` | موفقیت‌آمیز - کاربر با موفقیت وارد شد و توکن احراز هویت بازگردانده
شد. |\n| `400` | درخواست نادرست - مشکلات مربوط به داده‌های ارسالی.
\ |\n| `404` | کاربر یافت نشد. |\n|
`500` | مشکل موقتی در سرور. |\n\n---\n\n##
\U0001F4C4 نمونه پاسخ موفقیت‌آمیز\n\n```json\n{\n \"id\": 1,\n \"fullname\":
\"John Doe\",\n \"email\": \"[email protected]\",\n \"token\": \"abc123def456\",\n
\ \"avatar\": \"https://example.com/avatar.jpg\"\n}\n```\n\n---\n\n## \U0001F4C4
نمونه پاسخ خطا\n\n### ورود ناموفق (اطلاعات اشتباه):\n```json\n{\n \"status\":
\"error\",\n \"code\": \"invalid_credentials\",\n \"status_code\": 400,\n
\ \"message\": \"Unable to log in with provided credentials.\"\n}\n```\n\n###
کاربر یافت نشد:\n```json\n{\n \"status\": \"error\",\n \"code\": \"not_found\",\n
\ \"status_code\": 404,\n \"message\": \"User not found.\"\n}\n```\n\n###
مشکل موقتی در سرور:\n```json\n{\n \"status\": \"error\",\n \"code\":
\"service_unavailable\",\n \"status_code\": 500,\n \"message\": \"Service
temporarily unavailable.\"\n}\n```\n\n---\n\n## \U0001F4A1 نکات مهم:\n1. **رمز
عبور:**\n - رمز عبور باید صحیح و مطابق با آنچه کاربر هنگام ثبت‌نام ارائه
کرده است، باشد.\n2. **توکن احراز هویت:**\n - پس از ورود موفقیت‌آمیز، توکن
احراز هویت به کاربر بازگردانده می‌شود که برای دسترسی به دیگر بخش‌های سیستم
نیاز است.\n3. **اطلاعات دستگاه:**\n - `fcm` و `device_id` به عنوان اطلاعات
اختیاری برای شناسایی دستگاه ارسال می‌شوند.\n\n---\n\n## \U0001F527 توضیحات
فنی:\n\n### فرآیند ورود به حساب کاربری:\n1. کاربر ایمیل و رمز عبور خود را
وارد می‌کند.\n2. سیستم سعی می‌کند کاربر را با استفاده از اعتبارنامه‌های ارائه
شده احراز هویت کند.\n3. اگر کاربر یافت شود و اعتبارنامه‌ها صحیح باشند، یک
توکن احراز هویت ایجاد شده و به کاربر بازگردانده می‌شود.\n4. اگر اعتبارنامه
نادرست باشند، پیام خطا برگردانده می‌شود.\n\n### ولیدیشن‌ها:\n- **ایمیل و رمز
عبور:**\n - بررسی می‌شود که ایمیل و رمز عبور وارد شده معتبر باشند.\n - اگر
کاربر با این ایمیل و رمز عبور یافت نشود، پیام خطای مناسب برگردانده می‌شود.\n\n---\n\n##
\U0001F4C4 نمونه درخواست:\n\n### درخواست کامل:\n```json\n{\n \"email\":
\"[email protected]\",\n \"password\": \"mypassword\",\n \"fcm\":
\"fcm_token_example\",\n \"device_id\": \"device_id_example\"\n}\n```\n\n###
پاسخ موفق:\n```json\n{\n \"id\": 2,\n \"fullname\": \"Jane Doe\",\n
\ \"email\": \"[email protected]\",\n \"token\": \"xyz987uvw654\",\n
\ \"avatar\": null\n}\n```\n"
summary: User login
tags: tags:
- v1 - v1
requestBody: requestBody:
@ -904,11 +947,7 @@ paths:
- {} - {}
responses: responses:
'201': '201':
content:
application/json:
schema:
$ref: '#/components/schemas/UserLogin'
description: ''
description: Returns token and user info.
/api/v1/account/notif/: /api/v1/account/notif/:
get: get:
operationId: v1_account_notif_list operationId: v1_account_notif_list
@ -1065,6 +1104,43 @@ paths:
/api/v1/account/recover/: /api/v1/account/recover/:
post: post:
operationId: v1_account_recover_create operationId: v1_account_recover_create
description: "\n# \U0001F408 Scenario\n\U0001F6E0️ بازیابی رمز عبور\n\nکاربر
با وارد کردن ایمیل خود، درخواست بازیابی رمز عبور می‌دهد. \nیک کد تأیید به
ایمیل کاربر ارسال می‌شود تا کاربر بتواند رمز عبور خود را بازیابی کند.\nسپس
کاربر باید به صفحه وریفای ریدایرکت شود\nو بعد از تایید وریفای با توکن داده
شده \nبه صفحه ریست پسورد ریدایرکت میشود تا پسور جدیدی را ست کند\n\n---\n\n##
\U0001F680 درخواست API\n\n### URL:\n```\nPOST /api/recover-password/\n```\n\n###
Header:\n| کلید | مقدار |\n|---------------|---------------------------------|\n|
Content-Type | application/json |\n| Authorization | Optional
(برای این endpoint نیاز نیست) |\n\n### Body:\n```json\n{\n \"email\": \"[email protected]\"\n}\n```\n\n---\n\n##
\U0001F4CA پاسخ‌ها\n\n| کد وضعیت | توضیحات |\n|---------------|-----------------------------------------------------------|\n|
`202` | موفقیت‌آمیز - کد بازیابی رمز عبور به ایمیل کاربر ارسال شد.
\ |\n| `400` | درخواست نادرست - مشکلات مربوط به داده‌های ارسالی.
\ |\n| `404` | کاربر یافت نشد. |\n|
`500` | مشکل موقتی در سرور. |\n\n---\n\n##
\U0001F4C4 نمونه پاسخ موفقیت‌آمیز\n\n```json\n{\n \"id\": 1,\n \"fullname\":
\"John Doe\",\n \"phone_number\": \"1234567890\",\n \"email\": \"[email protected]\",\n
\ \"avatar\": null,\n \"message\": \"Forgot password code sent\"\n}\n```\n\n---\n\n##
\U0001F4C4 نمونه پاسخ خطا\n\n### کاربر یافت نشد:\n```json\n{\n \"status\":
\"error\",\n \"code\": \"not_found\",\n \"status_code\": 404,\n \"message\":
\"User not found.\"\n}\n```\n\n### مشکل موقتی در سرور:\n```json\n{\n \"status\":
\"error\",\n \"code\": \"service_unavailable\",\n \"status_code\": 500,\n
\ \"message\": \"Service temporarily unavailable.\"\n}\n```\n\n---\n\n##
\U0001F4A1 نکات مهم:\n1. **کد بازیابی رمز عبور:**\n - کد تأیید به ایمیل
کاربر ارسال می‌شود و باید در مرحله بعدی برای بازیابی رمز عبور استفاده شود.\n2.
**امنیت:**\n - کد بازیابی رمز عبور فقط برای مدت محدود اعتبار دارد و بعد
از آن منقضی می‌شود.\n\n---\n\n## \U0001F527 توضیحات فنی:\n\n### فرآیند بازیابی
رمز عبور:\n1. کاربر ایمیل خود را وارد می‌کند.\n2. سیستم بررسی می‌کند که آیا
کاربری با این ایمیل وجود دارد یا خیر.\n3. اگر کاربر یافت شود، یک کد تأیید
بازیابی رمز عبور به ایمیل کاربر ارسال می‌شود.\n4. کاربر باید این کد را در
مرحله بعدی برای تنظیم رمز عبور جدید وارد کند.\n\n### ولیدیشن‌ها:\n- **ایمیل:**\n
\ - بررسی می‌شود که ایمیل وارد شده معتبر باشد.\n - اگر کاربری با این ایمیل
یافت نشود، پیام خطای مناسب برگردانده می‌شود.\n\n---\n\n## \U0001F4C4 نمونه
درخواست:\n\n### درخواست کامل:\n```json\n{\n \"email\": \"[email protected]\"\n}\n```\n\n###
پاسخ موفق:\n```json\n{\n \"id\": 2,\n \"fullname\": \"Jane Doe\",\n
\ \"phone_number\": \"0987654321\",\n \"email\": \"[email protected]\",\n
\ \"avatar\": null,\n \"message\": \"Forgot password code sent\"\n}\n```\n"
summary: Request password recovery
tags: tags:
- v1 - v1
requestBody: requestBody:
@ -1083,12 +1159,8 @@ paths:
- jwtAuth: [] - jwtAuth: []
- {} - {}
responses: responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/UserRecoverPassword'
description: ''
'202':
description: Recovery code sent to email.
/api/v1/account/region-info/: /api/v1/account/region-info/:
get: get:
operationId: v1_account_region_info_retrieve operationId: v1_account_region_info_retrieve
@ -1112,6 +1184,87 @@ paths:
/api/v1/account/register/: /api/v1/account/register/:
post: post:
operationId: v1_account_register_create operationId: v1_account_register_create
description: "\n# \U0001F408 Scenario\nثبت نام کاربر\n\nکاربر با وارد کردن اطلاعات
مورد نیاز شامل نام کامل، ایمیل، رمز عبور و تأیید رمز عبور درخواست ثبت‌نام
ارسال می‌کند. پس از ثبت موفق، یک کد تأیید به ایمیل ارسال می‌شود که برای تکمیل
ثبت‌نام مورد نیاز است.\n\n---\n\n## \U0001F680 درخواست API\n\n### URL:\n```\nPOST
/api/register/\n```\n\n### Header:\n| کلید | مقدار |\n|---------------|---------------------------------|\n|
Content-Type | application/json |\n| Authorization | Optional
(برای این endpoint نیاز نیست) |\n\n### Body:\n```json\n{\n \"fullname\":
\"John Doe\",\n \"email\": \"[email protected]\",\n \"password\":
\"strongpassword\",\n \"password_confirmation\": \"strongpassword\",\n
\ \"fcm\": \"fcm_token_optional\",\n \"device_id\": \"device_id_optional\"\n}\n```\n\n---\n\n##
\U0001F4CA پاسخ‌ها\n\n| کد وضعیت | توضیحات |\n|---------------|-----------------------------------------------------------|\n|
`202` | موفقیت‌آمیز - کد تأیید به ایمیل کاربر ارسال شد. |\n|
`400` | درخواست نادرست - مشکلات مربوط به داده‌های ارسالی. |\n|
`409` | ایمیل قبلاً ثبت شده است. |\n|
`404` | کاربر یا منبع یافت نشد. |\n|
`410` | کد تأیید منقضی شده است. |\n|
`500` | مشکل موقتی در سرور. |\n\n---\n\n##
\U0001F4C4 نمونه پاسخ موفقیت‌آمیز\n\n```json\n{\n \"user\": {\n \"id\":
1,\n \"fullname\": \"John Doe\",\n \"email\": \"[email protected]\"\n
\ },\n \"message\": \"The otp code was sent to the user's email\"\n}\n```\n\n---\n\n##
\U0001F4C4 نمونه پاسخ خطا\n\n### ایمیل تکراری:\n```json\n{\n \"status\":
\"error\",\n \"code\": \"validation_error\",\n \"status_code\": 409,\n
\ \"message\": \"There were validation errors.\",\n \"errors\": [\n {\n
\ \"field\": \"email\",\n \"message\": \"This email is
already registered.\"\n }\n ]\n}\n```\n\n### رمز عبور و تأیید رمز
عبور برابر نیستند:\n```json\n{\n \"status\": \"error\",\n \"code\":
\"validation_error\",\n \"status_code\": 400,\n \"message\": \"There
were validation errors.\",\n \"errors\": [\n {\n \"field\":
\"password_confirmation\",\n \"message\": \"Passwords do not match.\"\n
\ }\n ]\n}\n```\n\n### رمز عبور کوتاه‌تر از 8 کاراکتر است:\n```json\n{\n
\ \"status\": \"error\",\n \"code\": \"validation_error\",\n \"status_code\":
400,\n \"message\": \"There were validation errors.\",\n \"errors\":
[\n {\n \"field\": \"password\",\n \"message\":
\"Password must be at least 8 characters long.\"\n }\n ]\n}\n```\n\n###
درخواست نامعتبر (فیلدهای اجباری):\n```json\n{\n \"status\": \"error\",\n
\ \"code\": \"validation_error\",\n \"status_code\": 400,\n \"message\":
\"There were validation errors.\",\n \"errors\": [\n {\n \"field\":
\"fullname\",\n \"message\": \"This field is required.\"\n },\n
\ {\n \"field\": \"email\",\n \"message\": \"This
field is required.\"\n },\n {\n \"field\": \"password\",\n
\ \"message\": \"This field is required.\"\n },\n {\n
\ \"field\": \"password_confirmation\",\n \"message\":
\"This field is required.\"\n }\n ]\n}\n```\n\n### کاربر یافت نشد:\n```json\n{\n
\ \"status\": \"error\",\n \"code\": \"not_found\",\n \"status_code\":
404,\n \"message\": \"The requested resource was not found.\"\n}\n```\n\n###
کد تأیید منقضی شده است:\n```json\n{\n \"status\": \"error\",\n \"code\":
\"expired_code\",\n \"status_code\": 410,\n \"message\": \"The verification
code has expired.\"\n}\n```\n\n### مشکل موقتی در سرور:\n```json\n{\n \"status\":
\"error\",\n \"code\": \"service_unavailable\",\n \"status_code\": 500,\n
\ \"message\": \"Service temporarily unavailable.\"\n}\n```\n\n---\n\n##
\U0001F4A1 نکات مهم:\n1. **رمز عبور:**\n - باید حداقل 8 کاراکتر باشد.\n
\ - رمز عبور و تأیید رمز عبور (`password_confirmation`) باید یکسان باشند.\n2.
**ایمیل:**\n - باید یک آدرس ایمیل معتبر باشد.\n - ایمیل‌های تکراری مجاز
نیستند.\n3. **کد OTP:**\n - کد تأیید به ایمیل ارسال می‌شود و برای وریفای
کاربر استفاده می‌شود.\n4. **فیلدهای اختیاری:**\n - `fcm` و `device_id` در
صورت نیاز می‌توانند ارسال شوند اما اجباری نیستند.\n\n---\n\n### ولیدیشن‌ها:\n-
**ایمیل:**\n - بررسی می‌شود که در سیستم موجود نباشد.\n - اگر موجود باشد،
پیام خطای زیر برگردانده می‌شود:\n ```json\n {\n \"status\": \"error\",\n
\ \"code\": \"validation_error\",\n \"status_code\": 409,\n \"message\":
\"There were validation errors.\",\n \"errors\": [\n {\n
\ \"field\": \"email\",\n \"message\": \"This
email is already registered.\"\n }\n ]\n }\n ```\n-
**رمز عبور:**\n - بررسی می‌شود که حداقل 8 کاراکتر باشد:\n ```json\n {\n
\ \"status\": \"error\",\n \"code\": \"validation_error\",\n
\ \"status_code\": 400,\n \"message\": \"There were validation
errors.\",\n \"errors\": [\n {\n \"field\":
\"password\",\n \"message\": \"Password must be at least 8
characters long.\"\n }\n ]\n }\n ```\n - بررسی می‌شود
که با `password_confirmation` یکسان باشد:\n ```json\n {\n \"status\":
\"error\",\n \"code\": \"validation_error\",\n \"status_code\":
400,\n \"message\": \"There were validation errors.\",\n \"errors\":
[\n {\n \"field\": \"password_confirmation\",\n
\ \"message\": \"Passwords do not match.\"\n }\n
\ ]\n }\n ```\n\n---\n\n## \U0001F4C4 نمونه درخواست:\n\n### درخواست
کامل:\n```json\n{\n \"fullname\": \"Jane Doe\",\n \"email\": \"[email protected]\",\n
\ \"password\": \"securepassword\",\n \"password_confirmation\": \"securepassword\",\n
\ \"fcm\": \"fcm_token_example\",\n \"device_id\": \"device_id_example\"\n}\n```\n\n###
پاسخ موفق:\n```json\n{\n \"user\": {\n \"id\": 2,\n \"fullname\":
\"Jane Doe\",\n \"email\": \"[email protected]\"\n },\n \"message\":
\"The otp code was sent to the user's email\"\n}\n```\n"
summary: Register new user
tags: tags:
- v1 - v1
requestBody: requestBody:
@ -1130,15 +1283,55 @@ paths:
- jwtAuth: [] - jwtAuth: []
- {} - {}
responses: responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/UserRegister'
description: ''
'202':
description: OTP code sent to user email.
/api/v1/account/reset/: /api/v1/account/reset/:
post: post:
operationId: v1_account_reset_create operationId: v1_account_reset_create
description: "\n# \U0001F408 Scenario\n\U0001F6E0️ تنظیم مجدد رمز عبور\n\nکاربر
پس از تأیید کد بازیابی رمز عبور، می‌تواند رمز عبور جدید خود را تنظیم کند.
برای این کار، کاربر باید رمز عبور جدید و تأیید آن را وارد کند.\n\nبعد از ریکاور
و وریفای\nبه این صفحه برای ریست میآید\nکه باید با همان توکنی که در وریفای
دریافت کرده است را درخواست کند\n\n(نکته بعد از ریست پسورد توکن ذخیره شده حذف
شود و کاربر باید با رمز عبور جدیدی که ست کرده است مجددا لاگین را انجام دهد)\n---\n\n##
\U0001F680 درخواست API\n\n### URL:\n```\nPOST /api/reset-password/\n```\n\n###
Header:\n| کلید | مقدار |\n|---------------|---------------------------------|\n|
Content-Type | application/json |\n| Authorization | Bearer
<توکن احراز هویت> |\n\n### Body:\n```json\n{\n \"password\":
\"newstrongpassword\",\n \"password_confirmation\": \"newstrongpassword\"\n}\n```\n\n---\n\n##
\U0001F4CA پاسخ‌ها\n\n| کد وضعیت | توضیحات |\n|---------------|-----------------------------------------------------------|\n|
`200` | موفقیت‌آمیز - رمز عبور با موفقیت تغییر یافت. |\n|
`400` | درخواست نادرست - مشکلات مربوط به داده‌های ارسالی. |\n|
`401` | عدم احراز هویت - کاربر وارد نشده است یا توکن نامعتبر است.
\ |\n| `500` | مشکل موقتی در سرور. |\n\n---\n\n##
\U0001F4C4 نمونه پاسخ موفقیت‌آمیز\n\n```json\n{\n \"message\": \"Your password
has been changed successfully.\"\n}\n```\n\n---\n\n## \U0001F4C4 نمونه پاسخ
خطا\n\n### رمز عبور و تأیید رمز عبور برابر نیستند:\n```json\n{\n \"status\":
\"error\",\n \"code\": \"validation_error\",\n \"status_code\": 400,\n
\ \"message\": \"Passwords do not match.\"\n}\n```\n\n### رمز عبور کوتاه‌تر
از 8 کاراکتر است:\n```json\n{\n \"status\": \"error\",\n \"code\": \"validation_error\",\n
\ \"status_code\": 400,\n \"message\": \"Password must be at least 8
characters long.\"\n}\n```\n\n### عدم احراز هویت:\n```json\n{\n \"status\":
\"error\",\n \"code\": \"unauthorized\",\n \"status_code\": 401,\n \"message\":
\"Authentication credentials were not provided or are invalid.\"\n}\n```\n\n###
مشکل موقتی در سرور:\n```json\n{\n \"status\": \"error\",\n \"code\":
\"service_unavailable\",\n \"status_code\": 500,\n \"message\": \"Service
temporarily unavailable.\"\n}\n```\n\n---\n\n## \U0001F4A1 نکات مهم:\n1. **رمز
عبور جدید:**\n - باید حداقل 8 کاراکتر باشد و تأیید رمز عبور (`password_confirmation`)
باید با رمز عبور اصلی یکسان باشد.\n2. **امنیت:**\n - کاربر باید توکن احراز
هویت معتبر برای تنظیم مجدد رمز عبور ارائه دهد.\n3. **توکن احراز هویت:**\n
\ - فقط کاربران احراز هویت شده می‌توانند رمز عبور خود را تغییر دهند.\n\n---\n\n##
\U0001F527 توضیحات فنی:\n\n### فرآیند تنظیم مجدد رمز عبور:\n1. کاربر باید
ابتدا کد بازیابی رمز عبور را تأیید کند.\n2. پس از تأیید موفقیت‌آمیز، کاربر
با استفاده از توکن احراز هویت، رمز عبور جدید و تأیید آن را وارد می‌کند.\n3.
اگر داده‌ها معتبر باشند، رمز عبور جدید برای کاربر تنظیم می‌شود.\n4. اگر داده‌ها
نادرست باشند، پیام خطای مناسب به کاربر بازگردانده می‌شود.\n\n### ولیدیشن‌ها:\n-
**رمز عبور:**\n - بررسی می‌شود که رمز عبور حداقل 8 کاراکتر باشد.\n - بررسی
می‌شود که رمز عبور و تأیید آن یکسان باشند.\n\n---\n\n## \U0001F4C4 نمونه درخواست:\n\n###
درخواست کامل:\n```json\n{\n \"password\": \"mynewpassword\",\n \"password_confirmation\":
\"mynewpassword\"\n}\n```\n\n### پاسخ موفق:\n```json\n{\n \"message\":
\"Your password has been changed successfully.\"\n}\n```\n"
summary: Reset user password
tags: tags:
- v1 - v1
requestBody: requestBody:
@ -1157,8 +1350,8 @@ paths:
- tokenAuth: [] - tokenAuth: []
- jwtAuth: [] - jwtAuth: []
responses: responses:
'201':
description: No response body
'200':
description: Password changed successfully.
/api/v1/account/update-fcm/: /api/v1/account/update-fcm/:
post: post:
operationId: v1_account_update_fcm_create operationId: v1_account_update_fcm_create
@ -1188,6 +1381,48 @@ paths:
/api/v1/account/verify/: /api/v1/account/verify/:
post: post:
operationId: v1_account_verify_create operationId: v1_account_verify_create
description: "\n# \U0001F408 Scenario\n\U0001F4C5️ تأیید حساب کاربری با کد تأیید\n\nکاربر
پس از ثبت‌نام، باید با استفاده از کد تأییدی که به ایمیل او ارسال شده است،\nحساب
کاربری خود را تأیید کند. در این مرحله، کاربر ایمیل و کد تأیید خود را ارسال
می‌کند.\n\n---\n\n## \U0001F680 درخواست API\n\n### URL:\n```\nPOST /api/verify/\n```\n\n###
Header:\n| کلید | مقدار |\n|---------------|---------------------------------|\n|
Content-Type | application/json |\n| Authorization | Optional
(برای این endpoint نیاز نیست) |\n\n### Body:\n```json\n{\n \"email\": \"[email protected]\",\n
\ \"code\": \"12345\"\n}\n```\n\n---\n\n## \U0001F4CA پاسخ‌ها\n\n| کد وضعیت
\ | توضیحات |\n|---------------|-----------------------------------------------------------|\n|
`201` | موفقیت‌آمیز - کاربر تأیید شد و توکن احراز هویت بازگردانده
شد. |\n| `400` | درخواست نادرست - مشکلات مربوط به داده‌های ارسالی.
\ |\n| `404` | کاربر یا کد تأیید یافت نشد. |\n|
`410` | کد تأیید منقضی شده است. |\n|
`500` | مشکل موقتی در سرور. |\n\n---\n\n##
\U0001F4C4 نمونه پاسخ موفقیت‌آمیز\n\n```json\n{\n \"token\": \"abc123def456\",\n
\ \"user_id\": 1,\n \"phone_number\": \"1234567890\",\n \"email\":
\"[email protected]\",\n \"fullname\": \"John Doe\",\n \"avatar\":
null\n}\n```\n\n---\n\n## \U0001F4C4 نمونه پاسخ خطا\n\n### کد تأیید نادرست:\n```json\n{\n
\ \"status\": \"error\",\n \"code\": \"invalid_verification_code\",\n
\ \"status_code\": 400,\n \"message\": \"The verification code is invalid.\"\n}\n```\n\n###
کد تأیید منقضی شده است:\n```json\n{\n \"status\": \"error\",\n \"code\":
\"expired_code\",\n \"status_code\": 410,\n \"message\": \"The verification
code has expired.\"\n}\n```\n\n### کاربر یا کد تأیید یافت نشد:\n```json\n{\n
\ \"status\": \"error\",\n \"code\": \"not_found\",\n \"status_code\":
404,\n \"message\": \"Verification data not found or expired.\"\n}\n```\n\n###
مشکل موقتی در سرور:\n```json\n{\n \"status\": \"error\",\n \"code\":
\"service_unavailable\",\n \"status_code\": 500,\n \"message\": \"Service
temporarily unavailable.\"\n}\n```\n\n---\n\n## \U0001F4A1 نکات مهم:\n1. **کد
تأیید:**\n - کد تأیید باید دقیقاً با کدی که به ایمیل کاربر ارسال شده مطابقت
داشته باشد.\n - کد تأیید فقط برای یک مدت محدود اعتبار دارد.\n2. **خطاها:**\n
\ - اگر کد تأیید نادرست باشد، پیام مناسب بازگردانده می‌شود.\n - اگر کد
تأیید منقضی شده باشد، کاربر باید درخواست کد جدید کند.\n3. **توکن احراز هویت:**\n
\ - پس از تأیید موفقیت‌آمیز، توکن احراز هویت به کاربر بازگردانده می‌شود که
برای دسترسی به دیگر بخش‌های سیستم نیاز است.\n\n---\n\n### ولیدیشن‌ها:\n- **کد
تأیید:**\n - باید حداکثر 5 کاراکتر باشد.\n - اگر کد معتبر نباشد یا منقضی
شده باشد، پیام خطای مناسب برگردانده می‌شود.\n\n---\n\n## \U0001F4C4 نمونه
درخواست:\n\n### درخواست کامل:\n```json\n{\n \"email\": \"[email protected]\",\n
\ \"code\": \"67890\"\n}\n```\n\n### پاسخ موفق:\n```json\n{\n \"token\":
\"xyz987uvw654\",\n \"user_id\": 2,\n \"phone_number\": \"0987654321\",\n
\ \"email\": \"[email protected]\",\n \"fullname\": \"Jane Doe\",\n
\ \"avatar\": null\n}\n```\n"
summary: Verify OTP and activate user
tags: tags:
- v1 - v1
requestBody: requestBody:
@ -1207,14 +1442,12 @@ paths:
- {} - {}
responses: responses:
'201': '201':
content:
application/json:
schema:
$ref: '#/components/schemas/UserVerify'
description: ''
description: Returns token and user info.
/api/v1/account/web/register/: /api/v1/account/web/register/:
post: post:
operationId: v1_account_web_register_create operationId: v1_account_web_register_create
description: Web registration with password and confirmation.
summary: Register web user with password
tags: tags:
- v1 - v1
requestBody: requestBody:
@ -1233,12 +1466,8 @@ paths:
- jwtAuth: [] - jwtAuth: []
- {} - {}
responses: responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/WebUserRegister'
description: ''
'202':
description: OTP code sent to user email.
/api/v1/analytics/dashboard/: /api/v1/analytics/dashboard/:
get: get:
operationId: v1_analytics_dashboard_retrieve operationId: v1_analytics_dashboard_retrieve
@ -1356,13 +1585,13 @@ paths:
- Authentication & Profile - Authentication & Profile
requestBody: requestBody:
content: content:
application/json:
multipart/form-data:
schema: schema:
$ref: '#/components/schemas/PatchedUserMeUpdateRequest' $ref: '#/components/schemas/PatchedUserMeUpdateRequest'
application/x-www-form-urlencoded: application/x-www-form-urlencoded:
schema: schema:
$ref: '#/components/schemas/PatchedUserMeUpdateRequest' $ref: '#/components/schemas/PatchedUserMeUpdateRequest'
multipart/form-data:
application/json:
schema: schema:
$ref: '#/components/schemas/PatchedUserMeUpdateRequest' $ref: '#/components/schemas/PatchedUserMeUpdateRequest'
security: security:
@ -1474,32 +1703,27 @@ paths:
/api/v1/auth/token/: /api/v1/auth/token/:
post: post:
operationId: v1_auth_token_create operationId: v1_auth_token_create
description: |-
Takes a set of user credentials and returns an access and refresh JSON web
token pair to prove the authentication of those credentials.
description: Takes a set of user credentials (either 'username' or 'email' +
password) and returns access & refresh token pair.
tags: tags:
- v1 - v1
requestBody: requestBody:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/TokenObtainPairRequest'
$ref: '#/components/schemas/CustomTokenObtainPairRequest'
application/x-www-form-urlencoded: application/x-www-form-urlencoded:
schema: schema:
$ref: '#/components/schemas/TokenObtainPairRequest'
$ref: '#/components/schemas/CustomTokenObtainPairRequest'
multipart/form-data: multipart/form-data:
schema: schema:
$ref: '#/components/schemas/TokenObtainPairRequest'
$ref: '#/components/schemas/CustomTokenObtainPairRequest'
required: true required: true
security: security:
- jwtAuth: [] - jwtAuth: []
responses: responses:
'200': '200':
content:
application/json:
schema:
$ref: '#/components/schemas/TokenObtainPair'
description: ''
description: No response body
/api/v1/auth/token/refresh/: /api/v1/auth/token/refresh/:
post: post:
operationId: v1_auth_token_refresh_create operationId: v1_auth_token_refresh_create
@ -6597,6 +6821,25 @@ components:
- is_completed - is_completed
- progress_percentage - progress_percentage
- total_lessons_count - total_lessons_count
CustomTokenObtainPairRequest:
type: object
description: JWT token obtain serializer that accepts either 'username' or 'email'
as the credential.
properties:
email:
type: string
writeOnly: true
minLength: 1
password:
type: string
writeOnly: true
minLength: 1
username:
type: string
writeOnly: true
minLength: 1
required:
- password
DeviceOsEnum: DeviceOsEnum:
enum: enum:
- android - android
@ -7480,14 +7723,6 @@ components:
* `registered` - Registered * `registered` - Registered
* `attended` - Attended * `attended` - Attended
* `cancelled` - Cancelled * `cancelled` - Cancelled
ExchangeToken:
type: object
properties:
temp_token:
type: string
maxLength: 128
required:
- temp_token
ExchangeTokenRequest: ExchangeTokenRequest:
type: object type: object
properties: properties:
@ -10707,11 +10942,21 @@ components:
title: Full Name title: Full Name
description: Full name of the user. description: Full name of the user.
maxLength: 255 maxLength: 255
first_name:
type: string
last_name:
type: string
name:
type: string
writeOnly: true
phone_number: phone_number:
type: string type: string
nullable: true nullable: true
description: e.g., +1 555 1234567 description: e.g., +1 555 1234567
maxLength: 128 maxLength: 128
phone:
type: string
writeOnly: true
avatar: avatar:
type: string type: string
format: binary format: binary
@ -10720,7 +10965,29 @@ components:
type: string type: string
nullable: true nullable: true
languages: {} languages: {}
preferred_language:
type: string
writeOnly: true
skills: {} skills: {}
expertise:
type: array
items:
type: string
minLength: 1
writeOnly: true
title:
type: string
rank:
type: string
institution:
type: string
writeOnly: true
institution_name:
type: string
website:
type: string
address:
type: string
country: country:
type: string type: string
nullable: true nullable: true
@ -10746,9 +11013,12 @@ components:
type: string type: string
minLength: 1 minLength: 1
description: Firebase Cloud Messaging token. description: Firebase Cloud Messaging token.
first_name:
type: string
last_name:
type: string
fullname: fullname:
type: string type: string
minLength: 1
avatar: avatar:
type: string type: string
nullable: true nullable: true
@ -11938,9 +12208,14 @@ components:
type: string type: string
writeOnly: true writeOnly: true
minLength: 6 minLength: 6
first_name:
type: string
maxLength: 150
last_name:
type: string
maxLength: 150
fullname: fullname:
type: string type: string
minLength: 1
maxLength: 255 maxLength: 255
phone_number: phone_number:
type: string type: string
@ -11969,7 +12244,6 @@ components:
maxLength: 255 maxLength: 255
required: required:
- email - email
- fullname
- password - password
RegisterResponse: RegisterResponse:
type: object type: object
@ -12723,32 +12997,6 @@ components:
- average_satisfaction_rating - average_satisfaction_rating
- open_tickets - open_tickets
- total_tickets - total_tickets
TokenObtainPair:
type: object
properties:
access:
type: string
readOnly: true
refresh:
type: string
readOnly: true
required:
- access
- refresh
TokenObtainPairRequest:
type: object
properties:
email:
type: string
writeOnly: true
minLength: 1
password:
type: string
writeOnly: true
minLength: 1
required:
- email
- password
TokenRefresh: TokenRefresh:
type: object type: object
properties: properties:
@ -12816,26 +13064,6 @@ components:
nullable: true nullable: true
title: FCM Token title: FCM Token
maxLength: 512 maxLength: 512
UserGuest:
type: object
properties:
device_id:
type: string
fcm:
type: string
device_os:
$ref: '#/components/schemas/DeviceOsEnum'
lat:
type: string
nullable: true
maxLength: 255
lon:
type: string
nullable: true
maxLength: 255
timezone:
type: string
nullable: true
UserGuestRequest: UserGuestRequest:
type: object type: object
properties: properties:
@ -12858,35 +13086,6 @@ components:
timezone: timezone:
type: string type: string
nullable: true nullable: true
UserLogin:
type: object
properties:
token:
type: string
readOnly: true
nullable: true
fullname:
type: string
readOnly: true
nullable: true
avatar:
type: string
readOnly: true
nullable: true
password:
type: string
fcm:
type: string
device_id:
type: string
timezone:
type: string
nullable: true
required:
- avatar
- fullname
- password
- token
UserLoginRequest: UserLoginRequest:
type: object type: object
properties: properties:
@ -12933,11 +13132,23 @@ components:
title: Full Name title: Full Name
description: Full name of the user. description: Full name of the user.
maxLength: 255 maxLength: 255
first_name:
type: string
readOnly: true
last_name:
type: string
readOnly: true
name:
type: string
readOnly: true
phone_number: phone_number:
type: string type: string
nullable: true nullable: true
description: e.g., +1 555 1234567 description: e.g., +1 555 1234567
maxLength: 128 maxLength: 128
phone:
type: string
readOnly: true
user_type: user_type:
allOf: allOf:
- $ref: '#/components/schemas/UserTypeEnum' - $ref: '#/components/schemas/UserTypeEnum'
@ -12950,7 +13161,32 @@ components:
type: string type: string
nullable: true nullable: true
languages: {} languages: {}
preferred_language:
type: string
readOnly: true
skills: {} skills: {}
title:
type: string
readOnly: true
rank:
type: string
readOnly: true
position:
type: string
readOnly: true
institution:
type: string
readOnly: true
institution_id:
type: integer
nullable: true
readOnly: true
website:
type: string
readOnly: true
address:
type: string
readOnly: true
country: country:
type: string type: string
nullable: true nullable: true
@ -12996,17 +13232,29 @@ components:
type: boolean type: boolean
readOnly: true readOnly: true
required: required:
- address
- date_joined - date_joined
- email - email
- first_name
- id - id
- institution
- institution_id
- is_active - is_active
- is_staff - is_staff
- is_verified - is_verified
- last_name
- name
- permissions - permissions
- phone
- position
- preferred_language
- rank
- roles - roles
- title
- user_type - user_type
- username - username
- verification_status - verification_status
- website
UserProfile: UserProfile:
type: object type: object
properties: properties:
@ -13020,6 +13268,10 @@ components:
fcm: fcm:
type: string type: string
description: Firebase Cloud Messaging token. description: Firebase Cloud Messaging token.
first_name:
type: string
last_name:
type: string
fullname: fullname:
type: string type: string
slug: slug:
@ -13092,9 +13344,12 @@ components:
type: string type: string
minLength: 1 minLength: 1
description: Firebase Cloud Messaging token. description: Firebase Cloud Messaging token.
first_name:
type: string
last_name:
type: string
fullname: fullname:
type: string type: string
minLength: 1
avatar: avatar:
type: string type: string
nullable: true nullable: true
@ -13129,17 +13384,6 @@ components:
* `male` - Male * `male` - Male
* `female` - Female * `female` - Female
* `other` - Other * `other` - Other
UserRecoverPassword:
type: object
description: |-
Validates that an email is provided and is in a valid format
without checking for database uniqueness.
properties:
email:
type: string
format: email
required:
- email
UserRecoverPasswordRequest: UserRecoverPasswordRequest:
type: object type: object
description: |- description: |-
@ -13152,31 +13396,13 @@ components:
minLength: 1 minLength: 1
required: required:
- email - email
UserRegister:
UserRegisterRequest:
type: object type: object
properties: properties:
id:
type: integer
readOnly: true
fullname:
first_name:
type: string type: string
nullable: true
title: Full Name
description: Full name of the user.
maxLength: 255
email:
last_name:
type: string type: string
format: email
fcm:
type: string
nullable: true
required:
- email
- fullname
- id
UserRegisterRequest:
type: object
properties:
fullname: fullname:
type: string type: string
nullable: true nullable: true
@ -13200,7 +13426,6 @@ components:
nullable: true nullable: true
required: required:
- email - email
- fullname
UserResetPasswordRequest: UserResetPasswordRequest:
type: object type: object
properties: properties:
@ -13234,21 +13459,6 @@ components:
* `institution_admin` - Institution Admin * `institution_admin` - Institution Admin
* `editor` - Editor * `editor` - Editor
* `viewer` - Viewer * `viewer` - Viewer
UserVerify:
type: object
properties:
code:
type: string
maxLength: 6
email:
type: string
format: email
device_id:
type: string
maxLength: 255
required:
- code
- email
UserVerifyRequest: UserVerifyRequest:
type: object type: object
properties: properties:
@ -13302,11 +13512,6 @@ components:
readOnly: true readOnly: true
nullable: true nullable: true
title: Admin Notes / Feedback title: Admin Notes / Feedback
reviewed_by_email:
type: string
format: email
readOnly: true
nullable: true
uploaded_at: uploaded_at:
type: string type: string
format: date-time format: date-time
@ -13322,7 +13527,6 @@ components:
- document_type_display - document_type_display
- id - id
- reviewed_at - reviewed_at
- reviewed_by_email
- status - status
- status_display - status_display
- uploaded_at - uploaded_at
@ -13362,28 +13566,6 @@ components:
* `pending` - Pending Review * `pending` - Pending Review
* `approved` - Approved & Verified * `approved` - Approved & Verified
* `rejected` - Rejected * `rejected` - Rejected
WebUserRegister:
type: object
properties:
id:
type: integer
readOnly: true
fullname:
type: string
nullable: true
title: Full Name
description: Full name of the user.
maxLength: 255
email:
type: string
format: email
fcm:
type: string
nullable: true
required:
- email
- fullname
- id
WebUserRegisterRequest: WebUserRegisterRequest:
type: object type: object
properties: properties:

16
utils/pagination.py

@ -5,7 +5,21 @@ from rest_framework.response import Response
class StandardResultsSetPagination(PageNumberPagination): class StandardResultsSetPagination(PageNumberPagination):
page_size = 16 page_size = 16
page_size_query_param = 'page_size' page_size_query_param = 'page_size'
max_page_size = 100
max_page_size = 3000
def get_paginated_response(self, data):
return Response({
'count': self.page.paginator.count,
'next': self.get_next_link(),
'previous': self.get_previous_link(),
'results': data,
})
class InstitutionPagination(PageNumberPagination):
page_size = 500
page_size_query_param = 'page_size'
max_page_size = 3000
def get_paginated_response(self, data): def get_paginated_response(self, data):
return Response({ return Response({

Loading…
Cancel
Save