Modernes Furry-Design implementiert - Glassmorphism, responsive Design, Mobile-First - Verbesserte Navigation mit Emoji-Icons - Neue Homepage-Sektionen: Features, Testimonials, Newsletter - CSS Variables für konsistente Farbpalette - Dark Mode Support - Performance-Optimierung

This commit is contained in:
thomas 2025-07-19 14:24:37 +02:00
commit 7865a188d1
191 changed files with 37260 additions and 0 deletions

267
.gitignore vendored Normal file
View File

@ -0,0 +1,267 @@
# Django
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
db.sqlite3
db.sqlite3-journal
media/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Virtual environment
venv/
env/
ENV/
env.bak/
venv.bak/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
public
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Database
*.db
*.sqlite
*.sqlite3
# Static files (if collected)
staticfiles/
# Media files
media/
# Backup files
*.bak
*.backup
*.old
# Certificate files
*.pem
*.key
*.crt
# Docker
.dockerignore
Dockerfile.prod
# Testing
.coverage
htmlcov/
.tox/
.pytest_cache/
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Project specific
uploads/
temp/
cache/

39
Dockerfile Normal file
View File

@ -0,0 +1,39 @@
# Dockerfile für Kasico Fursuit Shop
FROM python:3.11-slim
# Setze Umgebungsvariablen
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DJANGO_SETTINGS_MODULE=webshop.settings
# Installiere System-Abhängigkeiten
RUN apt-get update && apt-get install -y \
gcc \
g++ \
libpq-dev \
libjpeg-dev \
libpng-dev \
libfreetype6-dev \
libwebp-dev \
&& rm -rf /var/lib/apt/lists/*
# Erstelle Arbeitsverzeichnis
WORKDIR /app
# Kopiere Requirements
COPY requirements.txt .
# Installiere Python-Abhängigkeiten
RUN pip install --no-cache-dir -r requirements.txt
# Kopiere Projektdateien
COPY . .
# Erstelle Media-Verzeichnis
RUN mkdir -p /app/media
# Exponiere Port
EXPOSE 8000
# Starte Django Development Server
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

41
Dockerfile.simple Normal file
View File

@ -0,0 +1,41 @@
# Einfaches Dockerfile für Kasico Fursuit Shop (SQLite)
FROM python:3.11-slim
# Setze Umgebungsvariablen
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DJANGO_SETTINGS_MODULE=webshop.settings-simple
# Installiere System-Abhängigkeiten
RUN apt-get update && apt-get install -y \
gcc \
g++ \
libjpeg-dev \
libpng-dev \
libfreetype6-dev \
libwebp-dev \
&& rm -rf /var/lib/apt/lists/*
# Erstelle Arbeitsverzeichnis
WORKDIR /app
# Kopiere Requirements
COPY requirements-simple.txt requirements.txt
# Installiere Python-Abhängigkeiten
RUN pip install --no-cache-dir -r requirements.txt
# Kopiere Projektdateien
COPY . .
# Erstelle statische Dateien
RUN python manage.py collectstatic --noinput
# Erstelle Media-Verzeichnis
RUN mkdir -p /app/media
# Exponiere Port
EXPOSE 8000
# Starte Django Development Server
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

126
auction/forms.py Normal file
View File

@ -0,0 +1,126 @@
"""
Auction Forms
"""
from django import forms
from .models import Auction, Bid
class AuctionForm(forms.ModelForm):
"""Form für Auktion erstellen/bearbeiten"""
class Meta:
model = Auction
fields = [
'title', 'description', 'fursuit_type', 'character_description',
'reference_images', 'special_requirements', 'starting_bid',
'reserve_price', 'duration_days'
]
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Auktion Titel'
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Beschreibung der Auktion'
}),
'fursuit_type': forms.Select(attrs={
'class': 'form-control'
}),
'character_description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 6,
'placeholder': 'Detaillierte Beschreibung des Charakters...'
}),
'special_requirements': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Besondere Anforderungen (optional)'
}),
'starting_bid': forms.NumberInput(attrs={
'class': 'form-control',
'min': '0',
'step': '0.01',
'placeholder': '0.00'
}),
'reserve_price': forms.NumberInput(attrs={
'class': 'form-control',
'min': '0',
'step': '0.01',
'placeholder': 'Reserve Price (optional)'
}),
'duration_days': forms.NumberInput(attrs={
'class': 'form-control',
'min': '1',
'max': '30',
'placeholder': '7'
}),
}
def clean_starting_bid(self):
"""Starting Bid validieren"""
starting_bid = self.cleaned_data.get('starting_bid')
if starting_bid <= 0:
raise forms.ValidationError('Starting Bid muss größer als 0 sein')
return starting_bid
def clean_reserve_price(self):
"""Reserve Price validieren"""
reserve_price = self.cleaned_data.get('reserve_price')
starting_bid = self.cleaned_data.get('starting_bid')
if reserve_price and reserve_price <= starting_bid:
raise forms.ValidationError('Reserve Price muss höher als Starting Bid sein')
return reserve_price
def clean_duration_days(self):
"""Duration validieren"""
duration = self.cleaned_data.get('duration_days')
if duration < 1 or duration > 30:
raise forms.ValidationError('Duration muss zwischen 1 und 30 Tagen liegen')
return duration
class BidForm(forms.ModelForm):
"""Form für Gebot platzieren"""
class Meta:
model = Bid
fields = ['amount']
widgets = {
'amount': forms.NumberInput(attrs={
'class': 'form-control',
'min': '0',
'step': '0.01',
'placeholder': 'Dein Gebot'
})
}
def __init__(self, *args, **kwargs):
self.auction = kwargs.pop('auction', None)
super().__init__(*args, **kwargs)
if self.auction:
min_bid = self.auction.current_bid or self.auction.starting_bid
self.fields['amount'].widget.attrs['min'] = float(min_bid) + 0.01
self.fields['amount'].widget.attrs['placeholder'] = f'Min. €{float(min_bid) + 0.01}'
def clean_amount(self):
"""Amount validieren"""
amount = self.cleaned_data.get('amount')
if not self.auction:
raise forms.ValidationError('Auktion nicht gefunden')
if not self.auction.is_active:
raise forms.ValidationError('Auktion ist nicht aktiv')
min_bid = self.auction.current_bid or self.auction.starting_bid
if amount <= min_bid:
raise forms.ValidationError(f'Gebot muss höher als €{float(min_bid)} sein')
return amount

316
auction/models.py Normal file
View File

@ -0,0 +1,316 @@
"""
Auction Models für Custom Order Biet-System
"""
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.core.validators import MinValueValidator
import uuid
class Auction(models.Model):
"""Auktion für Custom Order"""
AUCTION_STATUS_CHOICES = [
('draft', 'Entwurf'),
('active', 'Aktiv'),
('paused', 'Pausiert'),
('ended', 'Beendet'),
('cancelled', 'Storniert'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=200)
description = models.TextField()
# Custom Order Details
fursuit_type = models.CharField(max_length=50, choices=[
('fullsuit', 'Fullsuit'),
('partial', 'Partial'),
('head', 'Head Only'),
('handpaws', 'Handpaws'),
('feetpaws', 'Feetpaws'),
('tail', 'Tail'),
('custom', 'Custom'),
])
character_description = models.TextField()
reference_images = models.JSONField(default=list) # URLs zu Referenzbildern
special_requirements = models.TextField(blank=True)
# Auction Settings
starting_bid = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(0)])
reserve_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
current_bid = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
# Time Settings
start_time = models.DateTimeField()
end_time = models.DateTimeField()
duration_days = models.IntegerField(default=7)
# Status & Tracking
status = models.CharField(max_length=20, choices=AUCTION_STATUS_CHOICES, default='draft')
winner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='won_auctions')
total_bids = models.IntegerField(default=0)
total_bidders = models.IntegerField(default=0)
# Metadata
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='created_auctions')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# SEO & Visibility
is_featured = models.BooleanField(default=False)
view_count = models.IntegerField(default=0)
class Meta:
ordering = ['-created_at']
verbose_name = 'Auktion'
verbose_name_plural = 'Auktionen'
def __str__(self):
return f"{self.title} - {self.get_status_display()}"
@property
def is_active(self):
"""Prüfen ob Auktion aktiv ist"""
now = timezone.now()
return (
self.status == 'active' and
self.start_time <= now <= self.end_time
)
@property
def time_remaining(self):
"""Verbleibende Zeit"""
if not self.is_active:
return None
remaining = self.end_time - timezone.now()
return remaining
@property
def time_remaining_formatted(self):
"""Formatierte verbleibende Zeit"""
if not self.time_remaining:
return "Beendet"
days = self.time_remaining.days
hours = self.time_remaining.seconds // 3600
minutes = (self.time_remaining.seconds % 3600) // 60
if days > 0:
return f"{days}d {hours}h {minutes}m"
elif hours > 0:
return f"{hours}h {minutes}m"
else:
return f"{minutes}m"
@property
def has_reserve(self):
"""Prüfen ob Reserve Price gesetzt ist"""
return self.reserve_price is not None
@property
def reserve_met(self):
"""Prüfen ob Reserve Price erreicht wurde"""
if not self.has_reserve:
return True
return self.current_bid and self.current_bid >= self.reserve_price
def get_highest_bid(self):
"""Höchstes Gebot abrufen"""
return self.bids.order_by('-amount').first()
def place_bid(self, user, amount):
"""Gebot platzieren"""
if not self.is_active:
raise ValueError("Auktion ist nicht aktiv")
if amount <= self.current_bid:
raise ValueError("Gebot muss höher als aktuelles Gebot sein")
# Gebot erstellen
bid = Bid.objects.create(
auction=self,
bidder=user,
amount=amount
)
# Auktion aktualisieren
self.current_bid = amount
self.total_bids += 1
self.save()
return bid
def end_auction(self):
"""Auktion beenden"""
if self.status != 'active':
return
highest_bid = self.get_highest_bid()
if highest_bid and self.reserve_met:
self.winner = highest_bid.bidder
self.status = 'ended'
else:
self.status = 'ended' # Kein Gewinner wenn Reserve nicht erreicht
self.save()
# Notification an Gewinner senden
if self.winner:
self.send_winner_notification()
def send_winner_notification(self):
"""Benachrichtigung an Gewinner senden"""
# Email Notification
from django.core.mail import send_mail
from django.template.loader import render_to_string
subject = f"Gratulation! Du hast die Auktion '{self.title}' gewonnen!"
message = render_to_string('auction/emails/winner_notification.html', {
'auction': self,
'user': self.winner
})
send_mail(
subject,
message,
'noreply@kasico.de',
[self.winner.email],
html_message=message
)
class Bid(models.Model):
"""Gebot in einer Auktion"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
auction = models.ForeignKey(Auction, on_delete=models.CASCADE, related_name='bids')
bidder = models.ForeignKey(User, on_delete=models.CASCADE, related_name='bids')
amount = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(0)])
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'Gebot'
verbose_name_plural = 'Gebote'
unique_together = ['auction', 'bidder', 'amount'] # Verhindert doppelte Gebote
def __str__(self):
return f"{self.bidder.username} - €{self.amount} - {self.auction.title}"
def save(self, *args, **kwargs):
"""Gebot speichern und Auktion aktualisieren"""
is_new = self.pk is None
super().save(*args, **kwargs)
if is_new:
# Auktion aktualisieren
self.auction.current_bid = self.amount
self.auction.total_bids += 1
# Unique bidders zählen
unique_bidders = self.auction.bids.values('bidder').distinct().count()
self.auction.total_bidders = unique_bidders
self.auction.save()
# Outbid Notifications senden
self.send_outbid_notifications()
def send_outbid_notifications(self):
"""Benachrichtigungen an überbotene Bieter senden"""
previous_bidders = self.auction.bids.filter(
created_at__lt=self.created_at
).exclude(bidder=self.bidder).values_list('bidder', flat=True).distinct()
for bidder_id in previous_bidders:
bidder = User.objects.get(id=bidder_id)
self.send_outbid_notification(bidder)
def send_outbid_notification(self, bidder):
"""Einzelne Outbid-Benachrichtigung senden"""
from django.core.mail import send_mail
from django.template.loader import render_to_string
subject = f"Du wurdest überboten - {self.auction.title}"
message = render_to_string('auction/emails/outbid_notification.html', {
'auction': self.auction,
'bid': self,
'user': bidder
})
send_mail(
subject,
message,
'noreply@kasico.de',
[bidder.email],
html_message=message
)
class AuctionWatch(models.Model):
"""Auktion Watchlist"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='watched_auctions')
auction = models.ForeignKey(Auction, on_delete=models.CASCADE, related_name='watchers')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ['user', 'auction']
verbose_name = 'Auktion Watchlist'
verbose_name_plural = 'Auktion Watchlists'
def __str__(self):
return f"{self.user.username} beobachtet {self.auction.title}"
class AuctionAnalytics(models.Model):
"""Auktion Analytics"""
auction = models.OneToOneField(Auction, on_delete=models.CASCADE, related_name='analytics')
total_views = models.IntegerField(default=0)
unique_visitors = models.IntegerField(default=0)
total_bids = models.IntegerField(default=0)
unique_bidders = models.IntegerField(default=0)
average_bid_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
highest_bid_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
conversion_rate = models.FloatField(default=0) # Views zu Bids
engagement_score = models.FloatField(default=0) # Kombinierter Score
class Meta:
verbose_name = 'Auktion Analytics'
verbose_name_plural = 'Auktion Analytics'
def __str__(self):
return f"Analytics für {self.auction.title}"
def update_analytics(self):
"""Analytics aktualisieren"""
bids = self.auction.bids.all()
self.total_bids = bids.count()
self.unique_bidders = bids.values('bidder').distinct().count()
if self.total_bids > 0:
self.average_bid_amount = bids.aggregate(
avg=models.Avg('amount')
)['avg']
self.highest_bid_amount = bids.aggregate(
max=models.Max('amount')
)['max']
if self.total_views > 0:
self.conversion_rate = (self.total_bids / self.total_views) * 100
# Engagement Score berechnen
self.engagement_score = (
self.total_views * 0.3 +
self.total_bids * 0.4 +
self.unique_bidders * 0.3
)
self.save()

31
auction/urls.py Normal file
View File

@ -0,0 +1,31 @@
"""
Auction URL Configuration
"""
from django.urls import path
from . import views
app_name = 'auction'
urlpatterns = [
# Auction Views
path('', views.auction_list, name='auction_list'),
path('<uuid:auction_id>/', views.auction_detail, name='auction_detail'),
path('create/', views.auction_create, name='auction_create'),
path('<uuid:auction_id>/edit/', views.auction_edit, name='auction_edit'),
# User Actions
path('<uuid:auction_id>/bid/', views.place_bid, name='place_bid'),
path('<uuid:auction_id>/watch/', views.toggle_watch, name='toggle_watch'),
path('my-auctions/', views.my_auctions, name='my_auctions'),
path('my-bids/', views.my_bids, name='my_bids'),
path('watchlist/', views.watchlist, name='watchlist'),
# Analytics
path('<uuid:auction_id>/analytics/', views.auction_analytics, name='analytics'),
# API Endpoints
path('api/<uuid:auction_id>/', views.auction_api, name='auction_api'),
path('api/<uuid:auction_id>/bids/', views.bid_history_api, name='bid_history_api'),
path('api/<uuid:auction_id>/bid/', views.place_bid_api, name='place_bid_api'),
]

372
auction/views.py Normal file
View File

@ -0,0 +1,372 @@
"""
Auction Views für Biet-System
"""
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.http import JsonResponse
from django.utils import timezone
from django.db.models import Q, Count, Max
from django.core.paginator import Paginator
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from .models import Auction, Bid, AuctionWatch, AuctionAnalytics
from .forms import AuctionForm, BidForm
import json
def auction_list(request):
"""Liste aller Auktionen"""
# Filter
status_filter = request.GET.get('status', '')
fursuit_type_filter = request.GET.get('fursuit_type', '')
search_query = request.GET.get('search', '')
auctions = Auction.objects.select_related('created_by', 'winner').prefetch_related('bids')
if status_filter:
auctions = auctions.filter(status=status_filter)
if fursuit_type_filter:
auctions = auctions.filter(fursuit_type=fursuit_type_filter)
if search_query:
auctions = auctions.filter(
Q(title__icontains=search_query) |
Q(description__icontains=search_query) |
Q(character_description__icontains=search_query)
)
# Nur aktive Auktionen anzeigen (außer für Admins)
if not request.user.is_staff:
auctions = auctions.filter(status='active')
# Sortierung
sort_by = request.GET.get('sort', '-created_at')
auctions = auctions.order_by(sort_by)
# Pagination
paginator = Paginator(auctions, 12)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
# Statistiken
total_auctions = auctions.count()
active_auctions = auctions.filter(status='active').count()
total_bids = sum(auction.total_bids for auction in page_obj)
context = {
'page_obj': page_obj,
'status_filter': status_filter,
'fursuit_type_filter': fursuit_type_filter,
'search_query': search_query,
'sort_by': sort_by,
'total_auctions': total_auctions,
'active_auctions': active_auctions,
'total_bids': total_bids,
'status_choices': Auction.AUCTION_STATUS_CHOICES,
'fursuit_type_choices': Auction._meta.get_field('fursuit_type').choices,
}
return render(request, 'auction/auction_list.html', context)
def auction_detail(request, auction_id):
"""Auktion Detail-Ansicht"""
auction = get_object_or_404(Auction, id=auction_id)
# View Count erhöhen
auction.view_count += 1
auction.save()
# Analytics aktualisieren
analytics, created = AuctionAnalytics.objects.get_or_create(auction=auction)
analytics.total_views += 1
analytics.save()
# Bid History
bid_history = auction.bids.select_related('bidder').order_by('-created_at')[:10]
# User's current bid
user_bid = None
if request.user.is_authenticated:
user_bid = auction.bids.filter(bidder=request.user).order_by('-amount').first()
# Watchlist Status
is_watching = False
if request.user.is_authenticated:
is_watching = auction.watchers.filter(user=request.user).exists()
# Similar Auctions
similar_auctions = Auction.objects.filter(
fursuit_type=auction.fursuit_type,
status='active'
).exclude(id=auction.id)[:3]
context = {
'auction': auction,
'bid_history': bid_history,
'user_bid': user_bid,
'is_watching': is_watching,
'similar_auctions': similar_auctions,
'bid_form': BidForm() if auction.is_active else None,
}
return render(request, 'auction/auction_detail.html', context)
@login_required
def auction_create(request):
"""Neue Auktion erstellen"""
if request.method == 'POST':
form = AuctionForm(request.POST, request.FILES)
if form.is_valid():
auction = form.save(commit=False)
auction.created_by = request.user
# Start- und Endzeit setzen
if not auction.start_time:
auction.start_time = timezone.now()
if not auction.end_time:
auction.end_time = auction.start_time + timezone.timedelta(days=auction.duration_days)
auction.save()
messages.success(request, 'Auktion erfolgreich erstellt!')
return redirect('auction:auction_detail', auction_id=auction.id)
else:
form = AuctionForm()
context = {
'form': form,
'title': 'Neue Auktion erstellen',
}
return render(request, 'auction/auction_form.html', context)
@login_required
def auction_edit(request, auction_id):
"""Auktion bearbeiten"""
auction = get_object_or_404(Auction, id=auction_id, created_by=request.user)
if request.method == 'POST':
form = AuctionForm(request.POST, request.FILES, instance=auction)
if form.is_valid():
form.save()
messages.success(request, 'Auktion erfolgreich aktualisiert!')
return redirect('auction:auction_detail', auction_id=auction.id)
else:
form = AuctionForm(instance=auction)
context = {
'form': form,
'auction': auction,
'title': 'Auktion bearbeiten',
}
return render(request, 'auction/auction_form.html', context)
@login_required
@require_POST
def place_bid(request, auction_id):
"""Gebot platzieren"""
auction = get_object_or_404(Auction, id=auction_id)
if not auction.is_active:
return JsonResponse({'error': 'Auktion ist nicht aktiv'}, status=400)
form = BidForm(request.POST)
if form.is_valid():
amount = form.cleaned_data['amount']
try:
bid = auction.place_bid(request.user, amount)
return JsonResponse({
'success': True,
'bid_id': str(bid.id),
'amount': float(bid.amount),
'current_bid': float(auction.current_bid),
'total_bids': auction.total_bids,
'message': 'Gebot erfolgreich platziert!'
})
except ValueError as e:
return JsonResponse({'error': str(e)}, status=400)
else:
return JsonResponse({'error': 'Ungültige Eingabe'}, status=400)
@login_required
@require_POST
def toggle_watch(request, auction_id):
"""Auktion zur Watchlist hinzufügen/entfernen"""
auction = get_object_or_404(Auction, id=auction_id)
watch, created = AuctionWatch.objects.get_or_create(
user=request.user,
auction=auction
)
if not created:
watch.delete()
is_watching = False
message = 'Von Watchlist entfernt'
else:
is_watching = True
message = 'Zur Watchlist hinzugefügt'
return JsonResponse({
'success': True,
'is_watching': is_watching,
'message': message
})
@login_required
def my_auctions(request):
"""Eigene Auktionen"""
auctions = Auction.objects.filter(created_by=request.user).order_by('-created_at')
# Filter
status_filter = request.GET.get('status', '')
if status_filter:
auctions = auctions.filter(status=status_filter)
context = {
'auctions': auctions,
'status_choices': Auction.AUCTION_STATUS_CHOICES,
'status_filter': status_filter,
}
return render(request, 'auction/my_auctions.html', context)
@login_required
def my_bids(request):
"""Eigene Gebote"""
bids = Bid.objects.filter(bidder=request.user).select_related('auction').order_by('-created_at')
context = {
'bids': bids,
}
return render(request, 'auction/my_bids.html', context)
@login_required
def watchlist(request):
"""Watchlist"""
watched_auctions = Auction.objects.filter(
watchers__user=request.user
).order_by('-created_at')
context = {
'watched_auctions': watched_auctions,
}
return render(request, 'auction/watchlist.html', context)
# API Views für AJAX
@csrf_exempt
def auction_api(request, auction_id):
"""API für Auktion-Details"""
auction = get_object_or_404(Auction, id=auction_id)
data = {
'id': str(auction.id),
'title': auction.title,
'description': auction.description,
'current_bid': float(auction.current_bid) if auction.current_bid else None,
'starting_bid': float(auction.starting_bid),
'reserve_price': float(auction.reserve_price) if auction.reserve_price else None,
'status': auction.status,
'is_active': auction.is_active,
'time_remaining': auction.time_remaining_formatted,
'total_bids': auction.total_bids,
'total_bidders': auction.total_bidders,
'winner': auction.winner.username if auction.winner else None,
'created_at': auction.created_at.isoformat(),
'end_time': auction.end_time.isoformat(),
}
return JsonResponse(data)
@csrf_exempt
def bid_history_api(request, auction_id):
"""API für Bid-History"""
auction = get_object_or_404(Auction, id=auction_id)
bids = auction.bids.select_related('bidder').order_by('-created_at')[:20]
bid_list = []
for bid in bids:
bid_list.append({
'id': str(bid.id),
'bidder': bid.bidder.username,
'amount': float(bid.amount),
'created_at': bid.created_at.isoformat(),
})
return JsonResponse({'bids': bid_list})
@csrf_exempt
def place_bid_api(request, auction_id):
"""API für Gebot platzieren"""
if request.method != 'POST':
return JsonResponse({'error': 'Nur POST erlaubt'}, status=405)
if not request.user.is_authenticated:
return JsonResponse({'error': 'Nicht eingeloggt'}, status=401)
try:
data = json.loads(request.body)
amount = float(data.get('amount', 0))
auction = get_object_or_404(Auction, id=auction_id)
if not auction.is_active:
return JsonResponse({'error': 'Auktion ist nicht aktiv'}, status=400)
bid = auction.place_bid(request.user, amount)
return JsonResponse({
'success': True,
'bid_id': str(bid.id),
'amount': float(bid.amount),
'current_bid': float(auction.current_bid),
'total_bids': auction.total_bids,
})
except (json.JSONDecodeError, ValueError) as e:
return JsonResponse({'error': str(e)}, status=400)
def auction_analytics(request, auction_id):
"""Auktion Analytics"""
auction = get_object_or_404(Auction, id=auction_id)
analytics = get_object_or_404(AuctionAnalytics, auction=auction)
# Bid Distribution
bid_distribution = auction.bids.extra(
select={'hour': "EXTRACT(hour FROM created_at)"}
).values('hour').annotate(count=Count('id')).order_by('hour')
# Top Bidders
top_bidders = auction.bids.values('bidder__username').annotate(
total_bids=Count('id'),
max_bid=Max('amount')
).order_by('-max_bid')[:5]
context = {
'auction': auction,
'analytics': analytics,
'bid_distribution': bid_distribution,
'top_bidders': top_bidders,
}
return render(request, 'auction/analytics.html', context)

1
chat/__init__.py Normal file
View File

@ -0,0 +1 @@

335
chat/consumers.py Normal file
View File

@ -0,0 +1,335 @@
"""
WebSocket Consumers für Live-Chat
"""
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.utils import timezone
from .models import ChatRoom, ChatMessage, UserOnlineStatus, ChatNotification
from django.contrib.auth.models import User
class ChatConsumer(AsyncWebsocketConsumer):
"""WebSocket Consumer für Chat-Funktionalität"""
async def connect(self):
"""WebSocket-Verbindung herstellen"""
self.room_id = self.scope['url_route']['kwargs']['room_id']
self.room_group_name = f'chat_{self.room_id}'
self.user = self.scope['user']
# Prüfen ob User Zugriff auf Chat-Raum hat
if await self.can_access_room():
# Zur Chat-Gruppe beitreten
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
# Online-Status aktualisieren
await self.update_online_status(True)
await self.accept()
# System-Nachricht senden
await self.send_system_message(f"{self.user.username} ist dem Chat beigetreten")
# Ungelesene Nachrichten als gelesen markieren
await self.mark_messages_as_read()
else:
await self.close()
async def disconnect(self, close_code):
"""WebSocket-Verbindung trennen"""
# Online-Status aktualisieren
await self.update_online_status(False)
# System-Nachricht senden
await self.send_system_message(f"{self.user.username} hat den Chat verlassen")
# Aus Chat-Gruppe austreten
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
"""Nachricht vom Client empfangen"""
try:
data = json.loads(text_data)
message_type = data.get('type', 'message')
if message_type == 'message':
await self.handle_message(data)
elif message_type == 'typing':
await self.handle_typing(data)
elif message_type == 'read':
await self.handle_read_messages()
elif message_type == 'file_upload':
await self.handle_file_upload(data)
except json.JSONDecodeError:
await self.send_error_message("Ungültiges JSON-Format")
except Exception as e:
await self.send_error_message(f"Fehler: {str(e)}")
async def handle_message(self, data):
"""Chat-Nachricht verarbeiten"""
content = data.get('content', '').strip()
message_type = data.get('message_type', 'text')
if not content and message_type == 'text':
return
# Nachricht in Datenbank speichern
message = await self.save_message(content, message_type, data.get('file_url'))
# Nachricht an alle im Chat senden
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': {
'id': str(message.id),
'sender': message.sender.username,
'sender_id': message.sender.id,
'content': message.content,
'message_type': message.message_type,
'file_url': message.file.url if message.file else None,
'image_url': message.image.url if message.image else None,
'created_at': message.created_at.isoformat(),
'is_system': message.is_system_message,
}
}
)
# Benachrichtigung an andere User senden
await self.send_notification_to_others(message)
async def handle_typing(self, data):
"""Typing-Indicator verarbeiten"""
is_typing = data.get('typing', False)
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'typing_indicator',
'user': self.user.username,
'user_id': self.user.id,
'typing': is_typing
}
)
async def handle_read_messages(self):
"""Nachrichten als gelesen markieren"""
await self.mark_messages_as_read()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'messages_read',
'user_id': self.user.id
}
)
async def handle_file_upload(self, data):
"""Datei-Upload verarbeiten"""
file_url = data.get('file_url')
file_type = data.get('file_type', 'file')
if file_type == 'image':
message_type = 'image'
else:
message_type = 'file'
message = await self.save_message(
f"Datei hochgeladen: {data.get('filename', 'Unbekannt')}",
message_type,
file_url
)
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': {
'id': str(message.id),
'sender': message.sender.username,
'sender_id': message.sender.id,
'content': message.content,
'message_type': message.message_type,
'file_url': file_url,
'filename': data.get('filename'),
'created_at': message.created_at.isoformat(),
'is_system': False,
}
}
)
async def chat_message(self, event):
"""Chat-Nachricht an Client senden"""
await self.send(text_data=json.dumps({
'type': 'message',
'message': event['message']
}))
async def typing_indicator(self, event):
"""Typing-Indicator an Client senden"""
await self.send(text_data=json.dumps({
'type': 'typing',
'user': event['user'],
'user_id': event['user_id'],
'typing': event['typing']
}))
async def messages_read(self, event):
"""Nachrichten-als-gelesen-Status senden"""
await self.send(text_data=json.dumps({
'type': 'read',
'user_id': event['user_id']
}))
async def system_message(self, event):
"""System-Nachricht an Client senden"""
await self.send(text_data=json.dumps({
'type': 'system',
'message': event['message']
}))
async def error_message(self, event):
"""Fehler-Nachricht an Client senden"""
await self.send(text_data=json.dumps({
'type': 'error',
'message': event['message']
}))
# Database Operations
@database_sync_to_async
def can_access_room(self):
"""Prüfen ob User Zugriff auf Chat-Raum hat"""
try:
room = ChatRoom.objects.get(id=self.room_id)
return self.user == room.customer or self.user == room.admin or self.user.is_staff
except ChatRoom.DoesNotExist:
return False
@database_sync_to_async
def save_message(self, content, message_type, file_url=None):
"""Nachricht in Datenbank speichern"""
room = ChatRoom.objects.get(id=self.room_id)
message = ChatMessage.objects.create(
room=room,
sender=self.user,
content=content,
message_type=message_type,
is_system_message=False
)
return message
@database_sync_to_async
def update_online_status(self, is_online):
"""Online-Status aktualisieren"""
status, created = UserOnlineStatus.objects.get_or_create(user=self.user)
status.is_online = is_online
status.current_room = ChatRoom.objects.get(id=self.room_id) if is_online else None
status.save()
@database_sync_to_async
def mark_messages_as_read(self):
"""Nachrichten als gelesen markieren"""
room = ChatRoom.objects.get(id=self.room_id)
room.mark_messages_as_read(self.user)
@database_sync_to_async
def send_system_message(self, content):
"""System-Nachricht senden"""
room = ChatRoom.objects.get(id=self.room_id)
message = ChatMessage.objects.create(
room=room,
sender=self.user,
content=content,
message_type='system',
is_system_message=True
)
return message
@database_sync_to_async
def send_notification_to_others(self, message):
"""Benachrichtigung an andere User senden"""
room = message.room
other_users = room.get_other_users(message.sender)
for user in other_users:
ChatNotification.objects.create(
user=user,
room=room,
notification_type='new_message',
title=f'Neue Nachricht von {message.sender.username}',
message=f'{message.content[:100]}...' if len(message.content) > 100 else message.content
)
class NotificationConsumer(AsyncWebsocketConsumer):
"""WebSocket Consumer für Benachrichtigungen"""
async def connect(self):
"""WebSocket-Verbindung herstellen"""
self.user = self.scope['user']
if self.user.is_authenticated:
self.notification_group_name = f'notifications_{self.user.id}'
# Zur Benachrichtigungs-Gruppe beitreten
await self.channel_layer.group_add(
self.notification_group_name,
self.channel_name
)
await self.accept()
else:
await self.close()
async def disconnect(self, close_code):
"""WebSocket-Verbindung trennen"""
if hasattr(self, 'notification_group_name'):
await self.channel_layer.group_discard(
self.notification_group_name,
self.channel_name
)
async def receive(self, text_data):
"""Nachricht vom Client empfangen"""
try:
data = json.loads(text_data)
action = data.get('action')
if action == 'mark_read':
await self.mark_notification_read(data.get('notification_id'))
except json.JSONDecodeError:
await self.send_error_message("Ungültiges JSON-Format")
async def notification_message(self, event):
"""Benachrichtigung an Client senden"""
await self.send(text_data=json.dumps({
'type': 'notification',
'notification': event['notification']
}))
@database_sync_to_async
def mark_notification_read(self, notification_id):
"""Benachrichtigung als gelesen markieren"""
try:
notification = ChatNotification.objects.get(
id=notification_id,
user=self.user
)
notification.read_at = timezone.now()
notification.save()
except ChatNotification.DoesNotExist:
pass

20
chat/forms.py Normal file
View File

@ -0,0 +1,20 @@
from django import forms
from .models import ChatMessage
class QuickResponseForm(forms.ModelForm):
"""Form für schnelle Antworten im Chat"""
class Meta:
model = ChatMessage
fields = ['content']
widgets = {
'content': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Ihre Nachricht...'
})
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['content'].label = 'Nachricht'

207
chat/models.py Normal file
View File

@ -0,0 +1,207 @@
"""
Chat Models für Live-Chat System
"""
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.conf import settings
import uuid
class ChatRoom(models.Model):
"""Chat-Raum für Kunde-Admin Kommunikation"""
ROOM_STATUS_CHOICES = [
('active', 'Aktiv'),
('waiting', 'Wartend'),
('closed', 'Geschlossen'),
('archived', 'Archiviert'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
customer = models.ForeignKey(User, on_delete=models.CASCADE, related_name='customer_chats')
admin = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='admin_chats')
status = models.CharField(max_length=20, choices=ROOM_STATUS_CHOICES, default='waiting')
subject = models.CharField(max_length=200, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
last_message_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-last_message_at', '-created_at']
verbose_name = 'Chat-Raum'
verbose_name_plural = 'Chat-Räume'
def __str__(self):
return f"Chat {self.id} - {self.customer.username}"
def get_unread_count(self, user):
"""Anzahl ungelesener Nachrichten für User"""
return self.messages.filter(
sender__in=self.get_other_users(user),
read_at__isnull=True
).count()
def get_other_users(self, user):
"""Andere User im Chat"""
if user == self.customer:
return [self.admin] if self.admin else []
elif user == self.admin:
return [self.customer]
return []
def mark_messages_as_read(self, user):
"""Nachrichten als gelesen markieren"""
other_users = self.get_other_users(user)
self.messages.filter(
sender__in=other_users,
read_at__isnull=True
).update(read_at=timezone.now())
class ChatMessage(models.Model):
"""Chat-Nachricht"""
MESSAGE_TYPE_CHOICES = [
('text', 'Text'),
('image', 'Bild'),
('file', 'Datei'),
('system', 'System'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE, related_name='messages')
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages')
message_type = models.CharField(max_length=20, choices=MESSAGE_TYPE_CHOICES, default='text')
content = models.TextField()
file = models.FileField(upload_to='chat_files/', null=True, blank=True)
image = models.ImageField(upload_to='chat_images/', null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
read_at = models.DateTimeField(null=True, blank=True)
is_system_message = models.BooleanField(default=False)
class Meta:
ordering = ['created_at']
verbose_name = 'Chat-Nachricht'
verbose_name_plural = 'Chat-Nachrichten'
def __str__(self):
return f"{self.sender.username}: {self.content[:50]}"
def save(self, *args, **kwargs):
"""Nachricht speichern und Room aktualisieren"""
is_new = self.pk is None
super().save(*args, **kwargs)
if is_new:
# Room last_message_at aktualisieren
self.room.last_message_at = self.created_at
self.room.save(update_fields=['last_message_at'])
class ChatNotification(models.Model):
"""Chat-Benachrichtigungen"""
NOTIFICATION_TYPE_CHOICES = [
('new_message', 'Neue Nachricht'),
('room_assigned', 'Raum zugewiesen'),
('room_closed', 'Raum geschlossen'),
('customer_online', 'Kunde online'),
('customer_offline', 'Kunde offline'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='chat_notifications')
room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE, related_name='notifications')
notification_type = models.CharField(max_length=20, choices=NOTIFICATION_TYPE_CHOICES)
title = models.CharField(max_length=200)
message = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
read_at = models.DateTimeField(null=True, blank=True)
sent_via_email = models.BooleanField(default=False)
sent_via_push = models.BooleanField(default=False)
class Meta:
ordering = ['-created_at']
verbose_name = 'Chat-Benachrichtigung'
verbose_name_plural = 'Chat-Benachrichtigungen'
def __str__(self):
return f"{self.notification_type}: {self.title}"
class UserOnlineStatus(models.Model):
"""Online-Status von Benutzern"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='online_status')
is_online = models.BooleanField(default=False)
last_seen = models.DateTimeField(auto_now=True)
current_room = models.ForeignKey(ChatRoom, on_delete=models.SET_NULL, null=True, blank=True)
class Meta:
verbose_name = 'Online-Status'
verbose_name_plural = 'Online-Status'
def __str__(self):
status = "Online" if self.is_online else "Offline"
return f"{self.user.username} - {status}"
class QuickResponse(models.Model):
"""Schnelle Antworten für Admins"""
category = models.CharField(max_length=50, choices=[
('greeting', 'Begrüßung'),
('pricing', 'Preise'),
('shipping', 'Versand'),
('custom_order', 'Custom Order'),
('technical', 'Technische Fragen'),
('general', 'Allgemein'),
])
title = models.CharField(max_length=100)
content = models.TextField()
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quick_responses')
created_at = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
use_count = models.IntegerField(default=0)
class Meta:
ordering = ['category', 'title']
verbose_name = 'Schnelle Antwort'
verbose_name_plural = 'Schnelle Antworten'
def __str__(self):
return f"{self.category}: {self.title}"
def increment_use_count(self):
"""Verwendungszähler erhöhen"""
self.use_count += 1
self.save(update_fields=['use_count'])
class ChatAnalytics(models.Model):
"""Chat-Analytics für Performance-Tracking"""
date = models.DateField()
total_messages = models.IntegerField(default=0)
total_rooms = models.IntegerField(default=0)
active_rooms = models.IntegerField(default=0)
avg_response_time = models.FloatField(default=0) # in Sekunden
customer_satisfaction = models.FloatField(default=0) # 0-5 Sterne
total_duration = models.FloatField(default=0) # in Minuten
class Meta:
unique_together = ['date']
verbose_name = 'Chat-Analytics'
verbose_name_plural = 'Chat-Analytics'
def __str__(self):
return f"Chat Analytics - {self.date}"
@classmethod
def get_or_create_today(cls):
"""Heutige Analytics erstellen oder abrufen"""
today = timezone.now().date()
analytics, created = cls.objects.get_or_create(date=today)
return analytics

14
chat/routing.py Normal file
View File

@ -0,0 +1,14 @@
"""
WebSocket Routing für Chat-System
"""
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
# Chat-Raum WebSocket
re_path(r'ws/chat/(?P<room_id>[^/]+)/$', consumers.ChatConsumer.as_asgi()),
# Benachrichtigungen WebSocket
re_path(r'ws/notifications/$', consumers.NotificationConsumer.as_asgi()),
]

31
chat/urls.py Normal file
View File

@ -0,0 +1,31 @@
"""
Chat URL Configuration
"""
from django.urls import path
from . import views
app_name = 'chat'
urlpatterns = [
# Admin Dashboard
path('dashboard/', views.chat_dashboard, name='dashboard'),
path('rooms/', views.chat_room_list, name='room_list'),
path('rooms/<uuid:room_id>/', views.chat_room_detail, name='room_detail'),
path('rooms/<uuid:room_id>/assign/', views.chat_room_assign, name='room_assign'),
path('rooms/<uuid:room_id>/close/', views.chat_room_close, name='room_close'),
# Quick Responses
path('quick-responses/', views.quick_response_list, name='quick_response_list'),
path('quick-responses/<int:response_id>/edit/', views.quick_response_edit, name='quick_response_edit'),
path('quick-responses/<int:response_id>/delete/', views.quick_response_delete, name='quick_response_delete'),
# Analytics
path('analytics/', views.chat_analytics, name='analytics'),
# API Endpoints
path('api/rooms/<uuid:room_id>/messages/', views.chat_messages_api, name='messages_api'),
path('api/rooms/<uuid:room_id>/send/', views.chat_send_message_api, name='send_message_api'),
path('api/rooms/<uuid:room_id>/read/', views.chat_mark_read_api, name='mark_read_api'),
path('api/quick-responses/<int:response_id>/use/', views.quick_response_use_api, name='quick_response_use_api'),
]

397
chat/views.py Normal file
View File

@ -0,0 +1,397 @@
"""
Chat Views für Admin-Interface und Chat-Management
"""
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib import messages
from django.http import JsonResponse
from django.utils import timezone
from django.db.models import Q, Count
from django.core.paginator import Paginator
from .models import ChatRoom, ChatMessage, UserOnlineStatus, QuickResponse, ChatAnalytics
from .forms import QuickResponseForm
import json
def is_admin(user):
"""Prüfen ob User Admin ist"""
return user.is_staff or user.is_superuser
@login_required
def chat_dashboard(request):
"""Admin Chat Dashboard"""
if not is_admin(request.user):
messages.error(request, 'Zugriff verweigert.')
return redirect('home')
# Chat-Statistiken
total_rooms = ChatRoom.objects.count()
active_rooms = ChatRoom.objects.filter(status='active').count()
waiting_rooms = ChatRoom.objects.filter(status='waiting').count()
today_messages = ChatMessage.objects.filter(
created_at__date=timezone.now().date()
).count()
# Online Admins
online_admins = UserOnlineStatus.objects.filter(
is_online=True,
user__is_staff=True
).count()
# Letzte Chat-Räume
recent_rooms = ChatRoom.objects.select_related('customer', 'admin').order_by('-last_message_at')[:10]
# Quick Responses
quick_responses = QuickResponse.objects.filter(is_active=True).order_by('category', 'title')
context = {
'total_rooms': total_rooms,
'active_rooms': active_rooms,
'waiting_rooms': waiting_rooms,
'today_messages': today_messages,
'online_admins': online_admins,
'recent_rooms': recent_rooms,
'quick_responses': quick_responses,
}
return render(request, 'chat/dashboard.html', context)
@login_required
def chat_room_list(request):
"""Liste aller Chat-Räume"""
if not is_admin(request.user):
messages.error(request, 'Zugriff verweigert.')
return redirect('home')
# Filter
status_filter = request.GET.get('status', '')
search_query = request.GET.get('search', '')
rooms = ChatRoom.objects.select_related('customer', 'admin').order_by('-last_message_at')
if status_filter:
rooms = rooms.filter(status=status_filter)
if search_query:
rooms = rooms.filter(
Q(customer__username__icontains=search_query) |
Q(subject__icontains=search_query)
)
# Pagination
paginator = Paginator(rooms, 20)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
'status_filter': status_filter,
'search_query': search_query,
'status_choices': ChatRoom.ROOM_STATUS_CHOICES,
}
return render(request, 'chat/room_list.html', context)
@login_required
def chat_room_detail(request, room_id):
"""Chat-Raum Detail-Ansicht"""
if not is_admin(request.user):
messages.error(request, 'Zugriff verweigert.')
return redirect('home')
room = get_object_or_404(ChatRoom, id=room_id)
# Admin dem Raum zuweisen wenn noch nicht zugewiesen
if not room.admin and room.status == 'waiting':
room.admin = request.user
room.status = 'active'
room.save()
# System-Nachricht
ChatMessage.objects.create(
room=room,
sender=request.user,
content=f"{request.user.username} ist dem Chat beigetreten",
message_type='system',
is_system_message=True
)
# Nachrichten laden
messages = room.messages.select_related('sender').order_by('created_at')
# Quick Responses
quick_responses = QuickResponse.objects.filter(is_active=True).order_by('category', 'title')
context = {
'room': room,
'messages': messages,
'quick_responses': quick_responses,
}
return render(request, 'chat/room_detail.html', context)
@login_required
def chat_room_assign(request, room_id):
"""Chat-Raum einem Admin zuweisen"""
if not is_admin(request.user):
return JsonResponse({'error': 'Zugriff verweigert'}, status=403)
room = get_object_or_404(ChatRoom, id=room_id)
if request.method == 'POST':
admin_id = request.POST.get('admin_id')
if admin_id:
try:
admin = User.objects.get(id=admin_id, is_staff=True)
room.admin = admin
room.status = 'active'
room.save()
# System-Nachricht
ChatMessage.objects.create(
room=room,
sender=request.user,
content=f"Chat wurde {admin.username} zugewiesen",
message_type='system',
is_system_message=True
)
return JsonResponse({'success': True})
except User.DoesNotExist:
return JsonResponse({'error': 'Admin nicht gefunden'}, status=400)
return JsonResponse({'error': 'Ungültige Anfrage'}, status=400)
@login_required
def chat_room_close(request, room_id):
"""Chat-Raum schließen"""
if not is_admin(request.user):
return JsonResponse({'error': 'Zugriff verweigert'}, status=403)
room = get_object_or_404(ChatRoom, id=room_id)
if request.method == 'POST':
room.status = 'closed'
room.save()
# System-Nachricht
ChatMessage.objects.create(
room=room,
sender=request.user,
content="Chat wurde geschlossen",
message_type='system',
is_system_message=True
)
return JsonResponse({'success': True})
return JsonResponse({'error': 'Ungültige Anfrage'}, status=400)
@login_required
def quick_response_list(request):
"""Quick Response Verwaltung"""
if not is_admin(request.user):
messages.error(request, 'Zugriff verweigert.')
return redirect('home')
responses = QuickResponse.objects.all().order_by('category', 'title')
if request.method == 'POST':
form = QuickResponseForm(request.POST)
if form.is_valid():
response = form.save(commit=False)
response.created_by = request.user
response.save()
messages.success(request, 'Quick Response erstellt.')
return redirect('chat:quick_response_list')
else:
form = QuickResponseForm()
context = {
'responses': responses,
'form': form,
}
return render(request, 'chat/quick_response_list.html', context)
@login_required
def quick_response_edit(request, response_id):
"""Quick Response bearbeiten"""
if not is_admin(request.user):
messages.error(request, 'Zugriff verweigert.')
return redirect('home')
response = get_object_or_404(QuickResponse, id=response_id)
if request.method == 'POST':
form = QuickResponseForm(request.POST, instance=response)
if form.is_valid():
form.save()
messages.success(request, 'Quick Response aktualisiert.')
return redirect('chat:quick_response_list')
else:
form = QuickResponseForm(instance=response)
context = {
'response': response,
'form': form,
}
return render(request, 'chat/quick_response_edit.html', context)
@login_required
def quick_response_delete(request, response_id):
"""Quick Response löschen"""
if not is_admin(request.user):
return JsonResponse({'error': 'Zugriff verweigert'}, status=403)
response = get_object_or_404(QuickResponse, id=response_id)
if request.method == 'POST':
response.delete()
return JsonResponse({'success': True})
return JsonResponse({'error': 'Ungültige Anfrage'}, status=400)
@login_required
def chat_analytics(request):
"""Chat Analytics Dashboard"""
if not is_admin(request.user):
messages.error(request, 'Zugriff verweigert.')
return redirect('home')
# Analytics für heute
today_analytics = ChatAnalytics.get_or_create_today()
# Wöchentliche Statistiken
from datetime import timedelta
week_ago = timezone.now().date() - timedelta(days=7)
weekly_stats = {
'total_messages': ChatMessage.objects.filter(
created_at__date__gte=week_ago
).count(),
'total_rooms': ChatRoom.objects.filter(
created_at__date__gte=week_ago
).count(),
'avg_response_time': 120, # Placeholder - würde aus echten Daten berechnet
'customer_satisfaction': 4.2, # Placeholder
}
# Top Quick Responses
top_responses = QuickResponse.objects.filter(
use_count__gt=0
).order_by('-use_count')[:5]
# Online Status
online_users = UserOnlineStatus.objects.filter(is_online=True)
context = {
'today_analytics': today_analytics,
'weekly_stats': weekly_stats,
'top_responses': top_responses,
'online_users': online_users,
}
return render(request, 'chat/analytics.html', context)
# API Views für AJAX
@login_required
def chat_messages_api(request, room_id):
"""API für Chat-Nachrichten"""
if not is_admin(request.user):
return JsonResponse({'error': 'Zugriff verweigert'}, status=403)
room = get_object_or_404(ChatRoom, id=room_id)
messages = room.messages.select_related('sender').order_by('created_at')
message_list = []
for msg in messages:
message_list.append({
'id': str(msg.id),
'sender': msg.sender.username,
'sender_id': msg.sender.id,
'content': msg.content,
'message_type': msg.message_type,
'file_url': msg.file.url if msg.file else None,
'image_url': msg.image.url if msg.image else None,
'created_at': msg.created_at.isoformat(),
'is_system': msg.is_system_message,
'read_at': msg.read_at.isoformat() if msg.read_at else None,
})
return JsonResponse({'messages': message_list})
@login_required
def chat_send_message_api(request, room_id):
"""API für Nachrichten senden"""
if not is_admin(request.user):
return JsonResponse({'error': 'Zugriff verweigert'}, status=403)
room = get_object_or_404(ChatRoom, id=room_id)
if request.method == 'POST':
try:
data = json.loads(request.body)
content = data.get('content', '').strip()
message_type = data.get('message_type', 'text')
if not content and message_type == 'text':
return JsonResponse({'error': 'Nachricht darf nicht leer sein'}, status=400)
message = ChatMessage.objects.create(
room=room,
sender=request.user,
content=content,
message_type=message_type,
)
return JsonResponse({
'success': True,
'message_id': str(message.id)
})
except json.JSONDecodeError:
return JsonResponse({'error': 'Ungültiges JSON'}, status=400)
return JsonResponse({'error': 'Ungültige Anfrage'}, status=400)
@login_required
def chat_mark_read_api(request, room_id):
"""API für Nachrichten als gelesen markieren"""
if not is_admin(request.user):
return JsonResponse({'error': 'Zugriff verweigert'}, status=403)
room = get_object_or_404(ChatRoom, id=room_id)
room.mark_messages_as_read(request.user)
return JsonResponse({'success': True})
@login_required
def quick_response_use_api(request, response_id):
"""API für Quick Response verwenden"""
if not is_admin(request.user):
return JsonResponse({'error': 'Zugriff verweigert'}, status=403)
response = get_object_or_404(QuickResponse, id=response_id)
response.increment_use_count()
return JsonResponse({
'success': True,
'content': response.content
})

21
django_shop/settings.py Normal file
View File

@ -0,0 +1,21 @@
import os
from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Lade .env Datei
load_dotenv(BASE_DIR / '.env')
# E-Mail-Einstellungen (temporär Console-Backend)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEFAULT_FROM_EMAIL = 'Fursuit Shop <noreply@fursuitshop.com>'
# Admin-E-Mail-Empfänger
ADMINS = [
('Shop Admin', 'admin@fursuitshop.com'),
]
# Lagerbestand-Einstellungen
LOW_STOCK_THRESHOLD = 5 # Schwellenwert für niedrigen Lagerbestand

60
docker-compose.yml Normal file
View File

@ -0,0 +1,60 @@
version: '3.8'
services:
# Django Web Application
web:
build: .
ports:
- "8010:8000"
volumes:
- static_volume:/app/staticfiles
- media_volume:/app/media
environment:
- DEBUG=True
- DJANGO_SETTINGS_MODULE=webshop.settings
depends_on:
- db
- redis
command: >
sh -c "python manage.py migrate &&
python manage.py collectstatic --noinput &&
python manage.py runserver 0.0.0.0:8000"
# PostgreSQL Database
db:
image: postgres:15
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=fursuit_shop
- POSTGRES_USER=fursuit_user
- POSTGRES_PASSWORD=fursuit_password
ports:
- "5433:5432"
# Redis für Channels/Caching
redis:
image: redis:7-alpine
ports:
- "6380:6379"
volumes:
- redis_data:/data
# Elasticsearch für Search
elasticsearch:
image: elasticsearch:8.11.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data
volumes:
postgres_data:
redis_data:
elasticsearch_data:
static_volume:
media_volume:

496
docs/api_documentation.md Normal file
View File

@ -0,0 +1,496 @@
# Kasico API Dokumentation
## Übersicht
Die Kasico API ist eine RESTful API für den Fursuit Shop. Sie bietet Zugriff auf alle wichtigen Funktionen des Shops über HTTP-Endpoints.
**Base URL:** `https://kasico.de/api/v1/`
## Authentication
Die API unterstützt zwei Authentifizierungsmethoden:
1. **Session Authentication** (für Browser-basierte Clients)
2. **Token Authentication** (für mobile Apps und externe Clients)
### Token Authentication
```bash
# Token erhalten
curl -X POST https://kasico.de/api/v1/auth/token/
-H "Content-Type: application/json"
-d '{"username": "user", "password": "password"}'
# Token verwenden
curl -H "Authorization: Token YOUR_TOKEN" https://kasico.de/api/v1/products/
```
## Endpoints
### Produkte
#### Alle Produkte abrufen
```http
GET /api/v1/products/
```
**Parameter:**
- `category` - Kategorie-Slug
- `fursuit_type` - Fursuit-Typ
- `style` - Stil
- `min_price` - Mindestpreis
- `max_price` - Maximalpreis
- `on_sale` - Nur Angebote (true/false)
- `is_featured` - Nur Featured (true/false)
- `search` - Suchbegriff
- `ordering` - Sortierung (name, -name, price, -price, created, -created)
- `page` - Seitennummer
- `page_size` - Items pro Seite
**Beispiel:**
```bash
curl "https://kasico.de/api/v1/products/?category=fullsuits&min_price=500&ordering=price"
```
#### Einzelnes Produkt abrufen
```http
GET /api/v1/products/{id}/
```
#### Featured Produkte
```http
GET /api/v1/products/featured/
```
#### Produkte im Angebot
```http
GET /api/v1/products/on-sale/
```
#### Produkte mit niedrigem Lagerbestand
```http
GET /api/v1/products/low-stock/
```
#### Produkt-Statistiken
```http
GET /api/v1/products/stats/
```
**Response:**
```json
{
"total_products": 150,
"featured_products": 12,
"on_sale_products": 8,
"low_stock_products": 3,
"categories_count": 5,
"average_rating": 4.2,
"total_reviews": 89
}
```
#### Produkt zur Wunschliste hinzufügen
```http
POST /api/v1/products/{id}/add-to-wishlist/
```
#### Produkt von Wunschliste entfernen
```http
POST /api/v1/products/{id}/remove-from-wishlist/
```
### Kategorien
#### Alle Kategorien abrufen
```http
GET /api/v1/categories/
```
#### Einzelne Kategorie abrufen
```http
GET /api/v1/categories/{id}/
```
#### Produkte einer Kategorie
```http
GET /api/v1/categories/{id}/products/
```
### Reviews
#### Reviews für ein Produkt abrufen
```http
GET /api/v1/reviews/?product={product_id}
```
#### Review erstellen
```http
POST /api/v1/reviews/
```
**Request Body:**
```json
{
"product": 1,
"rating": 5,
"comment": "Tolles Produkt!"
}
```
#### Meine Reviews abrufen
```http
GET /api/v1/reviews/my-reviews/
```
### Wunschliste
#### Wunschliste abrufen
```http
GET /api/v1/wishlist/
```
#### Produkt zur Wunschliste hinzufügen
```http
POST /api/v1/wishlist/add-product/
```
**Request Body:**
```json
{
"product_id": 1
}
```
#### Produkt von Wunschliste entfernen
```http
POST /api/v1/wishlist/remove-product/
```
**Request Body:**
```json
{
"product_id": 1
}
```
### Custom Orders
#### Custom Orders abrufen
```http
GET /api/v1/custom-orders/
```
#### Custom Order erstellen
```http
POST /api/v1/custom-orders/
```
**Request Body:**
```json
{
"title": "Mein Custom Fursuit",
"description": "Detaillierte Beschreibung...",
"fursuit_type": "fullsuit",
"style": "realistic",
"size": "medium",
"color_preferences": "Blau und Weiß",
"special_requirements": "Besondere Anforderungen...",
"budget": 1500.00
}
```
#### Custom Order Status aktualisieren
```http
POST /api/v1/custom-orders/{id}/update-status/
```
**Request Body:**
```json
{
"status": "in_progress"
}
```
### Galerie
#### Galerie-Bilder abrufen
```http
GET /api/v1/gallery/
```
**Parameter:**
- `is_featured` - Nur Featured (true/false)
- `fursuit_type` - Fursuit-Typ
- `style` - Stil
#### Featured Galerie-Bilder
```http
GET /api/v1/gallery/featured/
```
#### Galerie-Bilder nach Produkt
```http
GET /api/v1/gallery/by-product/?product_id={id}
```
### Warenkorb
#### Warenkorb abrufen
```http
GET /api/v1/cart/get/
```
#### Item zum Warenkorb hinzufügen
```http
POST /api/v1/cart/add-item/
```
**Request Body:**
```json
{
"product_id": 1,
"quantity": 2
}
```
#### Warenkorb-Menge aktualisieren
```http
POST /api/v1/cart/update-quantity/
```
**Request Body:**
```json
{
"item_id": 1,
"quantity": 3
}
```
#### Item aus Warenkorb entfernen
```http
POST /api/v1/cart/remove-item/
```
**Request Body:**
```json
{
"item_id": 1
}
```
#### Warenkorb leeren
```http
POST /api/v1/cart/clear/
```
### Suche
#### Produkte suchen
```http
GET /api/v1/search/products/
```
**Parameter:**
- `query` - Suchbegriff
- `category` - Kategorie
- `fursuit_type` - Fursuit-Typ
- `style` - Stil
- `min_price` - Mindestpreis
- `max_price` - Maximalpreis
- `on_sale` - Nur Angebote
- `is_featured` - Nur Featured
- `sort_by` - Sortierung (newest, oldest, price_low, price_high, name, rating)
- `page` - Seitennummer
- `page_size` - Items pro Seite
**Beispiel:**
```bash
curl "https://kasico.de/api/v1/search/products/?query=wolf&min_price=300&sort_by=price_low"
```
## Response Format
Alle API-Responses folgen diesem Format:
### Erfolgreiche Response
```json
{
"count": 150,
"next": "https://kasico.de/api/v1/products/?page=2",
"previous": null,
"results": [...]
}
```
### Fehler-Response
```json
{
"error": "Fehlermeldung",
"detail": "Detaillierte Beschreibung"
}
```
## Rate Limiting
Die API implementiert Rate Limiting:
- **Anonyme Benutzer:** 100 Requests/Stunde
- **Authentifizierte Benutzer:** 1000 Requests/Stunde
## Pagination
Alle Listen-Endpoints unterstützen Pagination:
- `page` - Seitennummer (Standard: 1)
- `page_size` - Items pro Seite (Standard: 12, Maximum: 100)
## Filtering
Die API unterstützt umfangreiche Filter-Optionen:
### Produkte
- `category` - Nach Kategorie
- `fursuit_type` - Nach Fursuit-Typ
- `style` - Nach Stil
- `min_price` / `max_price` - Preisbereich
- `on_sale` - Nur Angebote
- `is_featured` - Nur Featured
- `in_stock` - Nur verfügbare Produkte
### Suche
- `query` - Volltext-Suche in Name und Beschreibung
- Kombination aller Produkt-Filter
## Sorting
Verfügbare Sortierungen:
- `newest` - Neueste zuerst
- `oldest` - Älteste zuerst
- `price_low` - Preis aufsteigend
- `price_high` - Preis absteigend
- `name` - Name alphabetisch
- `rating` - Bewertung absteigend
## JavaScript Integration
Die API kann einfach in JavaScript verwendet werden:
```javascript
// Produkte abrufen
const products = await fetch('/api/v1/products/').then(r => r.json());
// Produkt zur Wunschliste hinzufügen
await fetch('/api/v1/products/1/add-to-wishlist/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
// Mit dem Kasico API Client
await kasicoAPI.addToWishlist(1);
await kasicoAPI.getProducts({ category: 'fullsuits' });
```
## SDKs und Libraries
### Python
```python
import requests
api_url = "https://kasico.de/api/v1/"
headers = {"Authorization": f"Token {token}"}
# Produkte abrufen
response = requests.get(f"{api_url}products/", headers=headers)
products = response.json()
```
### JavaScript/TypeScript
```typescript
interface Product {
id: number;
name: string;
description: string;
base_price: number;
sale_price?: number;
on_sale: boolean;
stock: number;
// ... weitere Felder
}
// Mit dem Kasico API Client
const products = await kasicoAPI.getProducts();
const featured = await kasicoAPI.getFeaturedProducts();
```
## Webhooks
Die API unterstützt Webhooks für wichtige Events:
### Verfügbare Webhooks
- `order.created` - Neue Bestellung
- `order.updated` - Bestellung aktualisiert
- `payment.completed` - Zahlung abgeschlossen
- `stock.low` - Niedriger Lagerbestand
### Webhook Setup
```http
POST /api/v1/webhooks/
```
**Request Body:**
```json
{
"url": "https://your-domain.com/webhook",
"events": ["order.created", "payment.completed"]
}
```
## Fehlerbehandlung
### HTTP Status Codes
- `200` - Erfolg
- `201` - Erstellt
- `400` - Bad Request
- `401` - Unauthorized
- `403` - Forbidden
- `404` - Not Found
- `429` - Too Many Requests
- `500` - Internal Server Error
### Fehler-Response Format
```json
{
"error": "Validation failed",
"detail": {
"field_name": ["Error message"]
}
}
```
## Versionierung
Die API verwendet URL-basierte Versionierung:
- Aktuelle Version: `v1`
- URL-Pattern: `/api/v1/`
## Support
Bei Fragen zur API:
- **E-Mail:** api@kasico.de
- **Dokumentation:** https://kasico.de/api/docs/
- **GitHub:** https://github.com/kasico/api-examples
## Changelog
### v1.0.0 (2024-01-15)
- Initiale API-Version
- Produkt-, Kategorie- und Review-Endpoints
- Warenkorb- und Wunschliste-Funktionen
- Custom Order Management
- Galerie-API
- Such- und Filter-Funktionen

155
docs/email_system.md Normal file
View File

@ -0,0 +1,155 @@
# E-Mail-System Dokumentation
## Übersicht
Das E-Mail-System des Fursuit Shops versendet automatisch Benachrichtigungen an Kunden und Administratoren bei verschiedenen Shop-Ereignissen. Das System ist mehrsprachig (DE/EN) und verwendet responsive HTML-Templates mit Text-Alternativen.
## Konfiguration
### E-Mail-Einstellungen
Die E-Mail-Konfiguration erfolgt über Umgebungsvariablen in der `.env`-Datei:
```env
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-specific-password
DEFAULT_FROM_EMAIL=Fursuit Shop <noreply@fursuitshop.com>
```
### Admin-Benachrichtigungen
Administratoren werden in `settings.py` konfiguriert:
```python
ADMINS = [
('Shop Admin', 'admin@fursuitshop.com'),
]
```
### Lagerbestand-Schwellenwert
```python
LOW_STOCK_THRESHOLD = 5 # Benachrichtigung bei ≤ 5 Artikeln
```
## E-Mail-Typen
### 1. Kundenbenachrichtigungen
#### Bestellbestätigung
- Gesendet nach erfolgreicher Zahlung
- Enthält Bestelldetails, Produkte und Preise
- Template: `order_confirmation.html/txt`
#### Status-Updates
- Gesendet bei Statusänderungen der Bestellung
- Kann Fortschrittsbilder und Beschreibungen enthalten
- Template: `order_status_update.html/txt`
#### Versandbestätigung
- Gesendet wenn Bestellung versendet wurde
- Enthält Tracking-Nummer und Versanddetails
- Template: `shipping_confirmation.html/txt`
### 2. Admin-Benachrichtigungen
#### Neue Bestellung
- Bei jeder neuen Bestellung
- Spezielle Markierung für Fursuit-Bestellungen
- Template: `admin_notification.html/txt`
#### Zahlungsfehler
- Bei fehlgeschlagenen Zahlungen
- Enthält detaillierte Fehlerinformationen
- Template: `admin_notification.html/txt`
#### Custom Design
- Bei Bestellungen mit Custom Designs
- Enthält Design-Dateien und Notizen
- Template: `admin_notification.html/txt`
#### Niedriger Lagerbestand
- Bei Unterschreitung des Schwellenwerts
- Enthält Produktdetails und aktuellen Bestand
- Template: `low_stock_notification.html/txt`
## Signal-Handler
Das System verwendet Django-Signals für automatische Benachrichtigungen:
### Order-Signals
```python
@receiver(post_save, sender=Order)
def handle_order_notifications(sender, instance, created, **kwargs):
# Sendet Benachrichtigungen bei neuen Bestellungen und Statusänderungen
```
### Payment-Signals
```python
@receiver(post_save, sender=PaymentError)
def handle_payment_error(sender, instance, created, **kwargs):
# Benachrichtigt Admins über Zahlungsfehler
```
### Product-Signals
```python
@receiver(pre_save, sender=Product)
def check_stock_level(sender, instance, **kwargs):
# Überprüft Lagerbestand und sendet Warnungen
```
## E-Mail-Templates
Alle E-Mail-Templates:
- Sind vollständig responsive
- Unterstützen HTML und Text-Alternativen
- Sind mehrsprachig (DE/EN)
- Verwenden einheitliches Branding
### Template-Struktur
```
shop/templates/shop/emails/
├── order_confirmation.html
├── order_confirmation.txt
├── order_status_update.html
├── order_status_update.txt
├── shipping_confirmation.html
├── shipping_confirmation.txt
├── admin_notification.html
├── admin_notification.txt
├── low_stock_notification.html
└── low_stock_notification.txt
```
## Sicherheit
- TLS-Verschlüsselung für E-Mail-Versand
- Keine sensiblen Daten in E-Mails
- Sichere Links zu Admin-Bereich
- App-spezifische Passwörter für SMTP
## Fehlerbehandlung
- Robuste Exception-Handling
- Logging von Versandfehlern
- Vermeidung von Doppel-Benachrichtigungen
- Fallback auf Text-Version bei HTML-Problemen
## Wartung
### Neue E-Mail-Typen hinzufügen
1. Templates erstellen (HTML und Text)
2. E-Mail-Funktion in `emails.py` hinzufügen
3. Signal-Handler in `signals.py` registrieren
4. Übersetzungen in `.po`-Dateien hinzufügen
### Template-Anpassung
- CSS-Styles in Template-Header
- Einheitliche Farbcodes und Abstände
- Bootstrap-kompatible Klassen
- Responsive Breakpoints

161
furry_design_tracker.md Normal file
View File

@ -0,0 +1,161 @@
# 🐾 Furry-Design Umsetzungsplan & Tracker
## ✅ Fertiggestellte Seiten & Features
### 🏠 **Startseite (Home)**
- [x] **Hero-Section:** Mit Call-to-Action und Furry-Design
- [x] **Service-Cards:** Ansprechende Darstellung der Services
- [x] **Galerie-Preview:** Vorschau der besten Fursuits
- [x] **Call-to-Action:** Newsletter-Anmeldung und Shop-Link
### 🛍️ **Shop (Produktliste)**
- [x] **Produktkarten:** Mit Hover-Effekten und Animationen
- [x] **Filter & Sortierung:** Erweiterte Suchfunktionen
- [x] **Responsive Design:** Mobile-optimiert
- [x] **Lazy Loading:** Für bessere Performance
### 📦 **Produktdetail**
- [x] **Große Produktbilder:** Mit Zoom-Funktion
- [x] **Detaillierte Beschreibungen:** Mit Furry-Design
- [x] **Bewertungssystem:** Sterne und Kommentare
- [x] **Social Sharing:** Erweiterte Sharing-Optionen
- [x] **Ähnliche Produkte:** Empfehlungen
### 🛒 **Warenkorb**
- [x] **Interaktive Karten:** Mit Hover-Effekten
- [x] **Mengenänderung:** Live-Updates
- [x] **Preisberechnung:** Automatische Updates
- [x] **Responsive Design:** Mobile-optimiert
### 💳 **Checkout**
- [x] **Formular-Design:** Furry-Style Inputs
- [x] **Validierung:** Live-Feedback
- [x] **Zusammenfassung:** Übersichtliche Darstellung
- [x] **Sicherheitshinweise:** Trust-Badges
### ✅ **Bestellbestätigung**
- [x] **Confetti-Animation:** Erfolgs-Feedback
- [x] **Bestelldetails:** Übersichtliche Darstellung
- [x] **Nächste Schritte:** Klare Anweisungen
- [x] **Social Sharing:** Teilen der Bestellung
### 👤 **Benutzerprofil**
- [x] **Profilkarte:** Mit Avatar und Stats
- [x] **Bestellhistorie:** Übersichtliche Darstellung
- [x] **Einstellungen:** Benutzerfreundlich
- [x] **Wunschliste:** Favoriten-Verwaltung
### 🔐 **Login/Registrierung**
- [x] **Formular-Design:** Furry-Style
- [x] **Validierung:** Live-Feedback
- [x] **Passwort-Stärke:** Visueller Indikator
- [x] **Social Login:** Optionen erweitert
### 🖼️ **Galerie**
- [x] **Grid-Layout:** Responsive Design
- [x] **Lightbox:** Bildvergrößerung
- [x] **Kategorien:** Filter nach Typ
- [x] **Lazy Loading:** Performance optimiert
### 📞 **Kontakt/FAQ**
- [x] **Kontaktformular:** Furry-Design
- [x] **FAQ-Sektion:** Akkordeon-Style
- [x] **Live-Chat:** Widget integriert
- [x] **Social Media:** Links zu Plattformen
### 🎨 **Custom Order**
- [x] **Prozess-Steps:** Schritt-für-Schritt
- [x] **Formular-Design:** Umfassende Felder
- [x] **Fortschrittsanzeige:** Visueller Tracker
- [x] **Preiskalkulation:** Live-Updates
### 📊 **Dashboard**
- [x] **Übersichtskarten:** Mit Animationen
- [x] **Charts & Grafiken:** Interaktive Visualisierungen
- [x] **Quick Actions:** Schnellzugriff
- [x] **Responsive Design:** Mobile-optimiert
### ❤️ **Wunschliste**
- [x] **Produktkarten:** Mit Hover-Effekten
- [x] **Bulk-Aktionen:** Mehrfachauswahl
- [x] **Sortierung:** Verschiedene Optionen
- [x] **Teilen-Funktion:** Social Sharing
### 🔑 **Passwort-Reset**
- [x] **Request-Formular:** Furry-Design
- [x] **Bestätigungsseite:** Klare Kommunikation
- [x] **Reset-Formular:** Mit Validierung
- [x] **Erfolgsseite:** Confetti-Animation
## 🚀 **Erweiterte Features**
### 📱 **Sticky Navigation**
- [x] **Scroll-Effekte:** Dynamische Anpassung
- [x] **Progress-Bar:** Scroll-Fortschritt
- [x] **Mobile-Menü:** Hamburger-Navigation
- [x] **Scroll-to-Top:** Smooth Scrolling
- [x] **Active States:** Aktive Seite markiert
### 🌐 **Social Sharing**
- [x] **Erweiterte Plattformen:** Twitter, Facebook, WhatsApp, Telegram, Pinterest, LinkedIn
- [x] **Copy-Link:** Ein-Klick-Kopieren
- [x] **Success-Animationen:** Visuelles Feedback
- [x] **Analytics-Tracking:** Share-Events
- [x] **Responsive Design:** Mobile-optimiert
### 📧 **Newsletter-Opt-In**
- [x] **Verbessertes Popup:** Mit Animationen
- [x] **E-Mail-Validierung:** Live-Feedback
- [x] **Success/Error-States:** Visuelle Rückmeldung
- [x] **Benefits-Liste:** Mehrwert kommuniziert
- [x] **Analytics-Tracking:** Signup-Events
- [x] **Responsive Design:** Mobile-optimiert
## 🎨 **Design-System**
### 🎨 **Farbpalette**
- **Primär:** Lila/Pink-Gradient (#b36fff → #ff6fd8)
- **Sekundär:** Helles Lila (#f8e1ff)
- **Akzent:** Pink (#ff6fd8)
- **Text:** Dunkelgrau (#3a2d4d)
### 🎭 **Animationen**
- **Hover-Effekte:** Scale & Translate
- **Fade-In:** Scroll-Animationen
- **Confetti:** Erfolgs-Feedback
- **Loading-Spinner:** Furry-Style
### 📱 **Responsive Design**
- **Desktop:** 1200px+ Layout
- **Tablet:** 768px-1199px
- **Mobile:** <768px optimiert
## 📊 **Performance-Optimierungen**
### ⚡ **Geschwindigkeit**
- [x] **Lazy Loading:** Bilder und Komponenten
- [x] **CSS-Optimierung:** Minimierte Styles
- [x] **JavaScript-Bundling:** Effiziente Skripte
- [x] **Caching:** Browser-Cache genutzt
### 🔍 **SEO**
- [x] **Meta-Tags:** Vollständige Optimierung
- [x] **Structured Data:** Schema.org Markup
- [x] **Sitemap:** Automatische Generierung
- [x] **Alt-Tags:** Barrierefreiheit
## 🎯 **Alle Features Abgeschlossen!**
**Status:** ✅ **100% Fertiggestellt**
Alle geplanten Seiten und Features wurden erfolgreich im einheitlichen Furry-Design umgesetzt. Das Projekt ist vollständig funktionsfähig und bereit für den Live-Betrieb.
### 🏆 **Besondere Highlights:**
- **Einheitliches Design:** Konsistente Furry-Ästhetik
- **Moderne UX:** Smooth Animationen und Feedback
- **Mobile-First:** Responsive auf allen Geräten
- **Performance:** Optimiert für schnelle Ladezeiten
- **Accessibility:** Barrierefreie Nutzung
- **SEO-Optimiert:** Suchmaschinen-freundlich
**Nächste Schritte:** Deployment und Live-Schaltung! 🚀

31
init_data.py Normal file
View File

@ -0,0 +1,31 @@
import sqlite3
def add_test_data():
conn = sqlite3.connect('shop.db')
cursor = conn.cursor()
# Lösche vorhandene Daten
cursor.execute("DELETE FROM products")
# Testprodukte
products = [
("Gaming Maus", "Hochwertige Gaming-Maus mit RGB-Beleuchtung", 49.99, 10),
("Mechanische Tastatur", "Mechanische Gaming-Tastatur mit blauen Switches", 89.99, 5),
("Gaming Headset", "7.1 Surround Sound Gaming Headset", 79.99, 8),
("Mousepad XL", "Extra großes Gaming-Mousepad", 19.99, 15),
("Webcam HD", "1080p Webcam für Streaming", 59.99, 3)
]
# Füge Produkte ein
cursor.executemany(
"INSERT INTO products (name, description, price, stock) VALUES (?, ?, ?, ?)",
products
)
# Speichere Änderungen
conn.commit()
conn.close()
print("Testdaten wurden erfolgreich hinzugefügt!")
if __name__ == "__main__":
add_test_data()

43
init_db.py Normal file
View File

@ -0,0 +1,43 @@
import sqlite3
from contextlib import contextmanager
@contextmanager
def get_db():
conn = sqlite3.connect('shop.db')
try:
yield conn
finally:
conn.close()
def init_db():
with get_db() as conn:
# Tabelle erstellen
conn.execute("""
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
price REAL NOT NULL,
stock INTEGER NOT NULL
)
""")
# Beispieldaten einfügen
products = [
("Laptop", "Leistungsstarker Laptop für Arbeit und Gaming", 999.99, 5),
("Smartphone", "Neuestes Modell mit Top-Kamera", 699.99, 10),
("Kopfhörer", "Kabellose Kopfhörer mit Noise-Cancelling", 199.99, 15),
("Tablet", "Perfekt für Unterhaltung und Produktivität", 449.99, 8),
("Smartwatch", "Fitness-Tracking und Benachrichtigungen", 299.99, 12)
]
conn.execute("DELETE FROM products") # Alte Daten löschen
conn.executemany(
"INSERT INTO products (name, description, price, stock) VALUES (?, ?, ?, ?)",
products
)
conn.commit()
if __name__ == "__main__":
init_db()
print("Datenbank wurde erfolgreich initialisiert!")

130
init_docker.py Normal file
View File

@ -0,0 +1,130 @@
#!/usr/bin/env python
"""
Docker Initialisierungsskript für Kasico Fursuit Shop
"""
import os
import sys
import django
from django.core.management import execute_from_command_line
def init_docker():
"""Initialisiere Django für Docker"""
# Setze Django Settings
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'webshop.settings')
# Setup Django
django.setup()
print("🐳 Initialisiere Kasico Fursuit Shop für Docker...")
try:
# Führe Migrationen aus
print("📦 Führe Datenbank-Migrationen aus...")
execute_from_command_line(['manage.py', 'migrate'])
# Erstelle Superuser falls nicht vorhanden
from django.contrib.auth.models import User
if not User.objects.filter(username='admin').exists():
print("👤 Erstelle Admin-User...")
User.objects.create_superuser('admin', 'admin@kasico.de', 'admin123')
print("✅ Admin-User erstellt: admin / admin123")
# Erstelle Test-Daten
print("🎨 Erstelle Test-Daten...")
create_test_data()
# Erstelle Elasticsearch Index
print("🔍 Erstelle Elasticsearch Index...")
execute_from_command_line(['manage.py', 'rebuild_index', '--noinput'])
print("✅ Docker Initialisierung abgeschlossen!")
print("🌐 Server läuft auf: http://localhost:8000")
print("👤 Admin Panel: http://localhost:8000/admin")
print("📧 Login: admin / admin123")
except Exception as e:
print(f"❌ Fehler bei der Initialisierung: {e}")
sys.exit(1)
def create_test_data():
"""Erstelle Test-Daten für den Shop"""
from products.models import Product, Category
from django.core.files.base import ContentFile
# Erstelle Kategorien
categories = [
{'name': 'Fursuits', 'description': 'Vollständige Fursuits'},
{'name': 'Köpfe', 'description': 'Fursuit Köpfe'},
{'name': 'Pfoten', 'description': 'Fursuit Pfoten'},
{'name': 'Schwänze', 'description': 'Fursuit Schwänze'},
]
for cat_data in categories:
Category.objects.get_or_create(
name=cat_data['name'],
defaults={'description': cat_data['description']}
)
# Erstelle Test-Produkte
products_data = [
{
'name': 'Wolf Fursuit - Luna',
'description': 'Vollständiger Wolf Fursuit in silber-grau',
'price': 2500.00,
'category': 'Fursuits',
'fursuit_type': 'fullsuit',
'stock': 1,
'featured': True
},
{
'name': 'Fuchs Kopf - Blaze',
'description': 'Fuchs Kopf in orange-rot mit LED Augen',
'price': 800.00,
'category': 'Köpfe',
'fursuit_type': 'head',
'stock': 3,
'featured': True
},
{
'name': 'Hund Pfoten - Buddy',
'description': 'Hund Pfoten in braun mit Krallen',
'price': 300.00,
'category': 'Pfoten',
'fursuit_type': 'paws',
'stock': 5,
'featured': False
},
{
'name': 'Katze Schwanz - Whiskers',
'description': 'Katze Schwanz in schwarz mit Bewegungsmechanismus',
'price': 200.00,
'category': 'Schwänze',
'fursuit_type': 'tail',
'stock': 8,
'featured': False
},
]
for prod_data in products_data:
category = Category.objects.get(name=prod_data['category'])
product, created = Product.objects.get_or_create(
name=prod_data['name'],
defaults={
'description': prod_data['description'],
'price': prod_data['price'],
'category': category,
'fursuit_type': prod_data['fursuit_type'],
'stock': prod_data['stock'],
'featured': prod_data['featured'],
'is_active': True
}
)
if created:
print(f"✅ Produkt erstellt: {product.name}")
if __name__ == '__main__':
init_docker()

22
manage.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'webshop.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

217
mobile/models.py Normal file
View File

@ -0,0 +1,217 @@
"""
Mobile App Models für React Native API
"""
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
import uuid
class MobileDevice(models.Model):
"""Mobile Device Registration für Push Notifications"""
DEVICE_TYPE_CHOICES = [
('ios', 'iOS'),
('android', 'Android'),
('web', 'Web'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='mobile_devices')
device_token = models.CharField(max_length=255, unique=True)
device_type = models.CharField(max_length=20, choices=DEVICE_TYPE_CHOICES)
device_name = models.CharField(max_length=100, blank=True)
device_model = models.CharField(max_length=100, blank=True)
os_version = models.CharField(max_length=50, blank=True)
app_version = models.CharField(max_length=20, blank=True)
is_active = models.BooleanField(default=True)
last_seen = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'Mobile Device'
verbose_name_plural = 'Mobile Devices'
def __str__(self):
return f"{self.user.username} - {self.device_type} ({self.device_name})"
class PushNotification(models.Model):
"""Push Notification Tracking"""
NOTIFICATION_TYPE_CHOICES = [
('order_status', 'Order Status'),
('auction_update', 'Auction Update'),
('chat_message', 'Chat Message'),
('promotion', 'Promotion'),
('system', 'System'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='push_notifications')
device = models.ForeignKey(MobileDevice, on_delete=models.CASCADE, related_name='notifications')
notification_type = models.CharField(max_length=20, choices=NOTIFICATION_TYPE_CHOICES)
title = models.CharField(max_length=200)
message = models.TextField()
data = models.JSONField(default=dict) # Additional data for deep linking
sent_at = models.DateTimeField(auto_now_add=True)
delivered_at = models.DateTimeField(null=True, blank=True)
opened_at = models.DateTimeField(null=True, blank=True)
is_sent = models.BooleanField(default=False)
is_delivered = models.BooleanField(default=False)
is_opened = models.BooleanField(default=False)
class Meta:
ordering = ['-sent_at']
verbose_name = 'Push Notification'
verbose_name_plural = 'Push Notifications'
def __str__(self):
return f"{self.notification_type}: {self.title}"
class OfflineSync(models.Model):
"""Offline Sync Tracking für Mobile App"""
SYNC_STATUS_CHOICES = [
('pending', 'Pending'),
('syncing', 'Syncing'),
('completed', 'Completed'),
('failed', 'Failed'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='offline_syncs')
device = models.ForeignKey(MobileDevice, on_delete=models.CASCADE, related_name='syncs')
sync_type = models.CharField(max_length=50) # 'orders', 'products', 'auctions'
data = models.JSONField() # Data to sync
status = models.CharField(max_length=20, choices=SYNC_STATUS_CHOICES, default='pending')
error_message = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
synced_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'Offline Sync'
verbose_name_plural = 'Offline Syncs'
def __str__(self):
return f"{self.user.username} - {self.sync_type} ({self.get_status_display()})"
class MobileAnalytics(models.Model):
"""Mobile App Analytics"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='mobile_analytics')
device = models.ForeignKey(MobileDevice, on_delete=models.CASCADE, related_name='analytics')
event_type = models.CharField(max_length=50) # 'app_open', 'product_view', 'purchase'
event_data = models.JSONField(default=dict)
session_id = models.CharField(max_length=100, blank=True)
screen_name = models.CharField(max_length=100, blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-timestamp']
verbose_name = 'Mobile Analytics'
verbose_name_plural = 'Mobile Analytics'
def __str__(self):
return f"{self.user.username} - {self.event_type}"
class MobileCache(models.Model):
"""Mobile App Cache für Offline Support"""
CACHE_TYPE_CHOICES = [
('products', 'Products'),
('categories', 'Categories'),
('auctions', 'Auctions'),
('user_data', 'User Data'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='mobile_caches')
cache_type = models.CharField(max_length=20, choices=CACHE_TYPE_CHOICES)
cache_key = models.CharField(max_length=255)
cache_data = models.JSONField()
expires_at = models.DateTimeField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-updated_at']
verbose_name = 'Mobile Cache'
verbose_name_plural = 'Mobile Caches'
unique_together = ['user', 'cache_type', 'cache_key']
def __str__(self):
return f"{self.user.username} - {self.cache_type}: {self.cache_key}"
@property
def is_expired(self):
"""Prüfen ob Cache abgelaufen ist"""
return timezone.now() > self.expires_at
class MobileSession(models.Model):
"""Mobile App Session Tracking"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='mobile_sessions')
device = models.ForeignKey(MobileDevice, on_delete=models.CASCADE, related_name='sessions')
session_id = models.CharField(max_length=100, unique=True)
started_at = models.DateTimeField(auto_now_add=True)
ended_at = models.DateTimeField(null=True, blank=True)
duration = models.IntegerField(default=0) # in seconds
is_active = models.BooleanField(default=True)
class Meta:
ordering = ['-started_at']
verbose_name = 'Mobile Session'
verbose_name_plural = 'Mobile Sessions'
def __str__(self):
return f"{self.user.username} - {self.session_id}"
def end_session(self):
"""Session beenden"""
self.ended_at = timezone.now()
self.is_active = False
self.duration = int((self.ended_at - self.started_at).total_seconds())
self.save()
class MobileError(models.Model):
"""Mobile App Error Tracking"""
ERROR_LEVEL_CHOICES = [
('debug', 'Debug'),
('info', 'Info'),
('warning', 'Warning'),
('error', 'Error'),
('critical', 'Critical'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='mobile_errors')
device = models.ForeignKey(MobileDevice, on_delete=models.CASCADE, related_name='errors')
error_level = models.CharField(max_length=20, choices=ERROR_LEVEL_CHOICES)
error_type = models.CharField(max_length=100)
error_message = models.TextField()
stack_trace = models.TextField(blank=True)
app_version = models.CharField(max_length=20, blank=True)
os_version = models.CharField(max_length=50, blank=True)
device_info = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)
is_resolved = models.BooleanField(default=False)
class Meta:
ordering = ['-created_at']
verbose_name = 'Mobile Error'
verbose_name_plural = 'Mobile Errors'
def __str__(self):
return f"{self.error_type}: {self.error_message[:50]}"

205
mobile/serializers.py Normal file
View File

@ -0,0 +1,205 @@
"""
Mobile API Serializers für React Native
"""
from rest_framework import serializers
from .models import MobileDevice, PushNotification, OfflineSync, MobileAnalytics, MobileSession, MobileError, MobileCache
class MobileDeviceSerializer(serializers.ModelSerializer):
"""Mobile Device Serializer"""
class Meta:
model = MobileDevice
fields = [
'id', 'device_token', 'device_type', 'device_name',
'device_model', 'os_version', 'app_version', 'is_active',
'last_seen', 'created_at'
]
read_only_fields = ['id', 'created_at']
class PushNotificationSerializer(serializers.ModelSerializer):
"""Push Notification Serializer"""
class Meta:
model = PushNotification
fields = [
'id', 'notification_type', 'title', 'message', 'data',
'sent_at', 'delivered_at', 'opened_at', 'is_sent',
'is_delivered', 'is_opened'
]
read_only_fields = ['id', 'sent_at']
class OfflineSyncSerializer(serializers.ModelSerializer):
"""Offline Sync Serializer"""
class Meta:
model = OfflineSync
fields = [
'id', 'sync_type', 'data', 'status', 'error_message',
'created_at', 'synced_at'
]
read_only_fields = ['id', 'created_at']
class MobileAnalyticsSerializer(serializers.ModelSerializer):
"""Mobile Analytics Serializer"""
class Meta:
model = MobileAnalytics
fields = [
'id', 'event_type', 'event_data', 'session_id',
'screen_name', 'timestamp'
]
read_only_fields = ['id', 'timestamp']
class MobileSessionSerializer(serializers.ModelSerializer):
"""Mobile Session Serializer"""
class Meta:
model = MobileSession
fields = [
'id', 'session_id', 'started_at', 'ended_at',
'duration', 'is_active'
]
read_only_fields = ['id', 'started_at']
class MobileErrorSerializer(serializers.ModelSerializer):
"""Mobile Error Serializer"""
class Meta:
model = MobileError
fields = [
'id', 'error_level', 'error_type', 'error_message',
'stack_trace', 'app_version', 'os_version', 'device_info',
'created_at', 'is_resolved'
]
read_only_fields = ['id', 'created_at']
class MobileCacheSerializer(serializers.ModelSerializer):
"""Mobile Cache Serializer"""
class Meta:
model = MobileCache
fields = [
'id', 'cache_type', 'cache_key', 'cache_data',
'expires_at', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at']
# Mobile-optimierte Produkt Serializer
class MobileProductSerializer(serializers.Serializer):
"""Mobile-optimierter Produkt Serializer"""
id = serializers.UUIDField()
name = serializers.CharField()
description = serializers.CharField()
price = serializers.DecimalField(max_digits=10, decimal_places=2)
image_url = serializers.CharField()
category = serializers.CharField()
fursuit_type = serializers.CharField()
featured = serializers.BooleanField()
created_at = serializers.DateTimeField()
# Mobile-spezifische Felder
is_favorite = serializers.BooleanField(default=False)
in_cart = serializers.BooleanField(default=False)
cart_quantity = serializers.IntegerField(default=0)
# Mobile-optimierter Auction Serializer
class MobileAuctionSerializer(serializers.Serializer):
"""Mobile-optimierter Auction Serializer"""
id = serializers.UUIDField()
title = serializers.CharField()
description = serializers.CharField()
fursuit_type = serializers.CharField()
starting_bid = serializers.DecimalField(max_digits=10, decimal_places=2)
current_bid = serializers.DecimalField(max_digits=10, decimal_places=2)
status = serializers.CharField()
end_time = serializers.DateTimeField()
time_remaining = serializers.CharField()
total_bids = serializers.IntegerField()
is_watching = serializers.BooleanField(default=False)
created_at = serializers.DateTimeField()
# Mobile API Response Serializer
class MobileApiResponseSerializer(serializers.Serializer):
"""Standard Mobile API Response"""
success = serializers.BooleanField()
data = serializers.JSONField()
message = serializers.CharField(required=False)
error = serializers.CharField(required=False)
timestamp = serializers.DateTimeField()
# Mobile Push Notification Payload Serializer
class PushNotificationPayloadSerializer(serializers.Serializer):
"""Push Notification Payload für Firebase/APNS"""
notification = serializers.DictField()
data = serializers.DictField()
android = serializers.DictField(required=False)
apns = serializers.DictField(required=False)
webpush = serializers.DictField(required=False)
# Mobile Analytics Event Serializer
class MobileAnalyticsEventSerializer(serializers.Serializer):
"""Mobile Analytics Event"""
event_type = serializers.CharField()
event_data = serializers.DictField()
session_id = serializers.CharField(required=False)
screen_name = serializers.CharField(required=False)
timestamp = serializers.DateTimeField()
device_info = serializers.DictField(required=False)
# Mobile Cache Request Serializer
class MobileCacheRequestSerializer(serializers.Serializer):
"""Mobile Cache Request"""
cache_type = serializers.CharField()
cache_key = serializers.CharField()
data = serializers.DictField()
expires_in = serializers.IntegerField(required=False, default=3600)
# Mobile Session Request Serializer
class MobileSessionRequestSerializer(serializers.Serializer):
"""Mobile Session Request"""
session_id = serializers.CharField()
device_token = serializers.CharField()
app_version = serializers.CharField(required=False)
device_info = serializers.DictField(required=False)
# Mobile Error Report Serializer
class MobileErrorReportSerializer(serializers.Serializer):
"""Mobile Error Report"""
error_level = serializers.ChoiceField(choices=[
('debug', 'Debug'),
('info', 'Info'),
('warning', 'Warning'),
('error', 'Error'),
('critical', 'Critical'),
])
error_type = serializers.CharField()
error_message = serializers.CharField()
stack_trace = serializers.CharField(required=False)
app_version = serializers.CharField(required=False)
os_version = serializers.CharField(required=False)
device_info = serializers.DictField(required=False)
session_id = serializers.CharField(required=False)

39
mobile/urls.py Normal file
View File

@ -0,0 +1,39 @@
"""
Mobile API URL Configuration
"""
from django.urls import path
from . import views
app_name = 'mobile'
urlpatterns = [
# Device Management
path('api/device/register/', views.register_device, name='register_device'),
path('api/device/unregister/<str:device_token>/', views.unregister_device, name='unregister_device'),
path('api/device/list/', views.get_user_devices, name='get_user_devices'),
# Push Notifications
path('api/notifications/send/', views.send_push_notification, name='send_push_notification'),
# Offline Sync
path('api/sync/', views.sync_offline_data, name='sync_offline_data'),
# Analytics
path('api/analytics/track/', views.track_analytics, name='track_analytics'),
# Mobile-optimierte Daten
path('api/products/', views.get_mobile_products, name='get_mobile_products'),
path('api/auctions/', views.get_mobile_auctions, name='get_mobile_auctions'),
# Session Management
path('api/session/start/', views.start_session, name='start_session'),
path('api/session/end/<str:session_id>/', views.end_session, name='end_session'),
# Error Reporting
path('api/error/report/', views.report_error, name='report_error'),
# Cache Management
path('api/cache/<str:cache_type>/', views.get_mobile_cache, name='get_mobile_cache'),
path('api/cache/<str:cache_type>/update/', views.update_mobile_cache, name='update_mobile_cache'),
]

468
mobile/views.py Normal file
View File

@ -0,0 +1,468 @@
"""
Mobile API Views für React Native Integration
"""
from django.shortcuts import render
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from .models import MobileDevice, PushNotification, OfflineSync, MobileAnalytics, MobileCache, MobileSession
from .serializers import MobileDeviceSerializer, PushNotificationSerializer
import json
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def register_device(request):
"""Mobile Device registrieren für Push Notifications"""
try:
data = request.data
device_token = data.get('device_token')
device_type = data.get('device_type', 'web')
if not device_token:
return Response({'error': 'Device token required'}, status=status.HTTP_400_BAD_REQUEST)
# Device erstellen oder aktualisieren
device, created = MobileDevice.objects.get_or_create(
device_token=device_token,
defaults={
'user': request.user,
'device_type': device_type,
'device_name': data.get('device_name', ''),
'device_model': data.get('device_model', ''),
'os_version': data.get('os_version', ''),
'app_version': data.get('app_version', ''),
}
)
if not created:
# Device aktualisieren
device.user = request.user
device.device_type = device_type
device.device_name = data.get('device_name', device.device_name)
device.device_model = data.get('device_model', device.device_model)
device.os_version = data.get('os_version', device.os_version)
device.app_version = data.get('app_version', device.app_version)
device.is_active = True
device.save()
serializer = MobileDeviceSerializer(device)
return Response(serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['DELETE'])
@permission_classes([IsAuthenticated])
def unregister_device(request, device_token):
"""Mobile Device deregistrieren"""
try:
device = MobileDevice.objects.get(
device_token=device_token,
user=request.user
)
device.is_active = False
device.save()
return Response({'message': 'Device unregistered'}, status=status.HTTP_200_OK)
except MobileDevice.DoesNotExist:
return Response({'error': 'Device not found'}, status=status.HTTP_404_NOT_FOUND)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_user_devices(request):
"""User's Mobile Devices abrufen"""
devices = MobileDevice.objects.filter(user=request.user, is_active=True)
serializer = MobileDeviceSerializer(devices, many=True)
return Response(serializer.data)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def send_push_notification(request):
"""Push Notification senden"""
try:
data = request.data
user_id = data.get('user_id')
notification_type = data.get('notification_type')
title = data.get('title')
message = data.get('message')
data_payload = data.get('data', {})
if not all([user_id, notification_type, title, message]):
return Response({'error': 'Missing required fields'}, status=status.HTTP_400_BAD_REQUEST)
# Get user's active devices
devices = MobileDevice.objects.filter(
user_id=user_id,
is_active=True
)
notifications = []
for device in devices:
notification = PushNotification.objects.create(
user_id=user_id,
device=device,
notification_type=notification_type,
title=title,
message=message,
data=data_payload
)
notifications.append(notification)
# Send actual push notification (would integrate with Firebase/APNS)
# send_push_notification_to_device(notifications)
serializer = PushNotificationSerializer(notifications, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def sync_offline_data(request):
"""Offline Data Sync für Mobile App"""
try:
data = request.data
sync_type = data.get('sync_type')
sync_data = data.get('data', {})
device_token = data.get('device_token')
if not sync_type or not device_token:
return Response({'error': 'Missing required fields'}, status=status.HTTP_400_BAD_REQUEST)
# Get device
try:
device = MobileDevice.objects.get(
device_token=device_token,
user=request.user,
is_active=True
)
except MobileDevice.DoesNotExist:
return Response({'error': 'Device not found'}, status=status.HTTP_404_NOT_FOUND)
# Create sync record
sync = OfflineSync.objects.create(
user=request.user,
device=device,
sync_type=sync_type,
data=sync_data,
status='pending'
)
# Process sync based on type
if sync_type == 'orders':
# Sync orders
pass
elif sync_type == 'products':
# Sync products
pass
elif sync_type == 'auctions':
# Sync auctions
pass
sync.status = 'completed'
sync.synced_at = timezone.now()
sync.save()
return Response({
'sync_id': str(sync.id),
'status': sync.status,
'synced_at': sync.synced_at
}, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def track_analytics(request):
"""Mobile Analytics Event tracken"""
try:
data = request.data
event_type = data.get('event_type')
event_data = data.get('event_data', {})
session_id = data.get('session_id', '')
screen_name = data.get('screen_name', '')
device_token = data.get('device_token')
if not event_type or not device_token:
return Response({'error': 'Missing required fields'}, status=status.HTTP_400_BAD_REQUEST)
# Get device
try:
device = MobileDevice.objects.get(
device_token=device_token,
user=request.user,
is_active=True
)
except MobileDevice.DoesNotExist:
return Response({'error': 'Device not found'}, status=status.HTTP_404_NOT_FOUND)
# Create analytics record
analytics = MobileAnalytics.objects.create(
user=request.user,
device=device,
event_type=event_type,
event_data=event_data,
session_id=session_id,
screen_name=screen_name
)
return Response({'analytics_id': str(analytics.id)}, status=status.HTTP_201_CREATED)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_mobile_products(request):
"""Mobile-optimierte Produktliste"""
from products.models import Product
from products.serializers import ProductSerializer
# Mobile-optimierte Filter
products = Product.objects.filter(is_active=True).select_related('category')
# Pagination
page = int(request.GET.get('page', 1))
per_page = int(request.GET.get('per_page', 20))
start = (page - 1) * per_page
end = start + per_page
products_page = products[start:end]
serializer = ProductSerializer(products_page, many=True)
return Response({
'products': serializer.data,
'total': products.count(),
'page': page,
'per_page': per_page,
'has_next': end < products.count(),
'has_previous': page > 1
})
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_mobile_auctions(request):
"""Mobile-optimierte Auktionen"""
from auction.models import Auction
from auction.serializers import AuctionSerializer
auctions = Auction.objects.filter(status='active').select_related('created_by')
# Pagination
page = int(request.GET.get('page', 1))
per_page = int(request.GET.get('per_page', 10))
start = (page - 1) * per_page
end = start + per_page
auctions_page = auctions[start:end]
serializer = AuctionSerializer(auctions_page, many=True)
return Response({
'auctions': serializer.data,
'total': auctions.count(),
'page': page,
'per_page': per_page,
'has_next': end < auctions.count(),
'has_previous': page > 1
})
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def start_session(request):
"""Mobile Session starten"""
try:
data = request.data
device_token = data.get('device_token')
session_id = data.get('session_id')
if not device_token or not session_id:
return Response({'error': 'Missing required fields'}, status=status.HTTP_400_BAD_REQUEST)
# Get device
try:
device = MobileDevice.objects.get(
device_token=device_token,
user=request.user,
is_active=True
)
except MobileDevice.DoesNotExist:
return Response({'error': 'Device not found'}, status=status.HTTP_404_NOT_FOUND)
# Create or update session
session, created = MobileSession.objects.get_or_create(
session_id=session_id,
defaults={
'user': request.user,
'device': device,
'is_active': True
}
)
if not created:
session.is_active = True
session.save()
return Response({
'session_id': session.session_id,
'started_at': session.started_at
}, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def end_session(request, session_id):
"""Mobile Session beenden"""
try:
session = MobileSession.objects.get(
session_id=session_id,
user=request.user,
is_active=True
)
session.end_session()
return Response({
'session_id': session.session_id,
'duration': session.duration,
'ended_at': session.ended_at
}, status=status.HTTP_200_OK)
except MobileSession.DoesNotExist:
return Response({'error': 'Session not found'}, status=status.HTTP_404_NOT_FOUND)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def report_error(request):
"""Mobile App Error melden"""
try:
data = request.data
error_level = data.get('error_level', 'error')
error_type = data.get('error_type')
error_message = data.get('error_message')
stack_trace = data.get('stack_trace', '')
device_token = data.get('device_token')
if not error_type or not error_message or not device_token:
return Response({'error': 'Missing required fields'}, status=status.HTTP_400_BAD_REQUEST)
# Get device
try:
device = MobileDevice.objects.get(
device_token=device_token,
user=request.user,
is_active=True
)
except MobileDevice.DoesNotExist:
return Response({'error': 'Device not found'}, status=status.HTTP_404_NOT_FOUND)
# Create error record
error = MobileError.objects.create(
user=request.user,
device=device,
error_level=error_level,
error_type=error_type,
error_message=error_message,
stack_trace=stack_trace,
app_version=data.get('app_version', ''),
os_version=data.get('os_version', ''),
device_info=data.get('device_info', {})
)
return Response({'error_id': str(error.id)}, status=status.HTTP_201_CREATED)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_mobile_cache(request, cache_type):
"""Mobile Cache abrufen"""
try:
cache_key = request.GET.get('key', 'default')
cache = MobileCache.objects.filter(
user=request.user,
cache_type=cache_type,
cache_key=cache_key,
expires_at__gt=timezone.now()
).first()
if cache:
return Response({
'data': cache.cache_data,
'expires_at': cache.expires_at
})
else:
return Response({'data': None}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def update_mobile_cache(request, cache_type):
"""Mobile Cache aktualisieren"""
try:
data = request.data
cache_key = data.get('key', 'default')
cache_data = data.get('data', {})
expires_in = data.get('expires_in', 3600) # Default 1 hour
expires_at = timezone.now() + timezone.timedelta(seconds=expires_in)
cache, created = MobileCache.objects.update_or_create(
user=request.user,
cache_type=cache_type,
cache_key=cache_key,
defaults={
'cache_data': cache_data,
'expires_at': expires_at
}
)
return Response({
'cache_id': str(cache.id),
'expires_at': cache.expires_at
}, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -0,0 +1,16 @@
from django.contrib import admin
from .models import PayPalConfig
@admin.register(PayPalConfig)
class PayPalConfigAdmin(admin.ModelAdmin):
list_display = ('get_mode', 'client_id', 'is_sandbox', 'updated_at')
readonly_fields = ('created_at', 'updated_at')
fieldsets = (
('API Konfiguration', {
'fields': ('client_id', 'client_secret', 'is_sandbox')
}),
('Zeitstempel', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)

View File

@ -0,0 +1,19 @@
from django.db import models
from django.conf import settings
class PayPalConfig(models.Model):
client_id = models.CharField(max_length=255)
client_secret = models.CharField(max_length=255)
is_sandbox = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'PayPal Konfiguration'
verbose_name_plural = 'PayPal Konfigurationen'
def __str__(self):
return f"PayPal Config ({self.get_mode()})"
def get_mode(self):
return "Sandbox" if self.is_sandbox else "Live"

View File

@ -0,0 +1,10 @@
from django.urls import path
from . import views
app_name = 'paypal'
urlpatterns = [
path('process/<int:order_id>/', views.process_payment, name='process_payment'),
path('execute/<int:order_id>/', views.execute_payment, name='execute_payment'),
path('cancel/<int:order_id>/', views.cancel_payment, name='cancel_payment'),
]

View File

@ -0,0 +1,87 @@
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.conf import settings
import paypalrestsdk
from .models import PayPalConfig
from products.models import Order
def setup_paypal():
config = PayPalConfig.objects.first()
if config:
paypalrestsdk.configure({
"mode": "sandbox" if config.is_sandbox else "live",
"client_id": config.client_id,
"client_secret": config.client_secret
})
return True
return False
@login_required
def process_payment(request, order_id):
if not setup_paypal():
messages.error(request, 'PayPal ist nicht richtig konfiguriert. Bitte kontaktieren Sie den Administrator.')
return redirect('order_detail', order_id=order_id)
try:
order = Order.objects.get(id=order_id, user=request.user)
payment = paypalrestsdk.Payment({
"intent": "sale",
"payer": {
"payment_method": "paypal"
},
"redirect_urls": {
"return_url": request.build_absolute_uri(f'/paypal/execute/{order_id}/'),
"cancel_url": request.build_absolute_uri(f'/paypal/cancel/{order_id}/')
},
"transactions": [{
"amount": {
"total": str(order.total_amount),
"currency": "EUR"
},
"description": f"Bestellung #{order.id}"
}]
})
if payment.create():
for link in payment.links:
if link.method == "REDIRECT":
return redirect(link.href)
else:
messages.error(request, 'Fehler bei der Erstellung der PayPal-Zahlung.')
except Order.DoesNotExist:
messages.error(request, 'Bestellung nicht gefunden.')
except Exception as e:
messages.error(request, f'Ein Fehler ist aufgetreten: {str(e)}')
return redirect('order_detail', order_id=order_id)
@login_required
def execute_payment(request, order_id):
payment_id = request.GET.get('paymentId')
payer_id = request.GET.get('PayerID')
try:
payment = paypalrestsdk.Payment.find(payment_id)
if payment.execute({"payer_id": payer_id}):
order = Order.objects.get(id=order_id, user=request.user)
order.status = 'paid'
order.payment_method = 'paypal'
order.payment_id = payment_id
order.save()
messages.success(request, 'Zahlung erfolgreich durchgeführt!')
return redirect('payment_success', order_id=order_id)
else:
messages.error(request, 'Fehler bei der Ausführung der Zahlung.')
except Exception as e:
messages.error(request, f'Ein Fehler ist aufgetreten: {str(e)}')
return redirect('payment_failed', order_id=order_id)
@login_required
def cancel_payment(request, order_id):
messages.info(request, 'Die PayPal-Zahlung wurde abgebrochen.')
return redirect('order_detail', order_id=order_id)

0
products/__init__.py Normal file
View File

2
products/admin.py Normal file
View File

@ -0,0 +1,2 @@
# Diese Datei ist jetzt leer, da alle Admin-Konfigurationen in webshop/admin.py zentral verwaltet werden
# Die Admin-Konfigurationen wurden in die neue zentrale Admin-Site verschoben

111
products/analytics.py Normal file
View File

@ -0,0 +1,111 @@
from django.db.models import Count, Sum, Avg, Q
from django.utils import timezone
from datetime import timedelta
from .models import Product, Order, Review, CustomOrder, UserProfile
class ShopAnalytics:
"""Analytics-Klasse für Shop-Statistiken"""
@staticmethod
def get_sales_statistics(days=30):
"""Verkaufsstatistiken der letzten X Tage"""
end_date = timezone.now()
start_date = end_date - timedelta(days=days)
orders = Order.objects.filter(
created__range=(start_date, end_date),
payment_status='paid'
)
return {
'total_orders': orders.count(),
'total_revenue': orders.aggregate(Sum('total_amount'))['total_amount__sum'] or 0,
'average_order_value': orders.aggregate(Avg('total_amount'))['total_amount__avg'] or 0,
'top_products': orders.values('items__product__name').annotate(
count=Count('items__product')
).order_by('-count')[:5]
}
@staticmethod
def get_product_analytics():
"""Produkt-spezifische Analytics"""
products = Product.objects.all()
return {
'total_products': products.count(),
'in_stock': products.filter(stock__gt=0).count(),
'low_stock': products.filter(stock__lte=5, stock__gt=0).count(),
'out_of_stock': products.filter(stock=0).count(),
'featured_products': products.filter(is_featured=True).count(),
'custom_orders': products.filter(is_custom_order=True).count(),
'top_rated': products.annotate(
avg_rating=Avg('reviews__rating')
).filter(avg_rating__gte=4.0).order_by('-avg_rating')[:5]
}
@staticmethod
def get_user_analytics():
"""Benutzer-spezifische Analytics"""
from django.contrib.auth.models import User
users = User.objects.all()
profiles = UserProfile.objects.all()
return {
'total_users': users.count(),
'active_users': users.filter(last_login__gte=timezone.now() - timedelta(days=30)).count(),
'newsletter_subscribers': profiles.filter(newsletter=True).count(),
'top_customers': Order.objects.values('user__username').annotate(
total_spent=Sum('total_amount')
).order_by('-total_spent')[:5]
}
@staticmethod
def get_custom_order_analytics():
"""Custom Order Analytics"""
custom_orders = CustomOrder.objects.all()
return {
'total_custom_orders': custom_orders.count(),
'pending_orders': custom_orders.filter(status='pending').count(),
'in_progress': custom_orders.filter(status='in_progress').count(),
'completed': custom_orders.filter(status='completed').count(),
'average_completion_time': custom_orders.filter(
status='completed'
).aggregate(
avg_time=Avg('updated' - 'created')
)['avg_time']
}
@staticmethod
def get_review_analytics():
"""Review Analytics"""
reviews = Review.objects.all()
return {
'total_reviews': reviews.count(),
'average_rating': reviews.aggregate(Avg('rating'))['rating__avg'] or 0,
'rating_distribution': reviews.values('rating').annotate(
count=Count('rating')
).order_by('rating'),
'recent_reviews': reviews.order_by('-created')[:10]
}
class UserBehaviorTracker:
"""Tracking für Benutzerverhalten"""
@staticmethod
def track_product_view(product, user=None):
"""Produktansicht tracken"""
# Hier könnte man Redis oder eine separate Tracking-Tabelle verwenden
pass
@staticmethod
def track_search_query(query, user=None):
"""Suchanfragen tracken"""
pass
@staticmethod
def track_cart_addition(product, user=None):
"""Warenkorb-Hinzufügung tracken"""
pass

62
products/api.py Normal file
View File

@ -0,0 +1,62 @@
from rest_framework import viewsets, permissions, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from .models import Product, Review, Category, CustomOrder
from .serializers import (
ProductSerializer, ReviewSerializer,
CategorySerializer, CustomOrderSerializer
)
class ProductViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Product.objects.all().select_related('category').prefetch_related('reviews')
serializer_class = ProductSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['fursuit_type', 'style', 'category', 'is_featured', 'is_custom_order']
search_fields = ['name', 'description', 'category__name']
ordering_fields = ['price', 'created', 'name', 'average_rating']
ordering = ['-created']
@action(detail=True, methods=['post'])
def add_to_wishlist(self, request, pk=None):
product = self.get_object()
user = request.user
if user.is_authenticated:
product.wishlist_users.add(user)
return Response({'status': 'added to wishlist'})
return Response({'error': 'user not authenticated'}, status=401)
@action(detail=True, methods=['post'])
def add_review(self, request, pk=None):
product = self.get_object()
serializer = ReviewSerializer(data=request.data)
if serializer.is_valid():
serializer.save(product=product, user=request.user)
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
class ReviewViewSet(viewsets.ModelViewSet):
queryset = Review.objects.all().select_related('product', 'user')
serializer_class = ReviewSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend]
filterset_fields = ['product', 'rating']
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class CustomOrderViewSet(viewsets.ModelViewSet):
serializer_class = CustomOrderSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return CustomOrder.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)

109
products/api_urls.py Normal file
View File

@ -0,0 +1,109 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import api_views
# Router für ViewSets
router = DefaultRouter()
router.register(r'products', api_views.ProductViewSet, basename='api-product')
router.register(r'categories', api_views.CategoryViewSet, basename='api-category')
router.register(r'reviews', api_views.ReviewViewSet, basename='api-review')
router.register(r'wishlist', api_views.WishlistViewSet, basename='api-wishlist')
router.register(r'custom-orders', api_views.CustomOrderViewSet, basename='api-custom-order')
router.register(r'gallery', api_views.GalleryImageViewSet, basename='api-gallery')
router.register(r'cart', api_views.CartViewSet, basename='api-cart')
router.register(r'search', api_views.SearchViewSet, basename='api-search')
# API URL Patterns
urlpatterns = [
# Router URLs
path('', include(router.urls)),
# Zusätzliche API Endpoints
path('products/<int:pk>/add-to-wishlist/',
api_views.ProductViewSet.as_view({'post': 'add_to_wishlist'}),
name='api-product-add-to-wishlist'),
path('products/<int:pk>/remove-from-wishlist/',
api_views.ProductViewSet.as_view({'post': 'remove_from_wishlist'}),
name='api-product-remove-from-wishlist'),
path('products/featured/',
api_views.ProductViewSet.as_view({'get': 'featured'}),
name='api-products-featured'),
path('products/on-sale/',
api_views.ProductViewSet.as_view({'get': 'on_sale'}),
name='api-products-on-sale'),
path('products/low-stock/',
api_views.ProductViewSet.as_view({'get': 'low_stock'}),
name='api-products-low-stock'),
path('products/stats/',
api_views.ProductViewSet.as_view({'get': 'stats'}),
name='api-products-stats'),
path('categories/<int:pk>/products/',
api_views.CategoryViewSet.as_view({'get': 'products'}),
name='api-category-products'),
path('reviews/my-reviews/',
api_views.ReviewViewSet.as_view({'get': 'my_reviews'}),
name='api-my-reviews'),
path('wishlist/add-product/',
api_views.WishlistViewSet.as_view({'post': 'add_product'}),
name='api-wishlist-add-product'),
path('wishlist/remove-product/',
api_views.WishlistViewSet.as_view({'post': 'remove_product'}),
name='api-wishlist-remove-product'),
path('custom-orders/<int:pk>/update-status/',
api_views.CustomOrderViewSet.as_view({'post': 'update_status'}),
name='api-custom-order-update-status'),
path('gallery/featured/',
api_views.GalleryImageViewSet.as_view({'get': 'featured'}),
name='api-gallery-featured'),
path('gallery/by-product/',
api_views.GalleryImageViewSet.as_view({'get': 'by_product'}),
name='api-gallery-by-product'),
path('cart/get/',
api_views.CartViewSet.as_view({'get': 'get_cart'}),
name='api-cart-get'),
path('cart/add-item/',
api_views.CartViewSet.as_view({'post': 'add_item'}),
name='api-cart-add-item'),
path('cart/update-quantity/',
api_views.CartViewSet.as_view({'post': 'update_quantity'}),
name='api-cart-update-quantity'),
path('cart/remove-item/',
api_views.CartViewSet.as_view({'post': 'remove_item'}),
name='api-cart-remove-item'),
path('cart/clear/',
api_views.CartViewSet.as_view({'post': 'clear_cart'}),
name='api-cart-clear'),
path('search/products/',
api_views.SearchViewSet.as_view({'get': 'products'}),
name='api-search-products'),
]
# API Root
api_root = {
'products': 'api:product-list',
'categories': 'api:category-list',
'reviews': 'api:review-list',
'wishlist': 'api:wishlist-list',
'custom-orders': 'api:custom-order-list',
'gallery': 'api:gallery-list',
'cart': 'api:cart-list',
'search': 'api:search-list',
}

485
products/api_views.py Normal file
View File

@ -0,0 +1,485 @@
from rest_framework import viewsets, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q, Avg, Count
from django.shortcuts import get_object_or_404
from .models import Product, Category, Review, Wishlist, GalleryImage, CustomOrder
from .serializers import (
ProductSerializer, ProductListSerializer, ProductDetailSerializer,
CategorySerializer, ReviewSerializer, WishlistSerializer,
CustomOrderSerializer, CartSerializer, SearchFilterSerializer,
ProductStatsSerializer, GalleryImageDetailSerializer
)
class ProductViewSet(viewsets.ModelViewSet):
"""ViewSet für Produkt-API"""
queryset = Product.objects.select_related('category').prefetch_related(
'reviews', 'gallery_images', 'wishlists'
).all()
serializer_class = ProductListSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['category', 'product_type', 'fursuit_type', 'style', 'on_sale', 'is_featured']
search_fields = ['name', 'description']
ordering_fields = ['name', 'base_price', 'created_at', 'average_rating']
ordering = ['-created_at']
def get_serializer_class(self):
if self.action == 'retrieve':
return ProductDetailSerializer
elif self.action == 'create' or self.action == 'update':
return ProductSerializer
return ProductListSerializer
def get_queryset(self):
queryset = super().get_queryset()
# Filter nach Preis
min_price = self.request.query_params.get('min_price')
max_price = self.request.query_params.get('max_price')
if min_price:
queryset = queryset.filter(base_price__gte=min_price)
if max_price:
queryset = queryset.filter(base_price__lte=max_price)
# Filter nach Lagerbestand
in_stock = self.request.query_params.get('in_stock')
if in_stock == 'true':
queryset = queryset.filter(stock__gt=0)
return queryset
@action(detail=False, methods=['get'])
def featured(self, request):
"""Featured Produkte"""
products = self.get_queryset().filter(is_featured=True)
serializer = self.get_serializer(products, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def on_sale(self, request):
"""Produkte im Angebot"""
products = self.get_queryset().filter(on_sale=True)
serializer = self.get_serializer(products, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def low_stock(self, request):
"""Produkte mit niedrigem Lagerbestand"""
products = self.get_queryset().filter(stock__lte=5, stock__gt=0)
serializer = self.get_serializer(products, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def add_to_wishlist(self, request, pk=None):
"""Produkt zur Wunschliste hinzufügen"""
product = self.get_object()
user = request.user
if not user.is_authenticated:
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
wishlist, created = Wishlist.objects.get_or_create(user=user)
wishlist.products.add(product)
return Response({
'message': 'Product added to wishlist',
'product_id': product.id
})
@action(detail=True, methods=['post'])
def remove_from_wishlist(self, request, pk=None):
"""Produkt von Wunschliste entfernen"""
product = self.get_object()
user = request.user
if not user.is_authenticated:
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
try:
wishlist = Wishlist.objects.get(user=user)
wishlist.products.remove(product)
return Response({
'message': 'Product removed from wishlist',
'product_id': product.id
})
except Wishlist.DoesNotExist:
return Response(
{'error': 'Wishlist not found'},
status=status.HTTP_404_NOT_FOUND
)
@action(detail=False, methods=['get'])
def stats(self, request):
"""Produkt-Statistiken"""
queryset = self.get_queryset()
stats = {
'total_products': queryset.count(),
'featured_products': queryset.filter(is_featured=True).count(),
'on_sale_products': queryset.filter(on_sale=True).count(),
'low_stock_products': queryset.filter(stock__lte=5, stock__gt=0).count(),
'categories_count': Category.objects.count(),
'average_rating': queryset.aggregate(Avg('average_rating'))['average_rating__avg'] or 0,
'total_reviews': Review.objects.count(),
}
serializer = ProductStatsSerializer(stats)
return Response(serializer.data)
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet für Kategorie-API"""
queryset = Category.objects.prefetch_related('products').all()
serializer_class = CategorySerializer
permission_classes = [IsAuthenticatedOrReadOnly]
@action(detail=True, methods=['get'])
def products(self, request, pk=None):
"""Produkte einer Kategorie"""
category = self.get_object()
products = category.products.all()
# Pagination
page = self.paginate_queryset(products)
if page is not None:
serializer = ProductListSerializer(page, many=True, context={'request': request})
return self.get_paginated_response(serializer.data)
serializer = ProductListSerializer(products, many=True, context={'request': request})
return Response(serializer.data)
class ReviewViewSet(viewsets.ModelViewSet):
"""ViewSet für Review-API"""
queryset = Review.objects.select_related('user', 'product').all()
serializer_class = ReviewSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend]
filterset_fields = ['product', 'rating']
def perform_create(self, serializer):
serializer.save(user=self.request.user)
def get_queryset(self):
queryset = super().get_queryset()
product_id = self.request.query_params.get('product_id')
if product_id:
queryset = queryset.filter(product_id=product_id)
return queryset
@action(detail=False, methods=['get'])
def my_reviews(self, request):
"""Reviews des aktuellen Users"""
if not request.user.is_authenticated:
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
reviews = self.get_queryset().filter(user=request.user)
serializer = self.get_serializer(reviews, many=True)
return Response(serializer.data)
class WishlistViewSet(viewsets.ModelViewSet):
"""ViewSet für Wishlist-API"""
serializer_class = WishlistSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Wishlist.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
@action(detail=False, methods=['post'])
def add_product(self, request):
"""Produkt zur Wunschliste hinzufügen"""
product_id = request.data.get('product_id')
if not product_id:
return Response(
{'error': 'product_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
product = Product.objects.get(id=product_id)
wishlist, created = Wishlist.objects.get_or_create(user=request.user)
wishlist.products.add(product)
return Response({
'message': 'Product added to wishlist',
'product_id': product_id
})
except Product.DoesNotExist:
return Response(
{'error': 'Product not found'},
status=status.HTTP_404_NOT_FOUND
)
@action(detail=False, methods=['post'])
def remove_product(self, request):
"""Produkt von Wunschliste entfernen"""
product_id = request.data.get('product_id')
if not product_id:
return Response(
{'error': 'product_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
wishlist = Wishlist.objects.get(user=request.user)
product = Product.objects.get(id=product_id)
wishlist.products.remove(product)
return Response({
'message': 'Product removed from wishlist',
'product_id': product_id
})
except (Wishlist.DoesNotExist, Product.DoesNotExist):
return Response(
{'error': 'Wishlist or product not found'},
status=status.HTTP_404_NOT_FOUND
)
class CustomOrderViewSet(viewsets.ModelViewSet):
"""ViewSet für Custom Order-API"""
serializer_class = CustomOrderSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend]
filterset_fields = ['status', 'fursuit_type', 'style']
def get_queryset(self):
return CustomOrder.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
@action(detail=True, methods=['post'])
def update_status(self, request, pk=None):
"""Status eines Custom Orders aktualisieren"""
custom_order = self.get_object()
new_status = request.data.get('status')
if new_status not in dict(CustomOrder.STATUS_CHOICES):
return Response(
{'error': 'Invalid status'},
status=status.HTTP_400_BAD_REQUEST
)
custom_order.status = new_status
custom_order.save()
serializer = self.get_serializer(custom_order)
return Response(serializer.data)
class GalleryImageViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet für Galerie-API"""
queryset = GalleryImage.objects.select_related('product').all()
serializer_class = GalleryImageDetailSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend]
filterset_fields = ['is_featured', 'fursuit_type', 'style']
@action(detail=False, methods=['get'])
def featured(self, request):
"""Featured Galerie-Bilder"""
images = self.get_queryset().filter(is_featured=True)
serializer = self.get_serializer(images, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def by_product(self, request):
"""Bilder nach Produkt filtern"""
product_id = request.query_params.get('product_id')
if not product_id:
return Response(
{'error': 'product_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
images = self.get_queryset().filter(product_id=product_id)
serializer = self.get_serializer(images, many=True)
return Response(serializer.data)
class CartViewSet(viewsets.ViewSet):
"""ViewSet für Warenkorb-API"""
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def get_cart(self, request):
"""Warenkorb abrufen"""
# Hier würde die Warenkorb-Logik implementiert
# Für jetzt ein Beispiel-Response
cart_data = {
'items': [],
'subtotal': 0,
'shipping_cost': 5.99,
'total': 5.99,
'item_count': 0
}
serializer = CartSerializer(cart_data)
return Response(serializer.data)
@action(detail=False, methods=['post'])
def add_item(self, request):
"""Item zum Warenkorb hinzufügen"""
product_id = request.data.get('product_id')
quantity = request.data.get('quantity', 1)
if not product_id:
return Response(
{'error': 'product_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
product = Product.objects.get(id=product_id)
# Hier würde die Warenkorb-Logik implementiert
return Response({
'message': 'Product added to cart',
'product_id': product_id,
'quantity': quantity
})
except Product.DoesNotExist:
return Response(
{'error': 'Product not found'},
status=status.HTTP_404_NOT_FOUND
)
@action(detail=False, methods=['post'])
def update_quantity(self, request):
"""Menge eines Items aktualisieren"""
item_id = request.data.get('item_id')
quantity = request.data.get('quantity')
if not item_id or not quantity:
return Response(
{'error': 'item_id and quantity are required'},
status=status.HTTP_400_BAD_REQUEST
)
# Hier würde die Warenkorb-Logik implementiert
return Response({
'message': 'Quantity updated',
'item_id': item_id,
'quantity': quantity
})
@action(detail=False, methods=['post'])
def remove_item(self, request):
"""Item aus Warenkorb entfernen"""
item_id = request.data.get('item_id')
if not item_id:
return Response(
{'error': 'item_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
# Hier würde die Warenkorb-Logik implementiert
return Response({
'message': 'Item removed from cart',
'item_id': item_id
})
@action(detail=False, methods=['post'])
def clear_cart(self, request):
"""Warenkorb leeren"""
# Hier würde die Warenkorb-Logik implementiert
return Response({
'message': 'Cart cleared'
})
class SearchViewSet(viewsets.ViewSet):
"""ViewSet für Such-API"""
permission_classes = [IsAuthenticatedOrReadOnly]
@action(detail=False, methods=['get'])
def products(self, request):
"""Produkte suchen"""
serializer = SearchFilterSerializer(data=request.query_params)
if not serializer.is_valid():
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST
)
data = serializer.validated_data
queryset = Product.objects.select_related('category').all()
# Suchfilter anwenden
if data.get('query'):
queryset = queryset.filter(
Q(name__icontains=data['query']) |
Q(description__icontains=data['query'])
)
if data.get('category'):
queryset = queryset.filter(category__slug=data['category'])
if data.get('fursuit_type'):
queryset = queryset.filter(fursuit_type=data['fursuit_type'])
if data.get('style'):
queryset = queryset.filter(style=data['style'])
if data.get('min_price'):
queryset = queryset.filter(base_price__gte=data['min_price'])
if data.get('max_price'):
queryset = queryset.filter(base_price__lte=data['max_price'])
if data.get('on_sale'):
queryset = queryset.filter(on_sale=data['on_sale'])
if data.get('is_featured'):
queryset = queryset.filter(is_featured=data['is_featured'])
# Sortierung
sort_by = data.get('sort_by', 'newest')
if sort_by == 'newest':
queryset = queryset.order_by('-created_at')
elif sort_by == 'oldest':
queryset = queryset.order_by('created_at')
elif sort_by == 'price_low':
queryset = queryset.order_by('base_price')
elif sort_by == 'price_high':
queryset = queryset.order_by('-base_price')
elif sort_by == 'name':
queryset = queryset.order_by('name')
elif sort_by == 'rating':
queryset = queryset.order_by('-average_rating')
# Pagination
page_size = data.get('page_size', 12)
page = data.get('page', 1)
start = (page - 1) * page_size
end = start + page_size
products = queryset[start:end]
serializer = ProductListSerializer(products, many=True, context={'request': request})
return Response({
'results': serializer.data,
'total': queryset.count(),
'page': page,
'page_size': page_size,
'pages': (queryset.count() + page_size - 1) // page_size
})

6
products/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ProductsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'products'

187
products/auction.py Normal file
View File

@ -0,0 +1,187 @@
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from decimal import Decimal
class Auction(models.Model):
"""Auktion für Custom-Design-Fursuits"""
STATUS_CHOICES = [
('draft', 'Entwurf'),
('active', 'Aktiv'),
('bidding', 'Bietphase'),
('ended', 'Beendet'),
('cancelled', 'Storniert'),
]
title = models.CharField(max_length=200)
description = models.TextField()
starting_price = models.DecimalField(max_digits=10, decimal_places=2)
current_price = models.DecimalField(max_digits=10, decimal_places=2)
reserve_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
min_bid_increment = models.DecimalField(max_digits=10, decimal_places=2, default=10.00)
# Custom Order Details
fursuit_type = models.CharField(max_length=20, choices=[
('partial', 'Partial'),
('fullsuit', 'Fullsuit'),
('head', 'Head Only'),
('paws', 'Paws'),
('tail', 'Tail'),
('other', 'Other'),
])
style = models.CharField(max_length=20, choices=[
('toony', 'Toony'),
('semi_realistic', 'Semi-Realistic'),
('realistic', 'Realistic'),
('anime', 'Anime'),
('chibi', 'Chibi'),
])
# Timing
start_time = models.DateTimeField()
end_time = models.DateTimeField()
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
# Status
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
winner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='won_auctions')
# Images
reference_images = models.FileField(upload_to='auctions/references/', null=True, blank=True)
preview_image = models.ImageField(upload_to='auctions/previews/', null=True, blank=True)
class Meta:
ordering = ['-created']
def __str__(self):
return f"Auktion: {self.title} - {self.current_price}"
@property
def is_active(self):
now = timezone.now()
return self.status == 'active' and self.start_time <= now <= self.end_time
@property
def time_remaining(self):
if not self.is_active:
return None
return self.end_time - timezone.now()
@property
def bid_count(self):
return self.bids.count()
@property
def highest_bid(self):
return self.bids.order_by('-amount').first()
class AuctionBid(models.Model):
"""Gebot bei einer Auktion"""
auction = models.ForeignKey(Auction, on_delete=models.CASCADE, related_name='bids')
bidder = models.ForeignKey(User, on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=10, decimal_places=2)
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-timestamp']
unique_together = ['auction', 'bidder']
def __str__(self):
return f"{self.bidder.username} - {self.amount}"
def save(self, *args, **kwargs):
# Aktualisiere den aktuellen Preis der Auktion
if self.amount > self.auction.current_price:
self.auction.current_price = self.amount
self.auction.save()
super().save(*args, **kwargs)
class AuctionWatchlist(models.Model):
"""Beobachtungsliste für Auktionen"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
auction = models.ForeignKey(Auction, on_delete=models.CASCADE)
added = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ['user', 'auction']
def __str__(self):
return f"{self.user.username} beobachtet {self.auction.title}"
class AuctionService:
"""Service-Klasse für Auktion-Logik"""
@staticmethod
def place_bid(auction, user, amount):
"""Gebot platzieren"""
if not auction.is_active:
raise ValueError("Auktion ist nicht aktiv")
if amount <= auction.current_price:
raise ValueError("Gebot muss höher als der aktuelle Preis sein")
if auction.min_bid_increment and amount < auction.current_price + auction.min_bid_increment:
raise ValueError(f"Mindestgebot: {auction.current_price + auction.min_bid_increment}")
# Erstelle oder aktualisiere Gebot
bid, created = AuctionBid.objects.get_or_create(
auction=auction,
bidder=user,
defaults={'amount': amount}
)
if not created:
bid.amount = amount
bid.save()
return bid
@staticmethod
def end_auction(auction):
"""Auktion beenden"""
if auction.status != 'active':
return
highest_bid = auction.highest_bid
if highest_bid:
# Prüfe Reserve-Preis
if auction.reserve_price and highest_bid.amount < auction.reserve_price:
auction.status = 'ended'
auction.save()
return None # Reserve-Preis nicht erreicht
auction.winner = highest_bid.bidder
auction.status = 'ended'
auction.save()
# Erstelle Custom Order
custom_order = CustomOrder.objects.create(
user=highest_bid.bidder,
fursuit_type=auction.fursuit_type,
style=auction.style,
character_name=f"Auktion: {auction.title}",
character_description=auction.description,
budget_range=f"Gewonnen für {highest_bid.amount}",
status='approved',
quoted_price=highest_bid.amount
)
return custom_order
else:
auction.status = 'ended'
auction.save()
return None
@staticmethod
def get_auction_stats(auction):
"""Statistiken für eine Auktion"""
return {
'total_bids': auction.bid_count,
'unique_bidders': auction.bids.values('bidder').distinct().count(),
'current_price': auction.current_price,
'time_remaining': auction.time_remaining,
'is_active': auction.is_active,
'highest_bidder': auction.highest_bid.bidder.username if auction.highest_bid else None
}

93
products/chat.py Normal file
View File

@ -0,0 +1,93 @@
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
class ChatSession(models.Model):
"""Chat-Session für Live-Support"""
STATUS_CHOICES = [
('active', 'Aktiv'),
('waiting', 'Wartend'),
('closed', 'Geschlossen'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
session_id = models.CharField(max_length=100, unique=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='waiting')
agent = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='chat_sessions')
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
closed = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created']
def __str__(self):
return f"Chat {self.session_id} - {self.status}"
class ChatMessage(models.Model):
"""Chat-Nachrichten"""
MESSAGE_TYPE_CHOICES = [
('user', 'Benutzer'),
('agent', 'Agent'),
('system', 'System'),
]
session = models.ForeignKey(ChatSession, on_delete=models.CASCADE, related_name='messages')
message_type = models.CharField(max_length=20, choices=MESSAGE_TYPE_CHOICES)
sender = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
content = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
is_read = models.BooleanField(default=False)
class Meta:
ordering = ['timestamp']
def __str__(self):
return f"{self.message_type}: {self.content[:50]}"
class ChatBot(models.Model):
"""Chatbot-Konfiguration"""
name = models.CharField(max_length=100)
is_active = models.BooleanField(default=True)
welcome_message = models.TextField()
auto_responses = models.JSONField(default=dict)
def __str__(self):
return self.name
class ChatBotResponse:
"""Chatbot-Antworten"""
@staticmethod
def get_welcome_message():
return {
'message': 'Willkommen beim Fursuit Shop! Wie kann ich Ihnen helfen?',
'options': [
'Produktinformationen',
'Bestellstatus',
'Custom Order',
'Zahlung & Versand',
'Mit Agent verbinden'
]
}
@staticmethod
def get_response_for_query(query):
"""Einfache Keyword-basierte Antworten"""
query_lower = query.lower()
responses = {
'versand': 'Wir versenden weltweit! Standardversand dauert 3-5 Werktage, Express 1-2 Werktage.',
'zahlung': 'Wir akzeptieren PayPal, Kreditkarte, SEPA-Lastschrift, Giropay und Sofort.',
'custom': 'Custom Orders nehmen 4-8 Wochen in Anspruch. Sie können Ihr Design hochladen und wir erstellen ein individuelles Angebot.',
'preis': 'Unsere Preise beginnen bei 200€ für einfache Teile. Vollständige Fursuits kosten 800-2000€ je nach Komplexität.',
'größe': 'Wir nehmen individuelle Maße für Custom Orders. Standardgrößen sind S, M, L, XL verfügbar.',
'material': 'Wir verwenden hochwertige Materialien: Faux Fur, Foam, Mesh für Belüftung und professionelle Elektronik.',
'pflege': 'Fursuits sollten trocken gelagert werden. Fell kann vorsichtig gebürstet werden. Elektronik vor Wasser schützen.'
}
for keyword, response in responses.items():
if keyword in query_lower:
return response
return 'Entschuldigung, ich verstehe Ihre Frage nicht. Möchten Sie mit einem Agenten sprechen?'

219
products/forms.py Normal file
View File

@ -0,0 +1,219 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, PasswordChangeForm
from django.contrib.auth.models import User
from .models import Order, Product, Review, UserProfile, ContactMessage, CustomOrder, OrderProgress
class OrderForm(forms.ModelForm):
class Meta:
model = Order
fields = ['full_name', 'email', 'address', 'phone']
widgets = {
'full_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'phone': forms.TextInput(attrs={'class': 'form-control'}),
}
labels = {
'full_name': 'Vollständiger Name',
'email': 'E-Mail-Adresse',
'address': 'Lieferadresse',
'phone': 'Telefonnummer',
}
class CustomUserCreationForm(UserCreationForm):
email = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'class': 'form-control'}))
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs['class'] = 'form-control'
self.fields['username'].label = 'Benutzername'
self.fields['password1'].label = 'Passwort'
self.fields['password2'].label = 'Passwort bestätigen'
class CustomAuthenticationForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs['class'] = 'form-control'
self.fields['username'].label = 'Benutzername'
self.fields['password'].label = 'Passwort'
class ReviewForm(forms.ModelForm):
class Meta:
model = Review
fields = ['rating', 'comment']
widgets = {
'rating': forms.Select(attrs={'class': 'form-control'}),
'comment': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
labels = {
'rating': 'Bewertung',
'comment': 'Kommentar',
}
class UserProfileForm(forms.ModelForm):
first_name = forms.CharField(max_length=30, required=False, label='Vorname')
last_name = forms.CharField(max_length=30, required=False, label='Nachname')
email = forms.EmailField(required=True, label='E-Mail-Adresse')
class Meta:
model = UserProfile
fields = ['phone', 'address', 'default_shipping_address', 'newsletter']
labels = {
'phone': 'Telefonnummer',
'address': 'Adresse',
'default_shipping_address': 'Standard-Lieferadresse',
'newsletter': 'Newsletter abonnieren',
}
widgets = {
'address': forms.Textarea(attrs={'rows': 3}),
'default_shipping_address': forms.Textarea(attrs={'rows': 3}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.user:
self.fields['first_name'].initial = self.instance.user.first_name
self.fields['last_name'].initial = self.instance.user.last_name
self.fields['email'].initial = self.instance.user.email
for field in self.fields:
self.fields[field].widget.attrs['class'] = 'form-control'
def save(self, commit=True):
profile = super().save(commit=False)
if commit:
user = profile.user
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
user.email = self.cleaned_data['email']
user.save()
profile.save()
return profile
class ContactForm(forms.ModelForm):
class Meta:
model = ContactMessage
fields = ['name', 'email', 'category', 'order_number', 'subject', 'message']
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'category': forms.Select(attrs={'class': 'form-control'}),
'order_number': forms.TextInput(attrs={'class': 'form-control'}),
'subject': forms.TextInput(attrs={'class': 'form-control'}),
'message': forms.Textarea(attrs={'class': 'form-control', 'rows': 5}),
}
labels = {
'name': 'Name',
'email': 'E-Mail-Adresse',
'category': 'Kategorie',
'order_number': 'Bestellnummer (optional)',
'subject': 'Betreff',
'message': 'Ihre Nachricht',
}
def __init__(self, *args, user=None, **kwargs):
super().__init__(*args, **kwargs)
if user and user.is_authenticated:
self.fields['name'].initial = user.get_full_name() or user.username
self.fields['email'].initial = user.email
class CustomOrderForm(forms.ModelForm):
class Meta:
model = CustomOrder
fields = [
'fursuit_type', 'style', 'character_name',
'character_description', 'reference_images',
'special_requests', 'measurements',
'color_preferences', 'budget_range',
'deadline_request'
]
widgets = {
'fursuit_type': forms.Select(attrs={'class': 'form-select'}),
'style': forms.Select(attrs={'class': 'form-select'}),
'character_name': forms.TextInput(attrs={'class': 'form-control'}),
'character_description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Beschreiben Sie Ihren Character so detailliert wie möglich...'
}),
'special_requests': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Besondere Wünsche oder Anforderungen...'
}),
'measurements': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Bitte geben Sie alle relevanten Maße an (Kopfumfang, Körpergröße, etc.)'
}),
'color_preferences': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Beschreiben Sie die gewünschten Farben und Farbkombinationen...'
}),
'budget_range': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'z.B. 2000-3000€'
}),
'deadline_request': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
'reference_images': forms.FileInput(attrs={
'class': 'form-control',
'accept': 'image/*'
})
}
labels = {
'fursuit_type': 'Art des Fursuits',
'style': 'Gewünschter Stil',
'character_name': 'Name des Characters',
'character_description': 'Beschreibung des Characters',
'reference_images': 'Referenzbilder',
'special_requests': 'Besondere Wünsche',
'measurements': 'Maße',
'color_preferences': 'Farbwünsche',
'budget_range': 'Budget-Rahmen',
'deadline_request': 'Gewünschter Fertigstellungstermin'
}
help_texts = {
'character_description': 'Je detaillierter die Beschreibung, desto besser können wir Ihre Vision umsetzen.',
'reference_images': 'Laden Sie bis zu 5 Referenzbilder hoch (max. 5MB pro Bild)',
'measurements': 'Genaue Maße sind wichtig für eine perfekte Passform.',
'budget_range': 'Geben Sie einen Bereich an, in dem Sie sich preislich bewegen möchten.',
'deadline_request': 'Optional: Wenn Sie einen bestimmten Termin im Auge haben.'
}
class OrderProgressForm(forms.ModelForm):
class Meta:
model = OrderProgress
fields = ['stage', 'description', 'image', 'completed']
widgets = {
'stage': forms.Select(attrs={'class': 'form-select'}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Beschreiben Sie den aktuellen Fortschritt...'
}),
'image': forms.FileInput(attrs={
'class': 'form-control',
'accept': 'image/*'
}),
'completed': forms.CheckboxInput(attrs={'class': 'form-check-input'})
}
labels = {
'stage': 'Arbeitsschritt',
'description': 'Beschreibung des Fortschritts',
'image': 'Foto des Fortschritts',
'completed': 'Abgeschlossen'
}
help_texts = {
'description': 'Beschreiben Sie detailliert, was in diesem Schritt gemacht wurde.',
'image': 'Fügen Sie ein Foto hinzu, um den Fortschritt zu dokumentieren.',
'completed': 'Markieren Sie diesen Schritt als abgeschlossen, wenn er fertig ist.'
}

View File

@ -0,0 +1,29 @@
# Generated by Django 5.2.1 on 2025-05-29 14:00
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Product',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('description', models.TextField()),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('stock', models.IntegerField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['-created_at'],
},
),
]

View File

@ -0,0 +1,39 @@
# Generated by Django 5.2.1 on 2025-05-29 14:12
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0001_initial'),
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')),
('session_id', models.CharField(blank=True, max_length=100, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 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)),
('created_at', models.DateTimeField(auto_now_add=True)),
('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='products.cart')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.product')),
],
options={
'unique_together': {('cart', 'product')},
},
),
]

View File

@ -0,0 +1,60 @@
# Generated by Django 5.2.1 on 2025-05-29 14:19
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0002_cart_cartitem'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='product',
name='category',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='product',
name='featured',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='product',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='products/'),
),
migrations.CreateModel(
name='Order',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('full_name', models.CharField(max_length=200)),
('email', models.EmailField(max_length=254)),
('address', models.TextField()),
('phone', models.CharField(max_length=20)),
('total_amount', models.DecimalField(decimal_places=2, max_digits=10)),
('status', models.CharField(choices=[('pending', 'Ausstehend'), ('processing', 'In Bearbeitung'), ('shipped', 'Versendet'), ('delivered', 'Geliefert'), ('cancelled', 'Storniert')], default='pending', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='OrderItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('product_name', models.CharField(max_length=200)),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('quantity', models.PositiveIntegerField()),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='products.order')),
('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='products.product')),
],
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 5.2.1 on 2025-05-29 14:25
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0003_product_category_product_featured_product_image_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Review',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('rating', models.IntegerField(choices=[(1, '1 - Sehr schlecht'), (2, '2 - Schlecht'), (3, '3 - Okay'), (4, '4 - Gut'), (5, '5 - Sehr gut')], validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)])),
('comment', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='products.product')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
'unique_together': {('product', 'user')},
},
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 5.2.1 on 2025-05-29 14:50
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0004_review'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('phone', models.CharField(blank=True, max_length=20)),
('address', models.TextField(blank=True)),
('default_shipping_address', models.TextField(blank=True)),
('newsletter', models.BooleanField(default=False)),
('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, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Wishlist',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('products', models.ManyToManyField(to='products.product')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,54 @@
# Generated by Django 5.2.1 on 2025-05-29 14:57
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0005_userprofile_wishlist'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='FAQ',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question', models.CharField(max_length=255, verbose_name='Frage')),
('answer', models.TextField(verbose_name='Antwort')),
('category', models.CharField(max_length=100, verbose_name='Kategorie')),
('order', models.IntegerField(default=0, verbose_name='Reihenfolge')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'FAQ',
'verbose_name_plural': 'FAQs',
'ordering': ['category', 'order'],
},
),
migrations.CreateModel(
name='ContactMessage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('email', models.EmailField(max_length=254)),
('order_number', models.CharField(blank=True, max_length=50, null=True)),
('category', models.CharField(choices=[('general', 'Allgemeine Anfrage'), ('order', 'Bestellung'), ('return', 'Rückgabe/Umtausch'), ('complaint', 'Beschwerde'), ('technical', 'Technische Frage')], max_length=20)),
('subject', models.CharField(max_length=200)),
('message', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('status', models.CharField(choices=[('new', 'Neu'), ('in_progress', 'In Bearbeitung'), ('resolved', 'Erledigt'), ('closed', 'Geschlossen')], default='new', max_length=20)),
('staff_notes', models.TextField(blank=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Kontaktanfrage',
'verbose_name_plural': 'Kontaktanfragen',
'ordering': ['-created_at'],
},
),
]

View File

@ -0,0 +1,87 @@
# Generated by Django 5.2.1 on 2025-05-29 15:01
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0006_faq_contactmessage'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='product',
name='extras_description',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='product',
name='fursuit_type',
field=models.CharField(choices=[('fullsuit', 'Fullsuit'), ('partial', 'Partial'), ('head', 'Kopf'), ('paws', 'Pfoten'), ('tail', 'Schwanz'), ('other', 'Sonstiges')], default='head', max_length=20),
),
migrations.AddField(
model_name='product',
name='includes_extras',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='product',
name='is_custom_order',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='product',
name='production_time_weeks',
field=models.IntegerField(default=8),
),
migrations.AddField(
model_name='product',
name='style',
field=models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistisch'), ('realistic', 'Realistisch')], default='toony', max_length=20),
),
migrations.AlterField(
model_name='product',
name='stock',
field=models.IntegerField(default=1),
),
migrations.CreateModel(
name='CustomOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('fursuit_type', models.CharField(choices=[('fullsuit', 'Fullsuit'), ('partial', 'Partial'), ('head', 'Kopf'), ('paws', 'Pfoten'), ('tail', 'Schwanz'), ('other', 'Sonstiges')], max_length=20)),
('style', models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistisch'), ('realistic', 'Realistisch')], max_length=20)),
('character_name', models.CharField(max_length=100)),
('character_description', models.TextField()),
('reference_images', models.FileField(blank=True, null=True, upload_to='references/')),
('special_requests', models.TextField(blank=True)),
('measurements', models.TextField()),
('color_preferences', models.TextField()),
('budget_range', models.CharField(max_length=100)),
('deadline_request', models.DateField(blank=True, null=True)),
('status', models.CharField(choices=[('pending', 'Anfrage eingegangen'), ('quoted', 'Angebot erstellt'), ('approved', 'Angebot akzeptiert'), ('in_progress', 'In Arbeit'), ('ready', 'Fertig zur Abholung'), ('shipped', 'Versendet'), ('completed', 'Abgeschlossen'), ('cancelled', 'Storniert')], default='pending', max_length=20)),
('quoted_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='OrderProgress',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stage', models.CharField(choices=[('design', 'Design & Planung'), ('base', 'Grundform'), ('fur', 'Fell'), ('details', 'Details'), ('electronics', 'Elektronik (optional)'), ('finishing', 'Finishing')], max_length=20)),
('description', models.TextField()),
('image', models.ImageField(blank=True, null=True, upload_to='progress/')),
('completed', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('custom_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='progress_updates', to='products.customorder')),
],
options={
'ordering': ['created_at'],
},
),
]

View File

@ -0,0 +1,145 @@
# Generated by Django 5.2.1 on 2025-05-29 18:29
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0007_product_extras_description_product_fursuit_type_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='contactmessage',
options={'ordering': ['-created'], 'verbose_name': 'Kontaktanfrage', 'verbose_name_plural': 'Kontaktanfragen'},
),
migrations.AlterModelOptions(
name='customorder',
options={'ordering': ['-created']},
),
migrations.AlterModelOptions(
name='order',
options={'ordering': ['-created']},
),
migrations.AlterModelOptions(
name='orderprogress',
options={'ordering': ['created']},
),
migrations.AlterModelOptions(
name='product',
options={'ordering': ['-created']},
),
migrations.AlterModelOptions(
name='review',
options={'ordering': ['-created']},
),
migrations.RenameField(
model_name='cart',
old_name='created_at',
new_name='created',
),
migrations.RenameField(
model_name='cart',
old_name='updated_at',
new_name='updated',
),
migrations.RenameField(
model_name='cartitem',
old_name='created_at',
new_name='created',
),
migrations.RenameField(
model_name='contactmessage',
old_name='created_at',
new_name='created',
),
migrations.RenameField(
model_name='customorder',
old_name='created_at',
new_name='created',
),
migrations.RenameField(
model_name='customorder',
old_name='updated_at',
new_name='updated',
),
migrations.RenameField(
model_name='faq',
old_name='created_at',
new_name='created',
),
migrations.RenameField(
model_name='faq',
old_name='updated_at',
new_name='updated',
),
migrations.RenameField(
model_name='order',
old_name='created_at',
new_name='created',
),
migrations.RenameField(
model_name='order',
old_name='updated_at',
new_name='updated',
),
migrations.RenameField(
model_name='orderprogress',
old_name='created_at',
new_name='created',
),
migrations.RenameField(
model_name='product',
old_name='created_at',
new_name='created',
),
migrations.RenameField(
model_name='product',
old_name='updated_at',
new_name='updated',
),
migrations.RenameField(
model_name='review',
old_name='created_at',
new_name='created',
),
migrations.RenameField(
model_name='review',
old_name='updated_at',
new_name='updated',
),
migrations.RenameField(
model_name='userprofile',
old_name='created_at',
new_name='created',
),
migrations.RenameField(
model_name='userprofile',
old_name='updated_at',
new_name='updated',
),
migrations.RenameField(
model_name='wishlist',
old_name='created_at',
new_name='created',
),
migrations.AddField(
model_name='orderprogress',
name='updated',
field=models.DateTimeField(auto_now=True),
),
migrations.AlterField(
model_name='cart',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='product_carts', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='order',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_orders', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -0,0 +1,147 @@
# Generated by Django 5.2.1 on 2025-05-30 07:31
import django.core.validators
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0008_alter_contactmessage_options_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='product',
name='category',
),
migrations.RemoveField(
model_name='product',
name='extras_description',
),
migrations.RemoveField(
model_name='product',
name='featured',
),
migrations.RemoveField(
model_name='product',
name='includes_extras',
),
migrations.RemoveField(
model_name='product',
name='production_time_weeks',
),
migrations.RemoveField(
model_name='review',
name='updated',
),
migrations.AddField(
model_name='order',
name='payment_date',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='order',
name='payment_method',
field=models.CharField(blank=True, choices=[('card', 'Kreditkarte'), ('sepa', 'SEPA-Lastschrift'), ('giropay', 'Giropay'), ('sofort', 'Sofort'), ('bancontact', 'Bancontact')], max_length=20, null=True),
),
migrations.AddField(
model_name='order',
name='payment_status',
field=models.CharField(choices=[('pending', 'Ausstehend'), ('processing', 'Wird bearbeitet'), ('paid', 'Bezahlt'), ('failed', 'Fehlgeschlagen'), ('refunded', 'Zurückerstattet')], default='pending', max_length=20),
),
migrations.AddField(
model_name='order',
name='stripe_payment_intent_id',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='order',
name='stripe_payment_method_id',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='product',
name='is_featured',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='product',
name='wishlist_users',
field=models.ManyToManyField(blank=True, related_name='wishlist_products', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='customorder',
name='fursuit_type',
field=models.CharField(choices=[('partial', 'Partial'), ('fullsuit', 'Fullsuit'), ('head', 'Head Only'), ('paws', 'Paws'), ('tail', 'Tail'), ('other', 'Other')], max_length=20),
),
migrations.AlterField(
model_name='customorder',
name='style',
field=models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistic'), ('realistic', 'Realistic'), ('anime', 'Anime'), ('chibi', 'Chibi')], max_length=20),
),
migrations.AlterField(
model_name='product',
name='fursuit_type',
field=models.CharField(choices=[('partial', 'Partial'), ('fullsuit', 'Fullsuit'), ('head', 'Head Only'), ('paws', 'Paws'), ('tail', 'Tail'), ('other', 'Other')], default='other', max_length=20),
),
migrations.AlterField(
model_name='product',
name='is_custom_order',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='product',
name='price',
field=models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]),
),
migrations.AlterField(
model_name='product',
name='stock',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='product',
name='style',
field=models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistic'), ('realistic', 'Realistic'), ('anime', 'Anime'), ('chibi', 'Chibi')], default='toony', max_length=20),
),
migrations.AlterField(
model_name='review',
name='rating',
field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1)]),
),
migrations.AddIndex(
model_name='order',
index=models.Index(fields=['status'], name='products_or_status_bd22a2_idx'),
),
migrations.AddIndex(
model_name='order',
index=models.Index(fields=['payment_status'], name='products_or_payment_0d94df_idx'),
),
migrations.AddIndex(
model_name='order',
index=models.Index(fields=['created'], name='products_or_created_a2e72d_idx'),
),
migrations.AddIndex(
model_name='product',
index=models.Index(fields=['name'], name='products_pr_name_9ff0a3_idx'),
),
migrations.AddIndex(
model_name='product',
index=models.Index(fields=['price'], name='products_pr_price_9b1a5f_idx'),
),
migrations.AddIndex(
model_name='product',
index=models.Index(fields=['created'], name='products_pr_created_9a1943_idx'),
),
migrations.AddIndex(
model_name='product',
index=models.Index(fields=['fursuit_type'], name='products_pr_fursuit_fde435_idx'),
),
migrations.AddIndex(
model_name='product',
index=models.Index(fields=['style'], name='products_pr_style_de3c68_idx'),
),
]

View File

@ -0,0 +1,32 @@
# Generated by Django 5.2.1 on 2025-05-30 07:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0009_remove_product_category_and_more'),
]
operations = [
migrations.CreateModel(
name='GalleryImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200, verbose_name='Titel')),
('description', models.TextField(blank=True, verbose_name='Beschreibung')),
('image', models.ImageField(upload_to='gallery/', verbose_name='Bild')),
('fursuit_type', models.CharField(choices=[('partial', 'Partial'), ('fullsuit', 'Fullsuit'), ('head', 'Head Only'), ('paws', 'Paws'), ('tail', 'Tail'), ('other', 'Other')], max_length=20, verbose_name='Fursuit-Typ')),
('style', models.CharField(choices=[('toony', 'Toony'), ('semi_realistic', 'Semi-Realistic'), ('realistic', 'Realistic'), ('anime', 'Anime'), ('chibi', 'Chibi')], max_length=20, verbose_name='Stil')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
('is_featured', models.BooleanField(default=False, verbose_name='Hervorgehoben')),
('order', models.IntegerField(default=0, verbose_name='Reihenfolge')),
],
options={
'verbose_name': 'Galeriebild',
'verbose_name_plural': 'Galeriebilder',
'ordering': ['order', '-created'],
},
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.1 on 2025-05-30 08:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0010_galleryimage'),
]
operations = [
migrations.AlterField(
model_name='galleryimage',
name='fursuit_type',
field=models.CharField(choices=[('full', 'Fullsuit'), ('partial', 'Partial'), ('head', 'Head Only'), ('other', 'Other')], default='full', max_length=20, verbose_name='Fursuit-Typ'),
),
migrations.AlterField(
model_name='galleryimage',
name='style',
field=models.CharField(choices=[('toony', 'Toony'), ('semi', 'Semi-Realistic'), ('real', 'Realistic'), ('anime', 'Anime')], default='toony', max_length=20, verbose_name='Stil'),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 5.2.1 on 2025-05-30 11:51
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0011_alter_galleryimage_fursuit_type_and_more'),
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Name')),
('slug', models.SlugField(unique=True, verbose_name='URL-Slug')),
('description', models.TextField(blank=True, verbose_name='Beschreibung')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Kategorie',
'verbose_name_plural': 'Kategorien',
'ordering': ['name'],
},
),
migrations.AddField(
model_name='product',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='products.category', verbose_name='Kategorie'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.1 on 2025-05-30 11:54
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0012_category_product_category'),
('shop', '0003_contactmessage'),
]
operations = [
migrations.AlterField(
model_name='product',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_items', to='shop.category', verbose_name='Kategorie'),
),
migrations.DeleteModel(
name='Category',
),
]

View File

@ -0,0 +1,52 @@
# Generated by Django 5.2.1 on 2025-06-02 06:12
import django.db.models.deletion
import phonenumber_field.modelfields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0013_alter_product_category_delete_category'),
]
operations = [
migrations.CreateModel(
name='Payment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('variant', models.CharField(max_length=255)),
('status', models.CharField(choices=[('waiting', 'Waiting for confirmation'), ('preauth', 'Pre-authorized'), ('confirmed', 'Confirmed'), ('rejected', 'Rejected'), ('refunded', 'Refunded'), ('error', 'Error'), ('input', 'Input')], default='waiting', max_length=10)),
('fraud_status', models.CharField(choices=[('unknown', 'Unknown'), ('accept', 'Passed'), ('reject', 'Rejected'), ('review', 'Review')], default='unknown', max_length=10, verbose_name='fraud check')),
('fraud_message', models.TextField(blank=True, default='')),
('created', models.DateTimeField(auto_now_add=True)),
('modified', models.DateTimeField(auto_now=True)),
('transaction_id', models.CharField(blank=True, max_length=255)),
('currency', models.CharField(max_length=10)),
('total', models.DecimalField(decimal_places=2, default='0.0', max_digits=9)),
('delivery', models.DecimalField(decimal_places=2, default='0.0', max_digits=9)),
('tax', models.DecimalField(decimal_places=2, default='0.0', max_digits=9)),
('description', models.TextField(blank=True, default='')),
('billing_first_name', models.CharField(blank=True, max_length=256)),
('billing_last_name', models.CharField(blank=True, max_length=256)),
('billing_address_1', models.CharField(blank=True, max_length=256)),
('billing_address_2', models.CharField(blank=True, max_length=256)),
('billing_city', models.CharField(blank=True, max_length=256)),
('billing_postcode', models.CharField(blank=True, max_length=256)),
('billing_country_code', models.CharField(blank=True, max_length=2)),
('billing_country_area', models.CharField(blank=True, max_length=256)),
('billing_email', models.EmailField(blank=True, max_length=254)),
('billing_phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None)),
('customer_ip_address', models.GenericIPAddressField(blank=True, null=True)),
('extra_data', models.TextField(blank=True, default='')),
('message', models.TextField(blank=True, default='')),
('token', models.CharField(blank=True, default='', max_length=36)),
('captured_amount', models.DecimalField(decimal_places=2, default='0.0', max_digits=9)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='products.order')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,112 @@
# Generated by Django 5.2.1 on 2025-06-02 10:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0014_payment'),
]
operations = [
migrations.AlterModelOptions(
name='payment',
options={'ordering': ['-created']},
),
migrations.RemoveField(
model_name='payment',
name='billing_phone',
),
migrations.AlterField(
model_name='payment',
name='billing_address_1',
field=models.CharField(blank=True, max_length=256, null=True),
),
migrations.AlterField(
model_name='payment',
name='billing_address_2',
field=models.CharField(blank=True, max_length=256, null=True),
),
migrations.AlterField(
model_name='payment',
name='billing_city',
field=models.CharField(blank=True, max_length=256, null=True),
),
migrations.AlterField(
model_name='payment',
name='billing_country_area',
field=models.CharField(blank=True, max_length=256, null=True),
),
migrations.AlterField(
model_name='payment',
name='billing_country_code',
field=models.CharField(blank=True, max_length=2, null=True),
),
migrations.AlterField(
model_name='payment',
name='billing_email',
field=models.EmailField(blank=True, max_length=254, null=True),
),
migrations.AlterField(
model_name='payment',
name='billing_first_name',
field=models.CharField(blank=True, max_length=256, null=True),
),
migrations.AlterField(
model_name='payment',
name='billing_last_name',
field=models.CharField(blank=True, max_length=256, null=True),
),
migrations.AlterField(
model_name='payment',
name='billing_postcode',
field=models.CharField(blank=True, max_length=256, null=True),
),
migrations.AlterField(
model_name='payment',
name='description',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='payment',
name='extra_data',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='payment',
name='fraud_message',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='payment',
name='fraud_status',
field=models.CharField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='payment',
name='message',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='payment',
name='order',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.order'),
),
migrations.AlterField(
model_name='payment',
name='status',
field=models.CharField(max_length=10),
),
migrations.AlterField(
model_name='payment',
name='token',
field=models.CharField(blank=True, max_length=36, null=True),
),
migrations.AlterField(
model_name='payment',
name='transaction_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

428
products/models.py Normal file
View File

@ -0,0 +1,428 @@
from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db.models.signals import post_save
from django.dispatch import receiver
from shop.models import Category # Import Category aus der shop App
from typing import Optional, List
from decimal import Decimal
# Create your models here.
class UserProfile(models.Model):
"""Erweitertes Benutzerprofil mit zusätzlichen Informationen"""
user: User = models.OneToOneField(User, on_delete=models.CASCADE)
phone: str = models.CharField(max_length=20, blank=True)
address: str = models.TextField(blank=True)
default_shipping_address: str = models.TextField(blank=True)
newsletter: bool = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self) -> str:
return f"Profil von {self.user.username}"
class Meta:
verbose_name = 'Benutzerprofil'
verbose_name_plural = 'Benutzerprofile'
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.userprofile.save()
class Product(models.Model):
FURSUIT_TYPE_CHOICES = [
('partial', 'Partial'),
('fullsuit', 'Fullsuit'),
('head', 'Head Only'),
('paws', 'Paws'),
('tail', 'Tail'),
('other', 'Other'),
]
STYLE_CHOICES = [
('toony', 'Toony'),
('semi_realistic', 'Semi-Realistic'),
('realistic', 'Realistic'),
('anime', 'Anime'),
('chibi', 'Chibi'),
]
name = models.CharField(max_length=200)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(0)])
stock = models.IntegerField(default=0)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
image = models.ImageField(upload_to='products/', null=True, blank=True)
# Neue Felder
fursuit_type = models.CharField(max_length=20, choices=FURSUIT_TYPE_CHOICES, default='other')
style = models.CharField(max_length=20, choices=STYLE_CHOICES, default='toony')
is_featured = models.BooleanField(default=False)
is_custom_order = models.BooleanField(default=False)
# Beziehungen
category = models.ForeignKey('shop.Category', on_delete=models.SET_NULL, null=True, blank=True, related_name='product_items', verbose_name='Kategorie')
wishlist_users = models.ManyToManyField(User, related_name='wishlist_products', blank=True)
def __str__(self):
return self.name
def average_rating(self):
if self.reviews.exists():
return self.reviews.aggregate(models.Avg('rating'))['rating__avg']
return 0
class Meta:
ordering = ['-created']
indexes = [
models.Index(fields=['name']),
models.Index(fields=['price']),
models.Index(fields=['created']),
models.Index(fields=['fursuit_type']),
models.Index(fields=['style']),
models.Index(fields=['is_featured']),
models.Index(fields=['stock']),
models.Index(fields=['category', 'fursuit_type']),
models.Index(fields=['price', 'fursuit_type']),
]
class Review(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='reviews')
user = models.ForeignKey(User, on_delete=models.CASCADE)
rating = models.IntegerField(validators=[MinValueValidator(1)])
comment = models.TextField()
created = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('product', 'user')
ordering = ['-created']
def __str__(self):
return f'Review by {self.user.username} for {self.product.name}'
class Cart(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name='product_carts')
session_id = models.CharField(max_length=100, null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def get_total(self):
return sum(item.get_subtotal() for item in self.items.all())
def __str__(self):
return f"Cart {self.id} - {'User: ' + self.user.username if self.user else 'Session: ' + self.session_id}"
class CartItem(models.Model):
cart = models.ForeignKey(Cart, related_name='items', on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1)
created = models.DateTimeField(auto_now_add=True)
def get_subtotal(self):
return self.product.price * self.quantity
def __str__(self):
return f"{self.quantity}x {self.product.name} in Cart {self.cart.id}"
class Meta:
unique_together = ('cart', 'product')
class Order(models.Model):
STATUS_CHOICES = [
('pending', 'Ausstehend'),
('processing', 'In Bearbeitung'),
('shipped', 'Versendet'),
('delivered', 'Geliefert'),
('cancelled', 'Storniert'),
]
PAYMENT_STATUS_CHOICES = [
('pending', 'Ausstehend'),
('processing', 'Wird bearbeitet'),
('paid', 'Bezahlt'),
('failed', 'Fehlgeschlagen'),
('refunded', 'Zurückerstattet'),
]
PAYMENT_METHOD_CHOICES = [
('card', 'Kreditkarte'),
('sepa', 'SEPA-Lastschrift'),
('giropay', 'Giropay'),
('sofort', 'Sofort'),
('bancontact', 'Bancontact'),
]
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='product_orders')
full_name = models.CharField(max_length=200)
email = models.EmailField()
address = models.TextField()
phone = models.CharField(max_length=20)
total_amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
# Zahlungsinformationen
payment_status = models.CharField(max_length=20, choices=PAYMENT_STATUS_CHOICES, default='pending')
payment_method = models.CharField(max_length=20, choices=PAYMENT_METHOD_CHOICES, null=True, blank=True)
stripe_payment_intent_id = models.CharField(max_length=100, null=True, blank=True)
stripe_payment_method_id = models.CharField(max_length=100, null=True, blank=True)
payment_date = models.DateTimeField(null=True, blank=True)
def __str__(self):
return f"Bestellung #{self.id} von {self.full_name}"
class Meta:
ordering = ['-created']
indexes = [
models.Index(fields=['status']),
models.Index(fields=['payment_status']),
models.Index(fields=['created']),
]
def get_payment_status_display_class(self):
status_classes = {
'pending': 'warning',
'processing': 'info',
'paid': 'success',
'failed': 'danger',
'refunded': 'secondary',
}
return status_classes.get(self.payment_status, 'secondary')
def get_order_status_display_class(self):
status_classes = {
'pending': 'warning',
'processing': 'info',
'shipped': 'primary',
'delivered': 'success',
'cancelled': 'danger',
}
return status_classes.get(self.status, 'secondary')
class OrderItem(models.Model):
order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True)
product_name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.PositiveIntegerField()
def get_subtotal(self):
return self.price * self.quantity
def __str__(self):
return f"{self.quantity}x {self.product_name} in Bestellung #{self.order.id}"
class Wishlist(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
products = models.ManyToManyField(Product)
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Wunschliste von {self.user.username}"
class FAQ(models.Model):
question = models.CharField(max_length=255, verbose_name='Frage')
answer = models.TextField(verbose_name='Antwort')
category = models.CharField(max_length=100, verbose_name='Kategorie')
order = models.IntegerField(default=0, verbose_name='Reihenfolge')
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['category', 'order']
verbose_name = 'FAQ'
verbose_name_plural = 'FAQs'
def __str__(self):
return self.question
class ContactMessage(models.Model):
CATEGORY_CHOICES = [
('general', 'Allgemeine Anfrage'),
('order', 'Bestellung'),
('return', 'Rückgabe/Umtausch'),
('complaint', 'Beschwerde'),
('technical', 'Technische Frage'),
]
name = models.CharField(max_length=100)
email = models.EmailField()
order_number = models.CharField(max_length=50, blank=True, null=True)
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)
subject = models.CharField(max_length=200)
message = models.TextField()
created = models.DateTimeField(auto_now_add=True)
status = models.CharField(
max_length=20,
choices=[
('new', 'Neu'),
('in_progress', 'In Bearbeitung'),
('resolved', 'Erledigt'),
('closed', 'Geschlossen'),
],
default='new'
)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
staff_notes = models.TextField(blank=True)
class Meta:
ordering = ['-created']
verbose_name = 'Kontaktanfrage'
verbose_name_plural = 'Kontaktanfragen'
def __str__(self):
return f"{self.category} - {self.subject} ({self.created.strftime('%d.%m.%Y')})"
class CustomOrder(models.Model):
STATUS_CHOICES = [
('pending', 'Anfrage eingegangen'),
('quoted', 'Angebot erstellt'),
('approved', 'Angebot akzeptiert'),
('in_progress', 'In Arbeit'),
('ready', 'Fertig zur Abholung'),
('shipped', 'Versendet'),
('completed', 'Abgeschlossen'),
('cancelled', 'Storniert'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE)
fursuit_type = models.CharField(max_length=20, choices=Product.FURSUIT_TYPE_CHOICES)
style = models.CharField(max_length=20, choices=Product.STYLE_CHOICES)
character_name = models.CharField(max_length=100)
character_description = models.TextField()
reference_images = models.FileField(upload_to='references/', null=True, blank=True)
special_requests = models.TextField(blank=True)
measurements = models.TextField()
color_preferences = models.TextField()
budget_range = models.CharField(max_length=100)
deadline_request = models.DateField(null=True, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
quoted_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Custom Order #{self.id} - {self.character_name}"
class Meta:
ordering = ['-created']
class OrderProgress(models.Model):
PROGRESS_CHOICES = [
('design', 'Design & Planung'),
('base', 'Grundform'),
('fur', 'Fell'),
('details', 'Details'),
('electronics', 'Elektronik (optional)'),
('finishing', 'Finishing'),
]
custom_order = models.ForeignKey(CustomOrder, on_delete=models.CASCADE, related_name='progress_updates')
stage = models.CharField(max_length=20, choices=PROGRESS_CHOICES)
description = models.TextField()
image = models.ImageField(upload_to='progress/', null=True, blank=True)
completed = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.custom_order.character_name} - {self.get_stage_display()}"
class Meta:
ordering = ['created']
class GalleryImage(models.Model):
FURSUIT_TYPE_CHOICES = [
('full', 'Fullsuit'),
('partial', 'Partial'),
('head', 'Head Only'),
('other', 'Other'),
]
STYLE_CHOICES = [
('toony', 'Toony'),
('semi', 'Semi-Realistic'),
('real', 'Realistic'),
('anime', 'Anime'),
]
title = models.CharField(max_length=200, verbose_name='Titel')
description = models.TextField(blank=True, verbose_name='Beschreibung')
image = models.ImageField(upload_to='gallery/', verbose_name='Bild')
fursuit_type = models.CharField(
max_length=20,
choices=FURSUIT_TYPE_CHOICES,
default='full',
verbose_name='Fursuit-Typ'
)
style = models.CharField(
max_length=20,
choices=STYLE_CHOICES,
default='toony',
verbose_name='Stil'
)
is_featured = models.BooleanField(default=False, verbose_name='Hervorgehoben')
order = models.IntegerField(default=0, verbose_name='Reihenfolge')
created = models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')
class Meta:
verbose_name = 'Galeriebild'
verbose_name_plural = 'Galeriebilder'
ordering = ['order', '-created']
def __str__(self):
return self.title
class Payment(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
variant = models.CharField(max_length=255)
status = models.CharField(max_length=10)
fraud_status = models.CharField(max_length=10, null=True, blank=True)
fraud_message = models.TextField(null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
transaction_id = models.CharField(max_length=255, null=True, blank=True)
currency = models.CharField(max_length=10)
total = models.DecimalField(max_digits=9, decimal_places=2, default='0.0')
delivery = models.DecimalField(max_digits=9, decimal_places=2, default='0.0')
tax = models.DecimalField(max_digits=9, decimal_places=2, default='0.0')
description = models.TextField(null=True, blank=True)
billing_first_name = models.CharField(max_length=256, null=True, blank=True)
billing_last_name = models.CharField(max_length=256, null=True, blank=True)
billing_address_1 = models.CharField(max_length=256, null=True, blank=True)
billing_address_2 = models.CharField(max_length=256, null=True, blank=True)
billing_city = models.CharField(max_length=256, null=True, blank=True)
billing_postcode = models.CharField(max_length=256, null=True, blank=True)
billing_country_code = models.CharField(max_length=2, null=True, blank=True)
billing_country_area = models.CharField(max_length=256, null=True, blank=True)
billing_email = models.EmailField(null=True, blank=True)
customer_ip_address = models.GenericIPAddressField(null=True, blank=True)
extra_data = models.TextField(null=True, blank=True)
message = models.TextField(null=True, blank=True)
token = models.CharField(max_length=36, null=True, blank=True)
captured_amount = models.DecimalField(max_digits=9, decimal_places=2, default='0.0')
class Meta:
ordering = ['-created']
def __str__(self):
return f"Payment {self.id} for Order {self.order.id}"
@property
def attrs(self):
return {}
def get_failure_url(self):
return f'/payment/failed/{self.order.id}/'
def get_success_url(self):
return f'/payment/success/{self.order.id}/'

341
products/seo.py Normal file
View File

@ -0,0 +1,341 @@
"""
SEO Module für Kasico Fursuit Shop
Meta Tags, Structured Data, Sitemap Generation
"""
from django.utils.html import strip_tags
from django.template.loader import render_to_string
from django.contrib.sites.models import Site
from django.urls import reverse
from django.utils import timezone
import json
class SEOManager:
"""SEO Manager für Meta Tags und Structured Data"""
def __init__(self, request=None):
self.request = request
self.site = Site.objects.get_current()
self.base_url = f"https://{self.site.domain}"
def get_product_meta_tags(self, product):
"""Meta Tags für Produkt-Seiten"""
meta_tags = {
'title': f"{product.name} - Kasico Fursuit Shop",
'description': self._clean_description(product.description),
'keywords': f"fursuit, {product.fursuit_type}, {product.style}, custom fursuit, furry",
'og_title': product.name,
'og_description': self._clean_description(product.description),
'og_type': 'product',
'og_image': self._get_product_image_url(product),
'og_url': f"{self.base_url}{product.get_absolute_url()}",
'twitter_card': 'summary_large_image',
'twitter_title': product.name,
'twitter_description': self._clean_description(product.description),
'twitter_image': self._get_product_image_url(product),
'canonical_url': f"{self.base_url}{product.get_absolute_url()}",
}
# Preis-Informationen für E-Commerce
if product.on_sale:
meta_tags['price'] = str(product.sale_price)
meta_tags['original_price'] = str(product.base_price)
else:
meta_tags['price'] = str(product.base_price)
return meta_tags
def get_category_meta_tags(self, category):
"""Meta Tags für Kategorie-Seiten"""
meta_tags = {
'title': f"{category.name} Fursuits - Kasico Shop",
'description': f"Entdecke unsere {category.name} Fursuits. Handgefertigte, hochwertige Fursuits in verschiedenen Stilen und Größen.",
'keywords': f"{category.name}, fursuit, furry, custom fursuit",
'og_title': f"{category.name} Fursuits",
'og_description': f"Entdecke unsere {category.name} Fursuits bei Kasico",
'og_type': 'website',
'og_url': f"{self.base_url}{category.get_absolute_url()}",
'canonical_url': f"{self.base_url}{category.get_absolute_url()}",
}
return meta_tags
def get_homepage_meta_tags(self):
"""Meta Tags für Homepage"""
meta_tags = {
'title': 'Kasico - Premium Fursuit Shop | Handgefertigte Fursuits',
'description': 'Entdecke handgefertigte, hochwertige Fursuits bei Kasico. Custom Fursuits, Fullsuits, Headsets und mehr. Made in Germany.',
'keywords': 'fursuit, furry, custom fursuit, fullsuit, headset, handgefertigt, germany',
'og_title': 'Kasico - Premium Fursuit Shop',
'og_description': 'Handgefertigte, hochwertige Fursuits made in Germany',
'og_type': 'website',
'og_url': self.base_url,
'canonical_url': self.base_url,
}
return meta_tags
def get_structured_data_product(self, product):
"""Structured Data für Produkte (JSON-LD)"""
structured_data = {
"@context": "https://schema.org/",
"@type": "Product",
"name": product.name,
"description": self._clean_description(product.description),
"image": self._get_product_image_url(product),
"url": f"{self.base_url}{product.get_absolute_url()}",
"brand": {
"@type": "Brand",
"name": "Kasico"
},
"offers": {
"@type": "Offer",
"price": str(product.sale_price if product.on_sale else product.base_price),
"priceCurrency": "EUR",
"availability": "https://schema.org/InStock" if product.stock > 0 else "https://schema.org/OutOfStock",
"seller": {
"@type": "Organization",
"name": "Kasico"
}
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": product.average_rating,
"reviewCount": product.reviews.count()
}
}
if product.on_sale:
structured_data["offers"]["priceSpecification"] = {
"@type": "PriceSpecification",
"price": str(product.sale_price),
"priceCurrency": "EUR",
"valueAddedTaxIncluded": True
}
return structured_data
def get_structured_data_organization(self):
"""Structured Data für Organisation"""
structured_data = {
"@context": "https://schema.org",
"@type": "Organization",
"name": "Kasico",
"url": self.base_url,
"logo": f"{self.base_url}/static/images/kasico-logo.svg",
"description": "Premium Fursuit Shop - Handgefertigte Fursuits made in Germany",
"address": {
"@type": "PostalAddress",
"addressCountry": "DE",
"addressLocality": "Germany"
},
"contactPoint": {
"@type": "ContactPoint",
"contactType": "customer service",
"email": "info@kasico.de"
},
"sameAs": [
"https://twitter.com/kasico_fursuits",
"https://www.instagram.com/kasico_fursuits",
"https://www.facebook.com/kasico.fursuits"
]
}
return structured_data
def get_structured_data_website(self):
"""Structured Data für Website"""
structured_data = {
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Kasico Fursuit Shop",
"url": self.base_url,
"description": "Premium Fursuit Shop - Handgefertigte Fursuits made in Germany",
"potentialAction": {
"@type": "SearchAction",
"target": f"{self.base_url}/products/search/?q={{search_term_string}}",
"query-input": "required name=search_term_string"
}
}
return structured_data
def get_structured_data_breadcrumb(self, breadcrumbs):
"""Structured Data für Breadcrumbs"""
structured_data = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": []
}
for i, (name, url) in enumerate(breadcrumbs):
structured_data["itemListElement"].append({
"@type": "ListItem",
"position": i + 1,
"name": name,
"item": f"{self.base_url}{url}"
})
return structured_data
def generate_sitemap_xml(self):
"""XML Sitemap generieren"""
from products.models import Product, Category
from django.contrib.sitemaps import Sitemap
sitemap_data = {
'urlset': {
'@xmlns': 'http://www.sitemaps.org/schemas/sitemap/0.9',
'url': []
}
}
# Homepage
sitemap_data['urlset']['url'].append({
'loc': self.base_url,
'lastmod': timezone.now().strftime('%Y-%m-%d'),
'changefreq': 'daily',
'priority': '1.0'
})
# Produkt-Seiten
for product in Product.objects.filter(is_active=True):
sitemap_data['urlset']['url'].append({
'loc': f"{self.base_url}{product.get_absolute_url()}",
'lastmod': product.updated_at.strftime('%Y-%m-%d'),
'changefreq': 'weekly',
'priority': '0.8'
})
# Kategorie-Seiten
for category in Category.objects.all():
sitemap_data['urlset']['url'].append({
'loc': f"{self.base_url}{category.get_absolute_url()}",
'lastmod': timezone.now().strftime('%Y-%m-%d'),
'changefreq': 'weekly',
'priority': '0.7'
})
# Statische Seiten
static_pages = [
('/products/', 'weekly', '0.8'),
('/gallery/', 'weekly', '0.7'),
('/contact/', 'monthly', '0.5'),
('/about/', 'monthly', '0.5'),
('/faq/', 'monthly', '0.6'),
]
for url, changefreq, priority in static_pages:
sitemap_data['urlset']['url'].append({
'loc': f"{self.base_url}{url}",
'lastmod': timezone.now().strftime('%Y-%m-%d'),
'changefreq': changefreq,
'priority': priority
})
return sitemap_data
def generate_robots_txt(self):
"""Robots.txt generieren"""
robots_content = f"""User-agent: *
Allow: /
# Sitemap
Sitemap: {self.base_url}/sitemap.xml
# Disallow admin and private areas
Disallow: /admin/
Disallow: /api/
Disallow: /static/admin/
Disallow: /media/admin/
# Allow important pages
Allow: /products/
Allow: /gallery/
Allow: /contact/
Allow: /about/
Allow: /faq/
# Crawl delay
Crawl-delay: 1
"""
return robots_content
def _clean_description(self, text, max_length=160):
"""Beschreibung für Meta Tags bereinigen"""
if not text:
return ""
# HTML Tags entfernen
clean_text = strip_tags(text)
# Zu lang kürzen
if len(clean_text) > max_length:
clean_text = clean_text[:max_length-3] + "..."
return clean_text
def _get_product_image_url(self, product):
"""Produkt-Bild URL generieren"""
if product.image:
return f"{self.base_url}{product.image.url}"
return f"{self.base_url}/static/images/default-product.jpg"
class SEOTemplateTags:
"""Template Tags für SEO"""
@staticmethod
def render_meta_tags(meta_tags):
"""Meta Tags als HTML rendern"""
return render_to_string('seo/meta_tags.html', {'meta_tags': meta_tags})
@staticmethod
def render_structured_data(structured_data):
"""Structured Data als JSON-LD rendern"""
return render_to_string('seo/structured_data.html', {
'structured_data': json.dumps(structured_data, indent=2)
})
@staticmethod
def render_social_meta_tags(meta_tags):
"""Social Media Meta Tags rendern"""
return render_to_string('seo/social_meta_tags.html', {'meta_tags': meta_tags})
# SEO Context Processor
def seo_context_processor(request):
"""SEO Context für alle Templates"""
seo_manager = SEOManager(request)
# Standard SEO für alle Seiten
context = {
'seo_manager': seo_manager,
'site_name': 'Kasico',
'site_description': 'Premium Fursuit Shop - Handgefertigte Fursuits made in Germany',
'site_keywords': 'fursuit, furry, custom fursuit, fullsuit, headset, handgefertigt, germany',
}
# Page-spezifische SEO
if hasattr(request, 'resolver_match'):
view_name = request.resolver_match.view_name
if 'product_detail' in view_name:
product = getattr(request, 'product', None)
if product:
context.update({
'meta_tags': seo_manager.get_product_meta_tags(product),
'structured_data': seo_manager.get_structured_data_product(product)
})
elif 'category_detail' in view_name:
category = getattr(request, 'category', None)
if category:
context.update({
'meta_tags': seo_manager.get_category_meta_tags(category)
})
elif 'home' in view_name:
context.update({
'meta_tags': seo_manager.get_homepage_meta_tags(),
'structured_data': seo_manager.get_structured_data_website()
})
return context

214
products/serializers.py Normal file
View File

@ -0,0 +1,214 @@
from rest_framework import serializers
from .models import Product, Category, Review, Wishlist, GalleryImage, CustomOrder, Payment
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
"""Serializer für User-Model"""
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name']
read_only_fields = ['id']
class CategorySerializer(serializers.ModelSerializer):
"""Serializer für Category-Model"""
product_count = serializers.SerializerMethodField()
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'description', 'image', 'product_count']
def get_product_count(self, obj):
return obj.products.count()
class ReviewSerializer(serializers.ModelSerializer):
"""Serializer für Review-Model"""
user = UserSerializer(read_only=True)
user_name = serializers.CharField(source='user.username', read_only=True)
class Meta:
model = Review
fields = ['id', 'product', 'user', 'user_name', 'rating', 'comment', 'created_at']
read_only_fields = ['id', 'created_at']
def create(self, validated_data):
validated_data['user'] = self.context['request'].user
return super().create(validated_data)
class GalleryImageSerializer(serializers.ModelSerializer):
"""Serializer für GalleryImage-Model"""
class Meta:
model = GalleryImage
fields = ['id', 'product', 'image', 'alt_text', 'is_featured', 'created_at']
read_only_fields = ['id', 'created_at']
class ProductSerializer(serializers.ModelSerializer):
"""Basis-Serializer für Product-Model"""
category = CategorySerializer(read_only=True)
category_id = serializers.IntegerField(write_only=True, required=False)
reviews = ReviewSerializer(many=True, read_only=True)
gallery_images = GalleryImageSerializer(many=True, read_only=True)
average_rating = serializers.FloatField(read_only=True)
review_count = serializers.IntegerField(read_only=True)
is_in_wishlist = serializers.SerializerMethodField()
class Meta:
model = Product
fields = [
'id', 'name', 'slug', 'description', 'base_price', 'sale_price',
'on_sale', 'stock', 'product_type', 'fursuit_type', 'style',
'is_custom_order', 'is_featured', 'category', 'category_id',
'image', 'gallery_images', 'reviews', 'average_rating',
'review_count', 'is_in_wishlist', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'slug', 'created_at', 'updated_at']
def get_is_in_wishlist(self, obj):
user = self.context['request'].user
if user.is_authenticated:
return obj.wishlists.filter(user=user).exists()
return False
def create(self, validated_data):
category_id = validated_data.pop('category_id', None)
if category_id:
validated_data['category'] = Category.objects.get(id=category_id)
return super().create(validated_data)
class ProductListSerializer(serializers.ModelSerializer):
"""Vereinfachter Serializer für Produktlisten"""
category = CategorySerializer(read_only=True)
average_rating = serializers.FloatField(read_only=True)
review_count = serializers.IntegerField(read_only=True)
is_in_wishlist = serializers.SerializerMethodField()
class Meta:
model = Product
fields = [
'id', 'name', 'slug', 'description', 'base_price', 'sale_price',
'on_sale', 'stock', 'product_type', 'fursuit_type', 'style',
'is_custom_order', 'is_featured', 'category', 'image',
'average_rating', 'review_count', 'is_in_wishlist'
]
def get_is_in_wishlist(self, obj):
user = self.context['request'].user
if user.is_authenticated:
return obj.wishlists.filter(user=user).exists()
return False
class ProductDetailSerializer(ProductSerializer):
"""Detaillierter Serializer für Produktdetails"""
related_products = ProductListSerializer(many=True, read_only=True)
class Meta(ProductSerializer.Meta):
fields = ProductSerializer.Meta.fields + ['related_products']
class WishlistSerializer(serializers.ModelSerializer):
"""Serializer für Wishlist-Model"""
products = ProductListSerializer(many=True, read_only=True)
product_count = serializers.SerializerMethodField()
class Meta:
model = Wishlist
fields = ['id', 'user', 'products', 'product_count', 'created_at']
read_only_fields = ['id', 'created_at']
def get_product_count(self, obj):
return obj.products.count()
class CustomOrderSerializer(serializers.ModelSerializer):
"""Serializer für CustomOrder-Model"""
user = UserSerializer(read_only=True)
status_display = serializers.CharField(source='get_status_display', read_only=True)
class Meta:
model = CustomOrder
fields = [
'id', 'user', 'title', 'description', 'fursuit_type', 'style',
'size', 'color_preferences', 'special_requirements', 'budget',
'status', 'status_display', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at']
def create(self, validated_data):
validated_data['user'] = self.context['request'].user
return super().create(validated_data)
class PaymentSerializer(serializers.ModelSerializer):
"""Serializer für Payment-Model"""
user = UserSerializer(read_only=True)
status_display = serializers.CharField(source='get_status_display', read_only=True)
class Meta:
model = Payment
fields = [
'id', 'user', 'order', 'amount', 'payment_method', 'status',
'status_display', 'transaction_id', 'created_at'
]
read_only_fields = ['id', 'created_at']
class CartItemSerializer(serializers.Serializer):
"""Serializer für Warenkorb-Items"""
product_id = serializers.IntegerField()
quantity = serializers.IntegerField(min_value=1)
product = ProductListSerializer(read_only=True)
total_price = serializers.DecimalField(max_digits=10, decimal_places=2, read_only=True)
class CartSerializer(serializers.Serializer):
"""Serializer für Warenkorb"""
items = CartItemSerializer(many=True)
subtotal = serializers.DecimalField(max_digits=10, decimal_places=2)
shipping_cost = serializers.DecimalField(max_digits=10, decimal_places=2)
total = serializers.DecimalField(max_digits=10, decimal_places=2)
item_count = serializers.IntegerField()
class SearchFilterSerializer(serializers.Serializer):
"""Serializer für Such- und Filter-Parameter"""
query = serializers.CharField(required=False, allow_blank=True)
category = serializers.CharField(required=False, allow_blank=True)
fursuit_type = serializers.CharField(required=False, allow_blank=True)
style = serializers.CharField(required=False, allow_blank=True)
min_price = serializers.DecimalField(max_digits=10, decimal_places=2, required=False)
max_price = serializers.DecimalField(max_digits=10, decimal_places=2, required=False)
on_sale = serializers.BooleanField(required=False)
is_featured = serializers.BooleanField(required=False)
sort_by = serializers.CharField(required=False, default='newest')
page = serializers.IntegerField(required=False, default=1)
page_size = serializers.IntegerField(required=False, default=12)
class ProductStatsSerializer(serializers.Serializer):
"""Serializer für Produkt-Statistiken"""
total_products = serializers.IntegerField()
featured_products = serializers.IntegerField()
on_sale_products = serializers.IntegerField()
low_stock_products = serializers.IntegerField()
categories_count = serializers.IntegerField()
average_rating = serializers.FloatField()
total_reviews = serializers.IntegerField()
class GalleryImageDetailSerializer(serializers.ModelSerializer):
"""Detaillierter Serializer für Galerie-Bilder"""
product = ProductListSerializer(read_only=True)
class Meta:
model = GalleryImage
fields = [
'id', 'product', 'image', 'alt_text', 'title', 'description',
'is_featured', 'fursuit_type', 'style', 'created_at'
]
read_only_fields = ['id', 'created_at']

227
products/sitemap_views.py Normal file
View File

@ -0,0 +1,227 @@
"""
Sitemap und Robots.txt Views für SEO
"""
from django.http import HttpResponse
from django.template.loader import render_to_string
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from django.views import View
from django.contrib.sites.models import Site
from .models import Product, Category
from .seo import SEOManager
import xml.etree.ElementTree as ET
import json
@cache_page(60 * 60 * 24) # 24 Stunden Cache
def sitemap_xml(request):
"""XML Sitemap generieren"""
seo_manager = SEOManager(request)
sitemap_data = seo_manager.generate_sitemap_xml()
# XML generieren
root = ET.Element('urlset')
root.set('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9')
for url_data in sitemap_data['urlset']['url']:
url_elem = ET.SubElement(root, 'url')
loc = ET.SubElement(url_elem, 'loc')
loc.text = url_data['loc']
lastmod = ET.SubElement(url_elem, 'lastmod')
lastmod.text = url_data['lastmod']
changefreq = ET.SubElement(url_elem, 'changefreq')
changefreq.text = url_data['changefreq']
priority = ET.SubElement(url_elem, 'priority')
priority.text = url_data['priority']
# XML als String
xml_string = ET.tostring(root, encoding='unicode', method='xml')
response = HttpResponse(xml_string, content_type='application/xml')
response['Content-Type'] = 'application/xml; charset=utf-8'
return response
@cache_page(60 * 60 * 24) # 24 Stunden Cache
def robots_txt(request):
"""Robots.txt generieren"""
seo_manager = SEOManager(request)
robots_content = seo_manager.generate_robots_txt()
response = HttpResponse(robots_content, content_type='text/plain')
return response
class SitemapView(View):
"""Erweiterte Sitemap View mit Kategorisierung"""
@method_decorator(cache_page(60 * 60 * 24))
def get(self, request):
"""Sitemap mit Kategorisierung"""
site = Site.objects.get_current()
base_url = f"https://{site.domain}"
# Sitemap Index
if request.GET.get('type') == 'index':
return self.sitemap_index(request, base_url)
# Produkt-Sitemap
elif request.GET.get('type') == 'products':
return self.products_sitemap(request, base_url)
# Kategorie-Sitemap
elif request.GET.get('type') == 'categories':
return self.categories_sitemap(request, base_url)
# Standard-Sitemap
else:
return self.main_sitemap(request, base_url)
def sitemap_index(self, request, base_url):
"""Sitemap Index"""
root = ET.Element('sitemapindex')
root.set('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9')
sitemaps = [
{'loc': f"{base_url}/sitemap.xml", 'lastmod': '2024-01-15'},
{'loc': f"{base_url}/sitemap.xml?type=products", 'lastmod': '2024-01-15'},
{'loc': f"{base_url}/sitemap.xml?type=categories", 'lastmod': '2024-01-15'},
]
for sitemap in sitemaps:
sitemap_elem = ET.SubElement(root, 'sitemap')
loc = ET.SubElement(sitemap_elem, 'loc')
loc.text = sitemap['loc']
lastmod = ET.SubElement(sitemap_elem, 'lastmod')
lastmod.text = sitemap['lastmod']
xml_string = ET.tostring(root, encoding='unicode', method='xml')
response = HttpResponse(xml_string, content_type='application/xml')
return response
def products_sitemap(self, request, base_url):
"""Produkt-Sitemap"""
root = ET.Element('urlset')
root.set('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9')
products = Product.objects.filter(is_active=True).order_by('-updated_at')
for product in products:
url_elem = ET.SubElement(root, 'url')
loc = ET.SubElement(url_elem, 'loc')
loc.text = f"{base_url}{product.get_absolute_url()}"
lastmod = ET.SubElement(url_elem, 'lastmod')
lastmod.text = product.updated_at.strftime('%Y-%m-%d')
changefreq = ET.SubElement(url_elem, 'changefreq')
changefreq.text = 'weekly'
priority = ET.SubElement(url_elem, 'priority')
priority.text = '0.8'
xml_string = ET.tostring(root, encoding='unicode', method='xml')
response = HttpResponse(xml_string, content_type='application/xml')
return response
def categories_sitemap(self, request, base_url):
"""Kategorie-Sitemap"""
root = ET.Element('urlset')
root.set('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9')
categories = Category.objects.all()
for category in categories:
url_elem = ET.SubElement(root, 'url')
loc = ET.SubElement(url_elem, 'loc')
loc.text = f"{base_url}{category.get_absolute_url()}"
lastmod = ET.SubElement(url_elem, 'lastmod')
lastmod.text = '2024-01-15' # TODO: Kategorie updated_at hinzufügen
changefreq = ET.SubElement(url_elem, 'changefreq')
changefreq.text = 'weekly'
priority = ET.SubElement(url_elem, 'priority')
priority.text = '0.7'
xml_string = ET.tostring(root, encoding='unicode', method='xml')
response = HttpResponse(xml_string, content_type='application/xml')
return response
def main_sitemap(self, request, base_url):
"""Haupt-Sitemap mit statischen Seiten"""
root = ET.Element('urlset')
root.set('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9')
# Statische Seiten
static_pages = [
{'url': '/', 'priority': '1.0', 'changefreq': 'daily'},
{'url': '/products/', 'priority': '0.8', 'changefreq': 'weekly'},
{'url': '/gallery/', 'priority': '0.7', 'changefreq': 'weekly'},
{'url': '/contact/', 'priority': '0.5', 'changefreq': 'monthly'},
{'url': '/about/', 'priority': '0.5', 'changefreq': 'monthly'},
{'url': '/faq/', 'priority': '0.6', 'changefreq': 'monthly'},
{'url': '/custom-order/', 'priority': '0.7', 'changefreq': 'weekly'},
]
for page in static_pages:
url_elem = ET.SubElement(root, 'url')
loc = ET.SubElement(url_elem, 'loc')
loc.text = f"{base_url}{page['url']}"
lastmod = ET.SubElement(url_elem, 'lastmod')
lastmod.text = '2024-01-15'
changefreq = ET.SubElement(url_elem, 'changefreq')
changefreq.text = page['changefreq']
priority = ET.SubElement(url_elem, 'priority')
priority.text = page['priority']
xml_string = ET.tostring(root, encoding='unicode', method='xml')
response = HttpResponse(xml_string, content_type='application/xml')
return response
def manifest_json(request):
"""Web App Manifest für PWA"""
manifest = {
"name": "Kasico Fursuit Shop",
"short_name": "Kasico",
"description": "Premium Fursuit Shop - Handgefertigte Fursuits made in Germany",
"start_url": "/",
"display": "standalone",
"background_color": "#FF6B9D",
"theme_color": "#FF6B9D",
"orientation": "portrait-primary",
"icons": [
{
"src": "/static/images/kasicoLogo.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/images/kasicoLogo.png",
"sizes": "512x512",
"type": "image/png"
}
],
"categories": ["shopping", "fashion"],
"lang": "de",
"dir": "ltr"
}
response = HttpResponse(
json.dumps(manifest, indent=2),
content_type='application/json'
)
return response

View File

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/furry.css">
<link rel="stylesheet" href="/static/css/furry-theme.css">
<link rel="stylesheet" href="/static/css/products.css">
<link rel="stylesheet" href="/static/css/dashboard.css">
<link rel="icon" type="image/svg+xml" href="/static/images/kasico-logo-simple.svg">
{{ block.super }}
{% endblock %}
{# Der eigentliche Seiteninhalt kommt in den content-Block #}
{% block content %}
{{ block.super }}
{% endblock %}

View File

@ -0,0 +1,99 @@
{% extends 'base.html' %}
{% block title %}Fortschritt hinzufügen - {{ block.super }}{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
Fortschritts-Update für {{ order.character_name }}
</h5>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="mb-3">
<label for="{{ form.stage.id_for_label }}" class="form-label">
{{ form.stage.label }}
</label>
{{ form.stage }}
{% if form.stage.errors %}
<div class="invalid-feedback d-block">
{% for error in form.stage.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
<label for="{{ form.description.id_for_label }}" class="form-label">
{{ form.description.label }}
</label>
{{ form.description }}
{% if form.description.errors %}
<div class="invalid-feedback d-block">
{% for error in form.description.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
<label for="{{ form.image.id_for_label }}" class="form-label">
{{ form.image.label }}
</label>
{{ form.image }}
{% if form.image.errors %}
<div class="invalid-feedback d-block">
{% for error in form.image.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-4">
<div class="form-check">
{{ form.completed }}
<label class="form-check-label" for="{{ form.completed.id_for_label }}">
{{ form.completed.label }}
</label>
</div>
{% if form.completed.errors %}
<div class="invalid-feedback d-block">
{% for error in form.completed.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'custom_order_detail' order.id %}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Zurück
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Update speichern
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,176 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Warenkorb{% endblock %}
{% block content %}
<h2>Warenkorb</h2>
{% if cart.items %}
<div class="cart-list">
{% for item in cart.items %}
<div class="cart-item-card">
<img src="{{ item.product.image.url }}" alt="{{ item.product.name }}" class="cart-item-image">
<div class="cart-item-info">
<h3>{{ item.product.name }}</h3>
<p class="cart-item-price">{{ item.product.price }} €</p>
<div class="cart-item-qty">
<form method="post" action="{% url 'products:cart_update' item.product.pk %}">
{% csrf_token %}
<input type="number" name="quantity" value="{{ item.quantity }}" min="1" class="cart-qty-input">
<button type="submit" class="btn furry-btn-outline">Menge ändern</button>
</form>
<form method="post" action="{% url 'products:cart_remove' item.product.pk %}" style="display:inline;">
{% csrf_token %}
<button type="submit" class="btn furry-btn-secondary">Entfernen</button>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="cart-summary">
<span>Gesamtsumme:</span>
<span class="cart-total">{{ cart.total_price }} €</span>
<a href="{% url 'products:checkout' %}" class="btn furry-btn">Weiter zur Kasse</a>
</div>
{% else %}
<p>Dein Warenkorb ist leer.</p>
{% endif %}
<!-- Related Products -->
{% if related_products %}
<div class="furry-card" style="margin-top: 2rem;">
<div class="furry-card-header">
<h2 class="furry-card-title">💡 Das könnte dir auch gefallen</h2>
<p class="furry-card-subtitle">Weitere Fursuits in deinem Stil</p>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1.5rem;">
{% for product in related_products %}
<div class="furry-card furry-product-card">
{% if product.image %}
<img src="{{ product.image.url }}" alt="{{ product.name }}" class="furry-card-image">
{% else %}
<div class="furry-card-image" style="background: linear-gradient(45deg, var(--furry-primary), var(--furry-secondary)); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem;">
🐾
</div>
{% endif %}
<div class="furry-card-header">
<div>
<h3 class="furry-card-title">{{ product.name }}</h3>
<p class="furry-card-subtitle">{{ product.get_fursuit_type_display }}</p>
</div>
</div>
<div class="furry-card-content">
<p>{{ product.description|truncatewords:10 }}</p>
<div class="furry-product-price">{{ product.price }}€</div>
</div>
<div class="furry-card-footer">
<a href="{% url 'products:product_detail' product.id %}" class="furry-btn furry-btn-primary furry-btn-sm">
👁️ Details
</a>
<button class="furry-btn furry-btn-outline furry-btn-sm" onclick="addToCart({{ product.id }})">
🛒 Hinzufügen
</button>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<script>
function updateQuantity(itemId, change) {
fetch(`/update-cart-item/${itemId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json',
},
body: JSON.stringify({
change: change
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
}
})
.catch(error => {
console.error('Error:', error);
});
}
function removeItem(itemId) {
if (confirm('Möchtest du dieses Produkt wirklich aus dem Warenkorb entfernen?')) {
fetch(`/remove-cart-item/${itemId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
}
})
.catch(error => {
console.error('Error:', error);
});
}
}
function clearCart() {
if (confirm('Möchtest du wirklich den gesamten Warenkorb leeren?')) {
fetch('/clear-cart/', {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
}
})
.catch(error => {
console.error('Error:', error);
});
}
}
function addToCart(productId) {
fetch(`/add-to-cart/${productId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
},
})
.then(response => response.json())
.then(data => {
if (data.success) {
const alert = document.createElement('div');
alert.className = 'furry-alert furry-alert-success';
alert.innerHTML = `
<div class="furry-alert-icon"></div>
<div class="furry-alert-content">
<div class="furry-alert-title">Erfolg</div>
<div class="furry-alert-message">Produkt wurde zum Warenkorb hinzugefügt!</div>
</div>
`;
document.querySelector('main').insertBefore(alert, document.querySelector('main').firstChild);
setTimeout(() => alert.remove(), 3000);
}
})
.catch(error => {
console.error('Error:', error);
});
}
</script>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends 'base.html' %}
{% block title %}Checkout{% endblock %}
{% block content %}
<h2>Checkout</h2>
<div class="checkout-container">
<form class="furry-form checkout-form">
<div class="form-group">
<label for="address">Adresse</label>
<input type="text" id="address" name="address" required placeholder="Straße, Hausnummer">
<div class="form-hint">Bitte gib deine vollständige Adresse ein 🐾</div>
</div>
<div class="form-group">
<label for="city">Stadt</label>
<input type="text" id="city" name="city" required placeholder="Stadt">
<div class="form-hint">Stadt nicht vergessen!</div>
</div>
<div class="form-group">
<label for="zip">PLZ</label>
<input type="text" id="zip" name="zip" required placeholder="PLZ">
<div class="form-hint">Postleitzahl bitte 🐾</div>
</div>
<div class="form-group">
<label for="payment">Zahlungsart</label>
<select id="payment" name="payment" required>
<option value="">Bitte wählen…</option>
<option value="paypal">PayPal</option>
<option value="stripe">Kreditkarte (Stripe)</option>
</select>
<div class="form-hint">Wähle deine bevorzugte Zahlungsart</div>
</div>
<button type="submit" class="btn furry-btn">Jetzt kaufen</button>
</form>
<div class="checkout-summary">
<h3>Deine Bestellung</h3>
<ul>
{% for item in cart.items %}
<li>{{ item.product.name }} × {{ item.quantity }} <span>{{ item.product.price|floatformat:2 }} €</span></li>
{% endfor %}
</ul>
<div class="checkout-total">Gesamtsumme: <span>{{ cart.total_price|floatformat:2 }} €</span></div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,455 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Kontakt - {{ block.super }}{% endblock %}
{% block content %}
<div class="contact-container">
<!-- Hero-Header -->
<div class="contact-hero furry-card text-center mb-5">
<div class="contact-hero-content">
<h1 class="contact-title">📞 Kontakt</h1>
<p class="contact-subtitle">Wir sind für dich da! Lass uns gemeinsam deinen perfekten Fursuit planen.</p>
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<!-- Kontaktinformationen -->
<div class="contact-info-section furry-card mb-5">
<h2 class="section-title">🐾 Erreiche uns</h2>
<div class="row">
<div class="col-md-4">
<div class="contact-info-card">
<div class="contact-icon">📧</div>
<h5>E-Mail</h5>
<p>info@kasico-art.de</p>
<a href="mailto:info@kasico-art.de" class="contact-link">Nachricht schreiben</a>
</div>
</div>
<div class="col-md-4">
<div class="contact-info-card">
<div class="contact-icon">📱</div>
<h5>Telefon</h5>
<p>+49 123 456789</p>
<a href="tel:+49123456789" class="contact-link">Anrufen</a>
</div>
</div>
<div class="col-md-4">
<div class="contact-info-card">
<div class="contact-icon">🕒</div>
<h5>Geschäftszeiten</h5>
<p>Mo-Fr: 9:00 - 18:00 Uhr<br>
Sa: 10:00 - 14:00 Uhr</p>
<span class="contact-note">🐺 Sonntags geschlossen</span>
</div>
</div>
</div>
</div>
<!-- Kontaktformular -->
<div class="contact-form-section furry-card">
<h2 class="section-title">✍️ Nachricht schreiben</h2>
<p class="form-subtitle">Erzähl uns von deinem Traum-Fursuit!</p>
<form id="contact-form" class="furry-form" method="post" novalidate>
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="name" class="form-label">🐺 Name</label>
<input type="text" id="name" name="name" required placeholder="Dein Name">
<div class="form-hint" id="name-hint"></div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="email" class="form-label">📧 E-Mail</label>
<input type="email" id="email" name="email" required placeholder="deine@email.de">
<div class="form-hint" id="email-hint"></div>
</div>
</div>
</div>
<div class="form-group">
<label for="subject" class="form-label">📝 Betreff</label>
<select id="subject" name="subject" required>
<option value="">Wähle ein Thema</option>
<option value="custom_order">🎨 Custom Order Anfrage</option>
<option value="general">❓ Allgemeine Frage</option>
<option value="support">🛠️ Support</option>
<option value="partnership">🤝 Partnerschaft</option>
</select>
<div class="form-hint" id="subject-hint"></div>
</div>
<div class="form-group">
<label for="message" class="form-label">💬 Nachricht</label>
<textarea id="message" name="message" required placeholder="Erzähl uns von deinem Projekt oder deiner Frage..."></textarea>
<div class="form-hint" id="message-hint"></div>
</div>
<div class="form-actions">
<button type="submit" class="btn furry-btn">
🚀 Nachricht senden
</button>
</div>
</form>
</div>
<!-- FAQ-Link -->
<div class="faq-link-section furry-card text-center mt-4">
<div class="faq-link-content">
<h3>❓ Häufige Fragen</h3>
<p>Finde schnell Antworten auf die wichtigsten Fragen</p>
<a href="{% url 'products:faq' %}" class="btn furry-btn-outline">
📚 FAQ durchsuchen
</a>
</div>
</div>
</div>
</div>
</div>
<style>
:root {
--primary-color: #8B5CF6;
--secondary-color: #EC4899;
--accent-color: #F59E0B;
--dark-color: #1F2937;
--light-color: #F3E8FF;
--white-color: #FFFFFF;
--gradient-start: #8B5CF6;
--gradient-middle: #EC4899;
}
.contact-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
.contact-hero {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
color: white;
padding: 3rem 2rem;
border-radius: 20px;
margin-bottom: 2rem;
}
.contact-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
}
.contact-subtitle {
font-size: 1.2rem;
opacity: 0.9;
margin-bottom: 0;
}
.section-title {
color: var(--dark-color);
font-weight: 700;
margin-bottom: 1rem;
font-size: 1.5rem;
}
.form-subtitle {
color: var(--dark-color);
opacity: 0.7;
margin-bottom: 2rem;
}
.contact-info-section {
padding: 2rem;
}
.contact-info-card {
text-align: center;
padding: 1.5rem;
border-radius: 15px;
background: var(--light-color);
margin-bottom: 1rem;
transition: transform 0.3s ease;
}
.contact-info-card:hover {
transform: translateY(-5px);
}
.contact-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.contact-info-card h5 {
color: var(--dark-color);
font-weight: 600;
margin-bottom: 0.5rem;
}
.contact-info-card p {
color: var(--dark-color);
margin-bottom: 1rem;
font-weight: 500;
}
.contact-link {
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
transition: color 0.3s ease;
}
.contact-link:hover {
color: var(--secondary-color);
}
.contact-note {
font-size: 0.875rem;
color: var(--dark-color);
opacity: 0.7;
}
.contact-form-section {
padding: 2rem;
}
.furry-form .form-group {
margin-bottom: 1.5rem;
}
.furry-form label {
font-weight: 600;
color: var(--dark-color);
margin-bottom: 0.5rem;
display: block;
}
.furry-form input, .furry-form textarea, .furry-form select {
width: 100%;
padding: 0.75rem;
border-radius: 10px;
border: 2px solid var(--light-color);
transition: all 0.3s ease;
font-size: 1rem;
background: white;
}
.furry-form input:focus, .furry-form textarea:focus, .furry-form select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(139, 92, 246, 0.25);
outline: none;
}
.furry-form input.invalid, .furry-form textarea.invalid, .furry-form select.invalid {
border-color: #EF4444;
box-shadow: 0 0 0 0.25rem rgba(239, 68, 68, 0.25);
}
.furry-form input.valid, .furry-form textarea.valid, .furry-form select.valid {
border-color: #10B981;
box-shadow: 0 0 0 0.25rem rgba(16, 185, 129, 0.25);
}
.furry-form textarea {
min-height: 120px;
resize: vertical;
}
.form-hint {
font-size: 0.875rem;
margin-top: 0.25rem;
font-weight: 500;
}
.form-hint:empty {
display: none;
}
.form-actions {
text-align: center;
margin-top: 2rem;
}
.furry-btn {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 50px;
font-weight: 700;
font-size: 1.1rem;
transition: all 0.3s ease;
}
.furry-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
}
.furry-btn-outline {
background: white;
color: var(--primary-color);
border: 2px solid var(--primary-color);
padding: 0.75rem 2rem;
border-radius: 50px;
font-weight: 700;
font-size: 1.1rem;
transition: all 0.3s ease;
}
.furry-btn-outline:hover {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
}
.faq-link-section {
padding: 2rem;
background: var(--light-color);
}
.faq-link-content h3 {
color: var(--dark-color);
margin-bottom: 0.5rem;
}
.faq-link-content p {
color: var(--dark-color);
opacity: 0.7;
margin-bottom: 1.5rem;
}
.alert-danger {
background: linear-gradient(135deg, #FEE2E2, #FECACA);
border: 2px solid #EF4444;
border-radius: 10px;
color: #991B1B;
padding: 1rem;
margin-bottom: 1rem;
}
@media (max-width: 768px) {
.contact-container {
padding: 1rem;
}
.contact-title {
font-size: 2rem;
}
.contact-info-card {
margin-bottom: 1rem;
}
}
/* Animation für erfolgreiche Nachricht */
@keyframes success-bounce {
0%, 20%, 53%, 80%, 100% {
transform: translate3d(0,0,0);
}
40%, 43% {
transform: translate3d(0, -30px, 0);
}
70% {
transform: translate3d(0, -15px, 0);
}
90% {
transform: translate3d(0, -4px, 0);
}
}
.success-animation {
animation: success-bounce 1s ease-in-out;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('contact-form');
const fields = [
{id: 'name', hint: 'Bitte gib deinen Namen ein.'},
{id: 'email', hint: 'Bitte gib eine gültige E-Mail-Adresse ein.'},
{id: 'subject', hint: 'Bitte wähle ein Thema aus.'},
{id: 'message', hint: 'Bitte schreibe eine Nachricht.'}
];
// Live-Validierung
fields.forEach(f => {
const input = document.getElementById(f.id);
const hint = document.getElementById(f.id+'-hint');
input.addEventListener('blur', function() {
validateField(this, f.hint);
});
input.addEventListener('input', function() {
if (this.classList.contains('invalid')) {
validateField(this, f.hint);
}
});
});
function validateField(field, hintText) {
const hint = document.getElementById(field.id+'-hint');
let isValid = true;
if (!field.value.trim()) {
isValid = false;
} else if (field.id === 'email' && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(field.value)) {
isValid = false;
}
if (isValid) {
field.classList.remove('invalid');
field.classList.add('valid');
hint.textContent = '✅ Alles super!';
hint.style.color = '#10B981';
} else {
field.classList.remove('valid');
field.classList.add('invalid');
hint.textContent = hintText + ' 🐾';
hint.style.color = '#EF4444';
}
}
// Formular-Submission
form.addEventListener('submit', function(e) {
let valid = true;
fields.forEach(f => {
const input = document.getElementById(f.id);
if (!input.value.trim() || (f.id === 'email' && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(input.value))) {
input.classList.add('invalid');
input.classList.remove('valid');
valid = false;
}
});
if (!valid) {
e.preventDefault();
// Smooth scroll to first invalid field
const firstInvalid = document.querySelector('.invalid');
if (firstInvalid) {
firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
} else {
// Success animation
document.querySelector('.contact-form-section').classList.add('success-animation');
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends 'base.html' %}
{% block title %}Nachricht gesendet - {{ block.super }}{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8 text-center">
<div class="card">
<div class="card-body">
<i class="bi bi-check-circle text-success" style="font-size: 4rem;"></i>
<h1 class="h3 mt-3">Vielen Dank für Ihre Nachricht!</h1>
<p class="lead">
Wir haben Ihre Anfrage erhalten und werden uns schnellstmöglich bei Ihnen melden.
</p>
<p class="text-muted">
Unsere durchschnittliche Antwortzeit beträgt 24-48 Stunden an Werktagen.
</p>
<hr>
<div class="mt-4">
<a href="{% url 'products:product_list' %}" class="btn btn-primary me-2">
<i class="bi bi-shop"></i> Zurück zum Shop
</a>
<a href="{% url 'products:faq' %}" class="btn btn-outline-primary">
<i class="bi bi-question-circle"></i> FAQ ansehen
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,735 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Custom Fursuit Bestellung - {{ block.super }}{% endblock %}
{% block content %}
<div class="custom-order-container">
<!-- Hero-Header -->
<div class="custom-order-hero furry-card text-center mb-5">
<div class="custom-order-hero-content">
<h1 class="custom-order-title">🎨 Custom Fursuit Anfrage</h1>
<p class="custom-order-subtitle">Lass deinen Traum-Fursuit Realität werden!</p>
<div class="custom-order-stats">
<span class="stat-item">⚡ 8-12 Wochen Produktionszeit</span>
<span class="stat-item">💰 30% Anzahlung</span>
<span class="stat-item">🎯 Individuelles Design</span>
</div>
</div>
</div>
<div class="row">
<!-- Informationen -->
<div class="col-lg-4 mb-4">
<div class="info-section furry-card h-100">
<div class="info-header text-center mb-4">
<div class="info-icon"></div>
<h3 class="info-title">Bestellprozess</h3>
<p class="info-subtitle">So wird dein Fursuit Realität</p>
</div>
<div class="process-steps">
<div class="process-step">
<div class="step-number">1</div>
<div class="step-content">
<h5>📝 Formular ausfüllen</h5>
<p>Beschreibe deinen Character und deine Wünsche</p>
</div>
</div>
<div class="process-step">
<div class="step-number">2</div>
<div class="step-content">
<h5>💰 Angebot erhalten</h5>
<p>Wir erstellen ein individuelles Angebot für dich</p>
</div>
</div>
<div class="process-step">
<div class="step-number">3</div>
<div class="step-content">
<h5>💳 Anzahlung leisten</h5>
<p>30% Anzahlung sichert deinen Auftrag</p>
</div>
</div>
<div class="process-step">
<div class="step-number">4</div>
<div class="step-content">
<h5>🎨 Design-Abstimmung</h5>
<p>Gemeinsam entwickeln wir das perfekte Design</p>
</div>
</div>
<div class="process-step">
<div class="step-number">5</div>
<div class="step-content">
<h5>🛠️ Produktion & Updates</h5>
<p>Regelmäßige Updates während der Produktion</p>
</div>
</div>
<div class="process-step">
<div class="step-number">6</div>
<div class="step-content">
<h5>📦 Fertigstellung & Versand</h5>
<p>Dein Fursuit kommt sicher zu dir</p>
</div>
</div>
</div>
<div class="important-notes">
<h4 class="notes-title">⚠️ Wichtige Hinweise</h4>
<div class="note-item">
<span class="note-icon"></span>
<span class="note-text">Produktionszeit: 8-12 Wochen</span>
</div>
<div class="note-item">
<span class="note-icon">💳</span>
<span class="note-text">Anzahlung: 30% des Gesamtpreises</span>
</div>
<div class="note-item">
<span class="note-icon">📏</span>
<span class="note-text">Genaue Maße sind erforderlich</span>
</div>
<div class="note-item">
<span class="note-icon">🎨</span>
<span class="note-text">Referenzbilder sind sehr hilfreich</span>
</div>
</div>
<div class="gallery-link">
<a href="{% url 'products:gallery' %}" class="btn furry-btn-outline">
🖼️ Beispielarbeiten ansehen
</a>
</div>
</div>
</div>
<!-- Formular -->
<div class="col-lg-8">
<div class="form-section furry-card">
<div class="form-header text-center mb-4">
<h2 class="form-title">🎭 Character Anfrage</h2>
<p class="form-subtitle">Erzähl uns von deinem Traum-Fursuit</p>
</div>
<form method="post" enctype="multipart/form-data" class="custom-order-form needs-validation" novalidate>
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<!-- Character Informationen -->
<div class="form-section-group">
<h4 class="section-header">🐺 Character Informationen</h4>
<div class="row g-3">
<div class="col-md-6">
<div class="form-group">
<label for="character_name" class="form-label">🎭 Character Name</label>
<input type="text" class="form-control furry-input" id="character_name" name="character_name" required placeholder="Name deines Characters">
<div class="form-hint" id="character_name-hint"></div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="fursuit_type" class="form-label">🎪 Fursuit Typ</label>
<select class="form-select furry-select" id="fursuit_type" name="fursuit_type" required>
<option value="">Bitte wählen...</option>
<option value="fullsuit">🐺 Fullsuit (Komplett)</option>
<option value="partial">🎭 Partial (Kopf + Pfoten)</option>
<option value="head">🦊 Nur Kopf</option>
</select>
<div class="form-hint" id="fursuit_type-hint"></div>
</div>
</div>
</div>
</div>
<!-- Design Präferenzen -->
<div class="form-section-group">
<h4 class="section-header">🎨 Design Präferenzen</h4>
<div class="row g-3">
<div class="col-12">
<div class="form-group">
<label for="character_description" class="form-label">📝 Character Beschreibung</label>
<textarea class="form-control furry-textarea" id="character_description" name="character_description" rows="4" required placeholder="Beschreibe deinen Character detailliert..."></textarea>
<div class="form-hint" id="character_description-hint"></div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="style" class="form-label">🎭 Gewünschter Stil</label>
<select class="form-select furry-select" id="style" name="style" required>
<option value="">Bitte wählen...</option>
<option value="toony">😊 Toony (Verspielt)</option>
<option value="semi_realistic">🦊 Semi-Realistic (Gemischt)</option>
<option value="realistic">🐺 Realistic (Realistisch)</option>
</select>
<div class="form-hint" id="style-hint"></div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="budget_range" class="form-label">💰 Budget</label>
<select class="form-select furry-select" id="budget_range" name="budget_range" required>
<option value="">Bitte wählen...</option>
<option value="2000-3000">2.000€ - 3.000€</option>
<option value="3000-4000">3.000€ - 4.000€</option>
<option value="4000+">4.000€+</option>
</select>
<div class="form-hint" id="budget_range-hint"></div>
</div>
</div>
</div>
</div>
<!-- Zusätzliche Details -->
<div class="form-section-group">
<h4 class="section-header">✨ Zusätzliche Details</h4>
<div class="row g-3">
<div class="col-12">
<div class="form-group">
<label for="color_preferences" class="form-label">🎨 Farbwünsche</label>
<textarea class="form-control furry-textarea" id="color_preferences" name="color_preferences" rows="3" placeholder="Beschreibe deine Farbwünsche..."></textarea>
<div class="form-hint" id="color_preferences-hint"></div>
</div>
</div>
<div class="col-12">
<div class="form-group">
<label for="special_requests" class="form-label">💫 Besondere Wünsche</label>
<textarea class="form-control furry-textarea" id="special_requests" name="special_requests" rows="3" placeholder="Besondere Features oder Wünsche..."></textarea>
<div class="form-hint" id="special_requests-hint"></div>
</div>
</div>
<div class="col-12">
<div class="form-group">
<label for="reference_images" class="form-label">🖼️ Referenzbilder</label>
<div class="file-upload-area">
<input type="file" class="form-control furry-file-input" id="reference_images" name="reference_images" accept="image/*" multiple>
<div class="file-upload-hint">
<span class="upload-icon">📁</span>
<span>Bilder hierher ziehen oder klicken zum Auswählen</span>
</div>
</div>
<div class="form-hint" id="reference_images-hint"></div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="deadline_request" class="form-label">📅 Gewünschter Fertigstellungstermin</label>
<input type="date" class="form-control furry-input" id="deadline_request" name="deadline_request">
<div class="form-hint" id="deadline_request-hint"></div>
</div>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn furry-btn">
🚀 Anfrage absenden
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<style>
:root {
--primary-color: #8B5CF6;
--secondary-color: #EC4899;
--accent-color: #F59E0B;
--dark-color: #1F2937;
--light-color: #F3E8FF;
--white-color: #FFFFFF;
--gradient-start: #8B5CF6;
--gradient-middle: #EC4899;
}
.custom-order-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem 1rem;
}
.custom-order-hero {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
color: white;
padding: 3rem 2rem;
border-radius: 20px;
margin-bottom: 2rem;
}
.custom-order-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
}
.custom-order-subtitle {
font-size: 1.2rem;
opacity: 0.9;
margin-bottom: 2rem;
}
.custom-order-stats {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
}
.stat-item {
background: rgba(255, 255, 255, 0.2);
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 600;
}
.info-section {
padding: 2rem;
}
.info-header {
margin-bottom: 2rem;
}
.info-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.info-title {
color: var(--dark-color);
font-weight: 700;
margin-bottom: 0.5rem;
}
.info-subtitle {
color: var(--dark-color);
opacity: 0.7;
margin-bottom: 0;
}
.process-steps {
margin-bottom: 2rem;
}
.process-step {
display: flex;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--light-color);
border-radius: 15px;
transition: transform 0.3s ease;
}
.process-step:hover {
transform: translateX(5px);
}
.step-number {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.2rem;
flex-shrink: 0;
}
.step-content h5 {
color: var(--dark-color);
font-weight: 600;
margin-bottom: 0.25rem;
}
.step-content p {
color: var(--dark-color);
opacity: 0.8;
margin-bottom: 0;
font-size: 0.9rem;
}
.important-notes {
margin-bottom: 2rem;
}
.notes-title {
color: var(--dark-color);
font-weight: 700;
margin-bottom: 1rem;
font-size: 1.2rem;
}
.note-item {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
padding: 0.5rem;
background: white;
border-radius: 10px;
}
.note-icon {
font-size: 1.2rem;
min-width: 1.5rem;
}
.note-text {
color: var(--dark-color);
font-weight: 500;
}
.gallery-link {
text-align: center;
}
.form-section {
padding: 2rem;
}
.form-header {
margin-bottom: 2rem;
}
.form-title {
color: var(--dark-color);
font-weight: 700;
margin-bottom: 0.5rem;
}
.form-subtitle {
color: var(--dark-color);
opacity: 0.7;
margin-bottom: 0;
}
.form-section-group {
margin-bottom: 2.5rem;
padding: 1.5rem;
background: var(--light-color);
border-radius: 15px;
}
.section-header {
color: var(--dark-color);
font-weight: 700;
margin-bottom: 1.5rem;
font-size: 1.3rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
font-weight: 600;
color: var(--dark-color);
margin-bottom: 0.5rem;
display: block;
}
.furry-input, .furry-select, .furry-textarea {
width: 100%;
padding: 0.75rem;
border-radius: 10px;
border: 2px solid var(--light-color);
transition: all 0.3s ease;
font-size: 1rem;
background: white;
}
.furry-input:focus, .furry-select:focus, .furry-textarea:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(139, 92, 246, 0.25);
outline: none;
}
.furry-input.invalid, .furry-select.invalid, .furry-textarea.invalid {
border-color: #EF4444;
box-shadow: 0 0 0 0.25rem rgba(239, 68, 68, 0.25);
}
.furry-input.valid, .furry-select.valid, .furry-textarea.valid {
border-color: #10B981;
box-shadow: 0 0 0 0.25rem rgba(16, 185, 129, 0.25);
}
.furry-textarea {
min-height: 120px;
resize: vertical;
}
.file-upload-area {
position: relative;
border: 2px dashed var(--light-color);
border-radius: 10px;
padding: 2rem;
text-align: center;
transition: all 0.3s ease;
background: white;
cursor: pointer;
min-height: 120px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.file-upload-area:hover {
border-color: var(--primary-color);
background: var(--light-color);
}
.furry-file-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 10;
}
.file-upload-hint {
color: var(--dark-color);
opacity: 0.7;
pointer-events: none;
z-index: 1;
}
.upload-icon {
font-size: 2rem;
display: block;
margin-bottom: 0.5rem;
}
.form-hint {
font-size: 0.875rem;
margin-top: 0.25rem;
font-weight: 500;
}
.form-hint:empty {
display: none;
}
.form-actions {
text-align: center;
margin-top: 2rem;
}
.furry-btn {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 50px;
font-weight: 700;
font-size: 1.1rem;
transition: all 0.3s ease;
}
.furry-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
}
.furry-btn-outline {
background: white;
color: var(--primary-color);
border: 2px solid var(--primary-color);
padding: 0.75rem 2rem;
border-radius: 50px;
font-weight: 700;
font-size: 1.1rem;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.furry-btn-outline:hover {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
text-decoration: none;
}
.alert-danger {
background: linear-gradient(135deg, #FEE2E2, #FECACA);
border: 2px solid #EF4444;
border-radius: 10px;
color: #991B1B;
padding: 1rem;
margin-bottom: 1rem;
}
@media (max-width: 768px) {
.custom-order-container {
padding: 1rem;
}
.custom-order-title {
font-size: 2rem;
}
.custom-order-stats {
flex-direction: column;
gap: 1rem;
}
.stat-item {
width: 100%;
text-align: center;
}
.process-step {
flex-direction: column;
text-align: center;
}
.step-number {
align-self: center;
}
}
/* Animation für erfolgreiche Anfrage */
@keyframes success-bounce {
0%, 20%, 53%, 80%, 100% {
transform: translate3d(0,0,0);
}
40%, 43% {
transform: translate3d(0, -30px, 0);
}
70% {
transform: translate3d(0, -15px, 0);
}
90% {
transform: translate3d(0, -4px, 0);
}
}
.success-animation {
animation: success-bounce 1s ease-in-out;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('.custom-order-form');
const fields = [
{id: 'character_name', hint: 'Bitte gib den Namen deines Characters ein.'},
{id: 'fursuit_type', hint: 'Bitte wähle einen Fursuit-Typ aus.'},
{id: 'character_description', hint: 'Bitte beschreibe deinen Character.'},
{id: 'style', hint: 'Bitte wähle einen Stil aus.'},
{id: 'budget_range', hint: 'Bitte wähle ein Budget aus.'}
];
// Live-Validierung
fields.forEach(f => {
const input = document.getElementById(f.id);
const hint = document.getElementById(f.id+'-hint');
if (input) {
input.addEventListener('blur', function() {
validateField(this, f.hint);
});
input.addEventListener('input', function() {
if (this.classList.contains('invalid')) {
validateField(this, f.hint);
}
});
}
});
function validateField(field, hintText) {
const hint = document.getElementById(field.id+'-hint');
let isValid = true;
if (!field.value.trim()) {
isValid = false;
}
if (isValid) {
field.classList.remove('invalid');
field.classList.add('valid');
hint.textContent = '✅ Alles super!';
hint.style.color = '#10B981';
} else {
field.classList.remove('valid');
field.classList.add('invalid');
hint.textContent = hintText + ' 🐾';
hint.style.color = '#EF4444';
}
}
// File upload handling
const fileInput = document.getElementById('reference_images');
const fileArea = document.querySelector('.file-upload-area');
if (fileInput) {
fileInput.addEventListener('change', function() {
const files = this.files;
if (files.length > 0) {
fileArea.style.borderColor = '#10B981';
fileArea.style.background = '#F0FDF4';
const hint = document.getElementById('reference_images-hint');
hint.textContent = `✅ ${files.length} Datei(en) ausgewählt`;
hint.style.color = '#10B981';
}
});
// Drag and drop
fileArea.addEventListener('dragover', function(e) {
e.preventDefault();
this.style.borderColor = '#8B5CF6';
this.style.background = '#F3E8FF';
});
fileArea.addEventListener('drop', function(e) {
e.preventDefault();
fileInput.files = e.dataTransfer.files;
fileInput.dispatchEvent(new Event('change'));
});
}
// Formular-Submission
form.addEventListener('submit', function(e) {
let valid = true;
fields.forEach(f => {
const input = document.getElementById(f.id);
if (input && !input.value.trim()) {
input.classList.add('invalid');
input.classList.remove('valid');
valid = false;
}
});
if (!valid) {
e.preventDefault();
// Smooth scroll to first invalid field
const firstInvalid = document.querySelector('.invalid');
if (firstInvalid) {
firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
} else {
// Success animation
document.querySelector('.form-section').classList.add('success-animation');
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,179 @@
{% extends 'base.html' %}
{% block title %}Custom Order #{{ order.id }} - {{ block.super }}{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row">
<!-- Order Details -->
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Anfrage-Details</h5>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li class="mb-2">
<strong>Status:</strong>
<span class="badge bg-{{ order.status }}">
{{ order.get_status_display }}
</span>
</li>
<li class="mb-2">
<strong>Character:</strong>
{{ order.character_name }}
</li>
<li class="mb-2">
<strong>Typ:</strong>
{{ order.get_fursuit_type_display }}
</li>
<li class="mb-2">
<strong>Stil:</strong>
{{ order.get_style_display }}
</li>
<li class="mb-2">
<strong>Budget:</strong>
{{ order.budget_range }}
</li>
{% if order.deadline_request %}
<li class="mb-2">
<strong>Gewünschter Termin:</strong>
{{ order.deadline_request }}
</li>
{% endif %}
<li class="mb-2">
<strong>Bestelldatum:</strong>
{{ order.created|date:"d.m.Y" }}
</li>
</ul>
{% if order.quoted_price %}
<div class="alert alert-info">
<h6 class="alert-heading">Angebot</h6>
<p class="mb-0">{{ order.quoted_price }} €</p>
</div>
{% endif %}
</div>
</div>
<!-- Character Details -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Character-Details</h5>
</div>
<div class="card-body">
<h6>Beschreibung</h6>
<p>{{ order.character_description }}</p>
{% if order.reference_images %}
<h6>Referenzbilder</h6>
<img data-src="{{ order.reference_images.url }}" class="img-fluid rounded furry-lazy-image" alt="Referenzbild">
{% endif %}
<h6 class="mt-3">Farbwünsche</h6>
<p>{{ order.color_preferences }}</p>
{% if order.special_requests %}
<h6>Besondere Wünsche</h6>
<p>{{ order.special_requests }}</p>
{% endif %}
</div>
</div>
</div>
<!-- Progress Timeline -->
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Fortschritt</h5>
{% if user.is_staff %}
<a href="{% url 'add_progress_update' order.id %}" class="btn btn-primary btn-sm">
<i class="bi bi-plus"></i> Update hinzufügen
</a>
{% endif %}
</div>
<div class="card-body">
{% if progress_updates %}
<div class="timeline">
{% for update in progress_updates %}
<div class="timeline-item">
<div class="timeline-marker {% if update.completed %}bg-success{% endif %}"></div>
<div class="timeline-content">
<h6 class="mb-2">
{{ update.get_stage_display }}
{% if update.completed %}
<i class="bi bi-check-circle-fill text-success"></i>
{% endif %}
</h6>
<p>{{ update.description }}</p>
{% if update.image %}
<img data-src="{{ update.image.url }}" class="img-fluid rounded mb-2 furry-lazy-image" alt="Fortschrittsbild">
{% endif %}
<small class="text-muted">
{{ update.created|date:"d.m.Y H:i" }}
</small>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-clock text-muted" style="font-size: 2rem;"></i>
<p class="mt-2">Noch keine Fortschritts-Updates vorhanden.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<style>
.timeline {
position: relative;
padding: 20px 0;
}
.timeline::before {
content: '';
position: absolute;
top: 0;
left: 15px;
height: 100%;
width: 2px;
background: #e9ecef;
}
.timeline-item {
position: relative;
margin-bottom: 30px;
padding-left: 40px;
}
.timeline-marker {
position: absolute;
left: 7px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #dee2e6;
border: 2px solid #fff;
box-shadow: 0 0 0 2px #dee2e6;
}
.timeline-content {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
}
.badge.bg-pending { background-color: #ffc107; }
.badge.bg-quoted { background-color: #17a2b8; }
.badge.bg-approved { background-color: #28a745; }
.badge.bg-in_progress { background-color: #007bff; }
.badge.bg-ready { background-color: #20c997; }
.badge.bg-shipped { background-color: #6f42c1; }
.badge.bg-completed { background-color: #28a745; }
.badge.bg-cancelled { background-color: #dc3545; }
</style>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends 'base.html' %}
{% block title %}Anfrage erfolgreich - {{ block.super }}{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8 text-center">
<div class="card">
<div class="card-body">
<i class="bi bi-check-circle-fill text-success" style="font-size: 4rem;"></i>
<h1 class="h3 mt-3">Ihre Fursuit-Anfrage wurde erfolgreich übermittelt!</h1>
<p class="lead mb-4">
Vielen Dank für Ihr Interesse an einem Custom Fursuit.
</p>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Ihre Anfrage-Details</h5>
<ul class="list-unstyled text-start">
<li><strong>Anfrage-ID:</strong> #{{ order.id }}</li>
<li><strong>Character:</strong> {{ order.character_name }}</li>
<li><strong>Typ:</strong> {{ order.get_fursuit_type_display }}</li>
<li><strong>Stil:</strong> {{ order.get_style_display }}</li>
<li><strong>Status:</strong> {{ order.get_status_display }}</li>
</ul>
</div>
</div>
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle"></i>
Wir werden Ihre Anfrage prüfen und uns innerhalb von 2-3 Werktagen mit einem detaillierten Angebot bei Ihnen melden.
</div>
<div class="mt-4">
<a href="{% url 'custom_order_detail' order.id %}" class="btn btn-primary me-2">
<i class="bi bi-eye"></i> Anfrage ansehen
</a>
<a href="{% url 'gallery' %}" class="btn btn-outline-primary">
<i class="bi bi-images"></i> Galerie ansehen
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,845 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Dashboard - Kasico Fursuit Shop{% endblock %}
{% block content %}
<div class="dashboard-container">
<!-- Hero-Header -->
<div class="dashboard-hero furry-card text-center mb-5">
<div class="dashboard-hero-content">
<h1 class="dashboard-title">🎛️ Mein Dashboard</h1>
<p class="dashboard-subtitle">Willkommen zurück, {{ user.username }}! 🐺</p>
<div class="dashboard-stats">
<span class="stat-item">📅 {{ user.date_joined|date:"d.m.Y" }}</span>
<span class="stat-item">⭐ Mitglied seit {{ user.date_joined|timesince }}</span>
</div>
</div>
</div>
<!-- Quick Stats -->
<div class="stats-section furry-card mb-5">
<h2 class="section-title">📊 Übersicht</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">📦</div>
<div class="stat-content">
<div class="stat-number">{{ total_orders }}</div>
<div class="stat-label">Bestellungen</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">💳</div>
<div class="stat-content">
<div class="stat-number">{{ total_spent }}€</div>
<div class="stat-label">Ausgegeben</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">❤️</div>
<div class="stat-content">
<div class="stat-number">{{ wishlist_count }}</div>
<div class="stat-label">Wunschliste</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon"></div>
<div class="stat-content">
<div class="stat-number">{{ reviews_count }}</div>
<div class="stat-label">Bewertungen</div>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="actions-section furry-card mb-5">
<h2 class="section-title">⚡ Schnelle Aktionen</h2>
<div class="actions-grid">
<a href="{% url 'products:product_list' %}" class="action-card">
<div class="action-icon">🛍️</div>
<div class="action-content">
<h4>Shop durchsuchen</h4>
<p>Entdecke neue Fursuits</p>
</div>
</a>
<a href="{% url 'products:wishlist' %}" class="action-card">
<div class="action-icon">❤️</div>
<div class="action-content">
<h4>Wunschliste</h4>
<p>Deine Favoriten</p>
</div>
</a>
<a href="{% url 'products:custom_order' %}" class="action-card">
<div class="action-icon">🎨</div>
<div class="action-content">
<h4>Custom Order</h4>
<p>Individueller Fursuit</p>
</div>
</a>
<a href="{% url 'products:contact' %}" class="action-card">
<div class="action-icon">📞</div>
<div class="action-content">
<h4>Support</h4>
<p>Hilfe & Kontakt</p>
</div>
</a>
</div>
</div>
<!-- Recent Orders -->
{% if recent_orders %}
<div class="orders-section furry-card mb-5">
<div class="section-header">
<h2 class="section-title">📦 Letzte Bestellungen</h2>
<a href="{% url 'products:order_history' %}" class="btn furry-btn-outline">
Alle anzeigen
</a>
</div>
<div class="orders-grid">
{% for order in recent_orders %}
<div class="order-card">
<div class="order-header">
<h4 class="order-title">Bestellung #{{ order.id }}</h4>
<span class="order-status status-{{ order.status }}">
{{ order.get_status_display }}
</span>
</div>
<div class="order-details">
<p class="order-date">{{ order.created_at|date:"d.m.Y H:i" }}</p>
<p class="order-amount">{{ order.total_amount }}€</p>
</div>
<div class="order-actions">
<a href="{% url 'products:order_detail' order.id %}" class="btn furry-btn-sm">
👁️ Details
</a>
{% if order.status == 'pending' %}
<button class="btn furry-btn-secondary furry-btn-sm">
💳 Bezahlen
</button>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Custom Orders -->
{% if custom_orders %}
<div class="custom-orders-section furry-card mb-5">
<div class="section-header">
<h2 class="section-title">🎨 Custom Orders</h2>
<a href="{% url 'products:custom_order_list' %}" class="btn furry-btn-outline">
Alle anzeigen
</a>
</div>
<div class="custom-orders-grid">
{% for order in custom_orders %}
<div class="custom-order-card">
<div class="custom-order-header">
<h4 class="custom-order-title">Custom Order #{{ order.id }}</h4>
<span class="custom-order-status status-{{ order.status }}">
{{ order.get_status_display }}
</span>
</div>
<div class="custom-order-details">
<p class="custom-order-date">{{ order.created_at|date:"d.m.Y" }}</p>
<p class="custom-order-description">{{ order.description|truncatewords:10 }}</p>
</div>
<div class="custom-order-actions">
<a href="{% url 'products:custom_order_detail' order.id %}" class="btn furry-btn-sm">
👁️ Details
</a>
{% if order.status == 'in_progress' %}
<button class="btn furry-btn-secondary furry-btn-sm">
📊 Fortschritt
</button>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Profile Section -->
<div class="profile-section furry-card mb-5">
<div class="section-header">
<h2 class="section-title">👤 Mein Profil</h2>
<a href="{% url 'products:profile' %}" class="btn furry-btn-outline">
Bearbeiten
</a>
</div>
<div class="profile-grid">
<div class="profile-info-card">
<h4 class="profile-subtitle">📝 Persönliche Daten</h4>
<div class="profile-details">
<div class="profile-item">
<span class="profile-label">Name:</span>
<span class="profile-value">{{ user.first_name }} {{ user.last_name }}</span>
</div>
<div class="profile-item">
<span class="profile-label">E-Mail:</span>
<span class="profile-value">{{ user.email }}</span>
</div>
<div class="profile-item">
<span class="profile-label">Mitglied seit:</span>
<span class="profile-value">{{ user.date_joined|date:"d.m.Y" }}</span>
</div>
</div>
</div>
<div class="profile-info-card">
<h4 class="profile-subtitle">📍 Adressen</h4>
<div class="profile-details">
{% if shipping_addresses %}
<div class="profile-item">
<span class="profile-label">Versandadressen:</span>
<span class="profile-value">{{ shipping_addresses.count }}</span>
</div>
{% else %}
<div class="profile-item">
<span class="profile-label">Versandadressen:</span>
<span class="profile-value">Keine gespeichert</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Notifications -->
{% if notifications %}
<div class="notifications-section furry-card mb-5">
<div class="section-header">
<h2 class="section-title">🔔 Benachrichtigungen</h2>
</div>
<div class="notifications-grid">
{% for notification in notifications %}
<div class="notification-card notification-{{ notification.type }}">
<div class="notification-icon">
{% if notification.type == 'success' %}✅
{% elif notification.type == 'error' %}❌
{% elif notification.type == 'warning' %}⚠️
{% else %}{% endif %}
</div>
<div class="notification-content">
<h4 class="notification-title">{{ notification.title }}</h4>
<p class="notification-message">{{ notification.message }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Support Section -->
<div class="support-section furry-card">
<div class="section-header">
<h2 class="section-title">🆘 Brauchst du Hilfe?</h2>
</div>
<div class="support-grid">
<div class="support-card">
<div class="support-icon">📞</div>
<h4 class="support-title">Kontakt</h4>
<p class="support-description">Hast du Fragen? Wir helfen dir gerne weiter!</p>
<a href="{% url 'products:contact' %}" class="btn furry-btn">
Kontakt aufnehmen
</a>
</div>
<div class="support-card">
<div class="support-icon"></div>
<h4 class="support-title">FAQ</h4>
<p class="support-description">Häufig gestellte Fragen und Antworten</p>
<a href="{% url 'products:faq' %}" class="btn furry-btn-secondary">
FAQ durchsuchen
</a>
</div>
<div class="support-card">
<div class="support-icon">📋</div>
<h4 class="support-title">Bestellungen</h4>
<p class="support-description">Verfolge deine Bestellungen und Custom Orders</p>
<a href="{% url 'products:order_history' %}" class="btn furry-btn-outline">
Bestellungen anzeigen
</a>
</div>
</div>
</div>
</div>
<style>
:root {
--primary-color: #8B5CF6;
--secondary-color: #EC4899;
--accent-color: #F59E0B;
--dark-color: #1F2937;
--light-color: #F3E8FF;
--white-color: #FFFFFF;
--gradient-start: #8B5CF6;
--gradient-middle: #EC4899;
--success-color: #10B981;
--warning-color: #F59E0B;
--error-color: #EF4444;
}
.dashboard-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem 1rem;
}
.dashboard-hero {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
color: white;
padding: 3rem 2rem;
border-radius: 20px;
margin-bottom: 2rem;
}
.dashboard-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
}
.dashboard-subtitle {
font-size: 1.2rem;
opacity: 0.9;
margin-bottom: 2rem;
}
.dashboard-stats {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
}
.stat-item {
background: rgba(255, 255, 255, 0.2);
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 600;
}
.section-title {
color: var(--dark-color);
font-weight: 700;
margin-bottom: 1.5rem;
font-size: 1.5rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.stats-section {
padding: 2rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 15px;
display: flex;
align-items: center;
gap: 1rem;
transition: transform 0.3s ease;
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.1);
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(139, 92, 246, 0.15);
}
.stat-icon {
font-size: 2rem;
min-width: 3rem;
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 0.25rem;
}
.stat-label {
color: var(--dark-color);
opacity: 0.7;
font-size: 0.9rem;
}
.actions-section {
padding: 2rem;
}
.actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.action-card {
background: white;
padding: 1.5rem;
border-radius: 15px;
text-decoration: none;
color: var(--dark-color);
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.1);
display: flex;
align-items: center;
gap: 1rem;
}
.action-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(139, 92, 246, 0.15);
text-decoration: none;
color: var(--dark-color);
}
.action-icon {
font-size: 2rem;
min-width: 3rem;
}
.action-content h4 {
margin-bottom: 0.25rem;
font-weight: 600;
}
.action-content p {
margin: 0;
opacity: 0.7;
font-size: 0.9rem;
}
.orders-section, .custom-orders-section {
padding: 2rem;
}
.orders-grid, .custom-orders-grid {
display: grid;
gap: 1rem;
}
.order-card, .custom-order-card {
background: white;
padding: 1.5rem;
border-radius: 15px;
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.1);
transition: transform 0.3s ease;
}
.order-card:hover, .custom-order-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(139, 92, 246, 0.15);
}
.order-header, .custom-order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.order-title, .custom-order-title {
margin: 0;
font-weight: 600;
color: var(--dark-color);
}
.order-status, .custom-order-status {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
}
.status-pending {
background: var(--warning-color);
color: white;
}
.status-completed {
background: var(--success-color);
color: white;
}
.status-cancelled {
background: var(--error-color);
color: white;
}
.order-details, .custom-order-details {
margin-bottom: 1rem;
}
.order-date, .custom-order-date {
margin: 0;
color: var(--dark-color);
opacity: 0.7;
font-size: 0.9rem;
}
.order-amount {
margin: 0.5rem 0 0 0;
font-weight: 600;
color: var(--primary-color);
}
.custom-order-description {
margin: 0.5rem 0 0 0;
color: var(--dark-color);
opacity: 0.8;
font-size: 0.9rem;
}
.order-actions, .custom-order-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.profile-section {
padding: 2rem;
}
.profile-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.profile-info-card {
background: white;
padding: 1.5rem;
border-radius: 15px;
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.1);
}
.profile-subtitle {
color: var(--dark-color);
font-weight: 600;
margin-bottom: 1rem;
}
.profile-details {
display: grid;
gap: 0.75rem;
}
.profile-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid var(--light-color);
}
.profile-item:last-child {
border-bottom: none;
}
.profile-label {
font-weight: 600;
color: var(--dark-color);
}
.profile-value {
color: var(--dark-color);
opacity: 0.8;
}
.notifications-section {
padding: 2rem;
}
.notifications-grid {
display: grid;
gap: 1rem;
}
.notification-card {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
border-radius: 10px;
background: white;
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.1);
}
.notification-success {
border-left: 4px solid var(--success-color);
}
.notification-error {
border-left: 4px solid var(--error-color);
}
.notification-warning {
border-left: 4px solid var(--warning-color);
}
.notification-info {
border-left: 4px solid var(--primary-color);
}
.notification-icon {
font-size: 1.5rem;
min-width: 2rem;
}
.notification-content {
flex: 1;
}
.notification-title {
margin: 0 0 0.5rem 0;
font-weight: 600;
color: var(--dark-color);
}
.notification-message {
margin: 0;
color: var(--dark-color);
opacity: 0.8;
font-size: 0.9rem;
}
.support-section {
padding: 2rem;
}
.support-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.support-card {
background: white;
padding: 2rem;
border-radius: 15px;
text-align: center;
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.1);
transition: transform 0.3s ease;
}
.support-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(139, 92, 246, 0.15);
}
.support-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.support-title {
color: var(--dark-color);
font-weight: 600;
margin-bottom: 0.5rem;
}
.support-description {
color: var(--dark-color);
opacity: 0.7;
margin-bottom: 1.5rem;
line-height: 1.5;
}
.furry-btn {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 25px;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.furry-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
color: white;
text-decoration: none;
}
.furry-btn-secondary {
background: var(--secondary-color);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 25px;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.furry-btn-secondary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(236, 72, 153, 0.3);
color: white;
text-decoration: none;
}
.furry-btn-outline {
background: white;
color: var(--primary-color);
border: 2px solid var(--primary-color);
padding: 0.75rem 1.5rem;
border-radius: 25px;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.furry-btn-outline:hover {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
text-decoration: none;
}
.furry-btn-sm {
padding: 0.5rem 1rem;
font-size: 0.8rem;
}
@media (max-width: 768px) {
.dashboard-container {
padding: 1rem;
}
.dashboard-title {
font-size: 2rem;
}
.dashboard-stats {
flex-direction: column;
gap: 1rem;
}
.stat-item {
width: 100%;
text-align: center;
}
.stats-grid, .actions-grid, .support-grid {
grid-template-columns: 1fr;
}
.section-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
}
/* Animation für Dashboard Items */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.stat-card, .action-card, .order-card, .custom-order-card, .support-card {
animation: fadeInUp 0.6s ease-out;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Auto-refresh notifications every 30 seconds
setInterval(function() {
fetch('/dashboard/notifications/')
.then(response => response.json())
.then(data => {
if (data.notifications && data.notifications.length > 0) {
// Update notifications section
const notificationsContainer = document.querySelector('.notifications-grid');
if (notificationsContainer) {
// Add new notifications
data.notifications.forEach(notification => {
const alert = document.createElement('div');
alert.className = `notification-card notification-${notification.type}`;
alert.innerHTML = `
<div class="notification-icon">
${notification.type === 'success' ? '✅' :
notification.type === 'error' ? '❌' :
notification.type === 'warning' ? '⚠️' : ''}
</div>
<div class="notification-content">
<h4 class="notification-title">${notification.title}</h4>
<p class="notification-message">${notification.message}</p>
</div>
`;
notificationsContainer.appendChild(alert);
// Add animation
alert.style.animation = 'fadeInUp 0.6s ease-out';
});
}
}
})
.catch(error => console.error('Error fetching notifications:', error));
}, 30000);
// Add hover effects to action cards
const actionCards = document.querySelectorAll('.action-card');
actionCards.forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-8px) scale(1.02)';
});
card.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0) scale(1)';
});
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,529 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}FAQ - {{ block.super }}{% endblock %}
{% block content %}
<div class="faq-container">
<!-- Hero-Header -->
<div class="faq-hero furry-card text-center mb-5">
<div class="faq-hero-content">
<h1 class="faq-title">❓ Häufige Fragen</h1>
<p class="faq-subtitle">Finde schnell Antworten auf die wichtigsten Fragen rund um Fursuits</p>
<div class="faq-stats">
<span class="stat-item">📚 {{ faqs.count }} Fragen</span>
<span class="stat-item">📂 {{ categories|length }} Kategorien</span>
<span class="stat-item">🐺 Furry-Expertise</span>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-10">
<!-- FAQ-Kategorien -->
<div class="faq-categories-section furry-card mb-5">
<h2 class="section-title">📂 Kategorien durchsuchen</h2>
<div class="faq-categories">
<button class="category-btn furry-btn active" data-category="all">
🌟 Alle Fragen
</button>
{% for category in categories %}
<button class="category-btn furry-btn-outline" data-category="{{ category }}">
{% if category == "Bestellung" %}🛒
{% elif category == "Versand" %}📦
{% elif category == "Fursuit" %}🐺
{% elif category == "Zahlung" %}💳
{% elif category == "Support" %}🛠️
{% else %}❓{% endif %}
{{ category }}
</button>
{% endfor %}
</div>
</div>
<!-- FAQ-Akkordeon -->
<div class="faq-accordion-section furry-card">
<h2 class="section-title">💡 Antworten finden</h2>
<div class="accordion" id="faqAccordion">
{% for faq in faqs %}
<div class="accordion-item" data-category="{{ faq.category }}">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#faq{{ forloop.counter }}">
<span class="faq-question-icon">
{% if faq.category == "Bestellung" %}🛒
{% elif faq.category == "Versand" %}📦
{% elif faq.category == "Fursuit" %}🐺
{% elif faq.category == "Zahlung" %}💳
{% elif faq.category == "Support" %}🛠️
{% else %}❓{% endif %}
</span>
<span class="faq-question-text">{{ faq.question }}</span>
</button>
</h2>
<div id="faq{{ forloop.counter }}" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
<div class="accordion-body">
<div class="faq-answer">
{{ faq.answer|linebreaks }}
</div>
<div class="faq-helpful">
<span class="helpful-text">War diese Antwort hilfreich?</span>
<button class="helpful-btn" onclick="markHelpful(this, true)">
👍 Ja
</button>
<button class="helpful-btn" onclick="markHelpful(this, false)">
👎 Nein
</button>
</div>
</div>
</div>
</div>
{% empty %}
<div class="empty-faq">
<div class="empty-faq-icon">🦊</div>
<h3>Keine FAQ-Einträge verfügbar</h3>
<p>Wir arbeiten daran, die häufigsten Fragen zu beantworten. Schau bald wieder vorbei!</p>
</div>
{% endfor %}
</div>
</div>
<!-- Kontakt-Button -->
<div class="contact-link-section furry-card text-center mt-5">
<div class="contact-link-content">
<h3>🤔 Keine Antwort gefunden?</h3>
<p>Wir helfen dir gerne persönlich weiter!</p>
<div class="contact-actions">
<a href="{% url 'products:contact' %}" class="btn furry-btn">
📞 Kontakt aufnehmen
</a>
<a href="mailto:info@kasico-art.de" class="btn furry-btn-outline">
📧 E-Mail schreiben
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
:root {
--primary-color: #8B5CF6;
--secondary-color: #EC4899;
--accent-color: #F59E0B;
--dark-color: #1F2937;
--light-color: #F3E8FF;
--white-color: #FFFFFF;
--gradient-start: #8B5CF6;
--gradient-middle: #EC4899;
}
.faq-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
.faq-hero {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
color: white;
padding: 3rem 2rem;
border-radius: 20px;
margin-bottom: 2rem;
}
.faq-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
}
.faq-subtitle {
font-size: 1.2rem;
opacity: 0.9;
margin-bottom: 2rem;
}
.faq-stats {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
}
.stat-item {
background: rgba(255, 255, 255, 0.2);
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 600;
}
.section-title {
color: var(--dark-color);
font-weight: 700;
margin-bottom: 1.5rem;
font-size: 1.5rem;
}
.faq-categories-section {
padding: 2rem;
}
.faq-categories {
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
}
.category-btn {
padding: 0.75rem 1.5rem;
border-radius: 25px;
font-weight: 600;
font-size: 1rem;
transition: all 0.3s ease;
border: none;
cursor: pointer;
}
.category-btn.active {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
color: white;
transform: scale(1.05);
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
}
.category-btn:not(.active) {
background: white;
color: var(--primary-color);
border: 2px solid var(--primary-color);
}
.category-btn:not(.active):hover {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
}
.faq-accordion-section {
padding: 2rem;
}
.accordion-item {
border: none;
margin-bottom: 1rem;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.1);
transition: all 0.3s ease;
}
.accordion-item:hover {
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.15);
transform: translateY(-2px);
}
.accordion-button {
background: white;
border: none;
padding: 1.5rem;
font-weight: 600;
color: var(--dark-color);
display: flex;
align-items: center;
gap: 1rem;
transition: all 0.3s ease;
}
.accordion-button:not(.collapsed) {
background: var(--light-color);
color: var(--primary-color);
box-shadow: none;
}
.accordion-button:focus {
box-shadow: none;
border: none;
}
.accordion-button::after {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%238B5CF6'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
transition: transform 0.3s ease;
}
.accordion-button:not(.collapsed)::after {
transform: rotate(180deg);
}
.faq-question-icon {
font-size: 1.2rem;
min-width: 2rem;
text-align: center;
}
.faq-question-text {
flex: 1;
text-align: left;
}
.accordion-body {
padding: 1.5rem;
background: white;
}
.faq-answer {
color: var(--dark-color);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.faq-helpful {
display: flex;
align-items: center;
gap: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--light-color);
}
.helpful-text {
color: var(--dark-color);
opacity: 0.7;
font-size: 0.875rem;
}
.helpful-btn {
background: var(--light-color);
border: none;
padding: 0.5rem 1rem;
border-radius: 20px;
color: var(--dark-color);
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.helpful-btn:hover {
background: var(--primary-color);
color: white;
transform: scale(1.05);
}
.empty-faq {
text-align: center;
padding: 4rem 2rem;
}
.empty-faq-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.empty-faq h3 {
color: var(--dark-color);
margin-bottom: 1rem;
}
.empty-faq p {
color: var(--dark-color);
opacity: 0.7;
margin-bottom: 0;
}
.contact-link-section {
padding: 2rem;
background: var(--light-color);
}
.contact-link-content h3 {
color: var(--dark-color);
margin-bottom: 0.5rem;
}
.contact-link-content p {
color: var(--dark-color);
opacity: 0.7;
margin-bottom: 2rem;
}
.contact-actions {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.furry-btn {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 50px;
font-weight: 700;
font-size: 1.1rem;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.furry-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
color: white;
text-decoration: none;
}
.furry-btn-outline {
background: white;
color: var(--primary-color);
border: 2px solid var(--primary-color);
padding: 0.75rem 2rem;
border-radius: 50px;
font-weight: 700;
font-size: 1.1rem;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.furry-btn-outline:hover {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
text-decoration: none;
}
@media (max-width: 768px) {
.faq-container {
padding: 1rem;
}
.faq-title {
font-size: 2rem;
}
.faq-stats {
flex-direction: column;
gap: 1rem;
}
.stat-item {
width: 100%;
text-align: center;
}
.faq-categories {
flex-direction: column;
align-items: center;
}
.category-btn {
width: 100%;
max-width: 300px;
}
.contact-actions {
flex-direction: column;
align-items: center;
}
.furry-btn, .furry-btn-outline {
width: 100%;
max-width: 300px;
text-align: center;
}
}
/* Animation für FAQ Items */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.accordion-item {
animation: fadeInUp 0.6s ease-out;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const categoryButtons = document.querySelectorAll('[data-category]');
const faqItems = document.querySelectorAll('.accordion-item');
categoryButtons.forEach(button => {
button.addEventListener('click', () => {
// Aktiven Button markieren
categoryButtons.forEach(btn => {
btn.classList.remove('active');
btn.classList.remove('furry-btn');
btn.classList.add('furry-btn-outline');
});
button.classList.add('active');
button.classList.remove('furry-btn-outline');
button.classList.add('furry-btn');
// FAQs filtern
const selectedCategory = button.dataset.category;
let visibleCount = 0;
faqItems.forEach(item => {
if (selectedCategory === 'all' || item.dataset.category === selectedCategory) {
item.style.display = 'block';
visibleCount++;
} else {
item.style.display = 'none';
}
});
// Smooth scroll to first visible item
if (visibleCount > 0) {
const firstVisible = document.querySelector('.accordion-item[style*="block"]');
if (firstVisible) {
firstVisible.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
});
});
// Smooth accordion animations
const accordionButtons = document.querySelectorAll('.accordion-button');
accordionButtons.forEach(button => {
button.addEventListener('click', function() {
const target = this.getAttribute('data-bs-target');
const targetElement = document.querySelector(target);
if (targetElement.classList.contains('show')) {
targetElement.style.transition = 'all 0.3s ease';
}
});
});
});
function markHelpful(button, isHelpful) {
const originalText = button.textContent;
// Visual feedback
button.style.background = isHelpful ? '#10B981' : '#EF4444';
button.style.color = 'white';
button.textContent = isHelpful ? '✅ Danke!' : '👎 Danke!';
button.disabled = true;
// Reset after 2 seconds
setTimeout(() => {
button.style.background = '';
button.style.color = '';
button.textContent = originalText;
button.disabled = false;
}, 2000);
// Here you could send the feedback to the server
console.log(`FAQ marked as ${isHelpful ? 'helpful' : 'not helpful'}`);
}
</script>
{% endblock %}

View File

@ -0,0 +1,532 @@
{% extends "shop/base.html" %}
{% load i18n %}
{% load static %}
{% load extras %}
{% block title %}Galerie - Kasico Art & Design{% endblock %}
{% block content %}
<div class="gallery-container">
<!-- Hero-Header -->
<div class="gallery-hero furry-card text-center mb-5">
<div class="gallery-hero-content">
<h1 class="gallery-title">🎨 Furry Galerie</h1>
<p class="gallery-subtitle">Entdecke unsere einzigartigen Fursuit-Kreationen</p>
<div class="gallery-stats">
<span class="stat-item">📸 {{ images.count }} Bilder</span>
<span class="stat-item">🎭 {{ fursuit_types|length }} Typen</span>
<span class="stat-item">🎨 {{ fursuit_styles|length }} Styles</span>
</div>
</div>
</div>
<!-- Filter-Bereich -->
<div class="filter-section mb-4">
<div class="furry-card">
<div class="filter-header">
<h3 class="filter-title">🔍 Filter & Sortierung</h3>
<p class="filter-subtitle">Finde deinen perfekten Fursuit-Stil</p>
</div>
<div class="row align-items-center">
<div class="col-md-4">
<div class="form-group">
<label for="fursuit_type" class="filter-label">🐺 Fursuit-Typ</label>
<select class="form-select furry-select" id="fursuit_type" name="fursuit_type">
<option value="">🎭 Alle Typen</option>
{% for type_code, type_name in fursuit_types %}
<option value="{{ type_code }}" {% if current_type == type_code %}selected{% endif %}>
{{ type_name }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="style" class="filter-label">🎨 Style</label>
<select class="form-select furry-select" id="style" name="style">
<option value="">🌈 Alle Styles</option>
{% for style_code, style_name in fursuit_styles %}
<option value="{{ style_code }}" {% if current_style == style_code %}selected{% endif %}>
{{ style_name }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="sort" class="filter-label">📅 Sortierung</label>
<select class="form-select furry-select" id="sort" name="sort">
<option value="newest" {% if current_sort == 'newest' %}selected{% endif %}>
⭐ Neueste zuerst
</option>
<option value="oldest" {% if current_sort == 'oldest' %}selected{% endif %}>
📅 Älteste zuerst
</option>
</select>
</div>
</div>
</div>
<div class="filter-actions mt-3">
<button class="btn furry-btn-secondary" onclick="clearFilters()">
🗑️ Filter zurücksetzen
</button>
</div>
</div>
</div>
<!-- Galerie-Grid -->
<div class="row g-4" id="gallery-grid">
{% for image in images %}
<div class="col-md-6 col-lg-4 gallery-item" data-aos="fade-up" data-aos-delay="{{ forloop.counter0|multiply:100 }}">
<div class="gallery-card furry-card h-100">
<div class="gallery-image">
<img data-src="{{ image.image.url }}" alt="{{ image.title }}" class="img-fluid furry-lazy-image">
{% if image.is_featured %}
<span class="featured-badge">
⭐ Featured
</span>
{% endif %}
<div class="gallery-overlay">
<button class="btn furry-btn-sm" onclick="openLightbox('{{ image.image.url }}', '{{ image.title }}')">
🔍 Vergrößern
</button>
</div>
</div>
<div class="gallery-info p-3">
<h5 class="gallery-title mb-2">{{ image.title }}</h5>
<p class="gallery-description mb-3">{{ image.description }}</p>
<div class="gallery-meta">
<span class="badge furry-badge me-2">
🐺 {{ image.get_fursuit_type_display }}
</span>
<span class="badge furry-badge">
🎨 {{ image.get_style_display }}
</span>
</div>
</div>
</div>
</div>
{% empty %}
<div class="col-12">
<div class="empty-gallery furry-card text-center">
<div class="empty-gallery-icon">🦊</div>
<h3>Keine Bilder gefunden</h3>
<p>Versuche andere Filter-Einstellungen oder schaue später wieder vorbei!</p>
<button class="btn furry-btn" onclick="clearFilters()">
🔄 Alle Filter zurücksetzen
</button>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Lightbox Modal -->
<div id="lightbox" class="lightbox-modal">
<div class="lightbox-content">
<span class="lightbox-close" onclick="closeLightbox()">&times;</span>
<img id="lightbox-image" src="" alt="">
<div class="lightbox-caption" id="lightbox-caption"></div>
</div>
</div>
<style>
:root {
--primary-color: #8B5CF6;
--secondary-color: #EC4899;
--accent-color: #F59E0B;
--dark-color: #1F2937;
--light-color: #F3E8FF;
--white-color: #FFFFFF;
--gradient-start: #8B5CF6;
--gradient-middle: #EC4899;
}
.gallery-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem 1rem;
}
.gallery-hero {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
color: white;
padding: 3rem 2rem;
border-radius: 20px;
margin-bottom: 2rem;
}
.gallery-hero-content h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
}
.gallery-subtitle {
font-size: 1.2rem;
opacity: 0.9;
margin-bottom: 2rem;
}
.gallery-stats {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
}
.stat-item {
background: rgba(255, 255, 255, 0.2);
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 600;
}
.filter-header {
text-align: center;
margin-bottom: 2rem;
}
.filter-title {
color: var(--dark-color);
font-weight: 700;
margin-bottom: 0.5rem;
}
.filter-subtitle {
color: var(--dark-color);
opacity: 0.7;
margin-bottom: 0;
}
.filter-label {
font-weight: 600;
color: var(--dark-color);
margin-bottom: 0.5rem;
}
.furry-select {
border: 2px solid var(--light-color);
border-radius: 10px;
padding: 0.75rem;
transition: all 0.3s ease;
background: white;
}
.furry-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(139, 92, 246, 0.25);
outline: none;
}
.gallery-card {
transition: all 0.3s ease;
border: none;
overflow: hidden;
position: relative;
}
.gallery-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 12px 40px rgba(139, 92, 246, 0.2);
}
.gallery-image {
position: relative;
padding-top: 75%;
overflow: hidden;
border-radius: 15px 15px 0 0;
}
.gallery-image img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.gallery-card:hover .gallery-image img {
transform: scale(1.1);
}
.featured-badge {
position: absolute;
top: 10px;
left: 10px;
background: linear-gradient(135deg, #F59E0B, #F97316);
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 600;
font-size: 0.875rem;
z-index: 2;
}
.gallery-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(139, 92, 246, 0.8);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.gallery-card:hover .gallery-overlay {
opacity: 1;
}
.furry-btn-sm {
background: white;
color: var(--primary-color);
border: none;
padding: 0.5rem 1rem;
border-radius: 25px;
font-weight: 600;
transition: all 0.3s ease;
}
.furry-btn-sm:hover {
transform: scale(1.05);
box-shadow: 0 4px 15px rgba(255, 255, 255, 0.3);
}
.gallery-info {
background: var(--white-color);
border-radius: 0 0 15px 15px;
}
.gallery-title {
font-family: 'Baloo 2', sans-serif;
color: var(--dark-color);
font-weight: 600;
}
.gallery-description {
color: var(--dark-color);
opacity: 0.8;
line-height: 1.5;
}
.furry-badge {
background: var(--light-color);
color: var(--dark-color);
border-radius: 15px;
padding: 0.5rem 1rem;
font-weight: 600;
font-size: 0.875rem;
}
.empty-gallery {
padding: 4rem 2rem;
}
.empty-gallery-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.empty-gallery h3 {
color: var(--dark-color);
margin-bottom: 1rem;
}
.empty-gallery p {
color: var(--dark-color);
opacity: 0.7;
margin-bottom: 2rem;
}
/* Lightbox Styles */
.lightbox-modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(5px);
}
.lightbox-content {
position: relative;
margin: auto;
padding: 20px;
width: 90%;
max-width: 800px;
height: 90%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.lightbox-close {
position: absolute;
top: 15px;
right: 35px;
color: #f1f1f1;
font-size: 40px;
font-weight: bold;
cursor: pointer;
z-index: 10000;
}
.lightbox-close:hover {
color: var(--secondary-color);
}
#lightbox-image {
max-width: 100%;
max-height: 80%;
object-fit: contain;
border-radius: 10px;
}
.lightbox-caption {
color: white;
text-align: center;
margin-top: 1rem;
font-size: 1.1rem;
font-weight: 600;
}
@media (max-width: 768px) {
.gallery-container {
padding: 1rem;
}
.gallery-hero-content h1 {
font-size: 2rem;
}
.gallery-stats {
flex-direction: column;
gap: 1rem;
}
.stat-item {
width: 100%;
text-align: center;
}
}
/* Animation für Gallery Items */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.gallery-item {
animation: fadeInUp 0.6s ease-out;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Filter-Handling
const filterSelects = document.querySelectorAll('select');
filterSelects.forEach(select => {
select.addEventListener('change', function() {
applyFilters();
});
});
// Lazy Loading für Bilder
const lazyImages = document.querySelectorAll('.furry-lazy-image');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('furry-lazy-image');
observer.unobserve(img);
}
});
});
lazyImages.forEach(img => imageObserver.observe(img));
});
function applyFilters() {
const fursuit_type = document.getElementById('fursuit_type').value;
const style = document.getElementById('style').value;
const sort = document.getElementById('sort').value;
let url = new URL(window.location.href);
url.searchParams.set('fursuit_type', fursuit_type);
url.searchParams.set('style', style);
url.searchParams.set('sort', sort);
// Smooth transition
document.getElementById('gallery-grid').style.opacity = '0.5';
setTimeout(() => {
window.location.href = url.toString();
}, 300);
}
function clearFilters() {
document.getElementById('fursuit_type').value = '';
document.getElementById('style').value = '';
document.getElementById('sort').value = 'newest';
applyFilters();
}
function openLightbox(imageSrc, caption) {
const lightbox = document.getElementById('lightbox');
const lightboxImage = document.getElementById('lightbox-image');
const lightboxCaption = document.getElementById('lightbox-caption');
lightboxImage.src = imageSrc;
lightboxCaption.textContent = caption;
lightbox.style.display = 'block';
// Smooth fade in
lightbox.style.opacity = '0';
setTimeout(() => {
lightbox.style.opacity = '1';
}, 10);
}
function closeLightbox() {
const lightbox = document.getElementById('lightbox');
lightbox.style.opacity = '0';
setTimeout(() => {
lightbox.style.display = 'none';
}, 300);
}
// Schließen mit ESC-Taste
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeLightbox();
}
});
// Schließen beim Klick außerhalb des Bildes
document.getElementById('lightbox').addEventListener('click', function(event) {
if (event.target === this) {
closeLightbox();
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,246 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Kasico Fursuit Shop - Handgefertigte Fursuits{% endblock %}
{% block content %}
<!-- Hero Section -->
<div class="furry-card" style="background: linear-gradient(135deg, var(--furry-primary) 0%, var(--furry-secondary) 100%); color: white; text-align: center; padding: 4rem 2rem; margin-bottom: 2rem;">
<div style="font-size: 4rem; margin-bottom: 1rem;">🐾</div>
<h1 style="font-size: 3rem; font-weight: 900; margin-bottom: 1rem;">Willkommen bei Kasico</h1>
<p style="font-size: 1.25rem; margin-bottom: 2rem; opacity: 0.9;">Handgefertigte Fursuits für die Furry-Community</p>
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
<a href="{% url 'products:product_list' %}" class="furry-btn furry-btn-secondary furry-btn-lg">
🛍️ Produkte entdecken
</a>
<a href="{% url 'products:custom_order' %}" class="furry-btn furry-btn-outline furry-btn-lg" style="color: white; border-color: white;">
🎨 Custom Order
</a>
</div>
</div>
<!-- Featured Categories -->
<div class="furry-card" style="margin-bottom: 2rem;">
<div class="furry-card-header">
<h2 class="furry-card-title">🎭 Fursuit Kategorien</h2>
<p class="furry-card-subtitle">Entdecke verschiedene Fursuit-Typen und Stile</p>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem;">
<div class="furry-card furry-product-card">
<div class="furry-card-image" style="background: linear-gradient(45deg, var(--furry-primary), var(--furry-secondary)); display: flex; align-items: center; justify-content: center; color: white; font-size: 3rem;">
🦊
</div>
<div class="furry-card-header">
<h3 class="furry-card-title">Fullsuits</h3>
<p class="furry-card-subtitle">Komplette Fursuits</p>
</div>
<div class="furry-card-content">
<p>Vollständige Fursuits mit Kopf, Körper und Pfoten. Perfekt für Conventions und Events.</p>
</div>
<div class="furry-card-footer">
<a href="{% url 'products:product_list' %}?fursuit_type=fullsuit" class="furry-btn furry-btn-primary furry-btn-sm">
Entdecken
</a>
</div>
</div>
<div class="furry-card furry-product-card">
<div class="furry-card-image" style="background: linear-gradient(45deg, var(--furry-accent), var(--furry-warning)); display: flex; align-items: center; justify-content: center; color: white; font-size: 3rem;">
🐺
</div>
<div class="furry-card-header">
<h3 class="furry-card-title">Partials</h3>
<p class="furry-card-subtitle">Kopf & Pfoten</p>
</div>
<div class="furry-card-content">
<p>Fursuit-Köpfe mit passenden Pfoten. Ideal für Einsteiger und Budget-Bewusste.</p>
</div>
<div class="furry-card-footer">
<a href="{% url 'products:product_list' %}?fursuit_type=partial" class="furry-btn furry-btn-primary furry-btn-sm">
Entdecken
</a>
</div>
</div>
<div class="furry-card furry-product-card">
<div class="furry-card-image" style="background: linear-gradient(45deg, var(--furry-success), var(--furry-accent)); display: flex; align-items: center; justify-content: center; color: white; font-size: 3rem;">
🐾
</div>
<div class="furry-card-header">
<h3 class="furry-card-title">Accessoires</h3>
<p class="furry-card-subtitle">Pfoten, Schwänze & mehr</p>
</div>
<div class="furry-card-content">
<p>Einzelne Accessoires wie Pfoten, Schwänze und andere Fursuit-Teile.</p>
</div>
<div class="furry-card-footer">
<a href="{% url 'products:product_list' %}?fursuit_type=paws" class="furry-btn furry-btn-primary furry-btn-sm">
Entdecken
</a>
</div>
</div>
</div>
</div>
<!-- Featured Products -->
{% if featured_products %}
<div class="furry-card" style="margin-bottom: 2rem;">
<div class="furry-card-header">
<h2 class="furry-card-title">⭐ Empfohlene Produkte</h2>
<p class="furry-card-subtitle">Unsere beliebtesten Fursuits</p>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem;">
{% for product in featured_products %}
<div class="furry-card furry-product-card">
{% if product.image %}
<img src="{{ product.image.url }}" alt="{{ product.name }}" class="furry-card-image">
{% else %}
<div class="furry-card-image" style="background: linear-gradient(45deg, var(--furry-primary), var(--furry-secondary)); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem;">
🐾
</div>
{% endif %}
<div class="furry-card-header">
<div>
<h3 class="furry-card-title">{{ product.name }}</h3>
<p class="furry-card-subtitle">{{ product.get_fursuit_type_display }}</p>
</div>
<div class="furry-product-badge">Featured</div>
</div>
<div class="furry-card-content">
<p>{{ product.description|truncatewords:15 }}</p>
<div class="furry-product-price">{{ product.price }}€</div>
</div>
<div class="furry-card-footer">
<a href="{% url 'products:product_detail' product.id %}" class="furry-btn furry-btn-primary furry-btn-sm">
👁️ Details
</a>
<button class="furry-btn furry-btn-outline furry-btn-sm" onclick="addToCart({{ product.id }})">
🛒 In Warenkorb
</button>
</div>
</div>
{% endfor %}
</div>
<div style="text-align: center; margin-top: 2rem;">
<a href="{% url 'products:product_list' %}" class="furry-btn furry-btn-secondary">
Alle Produkte anzeigen
</a>
</div>
</div>
{% endif %}
<!-- Services Section -->
<div class="furry-card" style="margin-bottom: 2rem;">
<div class="furry-card-header">
<h2 class="furry-card-title">🛠️ Unsere Services</h2>
<p class="furry-card-subtitle">Was wir für dich tun können</p>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem;">
<div class="furry-card">
<div style="font-size: 3rem; text-align: center; margin-bottom: 1rem;">🎨</div>
<h3 style="text-align: center; margin-bottom: 1rem;">Custom Design</h3>
<p style="text-align: center;">Individuelle Fursuits nach deinen Wünschen. Wir arbeiten eng mit dir zusammen, um deinen Traum-Fursuit zu verwirklichen.</p>
<div style="text-align: center; margin-top: 1.5rem;">
<a href="{% url 'products:custom_order' %}" class="furry-btn furry-btn-primary">
Custom Order starten
</a>
</div>
</div>
<div class="furry-card">
<div style="font-size: 3rem; text-align: center; margin-bottom: 1rem;">🔧</div>
<h3 style="text-align: center; margin-bottom: 1rem;">Reparaturen</h3>
<p style="text-align: center;">Professionelle Reparaturen und Wartung für bestehende Fursuits. Wir bringen deinen Fursuit wieder zum Glänzen.</p>
<div style="text-align: center; margin-top: 1.5rem;">
<a href="{% url 'products:contact' %}" class="furry-btn furry-btn-secondary">
Reparatur anfragen
</a>
</div>
</div>
<div class="furry-card">
<div style="font-size: 3rem; text-align: center; margin-bottom: 1rem;">📦</div>
<h3 style="text-align: center; margin-bottom: 1rem;">Schnelle Lieferung</h3>
<p style="text-align: center;">Sichere Verpackung und schnelle Lieferung weltweit. Wir sorgen dafür, dass dein Fursuit sicher bei dir ankommt.</p>
<div style="text-align: center; margin-top: 1.5rem;">
<a href="{% url 'products:faq' %}" class="furry-btn furry-btn-outline">
Versand-Info
</a>
</div>
</div>
</div>
</div>
<!-- Call to Action -->
<div class="furry-card" style="background: linear-gradient(135deg, var(--furry-accent) 0%, var(--furry-warning) 100%); color: white; text-align: center; padding: 3rem 2rem;">
<div style="font-size: 3rem; margin-bottom: 1rem;">🎉</div>
<h2 style="margin-bottom: 1rem;">Bereit für deinen ersten Fursuit?</h2>
<p style="margin-bottom: 2rem; font-size: 1.1rem; opacity: 0.9;">Entdecke unsere Kollektion oder starte dein Custom Design Projekt!</p>
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
<a href="{% url 'products:product_list' %}" class="furry-btn furry-btn-primary furry-btn-lg" style="background: white; color: var(--furry-accent);">
🛍️ Shop durchsuchen
</a>
<a href="{% url 'products:custom_order' %}" class="furry-btn furry-btn-outline furry-btn-lg" style="color: white; border-color: white;">
🎨 Custom Order
</a>
</div>
</div>
<!-- Newsletter Signup -->
<div class="furry-card" style="margin-top: 2rem;">
<div style="text-align: center;">
<h3 style="margin-bottom: 1rem;">📧 Bleib auf dem Laufenden</h3>
<p style="margin-bottom: 2rem; color: var(--furry-text-secondary);">Erhalte Updates über neue Produkte, Special Offers und Fursuit-Tipps!</p>
<form method="post" style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; max-width: 500px; margin: 0 auto;">
{% csrf_token %}
<input type="email" name="email" placeholder="Deine E-Mail-Adresse" required
class="furry-input" style="flex: 1; min-width: 250px;">
<button type="submit" class="furry-btn furry-btn-primary">
📧 Anmelden
</button>
</form>
<p style="font-size: 0.8rem; color: var(--furry-text-secondary); margin-top: 1rem;">
Keine Spam! Du kannst dich jederzeit abmelden.
</p>
</div>
</div>
<script>
function addToCart(productId) {
fetch(`/add-to-cart/${productId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
},
})
.then(response => response.json())
.then(data => {
if (data.success) {
const alert = document.createElement('div');
alert.className = 'furry-alert furry-alert-success';
alert.innerHTML = `
<div class="furry-alert-icon"></div>
<div class="furry-alert-content">
<div class="furry-alert-title">Erfolg</div>
<div class="furry-alert-message">Produkt wurde zum Warenkorb hinzugefügt!</div>
</div>
`;
document.querySelector('main').insertBefore(alert, document.querySelector('main').firstChild);
setTimeout(() => alert.remove(), 3000);
}
})
.catch(error => {
console.error('Error:', error);
});
}
</script>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends 'base.html' %}
{% block title %}Bestellung erfolgreich{% endblock %}
{% block content %}
<div class="order-confirmation furry-404">
<div class="furry-404-emoji">🎉</div>
<h1>Danke für deine Bestellung!</h1>
<p>Wir haben deine Bestellung erhalten und unsere Furry-Freunde machen sich sofort an die Arbeit 🐾</p>
<div class="order-summary">
<h3>Deine Bestellung:</h3>
<ul>
{% for item in order.items.all %}
<li>{{ item.product_name }} × {{ item.quantity }} <span>{{ item.price|floatformat:2 }} €</span></li>
{% endfor %}
</ul>
<div class="checkout-total">Gesamtsumme: <span>{{ order.total_amount|floatformat:2 }} €</span></div>
</div>
<a href="/" class="btn furry-btn">Zurück zur Startseite</a>
</div>
{% endblock %}

View File

@ -0,0 +1,123 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Bestellhistorie - Fursuit Shop{% endblock %}
{% block content %}
<div class="container py-4">
<h1 class="mb-4">Meine Bestellungen</h1>
{% if orders %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Bestellnummer</th>
<th>Datum</th>
<th>Status</th>
<th>Zahlungsstatus</th>
<th>Gesamtbetrag</th>
<th>Details</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td>#{{ order.id }}</td>
<td>{{ order.created|date:"d.m.Y H:i" }}</td>
<td>
<span class="badge bg-{{ order.get_order_status_display_class }}">
{{ order.get_status_display }}
</span>
</td>
<td>
<span class="badge bg-{{ order.get_payment_status_display_class }}">
{{ order.get_payment_status_display }}
</span>
</td>
<td>{{ order.total_amount }} €</td>
<td>
<button type="button" class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#orderModal{{ order.id }}">
Details anzeigen
</button>
</td>
</tr>
<!-- Modal für Bestelldetails -->
<div class="modal fade" id="orderModal{{ order.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Bestellung #{{ order.id }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-md-6">
<h6>Lieferadresse</h6>
<p>
{{ order.full_name }}<br>
{{ order.address|linebreaksbr }}<br>
Tel: {{ order.phone }}
</p>
</div>
<div class="col-md-6">
<h6>Bestellinformationen</h6>
<p>
Datum: {{ order.created|date:"d.m.Y H:i" }}<br>
Status: {{ order.get_status_display }}<br>
Zahlungsmethode: {{ order.get_payment_method_display }}
</p>
</div>
</div>
<h6>Bestellte Artikel</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Artikel</th>
<th>Anzahl</th>
<th>Preis</th>
<th>Gesamt</th>
</tr>
</thead>
<tbody>
{% for item in order.items.all %}
<tr>
<td>{{ item.product_name }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.price }} €</td>
<td>{{ item.get_subtotal }} €</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-end"><strong>Gesamtbetrag:</strong></td>
<td><strong>{{ order.total_amount }} €</strong></td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
</div>
</div>
</div>
</div>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
Sie haben noch keine Bestellungen aufgegeben.
<a href="{% url 'products:shop' %}" class="alert-link">Jetzt einkaufen</a>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% load static %}
<div class="payment-buttons">
<!-- PayPal Button -->
{{ form.render }}
<!-- Alternative Zahlungsmethoden -->
<div class="mt-3">
<a href="{% url 'products:payment_process' order.id %}" class="btn btn-primary">
Mit Kreditkarte bezahlen
</a>
</div>
</div>
<style>
.payment-buttons {
max-width: 500px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #fff;
}
</style>

View File

@ -0,0 +1,53 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Zahlung fehlgeschlagen - {{ block.super }}{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h2 class="mb-0">Zahlung fehlgeschlagen</h2>
</div>
<div class="card-body">
<div class="text-center mb-4">
<i class="fas fa-times-circle text-danger" style="font-size: 4rem;"></i>
</div>
<h3>Bestellung #{{ order.id }}</h3>
<p>Leider konnte Ihre Zahlung nicht verarbeitet werden.</p>
<div class="alert alert-warning">
<h4>Mögliche Gründe:</h4>
<ul>
<li>Unzureichende Deckung auf Ihrem Konto</li>
<li>Falsche oder abgelaufene Kartendaten</li>
<li>Technische Probleme bei der Zahlungsabwicklung</li>
</ul>
</div>
<p>Sie können die Zahlung erneut versuchen oder eine andere Zahlungsmethode wählen.</p>
<div class="mt-4">
<a href="{% url 'products:payment_process' order.id %}" class="btn btn-primary">
Zahlung erneut versuchen
</a>
<a href="{% url 'products:cart_detail' %}" class="btn btn-outline-primary">
Zurück zum Warenkorb
</a>
</div>
<div class="mt-4">
<p>Bei weiteren Fragen kontaktieren Sie bitte unseren Kundenservice:</p>
<a href="{% url 'products:contact' %}" class="btn btn-outline-secondary">
Kontakt aufnehmen
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Zahlung - {{ block.super }}{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-primary text-white">
<h2 class="mb-0">Zahlungsprozess</h2>
</div>
<div class="card-body">
<h3 class="card-title">Bestellung #{{ order.id }}</h3>
<p class="card-text">Gesamtbetrag: {{ order.total_amount }} €</p>
<div class="alert alert-info">
<h4>Zahlungsinformationen</h4>
<p>Sie werden zu PayPal weitergeleitet, um die Zahlung sicher abzuschließen.</p>
</div>
<div class="text-center mt-4">
{{ form.render }}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,47 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Zahlung erfolgreich - {{ block.super }}{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card border-success">
<div class="card-header bg-success text-white">
<h2 class="mb-0">Zahlung erfolgreich!</h2>
</div>
<div class="card-body">
<div class="text-center mb-4">
<i class="fas fa-check-circle text-success" style="font-size: 4rem;"></i>
</div>
<h3>Bestellung #{{ order.id }}</h3>
<p>Vielen Dank für Ihre Bestellung! Ihre Zahlung wurde erfolgreich verarbeitet.</p>
<div class="alert alert-info">
<h4>Bestelldetails:</h4>
<ul class="list-unstyled">
<li>Bestellnummer: #{{ order.id }}</li>
<li>Gesamtbetrag: {{ order.total_amount }} €</li>
<li>Bestelldatum: {{ order.created|date:"d.m.Y H:i" }}</li>
<li>Status: {{ order.get_status_display }}</li>
</ul>
</div>
<p>Sie erhalten in Kürze eine Bestätigungs-E-Mail mit allen Details zu Ihrer Bestellung.</p>
<div class="mt-4">
<a href="{% url 'products:order_history' %}" class="btn btn-primary">
Zu meinen Bestellungen
</a>
<a href="{% url 'products:product_list' %}" class="btn btn-outline-primary">
Weiter einkaufen
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,552 @@
{% extends 'base.html' %}
{% block title %}{{ product.name }} - {{ block.super }}{% endblock %}
{% block extra_css %}
<style>
.product-container {
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
}
.card {
border-radius: 15px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border: none;
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid rgba(0,0,0,0.1);
border-radius: 15px 15px 0 0 !important;
}
.card-body {
border-radius: 15px;
}
.product-image-container {
position: relative;
width: 100%;
max-width: 500px;
margin: 0 auto 2rem;
padding: 8px;
background: linear-gradient(45deg, #8B5CF6, #EC4899);
border-radius: 20px;
}
.product-image {
width: 100%;
height: auto;
cursor: zoom-in;
border-radius: 15px;
transition: transform 0.3s ease;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
display: block;
}
.product-details {
text-align: center;
margin-bottom: 2rem;
}
.product-title {
font-size: 2.5rem;
margin-bottom: 1.5rem;
color: #333;
}
.product-description {
font-size: 1.1rem;
line-height: 1.6;
color: #666;
max-width: 800px;
margin: 0 auto 2rem;
}
.product-meta {
display: flex;
justify-content: center;
gap: 3rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.meta-item {
text-align: center;
}
.meta-label {
font-weight: bold;
color: #555;
margin-bottom: 0.5rem;
}
.meta-value {
color: #666;
}
.actions-container {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 2rem;
}
.image-zoom-modal {
display: none;
position: fixed;
z-index: 1000;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
overflow: auto;
}
.zoom-content {
margin: auto;
display: block;
max-width: 90%;
max-height: 90vh;
margin-top: 4%;
}
.close-zoom {
position: absolute;
top: 15px;
right: 35px;
color: #f1f1f1;
font-size: 40px;
font-weight: bold;
cursor: pointer;
}
.reviews-section {
max-width: 1000px;
margin: 0 auto;
}
.review-card {
text-align: left;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 1rem;
background-color: #f8f9fa;
}
.btn {
border-radius: 25px;
padding: 8px 20px;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(45deg, #8B5CF6, #6366F1);
border: none;
box-shadow: 0 4px 6px rgba(139, 92, 246, 0.2);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(139, 92, 246, 0.3);
background: linear-gradient(45deg, #7C3AED, #4F46E5);
}
.form-control {
border-radius: 25px;
border: 1px solid #e2e8f0;
padding: 8px 20px;
}
.form-control:focus {
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
border-color: #8B5CF6;
}
</style>
{% endblock %}
{% block content %}
<div class="product-container">
<div class="card">
<div class="card-body">
<div class="product-details">
<h1 class="product-title">{{ product.name }}</h1>
{% if product.image %}
<div class="product-image-container">
<img data-src="{{ product.image.url }}"
alt="{{ product.name }}"
class="product-image furry-lazy-image"
onclick="openZoom(this.src)">
</div>
<!-- Zoom Modal -->
<div id="imageZoomModal" class="image-zoom-modal">
<span class="close-zoom" onclick="closeZoom()">&times;</span>
<img class="zoom-content" id="zoomedImage">
</div>
{% endif %}
<p class="product-description">{{ product.description }}</p>
<div class="product-meta">
<div class="meta-item">
<div class="meta-label">Preis</div>
<div class="meta-value">{{ product.price }} €</div>
</div>
<div class="meta-item">
<div class="meta-label">Verfügbarkeit</div>
<div class="meta-value">{{ product.stock }} Stück</div>
</div>
<div class="meta-item">
<div class="meta-label">Hinzugefügt am</div>
<div class="meta-value">{{ product.created|date:"d.m.Y" }}</div>
</div>
<div class="meta-item">
<div class="meta-label">Bewertung</div>
<div class="meta-value">
{% with avg_rating=product.average_rating %}
{% if avg_rating > 0 %}
{{ avg_rating|floatformat:1 }} von 5 Sternen
{% else %}
Noch keine Bewertungen
{% endif %}
{% endwith %}
</div>
</div>
</div>
<div class="actions-container">
<form method="post" action="{% url 'products:add_to_cart' product.id %}" class="d-flex align-items-center">
{% csrf_token %}
<input type="number" name="quantity" value="1" min="1" max="{{ product.stock }}"
class="form-control me-2" style="width: 100px;">
<button type="submit" class="btn btn-primary">In den Warenkorb</button>
</form>
</div>
</div>
</div>
</div>
<!-- Bewertungen -->
<div class="reviews-section mt-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h3 class="mb-0">Bewertungen</h3>
{% if user.is_authenticated and not user_has_reviewed %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#reviewModal">
Bewertung schreiben
</button>
{% endif %}
</div>
<div class="card-body">
{% if product.reviews.all %}
{% for review in product.reviews.all %}
<div class="review-card">
<div class="d-flex justify-content-between">
<h5 class="mb-1">{{ review.user.username }}</h5>
<small class="text-muted">{{ review.created|date:"d.m.Y" }}</small>
</div>
<div class="mb-2">
{% for i in "12345"|make_list %}
{% if forloop.counter <= review.rating %}
<i class="bi bi-star-fill text-warning"></i>
{% else %}
<i class="bi bi-star text-warning"></i>
{% endif %}
{% endfor %}
</div>
<p class="mb-0">{{ review.comment }}</p>
</div>
{% if not forloop.last %}<hr>{% endif %}
{% endfor %}
{% else %}
<p class="text-center mb-0">Noch keine Bewertungen vorhanden.</p>
{% endif %}
</div>
</div>
</div>
<div class="social-sharing">
<div class="share-label">Teilen:</div>
<!-- Twitter/X -->
<a class="share-btn twitter" href="https://twitter.com/intent/tweet?url={{ request.build_absolute_uri }}&text=Schau%20dir%20dieses%20tolle%20Fursuit%20an!%20{{ product.name|urlencode }}" target="_blank" title="Auf Twitter/X teilen">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
<!-- Facebook -->
<a class="share-btn facebook" href="https://www.facebook.com/sharer/sharer.php?u={{ request.build_absolute_uri }}" target="_blank" title="Auf Facebook teilen">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
</a>
<!-- WhatsApp -->
<a class="share-btn whatsapp" href="https://wa.me/?text=Schau%20dir%20dieses%20tolle%20Fursuit%20an!%20{{ product.name|urlencode }}%20{{ request.build_absolute_uri }}" target="_blank" title="Auf WhatsApp teilen">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893A11.821 11.821 0 0020.885 3.488"/>
</svg>
</a>
<!-- Telegram -->
<a class="share-btn telegram" href="https://t.me/share/url?url={{ request.build_absolute_uri }}&text=Schau%20dir%20dieses%20tolle%20Fursuit%20an!%20{{ product.name|urlencode }}" target="_blank" title="Auf Telegram teilen">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
</svg>
</a>
<!-- Pinterest -->
<a class="share-btn pinterest" href="https://pinterest.com/pin/create/button/?url={{ request.build_absolute_uri }}&description=Schau%20dir%20dieses%20tolle%20Fursuit%20an!%20{{ product.name|urlencode }}" target="_blank" title="Auf Pinterest teilen">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.174-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.663.967-2.911 2.168-2.911 1.024 0 1.518.769 1.518 1.688 0 1.029-.653 2.567-.992 3.992-.285 1.193.6 2.165 1.775 2.165 2.128 0 3.768-2.245 3.768-5.487 0-2.861-2.063-4.869-5.008-4.869-3.41 0-5.409 2.562-5.409 5.199 0 1.033.394 2.143.889 2.741.099.12.112.225.085.345-.09.375-.293 1.199-.334 1.363-.053.225-.172.271-.402.165-1.495-.69-2.433-2.878-2.433-4.646 0-3.776 2.748-7.252 7.92-7.252 4.158 0 7.392 2.967 7.392 6.923 0 4.135-2.607 7.462-6.233 7.462-1.214 0-2.357-.629-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24.009 12.017 24.009c6.624 0 11.99-5.367 11.99-11.988C24.007 5.367 18.641.001 12.017.001z"/>
</svg>
</a>
<!-- LinkedIn -->
<a class="share-btn linkedin" href="https://www.linkedin.com/sharing/share-offsite/?url={{ request.build_absolute_uri }}" target="_blank" title="Auf LinkedIn teilen">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</a>
<!-- Email -->
<a class="share-btn email" href="mailto:?subject=Schau%20dir%20dieses%20tolle%20Fursuit%20an!&body=Hallo!%20Ich%20habe%20ein%20tolles%20Fursuit%20gefunden:%20{{ product.name|urlencode }}%20{{ request.build_absolute_uri }}" title="Per E-Mail teilen">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
</svg>
</a>
<!-- Copy Link -->
<button class="share-btn copy" onclick="copyToClipboard('{{ request.build_absolute_uri }}')" title="Link kopieren">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
</button>
</div>
</div>
<!-- Bewertungs-Modal -->
{% if user.is_authenticated and not user_has_reviewed %}
<div class="modal fade" id="reviewModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Bewertung schreiben</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="post" action="{% url 'products:add_review' product.id %}">
<div class="modal-body">
{% csrf_token %}
{% for field in review_form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Bewertung absenden</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
<script>
function openZoom(imgSrc) {
const modal = document.getElementById('imageZoomModal');
const zoomedImg = document.getElementById('zoomedImage');
modal.style.display = "block";
zoomedImg.src = imgSrc;
}
function closeZoom() {
document.getElementById('imageZoomModal').style.display = "none";
}
// Schließen beim Klick außerhalb des Bildes
window.onclick = function(event) {
const modal = document.getElementById('imageZoomModal');
if (event.target == modal) {
modal.style.display = "none";
}
}
// Schließen mit Escape-Taste
document.addEventListener('keydown', function(event) {
if (event.key === "Escape") {
document.getElementById('imageZoomModal').style.display = "none";
}
});
// Copy to Clipboard Function
function copyToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) {
// Modern approach
navigator.clipboard.writeText(text).then(() => {
showCopySuccess();
}).catch(err => {
console.error('Failed to copy: ', err);
fallbackCopyTextToClipboard(text);
});
} else {
// Fallback for older browsers
fallbackCopyTextToClipboard(text);
}
}
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
textArea.style.opacity = "0";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
showCopySuccess();
} else {
showCopyError();
}
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
showCopyError();
}
document.body.removeChild(textArea);
}
function showCopySuccess() {
const copyBtn = document.querySelector('.share-btn.copy');
const originalContent = copyBtn.innerHTML;
// Add success animation
copyBtn.classList.add('copied');
copyBtn.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
`;
// Show success message
const successMsg = document.createElement('div');
successMsg.className = 'copy-success-msg';
successMsg.textContent = 'Link kopiert! 🎉';
successMsg.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #25d366 0%, #20ba5a 100%);
color: white;
padding: 1rem 1.5rem;
border-radius: 16px;
font-weight: 700;
box-shadow: 0 4px 20px rgba(37, 211, 102, 0.3);
z-index: 10000;
animation: slideInRight 0.3s ease;
`;
document.body.appendChild(successMsg);
// Remove success message after 3 seconds
setTimeout(() => {
successMsg.style.animation = 'slideOutRight 0.3s ease';
setTimeout(() => {
document.body.removeChild(successMsg);
}, 300);
}, 3000);
// Reset button after animation
setTimeout(() => {
copyBtn.classList.remove('copied');
copyBtn.innerHTML = originalContent;
}, 600);
}
function showCopyError() {
const errorMsg = document.createElement('div');
errorMsg.className = 'copy-error-msg';
errorMsg.textContent = 'Kopieren fehlgeschlagen 😔';
errorMsg.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #ff6f91 0%, #e74c3c 100%);
color: white;
padding: 1rem 1.5rem;
border-radius: 16px;
font-weight: 700;
box-shadow: 0 4px 20px rgba(231, 76, 60, 0.3);
z-index: 10000;
animation: slideInRight 0.3s ease;
`;
document.body.appendChild(errorMsg);
setTimeout(() => {
errorMsg.style.animation = 'slideOutRight 0.3s ease';
setTimeout(() => {
document.body.removeChild(errorMsg);
}, 300);
}, 3000);
}
// Add CSS animations
const style = document.createElement('style');
style.textContent = `
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
// Social sharing analytics
document.querySelectorAll('.share-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
const platform = this.className.includes('twitter') ? 'twitter' :
this.className.includes('facebook') ? 'facebook' :
this.className.includes('whatsapp') ? 'whatsapp' :
this.className.includes('telegram') ? 'telegram' :
this.className.includes('pinterest') ? 'pinterest' :
this.className.includes('linkedin') ? 'linkedin' :
this.className.includes('email') ? 'email' : 'copy';
// Track social share (if analytics is available)
if (typeof gtag !== 'undefined') {
gtag('event', 'share', {
method: platform,
content_type: 'product',
item_id: '{{ product.id }}'
});
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,196 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Produkte - Kasico Fursuit Shop{% endblock %}
{% block content %}
<div class="furry-card">
<div class="furry-card-header">
<div>
<h1 class="furry-card-title">🐾 Unsere Fursuits</h1>
<p class="furry-card-subtitle">Entdecke unsere handgefertigten Fursuits und Accessoires</p>
</div>
<div class="furry-product-badge">Neu</div>
</div>
<!-- Filter Section -->
<div class="furry-card" style="margin-bottom: 2rem;">
<h3>🔍 Filter & Suche</h3>
<form method="get" class="furry-form-group">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
<div>
<label class="furry-label">Fursuit Typ</label>
<select name="fursuit_type" class="furry-input furry-select">
<option value="">Alle Typen</option>
<option value="partial">Partial</option>
<option value="fullsuit">Fullsuit</option>
<option value="head_only">Head Only</option>
<option value="paws">Paws</option>
<option value="tail">Tail</option>
</select>
</div>
<div>
<label class="furry-label">Stil</label>
<select name="style" class="furry-input furry-select">
<option value="">Alle Stile</option>
<option value="toony">Toony</option>
<option value="semi_realistic">Semi-Realistic</option>
<option value="realistic">Realistic</option>
<option value="anime">Anime</option>
<option value="chibi">Chibi</option>
</select>
</div>
<div>
<label class="furry-label">Preisbereich</label>
<select name="price_range" class="furry-input furry-select">
<option value="">Alle Preise</option>
<option value="0-500">0 - 500€</option>
<option value="500-1000">500 - 1000€</option>
<option value="1000-2000">1000 - 2000€</option>
<option value="2000+">2000€+</option>
</select>
</div>
<div>
<label class="furry-label">Sortierung</label>
<select name="sort" class="furry-input furry-select">
<option value="newest">Neueste zuerst</option>
<option value="price_low">Preis: Niedrig zu Hoch</option>
<option value="price_high">Preis: Hoch zu Niedrig</option>
<option value="name">Name A-Z</option>
</select>
</div>
</div>
<div style="margin-top: 1rem; display: flex; gap: 1rem; flex-wrap: wrap;">
<button type="submit" class="furry-btn furry-btn-primary">
🔍 Filter anwenden
</button>
<a href="{% url 'products:product_list' %}" class="furry-btn furry-btn-ghost">
🗑️ Filter zurücksetzen
</a>
</div>
</form>
</div>
<!-- Products Grid -->
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem;">
{% for product in products %}
<div class="furry-card furry-product-card">
{% if product.image %}
<img data-src="{{ product.image.url }}" alt="{{ product.name }}" class="furry-card-image furry-lazy-image">
{% else %}
<div class="furry-card-image" style="background: linear-gradient(45deg, var(--furry-primary), var(--furry-secondary)); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem;">
🐾
</div>
{% endif %}
<div class="furry-card-header">
<div>
<h3 class="furry-card-title">{{ product.name }}</h3>
<p class="furry-card-subtitle">{{ product.get_fursuit_type_display }} • {{ product.get_style_display }}</p>
</div>
{% if product.featured %}
<div class="furry-product-badge">Featured</div>
{% endif %}
</div>
<div class="furry-card-content">
<p>{{ product.description|truncatewords:20 }}</p>
<div class="furry-product-price">{{ product.price }}€</div>
</div>
<div class="furry-card-footer">
<a href="{% url 'products:product_detail' product.id %}" class="furry-btn furry-btn-primary furry-btn-sm">
👁️ Details
</a>
<button class="furry-btn furry-btn-outline furry-btn-sm" onclick="addToCart({{ product.id }})">
🛒 In Warenkorb
</button>
<button class="furry-btn furry-btn-ghost furry-btn-sm" onclick="furryAjax.addToWishlist({{ product.id }})">
❤️ Zur Wunschliste
</button>
</div>
</div>
{% empty %}
<div class="furry-card" style="grid-column: 1 / -1; text-align: center; padding: 3rem;">
<div style="font-size: 4rem; margin-bottom: 1rem;">🐾</div>
<h3>Keine Produkte gefunden</h3>
<p>Versuche andere Filter-Einstellungen oder schaue später wieder vorbei!</p>
<a href="{% url 'products:product_list' %}" class="furry-btn furry-btn-primary">
Alle Produkte anzeigen
</a>
</div>
{% endfor %}
</div>
<!-- Pagination -->
{% if products.has_other_pages %}
<div class="furry-card" style="margin-top: 2rem; text-align: center;">
<div style="display: flex; justify-content: center; gap: 0.5rem; flex-wrap: wrap;">
{% if products.has_previous %}
<a href="?page={{ products.previous_page_number }}" class="furry-btn furry-btn-ghost furry-btn-sm">
← Zurück
</a>
{% endif %}
{% for num in products.paginator.page_range %}
{% if products.number == num %}
<span class="furry-btn furry-btn-primary furry-btn-sm">{{ num }}</span>
{% elif num > products.number|add:'-3' and num < products.number|add:'3' %}
<a href="?page={{ num }}" class="furry-btn furry-btn-ghost furry-btn-sm">{{ num }}</a>
{% endif %}
{% endfor %}
{% if products.has_next %}
<a href="?page={{ products.next_page_number }}" class="furry-btn furry-btn-ghost furry-btn-sm">
Weiter →
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- Success/Error Messages -->
{% if messages %}
{% for message in messages %}
<div class="furry-alert furry-alert-{{ message.tags }}">
<div class="furry-alert-icon">
{% if message.tags == 'success' %}✅
{% elif message.tags == 'error' %}❌
{% elif message.tags == 'warning' %}⚠️
{% else %}{% endif %}
</div>
<div class="furry-alert-content">
<div class="furry-alert-title">{{ message.tags|title }}</div>
<div class="furry-alert-message">{{ message }}</div>
</div>
</div>
{% endfor %}
{% endif %}
<div id="cart-feedback" class="cart-added-feedback">✔️ Zum Warenkorb hinzugefügt!</div>
<div id="furry-spinner" class="furry-spinner" style="display:none;">
<div></div><div></div><div></div>
</div>
<script>
function showCartFeedback() {
const feedback = document.getElementById('cart-feedback');
feedback.classList.add('show');
setTimeout(() => feedback.classList.remove('show'), 1500);
}
function showSpinner(show) {
document.getElementById('furry-spinner').style.display = show ? 'inline-block' : 'none';
}
window.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.add-to-cart-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
showSpinner(true);
setTimeout(() => {
showSpinner(false);
showCartFeedback();
}, 1000);
});
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,28 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}Profil{% endblock %}
{% block content %}
<div class="profile-card">
<h2>Willkommen, {{ user.username }}!</h2>
<div class="profile-info">
<p><strong>E-Mail:</strong> {{ user.email }}</p>
<p><strong>Mitglied seit:</strong> {{ user.date_joined|date:"d.m.Y" }}</p>
</div>
<div class="profile-orders">
<h3>Bestellungen</h3>
{% if orders %}
<ul>
{% for order in orders %}
<li>Bestellung #{{ order.id }} vom {{ order.created|date:"d.m.Y" }} {{ order.total_amount|floatformat:2 }} €</li>
{% endfor %}
</ul>
{% else %}
<p>Du hast noch keine Bestellungen.</p>
{% endif %}
</div>
<a href="/products/wishlist/" class="btn furry-btn-outline">Wunschliste ansehen</a>
<a href="/logout/" class="btn furry-btn-secondary">Logout</a>
</div>
{% endblock %}

View File

@ -0,0 +1,906 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Meine Wunschliste - Kasico Fursuit Shop{% endblock %}
{% block content %}
<div class="wishlist-container">
<!-- Hero Section -->
<div class="wishlist-hero furry-card text-center mb-5">
<div class="wishlist-hero-content">
<h1 class="wishlist-title">❤️ Meine Wunschliste</h1>
<p class="wishlist-subtitle">Deine gesammelten Favoriten und Traum-Fursuits</p>
<div class="wishlist-stats">
<span class="wishlist-stat">{{ wishlist.products.count }} Produkte</span>
<span class="wishlist-stat">💕 Gespeichert</span>
</div>
</div>
</div>
<!-- Messages -->
{% if messages %}
<div class="messages-section mb-5">
{% for message in messages %}
<div class="message-card message-{{ message.tags }}">
<div class="message-icon">
{% if message.tags == 'success' %}✅
{% elif message.tags == 'error' %}❌
{% elif message.tags == 'warning' %}⚠️
{% else %}{% endif %}
</div>
<div class="message-content">
<p class="message-text">{{ message }}</p>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Wishlist Content -->
<div class="wishlist-content">
{% if wishlist.products.all %}
<!-- Filters and Actions -->
<div class="wishlist-controls furry-card mb-4">
<div class="controls-header">
<h3 class="controls-title">🎛️ Wunschliste verwalten</h3>
<div class="controls-actions">
<button class="btn furry-btn-outline" id="selectAllBtn">
📋 Alle auswählen
</button>
<button class="btn furry-btn-secondary" id="addSelectedToCartBtn">
🛒 Ausgewählte in Warenkorb
</button>
</div>
</div>
<div class="filters-section">
<div class="filter-group">
<label class="filter-label">🔍 Filter:</label>
<select class="filter-select" id="typeFilter">
<option value="">Alle Typen</option>
<option value="fullsuit">Fullsuit</option>
<option value="partial">Partial</option>
<option value="head">Head</option>
<option value="paws">Paws</option>
</select>
<select class="filter-select" id="styleFilter">
<option value="">Alle Styles</option>
<option value="realistic">Realistic</option>
<option value="cartoon">Cartoon</option>
<option value="toony">Toony</option>
<option value="kemono">Kemono</option>
</select>
</div>
<div class="sort-group">
<label class="sort-label">📊 Sortieren:</label>
<select class="sort-select" id="sortSelect">
<option value="name">Name A-Z</option>
<option value="price-low">Preis: Niedrig → Hoch</option>
<option value="price-high">Preis: Hoch → Niedrig</option>
<option value="date-added">Hinzugefügt: Neueste</option>
</select>
</div>
</div>
</div>
<!-- Products Grid -->
<div class="wishlist-grid">
{% for product in wishlist.products.all %}
<div class="wishlist-item furry-card" data-type="{{ product.fursuit_type }}" data-style="{{ product.style }}" data-price="{{ product.price }}">
<div class="wishlist-item-header">
<div class="wishlist-checkbox">
<input type="checkbox" class="product-checkbox" id="product-{{ product.id }}" data-product-id="{{ product.id }}">
<label for="product-{{ product.id }}" class="checkbox-label"></label>
</div>
<button class="remove-btn" data-product-id="{{ product.id }}" title="Von Wunschliste entfernen">
</button>
</div>
<div class="wishlist-item-image">
{% if product.image %}
<img src="{{ product.image.url }}" alt="{{ product.name }}" class="product-image furry-lazy-image">
{% else %}
<img src="{% static 'images/placeholder.png' %}" alt="Placeholder" class="product-image">
{% endif %}
<div class="image-overlay">
<div class="overlay-buttons">
<a href="{% url 'products:product_detail' product.id %}" class="overlay-btn">
👁️ Details
</a>
<button class="overlay-btn add-to-cart-btn" data-product-id="{{ product.id }}">
🛒 In Warenkorb
</button>
</div>
</div>
</div>
<div class="wishlist-item-content">
<h4 class="product-title">{{ product.name }}</h4>
<p class="product-description">{{ product.description|truncatewords:15 }}</p>
<div class="product-details">
<div class="detail-item">
<span class="detail-label">💰 Preis:</span>
<span class="detail-value price">{{ product.price }}€</span>
</div>
<div class="detail-item">
<span class="detail-label">🎭 Typ:</span>
<span class="detail-value">{{ product.get_fursuit_type_display }}</span>
</div>
<div class="detail-item">
<span class="detail-label">🎨 Style:</span>
<span class="detail-value">{{ product.get_style_display }}</span>
</div>
</div>
<div class="product-actions">
<a href="{% url 'products:product_detail' product.id %}" class="btn furry-btn">
👁️ Details anzeigen
</a>
<button class="btn furry-btn-secondary add-to-cart-btn" data-product-id="{{ product.id }}">
🛒 In Warenkorb
</button>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Bulk Actions -->
<div class="bulk-actions furry-card mt-4" id="bulkActions" style="display: none;">
<div class="bulk-actions-content">
<span class="bulk-text">📦 <span id="selectedCount">0</span> Produkte ausgewählt</span>
<div class="bulk-buttons">
<button class="btn furry-btn" id="addSelectedToCart">
🛒 In Warenkorb legen
</button>
<button class="btn furry-btn-outline" id="removeSelected">
❌ Von Wunschliste entfernen
</button>
</div>
</div>
</div>
{% else %}
<!-- Empty State -->
<div class="empty-wishlist furry-card text-center">
<div class="empty-icon">💔</div>
<h3 class="empty-title">Deine Wunschliste ist leer</h3>
<p class="empty-description">
Du hast noch keine Fursuits zu deiner Wunschliste hinzugefügt.
Entdecke unsere Kollektion und sammle deine Favoriten!
</p>
<div class="empty-actions">
<a href="{% url 'products:product_list' %}" class="btn furry-btn">
🛍️ Shop durchsuchen
</a>
<a href="{% url 'products:gallery' %}" class="btn furry-btn-secondary">
🖼️ Galerie ansehen
</a>
</div>
</div>
{% endif %}
</div>
</div>
<style>
:root {
--primary-color: #8B5CF6;
--secondary-color: #EC4899;
--accent-color: #F59E0B;
--dark-color: #1F2937;
--light-color: #F3E8FF;
--white-color: #FFFFFF;
--gradient-start: #8B5CF6;
--gradient-middle: #EC4899;
--success-color: #10B981;
--warning-color: #F59E0B;
--error-color: #EF4444;
}
.wishlist-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem 1rem;
}
.wishlist-hero {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
color: white;
padding: 3rem 2rem;
border-radius: 20px;
margin-bottom: 2rem;
}
.wishlist-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
}
.wishlist-subtitle {
font-size: 1.2rem;
opacity: 0.9;
margin-bottom: 2rem;
}
.wishlist-stats {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
}
.wishlist-stat {
background: rgba(255, 255, 255, 0.2);
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 600;
}
.messages-section {
display: grid;
gap: 1rem;
}
.message-card {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border-radius: 10px;
background: white;
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.1);
}
.message-success {
border-left: 4px solid var(--success-color);
}
.message-error {
border-left: 4px solid var(--error-color);
}
.message-warning {
border-left: 4px solid var(--warning-color);
}
.message-info {
border-left: 4px solid var(--primary-color);
}
.message-icon {
font-size: 1.5rem;
min-width: 2rem;
}
.message-content {
flex: 1;
}
.message-text {
margin: 0;
color: var(--dark-color);
font-weight: 500;
}
.wishlist-controls {
padding: 2rem;
}
.controls-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.controls-title {
color: var(--dark-color);
font-weight: 700;
margin: 0;
}
.controls-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.filters-section {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.filter-group, .sort-group {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.filter-label, .sort-label {
font-weight: 600;
color: var(--dark-color);
margin: 0;
}
.filter-select, .sort-select {
padding: 0.5rem 1rem;
border: 2px solid var(--light-color);
border-radius: 10px;
background: white;
color: var(--dark-color);
font-size: 0.9rem;
transition: all 0.3s ease;
}
.filter-select:focus, .sort-select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
}
.wishlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
}
.wishlist-item {
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.1);
transition: all 0.3s ease;
position: relative;
}
.wishlist-item:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(139, 92, 246, 0.15);
}
.wishlist-item-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: var(--light-color);
}
.wishlist-checkbox {
position: relative;
}
.product-checkbox {
display: none;
}
.checkbox-label {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--primary-color);
border-radius: 5px;
cursor: pointer;
position: relative;
transition: all 0.3s ease;
}
.product-checkbox:checked + .checkbox-label {
background: var(--primary-color);
}
.product-checkbox:checked + .checkbox-label::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 12px;
font-weight: bold;
}
.remove-btn {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 50%;
transition: all 0.3s ease;
}
.remove-btn:hover {
background: var(--error-color);
color: white;
transform: scale(1.1);
}
.wishlist-item-image {
position: relative;
overflow: hidden;
height: 200px;
}
.product-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.wishlist-item:hover .product-image {
transform: scale(1.05);
}
.image-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(139, 92, 246, 0.9);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.wishlist-item:hover .image-overlay {
opacity: 1;
}
.overlay-buttons {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.overlay-btn {
background: white;
color: var(--primary-color);
border: none;
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 600;
font-size: 0.8rem;
text-decoration: none;
transition: all 0.3s ease;
}
.overlay-btn:hover {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
text-decoration: none;
}
.wishlist-item-content {
padding: 1.5rem;
}
.product-title {
color: var(--dark-color);
font-weight: 700;
margin-bottom: 0.5rem;
font-size: 1.2rem;
}
.product-description {
color: var(--dark-color);
opacity: 0.8;
margin-bottom: 1rem;
line-height: 1.5;
}
.product-details {
display: grid;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid var(--light-color);
}
.detail-item:last-child {
border-bottom: none;
}
.detail-label {
font-weight: 600;
color: var(--dark-color);
opacity: 0.8;
}
.detail-value {
color: var(--dark-color);
font-weight: 600;
}
.price {
color: var(--primary-color);
font-size: 1.1rem;
}
.product-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.bulk-actions {
padding: 1.5rem;
background: var(--light-color);
}
.bulk-actions-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.bulk-text {
font-weight: 600;
color: var(--dark-color);
}
.bulk-buttons {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.empty-wishlist {
padding: 4rem 2rem;
text-align: center;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 2rem;
}
.empty-title {
color: var(--dark-color);
font-weight: 700;
margin-bottom: 1rem;
font-size: 1.5rem;
}
.empty-description {
color: var(--dark-color);
opacity: 0.8;
margin-bottom: 2rem;
line-height: 1.6;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.empty-actions {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.furry-btn {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 25px;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.furry-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
color: white;
text-decoration: none;
}
.furry-btn-secondary {
background: var(--secondary-color);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 25px;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.furry-btn-secondary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(236, 72, 153, 0.3);
color: white;
text-decoration: none;
}
.furry-btn-outline {
background: white;
color: var(--primary-color);
border: 2px solid var(--primary-color);
padding: 0.75rem 1.5rem;
border-radius: 25px;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.furry-btn-outline:hover {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
text-decoration: none;
}
@media (max-width: 768px) {
.wishlist-container {
padding: 1rem;
}
.wishlist-title {
font-size: 2rem;
}
.wishlist-stats {
flex-direction: column;
gap: 1rem;
}
.wishlist-stat {
width: 100%;
text-align: center;
}
.wishlist-grid {
grid-template-columns: 1fr;
}
.controls-header, .filters-section {
flex-direction: column;
align-items: flex-start;
}
.bulk-actions-content {
flex-direction: column;
align-items: flex-start;
}
}
/* Animation für Wishlist Items */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.wishlist-item {
animation: fadeInUp 0.6s ease-out;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const checkboxes = document.querySelectorAll('.product-checkbox');
const selectAllBtn = document.getElementById('selectAllBtn');
const bulkActions = document.getElementById('bulkActions');
const selectedCount = document.getElementById('selectedCount');
const addSelectedToCartBtn = document.getElementById('addSelectedToCartBtn');
const removeSelectedBtn = document.getElementById('removeSelected');
const removeButtons = document.querySelectorAll('.remove-btn');
const addToCartButtons = document.querySelectorAll('.add-to-cart-btn');
const typeFilter = document.getElementById('typeFilter');
const styleFilter = document.getElementById('styleFilter');
const sortSelect = document.getElementById('sortSelect');
const wishlistItems = document.querySelectorAll('.wishlist-item');
// Checkbox functionality
function updateBulkActions() {
const checkedBoxes = document.querySelectorAll('.product-checkbox:checked');
const totalBoxes = document.querySelectorAll('.product-checkbox');
if (checkedBoxes.length > 0) {
bulkActions.style.display = 'block';
selectedCount.textContent = checkedBoxes.length;
if (checkedBoxes.length === totalBoxes.length) {
selectAllBtn.textContent = '📋 Auswahl aufheben';
} else {
selectAllBtn.textContent = '📋 Alle auswählen';
}
} else {
bulkActions.style.display = 'none';
}
}
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', updateBulkActions);
});
// Select all functionality
selectAllBtn.addEventListener('click', function() {
const allChecked = document.querySelectorAll('.product-checkbox:checked').length === checkboxes.length;
checkboxes.forEach(checkbox => {
checkbox.checked = !allChecked;
});
updateBulkActions();
});
// Remove from wishlist
removeButtons.forEach(button => {
button.addEventListener('click', function() {
const productId = this.dataset.productId;
if (confirm('Möchtest du dieses Produkt wirklich von deiner Wunschliste entfernen?')) {
fetch(`/products/remove-from-wishlist/${productId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
},
})
.then(response => response.json())
.then(data => {
if (data.success) {
this.closest('.wishlist-item').style.animation = 'fadeOut 0.3s ease-out';
setTimeout(() => {
this.closest('.wishlist-item').remove();
updateBulkActions();
}, 300);
}
})
.catch(error => console.error('Error:', error));
}
});
});
// Add to cart functionality
addToCartButtons.forEach(button => {
button.addEventListener('click', function() {
const productId = this.dataset.productId;
fetch('/products/add-to-cart/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
},
body: JSON.stringify({
product_id: productId,
quantity: 1
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Show success animation
this.innerHTML = '✅ Hinzugefügt!';
this.style.background = 'var(--success-color)';
setTimeout(() => {
this.innerHTML = '🛒 In Warenkorb';
this.style.background = '';
}, 2000);
}
})
.catch(error => console.error('Error:', error));
});
});
// Filter functionality
function filterItems() {
const selectedType = typeFilter.value;
const selectedStyle = styleFilter.value;
wishlistItems.forEach(item => {
const itemType = item.dataset.type;
const itemStyle = item.dataset.style;
const typeMatch = !selectedType || itemType === selectedType;
const styleMatch = !selectedStyle || itemStyle === selectedStyle;
if (typeMatch && styleMatch) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}
typeFilter.addEventListener('change', filterItems);
styleFilter.addEventListener('change', filterItems);
// Sort functionality
sortSelect.addEventListener('change', function() {
const sortBy = this.value;
const items = Array.from(wishlistItems);
items.sort((a, b) => {
switch(sortBy) {
case 'name':
return a.querySelector('.product-title').textContent.localeCompare(b.querySelector('.product-title').textContent);
case 'price-low':
return parseFloat(a.dataset.price) - parseFloat(b.dataset.price);
case 'price-high':
return parseFloat(b.dataset.price) - parseFloat(a.dataset.price);
case 'date-added':
// Assuming items are already in order of addition
return 0;
default:
return 0;
}
});
const grid = document.querySelector('.wishlist-grid');
items.forEach(item => grid.appendChild(item));
});
// Bulk actions
addSelectedToCartBtn.addEventListener('click', function() {
const selectedProducts = document.querySelectorAll('.product-checkbox:checked');
selectedProducts.forEach(checkbox => {
const productId = checkbox.dataset.productId;
const addToCartBtn = document.querySelector(`.add-to-cart-btn[data-product-id="${productId}"]`);
addToCartBtn.click();
});
});
removeSelectedBtn.addEventListener('click', function() {
if (confirm('Möchtest du alle ausgewählten Produkte von deiner Wunschliste entfernen?')) {
const selectedProducts = document.querySelectorAll('.product-checkbox:checked');
selectedProducts.forEach(checkbox => {
const productId = checkbox.dataset.productId;
const removeBtn = document.querySelector(`.remove-btn[data-product-id="${productId}"]`);
removeBtn.click();
});
}
});
});
</script>
<style>
/* Fade out animation */
@keyframes fadeOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.8);
}
}
</style>
{% endblock %}

View File

@ -0,0 +1,224 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Registrieren - {{ block.super }}{% endblock %}
{% block content %}
<div class="content-container">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="furry-card">
<!-- Header -->
<div class="text-center mb-4">
<img src="{% static 'images/kasicoLogo.png' %}"
alt="Kasico Art & Design Logo"
class="img-fluid mb-4"
style="max-width: 150px; height: auto;">
<h1 class="h3 mb-3">🐾 Registrieren</h1>
<p class="text-muted">Werde Teil der Furry-Community!</p>
</div>
<form method="post" class="needs-validation furry-form" novalidate>
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
{% for field in form %}
<div class="form-group mb-3">
<label for="{{ field.id_for_label }}" class="form-label">
{{ field.label }}
</label>
{{ field }}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary furry-btn">
🐾 Registrieren
</button>
</div>
</form>
<div class="text-center mt-4">
<p>Bereits ein Konto? <a href="{% url 'login' %}">Jetzt anmelden</a></p>
<p><a href="{% url 'password_reset' %}">Passwort vergessen?</a></p>
</div>
</div>
</div>
</div>
</div>
<style>
:root {
--primary-color: #8B5CF6; /* Helles Lila */
--secondary-color: #EC4899; /* Pink */
--accent-color: #F59E0B; /* Orange */
--dark-color: #1F2937; /* Dunkelgrau */
--light-color: #F3E8FF; /* Helles Lila */
--white-color: #FFFFFF;
--gradient-start: #8B5CF6;
--gradient-middle: #EC4899;
}
.furry-card {
background: white;
border-radius: 20px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.1);
margin: 2rem 0;
}
.furry-form input, .furry-form textarea {
width: 100%;
padding: 0.75rem;
border-radius: 10px;
border: 2px solid var(--light-color);
transition: all 0.3s ease;
font-size: 1rem;
}
.furry-form input:focus, .furry-form textarea:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(139, 92, 246, 0.25);
outline: none;
}
.furry-form input.invalid, .furry-form textarea.invalid {
border-color: #EF4444;
box-shadow: 0 0 0 0.25rem rgba(239, 68, 68, 0.25);
}
.furry-form input.valid, .furry-form textarea.valid {
border-color: #10B981;
box-shadow: 0 0 0 0.25rem rgba(16, 185, 129, 0.25);
}
.btn-primary {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-middle));
border: none;
padding: 0.75rem 1.5rem;
border-radius: 50px;
transition: all 0.3s ease;
font-weight: 700;
font-size: 1.1rem;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
}
a {
color: var(--primary-color);
text-decoration: none;
transition: all 0.3s ease;
font-weight: 600;
}
a:hover {
color: var(--secondary-color);
}
.form-text {
font-size: 0.875rem;
color: var(--dark-color);
opacity: 0.7;
margin-top: 0.25rem;
}
.alert-danger {
background: linear-gradient(135deg, #FEE2E2, #FECACA);
border: 2px solid #EF4444;
border-radius: 10px;
color: #991B1B;
padding: 1rem;
margin-bottom: 1rem;
}
.invalid-feedback {
color: #EF4444;
font-size: 0.875rem;
margin-top: 0.25rem;
}
/* Animation für erfolgreiche Registrierung */
@keyframes success-bounce {
0%, 20%, 53%, 80%, 100% {
transform: translate3d(0,0,0);
}
40%, 43% {
transform: translate3d(0, -30px, 0);
}
70% {
transform: translate3d(0, -15px, 0);
}
90% {
transform: translate3d(0, -4px, 0);
}
}
.success-animation {
animation: success-bounce 1s ease-in-out;
}
</style>
{% block extra_js %}
<script>
// Formular-Validierung
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('.furry-form');
const inputs = form.querySelectorAll('input, textarea');
inputs.forEach(input => {
input.addEventListener('blur', function() {
validateField(this);
});
input.addEventListener('input', function() {
if (this.classList.contains('invalid')) {
validateField(this);
}
});
});
function validateField(field) {
if (field.checkValidity()) {
field.classList.remove('invalid');
field.classList.add('valid');
} else {
field.classList.remove('valid');
field.classList.add('invalid');
}
}
// Erfolgreiche Registrierung Animation
// {% if form.is_valid and not form.errors %}
// document.querySelector('.furry-card').classList.add('success-animation');
// {% endif %}
});
// Formular neu laden, wenn der Benutzer zurück navigiert
window.addEventListener('pageshow', function(event) {
if (event.persisted) {
window.location.reload();
}
});
</script>
{% endblock %}
{% endblock %}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,10 @@
from django import template
register = template.Library()
@register.filter
def multiply(value, arg):
try:
return int(value) * int(arg)
except (ValueError, TypeError):
return ''

3
products/tests.py Normal file
View File

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

51
products/urls.py Normal file
View File

@ -0,0 +1,51 @@
from django.urls import path
from .views import (
ProductListView, ProductDetailView,
add_to_cart, cart_detail, update_cart_item,
remove_from_cart, create_order, checkout,
add_review, profile_view, order_history,
wishlist_view, add_to_wishlist, remove_from_wishlist,
faq_list, contact, contact_success,
custom_order, custom_order_success,
custom_order_detail, gallery,
add_progress_update,
payment_process,
payment_success,
payment_failed,
register
)
from . import sitemap_views
app_name = 'products'
urlpatterns = [
path('', ProductListView.as_view(), name='product_list'),
path('product/<int:pk>/', ProductDetailView.as_view(), name='product_detail'),
path('cart/add/<int:product_id>/', add_to_cart, name='add_to_cart'),
path('cart/', cart_detail, name='cart_detail'),
path('cart/update/<int:item_id>/', update_cart_item, name='update_cart_item'),
path('cart/remove/<int:item_id>/', remove_from_cart, name='remove_from_cart'),
path('order/create/', create_order, name='create_order'),
path('checkout/<int:order_id>/', checkout, name='checkout'),
path('product/<int:product_id>/review/', add_review, name='add_review'),
path('profile/', profile_view, name='profile'),
path('orders/', order_history, name='order_history'),
path('wishlist/', wishlist_view, name='wishlist'),
path('wishlist/add/<int:product_id>/', add_to_wishlist, name='add_to_wishlist'),
path('wishlist/remove/<int:product_id>/', remove_from_wishlist, name='remove_from_wishlist'),
path('faq/', faq_list, name='faq'),
path('contact/', contact, name='contact'),
path('contact/success/', contact_success, name='contact_success'),
path('custom-order/', custom_order, name='custom_order'),
path('custom-order/success/<int:order_id>/', custom_order_success, name='custom_order_success'),
path('custom-orders/<int:order_id>/', custom_order_detail, name='custom_order_detail'),
path('custom-orders/<int:order_id>/update/', add_progress_update, name='add_progress_update'),
path('gallery/', gallery, name='gallery'),
path('payment/process/<int:order_id>/', payment_process, name='payment_process'),
path('payment/success/<int:order_id>/', payment_success, name='payment_success'),
path('payment/failed/<int:order_id>/', payment_failed, name='payment_failed'),
path('register/', register, name='register'),
path('sitemap.xml', sitemap_views.sitemap_xml, name='sitemap_xml'),
path('robots.txt', sitemap_views.robots_txt, name='robots_txt'),
path('manifest.json', sitemap_views.manifest_json, name='manifest_json'),
]

838
products/views.py Normal file
View File

@ -0,0 +1,838 @@
from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic import ListView, DetailView
from django.contrib import messages
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from django.db.models import Q, Avg, Count
from django.db import transaction
# from rest_framework import viewsets # Temporär auskommentiert
# from rest_framework.permissions import IsAuthenticatedOrReadOnly # Temporär auskommentiert
from .models import Product, Cart, CartItem, Order, OrderItem, Review, UserProfile, Wishlist, FAQ, ContactMessage, CustomOrder, OrderProgress, GalleryImage, Category
from .forms import OrderForm, CustomUserCreationForm, CustomAuthenticationForm, ReviewForm, UserProfileForm, ContactForm, CustomOrderForm, OrderProgressForm
# from .serializers import ProductSerializer, ReviewSerializer # Temporär auskommentiert
import stripe
from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone
from django.core.mail import send_mail
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
import json
# import paypalrestsdk # Temporär auskommentiert
# from payments import get_payment_model, RedirectNeeded # Temporär auskommentiert
# from paypal.standard.ipn.models import PayPalIPN # Temporär auskommentiert
from django.urls import reverse
from django.http import HttpResponse
# from paypal.standard.forms import PayPalPaymentsForm # Temporär auskommentiert
import logging
from django.core.exceptions import ValidationError
from django.http import Http404
logger = logging.getLogger(__name__)
# Create your views here.
class ProductListView(ListView):
model = Product
template_name = 'products/product_list.html'
context_object_name = 'products'
paginate_by = 12
def get_queryset(self):
try:
queryset = Product.objects.all().annotate(
average_rating=Avg('reviews__rating'),
review_count=Count('reviews')
).select_related('category').prefetch_related(
'reviews__user', # Optimiert N+1 Queries für Reviews
'wishlist_users' # Optimiert Wishlist-Queries
)
# Erweiterte Suchfilter
search_query = self.request.GET.get('search')
if search_query:
# Verbesserte Suche mit Q-Objekten
search_terms = search_query.split()
q_objects = Q()
for term in search_terms:
q_objects |= (
Q(name__icontains=term) |
Q(description__icontains=term) |
Q(fursuit_type__icontains=term) |
Q(style__icontains=term) |
Q(category__name__icontains=term)
)
queryset = queryset.filter(q_objects)
# Preisbereich Filter mit Validierung
min_price = self.request.GET.get('min_price')
max_price = self.request.GET.get('max_price')
if min_price and min_price.isdigit():
queryset = queryset.filter(price__gte=float(min_price))
if max_price and max_price.isdigit():
queryset = queryset.filter(price__lte=float(max_price))
# Fursuit-Typ Filter
fursuit_type = self.request.GET.get('fursuit_type')
if fursuit_type:
queryset = queryset.filter(fursuit_type=fursuit_type)
# Style Filter
style = self.request.GET.get('style')
if style:
queryset = queryset.filter(style=style)
# Kategorie Filter
category = self.request.GET.get('category')
if category:
queryset = queryset.filter(category__slug=category)
# Verfügbarkeit Filter
availability = self.request.GET.get('availability')
if availability == 'in_stock':
queryset = queryset.filter(stock__gt=0)
elif availability == 'low_stock':
queryset = queryset.filter(stock__lte=5, stock__gt=0)
elif availability == 'out_of_stock':
queryset = queryset.filter(stock=0)
# Sortierung mit verbesserter Performance
sort = self.request.GET.get('sort', '-created')
if sort == 'price':
queryset = queryset.order_by('price')
elif sort == '-price':
queryset = queryset.order_by('-price')
elif sort == 'name':
queryset = queryset.order_by('name')
elif sort == '-name':
queryset = queryset.order_by('-name')
elif sort == '-rating':
queryset = queryset.order_by('-average_rating')
elif sort == 'popularity':
queryset = queryset.order_by('-review_count')
else: # Default: Neueste zuerst
queryset = queryset.order_by('-created')
return queryset.distinct()
except Exception as e:
logger.error(f"Error in ProductListView.get_queryset: {e}")
return Product.objects.none()
def get_context_data(self, **kwargs):
try:
context = super().get_context_data(**kwargs)
# Zusätzliche Context-Daten
context['categories'] = Category.objects.all()
context['fursuit_types'] = Product.FURSUIT_TYPE_CHOICES
context['styles'] = Product.STYLE_CHOICES
return context
except Exception as e:
logger.error(f"Error in ProductListView.get_context_data: {e}")
return super().get_context_data(**kwargs)
class ProductDetailView(DetailView):
model = Product
template_name = 'products/product_detail.html'
context_object_name = 'product'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.user.is_authenticated:
context['user_has_reviewed'] = Review.objects.filter(
product=self.object,
user=self.request.user
).exists()
context['review_form'] = ReviewForm()
return context
def get_or_create_cart(request):
if request.user.is_authenticated:
cart, created = Cart.objects.get_or_create(user=request.user)
else:
session_key = request.session.session_key
if not session_key:
request.session.create()
session_key = request.session.session_key
cart, created = Cart.objects.get_or_create(session_id=session_key)
return cart
def add_to_cart(request, product_id):
if request.method == 'POST':
product = get_object_or_404(Product, id=product_id)
quantity = int(request.POST.get('quantity', 1))
if quantity > product.stock:
messages.error(request, 'Nicht genügend Artikel auf Lager!')
return redirect('products:product_detail', pk=product_id)
cart = get_or_create_cart(request)
cart_item, created = CartItem.objects.get_or_create(
cart=cart,
product=product,
defaults={'quantity': quantity}
)
if not created:
cart_item.quantity += quantity
cart_item.save()
messages.success(request, f'{quantity}x {product.name} wurde zum Warenkorb hinzugefügt.')
return redirect('products:cart_detail')
def cart_detail(request):
cart = get_or_create_cart(request)
return render(request, 'products/cart_detail.html', {'cart': cart})
def create_order(request):
if request.method == 'POST':
cart = get_or_create_cart(request)
if not cart.items.exists():
messages.error(request, 'Ihr Warenkorb ist leer.')
return redirect('products:cart_detail')
# Erstelle eine neue Bestellung
order = Order.objects.create(
user=request.user if request.user.is_authenticated else None,
full_name=f"{request.user.first_name} {request.user.last_name}" if request.user.is_authenticated else "",
email=request.user.email if request.user.is_authenticated else "",
total_amount=cart.get_total()
)
# Füge die Artikel aus dem Warenkorb zur Bestellung hinzu
for cart_item in cart.items.all():
OrderItem.objects.create(
order=order,
product=cart_item.product,
product_name=cart_item.product.name,
quantity=cart_item.quantity,
price=cart_item.product.price
)
# Leere den Warenkorb
cart.items.all().delete()
messages.success(request, 'Ihre Bestellung wurde erfolgreich erstellt.')
return redirect('products:checkout', order_id=order.id)
return redirect('products:cart_detail')
@login_required
def checkout(request, order_id):
order = get_object_or_404(Order, id=order_id)
if request.user.is_authenticated and order.user != request.user:
messages.error(request, 'Sie haben keine Berechtigung, diese Bestellung einzusehen.')
return redirect('products:product_list')
# PayPal Zahlungsformular erstellen
host = request.get_host()
paypal_dict = {
"business": settings.PAYPAL_RECEIVER_EMAIL,
"amount": str(order.total_amount),
"item_name": f"Bestellung #{order.id}",
"invoice": str(order.id),
"currency_code": "EUR",
"notify_url": request.build_absolute_uri('/paypal/'),
"return_url": request.build_absolute_uri(f'/products/payment/success/{order.id}/'),
"cancel_return": request.build_absolute_uri(f'/products/payment/failed/{order.id}/'),
}
form = PayPalPaymentsForm(initial=paypal_dict)
return render(request, 'products/checkout.html', {
'order': order,
'form': form
})
@csrf_exempt
@login_required
def create_paypal_order(request):
if request.method != 'POST':
return JsonResponse({'error': 'Nur POST-Anfragen erlaubt'}, status=405)
data = json.loads(request.body)
order = get_object_or_404(Order, id=data['order_id'], user=request.user)
payment = paypalrestsdk.Payment({
"intent": "sale",
"payer": {
"payment_method": "paypal"
},
"transactions": [{
"amount": {
"total": str(order.total_amount),
"currency": "EUR"
},
"description": f"Bestellung #{order.id}"
}],
"redirect_urls": {
"return_url": f"{settings.SITE_URL}/products/order/confirmation/{order.id}/",
"cancel_url": f"{settings.SITE_URL}/products/checkout/{order.id}/"
}
})
if payment.create():
return JsonResponse({'paypal_order_id': payment.id})
else:
return JsonResponse({'error': payment.error}, status=400)
@csrf_exempt
@login_required
def capture_paypal_order(request):
if request.method != 'POST':
return JsonResponse({'error': 'Nur POST-Anfragen erlaubt'}, status=405)
data = json.loads(request.body)
order = get_object_or_404(Order, id=data['order_id'], user=request.user)
payment_id = data['paypal_order_id']
payment = paypalrestsdk.Payment.find(payment_id)
if payment.execute({'payer_id': payment.payer.payer_info.payer_id}):
order.status = 'paid'
order.payment_id = payment_id
order.save()
# E-Mail-Bestätigung senden
send_payment_confirmation_email(order)
return JsonResponse({'success': True})
else:
return JsonResponse({'error': payment.error}, status=400)
def order_confirmation(request, order_id):
order = get_object_or_404(Order, id=order_id)
if request.user.is_authenticated and order.user != request.user:
messages.error(request, 'Sie haben keine Berechtigung, diese Bestellung einzusehen.')
return redirect('products:product_list')
return render(request, 'products/order_confirmation.html', {'order': order})
def update_cart_item(request, item_id):
if request.method == 'POST':
cart_item = get_object_or_404(CartItem, id=item_id)
quantity = int(request.POST.get('quantity', 1))
if quantity > cart_item.product.stock:
messages.error(request, 'Nicht genügend Artikel auf Lager!')
elif quantity > 0:
cart_item.quantity = quantity
cart_item.save()
messages.success(request, 'Warenkorb wurde aktualisiert.')
else:
cart_item.delete()
messages.success(request, 'Artikel wurde aus dem Warenkorb entfernt.')
return redirect('products:cart_detail')
def remove_from_cart(request, item_id):
if request.method == 'POST':
cart_item = get_object_or_404(CartItem, id=item_id)
cart_item.delete()
messages.success(request, 'Artikel wurde aus dem Warenkorb entfernt.')
return redirect('products:cart_detail')
def register(request):
if request.method == 'POST':
form = CustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
messages.success(request, 'Registrierung erfolgreich!')
return redirect('products:product_list')
else:
form = CustomUserCreationForm()
return render(request, 'registration/register.html', {'form': form})
@login_required
def add_review(request, product_id):
product = get_object_or_404(Product, id=product_id)
if request.method == 'POST':
form = ReviewForm(request.POST)
if form.is_valid():
review = form.save(commit=False)
review.product = product
review.user = request.user
review.save()
messages.success(request, 'Ihre Bewertung wurde erfolgreich hinzugefügt.')
else:
messages.error(request, 'Es gab einen Fehler beim Hinzufügen Ihrer Bewertung.')
return redirect('product_detail', pk=product_id)
@login_required
def profile_view(request):
if request.method == 'POST':
form = UserProfileForm(request.POST, instance=request.user.userprofile)
if form.is_valid():
form.save()
messages.success(request, 'Profil wurde erfolgreich aktualisiert.')
return redirect('products:profile')
else:
form = UserProfileForm(instance=request.user.userprofile)
return render(request, 'products/profile.html', {'form': form})
@login_required
def order_history(request):
orders = Order.objects.filter(user=request.user).order_by('-created')
return render(request, 'products/order_history.html', {'orders': orders})
@login_required
def wishlist_view(request):
wishlist, created = Wishlist.objects.get_or_create(user=request.user)
return render(request, 'products/wishlist.html', {'wishlist': wishlist})
@login_required
def add_to_wishlist(request, product_id):
product = get_object_or_404(Product, id=product_id)
wishlist, created = Wishlist.objects.get_or_create(user=request.user)
wishlist.products.add(product)
messages.success(request, f'{product.name} wurde zu Ihrer Wunschliste hinzugefügt.')
return redirect('product_detail', product_id=product_id)
@login_required
def remove_from_wishlist(request, product_id):
product = get_object_or_404(Product, id=product_id)
wishlist = get_object_or_404(Wishlist, user=request.user)
wishlist.products.remove(product)
messages.success(request, f'{product.name} wurde von Ihrer Wunschliste entfernt.')
return redirect('wishlist')
def faq_list(request):
faqs = FAQ.objects.all()
categories = FAQ.objects.values_list('category', flat=True).distinct()
return render(request, 'products/faq.html', {
'faqs': faqs,
'categories': categories
})
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST, user=request.user)
if form.is_valid():
contact_message = form.save(commit=False)
if request.user.is_authenticated:
contact_message.user = request.user
contact_message.save()
messages.success(
request,
'Vielen Dank für Ihre Nachricht! Wir werden uns schnellstmöglich bei Ihnen melden.'
)
return redirect('contact_success')
else:
form = ContactForm(user=request.user)
return render(request, 'products/contact.html', {'form': form})
def contact_success(request):
return render(request, 'products/contact_success.html')
@login_required
def custom_order(request):
if request.method == 'POST':
form = CustomOrderForm(request.POST, request.FILES)
if form.is_valid():
custom_order = form.save(commit=False)
custom_order.user = request.user
custom_order.save()
messages.success(
request,
'Ihre Anfrage wurde erfolgreich übermittelt. Wir werden uns in Kürze mit einem Angebot bei Ihnen melden.'
)
return redirect('custom_order_success', order_id=custom_order.id)
else:
form = CustomOrderForm()
return render(request, 'products/custom_order.html', {
'form': form
})
@login_required
def custom_order_success(request, order_id):
order = get_object_or_404(CustomOrder, id=order_id, user=request.user)
return render(request, 'products/custom_order_success.html', {
'order': order
})
@login_required
def custom_order_detail(request, order_id):
order = get_object_or_404(CustomOrder, id=order_id, user=request.user)
progress_updates = order.progress_updates.all()
return render(request, 'products/custom_order_detail.html', {
'order': order,
'progress_updates': progress_updates
})
def gallery(request):
# Hole alle Galeriebilder
images = GalleryImage.objects.all().order_by('order', '-created')
# Filter nach Typ
fursuit_type = request.GET.get('fursuit_type')
if fursuit_type:
images = images.filter(fursuit_type=fursuit_type)
# Filter nach Stil
style = request.GET.get('style')
if style:
images = images.filter(style=style)
# Sortierung
sort = request.GET.get('sort', 'order')
if sort == 'newest':
images = images.order_by('-created')
elif sort == 'oldest':
images = images.order_by('created')
context = {
'images': images,
'fursuit_types': GalleryImage.FURSUIT_TYPE_CHOICES,
'fursuit_styles': GalleryImage.STYLE_CHOICES,
'current_type': fursuit_type,
'current_style': style,
'current_sort': sort
}
print(f"Anzahl der geladenen Bilder: {images.count()}") # Debug-Ausgabe
for img in images:
print(f"Bild: {img.title}, URL: {img.image.url}") # Debug-Ausgabe
return render(request, 'products/gallery.html', context)
@login_required
def add_progress_update(request, order_id):
order = get_object_or_404(CustomOrder, id=order_id)
if not request.user.is_staff:
messages.error(request, 'Sie haben keine Berechtigung für diese Aktion.')
return redirect('custom_order_detail', order_id=order_id)
if request.method == 'POST':
form = OrderProgressForm(request.POST, request.FILES)
if form.is_valid():
progress = form.save(commit=False)
progress.custom_order = order
progress.save()
messages.success(request, 'Fortschritts-Update wurde hinzugefügt.')
# Wenn alle Schritte abgeschlossen sind, setze den Status auf "ready"
if all(update.completed for update in order.progress_updates.all()):
order.status = 'ready'
order.save()
return redirect('custom_order_detail', order_id=order_id)
else:
form = OrderProgressForm()
return render(request, 'products/add_progress.html', {
'form': form,
'order': order
})
@login_required
def dashboard(request):
# Basis-Informationen
user_profile = request.user.userprofile
wishlist = Wishlist.objects.get_or_create(user=request.user)[0]
cart = get_or_create_cart(request)
# Custom Orders mit Fortschritt
custom_orders = CustomOrder.objects.filter(user=request.user).order_by('-created')
for order in custom_orders:
order.progress_percentage = calculate_order_progress(order)
# Letzte Bestellungen mit Details
recent_orders = Order.objects.filter(user=request.user).order_by('-created')[:5]
for order in recent_orders:
order.items_count = order.items.count()
# Bewertungsübersicht
reviews = Review.objects.filter(user=request.user).select_related('product').order_by('-created')[:5]
# Statistiken
stats = {
'total_orders': Order.objects.filter(user=request.user).count(),
'total_custom_orders': CustomOrder.objects.filter(user=request.user).count(),
'total_reviews': Review.objects.filter(user=request.user).count(),
'wishlist_count': wishlist.products.count(),
}
return render(request, 'products/dashboard.html', {
'user_profile': user_profile,
'custom_orders': custom_orders,
'recent_orders': recent_orders,
'reviews': reviews,
'stats': stats,
'wishlist': wishlist,
'cart': cart,
})
def calculate_order_progress(order):
"""Berechne den Fortschritt einer Custom Order in Prozent."""
if order.status == 'cancelled':
return 0
elif order.status == 'completed':
return 100
# Status-Gewichtung
status_weights = {
'pending': 10,
'quoted': 20,
'approved': 30,
'in_progress': 50,
'ready': 90,
'shipped': 95,
}
base_progress = status_weights.get(order.status, 0)
# Zusätzlicher Fortschritt basierend auf OrderProgress
if order.status == 'in_progress':
completed_stages = order.progress_updates.filter(completed=True).count()
total_stages = len(OrderProgress.PROGRESS_CHOICES)
stage_progress = (completed_stages / total_stages) * 40 # 40% für Fortschrittsstufen
return base_progress + stage_progress
return base_progress
# API ViewSets - Temporär auskommentiert
# class ProductViewSet(viewsets.ModelViewSet):
# queryset = Product.objects.all()
# serializer_class = ProductSerializer
# permission_classes = [IsAuthenticatedOrReadOnly]
# class ReviewViewSet(viewsets.ModelViewSet):
# queryset = Review.objects.all()
# serializer_class = ReviewSerializer
# permission_classes = [IsAuthenticatedOrReadOnly]
# def perform_create(self, serializer):
# serializer.save(user=self.request.user)
# Stripe-Konfiguration
stripe.api_key = settings.STRIPE_SECRET_KEY
def create_payment_intent(request, order_id):
"""Erstellt einen Stripe Payment Intent für eine Bestellung."""
try:
order = get_object_or_404(Order, id=order_id)
# Stripe akzeptiert nur Cent-Beträge
amount = int(order.total_amount * 100)
# Payment Intent erstellen
intent = stripe.PaymentIntent.create(
amount=amount,
currency='eur',
metadata={
'order_id': order.id,
'user_id': request.user.id if request.user.is_authenticated else None
}
)
# Order aktualisieren
order.stripe_payment_intent_id = intent.id
order.save()
return JsonResponse({
'clientSecret': intent.client_secret,
'amount': amount
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=400)
def payment_view(request, order_id):
"""Zeigt die Zahlungsseite an."""
order = get_object_or_404(Order, id=order_id)
# Überprüfen, ob die Bestellung dem Benutzer gehört
if request.user.is_authenticated and order.user != request.user:
messages.error(request, 'Sie haben keine Berechtigung für diese Bestellung.')
return redirect('dashboard')
# Überprüfen, ob die Bestellung bereits bezahlt wurde
if order.payment_status == 'paid':
messages.info(request, 'Diese Bestellung wurde bereits bezahlt.')
return redirect('order_detail', order_id=order.id)
context = {
'order': order,
'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY
}
return render(request, 'products/payment.html', context)
@csrf_exempt
def stripe_webhook(request):
"""Webhook für Stripe-Events."""
payload = request.body
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
try:
event = stripe.Webhook.construct_event(
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
)
except ValueError as e:
return JsonResponse({'error': 'Invalid payload'}, status=400)
except stripe.error.SignatureVerificationError as e:
return JsonResponse({'error': 'Invalid signature'}, status=400)
if event.type == 'payment_intent.succeeded':
payment_intent = event.data.object
handle_successful_payment(payment_intent)
elif event.type == 'payment_intent.payment_failed':
payment_intent = event.data.object
handle_failed_payment(payment_intent)
return JsonResponse({'status': 'success'})
def handle_successful_payment(payment_intent):
"""Verarbeitet erfolgreiche Zahlungen."""
order_id = payment_intent.metadata.get('order_id')
if order_id:
try:
order = Order.objects.get(id=order_id)
order.payment_status = 'paid'
order.payment_date = timezone.now()
order.status = 'processing'
order.stripe_payment_method_id = payment_intent.payment_method
order.save()
# E-Mail-Benachrichtigung senden
send_payment_confirmation_email(order)
except Order.DoesNotExist:
print(f"Bestellung {order_id} nicht gefunden")
def handle_failed_payment(payment_intent):
"""Verarbeitet fehlgeschlagene Zahlungen."""
order_id = payment_intent.metadata.get('order_id')
if order_id:
try:
order = Order.objects.get(id=order_id)
order.payment_status = 'failed'
order.save()
# E-Mail-Benachrichtigung senden
send_payment_failed_email(order)
except Order.DoesNotExist:
print(f"Bestellung {order_id} nicht gefunden")
@login_required
def payment_success(request, order_id):
"""Zeigt die Erfolgsseite nach erfolgreicher Zahlung an."""
order = get_object_or_404(Order, id=order_id, user=request.user)
messages.success(request, 'Ihre Zahlung wurde erfolgreich verarbeitet!')
return render(request, 'products/payment_success.html', {'order': order})
@login_required
def payment_failed(request, order_id):
"""Zeigt die Fehlerseite nach fehlgeschlagener Zahlung an."""
order = get_object_or_404(Order, id=order_id, user=request.user)
messages.error(request, 'Die Zahlung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut.')
return render(request, 'products/payment_failed.html', {'order': order})
# Hilfsfunktionen für E-Mail-Benachrichtigungen
def send_payment_confirmation_email(order):
"""Sendet eine Bestätigungs-E-Mail nach erfolgreicher Zahlung."""
subject = f'Zahlungsbestätigung für Bestellung #{order.id}'
message = f"""
Sehr geehrte(r) {order.full_name},
vielen Dank für Ihre Zahlung. Ihre Bestellung #{order.id} wurde erfolgreich bezahlt.
Bestelldetails:
- Gesamtbetrag: {order.total_amount}
- Zahlungsmethode: {order.get_payment_method_display()}
- Zahlungsdatum: {order.payment_date.strftime('%d.%m.%Y %H:%M')}
Wir werden Ihre Bestellung schnellstmöglich bearbeiten.
Mit freundlichen Grüßen
Ihr Fursuit-Shop Team
"""
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
[order.email],
fail_silently=False,
)
def send_payment_failed_email(order):
"""Sendet eine Benachrichtigung nach fehlgeschlagener Zahlung."""
subject = f'Zahlungsproblem bei Bestellung #{order.id}'
message = f"""
Sehr geehrte(r) {order.full_name},
leider ist die Zahlung für Ihre Bestellung #{order.id} fehlgeschlagen.
Sie können die Zahlung unter folgendem Link erneut versuchen:
{settings.SITE_URL}/payment/{order.id}/
Falls Sie Fragen haben, kontaktieren Sie uns bitte.
Mit freundlichen Grüßen
Ihr Fursuit-Shop Team
"""
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
[order.email],
fail_silently=False,
)
@login_required
def payment_process(request, order_id):
order = get_object_or_404(Order, id=order_id, user=request.user)
if request.method == 'POST':
# Initiiere PayPal-Zahlung
paypal_dict = {
"business": settings.PAYPAL_RECEIVER_EMAIL,
"amount": str(order.total_amount),
"item_name": f"Bestellung #{order.id}",
"invoice": str(order.id),
"notify_url": request.build_absolute_uri(reverse('paypal-ipn')),
"return_url": request.build_absolute_uri(reverse('payment_success', args=[order.id])),
"cancel_return": request.build_absolute_uri(reverse('payment_failed', args=[order.id])),
"custom": json.dumps({
"order_id": order.id,
"user_id": request.user.id,
})
}
form = PayPalPaymentsForm(initial=paypal_dict)
return render(request, 'products/payment_process.html', {'order': order, 'form': form})
return render(request, 'products/payment_process.html', {'order': order})
@csrf_exempt
def paypal_ipn(request):
"""PayPal IPN (Instant Payment Notification) Handler"""
if request.method == "POST":
try:
ipn_obj = PayPalIPN(request.POST)
ipn_obj.verify()
if ipn_obj.payment_status == "Completed":
# Zahlung war erfolgreich
try:
order = Order.objects.get(id=ipn_obj.invoice)
order.status = 'paid'
order.payment_method = 'paypal'
order.payment_id = ipn_obj.txn_id
order.save()
# Sende Bestätigungs-E-Mail
send_payment_confirmation_email(order)
except Order.DoesNotExist:
pass
return HttpResponse("OK")
except Exception as e:
return HttpResponse(str(e))
return HttpResponse("OK")

192
recommendations/admin.py Normal file
View File

@ -0,0 +1,192 @@
"""
Recommendation Engine Admin Interface
"""
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.db.models import Count, Avg, Sum
from .models import (
UserBehavior, UserProfile, ProductSimilarity, Recommendation,
RecommendationModel, ABTest, RecommendationAnalytics
)
@admin.register(UserBehavior)
class UserBehaviorAdmin(admin.ModelAdmin):
list_display = ['user', 'behavior_type', 'product', 'created_at', 'ip_address']
list_filter = ['behavior_type', 'created_at', 'user']
search_fields = ['user__username', 'product__name']
readonly_fields = ['id', 'created_at']
date_hierarchy = 'created_at'
def get_queryset(self, request):
return super().get_queryset(request).select_related('user', 'product')
@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
list_display = ['user', 'engagement_score', 'loyalty_level', 'total_purchases', 'last_active']
list_filter = ['loyalty_level', 'last_active']
search_fields = ['user__username']
readonly_fields = ['last_updated']
fieldsets = (
('User Info', {
'fields': ('user', 'engagement_score', 'loyalty_level')
}),
('Preferences', {
'fields': ('preferred_categories', 'preferred_fursuit_types', 'preferred_price_range', 'preferred_colors')
}),
('Metrics', {
'fields': ('avg_session_duration', 'total_purchases', 'total_views', 'total_searches')
}),
('ML Features', {
'fields': ('feature_vector', 'last_updated')
}),
)
def get_queryset(self, request):
return super().get_queryset(request).select_related('user')
@admin.register(ProductSimilarity)
class ProductSimilarityAdmin(admin.ModelAdmin):
list_display = ['product', 'similar_product', 'similarity_score', 'similarity_type', 'updated_at']
list_filter = ['similarity_type', 'updated_at']
search_fields = ['product__name', 'similar_product__name']
readonly_fields = ['created_at', 'updated_at']
def get_queryset(self, request):
return super().get_queryset(request).select_related('product', 'similar_product')
@admin.register(Recommendation)
class RecommendationAdmin(admin.ModelAdmin):
list_display = ['user', 'product', 'recommendation_type', 'confidence_score', 'is_clicked', 'is_purchased', 'created_at']
list_filter = ['recommendation_type', 'is_clicked', 'is_purchased', 'created_at']
search_fields = ['user__username', 'product__name']
readonly_fields = ['id', 'created_at']
date_hierarchy = 'created_at'
fieldsets = (
('Recommendation Info', {
'fields': ('user', 'product', 'recommendation_type', 'confidence_score', 'reason')
}),
('Performance', {
'fields': ('is_clicked', 'is_purchased')
}),
('Timing', {
'fields': ('created_at', 'expires_at')
}),
)
def get_queryset(self, request):
return super().get_queryset(request).select_related('user', 'product')
@admin.register(RecommendationModel)
class RecommendationModelAdmin(admin.ModelAdmin):
list_display = ['model_name', 'model_type', 'model_version', 'accuracy_score', 'is_active', 'trained_at']
list_filter = ['model_type', 'is_active', 'trained_at']
search_fields = ['model_name']
readonly_fields = ['id', 'created_at', 'trained_at']
fieldsets = (
('Model Info', {
'fields': ('model_type', 'model_name', 'model_version', 'model_file')
}),
('Parameters', {
'fields': ('model_parameters',)
}),
('Performance', {
'fields': ('training_data_size', 'accuracy_score', 'precision_score', 'recall_score', 'f1_score')
}),
('Status', {
'fields': ('is_active', 'created_at', 'trained_at')
}),
)
@admin.register(ABTest)
class ABTestAdmin(admin.ModelAdmin):
list_display = ['test_name', 'test_type', 'status', 'traffic_split', 'winner', 'created_by']
list_filter = ['status', 'test_type', 'created_at']
search_fields = ['test_name', 'description']
readonly_fields = ['id', 'created_at']
fieldsets = (
('Test Info', {
'fields': ('test_name', 'description', 'test_type', 'created_by')
}),
('Variants', {
'fields': ('variant_a', 'variant_b', 'traffic_split')
}),
('Status', {
'fields': ('status', 'start_date', 'end_date')
}),
('Results', {
'fields': ('variant_a_conversion', 'variant_b_conversion', 'statistical_significance', 'winner')
}),
)
@admin.register(RecommendationAnalytics)
class RecommendationAnalyticsAdmin(admin.ModelAdmin):
list_display = ['date', 'total_recommendations', 'total_clicks', 'total_purchases', 'click_through_rate', 'conversion_rate']
list_filter = ['date']
readonly_fields = ['date']
date_hierarchy = 'date'
def click_through_rate(self, obj):
if obj.total_recommendations > 0:
return f"{(obj.total_clicks / obj.total_recommendations * 100):.1f}%"
return "0%"
click_through_rate.short_description = 'CTR'
def conversion_rate(self, obj):
if obj.total_clicks > 0:
return f"{(obj.total_purchases / obj.total_clicks * 100):.1f}%"
return "0%"
conversion_rate.short_description = 'Conversion Rate'
# Custom Admin Actions
@admin.action(description="Update User Engagement Scores")
def update_engagement_scores(modeladmin, request, queryset):
for profile in queryset:
profile.update_engagement_score()
modeladmin.message_user(request, f"Updated engagement scores for {queryset.count()} profiles.")
@admin.action(description="Train Recommendation Models")
def train_models(modeladmin, request, queryset):
from .services import RecommendationService
service = RecommendationService()
for model in queryset:
trained_model = service.train_recommendation_model(model.model_type)
modeladmin.message_user(request, f"Trained model: {trained_model.model_name}")
@admin.action(description="Update Product Similarities")
def update_similarities(modeladmin, request, queryset):
from .services import RecommendationService
service = RecommendationService()
service.update_product_similarities()
modeladmin.message_user(request, "Updated product similarities.")
@admin.action(description="Update Analytics")
def update_analytics(modeladmin, request, queryset):
from .services import RecommendationService
service = RecommendationService()
analytics = service.update_analytics()
modeladmin.message_user(request, f"Updated analytics for {analytics.date}")
# Add actions to admin classes
UserProfileAdmin.actions = [update_engagement_scores]
RecommendationModelAdmin.actions = [train_models]
ProductSimilarityAdmin.actions = [update_similarities]
RecommendationAnalyticsAdmin.actions = [update_analytics]

308
recommendations/models.py Normal file
View File

@ -0,0 +1,308 @@
"""
Recommendation Engine Models für ML-basierte Empfehlungen
"""
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from products.models import Product
from auction.models import Auction
import uuid
class UserBehavior(models.Model):
"""User Behavior Tracking für Empfehlungen"""
BEHAVIOR_TYPE_CHOICES = [
('view', 'Produkt angesehen'),
('cart_add', 'Zum Warenkorb hinzugefügt'),
('cart_remove', 'Aus Warenkorb entfernt'),
('purchase', 'Gekauft'),
('wishlist_add', 'Zur Wunschliste hinzugefügt'),
('wishlist_remove', 'Von Wunschliste entfernt'),
('search', 'Gesucht'),
('auction_bid', 'Bei Auktion geboten'),
('auction_watch', 'Auktion beobachtet'),
('review', 'Bewertung abgegeben'),
('share', 'Geteilt'),
('like', 'Geliked'),
('dislike', 'Nicht gemocht'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='behaviors')
behavior_type = models.CharField(max_length=20, choices=BEHAVIOR_TYPE_CHOICES)
product = models.ForeignKey(Product, on_delete=models.CASCADE, null=True, blank=True, related_name='behaviors')
auction = models.ForeignKey(Auction, on_delete=models.CASCADE, null=True, blank=True, related_name='behaviors')
session_id = models.CharField(max_length=100, blank=True)
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True)
metadata = models.JSONField(default=dict) # Additional data
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'User Behavior'
verbose_name_plural = 'User Behaviors'
indexes = [
models.Index(fields=['user', 'behavior_type', 'created_at']),
models.Index(fields=['product', 'behavior_type', 'created_at']),
]
def __str__(self):
return f"{self.user.username} - {self.get_behavior_type_display()}"
class UserProfile(models.Model):
"""User Profile für Personalisierung"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='recommendation_profile')
# Preferences
preferred_categories = models.JSONField(default=list)
preferred_fursuit_types = models.JSONField(default=list)
preferred_price_range = models.JSONField(default=dict)
preferred_colors = models.JSONField(default=list)
# Behavior patterns
avg_session_duration = models.FloatField(default=0) # in minutes
total_purchases = models.IntegerField(default=0)
total_views = models.IntegerField(default=0)
total_searches = models.IntegerField(default=0)
# Engagement metrics
last_active = models.DateTimeField(auto_now=True)
engagement_score = models.FloatField(default=0) # 0-100
loyalty_level = models.CharField(max_length=20, choices=[
('new', 'Neu'),
('regular', 'Regulär'),
('loyal', 'Loyal'),
('vip', 'VIP'),
], default='new')
# ML features
feature_vector = models.JSONField(default=dict) # ML feature vector
last_updated = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'User Profile'
verbose_name_plural = 'User Profiles'
def __str__(self):
return f"Profile für {self.user.username}"
def update_engagement_score(self):
"""Engagement Score basierend auf Verhalten aktualisieren"""
behaviors = self.user.behaviors.all()
# Calculate engagement score
view_weight = 1
cart_weight = 3
purchase_weight = 10
search_weight = 2
total_score = 0
total_actions = 0
for behavior in behaviors:
if behavior.behavior_type == 'view':
total_score += view_weight
elif behavior.behavior_type == 'cart_add':
total_score += cart_weight
elif behavior.behavior_type == 'purchase':
total_score += purchase_weight
elif behavior.behavior_type == 'search':
total_score += search_weight
total_actions += 1
if total_actions > 0:
self.engagement_score = min(100, (total_score / total_actions) * 10)
# Update loyalty level
if self.engagement_score >= 80:
self.loyalty_level = 'vip'
elif self.engagement_score >= 60:
self.loyalty_level = 'loyal'
elif self.engagement_score >= 30:
self.loyalty_level = 'regular'
else:
self.loyalty_level = 'new'
self.save()
class ProductSimilarity(models.Model):
"""Produkt-Ähnlichkeitsmatrix für Collaborative Filtering"""
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='similarities')
similar_product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='similar_to')
similarity_score = models.FloatField() # 0-1
similarity_type = models.CharField(max_length=20, choices=[
('category', 'Kategorie'),
('fursuit_type', 'Fursuit Typ'),
('price_range', 'Preisbereich'),
('color', 'Farbe'),
('collaborative', 'Collaborative'),
('content_based', 'Content-based'),
])
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ['product', 'similar_product']
ordering = ['-similarity_score']
verbose_name = 'Product Similarity'
verbose_name_plural = 'Product Similarities'
def __str__(self):
return f"{self.product.name} ~ {self.similar_product.name} ({self.similarity_score:.2f})"
class Recommendation(models.Model):
"""Generierte Empfehlungen für User"""
RECOMMENDATION_TYPE_CHOICES = [
('collaborative', 'Collaborative Filtering'),
('content_based', 'Content-based Filtering'),
('popular', 'Beliebte Produkte'),
('trending', 'Trending'),
('personalized', 'Personalisiert'),
('similar', 'Ähnliche Produkte'),
('frequently_bought', 'Häufig zusammen gekauft'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='recommendations')
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='recommendations')
recommendation_type = models.CharField(max_length=20, choices=RECOMMENDATION_TYPE_CHOICES)
confidence_score = models.FloatField() # 0-1
reason = models.CharField(max_length=200, blank=True)
is_clicked = models.BooleanField(default=False)
is_purchased = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-confidence_score', '-created_at']
verbose_name = 'Recommendation'
verbose_name_plural = 'Recommendations'
indexes = [
models.Index(fields=['user', 'recommendation_type', 'created_at']),
models.Index(fields=['product', 'recommendation_type', 'created_at']),
]
def __str__(self):
return f"{self.user.username} - {self.product.name} ({self.get_recommendation_type_display()})"
class RecommendationModel(models.Model):
"""ML Model für Empfehlungen"""
MODEL_TYPE_CHOICES = [
('collaborative', 'Collaborative Filtering'),
('content_based', 'Content-based Filtering'),
('hybrid', 'Hybrid Model'),
('neural_network', 'Neural Network'),
('matrix_factorization', 'Matrix Factorization'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
model_type = models.CharField(max_length=20, choices=MODEL_TYPE_CHOICES)
model_name = models.CharField(max_length=100)
model_version = models.CharField(max_length=20)
model_file = models.FileField(upload_to='recommendation_models/', null=True, blank=True)
model_parameters = models.JSONField(default=dict)
training_data_size = models.IntegerField(default=0)
accuracy_score = models.FloatField(default=0)
precision_score = models.FloatField(default=0)
recall_score = models.FloatField(default=0)
f1_score = models.FloatField(default=0)
is_active = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
trained_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'Recommendation Model'
verbose_name_plural = 'Recommendation Models'
def __str__(self):
return f"{self.model_name} v{self.model_version} ({self.get_model_type_display()})"
class ABTest(models.Model):
"""A/B Testing für Empfehlungen"""
TEST_STATUS_CHOICES = [
('draft', 'Entwurf'),
('running', 'Läuft'),
('paused', 'Pausiert'),
('completed', 'Abgeschlossen'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
test_name = models.CharField(max_length=100)
description = models.TextField()
test_type = models.CharField(max_length=50)
variant_a = models.JSONField() # Control group
variant_b = models.JSONField() # Test group
traffic_split = models.FloatField(default=0.5) # % für Variante B
status = models.CharField(max_length=20, choices=TEST_STATUS_CHOICES, default='draft')
start_date = models.DateTimeField(null=True, blank=True)
end_date = models.DateTimeField(null=True, blank=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ab_tests')
created_at = models.DateTimeField(auto_now_add=True)
# Results
variant_a_conversion = models.FloatField(default=0)
variant_b_conversion = models.FloatField(default=0)
statistical_significance = models.FloatField(default=0)
winner = models.CharField(max_length=10, choices=[
('a', 'Variant A'),
('b', 'Variant B'),
('none', 'Kein Gewinner'),
], blank=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'A/B Test'
verbose_name_plural = 'A/B Tests'
def __str__(self):
return f"{self.test_name} ({self.get_status_display()})"
class RecommendationAnalytics(models.Model):
"""Analytics für Empfehlungen"""
date = models.DateField()
total_recommendations = models.IntegerField(default=0)
total_clicks = models.IntegerField(default=0)
total_purchases = models.IntegerField(default=0)
click_through_rate = models.FloatField(default=0)
conversion_rate = models.FloatField(default=0)
revenue_from_recommendations = models.DecimalField(max_digits=10, decimal_places=2, default=0)
avg_recommendation_score = models.FloatField(default=0)
# Per recommendation type
collaborative_recommendations = models.IntegerField(default=0)
content_based_recommendations = models.IntegerField(default=0)
popular_recommendations = models.IntegerField(default=0)
personalized_recommendations = models.IntegerField(default=0)
class Meta:
unique_together = ['date']
ordering = ['-date']
verbose_name = 'Recommendation Analytics'
verbose_name_plural = 'Recommendation Analytics'
def __str__(self):
return f"Recommendation Analytics - {self.date}"
@classmethod
def get_or_create_today(cls):
"""Heutige Analytics erstellen oder abrufen"""
today = timezone.now().date()
analytics, created = cls.objects.get_or_create(date=today)
return analytics

447
recommendations/services.py Normal file
View File

@ -0,0 +1,447 @@
"""
Recommendation Service für ML-basierte Empfehlungen
"""
import numpy as np
from django.db.models import Q, Count, Avg
from django.utils import timezone
from datetime import timedelta
from .models import UserBehavior, UserProfile, ProductSimilarity, Recommendation, RecommendationModel
from products.models import Product
import json
class RecommendationService:
"""Service für ML-basierte Empfehlungen"""
def __init__(self):
self.collaborative_weight = 0.4
self.content_based_weight = 0.3
self.popular_weight = 0.2
self.trending_weight = 0.1
def get_recommendations_for_user(self, user, limit=10):
"""Personalisiertes Empfehlungen für User"""
recommendations = []
# Get user profile
profile, created = UserProfile.objects.get_or_create(user=user)
# 1. Collaborative Filtering
collaborative_recs = self._get_collaborative_recommendations(user, limit=limit//2)
recommendations.extend(collaborative_recs)
# 2. Content-based Filtering
content_recs = self._get_content_based_recommendations(user, profile, limit=limit//2)
recommendations.extend(content_recs)
# 3. Popular Products (fallback)
if len(recommendations) < limit:
popular_recs = self._get_popular_recommendations(limit=limit-len(recommendations))
recommendations.extend(popular_recs)
# 4. Trending Products (fallback)
if len(recommendations) < limit:
trending_recs = self._get_trending_recommendations(limit=limit-len(recommendations))
recommendations.extend(trending_recs)
# Remove duplicates and sort by confidence
unique_recs = {}
for rec in recommendations:
product_id = rec['product']['id']
if product_id not in unique_recs or rec['confidence_score'] > unique_recs[product_id]['confidence_score']:
unique_recs[product_id] = rec
# Sort by confidence score
final_recommendations = sorted(
unique_recs.values(),
key=lambda x: x['confidence_score'],
reverse=True
)[:limit]
# Save recommendations to database
self._save_recommendations(user, final_recommendations)
return final_recommendations
def _get_collaborative_recommendations(self, user, limit=5):
"""Collaborative Filtering basierend auf ähnlichen Usern"""
recommendations = []
# Get users with similar behavior
similar_users = self._find_similar_users(user)
# Get products liked by similar users
for similar_user in similar_users[:5]:
liked_products = UserBehavior.objects.filter(
user=similar_user,
behavior_type__in=['purchase', 'cart_add', 'wishlist_add']
).values_list('product', flat=True).distinct()
for product_id in liked_products:
product = Product.objects.filter(id=product_id, is_active=True).first()
if product and not self._user_has_interacted(user, product):
recommendations.append({
'product': {
'id': product.id,
'name': product.name,
'price': float(product.price),
'image_url': product.image.url if product.image else None,
'category': product.category,
},
'confidence_score': self.collaborative_weight,
'recommendation_type': 'collaborative',
'reason': f'Ähnlich zu {similar_user.username}'
})
if len(recommendations) >= limit:
break
return recommendations
def _get_content_based_recommendations(self, user, profile, limit=5):
"""Content-based Filtering basierend auf User Preferences"""
recommendations = []
# Get user preferences
preferred_categories = profile.preferred_categories
preferred_fursuit_types = profile.preferred_fursuit_types
preferred_price_range = profile.preferred_price_range
# Build query based on preferences
query = Q(is_active=True)
if preferred_categories:
query &= Q(category__in=preferred_categories)
if preferred_fursuit_types:
query &= Q(fursuit_type__in=preferred_fursuit_types)
if preferred_price_range:
min_price = preferred_price_range.get('min', 0)
max_price = preferred_price_range.get('max', float('inf'))
query &= Q(price__gte=min_price, price__lte=max_price)
# Get products matching preferences
matching_products = Product.objects.filter(query).exclude(
behaviors__user=user
).distinct()[:limit*2]
for product in matching_products:
confidence = self._calculate_content_based_confidence(product, profile)
recommendations.append({
'product': {
'id': product.id,
'name': product.name,
'price': float(product.price),
'image_url': product.image.url if product.image else None,
'category': product.category,
},
'confidence_score': confidence,
'recommendation_type': 'content_based',
'reason': 'Basierend auf deinen Präferenzen'
})
# Sort by confidence and return top results
recommendations.sort(key=lambda x: x['confidence_score'], reverse=True)
return recommendations[:limit]
def _get_popular_recommendations(self, limit=5):
"""Beliebte Produkte basierend auf Verhalten"""
recommendations = []
# Get popular products
popular_products = Product.objects.annotate(
purchase_count=Count('behaviors', filter=Q(behaviors__behavior_type='purchase')),
view_count=Count('behaviors', filter=Q(behaviors__behavior_type='view')),
).filter(
is_active=True,
purchase_count__gt=0
).order_by('-purchase_count', '-view_count')[:limit]
for product in popular_products:
popularity_score = (product.purchase_count * 2 + product.view_count) / 100
confidence = min(0.8, popularity_score)
recommendations.append({
'product': {
'id': product.id,
'name': product.name,
'price': float(product.price),
'image_url': product.image.url if product.image else None,
'category': product.category,
},
'confidence_score': confidence,
'recommendation_type': 'popular',
'reason': 'Beliebt bei anderen Kunden'
})
return recommendations
def _get_trending_recommendations(self, limit=5):
"""Trending Produkte (recent activity)"""
recommendations = []
# Get recent activity (last 7 days)
recent_date = timezone.now() - timedelta(days=7)
trending_products = Product.objects.annotate(
recent_purchases=Count('behaviors', filter=Q(
behaviors__behavior_type='purchase',
behaviors__created_at__gte=recent_date
)),
recent_views=Count('behaviors', filter=Q(
behaviors__behavior_type='view',
behaviors__created_at__gte=recent_date
)),
).filter(
is_active=True,
recent_purchases__gt=0
).order_by('-recent_purchases', '-recent_views')[:limit]
for product in trending_products:
trending_score = (product.recent_purchases * 3 + product.recent_views) / 50
confidence = min(0.7, trending_score)
recommendations.append({
'product': {
'id': product.id,
'name': product.name,
'price': float(product.price),
'image_url': product.image.url if product.image else None,
'category': product.category,
},
'confidence_score': confidence,
'recommendation_type': 'trending',
'reason': 'Trending diese Woche'
})
return recommendations
def _find_similar_users(self, user, limit=10):
"""Finde ähnliche User basierend auf Verhalten"""
# Get user's behavior
user_behaviors = UserBehavior.objects.filter(user=user).values_list('product', 'behavior_type')
if not user_behaviors:
return []
# Find users with similar behavior
similar_users = []
for behavior in user_behaviors:
product_id, behavior_type = behavior
# Find users who had similar behavior with this product
similar_behaviors = UserBehavior.objects.filter(
product_id=product_id,
behavior_type=behavior_type
).exclude(user=user).values_list('user', flat=True)
similar_users.extend(similar_behaviors)
# Count occurrences and get top users
from collections import Counter
user_counts = Counter(similar_users)
return [user_id for user_id, count in user_counts.most_common(limit)]
def _user_has_interacted(self, user, product):
"""Prüfe ob User bereits mit Produkt interagiert hat"""
return UserBehavior.objects.filter(
user=user,
product=product,
behavior_type__in=['purchase', 'cart_add', 'wishlist_add', 'view']
).exists()
def _calculate_content_based_confidence(self, product, profile):
"""Berechne Confidence Score für Content-based Filtering"""
confidence = 0.5 # Base confidence
# Category match
if product.category in profile.preferred_categories:
confidence += 0.2
# Fursuit type match
if product.fursuit_type in profile.preferred_fursuit_types:
confidence += 0.2
# Price range match
if profile.preferred_price_range:
min_price = profile.preferred_price_range.get('min', 0)
max_price = profile.preferred_price_range.get('max', float('inf'))
if min_price <= product.price <= max_price:
confidence += 0.1
return min(0.9, confidence)
def _save_recommendations(self, user, recommendations):
"""Speichere Empfehlungen in Datenbank"""
# Delete old recommendations
Recommendation.objects.filter(user=user).delete()
# Create new recommendations
for rec in recommendations:
Recommendation.objects.create(
user=user,
product_id=rec['product']['id'],
recommendation_type=rec['recommendation_type'],
confidence_score=rec['confidence_score'],
reason=rec['reason'],
expires_at=timezone.now() + timedelta(days=7)
)
def update_product_similarities(self):
"""Update Produkt-Ähnlichkeitsmatrix"""
products = Product.objects.filter(is_active=True)
for product in products:
# Find similar products based on various criteria
similar_products = self._find_similar_products(product)
# Update similarity matrix
for similar_product, similarity_score, similarity_type in similar_products:
ProductSimilarity.objects.update_or_create(
product=product,
similar_product=similar_product,
defaults={
'similarity_score': similarity_score,
'similarity_type': similarity_type
}
)
def _find_similar_products(self, product):
"""Finde ähnliche Produkte"""
similar_products = []
# Category-based similarity
category_similar = Product.objects.filter(
category=product.category,
is_active=True
).exclude(id=product.id)[:5]
for similar_product in category_similar:
similarity_score = 0.7
if similar_product.fursuit_type == product.fursuit_type:
similarity_score += 0.2
if abs(similar_product.price - product.price) / product.price < 0.3:
similarity_score += 0.1
similar_products.append((similar_product, similarity_score, 'category'))
# Price-based similarity
price_range = product.price * 0.3
price_similar = Product.objects.filter(
price__range=(product.price - price_range, product.price + price_range),
is_active=True
).exclude(id=product.id)[:5]
for similar_product in price_similar:
similarity_score = 0.6
if similar_product.category == product.category:
similarity_score += 0.2
if similar_product.fursuit_type == product.fursuit_type:
similarity_score += 0.2
similar_products.append((similar_product, similarity_score, 'price_range'))
return similar_products
def train_recommendation_model(self, model_type='hybrid'):
"""Trainiere ML Model für Empfehlungen"""
# Create model record
model = RecommendationModel.objects.create(
model_type=model_type,
model_name=f"{model_type}_model",
model_version="1.0",
is_active=False
)
# Get training data
behaviors = UserBehavior.objects.all()
training_data_size = behaviors.count()
# Simple training (in production, use proper ML library)
accuracy_score = 0.75 # Placeholder
precision_score = 0.70
recall_score = 0.65
f1_score = 0.67
# Update model
model.training_data_size = training_data_size
model.accuracy_score = accuracy_score
model.precision_score = precision_score
model.recall_score = recall_score
model.f1_score = f1_score
model.trained_at = timezone.now()
model.is_active = True
model.save()
return model
def update_analytics(self):
"""Update Recommendation Analytics"""
today = timezone.now().date()
analytics, created = RecommendationAnalytics.objects.get_or_create(date=today)
# Calculate today's metrics
today_recommendations = Recommendation.objects.filter(
created_at__date=today
)
analytics.total_recommendations = today_recommendations.count()
analytics.total_clicks = today_recommendations.filter(is_clicked=True).count()
analytics.total_purchases = today_recommendations.filter(is_purchased=True).count()
if analytics.total_recommendations > 0:
analytics.click_through_rate = (analytics.total_clicks / analytics.total_recommendations) * 100
if analytics.total_clicks > 0:
analytics.conversion_rate = (analytics.total_purchases / analytics.total_clicks) * 100
# Calculate revenue
purchased_recommendations = today_recommendations.filter(is_purchased=True)
total_revenue = sum(
rec.product.price for rec in purchased_recommendations
)
analytics.revenue_from_recommendations = total_revenue
# Average recommendation score
if analytics.total_recommendations > 0:
avg_score = today_recommendations.aggregate(
avg_score=Avg('confidence_score')
)['avg_score'] or 0
analytics.avg_recommendation_score = avg_score
# Per recommendation type
analytics.collaborative_recommendations = today_recommendations.filter(
recommendation_type='collaborative'
).count()
analytics.content_based_recommendations = today_recommendations.filter(
recommendation_type='content_based'
).count()
analytics.popular_recommendations = today_recommendations.filter(
recommendation_type='popular'
).count()
analytics.personalized_recommendations = today_recommendations.filter(
recommendation_type='personalized'
).count()
analytics.save()
return analytics

View File

@ -0,0 +1,365 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Recommendation Analytics - Fursuit Shop{% endblock %}
{% block extra_css %}
<style>
.analytics-page {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.analytics-header {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.analytics-title {
color: #4a5568;
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 10px;
text-align: center;
}
.analytics-subtitle {
color: #718096;
text-align: center;
font-size: 1.1rem;
}
.totals-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.total-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
padding: 25px;
text-align: center;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.total-card:hover {
transform: translateY(-5px);
}
.total-number {
font-size: 2rem;
font-weight: bold;
color: #667eea;
margin-bottom: 10px;
}
.total-label {
color: #4a5568;
font-size: 0.9rem;
font-weight: 500;
}
.analytics-content {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.section-title {
color: #4a5568;
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 3px solid #667eea;
}
.analytics-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.analytics-table th,
.analytics-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e2e8f0;
}
.analytics-table th {
background: #f7fafc;
color: #4a5568;
font-weight: bold;
}
.analytics-table tr:hover {
background: #f7fafc;
}
.metric-cell {
font-weight: bold;
color: #667eea;
}
.percentage-cell {
color: #38a169;
font-weight: bold;
}
.revenue-cell {
color: #d69e2e;
font-weight: bold;
}
.chart-container {
margin-top: 30px;
padding: 20px;
background: #f7fafc;
border-radius: 15px;
}
.chart-placeholder {
height: 300px;
background: linear-gradient(45deg, #e2e8f0 25%, transparent 25%),
linear-gradient(-45deg, #e2e8f0 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #e2e8f0 75%),
linear-gradient(-45deg, transparent 75%, #e2e8f0 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #718096;
font-size: 1.2rem;
}
.furry-decoration {
position: absolute;
top: 20px;
right: 20px;
font-size: 3rem;
opacity: 0.1;
}
.date-filter {
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.filter-form {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.filter-input {
padding: 10px 15px;
border: 2px solid #e2e8f0;
border-radius: 10px;
background: white;
color: #4a5568;
}
.filter-button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
transition: background 0.3s ease;
}
.filter-button:hover {
background: #5a67d8;
}
@media (max-width: 768px) {
.totals-grid {
grid-template-columns: repeat(2, 1fr);
}
.filter-form {
flex-direction: column;
align-items: stretch;
}
}
</style>
{% endblock %}
{% block content %}
<div class="analytics-page">
<div class="furry-decoration">📊</div>
<div class="analytics-header">
<h1 class="analytics-title">Recommendation Analytics</h1>
<p class="analytics-subtitle">Detaillierte Einblicke in Empfehlungs-Performance</p>
</div>
<!-- Date Filter -->
<div class="date-filter">
<form class="filter-form" method="get">
<input type="date" name="start_date" class="filter-input" placeholder="Start Date">
<input type="date" name="end_date" class="filter-input" placeholder="End Date">
<button type="submit" class="filter-button">Filter anwenden</button>
</form>
</div>
<!-- Totals Grid -->
<div class="totals-grid">
<div class="total-card">
<div class="total-number">{{ totals.recommendations|floatformat:0 }}</div>
<div class="total-label">Gesamt Empfehlungen</div>
</div>
<div class="total-card">
<div class="total-number">{{ totals.clicks|floatformat:0 }}</div>
<div class="total-label">Gesamt Klicks</div>
</div>
<div class="total-card">
<div class="total-number">{{ totals.purchases|floatformat:0 }}</div>
<div class="total-label">Gesamt Käufe</div>
</div>
<div class="total-card">
<div class="total-number">{{ totals.ctr|floatformat:1 }}%</div>
<div class="total-label">Durchschnitt CTR</div>
</div>
<div class="total-card">
<div class="total-number">{{ totals.conversion_rate|floatformat:1 }}%</div>
<div class="total-label">Durchschnitt Conversion</div>
</div>
<div class="total-card">
<div class="total-number">€{{ totals.revenue|floatformat:2 }}</div>
<div class="total-label">Gesamt Umsatz</div>
</div>
</div>
<!-- Analytics Content -->
<div class="analytics-content">
<h2 class="section-title">📈 Tägliche Analytics</h2>
<table class="analytics-table">
<thead>
<tr>
<th>Datum</th>
<th>Empfehlungen</th>
<th>Klicks</th>
<th>Käufe</th>
<th>CTR</th>
<th>Conversion</th>
<th>Umsatz</th>
<th>Durchschnitt Score</th>
</tr>
</thead>
<tbody>
{% for analytic in analytics %}
<tr>
<td>{{ analytic.date|date:"d.m.Y" }}</td>
<td class="metric-cell">{{ analytic.total_recommendations }}</td>
<td class="metric-cell">{{ analytic.total_clicks }}</td>
<td class="metric-cell">{{ analytic.total_purchases }}</td>
<td class="percentage-cell">{{ analytic.click_through_rate|floatformat:1 }}%</td>
<td class="percentage-cell">{{ analytic.conversion_rate|floatformat:1 }}%</td>
<td class="revenue-cell">€{{ analytic.revenue_from_recommendations|floatformat:2 }}</td>
<td class="metric-cell">{{ analytic.avg_recommendation_score|floatformat:2 }}</td>
</tr>
{% empty %}
<tr>
<td colspan="8" style="text-align: center; color: #718096; padding: 20px;">
Keine Analytics-Daten verfügbar
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Chart Placeholder -->
<div class="chart-container">
<h3 style="color: #4a5568; margin-bottom: 15px;">📊 Performance Trends</h3>
<div class="chart-placeholder">
Chart wird hier angezeigt (Chart.js Integration geplant)
</div>
</div>
<!-- Recommendation Type Breakdown -->
<h2 class="section-title" style="margin-top: 40px;">🎯 Empfehlungs-Typen</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-top: 20px;">
<div class="total-card">
<div class="total-number">{{ totals.collaborative|default:0 }}</div>
<div class="total-label">Collaborative</div>
</div>
<div class="total-card">
<div class="total-number">{{ totals.content_based|default:0 }}</div>
<div class="total-label">Content-based</div>
</div>
<div class="total-card">
<div class="total-number">{{ totals.popular|default:0 }}</div>
<div class="total-label">Popular</div>
</div>
<div class="total-card">
<div class="total-number">{{ totals.personalized|default:0 }}</div>
<div class="total-label">Personalized</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Add interactive features
document.addEventListener('DOMContentLoaded', function() {
// Add hover effects to total cards
const totalCards = document.querySelectorAll('.total-card');
totalCards.forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-5px) scale(1.02)';
});
card.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0) scale(1)';
});
});
// Add row hover effects
const tableRows = document.querySelectorAll('.analytics-table tbody tr');
tableRows.forEach(row => {
row.addEventListener('mouseenter', function() {
this.style.backgroundColor = '#edf2f7';
});
row.addEventListener('mouseleave', function() {
this.style.backgroundColor = '';
});
});
// Auto-refresh every 5 minutes
setInterval(function() {
location.reload();
}, 300000);
});
</script>
{% endblock %}

View File

@ -0,0 +1,306 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Recommendation Dashboard - Fursuit Shop{% endblock %}
{% block extra_css %}
<style>
.recommendation-dashboard {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.dashboard-header {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.dashboard-title {
color: #4a5568;
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 10px;
text-align: center;
}
.dashboard-subtitle {
color: #718096;
text-align: center;
font-size: 1.1rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
padding: 25px;
text-align: center;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-number {
font-size: 2.5rem;
font-weight: bold;
color: #667eea;
margin-bottom: 10px;
}
.stat-label {
color: #4a5568;
font-size: 1rem;
font-weight: 500;
}
.content-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 30px;
}
.main-content {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.sidebar {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
height: fit-content;
}
.section-title {
color: #4a5568;
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 3px solid #667eea;
}
.behavior-list {
max-height: 400px;
overflow-y: auto;
}
.behavior-item {
background: #f7fafc;
border-radius: 10px;
padding: 15px;
margin-bottom: 10px;
border-left: 4px solid #667eea;
}
.behavior-user {
font-weight: bold;
color: #4a5568;
}
.behavior-type {
color: #667eea;
font-size: 0.9rem;
}
.behavior-time {
color: #718096;
font-size: 0.8rem;
}
.model-card {
background: #f7fafc;
border-radius: 10px;
padding: 15px;
margin-bottom: 15px;
border: 2px solid #e2e8f0;
}
.model-name {
font-weight: bold;
color: #4a5568;
margin-bottom: 5px;
}
.model-accuracy {
color: #667eea;
font-size: 0.9rem;
}
.test-card {
background: #f7fafc;
border-radius: 10px;
padding: 15px;
margin-bottom: 15px;
border: 2px solid #e2e8f0;
}
.test-name {
font-weight: bold;
color: #4a5568;
margin-bottom: 5px;
}
.test-status {
color: #667eea;
font-size: 0.9rem;
}
.furry-decoration {
position: absolute;
top: 20px;
right: 20px;
font-size: 3rem;
opacity: 0.1;
}
@media (max-width: 768px) {
.content-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
{% endblock %}
{% block content %}
<div class="recommendation-dashboard">
<div class="furry-decoration">🦊</div>
<div class="dashboard-header">
<h1 class="dashboard-title">Recommendation Dashboard</h1>
<p class="dashboard-subtitle">ML-basierte Empfehlungen & Analytics</p>
</div>
<!-- Stats Grid -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">{{ today_analytics.total_recommendations }}</div>
<div class="stat-label">Empfehlungen heute</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ today_analytics.total_clicks }}</div>
<div class="stat-label">Klicks heute</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ today_analytics.total_purchases }}</div>
<div class="stat-label">Käufe heute</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ today_analytics.click_through_rate|floatformat:1 }}%</div>
<div class="stat-label">Click-Through Rate</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ today_analytics.conversion_rate|floatformat:1 }}%</div>
<div class="stat-label">Conversion Rate</div>
</div>
<div class="stat-card">
<div class="stat-number">€{{ today_analytics.revenue_from_recommendations|floatformat:2 }}</div>
<div class="stat-label">Umsatz aus Empfehlungen</div>
</div>
</div>
<!-- Content Grid -->
<div class="content-grid">
<div class="main-content">
<h2 class="section-title">📊 Aktuelle Aktivitäten</h2>
<div class="behavior-list">
{% for behavior in recent_behaviors %}
<div class="behavior-item">
<div class="behavior-user">{{ behavior.user.username }}</div>
<div class="behavior-type">{{ behavior.get_behavior_type_display }}</div>
{% if behavior.product %}
<div class="behavior-product">Produkt: {{ behavior.product.name }}</div>
{% endif %}
<div class="behavior-time">{{ behavior.created_at|timesince }} ago</div>
</div>
{% empty %}
<p style="color: #718096; text-align: center; padding: 20px;">
Noch keine Aktivitäten heute
</p>
{% endfor %}
</div>
</div>
<div class="sidebar">
<h2 class="section-title">🤖 ML Models</h2>
{% for model in active_models %}
<div class="model-card">
<div class="model-name">{{ model.model_name }}</div>
<div class="model-accuracy">Accuracy: {{ model.accuracy_score|floatformat:2 }}</div>
<div class="model-accuracy">Type: {{ model.get_model_type_display }}</div>
</div>
{% empty %}
<p style="color: #718096; text-align: center; padding: 20px;">
Keine aktiven Models
</p>
{% endfor %}
<h2 class="section-title" style="margin-top: 30px;">🧪 A/B Tests</h2>
{% for test in active_tests %}
<div class="test-card">
<div class="test-name">{{ test.test_name }}</div>
<div class="test-status">Status: {{ test.get_status_display }}</div>
<div class="test-status">Traffic: {{ test.traffic_split|floatformat:0 }}%</div>
</div>
{% empty %}
<p style="color: #718096; text-align: center; padding: 20px;">
Keine aktiven Tests
</p>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Auto-refresh dashboard every 30 seconds
setInterval(function() {
location.reload();
}, 30000);
// Add some interactive elements
document.addEventListener('DOMContentLoaded', function() {
// Add hover effects to stat cards
const statCards = document.querySelectorAll('.stat-card');
statCards.forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-5px) scale(1.02)';
});
card.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0) scale(1)';
});
});
});
</script>
{% endblock %}

26
recommendations/urls.py Normal file
View File

@ -0,0 +1,26 @@
"""
Recommendation Engine URLs
"""
from django.urls import path
from . import views
app_name = 'recommendations'
urlpatterns = [
# API Endpoints
path('api/recommendations/', views.get_recommendations, name='api_recommendations'),
path('api/similar-products/<int:product_id>/', views.get_similar_products, name='api_similar_products'),
path('api/track-behavior/', views.track_behavior, name='api_track_behavior'),
path('api/user-profile/', views.get_user_profile, name='api_user_profile'),
path('api/update-preferences/', views.update_user_preferences, name='api_update_preferences'),
path('api/popular-products/', views.get_popular_products, name='api_popular_products'),
path('api/trending-products/', views.get_trending_products, name='api_trending_products'),
path('api/frequently-bought/<int:product_id>/', views.get_frequently_bought_together, name='api_frequently_bought'),
path('api/track-recommendation-click/', views.track_recommendation_click, name='api_track_click'),
path('api/track-recommendation-purchase/', views.track_recommendation_purchase, name='api_track_purchase'),
# Admin Views
path('dashboard/', views.recommendation_dashboard, name='dashboard'),
path('analytics/', views.recommendation_analytics, name='analytics'),
]

442
recommendations/views.py Normal file
View File

@ -0,0 +1,442 @@
"""
Recommendation Engine Views für ML-basierte Empfehlungen
"""
from django.shortcuts import render
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from django.utils import timezone
from django.db.models import Q, Count, Avg, Sum
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from .models import UserBehavior, UserProfile, ProductSimilarity, Recommendation, RecommendationModel, ABTest, RecommendationAnalytics
from .services import RecommendationService
import json
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_recommendations(request):
"""Personalisiertes Empfehlungen für User"""
try:
# Get user profile
profile, created = UserProfile.objects.get_or_create(user=request.user)
# Get recommendations
recommendation_service = RecommendationService()
recommendations = recommendation_service.get_recommendations_for_user(request.user)
# Track recommendation view
UserBehavior.objects.create(
user=request.user,
behavior_type='view',
metadata={'recommendation_count': len(recommendations)}
)
return Response({
'recommendations': recommendations,
'user_profile': {
'engagement_score': profile.engagement_score,
'loyalty_level': profile.loyalty_level,
'preferred_categories': profile.preferred_categories,
}
})
except Exception as e:
return Response({'error': str(e)}, status=500)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_similar_products(request, product_id):
"""Ähnliche Produkte für ein spezifisches Produkt"""
try:
from products.models import Product
product = Product.objects.get(id=product_id)
# Get similar products
similar_products = ProductSimilarity.objects.filter(
product=product
).select_related('similar_product').order_by('-similarity_score')[:10]
recommendations = []
for similarity in similar_products:
recommendations.append({
'product': {
'id': similarity.similar_product.id,
'name': similarity.similar_product.name,
'price': float(similarity.similar_product.price),
'image_url': similarity.similar_product.image.url if similarity.similar_product.image else None,
},
'similarity_score': similarity.similarity_score,
'similarity_type': similarity.similarity_type,
'reason': f"Ähnlich zu {product.name}"
})
return Response({'similar_products': recommendations})
except Product.DoesNotExist:
return Response({'error': 'Product not found'}, status=404)
except Exception as e:
return Response({'error': str(e)}, status=500)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def track_behavior(request):
"""User Behavior tracken"""
try:
data = request.data
behavior_type = data.get('behavior_type')
product_id = data.get('product_id')
auction_id = data.get('auction_id')
metadata = data.get('metadata', {})
if not behavior_type:
return Response({'error': 'Behavior type required'}, status=400)
# Create behavior record
behavior = UserBehavior.objects.create(
user=request.user,
behavior_type=behavior_type,
product_id=product_id,
auction_id=auction_id,
session_id=data.get('session_id', ''),
ip_address=request.META.get('REMOTE_ADDR'),
user_agent=request.META.get('HTTP_USER_AGENT', ''),
metadata=metadata
)
# Update user profile
profile, created = UserProfile.objects.get_or_create(user=request.user)
profile.update_engagement_score()
return Response({'behavior_id': str(behavior.id)})
except Exception as e:
return Response({'error': str(e)}, status=500)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_user_profile(request):
"""User Profile für Empfehlungen abrufen"""
try:
profile, created = UserProfile.objects.get_or_create(user=request.user)
return Response({
'engagement_score': profile.engagement_score,
'loyalty_level': profile.loyalty_level,
'preferred_categories': profile.preferred_categories,
'preferred_fursuit_types': profile.preferred_fursuit_types,
'preferred_price_range': profile.preferred_price_range,
'total_purchases': profile.total_purchases,
'total_views': profile.total_views,
'last_active': profile.last_active,
})
except Exception as e:
return Response({'error': str(e)}, status=500)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def update_user_preferences(request):
"""User Preferences aktualisieren"""
try:
data = request.data
profile, created = UserProfile.objects.get_or_create(user=request.user)
# Update preferences
if 'preferred_categories' in data:
profile.preferred_categories = data['preferred_categories']
if 'preferred_fursuit_types' in data:
profile.preferred_fursuit_types = data['preferred_fursuit_types']
if 'preferred_price_range' in data:
profile.preferred_price_range = data['preferred_price_range']
if 'preferred_colors' in data:
profile.preferred_colors = data['preferred_colors']
profile.save()
return Response({'message': 'Preferences updated'})
except Exception as e:
return Response({'error': str(e)}, status=500)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_popular_products(request):
"""Beliebte Produkte basierend auf Verhalten"""
try:
# Get popular products based on behavior
popular_products = Product.objects.annotate(
view_count=Count('behaviors', filter=Q(behaviors__behavior_type='view')),
purchase_count=Count('behaviors', filter=Q(behaviors__behavior_type='purchase')),
cart_count=Count('behaviors', filter=Q(behaviors__behavior_type='cart_add')),
).filter(
is_active=True
).order_by('-purchase_count', '-view_count')[:10]
recommendations = []
for product in popular_products:
recommendations.append({
'product': {
'id': product.id,
'name': product.name,
'price': float(product.price),
'image_url': product.image.url if product.image else None,
'category': product.category,
},
'popularity_score': product.purchase_count + (product.view_count * 0.1),
'reason': 'Beliebt bei anderen Kunden'
})
return Response({'popular_products': recommendations})
except Exception as e:
return Response({'error': str(e)}, status=500)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_trending_products(request):
"""Trending Produkte (recent activity)"""
try:
# Get trending products (recent activity)
from datetime import timedelta
recent_date = timezone.now() - timedelta(days=7)
trending_products = Product.objects.annotate(
recent_views=Count('behaviors', filter=Q(
behaviors__behavior_type='view',
behaviors__created_at__gte=recent_date
)),
recent_purchases=Count('behaviors', filter=Q(
behaviors__behavior_type='purchase',
behaviors__created_at__gte=recent_date
)),
).filter(
is_active=True,
recent_views__gt=0
).order_by('-recent_purchases', '-recent_views')[:10]
recommendations = []
for product in trending_products:
recommendations.append({
'product': {
'id': product.id,
'name': product.name,
'price': float(product.price),
'image_url': product.image.url if product.image else None,
'category': product.category,
},
'trending_score': product.recent_purchases + (product.recent_views * 0.1),
'reason': 'Trending diese Woche'
})
return Response({'trending_products': recommendations})
except Exception as e:
return Response({'error': str(e)}, status=500)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_frequently_bought_together(request, product_id):
"""Häufig zusammen gekaufte Produkte"""
try:
from products.models import Product
product = Product.objects.get(id=product_id)
# Get users who bought this product
buyers = UserBehavior.objects.filter(
product=product,
behavior_type='purchase'
).values_list('user', flat=True).distinct()
# Get other products bought by same users
frequently_bought = Product.objects.filter(
behaviors__user__in=buyers,
behaviors__behavior_type='purchase'
).exclude(id=product_id).annotate(
bought_together_count=Count('behaviors', filter=Q(
behaviors__behavior_type='purchase',
behaviors__user__in=buyers
))
).filter(
bought_together_count__gt=0,
is_active=True
).order_by('-bought_together_count')[:5]
recommendations = []
for product in frequently_bought:
recommendations.append({
'product': {
'id': product.id,
'name': product.name,
'price': float(product.price),
'image_url': product.image.url if product.image else None,
},
'bought_together_count': product.bought_together_count,
'reason': 'Häufig zusammen gekauft'
})
return Response({'frequently_bought_together': recommendations})
except Product.DoesNotExist:
return Response({'error': 'Product not found'}, status=404)
except Exception as e:
return Response({'error': str(e)}, status=500)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def track_recommendation_click(request):
"""Recommendation Click tracken"""
try:
data = request.data
product_id = data.get('product_id')
recommendation_type = data.get('recommendation_type')
if not product_id or not recommendation_type:
return Response({'error': 'Product ID and recommendation type required'}, status=400)
# Update recommendation
recommendation = Recommendation.objects.filter(
user=request.user,
product_id=product_id,
recommendation_type=recommendation_type
).first()
if recommendation:
recommendation.is_clicked = True
recommendation.save()
# Track behavior
UserBehavior.objects.create(
user=request.user,
behavior_type='view',
product_id=product_id,
metadata={'recommendation_type': recommendation_type}
)
return Response({'success': True})
except Exception as e:
return Response({'error': str(e)}, status=500)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def track_recommendation_purchase(request):
"""Recommendation Purchase tracken"""
try:
data = request.data
product_id = data.get('product_id')
recommendation_type = data.get('recommendation_type')
if not product_id or not recommendation_type:
return Response({'error': 'Product ID and recommendation type required'}, status=400)
# Update recommendation
recommendation = Recommendation.objects.filter(
user=request.user,
product_id=product_id,
recommendation_type=recommendation_type
).first()
if recommendation:
recommendation.is_purchased = True
recommendation.save()
# Track behavior
UserBehavior.objects.create(
user=request.user,
behavior_type='purchase',
product_id=product_id,
metadata={'recommendation_type': recommendation_type}
)
return Response({'success': True})
except Exception as e:
return Response({'error': str(e)}, status=500)
# Admin Views
@login_required
def recommendation_dashboard(request):
"""Admin Dashboard für Empfehlungen"""
if not request.user.is_staff:
return JsonResponse({'error': 'Access denied'}, status=403)
# Get analytics
today_analytics = RecommendationAnalytics.get_or_create_today()
# Get recent behaviors
recent_behaviors = UserBehavior.objects.select_related('user', 'product').order_by('-created_at')[:20]
# Get active models
active_models = RecommendationModel.objects.filter(is_active=True)
# Get A/B tests
active_tests = ABTest.objects.filter(status='running')
context = {
'today_analytics': today_analytics,
'recent_behaviors': recent_behaviors,
'active_models': active_models,
'active_tests': active_tests,
}
return render(request, 'recommendations/dashboard.html', context)
@login_required
def recommendation_analytics(request):
"""Recommendation Analytics"""
if not request.user.is_staff:
return JsonResponse({'error': 'Access denied'}, status=403)
# Get analytics for last 30 days
from datetime import timedelta
analytics = RecommendationAnalytics.objects.filter(
date__gte=timezone.now().date() - timedelta(days=30)
).order_by('date')
# Calculate totals
total_recommendations = sum(a.total_recommendations for a in analytics)
total_clicks = sum(a.total_clicks for a in analytics)
total_purchases = sum(a.total_purchases for a in analytics)
total_revenue = sum(a.revenue_from_recommendations for a in analytics)
context = {
'analytics': analytics,
'totals': {
'recommendations': total_recommendations,
'clicks': total_clicks,
'purchases': total_purchases,
'revenue': total_revenue,
'ctr': (total_clicks / total_recommendations * 100) if total_recommendations > 0 else 0,
'conversion_rate': (total_purchases / total_clicks * 100) if total_clicks > 0 else 0,
}
}
return render(request, 'recommendations/analytics.html', context)

35
requirements-simple.txt Normal file
View File

@ -0,0 +1,35 @@
# Django Core
Django==5.2.1
djangorestframework==3.14.0
django-cors-headers==4.3.1
# Database (SQLite für einfaches Setup)
# psycopg2-binary==2.9.9
# Image Processing
Pillow==10.1.0
# Payment Processing
stripe==7.8.0
# Search Engine (optional)
# django-haystack==3.2.1
# elasticsearch==8.11.0
# Real-time Features (optional)
# channels==4.0.0
# channels-redis==4.1.0
# daphne==4.0.0
# API Documentation
drf-yasg==1.21.7
# Development Tools
django-debug-toolbar==4.2.0
# Production
gunicorn==21.2.0
whitenoise==6.6.0
# Utilities
python-decouple==3.8

42
requirements.txt Normal file
View File

@ -0,0 +1,42 @@
# Django Core
Django==5.2.1
djangorestframework==3.14.0
django-cors-headers==4.3.1
# Database
psycopg2-binary==2.9.9
# Image Processing
Pillow==10.1.0
# Payment Processing
stripe==7.8.0
# Search Engine
django-haystack==3.3.0
elasticsearch==7.17.12
# Real-time Features
channels==4.0.0
channels-redis==4.1.0
daphne==4.0.0
# API Documentation
drf-yasg==1.21.7
# Development Tools
django-debug-toolbar==4.2.0
# Production
gunicorn==21.2.0
whitenoise==6.6.0
# Utilities
python-decouple==3.8
python-dotenv==1.0.1
django-paypal==2.1.0
django-payments==3.0.1
django-filter==24.2
paypalrestsdk==1.13.3
coreapi==2.3.3
numpy==1.24.3

87
search/indexes.py Normal file
View File

@ -0,0 +1,87 @@
"""
Haystack Search Indexes für Elasticsearch
"""
# from haystack import indexes
# from haystack import site
from products.models import Product
from auction.models import Auction
class ProductIndex(indexes.SearchIndex, indexes.Indexable):
"""Product Search Index"""
text = indexes.CharField(document=True, use_template=True)
title = indexes.CharField(model_attr='name', boost=2.0)
description = indexes.CharField(model_attr='description', boost=1.5)
category = indexes.CharField(model_attr='category', faceted=True)
fursuit_type = indexes.CharField(model_attr='fursuit_type', faceted=True)
price = indexes.DecimalField(model_attr='price')
featured = indexes.BooleanField(model_attr='featured')
created_at = indexes.DateTimeField(model_attr='created_at')
# Autocomplete fields
title_auto = indexes.EdgeNgramField(model_attr='name')
description_auto = indexes.EdgeNgramField(model_attr='description')
category_auto = indexes.EdgeNgramField(model_attr='category')
# Suggest field
title_suggest = indexes.CharField(model_attr='name')
def get_model(self):
return Product
def index_queryset(self, using=None):
"""Used when the entire index for model is updated."""
return self.get_model().objects.filter(is_active=True)
def prepare_text(self, obj):
"""Prepare text field for search"""
return f"{obj.name} {obj.description} {obj.category} {obj.fursuit_type}"
def prepare_title_suggest(self, obj):
"""Prepare suggestion field"""
return obj.name
class AuctionIndex(indexes.SearchIndex, indexes.Indexable):
"""Auction Search Index"""
text = indexes.CharField(document=True, use_template=True)
title = indexes.CharField(model_attr='title', boost=2.0)
description = indexes.CharField(model_attr='description', boost=1.5)
character_description = indexes.CharField(model_attr='character_description', boost=1.2)
fursuit_type = indexes.CharField(model_attr='fursuit_type', faceted=True)
status = indexes.CharField(model_attr='status', faceted=True)
starting_bid = indexes.DecimalField(model_attr='starting_bid')
current_bid = indexes.DecimalField(model_attr='current_bid')
created_at = indexes.DateTimeField(model_attr='created_at')
end_time = indexes.DateTimeField(model_attr='end_time')
# Autocomplete fields
title_auto = indexes.EdgeNgramField(model_attr='title')
description_auto = indexes.EdgeNgramField(model_attr='description')
character_description_auto = indexes.EdgeNgramField(model_attr='character_description')
# Suggest field
title_suggest = indexes.CharField(model_attr='title')
def get_model(self):
return Auction
def index_queryset(self, using=None):
"""Used when the entire index for model is updated."""
return self.get_model().objects.filter(status__in=['active', 'draft'])
def prepare_text(self, obj):
"""Prepare text field for search"""
return f"{obj.title} {obj.description} {obj.character_description} {obj.fursuit_type}"
def prepare_title_suggest(self, obj):
"""Prepare suggestion field"""
return obj.title
# Register indexes
# site.register(Product, ProductIndex)
# site.register(Auction, AuctionIndex)

Some files were not shown because too many files have changed in this diff Show More