boris-behav-obs 8.16.5__py3-none-any.whl → 9.7.1__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.
Files changed (125) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +24 -36
  4. boris/add_modifier.py +88 -80
  5. boris/add_modifier_ui.py +235 -131
  6. boris/advanced_event_filtering.py +23 -29
  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 +16 -34
  23. boris/config.py +102 -50
  24. boris/config_file.py +55 -64
  25. boris/connections.py +105 -58
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2108 -1275
  30. boris/core_qrc.py +15892 -10829
  31. boris/core_ui.py +941 -806
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +27 -7
  34. boris/dialog.py +461 -242
  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 +405 -281
  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 +180 -203
  43. boris/export_observation.py +60 -73
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +427 -218
  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 +304 -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 +16 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv2.py +128 -35
  58. boris/observation.py +493 -210
  59. boris/observation_operations.py +1010 -391
  60. boris/observation_ui.py +573 -363
  61. boris/observations_list.py +51 -58
  62. boris/otx_parser.py +74 -68
  63. boris/param_panel.py +45 -59
  64. boris/param_panel_ui.py +254 -138
  65. boris/player_dock_widget.py +91 -56
  66. boris/plot_data_module.py +18 -53
  67. boris/plot_events.py +56 -153
  68. boris/plot_events_rt.py +16 -30
  69. boris/plot_spectrogram_rt.py +80 -56
  70. boris/plot_waveform_rt.py +23 -48
  71. boris/plugins.py +431 -0
  72. boris/portion/__init__.py +18 -8
  73. boris/portion/const.py +35 -18
  74. boris/portion/dict.py +5 -5
  75. boris/portion/func.py +2 -2
  76. boris/portion/interval.py +21 -41
  77. boris/portion/io.py +41 -32
  78. boris/preferences.py +298 -123
  79. boris/preferences_ui.py +664 -225
  80. boris/project.py +293 -270
  81. boris/project_functions.py +610 -537
  82. boris/project_import_export.py +204 -213
  83. boris/project_ui.py +673 -441
  84. boris/qrc_boris.py +6 -3
  85. boris/qrc_boris5.py +6 -3
  86. boris/select_modifiers.py +62 -90
  87. boris/select_observations.py +19 -197
  88. boris/select_subj_behav.py +67 -39
  89. boris/state_events.py +51 -33
  90. boris/subjects_pad.py +6 -8
  91. boris/synthetic_time_budget.py +42 -26
  92. boris/time_budget_functions.py +169 -169
  93. boris/time_budget_widget.py +77 -89
  94. boris/transitions.py +41 -41
  95. boris/utilities.py +562 -222
  96. boris/version.py +3 -3
  97. boris/video_equalizer.py +16 -14
  98. boris/video_equalizer_ui.py +199 -130
  99. boris/video_operations.py +78 -28
  100. boris/view_df.py +104 -0
  101. boris/view_df_ui.py +75 -0
  102. boris/write_event.py +240 -136
  103. boris_behav_obs-9.7.1.dist-info/METADATA +140 -0
  104. boris_behav_obs-9.7.1.dist-info/RECORD +109 -0
  105. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.1.dist-info}/WHEEL +1 -1
  106. boris_behav_obs-9.7.1.dist-info/entry_points.txt +2 -0
  107. boris/README.TXT +0 -22
  108. boris/add_modifier.ui +0 -323
  109. boris/converters.ui +0 -289
  110. boris/core.qrc +0 -37
  111. boris/core.ui +0 -1571
  112. boris/edit_event.ui +0 -233
  113. boris/icons/logo_eye.ico +0 -0
  114. boris/map_creator.py +0 -982
  115. boris/observation.ui +0 -814
  116. boris/param_panel.ui +0 -379
  117. boris/preferences.ui +0 -537
  118. boris/project.ui +0 -1074
  119. boris/vlc_local.py +0 -90
  120. boris_behav_obs-8.16.5.dist-info/LICENSE.TXT +0 -674
  121. boris_behav_obs-8.16.5.dist-info/METADATA +0 -134
  122. boris_behav_obs-8.16.5.dist-info/RECORD +0 -107
  123. boris_behav_obs-8.16.5.dist-info/entry_points.txt +0 -2
  124. {boris → boris_behav_obs-9.7.1.dist-info/licenses}/LICENSE.TXT +0 -0
  125. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.1.dist-info}/top_level.txt +0 -0
@@ -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 file is part of BORIS.
7
7
 
@@ -28,9 +28,9 @@ import pandas as pd
28
28
  import tablib
29
29
  import pickle
30
30
 
31
- from PyQt5.QtCore import Qt
32
- from PyQt5.QtGui import QColor, QFont
33
- from PyQt5.QtWidgets import QApplication, QFileDialog, QListWidgetItem, QMessageBox, QTableWidgetItem
31
+ from PySide6.QtCore import Qt
32
+ from PySide6.QtGui import QFont
33
+ from PySide6.QtWidgets import QApplication, QFileDialog, QListWidgetItem, QMessageBox, QTableWidgetItem
34
34
 
35
35
 
36
36
  from . import config as cfg
@@ -62,7 +62,7 @@ def export_ethogram(self) -> None:
62
62
  """
63
63
  export ethogram in various format
64
64
  """
