SmartHeal commited on
Commit
0165d1a
·
verified ·
1 Parent(s): 16d2a6f

Update src/database.py

Browse files
Files changed (1) hide show
  1. src/database.py +567 -262
src/database.py CHANGED
@@ -1,22 +1,90 @@
1
- import mysql.connector
2
- from mysql.connector import Error
 
 
3
  import logging
4
  from datetime import datetime
5
- import json
6
- import uuid
7
- import os
 
8
  from PIL import Image
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  class DatabaseManager:
11
- """Database operations manager for SmartHeal application (aligned to existing schema)."""
12
-
13
- def __init__(self, mysql_config):
 
 
 
 
 
14
  self.mysql_config = mysql_config
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  self.test_connection()
16
  self._ensure_default_questionnaire()
17
 
18
- # ---------------------- Connection helpers ----------------------
19
-
20
  def test_connection(self):
21
  try:
22
  conn = self.get_connection()
@@ -48,7 +116,7 @@ class DatabaseManager:
48
  conn.commit()
49
  return cur.rowcount
50
  except Error as e:
51
- logging.error(f"Error executing query: {e} | SQL: {query} | Params: {params}")
52
  if conn:
53
  conn.rollback()
54
  return None
@@ -66,14 +134,13 @@ class DatabaseManager:
66
  cur.execute(query, params or ())
67
  return cur.fetchone()
68
  except Error as e:
69
- logging.error(f"Error executing query: {e} | SQL: {query} | Params: {params}")
70
  return None
71
  finally:
72
  if cur: cur.close()
73
  if conn and conn.is_connected(): conn.close()
74
 
75
  # ---------------------- One-time ensures ----------------------
76
-
77
  def _ensure_default_questionnaire(self):
78
  """Ensure a 'Default Patient Assessment' row exists in questionnaires."""
79
  try:
@@ -83,310 +150,583 @@ class DatabaseManager:
83
  )
84
  if not row:
85
  self.execute_query(
86
- "INSERT INTO questionnaires (name, description, created_at, updated_at) VALUES (%s, %s, NOW(), NOW())",
 
87
  ("Default Patient Assessment", "Standard patient wound assessment form")
88
  )
89
  logging.info("Created default questionnaire 'Default Patient Assessment'")
90
  except Exception as e:
91
  logging.error(f"Error ensuring default questionnaire: {e}")
92
 
93
- # ---------------------- Business ops ----------------------
 
 
 
 
 
94
 
95
- def save_questionnaire(self, questionnaire_data):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  """
97
- Creates/gets patient in EXISTING `patients` table, then creates a row in `questionnaire_responses`.
98
- Returns the `questionnaire_response_id` (int) or None.
99
  """
100
  conn = None
101
  cur = None
102
  try:
103
  conn = self.get_connection()
104
- if not conn:
105
- return None
106
  cur = conn.cursor(dictionary=True)
107
 
108
- # 1) Create or get patient (EXISTING `patients` table)
109
- patient_id = self._create_or_get_patient(cur, questionnaire_data)
110
- if not patient_id:
111
- raise Exception("Failed to get or create patient")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- # 2) Get template questionnaire id
114
- cur.execute("SELECT id FROM questionnaires WHERE name = %s LIMIT 1",
115
- ("Default Patient Assessment",))
116
- row = cur.fetchone()
117
- questionnaire_id = row["id"] if row else None
118
- if not questionnaire_id:
119
- raise Exception("Default questionnaire not found")
120
 
121
- # 3) Build response_data
122
  response_data = {
123
- 'patient_info': {
124
- 'name': questionnaire_data.get('patient_name'),
125
- 'age': questionnaire_data.get('patient_age'),
126
- 'gender': questionnaire_data.get('patient_gender')
 
 
 
 
 
 
 
 
127
  },
128
- 'wound_details': {
129
- 'location': questionnaire_data.get('wound_location'),
130
- 'duration': questionnaire_data.get('wound_duration'),
131
- 'pain_level': questionnaire_data.get('pain_level'),
132
- 'moisture_level': questionnaire_data.get('moisture_level'),
133
- 'infection_signs': questionnaire_data.get('infection_signs'),
134
- 'diabetic_status': questionnaire_data.get('diabetic_status')
135
  },
136
- 'medical_history': {
137
- 'previous_treatment': questionnaire_data.get('previous_treatment'),
138
- 'medical_history': questionnaire_data.get('medical_history'),
139
- 'medications': questionnaire_data.get('medications'),
140
- 'allergies': questionnaire_data.get('allergies'),
141
- 'additional_notes': questionnaire_data.get('additional_notes')
142
- }
143
  }
144
 
145
- practitioner_id = questionnaire_data.get('user_id')
146
- if not practitioner_id:
147
- # Fall back gracefully; your schema expects NOT NULL here.
148
- practitioner_id = 1
149
 
150
- # 4) Insert into `questionnaire_responses`
151
- insert_sql = """
152
  INSERT INTO questionnaire_responses
153
  (questionnaire_id, patient_id, practitioner_id, response_data, submitted_at)
154
- VALUES (%s, %s, %s, %s, %s)
155
- """
156
- cur.execute(insert_sql, (
157
- questionnaire_id,
158
- patient_id, # <-- This is patients.id (BIGINT) and WILL be filled now.
159
- practitioner_id,
160
- json.dumps(response_data, ensure_ascii=False),
161
- datetime.now()
162
  ))
163
- response_id = cur.lastrowid
164
  conn.commit()
165
-
166
- logging.info(f"✅ Saved questionnaire response ID {response_id} (patient_id={patient_id})")
167
- return response_id
168
-
169
  except Exception as e:
170
- logging.error(f"❌ Error saving questionnaire: {e}")
171
  if conn: conn.rollback()
 
172
  return None
173
  finally:
