Browse Source

add hadis corrections script and update serializers.

master
Mohsen Taba 1 month ago
parent
commit
522b9f1d59
  1. 38
      apps/hadis/management/commands/set_hadiscorrection_fixed_text.py
  2. 34
      apps/hadis/serializers/category.py
  3. 1
      entrypoint.sh
  4. 87
      recorder_integration_report.md

38
apps/hadis/management/commands/set_hadiscorrection_fixed_text.py

@ -0,0 +1,38 @@
from django.core.management.base import BaseCommand
from apps.hadis.models import HadisCorrection
ARABIC_TEXT = """بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ
الْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ، وَالصَّلَاةُ وَالسَّلَامُ عَلَى سَيِّدِنَا مُحَمَّدٍ وَآلِهِ الطَّاهِرِينَ.
اللَّهُمَّ زَيِّنَّا بِالْعِلْمِ وَالْحِلْمِ، وَاجْعَلْ أَلْسِنَتَنَا ذَاكِرَةً لَكَ وَقُلُوبَنَا خَاشِعَةً لَكَ.
وَارْزُقْنَا صِدْقَ الْقَوْلِ وَحُسْنَ الْعَمَلِ، إِنَّكَ أَنْتَ الْوَهَّابُ الْكَرِيمُ."""
class Command(BaseCommand):
help = "Set a fixed Arabic text for all HadisCorrection records."
def handle(self, *args, **options):
corrections = HadisCorrection.objects.all()
total = corrections.count()
if total == 0:
self.stdout.write(self.style.WARNING("No HadisCorrection records found."))
return
self.stdout.write(self.style.WARNING(f"Updating {total} HadisCorrection records..."))
updated_count = 0
for correction in corrections:
correction.text = ARABIC_TEXT
correction.save()
updated_count += 1
if updated_count % 100 == 0:
self.stdout.write(f"Updated {updated_count}/{total}...")
self.stdout.write(
self.style.SUCCESS(
f"Successfully updated {updated_count} HadisCorrection records."
)
)

34
apps/hadis/serializers/category.py