65
- extended_file_formats = [
65
+ extended_file_formats: list = [
66
66
  "BORIS project file (*.boris)",
67
67
  "Tab Separated Values (*.tsv)",
68
68
  "Comma Separated Values (*.csv)",
@@ -71,7 +71,7 @@ def export_ethogram(self) -> None:
71
71
  "Legacy Microsoft Excel Spreadsheet XLS (*.xls)",
72
72
  "HTML (*.html)",
73
73
  ]
74
- file_formats = ["boris", cfg.TSV_EXT, "csv", "ods", "xlsx", "xls", "html"]
74
+ file_formats: list = ["boris", cfg.TSV_EXT, cfg.CSV_EXT, cfg.ODS_EXT, cfg.XLSX_EXT, cfg.XLS_EXT, cfg.HTML_EXT]
75
75
 
76
76
  filediag_func = QFileDialog().getSaveFileName
77
77
 
@@ -79,62 +79,23 @@ def export_ethogram(self) -> None:
79
79
  if not file_name:
80
80
  return
81
81
 
82
- output_format = file_formats[extended_file_formats.index(filter_)]
82
+ output_format: str = file_formats[extended_file_formats.index(filter_)]
83
83
  if pl.Path(file_name).suffix != "." + output_format:
84
84
  file_name = str(pl.Path(file_name)) + "." + output_format
85
85
 
86
- ethogram_data = tablib.Dataset()
87
- ethogram_data.title = "Ethogram"
88
- if self.leProjectName.text():
89
- ethogram_data.title = f"Ethogram of {self.leProjectName.text()} project"
90
-
91
- ethogram_data.headers = [
92
- "Behavior code",
93
- "Behavior type",
94
- "Description",
95
- "Key",
96
- "Behavioral category",
97
- "Excluded behaviors",
98
- "modifiers",
99
- # "modifiers (JSON)",
100
- ]
101
-
102
- for r in range(self.twBehaviors.rowCount()):
103
- row = []
104
- for field in ("code", cfg.TYPE, "description", "key", cfg.COLOR, "category", "excluded"):
105
- row.append(self.twBehaviors.item(r, cfg.behavioursFields[field]).text())
106
-
107
- # modifiers
108
- if self.twBehaviors.item(r, cfg.behavioursFields[cfg.MODIFIERS]).text():
109
- modifiers_dict = eval(self.twBehaviors.item(r, cfg.behavioursFields[cfg.MODIFIERS]).text())
110
- modifiers_list = []
111
- for key in modifiers_dict:
112
- if modifiers_dict[key]["values"]:
113
- values = ", ".join(modifiers_dict[key]["values"])
114
- modifiers_list.append(f"{modifiers_dict[key]['name']} ({values})")
115
- else:
116
- modifiers_list.append(modifiers_dict[key]["name"])
117
-
118
- row.append(", ".join(modifiers_list))
119
- else:
120
- row.append("")
121
-
122
- ethogram_data.append(row)
123
-
124
86
  if output_format == "boris":
125
87
  r = self.check_ethogram()
126
88
  if cfg.CANCEL in r:
127
89
  return
128
90
  pj = dict(cfg.EMPTY_PROJECT)
129
91
  pj[cfg.ETHOGRAM] = dict(r)
130
- # behavioral categories
131
92
 
93
+ # behavioral categories
132
94
  pj[cfg.BEHAVIORAL_CATEGORIES] = list(self.pj[cfg.BEHAVIORAL_CATEGORIES])
95
+ pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = dict(self.pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {}))
133
96
 
134
97
  # project file indentation
135
- file_indentation = self.config_param.get(
136
- cfg.PROJECT_FILE_INDENTATION, cfg.PROJECT_FILE_INDENTATION_DEFAULT_VALUE
137
- )
98
+ file_indentation = self.config_param.get(cfg.PROJECT_FILE_INDENTATION, cfg.PROJECT_FILE_INDENTATION_DEFAULT_VALUE)
138
99
  try:
139
100
  with open(file_name, "w") as f_out:
140
101
  f_out.write(json.dumps(pj, indent=file_indentation))
@@ -148,6 +109,47 @@ def export_ethogram(self) -> None:
148
109
  )
149
110
 
150
111
  else:
112
+ ethogram_data = tablib.Dataset()
113
+ ethogram_data.title = "Ethogram"
114
+ if self.leProjectName.text():
115
+ ethogram_data.title = f"Ethogram of {self.leProjectName.text()} project"
116
+
117
+ ethogram_data.headers = [
118
+ "Behavior code",
119
+ "Behavior type",
120
+ "Description",
121
+ "Key",
122
+ "Color",
123
+ "Behavioral category",
124
+ "Excluded behaviors",
125
+ "Modifiers",
126
+ "Modifiers (JSON)",
127
+ ]
128
+
129
+ for r in range(self.twBehaviors.rowCount()):
130
+ row: list = []
131
+ for field in ("code", cfg.TYPE, "description", "key", cfg.COLOR, "category", "excluded"):
132
+ row.append(self.twBehaviors.item(r, cfg.behavioursFields[field]).text())
133
+
134
+ # modifiers
135
+ if self.twBehaviors.item(r, cfg.behavioursFields[cfg.MODIFIERS]).text():
136
+ # modifiers a string
137
+ modifiers_dict = json.loads(self.twBehaviors.item(r, cfg.behavioursFields[cfg.MODIFIERS]).text())
138
+ modifiers_list = []
139
+ for key in modifiers_dict:
140
+ values = ",".join(modifiers_dict[key]["values"])
141
+ modifiers_list.append(f"{modifiers_dict[key]['name']}:{values}")
142
+ row.append(";".join(modifiers_list))
143
+ # modifiers as JSON
144
+ row.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.MODIFIERS]).text())
145
+ else:
146
+ # modifiers a string
147
+ row.append("")
148
+ # modifiers as JSON
149
+ row.append("")
150
+
151
+ ethogram_data.append(row)
152
+
151
153
  ok, msg = export_observation.dataset_write(ethogram_data, file_name, output_format)
