Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
A
aisbf
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nexlab
aisbf
Commits
2be59d51
Commit
2be59d51
authored
May 12, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: refine market admin controls and user filters
parent
b17195c8
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
701 additions
and
13 deletions
+701
-13
database.py
aisbf/database.py
+17
-1
admin.py
aisbf/routes/dashboard/admin.py
+1
-0
settings.py
aisbf/routes/dashboard/settings.py
+9
-3
base.html
templates/base.html
+2
-1
admin_payment_settings.html
templates/dashboard/admin_payment_settings.html
+44
-0
users.html
templates/dashboard/users.html
+64
-8
test_dashboard_user_market_controls.py
tests/routes/test_dashboard_user_market_controls.py
+328
-0
test_database_user_market_filters.py
tests/test_database_user_market_filters.py
+236
-0
No files found.
aisbf/database.py
View file @
2be59d51
...
...
@@ -1503,7 +1503,8 @@ class DatabaseManager:
def
get_users_paginated
(
self
,
page
:
int
=
1
,
limit
:
int
=
25
,
search
:
str
=
None
,
order_by
:
str
=
'created_at'
,
direction
:
str
=
'desc'
,
status_filter
:
str
=
None
,
role_filter
:
str
=
None
)
->
Dict
:
status_filter
:
str
=
None
,
role_filter
:
str
=
None
,
tier_filter
:
str
=
None
,
market_export_filter
:
str
=
None
)
->
Dict
:
"""
Get paginated users with search, sorting, and filtering support.
...
...
@@ -1515,6 +1516,8 @@ class DatabaseManager:
direction: Sort direction (asc or desc)
status_filter: Optional status filter ('active', 'inactive', or None)
role_filter: Optional role filter ('admin', 'user', or None)
tier_filter: Optional account tier id filter
market_export_filter: Optional market export filter ('exporting', 'not_exporting', or None)
Returns:
Dictionary with 'users' list and 'total' count
...
...
@@ -1575,6 +1578,19 @@ class DatabaseManager:
where_conditions
.
append
(
f
'u.role = {placeholder}'
)
params
.
append
(
role_filter
)
if
tier_filter
:
where_conditions
.
append
(
f
'u.tier_id = {placeholder}'
)
params
.
append
(
tier_filter
)
if
market_export_filter
==
'exporting'
:
where_conditions
.
append
(
f
'EXISTS (SELECT 1 FROM market_listings ml WHERE ml.owner_user_id = u.id AND ml.is_active = 1)'
)
elif
market_export_filter
==
'not_exporting'
:
where_conditions
.
append
(
f
'NOT EXISTS (SELECT 1 FROM market_listings ml WHERE ml.owner_user_id = u.id AND ml.is_active = 1)'
)
# Build final WHERE clause
where_clause
=
'WHERE '
+
' AND '
.
join
(
where_conditions
)
if
where_conditions
else
''
...
...
aisbf/routes/dashboard/admin.py
View file @
2be59d51
...
...
@@ -1510,6 +1510,7 @@ async def dashboard_admin_payment_settings(request: Request):
"session"
:
request
.
session
,
"currency_symbol"
:
DatabaseRegistry
.
get_config_database
()
.
get_currency_settings
()
.
get
(
'currency_symbol'
,
'$'
),
"market_settings"
:
DatabaseRegistry
.
get_config_database
()
.
get_market_settings
(),
"market_listings"
:
DatabaseRegistry
.
get_config_database
()
.
list_market_listings
(
active_only
=
False
),
}
)
...
...
aisbf/routes/dashboard/settings.py
View file @
2be59d51
...
...
@@ -761,7 +761,9 @@ async def dashboard_users(
order_by
:
str
=
Query
(
'created_at'
,
pattern
=
'^(username|last_login|created_at|tier_name)$'
),
direction
:
str
=
Query
(
'desc'
,
pattern
=
'^(asc|desc)$'
),
status_filter
:
str
=
Query
(
None
,
pattern
=
'^(active|inactive)$'
),
role_filter
:
str
=
Query
(
None
,
pattern
=
'^(admin|user)$'
)
role_filter
:
str
=
Query
(
None
,
pattern
=
'^(admin|user)$'
),
tier_filter
:
str
=
Query
(
None
),
market_export_filter
:
str
=
Query
(
None
,
pattern
=
'^(exporting|not_exporting)$'
)
):
"""Admin user management page"""
auth_check
=
require_admin
(
request
)
...
...
@@ -778,7 +780,9 @@ async def dashboard_users(
order_by
=
order_by
,
direction
=
direction
,
status_filter
=
status_filter
,
role_filter
=
role_filter
role_filter
=
role_filter
,
tier_filter
=
tier_filter
,
market_export_filter
=
market_export_filter
,
)
users
=
result
[
'users'
]
...
...
@@ -816,7 +820,9 @@ async def dashboard_users(
"order_by"
:
order_by
,
"direction"
:
direction
,
"status_filter"
:
status_filter
,
"role_filter"
:
role_filter
"role_filter"
:
role_filter
,
"tier_filter"
:
tier_filter
,
"market_export_filter"
:
market_export_filter
,
}
}
)
...
...
templates/base.html
View file @
2be59d51
...
...
@@ -834,7 +834,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<a
href=
"{{ url_for(request, '/dashboard/rotations') }}"
{%
if
'/
rotations
'
in
request
.
path
%}
class=
"active"
{%
endif
%}
data-i18n=
"nav.rotations"
>
Rotations
</a>
<a
href=
"{{ url_for(request, '/dashboard/autoselect') }}"
{%
if
'/
autoselect
'
in
request
.
path
%}
class=
"active"
{%
endif
%}
data-i18n=
"nav.autoselect"
>
Autoselect
</a>
<a
href=
"{{ url_for(request, '/dashboard/studio') }}"
{%
if
'/
studio
'
in
request
.
path
%}
class=
"active"
{%
endif
%}
data-i18n=
"nav.studio"
>
Studio
</a>
{% if request.state.market_enabled is not defined or request.state.market_enabled %}
<a
href=
"{{ url_for(request, '/dashboard/market') }}"
{%
if
'/
market
'
in
request
.
path
and
'/
admin
/
market
'
not
in
request
.
path
%}
class=
"active"
{%
endif
%}
>
Market
</a>
{% endif %}
<a
href=
"{{ url_for(request, '/dashboard/prompts') }}"
{%
if
'/
prompts
'
in
request
.
path
%}
class=
"active"
{%
endif
%}
data-i18n=
"nav.prompts"
>
Prompts
</a>
<a
href=
"{{ url_for(request, '/dashboard/analytics') }}"
{%
if
'/
analytics
'
in
request
.
path
%}
class=
"active"
{%
endif
%}
data-i18n=
"nav.analytics"
>
Analytics
</a>
{% if request.session.user_id %}
...
...
@@ -846,7 +848,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<a
href=
"{{ url_for(request, '/dashboard/users') }}"
{%
if
'/
users
'
in
request
.
path
%}
class=
"active"
{%
endif
%}
data-i18n=
"nav.users"
>
Users
</a>
<a
href=
"{{ url_for(request, '/dashboard/settings') }}"
{%
if
'/
settings
'
in
request
.
path
%}
class=
"active"
{%
endif
%}
data-i18n=
"nav.settings"
>
Settings
</a>
<a
href=
"{{ url_for(request, '/dashboard/admin/tiers') }}"
{%
if
'/
admin
/
tiers
'
in
request
.
path
%}
class=
"active"
{%
endif
%}
data-i18n=
"nav.tiers"
>
Tiers
</a>
<a
href=
"{{ url_for(request, '/dashboard/admin/market') }}"
{%
if
'/
admin
/
market
'
in
request
.
path
%}
class=
"active"
{%
endif
%}
>
Market Admin
</a>
<a
href=
"{{ url_for(request, '/dashboard/admin/payment-settings') }}"
{%
if
'/
admin
/
payment-settings
'
in
request
.
path
%}
class=
"active"
{%
endif
%}
data-i18n=
"nav.payment_settings"
>
Payment Settings
</a>
{% endif %}
{% if show_upgrade_button %}
...
...
templates/dashboard/admin_payment_settings.html
View file @
2be59d51
...
...
@@ -44,6 +44,50 @@
</form>
</div>
<div
style=
"background: var(--bg-panel); border: 2px solid var(--color-link); border-radius: 8px; padding: 20px; margin-bottom: 20px;"
>
<h3
style=
"margin: 0 0 20px 0; color: var(--color-link);"
>
<i
class=
"fas fa-store-alt me-2"
></i>
Market Administration
</h3>
<div
style=
"overflow:auto; border: 1px solid var(--color-border); border-radius: 8px; background: var(--bg-page);"
>
<table
style=
"width:100%; border-collapse: collapse;"
>
<thead>
<tr
style=
"background: var(--bg-accent);"
>
<th
style=
"padding:10px; text-align:left;"
>
ID
</th>
<th
style=
"padding:10px; text-align:left;"
>
Title
</th>
<th
style=
"padding:10px; text-align:left;"
>
Owner
</th>
<th
style=
"padding:10px; text-align:left;"
>
Source
</th>
<th
style=
"padding:10px; text-align:left;"
>
Status
</th>
<th
style=
"padding:10px; text-align:right;"
>
1M Tokens
</th>
<th
style=
"padding:10px; text-align:right;"
>
1K Requests
</th>
<th
style=
"padding:10px; text-align:right;"
>
Revenue
</th>
<th
style=
"padding:10px; text-align:right;"
>
Requests
</th>
<th
style=
"padding:10px; text-align:center;"
>
Active
</th>
</tr>
</thead>
<tbody>
{% for listing in market_listings %}
<tr
style=
"border-top:1px solid var(--color-border);"
>
<td
style=
"padding:10px;"
>
{{ listing.id }}
</td>
<td
style=
"padding:10px;"
>
{{ listing.title }}
</td>
<td
style=
"padding:10px;"
>
{{ listing.owner_username }}
</td>
<td
style=
"padding:10px;"
>
{{ listing.source_type }} / {{ listing.source_id }}
</td>
<td
style=
"padding:10px;"
>
{{ 'Online' if listing.online else 'Offline' }}
</td>
<td
style=
"padding:10px; text-align:right;"
>
{{ listing.price_per_million_tokens }}
</td>
<td
style=
"padding:10px; text-align:right;"
>
{{ listing.price_per_1000_requests }}
</td>
<td
style=
"padding:10px; text-align:right;"
>
{{ '%.2f'|format((listing.stats or {}).gross_revenue or 0) }}
</td>
<td
style=
"padding:10px; text-align:right;"
>
{{ (listing.stats or {}).total_requests or 0 }}
</td>
<td
style=
"padding:10px; text-align:center;"
>
{{ 'Yes' if listing.is_active else 'No' }}
</td>
</tr>
{% else %}
<tr>
<td
colspan=
"10"
style=
"padding:16px; text-align:center; color: var(--color-muted);"
>
No market listings yet.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Global Currency Settings -->
<div
style=
"background: var(--bg-panel); border: 2px solid #17a2b8; border-radius: 8px; padding: 20px; margin-bottom: 20px;"
>
<h3
style=
"margin: 0 0 20px 0; color: var(--color-link);"
>
...
...
templates/dashboard/users.html
View file @
2be59d51
...
...
@@ -82,6 +82,23 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<option
value=
"user"
{%
if
filters
.
role_filter =
=
'
user
'
%}
selected
{%
endif
%}
>
User
</option>
</select>
<label
for=
"tier-filter"
style=
"color: var(--color-text); margin-left: 10px; margin-right: 5px;"
data-i18n=
"users_page.col_tier"
>
Tier:
</label>
<select
id=
"tier-filter"
style=
"padding: 8px; border-radius: 4px; border: 1px solid var(--bg-accent); background: var(--bg-page); color: var(--color-text);"
>
<option
value=
""
>
All
</option>
{% for tier in tiers %}
<option
value=
"{{ tier.id }}"
{%
if
filters
.
tier_filter =
=
tier
.
id
|
string
%}
selected
{%
endif
%}
>
{{ tier.name }}{% if not tier.is_visible %} (Hidden){% endif %}
</option>
{% endfor %}
</select>
<label
for=
"market-export-filter"
style=
"color: var(--color-text); margin-left: 10px; margin-right: 5px;"
>
Market:
</label>
<select
id=
"market-export-filter"
style=
"padding: 8px; border-radius: 4px; border: 1px solid var(--bg-accent); background: var(--bg-page); color: var(--color-text);"
>
<option
value=
""
>
All
</option>
<option
value=
"exporting"
{%
if
filters
.
market_export_filter =
=
'
exporting
'
%}
selected
{%
endif
%}
>
Exporting
</option>
<option
value=
"not_exporting"
{%
if
filters
.
market_export_filter =
=
'
not_exporting
'
%}
selected
{%
endif
%}
>
Not Exporting
</option>
</select>
<button
id=
"search-btn"
class=
"btn"
style=
"padding: 8px 16px;"
data-i18n=
"users_page.search_btn"
>
Search
</button>
<button
id=
"clear-btn"
class=
"btn btn-secondary"
style=
"padding: 8px 16px;"
data-i18n=
"users_page.clear_btn"
>
Clear
</button>
</div>
...
...
@@ -370,6 +387,10 @@ document.addEventListener('DOMContentLoaded', function() {
const
searchInput
=
document
.
getElementById
(
'search-input'
);
const
searchBtn
=
document
.
getElementById
(
'search-btn'
);
const
clearBtn
=
document
.
getElementById
(
'clear-btn'
);
const
statusFilter
=
document
.
getElementById
(
'status-filter'
);
const
roleFilter
=
document
.
getElementById
(
'role-filter'
);
const
tierFilter
=
document
.
getElementById
(
'tier-filter'
);
const
marketExportFilter
=
document
.
getElementById
(
'market-export-filter'
);
const
pageSize
=
document
.
getElementById
(
'page-size'
);
const
prevBtn
=
document
.
getElementById
(
'prev-btn'
);
const
nextBtn
=
document
.
getElementById
(
'next-btn'
);
...
...
@@ -543,19 +564,54 @@ document.addEventListener('DOMContentLoaded', function() {
clearBtn
.
addEventListener
(
'click'
,
function
()
{
if
(
searchInput
)
{
searchInput
.
value
=
''
;
updateUsers
({
search
:
''
,
page
:
1
});
}
if
(
statusFilter
)
{
statusFilter
.
value
=
''
;
}
if
(
roleFilter
)
{
roleFilter
.
value
=
''
;
}
if
(
tierFilter
)
{
tierFilter
.
value
=
''
;
}
if
(
marketExportFilter
)
{
marketExportFilter
.
value
=
''
;
}
updateUsers
({
search
:
''
,
status_filter
:
null
,
role_filter
:
null
,
tier_filter
:
null
,
market_export_filter
:
null
,
page
:
1
,
});
});
}
// Filter change handlers
document
.
getElementById
(
'status-filter'
).
addEventListener
(
'change'
,
function
()
{
updateUsers
({
status_filter
:
this
.
value
||
null
});
});
if
(
statusFilter
)
{
statusFilter
.
addEventListener
(
'change'
,
function
()
{
updateUsers
({
status_filter
:
this
.
value
||
null
});
});
}
document
.
getElementById
(
'role-filter'
).
addEventListener
(
'change'
,
function
()
{
updateUsers
({
role_filter
:
this
.
value
||
null
});
});
if
(
roleFilter
)
{
roleFilter
.
addEventListener
(
'change'
,
function
()
{
updateUsers
({
role_filter
:
this
.
value
||
null
});
});
}
if
(
tierFilter
)
{
tierFilter
.
addEventListener
(
'change'
,
function
()
{
updateUsers
({
tier_filter
:
this
.
value
||
null
});
});
}
if
(
marketExportFilter
)
{
marketExportFilter
.
addEventListener
(
'change'
,
function
()
{
updateUsers
({
market_export_filter
:
this
.
value
||
null
});
});
}
// Initial sorting listeners
attachSortingListeners
();
...
...
@@ -1070,4 +1126,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
</script>
{% endblock %}
\ No newline at end of file
{% endblock %}
tests/routes/test_dashboard_user_market_controls.py
0 → 100644
View file @
2be59d51
import
asyncio
import
json
import
sys
from
base64
import
b64encode
from
pathlib
import
Path
from
fastapi.responses
import
JSONResponse
from
fastapi.responses
import
HTMLResponse
from
fastapi.testclient
import
TestClient
from
itsdangerous
import
TimestampSigner
from
jinja2
import
Environment
,
FileSystemLoader
,
select_autoescape
from
aisbf.routes.dashboard
import
market
as
dashboard_market
from
aisbf.routes.dashboard
import
admin
as
dashboard_admin
from
aisbf.routes.dashboard
import
settings
as
dashboard_settings
from
aisbf.routes.dashboard.admin
import
api_save_market_settings
sys
.
path
.
insert
(
0
,
str
(
Path
(
__file__
)
.
resolve
()
.
parents
[
2
]))
from
main
import
app
class
TemplateCapture
:
def
__init__
(
self
):
self
.
calls
=
[]
self
.
_env
=
Environment
(
loader
=
FileSystemLoader
(
str
(
Path
(
__file__
)
.
resolve
()
.
parents
[
2
]
/
"templates"
)),
autoescape
=
select_autoescape
([
"html"
,
"xml"
]),
)
self
.
_env
.
globals
[
"url_for"
]
=
lambda
request
,
path
,
**
kwargs
:
path
def
TemplateResponse
(
self
,
*
args
,
**
kwargs
):
request
=
kwargs
[
"request"
]
name
=
kwargs
[
"name"
]
context
=
kwargs
[
"context"
]
self
.
calls
.
append
({
"request"
:
request
,
"name"
:
name
,
"context"
:
context
})
template
=
self
.
_env
.
get_template
(
name
)
return
HTMLResponse
(
template
.
render
(
**
context
))
class
DashboardUsersDbStub
:
def
__init__
(
self
):
self
.
paginated_calls
=
[]
def
get_users_paginated
(
self
,
**
kwargs
):
self
.
paginated_calls
.
append
(
kwargs
)
return
{
"users"
:
[{
"id"
:
1
,
"username"
:
"alice"
}],
"total"
:
1
,
}
def
get_all_tiers
(
self
):
return
[{
"id"
:
10
,
"name"
:
"Pro"
}]
class
MarketSettingsDbStub
:
def
__init__
(
self
):
self
.
current
=
{
"enabled"
:
True
,
"allow_user_publish"
:
True
,
"allow_admin_publish"
:
False
,
"allow_import"
:
True
,
"market_export_filter"
:
"all"
,
}
self
.
saved_payloads
=
[]
self
.
market_listings
=
[
{
"id"
:
7
,
"title"
:
"Flux Dev Pack"
,
"owner_username"
:
"alice"
,
"source_type"
:
"provider"
,
"source_id"
:
"runpod-test"
,
"online"
:
True
,
"price_per_million_tokens"
:
1.5
,
"price_per_1000_requests"
:
0.2
,
"stats"
:
{
"gross_revenue"
:
42.0
,
"total_requests"
:
12
},
"is_active"
:
True
,
}
]
def
get_market_settings
(
self
):
return
dict
(
self
.
current
)
def
save_market_settings
(
self
,
settings
):
self
.
saved_payloads
.
append
(
settings
)
self
.
current
=
dict
(
settings
)
return
True
def
list_market_listings
(
self
,
active_only
=
False
):
return
list
(
self
.
market_listings
)
def
get_currency_settings
(
self
):
return
{
"currency_symbol"
:
"$"
}
class
RegistryStub
:
def
__init__
(
self
,
db
):
self
.
_db
=
db
def
get_config_database
(
self
):
return
self
.
_db
class
AsyncJsonRequest
:
def
__init__
(
self
,
session
,
body
):
self
.
session
=
session
self
.
_body
=
body
async
def
json
(
self
):
return
self
.
_body
def
_find_session_secret
()
->
str
:
for
middleware
in
app
.
user_middleware
:
kwargs
=
getattr
(
middleware
,
"kwargs"
,
{})
secret_key
=
kwargs
.
get
(
"secret_key"
)
if
secret_key
:
return
secret_key
raise
AssertionError
(
"Session middleware secret key not found"
)
def
_set_session_cookie
(
client
:
TestClient
,
data
:
dict
)
->
None
:
signer
=
TimestampSigner
(
_find_session_secret
())
serialized
=
b64encode
(
json
.
dumps
(
data
)
.
encode
(
"utf-8"
))
signed
=
signer
.
sign
(
serialized
)
.
decode
(
"utf-8"
)
client
.
cookies
.
set
(
"session"
,
signed
)
def
_login_as_admin
(
client
:
TestClient
)
->
None
:
_set_session_cookie
(
client
,
{
"logged_in"
:
True
,
"username"
:
"admin"
,
"role"
:
"admin"
,
"user_id"
:
None
,
"expires_at"
:
4102444800
,
},
)
def
test_dashboard_users_passes_market_filters_and_preserves_markup_state
(
monkeypatch
):
db
=
DashboardUsersDbStub
()
templates
=
TemplateCapture
()
client
=
TestClient
(
app
)
_login_as_admin
(
client
)
monkeypatch
.
setattr
(
dashboard_settings
,
"DatabaseRegistry"
,
RegistryStub
(
db
))
monkeypatch
.
setattr
(
dashboard_settings
,
"_templates"
,
templates
)
response
=
client
.
get
(
"/dashboard/users"
,
params
=
{
"page"
:
2
,
"limit"
:
15
,
"search"
:
"ali"
,
"order_by"
:
"username"
,
"direction"
:
"asc"
,
"status_filter"
:
"active"
,
"role_filter"
:
"user"
,
"tier_filter"
:
"10"
,
"market_export_filter"
:
"exporting"
,
},
)
assert
response
.
status_code
==
200
assert
db
.
paginated_calls
==
[
{
"page"
:
2
,
"limit"
:
15
,
"search"
:
"ali"
,
"order_by"
:
"username"
,
"direction"
:
"asc"
,
"status_filter"
:
"active"
,
"role_filter"
:
"user"
,
"tier_filter"
:
"10"
,
"market_export_filter"
:
"exporting"
,
}
]
assert
templates
.
calls
[
-
1
][
"name"
]
==
"dashboard/users.html"
assert
templates
.
calls
[
-
1
][
"context"
][
"filters"
][
"tier_filter"
]
==
"10"
assert
templates
.
calls
[
-
1
][
"context"
][
"filters"
][
"market_export_filter"
]
==
"exporting"
assert
'id="tier-filter"'
in
response
.
text
assert
'id="tier-filter"'
in
response
.
text
assert
'<option value="10" selected'
in
response
.
text
assert
'Pro'
in
response
.
text
assert
'id="market-export-filter"'
in
response
.
text
assert
'<option value="exporting" selected'
in
response
.
text
assert
'<option value="not_exporting"'
in
response
.
text
def
test_api_save_market_settings_merges_market_enabled_without_dropping_existing_flags
(
monkeypatch
):
db
=
MarketSettingsDbStub
()
monkeypatch
.
setattr
(
dashboard_admin
,
"DatabaseRegistry"
,
RegistryStub
(
db
))
monkeypatch
.
setattr
(
dashboard_admin
,
"require_api_admin"
,
lambda
request
:
None
)
request
=
AsyncJsonRequest
(
session
=
{
"logged_in"
:
True
,
"role"
:
"admin"
,
"user_id"
:
None
},
body
=
{
"market_enabled"
:
False
},
)
response
=
asyncio
.
run
(
api_save_market_settings
(
request
))
assert
response
.
status_code
==
200
assert
db
.
saved_payloads
==
[
{
"enabled"
:
False
,
"allow_user_publish"
:
True
,
"allow_admin_publish"
:
True
,
"allow_import"
:
True
,
}
]
def
test_ensure_market_enabled_respects_market_settings_enabled_contract
(
monkeypatch
):
db
=
MarketSettingsDbStub
()
db
.
current
=
{
"enabled"
:
False
,
"allow_user_publish"
:
True
,
"allow_admin_publish"
:
False
,
"allow_import"
:
True
,
"market_export_filter"
:
"all"
,
"market_enabled"
:
True
,
}
monkeypatch
.
setattr
(
dashboard_market
,
"DatabaseRegistry"
,
RegistryStub
(
db
))
request
=
type
(
"RequestStub"
,
(),
{
"session"
:
{
"role"
:
"user"
}})()
response
,
settings
=
dashboard_market
.
_ensure_market_enabled
(
request
)
assert
isinstance
(
response
,
JSONResponse
)
assert
response
.
status_code
==
403
assert
json
.
loads
(
response
.
body
)
==
{
"error"
:
"Market is disabled"
}
assert
settings
[
"enabled"
]
is
False
def
test_admin_payment_settings_embeds_market_admin_controls_and_listings
(
monkeypatch
):
db
=
MarketSettingsDbStub
()
templates
=
TemplateCapture
()
client
=
TestClient
(
app
)
_login_as_admin
(
client
)
monkeypatch
.
setattr
(
dashboard_admin
,
"DatabaseRegistry"
,
RegistryStub
(
db
))
monkeypatch
.
setattr
(
dashboard_admin
,
"_templates"
,
templates
)
response
=
client
.
get
(
"/dashboard/admin/payment-settings"
)
assert
response
.
status_code
==
200
assert
templates
.
calls
[
-
1
][
"name"
]
==
"dashboard/admin_payment_settings.html"
assert
templates
.
calls
[
-
1
][
"context"
][
"market_settings"
][
"enabled"
]
is
True
assert
templates
.
calls
[
-
1
][
"context"
][
"market_listings"
][
0
][
"title"
]
==
"Flux Dev Pack"
assert
"Enable market"
in
response
.
text
assert
"Market Administration"
in
response
.
text
assert
"Flux Dev Pack"
in
response
.
text
def
test_base_nav_hides_market_admin_link
(
monkeypatch
):
templates
=
TemplateCapture
()
request
=
type
(
"RequestStub"
,
(),
{
"path"
:
"/dashboard/settings"
,
"scope"
:
{
"root_path"
:
""
},
"session"
:
{
"logged_in"
:
True
,
"role"
:
"admin"
,
"user_id"
:
None
,
"username"
:
"admin"
,
},
"state"
:
type
(
"StateStub"
,
(),
{
"welcome_modal_message"
:
None
,
"welcome_modal_source"
:
None
,
"show_footer_links"
:
True
,
},
)(),
},
)()
html
=
templates
.
_env
.
get_template
(
"base.html"
)
.
render
(
request
=
request
,
title
=
"Settings"
,
content
=
""
,
show_upgrade_button
=
False
,
__version__
=
"test"
,
)
assert
"Market Admin"
not
in
html
def
test_base_nav_hides_market_link_when_market_disabled
(
monkeypatch
):
templates
=
TemplateCapture
()
request
=
type
(
"RequestStub"
,
(),
{
"path"
:
"/dashboard/settings"
,
"scope"
:
{
"root_path"
:
""
},
"session"
:
{
"logged_in"
:
True
,
"role"
:
"admin"
,
"user_id"
:
None
,
"username"
:
"admin"
,
},
"state"
:
type
(
"StateStub"
,
(),
{
"welcome_modal_message"
:
None
,
"welcome_modal_source"
:
None
,
"show_footer_links"
:
True
,
"market_enabled"
:
False
,
},
)(),
},
)()
html
=
templates
.
_env
.
get_template
(
"base.html"
)
.
render
(
request
=
request
,
title
=
"Settings"
,
content
=
""
,
show_upgrade_button
=
False
,
__version__
=
"test"
,
)
assert
">Market</a>"
not
in
html
tests/test_database_user_market_filters.py
0 → 100644
View file @
2be59d51
import
pytest
from
aisbf.database
import
DatabaseManager
@
pytest
.
fixture
def
db_manager
(
tmp_path
):
db_path
=
tmp_path
/
"users.db"
db
=
DatabaseManager
({
'type'
:
'sqlite'
,
'sqlite_path'
:
str
(
db_path
),
})
pro_tier_id
=
db
.
create_tier
(
name
=
'Pro Tier'
,
description
=
'Paid plan'
,
price_monthly
=
10.0
,
price_yearly
=
100.0
,
)
users
=
{
'alice'
:
db
.
create_user
(
'alice'
,
'hash'
,
role
=
'user'
,
email
=
'alice@example.com'
,
display_name
=
'Alice Exporter'
,
),
'bob'
:
db
.
create_user
(
'bob'
,
'hash'
,
role
=
'user'
,
email
=
'bob@example.com'
,
display_name
=
'Bob Browser'
,
),
'carol'
:
db
.
create_user
(
'carol'
,
'hash'
,
role
=
'admin'
,
email
=
'carol@example.com'
,
display_name
=
'Carol Admin'
,
),
'dave'
:
db
.
create_user
(
'dave'
,
'hash'
,
role
=
'user'
,
email
=
'dave@example.com'
,
display_name
=
'Dave Disabled'
,
),
}
with
db
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
cursor
.
execute
(
'UPDATE users SET tier_id = ? WHERE id = ?'
,
(
pro_tier_id
,
users
[
'alice'
]),
)
cursor
.
execute
(
'UPDATE users SET is_active = 0 WHERE id = ?'
,
(
users
[
'dave'
],),
)
conn
.
commit
()
db
.
upsert_market_listing
(
users
[
'alice'
],
'alice'
,
{
'source_scope'
:
'user'
,
'source_type'
:
'provider'
,
'source_id'
:
'alice-provider'
,
'listing_key'
:
'provider:alice-provider'
,
'title'
:
'Alice Provider'
,
'description'
:
'Alice export listing'
,
'provider_id'
:
'alice-provider'
,
'model_id'
:
None
,
'endpoint'
:
'https://example.test/alice'
,
'currency_code'
:
'USD'
,
'price_per_million_tokens'
:
2.5
,
'price_per_1000_requests'
:
0.0
,
'provider_price_per_million_tokens'
:
2.5
,
'provider_price_per_1000_requests'
:
0.0
,
'metadata'
:
{
'provider_type'
:
'openai'
},
'config_snapshot'
:
{
'provider'
:
{
'type'
:
'openai'
}},
'is_active'
:
True
,
},
)
db
.
upsert_market_listing
(
users
[
'carol'
],
'carol'
,
{
'source_scope'
:
'user'
,
'source_type'
:
'provider'
,
'source_id'
:
'carol-provider'
,
'listing_key'
:
'provider:carol-provider'
,
'title'
:
'Carol Provider'
,
'description'
:
'Carol export listing'
,
'provider_id'
:
'carol-provider'
,
'model_id'
:
None
,
'endpoint'
:
'https://example.test/carol'
,
'currency_code'
:
'USD'
,
'price_per_million_tokens'
:
4.0
,
'price_per_1000_requests'
:
0.0
,
'provider_price_per_million_tokens'
:
4.0
,
'provider_price_per_1000_requests'
:
0.0
,
'metadata'
:
{
'provider_type'
:
'openai'
},
'config_snapshot'
:
{
'provider'
:
{
'type'
:
'openai'
}},
'is_active'
:
True
,
},
)
db
.
upsert_market_listing
(
users
[
'dave'
],
'dave'
,
{
'source_scope'
:
'user'
,
'source_type'
:
'provider'
,
'source_id'
:
'dave-provider'
,
'listing_key'
:
'provider:dave-provider'
,
'title'
:
'Dave Provider'
,
'description'
:
'Dave inactive export listing'
,
'provider_id'
:
'dave-provider'
,
'model_id'
:
None
,
'endpoint'
:
'https://example.test/dave'
,
'currency_code'
:
'USD'
,
'price_per_million_tokens'
:
1.0
,
'price_per_1000_requests'
:
0.0
,
'provider_price_per_million_tokens'
:
1.0
,
'provider_price_per_1000_requests'
:
0.0
,
'metadata'
:
{
'provider_type'
:
'openai'
},
'config_snapshot'
:
{
'provider'
:
{
'type'
:
'openai'
}},
'is_active'
:
False
,
},
)
return
{
'db'
:
db
,
'pro_tier_id'
:
pro_tier_id
,
}
def
_usernames
(
result
):
return
[
user
[
'username'
]
for
user
in
result
[
'users'
]]
def
test_get_users_paginated_filters_by_tier_id
(
db_manager
):
result
=
db_manager
[
'db'
]
.
get_users_paginated
(
tier_filter
=
db_manager
[
'pro_tier_id'
],
order_by
=
'username'
,
direction
=
'asc'
,
)
assert
result
[
'total'
]
==
1
assert
_usernames
(
result
)
==
[
'alice'
]
def
test_get_users_paginated_nonexistent_tier_id_returns_no_matches
(
db_manager
):
result
=
db_manager
[
'db'
]
.
get_users_paginated
(
tier_filter
=
999999
,
order_by
=
'username'
,
direction
=
'asc'
,
)
assert
result
[
'total'
]
==
0
assert
_usernames
(
result
)
==
[]
def
test_get_users_paginated_filters_users_with_market_exports
(
db_manager
):
result
=
db_manager
[
'db'
]
.
get_users_paginated
(
market_export_filter
=
'exporting'
,
order_by
=
'username'
,
direction
=
'asc'
,
)
assert
result
[
'total'
]
==
2
assert
_usernames
(
result
)
==
[
'alice'
,
'carol'
]
def
test_get_users_paginated_filters_users_without_market_exports
(
db_manager
):
result
=
db_manager
[
'db'
]
.
get_users_paginated
(
market_export_filter
=
'not_exporting'
,
order_by
=
'username'
,
direction
=
'asc'
,
)
assert
result
[
'total'
]
==
2
assert
_usernames
(
result
)
==
[
'bob'
,
'dave'
]
def
test_get_users_paginated_ignores_inactive_only_market_exports
(
db_manager
):
result
=
db_manager
[
'db'
]
.
get_users_paginated
(
market_export_filter
=
'exporting'
,
search
=
'dave'
,
order_by
=
'username'
,
direction
=
'asc'
,
)
assert
result
[
'total'
]
==
0
assert
_usernames
(
result
)
==
[]
def
test_get_users_paginated_unsupported_market_export_filter_falls_back_to_unfiltered
(
db_manager
):
result
=
db_manager
[
'db'
]
.
get_users_paginated
(
market_export_filter
=
'maybe'
,
order_by
=
'username'
,
direction
=
'asc'
,
)
assert
result
[
'total'
]
==
4
assert
_usernames
(
result
)
==
[
'alice'
,
'bob'
,
'carol'
,
'dave'
]
def
test_get_users_paginated_combines_market_export_and_existing_filters
(
db_manager
):
result
=
db_manager
[
'db'
]
.
get_users_paginated
(
search
=
'alice'
,
status_filter
=
'active'
,
role_filter
=
'user'
,
market_export_filter
=
'exporting'
,
order_by
=
'username'
,
direction
=
'asc'
,
)
assert
result
[
'total'
]
==
1
assert
_usernames
(
result
)
==
[
'alice'
]
def
test_get_users_paginated_combines_tier_and_existing_filters
(
db_manager
):
result
=
db_manager
[
'db'
]
.
get_users_paginated
(
search
=
'alice'
,
status_filter
=
'active'
,
role_filter
=
'user'
,
tier_filter
=
db_manager
[
'pro_tier_id'
],
order_by
=
'username'
,
direction
=
'asc'
,
)
assert
result
[
'total'
]
==
1
assert
_usernames
(
result
)
==
[
'alice'
]
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment