feat(market): add draft status and multiplier
This commit is contained in:
parent
79e7cef3ba
commit
e557fe2cda
@ -23,16 +23,29 @@ class MarketAdmin(admin.ModelAdmin):
|
||||
inlines = [MarketOptionInline]
|
||||
fieldsets = (
|
||||
("Info", {"fields": ["uuid", "title", "description"]}),
|
||||
("Configuration", {"fields": ["end_date"]}),
|
||||
("Configuration", {"fields": ["end_date", "multiplier"]}),
|
||||
("Status", {"fields": ["status", "winning_option"]}),
|
||||
("Metadata", {"fields": ["created_by", "created_at", "updated_at"]}),
|
||||
)
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
# Prevent any changes to resolved markets
|
||||
if obj and obj.status == Market.Status.RESOLVED:
|
||||
return False
|
||||
return super().has_change_permission(request, obj)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change: # Creating new market
|
||||
obj.created_by = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
@admin.action(description="Publish selected draft markets")
|
||||
def publish_markets(self, request, queryset):
|
||||
updated = queryset.filter(status=Market.Status.DRAFT).update(
|
||||
status=Market.Status.OPEN
|
||||
)
|
||||
self.message_user(request, f"Published {updated} market(s).")
|
||||
|
||||
@admin.action(description="Close selected markets")
|
||||
def close_markets(self, request, queryset):
|
||||
updated = queryset.filter(status=Market.Status.OPEN).update(
|
||||
@ -40,7 +53,7 @@ class MarketAdmin(admin.ModelAdmin):
|
||||
)
|
||||
self.message_user(request, f"Closed {updated} market(s).")
|
||||
|
||||
actions = ["close_markets"]
|
||||
actions = ["publish_markets", "close_markets"]
|
||||
|
||||
|
||||
@admin.register(MarketOption)
|
||||
|
||||
@ -20,8 +20,8 @@ router = Router(tags=["market"])
|
||||
|
||||
@router.get("/", response=List[MarketListSchema])
|
||||
def list_markets(request):
|
||||
"""List all markets."""
|
||||
markets = Market.objects.all()
|
||||
"""List all markets (excludes draft markets)."""
|
||||
markets = Market.objects.exclude(status=Market.Status.DRAFT)
|
||||
# Prefetch options with total_bets annotation sorted by total_bets desc, then text asc
|
||||
options_queryset = MarketOption.objects.annotate(
|
||||
total_bets=Coalesce(Sum("user_bets__amount"), 0)
|
||||
@ -52,7 +52,7 @@ def close_market(request, market_uuid: str):
|
||||
market = get_object_or_404(Market, uuid=market_uuid)
|
||||
market.status = Market.Status.CLOSED
|
||||
market.save(update_fields=["status", "updated_at"])
|
||||
return {"status": "closed"}
|
||||
return {"status": Market.Status.CLOSED}
|
||||
|
||||
|
||||
@router.post("/{market_uuid}/actions/resolve", response=MarketListSchema)
|
||||
@ -91,10 +91,12 @@ def resolve_market(request, market_uuid: str, payload: ResolveMarketSchema):
|
||||
users_to_update = []
|
||||
|
||||
with transaction.atomic():
|
||||
# Award payouts to winners
|
||||
# Award payouts to winners with multiplier
|
||||
if total_winning > 0:
|
||||
for bet in winning_bets:
|
||||
payout = round(bet.amount / total_winning * total_pot)
|
||||
payout = round(
|
||||
bet.amount / total_winning * total_pot * market.multiplier
|
||||
)
|
||||
bet.user.points += payout
|
||||
users_to_update.append(bet.user)
|
||||
point_changes.append(
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.7 on 2026-05-23 18:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("market", "0004_alter_marketoption_options_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="market",
|
||||
name="multiplier",
|
||||
field=models.FloatField(default=1.0),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.2.7 on 2026-05-23 18:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("market", "0005_market_multiplier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="market",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("draft", "Draft"),
|
||||
("open", "Open"),
|
||||
("closed", "Closed"),
|
||||
("resolved", "Resolved"),
|
||||
],
|
||||
default="draft",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -13,6 +13,7 @@ class BaseModel(models.Model):
|
||||
|
||||
class Market(BaseModel):
|
||||
class Status(models.TextChoices):
|
||||
DRAFT = "draft", "Draft"
|
||||
OPEN = "open", "Open"
|
||||
CLOSED = "closed", "Closed"
|
||||
RESOLVED = "resolved", "Resolved"
|
||||
@ -20,9 +21,10 @@ class Market(BaseModel):
|
||||
title = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
status = models.CharField(
|
||||
max_length=10, choices=Status.choices, default=Status.OPEN
|
||||
max_length=10, choices=Status.choices, default=Status.DRAFT
|
||||
)
|
||||
end_date = models.DateTimeField()
|
||||
multiplier = models.FloatField(default=1.0)
|
||||
created_by = models.ForeignKey("accounts.CustomUser", on_delete=models.PROTECT)
|
||||
winning_option = models.ForeignKey(
|
||||
"MarketOption",
|
||||
|
||||
@ -21,6 +21,7 @@ class MarketListSchema(Schema):
|
||||
description: str
|
||||
status: str
|
||||
end_date: datetime
|
||||
multiplier: float = 1.0
|
||||
created_at: datetime
|
||||
options: List[MarketOptionSchema]
|
||||
winning_option: Optional[MarketOptionSchema] = None
|
||||
|
||||
@ -67,18 +67,8 @@ def get_user_info(request):
|
||||
user = request.user
|
||||
|
||||
if user.is_authenticated:
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"email": user.email,
|
||||
"is_authenticated": True,
|
||||
"is_staff": user.is_staff,
|
||||
"is_superuser": user.is_superuser,
|
||||
"cas_groups": getattr(user, "cas_groups", []),
|
||||
}
|
||||
else:
|
||||
return user
|
||||
|
||||
return {
|
||||
"is_authenticated": False,
|
||||
"is_staff": False,
|
||||
|
||||
@ -77,19 +77,31 @@ onMounted(async () => {
|
||||
<div class="space-y-8">
|
||||
<!-- My Bets Section -->
|
||||
<div v-if="userInfo?.is_authenticated">
|
||||
<h3 class="text-2xl font-bold mb-4 flex items-center gap-2">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-2xl font-bold flex items-center gap-2">
|
||||
<i class="mdi mdi-heart text-error"></i>
|
||||
My Bets
|
||||
</h3>
|
||||
<div class="text-lg font-semibold">
|
||||
<span class="text-primary">{{ userInfo.points }}</span>
|
||||
<span class="text-base-content/60 ml-1">pts</span>
|
||||
</div>
|
||||
</div>
|
||||
<UserBets :markets="markets" @refresh="reloadPage" />
|
||||
</div>
|
||||
|
||||
<!-- All Markets Section -->
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold mb-4 flex items-center gap-2">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-2xl font-bold flex items-center gap-2">
|
||||
<i class="mdi mdi-list"></i>
|
||||
All Markets
|
||||
</h3>
|
||||
<a v-if="userInfo?.is_superuser" href="/admin/market/market/add/" class="btn btn-sm btn-primary">
|
||||
<i class="mdi mdi-plus"></i>
|
||||
Create Market
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="markets.length === 0" class="alert">
|
||||
<i class="mdi mdi-information mr-2"></i>
|
||||
@ -97,12 +109,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<MarketCard
|
||||
v-for="market in markets"
|
||||
:key="market.uuid"
|
||||
:market="market"
|
||||
@refresh="reloadPage"
|
||||
/>
|
||||
<MarketCard v-for="market in markets" :key="market.uuid" :market="market" @refresh="reloadPage" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -194,7 +194,7 @@ export const gamesApiListGames = <ThrowOnError extends boolean = false>(options?
|
||||
/**
|
||||
* List Markets
|
||||
*
|
||||
* List all markets.
|
||||
* List all markets (excludes draft markets).
|
||||
*/
|
||||
export const marketApiListMarkets = <ThrowOnError extends boolean = false>(options?: Options<MarketApiListMarketsData, ThrowOnError>) => (options?.client ?? client).get<MarketApiListMarketsResponses, unknown, ThrowOnError>({ url: '/api/market/', ...options });
|
||||
|
||||
|
||||
@ -30,6 +30,10 @@ export type UserInfoOut = {
|
||||
* Email
|
||||
*/
|
||||
email?: string | null;
|
||||
/**
|
||||
* Points
|
||||
*/
|
||||
points?: number;
|
||||
/**
|
||||
* Is Authenticated
|
||||
*/
|
||||
@ -986,6 +990,10 @@ export type MarketListSchema = {
|
||||
* End Date
|
||||
*/
|
||||
end_date: string;
|
||||
/**
|
||||
* Multiplier
|
||||
*/
|
||||
multiplier?: number;
|
||||
/**
|
||||
* Created At
|
||||
*/
|
||||
|
||||
@ -41,6 +41,8 @@ const timeRemaining = computed(() => {
|
||||
|
||||
const statusColor = computed(() => {
|
||||
switch (props.market.status) {
|
||||
case "draft":
|
||||
return "badge-secondary";
|
||||
case "open":
|
||||
return "badge-success";
|
||||
case "closed":
|
||||
@ -68,7 +70,7 @@ const getMultiplier = (option: MarketOption) => {
|
||||
const getPotentialGain = (option: MarketOption) => {
|
||||
if (!existingBet.value || existingBet.value.option.uuid !== option.uuid) return 0;
|
||||
const multiplier = getMultiplier(option);
|
||||
return Math.round(existingBet.value.amount * multiplier);
|
||||
return Math.round(existingBet.value.amount * multiplier * props.market.multiplier);
|
||||
};
|
||||
|
||||
const closeMarket = async () => {
|
||||
@ -194,10 +196,15 @@ onMounted(async () => {
|
||||
<p class="text-sm text-base-content/70">{{ market.description }}</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div :class="['badge', statusColor, 'text-white']">
|
||||
{{ market.status }}
|
||||
</div>
|
||||
<div v-if="market.status === 'open'" class="text-sm text-base-content/60 text-right">
|
||||
<div v-if="market.multiplier > 1" class="badge badge-accent text-white font-bold">
|
||||
{{ market.multiplier }}x
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="market.status === 'open' || market.status === 'closed'" class="text-sm text-base-content/60 text-right">
|
||||
<div>{{ timeRemaining }}</div>
|
||||
<div class="text-xs">until close</div>
|
||||
</div>
|
||||
|
||||
@ -18,7 +18,7 @@ const winningBets = computed(() => {
|
||||
return userBets.value.filter(bet => {
|
||||
const market = bet.market;
|
||||
return market?.status === "resolved" && market?.winning_option?.uuid === bet.option.uuid;
|
||||
});
|
||||
}).reverse();
|
||||
});
|
||||
|
||||
const losingBets = computed(() => {
|
||||
|
||||
@ -180,8 +180,9 @@ export interface Market {
|
||||
uuid: string
|
||||
title: string
|
||||
description: string
|
||||
status: 'open' | 'closed' | 'resolved'
|
||||
status: 'draft' | 'open' | 'closed' | 'resolved'
|
||||
end_date: string
|
||||
multiplier: number
|
||||
created_at: string
|
||||
options: MarketOption[]
|
||||
winning_option?: MarketOption | null
|
||||
|
||||
@ -221,6 +221,7 @@ class UserInfoOut(Schema):
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
points: int = 0
|
||||
is_authenticated: bool
|
||||
is_staff: bool
|
||||
is_superuser: bool
|
||||
|
||||
Loading…
Reference in New Issue
Block a user