ajout d'un toggle pour permission d'upload des documents dans admin pannel
CI - Security, Lint & Tests / validate (push) Failing after 1m3s
CI - Security, Lint & Tests / validate (push) Failing after 1m3s
ajout de compteur d'heure disponible ajout du calendrier de dispo dans les profils publics
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
"""Admin document-upload toggle — enable/disable gate and enforcement
|
||||
on contract upload routes.
|
||||
|
||||
ADMIN-006 style. The document_uploads_enabled setting gates every
|
||||
contract upload route for non-admins. Admins always bypass the lock.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.extensions import db
|
||||
from app.models import AppSettings, Contract, User
|
||||
|
||||
NON_ADMIN_ROLES = ['manager', 'coach']
|
||||
|
||||
|
||||
def _redirected(response):
|
||||
return response.status_code in (301, 302)
|
||||
|
||||
|
||||
class TestDocumentUploadToggle:
|
||||
"""Toggle logic — on/off round-trip and audit log."""
|
||||
|
||||
def test_toggle_from_enabled_to_disabled(self, client, as_role):
|
||||
as_role('admin')
|
||||
# Default is enabled
|
||||
client.post('/admin/toggle-document-uploads', data={}, follow_redirects=False)
|
||||
with client.application.app_context():
|
||||
assert not AppSettings.get_bool('document_uploads_enabled', default=True)
|
||||
|
||||
def test_toggle_from_disabled_to_enabled(self, client, as_role):
|
||||
as_role('admin')
|
||||
client.post('/admin/toggle-document-uploads', data={}, follow_redirects=False)
|
||||
client.post('/admin/toggle-document-uploads', data={}, follow_redirects=False)
|
||||
with client.application.app_context():
|
||||
assert AppSettings.get_bool('document_uploads_enabled', default=True)
|
||||
|
||||
def test_toggle_creates_audit_log(self, client, as_role):
|
||||
from app.models import AuditLog
|
||||
|
||||
as_role('admin')
|
||||
client.post('/admin/toggle-document-uploads', data={}, follow_redirects=False)
|
||||
|
||||
with client.application.app_context():
|
||||
entry = AuditLog.query.filter_by(action='document_uploads_toggled').first()
|
||||
assert entry is not None
|
||||
|
||||
def test_default_is_enabled(self, app):
|
||||
with app.app_context():
|
||||
assert AppSettings.get_bool('document_uploads_enabled', default=True)
|
||||
|
||||
|
||||
class TestDocumentUploadGateEnforcement:
|
||||
"""Non-admin roles are blocked when the toggle is off."""
|
||||
|
||||
def _contract_for(self, app, player_id):
|
||||
"""Create a minimal contract for enforcement tests."""
|
||||
from datetime import datetime
|
||||
|
||||
with app.app_context():
|
||||
admin = User.query.filter_by(role='admin').first()
|
||||
contract = Contract(
|
||||
player_id=player_id,
|
||||
uploaded_by_id=admin.id,
|
||||
original_filename='test.pdf',
|
||||
stored_filename='test.pdf',
|
||||
file_path='contrats signes/test.pdf',
|
||||
status='pending_player_signing',
|
||||
uploaded_at=datetime.utcnow(),
|
||||
)
|
||||
db.session.add(contract)
|
||||
db.session.commit()
|
||||
return contract.id
|
||||
|
||||
@pytest.mark.parametrize('role', NON_ADMIN_ROLES)
|
||||
def test_non_admin_cannot_upload_contract_when_disabled(
|
||||
self, app, client, as_role, make_user, role
|
||||
):
|
||||
as_role('admin')
|
||||
client.post('/admin/toggle-document-uploads', data={}) # disable
|
||||
as_role(role)
|
||||
|
||||
response = client.post(
|
||||
'/users/contracts/upload',
|
||||
data={'player_id': '', 'contract_file': ''},
|
||||
follow_redirects=True,
|
||||
)
|
||||
html = response.data.decode()
|
||||
# Default locale is French — the flash message says "désactivés"
|
||||
assert 'désactivés' in html or 'disabled' in html.lower()
|
||||
|
||||
@pytest.mark.parametrize('role', NON_ADMIN_ROLES)
|
||||
def test_admin_can_always_upload_contract(self, client, as_role, role):
|
||||
as_role('admin')
|
||||
# Pre-create a player for the contract
|
||||
|
||||
as_role('player')
|
||||
client.post('/admin/toggle-document-uploads', data={}) # disable
|
||||
|
||||
# Admin should still be able to reach the upload page
|
||||
response = client.get('/users/contracts/upload', follow_redirects=False)
|
||||
assert _redirected(response) or response.status_code == 200
|
||||
|
||||
def test_player_cannot_upload_signed_when_disabled(self, app, client, as_role, make_user):
|
||||
as_role('admin')
|
||||
client.post('/admin/toggle-document-uploads', data={}) # disable while logged in as admin
|
||||
player_id = make_user('player')
|
||||
contract_id = self._contract_for(app, player_id)
|
||||
|
||||
# Log in as the player
|
||||
with app.app_context():
|
||||
username = db.session.get(User, player_id).username
|
||||
client.post('/auth/login', data={'username': username, 'password': 'Password123'})
|
||||
|
||||
response = client.post(
|
||||
f'/users/contracts/{contract_id}/upload_signed',
|
||||
data={'signed_file': ''},
|
||||
follow_redirects=True,
|
||||
)
|
||||
html = response.data.decode()
|
||||
assert 'désactivés' in html or 'disabled' in html.lower()
|
||||
|
||||
def test_admin_can_always_upload(self, app, client, as_role):
|
||||
as_role('admin')
|
||||
client.post('/admin/toggle-document-uploads', data={}) # disable
|
||||
|
||||
# Admin GET the upload page — should be allowed (not redirected away)
|
||||
response = client.get('/users/contracts/upload', follow_redirects=False)
|
||||
# The admin bypasses the lock so they see the upload form (200)
|
||||
# or get redirected (if no players exist, etc). Either way is fine
|
||||
# as long as it's not the flash about uploads being disabled.
|
||||
body = response.data.decode() if response.status_code == 200 else ''
|
||||
# The toggle flash ("désactivés") could still be present, but the
|
||||
# upload-disabled message ("désactivés") must not appear in the body.
|
||||
# We check for the French lock message specifically.
|
||||
assert 'actuellement désactivés' not in body
|
||||
@@ -12,6 +12,7 @@ ADMIN_ROUTES = [
|
||||
'/admin',
|
||||
'/admin/backup/create',
|
||||
'/admin/toggle-tryouts',
|
||||
'/admin/toggle-document-uploads',
|
||||
'/admin/season/start',
|
||||
'/admin/season/end',
|
||||
'/admin/teams/wipe',
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Tests for the hours column in the user list and the read-only
|
||||
availability grid on public profiles."""
|
||||
|
||||
from datetime import time
|
||||
|
||||
from app.extensions import db
|
||||
from app.models import (
|
||||
CoachAvailability,
|
||||
PlayerDisponibility,
|
||||
)
|
||||
|
||||
|
||||
class TestHoursColumn:
|
||||
"""The manage-users table shows available hours for players/coaches."""
|
||||
|
||||
def test_admin_sees_hours_header(self, app, client, as_role):
|
||||
as_role('admin')
|
||||
body = client.get('/users').get_data(as_text=True)
|
||||
# Default locale is French, so _("Hours") renders as "Heures"
|
||||
assert 'Heures' in body
|
||||
|
||||
def test_player_without_slots_shows_0(self, app, client, as_role, make_user):
|
||||
as_role('admin')
|
||||
make_user('player') # create a player but stay logged in as admin
|
||||
body = client.get('/users').get_data(as_text=True)
|
||||
assert '0.0' in body
|
||||
|
||||
def test_player_with_slots_shows_hours(self, app, client, as_role, make_user):
|
||||
as_role('admin')
|
||||
player_id = make_user('player')
|
||||
with app.app_context():
|
||||
db.session.add(
|
||||
PlayerDisponibility(
|
||||
player_id=player_id,
|
||||
day_of_week=0,
|
||||
start_time=time(17, 0),
|
||||
end_time=time(19, 0),
|
||||
)
|
||||
)
|
||||
db.session.add(
|
||||
PlayerDisponibility(
|
||||
player_id=player_id,
|
||||
day_of_week=1,
|
||||
start_time=time(18, 0),
|
||||
end_time=time(20, 0),
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
body = client.get('/users').get_data(as_text=True)
|
||||
# 2h + 2h = 4.0
|
||||
assert '4.0' in body
|
||||
|
||||
def test_coach_without_slots_shows_0(self, app, client, as_role, make_user):
|
||||
as_role('admin')
|
||||
make_user('coach')
|
||||
body = client.get('/users').get_data(as_text=True)
|
||||
assert '0.0' in body
|
||||
|
||||
def test_admin_roles_show_dash(self, app, client, as_role, make_user):
|
||||
as_role('admin')
|
||||
make_user('manager')
|
||||
make_user('scout')
|
||||
body = client.get('/users').get_data(as_text=True)
|
||||
# managers and scouts should show mdash
|
||||
assert '\u2014' in body
|
||||
|
||||
|
||||
class TestAvailabilityOnPublicProfile:
|
||||
"""view_user renders a weekly availability grid for players/coaches."""
|
||||
|
||||
def test_player_without_availability_shows_no_grid(self, app, client, as_role):
|
||||
as_role('admin')
|
||||
player_id = as_role('player')
|
||||
body = client.get(f'/users/{player_id}/view').get_data(as_text=True)
|
||||
# Default locale is French
|
||||
assert 'Disponibilit\u00e9s' not in body
|
||||
|
||||
def test_player_with_slots_shows_grid(self, app, client, as_role, make_user):
|
||||
as_role('admin')
|
||||
player_id = make_user('player')
|
||||
with app.app_context():
|
||||
db.session.add(
|
||||
PlayerDisponibility(
|
||||
player_id=player_id,
|
||||
day_of_week=2,
|
||||
start_time=time(18, 0),
|
||||
end_time=time(18, 30),
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
body = client.get(f'/users/{player_id}/view').get_data(as_text=True)
|
||||
assert 'Disponibilit\u00e9s' in body
|
||||
assert '18:00' in body
|
||||
|
||||
def test_coach_with_slots_shows_grid(self, app, client, as_role, make_user):
|
||||
as_role('admin')
|
||||
coach_id = make_user('coach')
|
||||
with app.app_context():
|
||||
db.session.add(
|
||||
CoachAvailability(
|
||||
coach_id=coach_id,
|
||||
day_of_week=3,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(10, 30),
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
body = client.get(f'/users/{coach_id}/view').get_data(as_text=True)
|
||||
assert 'Disponibilit\u00e9s' in body
|
||||
assert '10:00' in body
|
||||
|
||||
def test_admin_profile_shows_no_grid(self, app, client, as_role):
|
||||
admin_id = as_role('admin')
|
||||
body = client.get(f'/users/{admin_id}/view').get_data(as_text=True)
|
||||
assert 'Disponibilit\u00e9s' not in body
|
||||
Reference in New Issue
Block a user