boris-behav-obs 8.12__py3-none-any.whl → 9.7.6__py3-none-any.whl
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.
Potentially problematic release.
This version of boris-behav-obs might be problematic. Click here for more details.
- boris/__init__.py +1 -1
- boris/__main__.py +1 -1
- boris/about.py +28 -39
- boris/add_modifier.py +122 -109
- boris/add_modifier_ui.py +239 -135
- boris/advanced_event_filtering.py +81 -45
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +228 -229
- boris/behavior_binary_table.py +33 -50
- boris/behaviors_coding_map.py +17 -18
- boris/boris_cli.py +6 -25
- boris/cmd_arguments.py +12 -1
- boris/coding_pad.py +42 -49
- boris/config.py +141 -65
- boris/config_file.py +58 -67
- boris/connections.py +107 -61
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2373 -1786
- boris/core_qrc.py +15895 -10743
- boris/core_ui.py +943 -798
- boris/db_functions.py +17 -42
- boris/dev.py +109 -8
- boris/dialog.py +482 -236
- boris/duration_widget.py +9 -14
- boris/edit_event.py +61 -31
- boris/edit_event_ui.py +208 -97
- boris/event_operations.py +408 -293
- boris/events_cursor.py +25 -17
- boris/events_snapshots.py +36 -82
- boris/exclusion_matrix.py +4 -9
- boris/export_events.py +184 -223
- boris/export_observation.py +74 -100
- boris/external_processes.py +123 -98
- boris/geometric_measurement.py +644 -290
- boris/gui_utilities.py +91 -14
- boris/image_overlay.py +4 -4
- boris/import_observations.py +190 -98
- boris/ipc_mpv.py +325 -0
- boris/irr.py +20 -57
- boris/latency.py +31 -24
- boris/measurement_widget.py +14 -18
- boris/media_file.py +17 -19
- boris/menu_options.py +17 -6
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +7 -9
- boris/mpv.py +1 -0
- boris/mpv2.py +732 -705
- boris/observation.py +533 -221
- boris/observation_operations.py +1025 -390
- boris/observation_ui.py +572 -362
- boris/observations_list.py +71 -53
- boris/otx_parser.py +74 -68
- boris/param_panel.py +31 -16
- boris/param_panel_ui.py +254 -138
- boris/player_dock_widget.py +90 -60
- boris/plot_data_module.py +25 -33
- boris/plot_events.py +127 -90
- boris/plot_events_rt.py +17 -31
- boris/plot_spectrogram_rt.py +95 -30
- boris/plot_waveform_rt.py +32 -21
- boris/plugins.py +431 -0
- boris/portion/__init__.py +18 -8
- boris/portion/const.py +35 -18
- boris/portion/dict.py +5 -5
- boris/portion/func.py +2 -2
- boris/portion/interval.py +21 -41
- boris/portion/io.py +41 -32
- boris/preferences.py +306 -83
- boris/preferences_ui.py +684 -227
- boris/project.py +448 -293
- boris/project_functions.py +671 -238
- boris/project_import_export.py +213 -222
- boris/project_ui.py +674 -438
- boris/qrc_boris.py +6 -3
- boris/qrc_boris5.py +6 -3
- boris/select_modifiers.py +74 -48
- boris/select_observations.py +20 -198
- boris/select_subj_behav.py +67 -39
- boris/state_events.py +52 -35
- boris/subjects_pad.py +6 -9
- boris/synthetic_time_budget.py +45 -28
- boris/time_budget_functions.py +171 -171
- boris/time_budget_widget.py +84 -114
- boris/transitions.py +41 -47
- boris/utilities.py +627 -236
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +95 -29
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
- {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
- boris/README.TXT +0 -22
- boris/add_modifier.ui +0 -323
- boris/converters.ui +0 -289
- boris/core.qrc +0 -36
- boris/core.ui +0 -1556
- boris/edit_event.ui +0 -233
- boris/icons/logo_eye.ico +0 -0
- boris/map_creator.py +0 -850
- boris/observation.ui +0 -814
- boris/param_panel.ui +0 -379
- boris/preferences.ui +0 -537
- boris/project.ui +0 -1069
- boris/project_server.py +0 -236
- boris/vlc.py +0 -10343
- boris/vlc_local.py +0 -90
- boris_behav_obs-8.12.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.12.dist-info/METADATA +0 -128
- boris_behav_obs-8.12.dist-info/RECORD +0 -108
- boris_behav_obs-8.12.dist-info/entry_points.txt +0 -3
- {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
boris/gui_utilities.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This program is free software; you can redistribute it and/or modify
|
|
7
7
|
it under the terms of the GNU General Public License as published by
|
|
@@ -21,8 +21,18 @@ Copyright 2012-2023 Olivier Friard
|
|
|
21
21
|
|
|
22
22
|
import pathlib as pl
|
|
23
23
|
import logging
|
|
24
|
-
from
|
|
25
|
-
from
|
|
24
|
+
from PySide6.QtCore import QSettings
|
|
25
|
+
from PySide6.QtWidgets import QWidget, QApplication
|
|
26
|
+
from PySide6.QtGui import QIcon
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def theme_mode() -> str:
|
|
30
|
+
"""
|
|
31
|
+
return the theme mode (dark or light) of the OS
|
|
32
|
+
"""
|
|
33
|
+
palette = QApplication.instance().palette()
|
|
34
|
+
color = palette.window().color()
|
|
35
|
+
return "dark" if color.value() < 128 else "light" # Dark mode if the color value is less than 128
|
|
26
36
|
|
|
27
37
|
|
|
28
38
|
def save_geometry(widget: QWidget, widget_name: str):
|
|
@@ -30,29 +40,96 @@ def save_geometry(widget: QWidget, widget_name: str):
|
|
|
30
40
|
save window geometry in ini file
|
|
31
41
|
"""
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
ini_file_path = pl.Path.home() / pl.Path(".boris")
|
|
44
|
+
if ini_file_path.is_file():
|
|
45
|
+
try:
|
|
36
46
|
settings = QSettings(str(ini_file_path), QSettings.IniFormat)
|
|
37
47
|
settings.setValue(f"{widget_name} geometry", widget.saveGeometry())
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
except Exception:
|
|
49
|
+
logging.warning(f"error during saving {widget_name} geometry")
|
|
40
50
|
|
|
41
51
|
|
|
42
|
-
def restore_geometry(widget: QWidget, widget_name: str,
|
|
52
|
+
def restore_geometry(widget: QWidget, widget_name: str, default_width_height):
|
|
43
53
|
"""
|
|
44
54
|
restore window geometry in ini file
|
|
45
55
|
"""
|
|
46
56
|
|
|
57
|
+
def default_resize(widget, default_width_height):
|
|
58
|
+
if default_width_height != (0, 0):
|
|
59
|
+
try:
|
|
60
|
+
widget.resize(default_width_height[0], default_width_height[1])
|
|
61
|
+
except Exception:
|
|
62
|
+
logging.warning("Error during restoring default")
|
|
63
|
+
|
|
64
|
+
logging.debug(f"restore geometry function for {widget_name}")
|
|
47
65
|
try:
|
|
48
66
|
ini_file_path = pl.Path.home() / pl.Path(".boris")
|
|
49
67
|
if ini_file_path.is_file():
|
|
50
68
|
settings = QSettings(str(ini_file_path), QSettings.IniFormat)
|
|
51
69
|
widget.restoreGeometry(settings.value(f"{widget_name} geometry"))
|
|
70
|
+
logging.debug(f"geometry restored for {widget_name} {settings.value(f'{widget_name} geometry')}")
|
|
71
|
+
else:
|
|
72
|
+
default_resize(widget, default_width_height)
|
|
52
73
|
except Exception:
|
|
53
74
|
logging.warning(f"error during restoring {widget_name} geometry")
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
default_resize(widget, default_width_height)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def set_icons(self, theme_mode: str) -> None:
|
|
79
|
+
"""
|
|
80
|
+
set icons of actions
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
# menu
|
|
84
|
+
self.action_obs_list.setIcon(QIcon(f":/observations_list_{theme_mode}"))
|
|
85
|
+
|
|
86
|
+
self.actionTime_budget.setIcon(QIcon(f":/time_budget_{theme_mode}"))
|
|
87
|
+
self.actionPlot_events2.setIcon(QIcon(f":/plot_events_{theme_mode}"))
|
|
88
|
+
self.action_advanced_event_filtering.setIcon(QIcon(f":/filter_{theme_mode}"))
|
|
89
|
+
|
|
90
|
+
self.actionPreferences.setIcon(QIcon(f":/preferences_{theme_mode}"))
|
|
91
|
+
|
|
92
|
+
self.actionPlay.setIcon(QIcon(f":/play_{theme_mode}"))
|
|
93
|
+
self.actionReset.setIcon(QIcon(f":/reset_{theme_mode}"))
|
|
94
|
+
self.actionJumpBackward.setIcon(QIcon(f":/jump_backward_{theme_mode}"))
|
|
95
|
+
self.actionJumpForward.setIcon(QIcon(f":/jump_forward_{theme_mode}"))
|
|
96
|
+
|
|
97
|
+
self.actionFaster.setIcon(QIcon(f":/faster_{theme_mode}"))
|
|
98
|
+
self.actionSlower.setIcon(QIcon(f":/slower_{theme_mode}"))
|
|
99
|
+
self.actionNormalSpeed.setIcon(QIcon(f":/normal_speed_{theme_mode}"))
|
|
100
|
+
|
|
101
|
+
self.actionPrevious.setIcon(QIcon(f":/previous_{theme_mode}"))
|
|
102
|
+
self.actionNext.setIcon(QIcon(f":/next_{theme_mode}"))
|
|
103
|
+
|
|
104
|
+
self.actionSnapshot.setIcon(QIcon(f":/snapshot_{theme_mode}"))
|
|
105
|
+
|
|
106
|
+
self.actionFrame_backward.setIcon(QIcon(f":/frame_backward_{theme_mode}"))
|
|
107
|
+
self.actionFrame_forward.setIcon(QIcon(f":/frame_forward_{theme_mode}"))
|
|
108
|
+
self.actionCloseObs.setIcon(QIcon(f":/close_observation_{theme_mode}"))
|
|
109
|
+
self.actionCurrent_Time_Budget.setIcon(QIcon(f":/time_budget_{theme_mode}"))
|
|
110
|
+
self.actionPlot_current_observation.setIcon(QIcon(f":/plot_events_{theme_mode}"))
|
|
111
|
+
|
|
112
|
+
self.actionPlot_events_in_real_time.setIcon(QIcon(f":/plot_real_time_{theme_mode}"))
|
|
113
|
+
|
|
114
|
+
self.actionBehavior_bar_plot.setIcon(QIcon(f":/plot_time_budget_{theme_mode}"))
|
|
115
|
+
self.actionPlot_current_time_budget.setIcon(QIcon(f":/plot_time_budget_{theme_mode}"))
|
|
116
|
+
self.action_geometric_measurements.setIcon(QIcon(f":/measurement_{theme_mode}"))
|
|
117
|
+
self.actionFind_in_current_obs.setIcon(QIcon(f":/find_{theme_mode}"))
|
|
118
|
+
self.actionExplore_project.setIcon(QIcon(f":/explore_{theme_mode}"))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def resize_center(app, window, width: int, height: int) -> None:
|
|
122
|
+
"""
|
|
123
|
+
resize and center window
|
|
124
|
+
"""
|
|
125
|
+
window.resize(width, height)
|
|
126
|
+
screen_geometry = app.primaryScreen().geometry()
|
|
127
|
+
if window.height() > screen_geometry.height():
|
|
128
|
+
window.resize(window.width(), int(screen_geometry.height() * 0.8))
|
|
129
|
+
if window.width() > screen_geometry.width():
|
|
130
|
+
window.resize(screen_geometry.width(), window.height())
|
|
131
|
+
# center
|
|
132
|
+
center_x = (screen_geometry.width() - window.width()) // 2
|
|
133
|
+
center_y = (screen_geometry.height() - window.height()) // 2
|
|
134
|
+
|
|
135
|
+
window.move(center_x, center_y)
|
boris/image_overlay.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
This program is free software; you can redistribute it and/or modify
|
|
@@ -31,7 +31,7 @@ def add_image_overlay(self) -> None:
|
|
|
31
31
|
add an image overlay on video from an image
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
|
-
logging.debug(
|
|
34
|
+
logging.debug("function add_image_overlay")
|
|
35
35
|
|
|
36
36
|
try:
|
|
37
37
|
w = dialog.Video_overlay_dialog()
|
|
@@ -66,7 +66,7 @@ def remove_image_overlay(self) -> None:
|
|
|
66
66
|
keys_to_delete.append(n_player)
|
|
67
67
|
try:
|
|
68
68
|
self.overlays[int(n_player) - 1].remove()
|
|
69
|
-
except:
|
|
70
|
-
logging.debug("
|
|
69
|
+
except Exception:
|
|
70
|
+
logging.debug("Error removing image overlay")
|
|
71
71
|
for n_player in keys_to_delete:
|
|
72
72
|
del self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.OVERLAY][n_player]
|
boris/import_observations.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This program is free software; you can redistribute it and/or modify
|
|
7
7
|
it under the terms of the GNU General Public License as published by
|
|
@@ -19,11 +19,13 @@ Copyright 2012-2023 Olivier Friard
|
|
|
19
19
|
MA 02110-1301, USA.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
import json
|
|
24
22
|
import datetime
|
|
23
|
+
import gzip
|
|
24
|
+
import json
|
|
25
|
+
import pandas as pd
|
|
26
|
+
from pathlib import Path
|
|
25
27
|
|
|
26
|
-
from
|
|
28
|
+
from PySide6.QtWidgets import (
|
|
27
29
|
QMessageBox,
|
|
28
30
|
QFileDialog,
|
|
29
31
|
)
|
|
@@ -33,17 +35,12 @@ from . import dialog
|
|
|
33
35
|
from . import utilities as util
|
|
34
36
|
|
|
35
37
|
|
|
36
|
-
def
|
|
38
|
+
def load_observations_from_boris_project(self, project_file_path: str):
|
|
37
39
|
"""
|
|
38
|
-
import observations from project file
|
|
40
|
+
import observations from a BORIS project file
|
|
39
41
|
"""
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
None, "Choose a BORIS project file", "", "Project files (*.boris);;All files (*)"
|
|
43
|
-
)
|
|
44
|
-
fileName = fn[0] if type(fn) is tuple else fn
|
|
45
|
-
|
|
46
|
-
if self.projectFileName and fileName == self.projectFileName:
|
|
43
|
+
if self.projectFileName and project_file_path == self.projectFileName:
|
|
47
44
|
QMessageBox.critical(
|
|
48
45
|
None,
|
|
49
46
|
cfg.programName,
|
|
@@ -53,98 +50,193 @@ def import_observations(self):
|
|
|
53
50
|
)
|
|
54
51
|
return
|
|
55
52
|
|
|
56
|
-
if
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
if project_file_path.endswith(".boris.gz"):
|
|
54
|
+
file_in = gzip.open(project_file_path, mode="rt", encoding="utf-8")
|
|
55
|
+
else:
|
|
56
|
+
file_in = open(project_file_path, "r")
|
|
57
|
+
file_content = file_in.read()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
fromProject = json.loads(file_content)
|
|
61
|
+
except Exception:
|
|
62
|
+
QMessageBox.critical(self, cfg.programName, "This project file seems corrupted")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# transform time to decimal
|
|
66
|
+
fromProject = util.convert_time_to_decimal(fromProject) # function in utilities.py
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
dbc = dialog.ChooseObservationsToImport("Choose the observations to import:", sorted(list(fromProject[cfg.OBSERVATIONS].keys())))
|
|
69
|
+
|
|
70
|
+
if not dbc.exec_():
|
|
71
|
+
return
|
|
72
|
+
selected_observations = dbc.get_selected_observations()
|
|
73
|
+
if selected_observations:
|
|
74
|
+
flagImported = False
|
|
75
|
+
|
|
76
|
+
# set of behaviors in current projet ethogram
|
|
77
|
+
behav_set = set([self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] for idx in self.pj[cfg.ETHOGRAM]])
|
|
78
|
+
|
|
79
|
+
# set of subjects in current projet
|
|
80
|
+
subjects_set = set([self.pj[cfg.SUBJECTS][idx][cfg.SUBJECT_NAME] for idx in self.pj[cfg.SUBJECTS]])
|
|
81
|
+
|
|
82
|
+
for obs_id in selected_observations:
|
|
83
|
+
# check if behaviors are in current project ethogram
|
|
84
|
+
new_behav_set = set(
|
|
85
|
+
[
|
|
86
|
+
event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
|
|
87
|
+
for event in fromProject[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]
|
|
88
|
+
if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] not in behav_set
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
if new_behav_set:
|
|
92
|
+
diag_result = dialog.MessageDialog(
|
|
93
|
+
cfg.programName,
|
|
94
|
+
(f"Some coded behaviors in <b>{obs_id}</b> are not defined in the ethogram:<br><b>{', '.join(new_behav_set)}</b>"),
|
|
95
|
+
["Interrupt import", "Skip observation", "Import observation"],
|
|
96
|
+
)
|
|
97
|
+
if diag_result == "Interrupt import":
|
|
98
|
+
return
|
|
99
|
+
if diag_result == "Skip observation":
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
# check if subjects are in current project
|
|
103
|
+
new_subject_set = set(
|
|
104
|
+
[
|
|
105
|
+
event[cfg.EVENT_SUBJECT_FIELD_IDX]
|
|
106
|
+
for event in fromProject[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]
|
|
107
|
+
if event[cfg.EVENT_SUBJECT_FIELD_IDX] not in subjects_set
|
|
108
|
+
]
|
|
109
|
+
)
|
|
110
|
+
if new_subject_set and new_subject_set != {""}:
|
|
111
|
+
diag_result = dialog.MessageDialog(
|
|
112
|
+
cfg.programName,
|
|
113
|
+
(f"Some coded subjects in <b>{obs_id}</b> are not defined in the project:<br><b>{', '.join(new_subject_set)}</b>"),
|
|
114
|
+
["Interrupt import", "Skip observation", "Import observation"],
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if diag_result == "Interrupt import":
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if diag_result == "Skip observation":
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if obs_id in self.pj[cfg.OBSERVATIONS].keys():
|
|
124
|
+
diag_result = dialog.MessageDialog(
|
|
125
|
+
cfg.programName,
|
|
126
|
+
(f"The observation <b>{obs_id}</b>already exists in the current project.<br>"),
|
|
127
|
+
["Interrupt import", "Skip observation", "Rename observation"],
|
|
128
|
+
)
|
|
129
|
+
if diag_result == "Interrupt import":
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
if diag_result == "Rename observation":
|
|
133
|
+
self.pj[cfg.OBSERVATIONS][f"{obs_id} (imported at {util.datetime_iso8601(datetime.datetime.now())})"] = dict(
|
|
134
|
+
fromProject[cfg.OBSERVATIONS][obs_id]
|
|
135
|
+
)
|
|
136
|
+
flagImported = True
|
|
137
|
+
else:
|
|
138
|
+
self.pj[cfg.OBSERVATIONS][obs_id] = dict(fromProject[cfg.OBSERVATIONS][obs_id])
|
|
139
|
+
flagImported = True
|
|
140
|
+
|
|
141
|
+
if flagImported:
|
|
142
|
+
QMessageBox.information(self, cfg.programName, "Observations imported successfully")
|
|
143
|
+
self.project_changed()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def load_observations_from_spreadsheet(self, project_file_path: str):
|
|
147
|
+
"""
|
|
148
|
+
import observations from a spreadsheet file
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
if Path(project_file_path).suffix.lower() == ".xlsx":
|
|
152
|
+
engine = "openpyxl"
|
|
153
|
+
elif Path(project_file_path).suffix.lower() == ".ods":
|
|
154
|
+
engine = "odf"
|
|
155
|
+
else:
|
|
156
|
+
return
|
|
65
157
|
|
|
66
|
-
|
|
67
|
-
|
|
158
|
+
try:
|
|
159
|
+
df = pd.read_excel(project_file_path, sheet_name=0, engine=engine)
|
|
160
|
+
except Exception:
|
|
161
|
+
QMessageBox.warning(
|
|
162
|
+
None,
|
|
163
|
+
cfg.programName,
|
|
164
|
+
("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
|
|
165
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
166
|
+
QMessageBox.NoButton,
|
|
68
167
|
)
|
|
168
|
+
return
|
|
69
169
|
|
|
70
|
-
|
|
170
|
+
expected_labels: list = ("time", "subject", "code", "modifier", "comment")
|
|
171
|
+
|
|
172
|
+
df.columns = df.columns.str.upper()
|
|
173
|
+
|
|
174
|
+
for column in expected_labels:
|
|
175
|
+
if column.upper() not in list(df.columns):
|
|
176
|
+
QMessageBox.warning(
|
|
177
|
+
None,
|
|
178
|
+
cfg.programName,
|
|
179
|
+
(
|
|
180
|
+
f"The {column} column was not found in the file header.<br>"
|
|
181
|
+
"For information the current file header contains the following labels:<br>"
|
|
182
|
+
f"{'<br>'.join(['<b>' + util.replace_leading_trailing_chars(x, ' ', '█') + '</b>' for x in df.columns])}<br>"
|
|
183
|
+
"<br>"
|
|
184
|
+
"The first row of the spreadsheet must contain the following labels:<br>"
|
|
185
|
+
f"{'<br>'.join(['<b>' + x + '</b>' for x in expected_labels])}<br>"
|
|
186
|
+
"<br>The order is not mandatory."
|
|
187
|
+
),
|
|
188
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
189
|
+
QMessageBox.NoButton,
|
|
190
|
+
)
|
|
191
|
+
return 1
|
|
192
|
+
event: dict = {}
|
|
193
|
+
events: list = []
|
|
194
|
+
for _, row in df.iterrows():
|
|
195
|
+
for label in expected_labels:
|
|
196
|
+
event[label] = row[label.upper()] if str(row[label.upper()]) != "nan" else ""
|
|
197
|
+
events.append([event["time"], event["subject"], event["code"], event["modifier"], event["comment"]])
|
|
198
|
+
|
|
199
|
+
if events:
|
|
200
|
+
self.pj[cfg.OBSERVATIONS][self.observationId]["events"].extend(events)
|
|
201
|
+
self.load_tw_events(self.observationId)
|
|
202
|
+
|
|
203
|
+
QMessageBox.information(self, cfg.programName, "Observations imported successfully")
|
|
204
|
+
self.project_changed()
|
|
71
205
|
|
|
72
|
-
selected_observations = dbc.get_selected_observations()
|
|
73
|
-
if selected_observations:
|
|
74
|
-
flagImported = False
|
|
75
206
|
|
|
76
|
-
|
|
77
|
-
|
|
207
|
+
def import_observations(self):
|
|
208
|
+
"""
|
|
209
|
+
import observations from project file
|
|
210
|
+
"""
|
|
78
211
|
|
|
79
|
-
|
|
80
|
-
|
|
212
|
+
file_name, _ = QFileDialog().getOpenFileName(
|
|
213
|
+
None, "Choose a file", "", "BORIS project files (*.boris *.boris.gz);;Spreadsheet files (*.ods *.xlsx *);;All files (*)"
|
|
214
|
+
)
|
|
81
215
|
|
|
82
|
-
|
|
216
|
+
if not file_name:
|
|
217
|
+
return
|
|
83
218
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
[
|
|
109
|
-
event[cfg.EVENT_SUBJECT_FIELD_IDX]
|
|
110
|
-
for event in fromProject[cfg.OBSERVATIONS][obsId][cfg.EVENTS]
|
|
111
|
-
if event[cfg.EVENT_SUBJECT_FIELD_IDX] not in subjects_set
|
|
112
|
-
]
|
|
113
|
-
)
|
|
114
|
-
if new_subject_set and new_subject_set != {""}:
|
|
115
|
-
diag_result = dialog.MessageDialog(
|
|
116
|
-
cfg.programName,
|
|
117
|
-
(
|
|
118
|
-
f"Some coded subjects in <b>{obsId}</b> are not defined in the project:<br>"
|
|
119
|
-
f"<b>{', '.join(new_subject_set)}</b>"
|
|
120
|
-
),
|
|
121
|
-
["Interrupt import", "Skip observation", "Import observation"],
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
if diag_result == "Interrupt import":
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
if diag_result == "Skip observation":
|
|
128
|
-
continue
|
|
129
|
-
|
|
130
|
-
if obsId in self.pj[cfg.OBSERVATIONS].keys():
|
|
131
|
-
diag_result = dialog.MessageDialog(
|
|
132
|
-
cfg.programName,
|
|
133
|
-
(f"The observation <b>{obsId}</b>" "already exists in the current project.<br>"),
|
|
134
|
-
["Interrupt import", "Skip observation", "Rename observation"],
|
|
135
|
-
)
|
|
136
|
-
if diag_result == "Interrupt import":
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
-
if diag_result == "Rename observation":
|
|
140
|
-
self.pj[cfg.OBSERVATIONS][
|
|
141
|
-
f"{obsId} (imported at {util.datetime_iso8601(datetime.datetime.now())})"
|
|
142
|
-
] = dict(fromProject[cfg.OBSERVATIONS][obsId])
|
|
143
|
-
flagImported = True
|
|
144
|
-
else:
|
|
145
|
-
self.pj[cfg.OBSERVATIONS][obsId] = dict(fromProject[cfg.OBSERVATIONS][obsId])
|
|
146
|
-
flagImported = True
|
|
147
|
-
|
|
148
|
-
if flagImported:
|
|
149
|
-
QMessageBox.information(self, cfg.programName, "Observations imported successfully")
|
|
150
|
-
self.project_changed()
|
|
219
|
+
if file_name.endswith(".boris") or file_name.endswith(".boris.gz"):
|
|
220
|
+
load_observations_from_boris_project(self, file_name)
|
|
221
|
+
|
|
222
|
+
elif Path(file_name).suffix.lower() in (".ods", ".xlsx"):
|
|
223
|
+
if not self.observationId:
|
|
224
|
+
QMessageBox.warning(
|
|
225
|
+
None,
|
|
226
|
+
cfg.programName,
|
|
227
|
+
("Please open or create a new observation before importing from a spreadsheet file"),
|
|
228
|
+
QMessageBox.Ok,
|
|
229
|
+
QMessageBox.NoButton,
|
|
230
|
+
)
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
load_observations_from_spreadsheet(self, file_name)
|
|
234
|
+
|
|
235
|
+
else:
|
|
236
|
+
QMessageBox.warning(
|
|
237
|
+
None,
|
|
238
|
+
cfg.programName,
|
|
239
|
+
("The type of file was not recognized. Must be a BORIS project or a Microsoft-Excel XLSX format or OpenDocument ODS"),
|
|
240
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
241
|
+
QMessageBox.NoButton,
|
|
242
|
+
)
|