diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index 201e05d..81c4a3a 100644 --- a/apps/hadis/urls.py +++ b/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//xmind/', cached_view(HadisCategoryXMindView.as_view()), name='hadis-category-xmind'), # ← Must be before other category paths + path('category//xmind/', HadisCategoryXMindView.as_view(), name='hadis-category-xmind'), # ← Must be before other category paths path('category//corrections/', CategoryHadisCorrectionsView.as_view(), name='category-hadis-corrections'), path('category//', HadisListView.as_view(), name='hadis-list'), path('arguments/filters/', cached_view(HadisFiltersView.as_view()), name='hadis-filters'), diff --git a/apps/hadis/views/category.py b/apps/hadis/views/category.py index b9d6988..93fa06e 100644 --- a/apps/hadis/views/category.py +++ b/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