152
154
  if not ok:
153
155
  QMessageBox.critical(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
@@ -157,15 +159,15 @@ def export_subjects(self) -> None:
157
159
  """
158
160
  export the subjetcs list in various format
159
161
  """
160
- extended_file_formats = [
162
+ extended_file_formats: list = [
161
163
  cfg.TSV,
162
164
  cfg.CSV,
163
165
  cfg.ODS,
164
166
  cfg.XLSX,
165
167
  cfg.XLS,
166
- cfg.HMTL,
168
+ cfg.HTML,
167
169
  ]
168
- file_formats = [cfg.TSV_EXT, "csv", "ods", "xlsx", "xls", "html"]
170
+ file_formats: list = [cfg.TSV_EXT, cfg.CSV_EXT, cfg.ODS_EXT, cfg.XLSX_EXT, cfg.XLS_EXT, cfg.HTML_EXT]
169
171
 
170
172
  filediag_func = QFileDialog().getSaveFileName
171
173
 
@@ -182,15 +184,14 @@ def export_subjects(self) -> None:
182
184
  if self.leProjectName.text():
183
185
  subjects_data.title = f"Subjects defined in the {self.leProjectName.text()} project"
184
186
 
185
- subjects_data.headers = [
187
+ subjects_data.headers: list = [
186
188
  "Key",
187
189
  "Subject name",
188
190
  "Description",
189
191
  ]
190
192
 
191
193
  for r in range(self.twSubjects.rowCount()):
192
-
193
- row = []
194
+ row: list = []
194
195
  for idx, _ in enumerate(("Key", "Subject name", "Description")):
195
196
  row.append(self.twSubjects.item(r, idx).text())
196
197
 
@@ -223,7 +224,7 @@ def select_behaviors(
223
224
  paramPanelWindow.resize(800, 600)
224
225
  paramPanelWindow.setWindowTitle(title)
225
226
  paramPanelWindow.lbBehaviors.setText(text)
226
- for w in [
227
+ for w in (
227
228
  paramPanelWindow.lwSubjects,
228
229
  paramPanelWindow.pbSelectAllSubjects,
229
230
  paramPanelWindow.pbUnselectAllSubjects,
@@ -233,7 +234,7 @@ def select_behaviors(
233
234
  paramPanelWindow.cbExcludeBehaviors,
234
235
  paramPanelWindow.frm_time,
235
236
  paramPanelWindow.frm_time_bin_size,
236
- ]:
237
+ ):
237
238
  w.setVisible(False)
238
239
 
239
240
  if behavioral_categories:
@@ -245,9 +246,7 @@ def select_behaviors(
245
246
  categories = ["###no category###"]
246
247
 
247
248
  for category in categories:
248
-
249
249
  if category != "###no category###":
250
-
251
250
  if category == "":
252
251
  paramPanelWindow.item = QListWidgetItem("No category")
253
252
  paramPanelWindow.item.setData(34, "No category")
@@ -265,7 +264,6 @@ def select_behaviors(
265
264
 
266
265
  # check if behavior type must be shown
267
266
  for behavior in [ethogram[x][cfg.BEHAVIOR_CODE] for x in util.sorted_keys(ethogram)]:
268
-
269
267
  if (categories == ["###no category###"]) or (
270
268
  behavior
271
269
  in [
@@ -274,7 +272,6 @@ def select_behaviors(
274
272
  if cfg.BEHAVIOR_CATEGORY in ethogram[x] and ethogram[x][cfg.BEHAVIOR_CATEGORY] == category
275
273
  ]
276
274
  ):
277
-
278
275
  paramPanelWindow.item = QListWidgetItem(behavior)
279
276
  paramPanelWindow.item.setCheckState(Qt.Unchecked)
280
277
 
@@ -313,6 +310,7 @@ def import_ethogram_from_dict(self, project: dict):
313
310
  """
314
311
  # import behavioral_categories
315
312
  self.pj[cfg.BEHAVIORAL_CATEGORIES] = list(project.get(cfg.BEHAVIORAL_CATEGORIES, []))
313
+ self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = list(project.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {}))
316
314
 
317
315
  # configuration of behaviours
318
316
  if not (cfg.ETHOGRAM in project and project[cfg.ETHOGRAM]):
@@ -322,7 +320,7 @@ def import_ethogram_from_dict(self, project: dict):
322
320
  if self.twBehaviors.rowCount():
323
321
  response = dialog.MessageDialog(
324
322
  cfg.programName,
325
- ("Some behaviors are already configured. " "Do you want to append behaviors or replace them?"),
323
+ ("Some behaviors are already configured. Do you want to append behaviors or replace them?"),
326
324
  [cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
327
325
  )
328
326
  if response == cfg.REPLACE:
@@ -340,39 +338,42 @@ def import_ethogram_from_dict(self, project: dict):
340
338
  )
341
339
 
342
340
  for i in util.sorted_keys(project[cfg.ETHOGRAM]):
343
-
344
341
  if project[cfg.ETHOGRAM][i][cfg.BEHAVIOR_CODE] not in behaviors_to_import:
345
342
  continue
346
343
 
347
344
  self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
348
345
 
349
346
  for field in project[cfg.ETHOGRAM][i]:
350
-
351
347
  item = QTableWidgetItem()
352
348
 
353
349
  if field == cfg.TYPE:
354
350
  item.setText(project[cfg.ETHOGRAM][i][field])
355
351
  item.setFlags(Qt.ItemIsEnabled)
356
- item.setBackground(QColor(230, 230, 230))
352
+ # item.setBackground(QColor(230, 230, 230))
353
+ item.setBackground(self.not_editable_column_color())
357
354
 
358
355
  else:
359
- if field == cfg.MODIFIERS and isinstance(project[cfg.ETHOGRAM][i][field], str):
360
- modif_set_dict = {}
361
- if project[cfg.ETHOGRAM][i][field]:
362
- modif_set_list = project[cfg.ETHOGRAM][i][field].split("|")
363
- for modif_set in modif_set_list:
364
- modif_set_dict[str(len(modif_set_dict))] = {
365
- "name": "",
366
- "type": cfg.SINGLE_SELECTION,
367
- "values": modif_set.split(","),
368
- }
369
- project[cfg.ETHOGRAM][i][field] = dict(modif_set_dict)
370
-
371
- item.setText(str(project[cfg.ETHOGRAM][i][field]))
356
+ if field == cfg.MODIFIERS:
357
+ if isinstance(project[cfg.ETHOGRAM][i][field], str):
358
+ modif_set_dict = {}
359
+ if project[cfg.ETHOGRAM][i][field]:
360
+ modif_set_list = project[cfg.ETHOGRAM][i][field].split("|")
361
+ for modif_set in modif_set_list:
362
+ modif_set_dict[str(len(modif_set_dict))] = {
363
+ "name": "",
364
+ "type": cfg.SINGLE_SELECTION,
365
+ "values": modif_set.split(","),
366
+ }
367
+ project[cfg.ETHOGRAM][i][field] = dict(modif_set_dict)
368
+ else:
369
+ item.setText(json.dumps(project[cfg.ETHOGRAM][i][field]))
370
+ else:
371
+ item.setText(project[cfg.ETHOGRAM][i][field])
372
372
 
373
373
  if field not in cfg.ETHOGRAM_EDITABLE_FIELDS:
374
374
  item.setFlags(Qt.ItemIsEnabled)
375
- item.setBackground(QColor(230, 230, 230))
375
+ # item.setBackground(QColor(230, 230, 230))
376
+ item.setBackground(self.not_editable_column_color())
376
377
 
377
378
  self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field], item)
378
379
 
@@ -382,9 +383,12 @@ def import_ethogram_from_dict(self, project: dict):
382
383
  def load_dataframe_into_behaviors_tablewidget(self, df: pd.DataFrame) -> int:
383
384
  """
384
385
  Load pandas dataframe into the twBehaviors table widget
386
+
387
+ Returns:
388
+ int: 0 if no error else error code
385
389
  """
386
390
 
387
- expected_labels = [
391
+ expected_labels: list = [
388
392
  "Behavior code",
389
393
  "Behavior type",
390
394
  "Description",
@@ -393,19 +397,32 @@ def load_dataframe_into_behaviors_tablewidget(self, df: pd.DataFrame) -> int:
393
397
  "Excluded behaviors",
394
398
  ]
395
399
 
400
+ ethogram_header: dict = {
401
+ "code": "Behavior code",
402
+ "description": "Description",
403
+ "key": "Key",
404
+ "color": "Color",
405
+ "category": "Behavioral category",
406
+ "excluded": "Excluded behaviors",
407
+ "modifiers": "modifiers (JSON)",
408
+ }
409
+
410
+ # change all column names to uppercase
411
+ df.columns = df.columns.str.upper()
412
+
396
413
  for column in expected_labels:
397
- if column not in list(df.columns):
414
+ if column.upper() not in list(df.columns):
398
415
  QMessageBox.warning(
399
416
  None,
400
417
  cfg.programName,
401
418
  (
402
- f"The {column } column was not found in the file header.<br>"
419
+ f"The {column} column was not found in the file header.<br>"
403
420
  "For information the current file header contains the following labels:<br>"
404
421
  f"{'<br>'.join(['<b>' + util.replace_leading_trailing_chars(x, ' ', '&#9608;') + '</b>' for x in df.columns])}<br>"
405
422
  "<br>"
406
423
  "The first row of the spreadsheet must contain the following labels:<br>"
407
424
  f"{'<br>'.join(['<b>' + x + '</b>' for x in expected_labels])}<br>"
408
- "<br>The order is not mandatory but respect the case!"
425
+ "<br>The order is not mandatory."
409
426
  ),
410
427
  QMessageBox.Ok | QMessageBox.Default,
411
428
  QMessageBox.NoButton,
@@ -413,16 +430,12 @@ def load_dataframe_into_behaviors_tablewidget(self, df: pd.DataFrame) -> int:
413
430
  return 1
414
431
 
415
432
  for _, row in df.iterrows():
416
-
417
- behavior = {
418
- "key": row["Key"] if str(row["Key"]) != "nan" else "",
419
- "code": row["Behavior code"] if str(row["Behavior code"]) != "nan" else "",
420
- "description": row["Description"] if str(row["Description"]) != "nan" else "",
421
- "modifiers": "",
422
- "excluded": row["Excluded behaviors"] if str(row["Excluded behaviors"]) != "nan" else "",
423
- "coding map": "",
424
- "category": row["Behavioral category"] if str(row["Behavioral category"]) != "nan" else "",
425
- }
433
+ behavior = {"coding map": ""}
434
+ for x in ethogram_header:
435
+ if ethogram_header[x].upper() in row:
436
+ behavior[x] = row[ethogram_header[x].upper()] if str(row[ethogram_header[x].upper()]) != "nan" else ""
437
+ else:
438
+ behavior[x] = ""
426
439
 
427
440
  self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
428
441
 
@@ -430,9 +443,9 @@ def load_dataframe_into_behaviors_tablewidget(self, df: pd.DataFrame) -> int:
430
443
  if field_type == cfg.TYPE:
431
444
  item = QTableWidgetItem(cfg.DEFAULT_BEHAVIOR_TYPE)
432
445
  # add type combobox
433
- if cfg.POINT in row["Behavior type"].upper():
446
+ if cfg.POINT in row["Behavior type".upper()].upper():
434
447
  item = QTableWidgetItem(cfg.POINT_EVENT)
435
- elif cfg.STATE in row["Behavior type"].upper():
448
+ elif cfg.STATE in row["Behavior type".upper()].upper():
436
449
  item = QTableWidgetItem(cfg.STATE_EVENT)
437
450
  else:
438
451
  QMessageBox.critical(
@@ -449,7 +462,8 @@ def load_dataframe_into_behaviors_tablewidget(self, df: pd.DataFrame) -> int:
449
462
 
450
463
  if field_type not in cfg.ETHOGRAM_EDITABLE_FIELDS:
451
464
  item.setFlags(Qt.ItemIsEnabled)
452
- item.setBackground(QColor(230, 230, 230))
465
+ # item.setBackground(QColor(230, 230, 230))
466
+ item.setBackground(self.not_editable_column_color())
453
467
 
454
468
  self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
455
469
 
@@ -457,12 +471,12 @@ def load_dataframe_into_behaviors_tablewidget(self, df: pd.DataFrame) -> int:
457
471
 
458
472
 
459
473
  def import_behaviors_from_project(self):
460
-
461
- fn = QFileDialog().getOpenFileName(
462
- self, "Import behaviors from project file", "", ("Project files (*.boris *.boris.gz);;" "All files (*)")
474
+ """
475
+ import ethogram from a BORIS project file
476
+ """
477
+ file_name, _ = QFileDialog.getOpenFileName(
478
+ self, "Import behaviors from BORIS project file", "", ("Project files (*.boris *.boris.gz);;All files (*)")
463
479
  )
464
- file_name = fn[0] if type(fn) is tuple else fn
465
-
466
480
  if not file_name:
467
481
  return
468
482
  _, _, project, _ = project_functions.open_project_json(file_name)
@@ -472,7 +486,7 @@ def import_behaviors_from_project(self):
472
486
 
473
487
  def import_behaviors_from_text_file(self):
474
488
  """
475
- Import behaviors from text file (CSV or TSV)
489
+ Import ethogram from text file (CSV or TSV)
476
490
  """
477
491
 
478
492
  if self.twBehaviors.rowCount():
@@ -484,10 +498,9 @@ def import_behaviors_from_text_file(self):
484
498
  if response == cfg.CANCEL:
485
499
  return
486
500
 
487
- fn = QFileDialog().getOpenFileName(
501
+ file_name, _ = QFileDialog.getOpenFileName(
488
502
  self, "Import behaviors from text file (CSV, TSV)", "", "Text files (*.txt *.tsv *.csv);;All files (*)"
489
503
  )
490
- file_name = fn[0] if type(fn) is tuple else fn
491
504
 
492
505
  if not file_name:
493
506
  return
@@ -538,25 +551,22 @@ def import_behaviors_from_spreadsheet(self):
538
551
  if response == cfg.CANCEL:
539
552
  return
540
553
 
541
- fn = QFileDialog().getOpenFileName(
542
- self, "Import behaviors from a spreadsheet file", "", "Spreadsheet files (*.xlsx);;All files (*)"
554
+ file_name, _ = QFileDialog.getOpenFileName(
555
+ self, "Import behaviors from a spreadsheet file", "", "Spreadsheet files (*.xlsx *.ods);;All files (*)"
543
556
  )
544
- file_name = fn[0] if type(fn) is tuple else fn
545
557
 
546
558
  if not file_name:
547
559
  return
548
560
 
549
561
  if pl.Path(file_name).suffix.upper() == ".XLSX":
550
562
  engine = "openpyxl"
551
- """
552
- elif pl.Path(file_name).suffix.upper() == ".ODS":
553
- engine = "odf"
554
- """
563
+ elif pl.Path(file_name).suffix.upper() == ".ODS":
564
+ engine = "odf"
555
565
  else:
556
566
  QMessageBox.warning(
557
567
  None,
558
568
  cfg.programName,
559
- ("The type of file was not recognized. Must be Microsoft-Excel XLSX format"),
569
+ ("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
560
570
  QMessageBox.Ok | QMessageBox.Default,
561
571
  QMessageBox.NoButton,
562
572
  )
@@ -568,7 +578,7 @@ def import_behaviors_from_spreadsheet(self):
568
578
  QMessageBox.warning(
569
579
  None,
570
580
  cfg.programName,
571
- ("The type of file was not recognized. Must be Microsoft-Excel XLSX format"),
581
+ ("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
572
582
  QMessageBox.Ok | QMessageBox.Default,
573
583
  QMessageBox.NoButton,
574
584
  )
@@ -632,9 +642,7 @@ def import_behaviors_from_clipboard(self):
632
642
  for idx, field in enumerate(row.split("\t")):
633
643
  if idx == 0:
634
644
  behavior["type"] = (
635
- cfg.STATE_EVENT
636
- if cfg.STATE in field.upper()
637
- else (cfg.POINT_EVENT if cfg.POINT in field.upper() else "")
645
+ cfg.STATE_EVENT if cfg.STATE in field.upper() else (cfg.POINT_EVENT if cfg.POINT in field.upper() else "")
638
646
  )
639
647
  if idx == 1:
640
648
  behavior["key"] = field.strip() if len(field.strip()) == 1 else ""
@@ -653,11 +661,10 @@ def import_behaviors_from_clipboard(self):
653
661
  else:
654
662
  item = QTableWidgetItem(behavior.get(field_type, ""))
655
663
 
656
- if (
657
- field_type not in cfg.ETHOGRAM_EDITABLE_FIELDS
658
- ): # [TYPE, "excluded", "coding map", "modifiers", "category"]:
664
+ if field_type not in cfg.ETHOGRAM_EDITABLE_FIELDS: # [TYPE, "excluded", "coding map", "modifiers", "category"]:
659
665
  item.setFlags(Qt.ItemIsEnabled)
660
- item.setBackground(QColor(230, 230, 230))
666
+ # item.setBackground(QColor(230, 230, 230))
667
+ item.setBackground(self.not_editable_column_color())
661
668
 
662
669
  self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
663
670
 
@@ -676,74 +683,68 @@ def import_behaviors_from_JWatcher(self):
676
683
  if response == cfg.CANCEL:
677
684
  return
678
685
 
679
- fn = QFileDialog().getOpenFileName(
680
- self, "Import behaviors from JWatcher", "", "Global Definition File (*.gdf);;All files (*)"
681
- )
682
- fileName = fn[0] if type(fn) is tuple else fn
686
+ fileName, _ = QFileDialog().getOpenFileName(self, "Import behaviors from JWatcher", "", "Global Definition File (*.gdf);;All files (*)")
683
687
 
684
- if fileName:
685
- if self.twBehaviors.rowCount() and response == cfg.REPLACE:
686
- self.twBehaviors.setRowCount(0)
688
+ if not fileName:
689
+ return
690
+ if self.twBehaviors.rowCount() and response == cfg.REPLACE:
691
+ self.twBehaviors.setRowCount(0)
687
692
 
688
- with open(fileName, "r") as f:
689
- rows = f.readlines()
690
-
691
- for idx, row in enumerate(rows):
692
- if row and row[0] == "#":
693
- continue
694
-
695
- if "Behavior.name." in row and "=" in row:
696
- key, code = row.split("=")
697
- key = key.replace("Behavior.name.", "")
698
- # read description
699
- if idx < len(rows) and "Behavior.description." in rows[idx + 1]:
700
- description = rows[idx + 1].split("=")[-1]
701
-
702
- behavior = {
703
- "key": key,
704
- "code": code,
705
- "description": description,
706
- "modifiers": "",
707
- "excluded": "",
708
- "coding map": "",
709
- "category": "",
710
- }
711
-
712
- self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
713
-
714
- for field_type in cfg.behavioursFields:
715
- if field_type == cfg.TYPE:
716
- item = QTableWidgetItem(cfg.DEFAULT_BEHAVIOR_TYPE)
717
- else:
718
- item = QTableWidgetItem(behavior[field_type])
693
+ with open(fileName, "r") as f:
694
+ rows = f.readlines()
695
+
696
+ for idx, row in enumerate(rows):
697
+ if row and row[0] == "#":
698
+ continue
699
+
700
+ if "Behavior.name." in row and "=" in row:
701
+ key, code = row.split("=")
702
+ key = key.replace("Behavior.name.", "")
703
+ # read description
704
+ if idx < len(rows) and "Behavior.description." in rows[idx + 1]:
705
+ description = rows[idx + 1].split("=")[-1]
706
+
707
+ behavior = {
708
+ "key": key,
709
+ "code": code,
710
+ "description": description,
711
+ "modifiers": "",
712
+ "excluded": "",
713
+ "coding map": "",
714
+ "category": "",
715
+ }
716
+
717
+ self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
718
+
719
+ for field_type in cfg.behavioursFields:
720
+ if field_type == cfg.TYPE:
721
+ item = QTableWidgetItem(cfg.DEFAULT_BEHAVIOR_TYPE)
722
+ else:
723
+ item = QTableWidgetItem(behavior[field_type])
719
724
 
720
- if field_type in [cfg.TYPE, "excluded", "category", "coding map", "modifiers"]:
721
- item.setFlags(Qt.ItemIsEnabled)
722
- item.setBackground(QColor(230, 230, 230))
725
+ if field_type in [cfg.TYPE, "excluded", "category", "coding map", "modifiers"]:
726
+ item.setFlags(Qt.ItemIsEnabled)
727
+ # item.setBackground(QColor(230, 230, 230))
728
+ item.setBackground(self.not_editable_column_color())
723
729
 
724
- self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
730
+ self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
725
731
 
726
732
 
727
733
  def import_behaviors_from_repository(self):
728
734
  """
729
735
  import behaviors from the BORIS ethogram repository
730
736
  """
731
- ethogram_repository_URL = "http://www.boris.unito.it/static/ethograms/ethogram_list.json"
737
+
732
738
  try:
733
- ethogram_list = urllib.request.urlopen(ethogram_repository_URL).read().strip().decode("utf-8")
739
+ ethogram_list = urllib.request.urlopen(f"{cfg.ETHOGRAM_REPOSITORY_URL}/ethogram_list.json").read().strip().decode("utf-8")
734
740
  except Exception:
735
-
736
- QMessageBox.critical(
737
- self, cfg.programName, "An error occured during retrieving the ethogram list from BORIS repository"
738
- )
741
+ QMessageBox.critical(self, cfg.programName, "An error occured during retrieving the ethogram list from BORIS repository")
739
742
  return
740
743
 
741
744
  try:
742
745
  ethogram_list_list = json.loads(ethogram_list)
743
746
  except Exception:
744
- QMessageBox.critical(
745
- self, cfg.programName, "An error occured during loading ethogram list from BORIS repository"
746
- )
747
+ QMessageBox.critical(self, cfg.programName, "An error occured during loading ethogram list from BORIS repository")
747
748
  return
748
749
 
749
750
  choice_dialog = dialog.ChooseObservationsToImport(
@@ -769,17 +770,9 @@ def import_behaviors_from_repository(self):
769
770
  break
770
771
 
771
772
  try:
772
- boris_project_str = (
773
- urllib.request.urlopen(f"http://www.boris.unito.it/static/ethograms/{file_name}")
774
- .read()
775
- .strip()
776
- .decode("utf-8")
777
- )
773
+ boris_project_str = urllib.request.urlopen(f"{cfg.ETHOGRAM_REPOSITORY_URL}/{file_name}").read().strip().decode("utf-8")
778
774
  except Exception:
779
-
780
- QMessageBox.critical(
781
- self, cfg.programName, f"An error occured during retrieving {file_name} from BORIS repository"
782
- )
775
+ QMessageBox.critical(self, cfg.programName, f"An error occured during retrieving {file_name} from BORIS repository")
783
776
  return
784
777
  boris_project = json.loads(boris_project_str)
785
778
 
@@ -789,16 +782,27 @@ def import_behaviors_from_repository(self):
789
782
  def load_dataframe_into_subjects_tablewidget(self, df: pd.DataFrame) -> int:
790
783
  """
791
784
  Load pandas dataframe into the twSubjects table widget
785
+
786
+ Returns:
787
+ int: 0 if no error else error code
788
+
792
789
  """
793
- for column in ["Subject name", "Description", "Key"]:
794
- if column not in list(df.columns):
790
+
791
+ expected_labels: list = ["Key", "Subject name", "Description"]
792
+
793
+ # change all column names to uppercase
794
+ df.columns = df.columns.str.upper()
795
+
796
+ for column in expected_labels:
797
+ if column.upper() not in list(df.columns):
795
798
  QMessageBox.warning(
796
799
  None,
797
800
  cfg.programName,
798
801
  (
802
+ f"The column {column} was not found in the file header.<br>"
799
803
  "The first row of spreadsheet must contain the following labels:<br>"
800
804
  "Subject name, Description, Key<br>"
801
- "Respect the case!"
805
+ "The order is not mandatory."
802
806
  ),
803
807
  QMessageBox.Ok | QMessageBox.Default,
804
808
  QMessageBox.NoButton,
@@ -806,14 +810,13 @@ def load_dataframe_into_subjects_tablewidget(self, df: pd.DataFrame) -> int:
806
810
  return 1
807
811
 
808
812
  for _, row in df.iterrows():
809
-
810
813
  self.twSubjects.setRowCount(self.twSubjects.rowCount() + 1)
811
814
 
812
- for idx, field in enumerate(("Key", "Subject name", "Description")):
815
+ for idx, field in enumerate(expected_labels):
813
816
  self.twSubjects.setItem(
814
817
  self.twSubjects.rowCount() - 1,
815
818
  idx,
816
- QTableWidgetItem(str(row[field]) if str(row[field]) != "nan" else ""),
819
+ QTableWidgetItem(str(row[field.upper()]) if str(row[field.upper()]) != "nan" else ""),
817
820
  )
818
821
 
819
822
  return 0
@@ -885,11 +888,9 @@ def import_subjects_from_project(self):
885
888
  import subjects from a BORIS project
886
889
  """
887
890
 
888
- fn = QFileDialog().getOpenFileName(
889
- self, "Import subjects from project file", "", ("Project files (*.boris *.boris.gz);;" "All files (*)")
891
+ file_name, _ = QFileDialog().getOpenFileName(
892
+ self, "Import subjects from project file", "", ("Project files (*.boris *.boris.gz);;All files (*)")
890
893
  )
891
- file_name = fn[0] if type(fn) is tuple else fn
892
-
893
894
  if not file_name:
894
895
  return
895
896
 
@@ -908,7 +909,7 @@ def import_subjects_from_project(self):
908
909
  if self.twSubjects.rowCount():
909
910
  response = dialog.MessageDialog(
910
911
  cfg.programName,
911
- ("There are subjects already configured. " "Do you want to append subjects or replace them?"),
912
+ ("There are subjects already configured. Do you want to append subjects or replace them?"),
912
913
  [cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
913
914
  )
914
915
 
@@ -919,11 +920,9 @@ def import_subjects_from_project(self):
919
920
  return
920
921
 
921
922
  for idx in util.sorted_keys(project[cfg.SUBJECTS]):
922
-
923
923
  self.twSubjects.setRowCount(self.twSubjects.rowCount() + 1)
924
924
 
925
925
  for idx2, sbjField in enumerate(cfg.subjectsFields):
926
-
927
926
  if sbjField in project[cfg.SUBJECTS][idx]:
928
927
  self.twSubjects.setItem(
929
928
  self.twSubjects.rowCount() - 1,
@@ -944,18 +943,16 @@ def import_subjects_from_text_file(self):
944
943
  if self.twSubjects.rowCount():
945
944
  response = dialog.MessageDialog(
946
945
  cfg.programName,
947
- ("There are subjects already configured. " "Do you want to append subjects or replace them?"),
946
+ ("There are subjects already configured. Do you want to append subjects or replace them?"),
948
947
  [cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
949
948
  )
950
949
 
951
950
  if response == cfg.CANCEL:
952
951
  return
953
952
 
954
- fn = QFileDialog().getOpenFileName(
953
+ file_name, _ = QFileDialog().getOpenFileName(
955
954
  self, "Import behaviors from text file (CSV, TSV)", "", "Text files (*.txt *.tsv *.csv);;All files (*)"
956
955
  )
957
- file_name = fn[0] if type(fn) is tuple else fn
958
-
959
956
  if not file_name:
960
957
  return
961
958
 
@@ -999,18 +996,16 @@ def import_subjects_from_spreadsheet(self):
999
996
  if self.twSubjects.rowCount():
1000
997
  response = dialog.MessageDialog(
1001
998
  cfg.programName,
1002
- ("There are subjects already configured. " "Do you want to append subjects or replace them?"),
999
+ ("There are subjects already configured. Do you want to append subjects or replace them?"),
1003
1000
  [cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
1004
1001
  )
1005
1002
 
1006
1003
  if response == cfg.CANCEL:
1007
1004
  return
1008
1005
 
1009
- fn = QFileDialog().getOpenFileName(
1010
- self, "Import subjects from a spreadsheet file", "", "Spreadsheet files (*.xlsx);;All files (*)"
1006
+ file_name, _ = QFileDialog().getOpenFileName(
1007
+ self, "Import subjects from a spreadsheet file", "", "Spreadsheet files (*.xlsx *.ods);;All files (*)"
1011
1008
  )
1012
- file_name = fn[0] if type(fn) is tuple else fn
1013
-
1014
1009
  if not file_name:
1015
1010
  return
1016
1011
 
@@ -1019,13 +1014,13 @@ def import_subjects_from_spreadsheet(self):
1019
1014
 
1020
1015
  if pl.Path(file_name).suffix.upper() == ".XLSX":
1021
1016
  engine = "openpyxl"
1022
- '''elif pl.Path(file_name).suffix.upper() == ".ODS":
1023
- engine = "odf"'''
1017
+ elif pl.Path(file_name).suffix.upper() == ".ODS":
1018
+ engine = "odf"
1024
1019
  else:
1025
1020
  QMessageBox.warning(
1026
1021
  None,
1027
1022
  cfg.programName,
1028
- ("The type of file was not recognized. Must be Microsoft-Excel XLSX format"),
1023
+ ("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
1029
1024
  QMessageBox.Ok | QMessageBox.Default,
1030
1025
  QMessageBox.NoButton,
1031
1026
  )
@@ -1037,7 +1032,7 @@ def import_subjects_from_spreadsheet(self):
1037
1032
  QMessageBox.warning(
1038
1033
  None,
1039
1034
  cfg.programName,
1040
- ("The type of file was not recognized. Must be Microsoft-Excel XLSX format"),
1035
+ ("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
1041
1036
  QMessageBox.Ok | QMessageBox.Default,
1042
1037
  QMessageBox.NoButton,
1043
1038
  )
@@ -1051,15 +1046,12 @@ def import_indep_variables_from_project(self):
1051
1046
  import independent variables from another project
1052
1047
  """
1053
1048
 
1054
- fn = QFileDialog().getOpenFileName(
1049
+ file_name, _ = QFileDialog().getOpenFileName(
1055
1050
  self,
1056
1051
  "Import independent variables from project file",
1057
1052
  "",
1058
- ("Project files (*.boris *.boris.gz);;" "All files (*)"),
1053
+ ("Project files (*.boris *.boris.gz);;All files (*)"),
1059
1054
  )
1060
-
1061
- file_name = fn[0] if type(fn) is tuple else fn
1062
-
1063
1055
  if not file_name:
1064
1056
  return
1065
1057
 
@@ -1082,7 +1074,6 @@ def import_indep_variables_from_project(self):
1082
1074
  existing_var.append(self.twVariables.item(r, 0).text().strip().upper())
1083
1075
 
1084
1076
  for i in util.sorted_keys(project[cfg.INDEPENDENT_VARIABLES]):
1085
-
1086
1077
  self.twVariables.setRowCount(self.twVariables.rowCount() + 1)
1087
1078
  flag_renamed = False
1088
1079
  for idx, field in enumerate(cfg.tw_indVarFields):