mbu-dev-shared-components 2.4.2__tar.gz → 2.4.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/PKG-INFO +5 -1
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/msoffice365/sharepoint_api/files.py +273 -4
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/application/base_ui.py +11 -4
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/application/document.py +6 -3
- mbu_dev_shared_components-2.4.4/mbu_dev_shared_components/tests/go_tests/go_integration_tests.py +278 -0
- mbu_dev_shared_components-2.4.4/mbu_dev_shared_components/tests/go_tests/objects_tests.py +124 -0
- mbu_dev_shared_components-2.4.4/mbu_dev_shared_components/tests/msoffice_tests/msoffice_integration_tests.py +188 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components.egg-info/PKG-INFO +5 -1
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components.egg-info/SOURCES.txt +3 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components.egg-info/requires.txt +5 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/pyproject.toml +8 -1
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/LICENSE +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/README.md +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/database/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/database/constants.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/database/logging.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/database/utility.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/getorganized/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/getorganized/auth.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/getorganized/cases.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/getorganized/contacts.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/getorganized/documents.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/getorganized/objects.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/google/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/google/api/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/google/api/auth.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/google/workspace/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/google/workspace/alerts.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/msoffice365/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/msoffice365/excel/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/msoffice365/excel/excel_reader.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/msoffice365/sharepoint_api/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/os2forms/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/os2forms/documents.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/os2forms/forms.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/romexis/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/romexis/db_handler.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/romexis/helper_functions.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/sap/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/sap/create_invoice.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/application/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/application/app_handler.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/application/appointment.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/application/clinic.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/application/edi_portal.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/application/event.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/application/exceptions.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/application/handler_base.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/application/journal_note.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/application/patient.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/database/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/solteqtand/database/db_handler.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/utils/__init__.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/utils/db_stored_procedure_executor.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/utils/fernet_encryptor.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/utils/file_handler.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components/utils/json_handler.py +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components.egg-info/dependency_links.txt +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/mbu_dev_shared_components.egg-info/top_level.txt +0 -0
- {mbu_dev_shared_components-2.4.2 → mbu_dev_shared_components-2.4.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mbu_dev_shared_components
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.4
|
|
4
4
|
Summary: Shared components to use in RPA projects
|
|
5
5
|
Author-email: MBU <rpa@mbu.aarhus.dk>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,7 +19,11 @@ Requires-Dist: uiautomation
|
|
|
19
19
|
Requires-Dist: pillow
|
|
20
20
|
Requires-Dist: psutil
|
|
21
21
|
Requires-Dist: docx2pdf
|
|
22
|
+
Requires-Dist: pandas>=2.2.3
|
|
22
23
|
Requires-Dist: rawpy
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-dependency>=0.5.1; extra == "dev"
|
|
23
27
|
Dynamic: license-file
|
|
24
28
|
|
|
25
29
|
# MBU Dev Shared Components
|
|
@@ -29,12 +29,25 @@ Example:
|
|
|
29
29
|
sp.download_files("FolderName", "C:\\LocalPath")
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
from pathlib import PurePath
|
|
33
|
-
from typing import Optional, List
|
|
34
32
|
import os
|
|
33
|
+
|
|
34
|
+
import math
|
|
35
|
+
|
|
36
|
+
import traceback
|
|
37
|
+
|
|
38
|
+
from pathlib import PurePath
|
|
39
|
+
|
|
40
|
+
from io import BytesIO
|
|
41
|
+
|
|
42
|
+
from typing import Optional, List, Dict, Any
|
|
43
|
+
|
|
44
|
+
from openpyxl.styles import Font, Alignment
|
|
45
|
+
from openpyxl import load_workbook
|
|
46
|
+
|
|
47
|
+
import pandas as pd
|
|
48
|
+
|
|
35
49
|
from office365.runtime.auth.user_credential import UserCredential
|
|
36
50
|
from office365.sharepoint.client_context import ClientContext
|
|
37
|
-
|
|
38
51
|
from office365.sharepoint.files.file import File
|
|
39
52
|
|
|
40
53
|
|
|
@@ -136,7 +149,6 @@ class Sharepoint:
|
|
|
136
149
|
file_content = File.open_binary(self.ctx, file_url)
|
|
137
150
|
return file_content.content
|
|
138
151
|
except Exception:
|
|
139
|
-
import traceback
|
|
140
152
|
print("Failed to download file:")
|
|
141
153
|
traceback.print_exc()
|
|
142
154
|
return None
|
|
@@ -249,3 +261,260 @@ class Sharepoint:
|
|
|
249
261
|
print(f"File '{file_name}' uploaded successfully to '{folder_url}'.")
|
|
250
262
|
except Exception as e:
|
|
251
263
|
print(f"Failed to upload file '{file_name}': {e}")
|
|
264
|
+
|
|
265
|
+
def append_row_to_sharepoint_excel(
|
|
266
|
+
self,
|
|
267
|
+
required_headers: Optional[List[str]] = None,
|
|
268
|
+
folder_name: str = "",
|
|
269
|
+
excel_file_name: str = "",
|
|
270
|
+
sheet_name: str = "",
|
|
271
|
+
new_row: Dict = None,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""
|
|
274
|
+
• Appends a row to an existing Excel file.
|
|
275
|
+
• Sorts and formats based on provided parameters.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
# 1. Pull file
|
|
279
|
+
binary_file = self.fetch_file_using_open_binary(excel_file_name, folder_name)
|
|
280
|
+
|
|
281
|
+
if binary_file is None:
|
|
282
|
+
raise FileNotFoundError(f"File '{excel_file_name}' not found in folder '{folder_name}'.")
|
|
283
|
+
|
|
284
|
+
wb = load_workbook(BytesIO(binary_file))
|
|
285
|
+
|
|
286
|
+
if sheet_name not in wb.sheetnames:
|
|
287
|
+
raise ValueError(f"Sheet '{sheet_name}' not found in '{excel_file_name}'")
|
|
288
|
+
|
|
289
|
+
ws = wb[sheet_name]
|
|
290
|
+
|
|
291
|
+
# 2. Validate headers
|
|
292
|
+
if required_headers:
|
|
293
|
+
current_headers = [cell.value for cell in ws[1]]
|
|
294
|
+
|
|
295
|
+
if current_headers != required_headers:
|
|
296
|
+
raise ValueError(
|
|
297
|
+
f"Header mismatch in sheet '{sheet_name}'!\n"
|
|
298
|
+
f"Expected: {required_headers}\n"
|
|
299
|
+
f"Found: {current_headers}"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# 2.5 Clean up empty rows before appending
|
|
303
|
+
for row_idx in range(ws.max_row, 1, -1): # Start from bottom, skip header
|
|
304
|
+
row_values = [cell.value for cell in ws[row_idx]]
|
|
305
|
+
|
|
306
|
+
if all(cell is None for cell in row_values):
|
|
307
|
+
ws.delete_rows(row_idx)
|
|
308
|
+
|
|
309
|
+
# 3. Append new row to sheet
|
|
310
|
+
ws.append([new_row.get(header.value, "") for header in ws[1]])
|
|
311
|
+
|
|
312
|
+
# 4. Save and upload
|
|
313
|
+
temp_stream = BytesIO()
|
|
314
|
+
|
|
315
|
+
wb.save(temp_stream)
|
|
316
|
+
|
|
317
|
+
temp_stream.seek(0)
|
|
318
|
+
|
|
319
|
+
self.upload_file_from_bytes(temp_stream.getvalue(), excel_file_name, folder_name)
|
|
320
|
+
|
|
321
|
+
print(f"✔ Added row + sorted '{sheet_name}' in '{excel_file_name}'.")
|
|
322
|
+
|
|
323
|
+
def format_and_sort_excel_file(
|
|
324
|
+
self,
|
|
325
|
+
folder_name: str,
|
|
326
|
+
excel_file_name: str,
|
|
327
|
+
sheet_name: str,
|
|
328
|
+
sorting_keys: Optional[List[Dict[str, Any]]] = None,
|
|
329
|
+
font_config: Optional[Dict[int, Dict[str, Any]]] = None,
|
|
330
|
+
bold_rows: Optional[List[int]] = None,
|
|
331
|
+
italic_rows: Optional[List[int]] = None,
|
|
332
|
+
align_horizontal: str = "center",
|
|
333
|
+
align_vertical: str = "center",
|
|
334
|
+
column_widths: Any = "auto",
|
|
335
|
+
freeze_panes: Optional[str] = None,
|
|
336
|
+
):
|
|
337
|
+
"""
|
|
338
|
+
Sorts and formats an Excel worksheet based on provided styling and sorting rules.
|
|
339
|
+
|
|
340
|
+
Params:
|
|
341
|
+
folder_name: Name of the folder where the file resides
|
|
342
|
+
excel_file_name: Name of the excel file
|
|
343
|
+
sheet_name: Name of the sheet that will be sorted
|
|
344
|
+
sorting_keys: List of dicts like [{"key": "A", "ascending": True, "type": "datetime"}]
|
|
345
|
+
bold_rows: List of row numbers to bold (defaults to [1])
|
|
346
|
+
italic_rows: List of row numbers to italicize
|
|
347
|
+
font_config: Dict of row -> font config (overrides bold/italic)
|
|
348
|
+
align_horizontal: Horizontal text alignment
|
|
349
|
+
align_vertical: Vertical text alignment
|
|
350
|
+
column_widths: "auto" or an int to represent a pixel value
|
|
351
|
+
freeze_panes: E.g., "A2" to freeze header row
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Modified worksheet
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
# Step 1 - Fetch the file to update from SharePoint and load it as a workbook
|
|
358
|
+
# This ensures we don't override any other sheets in the excel file
|
|
359
|
+
binary_file = self.fetch_file_using_open_binary(excel_file_name, folder_name)
|
|
360
|
+
if binary_file is None:
|
|
361
|
+
raise FileNotFoundError(f"File '{excel_file_name}' not found in folder '{folder_name}'.")
|
|
362
|
+
|
|
363
|
+
wb = load_workbook(BytesIO(binary_file))
|
|
364
|
+
if sheet_name not in wb.sheetnames:
|
|
365
|
+
raise ValueError(f"Sheet '{sheet_name}' not found in '{excel_file_name}'")
|
|
366
|
+
|
|
367
|
+
ws = wb[sheet_name]
|
|
368
|
+
|
|
369
|
+
# Step 2 - Read data into DataFrame
|
|
370
|
+
rows = list(ws.iter_rows(values_only=True))
|
|
371
|
+
header, *data_rows = rows
|
|
372
|
+
df = pd.DataFrame(data_rows, columns=header)
|
|
373
|
+
|
|
374
|
+
# Step 3 – Prepare sorting logic
|
|
375
|
+
# For each sorting instruction, we:
|
|
376
|
+
# - Extract the column to sort by (using letter, index, or name)
|
|
377
|
+
# - Convert the column values to the desired data type if specified (str, int, float, datetime)
|
|
378
|
+
# - Track which columns to sort and in which order (ascending or descending)
|
|
379
|
+
#
|
|
380
|
+
# This ensures the DataFrame is sorted correctly, even when types like dates or numbers need conversion.
|
|
381
|
+
if sorting_keys:
|
|
382
|
+
sort_columns = []
|
|
383
|
+
ascending_flags = []
|
|
384
|
+
|
|
385
|
+
for item in sorting_keys:
|
|
386
|
+
key = item.get("key")
|
|
387
|
+
ascending = item.get("ascending", True)
|
|
388
|
+
dtype = item.get("type")
|
|
389
|
+
|
|
390
|
+
if isinstance(key, int):
|
|
391
|
+
col_name = header[key]
|
|
392
|
+
|
|
393
|
+
elif isinstance(key, str) and key.isalpha():
|
|
394
|
+
col_name = header[ord(key.upper()) - ord("A")]
|
|
395
|
+
|
|
396
|
+
else:
|
|
397
|
+
col_name = key
|
|
398
|
+
|
|
399
|
+
sort_columns.append(col_name)
|
|
400
|
+
ascending_flags.append(ascending)
|
|
401
|
+
|
|
402
|
+
if dtype == "datetime":
|
|
403
|
+
df[col_name] = pd.to_datetime(df[col_name], dayfirst=True, errors="coerce")
|
|
404
|
+
|
|
405
|
+
elif dtype == "int":
|
|
406
|
+
df[col_name] = pd.to_numeric(df[col_name], errors="coerce", downcast="integer")
|
|
407
|
+
|
|
408
|
+
elif dtype == "float":
|
|
409
|
+
df[col_name] = pd.to_numeric(df[col_name], errors="coerce", downcast="float")
|
|
410
|
+
|
|
411
|
+
elif dtype == "str":
|
|
412
|
+
df[col_name] = df[col_name].astype(str)
|
|
413
|
+
|
|
414
|
+
# Step 4 – Sort
|
|
415
|
+
df.sort_values(by=sort_columns, ascending=ascending_flags, inplace=True)
|
|
416
|
+
|
|
417
|
+
# Step 5 - Overwrite worksheet
|
|
418
|
+
ws.delete_rows(1, ws.max_row)
|
|
419
|
+
|
|
420
|
+
ws.append(header)
|
|
421
|
+
|
|
422
|
+
for _, row in df.iterrows():
|
|
423
|
+
ws.append(list(row))
|
|
424
|
+
|
|
425
|
+
# Step 6 – Adjust column widths and apply wrapping if needed
|
|
426
|
+
#
|
|
427
|
+
# If column_widths is "auto":
|
|
428
|
+
# - Calculate the max content length in each column and set the column width accordingly (+2 for padding)
|
|
429
|
+
#
|
|
430
|
+
# If column_widths is a single int:
|
|
431
|
+
# - Use it as a global max width across all columns
|
|
432
|
+
# - If content fits, set width based on actual content length
|
|
433
|
+
# - If content exceeds the max width clamp column width and enable wrap_text for that column's cells
|
|
434
|
+
#
|
|
435
|
+
# Then, for wrapped cells, auto-adjust the row height:
|
|
436
|
+
# - Estimate how many lines the wrapped text would occupy and set row height accordingly to ensure all content is visible
|
|
437
|
+
if column_widths in (None, "auto"):
|
|
438
|
+
for col in ws.columns:
|
|
439
|
+
max_len = max(len(str(cell.value or "")) for cell in col)
|
|
440
|
+
|
|
441
|
+
ws.column_dimensions[col[0].column_letter].width = max_len + 2
|
|
442
|
+
|
|
443
|
+
elif isinstance(column_widths, int):
|
|
444
|
+
for col in ws.columns:
|
|
445
|
+
col_letter = col[0].column_letter
|
|
446
|
+
|
|
447
|
+
max_len = max(len(str(cell.value or "")) for cell in col)
|
|
448
|
+
|
|
449
|
+
# If content fits, auto-size
|
|
450
|
+
if max_len + 2 <= column_widths:
|
|
451
|
+
ws.column_dimensions[col_letter].width = max_len + 2
|
|
452
|
+
|
|
453
|
+
# Else, cap width and enable wrap
|
|
454
|
+
else:
|
|
455
|
+
ws.column_dimensions[col_letter].width = column_widths
|
|
456
|
+
|
|
457
|
+
for cell in col:
|
|
458
|
+
cell.alignment = Alignment(wrap_text=True)
|
|
459
|
+
|
|
460
|
+
# Here we handle row height
|
|
461
|
+
for row in ws.iter_rows():
|
|
462
|
+
max_line_count = 1
|
|
463
|
+
|
|
464
|
+
for cell in row:
|
|
465
|
+
if cell.value and cell.alignment and cell.alignment.wrap_text:
|
|
466
|
+
col_letter = cell.column_letter
|
|
467
|
+
col_width = ws.column_dimensions[col_letter].width or 10
|
|
468
|
+
chars_per_line = col_width * 1.2
|
|
469
|
+
lines = str(cell.value).split("\n")
|
|
470
|
+
line_count = sum(math.ceil(len(line) / chars_per_line) for line in lines)
|
|
471
|
+
max_line_count = max(max_line_count, line_count)
|
|
472
|
+
|
|
473
|
+
ws.row_dimensions[row[0].row].height = max_line_count * 20
|
|
474
|
+
|
|
475
|
+
else:
|
|
476
|
+
raise ValueError(f"Column width provided with incorrect datatype - datatype int expected, instead column width is of datatype {type(column_widths)}")
|
|
477
|
+
|
|
478
|
+
# Step 7 - Freeze panes if needed
|
|
479
|
+
if freeze_panes:
|
|
480
|
+
ws.freeze_panes = freeze_panes
|
|
481
|
+
|
|
482
|
+
# Step 8 – Apply base formatting
|
|
483
|
+
# For each cell in the worksheet:
|
|
484
|
+
# - Apply font styling based on either a custom `font_config` (row-specific) or default to bold/italic based on row number (e.g., header rows)
|
|
485
|
+
# - Set horizontal and vertical alignment for consistent layout
|
|
486
|
+
# - Disable text wrapping by default (wrapping will be handled later if needed)
|
|
487
|
+
#
|
|
488
|
+
# This ensures a clean, uniform look across the sheet while allowing for custom styling where defined.
|
|
489
|
+
for row_idx, row in enumerate(ws.iter_rows(), start=1):
|
|
490
|
+
for cell in row:
|
|
491
|
+
if font_config and row_idx in font_config:
|
|
492
|
+
config = font_config[row_idx]
|
|
493
|
+
|
|
494
|
+
cell.font = Font(
|
|
495
|
+
name=config.get("name", "Calibri"),
|
|
496
|
+
size=config.get("size", 11),
|
|
497
|
+
bold=config.get("bold", False),
|
|
498
|
+
italic=config.get("italic", False),
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
else:
|
|
502
|
+
cell.font = Font(
|
|
503
|
+
bold=row_idx in bold_rows if bold_rows else False,
|
|
504
|
+
italic=row_idx in italic_rows if italic_rows else False,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
cell.alignment = Alignment(
|
|
508
|
+
horizontal=align_horizontal,
|
|
509
|
+
vertical=align_vertical,
|
|
510
|
+
wrap_text=cell.alignment.wrap_text
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# Step 9 - Save and re-upload
|
|
514
|
+
temp_stream = BytesIO()
|
|
515
|
+
|
|
516
|
+
wb.save(temp_stream)
|
|
517
|
+
|
|
518
|
+
temp_stream.seek(0)
|
|
519
|
+
|
|
520
|
+
self.upload_file_from_bytes(temp_stream.getvalue(), excel_file_name, folder_name)
|
|
@@ -99,11 +99,18 @@ class BaseUI:
|
|
|
99
99
|
|
|
100
100
|
def close_window(self, window_to_close: auto.WindowControl) -> None:
|
|
101
101
|
"""Closes specified window."""
|
|
102
|
+
window_name = window_to_close.Name
|
|
102
103
|
window_to_close.SetFocus()
|
|
103
104
|
window_to_close.GetWindowPattern().Close()
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
pop_up_window.ButtonControl(Name="Ja").Click(simulateMove=False, waitTime=0)
|
|
106
|
+
# Handle popup when closin main window
|
|
107
|
+
if window_name.lower().startswith("hovedvindue"):
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
pop_up_window = window_to_close.WindowControl(Name="TMT - Afslut")
|
|
110
|
+
pop_up_window.SetFocus()
|
|
111
|
+
pop_up_window.ButtonControl(Name="Ja").Click(simulateMove=False, waitTime=0)
|
|
112
|
+
|
|
113
|
+
time.sleep(2)
|
|
114
|
+
|
|
115
|
+
else:
|
|
116
|
+
self.app_window = self.wait_for_control(search_params={'AutomationId': 'FormFront'}, control_type=auto.WindowControl)
|
|
@@ -97,6 +97,8 @@ class DocumentHandler(HandlerBase):
|
|
|
97
97
|
Under “Print/Flet patienter” → select template → merge → wait for Word to open,
|
|
98
98
|
convert to PDF, kill WINWORD.EXE, then create_document() with the new PDF.
|
|
99
99
|
"""
|
|
100
|
+
folder_path = rf"{os.environ.get('USERPROFILE')}\AppData\Local\Temp\Care\TMTand"
|
|
101
|
+
|
|
100
102
|
try:
|
|
101
103
|
self.open_tab("Stamkort")
|
|
102
104
|
|
|
@@ -169,8 +171,8 @@ class DocumentHandler(HandlerBase):
|
|
|
169
171
|
if new_value != metadata['templateName']:
|
|
170
172
|
raise ValueError(f"Failed to set the correct status. Expected '{metadata['templateName']}', but got '{new_value}'.")
|
|
171
173
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
if os.path.exists(folder_path):
|
|
175
|
+
shutil.rmtree(folder_path, ignore_errors=True)
|
|
174
176
|
|
|
175
177
|
form_mail_merge.PaneControl(AutomationId="ButtonMerge").GetLegacyIAccessiblePattern().DoDefaultAction()
|
|
176
178
|
|
|
@@ -229,7 +231,8 @@ class DocumentHandler(HandlerBase):
|
|
|
229
231
|
print(f"Error while creating document from template: {e}")
|
|
230
232
|
raise
|
|
231
233
|
finally:
|
|
232
|
-
|
|
234
|
+
if os.path.exists(folder_path):
|
|
235
|
+
shutil.rmtree(folder_path, ignore_errors=True)
|
|
233
236
|
|
|
234
237
|
def send_discharge_document_digitalpost(self, metadata: dict) -> None:
|
|
235
238
|
"""
|
mbu_dev_shared_components-2.4.4/mbu_dev_shared_components/tests/go_tests/go_integration_tests.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration tests for the GetOrganized API.
|
|
3
|
+
|
|
4
|
+
These tests validate the following key functionalities:
|
|
5
|
+
1. Authentication success using CPR lookup
|
|
6
|
+
2. Contact lookup returns correct citizen profile
|
|
7
|
+
3. Retrieval of metadata for a specific case (Borgermappe)
|
|
8
|
+
4. Case lookup using case properties
|
|
9
|
+
5. Document metadata retrieval
|
|
10
|
+
6. Document search (legacy and modern search APIs)
|
|
11
|
+
|
|
12
|
+
Expected to run daily to ensure API stability and data consistency.
|
|
13
|
+
|
|
14
|
+
Required environment variables:
|
|
15
|
+
- GO_API_ENDPOINT: Base URL for GetOrganized API
|
|
16
|
+
- GO_API_USERNAME: API username for authentication
|
|
17
|
+
- GO_API_PASSWORD: API password
|
|
18
|
+
- DADJ_FULL_NAME: Full name of test citizen
|
|
19
|
+
- DADJ_SSN: CPR number of test citizen
|
|
20
|
+
- DADJ_GO_ID: Internal ID of test citizen in GO
|
|
21
|
+
- DADJ_BORGERMAPPE_SAGS_ID: Case ID of the test citizen's "Borgermappe"
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import xml.etree.ElementTree as ET
|
|
26
|
+
import requests
|
|
27
|
+
import pytest
|
|
28
|
+
|
|
29
|
+
from mbu_dev_shared_components.getorganized import contacts
|
|
30
|
+
from mbu_dev_shared_components.getorganized import cases
|
|
31
|
+
from mbu_dev_shared_components.getorganized import documents
|
|
32
|
+
from mbu_dev_shared_components.getorganized import objects
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# -------------------------------
|
|
36
|
+
# Helper to load environment vars safely
|
|
37
|
+
# -------------------------------
|
|
38
|
+
def _get_cfg(key: str) -> str:
|
|
39
|
+
val = os.getenv(key)
|
|
40
|
+
|
|
41
|
+
if not val:
|
|
42
|
+
pytest.skip(f"env var '{key}' not set → skipping integration test")
|
|
43
|
+
|
|
44
|
+
return val
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.fixture(scope="module")
|
|
48
|
+
def go_env():
|
|
49
|
+
"""
|
|
50
|
+
Loads required environment variables for the GetOrganized API tests.
|
|
51
|
+
Skips tests if any required variable is missing.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
"endpoint": _get_cfg("GO_API_ENDPOINT"),
|
|
56
|
+
"username": _get_cfg("GO_API_USERNAME"),
|
|
57
|
+
"password": _get_cfg("GO_API_PASSWORD"),
|
|
58
|
+
"full_name": _get_cfg("DADJ_FULL_NAME"),
|
|
59
|
+
"ssn": _get_cfg("DADJ_SSN"),
|
|
60
|
+
"go_id": _get_cfg("DADJ_GO_ID"),
|
|
61
|
+
"case_id": _get_cfg("DADJ_BORGERMAPPE_SAGS_ID"),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_authentication_success(go_env):
|
|
66
|
+
"""
|
|
67
|
+
Ensures valid credentials can access the API without 401/403.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
resp = contacts.contact_lookup(
|
|
71
|
+
person_ssn=go_env["ssn"],
|
|
72
|
+
api_endpoint=f"{go_env['endpoint']}/_goapi/contacts/readitem",
|
|
73
|
+
api_username=go_env["username"],
|
|
74
|
+
api_password=go_env["password"],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
assert resp.status_code != 401
|
|
78
|
+
assert resp.status_code != 403
|
|
79
|
+
assert resp.ok
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_contact_lookup_returns_expected_name(go_env):
|
|
83
|
+
"""
|
|
84
|
+
Verifies that contact lookup returns correct full name, ID, and CPR.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
resp = contacts.contact_lookup(
|
|
88
|
+
person_ssn=go_env["ssn"],
|
|
89
|
+
api_endpoint=f"{go_env['endpoint']}/_goapi/contacts/readitem",
|
|
90
|
+
api_username=go_env["username"],
|
|
91
|
+
api_password=go_env["password"],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
assert resp.ok
|
|
95
|
+
|
|
96
|
+
data = resp.json()
|
|
97
|
+
|
|
98
|
+
assert data["FullName"] == go_env["full_name"]
|
|
99
|
+
assert data["ID"] == go_env["go_id"]
|
|
100
|
+
assert data["CPR"] == go_env["ssn"]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_case_metadata_structure(go_env):
|
|
104
|
+
"""
|
|
105
|
+
Validates the metadata structure of a known 'Borgermappe' case.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
resp = cases.get_case_metadata(
|
|
109
|
+
api_endpoint=f"{go_env['endpoint']}/_goapi/Cases/Metadata/{go_env['case_id']}",
|
|
110
|
+
api_username=go_env["username"],
|
|
111
|
+
api_password=go_env["password"],
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
assert resp.ok
|
|
115
|
+
|
|
116
|
+
# Extract XML metadata and parse attributes
|
|
117
|
+
resp_metadata_xml = resp.json().get("Metadata")
|
|
118
|
+
|
|
119
|
+
assert resp_metadata_xml
|
|
120
|
+
|
|
121
|
+
data = ET.fromstring(resp_metadata_xml).attrib
|
|
122
|
+
|
|
123
|
+
assert data["ows_CaseID"] == go_env["case_id"]
|
|
124
|
+
|
|
125
|
+
assert data["ows_CaseCategory"] == "Borgermappe"
|
|
126
|
+
|
|
127
|
+
assert data["ows_CCMContactData"] == f"{go_env['full_name']};#{go_env['go_id']};#{go_env['ssn']};#;#"
|
|
128
|
+
|
|
129
|
+
assert data["ows_CCMContactData_CPR"] == go_env["ssn"]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_find_case_by_case_properties(go_env):
|
|
133
|
+
"""
|
|
134
|
+
Searches for a case using known properties of the test citizen.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
case_data_json = objects.CaseDataJson()
|
|
138
|
+
|
|
139
|
+
case_data = case_data_json.search_citizen_folder_data_json(
|
|
140
|
+
case_type_prefix="BOR",
|
|
141
|
+
person_full_name=go_env["full_name"],
|
|
142
|
+
person_id=go_env["go_id"],
|
|
143
|
+
person_ssn=go_env["ssn"]
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
resp = cases.find_case_by_case_properties(
|
|
147
|
+
case_data=case_data,
|
|
148
|
+
api_endpoint=f"{go_env['endpoint']}/_goapi/Cases/FindByCaseProperties",
|
|
149
|
+
api_username=go_env["username"],
|
|
150
|
+
api_password=go_env["password"],
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
assert resp.ok
|
|
154
|
+
|
|
155
|
+
data = resp.json().get("CasesInfo")
|
|
156
|
+
|
|
157
|
+
assert isinstance(data, list)
|
|
158
|
+
|
|
159
|
+
assert len(data) == 1
|
|
160
|
+
|
|
161
|
+
assert data[0].get("CaseID") == go_env["case_id"]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_get_document_metadata(go_env):
|
|
165
|
+
"""
|
|
166
|
+
Fetches metadata for a known document tied to the test citizen.
|
|
167
|
+
"""
|
|
168
|
+
document_id = 14583373 # ID must be valid for test profile
|
|
169
|
+
|
|
170
|
+
resp = documents.get_document_metadata(
|
|
171
|
+
api_endpoint=f"{go_env['endpoint']}/_goapi/Documents/Metadata/{document_id}",
|
|
172
|
+
api_username=go_env["username"],
|
|
173
|
+
api_password=go_env["password"],
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
assert resp.ok
|
|
177
|
+
|
|
178
|
+
data = ET.fromstring(resp.json().get("Metadata")).attrib
|
|
179
|
+
|
|
180
|
+
assert "ows_Title" in data
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_search_documents(go_env):
|
|
184
|
+
"""
|
|
185
|
+
Performs a legacy document search using full name + CPR.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
search_term = f"{go_env['full_name']} {go_env['ssn']}"
|
|
189
|
+
|
|
190
|
+
resp = documents.search_documents(
|
|
191
|
+
search_term=search_term,
|
|
192
|
+
api_endpoint=f"{go_env['endpoint']}/_goapi/Search/Results",
|
|
193
|
+
api_username=go_env["username"],
|
|
194
|
+
api_password=go_env["password"],
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
assert resp.ok
|
|
198
|
+
|
|
199
|
+
results = resp.json().get("Rows").get("Results")
|
|
200
|
+
total_rows = resp.json().get("TotalRows")
|
|
201
|
+
|
|
202
|
+
assert isinstance(results, list)
|
|
203
|
+
|
|
204
|
+
if total_rows == 0:
|
|
205
|
+
assert len(results) == 0
|
|
206
|
+
|
|
207
|
+
else:
|
|
208
|
+
assert len(results) == total_rows
|
|
209
|
+
|
|
210
|
+
for doc in results:
|
|
211
|
+
assert "title" in doc
|
|
212
|
+
assert "created" in doc
|
|
213
|
+
assert "caseid" in doc
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _validate_modern_search_response(resp: requests.Response):
|
|
217
|
+
"""
|
|
218
|
+
Helper: Validates structure of modern search response.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
assert resp.ok
|
|
222
|
+
|
|
223
|
+
json_data = resp.json()
|
|
224
|
+
|
|
225
|
+
results = json_data.get("results", {}).get("Results", [])
|
|
226
|
+
|
|
227
|
+
total_rows = json_data.get("totalRows")
|
|
228
|
+
|
|
229
|
+
assert isinstance(results, list)
|
|
230
|
+
|
|
231
|
+
if total_rows == 0:
|
|
232
|
+
assert len(results) == 0
|
|
233
|
+
|
|
234
|
+
else:
|
|
235
|
+
assert len(results) == total_rows
|
|
236
|
+
for doc in results:
|
|
237
|
+
|
|
238
|
+
assert "title" in doc
|
|
239
|
+
assert "created" in doc
|
|
240
|
+
assert "caseid" in doc
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_modern_search(go_env):
|
|
244
|
+
"""
|
|
245
|
+
Runs two modern searches: one without date filter, one with date filter.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
search_term = f"{go_env['full_name']} {go_env['ssn']}"
|
|
249
|
+
|
|
250
|
+
# Search 1: All-time
|
|
251
|
+
resp_1 = documents.modern_search(
|
|
252
|
+
page_index=0,
|
|
253
|
+
search_term=search_term,
|
|
254
|
+
start_date=None,
|
|
255
|
+
end_date=None,
|
|
256
|
+
only_items=False,
|
|
257
|
+
case_type_prefix="BOR",
|
|
258
|
+
api_endpoint=f"{go_env['endpoint']}/_goapi/search/ExecuteModernSearch",
|
|
259
|
+
api_username=go_env["username"],
|
|
260
|
+
api_password=go_env["password"],
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Search 2: Specific March 2025 range
|
|
264
|
+
resp_2 = documents.modern_search(
|
|
265
|
+
page_index=0,
|
|
266
|
+
search_term=search_term,
|
|
267
|
+
start_date="2025-03-01",
|
|
268
|
+
end_date="2025-03-31",
|
|
269
|
+
only_items=False,
|
|
270
|
+
case_type_prefix="BOR",
|
|
271
|
+
api_endpoint=f"{go_env['endpoint']}/_goapi/search/ExecuteModernSearch",
|
|
272
|
+
api_username=go_env["username"],
|
|
273
|
+
api_password=go_env["password"],
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
_validate_modern_search_response(resp_1)
|
|
277
|
+
|
|
278
|
+
_validate_modern_search_response(resp_2)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for data structure creation methods in CaseDataJson.
|
|
3
|
+
These are pure functions that return formatted dictionaries, used in API requests.
|
|
4
|
+
|
|
5
|
+
Should run on pull requests to ensure the data structure is correct.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from mbu_dev_shared_components.getorganized.objects import CaseDataJson
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def case_data_handler():
|
|
14
|
+
"""
|
|
15
|
+
Fixture to provide an instance of CaseDataJson for testing.
|
|
16
|
+
"""
|
|
17
|
+
return CaseDataJson()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_search_citizen_folder_data_json(case_data_handler: CaseDataJson):
|
|
21
|
+
"""
|
|
22
|
+
Ensure that the search_citizen_folder_data_json method correctly builds
|
|
23
|
+
a search structure for a citizen's full data and filters by 'Borgermappe' case category.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
result = case_data_handler.search_citizen_folder_data_json(
|
|
27
|
+
case_type_prefix="BOR",
|
|
28
|
+
person_full_name="Test Person",
|
|
29
|
+
person_id="12345",
|
|
30
|
+
person_ssn="0101011234"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert result["LogicalOperator"] == "AND"
|
|
34
|
+
assert result["ExcludeDeletedCases"] == "True"
|
|
35
|
+
assert result["ReturnCasesNumber"] == "1"
|
|
36
|
+
|
|
37
|
+
assert {
|
|
38
|
+
"InternalName": "ows_CCMContactData",
|
|
39
|
+
"Value": "Test Person;#12345;#0101011234;#;#",
|
|
40
|
+
"DataType": "Text",
|
|
41
|
+
"ComparisonType": "Equal",
|
|
42
|
+
"IsMultiValue": "False"
|
|
43
|
+
} in result["FieldProperties"]
|
|
44
|
+
|
|
45
|
+
assert {
|
|
46
|
+
"InternalName": "ows_CaseCategory",
|
|
47
|
+
"Value": "Borgermappe",
|
|
48
|
+
"DataType": "Text",
|
|
49
|
+
"ComparisonType": "Equal",
|
|
50
|
+
"IsMultiValue": "False"
|
|
51
|
+
} in result["FieldProperties"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_generic_search_case_data_json_with_name_and_extra_field(case_data_handler: CaseDataJson):
|
|
55
|
+
"""
|
|
56
|
+
Ensure that generic_search_case_data_json assembles a correct structure when using:
|
|
57
|
+
- a custom case_type_prefix,
|
|
58
|
+
- a non-default number of return cases,
|
|
59
|
+
- full citizen data,
|
|
60
|
+
- and extra field properties.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
field_properties = {
|
|
64
|
+
"ows_Title": "Lønbilag"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
result = case_data_handler.generic_search_case_data_json(
|
|
68
|
+
case_type_prefix="PER",
|
|
69
|
+
person_full_name="Test Person",
|
|
70
|
+
person_id="12345",
|
|
71
|
+
person_ssn="0101011234",
|
|
72
|
+
include_name=True,
|
|
73
|
+
returned_cases_number="5",
|
|
74
|
+
field_properties=field_properties
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
assert result["CaseTypePrefixes"] == ["PER"]
|
|
78
|
+
|
|
79
|
+
assert {
|
|
80
|
+
"InternalName": "ows_CCMContactData",
|
|
81
|
+
"Value": "Test Person;#12345;#0101011234;#;#",
|
|
82
|
+
"DataType": "Text",
|
|
83
|
+
"ComparisonType": "Equal",
|
|
84
|
+
"IsMultiValue": "False"
|
|
85
|
+
} in result["FieldProperties"]
|
|
86
|
+
|
|
87
|
+
assert result["ReturnCasesNumber"] == "5"
|
|
88
|
+
|
|
89
|
+
assert {
|
|
90
|
+
"InternalName": "ows_Title",
|
|
91
|
+
"Value": "Lønbilag",
|
|
92
|
+
"DataType": "Text",
|
|
93
|
+
"ComparisonType": "Equal",
|
|
94
|
+
"IsMultiValue": "False"
|
|
95
|
+
} in result["FieldProperties"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_generic_search_case_data_json_without_name(case_data_handler: CaseDataJson):
|
|
99
|
+
"""
|
|
100
|
+
Ensure that generic_search_case_data_json builds correct contact data when include_name=False,
|
|
101
|
+
and only includes one field (ows_CCMContactData) by default when no additional fields are passed.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
result = case_data_handler.generic_search_case_data_json(
|
|
105
|
+
case_type_prefix="BOR",
|
|
106
|
+
person_full_name="Daniel Tester",
|
|
107
|
+
person_id="12345",
|
|
108
|
+
person_ssn="0101011234",
|
|
109
|
+
include_name=False
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
assert result["CaseTypePrefixes"] == ["BOR"]
|
|
113
|
+
|
|
114
|
+
assert {
|
|
115
|
+
"InternalName": "ows_CCMContactData",
|
|
116
|
+
"Value": ";#12345;#0101011234;#;#",
|
|
117
|
+
"DataType": "Text",
|
|
118
|
+
"ComparisonType": "Equal",
|
|
119
|
+
"IsMultiValue": "False"
|
|
120
|
+
} in result["FieldProperties"]
|
|
121
|
+
|
|
122
|
+
assert result["ReturnCasesNumber"] == "1"
|
|
123
|
+
|
|
124
|
+
assert len(result["FieldProperties"]) == 1
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration tests for verifying Microsoft Office Excel functionality through SharePoint.
|
|
3
|
+
|
|
4
|
+
Tests included:
|
|
5
|
+
1. `test_list_files_from_sharepoint_folder`:
|
|
6
|
+
- Verifies that a known test Excel file exists in the SharePoint folder.
|
|
7
|
+
|
|
8
|
+
2. `test_append_row_to_excel`:
|
|
9
|
+
- Appends a row to a specific sheet in the Excel file.
|
|
10
|
+
- Verifies the row was successfully added.
|
|
11
|
+
|
|
12
|
+
3. `test_format_and_sort_excel_file`:
|
|
13
|
+
- Sorts the Excel sheet by date/time (descending).
|
|
14
|
+
- Applies formatting (bold headers, auto column widths).
|
|
15
|
+
- Verifies that sorting has changed the order.
|
|
16
|
+
|
|
17
|
+
Important constants:
|
|
18
|
+
- FOLDER_NAME = "MSOffice tests"
|
|
19
|
+
- FILE_NAME = "Test_Append_Rows.xlsx"
|
|
20
|
+
- SHEET_NAME = "Upload logs"
|
|
21
|
+
- HEADERS = ["Upload date", "Upload time"]
|
|
22
|
+
|
|
23
|
+
Requires environment variables:
|
|
24
|
+
- MSOFFICE_USERNAME
|
|
25
|
+
- MSOFFICE_PASSWORD
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import os
|
|
29
|
+
import datetime
|
|
30
|
+
|
|
31
|
+
from io import BytesIO
|
|
32
|
+
|
|
33
|
+
import pytest
|
|
34
|
+
import openpyxl
|
|
35
|
+
|
|
36
|
+
from mbu_dev_shared_components.msoffice365.sharepoint_api.files import Sharepoint
|
|
37
|
+
|
|
38
|
+
FOLDER_NAME = "MSOffice tests"
|
|
39
|
+
CURRENT_DATE = datetime.datetime.now().strftime("%d-%m-%Y")
|
|
40
|
+
CURRENT_TIME = datetime.datetime.now().strftime("%H:%M")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture(scope="module")
|
|
44
|
+
def sharepoint_api():
|
|
45
|
+
"""
|
|
46
|
+
Authenticates with SharePoint and returns a Sharepoint API instance.
|
|
47
|
+
Skips test if required environment variables are missing.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def _get_cfg(key: str) -> str:
|
|
51
|
+
val = os.getenv(key)
|
|
52
|
+
|
|
53
|
+
if not val:
|
|
54
|
+
pytest.skip(f"env var '{key}' not set → skipping integration test")
|
|
55
|
+
|
|
56
|
+
return val
|
|
57
|
+
|
|
58
|
+
username = _get_cfg("MSOFFICE_USERNAME")
|
|
59
|
+
password = _get_cfg("MSOFFICE_PASSWORD")
|
|
60
|
+
|
|
61
|
+
site_url = "https://aarhuskommune.sharepoint.com"
|
|
62
|
+
site_name = "MBURPA"
|
|
63
|
+
document_library = "Delte dokumenter"
|
|
64
|
+
|
|
65
|
+
sp = Sharepoint(
|
|
66
|
+
username=username,
|
|
67
|
+
password=password,
|
|
68
|
+
site_url=site_url,
|
|
69
|
+
site_name=site_name,
|
|
70
|
+
document_library=document_library
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
assert sp.ctx is not None, "SharePoint authentication failed"
|
|
74
|
+
|
|
75
|
+
return sp
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.dependency(name="list_files")
|
|
79
|
+
def test_list_files_from_sharepoint_folder(sharepoint_api: Sharepoint):
|
|
80
|
+
"""
|
|
81
|
+
Test 1: Check that the expected Excel file exists in the SharePoint folder.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
files = sharepoint_api.fetch_files_list(FOLDER_NAME)
|
|
85
|
+
|
|
86
|
+
file_names = [f["Name"] for f in files]
|
|
87
|
+
|
|
88
|
+
assert "Test_Append_Rows.xlsx" in file_names
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.dependency(name="append_row", depends=["list_files"])
|
|
92
|
+
def test_append_row_to_excel(sharepoint_api: Sharepoint):
|
|
93
|
+
"""
|
|
94
|
+
Test 2: Append a row to the Excel file and verify it was added correctly.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
file_name = "Test_Append_Rows.xlsx"
|
|
98
|
+
|
|
99
|
+
sheet_name = "Upload logs"
|
|
100
|
+
|
|
101
|
+
headers = ["Upload date", "Upload time"]
|
|
102
|
+
|
|
103
|
+
data = {
|
|
104
|
+
"Upload date": CURRENT_DATE,
|
|
105
|
+
"Upload time": CURRENT_TIME
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Append the row to the SharePoint-hosted Excel file
|
|
109
|
+
sharepoint_api.append_row_to_sharepoint_excel(
|
|
110
|
+
required_headers=headers,
|
|
111
|
+
folder_name=FOLDER_NAME,
|
|
112
|
+
excel_file_name=file_name,
|
|
113
|
+
sheet_name=sheet_name,
|
|
114
|
+
new_row=data,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Fetch the updated file and check that the last row matches the new data
|
|
118
|
+
binary_file = sharepoint_api.fetch_file_using_open_binary(file_name, FOLDER_NAME)
|
|
119
|
+
|
|
120
|
+
wb = openpyxl.load_workbook(BytesIO(binary_file))
|
|
121
|
+
|
|
122
|
+
ws = wb[sheet_name]
|
|
123
|
+
|
|
124
|
+
newest_row = list(ws.iter_rows(values_only=True))[-1]
|
|
125
|
+
|
|
126
|
+
assert newest_row[0] == CURRENT_DATE
|
|
127
|
+
assert newest_row[1] == CURRENT_TIME
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.mark.dependency(depends=["append_row"])
|
|
131
|
+
def test_format_and_sort_excel_file(sharepoint_api: Sharepoint):
|
|
132
|
+
"""
|
|
133
|
+
Test 3: Format and sort the Excel file by date and time.
|
|
134
|
+
Ensures that sorting changes the order and formatting is applied.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
file_name = "Test_Append_Rows.xlsx"
|
|
138
|
+
|
|
139
|
+
sheet_name = "Upload logs"
|
|
140
|
+
|
|
141
|
+
# Download the file before sorting to capture original row order
|
|
142
|
+
test_file = sharepoint_api.fetch_file_using_open_binary(file_name, FOLDER_NAME)
|
|
143
|
+
|
|
144
|
+
wb = openpyxl.load_workbook(BytesIO(test_file))
|
|
145
|
+
|
|
146
|
+
ws = wb[sheet_name]
|
|
147
|
+
|
|
148
|
+
all_rows = list(ws.iter_rows(values_only=True))
|
|
149
|
+
|
|
150
|
+
top_before = all_rows[1] # First data row
|
|
151
|
+
|
|
152
|
+
bottom_before = all_rows[-1] # Last data row
|
|
153
|
+
|
|
154
|
+
# Sort by first two columns (date, time) in descending order
|
|
155
|
+
sorting_keys = [
|
|
156
|
+
{"key": "A", "ascending": False, "type": "str"},
|
|
157
|
+
{"key": "B", "ascending": False, "type": "str"},
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
# Apply sorting and formatting
|
|
161
|
+
sharepoint_api.format_and_sort_excel_file(
|
|
162
|
+
folder_name=FOLDER_NAME,
|
|
163
|
+
excel_file_name=file_name,
|
|
164
|
+
sheet_name=sheet_name,
|
|
165
|
+
sorting_keys=sorting_keys,
|
|
166
|
+
bold_rows=[1],
|
|
167
|
+
column_widths="auto",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Re-fetch the file after sorting and verify order has changed
|
|
171
|
+
test_file = sharepoint_api.fetch_file_using_open_binary(file_name, FOLDER_NAME)
|
|
172
|
+
|
|
173
|
+
wb = openpyxl.load_workbook(BytesIO(test_file))
|
|
174
|
+
|
|
175
|
+
ws = wb[sheet_name]
|
|
176
|
+
|
|
177
|
+
all_rows = list(ws.iter_rows(values_only=True))
|
|
178
|
+
|
|
179
|
+
top_after = all_rows[1]
|
|
180
|
+
|
|
181
|
+
bottom_after = all_rows[-1]
|
|
182
|
+
|
|
183
|
+
# Check that sorting actually changed the row order
|
|
184
|
+
assert top_before != top_after
|
|
185
|
+
assert bottom_before != bottom_after
|
|
186
|
+
|
|
187
|
+
# Check that the top row after sorting is the one just added
|
|
188
|
+
assert top_after[0] == CURRENT_DATE
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mbu_dev_shared_components
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.4
|
|
4
4
|
Summary: Shared components to use in RPA projects
|
|
5
5
|
Author-email: MBU <rpa@mbu.aarhus.dk>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,7 +19,11 @@ Requires-Dist: uiautomation
|
|
|
19
19
|
Requires-Dist: pillow
|
|
20
20
|
Requires-Dist: psutil
|
|
21
21
|
Requires-Dist: docx2pdf
|
|
22
|
+
Requires-Dist: pandas>=2.2.3
|
|
22
23
|
Requires-Dist: rawpy
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-dependency>=0.5.1; extra == "dev"
|
|
23
27
|
Dynamic: license-file
|
|
24
28
|
|
|
25
29
|
# MBU Dev Shared Components
|
|
@@ -49,6 +49,9 @@ mbu_dev_shared_components/solteqtand/application/journal_note.py
|
|
|
49
49
|
mbu_dev_shared_components/solteqtand/application/patient.py
|
|
50
50
|
mbu_dev_shared_components/solteqtand/database/__init__.py
|
|
51
51
|
mbu_dev_shared_components/solteqtand/database/db_handler.py
|
|
52
|
+
mbu_dev_shared_components/tests/go_tests/go_integration_tests.py
|
|
53
|
+
mbu_dev_shared_components/tests/go_tests/objects_tests.py
|
|
54
|
+
mbu_dev_shared_components/tests/msoffice_tests/msoffice_integration_tests.py
|
|
52
55
|
mbu_dev_shared_components/utils/__init__.py
|
|
53
56
|
mbu_dev_shared_components/utils/db_stored_procedure_executor.py
|
|
54
57
|
mbu_dev_shared_components/utils/fernet_encryptor.py
|
|
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "mbu_dev_shared_components"
|
|
11
|
-
version = "2.4.
|
|
11
|
+
version = "2.4.4" # Specify the version manually here
|
|
12
12
|
authors = [
|
|
13
13
|
{ name="MBU", email="rpa@mbu.aarhus.dk" },
|
|
14
14
|
]
|
|
@@ -31,5 +31,12 @@ dependencies = [
|
|
|
31
31
|
"pillow",
|
|
32
32
|
"psutil",
|
|
33
33
|
"docx2pdf",
|
|
34
|
+
"pandas >= 2.2.3",
|
|
34
35
|
"rawpy",
|
|
35
36
|
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest >= 7.0",
|
|
41
|
+
"pytest-dependency >= 0.5.1"
|
|
42
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|