Browse Source

feat(hadis): add single-file sqlite export and optimize sync serializers

- Optimize HadisSyncSerializer: remove unused number, address_details, and book descriptions
- Separate sync serializers for corrections and interpretations (omitting redundant hadis_info and category_info)
- Add HadisDatabaseVersion model and migration for tracking offline SQLite DB
- Add export_hadis_sqlite management command with gzip compression and single-file overwrite
- Add GET /api/hadis/sync/database/ endpoint and enhance version serializers
- Automatically trigger background SQLite database export on version increment in admin
master
Mohsen Taba 1 week ago
parent
commit
354a64c742
  1. 21
      apps/hadis/admin/version.py
  2. 415
      apps/hadis/management/commands/export_hadis_sqlite.py
  3. 34
      apps/hadis/migrations/0040_hadisdatabaseversion.py
  4. 28
      apps/hadis/models/version.py
  5. 35
      apps/hadis/serializers/hadis.py
  6. 42
      apps/hadis/serializers/version.py
  7. 3
      apps/hadis/urls.py
  8. 25
      apps/hadis/views/version.py
  9. 25
      apps/hadis/views_admin.py

21
apps/hadis/admin/version.py

@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin
from utils.admin import dovoodi_admin_site
from ..models import ContentRelease
from ..models import ContentRelease, HadisDatabaseVersion
class ContentReleaseAdmin(ModelAdmin):
@ -25,5 +25,22 @@ class ContentReleaseAdmin(ModelAdmin):
)
# Register model with the custom admin site
class HadisDatabaseVersionAdmin(ModelAdmin):
"""Admin for HadisDatabaseVersion SQLite files"""
list_display = ('version', 'file_size_display', 'hadis_count', 'categories_count', 'is_active', 'created_at')
list_filter = ('is_active', 'created_at')
search_fields = ('version', 'notes', 'checksum_md5')
readonly_fields = ('created_at', 'checksum_md5', 'file_size')
ordering = ('-version',)
def file_size_display(self, obj):
if obj.file_size:
return f"{obj.file_size / (1024 * 1024):.2f} MB"
return "0 MB"
file_size_display.short_description = _("حجم فایل")
# Register models with the custom admin site
dovoodi_admin_site.register(ContentRelease, ContentReleaseAdmin)
dovoodi_admin_site.register(HadisDatabaseVersion, HadisDatabaseVersionAdmin)

415
apps/hadis/management/commands/export_hadis_sqlite.py