174
- if cur: cur.close()
175
- if conn: conn.close()
176
-
177
- def _create_or_get_patient(self, cur, questionnaire_data):
178
- """
179
- Works against the EXISTING `patients` table:
180
- columns include id (PK), uuid, name, age (int), gender (varchar), illness, allergy, notes, etc.
181
- Returns patients.id (int).
182
- """
 
 
183
  try:
184
- name = questionnaire_data.get('patient_name')
185
- age = questionnaire_data.get('patient_age')
186
- gender = questionnaire_data.get('patient_gender')
187
-
188
- # Try to find an existing patient via (name, age, gender)
189
- cur.execute("""
190
- SELECT id FROM patients
191
- WHERE name = %s AND (age = %s OR %s IS NULL) AND (gender = %s OR %s IS NULL)
192
- LIMIT 1
193
- """, (name, age, age, gender, gender))
194
- row = cur.fetchone()
195
- if row:
196
- return row["id"]
197
-
198
- # Create new patient
199
  cur.execute("""
200
- INSERT INTO patients (uuid, name, age, gender, illness, allergy, notes, created_at, updated_at)
201
- VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
202
  """, (
203
- str(uuid.uuid4()),
204
- name,
205
- int(age) if (isinstance(age, (int, float, str)) and str(age).isdigit()) else None,
206
- gender,
207
- questionnaire_data.get('medical_history', ''),
208
- questionnaire_data.get('allergies', ''),
209
- questionnaire_data.get('additional_notes', '')
 
210
  ))
211
- return cur.lastrowid
212
-
213
  except Exception as e:
214
- logging.error(f"Error creating/getting patient: {e}")
 
215
  return None
 
 
 
 
 
 
216
 
217
- def _get_patient_uuid(self, patient_id, cur=None):
218
- """Fetch patients.uuid for a given patients.id (helps when tables store patient_id as VARCHAR uuid)."""
219
- owns_cursor = False
220
- conn = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  try:
222
- if cur is None:
223
- conn = self.get_connection()
224
- if not conn:
225
- return None
226
- cur = conn.cursor(dictionary=True)
227
- owns_cursor = True
228
-
229
- cur.execute("SELECT uuid FROM patients WHERE id = %s LIMIT 1", (patient_id,))
230
- row = cur.fetchone()
231
- return row["uuid"] if row else None
 
 
232
  except Exception as e:
233
- logging.error(f"Error fetching patient uuid: {e}")
234
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  finally:
236
- if owns_cursor and cur: cur.close()
237
- if owns_cursor and conn: conn.close()
 
 
 
238
 
239
- def save_wound_image(self, patient_id, image):
 
240
  """
241
- Save wound image to filesystem and EXISTING `wound_images` table.
242
- Your `wound_images.patient_id` is VARCHAR, so we store patients.uuid there.
243
- Returns dict {id, filename, path} or None.
 
244
  """
245
  try:
246
- # 1) Persist to disk
247
- image_uid = str(uuid.uuid4())
248
- filename = f"wound_{image_uid}.jpg"
249
- os.makedirs("uploads", exist_ok=True)
250
- file_path = os.path.join("uploads", filename)
251
-
252
- if hasattr(image, 'save'):
253
- image.save(file_path, format='JPEG', quality=95)
254
- width, height = image.size
255
- file_size = os.path.getsize(file_path)
256
- original_filename = getattr(image, "filename", filename)
257
  elif isinstance(image, str) and os.path.exists(image):
258
  with Image.open(image) as pil:
259
- pil = pil.convert('RGB')
260
- pil.save(file_path, format='JPEG', quality=95)
261
- width, height = pil.size
262
- file_size = os.path.getsize(file_path)
263
- original_filename = os.path.basename(image)
264
  else:
265
  logging.error("Invalid image object/path")
266
  return None
267
 
268
- # 2) Resolve patients.uuid (VARCHAR target)
269
  conn = self.get_connection()
270
  if not conn: return None
271
  cur = conn.cursor()
272
- try:
273
- patient_uuid = self._get_patient_uuid(patient_id)
274
- if not patient_uuid:
275
- raise Exception("Patient UUID not found for given patient_id")
276
 
277
- # Insert into existing wound_images schema (uuid, patient_id (varchar), image, width, height, etc.)
278
- cur.execute("""
279
- INSERT INTO wound_images (
280
- uuid, patient_id, image, width, height, area, notes, created_at, updated_at
281
- ) VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
282
- """, (
283
- str(uuid.uuid4()),
284
- patient_uuid, # VARCHAR column → store patient's UUID
285
- file_path,
286
- str(width),
287
- str(height),
288
- None,
289
- None
290
- ))
291
- conn.commit()
292
- image_db_id = cur.lastrowid
293
- logging.info(f"Image saved with ID: {image_db_id}")
294
- return {'id': image_db_id, 'filename': filename, 'path': file_path}
295
- finally:
296
- cur.close()
297
  conn.close()
 
 
298
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  except Exception as e:
300
- logging.error(f"Image save error: {e}")
 
301
  return None
 
 
 
 
 
 
302
 
303
- def save_analysis(self, questionnaire_response_id, image_id, analysis_data):
304
  """
305
- Save AI analysis results.
306
- Your live `ai_analyses` table expects `questionnaire_id` (the TEMPLATE), not the response id.
307
- So we first look up the template id from questionnaire_responses and store that.
308
  """
309
  try:
310
- # Resolve template questionnaire_id from the response
311
  row = self.execute_query_one(
312
  "SELECT questionnaire_id FROM questionnaire_responses WHERE id = %s LIMIT 1",
313
  (questionnaire_response_id,)
314
  )
315
  if not row:
316
- logging.error("No questionnaire_response found for analysis save")
317
  return None
318
  questionnaire_id = row["questionnaire_id"]
319
 