@ -6,11 +6,21 @@ from django.utils.translation import gettext_lazy as _
from ..models import HadisSect, HadisCategory, Hadis , HadisCategory
from django.utils.translation import get_language
def _get_localized_item_value(item):
"""
Prefer `title`, but fall back to `text` for older/mis-shaped localized entries.
"""
if not isinstance(item, dict):
return None
return item.get("title") or item.get("text")
def get_localized_text(json_list, request=None, fallback_lang="en", language_code=None):
"""
Extract localized text from a JSON list based on language.
Expects: [{"language_code": "en", "text": "..."}, ...]
Supports items shaped like:
- {"language_code": "en", "title": "..."}
- {"language_code": "en", "text": "..."}
Returns: Single text string or None
"""
if not json_list or not isinstance(json_list, list):
@ -25,16 +35,20 @@ def get_localized_text(json_list, request=None, fallback_lang="en", language_cod
# 1) Exact match
for item in json_list:
if isinstance(item, dict) and item.get("language_code") == language_code:
return item.get("title")
value = _get_localized_item_value(item)
if value:
return value
# 2) Fallback to English
for item in json_list:
if isinstance(item, dict) and item.get("language_code") == "en":
return item.get("title")
value = _get_localized_item_value(item)
if value:
return value
# 3) First available
if json_list and isinstance(json_list[0], dict):
return json_list[0].get("title")
return _get_localized_item_value(json_list[0])
return None
@ -47,7 +61,7 @@ class LocalizedField(serializers.Field):
"""
def to_representation(self, value):
# Expecting value to be a list of {"language_code": "...", "text": "..."}
# Expecting value to be a list of localized dicts using `title` or `text`
if not value or not isinstance(value, list):
return None
@ -60,16 +74,20 @@ class LocalizedField(serializers.Field):
# 1) Exact match with request language
for item in value:
if item.get("language_code") == language_code:
return item.get("title")
localized_value = _get_localized_item_value(item)
if localized_value:
return localized_value
# 2) Fallback to English
for item in value:
if item.get("language_code") == "en":
return item.get("title")
localized_value = _get_localized_item_value(item)
if localized_value:
return localized_value
# 3) Fallback to first item
first = value[0]
return first.get("title") if isinstance(first, dict) else None
return _get_localized_item_value(first)
class SimpleCategory(serializers.ModelSerializer):
title = LocalizedField()

1
entrypoint.sh

@ -5,6 +5,7 @@ python manage.py migrate
# python manage.py seed_images
# python manage.py compilemessages
python manage.py collectstatic --noinput
python manage.py set_hadiscorrection_fixed_text
# Seed Russian data (only runs once, skips if data exists)
# python manage.py seed_corrections
# python manage.py seed_russian_data

87
recorder_integration_report.md

@ -0,0 +1,87 @@
# گزارش ادغام رکوردر با سیستم PlugNMeet و Django
این گزارش شامل تمام مراحل فنی طی شده جهت راه‌اندازی، اتصال، رفع باگ و بهینه‌سازی رکوردر جلسات آنلاین (`plugNmeet-recorder`) و ادغام کامل آن با پنل مدیریتی جنگو است.
---
## ۱. راه‌اندازی و شبیه‌سازی محلی (Cloning & Docker Setup)
برای بالا آوردن سیستم ضبط به صورت ایزوله و ارتباط آن با سرور کلاس‌ها، کارهای زیر انجام شد:
* **شبیه‌سازی رکوردر**: مخزن رسمی رکوردر در مسیر موازی پروژه تحت عنوان `plugNmeet-recorder` کلون گردید.
* **پیکربندی شبکه (Docker Compose)**: در فایل `docker-compose.yml` مربوط به سرور ویدیو کنفرانس، کانتینر رکوردر به کانتینرهای دیگر متصل شد:
* به کانتینرها اجازه دسترسی به شبکه اشتراکی پروژه (`imam-javad_backend_imam-javad`) داده شد تا رکوردر بتواند به وب‌هوک جنگو و سرور nats متصل شود.
* با استفاده از `extra_hosts` آی‌پی دامنه کلاس‌ها به گیت‌وی کانتینرها مپ شد تا مرورگر داخلی رکوردر (Chrome بدون واسط گرافیکی) بتواند بدون مشکل با دامنه کلاس کار کند:
```yaml
extra_hosts:
- "meet.imamjavad.online:host-gateway"
```
* تنظیم ولوم مشترک برای فایل‌های ذخیره‌شده ضبط (`plugnmeet-recordings`) صورت گرفت.
---
## ۲. تنظیمات رکوردر (`recorder_config.yaml`)
پیکربندی فایل تنظیمات رکوردر برای سازگاری کامل با سیستم محلی به شرح زیر انجام شد:
* تنظیم شناسه رکوردر به `node_01` و حالت اجرایی به `both` (انجام همزمان ضبط زنده و فشرده‌سازی پس از اتمام ضبط).
* اتصال به وب‌سرویس nats از طریق:
```yaml
nats_info:
nats_urls:
- "nats://plugnmeet-nats:4222"
```
* تنظیم اعتبارسنجی اتصال به API در بخش `plugNmeet_info` با استفاده از `api_key` و `api_secret` مشترک و هماهنگ‌شده با جنگو.
* آدرس‌دهی فایل خروجی رکوردر به پوشه اشتراکی `/recording_files`.
---
## ۳. پیاده‌سازی گیرنده وب‌هوک زنده (Django Webhook)
در فایل [webhook.py](file:///f:/WORK/CODE/WORK/imam-javad/backend/apps/course/views/webhook.py)، تغییرات اساسی جهت دریافت اطلاعات فایل نهایی و وضعیت کاربران اعمال شد:
### الف) رفع باگ مسیر رویداد ضبط (Webhook Routing)
سرور PlugNMeet رویداد اتمام ضبط و آماده‌سازی فایل فشرده‌شده را با کلید `recording_proceeded` (حروف کوچک) ارسال می‌کرد. مسیردهی متناظر در جنگو اصلاح شد تا متد پردازشی درست فراخوانی گردد.
### ب) اعتبارسنجی دوگانه امنیت (Signature Verification)
بررسی امنیتی پیام‌های دریافتی مجهز به دو لایه شد:
1. **اعتبارسنجی JWT (استاندارد PlugNMeet)**: بررسی صحت امضا با توکن رمزنگاری‌شده با الگوریتم HS256 و کلید `PLUGNMEET_API_SECRET`.
2. **بررسی پشتیبان HMAC-SHA256**: برای سناریوهای تستی و کلاینت‌های قدیمی که توکن JWT خام ارسال می‌کنند.
### ج) دانلود و پردازش خودکار فایل ویدیویی
درون متد `_handle_recording_proceeded` فرآیند زیر پیاده شد:
1. **دانلود امن**: توکن موقت دانلود از API دریافت شده و فایل ویدیویی از آدرس وب‌سرویس دانلود و در حافظه موقت سیستم ذخیره می‌گردد.
2. **محاسبه مدت زمان ویدیو (Video Duration)**: با اجرای ابزار سیستم `ffprobe` در پس‌زمینه، طول دقیق ویدیو به صورت پویا استخراج می‌شود.
3. **ثبت در پایگاه داده**: فایل در مدل `LiveSessionRecording` ذخیره می‌گردد.
4. **تولید خودکار کاور تصویر (Thumbnail)**: با استفاده از دستورات `ffmpeg` فریمی در ثانیه اول ویدیو استخراج و تغییر سایز داده شده و به عنوان پیش‌نمایش در فیلد مربوطه ذخیره می‌شود.
---
## ۴. ثبت خودکار درس در دوره آموزشی (Django Signals)
پس از ثبت فایل ضبط شده، یک پروسه اتوماتیک در فایل [signals.py](file:///f:/WORK/CODE/WORK/imam-javad/backend/apps/course/signals.py) از طریق سیگنال `post_save` روی مدل `LiveSessionRecording` فعال می‌شود:
1. **جلوگیری از ثبت تکراری**: با فیلتر کردن نام فایلی که ثبت می‌شود، تضمین می‌گردد وب‌هوک‌های همزمان یا ذخیره‌سازی‌های مجدد باعث ایجاد درس‌های تکراری نشوند.
2. **ساخت هوشمند چپتر**: تاریخ ضبط جلسه استخراج شده و به عنوان نام چپتر (به فرمت `YYYY-MM-DD`) ثبت می‌شود. در صورتی که این چپتر از قبل برای این دوره وجود داشته باشد، از همان چپتر استفاده می‌شود.
3. **نام‌گذاری خودکار درس‌ها (Part n)**:
* تعداد درس‌های موجود در **همان چپتر مشخص** شمرده می‌شود.
* بر اساس فرمول `تعداد درس‌ها + ۱` نام درس به عنوان بخش بعدی (مثلا `Part 4` در صورتی که ۳ درس در چپتر باشد) تعیین می‌شود.
4. **ساخت شیء درس و انتساب به دوره**: آبجکت `Lesson` ساخته شده و به مدل واسط `CourseLesson` مرتبط می‌گردد.
---
## ۵. سیستم پایش حضور و غیاب مطمئن (Fail-safe Participant Tracking)
وب‌هوک‌های LiveKit مربوط به ملحق شدن و خروج کاربران در زمان تست‌های محلی ممکن است به دلیل مسائل شبکه یا فایروال به درستی کار نکنند. به همین منظور مکانیزم پشتیبان (Fail-safe) زیر طراحی شد:
* **ثبت زودهنگام حضور (در [live_session.py](file:///f:/WORK/CODE/WORK/imam-javad/backend/apps/course/views/live_session.py))**:
در متد `CourseLiveSessionTokenAPIView.post` که توکن دسترسی برای کاربران تولید می‌کند، درست قبل از ورود کاربر به کلاس، یک رکورد حضور در مدل `LiveSessionUser` با وضعیت آنلاین ایجاد/بروزرسانی می‌شود.
* **ثبت خودکار خروج در زمان بسته شدن کلاس**:
در متد پایش فعالِ کلاس (`_verify_room_is_active`) و متد دریافت اتمام رویداد وب‌هوک، به محض اینکه سرور تشخیص دهد کلاس به اتمام رسیده است، وضعیت تمام کاربران حاضر در آن جلسه به صورت خودکار آفلاین (`is_online=False`) شده و زمان خروج آن‌ها در فیلد `exited_at` ثبت می‌گردد.
* **نادیده گرفتن ربات‌های سیستمی**:
رویدادهای کاربرانی نظیر `RECORDER_BOT` (که مرورگر رکوردر است) به دلیل عدم همخوانی با شناسه‌های کاربری عددی از محاسبات پایگاه داده فیلتر و نادیده گرفته می‌شوند تا پایگاه داده با خطای تبدیل نوع (Type Casting) روبرو نشود.
---
> [!TIP]
> برای اجرای تست سناریوها در محیط توسعه محلی بدون نیاز به اجرای دستی مرورگرها، می‌توانید از اسکریپت به‌روزشده `backend/scripts/test_webhook.py` همراه با شناسه رویدادهای مختلف نظیر `RECORDING_PROCEEDED` استفاده نمایید.
Loading…
Cancel
Save