From 522b9f1d592a22977c29ada596a0bd66856d09d1 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Mon, 8 Jun 2026 09:54:50 +0330 Subject: [PATCH] add hadis corrections script and update serializers. --- .../set_hadiscorrection_fixed_text.py | 38 ++++++++ apps/hadis/serializers/category.py | 34 ++++++-- entrypoint.sh | 1 + recorder_integration_report.md | 87 +++++++++++++++++++ 4 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 apps/hadis/management/commands/set_hadiscorrection_fixed_text.py create mode 100644 recorder_integration_report.md diff --git a/apps/hadis/management/commands/set_hadiscorrection_fixed_text.py b/apps/hadis/management/commands/set_hadiscorrection_fixed_text.py new file mode 100644 index 0000000..a01c5b1 --- /dev/null +++ b/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." + ) + ) diff --git a/apps/hadis/serializers/category.py b/apps/hadis/serializers/category.py index f4830d4..2dbb7c1 100644 --- a/apps/hadis/serializers/category.py +++ b/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() diff --git a/entrypoint.sh b/entrypoint.sh index 3be9777..8f8d826 100755 --- a/entrypoint.sh +++ b/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 diff --git a/recorder_integration_report.md b/recorder_integration_report.md new file mode 100644 index 0000000..daf93cc --- /dev/null +++ b/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` استفاده نمایید.