Add page Tour de garde SecOps : import xlsx + table + vue hebdo + competences
This commit is contained in:
parent
803016458d
commit
5fedfb5f80
@ -12,7 +12,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
|||||||
from .config import APP_NAME, APP_VERSION
|
from .config import APP_NAME, APP_VERSION
|
||||||
from .dependencies import get_current_user, get_user_perms
|
from .dependencies import get_current_user, get_user_perms
|
||||||
from .database import SessionLocal, SessionLocalDemo
|
from .database import SessionLocal, SessionLocalDemo
|
||||||
from .routers import auth, dashboard, servers, settings, users, campaigns, planning, specifics, audit, contacts, qualys, qualys_tags, quickwin, referentiel, patching, applications, patch_history
|
from .routers import auth, dashboard, servers, settings, users, campaigns, planning, specifics, audit, contacts, qualys, qualys_tags, quickwin, referentiel, patching, applications, patch_history, duty
|
||||||
|
|
||||||
|
|
||||||
class PermissionsMiddleware(BaseHTTPMiddleware):
|
class PermissionsMiddleware(BaseHTTPMiddleware):
|
||||||
@ -71,6 +71,7 @@ app.include_router(quickwin.router)
|
|||||||
app.include_router(referentiel.router)
|
app.include_router(referentiel.router)
|
||||||
app.include_router(patching.router)
|
app.include_router(patching.router)
|
||||||
app.include_router(patch_history.router)
|
app.include_router(patch_history.router)
|
||||||
|
app.include_router(duty.router)
|
||||||
app.include_router(applications.router)
|
app.include_router(applications.router)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
57
app/routers/duty.py
Normal file
57
app/routers/duty.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"""Router Tour de garde SecOps"""
|
||||||
|
from fastapi import APIRouter, Request, Depends, Query
|
||||||
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from sqlalchemy import text
|
||||||
|
from ..dependencies import get_db, get_current_user
|
||||||
|
from ..config import APP_NAME
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
templates = Jinja2Templates(directory="app/templates")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/duty", response_class=HTMLResponse)
|
||||||
|
async def duty_page(request: Request, db=Depends(get_db),
|
||||||
|
year: str = Query("")):
|
||||||
|
user = get_current_user(request)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login")
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
year = int(year) if year and year.isdigit() else datetime.now().year
|
||||||
|
current_week = datetime.now().isocalendar()[1]
|
||||||
|
current_year = datetime.now().year
|
||||||
|
|
||||||
|
rows = db.execute(text("""
|
||||||
|
SELECT * FROM secops_duty
|
||||||
|
WHERE year = :y
|
||||||
|
ORDER BY week_number
|
||||||
|
"""), {"y": year}).fetchall()
|
||||||
|
|
||||||
|
years = db.execute(text("""
|
||||||
|
SELECT DISTINCT year FROM secops_duty ORDER BY year DESC
|
||||||
|
""")).fetchall()
|
||||||
|
|
||||||
|
# Competences (hardcoded pour l'instant, pourra etre en DB plus tard)
|
||||||
|
competences = [
|
||||||
|
{"nom": "Ayoub", "s1": True, "commvault": False, "m365": True, "symantec": True},
|
||||||
|
{"nom": "Mouaad", "s1": True, "commvault": False, "m365": True, "symantec": True},
|
||||||
|
{"nom": "Khalid", "s1": True, "commvault": False, "m365": True, "symantec": True},
|
||||||
|
{"nom": "Thierno", "s1": True, "commvault": True, "m365": True, "symantec": True},
|
||||||
|
{"nom": "Paul", "s1": True, "commvault": True, "m365": True, "symantec": True},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Stats : qui a le plus de gardes
|
||||||
|
stats = {}
|
||||||
|
for r in rows:
|
||||||
|
for field in ["tdg_s1", "tdg_symantec", "tdg_m365", "tdg_dmz"]:
|
||||||
|
name = getattr(r, field, None)
|
||||||
|
if name:
|
||||||
|
stats[name] = stats.get(name, 0) + 1
|
||||||
|
|
||||||
|
return templates.TemplateResponse("duty.html", {
|
||||||
|
"request": request, "user": user, "app_name": APP_NAME,
|
||||||
|
"rows": rows, "year": year, "years": [y.year for y in years],
|
||||||
|
"current_week": current_week, "current_year": current_year,
|
||||||
|
"competences": competences, "stats": stats,
|
||||||
|
})
|
||||||
@ -90,6 +90,7 @@
|
|||||||
{% if p.servers in ('edit','admin') or p.campaigns in ('edit','admin') or p.quickwin in ('edit','admin') %}<a href="/patching/config-exclusions" class="block px-3 py-1.5 rounded-md text-xs hover:bg-cyber-border/30 {% if 'config-exclusions' in path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6">Config exclusions</a>{% endif %}
|
{% if p.servers in ('edit','admin') or p.campaigns in ('edit','admin') or p.quickwin in ('edit','admin') %}<a href="/patching/config-exclusions" class="block px-3 py-1.5 rounded-md text-xs hover:bg-cyber-border/30 {% if 'config-exclusions' in path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6">Config exclusions</a>{% endif %}
|
||||||
{% if p.campaigns in ('edit','admin') or p.quickwin in ('edit','admin') %}<a href="/patching/validations" class="block px-3 py-1.5 rounded-md text-xs hover:bg-cyber-border/30 {% if '/patching/validations' in path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6">Validations</a>{% endif %}
|
{% if p.campaigns in ('edit','admin') or p.quickwin in ('edit','admin') %}<a href="/patching/validations" class="block px-3 py-1.5 rounded-md text-xs hover:bg-cyber-border/30 {% if '/patching/validations' in path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6">Validations</a>{% endif %}
|
||||||
<a href="/patching/historique" class="block px-3 py-1.5 rounded-md text-xs hover:bg-cyber-border/30 {% if '/patching/historique' in path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6">Historique</a>
|
<a href="/patching/historique" class="block px-3 py-1.5 rounded-md text-xs hover:bg-cyber-border/30 {% if '/patching/historique' in path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6">Historique</a>
|
||||||
|
<a href="/duty" class="block px-3 py-1.5 rounded-md text-xs hover:bg-cyber-border/30 {% if '/duty' in path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6">Tour de garde</a>
|
||||||
|
|
||||||
{# Quickwin sous-groupe #}
|
{# Quickwin sous-groupe #}
|
||||||
{% if p.campaigns or p.quickwin %}
|
{% if p.campaigns or p.quickwin %}
|
||||||
|
|||||||
125
app/templates/duty.html
Normal file
125
app/templates/duty.html
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}Tour de garde SecOps{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold text-cyber-accent">Tour de garde SecOps</h2>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Planning hebdomadaire des astreintes et responsabilites.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{% for y in years %}<a href="?year={{ y }}" class="btn-sm {% if y == year %}bg-cyber-accent text-black{% else %}bg-cyber-border text-gray-300{% endif %} px-3 py-1 text-xs">{{ y }}</a>{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Semaine en cours -->
|
||||||
|
{% for r in rows %}
|
||||||
|
{% if r.week_number == current_week and year == current_year %}
|
||||||
|
<div class="card p-4 mb-4" style="border-left:4px solid #00ff88;">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<h3 class="text-sm font-bold text-cyber-green">Cette semaine — {{ r.week_code }}</h3>
|
||||||
|
<span class="text-xs text-gray-500">{{ r.week_start.strftime('%d/%m') if r.week_start else '' }} → {{ r.week_end.strftime('%d/%m/%Y') if r.week_end else '' }}</span>
|
||||||
|
</div>
|
||||||
|
{% if r.absences %}<p class="text-xs text-cyber-yellow mb-2">Absences : {{ r.absences }}</p>{% endif %}
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:8px;">
|
||||||
|
<div class="card p-2 text-center" style="flex:1;min-width:0;background:#111827">
|
||||||
|
<div class="text-sm font-bold text-cyan-400">{{ r.tdg_s1 or '-' }}</div>
|
||||||
|
<div style="font-size:10px" class="text-gray-500">Sentinel One</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-2 text-center" style="flex:1;min-width:0;background:#111827">
|
||||||
|
<div class="text-sm font-bold text-purple-400">{{ r.tdg_symantec or '-' }}</div>
|
||||||
|
<div style="font-size:10px" class="text-gray-500">Symantec</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-2 text-center" style="flex:1;min-width:0;background:#111827">
|
||||||
|
<div class="text-sm font-bold text-blue-400">{{ r.tdg_m365 or '-' }}</div>
|
||||||
|
<div style="font-size:10px" class="text-gray-500">M365</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-2 text-center" style="flex:1;min-width:0;background:#111827">
|
||||||
|
<div class="text-sm font-bold text-green-400">{{ r.tdg_commvault or '-' }}</div>
|
||||||
|
<div style="font-size:10px" class="text-gray-500">Commvault</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-2 text-center" style="flex:1;min-width:0;background:#111827">
|
||||||
|
<div class="text-sm font-bold text-red-400">{{ r.tdg_dmz or '-' }}</div>
|
||||||
|
<div style="font-size:10px" class="text-gray-500">DMZ</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-2 text-center" style="flex:1;min-width:0;background:#111827">
|
||||||
|
<div class="text-sm font-bold text-yellow-400">{{ r.tdg_meteo or '-' }}</div>
|
||||||
|
<div style="font-size:10px" class="text-gray-500">Meteo</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-2 text-center" style="flex:1;min-width:0;background:#111827">
|
||||||
|
<div class="text-sm font-bold text-orange-400">{{ r.tdg_incident_majeur or '-' }}</div>
|
||||||
|
<div style="font-size:10px" class="text-gray-500">Incident Maj.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Competences -->
|
||||||
|
<div class="card p-4 mb-4">
|
||||||
|
<h3 class="text-sm font-bold text-cyber-accent mb-3">Matrice competences</h3>
|
||||||
|
<table class="w-full table-cyber text-xs">
|
||||||
|
<thead><tr>
|
||||||
|
<th class="p-2 text-left">Nom</th>
|
||||||
|
<th class="p-2 text-center">S1</th>
|
||||||
|
<th class="p-2 text-center">Commvault</th>
|
||||||
|
<th class="p-2 text-center">M365</th>
|
||||||
|
<th class="p-2 text-center">Symantec</th>
|
||||||
|
<th class="p-2 text-center">Gardes {{ year }}</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for c in competences %}
|
||||||
|
<tr class="border-t border-cyber-border/30">
|
||||||
|
<td class="p-2 font-bold text-cyber-accent">{{ c.nom }}</td>
|
||||||
|
<td class="p-2 text-center">{% if c.s1 %}<span class="text-cyber-green">✓</span>{% else %}<span class="text-gray-600">—</span>{% endif %}</td>
|
||||||
|
<td class="p-2 text-center">{% if c.commvault %}<span class="text-cyber-green">✓</span>{% else %}<span class="text-gray-600">—</span>{% endif %}</td>
|
||||||
|
<td class="p-2 text-center">{% if c.m365 %}<span class="text-cyber-green">✓</span>{% else %}<span class="text-gray-600">—</span>{% endif %}</td>
|
||||||
|
<td class="p-2 text-center">{% if c.symantec %}<span class="text-cyber-green">✓</span>{% else %}<span class="text-gray-600">—</span>{% endif %}</td>
|
||||||
|
<td class="p-2 text-center text-cyber-accent font-bold">{{ stats.get(c.nom, 0) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tableau complet -->
|
||||||
|
<div class="card overflow-x-auto">
|
||||||
|
<table class="w-full table-cyber text-xs">
|
||||||
|
<thead><tr>
|
||||||
|
<th class="p-2 text-center" style="min-width:40px">Sem.</th>
|
||||||
|
<th class="p-2 text-left" style="min-width:100px">Dates</th>
|
||||||
|
<th class="p-2 text-left" style="min-width:120px">Absences</th>
|
||||||
|
<th class="p-2 text-center">S1</th>
|
||||||
|
<th class="p-2 text-center">Symantec</th>
|
||||||
|
<th class="p-2 text-center">M365</th>
|
||||||
|
<th class="p-2 text-center">Commvault</th>
|
||||||
|
<th class="p-2 text-center">Meteo</th>
|
||||||
|
<th class="p-2 text-center">DMZ</th>
|
||||||
|
<th class="p-2 text-center">SafeNet</th>
|
||||||
|
<th class="p-2 text-center">Quarant.</th>
|
||||||
|
<th class="p-2 text-center">Inc. Maj.</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for r in rows %}
|
||||||
|
<tr class="border-t border-cyber-border/30 {% if r.week_number == current_week and year == current_year %}bg-cyber-accent/10{% endif %}" {% if r.week_number == current_week and year == current_year %}id="current"{% endif %}>
|
||||||
|
<td class="p-2 text-center font-bold {% if r.week_number == current_week and year == current_year %}text-cyber-green{% else %}text-gray-400{% endif %}">{{ r.week_code }}</td>
|
||||||
|
<td class="p-2 text-gray-400" style="font-size:10px">{{ r.week_start.strftime('%d/%m') if r.week_start else '' }} → {{ r.week_end.strftime('%d/%m') if r.week_end else '' }}</td>
|
||||||
|
<td class="p-2 text-cyber-yellow" style="font-size:10px;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="{{ r.absences or '' }}">{{ r.absences or '' }}</td>
|
||||||
|
<td class="p-2 text-center text-cyan-400">{{ r.tdg_s1 or '-' }}</td>
|
||||||
|
<td class="p-2 text-center text-purple-400">{{ r.tdg_symantec or '-' }}</td>
|
||||||
|
<td class="p-2 text-center text-blue-400">{{ r.tdg_m365 or '-' }}</td>
|
||||||
|
<td class="p-2 text-center text-green-400">{{ r.tdg_commvault or '-' }}</td>
|
||||||
|
<td class="p-2 text-center text-yellow-400">{{ r.tdg_meteo or '-' }}</td>
|
||||||
|
<td class="p-2 text-center text-red-400">{{ r.tdg_dmz or '-' }}</td>
|
||||||
|
<td class="p-2 text-center text-gray-300">{{ r.tdg_safenet or '-' }}</td>
|
||||||
|
<td class="p-2 text-center text-gray-300">{{ r.tdg_quarantaine or '-' }}</td>
|
||||||
|
<td class="p-2 text-center text-orange-400">{{ r.tdg_incident_majeur or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('current')?.scrollIntoView({behavior:'smooth', block:'center'});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
BIN
deploy/Tour de garde secops_2026.xlsx
Normal file
BIN
deploy/Tour de garde secops_2026.xlsx
Normal file
Binary file not shown.
32
deploy/migrations/2026-04-18_secops_duty.sql
Normal file
32
deploy/migrations/2026-04-18_secops_duty.sql
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
-- Migration 2026-04-18 : table tour de garde SecOps
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS secops_duty (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
year smallint NOT NULL,
|
||||||
|
week_number smallint NOT NULL,
|
||||||
|
week_code varchar(5) NOT NULL,
|
||||||
|
week_start date,
|
||||||
|
week_end date,
|
||||||
|
absences text,
|
||||||
|
tdg_s1 varchar(50),
|
||||||
|
tdg_symantec varchar(50),
|
||||||
|
tdg_m365 varchar(50),
|
||||||
|
tdg_commvault varchar(50),
|
||||||
|
tdg_meteo varchar(50),
|
||||||
|
tdg_dmz varchar(50),
|
||||||
|
tdg_safenet varchar(50),
|
||||||
|
tdg_quarantaine varchar(50),
|
||||||
|
tdg_securisation varchar(50),
|
||||||
|
tdg_incident_majeur varchar(50),
|
||||||
|
tdg_incident_critique varchar(50),
|
||||||
|
emails_dest varchar(100),
|
||||||
|
created_at timestamptz DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS secops_duty_year_week_uniq ON secops_duty (year, week_number);
|
||||||
|
CREATE INDEX IF NOT EXISTS secops_duty_week_idx ON secops_duty (year, week_number);
|
||||||
|
|
||||||
|
COMMENT ON TABLE secops_duty IS 'Tour de garde SecOps hebdomadaire (source: Tour de garde secops_2026.xlsx)';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
159
tools/import_tour_de_garde_xlsx.py
Normal file
159
tools/import_tour_de_garde_xlsx.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
"""Import tour de garde SecOps depuis Tour de garde secops_2026.xlsx.
|
||||||
|
|
||||||
|
Lit la feuille 'Tour de garde', UPSERT dans secops_duty.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python tools/import_tour_de_garde_xlsx.py [xlsx] [--truncate]
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import glob
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
|
import openpyxl
|
||||||
|
from sqlalchemy import create_engine, text
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
DATABASE_URL = (os.getenv("DATABASE_URL_DEMO")
|
||||||
|
or os.getenv("DATABASE_URL")
|
||||||
|
or "postgresql://patchcenter:PatchCenter2026!@localhost:5432/patchcenter_db")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dates(c_val):
|
||||||
|
if not c_val:
|
||||||
|
return None, None
|
||||||
|
s = str(c_val).strip()
|
||||||
|
m = re.search(r"(\d{2})/(\d{2})/(\d{4}).*?(\d{2})/(\d{2})/(\d{4})", s)
|
||||||
|
if m:
|
||||||
|
d1 = date(int(m.group(3)), int(m.group(2)), int(m.group(1)))
|
||||||
|
d2 = date(int(m.group(6)), int(m.group(5)), int(m.group(4)))
|
||||||
|
return d1, d2
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def s(val):
|
||||||
|
if val is None:
|
||||||
|
return None
|
||||||
|
t = str(val).strip()
|
||||||
|
return t or None
|
||||||
|
|
||||||
|
|
||||||
|
def find_xlsx():
|
||||||
|
for p in [
|
||||||
|
ROOT / "deploy" / "Tour de garde secops_2026.xlsx",
|
||||||
|
ROOT / "deploy" / "Tour_de_garde_secops_2026.xlsx",
|
||||||
|
]:
|
||||||
|
if p.exists():
|
||||||
|
return str(p)
|
||||||
|
hits = glob.glob(str(ROOT / "deploy" / "*our*garde*.xlsx"))
|
||||||
|
return hits[0] if hits else None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tour_de_garde(xlsx_path):
|
||||||
|
wb = openpyxl.load_workbook(xlsx_path, data_only=True)
|
||||||
|
ws_name = next((n for n in wb.sheetnames if "garde" in n.lower()), None)
|
||||||
|
if not ws_name:
|
||||||
|
raise SystemExit(f"[ERR] Sheet 'Tour de garde' introuvable. Sheets: {wb.sheetnames}")
|
||||||
|
ws = wb[ws_name]
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for i, row in enumerate(ws.iter_rows(values_only=True)):
|
||||||
|
if i == 0:
|
||||||
|
continue
|
||||||
|
week_code = row[0]
|
||||||
|
if not week_code or not str(week_code).strip().startswith("S"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
wc = str(week_code).strip()
|
||||||
|
m = re.match(r"S(\d+)", wc)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
week_num = int(m.group(1))
|
||||||
|
|
||||||
|
d1, d2 = parse_dates(row[2])
|
||||||
|
year = d1.year if d1 and d1.month > 6 else (d2.year if d2 else 2026)
|
||||||
|
if d1 and d1.month == 12 and week_num <= 1:
|
||||||
|
year = d2.year if d2 else d1.year + 1
|
||||||
|
|
||||||
|
rows.append({
|
||||||
|
"year": year,
|
||||||
|
"week_number": week_num,
|
||||||
|
"week_code": wc,
|
||||||
|
"week_start": d1,
|
||||||
|
"week_end": d2,
|
||||||
|
"absences": s(row[1]),
|
||||||
|
"tdg_s1": s(row[3]),
|
||||||
|
"tdg_symantec": s(row[4]),
|
||||||
|
"tdg_m365": s(row[5]),
|
||||||
|
"emails_dest": s(row[6]),
|
||||||
|
"tdg_commvault": s(row[7]),
|
||||||
|
"tdg_meteo": s(row[8]),
|
||||||
|
"tdg_dmz": s(row[11]) if len(row) > 11 else None,
|
||||||
|
"tdg_safenet": s(row[12]) if len(row) > 12 else None,
|
||||||
|
"tdg_quarantaine": s(row[13]) if len(row) > 13 else None,
|
||||||
|
"tdg_securisation": s(row[14]) if len(row) > 14 else None,
|
||||||
|
"tdg_incident_majeur": s(row[16]) if len(row) > 16 else None,
|
||||||
|
"tdg_incident_critique": s(row[17]) if len(row) > 17 else None,
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
SQL_UPSERT = text("""
|
||||||
|
INSERT INTO secops_duty
|
||||||
|
(year, week_number, week_code, week_start, week_end, absences,
|
||||||
|
tdg_s1, tdg_symantec, tdg_m365, tdg_commvault, tdg_meteo,
|
||||||
|
tdg_dmz, tdg_safenet, tdg_quarantaine, tdg_securisation,
|
||||||
|
tdg_incident_majeur, tdg_incident_critique, emails_dest)
|
||||||
|
VALUES
|
||||||
|
(:year, :week_number, :week_code, :week_start, :week_end, :absences,
|
||||||
|
:tdg_s1, :tdg_symantec, :tdg_m365, :tdg_commvault, :tdg_meteo,
|
||||||
|
:tdg_dmz, :tdg_safenet, :tdg_quarantaine, :tdg_securisation,
|
||||||
|
:tdg_incident_majeur, :tdg_incident_critique, :emails_dest)
|
||||||
|
ON CONFLICT (year, week_number) DO UPDATE SET
|
||||||
|
week_code = EXCLUDED.week_code,
|
||||||
|
week_start = EXCLUDED.week_start,
|
||||||
|
week_end = EXCLUDED.week_end,
|
||||||
|
absences = EXCLUDED.absences,
|
||||||
|
tdg_s1 = EXCLUDED.tdg_s1,
|
||||||
|
tdg_symantec = EXCLUDED.tdg_symantec,
|
||||||
|
tdg_m365 = EXCLUDED.tdg_m365,
|
||||||
|
tdg_commvault = EXCLUDED.tdg_commvault,
|
||||||
|
tdg_meteo = EXCLUDED.tdg_meteo,
|
||||||
|
tdg_dmz = EXCLUDED.tdg_dmz,
|
||||||
|
tdg_safenet = EXCLUDED.tdg_safenet,
|
||||||
|
tdg_quarantaine = EXCLUDED.tdg_quarantaine,
|
||||||
|
tdg_securisation = EXCLUDED.tdg_securisation,
|
||||||
|
tdg_incident_majeur = EXCLUDED.tdg_incident_majeur,
|
||||||
|
tdg_incident_critique = EXCLUDED.tdg_incident_critique,
|
||||||
|
emails_dest = EXCLUDED.emails_dest
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
xlsx = sys.argv[1] if len(sys.argv) > 1 else find_xlsx()
|
||||||
|
if not xlsx or not os.path.exists(xlsx):
|
||||||
|
print("[ERR] Fichier Tour de garde introuvable. Place-le dans deploy/")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"[INFO] Fichier: {xlsx}")
|
||||||
|
rows = parse_tour_de_garde(xlsx)
|
||||||
|
print(f"[INFO] Semaines parsees: {len(rows)}")
|
||||||
|
|
||||||
|
engine = create_engine(DATABASE_URL)
|
||||||
|
print(f"[INFO] DB: {DATABASE_URL.rsplit('@', 1)[-1]}")
|
||||||
|
|
||||||
|
truncate = "--truncate" in sys.argv
|
||||||
|
with engine.begin() as conn:
|
||||||
|
if truncate:
|
||||||
|
conn.execute(text("TRUNCATE TABLE secops_duty RESTART IDENTITY"))
|
||||||
|
print("[INFO] TRUNCATE secops_duty")
|
||||||
|
for r in rows:
|
||||||
|
conn.execute(SQL_UPSERT, r)
|
||||||
|
|
||||||
|
print(f"[OK] UPSERT: {len(rows)} semaines")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user