This commit is contained in:
copyrighttxt
2026-07-06 13:28:00 -04:00
commit 05211f73ef
441 changed files with 94128 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
{% extends '__layout__.html' %}
{% block title %}{{asset.name}}{% endblock %}
{% block head %}
<link href="/static/css/catalog.css" rel="stylesheet"/>
<script src="/static/js/catalog.js?version=5"></script>
<style>
.text-secondary {
color: rgb(199, 199, 199) !important;
}
.text-limited {
color: rgb(255,255,255) !important;
background-color: rgb(9, 136, 62);
width: fit-content;
padding-left: 10px;
font-size: 20px;
border-top-right-radius: 5px;
margin: 0;
}
.text-limitedu {
color: rgb(0,0,0) !important;
background-color: rgb(245, 245, 52);
padding-right: 3px;
font-weight: 800;
border-top-right-radius: 5px;
padding-left: 3px;
}
.text-robux {
color: rgb(26, 212, 103) !important;
font-size: small;
font-weight: 600;
}
</style>
<div id="csrf_token" data-csrf-token="{{ csrf_token() }}"></div>
<meta id="asset-info" data-asset-name="{{asset.name}}" data-asset-id="{{asset.id}}" data-asset-onsale="{% if asset.is_for_sale: %}true{%else%}false{%endif%}" {% if doesUserOwnAsset: %}data-user-ownasset="true"{% endif %} {% if asset.is_limited: %}data-islimited="true"{%endif%} {%if OffsaleAt: %}data-offsale="{{OffsaleAt}}"{%endif%}>
{% endblock %}
{% block content %}
<div id="main">
<div class="container ps-5 pe-5 ps-md-0 pe-md-0" style="max-width: 800px;">
<a href="/catalog/" class="text-decoration-none text-white">< Return to Catalog</a>
<h1>{{asset.name}}</h1>
<div class="row">
<div class="col-md-5 col-12">
<div class="p-2 rounded overflow-hidden position-relative" style="background-color: rgb(36, 36, 36);">
<img width="100%" src="/Thumbs/Asset.ashx?assetId={{asset.id}}&x=420&y=420" style="aspect-ratio: 1/1;" alt="{{asset.name}}">
{% if asset.is_limited: %}<p class="position-absolute m-0 fw-bold text-limited" style="bottom:-2px;left:0px;">LIMITED {%if asset.is_limited_unique: %}<span class="text-limitedu" style="padding-top:1.5px">U</span>{%else%}<span class="me-2"></span>{%endif%}</p>{% endif %}
</div>
</div>
<div class="col-md-4 p-1">
<div class="d-flex mt-3 mt-md-0">
{% if asset.creator_type == 0 :%}
<img width="80" height="80" src="/Thumbs/Avatar.ashx?userId={{creator.id}}&x=100&y=100" alt="{{creator.username}}">
<div style="font-size:13px;">
<p class="m-0 text-secondary">Creator: <a href="/users/{{creator.id}}/profile" class="text-decoration-none">{{creator.username}}</a></p>
<p class="m-0 text-secondary">Created: <span class="text-white">{{createddate}}</span></p>
<p class="m-0 text-secondary">Updated: <span class="text-white">{{updateddate}}</span></p>
</div>
{% else %}
<img width="80" height="80" src="/Thumbs/GroupIcon.ashx?groupid={{creator.id}}&x=100&y=100" alt="{{creator.name}}">
<div style="font-size:13px;">
<p class="m-0 text-secondary">Creator: <a href="/groups/{{creator.id}}/--" class="text-decoration-none">{{creator.name}}</a></p>
<p class="m-0 text-secondary">Created: <span class="text-white">{{createddate}}</span></p>
<p class="m-0 text-secondary">Updated: <span class="text-white">{{updateddate}}</span></p>
</div>
{% endif %}
</div>
<div class="linebreak"></div>
<p>
{{asset.description}}
</p>
</div>
<div class="col-md-3 p-1" >
{% if OffsaleAt: %}
<p class="m-0 text-secondary mb-2 w-100 text-center" style="font-size: 14px;">Offsale in: <span class="text-danger" id="offsale-countdown">00:00:00</span> <i class="bi bi-alarm"></i></p>
{% endif %}
<div class="border rounded p-2 text-center">
{% if asset.is_for_sale: %}
{% if asset.price_robux == 0 and asset.price_tix == 0: %}
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">FREE</span></p>
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="0" data-expected-price="{{asset.price_robux}}">Take One</a>
{% endif %}
{% if asset.price_robux > 0: %}
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">{{asset.price_robux}} Robux</span></p>
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="0" data-expected-price="{{asset.price_robux}}">Buy with Robux</a>
{% endif %}
{% if asset.price_robux > 0 and asset.price_tix > 0: %}
<div class="d-flex align-items-center mt-2 mb-2">
<div class="border-top flex-fill me-2"></div>
<p class="text-secondary m-0" style="font-size: 13px;">OR</p>
<div class="border-top flex-fill ms-2"></div>
</div>
{% endif %}
{% if asset.price_tix > 0: %}
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">{{asset.price_tix}} Tickets</span></p>
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="1" data-expected-price="{{asset.price_tix}}">Buy with Tickets</a>
{% endif %}
{% else: %}
{% if asset.is_limited: %}
{% if BestPrice != "None": %}
<p class="m-0 text-secondary" style="font-size:14px;">Best Price: <span class="text-white">R$ {{BestPrice}}</span></p>
{% if BestPriceResult.userid != currentuser.id %}
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="0" data-islimited="true" data-expectedowner="{{BestPriceResult.userid}}" data-expectedid="{{BestPriceResult.id}}" data-expected-price="{{BestPrice}}">Buy Now</a>
{%else%}
<btn class="btn btn-primary fw-bold w-100 btn-sm disabled">Buy with Robux</btn>
{%endif%}
{%else%}
<btn class="btn btn-primary fw-bold w-100 btn-sm disabled">No Resellers Available</btn>
{%endif%}
{% if userOwnAmountCount > 0: %}
<p class="m-0 text-secondary" style="font-size:12px;">( {{userOwnAmountCount}} owned )</p>
{%endif%}
{%else%}
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">Offsale</span></p>
<btn class="btn btn-primary fw-bold w-100 btn-sm disabled">Buy with Robux</btn>
{%endif%}
{% endif %}
<p class="m-0 mt-1 text-secondary" style="font-size:12px;">( {{asset.sale_count}} sold )</p>
</div>
{% if asset.is_limited and userOwnAmountCount > 0 and not asset.is_for_sale: %}
<div class="border rounded p-2 text-center mt-2">
<a href="/catalog/resell/{{asset.id}}" class="btn btn-success fw-bold btn-sm w-100">Manage your items</a>
</div>
{%endif%}
</div>
</div>
{% if asset.is_limited and not asset.is_for_sale: %}
<div class="linebreak mt-3"></div>
<div class="row">
<div class="col-md-6">
<h5>Private Sales</h5>
{% for sale in PrivateSales: %}
<div class="w-100 m-2 d-flex">
<img class="rounded-2 overflow-hidden" width="60" height="60" src="/Thumbs/Avatar.ashx?x=60&y=60&userId={{sale.sellerid}}" alt="{{sale.seller}}"/>
<div class="ms-2">
<a href="/users/{{sale.sellerid}}/profile" class="text-decoration-none text-white m-0">{{sale.seller}}</a>
<p class="m-0 text-robux">R$ {{sale.price}}</p>
<p class="m-0 text-secondary" style="font-size:13px;">Serial: {%if asset.is_limited_unique: %}{{sale.serial}}{%else%}N/A{%endif%}</p>
</div>
<div class="ms-auto align-items-center d-flex">
{% if sale.sellerid != currentuser.id: %}
<button class="btn btn-sm btn-primary purchase-button" data-currency-type="0" data-islimited="true" data-expectedowner="{{sale.sellerid}}" data-expectedid="{{sale.uaid}}" data-expected-price="{{sale.price}}">Buy Now</button>
{%else%}
<button class="btn btn-sm btn-primary disabled">Buy Now</button>
{%endif%}
</div>
</div>
{%endfor%}
<div class="d-flex">
{% if PreviousPage > 0: %}
<a href="/catalog/{{asset.id}}/?page={{PreviousPage}}" class="ms-auto text-decoration-none">Previous</a>
{%else%}
<a class="ms-auto text-decoration-none text-secondary">Previous</a>
{% endif %}
<p class="ms-1 me-1">{{PageNumber}}</p>
{% if NextPage > 0: %}
<a href="/catalog/{{asset.id}}/?page={{NextPage}}" class="me-auto text-decoration-none">Next</a>
{%else%}
<a class="me-auto text-decoration-none text-secondary">Next</a>
{% endif %}
</div>
</div>
<div class="col-md-6">
<p class="m-0 text-secondary" style="font-size:small;">Recent Average Price: <span class="text-robux">R${{AssetRap}}</span></p>
</div>
</div>
{%endif%}
</div>
</div>
<div class="modal fade" id="purchaseModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="purchase-modal-title">Confirm purchase</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="purchase-modal-content">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="purchase-modal-close" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" id="purchase-modal-btn">Purchase</button>
</div>
</div>
</div>
</div>
{% endblock %}
+53
View File
@@ -0,0 +1,53 @@
{% extends '__layout__.html' %}
{% block title %}{{badge.name}}{% endblock %}
{% block head %}
<link href="/static/css/catalog.css" rel="stylesheet"/>
<style>
.text-secondary {
color: rgb(199, 199, 199) !important;
}
</style>
{% endblock %}
{% block content %}
<div id="main">
<div class="container ps-5 pe-5 ps-md-0 pe-md-0" style="max-width: 800px;">
<h1>{{badge.name}}</h1>
<div class="row">
<div class="col-md-5 col-12">
<div class="p-2 rounded overflow-hidden position-relative{% if AssociatedPlaceAssetObj != None %} rounded-bottom-0 {%endif%}" style="background-color: rgb(36, 36, 36);">
<img width="100%" src="/Thumbs/Asset.ashx?assetId={{badge.icon_image_id}}&x=420&y=420" style="aspect-ratio: 1/1;" alt="{{badge.name}}">
</div>
{% if AssociatedPlaceAssetObj != None: %}
<div class="rounded-bottom overflow-hidden d-flex p-1" style="background-color: rgb(29, 29, 29);">
<p class="m-0 text-secondary me-2" style="font-size: 13px;">Earn this Badge in: <a class="text-decoration-none text-white" href="/games/{{AssociatedPlaceAssetObj.id}}/">{{AssociatedPlaceAssetObj.name}}</a></p>
<a class="text-decoration-none ms-auto" href="/games/{{AssociatedPlaceAssetObj.id}}/"><img src="/Thumbs/PlaceIcon.ashx?assetId={{AssociatedPlaceAssetObj.id}}&x=60&y=60" height="60px" width="60px" class="rounded"></a>
</div>
{% endif %}
</div>
<div class="col-md-4 p-1">
<div class="d-flex mt-3 mt-md-0">
{% if AssociatedPlaceAssetObj.creator_type == 0 :%}
<img width="80" height="80" src="/Thumbs/Avatar.ashx?userId={{creator.id}}&x=100&y=100" alt="{{creator.username}}">
<div style="font-size:13px;">
<p class="m-0 text-secondary">Creator: <a href="/users/{{creator.id}}/profile" class="text-decoration-none">{{creator.username}}</a></p>
<p class="m-0 text-secondary">Created: <span class="text-white">{{createddate}}</span></p>
<p class="m-0 text-secondary">Updated: <span class="text-white">{{updateddate}}</span></p>
</div>
{% else %}
<img width="80" height="80" src="/Thumbs/GroupIcon.ashx?groupid={{creator.id}}&x=100&y=100" alt="{{creator.name}}">
<div style="font-size:13px;">
<p class="m-0 text-secondary">Creator: <a href="/groups/{{creator.id}}/--" class="text-decoration-none">{{creator.name}}</a></p>
<p class="m-0 text-secondary">Created: <span class="text-white">{{createddate}}</span></p>
<p class="m-0 text-secondary">Updated: <span class="text-white">{{updateddate}}</span></p>
</div>
{% endif %}
</div>
<div class="linebreak"></div>
<p>
{{badge.description}}
</p>
</div>
</div>
</div>
</div>
{% endblock %}
+824
View File
@@ -0,0 +1,824 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify, abort
from app.util import auth, turnstile, websiteFeatures
from app.routes.thumbnailer import TakeUserThumbnail
from app.util.redislock import acquire_lock, release_lock
from app.extensions import db, csrf, limiter, redis_controller, user_limiter
from app.models.asset import Asset
from app.models.usereconomy import UserEconomy
from app.models.userassets import UserAsset
from app.models.user_avatar_asset import UserAvatarAsset
from app.models.user import User
from app.models.package_asset import PackageAsset
from app.models.gamepass_link import GamepassLink
from app.models.asset_rap import AssetRap
from app.models.groups import Group
from app.models.place_badge import PlaceBadge, UserBadge
from app.models.universe import Universe
from app.models.limited_item_transfers import LimitedItemTransfer
from app.routes.badgesapi import CalculateBadgeRarity, GetBadgeAwardedPastDay
from app.pages.catalog.catalogtypes import CatalogTypes
from app.pages.messages.messages import CreateSystemMessage
from slugify import slugify
from datetime import datetime
from sqlalchemy import text, and_, or_
import math
from app.enums.AssetType import AssetType
from app.util.transactions import CreateTransactionForSale
import logging
import calendar
import time
import redis_lock
from app.util.membership import GetUserMembership
from app.enums.MembershipType import MembershipType
from app.enums.LimitedItemTransferMethod import LimitedItemTransferMethod
from app.services.economy import IncrementTargetBalance, AdjustAssetRap, GetAssetRap, TaxCurrencyAmount, GetUserBalance, GetGroupBalance, DecrementTargetBalance,EconomyLockAcquireException, InsufficientFundsException
from app.services.groups import GetUserFromId, GetGroupFromId
CatalogRoute = Blueprint('catalog', __name__, template_folder="pages", url_prefix="/catalog")
LibraryRoute = Blueprint('library', __name__, template_folder="pages", url_prefix="/library")
BadgesPageRoute = Blueprint('badges_page', __name__, template_folder="pages", url_prefix="/badges")
def IncrementAssetCreator( AssetObject : Asset, AmountGiven : int, CurrencyType : int):
""" Increments the robux of the asset creator"""
CurrencyToGive = math.floor(AmountGiven * 0.7)
if CurrencyToGive <= 0:
return True
if AssetObject.creator_type == 0:
# User
IncrementTargetBalance(GetUserFromId(AssetObject.creator_id), CurrencyToGive, CurrencyType)
return True
elif AssetObject.creator_type == 1:
# Group
IncrementTargetBalance(GetGroupFromId(AssetObject.creator_id), CurrencyToGive, CurrencyType)
return True
def ConvertQueryToAsset( queryResult ):
"""Converts a query result to a dict with the same keys as the Asset model"""
return {
"id": queryResult[0],
"roblox_asset_id": queryResult[1],
"name": queryResult[2],
"description": queryResult[3],
"created_at": queryResult[4],
"updated_at": queryResult[5],
"asset_type": queryResult[6],
"asset_genre": queryResult[7],
"creator_type": queryResult[8],
"creator_id": queryResult[9],
"moderation_status": queryResult[10],
"is_for_sale": queryResult[11],
"price_robux": queryResult[12],
"price_tix": queryResult[13],
"is_limited": queryResult[14],
"is_limited_unique": queryResult[15],
"serial_count": queryResult[16],
"sale_count": queryResult[17],
"offsale_at": queryResult[18]
}
OrderTypes = {
0: "created_at DESC", # Default
1: "price_robux ASC", # Lowest to Highest
2: "price_robux DESC", # Highest to Lowest
3: "updated_at DESC", # Recently Updated
4: "sale_count DESC" # Best Selling
}
import urllib.parse
@CatalogRoute.route("/", methods=["POST"])
@auth.authenticated_required
@csrf.exempt
def catalog_search():
CategoryType = int(request.args.get(key="category", default=0, type=int))
OrderBy = request.form.get(key="order-by", default=0, type=int)
if OrderBy not in OrderTypes:
OrderBy = 0
SearchQuery = request.form.get(key="search-input", default="", type=str)
if len(SearchQuery) < 3:
SearchQuery = ""
if SearchQuery == "":
return redirect(f"/catalog/?sort={OrderBy}&category={str(CategoryType)}")
# Make sure the query is url safe
SearchQuery = urllib.parse.quote(SearchQuery)
return redirect(f"/catalog/?q={SearchQuery}&sort={OrderBy}&category={str(CategoryType)}")
@CatalogRoute.route("/", methods=["GET"])
@auth.authenticated_required
def catalog_page():
CatalogType = int(request.args.get("category", 0))
if CatalogType not in CatalogTypes:
return redirect("/catalog/")
if request.args.get("page") is None:
Page = 1
else:
try:
Page = int(request.args.get("page"))
except:
Page = 1
SearchInput = request.args.get("q", "", type=str)
"""
Types of sorting:
0 - Relevance (Default) [ Just sort by created_at ]
1 - Price (Lowest to Highest)
2 - Price (Highest to Lowest)
3 - Recently Updated
4 - Best Selling
"""
if request.args.get("sort") is None:
SortType = 0
else:
try:
SortType = int(request.args.get("sort"))
except:
SortType = 0
if SortType > 4 or SortType < 0:
SortType = 0
SearchQuery = Asset.query
CatalogTypesDict = {
0: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
AssetType.Hat,
AssetType.TShirt,
AssetType.Shirt,
AssetType.Pants,
AssetType.Gear,
AssetType.Face,
AssetType.Head,
AssetType.HairAccessory,
AssetType.FaceAccessory,
AssetType.NeckAccessory,
AssetType.ShoulderAccessory,
AssetType.FrontAccessory,
AssetType.BackAccessory,
AssetType.WaistAccessory,
AssetType.Package
))).filter(and_(Asset.creator_id.in_([1]), Asset.creator_type == 0)).filter(or_(
Asset.is_for_sale == True, Asset.is_limited == True
)),
1: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
AssetType.Hat,
AssetType.HairAccessory,
AssetType.FaceAccessory,
AssetType.NeckAccessory,
AssetType.ShoulderAccessory,
AssetType.FrontAccessory,
AssetType.BackAccessory,
AssetType.WaistAccessory,
))).filter(and_(Asset.creator_id.in_([1]), Asset.creator_type == 0)).filter(or_(
Asset.is_for_sale == True, Asset.is_limited == True
)),
2: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Gear).filter(and_(Asset.creator_id.in_([1]), Asset.creator_type == 0, Asset.is_for_sale == True)),
3: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Face).filter(and_(Asset.creator_id.in_([1]), Asset.creator_type == 0, Asset.is_for_sale == True)),
4: lambda queryObj: queryObj.filter_by(is_limited = True),
5: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
AssetType.Hat,
AssetType.HairAccessory,
AssetType.FaceAccessory,
AssetType.NeckAccessory,
AssetType.ShoulderAccessory,
AssetType.FrontAccessory,
AssetType.BackAccessory,
AssetType.WaistAccessory,
))).filter_by(is_limited = True),
6: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Gear).filter_by(is_limited = True),
7: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Face).filter_by(is_limited = True),
8: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
AssetType.Hat,
AssetType.TShirt,
AssetType.Shirt,
AssetType.Pants,
AssetType.Package,
))).filter_by(is_for_sale = True),
9: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
AssetType.Hat,
AssetType.HairAccessory,
AssetType.FaceAccessory,
AssetType.NeckAccessory,
AssetType.ShoulderAccessory,
AssetType.FrontAccessory,
AssetType.BackAccessory,
AssetType.WaistAccessory,
))).filter_by(is_for_sale = True),
10: lambda queryObj: queryObj.filter_by(asset_type = AssetType.Shirt).filter_by(is_for_sale = True),
11: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.TShirt).filter_by(is_for_sale = True),
12: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Pants).filter_by(is_for_sale = True),
13: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Package).filter_by(is_for_sale = True),
14: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
AssetType.Head,
AssetType.Face,
AssetType.Package
))).filter_by(is_for_sale = True),
15: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Head).filter_by(is_for_sale = True),
16: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Face).filter_by(is_for_sale = True),
41: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.HairAccessory).filter_by(is_for_sale = True),
}
if CatalogType > 41 or CatalogType < 0:
CatalogType = 0
SearchQuery = CatalogTypesDict[CatalogType](SearchQuery)
if SearchInput != "" and len(SearchInput) > 3:
SearchQuery = SearchQuery.filter( Asset.name.ilike(f"%{SearchInput}%") )
else:
SearchInput = ""
if SortType == 0:
SearchQuery = SearchQuery.order_by(Asset.created_at.desc())
elif SortType == 1:
SearchQuery = SearchQuery.order_by(Asset.price_robux.asc())
elif SortType == 2:
SearchQuery = SearchQuery.order_by(Asset.price_robux.desc())
elif SortType == 3:
SearchQuery = SearchQuery.order_by(Asset.updated_at.desc())
elif SortType == 4:
SearchQuery = SearchQuery.order_by(Asset.sale_count.desc())
else:
SearchQuery = SearchQuery.order_by(Asset.created_at.desc())
SearchQuery = SearchQuery.paginate(
page=Page,
per_page=24,
error_out=False
)
for AssetObj in SearchQuery.items:
if AssetObj.is_limited and not AssetObj.is_for_sale:
BestPriceResult : UserAsset = UserAsset.query.filter_by(assetid=AssetObj.id, is_for_sale=True).order_by(UserAsset.price.asc()).first()
if BestPriceResult is not None:
AssetObj.best_price = str(BestPriceResult.price)
else:
AssetObj.best_price = "--"
if Page == 1:
PreviousPage = -1
else:
PreviousPage = Page -1
if SearchQuery.has_next:
NextPage = Page + 1
else:
NextPage = -1
return render_template("catalog/index.html",
categoryname=CatalogTypes[CatalogType]["name"],
queryResults = SearchQuery.items,
categoryid=CatalogType,
PreviousPage=PreviousPage,
NextPage=NextPage,
PageNumber=Page,
CatalogType=CatalogType,
SortType=SortType,
query=SearchInput,
totalPages = SearchQuery.pages,
totalResults = SearchQuery.total)
@CatalogRoute.route("/<int:assetid>/", methods=["GET"])
@CatalogRoute.route("/<int:assetid>/<assetname>", methods=["GET"])
@auth.authenticated_required
def asset_page(assetid, assetname=None):
AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObject is None:
return redirect("/catalog")
SlugName = slugify(AssetObject.name, lowercase=False)
if SlugName is None or SlugName == "":
SlugName = "unnamed"
if assetname is None:
if request.args.get("page") is None:
return redirect(f"/catalog/{assetid}/{SlugName}")
else:
return redirect(f"/catalog/{assetid}/{SlugName}?page={request.args.get('page')}")
if assetname != SlugName:
if request.args.get("page") is None:
return redirect(f"/catalog/{assetid}/{SlugName}")
else:
return redirect(f"/catalog/{assetid}/{SlugName}?page={request.args.get('page')}")
if AssetObject.asset_type.value not in [2,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
if AssetObject.asset_type.value == 9:
return redirect(f"/games/{assetid}/")
if AssetObject.asset_type.value in [1,3,4,5,10,13,24,34,38,40]:
return redirect(f"/library/{assetid}/")
return redirect("/catalog/")
CreatorObj : User | Group = User.query.filter_by(id=AssetObject.creator_id).first() if AssetObject.creator_type == 0 else Group.query.filter_by(id=AssetObject.creator_id).first()
Created = AssetObject.created_at.strftime("%d/%m/%Y")
Updated = AssetObject.updated_at.strftime("%d/%m/%Y")
AuthenticatedUser : User = auth.GetCurrentUser()
doesUserOwnAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).first() is not None
BestPriceResult : UserAsset = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).first()
BestPrice = "None"
if BestPriceResult is not None:
BestPrice = str(BestPriceResult.price)
UserOwns = 0
PrivateSaleList = []
NextPage = 0
PreviousPage = 0
PageNumber = 0
AssetRap = 0
if AssetObject.is_limited and not AssetObject.is_for_sale:
AssetRap = GetAssetRap(assetid)
UserOwns = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).count()
Page = 1
if request.args.get("page"):
try:
Page = int(request.args.get("page"))
except:
pass
PrivateSales = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).paginate(page=Page, per_page=5)
for sale in PrivateSales.items:
SellerUser : User = User.query.filter_by(id=sale.userid).first()
PrivateSaleList.append({
"price": sale.price,
"seller": SellerUser.username,
"sellerid": SellerUser.id,
"serial": sale.serial,
"uaid": sale.id
})
if PrivateSales.has_next:
NextPage = Page + 1
else:
NextPage = -1
if Page > 1:
PreviousPage = Page - 1
else:
PreviousPage = -1
PageNumber = Page
OffsaleAt = None
if AssetObject.offsale_at is not None:
if datetime.utcnow() > AssetObject.offsale_at:
AssetObject.is_for_sale = False
AssetObject.offsale_at = None
db.session.commit()
else:
OffsaleAt = int(calendar.timegm(AssetObject.offsale_at.timetuple()))
return render_template("catalog/asset.html", asset=AssetObject, creator=CreatorObj, createddate=Created,
updateddate=Updated, doesUserOwnAsset=doesUserOwnAsset, BestPrice=BestPrice,
BestPriceResult=BestPriceResult, userOwnAmountCount=UserOwns, PrivateSales=PrivateSaleList,
NextPage=NextPage, PreviousPage=PreviousPage, PageNumber=PageNumber, AssetRap=AssetRap,
OffsaleAt=OffsaleAt)
@CatalogRoute.route("/resell/<int:assetid>", methods=["GET"])
@auth.authenticated_required
def resell_page(assetid):
AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObject is None:
return redirect("/catalog")
if not AssetObject.is_limited:
return redirect(f"/catalog/{str(AssetObject.id)}/")
if AssetObject.is_for_sale:
return redirect(f"/catalog/{str(AssetObject.id)}/") # Users cant resell items when it is still for sale
AuthenticatedUser : User = auth.GetCurrentUser()
UserOwns = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).count()
if UserOwns == 0:
return redirect(f"/catalog/{str(AssetObject.id)}/")
AllUserAssets = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).all()
UserAssetCurrentlyForSale = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid, is_for_sale=True).count()
UserAssetNotForSale = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid, is_for_sale=False).count()
return render_template("catalog/resell.html", asset=AssetObject, userassets=AllUserAssets, userassetforsalecount=UserAssetCurrentlyForSale, userassetnotforsalecount=UserAssetNotForSale, isOTPRequired = AuthenticatedUser.TOTPEnabled)
@CatalogRoute.route("/resell/<int:assetid>", methods=["POST"])
@auth.authenticated_required
@user_limiter.limit("1/second")
def resell_page_post(assetid):
AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObject is None:
return redirect("/catalog")
if not AssetObject.is_limited:
return redirect(f"/catalog/{str(AssetObject.id)}/")
if AssetObject.is_for_sale:
return redirect(f"/catalog/{str(AssetObject.id)}/")
AuthenticatedUser : User = auth.GetCurrentUser()
if "uaid" not in request.form or "itemprice" not in request.form:
flash("Invalid request", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
try:
ItemPrice = int(request.form["itemprice"])
UAID = int(request.form["uaid"])
except:
flash("Invalid request", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
UserAssetObj : UserAsset = UserAsset.query.filter_by(id=UAID).first()
if UserAssetObj is None:
flash("Invalid request", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if UserAssetObj.userid != AuthenticatedUser.id:
flash("Invalid request", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if UserAssetObj.assetid != AssetObject.id:
flash("Invalid request", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if not websiteFeatures.GetWebsiteFeature("ItemReselling"):
flash("Item reselling is currently disabled", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if AuthenticatedUser.TOTPEnabled:
if "2fa-code" not in request.form:
flash("Invalid request", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
TOTPCode = request.form["2fa-code"]
if len(TOTPCode) != 6:
flash("Invalid 2FA code", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if not auth.Validate2FACode(AuthenticatedUser.id, TOTPCode):
flash("Invalid 2FA code", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if ItemPrice < 1 or ItemPrice > 999999999:
flash("Invalid price ( 1 - 999999999 )", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
if UserAssetObj.is_for_sale:
flash("This asset is already for sale", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
if UserCurrentMembership == MembershipType.NonBuildersClub:
flash("You must be a Builders Club member to sell items", "error")
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
UserAssetObj.is_for_sale = True
UserAssetObj.price = ItemPrice
db.session.commit()
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
@CatalogRoute.route("/resell/<int:uaid>/takeoff", methods=["POST"])
@auth.authenticated_required
@csrf.exempt
def resell_takeoff(uaid):
AuthenticatedUser : User = auth.GetCurrentUser()
UserAssetObject : UserAsset = UserAsset.query.filter_by(id=uaid).first()
if UserAssetObject is None:
flash("Invalid asset", "error")
return redirect("/catalog")
if UserAssetObject.userid != AuthenticatedUser.id:
flash("Invalid asset", "error")
return redirect("/catalog")
if not UserAssetObject.is_for_sale:
flash("This asset is not for sale", "error")
return redirect(f"/catalog/resell/{str(UserAssetObject.assetid)}")
UserAssetObject.is_for_sale = False
UserAssetObject.price = 0
db.session.commit()
return redirect(f"/catalog/resell/{str(UserAssetObject.assetid)}")
@CatalogRoute.route("/api/purchase", methods=["POST"])
@auth.authenticated_required_api
@limiter.limit("1/second")
@user_limiter.limit("1/second")
def api_purchase():
JSONData = request.json
if "assetId" not in JSONData or "expectedPrice" not in JSONData or "currencyType" not in JSONData:
return jsonify({"success": False, "message": "Invalid request"}),400
if not websiteFeatures.GetWebsiteFeature("EconomyPurchase"):
return jsonify({"success": False, "message": "Purchasing is temporarily disabled"}),400
try:
JSONData["assetId"] = int(JSONData["assetId"])
JSONData["expectedPrice"] = int(JSONData["expectedPrice"])
JSONData["currencyType"] = int(JSONData["currencyType"])
except:
return jsonify({"success": False, "message": "Invalid request"}),400
AssetObject : Asset = Asset.query.filter_by(id=JSONData["assetId"]).first()
if AssetObject is None:
return jsonify({"success": False, "message": "Invalid asset"}),400
AuthenticatedUser : User = auth.GetCurrentUser()
try:
with redis_lock.Lock(redis_client = redis_controller, name=f"asset:{str(AssetObject.id)}", expire=15, auto_renewal=True, strict=True) as lock:
if AssetObject.offsale_at is not None:
if datetime.utcnow() > AssetObject.offsale_at:
AssetObject.is_for_sale = False
AssetObject.offsale_at = None
db.session.commit()
if AssetObject.is_for_sale == False:
return jsonify({"success": False, "message": "Asset is not for sale"}),400
if JSONData["currencyType"] == 0:
if JSONData["expectedPrice"] != AssetObject.price_robux:
return jsonify({"success": False, "message": "Expected Price is different from current price"}),400
elif JSONData["currencyType"] == 1:
if JSONData["expectedPrice"] != AssetObject.price_tix:
return jsonify({"success": False, "message": "Expected Price is different from current price"}),400
else:
return jsonify({"success": False, "message": "Invalid currency type"}),400
if AssetObject.price_robux == 0 and AssetObject.price_tix != 0 and JSONData["currencyType"] == 0:
return jsonify({"success": False, "message": "Asset is not being sold in this currency"}),400
if AssetObject.price_tix == 0 and AssetObject.price_robux != 0 and JSONData["currencyType"] == 1:
return jsonify({"success": False, "message": "Asset is not being sold in this currency"}),400
UserAssetObj : UserAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=AssetObject.id).first()
if UserAssetObj is not None:
return jsonify({"success": False, "message": "User already owns asset"}),400
PurchaserRobuxBal, PurchaserTicketsBal = GetUserBalance(AuthenticatedUser)
if JSONData["currencyType"] == 0:
if PurchaserRobuxBal < AssetObject.price_robux:
return jsonify({"success": False, "message": "Insufficient funds"}),400
try:
DecrementTargetBalance(
Target = AuthenticatedUser,
Amount = AssetObject.price_robux,
CurrencyType = 0
)
except InsufficientFundsException:
return jsonify({"success": False, "message": "Insufficient funds"}),400
except EconomyLockAcquireException:
return jsonify({"success": False, "message": "Failed to acquire lock"}),400
except Exception as e:
logging.error(f"/api/purchase : Failed to decrement balance for user {str(AuthenticatedUser.id)}, message: {str(e)}")
return jsonify({"success": False, "message": "Failed to decrement balance"}),400
IncrementAssetCreator(
AssetObject = AssetObject,
AmountGiven = AssetObject.price_robux,
CurrencyType = 0
)
elif JSONData["currencyType"] == 1:
if PurchaserTicketsBal < AssetObject.price_tix:
return jsonify({"success": False, "message": "Insufficient funds"}),400
try:
DecrementTargetBalance(
Target = AuthenticatedUser,
Amount = AssetObject.price_tix,
CurrencyType = 1
)
except InsufficientFundsException:
return jsonify({"success": False, "message": "Insufficient funds"}),400
except EconomyLockAcquireException:
return jsonify({"success": False, "message": "Failed to acquire lock"}),400
except Exception as e:
logging.error(f"/api/purchase : Failed to decrement balance for user {str(AuthenticatedUser.id)}, message: {str(e)}")
return jsonify({"success": False, "message": "Failed to decrement balance"}),400
IncrementAssetCreator(
AssetObject = AssetObject,
AmountGiven = AssetObject.price_tix,
CurrencyType = 1
)
else:
return jsonify({"success": False, "message": "Invalid currency type"}),400
if AssetObject.is_limited_unique:
ItemSerial = AssetObject.sale_count + 1
if AssetObject.sale_count + 1 >= AssetObject.serial_count:
AssetObject.is_for_sale = False
if AssetObject.is_limited_unique:
UserAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=AssetObject.id, serial=ItemSerial)
else:
UserAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=AssetObject.id)
if AssetObject.asset_type == AssetType.Package:
PackageAssets = PackageAsset.query.filter_by(package_asset_id=AssetObject.id).all()
for PackageAssetObj in PackageAssets:
NewPackageAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=PackageAssetObj.asset_id)
db.session.add(NewPackageAssetObj)
AssetObject.sale_count += 1
db.session.add(UserAssetObj)
db.session.commit()
try:
if AssetObject.creator_type == 0:
SellerUserObj : User = User.query.filter_by(id=AssetObject.creator_id).first()
CreateTransactionForSale(
AssetObj = AssetObject,
PurchasePrice = JSONData["expectedPrice"],
PurchaseCurrencyType = JSONData["currencyType"],
Seller = SellerUserObj,
Buyer = AuthenticatedUser,
ApplyTaxAutomatically = True
)
else:
GroupObj : Group = Group.query.filter_by(id=AssetObject.creator_id).first()
CreateTransactionForSale(
AssetObj = AssetObject,
PurchasePrice = JSONData["expectedPrice"],
PurchaseCurrencyType = JSONData["currencyType"],
Seller = GroupObj,
Buyer = AuthenticatedUser,
ApplyTaxAutomatically = True
)
except Exception as e:
logging.warn(f"Failed to create transaction log for sale of asset {str(AssetObject.id)}, message: {str(e)}")
pass
return jsonify({"success": True, "message": "Asset purchased successfully"}),200
except AssertionError as e:
return jsonify({"success": False, "message": "Failed to acquire lock"}),400
@CatalogRoute.route("/api/purchase-limited", methods=["POST"])
@auth.authenticated_required_api
@limiter.limit("1/second")
@user_limiter.limit("1/second")
def api_purchase_limited():
JSONData = request.json
if "assetId" not in JSONData or "expectedPrice" not in JSONData or "expectedOwner" not in JSONData or "itemOwnershipId" not in JSONData:
return jsonify({"success": False, "message": "Invalid request"}),400
# Expected Types: assetId (int), expectedPrice (int), expectedOwner (int), itemOwnershipId (int)
if type(JSONData["assetId"]) != int or type(JSONData["expectedPrice"]) != int or type(JSONData["expectedOwner"]) != int or type(JSONData["itemOwnershipId"]) != int:
return jsonify({"success": False, "message": "Invalid request"}),400
AuthenticatedUser : User = auth.GetAuthenticatedUser(request.cookies.get(".ROBLOSECURITY"))
EconomyLock = acquire_lock(f"economy:{str(AuthenticatedUser.id)}", acquire_timeout=5, lock_timeout=1)
if EconomyLock is False:
return jsonify({"success": False, "message": "Failed to acquire lock"}),400
AssetObject : Asset = Asset.query.filter_by(id=JSONData["assetId"]).first()
if AssetObject is None:
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": "Invalid asset"}),400
if AssetObject.is_limited == False:
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": "Asset is not limited"}),400
LimitedAsset : UserAsset = UserAsset.query.filter_by(assetid=AssetObject.id, id=JSONData["itemOwnershipId"]).first()
if LimitedAsset is None:
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": "Invalid asset info"}),400
ItemLock = acquire_lock(f"item:{str(LimitedAsset.id)}", acquire_timeout=5, lock_timeout=1)
if ItemLock is False:
return jsonify({"success": False, "message": "Failed to acquire lock ( This is usually caused by too many people purchasing this item at the same time. )"}),400
if LimitedAsset.is_for_sale == False:
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": "Item is not for sale"}),400
if JSONData["expectedOwner"] != LimitedAsset.userid:
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": f"Expected Owner ({str(JSONData['expectedOwner'])}) does not match current item owner({str(LimitedAsset.userid)})"}),400
if JSONData["expectedPrice"] != LimitedAsset.price:
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": f"Expected Price ({str(JSONData['expectedPrice'])}) does not match current item price ({str(LimitedAsset.price)})"}),400
if AuthenticatedUser.id == LimitedAsset.userid:
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
return jsonify({"success": False, "message": "You cannot purchase your own item"}),400
UserEconomyObj : UserEconomy = UserEconomy.query.filter_by(userid=AuthenticatedUser.id).first()
if UserEconomyObj.robux < LimitedAsset.price:
redis_controller.delete(f"economy:{str(AuthenticatedUser.id)}")
return jsonify({"success": False, "message": "Insufficient funds"}),400
UserEconomyObj.robux -= LimitedAsset.price
OriginalPrice : int = LimitedAsset.price
db.session.commit()
IncrementTargetBalance(GetUserFromId(LimitedAsset.userid), TaxCurrencyAmount(LimitedAsset.price), 0)
AdjustAssetRap(LimitedAsset.assetid, LimitedAsset.price)
LimitedAsset.userid = AuthenticatedUser.id
LimitedAsset.is_for_sale = False
LimitedAsset.price = 0
LimitedAsset.updated = datetime.utcnow()
db.session.commit()
# We release the locks first as taking a thumbnail can take a while
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
PreviousOwnerId : int = JSONData["expectedOwner"]
# Check how many items the previous owner had of the same asset
PreviousOwnerItems = UserAsset.query.filter_by(userid=PreviousOwnerId, assetid=AssetObject.id).count()
if PreviousOwnerItems == 0:
# Remove the asset from the previous owner avatar if there is any
AvatarAsset = UserAvatarAsset.query.filter_by(user_id=PreviousOwnerId, asset_id=AssetObject.id).first()
if AvatarAsset is not None:
db.session.delete(AvatarAsset)
db.session.commit()
TakeUserThumbnail(PreviousOwnerId, True, False)
CreateSystemMessage(
f"Your {AssetObject.name} has been sold!",
f"Your item {AssetObject.name} ( UAID: {str(LimitedAsset.id)} / Serial: {str(LimitedAsset.serial)} ) has been sold for R$ {str(OriginalPrice)} to {AuthenticatedUser.username} ( {str(AuthenticatedUser.id)} ).",
PreviousOwnerId
)
SellerUserObj : User = User.query.filter_by(id=PreviousOwnerId).first()
try:
CreateTransactionForSale(
AssetObj = AssetObject,
PurchasePrice = OriginalPrice,
PurchaseCurrencyType = 0,
Seller = SellerUserObj,
Buyer = AuthenticatedUser,
ApplyTaxAutomatically = True
)
except Exception as e:
logging.warn(f"Failed to create transaction log for sale of asset {str(AssetObject.id)}, message: {str(e)}")
pass
try:
newLimitedTransfer = LimitedItemTransfer(
original_owner_id = SellerUserObj.id,
new_owner_id = AuthenticatedUser.id,
asset_id = AssetObject.id,
user_asset_id = LimitedAsset.id,
transfer_method = LimitedItemTransferMethod.Purchase,
purchased_price = OriginalPrice
)
db.session.add(newLimitedTransfer)
db.session.commit()
except Exception as e:
logging.warn(f"Failed to create limited item transfer log for asset {str(AssetObject.id)}, message: {str(e)}")
pass
return jsonify({"success": True, "message": "Item purchased successfully"}),200
@LibraryRoute.route("/<int:assetid>/", methods=["GET"])
@LibraryRoute.route("/<int:assetid>/<assetname>", methods=["GET"])
@auth.authenticated_required
def asset_page(assetid, assetname=None):
AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObject is None:
return redirect("/library")
SlugName = slugify(AssetObject.name, lowercase=False)
if SlugName is None or SlugName == "":
SlugName = "unnamed"
if assetname is None:
if request.args.get("page") is None:
return redirect(f"/library/{assetid}/{SlugName}")
else:
return redirect(f"/library/{assetid}/{SlugName}?page={request.args.get('page')}")
if assetname != SlugName:
if request.args.get("page") is None:
return redirect(f"/library/{assetid}/{SlugName}")
else:
return redirect(f"/library/{assetid}/{SlugName}?page={request.args.get('page')}")
if AssetObject.asset_type.value in [2,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
return redirect(f"/catalog/{str(AssetObject.id)}/")
if AssetObject.asset_type.value == 9:
return redirect(f"/games/{assetid}/")
CreatorObj : User | Group = User.query.filter_by(id=AssetObject.creator_id).first() if AssetObject.creator_type == 0 else Group.query.filter_by(id=AssetObject.creator_id).first()
Created = AssetObject.created_at.strftime("%d/%m/%Y")
Updated = AssetObject.updated_at.strftime("%d/%m/%Y")
AuthenticatedUser : User = auth.GetCurrentUser()
doesUserOwnAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).first() is not None
BestPriceResult : UserAsset = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).first()
BestPrice = "None"
if BestPriceResult is not None:
BestPrice = str(BestPriceResult.price)
UserOwns = 0
PrivateSaleList = []
NextPage = 0
PreviousPage = 0
PageNumber = 0
AssetRap = 0
if AssetObject.is_limited and not AssetObject.is_for_sale:
AssetRap = GetAssetRap(assetid)
UserOwns = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).count()
Page = 1
if request.args.get("page"):
try:
Page = int(request.args.get("page"))
except:
pass
PrivateSales = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).paginate(page=Page, per_page=5).items
for sale in PrivateSales:
SellerUser : User = User.query.filter_by(id=sale.userid).first()
PrivateSaleList.append({
"price": sale.price,
"seller": SellerUser.username,
"sellerid": SellerUser.id,
"serial": sale.serial,
"uaid": sale.id
})
if len(UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).paginate(page=Page+1, per_page=5, error_out=False).items) > 0:
NextPage = Page + 1
else:
NextPage = -1
if Page > 1:
PreviousPage = Page - 1
else:
PreviousPage = -1
PageNumber = Page
GamepassLinkObj = None
GamepassRootPlaceAsset = None
if AssetObject.asset_type == AssetType.GamePass:
GamepassLinkObj : GamepassLink = GamepassLink.query.filter_by(gamepass_id=AssetObject.id).first()
GamepassUniverse : Universe = Universe.query.filter_by(id=GamepassLinkObj.universe_id).first()
GamepassRootPlaceAsset : Asset = Asset.query.filter_by( id = GamepassUniverse.root_place_id ).first()
return render_template("catalog/library.html", asset=AssetObject, creator=CreatorObj, createddate=Created,
updateddate=Updated, doesUserOwnAsset=doesUserOwnAsset, BestPrice=BestPrice,
BestPriceResult=BestPriceResult, userOwnAmountCount=UserOwns, PrivateSales=PrivateSaleList,
NextPage=NextPage, PreviousPage=PreviousPage, PageNumber=PageNumber, AssetRap=AssetRap,
GamepassLinkObj=GamepassLinkObj, GamepassRootPlaceAsset=GamepassRootPlaceAsset)
@BadgesPageRoute.route("/<int:badgeid>/", methods=["GET"])
@BadgesPageRoute.route("/<int:badgeid>/<badgename>", methods=["GET"])
@auth.authenticated_required
def badge_page( badgeid : int, badgename : str = None ):
BadgeObject : PlaceBadge = PlaceBadge.query.filter_by(id=badgeid).first()
if BadgeObject is None:
return abort(404)
if badgename is None:
return redirect(f"/badges/{badgeid}/{slugify(BadgeObject.name, lowercase=False)}")
elif badgename != slugify(BadgeObject.name, lowercase=False):
return redirect(f"/badges/{badgeid}/{slugify(BadgeObject.name, lowercase=False)}")
AuthenticatedUser : User = auth.GetCurrentUser()
UserBadgeObj : UserBadge = UserBadge.query.filter_by(user_id=AuthenticatedUser.id, badge_id=badgeid).first()
AssociatedPlaceAssetObj : Asset = Asset.query.filter_by(id=BadgeObject.associated_place_id).first()
CreatorObj : User | Group = User.query.filter_by(id=AssociatedPlaceAssetObj.creator_id).first() if AssociatedPlaceAssetObj.creator_type == 0 else Group.query.filter_by(id=AssociatedPlaceAssetObj.creator_id).first()
Created = BadgeObject.created_at.strftime("%d/%m/%Y")
Updated = BadgeObject.updated_at.strftime("%d/%m/%Y")
return render_template(
"catalog/badges.html",
badge = BadgeObject,
userbadge = UserBadgeObj,
AssociatedPlaceAssetObj = AssociatedPlaceAssetObj,
creator = CreatorObj,
createddate = Created,
updateddate = Updated
)
+76
View File
@@ -0,0 +1,76 @@
from app.models.asset import Asset
CatalogTypes = {
0 : {
"name": "Featured Items",
"query": "SELECT * FROM asset WHERE asset_type IN ('TShirt','Hat','Shirt','Pants','Decal','Head','Face','Gear','Package','HairAccessory','FaceAccessory','NeckAccessory','ShoulderAccessory','FrontAccessory','FrontAccessory','BackAccessory','WaistAccessory') AND creator_id = 1 AND creator_type = 0 AND (is_for_sale = true OR is_limited = true)"
},
1 : {
"name": "Featured Hats",
"query": "SELECT * FROM asset WHERE asset_type IN ('Hat','HairAccessory','FaceAccessory','NeckAccessory','ShoulderAccessory','FrontAccessory','FrontAccessory','BackAccessory','WaistAccessory') AND creator_id = 1 AND creator_type = 0 AND (is_for_sale = true OR is_limited = true)"
},
2 : {
"name": "Featured Gears",
"query": "SELECT * FROM asset WHERE asset_type IN ('Gear') AND creator_id = 1 AND creator_type = 0 AND (is_for_sale = true OR is_limited = true)"
},
3 : {
"name": "Featured Faces",
"query": "SELECT * FROM asset WHERE asset_type IN ('Face') AND creator_id = 1 AND creator_type = 0 AND (is_for_sale = true OR is_limited = true)"
},
4 : {
"name": "Collectible Items",
"query": "SELECT * FROM asset WHERE is_limited = true"
},
5 : {
"name": "Collectible Hats",
"query": "SELECT * FROM asset WHERE asset_type IN ('Hat','HairAccessory','FaceAccessory','NeckAccessory','ShoulderAccessory','FrontAccessory','FrontAccessory','BackAccessory','WaistAccessory') AND is_limited = true"
},
6 : {
"name": "Collectible Gears",
"query": "SELECT * FROM asset WHERE asset_type IN ('Gear') AND is_limited = true"
},
7 : {
"name": "Collectible Faces",
"query": "SELECT * FROM asset WHERE asset_type IN ('Face') AND is_limited = true"
},
8 : {
"name": "All Clothing",
"query": "SELECT * FROM asset WHERE asset_type IN ('TShirt','Hat','Shirt','Pants','Package') AND is_for_sale = true"
},
9 : {
"name": "Hats",
"query": "SELECT * FROM asset WHERE asset_type IN ('Hat','HairAccessory','FaceAccessory','NeckAccessory','ShoulderAccessory','FrontAccessory','FrontAccessory','BackAccessory','WaistAccessory') AND is_for_sale = true"
},
10 : {
"name": "Shirts",
"query": "SELECT * FROM asset WHERE asset_type IN ('Shirt') AND is_for_sale = true"
},
11 : {
"name": "T-Shirts",
"query": "SELECT * FROM asset WHERE asset_type IN ('TShirt') AND is_for_sale = true"
},
12 : {
"name": "Pants",
"query": "SELECT * FROM asset WHERE asset_type IN ('Pants') AND is_for_sale = true"
},
13 : {
"name": "Packages",
"query": "SELECT * FROM asset WHERE asset_type in ('Package') AND is_for_sale = true"
},
14 : {
"name": "Body Parts",
"query": "SELECT * FROM asset WHERE asset_type IN ('Head','Face') AND is_for_sale = true"
},
15 : {
"name": "Heads",
"query": "SELECT * FROM asset WHERE asset_type IN ('Head') AND is_for_sale = true"
},
16 : {
"name": "Faces",
"query": "SELECT * FROM asset WHERE asset_type IN ('Face') AND is_for_sale = true"
},
41 : {
"name": "Hairs",
"query": "SELECT * FROM asset WHERE asset_type IN ('HairAccessory') AND (is_for_sale = true OR is_limited = true)"
},
}
+185
View File
@@ -0,0 +1,185 @@
{% extends '__layout__.html' %}
{% block title %}Catalog{% endblock %}
{% block head %}
<link href="/static/css/catalog.css" rel="stylesheet"/>
<style>
.text-secondary {
color: rgb(199, 199, 199) !important;
}
a {
text-decoration: none !important;
color: inherit;
}
.text-tickets {
color: rgb(224, 224, 60) !important;
font-size: small;
font-weight: 600;
}
.text-robux {
color: rgb(26, 212, 103) !important;
font-size: small;
font-weight: 600;
}
.text-limited {
color: rgb(255,255,255) !important;
background-color: rgb(9, 136, 62);
width: fit-content;
padding-left: 10px;
font-size: 10px;
border-top-right-radius: 5px;
margin: 0;
}
.text-limitedu {
color: rgb(0,0,0) !important;
background-color: rgb(245, 245, 52);
padding-right: 3px;
font-weight: 800;
border-top-right-radius: 5px;
padding-left: 3px;
}
.text-remaining {
color: rgb(197, 21, 21) !important;
font-size: 10px;
font-weight: 600;
}
</style>
<meta id="catalog-info" data-category-id="{{categoryid}}">
{% endblock %}
{% block content %}
<div id="main">
<div class="container" style="max-width: 1200px;">
<div class="d-md-flex">
<h1>Catalog</h1>
<div class="ms-md-5 row d-flex w-100 align-items-center">
<form method="post">
<div class="input-group">
<div class="form-floating" style="width: 52%;">
<input type="text" class="form-control" id="search-input" name="search-input" {%if query != None: %}value="{{query}}"{%endif%} placeholder="Awesome Shirt">
<label for="search-input">Search</label>
</div>
<div class="form-floating" style="min-width: 160px;">
<select class="form-select" id="order-by" name="order-by">
<option value="0">Relevance</option>
<option value="1" {%if SortType == 1 %}selected{%endif%}>Price (Low to High)</option>
<option value="2" {%if SortType == 2 %}selected{%endif%}>Price (High to Low)</option>
<option value="3" {%if SortType == 3 %}selected{%endif%}>Recently Updated</option>
<option value="4" {%if SortType == 4 %}selected{%endif%}>Best Selling</option>
</select>
<label for="order-by">Order By</label>
</div>
<button type="submit" class="btn btn-outline-primary fw-bold" style="min-width: 50px;"><i class="bi bi-search"></i></button>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-md-2 p-0 mt-3 pb-3">
<div class="border rounded p-2" style="background-color: rgb(19, 19, 19);">
<h5 class="text-center m-1" style="font-size: 20px;">Categories</h5>
<div class="row">
<div class="text-white col-md-12 col-sm-6">
<a href="/catalog/?category=0"><b>Featured</b></a>
<div class="ms-2 text-secondary">
<a href="/catalog/?category=1"><p class="m-0">Featured Hats</p></a>
<a href="/catalog/?category=2"><p class="m-0">Featured Gears</p></a>
<a href="/catalog/?category=3"><p class="m-0">Featured Faces</p></a>
</div>
</div>
<div class="text-white col-md-12 col-sm-6">
<a href="/catalog/?category=4"><b>Collectibles</b></a>
<div class="ms-2 text-secondary">
<a href="/catalog/?category=5"><p class="m-0">Collectible Hats</p></a>
<a href="/catalog/?category=6"><p class="m-0">Collectible Gears</p></a>
<a href="/catalog/?category=7"><p class="m-0">Collectible Faces</p></a>
</div>
</div>
<div class="text-white col-md-12 col-sm-6">
<a href="/catalog/?category=8"><b>Clothing</b></a>
<div class="ms-2 text-secondary">
<a href="/catalog/?category=9"><p class="m-0">Hats</p></a>
<a href="/catalog/?category=10"><p class="m-0">Shirts</p></a>
<a href="/catalog/?category=11"><p class="m-0">T-Shirts</p></a>
<a href="/catalog/?category=12"><p class="m-0">Pants</p></a>
<a href="/catalog/?category=13"><p class="m-0">Packages</p></a>
</div>
</div>
<div class="text-white col-md-12 col-sm-6">
<a href="/catalog/?category=14"><b>Body Parts</b></a>
<div class="ms-2 text-secondary">
<a href="/catalog/?category=41"><p class="m-0">Hairs</p></a>
<a href="/catalog/?category=15"><p class="m-0">Heads</p></a>
<a href="/catalog/?category=16"><p class="m-0">Faces</p></a>
<a href="/catalog/?category=13"><p class="m-0">Packages</p></a>
</div>
</div>
</div>
</div>
</div>
<div class="col mt-3">
<h3 class="mb-1">{{categoryname}}</h3>
<div class="row">
{% for item in queryResults %}
<div class="col-md-2 col-3 p-1">
<a href="/catalog/{{item.id}}/">
<div class="rounded item-card p-2">
<div class="border rounded overflow-hidden position-relative">
<img src="/Thumbs/Asset.ashx?x=180&y=180&assetId={{item.id}}" width="100%" style="aspect-ratio: 1/1;" alt="{{item.name}}">
{% if item.is_limited: %}<p class="position-absolute m-0 fw-bold text-limited" style="bottom:-1px;left:0px;">LIMITED {%if item.is_limited_unique: %}<span class="text-limitedu">U</span>{%else%}<span class="me-2"></span>{%endif%}</p>{% endif %}
{% if item.offsale_at != None %}<p class="position-absolute bg-danger m-0" style="top: 0px;right: 0px;border-bottom-left-radius: 10px;padding: 5px;"><i class="bi bi-alarm text-white"></i></p>{% endif %}
</div>
<p class="text-secondary m-0" style="font-size:12px;">{{item.name}}</p>
{% if item.is_for_sale: %}
{% if item.price_robux == 0 and item.price_tix == 0: %}
<p class="text-robux m-0 ">FREE</p>
{% else %}
{% if item.price_robux != 0: %}
<p class="text-robux m-0">R$ {{item.price_robux}}</p>
{% endif %}
{% if item.price_tix != 0: %}
<p class="text-tickets m-0">T$ {{item.price_tix}}</p>
{% endif %}
{% endif %}
{% if item.is_limited_unique : %}
<p class="text-remaining m-0">Remaining: <span class="text-white">{{item.serial_count - item.sale_count}}</span></p>
{%endif%}
{% else %}
{% if item.is_limited: %}
{% if item.price_robux == 0 and item.price_tix != 0: %}
<p class="text-tickets m-0"><span class="text-secondary fw-normal">was</span> T$ {{item.price_tix or "-"}}</p>
{% elif item.price_robux != 0 and item.price_tix != 0: %}
<p class="text-robux m-0"><span class="text-secondary fw-normal">was</span> R$ {{item.price_robux or "-"}}</p>
{% elif item.price_robux != 0 and item.price_tix == 0: %}
<p class="text-robux m-0"><span class="text-secondary fw-normal">was</span> R$ {{item.price_robux or "-"}}</p>
{% else %}
<p class="text-robux m-0"><span class="text-secondary fw-normal">was</span> FREE</p>
{% endif %}
<p class="text-robux m-0"><span class="text-secondary fw-normal">now</span> R$ {{item.best_price}}</p>
{%else%}
<p class="text-secondary m-0 ">Offsale</p>
{% endif %}
{% endif %}
</div></a>
</div>
{% endfor %}
</div>
{% if len(queryResults) == 0: %}
<p class="w-100 text-center mt-5 mb-5">No results found</p>
{% endif %}
<div class="d-flex">
{% if PreviousPage > 0: %}
<a href="/catalog/?category={{CatalogType}}&page={{PreviousPage}}&sort={{SortType}}" class="ms-auto text-decoration-none text-primary">Previous</a>
{%else%}
<a class="ms-auto text-decoration-none text-secondary">Previous</a>
{% endif %}
<p class="ms-2 me-2 text-white">Page {{PageNumber}} of {{totalPages}}</p>
{% if NextPage > 0: %}
<a href="/catalog/?category={{CatalogType}}&page={{NextPage}}&sort={{SortType}}" class="me-auto text-decoration-none text-primary">Next</a>
{%else%}
<a class="me-auto text-decoration-none text-secondary">Next</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+186
View File
@@ -0,0 +1,186 @@
{% extends '__layout__.html' %}
{% block title %}{{asset.name}}{% endblock %}
{% block head %}
<link href="/static/css/catalog.css" rel="stylesheet"/>
<script src="/static/js/catalog.js"></script>
<style>
.text-secondary {
color: rgb(199, 199, 199) !important;
}
.text-limited {
color: rgb(255,255,255) !important;
background-color: rgb(9, 136, 62);
width: fit-content;
padding-left: 10px;
font-size: 20px;
border-top-right-radius: 5px;
margin: 0;
}
.text-limitedu {
color: rgb(0,0,0) !important;
background-color: rgb(245, 245, 52);
padding-right: 3px;
font-weight: 800;
border-top-right-radius: 5px;
padding-left: 3px;
}
.text-robux {
color: rgb(26, 212, 103) !important;
font-size: small;
font-weight: 600;
}
</style>
<div id="csrf_token" data-csrf-token="{{ csrf_token() }}"></div>
<meta id="asset-info" data-asset-name="{{asset.name}}" data-asset-id="{{asset.id}}" data-asset-onsale="{% if asset.is_for_sale: %}true{%else%}false{%endif%}" {% if doesUserOwnAsset: %}data-user-ownasset="true"{% endif %} {% if asset.is_limited: %}data-islimited="true"{%endif%}>
{% endblock %}
{% block content %}
<div id="main">
<div class="container ps-5 pe-5 ps-md-0 pe-md-0" style="max-width: 800px;">
<h1>{{asset.name}}</h1>
<div class="row">
<div class="col-md-5 col-12">
<div class="p-2 rounded overflow-hidden position-relative{% if GamepassLinkObj != None: %} rounded-bottom-0 {%endif%}" style="background-color: rgb(36, 36, 36);">
<img width="100%" src="/Thumbs/Asset.ashx?assetId={{asset.id}}&x=420&y=420" style="aspect-ratio: 1/1;" alt="{{asset.name}}">
{% if asset.is_limited: %}<p class="position-absolute m-0 fw-bold text-limited" style="bottom:-2px;left:0px;">LIMITED {%if asset.is_limited_unique: %}<span class="text-limitedu">U</span>{%else%}<span class="me-2"></span>{%endif%}</p>{% endif %}
</div>
{% if GamepassLinkObj != None: %}
<div class="rounded-bottom overflow-hidden d-flex p-1" style="background-color: rgb(29, 29, 29);">
<p class="m-0 text-white me-2" style="font-size: 13px;">Use this Gamepass in: <a class="text-decoration-none" href="/games/{{GamepassRootPlaceAsset.id}}/">{{GamepassRootPlaceAsset.name}}</a></p>
<a class="text-decoration-none ms-auto" href="/games/{{GamepassRootPlaceAsset.id}}/"><img src="/Thumbs/PlaceIcon.ashx?assetId={{GamepassRootPlaceAsset.id}}&x=60&y=60" height="60px" width="60px" class="rounded"></a>
</div>
{% endif %}
</div>
<div class="col-md-4 p-1">
<div class="d-flex mt-3 mt-md-0">
{% if asset.creator_type == 0 :%}
<img width="80" height="80" src="/Thumbs/Avatar.ashx?userId={{creator.id}}&x=100&y=100" alt="{{creator.username}}">
<div style="font-size:13px;">
<p class="m-0 text-secondary">Creator: <a href="/users/{{creator.id}}/profile" class="text-decoration-none">{{creator.username}}</a></p>
<p class="m-0 text-secondary">Created: <span class="text-white">{{createddate}}</span></p>
<p class="m-0 text-secondary">Updated: <span class="text-white">{{updateddate}}</span></p>
</div>
{% else %}
<img width="80" height="80" src="/Thumbs/GroupIcon.ashx?groupid={{creator.id}}&x=100&y=100" alt="{{creator.name}}">
<div style="font-size:13px;">
<p class="m-0 text-secondary">Creator: <a href="/groups/{{creator.id}}/--" class="text-decoration-none">{{creator.name}}</a></p>
<p class="m-0 text-secondary">Created: <span class="text-white">{{createddate}}</span></p>
<p class="m-0 text-secondary">Updated: <span class="text-white">{{updateddate}}</span></p>
</div>
{% endif %}
</div>
<div class="linebreak"></div>
<p>
{{asset.description}}
</p>
</div>
<div class="col-md-3 p-1" >
<div class="border rounded p-2 text-center">
{% if asset.is_for_sale: %}
{% if asset.price_robux == 0 and asset.price_tix == 0: %}
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">FREE</span></p>
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="0" data-expected-price="{{asset.price_robux}}">Take One</a>
{% endif %}
{% if asset.price_robux > 0: %}
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">{{asset.price_robux}} Robux</span></p>
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="0" data-expected-price="{{asset.price_robux}}">Buy with Robux</a>
{% endif %}
{% if asset.price_robux > 0 and asset.price_tix > 0: %}
<div class="d-flex align-items-center mt-2 mb-2">
<div class="border-top flex-fill me-2"></div>
<p class="text-secondary m-0" style="font-size: 13px;">OR</p>
<div class="border-top flex-fill ms-2"></div>
</div>
{% endif %}
{% if asset.price_tix > 0: %}
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">{{asset.price_tix}} Tickets</span></p>
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="1" data-expected-price="{{asset.price_tix}}">Buy with Tickets</a>
{% endif %}
{% else: %}
{% if asset.is_limited: %}
{% if BestPrice != "None": %}
<p class="m-0 text-secondary" style="font-size:14px;">Best Price: <span class="text-white">R$ {{BestPrice}}</span></p>
{% if BestPriceResult.userid != currentuser.id %}
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="0" data-islimited="true" data-expectedowner="{{BestPriceResult.userid}}" data-expectedid="{{BestPriceResult.id}}" data-expected-price="{{BestPrice}}">Buy Now</a>
{%else%}
<btn class="btn btn-primary fw-bold w-100 btn-sm disabled">Buy with Robux</btn>
{%endif%}
{%else%}
<btn class="btn btn-primary fw-bold w-100 btn-sm disabled">No Resellers Available</btn>
{%endif%}
{% if userOwnAmountCount > 0: %}
<p class="m-0 text-secondary" style="font-size:12px;">( {{userOwnAmountCount}} owned )</p>
{%endif%}
{%else%}
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">Offsale</span></p>
<btn class="btn btn-primary fw-bold w-100 btn-sm disabled">Buy with Robux</btn>
{%endif%}
{% endif %}
<p class="m-0 mt-1 text-secondary" style="font-size:12px;">( {{asset.sale_count}} sold )</p>
</div>
{% if asset.is_limited and userOwnAmountCount > 0 and not asset.is_for_sale: %}
<div class="border rounded p-2 text-center mt-2">
<a href="/catalog/resell/{{asset.id}}" class="btn btn-success fw-bold btn-sm w-100">Manage your items</a>
</div>
{%endif%}
</div>
</div>
{% if asset.is_limited and not asset.is_for_sale: %}
<div class="linebreak mt-3"></div>
<div class="row">
<div class="col-md-6">
<h5>Private Sales</h5>
{% for sale in PrivateSales: %}
<div class="w-100 m-2 d-flex">
<img class="rounded-2 overflow-hidden" width="60" height="60" src="/Thumbs/Avatar.ashx?x=60&y=60&userId={{sale.sellerid}}" alt="{{sale.seller}}"/>
<div class="ms-2">
<a href="/users/{{sale.sellerid}}/profile" class="text-decoration-none text-white m-0">{{sale.seller}}</a>
<p class="m-0 text-robux">R$ {{sale.price}}</p>
<p class="m-0 text-secondary" style="font-size:13px;">Serial: {%if asset.is_limited_unique: %}{{sale.serial}}{%else%}N/A{%endif%}</p>
</div>
<div class="ms-auto align-items-center d-flex">
{% if sale.sellerid != currentuser.id: %}
<button class="btn btn-sm btn-primary purchase-button" data-currency-type="0" data-islimited="true" data-expectedowner="{{sale.sellerid}}" data-expectedid="{{sale.uaid}}" data-expected-price="{{sale.price}}">Buy Now</button>
{%else%}
<button class="btn btn-sm btn-primary disabled">Buy Now</button>
{%endif%}
</div>
</div>
{%endfor%}
<div class="d-flex">
{% if PreviousPage > 0: %}
<a href="/catalog/{{asset.id}}/?page={{PreviousPage}}" class="ms-auto text-decoration-none">Previous</a>
{%else%}
<a class="ms-auto text-decoration-none text-secondary">Previous</a>
{% endif %}
<p class="ms-1 me-1">{{PageNumber}}</p>
{% if NextPage > 0: %}
<a href="/catalog/{{asset.id}}/?page={{NextPage}}" class="me-auto text-decoration-none">Next</a>
{%else%}
<a class="me-auto text-decoration-none text-secondary">Next</a>
{% endif %}
</div>
</div>
<div class="col-md-6">
<p class="m-0 text-secondary" style="font-size:small;">Recent Average Price: <span class="text-robux">R${{AssetRap}}</span></p>
</div>
</div>
{%endif%}
</div>
</div>
<div class="modal fade" id="purchaseModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="purchase-modal-title">Confirm purchase</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="purchase-modal-content">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="purchase-modal-close" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" id="purchase-modal-btn">Purchase</button>
</div>
</div>
</div>
</div>
{% endblock %}
+65
View File
@@ -0,0 +1,65 @@
{% extends '__layout__.html' %}
{% block title %}{{asset.name}}{% endblock %}
{% block head %}
<link href="/static/css/catalog.css" rel="stylesheet"/>
{% endblock %}
{% block content %}
<div id="main">
<div class="container" style="max-width: 800px;">
<a href="/catalog/{{asset.id}}/" class="text-decoration-none text-white">< Return to Item Page</a>
<h1>{{asset.name}}</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div>
{% for message in messages %}
<div class="alert border border-danger p-2 text-center messagealerts">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% if userassetnotforsalecount > 0: %}
<div class="linebreak"></div>
<form method="post">
<select class="form-control w-100" name="uaid">
{% for userasset in userassets %}
{% if not userasset.is_for_sale: %}
<option value="{{userasset.id}}">UAID: {{userasset.id}} | Serial: {% if asset.is_limited_unique: %}{{userasset.serial}}{%else%}N/A{%endif%}</option>
{%endif%}
{% endfor %}
</select>
<input type="number" class="form-control mt-3" name="itemprice" required placeholder="Price ( min: 1 )">
<p class="text-secondary m-0" style="font-size:small;">Note: Once sold you will only recieve 70% of the actual price it was sold for.</p>
{%if isOTPRequired: %}
<input type="text" class="form-control mt-1" name="2fa-code" required placeholder="2FA Code">
<p class="text-secondary m-0" style="font-size:small;">As you have 2FA Enabled it is required to authorise any sale of your items</p>
{%endif%}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-primary mt-4 w-100 fw-bold text-center">Resell Item</button>
</form>
{%endif%}
{% if userassetforsalecount > 0: %}
<div class="linebreak"></div>
<h5>Manage onsale items</h5>
{% for userasset in userassets %}
{% if userasset.is_for_sale: %}
<div class="border p-2 m-2 rounded">
<div class="row">
<div class="col-md-10">
<h5 class="m-0">UAID: {{userasset.id}} | Serial: {% if asset.is_limited_unique: %}{{userasset.serial}}{%else%}N/A{%endif%}</h5>
<p class="m-0 fs-6">Price: {{userasset.price}}</p>
</div>
<div class="col-md-2">
<form method="post" action="/catalog/resell/{{userasset.id}}/takeoff">
<button type="submit" class="btn btn-danger btn-sm fw-bold">Take Offsale</button>
</form>
</div>
</div>
</div>
{%endif%}
{%endfor%}
{%endif%}
</div>
</div>
{% endblock %}