320
- query = """
321
- INSERT INTO ai_analyses (
322
- questionnaire_id, image_id, analysis_data, summary,
323
- recommendations, risk_score, risk_level,
324
- processing_time, model_version, created_at
325
- ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
326
- """
327
- params = (
328
- questionnaire_id,
329
- image_id,
330
- json.dumps(analysis_data) if analysis_data else None,
331
- (analysis_data or {}).get('summary', ''),
332
- (analysis_data or {}).get('recommendations', ''),
333
- (analysis_data or {}).get('risk_score', 0),
334
- (analysis_data or {}).get('risk_level', 'Unknown'),
335
- (analysis_data or {}).get('processing_time', 0.0),
336
- (analysis_data or {}).get('model_version', 'v1.0'),
337
- datetime.now()
338
  )
339
- return self.execute_query(query, params, fetch=False)
340
  except Exception as e:
341
- logging.error(f"Analysis save error: {e}")
342
  return None
343
 
344
- def get_user_history(self, user_id):
 
345
  """
346
- Latest 20 submissions for a practitioner, with optional analysis summary if present.
347
- Aligns to existing schema: joins questionnaire_responses -> questionnaires and LEFT joins ai_analyses (by template).
348
  """
349
- try:
350
- query = """
351
- SELECT
352
- r.id AS response_id,
353
- r.submitted_at,
 
354
  p.name AS patient_name,
355
  p.age AS patient_age,
356
  p.gender AS patient_gender,
357
- q.name AS questionnaire_name,
358
- a.risk_level,
359
  a.summary,
360
- a.recommendations
361
- FROM questionnaire_responses r
362
- JOIN patients p ON p.id = r.patient_id
363
- JOIN questionnaires q ON q.id = r.questionnaire_id
364
- LEFT JOIN ai_analyses a ON a.questionnaire_id = r.questionnaire_id
365
- WHERE r.practitioner_id = %s
366
- ORDER BY r.submitted_at DESC
 
 
 
 
367
  LIMIT 20
368
- """
369
- result = self.execute_query(query, (user_id,), fetch=True)
370
- return result or []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  except Exception as e:
372
- logging.error(f"Error fetching user history: {e}")
373
- return []
374
 
375
- # Convenience for existing callers (kept signature)
376
- def create_organization(self, org_data):
377
  try:
378
- query = """INSERT INTO organizations (name, email, phone, country_code, department, location, created_at, updated_at)
379
- VALUES (%s, %s, %s, %s, %s, %s, NOW(), NOW())"""
380
- params = (
 
381
  org_data.get('org_name', ''),
382
  org_data.get('email', ''),
383
  org_data.get('phone', ''),
384
  org_data.get('country_code', ''),
385
  org_data.get('department', ''),
386
- org_data.get('location', '')
387
- )
388
- rc = self.execute_query(query, params)
389
- if rc:
390
  row = self.execute_query_one(
391
  "SELECT id FROM organizations WHERE name = %s ORDER BY created_at DESC LIMIT 1",
392
  (org_data.get('org_name', ''),)
@@ -395,39 +735,4 @@ class DatabaseManager:
395
  return None
396
  except Exception as e:
397
  logging.error(f"Error creating organization: {e}")
398
- return None
399
-
400
- def get_organizations(self):
401
- try:
402
- query = "SELECT id, name as org_name, location FROM organizations ORDER BY name"
403
- result = self.execute_query(query, fetch=True)
404
- return result or [{'id': 1, 'org_name': 'Default Hospital', 'location': 'Default Location'}]
405
- except Exception as e:
406
- logging.error(f"Error getting organizations: {e}")
407
- return [{'id': 1, 'org_name': 'Default Hospital', 'location': 'Default Location'}]
408
-
409
- # Back-compat helper kept (writes same as save_analysis, minimal data)
410
- def save_analysis_result(self, questionnaire_response_id, analysis_result):
411
- try:
412
- # store under template id like save_analysis()
413
- row = self.execute_query_one(
414
- "SELECT questionnaire_id FROM questionnaire_responses WHERE id = %s LIMIT 1",
415
- (questionnaire_response_id,)
416
- )
417
- if not row:
418
- logging.error("No questionnaire_response found for analysis_result save")
419
- return None
420
- questionnaire_id = row["questionnaire_id"]
421
-
422
- query = """INSERT INTO ai_analyses (questionnaire_id, analysis_data, summary, created_at)
423
- VALUES (%s, %s, %s, %s)"""
424
- params = (
425
- questionnaire_id,
426
- json.dumps(analysis_result) if analysis_result else None,
427
- (analysis_result or {}).get('summary', 'Analysis completed'),
428
- datetime.now()
429
- )
430
- return self.execute_query(query, params)
431
- except Exception as e:
432
- logging.error(f"Error saving analysis result: {e}")
433
- return None
 
1
+ # database.py (SmartHeal) — dataset ID hardcoded
2
+
3
+ import os
4
+ import json
5
  import logging
6
  from datetime import datetime
7
+ from typing import Optional, Dict, Any, List
8
+
9
+ import mysql.connector
10
+ from mysql.connector import Error
11
  from PIL import Image
12
 
13
+ # ----------------------------- Hardcoded Dataset -----------------------------
14
+ DATASET_ID = "SmartHeal/wound-image-uploads" # <— hardcoded as requested
15
+
16
+ # Optional HF dataset integration
17
+ _HF_AVAILABLE = False
18
+ try:
19
+ from huggingface_hub import HfApi, HfFolder, create_repo
20
+ _HF_AVAILABLE = True
21
+ except Exception:
22
+ _HF_AVAILABLE = False
23
+
24
+
25
+ # ----------------------------- helpers -----------------------------
26
+ def _now():
27
+ return datetime.now()
28
+
29
+ def _abs(p: str) -> str:
30
+ try:
31
+ return os.path.abspath(p)
32
+ except Exception:
33
+ return p
34
+
35
+ def _ensure_dir(path: str):
36
+ os.makedirs(path, exist_ok=True)
37
+
38
+ def _file_url(path: str) -> str:
39
+ """
40
+ For local files, return /file=/abs/path (works in Gradio/Spaces).
41
+ For remote (http/https), return as-is.
42
+ """
43
+ if not path:
44
+ return ""
45
+ s = str(path)
46
+ if s.startswith("http://") or s.startswith("https://"):
47
+ return s
48
+ return f"/file={_abs(s)}"
49
+
50
+
51
+ # -------------------------- DatabaseManager --------------------------
52
  class DatabaseManager:
53
+ """
54
+ Database operations manager for SmartHeal
55
+ - Keeps patient_id (INT) consistent for questionnaire_responses / ai_analyses joins
56
+ - Uses patients.uuid (CHAR 36) in wounds / wound_images where those tables store VARCHAR
57
+ - Saves images locally OR to a hardcoded Hugging Face dataset and stores the URL
58
+ """
59
+
60
+ def __init__(self, mysql_config: Dict[str, Any], hf_token: Optional[str] = None):
61
  self.mysql_config = mysql_config
62
+
63
+ # storage backend: hardcoded dataset id; token from arg or env
64
+ self.dataset_id = DATASET_ID
65
+ self.hf_token = hf_token or os.getenv("HUGGINGFACE_TOKEN") or os.getenv("HF_TOKEN")
66
+ self.use_dataset = bool(self.dataset_id and self.hf_token and _HF_AVAILABLE)
67
+
68
+ if self.use_dataset:
69
+ logging.info(f"HF dataset storage enabled: {self.dataset_id}")
70
+ try:
71
+ HfFolder.save_token(self.hf_token)
72
+ try:
73
+ create_repo(repo_id=self.dataset_id, repo_type="dataset", exist_ok=True, token=self.hf_token)
74
+ except Exception:
75
+ pass
76
+ self.hf_api = HfApi(token=self.hf_token)
77
+ except Exception as e:
78
+ logging.warning(f"Could not initialize HF dataset backend, falling back to local. Err: {e}")
79
+ self.use_dataset = False
80
+ else:
81
+ logging.info("Using LOCAL storage (uploads/)")
82
+
83
+ _ensure_dir("uploads")
84
  self.test_connection()
85
  self._ensure_default_questionnaire()
86
 
87
+ # -------------------- low-level connection utils --------------------
 
88
  def test_connection(self):
89
  try:
90
  conn = self.get_connection()
 
116
  conn.commit()
117
  return cur.rowcount
118
  except Error as e:
119
+ logging.error(f"Error executing query: {e}\nSQL: {query}\nParams: {params}")
120
  if conn:
121
  conn.rollback()
122
  return None
 
134
  cur.execute(query, params or ())
135
  return cur.fetchone()
136
  except Error as e:
137
+ logging.error(f"Error executing query: {e}\nSQL: {query}\nParams: {params}")
138
  return None
139
  finally:
140
  if cur: cur.close()
141
  if conn and conn.is_connected(): conn.close()
142
 
143
  # ---------------------- One-time ensures ----------------------
 
144
  def _ensure_default_questionnaire(self):
145
  """Ensure a 'Default Patient Assessment' row exists in questionnaires."""
146
  try:
 
150
  )
