first commit
This commit is contained in:
140
cart/API_DOCS.md
Normal file
140
cart/API_DOCS.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Sepet (Cart) API Kullanım Kılavuzu
|
||||
|
||||
Bu doküman, alışveriş sepeti (Shopping Cart) API'sinin nasıl kullanılacağını, uç noktaları (endpoints), istek parametrelerini ve örnek senaryoları içerir.
|
||||
|
||||
## Genel Bilgiler
|
||||
|
||||
- **Base URL:** `/api/v1/cart/`
|
||||
- **Mantık:** Sepet, sunucu tarafında **Session (Oturum)** tabanlı çalışır.
|
||||
- **Önemli Not:** İstemci (Frontend/Mobile), sunucudan dönen `sessionid` çerezini (cookie) saklamalı ve sonraki tüm isteklerde header içinde geri göndermelidir. Aksi takdirde her istekte yeni, boş bir sepet oluşturulur.
|
||||
|
||||
---
|
||||
|
||||
## Uç Noktalar (Endpoints)
|
||||
|
||||
### 1. Sepeti Görüntüle
|
||||
|
||||
Mevcut sepetin içeriğini ve toplam tutarını getirir.
|
||||
|
||||
- **URL:** `/api/v1/cart/`
|
||||
- **Method:** `GET`
|
||||
|
||||
**Örnek Yanıt:**
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"product": {
|
||||
"id": 1,
|
||||
"title": "Örnek Ürün",
|
||||
"price": 100.00,
|
||||
"images": "http://localhost:8000/media/...",
|
||||
...
|
||||
},
|
||||
"quantity": 2,
|
||||
"price": "100.00",
|
||||
"total_price": "200.00"
|
||||
}
|
||||
],
|
||||
"total_price": "200.00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Sepete Ürün Ekle / Güncelle
|
||||
|
||||
Sepete yeni bir ürün ekler veya mevcut ürünün miktarını değiştirir.
|
||||
|
||||
- **URL:** `/api/v1/cart/add/`
|
||||
- **Method:** `POST`
|
||||
|
||||
**Parametreler (Body - JSON):**
|
||||
|
||||
| Parametre | Tip | Zorunlu | Açıklama |
|
||||
|-----------|-----|---------|----------|
|
||||
| `product_id` | Integer | Evet | Eklenecek ürünün ID'si. |
|
||||
| `quantity` | Integer | Hayır | Miktar (Varsayılan: 1). |
|
||||
| `override_quantity` | Boolean | Hayır | `true` ise miktarı direkt eşitler, `false` ise mevcut miktarın üzerine ekler (Varsayılan: `false`). |
|
||||
|
||||
#### Senaryo A: Sepete Ürün Ekleme (veya Miktar Artırma)
|
||||
Mevcut miktarın üzerine ekler. (Örn: Sepette 1 tane var, 2 tane daha ekle = 3 olur).
|
||||
|
||||
**İstek:**
|
||||
```json
|
||||
{
|
||||
"product_id": 1,
|
||||
"quantity": 2,
|
||||
"override_quantity": false
|
||||
}
|
||||
```
|
||||
|
||||
#### Senaryo B: Miktarı Güncelleme / Azaltma
|
||||
Miktarı direkt olarak belirtilen sayıya eşitler. (Örn: Sepette 5 tane var, 4'e düşürmek istiyorsunuz).
|
||||
|
||||
**İstek:**
|
||||
```json
|
||||
{
|
||||
"product_id": 1,
|
||||
"quantity": 4,
|
||||
"override_quantity": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Senaryo C: Miktarı Sıfırlayarak Silme
|
||||
Eğer `override_quantity: true` iken `quantity: 0` gönderirseniz, ürün sepetten silinir.
|
||||
|
||||
**İstek:**
|
||||
```json
|
||||
{
|
||||
"product_id": 1,
|
||||
"quantity": 0,
|
||||
"override_quantity": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Sepetten Ürün Silme
|
||||
|
||||
Belirli bir ürünü sepetten tamamen kaldırır.
|
||||
|
||||
- **URL:** `/api/v1/cart/remove/<product_id>/`
|
||||
- **Method:** `DELETE`
|
||||
|
||||
**Örnek:** `/api/v1/cart/remove/1/`
|
||||
|
||||
**Yanıt:** Güncel sepet içeriğini döndürür (Ekleme işlemiyle aynı formatta).
|
||||
|
||||
---
|
||||
|
||||
### 4. Sepeti Temizle
|
||||
|
||||
Sepetteki tüm ürünleri siler.
|
||||
|
||||
- **URL:** `/api/v1/cart/clear/`
|
||||
- **Method:** `POST`
|
||||
|
||||
**Yanıt:**
|
||||
```json
|
||||
{
|
||||
"message": "Cart cleared"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Entegrasyonu İçin İpuçları (React/Vue/Mobile)
|
||||
|
||||
1. **Cookie Yönetimi:** Axios veya Fetch kullanırken `credentials: 'include'` veya `withCredentials: true` ayarının açık olduğundan emin olun. Bu, Django'nun session cookie'sini tarayıcının saklamasını ve göndermesini sağlar.
|
||||
|
||||
```javascript
|
||||
// Axios Örneği
|
||||
axios.post('/api/v1/cart/add/', data, {
|
||||
withCredentials: true
|
||||
});
|
||||
```
|
||||
|
||||
2. **Ürün Detayları:** Sepet yanıtı (`items` dizisi) içindeki `product` objesi, `ProductSerializer`'dan gelen tüm veriyi (resim, slug, başlık vb.) içerir. Ekstra bir istek atmanıza gerek yoktur.
|
||||
|
||||
3. **Toplam Fiyat:** Sepet toplamı `total_price` alanında string decimal olarak gelir (Örn: "1250.50"). Frontend'de gösterirken formatlamanız gerekebilir.
|
||||
0
cart/__init__.py
Normal file
0
cart/__init__.py
Normal file
3
cart/admin.py
Normal file
3
cart/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
9
cart/apps.py
Normal file
9
cart/apps.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CartConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'cart'
|
||||
|
||||
def ready(self):
|
||||
import cart.signals
|
||||
127
cart/cart.py
Normal file
127
cart/cart.py
Normal file
@@ -0,0 +1,127 @@
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
from product.models import Product
|
||||
from .models import Cart as CartModel, CartItem
|
||||
|
||||
class Cart(object):
|
||||
def __init__(self, request):
|
||||
"""
|
||||
Initialize the cart.
|
||||
"""
|
||||
self.session = request.session
|
||||
self.user = request.user
|
||||
|
||||
# Session cart initialization
|
||||
cart = self.session.get(settings.CART_SESSION_ID)
|
||||
if not cart:
|
||||
cart = self.session[settings.CART_SESSION_ID] = {}
|
||||
self.cart = cart
|
||||
|
||||
def add(self, product, quantity=1, override_quantity=False):
|
||||
"""
|
||||
Add a product to the cart or update its quantity.
|
||||
"""
|
||||
if self.user.is_authenticated:
|
||||
self._add_db(product, quantity, override_quantity)
|
||||
else:
|
||||
self._add_session(product, quantity, override_quantity)
|
||||
|
||||
def _add_session(self, product, quantity, override_quantity):
|
||||
product_id = str(product.id)
|
||||
if product_id not in self.cart:
|
||||
self.cart[product_id] = {'quantity': 0, 'price': str(product.price)}
|
||||
|
||||
if override_quantity:
|
||||
self.cart[product_id]['quantity'] = quantity
|
||||
else:
|
||||
self.cart[product_id]['quantity'] += quantity
|
||||
|
||||
if self.cart[product_id]['quantity'] <= 0:
|
||||
self.remove(product)
|
||||
else:
|
||||
self.save()
|
||||
|
||||
def _add_db(self, product, quantity, override_quantity):
|
||||
cart, created = CartModel.objects.get_or_create(user=self.user)
|
||||
cart_item, item_created = CartItem.objects.get_or_create(cart=cart, product=product)
|
||||
|
||||
if override_quantity:
|
||||
cart_item.quantity = quantity
|
||||
else:
|
||||
if not item_created:
|
||||
cart_item.quantity += quantity
|
||||
else:
|
||||
cart_item.quantity = quantity # Yeni oluşturulduysa zaten default 1 değil, gelen quantity olmalı
|
||||
|
||||
if cart_item.quantity <= 0:
|
||||
cart_item.delete()
|
||||
else:
|
||||
cart_item.save()
|
||||
|
||||
def save(self):
|
||||
# mark the session as "modified" to make sure it gets saved
|
||||
self.session.modified = True
|
||||
|
||||
def remove(self, product):
|
||||
"""
|
||||
Remove a product from the cart.
|
||||
"""
|
||||
if self.user.is_authenticated:
|
||||
CartItem.objects.filter(cart__user=self.user, product=product).delete()
|
||||
else:
|
||||
product_id = str(product.id)
|
||||
if product_id in self.cart:
|
||||
del self.cart[product_id]
|
||||
self.save()
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Iterate over the items in the cart and get the products
|
||||
from the database.
|
||||
"""
|
||||
if self.user.is_authenticated:
|
||||
# DB'den oku
|
||||
cart, created = CartModel.objects.get_or_create(user=self.user)
|
||||
for item in cart.items.select_related('product').all():
|
||||
yield {
|
||||
'product': item.product,
|
||||
'quantity': item.quantity,
|
||||
'price': Decimal(item.product.price),
|
||||
'total_price': Decimal(item.product.price) * item.quantity
|
||||
}
|
||||
else:
|
||||
# Session'dan oku
|
||||
product_ids = self.cart.keys()
|
||||
products = Product.objects.filter(id__in=product_ids)
|
||||
cart = self.cart.copy()
|
||||
for product in products:
|
||||
cart[str(product.id)]['product'] = product
|
||||
for item in cart.values():
|
||||
item['price'] = Decimal(item['price'])
|
||||
item['total_price'] = item['price'] * item['quantity']
|
||||
yield item
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Count all items in the cart.
|
||||
"""
|
||||
if self.user.is_authenticated:
|
||||
cart, created = CartModel.objects.get_or_create(user=self.user)
|
||||
return sum(item.quantity for item in cart.items.all())
|
||||
else:
|
||||
return sum(item['quantity'] for item in self.cart.values())
|
||||
|
||||
def get_total_price(self):
|
||||
if self.user.is_authenticated:
|
||||
cart, created = CartModel.objects.get_or_create(user=self.user)
|
||||
return sum(item.total_price for item in cart.items.all())
|
||||
else:
|
||||
return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values())
|
||||
|
||||
def clear(self):
|
||||
if self.user.is_authenticated:
|
||||
cart, created = CartModel.objects.get_or_create(user=self.user)
|
||||
cart.items.all().delete()
|
||||
else:
|
||||
del self.session[settings.CART_SESSION_ID]
|
||||
self.save()
|
||||
4
cart/context_processors.py
Normal file
4
cart/context_processors.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .cart import Cart
|
||||
|
||||
def cart(request):
|
||||
return {'cart': Cart(request)}
|
||||
11
cart/forms.py
Normal file
11
cart/forms.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django import forms
|
||||
|
||||
PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i in range(1, 21)]
|
||||
|
||||
class CartAddProductForm(forms.Form):
|
||||
quantity = forms.TypedChoiceField(
|
||||
choices=PRODUCT_QUANTITY_CHOICES,
|
||||
coerce=int)
|
||||
override = forms.BooleanField(required=False,
|
||||
initial=False,
|
||||
widget=forms.HiddenInput)
|
||||
39
cart/migrations/0001_initial.py
Normal file
39
cart/migrations/0001_initial.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# Generated by Django 6.0 on 2026-01-19 14:31
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('product', '0022_alter_product_content_alter_product_images'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Cart',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='cart', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CartItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', models.PositiveIntegerField(default=1)),
|
||||
('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='cart.cart')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='product.product')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('cart', 'product')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
cart/migrations/__init__.py
Normal file
0
cart/migrations/__init__.py
Normal file
33
cart/models.py
Normal file
33
cart/models.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from product.models import Product
|
||||
|
||||
class Cart(models.Model):
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='cart')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.email} - Cart"
|
||||
|
||||
def get_total_price(self):
|
||||
return sum(item.total_price for item in self.items.all())
|
||||
|
||||
class CartItem(models.Model):
|
||||
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items')
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
quantity = models.PositiveIntegerField(default=1)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('cart', 'product')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.cart.user.email} - {self.product.title}"
|
||||
|
||||
@property
|
||||
def price(self):
|
||||
return self.product.price
|
||||
|
||||
@property
|
||||
def total_price(self):
|
||||
return self.price * self.quantity
|
||||
25
cart/serializers.py
Normal file
25
cart/serializers.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from rest_framework import serializers
|
||||
from product.models import Product
|
||||
from product.serializers import ProductSerializer
|
||||
|
||||
# CartItemSerializer artık hem dict (session) hem de model instance (db) ile çalışabilmeli.
|
||||
# Serializer'lar varsayılan olarak object attribute'larına erişir.
|
||||
# Dict erişimi için source kullanabiliriz veya to_representation override edebiliriz.
|
||||
# Ancak en temizi, view tarafında veriyi standart bir yapıya (list of dicts) dönüştürüp serializer'a vermektir.
|
||||
# Cart sınıfındaki __iter__ metodu zaten bunu yapıyor (hem DB hem Session için dict döndürüyor).
|
||||
# Bu yüzden mevcut serializer yapısını koruyabiliriz.
|
||||
|
||||
class CartItemSerializer(serializers.Serializer):
|
||||
product = ProductSerializer(read_only=True)
|
||||
quantity = serializers.IntegerField()
|
||||
price = serializers.DecimalField(max_digits=10, decimal_places=2)
|
||||
total_price = serializers.DecimalField(max_digits=10, decimal_places=2)
|
||||
|
||||
class CartSerializer(serializers.Serializer):
|
||||
items = CartItemSerializer(many=True, read_only=True)
|
||||
total_price = serializers.DecimalField(max_digits=10, decimal_places=2)
|
||||
|
||||
class CartAddProductSerializer(serializers.Serializer):
|
||||
product_id = serializers.IntegerField()
|
||||
quantity = serializers.IntegerField(min_value=0, default=1)
|
||||
override_quantity = serializers.BooleanField(required=False, default=False)
|
||||
39
cart/signals.py
Normal file
39
cart/signals.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from django.contrib.auth.signals import user_logged_in
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
from .models import Cart, CartItem
|
||||
from product.models import Product
|
||||
|
||||
@receiver(user_logged_in)
|
||||
def merge_cart_on_login(sender, user, request, **kwargs):
|
||||
"""
|
||||
Kullanıcı giriş yaptığında session sepetini veritabanı sepetiyle birleştirir.
|
||||
"""
|
||||
session_cart = request.session.get(settings.CART_SESSION_ID)
|
||||
|
||||
if session_cart:
|
||||
# Kullanıcının DB sepetini al veya oluştur
|
||||
db_cart, created = Cart.objects.get_or_create(user=user)
|
||||
|
||||
for product_id, item_data in session_cart.items():
|
||||
quantity = item_data['quantity']
|
||||
product = Product.objects.get(id=product_id)
|
||||
|
||||
# Ürün zaten DB sepetinde var mı?
|
||||
cart_item, item_created = CartItem.objects.get_or_create(cart=db_cart, product=product)
|
||||
|
||||
if not item_created:
|
||||
# Varsa miktarı artır
|
||||
cart_item.quantity += quantity
|
||||
else:
|
||||
# Yoksa miktarı ayarla (default 1 olduğu için üzerine eklemiyoruz, direkt atıyoruz ama get_or_create default ile oluşturduysa quantity 1 olabilir, o yüzden dikkat)
|
||||
# get_or_create default=1 ile oluşturur. Biz session'dan geleni kullanmalıyız.
|
||||
# Ancak item_created True ise yeni oluştu demektir ve default değeri almıştır.
|
||||
# Bizim session'daki quantity'yi atamamız lazım.
|
||||
cart_item.quantity = quantity
|
||||
|
||||
cart_item.save()
|
||||
|
||||
# Session sepetini temizle
|
||||
del request.session[settings.CART_SESSION_ID]
|
||||
request.session.modified = True
|
||||
3
cart/tests.py
Normal file
3
cart/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
11
cart/urls.py
Normal file
11
cart/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'cart'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.CartDetailView.as_view(), name='cart_detail'),
|
||||
path('add/', views.CartAddView.as_view(), name='cart_add'),
|
||||
path('remove/<int:product_id>/', views.CartRemoveView.as_view(), name='cart_remove'),
|
||||
path('clear/', views.CartClearView.as_view(), name='cart_clear'),
|
||||
]
|
||||
66
cart/views.py
Normal file
66
cart/views.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.shortcuts import get_object_or_404
|
||||
from product.models import Product
|
||||
from .cart import Cart
|
||||
from .serializers import CartSerializer, CartAddProductSerializer
|
||||
|
||||
class CartDetailView(APIView):
|
||||
def get(self, request):
|
||||
cart = Cart(request)
|
||||
# Cart.__iter__ zaten dict döndürüyor, direkt listeye çevirebiliriz.
|
||||
cart_items = list(cart)
|
||||
|
||||
data = {
|
||||
'items': cart_items,
|
||||
'total_price': cart.get_total_price()
|
||||
}
|
||||
|
||||
serializer = CartSerializer(data)
|
||||
return Response(serializer.data)
|
||||
|
||||
class CartAddView(APIView):
|
||||
def post(self, request):
|
||||
serializer = CartAddProductSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
product_id = serializer.validated_data['product_id']
|
||||
quantity = serializer.validated_data['quantity']
|
||||
override_quantity = serializer.validated_data['override_quantity']
|
||||
|
||||
product = get_object_or_404(Product, id=product_id)
|
||||
cart = Cart(request)
|
||||
cart.add(product=product, quantity=quantity, override_quantity=override_quantity)
|
||||
|
||||
# Güncel sepeti döndür
|
||||
cart_items = list(cart)
|
||||
|
||||
data = {
|
||||
'items': cart_items,
|
||||
'total_price': cart.get_total_price()
|
||||
}
|
||||
|
||||
return Response(CartSerializer(data).data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class CartRemoveView(APIView):
|
||||
def delete(self, request, product_id):
|
||||
product = get_object_or_404(Product, id=product_id)
|
||||
cart = Cart(request)
|
||||
cart.remove(product)
|
||||
|
||||
# Güncel sepeti döndür
|
||||
cart_items = list(cart)
|
||||
|
||||
data = {
|
||||
'items': cart_items,
|
||||
'total_price': cart.get_total_price()
|
||||
}
|
||||
|
||||
return Response(CartSerializer(data).data, status=status.HTTP_200_OK)
|
||||
|
||||
class CartClearView(APIView):
|
||||
def post(self, request):
|
||||
cart = Cart(request)
|
||||
cart.clear()
|
||||
return Response({'message': 'Cart cleared'}, status=status.HTTP_200_OK)
|
||||
Reference in New Issue
Block a user