import uuid from pathlib import Path from django.conf import settings from django.db.models import Q from django_filters.rest_framework import DjangoFilterBackend from rest_framework import permissions, status, viewsets from rest_framework.decorators import action from rest_framework.parsers import FormParser, MultiPartParser from rest_framework.response import Response from content.models import Article, Organize, Professor from content.serializers import ( ArticleListSerializer, OrganizeListSerializer, ProfessorSerializer, ) from content.views import IsAdminOrReadOnly, PublicMixin ALLOWED_IMAGE_EXT = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"} MAX_UPLOAD_BYTES = 8 * 1024 * 1024 from .models import ( AboutInfo, ContactUs, CopyrightInfo, ExtendLink, HomeBanner, Keyword, News, Publication, Report, ) from .serializers import ( AboutInfoSerializer, ContactUsSerializer, CopyrightInfoSerializer, ExtendLinkSerializer, HomeBannerSerializer, KeywordSerializer, NewsListSerializer, NewsSerializer, PublicationListSerializer, PublicationSerializer, ReportSerializer, ) class NewsViewSet(PublicMixin, viewsets.ModelViewSet): queryset = News.objects.all() permission_classes = [IsAdminOrReadOnly] filter_backends = [DjangoFilterBackend] filterset_fields = ["is_disabled"] def get_serializer_class(self): if self.action == "list": return NewsListSerializer return NewsSerializer def retrieve(self, request, *args, **kwargs): instance = self.get_object() News.objects.filter(pk=instance.pk).update(read_num=instance.read_num + 1) instance.refresh_from_db() return Response(self.get_serializer(instance).data) class PublicationViewSet(PublicMixin, viewsets.ModelViewSet): queryset = Publication.objects.all() permission_classes = [IsAdminOrReadOnly] filter_backends = [DjangoFilterBackend] filterset_fields = ["is_disabled"] def get_serializer_class(self): if self.action == "list": return PublicationListSerializer return PublicationSerializer class ReportViewSet(PublicMixin, viewsets.ModelViewSet): queryset = Report.objects.all() serializer_class = ReportSerializer permission_classes = [IsAdminOrReadOnly] filter_backends = [DjangoFilterBackend] filterset_fields = ["year", "is_disabled"] class AboutInfoViewSet(viewsets.ModelViewSet): queryset = AboutInfo.objects.all() serializer_class = AboutInfoSerializer permission_classes = [IsAdminOrReadOnly] @action(detail=False, methods=["get"]) def current(self, request): obj = self.get_queryset().first() if not obj: return Response({}) return Response(self.get_serializer(obj).data) class CopyrightInfoViewSet(viewsets.ModelViewSet): queryset = CopyrightInfo.objects.all() serializer_class = CopyrightInfoSerializer permission_classes = [IsAdminOrReadOnly] @action(detail=False, methods=["get"]) def current(self, request): obj = self.get_queryset().first() if not obj: return Response({}) return Response(self.get_serializer(obj).data) class ContactUsViewSet(viewsets.ModelViewSet): queryset = ContactUs.objects.all() serializer_class = ContactUsSerializer permission_classes = [IsAdminOrReadOnly] @action(detail=False, methods=["get"]) def current(self, request): obj = self.get_queryset().first() if not obj: return Response({}) return Response(self.get_serializer(obj).data) class ExtendLinkViewSet(PublicMixin, viewsets.ModelViewSet): queryset = ExtendLink.objects.all() serializer_class = ExtendLinkSerializer permission_classes = [IsAdminOrReadOnly] class HomeBannerViewSet(PublicMixin, viewsets.ModelViewSet): queryset = HomeBanner.objects.all() serializer_class = HomeBannerSerializer permission_classes = [IsAdminOrReadOnly] @action( detail=False, methods=["post"], url_path="upload", parser_classes=[MultiPartParser, FormParser], permission_classes=[permissions.IsAdminUser], ) def upload(self, request): """上传轮播图片,返回可直接写入 image_url 的路径。""" file = request.FILES.get("file") if not file: return Response({"detail": "请选择图片文件"}, status=status.HTTP_400_BAD_REQUEST) if file.size > MAX_UPLOAD_BYTES: return Response({"detail": "图片不能超过 8MB"}, status=status.HTTP_400_BAD_REQUEST) ext = Path(file.name).suffix.lower() if ext not in ALLOWED_IMAGE_EXT: return Response( {"detail": f"不支持的格式,请上传 {', '.join(sorted(ALLOWED_IMAGE_EXT))}"}, status=status.HTTP_400_BAD_REQUEST, ) filename = f"{uuid.uuid4().hex}{ext}" rel_dir = Path("banners") abs_dir = Path(settings.MEDIA_ROOT) / rel_dir abs_dir.mkdir(parents=True, exist_ok=True) abs_path = abs_dir / filename with open(abs_path, "wb") as out: for chunk in file.chunks(): out.write(chunk) url = f"/{settings.MEDIA_URL.strip('/')}/{rel_dir.as_posix()}/{filename}" return Response({"url": url, "name": file.name}) class KeywordViewSet(viewsets.ModelViewSet): queryset = Keyword.objects.all() serializer_class = KeywordSerializer permission_classes = [IsAdminOrReadOnly] def get_queryset(self): qs = super().get_queryset() if self.action in ("list", "retrieve"): user = self.request.user if not (user and user.is_staff): qs = qs.filter(is_show=True) return qs class SearchViewSet(viewsets.ViewSet): """全站搜索(文献 / 机构 / 专家 / 新闻)。""" permission_classes = [permissions.AllowAny] def list(self, request): q = (request.query_params.get("q") or "").strip() if not q: return Response({"articles": [], "organizes": [], "professors": [], "news": []}) articles = Article.objects.filter( is_disabled=False, is_deleted=False ).filter(Q(title__icontains=q) | Q(introduction__icontains=q))[:20] organizes = Organize.objects.filter( is_disabled=False, name__icontains=q )[:10] professors = Professor.objects.filter( is_disabled=False, name__icontains=q )[:10] news = News.objects.filter(is_disabled=False, title__icontains=q)[:10] return Response( { "articles": ArticleListSerializer(articles, many=True).data, "organizes": OrganizeListSerializer(organizes, many=True).data, "professors": ProfessorSerializer(professors, many=True).data, "news": NewsListSerializer(news, many=True).data, } )