138 lines
4.6 KiB
Python
138 lines
4.6 KiB
Python
from rest_framework.views import APIView
|
|
from rest_framework.response import Response
|
|
from rest_framework import status
|
|
from django.core.cache import cache
|
|
from django.db.models import Q
|
|
from django.core.paginator import Paginator
|
|
import logging
|
|
import hashlib
|
|
import re
|
|
|
|
from app.feeds.models import Post
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SearchView(APIView):
|
|
"""Search posts by free text or tags with pagination"""
|
|
|
|
def get(self, request):
|
|
query = request.query_params.get("q")
|
|
tag = request.query_params.get("tag")
|
|
page = int(request.query_params.get("page", 1))
|
|
per_page = min(int(request.query_params.get("perPage", 10)), 50)
|
|
|
|
# Validate search parameters
|
|
if not query and not tag:
|
|
return Response(
|
|
{
|
|
"type": "Error",
|
|
"errors": ["Either 'q' (query) or 'tag' parameter is required"],
|
|
"data": None,
|
|
},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
if page < 1:
|
|
return Response(
|
|
{
|
|
"type": "Error",
|
|
"errors": ["Page number must be 1 or greater"],
|
|
"data": None,
|
|
},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
# Build cache key
|
|
search_term = query if query else f"tag:{tag}"
|
|
cache_key = f"search_{hashlib.md5(search_term.encode()).hexdigest()[:8]}_{page}_{per_page}"
|
|
cached_response = cache.get(cache_key)
|
|
|
|
if cached_response is not None:
|
|
return Response(cached_response, status=status.HTTP_200_OK)
|
|
|
|
# Build search query
|
|
posts_query = Post.objects.select_related("profile").order_by("-created_at")
|
|
|
|
if query:
|
|
# Search in content using case-insensitive contains
|
|
posts_query = posts_query.filter(
|
|
Q(content__icontains=query) | Q(tags__icontains=query)
|
|
)
|
|
search_type = "query"
|
|
search_value = query
|
|
else:
|
|
# Search by specific tag (exact word match, case insensitive)
|
|
# Match tag as complete word: at start, end, middle, or alone
|
|
# Examples: "emacs", "emacs org-mode", "python emacs", "emacs-lisp emacs"
|
|
tag_escaped = re.escape(tag)
|
|
# Use word boundaries to match complete words only
|
|
tag_pattern = rf"(^|[\s]){tag_escaped}([\s]|$)"
|
|
posts_query = posts_query.filter(tags__iregex=tag_pattern)
|
|
search_type = "tag"
|
|
search_value = tag
|
|
|
|
# Get total count for pagination
|
|
total_posts = posts_query.count()
|
|
|
|
# Apply pagination
|
|
paginator = Paginator(posts_query, per_page)
|
|
|
|
if page > paginator.num_pages and total_posts > 0:
|
|
return Response(
|
|
{
|
|
"type": "Error",
|
|
"errors": [
|
|
f"Page {page} does not exist. Maximum page is {paginator.num_pages}"
|
|
],
|
|
"data": None,
|
|
},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
posts_page = paginator.get_page(page)
|
|
|
|
# Build data array with post URLs
|
|
data = []
|
|
for post in posts_page:
|
|
post_url = f"{post.profile.feed}#{post.post_id}"
|
|
data.append(post_url)
|
|
|
|
# Generate pagination links
|
|
base_url = "/search"
|
|
if query:
|
|
base_url += f"?q={query}"
|
|
else:
|
|
base_url += f"?tag={tag}"
|
|
|
|
if per_page != 10:
|
|
base_url += f"&perPage={per_page}"
|
|
|
|
# Build response
|
|
response_data = {
|
|
"type": "Success",
|
|
"errors": [],
|
|
"data": data,
|
|
"meta": {
|
|
search_type: search_value,
|
|
"total": total_posts,
|
|
"page": page,
|
|
"perPage": per_page,
|
|
"hasNext": posts_page.has_next(),
|
|
"hasPrevious": posts_page.has_previous(),
|
|
},
|
|
"_links": {
|
|
"self": {"href": f"{base_url}&page={page}", "method": "GET"},
|
|
"next": {"href": f"{base_url}&page={page + 1}", "method": "GET"}
|
|
if posts_page.has_next()
|
|
else None,
|
|
"previous": {"href": f"{base_url}&page={page - 1}", "method": "GET"}
|
|
if posts_page.has_previous()
|
|
else None,
|
|
},
|
|
}
|
|
|
|
# Cache permanently (will be cleared by scan_feeds task)
|
|
cache.set(cache_key, response_data, None)
|
|
|
|
return Response(response_data, status=status.HTTP_200_OK)
|