Browse Source
feat(links): auto-populate link titles with domain fallback on model save and serializers
master
feat(links): auto-populate link titles with domain fallback on model save and serializers
master
4 changed files with 201 additions and 68 deletions
-
11apps/hadis/models/hadis.py
-
69apps/hadis/serializers/hadis.py
-
5apps/hadis/utils/__init__.py
-
184apps/hadis/utils/links.py
@ -0,0 +1,184 @@ |
|||
import re |
|||
from urllib.parse import urlparse |
|||
|
|||
|
|||
def extract_domain_from_url(url: str) -> str: |
|||
""" |
|||
Extracts the root domain / hostname without 'www.' from a URL. |
|||
Examples: |
|||
'https://www.digikala.com/product/123' -> 'digikala.com' |
|||
'http://dorar.net/hadith/3420' -> 'dorar.net' |
|||
'www.al-islam.org/path' -> 'al-islam.org' |
|||
'digikala.com/item' -> 'digikala.com' |
|||
""" |
|||
if not url or not isinstance(url, str): |
|||
return "" |
|||
trimmed = url.strip() |
|||
if not trimmed: |
|||
return "" |
|||
|
|||
candidate = trimmed |
|||
if not re.match(r"^[a-zA-Z][a-zA-Z0-9+\-.]*://", candidate): |
|||
candidate = "https://" + candidate |
|||
|
|||
try: |
|||
parsed = urlparse(candidate) |
|||
hostname = (parsed.hostname or "").lower().strip() |
|||
if hostname.startswith("www."): |
|||
hostname = hostname[4:] |
|||
if hostname: |
|||
return hostname |
|||
except Exception: |
|||
pass |
|||
|
|||
# Regex fallback if urlparse failed |
|||
match = re.match(r"^(?:https?://)?(?:www\.)?([^/\s:?#]+)", trimmed, re.IGNORECASE) |
|||
if match: |
|||
return match.group(1).lower().strip() |
|||
|
|||
return "" |
|||
|
|||
|
|||
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. |
|||
Returns the normalized structure ready to be stored in JSONField. |
|||
""" |
|||
if not links_data: |
|||
return {} |
|||
|
|||
if isinstance(links_data, str): |
|||
trimmed = links_data.strip() |
|||
if trimmed.startswith(("{", "[")): |
|||
try: |
|||
import json |
|||
parsed = json.loads(trimmed) |
|||
return normalize_links_for_storage(parsed) |
|||
except (ValueError, TypeError, json.JSONDecodeError): |
|||
pass |
|||
return {} |
|||
|
|||
# Case 1: Dict format { title: url } |
|||
if isinstance(links_data, dict): |
|||
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() |
|||
t = str(title).strip() if title else "" |
|||
if not t or t.lower() in ("link", "untitled", "url", "default"): |
|||
t = extract_domain_from_url(url) or "Link" |
|||
|
|||
# Deduplicate key if multiple links share the same domain/title |
|||
key = t |
|||
counter = 2 |
|||
while key in normalized and normalized[key] != url: |
|||
key = f"{t} ({counter})" |
|||
counter += 1 |
|||
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() |
|||
t = str(url_or_obj.get("title") or title or "").strip() |
|||
if not t or t.lower() in ("link", "untitled", "url", "default"): |
|||
t = extract_domain_from_url(url) or "Link" |
|||
key = t |
|||
counter = 2 |
|||
while key in normalized and normalized[key] != url: |
|||
key = f"{t} ({counter})" |
|||
counter += 1 |
|||
normalized[key] = url |
|||
return normalized |
|||
|
|||
# Case 2: List format [ {"title": "...", "url": "..."}, ... ] |
|||
if isinstance(links_data, list): |
|||
normalized = [] |
|||
for item in links_data: |
|||
if isinstance(item, str) and item.strip(): |
|||
url = item.strip() |
|||
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() |
|||
title = str(item.get("title") or item.get("name") or "").strip() |
|||
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}) |
|||
return normalized |
|||
|
|||
return links_data |
|||
|
|||
|
|||
def format_links_output(links_data): |
|||
""" |
|||
Standardizes links into a list of structured objects: |
|||
[ |
|||
{"title": "dorar.net", "url": "https://dorar.net/hadith/3420"}, |
|||
{"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'. |
|||
""" |
|||
if not links_data: |
|||
return [] |
|||
|
|||
if isinstance(links_data, str): |
|||
trimmed = links_data.strip() |
|||
if trimmed.startswith(("[", "{")): |
|||
try: |
|||
import json |
|||
parsed = json.loads(trimmed) |
|||
return format_links_output(parsed) |
|||
except (ValueError, TypeError, json.JSONDecodeError): |
|||
pass |
|||
if trimmed: |
|||
return [{"title": extract_domain_from_url(trimmed) or "Link", "url": trimmed}] |
|||
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() |
|||
t = str(title).strip() if title else "" |
|||
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() |
|||
t = str(url_or_obj.get("title") or title or "").strip() |
|||
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, list): |
|||
out.extend(format_links_output(url_or_obj)) |
|||
return out |
|||
|
|||
if isinstance(links_data, list): |
|||
out = [] |
|||
for item in links_data: |
|||
if isinstance(item, str) and item.strip(): |
|||
url = item.strip() |
|||
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() |
|||
t = str(item.get("title") or item.get("name") or "").strip() |
|||
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(item, list): |
|||
out.extend(format_links_output(item)) |
|||
return out |
|||
|
|||
return [] |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue