ruslanmv commited on
Commit
3ecb46e
·
1 Parent(s): 08f13c5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +202 -5
app.py CHANGED
@@ -1,10 +1,207 @@
1
- from flask import Flask
 
 
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  app = Flask(__name__)
4
 
5
- @app.route('/')
6
- def hello_world():
7
- return "Hello, World from Hugging Face Space!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
 
9
  if __name__ == '__main__':
10
- app.run(host='0.0.0.0', debug=True)
 
1
+ from __future__ import annotations
2
+ import os
3
+ import logging
4
 
5
+ # ----------------------------------------------------------------
6
+ # Suppress Colab's debugpy shape‐inspection errors by patching get_shape
7
+ try:
8
+ import google.colab._debugpy_repr as _debugpy_repr
9
+ _debugpy_repr.get_shape = lambda obj: None
10
+ logging.getLogger().setLevel(logging.WARNING)
11
+ except ImportError:
12
+ pass
13
+ # ----------------------------------------------------------------
14
+
15
+ import pandas as pd
16
+ from flask import Flask, jsonify, request, render_template_string
17
+ from unidecode import unidecode
18
+ from pyngrok import ngrok, conf
19
+ from typing import Dict, List, Optional
20
+
21
+ # --- Data layer ---
22
+ CSV_PATH = "equiparazioni.csv"
23
+ if os.path.exists(CSV_PATH):
24
+ MAP = pd.read_csv(CSV_PATH)
25
+ else:
26
+ MAP = pd.DataFrame([
27
+ {"col_1_dl": "Ingegneria informatica", "col_2_ls": "35/S – Ingegneria informatica", "col_3_lm": "LM-32 – Ingegneria informatica"},
28
+ {"col_1_dl": "Scienze dell’economia", "col_2_ls": "64/S – Scienze dell’economia", "col_3_lm": "LM-56 – Scienze dell’economia"},
29
+ ])
30
+
31
+ # --- Logic layer ---
32
+ def _norm(txt: str | None) -> str:
33
+ if txt is None or pd.isna(txt):
34
+ return ""
35
+ return "".join(c for c in unidecode(str(txt)).lower() if c.isalnum())
36
+
37
+ def find_triplet(title: str) -> Optional[Dict[str, str]]:
38
+ key = _norm(title)
39
+ if not key:
40
+ return None
41
+ for _, row in MAP.iterrows():
42
+ if key in {_norm(row.col_1_dl), _norm(row.col_2_ls), _norm(row.col_3_lm)}:
43
+ return {k: v if pd.notna(v) else "" for k, v in row.to_dict().items()}
44
+ return None
45
+
46
+ def satisfies_once(candidate_title: str, required_title: str) -> bool:
47
+ cand_triplet = find_triplet(candidate_title)
48
+ req_triplet = find_triplet(required_title)
49
+ if cand_triplet is None or req_triplet is None:
50
+ return False
51
+ return cand_triplet == req_triplet
52
+
53
+ def satisfies(candidate_title: str, required_titles: List[str]) -> bool:
54
+ if not required_titles:
55
+ return True
56
+ return any(satisfies_once(candidate_title, r) for r in required_titles)
57
+
58
+ # --- Web layer ---
59
  app = Flask(__name__)
60
 
