feat: refine market admin controls and user filters

parent b17195c8
......@@ -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 ''
......
......@@ -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),
}
)
......
......@@ -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,
}
}
)
......
......@@ -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 %}
......
......@@ -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);">
......
......@@ -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 %}
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
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']
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment