|
|
@ -2,11 +2,11 @@ from rest_framework.generics import ListAPIView |
|
|
from rest_framework.response import Response |
|
|
from rest_framework.response import Response |
|
|
from django.shortcuts import get_object_or_404 |
|
|
from django.shortcuts import get_object_or_404 |
|
|
from utils.pagination import NoPagination |
|
|
from utils.pagination import NoPagination |
|
|
from django.db.models import Q |
|
|
|
|
|
|
|
|
from django.db.models import Q, Prefetch |
|
|
from collections import defaultdict |
|
|
from collections import defaultdict |
|
|
from utils.pagination import StandardResultsSetPagination |
|
|
from utils.pagination import StandardResultsSetPagination |
|
|
from utils import absolute_https_url |
|
|
from utils import absolute_https_url |
|
|
from ..models import HadisSect, HadisCategory,Hadis |
|
|
|
|
|
|
|
|
from ..models import HadisSect, HadisCategory, Hadis, HadisTransmitter |
|
|
from apps.bookmark.serializers.bookmark import BookmarkStatusSerializer |
|
|
from apps.bookmark.serializers.bookmark import BookmarkStatusSerializer |
|
|
from ..serializers import ( |
|
|
from ..serializers import ( |
|
|
HadisCategorySectListSerializer, |
|
|
HadisCategorySectListSerializer, |
|
|
@ -15,6 +15,7 @@ from ..serializers import ( |
|
|
HadisCategorySelectSerializer , |
|
|
HadisCategorySelectSerializer , |
|
|
HadisCategorySelectSourceSerializer, |
|
|
HadisCategorySelectSourceSerializer, |
|
|
get_localized_text, |
|
|
get_localized_text, |
|
|
|
|
|
get_arabic_localized_text, |
|
|
SimpleCategory |
|
|
SimpleCategory |
|
|
) |
|
|
) |
|
|
from ..docs import ( |
|
|
from ..docs import ( |
|
|
@ -506,65 +507,83 @@ def debug_headers(request): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from rest_framework.views import APIView |
|
|
from rest_framework.views import APIView |
|
|
|
|
|
|
|
|
class HadisCategoryXMindView(APIView): |
|
|
class HadisCategoryXMindView(APIView): |
|
|
""" |
|
|
""" |
|
|
Returns a mind-map JSON structure for a specific category and its hadiths. |
|
|
|
|
|
|
|
|
Returns a mind-map JSON structure for a specific category and its transmission chains ending in hadiths. |
|
|
Root -> Category |
|
|
Root -> Category |
|
|
Children -> Hadiths |
|
|
|
|
|
|
|
|
Level 1+ -> Narrators in transmission chains (merged prefixes) |
|
|
|
|
|
Leaves -> Hadiths |
|
|
""" |
|
|
""" |
|
|
|
|
|
|
|
|
def get_localized_text(self, json_field, lang): |
|
|
|
|
|
"""Helper to extract text from your JSON structure""" |
|
|
|
|
|
|
|
|
def get_localized(self, json_field, lang): |
|
|
if not json_field or not isinstance(json_field, list): |
|
|
if not json_field or not isinstance(json_field, list): |
|
|
return "Unknown" |
|
|
|
|
|
|
|
|
|
|
|
# 1. Try specific language |
|
|
|
|
|
|
|
|
return "" |
|
|
for item in json_field: |
|
|
for item in json_field: |
|
|
if item.get('language_code') == lang: |
|
|
|
|
|
|
|
|
if isinstance(item, dict) and item.get('language_code') == lang: |
|
|
return item.get('text', '') |
|
|
return item.get('text', '') |
|
|
|
|
|
|
|
|
# 2. Fallback to English |
|
|
|
|
|
for item in json_field: |
|
|
for item in json_field: |
|
|
if item.get('language_code') == 'en': |
|
|
|
|
|
|
|
|
if isinstance(item, dict) and item.get('language_code') == 'en': |
|
|
return item.get('text', '') |
|
|
return item.get('text', '') |
|
|
|
|
|
|
|
|
# 3. Fallback to first available |
|
|
|
|
|
if len(json_field) > 0: |
|
|
|
|
|
|
|
|
if len(json_field) > 0 and isinstance(json_field[0], dict): |
|
|
return json_field[0].get('text', '') |
|
|
return json_field[0].get('text', '') |
|
|
|
|
|
return "" |
|
|
|
|
|
|
|
|
return "Unknown" |
|
|
|
|
|
|
|
|
def get_arabic(self, json_field): |
|
|
|
|
|
return get_arabic_localized_text(json_field) or "" |
|
|
|
|
|
|
|
|
|
|
|
def _get_thumbnail_url(self, thumbnail, request): |
|
|
|
|
|
if not thumbnail: |
|
|
|
|
|
return None |
|
|
|
|
|
try: |
|
|
|
|
|
if request: |
|
|
|
|
|
return request.build_absolute_uri(thumbnail.url) |
|
|
|
|
|
except Exception: |
|
|
|
|
|
pass |
|
|
|
|
|
return thumbnail.url |
|
|
|
|
|
|
|
|
@hadis_category_xmind_swagger |
|
|
@hadis_category_xmind_swagger |
|
|
def get(self, request, category_slug): |
|
|
def get(self, request, category_slug): |
|
|
# 1. Determine Language (support ?lang=ru or Accept-Language header) |
|
|
|
|
|
lang = request.query_params.get('lang','en') |
|
|
|
|
|
|
|
|
|
|
|
# 2. Get the Category (Root Node) |
|
|
|
|
|
|
|
|
lang = request.query_params.get('lang', 'en') |
|
|
category = get_object_or_404(HadisCategory, slug=category_slug) |
|
|
category = get_object_or_404(HadisCategory, slug=category_slug) |
|
|
root_title = self.get_localized_text(category.title, lang) |
|
|
|
|
|
|
|
|
root_title = self.get_localized(category.title, lang) or category.slug |
|
|
|
|
|
|
|
|
|
|
|
user = request.user if request and hasattr(request, 'user') else None |
|
|
|
|
|
if user and user.is_anonymous: |
|
|
|
|
|
user = None |
|
|
|
|
|
|
|
|
|
|
|
# Fetch active hadiths with their transmitters and corrections |
|
|
|
|
|
transmitter_qs = HadisTransmitter.objects.select_related( |
|
|
|
|
|
'transmitter__reliability', |
|
|
|
|
|
'status' |
|
|
|
|
|
).order_by('chain_index', 'order') |
|
|
|
|
|
|
|
|
# 3. Get the Hadiths (Child Nodes) |
|
|
|
|
|
# Note: Mind maps generally show ALL nodes, so we avoid pagination here. |
|
|
|
|
|
# If you have 1000+ hadiths, consider limiting this query (e.g., [:50]). |
|
|
|
|
|
hadiths = Hadis.objects.filter( |
|
|
hadiths = Hadis.objects.filter( |
|
|
category=category, |
|
|
category=category, |
|
|
status=True |
|
|
status=True |
|
|
).order_by('number').only('id', 'number', 'title', 'title_narrator', 'translation', 'text', 'slug', 'share_link') |
|
|
|
|
|
|
|
|
).order_by('number').prefetch_related( |
|
|
|
|
|
Prefetch('transmitters', queryset=transmitter_qs), |
|
|
|
|
|
'hadiscorrection_set' |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
# 4. Get user for bookmark check |
|
|
|
|
|
user = request.user if request and hasattr(request, 'user') else None |
|
|
|
|
|
if user and user.is_anonymous: |
|
|
|
|
|
user = None |
|
|
|
|
|
|
|
|
# Build prefix tree (Trie-like structure) |
|
|
|
|
|
# Root: Category |
|
|
|
|
|
# Branches: Narrator nodes along transmission chains |
|
|
|
|
|
# Leaves: Hadiths |
|
|
|
|
|
root_tree = { |
|
|
|
|
|
"id": f"cat-{category.id}", |
|
|
|
|
|
"slug": category.slug, |
|
|
|
|
|
"node_type": "category", |
|
|
|
|
|
"title": root_title, |
|
|
|
|
|
"children_map": {}, # key: (node_type, id) -> subtree dict |
|
|
|
|
|
"leaf_hadiths": set() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
# 5. Build Child Nodes List |
|
|
|
|
|
children_nodes = [] |
|
|
|
|
|
for hadis in hadiths: |
|
|
for hadis in hadiths: |
|
|
# Get Title |
|
|
|
|
|
hadis_title = self.get_localized_text(hadis.title, lang) |
|
|
|
|
|
hadis_title_narrator = self.get_localized_text(hadis.title_narrator, lang) |
|
|
|
|
|
hadis_translation = self.get_localized_text(hadis.translation, lang) |
|
|
|
|
|
|
|
|
hadis_title = self.get_localized(hadis.title, lang) |
|
|
|
|
|
hadis_title_narrator = self.get_localized(hadis.title_narrator, lang) |
|
|
|
|
|
hadis_translation = self.get_localized(hadis.translation, lang) |
|
|
|
|
|
|
|
|
# Get bookmark status |
|
|
|
|
|
bookmark_info = BookmarkStatusSerializer.get_bookmark_info( |
|
|
bookmark_info = BookmarkStatusSerializer.get_bookmark_info( |
|
|
obj=hadis, |
|
|
obj=hadis, |
|
|
user=user, |
|
|
user=user, |
|
|
@ -572,27 +591,157 @@ class HadisCategoryXMindView(APIView): |
|
|
) |
|
|
) |
|
|
is_bookmarked = bookmark_info.get('is_bookmarked', False) |
|
|
is_bookmarked = bookmark_info.get('is_bookmarked', False) |
|
|
|
|
|
|
|
|
children_nodes.append({ |
|
|
|
|
|
"id": hadis.id, |
|
|
|
|
|
|
|
|
corrections_data = [ |
|
|
|
|
|
{ |
|
|
|
|
|
"id": c.id, |
|
|
|
|
|
"slug": c.slug, |
|
|
|
|
|
"title": self.get_localized(c.title, lang), |
|
|
|
|
|
"narrator": c.narrator, |
|
|
|
|
|
"text": c.text, |
|
|
|
|
|
"translation": self.get_localized(c.translation, lang), |
|
|
|
|
|
"share_link": c.share_link, |
|
|
|
|
|
} |
|
|
|
|
|
for c in hadis.hadiscorrection_set.all() |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
hadis_node_data = { |
|
|
|
|
|
"id": f"hadis-{hadis.id}", |
|
|
|
|
|
"hadis_id": hadis.id, |
|
|
|
|
|
"node_type": "hadith", |
|
|
"slug": hadis.slug, |
|
|
"slug": hadis.slug, |
|
|
|
|
|
"number": hadis.number, |
|
|
"title": hadis_title, |
|
|
"title": hadis_title, |
|
|
"title_narrator": hadis_title_narrator, |
|
|
"title_narrator": hadis_title_narrator, |
|
|
"text": hadis.text, |
|
|
"text": hadis.text, |
|
|
"translation": hadis_translation, |
|
|
"translation": hadis_translation, |
|
|
"share_link": hadis.share_link, |
|
|
"share_link": hadis.share_link, |
|
|
"is_bookmarked": is_bookmarked, |
|
|
"is_bookmarked": is_bookmarked, |
|
|
# Optional: Add 'href' if you want XMind to handle links, |
|
|
|
|
|
# but usually the frontend handles the 'click' event based on ID. |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
"corrections": corrections_data, |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
# Group transmitters by chain_index |
|
|
|
|
|
chain_groups = defaultdict(list) |
|
|
|
|
|
for ht in hadis.transmitters.all(): |
|
|
|
|
|
if ht.transmitter: |
|
|
|
|
|
chain_groups[ht.chain_index].append(ht) |
|
|
|
|
|
|
|
|
|
|
|
if not chain_groups: |
|
|
|
|
|
# No transmitters: directly attach hadith to category root |
|
|
|
|
|
hadis_key = ("hadith", hadis.id) |
|
|
|
|
|
if hadis_key not in root_tree["children_map"]: |
|
|
|
|
|
root_tree["children_map"][hadis_key] = { |
|
|
|
|
|
"data": hadis_node_data, |
|
|
|
|
|
"children_map": {}, |
|
|
|
|
|
"leaf_hadiths": set() |
|
|
|
|
|
} |
|
|
|
|
|
root_tree["children_map"][hadis_key]["leaf_hadiths"].add(hadis.id) |
|
|
|
|
|
root_tree["leaf_hadiths"].add(hadis.id) |
|
|
|
|
|
else: |
|
|
|
|
|
for chain_idx, ht_list in chain_groups.items(): |
|
|
|
|
|
ht_list.sort(key=lambda x: x.order) |
|
|
|
|
|
|
|
|
|
|
|
curr = root_tree |
|
|
|
|
|
curr["leaf_hadiths"].add(hadis.id) |
|
|
|
|
|
|
|
|
|
|
|
for ht in ht_list: |
|
|
|
|
|
tr = ht.transmitter |
|
|
|
|
|
tr_key = ("narrator", tr.id) |
|
|
|
|
|
|
|
|
|
|
|
if tr_key not in curr["children_map"]: |
|
|
|
|
|
rel_obj = tr.reliability or ht.status |
|
|
|
|
|
rel_data = None |
|
|
|
|
|
if rel_obj: |
|
|
|
|
|
rel_data = { |
|
|
|
|
|
"id": rel_obj.id, |
|
|
|
|
|
"title": self.get_localized(rel_obj.title, lang), |
|
|
|
|
|
"color": getattr(rel_obj, 'color', None), |
|
|
|
|
|
"main_color_code": getattr(rel_obj, 'main_color_code', None), |
|
|
|
|
|
"slug": getattr(rel_obj, 'slug', None), |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
tr_node_data = { |
|
|
|
|
|
"id": f"narrator-{tr.id}", |
|
|
|
|
|
"transmitter_id": tr.id, |
|
|
|
|
|
"node_type": "narrator", |
|
|
|
|
|
"slug": tr.slug, |
|
|
|
|
|
"title": self.get_localized(tr.full_name, lang) or tr.slug, |
|
|
|
|
|
"full_name": self.get_localized(tr.full_name, lang), |
|
|
|
|
|
"arabic_full_name": self.get_arabic(tr.full_name), |
|
|
|
|
|
"kunya": self.get_localized(tr.kunya, lang), |
|
|
|
|
|
"arabic_kunya": self.get_arabic(tr.kunya), |
|
|
|
|
|
"known_as": self.get_localized(tr.known_as, lang), |
|
|
|
|
|
"arabic_known_as": self.get_arabic(tr.known_as), |
|
|
|
|
|
"nickname": self.get_localized(tr.nickname, lang), |
|
|
|
|
|
"arabic_nickname": self.get_arabic(tr.nickname), |
|
|
|
|
|
"birth_year_hijri": tr.birth_year_hijri, |
|
|
|
|
|
"death_year_hijri": tr.death_year_hijri, |
|
|
|
|
|
"age_at_death": tr.age_at_death, |
|
|
|
|
|
"generation": tr.generation, |
|
|
|
|
|
"tadlis": tr.tadlis, |
|
|
|
|
|
"ikhtilat": tr.ikhtilat, |
|
|
|
|
|
"companion_type": tr.companion_type, |
|
|
|
|
|
"madhhab": tr.madhhab, |
|
|
|
|
|
"in_sahih_muslim": tr.in_sahih_muslim, |
|
|
|
|
|
"in_sahih_bukhari": tr.in_sahih_bukhari, |
|
|
|
|
|
"reliability": rel_data, |
|
|
|
|
|
"thumbnail": self._get_thumbnail_url(tr.thumbnail, request), |
|
|
|
|
|
"share_link": tr.share_link, |
|
|
|
|
|
} |
|
|
|
|
|
curr["children_map"][tr_key] = { |
|
|
|
|
|
"data": tr_node_data, |
|
|
|
|
|
"children_map": {}, |
|
|
|
|
|
"leaf_hadiths": set() |
|
|
|
|
|
} |
|
|
|
|
|
curr = curr["children_map"][tr_key] |
|
|
|
|
|
curr["leaf_hadiths"].add(hadis.id) |
|
|
|
|
|
|
|
|
|
|
|
# Attach hadith leaf under the last narrator of this chain |
|
|
|
|
|
hadis_key = ("hadith", hadis.id) |
|
|
|
|
|
if hadis_key not in curr["children_map"]: |
|
|
|
|
|
curr["children_map"][hadis_key] = { |
|
|
|
|
|
"data": hadis_node_data, |
|
|
|
|
|
"children_map": {}, |
|
|
|
|
|
"leaf_hadiths": set() |
|
|
|
|
|
} |
|
|
|
|
|
curr["children_map"][hadis_key]["leaf_hadiths"].add(hadis.id) |
|
|
|
|
|
|
|
|
|
|
|
# Convert internal trie to the standard XMind JSON tree structure |
|
|
|
|
|
def format_tree_node(node_dict, path_prefix=""): |
|
|
|
|
|
node_data = dict(node_dict["data"]) if "data" in node_dict else {} |
|
|
|
|
|
children_map = node_dict.get("children_map", {}) |
|
|
|
|
|
|
|
|
|
|
|
# Form unique instance ID for tree canvas |
|
|
|
|
|
base_id = node_data.get("id", "node") |
|
|
|
|
|
unique_node_id = f"{path_prefix}_{base_id}" if path_prefix else base_id |
|
|
|
|
|
node_data["node_id"] = unique_node_id |
|
|
|
|
|
|
|
|
|
|
|
attached = [] |
|
|
|
|
|
for (child_type, child_id), child_subtree in children_map.items(): |
|
|
|
|
|
formatted_child = format_tree_node(child_subtree, path_prefix=unique_node_id) |
|
|
|
|
|
attached.append(formatted_child) |
|
|
|
|
|
|
|
|
|
|
|
node_data["count"] = len(attached) |
|
|
|
|
|
node_data["leaf_count"] = len(node_dict.get("leaf_hadiths", set())) |
|
|
|
|
|
node_data["children"] = { |
|
|
|
|
|
"attached": attached |
|
|
|
|
|
} |
|
|
|
|
|
return node_data |
|
|
|
|
|
|
|
|
|
|
|
root_attached = [] |
|
|
|
|
|
for (child_type, child_id), child_subtree in root_tree["children_map"].items(): |
|
|
|
|
|
root_attached.append(format_tree_node(child_subtree, path_prefix="root")) |
|
|
|
|
|
|
|
|
# 6. Construct XMind JSON Structure |
|
|
|
|
|
data = { |
|
|
data = { |
|
|
"rootTopic": { |
|
|
"rootTopic": { |
|
|
"id": f"cat-{category.id}", |
|
|
|
|
|
|
|
|
"id": root_tree["id"], |
|
|
|
|
|
"node_type": "category", |
|
|
|
|
|
"slug": category.slug, |
|
|
"title": root_title, |
|
|
"title": root_title, |
|
|
"structureClass": "org.xmind.ui.map.unbalanced", # Standard right-branching map |
|
|
|
|
|
|
|
|
"count": len(root_attached), |
|
|
|
|
|
"leaf_count": len(root_tree["leaf_hadiths"]), |
|
|
|
|
|
"structureClass": "org.xmind.ui.map.unbalanced", |
|
|
"children": { |
|
|
"children": { |
|
|
"attached": children_nodes |
|
|
|
|
|
|
|
|
"attached": root_attached |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|