boris-behav-obs 9.7.7__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.

Files changed (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
boris/gui_utilities.py ADDED
@@ -0,0 +1,135 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This program is free software; you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation; either version 2 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program; if not, write to the Free Software
18
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19
+ MA 02110-1301, USA.
20
+ """
21
+
22
+ import pathlib as pl
23
+ import logging
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
36
+
37
+
38
+ def save_geometry(widget: QWidget, widget_name: str):
39
+ """
40
+ save window geometry in ini file
41
+ """
42
+
43
+ ini_file_path = pl.Path.home() / pl.Path(".boris")
44
+ if ini_file_path.is_file():
45
+ try:
46
+ settings = QSettings(str(ini_file_path), QSettings.IniFormat)
47
+ settings.setValue(f"{widget_name} geometry", widget.saveGeometry())
48
+ except Exception:
49
+ logging.warning(f"error during saving {widget_name} geometry")
50
+
51
+
52
+ def restore_geometry(widget: QWidget, widget_name: str, default_width_height):
53
+ """
54
+ restore window geometry in ini file
55
+ """
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}")
65
+ try:
66
+ ini_file_path = pl.Path.home() / pl.Path(".boris")
67
+ if ini_file_path.is_file():
68
+ settings = QSettings(str(ini_file_path), QSettings.IniFormat)
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)
73
+ except Exception:
74
+ logging.warning(f"error during restoring {widget_name} geometry")
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 ADDED
@@ -0,0 +1,72 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+
7
+ This program is free software; you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation; either version 2 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with this program; if not, write to the Free Software
19
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20
+ MA 02110-1301, USA.
21
+
22
+ """
23
+
24
+ import logging
25
+ from . import config as cfg
26
+ from . import dialog
27
+
28
+
29
+ def add_image_overlay(self) -> None:
30
+ """
31
+ add an image overlay on video from an image
32
+ """
33
+
34
+ logging.debug("function add_image_overlay")
35
+
36
+ try:
37
+ w = dialog.Video_overlay_dialog()
38
+ items = [f"Player #{i + 1}" for i, _ in enumerate(self.dw_player)]
39
+ w.cb_player.addItems(items)
40
+ if not w.exec_():
41
+ return
42
+
43
+ idx = w.cb_player.currentIndex()
44
+
45
+ if cfg.OVERLAY not in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
46
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.OVERLAY] = {}
47
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.OVERLAY][str(idx + 1)] = {
48
+ "file name": w.le_file_path.text(),
49
+ "overlay position": w.le_overlay_position.text(),
50
+ "transparency": w.sb_overlay_transparency.value(),
51
+ }
52
+ self.overlays[idx] = self.dw_player[idx].player.create_image_overlay()
53
+ self.project_changed()
54
+ self.resize_dw(idx)
55
+
56
+ except Exception:
57
+ logging.debug("error in add_image_overlay function")
58
+
59
+
60
+ def remove_image_overlay(self) -> None:
61
+ """
62
+ remove image overlay from all players
63
+ """
64
+ keys_to_delete: list = []
65
+ for n_player in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO].get(cfg.OVERLAY, {}):
66
+ keys_to_delete.append(n_player)
67
+ try:
68
+ self.overlays[int(n_player) - 1].remove()
69
+ except Exception:
70
+ logging.debug("Error removing image overlay")
71
+ for n_player in keys_to_delete:
72
+ del self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.OVERLAY][n_player]
@@ -0,0 +1,242 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This program is free software; you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation; either version 2 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program; if not, write to the Free Software
18
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19
+ MA 02110-1301, USA.
20
+ """
21
+
22
+ import datetime
23
+ import gzip
24
+ import json
25
+ import pandas as pd
26
+ from pathlib import Path
27
+
28
+ from PySide6.QtWidgets import (
29
+ QMessageBox,
30
+ QFileDialog,
31
+ )
32
+
33
+ from . import config as cfg
34
+ from . import dialog
35
+ from . import utilities as util
36
+
37
+
38
+ def load_observations_from_boris_project(self, project_file_path: str):
39
+ """
40
+ import observations from a BORIS project file
41
+ """
42
+
43
+ if self.projectFileName and project_file_path == self.projectFileName:
44
+ QMessageBox.critical(
45
+ None,
46
+ cfg.programName,
47
+ "This project is already open",
48
+ QMessageBox.Ok | QMessageBox.Default,
49
+ QMessageBox.NoButton,
50
+ )
51
+ return
52
+
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
67
+
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
157
+
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,
167
+ )
168
+ return
169
+
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, ' ', '&#9608;') + '</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()
205
+
206
+
207
+ def import_observations(self):
208
+ """
209
+ import observations from project file
210
+ """
211
+
212
+ file_name, _ = QFileDialog().getOpenFileName(
213
+ None, "Choose a file", "", "BORIS project files (*.boris *.boris.gz);;Spreadsheet files (*.ods *.xlsx *);;All files (*)"
214
+ )
215
+
216
+ if not file_name:
217
+ return
218
+
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
+ )