Fiabilise l'import initial du patrimoine
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
This commit is contained in:
parent
d84e29f814
commit
a0e8d3003d
4 changed files with 555 additions and 1174 deletions
|
|
@ -11,9 +11,11 @@ from flask_login import login_required, current_user
|
|||
from werkzeug.security import generate_password_hash
|
||||
from ...extensions import db, csrf
|
||||
from ..models.user import User
|
||||
from ..models.college import College, Building
|
||||
from ..models.college import College, Building, Zone, Room
|
||||
from ..models.equipment import EquipmentCategory
|
||||
from ..models.maintenance import Lot
|
||||
from ..models.planning import WorkSchedule, CollegeClosure
|
||||
from ..setup_catalog import EQUIPMENT_CATEGORIES, LOTS, grouped_catalog
|
||||
|
||||
setup_wizard_bp = Blueprint('setup_wizard', __name__, url_prefix='/setup-wizard')
|
||||
|
||||
|
|
@ -53,14 +55,8 @@ def protect_initial_setup():
|
|||
|
||||
|
||||
def load_lots_data():
|
||||
"""Charge les données des lots depuis le fichier JSON."""
|
||||
json_path = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'lots_categories_tasks.json')
|
||||
try:
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Erreur chargement lots: {e}")
|
||||
return {}
|
||||
"""Retourne le catalogue embarqué issu du dump SQL de référence."""
|
||||
return grouped_catalog()
|
||||
|
||||
|
||||
class SetupProgress(db.Model):
|
||||
|
|
@ -68,7 +64,8 @@ class SetupProgress(db.Model):
|
|||
__tablename__ = 'setup_progress'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
step = db.Column(db.Integer, default=1)
|
||||
# Numéro de la dernière étape validée. Zéro signifie « aucune ».
|
||||
step = db.Column(db.Integer, default=0)
|
||||
admin_username = db.Column(db.String(100), nullable=True)
|
||||
college_name = db.Column(db.String(200), nullable=True)
|
||||
college_address = db.Column(db.String(255), nullable=True)
|
||||
|
|
@ -128,6 +125,26 @@ class SetupProgress(db.Model):
|
|||
|
||||
def to_dict(self):
|
||||
"""Convertit en dictionnaire pour le template."""
|
||||
buildings = [
|
||||
{'id': item.id, 'name': item.name, 'description': item.description or ''}
|
||||
for item in Building.query.order_by(Building.name, Building.id).all()
|
||||
]
|
||||
zones = [
|
||||
{'id': item.id, 'name': item.name, 'building_id': item.building_id}
|
||||
for item in Zone.query.order_by(Zone.building_id, Zone.name, Zone.id).all()
|
||||
]
|
||||
rooms = [
|
||||
{
|
||||
'id': item.id,
|
||||
'name': item.name,
|
||||
'code': '' if item.code in (None, 'None') else item.code,
|
||||
'building_id': item.building_id,
|
||||
'zone_id': item.zone_id,
|
||||
'floor': item.floor or 0,
|
||||
'room_type_id': item.room_type_id,
|
||||
}
|
||||
for item in Room.query.order_by(Room.building_id, Room.name, Room.id).all()
|
||||
]
|
||||
return {
|
||||
'step': self.step,
|
||||
'data': {
|
||||
|
|
@ -138,7 +155,9 @@ class SetupProgress(db.Model):
|
|||
'zip': self.college_zip or '',
|
||||
'city': self.college_city or '',
|
||||
},
|
||||
'buildings': self.get_buildings(),
|
||||
'buildings': buildings,
|
||||
'zones': zones,
|
||||
'rooms': rooms,
|
||||
'lots': self.get_lots(),
|
||||
'categories': json.loads(self.categories_json) if self.categories_json else [],
|
||||
'vacances_zone': self.vacances_zone or '',
|
||||
|
|
@ -185,7 +204,9 @@ def api_save_step(step):
|
|||
return jsonify({'error': 'Setup déjà terminé'}), 403
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
data = request.get_json(silent=True)
|
||||
if not isinstance(data, dict):
|
||||
return jsonify({'error': 'Corps JSON invalide'}), 400
|
||||
progress = SetupProgress.get()
|
||||
|
||||
if step == 1:
|
||||
|
|
@ -263,43 +284,153 @@ def api_save_step(step):
|
|||
elif step == 3:
|
||||
# Étape 3 : Bâtiments
|
||||
buildings = data.get('buildings', [])
|
||||
if not isinstance(buildings, list) or not buildings:
|
||||
return jsonify({'error': 'Ajoutez au moins un bâtiment'}), 400
|
||||
|
||||
# Supprimer les anciens bâtiments
|
||||
Building.query.delete()
|
||||
|
||||
saved = []
|
||||
for b in buildings:
|
||||
building = Building(
|
||||
name=b.get('name'),
|
||||
code=b.get('code', ''),
|
||||
floors=b.get('floors', 1)
|
||||
)
|
||||
if not isinstance(b, dict):
|
||||
return jsonify({'error': 'Bâtiment invalide'}), 400
|
||||
name = str(b.get('name') or '').strip()
|
||||
if not name:
|
||||
return jsonify({'error': 'Le nom de chaque bâtiment est requis'}), 400
|
||||
building_id = b.get('id')
|
||||
building = db.session.get(Building, building_id) if building_id else None
|
||||
if building is None:
|
||||
building = Building.query.filter(db.func.lower(Building.name) == name.lower()).first()
|
||||
if building is None:
|
||||
building = Building(name=name)
|
||||
db.session.add(building)
|
||||
db.session.flush()
|
||||
building.name = name
|
||||
building.description = str(b.get('description') or '').strip()
|
||||
saved.append({'id': building.id, 'name': building.name, 'description': building.description or ''})
|
||||
|
||||
progress.set_buildings(buildings)
|
||||
progress.step = 4
|
||||
progress.set_buildings(saved)
|
||||
progress.step = max(progress.step or 0, 3)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'step': 4})
|
||||
return jsonify({'success': True, 'step': 3, 'buildings': saved})
|
||||
|
||||
elif step == 4:
|
||||
# Étape 4 : Lots/Catégories
|
||||
categories = data.get('categories', [])
|
||||
# Étape 4 : Zones. Les absences du formulaire ne suppriment rien.
|
||||
zones = data.get('zones', [])
|
||||
if not isinstance(zones, list):
|
||||
return jsonify({'error': 'Liste de zones invalide'}), 400
|
||||
saved = []
|
||||
for item in zones:
|
||||
if not isinstance(item, dict):
|
||||
return jsonify({'error': 'Zone invalide'}), 400
|
||||
name = str(item.get('name') or '').strip()
|
||||
try:
|
||||
building_id = int(item.get('building_id'))
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({'error': f'Bâtiment invalide pour la zone {name or "sans nom"}'}), 400
|
||||
if not name or db.session.get(Building, building_id) is None:
|
||||
return jsonify({'error': 'Chaque zone doit avoir un nom et un bâtiment existant'}), 400
|
||||
zone_id = item.get('id')
|
||||
zone = db.session.get(Zone, zone_id) if zone_id else None
|
||||
if zone is None:
|
||||
zone = Zone.query.filter(
|
||||
Zone.building_id == building_id,
|
||||
db.func.lower(Zone.name) == name.lower(),
|
||||
).first()
|
||||
if zone is None:
|
||||
zone = Zone(name=name, building_id=building_id)
|
||||
db.session.add(zone)
|
||||
db.session.flush()
|
||||
zone.name = name
|
||||
zone.building_id = building_id
|
||||
saved.append({'id': zone.id, 'name': zone.name, 'building_id': zone.building_id})
|
||||
|
||||
# Créer les lots sélectionnés
|
||||
for cat in categories:
|
||||
lot = Lot.query.filter_by(name=cat).first()
|
||||
if not lot:
|
||||
lot = Lot(name=cat, description=f'Lot {cat}')
|
||||
db.session.add(lot)
|
||||
|
||||
progress.categories_json = json.dumps(categories)
|
||||
progress.step = 5
|
||||
progress.step = max(progress.step or 0, 4)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'step': 5})
|
||||
return jsonify({'success': True, 'step': 4, 'zones': saved})
|
||||
|
||||
elif step == 5:
|
||||
# Étape 5 : Vacances et horaires
|
||||
# Étape 5 : Salles. Mise à jour/création uniquement.
|
||||
rooms = data.get('rooms', [])
|
||||
if not isinstance(rooms, list):
|
||||
return jsonify({'error': 'Liste de salles invalide'}), 400
|
||||
saved = []
|
||||
for item in rooms:
|
||||
if not isinstance(item, dict):
|
||||
return jsonify({'error': 'Salle invalide'}), 400
|
||||
name = str(item.get('name') or '').strip()
|
||||
try:
|
||||
building_id = int(item.get('building_id'))
|
||||
floor = int(item.get('floor') or 0)
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({'error': f'Bâtiment ou étage invalide pour {name or "la salle"}'}), 400
|
||||
building = db.session.get(Building, building_id)
|
||||
if not name or building is None:
|
||||
return jsonify({'error': 'Chaque salle doit avoir un nom et un bâtiment existant'}), 400
|
||||
zone_id = item.get('zone_id') or None
|
||||
if zone_id is not None:
|
||||
try:
|
||||
zone_id = int(zone_id)
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({'error': f'Zone invalide pour {name}'}), 400
|
||||
zone = db.session.get(Zone, zone_id)
|
||||
if zone is None or zone.building_id != building_id:
|
||||
return jsonify({'error': f'La zone de {name} ne correspond pas à son bâtiment'}), 400
|
||||
room_id = item.get('id')
|
||||
room = db.session.get(Room, room_id) if room_id else None
|
||||
if room is None:
|
||||
room = Room.query.filter(
|
||||
Room.building_id == building_id,
|
||||
db.func.lower(Room.name) == name.lower(),
|
||||
).first()
|
||||
if room is None:
|
||||
room = Room(name=name, building_id=building_id)
|
||||
db.session.add(room)
|
||||
db.session.flush()
|
||||
room.name = name
|
||||
room.code = str(item.get('code') or '').strip() or None
|
||||
room.building_id = building_id
|
||||
room.zone_id = zone_id
|
||||
room.floor = floor
|
||||
saved.append({
|
||||
'id': room.id, 'name': room.name, 'code': room.code or '',
|
||||
'building_id': room.building_id, 'zone_id': room.zone_id, 'floor': room.floor,
|
||||
})
|
||||
|
||||
progress.step = max(progress.step or 0, 5)
|
||||
db.session.commit()
|
||||
return jsonify({'success': True, 'step': 5, 'rooms': saved})
|
||||
|
||||
elif step == 6:
|
||||
# Étape 6 : catalogue fixe des catégories et lots du dump SQL.
|
||||
categories_by_name = {}
|
||||
for name in EQUIPMENT_CATEGORIES:
|
||||
category = EquipmentCategory.query.filter(
|
||||
db.func.lower(EquipmentCategory.name) == name.lower()
|
||||
).first()
|
||||
if category is None:
|
||||
category = EquipmentCategory(name=name)
|
||||
db.session.add(category)
|
||||
db.session.flush()
|
||||
categories_by_name[name] = category
|
||||
|
||||
for lot_name, category_name in LOTS:
|
||||
lot = Lot.query.filter(db.func.lower(Lot.name) == lot_name.lower()).first()
|
||||
if lot is None:
|
||||
lot = Lot(name=lot_name)
|
||||
db.session.add(lot)
|
||||
lot.category_id = categories_by_name[category_name].id
|
||||
|
||||
progress.categories_json = json.dumps(list(EQUIPMENT_CATEGORIES), ensure_ascii=False)
|
||||
progress.set_lots([name for name, _category in LOTS])
|
||||
progress.step = max(progress.step or 0, 6)
|
||||
db.session.commit()
|
||||
return jsonify({
|
||||
'success': True, 'step': 6,
|
||||
'categories_count': len(EQUIPMENT_CATEGORIES), 'lots_count': len(LOTS),
|
||||
})
|
||||
|
||||
elif step == 7:
|
||||
# Étape 7 : Vacances et horaires
|
||||
zone = data.get('zone', 'A')
|
||||
annee = data.get('annee', 2024)
|
||||
import_vacances = data.get('import_vacances', False)
|
||||
|
|
@ -315,10 +446,21 @@ def api_save_step(step):
|
|||
'custom_closures': custom_closures,
|
||||
'personal_leaves': personal_leaves
|
||||
})
|
||||
progress.step = 6
|
||||
progress.step = max(progress.step or 0, 7)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'step': 6})
|
||||
return jsonify({'success': True, 'step': 7})
|
||||
|
||||
elif step in (8, 9):
|
||||
# Intégrations facultatives : le paramétrage complet reste dans leurs pages dédiées.
|
||||
progress.step = max(progress.step or 0, step)
|
||||
db.session.commit()
|
||||
return jsonify({'success': True, 'step': step, 'skipped': True})
|
||||
|
||||
elif step == 10:
|
||||
progress.step = max(progress.step or 0, 10)
|
||||
db.session.commit()
|
||||
return jsonify({'success': True, 'step': 10})
|
||||
|
||||
else:
|
||||
return jsonify({'error': 'Étape invalide'}), 400
|
||||
|
|
@ -341,7 +483,7 @@ def api_complete():
|
|||
admin = User.query.filter_by(role='admin', is_active=True).first()
|
||||
if not admin:
|
||||
return jsonify({'error': 'Un administrateur actif est requis'}), 400
|
||||
if progress.step < 6:
|
||||
if progress.step < 10:
|
||||
return jsonify({'error': 'Toutes les étapes doivent être terminées'}), 400
|
||||
|
||||
# Créer le fichier .setup_complete
|
||||
|
|
|
|||
87
app_new/core/setup_catalog.py
Normal file
87
app_new/core/setup_catalog.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""Catalogue patrimonial livré avec l'assistant de première installation.
|
||||
|
||||
Ces valeurs reprennent les catégories et les lots du dump SQL de référence.
|
||||
Les noms sont les clés fonctionnelles utilisées pour une synchronisation idempotente :
|
||||
le wizard ne supprime jamais une ligne déjà présente dans la base.
|
||||
"""
|
||||
|
||||
EQUIPMENT_CATEGORIES = (
|
||||
"Chauffage", "Courants faibles", "Gros œuvre", "Revêtement de façades",
|
||||
"Menuiseries extérieures", "Cloisons", "Menuiseries intérieures", "Métallerie",
|
||||
"Faux-plafonds", "Revêtements de sols souples", "Peinture", "Signalétique",
|
||||
"Ventilation", "Plomberie", "Traitement de l'eau", "Gaz", "Courants forts",
|
||||
"Portes et portails", "Ascenseur", "Réseaux enterrés", "Voirie", "Espaces verts",
|
||||
"Clôtures", "Equipements sportifs", "Occultations", "Eau chaude sanitaire",
|
||||
"Jeux d'enfants", "Couverture et étanchéité", "Désenfumage", "Flocages",
|
||||
"Charpente", "Paratonnerre", "Revêtements de sols durs", "Revêtements de sols",
|
||||
"Divers", "Mobilier",
|
||||
)
|
||||
|
||||
LOTS = (
|
||||
("Local chaufferie : ventilations haute et basse, extincteur, bloc autonome, conduit de fumées, siphon de sol, porte d'accès", "Chauffage"),
|
||||
("Chaudières et équipements (armoire électrique, vannes, panoplie, vase d'expansion, pot à boues, …)", "Chauffage"),
|
||||
("Réseaux de distribution et radiateurs", "Chauffage"),
|
||||
("Chaudières dans logements", "Chauffage"), ("Régulation", "Chauffage"),
|
||||
("Système de sécurité incendie, détection, centrale de mise en sécurité, équipement d'alarme", "Courants faibles"),
|
||||
("Déclencheur manuel", "Courants faibles"), ("Autocommutateur", "Courants faibles"),
|
||||
("Alarme technique", "Courants faibles"), ("Caméras", "Courants faibles"),
|
||||
("Sonorisation - horloge", "Courants faibles"),
|
||||
("Vidéoprojecteurs fixes (TICE salles de classes)", "Courants faibles"),
|
||||
("Alarme anti-agression - Protection des travailleurs isolés (PTI)", "Courants faibles"),
|
||||
("Alarme anti-intrusion", "Courants faibles"), ("Interphonie - Contrôle d'accès", "Courants faibles"),
|
||||
("Vide sanitaire", "Gros œuvre"), ("Structure béton : poteaux, voiles, planchers, poutres", "Gros œuvre"),
|
||||
("Structure béton : poteaux, voiles, planchers, poutres, balcons, consoles, appuis de fenêtre, escaliers béton", "Gros œuvre"),
|
||||
("Bardages", "Revêtement de façades"), ("Enduits de façade", "Revêtement de façades"),
|
||||
("Portes extérieures", "Menuiseries extérieures"), ("Fenêtres", "Menuiseries extérieures"),
|
||||
("Joints de vitrages et joints entre ouvrants et dormants", "Menuiseries extérieures"),
|
||||
("Cloisons intérieures", "Cloisons"), ("Portes coupe-feu", "Menuiseries intérieures"),
|
||||
("Portes de distribution simples", "Menuiseries intérieures"),
|
||||
("Escaliers, mains courantes, garde-corps, ouvrages de métallerie", "Métallerie"),
|
||||
("Rideaux et grilles métalliques (manuels ou motorisés)", "Métallerie"),
|
||||
("Faux plafonds minéraux ou métalliques", "Faux-plafonds"), ("Faux plafonds métalliques", "Faux-plafonds"),
|
||||
("Sols souples linoléum, PVC, sols coulés", "Revêtements de sols souples"), ("Peintures", "Peinture"),
|
||||
("Signalétique de sécurité et d'orientation", "Signalétique"), ("Groupes froids", "Ventilation"),
|
||||
("Centrales de traitement d'air (CTA)", "Ventilation"), ("Climatiseur split", "Ventilation"),
|
||||
("Extracteurs, gaines, clapets coupe-feu", "Ventilation"), ("Surpresseur d'eau", "Plomberie"),
|
||||
("Canalisations d'eau froide", "Plomberie"), ("Equipement sanitaires terminaux", "Plomberie"),
|
||||
("Robinetterie", "Plomberie"), ("Compteurs d'arrivée d'eau, gaz", "Plomberie"),
|
||||
("Extincteurs", "Plomberie"), ("Disconnecteurs", "Plomberie"),
|
||||
("RIA, colonnes sèches et humides", "Plomberie"), ("Adoucisseurs", "Traitement de l'eau"),
|
||||
("Alimentation, détente et distribution gaz", "Gaz"), ("Onduleur", "Courants forts"),
|
||||
("Local transformateur privatif et local TGBT", "Courants forts"), ("Armoires divisionnaires", "Courants forts"),
|
||||
("Eclairage de sécurité", "Courants forts"), ("Compteurs électrique", "Courants forts"),
|
||||
("Eclairage", "Courants forts"), ("Eclairage extérieur", "Courants forts"),
|
||||
("courants forts Eclairage", "Courants forts"), ("courants forts Eclairage extérieur", "Courants forts"),
|
||||
("courants forts Eclairage de sécurité", "Courants forts"),
|
||||
("Portes automatiques - portes basculantes", "Portes et portails"), ("Portail électrique", "Portes et portails"),
|
||||
("Ascenseur", "Ascenseur"), ("Réseaux enterrés, regards de visite, caniveaux et avaloirs", "Réseaux enterrés"),
|
||||
("Séparateur à graisses à fécules, à hydrocarbures", "Réseaux enterrés"),
|
||||
("Pompes de relevage des eaux usées ou des eaux pluviales", "Réseaux enterrés"),
|
||||
("Voirie, chaussées", "Voirie"), ("Voirie, dallages", "Voirie"), ("Parking enherbé", "Espaces verts"),
|
||||
("Pelouses", "Espaces verts"), ("Arbres de haute tige", "Espaces verts"),
|
||||
("Arbustes et haies (à hauteur d'homme)", "Espaces verts"), ("Clôtures", "Clôtures"),
|
||||
("Buts de football, volley, paniers de basket", "Equipements sportifs"), ("Murs d'escalade", "Equipements sportifs"),
|
||||
("Stores et volets roulants", "Occultations"),
|
||||
("Production d'eau chaude (chaudière, ballon)", "Eau chaude sanitaire"),
|
||||
("Distribution d'eau chaude", "Eau chaude sanitaire"), ("Toboggans, structures, jeux divers", "Jeux d'enfants"),
|
||||
("Toiture terrasse traditionnelle ou végétalisée", "Couverture et étanchéité"),
|
||||
("Toiture à pente", "Couverture et étanchéité"), ("Descentes d'eaux pluviales (EP)", "Couverture et étanchéité"),
|
||||
("Trappes, lanterneaux, skydômes, verrières (installations qui n'assurent pas le désenfumage)", "Couverture et étanchéité"),
|
||||
("Echelles, garde aux corps, protections pour interventions ultérieures en toiture", "Couverture et étanchéité"),
|
||||
("Ligne de vie", "Couverture et étanchéité"),
|
||||
("Exutoires de désenfumage et mécanisme d'ouverture", "Désenfumage"),
|
||||
("Flocages et peintures intumescentes", "Flocages"), ("Charpente bois", "Charpente"),
|
||||
("Charpente métallique", "Charpente"),
|
||||
("Paratonnerres et paratonnerres à dispositifs d'amorçage (PDA)", "Paratonnerre"),
|
||||
("Sols durs et faïences murales", "Revêtements de sols durs"),
|
||||
("souples Sols souples linoléum, PVC, sols coulés", "Revêtements de sols"),
|
||||
("Informatique", "Divers"), ("Mobilier", "Mobilier"),
|
||||
)
|
||||
|
||||
|
||||
def grouped_catalog():
|
||||
"""Format compact destiné à l'aperçu du wizard."""
|
||||
grouped = {category: [] for category in EQUIPMENT_CATEGORIES}
|
||||
for lot_name, category_name in LOTS:
|
||||
grouped[category_name].append(lot_name)
|
||||
return grouped
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -46,3 +46,86 @@ def test_completed_setup_rejects_mutation(client, monkeypatch, tmp_path):
|
|||
'password': 'a-strong-password',
|
||||
})
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def _authorize_setup(client, monkeypatch, tmp_path):
|
||||
from app_new.core.routes import setup_wizard
|
||||
|
||||
monkeypatch.setattr(setup_wizard, 'DATA_DIR', str(tmp_path))
|
||||
token = 'test-setup-token-with-at-least-32-characters'
|
||||
response = client.get(f'/setup-wizard/?token={token}')
|
||||
assert response.status_code == 302
|
||||
|
||||
|
||||
def test_setup_updates_buildings_without_deleting_existing(client, app, monkeypatch, tmp_path):
|
||||
from app_new import db
|
||||
from app_new.core.models.college import Building
|
||||
|
||||
_authorize_setup(client, monkeypatch, tmp_path)
|
||||
with app.app_context():
|
||||
existing = Building(name='Bâtiment conservé', description='Ancienne description')
|
||||
untouched = Building(name='Bâtiment hors formulaire')
|
||||
db.session.add_all([existing, untouched])
|
||||
db.session.commit()
|
||||
existing_id, untouched_id = existing.id, untouched.id
|
||||
|
||||
response = client.post('/setup-wizard/api/step/3', json={'buildings': [
|
||||
{'id': existing_id, 'name': 'Bâtiment conservé', 'description': 'Mise à jour'},
|
||||
{'name': 'Nouveau bâtiment', 'description': ''},
|
||||
]})
|
||||
assert response.status_code == 200
|
||||
|
||||
with app.app_context():
|
||||
assert db.session.get(Building, existing_id).description == 'Mise à jour'
|
||||
assert db.session.get(Building, untouched_id) is not None
|
||||
assert Building.query.filter_by(name='Nouveau bâtiment').count() == 1
|
||||
|
||||
|
||||
def test_setup_creates_zones_and_rooms_with_valid_links(client, app, monkeypatch, tmp_path):
|
||||
from app_new import db
|
||||
from app_new.core.models.college import Building, Room, Zone
|
||||
|
||||
_authorize_setup(client, monkeypatch, tmp_path)
|
||||
with app.app_context():
|
||||
building = Building(name='Bâtiment relations wizard')
|
||||
db.session.add(building)
|
||||
db.session.commit()
|
||||
building_id = building.id
|
||||
|
||||
zone_response = client.post('/setup-wizard/api/step/4', json={'zones': [
|
||||
{'name': 'Zone wizard', 'building_id': building_id},
|
||||
]})
|
||||
assert zone_response.status_code == 200
|
||||
zone_id = zone_response.get_json()['zones'][0]['id']
|
||||
|
||||
room_response = client.post('/setup-wizard/api/step/5', json={'rooms': [
|
||||
{'name': 'Salle wizard', 'code': 'W01', 'building_id': building_id, 'zone_id': zone_id, 'floor': 1},
|
||||
]})
|
||||
assert room_response.status_code == 200
|
||||
with app.app_context():
|
||||
zone = db.session.get(Zone, zone_id)
|
||||
room = Room.query.filter_by(name='Salle wizard').one()
|
||||
assert zone.building_id == building_id
|
||||
assert room.building_id == building_id
|
||||
assert room.zone_id == zone_id
|
||||
assert room.floor == 1
|
||||
|
||||
|
||||
def test_setup_imports_embedded_lot_catalog_idempotently(client, app, monkeypatch, tmp_path):
|
||||
from app_new.core.models.equipment import EquipmentCategory
|
||||
from app_new.core.models.maintenance import Lot
|
||||
from app_new.core.setup_catalog import EQUIPMENT_CATEGORIES, LOTS
|
||||
|
||||
_authorize_setup(client, monkeypatch, tmp_path)
|
||||
first = client.post('/setup-wizard/api/step/6', json={})
|
||||
second = client.post('/setup-wizard/api/step/6', json={})
|
||||
assert first.status_code == second.status_code == 200
|
||||
assert first.get_json()['categories_count'] == len(EQUIPMENT_CATEGORIES)
|
||||
assert first.get_json()['lots_count'] == len(LOTS)
|
||||
|
||||
with app.app_context():
|
||||
for name in EQUIPMENT_CATEGORIES:
|
||||
assert EquipmentCategory.query.filter_by(name=name).count() == 1
|
||||
for name, category_name in LOTS:
|
||||
lot = Lot.query.filter_by(name=name).one()
|
||||
assert lot.category.name == category_name
|
||||
|
|
|
|||
Loading…
Reference in a new issue