From e557fe2cda9ef4815e90c806670f8d5c23434e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Sat, 23 May 2026 20:54:19 +0200 Subject: [PATCH] feat(market): add draft status and multiplier --- polylan_submitter/market/admin.py | 17 +++++++-- polylan_submitter/market/api.py | 12 ++++--- .../migrations/0005_market_multiplier.py | 17 +++++++++ .../migrations/0006_alter_market_status.py | 26 ++++++++++++++ polylan_submitter/market/models.py | 4 ++- polylan_submitter/market/schemas.py | 1 + polylan_submitter/polylan_submitter/api.py | 24 ++++--------- polylan_submitter/src/Market.vue | 35 +++++++++++-------- polylan_submitter/src/api/sdk.gen.ts | 2 +- polylan_submitter/src/api/types.gen.ts | 8 +++++ .../src/components/MarketCard.vue | 15 +++++--- polylan_submitter/src/components/UserBets.vue | 2 +- polylan_submitter/src/types/index.ts | 3 +- polylan_submitter/submissions/schemas.py | 1 + 14 files changed, 121 insertions(+), 46 deletions(-) create mode 100644 polylan_submitter/market/migrations/0005_market_multiplier.py create mode 100644 polylan_submitter/market/migrations/0006_alter_market_status.py diff --git a/polylan_submitter/market/admin.py b/polylan_submitter/market/admin.py index ed40306..b67fca8 100644 --- a/polylan_submitter/market/admin.py +++ b/polylan_submitter/market/admin.py @@ -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) diff --git a/polylan_submitter/market/api.py b/polylan_submitter/market/api.py index 89e1c07..f41be0f 100644 --- a/polylan_submitter/market/api.py +++ b/polylan_submitter/market/api.py @@ -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( diff --git a/polylan_submitter/market/migrations/0005_market_multiplier.py b/polylan_submitter/market/migrations/0005_market_multiplier.py new file mode 100644 index 0000000..36f7feb --- /dev/null +++ b/polylan_submitter/market/migrations/0005_market_multiplier.py @@ -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), + ), + ] diff --git a/polylan_submitter/market/migrations/0006_alter_market_status.py b/polylan_submitter/market/migrations/0006_alter_market_status.py new file mode 100644 index 0000000..aa6f876 --- /dev/null +++ b/polylan_submitter/market/migrations/0006_alter_market_status.py @@ -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, + ), + ), + ] diff --git a/polylan_submitter/market/models.py b/polylan_submitter/market/models.py index 574315e..ce94a92 100644 --- a/polylan_submitter/market/models.py +++ b/polylan_submitter/market/models.py @@ -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", diff --git a/polylan_submitter/market/schemas.py b/polylan_submitter/market/schemas.py index 1d49f0e..c10da3e 100644 --- a/polylan_submitter/market/schemas.py +++ b/polylan_submitter/market/schemas.py @@ -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 diff --git a/polylan_submitter/polylan_submitter/api.py b/polylan_submitter/polylan_submitter/api.py index 9bbfa74..6a3f383 100644 --- a/polylan_submitter/polylan_submitter/api.py +++ b/polylan_submitter/polylan_submitter/api.py @@ -67,20 +67,10 @@ 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 { - "is_authenticated": False, - "is_staff": False, - "is_superuser": False, - } + return user + + return { + "is_authenticated": False, + "is_staff": False, + "is_superuser": False, + } diff --git a/polylan_submitter/src/Market.vue b/polylan_submitter/src/Market.vue index b7752a3..838bde1 100644 --- a/polylan_submitter/src/Market.vue +++ b/polylan_submitter/src/Market.vue @@ -77,19 +77,31 @@ onMounted(async () => {
-

- - My Bets -

+
+

+ + My Bets +

+
+ {{ userInfo.points }} + pts +
+
-

- - All Markets -

+
+

+ + All Markets +

+ + + Create Market + +
@@ -97,12 +109,7 @@ onMounted(async () => {
- +
diff --git a/polylan_submitter/src/api/sdk.gen.ts b/polylan_submitter/src/api/sdk.gen.ts index a5ef664..ed159b3 100644 --- a/polylan_submitter/src/api/sdk.gen.ts +++ b/polylan_submitter/src/api/sdk.gen.ts @@ -194,7 +194,7 @@ export const gamesApiListGames = (options? /** * List Markets * - * List all markets. + * List all markets (excludes draft markets). */ export const marketApiListMarkets = (options?: Options) => (options?.client ?? client).get({ url: '/api/market/', ...options }); diff --git a/polylan_submitter/src/api/types.gen.ts b/polylan_submitter/src/api/types.gen.ts index 35bffdb..7a20e90 100644 --- a/polylan_submitter/src/api/types.gen.ts +++ b/polylan_submitter/src/api/types.gen.ts @@ -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 */ diff --git a/polylan_submitter/src/components/MarketCard.vue b/polylan_submitter/src/components/MarketCard.vue index 69c863b..e08b976 100644 --- a/polylan_submitter/src/components/MarketCard.vue +++ b/polylan_submitter/src/components/MarketCard.vue @@ -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 () => {

{{ market.description }}

-
- {{ market.status }} +
+
+ {{ market.status }} +
+
+ {{ market.multiplier }}x +
-
+
{{ timeRemaining }}
until close
diff --git a/polylan_submitter/src/components/UserBets.vue b/polylan_submitter/src/components/UserBets.vue index 2409b60..b7d0ed7 100644 --- a/polylan_submitter/src/components/UserBets.vue +++ b/polylan_submitter/src/components/UserBets.vue @@ -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(() => { diff --git a/polylan_submitter/src/types/index.ts b/polylan_submitter/src/types/index.ts index 04b291a..a92d042 100644 --- a/polylan_submitter/src/types/index.ts +++ b/polylan_submitter/src/types/index.ts @@ -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 diff --git a/polylan_submitter/submissions/schemas.py b/polylan_submitter/submissions/schemas.py index 2ce691c..0576c49 100644 --- a/polylan_submitter/submissions/schemas.py +++ b/polylan_submitter/submissions/schemas.py @@ -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