1.0
This commit is contained in:
parent
dc31487dac
commit
14c803de2e
3
__init__.py
Normal file
3
__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
def classFactory(iface):
|
||||
from .main_plugin import AnimationPlugin
|
||||
return AnimationPlugin(iface)
|
||||
0
animation_form.py
Normal file
0
animation_form.py
Normal file
173
db_manager.py
Normal file
173
db_manager.py
Normal file
@ -0,0 +1,173 @@
|
||||
import psycopg2
|
||||
import json
|
||||
|
||||
class DBManager:
|
||||
def __init__(self, host, port, dbname, user, password):
|
||||
self.conn = psycopg2.connect(
|
||||
host=host, port=port, dbname=dbname, user=user, password=password
|
||||
)
|
||||
self.conn.autocommit = False
|
||||
|
||||
def close(self):
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
|
||||
def fetch_all(self, query, params=None):
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute(query, params)
|
||||
return cur.fetchall()
|
||||
|
||||
def execute(self, query, params=None):
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute(query, params)
|
||||
self.conn.commit()
|
||||
|
||||
# --- Reference data loaders ---
|
||||
def get_animateurs(self):
|
||||
return self.fetch_all("SELECT code_animateur, animateur FROM animation.animateurs ORDER BY animateur")
|
||||
|
||||
def get_lieux(self):
|
||||
return self.fetch_all("SELECT code_lieux, lieux FROM animation.lieux_anim ORDER BY lieux")
|
||||
|
||||
def get_themes(self):
|
||||
return self.fetch_all("SELECT code_type_anim, theme_animation FROM animation.theme_animation ORDER BY theme_animation")
|
||||
|
||||
def get_types_groupe(self):
|
||||
return self.fetch_all("SELECT code_groupe, type_groupe FROM animation.type_groupe ORDER BY type_groupe")
|
||||
|
||||
def get_codes_annulation(self):
|
||||
return self.fetch_all("SELECT code_annul, type_annulation FROM animation.code_annulation ORDER BY type_annulation")
|
||||
|
||||
def get_type_dossier_ens(self):
|
||||
return self.fetch_all("SELECT code_dossier_ens, type_dossier_ens FROM animation.type_dossier_ens ORDER BY type_dossier_ens")
|
||||
|
||||
def get_etablissements(self):
|
||||
return self.fetch_all("SELECT code_etablissement, nom_etablissement, commune FROM animation.liste_etablissements ORDER BY nom_etablissement")
|
||||
|
||||
def get_all_animations(self):
|
||||
"""Retourne toutes les animations pour la liste déroulante (date + lieu + thème)."""
|
||||
return self.fetch_all("""
|
||||
SELECT da.code_animation,
|
||||
da.date_anim,
|
||||
da.lieux,
|
||||
ta.theme_animation
|
||||
FROM animation.donnees_animation da
|
||||
LEFT JOIN animation.theme_animation ta ON ta.code_type_anim = da.code_type_anim
|
||||
ORDER BY da.date_anim DESC, da.code_animation DESC
|
||||
""")
|
||||
|
||||
def get_animation_by_id(self, code_animation):
|
||||
rows = self.fetch_all(
|
||||
"SELECT * FROM animation.donnees_animation WHERE code_animation = %s", (code_animation,)
|
||||
)
|
||||
if rows:
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute("SELECT * FROM animation.donnees_animation WHERE code_animation = %s", (code_animation,))
|
||||
cols = [d[0] for d in cur.description]
|
||||
cur.execute("SELECT * FROM animation.donnees_animation WHERE code_animation = %s", (code_animation,))
|
||||
row = cur.fetchone()
|
||||
return dict(zip(cols, row)) if row else None
|
||||
return None
|
||||
|
||||
# --- Insertions ---
|
||||
def add_animateur(self, nom):
|
||||
rows = self.fetch_all("SELECT MAX(code_animateur) FROM animation.animateurs")
|
||||
next_code = (rows[0][0] or 0) + 1
|
||||
self.execute("INSERT INTO animation.animateurs (code_animateur, animateur) VALUES (%s, %s)", (next_code, nom))
|
||||
return next_code, nom
|
||||
|
||||
def add_lieux(self, lieux, type_lieux='', type_lieux_regroupement='', geom_wkt=None, srid=4326):
|
||||
rows = self.fetch_all("SELECT MAX(code_lieux) FROM animation.lieux_anim")
|
||||
next_code = (rows[0][0] or 0) + 1
|
||||
if geom_wkt:
|
||||
self.execute(
|
||||
"INSERT INTO animation.lieux_anim (code_lieux, lieux, type_lieux, type_lieux_regroupement, geom) "
|
||||
"VALUES (%s,%s,%s,%s,ST_SetSRID(ST_GeomFromText(%s),%s))",
|
||||
(next_code, lieux, type_lieux, type_lieux_regroupement, geom_wkt, srid)
|
||||
)
|
||||
else:
|
||||
self.execute(
|
||||
"INSERT INTO animation.lieux_anim (code_lieux, lieux, type_lieux, type_lieux_regroupement) VALUES (%s,%s,%s,%s)",
|
||||
(next_code, lieux, type_lieux, type_lieux_regroupement)
|
||||
)
|
||||
return next_code, lieux
|
||||
|
||||
def add_theme(self, theme, famille=''):
|
||||
rows = self.fetch_all("SELECT MAX(code_type_anim) FROM animation.theme_animation")
|
||||
next_code = (rows[0][0] or 0) + 1
|
||||
self.execute("INSERT INTO animation.theme_animation (code_type_anim, theme_animation, famille_theme_animation) VALUES (%s,%s,%s)",
|
||||
(next_code, theme, famille))
|
||||
return next_code, theme
|
||||
|
||||
def add_annulation(self, type_annulation):
|
||||
rows = self.fetch_all("SELECT MAX(code_annul) FROM animation.code_annulation")
|
||||
next_code = (rows[0][0] or 0) + 1
|
||||
self.execute("INSERT INTO animation.code_annulation (code_annul, type_annulation) VALUES (%s,%s)",
|
||||
(next_code, type_annulation))
|
||||
return next_code, type_annulation
|
||||
|
||||
def add_type_dossier(self, type_dossier):
|
||||
rows = self.fetch_all("SELECT MAX(code_dossier_ens) FROM animation.type_dossier_ens")
|
||||
next_code = (rows[0][0] or 0) + 1
|
||||
self.execute("INSERT INTO animation.type_dossier_ens (code_dossier_ens, type_dossier_ens) VALUES (%s,%s)",
|
||||
(next_code, type_dossier))
|
||||
return next_code, type_dossier
|
||||
|
||||
def add_etablissement(self, nom, commune, prive=False, geom_wkt=None, srid=4326):
|
||||
rows = self.fetch_all("SELECT MAX(code_etablissement) FROM animation.liste_etablissements")
|
||||
next_code = (rows[0][0] or 0) + 1
|
||||
if geom_wkt:
|
||||
self.execute(
|
||||
"INSERT INTO animation.liste_etablissements (code_etablissement, nom_etablissement, prive, commune, geom) "
|
||||
"VALUES (%s,%s,%s,%s,ST_SetSRID(ST_GeomFromText(%s),%s))",
|
||||
(next_code, nom, prive, commune, geom_wkt, srid)
|
||||
)
|
||||
else:
|
||||
self.execute(
|
||||
"INSERT INTO animation.liste_etablissements (code_etablissement, nom_etablissement, prive, commune) VALUES (%s,%s,%s,%s)",
|
||||
(next_code, nom, prive, commune)
|
||||
)
|
||||
return next_code, nom, commune
|
||||
|
||||
def get_next_code_animation(self):
|
||||
rows = self.fetch_all("SELECT MAX(code_animation) FROM animation.donnees_animation")
|
||||
return (rows[0][0] or 0) + 1
|
||||
|
||||
def insert_donnees_animation(self, data: dict):
|
||||
sql = """
|
||||
INSERT INTO animation.donnees_animation (
|
||||
code_animation, date_anim, duree_anim, animateurs, prepa_deplacemt, temps_total,
|
||||
code_groupe, precisions_type_de_groupe, commune_provenance, nom_etablissement,
|
||||
nbre_pers_total, nbre_pers_enfants, nbre_pers_adultes,
|
||||
lieux, precisions_lieux_anim, accueil_pin, theme_detaille,
|
||||
code_type_anim, payant, type_annul, remarques,
|
||||
type_dossier_ens, realise_par_tiers, detail_tiers
|
||||
) VALUES (
|
||||
%(code_animation)s, %(date_anim)s, %(duree_anim)s, %(animateurs)s, %(prepa_deplacemt)s, %(temps_total)s,
|
||||
%(code_groupe)s, %(precisions_type_de_groupe)s, %(commune_provenance)s, %(nom_etablissement)s,
|
||||
%(nbre_pers_total)s, %(nbre_pers_enfants)s, %(nbre_pers_adultes)s,
|
||||
%(lieux)s, %(precisions_lieux_anim)s, %(accueil_pin)s, %(theme_detaille)s,
|
||||
%(code_type_anim)s, %(payant)s, %(type_annul)s, %(remarques)s,
|
||||
%(type_dossier_ens)s, %(realise_par_tiers)s, %(detail_tiers)s
|
||||
)
|
||||
"""
|
||||
self.execute(sql, data)
|
||||
|
||||
def update_donnees_animation(self, code_animation, data: dict):
|
||||
sql = """
|
||||
UPDATE animation.donnees_animation SET
|
||||
date_anim=%(date_anim)s, duree_anim=%(duree_anim)s, animateurs=%(animateurs)s,
|
||||
prepa_deplacemt=%(prepa_deplacemt)s, temps_total=%(temps_total)s,
|
||||
code_groupe=%(code_groupe)s, precisions_type_de_groupe=%(precisions_type_de_groupe)s,
|
||||
commune_provenance=%(commune_provenance)s, nom_etablissement=%(nom_etablissement)s,
|
||||
nbre_pers_total=%(nbre_pers_total)s, nbre_pers_enfants=%(nbre_pers_enfants)s,
|
||||
nbre_pers_adultes=%(nbre_pers_adultes)s,
|
||||
lieux=%(lieux)s, precisions_lieux_anim=%(precisions_lieux_anim)s,
|
||||
accueil_pin=%(accueil_pin)s, theme_detaille=%(theme_detaille)s,
|
||||
code_type_anim=%(code_type_anim)s, payant=%(payant)s, type_annul=%(type_annul)s,
|
||||
remarques=%(remarques)s, type_dossier_ens=%(type_dossier_ens)s,
|
||||
realise_par_tiers=%(realise_par_tiers)s, detail_tiers=%(detail_tiers)s
|
||||
WHERE code_animation=%(code_animation)s
|
||||
"""
|
||||
data['code_animation'] = code_animation
|
||||
self.execute(sql, data)
|
||||
42
dialog_add.py
Normal file
42
dialog_add.py
Normal file
@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from qgis.PyQt.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QFormLayout, QHBoxLayout,
|
||||
QLineEdit, QCheckBox, QPushButton
|
||||
)
|
||||
|
||||
|
||||
class AddEtablissementDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Ajouter un établissement")
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
form = QFormLayout()
|
||||
self.nom_edit = QLineEdit()
|
||||
self.nom_edit.setPlaceholderText("Nom de l'établissement")
|
||||
self.commune_edit = QLineEdit()
|
||||
self.commune_edit.setPlaceholderText("Commune")
|
||||
self.prive_check = QCheckBox("Établissement privé")
|
||||
|
||||
form.addRow("Nom* :", self.nom_edit)
|
||||
form.addRow("Commune :", self.commune_edit)
|
||||
form.addRow("", self.prive_check)
|
||||
layout.addLayout(form)
|
||||
|
||||
btns = QHBoxLayout()
|
||||
ok = QPushButton("Ajouter")
|
||||
cancel = QPushButton("Annuler")
|
||||
btns.addWidget(ok)
|
||||
btns.addWidget(cancel)
|
||||
layout.addLayout(btns)
|
||||
|
||||
ok.clicked.connect(self.accept)
|
||||
cancel.clicked.connect(self.reject)
|
||||
|
||||
def values(self):
|
||||
return (
|
||||
self.nom_edit.text().strip(),
|
||||
self.prive_check.isChecked(),
|
||||
self.commune_edit.text().strip() or None,
|
||||
)
|
||||
257
dialog_auth.py
Normal file
257
dialog_auth.py
Normal file
@ -0,0 +1,257 @@
|
||||
from qgis.PyQt.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QFormLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QMessageBox,
|
||||
QSpinBox,
|
||||
QComboBox,
|
||||
QWidget,
|
||||
)
|
||||
from qgis.PyQt.QtCore import Qt
|
||||
from qgis.core import QgsApplication, QgsAuthMethodConfig, QgsSettings
|
||||
from .db_manager import DBManager
|
||||
|
||||
SETTINGS_SELECTED_CONNECTION = "000"
|
||||
|
||||
|
||||
def try_connect_from_keychain():
|
||||
"""
|
||||
Tente de se connecter via une connexion PostGIS déjà enregistrée dans QGIS
|
||||
(et potentiellement associée à une config d'authentification 'authcfg').
|
||||
Retourne un DBManager connecté ou None.
|
||||
"""
|
||||
try:
|
||||
settings = QgsSettings()
|
||||
connection_name = settings.value(SETTINGS_SELECTED_CONNECTION, "", type=str).strip()
|
||||
if not connection_name:
|
||||
return None
|
||||
|
||||
params = read_postgis_connection(connection_name)
|
||||
if not params:
|
||||
return None
|
||||
|
||||
return connect_from_params(params)
|
||||
except Exception as e:
|
||||
print(f"Keychain connection failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_uri(uri_string):
|
||||
"""Parse QGIS URI format: host=value port=value dbname=value..."""
|
||||
params = {}
|
||||
# Remplacer les espaces de séparation par du split plus robuste
|
||||
parts = uri_string.split()
|
||||
for part in parts:
|
||||
if '=' in part:
|
||||
key, value = part.split('=', 1)
|
||||
params[key.strip()] = value.strip("'\"")
|
||||
return params
|
||||
|
||||
|
||||
def list_postgis_connections():
|
||||
"""Retourne les noms des connexions PostgreSQL enregistrées dans QGIS."""
|
||||
settings = QgsSettings()
|
||||
settings.beginGroup("PostgreSQL/connections")
|
||||
names = settings.childGroups()
|
||||
settings.endGroup()
|
||||
return sorted(names)
|
||||
|
||||
|
||||
def read_postgis_connection(name):
|
||||
"""
|
||||
Lit une connexion PostGIS depuis les settings QGIS.
|
||||
Retourne un dict avec host/port/dbname/user/password/authcfg (password peut être vide).
|
||||
"""
|
||||
settings = QgsSettings()
|
||||
base = f"PostgreSQL/connections/{name}"
|
||||
host = settings.value(f"{base}/host", "", type=str)
|
||||
port = settings.value(f"{base}/port", "5432", type=str)
|
||||
dbname = settings.value(f"{base}/database", "", type=str)
|
||||
user = settings.value(f"{base}/username", "", type=str)
|
||||
password = settings.value(f"{base}/password", "", type=str)
|
||||
authcfg = settings.value(f"{base}/authcfg", "", type=str)
|
||||
|
||||
if not host and not dbname:
|
||||
return None
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"host": host or "localhost",
|
||||
"port": int(port) if str(port).strip() else 5432,
|
||||
"dbname": dbname,
|
||||
"user": user,
|
||||
"password": password,
|
||||
"authcfg": authcfg,
|
||||
}
|
||||
|
||||
|
||||
def load_auth_config(authcfg):
|
||||
"""Charge une config d'auth QGIS et retourne un dict utile."""
|
||||
if not authcfg:
|
||||
return None
|
||||
auth_mgr = QgsApplication.authManager()
|
||||
cfg = QgsAuthMethodConfig()
|
||||
auth_mgr.loadAuthenticationConfig(authcfg, cfg, True)
|
||||
config_map = cfg.configMap() or {}
|
||||
uri = cfg.uri() or ""
|
||||
return {
|
||||
"id": cfg.id(),
|
||||
"name": cfg.name(),
|
||||
"method": cfg.method(),
|
||||
"config_map": config_map,
|
||||
"uri": uri,
|
||||
}
|
||||
|
||||
|
||||
def connect_from_params(params):
|
||||
"""Construit un DBManager à partir des paramètres + authcfg si présent."""
|
||||
authcfg = (params.get("authcfg") or "").strip()
|
||||
user = params.get("user") or ""
|
||||
password = params.get("password") or ""
|
||||
|
||||
if authcfg:
|
||||
auth = load_auth_config(authcfg)
|
||||
if auth:
|
||||
# Pour les configs de type Basic, on trouve souvent username/password dans configMap
|
||||
user = user or auth["config_map"].get("username") or auth["config_map"].get("user") or ""
|
||||
password = password or auth["config_map"].get("password") or ""
|
||||
if not password and auth.get("uri"):
|
||||
uri_params = parse_uri(auth["uri"])
|
||||
user = user or uri_params.get("user", "")
|
||||
password = password or uri_params.get("password", "")
|
||||
|
||||
if not params.get("dbname"):
|
||||
raise ValueError("Base de données manquante dans la connexion PostGIS QGIS.")
|
||||
if not user:
|
||||
raise ValueError("Utilisateur manquant (connexion QGIS + authcfg n'ont pas fourni de user).")
|
||||
if not password:
|
||||
raise ValueError("Mot de passe manquant (connexion QGIS + authcfg n'ont pas fourni de password).")
|
||||
|
||||
return DBManager(
|
||||
host=params.get("host") or "localhost",
|
||||
port=int(params.get("port") or 5432),
|
||||
dbname=params.get("dbname") or "",
|
||||
user=user,
|
||||
password=password,
|
||||
)
|
||||
|
||||
class AuthDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Connexion à la base de données – Animation")
|
||||
self.setMinimumWidth(420)
|
||||
self.db = None
|
||||
self._build_ui()
|
||||
|
||||
def _build_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
title = QLabel("<h2>Animation – Connexion BDD</h2>")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
|
||||
info = QLabel(
|
||||
"<small>Sélectionnez une connexion PostgreSQL déjà enregistrée dans QGIS "
|
||||
"(idéalement avec une configuration d’authentification associée).</small>"
|
||||
)
|
||||
info.setWordWrap(True)
|
||||
layout.addWidget(info)
|
||||
|
||||
form = QFormLayout()
|
||||
self.connection_combo = QComboBox()
|
||||
self.refresh_button = QPushButton("Actualiser")
|
||||
refresh_layout = QHBoxLayout()
|
||||
refresh_layout.addWidget(self.connection_combo, 1)
|
||||
refresh_layout.addWidget(self.refresh_button)
|
||||
refresh_widget = QWidget()
|
||||
refresh_widget.setLayout(refresh_layout)
|
||||
form.addRow("Connexion PostGIS :", refresh_widget)
|
||||
|
||||
self.manual_host_edit = QLineEdit()
|
||||
self.manual_port_spin = QSpinBox()
|
||||
self.manual_port_spin.setRange(1, 65535)
|
||||
self.manual_port_spin.setValue(5432)
|
||||
self.manual_db_edit = QLineEdit()
|
||||
self.manual_user_edit = QLineEdit()
|
||||
self.manual_password_edit = QLineEdit()
|
||||
self.manual_password_edit.setEchoMode(QLineEdit.Password)
|
||||
|
||||
form.addRow(QLabel("<b>Fallback manuel</b> (si la connexion QGIS ne contient pas le secret)"), QLabel(""))
|
||||
form.addRow("Hôte :", self.manual_host_edit)
|
||||
form.addRow("Port :", self.manual_port_spin)
|
||||
form.addRow("Base de données :", self.manual_db_edit)
|
||||
form.addRow("Utilisateur :", self.manual_user_edit)
|
||||
form.addRow("Mot de passe :", self.manual_password_edit)
|
||||
|
||||
layout.addLayout(form)
|
||||
|
||||
btn_layout = QHBoxLayout()
|
||||
self.btn_cancel = QPushButton("Annuler")
|
||||
self.btn_cancel.clicked.connect(self.reject)
|
||||
self.btn_connect = QPushButton("Se connecter")
|
||||
self.btn_connect.setDefault(True)
|
||||
self.btn_connect.clicked.connect(self.connect)
|
||||
btn_layout.addWidget(self.btn_cancel)
|
||||
btn_layout.addWidget(self.btn_connect)
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
self.refresh_button.clicked.connect(self.refresh_connections)
|
||||
self.connection_combo.currentIndexChanged.connect(self._on_connection_changed)
|
||||
self.refresh_connections()
|
||||
|
||||
def connect(self):
|
||||
try:
|
||||
params = self._params_from_selection()
|
||||
if params:
|
||||
self.db = connect_from_params(params)
|
||||
QgsSettings().setValue(SETTINGS_SELECTED_CONNECTION, params.get("name", ""))
|
||||
self.accept()
|
||||
return
|
||||
|
||||
# fallback manuel
|
||||
self.db = DBManager(
|
||||
host=self.manual_host_edit.text().strip() or "localhost",
|
||||
port=self.manual_port_spin.value(),
|
||||
dbname=self.manual_db_edit.text().strip(),
|
||||
user=self.manual_user_edit.text().strip(),
|
||||
password=self.manual_password_edit.text(),
|
||||
)
|
||||
self.accept()
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur de connexion", str(e))
|
||||
|
||||
def refresh_connections(self):
|
||||
self.connection_combo.clear()
|
||||
self.connection_combo.addItem("", "")
|
||||
for name in list_postgis_connections():
|
||||
self.connection_combo.addItem(name, name)
|
||||
|
||||
# Pré-sélection du dernier choix
|
||||
last = QgsSettings().value(SETTINGS_SELECTED_CONNECTION, "", type=str)
|
||||
if last:
|
||||
idx = self.connection_combo.findData(last)
|
||||
if idx >= 0:
|
||||
self.connection_combo.setCurrentIndex(idx)
|
||||
else:
|
||||
self._on_connection_changed()
|
||||
|
||||
def _on_connection_changed(self):
|
||||
params = self._params_from_selection()
|
||||
if not params:
|
||||
return
|
||||
# Pré-remplir le fallback manuel avec les valeurs non sensibles
|
||||
self.manual_host_edit.setText(params.get("host", "") or "")
|
||||
self.manual_port_spin.setValue(int(params.get("port") or 5432))
|
||||
self.manual_db_edit.setText(params.get("dbname", "") or "")
|
||||
self.manual_user_edit.setText(params.get("user", "") or "")
|
||||
self.manual_password_edit.clear()
|
||||
|
||||
def _params_from_selection(self):
|
||||
name = (self.connection_combo.currentData() or "").strip()
|
||||
if not name:
|
||||
return None
|
||||
return read_postgis_connection(name)
|
||||
718
dialog_saisie.py
Normal file
718
dialog_saisie.py
Normal file
@ -0,0 +1,718 @@
|
||||
import json
|
||||
from datetime import date
|
||||
|
||||
from qgis.PyQt.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
|
||||
QLabel, QLineEdit, QPushButton, QMessageBox, QComboBox,
|
||||
QDateEdit, QSpinBox, QCheckBox, QScrollArea, QWidget,
|
||||
QListWidget, QListWidgetItem, QInputDialog, QSizePolicy,
|
||||
QFrame, QGroupBox
|
||||
)
|
||||
from qgis.PyQt.QtCore import Qt, QDate
|
||||
from qgis.PyQt.QtGui import QFont, QColor
|
||||
from qgis.core import QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsProject
|
||||
from qgis.utils import iface
|
||||
|
||||
from .map_point_tool import PointCaptureTool
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Widgets utilitaires
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ComboWithAdd(QWidget):
|
||||
def __init__(self, add_callback, parent=None):
|
||||
super().__init__(parent)
|
||||
self._add_callback = add_callback
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.combo = QComboBox()
|
||||
self.combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.btn_add = QPushButton("+")
|
||||
self.btn_add.setFixedWidth(30)
|
||||
self.btn_add.setToolTip("Ajouter un nouvel élément")
|
||||
self.btn_add.clicked.connect(lambda: self._add_callback(self))
|
||||
layout.addWidget(self.combo)
|
||||
layout.addWidget(self.btn_add)
|
||||
|
||||
def current_data(self):
|
||||
return self.combo.currentData()
|
||||
|
||||
def current_text(self):
|
||||
return self.combo.currentText()
|
||||
|
||||
def populate(self, items, placeholder=None):
|
||||
self.combo.clear()
|
||||
if placeholder:
|
||||
self.combo.addItem(placeholder, None)
|
||||
for code, label in items:
|
||||
self.combo.addItem(label, code)
|
||||
|
||||
def add_item(self, code, label):
|
||||
self.combo.addItem(label, code)
|
||||
self.combo.setCurrentIndex(self.combo.count() - 1)
|
||||
|
||||
def set_by_text(self, text):
|
||||
idx = self.combo.findText(text)
|
||||
if idx >= 0:
|
||||
self.combo.setCurrentIndex(idx)
|
||||
|
||||
def set_by_data(self, data):
|
||||
idx = self.combo.findData(data)
|
||||
if idx >= 0:
|
||||
self.combo.setCurrentIndex(idx)
|
||||
|
||||
|
||||
class AnimateursWidget(QWidget):
|
||||
def __init__(self, db, parent=None):
|
||||
super().__init__(parent)
|
||||
self.db = db
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.list_widget = QListWidget()
|
||||
self.list_widget.setMaximumHeight(90)
|
||||
layout.addWidget(self.list_widget)
|
||||
|
||||
btn_layout = QHBoxLayout()
|
||||
self.combo = QComboBox()
|
||||
self.combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
btn_add = QPushButton("Ajouter")
|
||||
btn_add.clicked.connect(self._add_existing)
|
||||
btn_new = QPushButton("+ Nouveau")
|
||||
btn_new.clicked.connect(self._add_new)
|
||||
btn_remove = QPushButton("Retirer")
|
||||
btn_remove.clicked.connect(self._remove_selected)
|
||||
btn_layout.addWidget(self.combo)
|
||||
btn_layout.addWidget(btn_add)
|
||||
btn_layout.addWidget(btn_new)
|
||||
btn_layout.addWidget(btn_remove)
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
self.load_animateurs()
|
||||
|
||||
def load_animateurs(self):
|
||||
self.combo.clear()
|
||||
for code, nom in self.db.get_animateurs():
|
||||
self.combo.addItem(nom, code)
|
||||
|
||||
def _add_existing(self):
|
||||
code = self.combo.currentData()
|
||||
nom = self.combo.currentText()
|
||||
if code is None:
|
||||
return
|
||||
for i in range(self.list_widget.count()):
|
||||
if self.list_widget.item(i).data(Qt.UserRole) == code:
|
||||
return
|
||||
item = QListWidgetItem(nom)
|
||||
item.setData(Qt.UserRole, code)
|
||||
self.list_widget.addItem(item)
|
||||
|
||||
def _add_new(self):
|
||||
nom, ok = QInputDialog.getText(self, "Nouvel animateur", "Nom :")
|
||||
if ok and nom.strip():
|
||||
try:
|
||||
code, label = self.db.add_animateur(nom.strip())
|
||||
self.combo.addItem(label, code)
|
||||
item = QListWidgetItem(label)
|
||||
item.setData(Qt.UserRole, code)
|
||||
self.list_widget.addItem(item)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
|
||||
def _remove_selected(self):
|
||||
for item in self.list_widget.selectedItems():
|
||||
self.list_widget.takeItem(self.list_widget.row(item))
|
||||
|
||||
def get_value(self):
|
||||
result = []
|
||||
for i in range(self.list_widget.count()):
|
||||
item = self.list_widget.item(i)
|
||||
result.append({"code": item.data(Qt.UserRole), "nom": item.text()})
|
||||
return json.dumps(result, ensure_ascii=False) if result else None
|
||||
|
||||
def set_value(self, json_str):
|
||||
self.list_widget.clear()
|
||||
if not json_str:
|
||||
return
|
||||
try:
|
||||
data = json.loads(json_str) if isinstance(json_str, str) else json_str
|
||||
for entry in data:
|
||||
item = QListWidgetItem(entry.get("nom", ""))
|
||||
item.setData(Qt.UserRole, entry.get("code"))
|
||||
self.list_widget.addItem(item)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dialogue principal
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class SaisieDialog(QDialog):
|
||||
def __init__(self, db, parent=None):
|
||||
super().__init__(parent)
|
||||
self.db = db
|
||||
self._etab_map = {} # code -> (nom, commune)
|
||||
self._edit_mode = False # True = on édite une animation existante
|
||||
self._edit_code = None # code_animation en cours d'édition
|
||||
self._pending_geom = None # (x, y, crs) en attente pour ajout lieu/etab
|
||||
self._pending_geom_type = None # 'lieu' ou 'etab'
|
||||
self._point_tool = None
|
||||
|
||||
self.setWindowTitle("Animation – Saisie / Édition")
|
||||
self.setMinimumWidth(720)
|
||||
self.setMinimumHeight(820)
|
||||
self._build_ui()
|
||||
self._load_reference_data()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Construction UI
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _build_ui(self):
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setSpacing(6)
|
||||
|
||||
# --- Bandeau titre ---
|
||||
self.title_label = QLabel("<h2>Nouvelle animation</h2>")
|
||||
self.title_label.setAlignment(Qt.AlignCenter)
|
||||
main_layout.addWidget(self.title_label)
|
||||
|
||||
# --- Sélecteur d'animation existante ---
|
||||
grp_sel = QGroupBox("Charger / modifier une animation existante")
|
||||
sel_layout = QHBoxLayout(grp_sel)
|
||||
self.anim_combo = QComboBox()
|
||||
self.anim_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.anim_combo.setPlaceholderText("── Nouvelle saisie ──")
|
||||
self.anim_combo.addItem("── Nouvelle saisie ──", None)
|
||||
self.anim_combo.currentIndexChanged.connect(self._on_anim_selected)
|
||||
sel_layout.addWidget(QLabel("Animation :"))
|
||||
sel_layout.addWidget(self.anim_combo)
|
||||
btn_refresh = QPushButton("🔄")
|
||||
btn_refresh.setFixedWidth(32)
|
||||
btn_refresh.setToolTip("Rafraîchir la liste")
|
||||
btn_refresh.clicked.connect(self._reload_animations_list)
|
||||
sel_layout.addWidget(btn_refresh)
|
||||
main_layout.addWidget(grp_sel)
|
||||
|
||||
# --- Zone scrollable du formulaire ---
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
container = QWidget()
|
||||
self.fl = QFormLayout(container)
|
||||
self.fl.setLabelAlignment(Qt.AlignRight)
|
||||
self.fl.setSpacing(8)
|
||||
scroll.setWidget(container)
|
||||
main_layout.addWidget(scroll)
|
||||
|
||||
fl = self.fl
|
||||
|
||||
# ---- Date & Durée ----
|
||||
self._section(fl, "Date et durée")
|
||||
|
||||
self.date_edit = QDateEdit(QDate.currentDate())
|
||||
self.date_edit.setCalendarPopup(True)
|
||||
self.date_edit.setDisplayFormat("dd/MM/yyyy")
|
||||
fl.addRow("Date *:", self.date_edit)
|
||||
|
||||
dur_w = QWidget()
|
||||
dur_l = QHBoxLayout(dur_w)
|
||||
dur_l.setContentsMargins(0, 0, 0, 0)
|
||||
self.duree_h = QSpinBox()
|
||||
self.duree_h.setRange(0, 24)
|
||||
self.duree_h.setSuffix(" h")
|
||||
self.duree_min = QSpinBox()
|
||||
self.duree_min.setRange(0, 59)
|
||||
self.duree_min.setSuffix(" min")
|
||||
dur_l.addWidget(self.duree_h)
|
||||
dur_l.addWidget(self.duree_min)
|
||||
dur_l.addStretch()
|
||||
fl.addRow("Durée :", dur_w)
|
||||
|
||||
self.prepa_spin = QSpinBox()
|
||||
self.prepa_spin.setRange(0, 9999)
|
||||
self.prepa_spin.setSuffix(" min")
|
||||
for w in (self.duree_h, self.duree_min, self.prepa_spin):
|
||||
w.valueChanged.connect(self._update_temps_total)
|
||||
fl.addRow("Prépa / déplacement :", self.prepa_spin)
|
||||
|
||||
self.temps_total_label = QLabel("0 min")
|
||||
fl.addRow("Temps total (calculé) :", self.temps_total_label)
|
||||
|
||||
# ---- Animateurs ----
|
||||
self._sep(fl)
|
||||
self._section(fl, "Animateurs")
|
||||
self.animateurs_widget = AnimateursWidget(self.db)
|
||||
fl.addRow("Animateurs :", self.animateurs_widget)
|
||||
|
||||
# ---- Groupe ----
|
||||
self._sep(fl)
|
||||
self._section(fl, "Groupe")
|
||||
self.type_groupe_combo = ComboWithAdd(self._add_type_groupe)
|
||||
fl.addRow("Type de groupe :", self.type_groupe_combo)
|
||||
self.precisions_groupe_edit = QLineEdit()
|
||||
fl.addRow("Précisions groupe :", self.precisions_groupe_edit)
|
||||
|
||||
# ---- Établissement ----
|
||||
self._sep(fl)
|
||||
self._section(fl, "Établissement / Provenance")
|
||||
self.etab_combo = ComboWithAdd(self._add_etablissement)
|
||||
self.etab_combo.combo.currentIndexChanged.connect(self._on_etab_changed)
|
||||
fl.addRow("Établissement :", self.etab_combo)
|
||||
self.commune_label = QLabel("")
|
||||
self.commune_label.setStyleSheet("color:#555; font-style:italic;")
|
||||
fl.addRow("Commune (auto) :", self.commune_label)
|
||||
|
||||
# ---- Participants ----
|
||||
self._sep(fl)
|
||||
self._section(fl, "Participants")
|
||||
self.enfants_spin = QSpinBox()
|
||||
self.enfants_spin.setRange(0, 9999)
|
||||
self.enfants_spin.valueChanged.connect(self._update_total_pers)
|
||||
fl.addRow("Enfants :", self.enfants_spin)
|
||||
self.adultes_spin = QSpinBox()
|
||||
self.adultes_spin.setRange(0, 9999)
|
||||
self.adultes_spin.valueChanged.connect(self._update_total_pers)
|
||||
fl.addRow("Adultes :", self.adultes_spin)
|
||||
self.total_pers_label = QLabel("0")
|
||||
fl.addRow("Total (calculé) :", self.total_pers_label)
|
||||
|
||||
# ---- Lieu ----
|
||||
self._sep(fl)
|
||||
self._section(fl, "Lieu")
|
||||
self.lieux_combo = ComboWithAdd(self._add_lieu)
|
||||
fl.addRow("Lieu :", self.lieux_combo)
|
||||
self.precisions_lieux_edit = QLineEdit()
|
||||
fl.addRow("Précisions lieu :", self.precisions_lieux_edit)
|
||||
self.accueil_pin_check = QCheckBox("Oui")
|
||||
fl.addRow("Accueil PIN :", self.accueil_pin_check)
|
||||
|
||||
# ---- Thème ----
|
||||
self._sep(fl)
|
||||
self._section(fl, "Thème")
|
||||
self.theme_combo = ComboWithAdd(self._add_theme)
|
||||
fl.addRow("Thème :", self.theme_combo)
|
||||
self.theme_detaille_edit = QLineEdit()
|
||||
fl.addRow("Thème détaillé :", self.theme_detaille_edit)
|
||||
|
||||
# ---- Statuts ----
|
||||
self._sep(fl)
|
||||
self._section(fl, "Statuts")
|
||||
self.payant_check = QCheckBox("Oui")
|
||||
fl.addRow("Payant :", self.payant_check)
|
||||
self.annul_combo = ComboWithAdd(self._add_annulation)
|
||||
fl.addRow("Annulation :", self.annul_combo)
|
||||
self.dossier_combo = ComboWithAdd(self._add_dossier)
|
||||
fl.addRow("Dossier enseignant :", self.dossier_combo)
|
||||
self.tiers_check = QCheckBox("Oui")
|
||||
self.tiers_check.stateChanged.connect(self._on_tiers_changed)
|
||||
fl.addRow("Réalisé par tiers :", self.tiers_check)
|
||||
self.detail_tiers_edit = QLineEdit()
|
||||
self.detail_tiers_edit.setEnabled(False)
|
||||
fl.addRow("Détail tiers :", self.detail_tiers_edit)
|
||||
|
||||
# ---- Remarques ----
|
||||
self._sep(fl)
|
||||
self.remarques_edit = QLineEdit()
|
||||
fl.addRow("Remarques :", self.remarques_edit)
|
||||
|
||||
# --- Boutons d'action ---
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_cancel = QPushButton("Fermer")
|
||||
btn_cancel.clicked.connect(self.reject)
|
||||
|
||||
self.btn_new_form = QPushButton("✚ Nouveau")
|
||||
self.btn_new_form.setToolTip("Réinitialiser pour une nouvelle saisie")
|
||||
self.btn_new_form.clicked.connect(self._new_form)
|
||||
|
||||
self.btn_save_new = QPushButton("Enregistrer et nouveau")
|
||||
self.btn_save_new.clicked.connect(self._save_and_new)
|
||||
|
||||
self.btn_save = QPushButton("Enregistrer")
|
||||
self.btn_save.setDefault(True)
|
||||
self.btn_save.clicked.connect(self._save)
|
||||
|
||||
btn_layout.addWidget(btn_cancel)
|
||||
btn_layout.addWidget(self.btn_new_form)
|
||||
btn_layout.addStretch()
|
||||
btn_layout.addWidget(self.btn_save_new)
|
||||
btn_layout.addWidget(self.btn_save)
|
||||
main_layout.addLayout(btn_layout)
|
||||
|
||||
def _section(self, fl, title):
|
||||
lbl = QLabel(f"<b>{title}</b>")
|
||||
fl.addRow(lbl)
|
||||
|
||||
def _sep(self, fl):
|
||||
line = QFrame()
|
||||
line.setFrameShape(QFrame.HLine)
|
||||
line.setStyleSheet("color:#ccc;")
|
||||
fl.addRow(line)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Chargement données de référence
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _load_reference_data(self):
|
||||
try:
|
||||
self.type_groupe_combo.populate(self.db.get_types_groupe(), "-- Sélectionner --")
|
||||
etabs = self.db.get_etablissements()
|
||||
self._etab_map = {c: (n, com) for c, n, com in etabs}
|
||||
self.etab_combo.populate(
|
||||
[(c, f"{n} ({com})" if com else n) for c, n, com in etabs],
|
||||
"-- Sélectionner --"
|
||||
)
|
||||
self.lieux_combo.populate(self.db.get_lieux(), "-- Sélectionner --")
|
||||
self.theme_combo.populate(self.db.get_themes(), "-- Sélectionner --")
|
||||
self.annul_combo.populate(self.db.get_codes_annulation(), "Non")
|
||||
self._set_default(self.annul_combo, "Non")
|
||||
self.dossier_combo.populate(self.db.get_type_dossier_ens(), "Non")
|
||||
self._set_default(self.dossier_combo, "Non")
|
||||
self._reload_animations_list()
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur chargement", str(e))
|
||||
|
||||
def _set_default(self, combo_widget, text):
|
||||
idx = combo_widget.combo.findText(text)
|
||||
if idx >= 0:
|
||||
combo_widget.combo.setCurrentIndex(idx)
|
||||
|
||||
def _reload_animations_list(self):
|
||||
self.anim_combo.blockSignals(True)
|
||||
self.anim_combo.clear()
|
||||
self.anim_combo.addItem("── Nouvelle saisie ──", None)
|
||||
try:
|
||||
rows = self.db.get_all_animations()
|
||||
for code, date_anim, lieux, theme in rows:
|
||||
date_str = date_anim.strftime("%d/%m/%Y") if date_anim else "?"
|
||||
lieux_str = lieux or "Lieu ?"
|
||||
theme_str = theme or "Thème ?"
|
||||
label = f"{date_str} | {lieux_str} | {theme_str} [#{code}]"
|
||||
self.anim_combo.addItem(label, code)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Erreur", f"Impossible de charger les animations : {e}")
|
||||
self.anim_combo.blockSignals(False)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Événements
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _on_anim_selected(self, idx):
|
||||
code = self.anim_combo.itemData(idx)
|
||||
if code is None:
|
||||
self._new_form()
|
||||
return
|
||||
try:
|
||||
data = self.db.get_animation_by_id(code)
|
||||
if data:
|
||||
self._edit_mode = True
|
||||
self._edit_code = code
|
||||
self.title_label.setText(f"<h2>Édition animation #{code}</h2>")
|
||||
self.btn_save.setText("Mettre à jour")
|
||||
self.btn_save_new.setVisible(False)
|
||||
self._populate_form(data)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
|
||||
def _populate_form(self, data):
|
||||
"""Remplit le formulaire avec les données d'une animation existante."""
|
||||
if data.get('date_anim'):
|
||||
d = data['date_anim']
|
||||
self.date_edit.setDate(QDate(d.year, d.month, d.day))
|
||||
|
||||
duree = data.get('duree_anim', '0h00') or '0h00'
|
||||
try:
|
||||
if 'h' in duree:
|
||||
h, m = duree.split('h')
|
||||
self.duree_h.setValue(int(h))
|
||||
self.duree_min.setValue(int(m) if m else 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.prepa_spin.setValue(data.get('prepa_deplacemt') or 0)
|
||||
self.animateurs_widget.set_value(data.get('animateurs'))
|
||||
self.type_groupe_combo.set_by_data(data.get('code_groupe'))
|
||||
self.precisions_groupe_edit.setText(data.get('precisions_type_de_groupe') or '')
|
||||
|
||||
# Établissement : chercher par nom
|
||||
nom_etab = data.get('nom_etablissement') or ''
|
||||
for code, (nom, com) in self._etab_map.items():
|
||||
if nom == nom_etab:
|
||||
self.etab_combo.set_by_data(code)
|
||||
break
|
||||
|
||||
self.enfants_spin.setValue(data.get('nbre_pers_enfants') or 0)
|
||||
self.adultes_spin.setValue(data.get('nbre_pers_adultes') or 0)
|
||||
self.lieux_combo.set_by_text(data.get('lieux') or '')
|
||||
self.precisions_lieux_edit.setText(data.get('precisions_lieux_anim') or '')
|
||||
self.accueil_pin_check.setChecked(bool(data.get('accueil_pin')))
|
||||
self.theme_detaille_edit.setText(data.get('theme_detaille') or '')
|
||||
self.theme_combo.set_by_data(data.get('code_type_anim'))
|
||||
self.payant_check.setChecked(bool(data.get('payant')))
|
||||
self.annul_combo.set_by_text(data.get('type_annul') or 'Non')
|
||||
self.remarques_edit.setText(data.get('remarques') or '')
|
||||
self.dossier_combo.set_by_text(data.get('type_dossier_ens') or 'Non')
|
||||
realise = bool(data.get('realise_par_tiers'))
|
||||
self.tiers_check.setChecked(realise)
|
||||
self.detail_tiers_edit.setText(data.get('detail_tiers') or '')
|
||||
self.detail_tiers_edit.setEnabled(realise)
|
||||
|
||||
def _new_form(self):
|
||||
self._edit_mode = False
|
||||
self._edit_code = None
|
||||
self.title_label.setText("<h2>Nouvelle animation</h2>")
|
||||
self.btn_save.setText("Enregistrer")
|
||||
self.btn_save_new.setVisible(True)
|
||||
self.anim_combo.blockSignals(True)
|
||||
self.anim_combo.setCurrentIndex(0)
|
||||
self.anim_combo.blockSignals(False)
|
||||
self._reset_form()
|
||||
|
||||
def _on_etab_changed(self, idx):
|
||||
code = self.etab_combo.combo.itemData(idx)
|
||||
if code and code in self._etab_map:
|
||||
self.commune_label.setText(self._etab_map[code][1] or "")
|
||||
else:
|
||||
self.commune_label.setText("")
|
||||
|
||||
def _on_tiers_changed(self, state):
|
||||
self.detail_tiers_edit.setEnabled(state == Qt.Checked)
|
||||
|
||||
def _update_temps_total(self):
|
||||
total = self.duree_h.value() * 60 + self.duree_min.value() + self.prepa_spin.value()
|
||||
self.temps_total_label.setText(f"{total} min")
|
||||
|
||||
def _update_total_pers(self):
|
||||
self.total_pers_label.setText(str(self.enfants_spin.value() + self.adultes_spin.value()))
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Capture géométrie sur carte
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _start_point_capture(self, geom_type, on_captured_callback):
|
||||
"""
|
||||
Lance l'outil de clic sur la carte. geom_type = 'lieu' ou 'etab'.
|
||||
Après le clic, appelle on_captured_callback(wkt, srid).
|
||||
"""
|
||||
canvas = iface.mapCanvas()
|
||||
if not canvas:
|
||||
QMessageBox.warning(self, "Erreur", "Impossible d'accéder à la carte QGIS.")
|
||||
return
|
||||
|
||||
prev_tool = canvas.mapTool()
|
||||
tool = PointCaptureTool(canvas, prev_tool)
|
||||
|
||||
def on_point(x, y, crs):
|
||||
# Convertir en WGS84 (4326) si nécessaire
|
||||
wgs84 = QgsCoordinateReferenceSystem("EPSG:4326")
|
||||
if crs != wgs84:
|
||||
transform = QgsCoordinateTransform(crs, wgs84, QgsProject.instance())
|
||||
from qgis.core import QgsPointXY
|
||||
pt = transform.transform(x, y)
|
||||
x, y = pt.x(), pt.y()
|
||||
wkt = f"POINT({x} {y})"
|
||||
on_captured_callback(wkt, 4326)
|
||||
self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
|
||||
self.activateWindow()
|
||||
|
||||
tool.pointCaptured.connect(on_point)
|
||||
self._point_tool = tool
|
||||
|
||||
# Minimiser le dialogue et activer l'outil
|
||||
self.showMinimized()
|
||||
canvas.setMapTool(tool)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Callbacks ajout nouveaux éléments
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _add_type_groupe(self, widget):
|
||||
QMessageBox.information(self, "Info",
|
||||
"L'ajout d'un type de groupe nécessite un code entier.\n"
|
||||
"Veuillez contacter l'administrateur de la base.")
|
||||
|
||||
def _add_lieu(self, widget):
|
||||
val, ok = QInputDialog.getText(self, "Nouveau lieu", "Nom du lieu :")
|
||||
if not ok or not val.strip():
|
||||
return
|
||||
nom = val.strip()
|
||||
|
||||
reply = QMessageBox.question(self, "Géométrie",
|
||||
"Souhaitez-vous localiser ce lieu sur la carte ?",
|
||||
QMessageBox.Yes | QMessageBox.No)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
def on_geom(wkt, srid):
|
||||
try:
|
||||
code, label = self.db.add_lieux(nom, geom_wkt=wkt, srid=srid)
|
||||
widget.add_item(code, label)
|
||||
QMessageBox.information(self, "Succès", f"Lieu « {label} » ajouté avec géométrie.")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
self._start_point_capture('lieu', on_geom)
|
||||
else:
|
||||
try:
|
||||
code, label = self.db.add_lieux(nom)
|
||||
widget.add_item(code, label)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
|
||||
def _add_theme(self, widget):
|
||||
val, ok = QInputDialog.getText(self, "Nouveau thème", "Libellé :")
|
||||
if ok and val.strip():
|
||||
try:
|
||||
code, label = self.db.add_theme(val.strip())
|
||||
widget.add_item(code, label)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
|
||||
def _add_annulation(self, widget):
|
||||
val, ok = QInputDialog.getText(self, "Nouveau type d'annulation", "Libellé :")
|
||||
if ok and val.strip():
|
||||
try:
|
||||
code, label = self.db.add_annulation(val.strip())
|
||||
widget.add_item(code, label)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
|
||||
def _add_dossier(self, widget):
|
||||
val, ok = QInputDialog.getText(self, "Nouveau type de dossier", "Libellé :")
|
||||
if ok and val.strip():
|
||||
try:
|
||||
code, label = self.db.add_type_dossier(val.strip())
|
||||
widget.add_item(code, label)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
|
||||
def _add_etablissement(self, widget):
|
||||
nom, ok = QInputDialog.getText(self, "Nouvel établissement", "Nom :")
|
||||
if not ok or not nom.strip():
|
||||
return
|
||||
commune, ok2 = QInputDialog.getText(self, "Nouvel établissement", "Commune :")
|
||||
if not ok2:
|
||||
return
|
||||
nom = nom.strip()
|
||||
commune = commune.strip()
|
||||
|
||||
reply = QMessageBox.question(self, "Géométrie",
|
||||
"Souhaitez-vous localiser cet établissement sur la carte ?",
|
||||
QMessageBox.Yes | QMessageBox.No)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
def on_geom(wkt, srid):
|
||||
try:
|
||||
code, label, com = self.db.add_etablissement(nom, commune, geom_wkt=wkt, srid=srid)
|
||||
self._etab_map[code] = (label, com)
|
||||
display = f"{label} ({com})" if com else label
|
||||
widget.add_item(code, display)
|
||||
QMessageBox.information(self, "Succès", f"Établissement « {label} » ajouté avec géométrie.")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
self._start_point_capture('etab', on_geom)
|
||||
else:
|
||||
try:
|
||||
code, label, com = self.db.add_etablissement(nom, commune)
|
||||
self._etab_map[code] = (label, com)
|
||||
display = f"{label} ({com})" if com else label
|
||||
widget.add_item(code, display)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Collecte, validation, enregistrement
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _collect_data(self):
|
||||
duree_str = f"{self.duree_h.value()}h{self.duree_min.value():02d}"
|
||||
duree_min_total = self.duree_h.value() * 60 + self.duree_min.value()
|
||||
temps_total = duree_min_total + self.prepa_spin.value() or None
|
||||
|
||||
etab_code = self.etab_combo.current_data()
|
||||
nom_etab, commune_prov = (None, None)
|
||||
if etab_code and etab_code in self._etab_map:
|
||||
nom_etab, commune_prov = self._etab_map[etab_code]
|
||||
|
||||
lieux_txt = self.lieux_combo.current_text() if self.lieux_combo.current_data() else None
|
||||
type_annul = self.annul_combo.current_text() if self.annul_combo.current_data() else None
|
||||
type_dossier = self.dossier_combo.current_text() if self.dossier_combo.current_data() else None
|
||||
realise_tiers = self.tiers_check.isChecked()
|
||||
|
||||
return {
|
||||
"date_anim": self.date_edit.date().toPyDate(),
|
||||
"duree_anim": duree_str,
|
||||
"animateurs": self.animateurs_widget.get_value(),
|
||||
"prepa_deplacemt": self.prepa_spin.value() or None,
|
||||
"temps_total": temps_total,
|
||||
"code_groupe": self.type_groupe_combo.current_data(),
|
||||
"precisions_type_de_groupe": self.precisions_groupe_edit.text().strip() or None,
|
||||
"commune_provenance": commune_prov,
|
||||
"nom_etablissement": nom_etab,
|
||||
"nbre_pers_enfants": self.enfants_spin.value() or None,
|
||||
"nbre_pers_adultes": self.adultes_spin.value() or None,
|
||||
"nbre_pers_total": (self.enfants_spin.value() + self.adultes_spin.value()) or None,
|
||||
"lieux": lieux_txt,
|
||||
"precisions_lieux_anim": self.precisions_lieux_edit.text().strip() or None,
|
||||
"accueil_pin": self.accueil_pin_check.isChecked(),
|
||||
"theme_detaille": self.theme_detaille_edit.text().strip() or None,
|
||||
"code_type_anim": self.theme_combo.current_data(),
|
||||
"payant": self.payant_check.isChecked(),
|
||||
"type_annul": type_annul,
|
||||
"remarques": self.remarques_edit.text().strip() or None,
|
||||
"type_dossier_ens": type_dossier,
|
||||
"realise_par_tiers": realise_tiers,
|
||||
"detail_tiers": self.detail_tiers_edit.text().strip() if realise_tiers else None,
|
||||
}
|
||||
|
||||
def _reset_form(self):
|
||||
self.date_edit.setDate(QDate.currentDate())
|
||||
self.duree_h.setValue(0)
|
||||
self.duree_min.setValue(0)
|
||||
self.prepa_spin.setValue(0)
|
||||
self.animateurs_widget.list_widget.clear()
|
||||
self.type_groupe_combo.combo.setCurrentIndex(0)
|
||||
self.precisions_groupe_edit.clear()
|
||||
self.etab_combo.combo.setCurrentIndex(0)
|
||||
self.commune_label.setText("")
|
||||
self.enfants_spin.setValue(0)
|
||||
self.adultes_spin.setValue(0)
|
||||
self.lieux_combo.combo.setCurrentIndex(0)
|
||||
self.precisions_lieux_edit.clear()
|
||||
self.accueil_pin_check.setChecked(False)
|
||||
self.theme_combo.combo.setCurrentIndex(0)
|
||||
self.theme_detaille_edit.clear()
|
||||
self.payant_check.setChecked(False)
|
||||
self.annul_combo.combo.setCurrentIndex(0)
|
||||
self.remarques_edit.clear()
|
||||
self.dossier_combo.combo.setCurrentIndex(0)
|
||||
self.tiers_check.setChecked(False)
|
||||
self.detail_tiers_edit.clear()
|
||||
|
||||
def _save(self):
|
||||
try:
|
||||
data = self._collect_data()
|
||||
if self._edit_mode:
|
||||
self.db.update_donnees_animation(self._edit_code, data)
|
||||
QMessageBox.information(self, "Succès", f"Animation #{self._edit_code} mise à jour.")
|
||||
self._reload_animations_list()
|
||||
else:
|
||||
code = self.db.get_next_code_animation()
|
||||
data['code_animation'] = code
|
||||
self.db.insert_donnees_animation(data)
|
||||
QMessageBox.information(self, "Succès", f"Animation #{code} enregistrée.")
|
||||
self._reload_animations_list()
|
||||
self.accept()
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
|
||||
def _save_and_new(self):
|
||||
try:
|
||||
data = self._collect_data()
|
||||
code = self.db.get_next_code_animation()
|
||||
data['code_animation'] = code
|
||||
self.db.insert_donnees_animation(data)
|
||||
QMessageBox.information(self, "Succès", f"Animation #{code} enregistrée.")
|
||||
self._reload_animations_list()
|
||||
self._new_form()
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erreur", str(e))
|
||||
32
main_plugin.py
Normal file
32
main_plugin.py
Normal file
@ -0,0 +1,32 @@
|
||||
from qgis.PyQt.QtWidgets import QAction, QMessageBox, QDialog
|
||||
from .dialog_auth import AuthDialog, try_connect_from_keychain
|
||||
from .dialog_saisie import SaisieDialog
|
||||
|
||||
|
||||
class AnimationPlugin:
|
||||
def __init__(self, iface):
|
||||
self.iface = iface
|
||||
self.action = None
|
||||
|
||||
def initGui(self):
|
||||
self.action = QAction("Saisie Animation", self.iface.mainWindow())
|
||||
self.action.triggered.connect(self.run)
|
||||
self.iface.addToolBarIcon(self.action)
|
||||
self.iface.addPluginToMenu("Animation", self.action)
|
||||
|
||||
def unload(self):
|
||||
self.iface.removePluginMenu("Animation", self.action)
|
||||
self.iface.removeToolBarIcon(self.action)
|
||||
|
||||
def run(self):
|
||||
# Tenter la connexion automatique via le coffre-fort QGIS
|
||||
db = try_connect_from_keychain()
|
||||
if db:
|
||||
dlg = SaisieDialog(db, self.iface.mainWindow())
|
||||
dlg.exec_()
|
||||
else:
|
||||
# Fallback : sélection d'une connexion QGIS (ou saisie manuelle)
|
||||
dlg = AuthDialog(self.iface.mainWindow())
|
||||
if dlg.exec_() == QDialog.Accepted and dlg.db:
|
||||
saisie = SaisieDialog(dlg.db, self.iface.mainWindow())
|
||||
saisie.exec_()
|
||||
37
map_point_tool.py
Normal file
37
map_point_tool.py
Normal file
@ -0,0 +1,37 @@
|
||||
from qgis.gui import QgsMapTool
|
||||
from qgis.PyQt.QtCore import pyqtSignal
|
||||
from qgis.PyQt.QtGui import QCursor
|
||||
from qgis.PyQt.QtCore import Qt
|
||||
|
||||
|
||||
class PointCaptureTool(QgsMapTool):
|
||||
"""
|
||||
Outil de capture d'un point sur la carte QGIS.
|
||||
Émet pointCaptured(x, y, crs) au clic gauche.
|
||||
Se désactive automatiquement après un clic.
|
||||
"""
|
||||
pointCaptured = pyqtSignal(float, float, object)
|
||||
|
||||
def __init__(self, canvas, previous_tool=None):
|
||||
super().__init__(canvas)
|
||||
self.canvas = canvas
|
||||
self.previous_tool = previous_tool
|
||||
self.setCursor(QCursor(Qt.CrossCursor))
|
||||
|
||||
def canvasReleaseEvent(self, event):
|
||||
if event.button() == Qt.LeftButton:
|
||||
point = self.toMapCoordinates(event.pos())
|
||||
crs = self.canvas.mapSettings().destinationCrs()
|
||||
self.pointCaptured.emit(point.x(), point.y(), crs)
|
||||
# Restaurer l'outil précédent
|
||||
if self.previous_tool:
|
||||
self.canvas.setMapTool(self.previous_tool)
|
||||
else:
|
||||
self.canvas.unsetMapTool(self)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == Qt.Key_Escape:
|
||||
if self.previous_tool:
|
||||
self.canvas.setMapTool(self.previous_tool)
|
||||
else:
|
||||
self.canvas.unsetMapTool(self)
|
||||
8
metadata.txt
Normal file
8
metadata.txt
Normal file
@ -0,0 +1,8 @@
|
||||
[general]
|
||||
name=Animation Saisie
|
||||
qgisMinimumVersion=3.0
|
||||
description=Plugin de saisie des données d'animation
|
||||
version=1.0
|
||||
author=Animation
|
||||
email=contact@animation.fr
|
||||
about=Plugin permettant la saisie des données dans la table animation.donnees_animation
|
||||
Loading…
x
Reference in New Issue
Block a user