151
  if not row:
152
  self.execute_query(
153
+ "INSERT INTO questionnaires (name, description, created_at, updated_at) "
154
+ "VALUES (%s, %s, NOW(), NOW())",
155
  ("Default Patient Assessment", "Standard patient wound assessment form")
156
  )
157
  logging.info("Created default questionnaire 'Default Patient Assessment'")
158
  except Exception as e:
159
  logging.error(f"Error ensuring default questionnaire: {e}")
160
 
161
+ # ------------------------------ patients ------------------------------
162
+ def _normalize_age(self, age_val: Any) -> Optional[int]:
163
+ try:
164
+ return int(age_val) if age_val not in [None, ""] else None
165
+ except Exception:
166
+ return None
167
 
168
+ def get_patient_by_id(self, patient_id: int) -> Optional[Dict[str, Any]]:
169
+ return self.execute_query_one(
170
+ "SELECT id, uuid, name, age, gender FROM patients WHERE id = %s LIMIT 1",
171
+ (patient_id,)
172
+ )
173
+
174
+ def get_patient_by_name_age_gender(self, name: str, age: Any, gender: str) -> Optional[Dict[str, Any]]:
175
+ age_val = self._normalize_age(age)
176
+ if age_val is None:
177
+ return self.execute_query_one(
178
+ "SELECT id, uuid, name, age, gender FROM patients "
179
+ "WHERE name = %s AND age IS NULL AND gender = %s LIMIT 1",
180
+ (name, gender),
181
+ )
182
+ return self.execute_query_one(
183
+ "SELECT id, uuid, name, age, gender FROM patients "
184
+ "WHERE name = %s AND age = %s AND gender = %s LIMIT 1",
185
+ (name, age_val, gender),
186
+ )
187
+
188
+ def create_patient(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
189
+ conn = None
190
+ cur = None
191
+ try:
192
+ conn = self.get_connection()
193
+ if not conn: return None
194
+ cur = conn.cursor()
195
+ import uuid as _uuid
196
+ p_uuid = str(_uuid.uuid4())
197
+ cur.execute("""
198
+ INSERT INTO patients (uuid, name, age, gender, illness, allergy, notes, created_at)
199
+ VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
200
+ """, (
201
+ p_uuid,
202
+ data.get("patient_name"),
203
+ self._normalize_age(data.get("patient_age")),
204
+ data.get("patient_gender"),
205
+ data.get("medical_history", ""),
206
+ data.get("allergies", ""),
207
+ data.get("additional_notes", ""),
208
+ _now()
209
+ ))
210
+ conn.commit()
211
+ return {"id": cur.lastrowid, "uuid": p_uuid}
212
+ except Exception as e:
213
+ if conn: conn.rollback()
214
+ logging.error(f"create_patient error: {e}")
215
+ return None
216
+ finally:
217
+ try:
218
+ if cur: cur.close()
219
+ if conn: conn.close()
220
+ except Exception:
221
+ pass
222
+
223
+ def list_patients_for_practitioner(self, practitioner_id: int) -> List[Dict[str, Any]]:
224
+ return self.execute_query("""
225
+ SELECT
226
+ p.id, p.uuid, p.name, p.age, p.gender,
227
+ COUNT(qr.id) AS total_visits,
228
+ MAX(qr.submitted_at) AS last_visit,
229
+ MIN(qr.submitted_at) AS first_visit
230
+ FROM questionnaire_responses qr
231
+ JOIN patients p ON p.id = qr.patient_id
232
+ WHERE qr.practitioner_id = %s
233
+ GROUP BY p.id, p.uuid, p.name, p.age, p.gender
234
+ ORDER BY last_visit DESC
235
+ """, (practitioner_id,), fetch=True) or []
236
+
237
+ # --------------------------- questionnaires/visits ---------------------------
238
+ def _ensure_default_questionnaire_id(self, created_by_user_id: Optional[int] = None) -> int:
239
+ row = self.execute_query_one(
240
+ "SELECT id FROM questionnaires WHERE name = 'Default Patient Assessment' LIMIT 1"
241
+ )
242
+ if row:
243
+ return int(row["id"])
244
+ self.execute_query("""
245
+ INSERT INTO questionnaires (name, description, created_by, created_at, updated_at)
246
+ VALUES ('Default Patient Assessment', 'Standard patient wound assessment form', %s, NOW(), NOW())
247
+ """, (created_by_user_id,))
248
+ row = self.execute_query_one(
249
+ "SELECT id FROM questionnaires WHERE name = 'Default Patient Assessment' ORDER BY id DESC LIMIT 1"
250
+ )
251
+ return int(row["id"]) if row else 1
252
+
253
+ def save_questionnaire(self, questionnaire_data: Dict[str, Any], existing_patient_id: Optional[int] = None):
254
  """
255
+ Create a visit (questionnaire_responses) for an existing or new patient.
256
+ Returns: {'response_id', 'patient_id', 'patient_uuid'}
257
  """
258
  conn = None
259
  cur = None
260
  try:
261
  conn = self.get_connection()
262
+ if not conn: return None
 
263
  cur = conn.cursor(dictionary=True)
264
 
265
+ # Resolve patient
266
+ if existing_patient_id:
267
+ p = self.get_patient_by_id(existing_patient_id)
268
+ if not p:
269
+ raise Exception("Existing patient id not found")
270
+ patient_id = p["id"]
271
+ patient_uuid = p["uuid"]
272
+ else:
273
+ found = self.get_patient_by_name_age_gender(
274
+ questionnaire_data.get("patient_name"),
275
+ questionnaire_data.get("patient_age"),
276
+ questionnaire_data.get("patient_gender"),
277
+ )
278
+ if found:
279
+ patient_id, patient_uuid = found["id"], found["uuid"]
280
+ else:
281
+ created = self.create_patient(questionnaire_data)
282
+ if not created:
283
+ raise Exception("Failed to create patient")
284
+ patient_id, patient_uuid = created["id"], created["uuid"]
285
 
286
+ qid = self._ensure_default_questionnaire_id(questionnaire_data.get("user_id"))
 
 
 
 
 
 
287
 
 
288
  response_data = {
289
+ "patient_info": {
290
+ "name": questionnaire_data.get("patient_name"),
291
+ "age": questionnaire_data.get("patient_age"),
292
+ "gender": questionnaire_data.get("patient_gender"),
293
+ },
294
+ "wound_details": {
295
+ "location": questionnaire_data.get("wound_location"),
296
+ "duration": questionnaire_data.get("wound_duration"),
297
+ "pain_level": questionnaire_data.get("pain_level"),
298
+ "moisture_level": questionnaire_data.get("moisture_level"),
299
+ "infection_signs": questionnaire_data.get("infection_signs"),
300
+ "diabetic_status": questionnaire_data.get("diabetic_status"),
301
  },
302
+ "medical_history": {
303
+ "previous_treatment": questionnaire_data.get("previous_treatment"),
304
+ "medical_history": questionnaire_data.get("medical_history"),
305
+ "medications": questionnaire_data.get("medications"),
306
+ "allergies": questionnaire_data.get("allergies"),
307
+ "additional_notes": questionnaire_data.get("additional_notes"),
 
308
  },
 
 
 
 
 
 
 
309
  }
310
 
311
+ practitioner_id = questionnaire_data.get("user_id") or 1
 
 
 
312
 
313
+ cur.execute("""
 
314
  INSERT INTO questionnaire_responses
315
  (questionnaire_id, patient_id, practitioner_id, response_data, submitted_at)
316
+ VALUES (%s,%s,%s,%s,%s)
317
+ """, (
318
+ qid, patient_id, practitioner_id, json.dumps(response_data), _now()
 
 
 
 
 
319
  ))
 
320
  conn.commit()
321
+ response_id = cur.lastrowid
322
+ logging.info(f"✅ Saved response ID {response_id}")
323
+ return {"response_id": response_id, "patient_id": patient_id, "patient_uuid": patient_uuid}
 
324
  except Exception as e:
 
325
  if conn: conn.rollback()
326
+ logging.error(f"save_questionnaire error: {e}")
327
  return None
328
  finally:
329
+ try:
330
+ if cur: cur.close()
331
+ if conn: conn.close()
332
+ except Exception:
333
+ pass
334
+
335
+ # ------------------------------- wounds -------------------------------
336
+ def create_wound(self, patient_uuid: str, questionnaire_data: Dict[str, Any]) -> Optional[str]:
337
+ """Create wound (returns wound_uuid). Stores patient_uuid in wounds.patient_id (VARCHAR)."""
338
+ conn = None
339
+ cur = None
340
  try:
341
+ conn = self.get_connection()
342
+ if not conn: return None
343
+ cur = conn.cursor()
344
+ import uuid as _uuid
345
+ wound_uuid = str(_uuid.uuid4())
 
 
 
 
 
 
 
 
 
 
346
  cur.execute("""
347
+ INSERT INTO wounds (uuid, patient_id, position, category, moisture, infection, notes, created_at)
348
+ VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
349
  """, (
350
+ wound_uuid,
351
+ patient_uuid,
352
+ questionnaire_data.get("wound_location") or "",
353
+ "Assessment",
354
+ questionnaire_data.get("moisture_level") or "",
355
+ questionnaire_data.get("infection_signs") or "",
356
+ questionnaire_data.get("additional_notes") or "",
357
+ _now()
358
  ))
359
+ conn.commit()
360
+ return wound_uuid
361
  except Exception as e:
362
+ if conn: conn.rollback()
363
+ logging.error(f"create_wound error: {e}")
364
  return None
365
+ finally:
366
+ try:
367
+ if cur: cur.close()
368
+ if conn: conn.close()
369
+ except Exception:
370
+ pass
371
 
372
+ # --------------------------- image storage ---------------------------
373
+ def _read_image_size(self, image_path: str):
374
+ try:
375
+ with Image.open(image_path) as im:
376
+ return im.size # (w, h)
377
+ except Exception:
378
+ return (None, None)
379
+
380
+ def _copy_to_uploads(self, src_path: str, suffix: str) -> str:
381
+ ext = os.path.splitext(src_path)[1] or ".jpg"
382
+ base = f"wound_{datetime.utcnow().strftime('%Y%m%d_%H%M%S_%f')}_{suffix}{ext}"
383
+ dst = os.path.join("uploads", base)
384
+ try:
385
+ with open(src_path, "rb") as r, open(dst, "wb") as w:
386
+ w.write(r.read())
387
+ return dst
388
+ except Exception:
389
+ return src_path
390
+
391
+ def _hf_upload_and_url(self, local_path: str, subdir: str) -> Optional[str]:
392
+ """Upload a file to the (hardcoded) HF dataset and return a public resolve URL."""
393
+ if not self.use_dataset:
394
+ return None
395
  try:
396
+ # destination path in repo (datasets use 'main' branch)
397
+ day = datetime.utcnow().strftime("%Y/%m/%d")
398
+ fname = os.path.basename(local_path)
399
+ repo_path = f"{subdir}/{day}/{fname}"
400
+ self.hf_api.upload_file(
401
+ path_or_fileobj=local_path,
402
+ path_in_repo=repo_path,
403
+ repo_id=self.dataset_id,
404
+ repo_type="dataset",
405
+ commit_message=f"Add {repo_path}"
406
+ )
407
+ return f"https://huggingface.co/datasets/{self.dataset_id}/resolve/main/{repo_path}"
408
  except Exception as e:
409
+ logging.error(f"HF upload failed: {e}")
410
  return None
411
+
412
+ def _insert_wound_image_row(self, conn, cur, patient_uuid: str, wound_uuid: Optional[str], image_path_or_url: str) -> Optional[int]:
413
+ """Insert one row in wound_images; width/height detected for local files only."""
414
+ width, height = (None, None)
415
+ if not (image_path_or_url.startswith("http://") or image_path_or_url.startswith("https://")):
416
+ width, height = self._read_image_size(image_path_or_url)
417
+
418
+ import uuid as _uuid
419
+ img_uuid = str(_uuid.uuid4())
420
+ cur.execute("""
421
+ INSERT INTO wound_images (uuid, patient_id, wound_id, image, width, height, created_at)
422
+ VALUES (%s,%s,%s,%s,%s,%s,%s)
423
+ """, (
424
+ img_uuid,
425
+ patient_uuid,
426
+ wound_uuid,
427
+ image_path_or_url,
428
+ int(width) if width else None,
429
+ int(height) if height else None,
430
+ _now()
431
+ ))
432
+ conn.commit()
433
+ return cur.lastrowid
434
+
435
+ def save_wound_images_bundle(
436
+ self,
437
+ patient_uuid: str,
438
+ original_path: str,
439
+ analysis_result: Dict[str, Any],
440
+ wound_uuid: Optional[str] = None
441
+ ) -> Dict[str, Any]:
442
+ """
443
+ Save original + detection + segmentation images to wound_images.
444
+ Stores **HF dataset URLs** when configured, else local paths.
445
+ Returns display URLs for UI.
446
+ """
447
+ out = {
448
+ "original_id": None, "original_url": None,
449
+ "detection_id": None, "detection_url": None,
450
+ "segmentation_id": None, "segmentation_url": None
451
+ }
452
+
453
+ conn = self.get_connection()
454
+ if not conn: return out
455
+ cur = conn.cursor()
456
+
457
+ try:
458
+ # Ensure wound row exists if not provided (minimal)
459
+ if not wound_uuid:
460
+ import uuid as _uuid
461
+ wound_uuid = str(_uuid.uuid4())
462
+ cur.execute("""
463
+ INSERT INTO wounds (uuid, patient_id, category, notes, created_at)
464
+ VALUES (%s,%s,%s,%s,%s)
465
+ """, (wound_uuid, patient_uuid, "Assessment", "", _now()))
466
+ conn.commit()
467
+
468
+ # 1) Original
469
+ local_orig = self._copy_to_uploads(original_path, "original")
470
+ url_orig = self._hf_upload_and_url(local_orig, subdir="original") if self.use_dataset else None
471
+ store_orig = url_orig or local_orig
472
+ img_id = self._insert_wound_image_row(conn, cur, patient_uuid, wound_uuid, store_orig)
473
+ out["original_id"] = img_id
474
+ out["original_url"] = _file_url(store_orig)
475
+
476
+ # 2) Detection / Segmentation from analysis_result
477
+ va = (analysis_result or {}).get("visual_analysis", {}) or {}
478
+ det = va.get("detection_image_path")
479
+ seg = va.get("segmentation_image_path")
480
+
481
+ if det and os.path.exists(det):
482
+ local_det = self._copy_to_uploads(det, "detect")
483
+ url_det = self._hf_upload_and_url(local_det, subdir="detect") if self.use_dataset else None
484
+ store_det = url_det or local_det
485
+ did = self._insert_wound_image_row(conn, cur, patient_uuid, wound_uuid, store_det)
486
+ out["detection_id"] = did
487
+ out["detection_url"] = _file_url(store_det)
488
+
489
+ if seg and os.path.exists(seg):
490
+ local_seg = self._copy_to_uploads(seg, "segment")
491
+ url_seg = self._hf_upload_and_url(local_seg, subdir="segment") if self.use_dataset else None
492
+ store_seg = url_seg or local_seg
493
+ sid = self._insert_wound_image_row(conn, cur, patient_uuid, wound_uuid, store_seg)
494
+ out["segmentation_id"] = sid
495
+ out["segmentation_url"] = _file_url(store_seg)
496
+
497
+ return out
498
+ except Exception as e:
499
+ logging.error(f"save_wound_images_bundle error: {e}", exc_info=True)
500
+ try:
501
+ conn.rollback()
502
+ except Exception:
503
+ pass
504
+ return out
505
  finally:
506
+ try:
507
+ cur.close()
508
+ conn.close()
509
+ except Exception:
510
+ pass
511
 
512
+ # For older callers that only save one image
513
+ def save_wound_image(self, patient_id: int, image) -> Optional[Dict[str, Any]]:
514
  """
515
+ Back-compat single-image save:
516
+ - If `image` is a PIL.Image -> we write to uploads and (optionally) the dataset.
517
+ - If `image` is a filepath -> we copy + (optionally) upload.
518
+ Stores patients.uuid into wound_images.patient_id (VARCHAR).
519
  """
520
  try:
521
+ # Normalize to a temporary local path first
522
+ _ensure_dir("uploads")
523
+ image_uid = os.urandom(8).hex()
524
+ tmp_local = os.path.join("uploads", f"wound_{image_uid}_tmp.jpg")
525
+
526
+ if hasattr(image, "save"):
527
+ image.save(tmp_local, format="JPEG", quality=95)
 
 
 
 
528
  elif isinstance(image, str) and os.path.exists(image):
529
  with Image.open(image) as pil:
530
+ pil = pil.convert("RGB")
531
+ pil.save(tmp_local, format="JPEG", quality=95)
 
 
 
532
  else:
533
  logging.error("Invalid image object/path")
534
  return None
535
 
536
+ # Resolve patients.uuid
537
  conn = self.get_connection()
538
  if not conn: return None
539
  cur = conn.cursor()
 
 
 
 
540
 
541
+ cur2 = conn.cursor(dictionary=True)
542
+ cur2.execute("SELECT uuid FROM patients WHERE id = %s LIMIT 1", (patient_id,))
543
+ row = cur2.fetchone()
544
+ cur2.close()
545
+ if not row:
546
+ logging.error("Patient id not found while saving image")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  conn.close()
548
+ return None
549
+ patient_uuid = row["uuid"]
550
 
551
+ # Push to uploads/dataset with final name
552
+ local_path = self._copy_to_uploads(tmp_local, "original")
553
+ url = self._hf_upload_and_url(local_path, subdir="original") if self.use_dataset else None
554
+ path_or_url = url or local_path
555
+
556
+ width, height = self._read_image_size(local_path)
557
+ import uuid as _uuid
558
+ img_uuid = str(_uuid.uuid4())
559
+ cur.execute("""
560
+ INSERT INTO wound_images (uuid, patient_id, image, width, height, created_at)
561
+ VALUES (%s,%s,%s,%s,%s,%s)
562
+ """, (
563
+ img_uuid,
564
+ patient_uuid,
565
+ path_or_url,
566
+ int(width) if width else None,
567
+ int(height) if height else None,
568
+ _now()
569
+ ))
570
+ conn.commit()
571
+ image_db_id = cur.lastrowid
572
+ cur.close()
573
+ conn.close()
574
+
575
+ return {
576
+ "id": image_db_id,
577
+ "filename": os.path.basename(local_path),
578
+ "path": path_or_url,
579
+ "url": _file_url(path_or_url),
580
+ }
581
+ except Exception as e:
582
+ logging.error(f"Image save error: {e}", exc_info=True)
583
+ return None
584
+
585
+ # ------------------------------- analyses -------------------------------
586
+ def save_analysis(self, questionnaire_id: int, image_id: Optional[int], analysis_data: Dict[str, Any]) -> Optional[int]:
587
+ conn = None
588
+ cur = None
589
+ try:
590
+ conn = self.get_connection()
591
+ if not conn: return None
592
+ cur = conn.cursor()
593
+ cur.execute("""
594
+ INSERT INTO ai_analyses (
595
+ questionnaire_id, image_id, analysis_data, summary, recommendations,
596
+ risk_score, risk_level, processing_time, model_version, created_at
597
+ ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
598
+ """, (
599
+ questionnaire_id,
600
+ image_id,
601
+ json.dumps(analysis_data) if analysis_data else None,
602
+ (analysis_data or {}).get("summary", ""),
603
+ (analysis_data or {}).get("recommendations", ""),
604
+ int((analysis_data or {}).get("risk_score", 0) or 0),
605
+ (analysis_data or {}).get("risk_level", "Unknown"),
606
+ float((analysis_data or {}).get("processing_time", 0.0) or 0.0),
607
+ (analysis_data or {}).get("model_version", "v1.0"),
608
+ _now()
609
+ ))
610
+ conn.commit()
611
+ return cur.lastrowid
612
  except Exception as e:
613
+ if conn: conn.rollback()
614
+ logging.error(f"save_analysis error: {e}")
615
  return None
616
+ finally:
617
+ try:
618
+ if cur: cur.close()
619
+ if conn: conn.close()
620
+ except Exception:
621
+ pass
622
 
623
+ def save_analysis_result(self, questionnaire_response_id: int, analysis_result: Dict[str, Any]):
624
  """
625
+ Compatibility wrapper used by some callers (without image_id).
626
+ Stores under the questionnaire template linked to the response.
 
627
  """
628
  try:
 
629
  row = self.execute_query_one(
630
  "SELECT questionnaire_id FROM questionnaire_responses WHERE id = %s LIMIT 1",
631
  (questionnaire_response_id,)
632
  )
633
  if not row:
634
+ logging.error("No questionnaire_response found for analysis_result save")
635
  return None
636
  questionnaire_id = row["questionnaire_id"]
637
 
638
+ return self.save_analysis(
639
+ questionnaire_id=questionnaire_id,
640
+ image_id=None,
641
+ analysis_data=analysis_result or {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
  )
 
643
  except Exception as e:
644
+ logging.error(f"Error saving analysis result: {e}")
645
  return None
646
 
647
+ # ------------------------------- queries for UI -------------------------------
648
+ def get_user_history_rows(self, user_id: int) -> List[Dict[str, Any]]:
649
  """
650
+ Latest 20 visits across patients; joins ai_analyses.image_id -> wound_images to pull display image.
 
651
  """
652
+ return self.execute_query("""
653
+ SELECT
654
+ qr.id AS response_id,
655
+ qr.submitted_at AS visit_date,
656
+ p.id AS patient_id,
657
+ p.uuid AS patient_uuid,
658
  p.name AS patient_name,
659
  p.age AS patient_age,
660
  p.gender AS patient_gender,
 
 
661
  a.summary,
662
+ a.recommendations,
663
+ a.risk_score,
664
+ a.risk_level,
665
+ a.analysis_data,
666
+ wi.image AS image_url
667
+ FROM questionnaire_responses qr
668
+ JOIN patients p ON p.id = qr.patient_id
669
+ LEFT JOIN ai_analyses a ON a.questionnaire_id = qr.id
670
+ LEFT JOIN wound_images wi ON wi.id = a.image_id
671
+ WHERE qr.practitioner_id = %s
672
+ ORDER BY qr.submitted_at DESC
673
  LIMIT 20
674
+ """, (user_id,), fetch=True) or []
675
+
676
+ def get_patient_progression_rows(self, user_id: int, patient_id: int) -> List[Dict[str, Any]]:
677
+ """
678
+ Chronological visits for a given patient (INT id).
679
+ """
680
+ return self.execute_query("""
681
+ SELECT
682
+ qr.id AS response_id,
683
+ qr.submitted_at AS visit_date,
684
+ p.id AS patient_id,
685
+ p.uuid AS patient_uuid,
686
+ p.name AS patient_name,
687
+ p.age AS patient_age,
688
+ p.gender AS patient_gender,
689
+ a.summary,
690
+ a.recommendations,
691
+ a.risk_score,
692
+ a.risk_level,
693
+ a.analysis_data,
694
+ wi.image AS image_url
695
+ FROM questionnaire_responses qr
696
+ JOIN patients p ON p.id = qr.patient_id
697
+ LEFT JOIN ai_analyses a ON a.questionnaire_id = qr.id
698
+ LEFT JOIN wound_images wi ON wi.id = a.image_id
699
+ WHERE qr.practitioner_id = %s AND p.id = %s
700
+ ORDER BY qr.submitted_at ASC
701
+ """, (user_id, patient_id), fetch=True) or []
702
+
703
+ # ----------------------------- organizations -----------------------------
704
+ def get_organizations(self):
705
+ try:
706
+ rows = self.execute_query(
707
+ "SELECT id, name as org_name, location FROM organizations ORDER BY name",
708
+ fetch=True
709
+ )
710
+ return rows or [{'id': 1, 'org_name': 'Default Hospital', 'location': 'Default Location'}]
711
  except Exception as e:
712
+ logging.error(f"Error getting organizations: {e}")
713
+ return [{'id': 1, 'org_name': 'Default Hospital', 'location': 'Default Location'}]
714
 
715
+ def create_organization(self, org_data: Dict[str, Any]):
 
716
  try:
717
+ res = self.execute_query("""
718
+ INSERT INTO organizations (name, email, phone, country_code, department, location, created_at)
719
+ VALUES (%s,%s,%s,%s,%s,%s,%s)
720
+ """, (
721
  org_data.get('org_name', ''),
722
  org_data.get('email', ''),
723
  org_data.get('phone', ''),
724
  org_data.get('country_code', ''),
725
  org_data.get('department', ''),
726
+ org_data.get('location', ''),
727
+ _now()
728
+ ))
729
+ if res:
730
  row = self.execute_query_one(
731
  "SELECT id FROM organizations WHERE name = %s ORDER BY created_at DESC LIMIT 1",
732
  (org_data.get('org_name', ''),)
 
735
  return None
736
  except Exception as e:
737
  logging.error(f"Error creating organization: {e}")
738
+ return None