feat(market): basic page + submit

This commit is contained in:
2026-05-23 18:30:50 +02:00
parent ce30539808
commit f7c7eba4da
17 changed files with 866 additions and 9 deletions
+63 -1
View File
@@ -1,3 +1,65 @@
from django.contrib import admin
from market.models import Market, MarketOption, UserBet
# Register your models here.
class MarketOptionInline(admin.TabularInline):
model = MarketOption
extra = 1
fields = ["text", "position"]
@admin.register(Market)
class MarketAdmin(admin.ModelAdmin):
list_display = ["title", "type", "status", "end_date", "created_by", "created_at"]
list_filter = ["status", "type", "created_at"]
search_fields = ["uuid", "title"]
readonly_fields = [
"uuid",
"created_at",
"updated_at",
"created_by",
"winning_option",
]
inlines = [MarketOptionInline]
fieldsets = (
("Info", {"fields": ["uuid", "title", "description"]}),
("Configuration", {"fields": ["type", "end_date"]}),
("Status", {"fields": ["status", "winning_option"]}),
("Metadata", {"fields": ["created_by", "created_at", "updated_at"]}),
)
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="Close selected markets")
def close_markets(self, request, queryset):
updated = queryset.filter(status=Market.Status.OPEN).update(
status=Market.Status.CLOSED
)
self.message_user(request, f"Closed {updated} market(s).")
actions = ["close_markets"]
@admin.register(MarketOption)
class MarketOptionAdmin(admin.ModelAdmin):
list_display = ["text", "market", "position"]
list_filter = ["market"]
search_fields = ["uuid", "text", "market__title"]
readonly_fields = ["uuid"]
@admin.register(UserBet)
class UserBetAdmin(admin.ModelAdmin):
list_display = ["user", "option", "amount", "created_at"]
list_filter = ["created_at", "option__market"]
search_fields = ["uuid", "user__username", "option__text"]
readonly_fields = ["uuid", "user", "option", "amount", "created_at", "updated_at"]
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
+99
View File
@@ -0,0 +1,99 @@
from typing import List
from ninja import Router
from ninja.errors import HttpError
from django.shortcuts import get_object_or_404
from market.models import Market, MarketOption, UserBet
from market.schemas import (
MarketListSchema,
ResolveMarketSchema,
UserBetCreateSchema,
UserBetSchema,
)
router = Router(tags=["market"])
@router.get("/", response=List[MarketListSchema])
def list_markets(request):
"""List all markets."""
return Market.objects.prefetch_related("options").all()
@router.get("/user/bets", response=List[UserBetSchema])
def list_user_bets(request):
"""List all bets placed by the current user."""
if not request.user.is_authenticated:
raise HttpError(401, "Authentication required")
return UserBet.objects.filter(user=request.user).select_related(
"option__market"
).prefetch_related("option__market__options")
@router.post("/{market_uuid}/actions/close")
def close_market(request, market_uuid: str):
"""Close a market. Admin only."""
if not request.user.is_staff:
raise HttpError(403, "Permission denied")
market = get_object_or_404(Market, uuid=market_uuid)
market.status = Market.Status.CLOSED
market.save(update_fields=["status", "updated_at"])
return {"status": "closed"}
@router.post("/{market_uuid}/actions/resolve", response=MarketListSchema)
def resolve_market(request, market_uuid: str, payload: ResolveMarketSchema):
"""Resolve a market with a winning option. Admin only."""
if not request.user.is_staff:
raise HttpError(403, "Permission denied")
market = get_object_or_404(Market, uuid=market_uuid)
winning_option = get_object_or_404(MarketOption, uuid=payload.winning_option_uuid)
if winning_option.market_id != market.id:
raise HttpError(400, "Option does not belong to this market")
market.winning_option = winning_option
market.status = Market.Status.RESOLVED
market.save(update_fields=["winning_option", "status", "updated_at"])
return market
@router.post("/{market_uuid}/bets", response=UserBetSchema)
def create_bet(request, market_uuid: str, payload: UserBetCreateSchema):
"""Place a bet on a market option."""
if not request.user.is_authenticated:
raise HttpError(401, "Authentication required")
market = get_object_or_404(Market, uuid=market_uuid)
option = get_object_or_404(MarketOption, uuid=payload.option_uuid)
if option.market_id != market.id:
raise HttpError(400, "Option does not belong to this market")
if market.status != Market.Status.OPEN:
raise HttpError(400, "Market is not open for betting")
# Check if user already has a bet on a different option in this market
existing_bet_on_market = UserBet.objects.filter(
user=request.user,
option__market=market
).exclude(option=option).first()
if existing_bet_on_market:
raise HttpError(400, "You can only bet on one option per market")
# Check if user already has a bet on this option
existing_bet = UserBet.objects.filter(user=request.user, option=option).first()
if existing_bet and payload.amount < existing_bet.amount:
raise HttpError(400, "Cannot decrease bet amount. You can only increase it.")
user_bet, created = UserBet.objects.update_or_create(
user=request.user,
option=option,
defaults={"amount": payload.amount},
)
return user_bet
@@ -7,7 +7,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
+9 -4
View File
@@ -1,6 +1,5 @@
import uuid
from django.db import models
from django.utils import timezone
class BaseModel(models.Model):
@@ -25,7 +24,9 @@ class Market(BaseModel):
title = models.CharField(max_length=255)
description = models.TextField(blank=True)
type = models.CharField(max_length=10, choices=Type.choices, default=Type.YES_NO)
status = models.CharField(max_length=10, choices=Status.choices, default=Status.OPEN)
status = models.CharField(
max_length=10, choices=Status.choices, default=Status.OPEN
)
end_date = models.DateTimeField()
created_by = models.ForeignKey("accounts.CustomUser", on_delete=models.PROTECT)
winning_option = models.ForeignKey(
@@ -69,8 +70,12 @@ class MarketOption(BaseModel):
class UserBet(BaseModel):
user = models.ForeignKey("accounts.CustomUser", on_delete=models.CASCADE, related_name="bets")
option = models.ForeignKey(MarketOption, on_delete=models.CASCADE, related_name="user_bets")
user = models.ForeignKey(
"accounts.CustomUser", on_delete=models.CASCADE, related_name="bets"
)
option = models.ForeignKey(
MarketOption, on_delete=models.CASCADE, related_name="user_bets"
)
amount = models.PositiveIntegerField()
class Meta:
+59
View File
@@ -0,0 +1,59 @@
from datetime import datetime
from typing import List, Optional, Any
from uuid import UUID
from ninja import Schema
from pydantic import field_serializer, model_validator
class MarketOptionSchema(Schema):
uuid: UUID
text: str
position: int
@field_serializer('uuid')
def serialize_uuid(self, value: UUID) -> str:
return str(value)
class MarketListSchema(Schema):
uuid: UUID
title: str
description: str
type: str
status: str
end_date: datetime
created_at: datetime
options: List[MarketOptionSchema]
winning_option: Optional[MarketOptionSchema] = None
@field_serializer('uuid')
def serialize_uuid(self, value: UUID) -> str:
return str(value)
class ResolveMarketSchema(Schema):
winning_option_uuid: str
class UserBetCreateSchema(Schema):
option_uuid: str
amount: int
class UserBetSchema(Schema):
uuid: UUID
amount: int
created_at: datetime
option: MarketOptionSchema
market: Optional[MarketListSchema] = None
@field_serializer('uuid')
def serialize_uuid(self, value: UUID) -> str:
return str(value)
@model_validator(mode='before')
@classmethod
def resolve_market_from_option(cls, data: Any) -> Any:
if hasattr(data, 'option') and hasattr(data.option, 'market'):
data.market = data.option.market
return data
-1
View File
@@ -1,3 +1,2 @@
from django.test import TestCase
# Create your tests here.
+6 -1
View File
@@ -1,3 +1,8 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from django.http import HttpRequest
# Create your views here.
@login_required
def market_home(request: HttpRequest):
return render(request, "market.html", {})