@ -0,0 +1,415 @@
import os
import gzip
import json
import sqlite3
import hashlib
from pathlib import Path
from django.core.management.base import BaseCommand
from django.conf import settings
from django.db import transaction
from apps.hadis.models import (
Hadis,
HadisCategory,
HadisInterpretation,
HadisCorrection,
HadisDatabaseVersion
)
class Command(BaseCommand):
help = "Export Hadis and Categories into an optimized, compressed SQLite database for offline mobile sync."
def add_arguments(self, parser):
parser.add_argument(
'--notes',
type=str,
default='',
help='Release notes for this SQLite database version'
)
parser.add_argument(
'--force-version',
type=int,
default=None,
help='Force a specific version number'
)
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS("Starting Hadis SQLite database generation (Single-File Architecture)..."))
# 1. Determine destination and version number
latest_ver = HadisDatabaseVersion.objects.order_by('-version').first()
if options['force_version'] is not None:
new_version = options['force_version']
else:
new_version = (latest_ver.version + 1) if latest_ver else 1
output_dir = Path(settings.MEDIA_ROOT) / 'hadis_db'
output_dir.mkdir(parents=True, exist_ok=True)
temp_db_path = output_dir / "temp_hadis_build.sqlite"
final_gz_name = "hadis_offline.sqlite.gz"
final_gz_path = output_dir / final_gz_name
if temp_db_path.exists():
temp_db_path.unlink()
# 2. Initialize SQLite database
conn = sqlite3.connect(str(temp_db_path))
cursor = conn.cursor()
# Optimize SQLite for bulk writes
cursor.execute("PRAGMA synchronous = OFF;")
cursor.execute("PRAGMA journal_mode = MEMORY;")
cursor.execute("PRAGMA encoding = 'UTF-8';")
self.stdout.write("Creating SQLite tables and indexes...")
self._create_tables(cursor)
# 3. Export data
categories_count = self._export_categories(cursor)
interp_count = self._export_category_interpretations(cursor)
hadis_count, corr_count = self._export_hadiths_and_corrections(cursor)
conn.commit()
# Create indexes after insertion for maximum performance
self.stdout.write("Building indexes...")
self._create_indexes(cursor)
conn.commit()
conn.close()
# 4. Gzip compress directly to the single target file
self.stdout.write(f"Compressing database to single file: {final_gz_name}...")
hasher = hashlib.md5()
temp_gz_path = output_dir / "temp_hadis_build.sqlite.gz"
with open(temp_db_path, 'rb') as f_in:
with gzip.open(temp_gz_path, 'wb', compresslevel=9) as f_out:
while chunk := f_in.read(1024 * 1024):
f_out.write(chunk)
hasher.update(chunk)
checksum = hasher.hexdigest()
temp_size = temp_db_path.stat().st_size
# Atomic replacement: replace final_gz_path with temp_gz_path
if final_gz_path.exists():
final_gz_path.unlink()
temp_gz_path.rename(final_gz_path)
file_size = final_gz_path.stat().st_size
# Clean up any remaining temp files in output_dir
temp_db_path.unlink(missing_ok=True)
for extra_file in output_dir.iterdir():
if extra_file.name != final_gz_name:
try:
extra_file.unlink(missing_ok=True)
except OSError:
pass
# 5. Record version in database (Single Active Record)
with transaction.atomic():
# Delete old records to keep DB clean
HadisDatabaseVersion.objects.all().delete()
db_version = HadisDatabaseVersion.objects.create(
version=new_version,
file=f"hadis_db/{final_gz_name}",
file_size=file_size,
checksum_md5=checksum,
hadis_count=hadis_count,
categories_count=categories_count,
is_active=True,
notes=options['notes'],
)
self.stdout.write(self.style.SUCCESS(
f"Successfully updated single Hadis SQLite DB to v{new_version}!\n"
f" - Uncompressed size: {temp_size / (1024*1024):.2f} MB\n"
f" - Gzipped size: {file_size / (1024*1024):.2f} MB\n"
f" - Hadiths: {hadis_count}\n"
f" - Categories: {categories_count}\n"
f" - Interpretations: {interp_count}\n"
f" - Corrections: {corr_count}\n"
f" - MD5 Checksum: {checksum}\n"
f" - File path: {final_gz_path}"
))
def _create_tables(self, cursor):
cursor.executescript("""
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY,
title TEXT,
slug TEXT UNIQUE,
source_type TEXT,
sect_type TEXT
);
CREATE TABLE IF NOT EXISTS category_interpretations (
id INTEGER PRIMARY KEY,
category_id INTEGER,
title TEXT,
slug TEXT,
narrator TEXT,
text TEXT,
translation TEXT,
references_json TEXT,
links_json TEXT,
FOREIGN KEY (category_id) REFERENCES categories (id)
);
CREATE TABLE IF NOT EXISTS hadiths (
id INTEGER PRIMARY KEY,
slug TEXT UNIQUE,
category_id INTEGER,
category_slug TEXT,
title TEXT,
title_narrator TEXT,
text TEXT,
translation_json TEXT,
address TEXT,
share_link TEXT,
hadis_status_id INTEGER,
status_title TEXT,
status_color TEXT,
status_main_color_code TEXT,
status_text TEXT,
tags_json TEXT,
references_json TEXT,
reference_images_json TEXT,
narrators_json TEXT,
explanations_json TEXT,
FOREIGN KEY (category_id) REFERENCES categories (id)
);
CREATE TABLE IF NOT EXISTS hadith_corrections (
id INTEGER PRIMARY KEY,
hadis_id INTEGER,
title TEXT,
slug TEXT,
narrator TEXT,
description TEXT,
translation TEXT,
share_link TEXT,
address TEXT,
images_json TEXT,
references_json TEXT,
links_json TEXT,
FOREIGN KEY (hadis_id) REFERENCES hadiths (id)
);
""")
def _create_indexes(self, cursor):
cursor.executescript("""
CREATE INDEX IF NOT EXISTS idx_cat_slug ON categories(slug);
CREATE INDEX IF NOT EXISTS idx_interp_cat ON category_interpretations(category_id);
CREATE INDEX IF NOT EXISTS idx_hadith_cat ON hadiths(category_id);
CREATE INDEX IF NOT EXISTS idx_hadith_slug ON hadiths(slug);
CREATE INDEX IF NOT EXISTS idx_corr_hadis ON hadith_corrections(hadis_id);
""")
def _export_categories(self, cursor):
cats = HadisCategory.objects.all()
rows = [
(
c.id,
json.dumps(c.title, ensure_ascii=False) if isinstance(c.title, (dict, list)) else (c.title or ''),
c.slug,
getattr(c, 'source_type', None),
getattr(c, 'sect_type', None),
)
for c in cats
]
cursor.executemany(
"INSERT INTO categories (id, title, slug, source_type, sect_type) VALUES (?, ?, ?, ?, ?)",
rows
)
return len(rows)
def _export_category_interpretations(self, cursor):
interps = HadisInterpretation.objects.select_related('category').prefetch_related(
'references__book_reference', 'references__images'
).all()
rows = []
for inp in interps:
refs_list = []
for ref in inp.references.all():
book = ref.book_reference
refs_list.append({
'id': ref.id,
'book_title': getattr(book, 'title', None),
'address': getattr(ref, 'address', None),
'edition': getattr(ref, 'edition_id', None),
'volume': getattr(ref, 'volume_id', None),
'pages': getattr(ref, 'pages', None),
'hadith_number': getattr(ref, 'hadith_number', None),
})
rows.append((
inp.id,
inp.category_id,
json.dumps(inp.title, ensure_ascii=False) if isinstance(inp.title, (dict, list)) else (inp.title or ''),
inp.slug,
inp.narrator or '',
inp.text or '',
json.dumps(inp.translation, ensure_ascii=False) if isinstance(inp.translation, (dict, list)) else (inp.translation or ''),
json.dumps(refs_list, ensure_ascii=False),
json.dumps(inp.links or [], ensure_ascii=False),
))
cursor.executemany(
"""INSERT INTO category_interpretations
(id, category_id, title, slug, narrator, text, translation, references_json, links_json)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
rows
)
return len(rows)
def _export_hadiths_and_corrections(self, cursor):
hadiths = (
Hadis.objects
.filter(status=True)
.select_related('category', 'hadis_status')
.prefetch_related(
'tags',
'references__book_reference__author',
'references__images',
'transmitters__transmitter__reliability',
'transmitters__uncertain_transmitters__reliability',
'transmitters__narrator_layer',
'hadiscorrection_set__references__images',
)
.order_by('id')
)
hadis_rows = []
correction_rows = []
for h in hadiths:
# 1. Status Block
status_id = h.hadis_status.id if h.hadis_status else None
status_title = h.hadis_status.title if h.hadis_status else None
status_color = h.hadis_status.color if h.hadis_status else None
status_code = h.hadis_status.main_color_code if h.hadis_status else None
# 2. Tags
tags_list = [{'id': t.id, 'title': t.title} for t in h.tags.all()]
# 3. References & Reference Images
refs_list = []
ref_imgs_list = []
for ref in h.references.all():
book = ref.book_reference
authors = [{'id': book.author.id, 'name': book.author.name}] if book and book.author else []
refs_list.append({
'id': ref.id,
'title': book.title if book else None,
'authors': authors,
'share_link': getattr(book, 'share_link', None) if book else None,
})
for img in ref.images.all():
ref_imgs_list.append({
'id': img.id,
'thumbnail': img.thumbnail.url if img.thumbnail else None,
'priority': img.priority,
})
# 4. Narrators
transmitters_data = []
for tr in h.transmitters.all():
t = tr.transmitter
rel = t.reliability if t else None
transmitters_data.append({
'id': t.id if t else None,
'name': t.full_name if t else None,
'slug': t.slug if t else None,
'layer_id': tr.narrator_layer_id,
'is_uncertain': tr.is_uncertain,
'reliability': {
'id': rel.id,
'title': rel.title,
'slug': rel.slug,
'color': rel.color,
'main_color_code': rel.main_color_code,
} if rel else None,
})
# 5. Explanations
explanations_data = []
if hasattr(h, 'explanations') and h.explanations:
for exp in h.explanations.all():
explanations_data.append({
'title': exp.title,
'detail': getattr(exp, 'detail', '')
})
hadis_rows.append((
h.id,
h.slug,
h.category_id,
h.category.slug if h.category else None,
json.dumps(h.title, ensure_ascii=False) if isinstance(h.title, (dict, list)) else (h.title or ''),
json.dumps(h.title_narrator, ensure_ascii=False) if isinstance(h.title_narrator, (dict, list)) else (h.title_narrator or ''),
h.text or '',
json.dumps(h.translation or [], ensure_ascii=False),
json.dumps(h.address, ensure_ascii=False) if isinstance(h.address, (dict, list)) else (h.address or ''),
h.share_link or '',
status_id,
json.dumps(status_title, ensure_ascii=False) if isinstance(status_title, (dict, list)) else (status_title or ''),
status_color or '',
status_code or '',
json.dumps(h.hadis_status_text, ensure_ascii=False) if isinstance(h.hadis_status_text, (dict, list)) else (h.hadis_status_text or ''),
json.dumps(tags_list, ensure_ascii=False),
json.dumps(refs_list, ensure_ascii=False),
json.dumps(ref_imgs_list, ensure_ascii=False),
json.dumps({'transmitters': transmitters_data}, ensure_ascii=False),
json.dumps(explanations_data, ensure_ascii=False),
))
# 6. Corrections
for corr in h.hadiscorrection_set.all():
corr_imgs = []
corr_refs = []
for cref in corr.references.all():
for cimg in cref.images.all():
if cimg.image:
corr_imgs.append({'id': cimg.id, 'image': cimg.image.url})
corr_refs.append({
'id': cref.id,
'address': getattr(cref, 'address', None),
})
correction_rows.append((
corr.id,
h.id,
json.dumps(corr.title, ensure_ascii=False) if isinstance(corr.title, (dict, list)) else (corr.title or ''),
corr.slug,
corr.narrator or '',
corr.text or '',
json.dumps(corr.translation, ensure_ascii=False) if isinstance(corr.translation, (dict, list)) else (corr.translation or ''),
corr.share_link or '',
corr.address if hasattr(corr, 'address') else '',
json.dumps(corr_imgs, ensure_ascii=False),
json.dumps(corr_refs, ensure_ascii=False),
json.dumps(corr.links or [], ensure_ascii=False),
))
cursor.executemany(
"""INSERT INTO hadiths (
id, slug, category_id, category_slug, title, title_narrator, text,
translation_json, address, share_link, hadis_status_id, status_title,
status_color, status_main_color_code, status_text, tags_json,
references_json, reference_images_json, narrators_json, explanations_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
hadis_rows
)
cursor.executemany(
"""INSERT INTO hadith_corrections (
id, hadis_id, title, slug, narrator, description, translation,
share_link, address, images_json, references_json, links_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
correction_rows
)
return len(hadis_rows), len(correction_rows)

34
apps/hadis/migrations/0040_hadisdatabaseversion.py

@ -0,0 +1,34 @@
# Generated by Django on 2026-09-10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hadis', '0039_bookvolume_file'),
]
operations = [
migrations.CreateModel(
name='HadisDatabaseVersion',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('version', models.PositiveIntegerField(unique=True, verbose_name='نسخه دیتابیس')),
('file', models.FileField(upload_to='hadis_db/', verbose_name='فایل فشرده دیتابیس (.sqlite.gz)')),
('file_size', models.BigIntegerField(default=0, verbose_name='حجم فایل (بایت)')),
('checksum_md5', models.CharField(blank=True, max_length=64, verbose_name='هش MD5')),
('hadis_count', models.PositiveIntegerField(default=0, verbose_name='تعداد احادیث')),
('categories_count', models.PositiveIntegerField(default=0, verbose_name='تعداد دسته‌بندی‌ها')),
('is_active', models.BooleanField(default=True, verbose_name='نسخه فعال')),
('notes', models.TextField(blank=True, verbose_name='یادداشت‌های نسخه')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='آخرین به‌روزرسانی')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='زمان ایجاد')),
],
options={
'verbose_name': 'نسخه دیتابیس SQLite احادیث',
'verbose_name_plural': 'نسخه‌های دیتابیس SQLite احادیث',
'ordering': ['-version'],
},
),
]

28
apps/hadis/models/version.py

@ -1,5 +1,7 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
import os
class ContentRelease(models.Model):
version_name = models.CharField(max_length=50, verbose_name=_('Version Name')) # e.g., "v1.2 - Muharram Update"
@ -11,3 +13,29 @@ class ContentRelease(models.Model):
class Meta:
ordering = ['-published_at']
class HadisDatabaseVersion(models.Model):
"""مدل نگهداری نسخه فعلی فایل فشرده SQLite احادیث (Single-File Architecture)"""
version = models.PositiveIntegerField(default=1, verbose_name=_("شماره نسخه دیتابیس"))
file = models.FileField(upload_to='hadis_db/', verbose_name=_("فایل فشرده دیتابیس (.sqlite.gz)"))
file_size = models.BigIntegerField(default=0, verbose_name=_("حجم فایل (بایت)"))
checksum_md5 = models.CharField(max_length=64, blank=True, verbose_name=_("هش MD5"))
hadis_count = models.PositiveIntegerField(default=0, verbose_name=_("تعداد احادیث"))
categories_count = models.PositiveIntegerField(default=0, verbose_name=_("تعداد دسته‌بندی‌ها"))
is_active = models.BooleanField(default=True, verbose_name=_("نسخه فعال"))
notes = models.TextField(blank=True, verbose_name=_("یادداشت‌های نسخه"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("آخرین به‌روزرسانی"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("زمان ایجاد"))
class Meta:
ordering = ['-version']
verbose_name = _("نسخه دیتابیس SQLite احادیث")
verbose_name_plural = _("نسخه‌های دیتابیس SQLite احادیث")
def __str__(self):
return f"Hadis SQLite DB v{self.version} ({self.hadis_count} احادیث)"
@classmethod
def get_latest_active(cls):
return cls.objects.filter(is_active=True).order_by('-version').first()

35
apps/hadis/serializers/hadis.py

@ -128,7 +128,7 @@ class HadisSyncSerializer(serializers.ModelSerializer):
model = Hadis
fields = [
# header (no-extend)
'id', 'number', 'slug', 'category_id', 'category_slug', 'title', 'title_narrator', 'text', 'translation',
'id', 'slug', 'category_id', 'category_slug', 'title', 'title_narrator', 'text', 'translation',
# grouped sections
'detail', 'narrators', 'explanations', 'corrections', 'interpretations'
]
@ -170,7 +170,7 @@ class HadisSyncSerializer(serializers.ModelSerializer):
{'id': a.id, 'name': get_localized_text(a.name, request)}
for a in authors
],
'description': book.description if book else None,
'description': None,
'share_link': book.share_link if book else None,
})
@ -184,17 +184,9 @@ class HadisSyncSerializer(serializers.ModelSerializer):
'priority': img.priority,
})
# Process address_details
address_details_list = []
if hasattr(obj, 'address_details') and obj.address_details and isinstance(obj.address_details, list):
address_details_list = sorted(
obj.address_details,
key=lambda x: x.get('priority', 0)
)
return {
'address': get_localized_text(obj.address, request),
'address_details': address_details_list,
'address_details': [],
'hadis_status': status_block,
'status_text': get_localized_text(obj.hadis_status_text, request),
'share_link': obj.share_link,
@ -295,18 +287,17 @@ class HadisSyncSerializer(serializers.ModelSerializer):
return explanations_list if explanations_list else None
def get_corrections(self, obj):
"""گرفتن لیست کامل تصحیحات با تمام جزئیات و رفرنس‌ها"""
# استفاده از سریالایزر Detail که در مرحله قبل ساختیم
return HadisCorrectionDetailSerializer(
"""گرفتن لیست تصحیحات بهینه‌شده مخصوص سینک"""
return HadisSyncCorrectionSerializer(
obj.hadiscorrection_set.all(),
many=True,
context=self.context
).data
def get_interpretations(self, obj):
"""گرفتن لیست کامل تفاسیر متصل به دسته‌بندی این حدیث با جزئیات کامل"""
"""گرفتن لیست تفاسیر بهینه‌شده مخصوص سینک"""
if obj.category:
return HadisInterpretationDetailSerializer(
return HadisSyncInterpretationSerializer(
obj.category.interpretations.all(),
many=True,
context=self.context
@ -1737,6 +1728,18 @@ class HadisInterpretationDetailSerializer(serializers.ModelSerializer):
return images_list
class HadisSyncCorrectionSerializer(HadisCorrectionDetailSerializer):
"""نسخه سبک و بهینه تصحیحات مخصوص سینک بدون hadis_info"""
class Meta(HadisCorrectionDetailSerializer.Meta):
fields = ['id', 'title', 'slug', 'narrator', 'description', 'translation', 'share_link', 'address', 'images', 'references', 'links']
class HadisSyncInterpretationSerializer(HadisInterpretationDetailSerializer):
"""نسخه سبک و بهینه تفاسیر مخصوص سینک بدون category_info"""
class Meta(HadisInterpretationDetailSerializer.Meta):
fields = ['id', 'title', 'slug', 'narrator', 'text', 'translation', 'address', 'images', 'links', 'references']
class TransmitterOriginalTextDetailSerializer(serializers.ModelSerializer):
title = LocalizedField()
text = LocalizedField()

42
apps/hadis/serializers/version.py

@ -1,9 +1,38 @@
from rest_framework import serializers
from ..models import ContentRelease
from ..models import ContentRelease, HadisDatabaseVersion
from ..utils.urls import absolute_https_url
class HadisDatabaseVersionSerializer(serializers.ModelSerializer):
"""سریالایزر دریافت آخرین اطلاعات فایل فشرده دیتابیس SQLite جهت دانلود و سینک آفلاین"""
download_url = serializers.SerializerMethodField()
class Meta:
model = HadisDatabaseVersion
fields = [
'version',
'download_url',
'file_size',
'checksum_md5',
'hadis_count',
'categories_count',
'is_active',
'notes',
'created_at',
]
def get_download_url(self, obj):
if obj.file:
request = self.context.get('request')
if request:
return absolute_https_url(obj.file.url, request)
return obj.file.url
return None
class ContentReleaseSyncSerializer(serializers.ModelSerializer):
"""Serializer for syncing content release data for offline mode"""
"""Serializer for syncing content release data for offline mode with SQLite support"""
sqlite_database = serializers.SerializerMethodField()
class Meta:
model = ContentRelease
@ -12,5 +41,12 @@ class ContentReleaseSyncSerializer(serializers.ModelSerializer):
'version_name',
'published_at',
'description',
'is_active'
'is_active',
'sqlite_database',
]
def get_sqlite_database(self, obj):
latest_db = HadisDatabaseVersion.get_latest_active()
if latest_db:
return HadisDatabaseVersionSerializer(latest_db, context=self.context).data
return None

3
apps/hadis/urls.py

@ -11,7 +11,7 @@ from .views.transmitter import (
)
from .views.reference import BookDetailView, BookReferencesView, BookReferenceSyncView, BookAttributeView, ReferenceExcerptsListView
from .views.reference_v2 import BookReferenceV2DetailView, BookReferenceV2SyncView, BookReferenceV2ListView
from .views.version import ContentReleaseSyncView
from .views.version import ContentReleaseSyncView, HadisDatabaseVersionView
from .views.info import HadisInfoView
from .views_admin import (
AdminHadisCategoryViewSet, AdminHadisViewSet, AdminHadisCorrectionViewSet, AdminCorrectionReferenceViewSet, AdminHadisInterpretationViewSet, AdminHadisStatusViewSet, AdminHadisTagViewSet, AdminHadisCollectionViewSet,
@ -90,6 +90,7 @@ urlpatterns = [
path('sync/references/', cached_view(BookReferenceSyncView.as_view()), name='reference-sync'),
path('v2/sync/references/', cached_view(BookReferenceV2SyncView.as_view()), name='reference-sync-v2'),
path('sync/version/', ContentReleaseSyncView.as_view(), name='content-release-sync'),
path('sync/database/', HadisDatabaseVersionView.as_view(), name='hadis-database-sync'),
path('sync/filters/', cached_view(HadisFiltersSyncAPIView.as_view()), name='hadis-filters-sync'),
path('info/', cached_view(HadisInfoView.as_view()), name='hadis-info'),

25
apps/hadis/views/version.py

@ -1,8 +1,9 @@
from rest_framework.generics import RetrieveAPIView
from rest_framework.response import Response
from rest_framework import status
from ..models import ContentRelease
from ..serializers import ContentReleaseSyncSerializer
from ..models import ContentRelease, HadisDatabaseVersion
from ..serializers.version import ContentReleaseSyncSerializer, HadisDatabaseVersionSerializer
from ..docs import content_release_sync_swagger
@ -18,3 +19,23 @@ class ContentReleaseSyncView(RetrieveAPIView):
def get_object(self):
return ContentRelease.objects.filter(is_active=True).order_by('-published_at').first()
class HadisDatabaseVersionView(RetrieveAPIView):
"""
API view to get the latest pre-built SQLite database info and direct Nginx download URL
"""
serializer_class = HadisDatabaseVersionSerializer
def get_object(self):
return HadisDatabaseVersion.get_latest_active()
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
if not instance:
return Response(
{"detail": "No active SQLite database version found."},
status=status.HTTP_404_NOT_FOUND
)
serializer = self.get_serializer(instance)
return Response(serializer.data)

25
apps/hadis/views_admin.py

@ -990,6 +990,9 @@ class AdminContentReleaseViewSet(ModelViewSet):
from .signals import invalidate_hadis_cache
invalidate_hadis_cache()
# تریگر خودکار ساخت و فشرده‌سازی دیتابیس SQLite در پس‌زمینه
self._trigger_sqlite_export(notes=f"Auto-generated for version {version_name}")
return Response({
"status": "success",
"message": f"Version incremented to {version_name}",
@ -997,10 +1000,30 @@ class AdminContentReleaseViewSet(ModelViewSet):
"next_suggested_version": compute_next_version(version_name)
})
def _trigger_sqlite_export(self, notes=""):
"""اجرای پس‌زمینه دستور تولید دیتابیس SQLite بدون متوقف کردن پاسخ HTTP"""
import threading
import logging
from django.core.management import call_command
logger = logging.getLogger(__name__)
def _task():
try:
logger.info("Starting background SQLite export...")
call_command('export_hadis_sqlite', notes=notes)
logger.info("Background SQLite export completed successfully.")
except Exception as e:
logger.error(f"Failed to export SQLite in background: {e}", exc_info=True)
thread = threading.Thread(target=_task, daemon=True)
thread.start()
def perform_create(self, serializer):
super().perform_create(serializer)
instance = serializer.save()
from .signals import invalidate_hadis_cache
invalidate_hadis_cache()
self._trigger_sqlite_export(notes=f"Auto-generated for created version {instance.version_name}")
def perform_update(self, serializer):
super().perform_update(serializer)

Loading…
Cancel
Save