Files
mobiniqow 831e656fa3 Fix OTP/SMS, ledger accounting, order tracking, and add reviews app
- Add MessageTemplate.runtime_field_labels so mobile's per-field
  execution-time labels are actually persisted (were silently dropped)
- Fix AdvanceOrderStageView reading the wrong extra-input key, and be
  honest about SMS send failures instead of always claiming success
- Add admin gift-gem SMS notification via Kavenegar
- Post real double-entry Transactions for plain (non-invoice) ledger
  entries so trial balance, the معین screen, and CSV/XLSX exports
  actually include everyday income/expense entries; backfill existing
  historical entries via data migration
- Rewrite CSV ledger export to produce an actual دفتر معین structure
  (per-account running balance) instead of a flat row dump
- Add invoice percent/promotional discounts, tax presets, ledger
  discount-percent handling, product SKU/category/expiry fields, and
  manual ledger-book archiving (replacing auto-archive on new book)
- Add reviews app (models, API, admin) for the comments/reviews feature
- Update tests for the above; fix two stale assertions left over from
  earlier work in this branch (معین sheet name, book auto-close)
2026-09-15 23:18:05 +03:30

89 lines
3.5 KiB
Python

from django.db import IntegrityError
from django.db.models import Count
from rest_framework import status
from rest_framework.exceptions import NotFound
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.reviews.models import Review, ReviewLike
from .serializers import ReviewSerializer, ReviewWriteSerializer
class ReviewListView(APIView):
"""فهرستِ عمومیِ نظراتِ تاییدشده — پرلایک‌ترین‌ها اول."""
permission_classes = [IsAuthenticated]
def get(self, request):
reviews = (
Review.objects.filter(is_approved=True)
.select_related("user")
.annotate(like_count=Count("likes"))
.order_by("-like_count", "-created_at")
)
liked_ids = set(
ReviewLike.objects.filter(user=request.user, review__in=reviews).values_list(
"review_id", flat=True
)
)
serializer = ReviewSerializer(
reviews, many=True, context={"request": request, "liked_ids": liked_ids}
)
return Response(serializer.data)
class MyReviewView(APIView):
"""نظرِ خودِ کاربر — صرف‌نظر از تایید یا نبودنِ آن. یک کاربر همیشه دقیقا
صفر یا یک نظر دارد؛ نوشتنِ دوباره همان نظر را جایگزین و تاییدش را
ریست می‌کند."""
permission_classes = [IsAuthenticated]
def get(self, request):
review = Review.objects.filter(user=request.user).first()
if review is None:
return Response(None)
return Response(ReviewSerializer(review, context={"request": request}).data)
def patch(self, request):
serializer = ReviewWriteSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
review, _created = Review.objects.update_or_create(
user=request.user,
defaults={
"body": serializer.validated_data["body"],
"is_approved": False,
},
)
return Response(
ReviewSerializer(review, context={"request": request}).data,
status=status.HTTP_200_OK,
)
def delete(self, request):
Review.objects.filter(user=request.user).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class ReviewLikeToggleView(APIView):
"""لایک/آن‌لایکِ یک نظرِ تاییدشده — لایکِ نظرِ تاییدنشده‌ی خودِ کسی معنی
ندارد، چون در فهرستِ عمومی اصلا دیده نمی‌شود."""
permission_classes = [IsAuthenticated]
def post(self, request, pk):
review = Review.objects.filter(pk=pk, is_approved=True).first()
if review is None:
raise NotFound("نظر یافت نشد.")
deleted, _ = ReviewLike.objects.filter(review=review, user=request.user).delete()
if not deleted:
try:
ReviewLike.objects.create(review=review, user=request.user)
except IntegrityError:
pass # درخواستِ هم‌زمانِ دوم — بی‌ضرر، نتیجه هرحال «لایک‌شده» است.
like_count = review.likes.count()
liked_by_me = review.likes.filter(user=request.user).exists()
return Response({"like_count": like_count, "liked_by_me": liked_by_me})