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.

Files changed (128) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  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 +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +42 -49
  23. boris/config.py +141 -65
  24. boris/config_file.py +58 -67
  25. boris/connections.py +107 -61
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2373 -1786
  30. boris/core_qrc.py +15895 -10743
  31. boris/core_ui.py +943 -798
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +109 -8
  34. boris/dialog.py +482 -236
  35. boris/duration_widget.py +9 -14
  36. boris/edit_event.py +61 -31
  37. boris/edit_event_ui.py +208 -97
  38. boris/event_operations.py +408 -293
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +184 -223
  43. boris/export_observation.py +74 -100
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +644 -290
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +4 -4
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +20 -57
  51. boris/latency.py +31 -24
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +17 -19
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +533 -221
  60. boris/observation_operations.py +1025 -390
  61. boris/observation_ui.py +572 -362
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -68
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +25 -33
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +306 -83
  80. boris/preferences_ui.py +684 -227
  81. boris/project.py +448 -293
  82. boris/project_functions.py +671 -238
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -198
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +52 -35
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +627 -236
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +95 -29
  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.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/converters.ui +0 -289
  111. boris/core.qrc +0 -36
  112. boris/core.ui +0 -1556
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -850
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1069
  120. boris/project_server.py +0 -236
  121. boris/vlc.py +0 -10343
  122. boris/vlc_local.py +0 -90
  123. boris_behav_obs-8.12.dist-info/LICENSE.TXT +0 -674
  124. boris_behav_obs-8.12.dist-info/METADATA +0 -128
  125. boris_behav_obs-8.12.dist-info/RECORD +0 -108
  126. boris_behav_obs-8.12.dist-info/entry_points.txt +0 -3
  127. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  128. {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-2023 Olivier Friard
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 PyQt5.QtCore import QSettings
25
- from PyQt5.QtWidgets import QWidget
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
- try:
34
- ini_file_path = pl.Path.home() / pl.Path(".boris")
35
- if ini_file_path.is_file():
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
- except Exception:
39
- logging.warning(f"error during saving {widget_name} geometry")
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, default_geometry):
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
- if default_geometry != (0, 0):
55
- try:
56
- widget.resize(default_geometry[0], default_geometry[1])
57
- except Exception:
58
- logging.warning(f"error during restoring default")
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-2023 Olivier Friard
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(f"function add_image_overlay")
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("error removing overlay")
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]
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2023 Olivier Friard
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 PyQt5.QtWidgets import (
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 import_observations(self):
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
- fn = QFileDialog().getOpenFileName(
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 fileName:
57
- try:
58
- fromProject = json.loads(open(fileName, "r").read())
59
- except Exception:
60
- QMessageBox.critical(self, cfg.programName, "This project file seems corrupted")
61
- return
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
- # transform time to decimal
64
- fromProject = util.convert_time_to_decimal(fromProject) # function in utilities.py
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
- dbc = dialog.ChooseObservationsToImport(
67
- "Choose the observations to import:", sorted(list(fromProject[cfg.OBSERVATIONS].keys()))
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
- if dbc.exec_():
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()
71
205
 
72
- selected_observations = dbc.get_selected_observations()
73
- if selected_observations:
74
- flagImported = False
75
206
 
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]])
207
+ def import_observations(self):
208
+ """
209
+ import observations from project file
210
+ """
78
211
 
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]])
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
- for obsId in selected_observations:
216
+ if not file_name:
217
+ return
83
218
 
84
- # check if behaviors are in current project ethogram
85
- new_behav_set = set(
86
- [
87
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
88
- for event in fromProject[cfg.OBSERVATIONS][obsId][cfg.EVENTS]
89
- if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] not in behav_set
90
- ]
91
- )
92
- if new_behav_set:
93
- diag_result = dialog.MessageDialog(
94
- cfg.programName,
95
- (
96
- f"Some coded behaviors in <b>{obsId}</b> are "
97
- f"not defined in the ethogram:<br><b>{', '.join(new_behav_set)}</b>"
98
- ),
99
- ["Interrupt import", "Skip observation", "Import observation"],
100
- )
101
- if diag_result == "Interrupt import":
102
- return
103
- if diag_result == "Skip observation":
104
- continue
105
-
106
- # check if subjects are in current project
107
- new_subject_set = set(
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
+ )