61
+ @app.route("/titles")
62
+ def all_titles():
63
+ titles = (
64
+ MAP["col_1_dl"].dropna().tolist() +
65
+ MAP["col_2_ls"].dropna().tolist() +
66
+ MAP["col_3_lm"].dropna().tolist()
67
+ )
68
+ return jsonify(sorted(set(titles)))
69
+
70
+ @app.route("/check", methods=["POST"])
71
+ def check():
72
+ data = request.get_json(force=True)
73
+ candidate = data.get("candidate", "")
74
+ required = data.get("required", [])
75
+ approved = satisfies(candidate, required)
76
+ mapping = find_triplet(candidate) or {}
77
+ return jsonify({"approved": approved, "mapping": mapping})
78
+
79
+ HTML_PAGE = """
80
+ <!doctype html>
81
+ <html lang="it">
82
+ <head>
83
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
84
+ <title>Validatore Titoli – HR</title>
85
+ <style>
86
+ :root { --main-bg: #fff; --text-color: #333; --border-color: #ccc; --badge-bg: #eef; --green: #4CAF50; --red: #F44336; }
87
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; margin:2em; background:var(--main-bg); color:var(--text-color); line-height:1.6; }
88
+ h1,h3 { color:#0056b3; }
89
+ .grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(250px,1fr)); gap:24px; }
90
+ select,button { width:100%; padding:10px; border-radius:5px; border:1px solid var(--border-color); font-size:1rem; }
91
+ select { height:250px; }
92
+ button { background:#007bff; color:#fff; cursor:pointer; border:none; margin-top:8px; transition:background .2s; }
93
+ button:hover { background:#0056b3; }
94
+ #requiredList { min-height:100px; border:1px solid var(--border-color); padding:8px; overflow-y:auto; border-radius:5px; background:#f9f9f9; }
95
+ .badge { display:inline-flex; align-items:center; padding:5px 10px; margin:4px; background:var(--badge-bg); border-radius:15px; font-size:.9em; cursor:pointer; }
96
+ .badge:hover::after { content:' ❌'; color:var(--red); }
97
+ .result { padding:15px; text-align:center; font-weight:bold; color:#fff; border-radius:5px; transition:background .3s; }
98
+ .approved { background:var(--green); }
99
+ .rejected { background:var(--red); }
100
+ #mapping { font-size:.9em; border:1px solid #ddd; padding:12px; border-radius:5px; background:#f9f9f9; }
101
+ #mapping strong { color:#0056b3; }
102
+ </style>
103
+ </head>
104
+ <body>
105
+ <h1>Validatore Titoli di Studio</h1>
106
+ <div class="grid">
107
+ <div>
108
+ <h3>1. Titolo del Candidato</h3>
109
+ <select id="candidateSelect" size="15"></select>
110
+ </div>
111
+ <div>
112
+ <h3>2. Requisiti del Bando</h3>
113
+ <select id="requiredSelect" size="10"></select>
114
+ <button id="addBtn">Aggiungi Requisito ➜</button>
115
+ </div>
116
+ <div>
117
+ <h3>Titoli Richiesti (OR)</h3>
118
+ <div id="requiredList"></div>
119
+ <h3 style="margin-top:20px;">Mapping del Candidato</h3>
120
+ <div id="mapping">(selezionare un candidato)</div>
121
+ </div>
122
+ <div>
123
+ <h3>4. Esito</h3>
124
+ <div id="resultBox" class="result">...</div>
125
+ </div>
126
+ </div>
127
+ <script>
128
+ let allTitles = [], requiredTitles = new Set();
129
+ const candidateSelect = document.getElementById('candidateSelect'),
130
+ requiredSelect = document.getElementById('requiredSelect'),
131
+ addBtn = document.getElementById('addBtn'),
132
+ requiredListDiv = document.getElementById('requiredList'),
133
+ resultBox = document.getElementById('resultBox'),
134
+ mappingDiv = document.getElementById('mapping');
135
+
136
+ async function populateTitles() {
137
+ const r = await fetch('/titles');
138
+ allTitles = await r.json();
139
+ const opts = allTitles.map(t => `<option value="${t}">${t}</option>`).join('');
140
+ candidateSelect.innerHTML = opts;
141
+ requiredSelect.innerHTML = opts;
142
+ candidateSelect.onchange = runCheck;
143
+ addBtn.onclick = () => {
144
+ const val = requiredSelect.value;
145
+ if (val && !requiredTitles.has(val)) {
146
+ requiredTitles.add(val);
147
+ renderRequiredList();
148
+ runCheck();
149
+ }
150
+ };
151
+ }
152
+
153
+ function renderRequiredList() {
154
+ requiredListDiv.innerHTML = '';
155
+ requiredTitles.forEach(title => {
156
+ const span = document.createElement('span');
157
+ span.textContent = title;
158
+ span.className = 'badge';
159
+ span.onclick = () => {
160
+ requiredTitles.delete(title);
161
+ renderRequiredList();
162
+ runCheck();
163
+ };
164
+ requiredListDiv.appendChild(span);
165
+ });
166
+ }
167
+
168
+ async function runCheck() {
169
+ const cand = candidateSelect.value;
170
+ if (!cand) return;
171
+ const res = await (await fetch('/check', {
172
+ method: 'POST',
173
+ headers: {'Content-Type':'application/json'},
174
+ body: JSON.stringify({ candidate: cand, required: Array.from(requiredTitles) })
175
+ })).json();
176
+
177
+ if (res.mapping && Object.keys(res.mapping).length) {
178
+ mappingDiv.innerHTML =
179
+ `<strong>Vecchio Ord. (DL):</strong> ${res.mapping.col_1_dl || 'N/A'}<br>` +
180
+ `<strong>Specialistica (LS):</strong> ${res.mapping.col_2_ls || 'N/A'}<br>` +
181
+ `<strong>Magistrale (LM):</strong> ${res.mapping.col_3_lm || 'N/A'}`;
182
+ } else {
183
+ mappingDiv.textContent = 'Titolo non riconosciuto.';
184
+ }
185
+
186
+ if (!requiredTitles.size) {
187
+ resultBox.className = 'result';
188
+ resultBox.textContent = 'Aggiungere requisiti';
189
+ } else {
190
+ resultBox.className = res.approved ? 'result approved' : 'result rejected';
191
+ resultBox.textContent = res.approved ? 'APPROVATO' : 'NON APPROVATO';
192
+ }
193
+ }
194
+
195
+ populateTitles();
196
+ </script>
197
+ </body>
198
+ </html>
199
+ """
200
+
201
+ @app.route("/")
202
+ def index():
203
+ return render_template_string(HTML_PAGE)
204
 
205
+ # --- App Runner ---
206
  if __name__ == '__main__':
207
+ app.run(host='0.0.0.0', debug=True)