316 lines
10 KiB
Python
316 lines
10 KiB
Python
"""
|
|
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() |