Browse Source

feat(hadis): enhance xmind category tree api and narrator order

- Sort narrator chains in reverse order so each branch leads from first narrator to primary narrator/hadith
- Add support for uncertain transmitters with candidate details in xmind tree
- Localize reliability titles using get_localized_text
- Disable caching on category xmind endpoint and add no-cache response headers
master
Mohsen Taba 2 weeks ago
parent
commit
32d4e58555
  1. 2
      apps/hadis/urls.py
  2. 189
      apps/hadis/views/category.py

2
apps/hadis/urls.py

@ -100,7 +100,7 @@ urlpatterns = [
path('categories/', cached_view(CategoriesView.as_view()), name='categories'), # ← Least specific LAST
# Hadis paths
path('category/<str:category_slug>/xmind/', cached_view(HadisCategoryXMindView.as_view()), name='hadis-category-xmind'), # ← Must be before other category paths
path('category/<str:category_slug>/xmind/', HadisCategoryXMindView.as_view(), name='hadis-category-xmind'), # ← Must be before other category paths
path('category/<str:category_slug>/corrections/', CategoryHadisCorrectionsView.as_view(), name='category-hadis-corrections'),
path('category/<str:category_slug>/', HadisListView.as_view(), name='hadis-list'),
path('arguments/filters/', cached_view(HadisFiltersView.as_view()), name='hadis-filters'),

189
apps/hadis/views/category.py

@ -516,18 +516,8 @@ class HadisCategoryXMindView(APIView):
Leaves -> Hadiths
"""
def get_localized(self, json_field, lang):
if not json_field or not isinstance(json_field, list):
return ""
for item in json_field:
if isinstance(item, dict) and item.get('language_code') == lang:
return item.get('text', '')
for item in json_field:
if isinstance(item, dict) and item.get('language_code') == 'en':
return item.get('text', '')
if len(json_field) > 0 and isinstance(json_field[0], dict):
return json_field[0].get('text', '')
return ""
def get_localized(self, json_field, lang='en'):
return get_localized_text(json_field, language_code=lang) or ""
def get_arabic(self, json_field):
return get_arabic_localized_text(json_field) or ""
@ -544,7 +534,7 @@ class HadisCategoryXMindView(APIView):
@hadis_category_xmind_swagger
def get(self, request, category_slug):
lang = request.query_params.get('lang', 'en')
lang = request.query_params.get('lang') or request.query_params.get('language_code') or 'en'
category = get_object_or_404(HadisCategory, slug=category_slug)
root_title = self.get_localized(category.title, lang) or category.slug
@ -555,14 +545,18 @@ class HadisCategoryXMindView(APIView):
# Fetch active hadiths with their transmitters and corrections
transmitter_qs = HadisTransmitter.objects.select_related(
'transmitter__reliability',
'status'
).order_by('chain_index', 'order')
'status',
'narrator_layer'
).prefetch_related(
'uncertain_transmitters__reliability'
).order_by('order')
hadiths = Hadis.objects.filter(
category=category,
status=True
).order_by('number').prefetch_related(
Prefetch('transmitters', queryset=transmitter_qs),
'narrator_layers',
'hadiscorrection_set'
)
@ -619,13 +613,15 @@ class HadisCategoryXMindView(APIView):
"corrections": corrections_data,
}
# Group transmitters by chain_index
chain_groups = defaultdict(list)
# Group transmitters by narrative layer (NarratorLayer)
# Each narrative layer forms its own transmission branch
layer_groups = defaultdict(list)
for ht in hadis.transmitters.all():
if ht.transmitter:
chain_groups[ht.chain_index].append(ht)
layer_key = ht.narrator_layer_id if ht.narrator_layer_id is not None else f"chain-{ht.chain_index}"
layer_groups[layer_key].append(ht)
if not chain_groups:
if not layer_groups:
# No transmitters: directly attach hadith to category root
hadis_key = ("hadith", hadis.id)
if hadis_key not in root_tree["children_map"]:
@ -637,18 +633,34 @@ class HadisCategoryXMindView(APIView):
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)
# Sort layers by layer number if available
sorted_layer_items = sorted(
layer_groups.items(),
key=lambda item: (item[1][0].narrator_layer.number if item[1] and item[1][0].narrator_layer else 0)
)
for layer_key, ht_list in sorted_layer_items:
# Sort transmitters in reverse order so the chain goes from first narrator to last narrator reaching the hadith
ht_list.sort(key=lambda x: x.order, reverse=True)
curr = root_tree
curr["leaf_hadiths"].add(hadis.id)
for ht in ht_list:
tr = ht.transmitter
tr_key = ("narrator", tr.id)
is_uncertain = getattr(ht, 'is_uncertain', False)
cand_objs = list(ht.uncertain_transmitters.all()) if is_uncertain else []
is_node_uncertain = bool(is_uncertain and len(cand_objs) > 0)
if is_node_uncertain:
cand_ids = tuple(sorted([ut.id for ut in cand_objs]))
tr_key = ("uncertain", cand_ids)
else:
cand_ids = ()
tr_key = ("narrator", tr.id if tr else ht.id)
if tr_key not in curr["children_map"]:
rel_obj = tr.reliability or ht.status
rel_obj = (tr.reliability if tr else None) or ht.status
rel_data = None
if rel_obj:
rel_data = {
@ -659,33 +671,94 @@ class HadisCategoryXMindView(APIView):
"slug": getattr(rel_obj, 'slug', None),
}
layer_obj = ht.narrator_layer
layer_data = None
if layer_obj:
layer_data = {
"id": layer_obj.id,
"number": layer_obj.number,
"slug": layer_obj.slug,
"name": self.get_localized(layer_obj.name, lang),
"description": self.get_localized(layer_obj.description, lang),
}
uncertain_candidates = []
if is_node_uncertain:
for ut in cand_objs:
ut_rel = ut.reliability
ut_rel_data = None
if ut_rel:
ut_rel_data = {
"id": ut_rel.id,
"title": self.get_localized(ut_rel.title, lang),
"color": getattr(ut_rel, 'color', None),
"main_color_code": getattr(ut_rel, 'main_color_code', None),
"slug": getattr(ut_rel, 'slug', None),
}
uncertain_candidates.append({
"id": ut.id,
"transmitter_id": ut.id,
"slug": ut.slug,
"name": self.get_localized(ut.full_name, lang) or ut.slug,
"title": self.get_localized(ut.full_name, lang) or ut.slug,
"full_name": self.get_localized(ut.full_name, lang),
"arabic_full_name": self.get_arabic(ut.full_name),
"kunya": self.get_localized(ut.kunya, lang),
"arabic_kunya": self.get_arabic(ut.kunya),
"known_as": self.get_localized(ut.known_as, lang),
"arabic_known_as": self.get_arabic(ut.known_as),
"nickname": self.get_localized(ut.nickname, lang),
"arabic_nickname": self.get_arabic(ut.nickname),
"birth_year_hijri": ut.birth_year_hijri,
"death_year_hijri": ut.death_year_hijri,
"reliability": ut_rel_data,
"thumbnail": self._get_thumbnail_url(ut.thumbnail, request),
"share_link": ut.share_link,
})
node_type_str = "uncertain" if is_node_uncertain else "narrator"
node_id_str = f"uncertain-{'-'.join(map(str, cand_ids))}" if is_node_uncertain else f"narrator-{tr.id if tr else ht.id}"
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,
"id": node_id_str,
"transmitter_id": tr.id if tr else (uncertain_candidates[0]["id"] if uncertain_candidates else None),
"node_type": node_type_str,
"tag": node_type_str,
"slug": tr.slug if tr else (uncertain_candidates[0]["slug"] if uncertain_candidates else None),
"title": "Uncertain narrator" if is_node_uncertain else (self.get_localized(tr.full_name, lang) or tr.slug if tr else "Narrator"),
"full_name": "Uncertain narrator" if is_node_uncertain else (self.get_localized(tr.full_name, lang) if tr else "Narrator"),
"arabic_full_name": self.get_arabic(tr.full_name) if tr else None,
"kunya": self.get_localized(tr.kunya, lang) if tr else None,
"arabic_kunya": self.get_arabic(tr.kunya) if tr else None,
"known_as": self.get_localized(tr.known_as, lang) if tr else None,
"arabic_known_as": self.get_arabic(tr.known_as) if tr else None,
"nickname": self.get_localized(tr.nickname, lang) if tr else None,
"arabic_nickname": self.get_arabic(tr.nickname) if tr else None,
"birth_year_hijri": tr.birth_year_hijri if tr else None,
"death_year_hijri": tr.death_year_hijri if tr else None,
"age_at_death": tr.age_at_death if tr else None,
"generation": tr.generation if tr else None,
"tadlis": tr.tadlis if tr else None,
"ikhtilat": tr.ikhtilat if tr else None,
"companion_type": tr.companion_type if tr else None,
"madhhab": tr.madhhab if tr else None,
"in_sahih_muslim": tr.in_sahih_muslim if tr else None,
"in_sahih_bukhari": tr.in_sahih_bukhari if tr else None,
"reliability": rel_data,
"thumbnail": self._get_thumbnail_url(tr.thumbnail, request),
"share_link": tr.share_link,
"is_uncertain": is_node_uncertain,
"uncertain_transmitters": uncertain_candidates,
"transmitter": {
"id": tr.id,
"name": self.get_localized(tr.full_name, lang) or tr.slug,
"full_name": self.get_localized(tr.full_name, lang),
"slug": tr.slug,
"reliability": rel_data,
} if tr else None,
"layer": layer_data,
"layer_number": layer_obj.number if layer_obj else None,
"layer_name": self.get_localized(layer_obj.name, lang) if layer_obj else None,
"thumbnail": self._get_thumbnail_url(tr.thumbnail, request) if tr else None,
"share_link": tr.share_link if tr else None,
}
curr["children_map"][tr_key] = {
"data": tr_node_data,
@ -705,18 +778,16 @@ class HadisCategoryXMindView(APIView):
}
curr["children_map"][hadis_key]["leaf_hadiths"].add(hadis.id)
# Convert internal trie to the standard XMind JSON tree structure
# Format prefix tree into recursive XMind JSON 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 = dict(node_dict["data"])
node_type = node_data.get("node_type", "unknown")
raw_id = node_data.get("id", "0")
unique_node_id = f"{path_prefix}_{node_type}_{raw_id}"
node_data["node_id"] = unique_node_id
attached = []
for (child_type, child_id), child_subtree in children_map.items():
for (child_type, child_id), child_subtree in node_dict["children_map"].items():
formatted_child = format_tree_node(child_subtree, path_prefix=unique_node_id)
attached.append(formatted_child)
@ -746,4 +817,8 @@ class HadisCategoryXMindView(APIView):
}
}
return Response(data)
response = Response(data)
response['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response['Pragma'] = 'no-cache'
response['Expires'] = '0'
return response
Loading…
Cancel
Save