diff --git a/apps/hadis/management/commands/populate_narrator_links.py b/apps/hadis/management/commands/populate_narrator_links.py index 98033a7..5d83be3 100644 --- a/apps/hadis/management/commands/populate_narrator_links.py +++ b/apps/hadis/management/commands/populate_narrator_links.py @@ -1,32 +1,14 @@ # backend/apps/hadis/management/commands/populate_narrator_links.py from django.core.management.base import BaseCommand -from apps.hadis.models import HadisCorrection, HadisInterpretation + class Command(BaseCommand): - help = 'Populates the default narrator and links for all existing Corrections and Interpretations.' + help = 'Deprecated command (dummy narrator and links seeding disabled).' def handle(self, *args, **options): - self.stdout.write(self.style.WARNING("Updating default values for narrator and links...")) - - default_narrator = "زُرارَةُ بْنُ أَعْيَنَ الشَّيبانِيّ" - default_links = [ - {"title": "Первая ссылка Первая ссылка ссылкассы...", "link": "google.com"}, - {"title": "Первая ссылка.", "link": "google.com"}, - {"title": "Первая ссылка.", "link": "google.com"} - ] - - # آپدیت تمام رکوردهای جدول تصحیحات - corr_count = HadisCorrection.objects.all().update( - narrator=default_narrator, - links=default_links + self.stdout.write( + self.style.WARNING( + "populate_narrator_links is disabled to prevent dummy links from being written." + ) ) - - # آپدیت تمام رکوردهای جدول تفاسیر - interp_count = HadisInterpretation.objects.all().update( - narrator=default_narrator, - links=default_links - ) - - self.stdout.write(self.style.SUCCESS(f"✅ Successfully updated {corr_count} HadisCorrections!")) - self.stdout.write(self.style.SUCCESS(f"✅ Successfully updated {interp_count} HadisInterpretations!")) \ No newline at end of file diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index abbebc0..e49b88d 100644 --- a/apps/hadis/serializers/hadis.py +++ b/apps/hadis/serializers/hadis.py @@ -1472,7 +1472,16 @@ class HadisReferenceSerializer(serializers.ModelSerializer): request = self.context.get('request') if obj.book_volume and obj.book_volume.file: return absolute_https_url(obj.book_volume.file.url, request) if request else absolute_https_url(obj.book_volume.file.url) - return obj.url or None + if getattr(obj, 'url', None): + return obj.url + edition = obj.edition + if not edition and getattr(obj, 'book_volume', None) and obj.book_volume.edition: + edition = obj.book_volume.edition + if not edition and getattr(obj, 'book_reference', None) and hasattr(obj.book_reference, 'editions'): + edition = obj.book_reference.editions.first() + if edition and getattr(edition, 'source_url', None): + return edition.source_url + return None def get_book(self, obj): if obj.book_reference: @@ -1633,15 +1642,23 @@ class HadisCorrectionSerializer(serializers.ModelSerializer): address = serializers.SerializerMethodField() images = serializers.SerializerMethodField() links = serializers.SerializerMethodField() + references = serializers.SerializerMethodField() hadis_info = serializers.SerializerMethodField() class Meta: model = HadisCorrection - fields = ['id', 'title', 'slug', 'narrator', 'description', 'translation', 'share_link', 'bookmark', 'address', 'images', 'links', 'hadis_info'] + fields = ['id', 'title', 'slug', 'narrator', 'description', 'translation', 'share_link', 'bookmark', 'address', 'images', 'links', 'references', 'hadis_info'] def get_links(self, obj): return format_links_output(obj.links) + def get_references(self, obj): + return DetailedCorrectionReferenceSerializer( + obj.references.all(), + many=True, + context=self.context + ).data + def get_hadis_info(self, obj): if obj.hadis: request = self.context.get('request') @@ -1705,6 +1722,7 @@ class HadisBasicSerializer(serializers.ModelSerializer): category = serializers.SerializerMethodField() categories = serializers.SerializerMethodField() bookmark = serializers.SerializerMethodField() + links = serializers.SerializerMethodField() title = LocalizedField() title_narrator = LocalizedField() @@ -1715,9 +1733,12 @@ class HadisBasicSerializer(serializers.ModelSerializer): model = Hadis fields = [ 'id', 'slug', 'title', 'bookmark', 'title_narrator', 'text', - 'translation', 'share_link', 'explanation', 'category', 'categories' + 'translation', 'share_link', 'explanation', 'category', 'categories', 'links' ] + def get_links(self, obj): + return format_links_output(obj.links) + def get_category(self, obj): cat = obj.category if not cat and hasattr(obj, 'categories'): @@ -1782,6 +1803,7 @@ class HadisDetailSerializer(serializers.ModelSerializer): ) reference_images = SerializerMethodField() address = LocalizedField() + links = serializers.SerializerMethodField() category_slug = serializers.CharField(source='category.slug', read_only=True) category = serializers.SerializerMethodField() categories = serializers.SerializerMethodField() @@ -1793,6 +1815,9 @@ class HadisDetailSerializer(serializers.ModelSerializer): 'tags', 'references', 'reference_images', 'address' ] + def get_links(self, obj): + return format_links_output(obj.links) + def get_category(self, obj): cat = obj.category if not cat and hasattr(obj, 'categories'): @@ -2109,6 +2134,15 @@ class DetailedCorrectionReferenceSerializer(BaseDetailedReferenceSerializer): request = self.context.get('request') if obj.book_volume and obj.book_volume.file: return absolute_https_url(obj.book_volume.file.url, request) if request else absolute_https_url(obj.book_volume.file.url) + if getattr(obj, 'url', None): + return obj.url + edition = obj.edition + if not edition and getattr(obj, 'book_volume', None) and obj.book_volume.edition: + edition = obj.book_volume.edition + if not edition and getattr(obj, 'book_reference', None) and hasattr(obj.book_reference, 'editions'): + edition = obj.book_reference.editions.first() + if edition and getattr(edition, 'source_url', None): + return edition.source_url return None volume = serializers.SerializerMethodField() diff --git a/apps/hadis/utils/links.py b/apps/hadis/utils/links.py index 51b5f05..a72c575 100644 --- a/apps/hadis/utils/links.py +++ b/apps/hadis/utils/links.py @@ -2,6 +2,45 @@ import re from urllib.parse import urlparse +def ensure_external_url(url: str) -> str: + """ + Ensures a URL string has an explicit http:// or https:// scheme so browsers + never treat domain-only links (e.g. 'mamad.com') as relative paths. + """ + if not url or not isinstance(url, str): + return "" + trimmed = url.strip() + if not trimmed: + return "" + if re.match(r"^[a-zA-Z][a-zA-Z0-9+\-.]*://", trimmed): + return trimmed + if trimmed.startswith("//"): + return "https:" + trimmed + if trimmed.startswith("/"): + return trimmed + return "https://" + trimmed.lstrip("/") + + +def is_dummy_placeholder_link(title: str, url: str) -> bool: + """ + Detects legacy seeded dummy links ('Первая ссылка...' -> 'google.com') + so they are never stored or returned. + """ + t = (title or "").strip() + u = (url or "").strip().lower() + if "Первая ссылка" in t: + return True + if u in ( + "google.com", + "https://google.com", + "http://google.com", + "https://www.google.com", + "http://www.google.com", + ) and ("Первая" in t or t in ("google.com", "Link", "")): + return True + return False + + def extract_domain_from_url(url: str) -> str: """ Extracts the root domain / hostname without 'www.' from a URL. @@ -17,9 +56,7 @@ def extract_domain_from_url(url: str) -> str: if not trimmed: return "" - candidate = trimmed - if not re.match(r"^[a-zA-Z][a-zA-Z0-9+\-.]*://", candidate): - candidate = "https://" + candidate + candidate = ensure_external_url(trimmed) try: parsed = urlparse(candidate) @@ -42,7 +79,8 @@ def extract_domain_from_url(url: str) -> str: def normalize_links_for_storage(links_data): """ Ensures any link without a title (or with an empty / generic placeholder title) - has its title automatically set to the domain of the URL. + has its title automatically set to the domain of the URL, and ensures the URL + starts with http:// or https://. Returns the normalized structure ready to be stored in JSONField. """ if not links_data: @@ -64,8 +102,11 @@ def normalize_links_for_storage(links_data): normalized = {} for title, url_or_obj in links_data.items(): if isinstance(url_or_obj, str) and url_or_obj.strip(): - url = url_or_obj.strip() + raw_url = url_or_obj.strip() t = str(title).strip() if title else "" + if is_dummy_placeholder_link(t, raw_url): + continue + url = ensure_external_url(raw_url) if not t or t.lower() in ("link", "untitled", "url", "default"): t = extract_domain_from_url(url) or "Link" @@ -78,10 +119,13 @@ def normalize_links_for_storage(links_data): normalized[key] = url elif isinstance(url_or_obj, dict): - url = url_or_obj.get("url") or url_or_obj.get("link") or url_or_obj.get("href") - if url and isinstance(url, str) and url.strip(): - url = url.strip() + raw_url = url_or_obj.get("url") or url_or_obj.get("link") or url_or_obj.get("href") + if raw_url and isinstance(raw_url, str) and raw_url.strip(): + raw_url = raw_url.strip() t = str(url_or_obj.get("title") or title or "").strip() + if is_dummy_placeholder_link(t, raw_url): + continue + url = ensure_external_url(raw_url) if not t or t.lower() in ("link", "untitled", "url", "default"): t = extract_domain_from_url(url) or "Link" key = t @@ -97,14 +141,20 @@ def normalize_links_for_storage(links_data): normalized = [] for item in links_data: if isinstance(item, str) and item.strip(): - url = item.strip() + raw_url = item.strip() + if is_dummy_placeholder_link("", raw_url): + continue + url = ensure_external_url(raw_url) title = extract_domain_from_url(url) or "Link" normalized.append({"title": title, "url": url}) elif isinstance(item, dict): - url = item.get("url") or item.get("link") or item.get("href") or item.get("value") - if url and isinstance(url, str) and url.strip(): - url = url.strip() + raw_url = item.get("url") or item.get("link") or item.get("href") or item.get("value") + if raw_url and isinstance(raw_url, str) and raw_url.strip(): + raw_url = raw_url.strip() title = str(item.get("title") or item.get("name") or "").strip() + if is_dummy_placeholder_link(title, raw_url): + continue + url = ensure_external_url(raw_url) if not title or title.lower() in ("link", "untitled", "url", "default"): title = extract_domain_from_url(url) or "Link" normalized.append({"title": title, "url": url}) @@ -121,7 +171,8 @@ def format_links_output(links_data): {"title": "islamweb.net", "url": "https://www.islamweb.net/ar/fatwa/1909"} ] Handles dict, list of dicts, list of strings, and raw JSON strings cleanly. - Always ensures title defaults to domain if title is empty or generic 'Link'. + Always ensures title defaults to domain if title is empty or generic 'Link', + and ensures url has an explicit http(s):// scheme. """ if not links_data: return [] @@ -135,24 +186,31 @@ def format_links_output(links_data): return format_links_output(parsed) except (ValueError, TypeError, json.JSONDecodeError): pass - if trimmed: - return [{"title": extract_domain_from_url(trimmed) or "Link", "url": trimmed}] + if trimmed and not is_dummy_placeholder_link("", trimmed): + url = ensure_external_url(trimmed) + return [{"title": extract_domain_from_url(url) or "Link", "url": url}] return [] if isinstance(links_data, dict): out = [] for title, url_or_obj in links_data.items(): if isinstance(url_or_obj, str) and url_or_obj.strip(): - url = url_or_obj.strip() + raw_url = url_or_obj.strip() t = str(title).strip() if title else "" + if is_dummy_placeholder_link(t, raw_url): + continue + url = ensure_external_url(raw_url) if not t or t.lower() in ("link", "untitled", "url", "default"): t = extract_domain_from_url(url) or "Link" out.append({"title": t, "url": url}) elif isinstance(url_or_obj, dict): - url = url_or_obj.get("url") or url_or_obj.get("link") or url_or_obj.get("href") - if url and isinstance(url, str) and url.strip(): - url = url.strip() + raw_url = url_or_obj.get("url") or url_or_obj.get("link") or url_or_obj.get("href") + if raw_url and isinstance(raw_url, str) and raw_url.strip(): + raw_url = raw_url.strip() t = str(url_or_obj.get("title") or title or "").strip() + if is_dummy_placeholder_link(t, raw_url): + continue + url = ensure_external_url(raw_url) if not t or t.lower() in ("link", "untitled", "url", "default"): t = extract_domain_from_url(url) or "Link" out.append({"title": t, "url": url}) @@ -164,16 +222,22 @@ def format_links_output(links_data): out = [] for item in links_data: if isinstance(item, str) and item.strip(): - url = item.strip() + raw_url = item.strip() + if is_dummy_placeholder_link("", raw_url): + continue + url = ensure_external_url(raw_url) out.append({ "title": extract_domain_from_url(url) or "Link", "url": url }) elif isinstance(item, dict): - url = item.get("url") or item.get("link") or item.get("href") or item.get("value") - if url and isinstance(url, str) and url.strip(): - url = url.strip() + raw_url = item.get("url") or item.get("link") or item.get("href") or item.get("value") + if raw_url and isinstance(raw_url, str) and raw_url.strip(): + raw_url = raw_url.strip() t = str(item.get("title") or item.get("name") or "").strip() + if is_dummy_placeholder_link(t, raw_url): + continue + url = ensure_external_url(raw_url) if not t or t.lower() in ("link", "untitled", "url", "default"): t = extract_domain_from_url(url) or "Link" out.append({"title": t, "url": url}) diff --git a/apps/hadis/views/hadis.py b/apps/hadis/views/hadis.py index a255850..1b2e984 100644 --- a/apps/hadis/views/hadis.py +++ b/apps/hadis/views/hadis.py @@ -825,7 +825,7 @@ class HadisCorrectionsView(ListAPIView): """ serializer_class = HadisCorrectionSerializer authentication_classes = [TokenAuthentication] - permission_classes = [IsAuthenticated] + permission_classes = [AllowAny] pagination_class = StandardResultsSetPagination lookup_field = 'slug' lookup_url_kwarg = 'hadis_slug' @@ -854,10 +854,19 @@ class HadisCorrectionsView(ListAPIView): def get_queryset(self): hadis_slug = self.kwargs.get('hadis_slug') try: - hadis = Hadis.objects.get(slug=hadis_slug, status=True) - if not HadisCorrection.objects.filter(hadis=hadis).exists(): - return Hadis.objects.none() - queryset = HadisCorrection.objects.filter(hadis=hadis) + hadis = Hadis.objects.filter(slug=hadis_slug, status=True).first() + if not hadis and str(hadis_slug).isdigit(): + hadis = Hadis.objects.filter(id=int(hadis_slug), status=True).first() + if not hadis: + return HadisCorrection.objects.none() + queryset = HadisCorrection.objects.filter(hadis=hadis).select_related( + 'hadis' + ).prefetch_related( + 'references__book_reference__author', + 'references__edition', + 'references__book_volume', + 'references__images' + ).order_by('priority', '-created_at') # Filter by bookmarks if provided is_bookmark = self.request.query_params.get('is_bookmark', '').lower() @@ -956,6 +965,9 @@ class CategoryHadisCorrectionsView(ListAPIView): hadis_id__in=hadis_ids ).select_related('hadis').prefetch_related( 'references', + 'references__book_reference__author', + 'references__edition', + 'references__book_volume', 'references__images' ).order_by('priority', '-created_at') @@ -1341,6 +1353,21 @@ class HadisCorrectionDetailView(RetrieveAPIView): 'references__images' ) + def get_object(self): + queryset = self.filter_queryset(self.get_queryset()) + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} + + obj = queryset.filter(**filter_kwargs).first() + if not obj and str(self.kwargs[lookup_url_kwarg]).isdigit(): + obj = queryset.filter(id=int(self.kwargs[lookup_url_kwarg])).first() + + if not obj: + raise Http404("No HadisCorrection matches the given query.") + + self.check_object_permissions(self.request, obj) + return obj + class HadisInterpretationDetailView(RetrieveAPIView): """API View to get full details of a specific HadisInterpretation by slug""" serializer_class = HadisInterpretationDetailSerializer @@ -1403,12 +1430,23 @@ class HadisInterpretsListView(ListAPIView): def get_queryset(self): hadis_slug = self.kwargs.get('hadis_slug') try: - hadis = Hadis.objects.select_related('category').get(slug=hadis_slug, status=True) - if not hadis.category_id: + hadis = Hadis.objects.select_related('category').prefetch_related('categories').filter(slug=hadis_slug, status=True).first() + if not hadis and str(hadis_slug).isdigit(): + hadis = Hadis.objects.select_related('category').prefetch_related('categories').filter(id=int(hadis_slug), status=True).first() + if not hadis: + return HadisInterpretation.objects.none() + + cat_ids = set() + if hadis.category_id: + cat_ids.add(hadis.category_id) + if hasattr(hadis, 'categories'): + cat_ids.update(hadis.categories.values_list('id', flat=True)) + + if not cat_ids: return HadisInterpretation.objects.none() return HadisInterpretation.objects.filter( - category_id=hadis.category_id + category_id__in=list(cat_ids) ).select_related( 'category' ).prefetch_related(