boris-behav-obs 8.9.16__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 (129) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +36 -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 +161 -77
  24. boris/config_file.py +63 -83
  25. boris/connections.py +112 -57
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2511 -1824
  30. boris/core_qrc.py +15895 -10185
  31. boris/core_ui.py +946 -792
  32. boris/db_functions.py +21 -41
  33. boris/dev.py +134 -0
  34. boris/dialog.py +505 -244
  35. boris/duration_widget.py +15 -20
  36. boris/edit_event.py +84 -28
  37. boris/edit_event_ui.py +214 -78
  38. boris/event_operations.py +517 -415
  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 +213 -583
  43. boris/export_observation.py +98 -611
  44. boris/external_processes.py +156 -97
  45. boris/geometric_measurement.py +652 -287
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +9 -9
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +26 -63
  51. boris/latency.py +34 -25
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +52 -84
  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 +655 -310
  60. boris/observation_operations.py +1036 -404
  61. boris/observation_ui.py +584 -356
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -80
  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 +43 -46
  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 +685 -228
  81. boris/project.py +448 -293
  82. boris/project_functions.py +689 -254
  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 -199
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +53 -37
  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 +766 -266
  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 +125 -28
  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.9.16.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/boris_ui.py +0 -886
  111. boris/converters.ui +0 -289
  112. boris/core.qrc +0 -35
  113. boris/core.ui +0 -1543
  114. boris/edit_event.ui +0 -175
  115. boris/icons/logo_eye.ico +0 -0
  116. boris/map_creator.py +0 -850
  117. boris/observation.ui +0 -773
  118. boris/param_panel.ui +0 -379
  119. boris/preferences.ui +0 -537
  120. boris/project.ui +0 -1069
  121. boris/project_server.py +0 -236
  122. boris/vlc.py +0 -10343
  123. boris/vlc_local.py +0 -90
  124. boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
  125. boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
  126. boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
  127. boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
  128. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  129. {boris_behav_obs-8.9.16.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
@@ -26,16 +26,16 @@ from . import config as cfg
26
26
  from . import dialog
27
27
 
28
28
 
29
- def add_image_overlay(self):
29
+ def add_image_overlay(self) -> None:
30
30
  """
31
- add an image overlay on video
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()
38
- items = list([f"Player #{i + 1}" for i, _ in enumerate(self.dw_player)])
38
+ items = [f"Player #{i + 1}" for i, _ in enumerate(self.dw_player)]
39
39
  w.cb_player.addItems(items)
40
40
  if not w.exec_():
41
41
  return
@@ -57,16 +57,16 @@ def add_image_overlay(self):
57
57
  logging.debug("error in add_image_overlay function")
58
58
 
59
59
 
60
- def remove_image_overlay(self):
60
+ def remove_image_overlay(self) -> None:
61
61
  """
62
62
  remove image overlay from all players
63
63
  """
64
- keys_to_delete = []
64
+ keys_to_delete: list = []
65
65
  for n_player in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO].get(cfg.OVERLAY, {}):
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
+ )