first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:27:56 +03:00
commit d9f1ea341e
1021 changed files with 70645 additions and 0 deletions

140
cart/API_DOCS.md Normal file
View 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
View File

3
cart/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

9
cart/apps.py Normal file
View 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
View 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()

View File

@@ -0,0 +1,4 @@
from .cart import Cart
def cart(request):
return {'cart': Cart(request)}

11
cart/forms.py Normal file
View 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)

View 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')},
},
),
]

View File

33
cart/models.py Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
cart/urls.py Normal file
View 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
View 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)