-
Notifications
You must be signed in to change notification settings - Fork 118
Add CamerasTable to SessionsDock
#1671
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add CamerasTable to SessionsDock
#1671
Conversation
WalkthroughThe recent updates focus on enhancing the user interface and functionality of a GUI application, specifically by improving session management and camera-video linkage. New features include the ability to unlink videos from cameras, better handling of camera selections in the GUI, and updates to data views and widgets to support these changes. The modifications span across app initialization, command actions, and GUI components, aiming to provide a more intuitive and flexible user experience. Changes
Poem
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (invoked as PR comments)
Additionally, you can add CodeRabbit Configration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is an error in the on_data_update call, which is likely why the cameras table was not updating when I load a project with a RecordingSession.
sleap/gui/app.py
Outdated
| self.state["last_interacted_frame"] = self.state["labeled_frame"] | ||
|
|
||
| if _has_topic([UpdateTopic.sessions]): | ||
| self.sessions_dock.table.model().items = self.labels.videos |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we may need to specify which table in the list of tables here, perhaps self.sessions_dock.camera_table (see what is in self.sessions_dock.table below).
sleap/sleap/gui/widgets/docks.py
Line 70 in 7f74a8b
| self.table = self.create_tables() |
sleap/sleap/gui/widgets/docks.py
Lines 625 to 636 in 7f74a8b
| def create_tables(self) -> GenericTableView: | |
| if self.model is None: | |
| self.create_models() | |
| main_window = self.main_window | |
| self.camera_table = GenericTableView( | |
| state=main_window.state, | |
| row_name="camera", | |
| model=self.camera_model, | |
| ) | |
| return [self.camera_table] |
I received the following error message with the current implementation:
Happy SLEAPing! :)
Restoring GUI state...
Traceback (most recent call last):
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 290, in openProject
self.execute(OpenProject, filename=filename, first_open=first_open)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 244, in execute
command().execute(context=self, params=kwargs)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 140, in execute
self.do_with_signal(context, params)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 164, in do_with_signal
cls.do_action(context, params)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 759, in do_action
new_window.commands.loadProjectFile(filename)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 275, in loadProjectFile
self.execute(LoadProjectFile, filename=filename)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 244, in execute
command().execute(context=self, params=kwargs)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 140, in execute
self.do_with_signal(context, params)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 164, in do_with_signal
cls.do_action(context, params)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 710, in do_action
context.app.on_data_update([UpdateTopic.project, UpdateTopic.all])
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\app.py", line 1240, in on_data_update
self.sessions_dock.table.model().items = self.labels.videos
AttributeError: 'list' object has no attribute 'model'
sleap/gui/commands.py
Outdated
| def do_action(context: CommandContext, params: dict): | ||
| video = params.get("video", None) | ||
| cam = params.get("Camcorder", None) | ||
| recording_session = params.get("RecordingSession", None) | ||
|
|
||
| if video is not None and recording_session is not None \ | ||
| and cam is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should just handle this by disabling the Unlink Button if the GUI is not in the correct state.
Something similar to what the "Show Video" button uses:
Line 1126 in 60a441f
| self._buttons["show video"].setEnabled(has_selected_video) |
Note that any item that is selected in a table will by default be set in the GuiState by prepending a "selected_" to the TableView.row_name:
Lines 278 to 291 in 60a441f
| Note that by default "selected_" is used for the state key, e.g., | |
| "selected_foo", but you can set the `name_prefix` attribute/init arg if | |
| for some reason you need this to be different. For instance, the table | |
| of instances in the GUI sets this to "" so that the row for an instance | |
| is automatically selected when `state["instance"]` is set outside the table. | |
| "ellipsis_left" can be used to make the TableView truncate cell content on | |
| the left instead of the right side. By default, the argument is set to | |
| False, i.e. truncation on the right side, which is also the default for | |
| QTableView. | |
| """ | |
| row_name: Optional[str] = None | |
| name_prefix: str = "selected_" |
In this case, you've used a row_name="camera":
sleap/sleap/gui/widgets/docks.py
Lines 630 to 634 in 7f74a8b
| self.camera_table = GenericTableView( | |
| state=main_window.state, | |
| row_name="camera", | |
| model=self.camera_model, | |
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To update the table, we need to add some update topics to the UnlinkVideo class.
| class UnlinkVideo(EditCommand): | ||
| @staticmethod |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are missing the callback topics here.
| class UnlinkVideo(EditCommand): | |
| @staticmethod | |
| class UnlinkVideo(EditCommand): | |
| topics = [UpdateTopic.sessions] | |
| @staticmethod |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review Status
Actionable comments generated: 10
Configuration used: CodeRabbit UI
Files selected for processing (6)
- sleap/gui/app.py (3 hunks)
- sleap/gui/commands.py (3 hunks)
- sleap/gui/dataviews.py (2 hunks)
- sleap/gui/widgets/docks.py (3 hunks)
- tests/gui/test_dataviews.py (2 hunks)
- tests/gui/widgets/test_docks.py (2 hunks)
Additional comments: 4
sleap/gui/widgets/docks.py (1)
- 578-579: The
SessionsDockconstructor initializesmodel_typewith a list containingself.camera_model_type, butDockWidget's constructor seems to expect either a single model type orNone. Verify if passing a list is intended and correctly handled in the superclass.sleap/gui/dataviews.py (1)
- 669-670: The method
object_to_itemsusesobj.linked_cameras, which implies thatRecordingSessionhas alinked_camerasattribute. Ensure that this attribute exists and correctly returns a list ofCamcorderobjects paired with their respectiveVideoobjects. Iflinked_camerasis a method or property that computes this pairing dynamically, its implementation needs to be efficient to avoid performance issues in the GUI.sleap/gui/app.py (1)
- 1097-1098: The addition of
has_selected_camcorderchecks the presence of a selected camera in the table and uses this to enable the "Unlink Video" button. Ensure that the stateselected_camera_tableis correctly updated elsewhere in the code to reflect the current selection status accurately.sleap/gui/commands.py (1)
- 3917-3920: The method
camcorder.get_video(recording_session)and subsequent removal of the video from the session (recording_session.remove_video(video)) are not standard operations in the provided context. Ensure these methods are correctly implemented in their respective classes and handle cases where the video might not be found or already unlinked.
tests/gui/test_dataviews.py
Outdated
| def test_camera_table(qtbot, multiview_min_session_labels): | ||
| table = CamerasTableModel(items=multiview_min_session_labels.sessions[0]) | ||
|
|
||
| assert table.columnCount() == 2 | ||
| assert table.rowCount() == 8 | ||
|
|
||
| num_rows = table.rowCount() | ||
|
|
||
| table = GenericTableView( | ||
| row_name="instance", | ||
| is_sortable=True, | ||
| name_prefix="", | ||
| model=CamerasTableModel(items=multiview_min_session_labels.sessions[0]), | ||
| ) | ||
|
|
||
| # Testing if all comcorders are presented in the correct row | ||
| camcorders = multiview_min_session_labels.sessions[0].linked_cameras | ||
|
|
||
| for i in range(num_rows): | ||
| table.selectRow(i) | ||
| assert table.getSelectedRowItem() == camcorders[i] | ||
| assert table.model().data(table.currentIndex()) == camcorders[i].name | ||
|
|
||
| # Testing if a comcorder change is reflected | ||
| idxs_to_remove = [1, 2, 7] | ||
| for idx in idxs_to_remove: | ||
| multiview_min_session_labels.sessions[0].remove_video(camcorders[idx].get_video(multiview_min_session_labels.sessions[0])) | ||
|
|
||
| removed_camcorder = [cam for i, cam in enumerate(camcorders) if i not in idxs_to_remove] | ||
|
|
||
| for i in range(num_rows-3): | ||
| table.selectRow(i) | ||
| assert table.getSelectedRowItem() == camcorders[i] | ||
| assert table.model().data(table.currentIndex()) == removed_camcorder[i].name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test test_camera_table uses a hardcoded value for rowCount() assertion, which might lead to test fragility if the test data changes. Consider deriving the expected row count directly from the test data to make the test more robust.
- assert table.rowCount() == 8
+ assert table.rowCount() == len(multiview_min_session_labels.sessions[0].linked_cameras)Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.
| def test_camera_table(qtbot, multiview_min_session_labels): | |
| table = CamerasTableModel(items=multiview_min_session_labels.sessions[0]) | |
| assert table.columnCount() == 2 | |
| assert table.rowCount() == 8 | |
| num_rows = table.rowCount() | |
| table = GenericTableView( | |
| row_name="instance", | |
| is_sortable=True, | |
| name_prefix="", | |
| model=CamerasTableModel(items=multiview_min_session_labels.sessions[0]), | |
| ) | |
| # Testing if all comcorders are presented in the correct row | |
| camcorders = multiview_min_session_labels.sessions[0].linked_cameras | |
| for i in range(num_rows): | |
| table.selectRow(i) | |
| assert table.getSelectedRowItem() == camcorders[i] | |
| assert table.model().data(table.currentIndex()) == camcorders[i].name | |
| # Testing if a comcorder change is reflected | |
| idxs_to_remove = [1, 2, 7] | |
| for idx in idxs_to_remove: | |
| multiview_min_session_labels.sessions[0].remove_video(camcorders[idx].get_video(multiview_min_session_labels.sessions[0])) | |
| removed_camcorder = [cam for i, cam in enumerate(camcorders) if i not in idxs_to_remove] | |
| for i in range(num_rows-3): | |
| table.selectRow(i) | |
| assert table.getSelectedRowItem() == camcorders[i] | |
| assert table.model().data(table.currentIndex()) == removed_camcorder[i].name | |
| def test_camera_table(qtbot, multiview_min_session_labels): | |
| table = CamerasTableModel(items=multiview_min_session_labels.sessions[0]) | |
| assert table.columnCount() == 2 | |
| assert table.rowCount() == len(multiview_min_session_labels.sessions[0].linked_cameras) | |
| num_rows = table.rowCount() | |
| table = GenericTableView( | |
| row_name="instance", | |
| is_sortable=True, | |
| name_prefix="", | |
| model=CamerasTableModel(items=multiview_min_session_labels.sessions[0]), | |
| ) | |
| # Testing if all comcorders are presented in the correct row | |
| camcorders = multiview_min_session_labels.sessions[0].linked_cameras | |
| for i in range(num_rows): | |
| table.selectRow(i) | |
| assert table.getSelectedRowItem() == camcorders[i] | |
| assert table.model().data(table.currentIndex()) == camcorders[i].name | |
| # Testing if a comcorder change is reflected | |
| idxs_to_remove = [1, 2, 7] | |
| for idx in idxs_to_remove: | |
| multiview_min_session_labels.sessions[0].remove_video(camcorders[idx].get_video(multiview_min_session_labels.sessions[0])) | |
| removed_camcorder = [cam for i, cam in enumerate(camcorders) if i not in idxs_to_remove] | |
| for i in range(num_rows-3): | |
| table.selectRow(i) | |
| assert table.getSelectedRowItem() == camcorders[i] | |
| assert table.model().data(table.currentIndex()) == removed_camcorder[i].name |
tests/gui/test_dataviews.py
Outdated
| assert table.getSelectedRowItem() == camcorders[i] | ||
| assert table.model().data(table.currentIndex()) == removed_camcorder[i].name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop for verifying removed camcorders iterates over the original number of rows minus 3, assuming specific camcorders are removed. This approach is fragile and may not accurately reflect the GUI's state after removals. Ensure the test dynamically adjusts based on the actual state of the GUI and the session object.
tests/gui/widgets/test_docks.py
Outdated
| def test_sessions_dock(qtbot, multiview_min_session_labels): | ||
| main_window = MainWindow() | ||
| dock = SessionsDock(main_window) | ||
|
|
||
| # Testing if dock object loads corretly | ||
| assert dock.name == "Sessions" | ||
| assert dock.main_window is main_window | ||
| assert dock.wgt_layout is dock.widget().layout() | ||
|
|
||
| # loading label file | ||
| main_window.commands.loadLabelsObject(multiview_min_session_labels) | ||
|
|
||
|
|
||
| # Testing if cameras_table is loaded correctly | ||
| camcorders = multiview_min_session_labels.sessions[0].linked_cameras | ||
|
|
||
| for i in range(8): | ||
| main_window.sessions_dock.camera_table.selectRow(i) | ||
| assert main_window.sessions_dock.camera_table.getSelectedRowItem() == camcorders[i] | ||
| assert main_window.sessions_dock.camera_table.model().data(main_window.sessions_dock.camera_table.currentIndex()) == camcorders[i].name | ||
|
|
||
| # Testing if unlink video command works for camera table | ||
| idxs_to_remove = [0, 4, 7] | ||
| to_remove_camcorders = [cam for i, cam in enumerate(camcorders) if i in idxs_to_remove] | ||
| leftover_camcorder = [cam for i, cam in enumerate(camcorders) if i not in idxs_to_remove] | ||
|
|
||
| for cam in to_remove_camcorders: | ||
| main_window.state["selected_camera_table"] = cam | ||
| main_window._buttons["unlink video"].click() | ||
|
|
||
|
|
||
| for i in range(5): | ||
| main_window.sessions_dock.camera_table.selectRow(i) | ||
| assert main_window.sessions_dock.camera_table.getSelectedRowItem() == leftover_camcorder[i] | ||
| assert main_window.sessions_dock.camera_table.model().data(main_window.sessions_dock.camera_table.currentIndex()) == leftover_camcorder[i].name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests for SessionsDock do not verify the GUI's response to the unlink video action, such as checking if the UI correctly disables/enables buttons based on the state. Ensure tests cover the full range of expected GUI behavior to provide comprehensive coverage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed on adding a check that the unlink video button is properly disabled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@roomrys: To add a check for the "unlink video" button being properly disabled, you'll want to extend the test_sessions_dock function with assertions that verify the button's enabled state at key points in your test flow. Here's a general approach:
- After loading the label file and before performing any actions, assert that the "unlink video" button is initially disabled, as no camcorder is selected yet.
- After selecting a camcorder (e.g.,
main_window.sessions_dock.camera_table.selectRow(i)), assert that the "unlink video" button becomes enabled. - After unlinking a video from a camcorder, assert that the button is disabled again if no other camcorder is selected or if the selected camcorder does not have a linked video.
You can access the button's enabled state using a property or method specific to the GUI framework you're using. For example, if using PyQt or PySide, you might check the button's isEnabled() method. Here's a simplified example:
# Example using PyQt/PySide, adjust according to your actual GUI framework and button access method
assert not main_window._buttons["unlink video"].isEnabled(), "Button should be disabled initially"
# After selecting a camcorder
main_window.sessions_dock.camera_table.selectRow(i)
assert main_window._buttons["unlink video"].isEnabled(), "Button should be enabled after selecting a camcorder"
# After unlinking a video
main_window._buttons["unlink video"].click()
assert not main_window._buttons["unlink video"].isEnabled(), "Button should be disabled after unlinking"Make sure to adjust the example to fit your application's structure and the way you access UI elements. This will ensure your tests accurately reflect the intended user interactions and UI behavior.
sleap/gui/widgets/docks.py
Outdated
| def create_models(self) -> GenericTableModel: | ||
| main_window = self.main_window | ||
| self.camera_model = self.camera_model_type( | ||
| items=main_window.state["selected_session"], context=main_window.commands | ||
| ) | ||
|
|
||
| return {"camera_model": self.camera_model} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method create_models returns a dictionary instead of a GenericTableModel instance. This does not align with the expected return type for model creation methods in dock widgets. Ensure consistency in return types across the application.
- return {"camera_model": self.camera_model}
+ return self.camera_modelCommittable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.
| def create_models(self) -> GenericTableModel: | |
| main_window = self.main_window | |
| self.camera_model = self.camera_model_type( | |
| items=main_window.state["selected_session"], context=main_window.commands | |
| ) | |
| return {"camera_model": self.camera_model} | |
| def create_models(self) -> GenericTableModel: | |
| main_window = self.main_window | |
| self.camera_model = self.camera_model_type( | |
| items=main_window.state["selected_session"], context=main_window.commands | |
| ) | |
| return self.camera_model |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like your idea of using a dictionary to describe the models that are returned. Let's just update the typehinting:
def create_models(self) -> Union[GenericTableModel, Dict[str, GenericTableModel]]:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@roomrys: Acknowledged. Updating the type hinting as suggested introduces flexibility in handling multiple models within dock widgets, which can be particularly useful for complex interfaces. It's important to ensure that other parts of the application that interact with these models are compatible with this approach. This includes reviewing methods like create_tables and any other functionality that relies on the output of create_models to handle both a single model and a dictionary of models seamlessly. Additionally, documentation and possibly some internal logic may need updates to align with this change. If you've considered these aspects and are comfortable with the implications, this update can enhance the application's architecture by accommodating more complex data management scenarios.
sleap/gui/widgets/docks.py
Outdated
| if self.model is None: | ||
| self.create_models() | ||
|
|
||
| main_window = self.main_window | ||
| self.camera_table = GenericTableView( | ||
| state=main_window.state, | ||
| row_name="camera_table", | ||
| model=self.camera_model, | ||
| ) | ||
|
|
||
| return {"camera_table": self.camera_table} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method create_tables returns a dictionary instead of a GenericTableView instance, which is inconsistent with the expected return type for table creation methods in dock widgets. Adjust to return the table directly.
- return {"camera_table": self.camera_table}
+ return self.camera_tableCommittable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.
| if self.model is None: | |
| self.create_models() | |
| main_window = self.main_window | |
| self.camera_table = GenericTableView( | |
| state=main_window.state, | |
| row_name="camera_table", | |
| model=self.camera_model, | |
| ) | |
| return {"camera_table": self.camera_table} | |
| if self.model is None: | |
| self.create_models() | |
| main_window = self.main_window | |
| self.camera_table = GenericTableView( | |
| state=main_window.state, | |
| row_name="camera_table", | |
| model=self.camera_model, | |
| ) | |
| return self.camera_table |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do like how you used this in the _on_data_update callback... could you at least update the type hinting for the return on this function:
def create_tables(self) -> Union[GenericTableView, Dict[str, GenericTableView]]:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@roomrys: Absolutely, updating the type hinting for the create_tables method to accurately reflect its return type as GenericTableView makes perfect sense, especially given its usage in the _on_data_update callback. This will enhance code readability and maintainability. Thank you for pointing this out!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Table works great. We just have one more piece to do which would require using the GuiState for the table instead of directly using the list. To accomplish this, we would also need to set the GuiState for "session" when loading the Labels object and when loading a Video that is part of a RecordingSession.
tests/gui/test_dataviews.py
Outdated
| for i in range(num_rows-3): | ||
| table.selectRow(i) | ||
| assert table.getSelectedRowItem() == camcorders[i] | ||
| assert table.model().data(table.currentIndex()) == removed_camcorder[i].name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is testing what you think it is testing (that cameras are removed from table) as the table model is not automatically updated if we just use API calls. Since we are just testing the Table (without the entire SessionsDock - which has the UnlinkVideo button), we can just do the data update ourselves with:
table.model().items = multiview_min_session_labels.sessions[0]Also! I would like a test that all the Camcorders in the RecordingSession are displayed (regardless of linked Video), but only linked Videos have a path displayed - otherwise just an empty string for the Video.
tests/gui/test_dataviews.py
Outdated
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lint with black test/gui/test_dataview/py or set-up black to formatOnSave in vscode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review Status
Actionable comments generated: 1
Configuration used: CodeRabbit UI
Files selected for processing (7)
- sleap/gui/app.py (4 hunks)
- sleap/gui/commands.py (4 hunks)
- sleap/gui/dataviews.py (2 hunks)
- sleap/gui/widgets/docks.py (3 hunks)
- sleap/io/cameras.py (2 hunks)
- tests/gui/test_dataviews.py (2 hunks)
- tests/gui/widgets/test_docks.py (2 hunks)
Additional comments: 20
tests/gui/test_dataviews.py (1)
- 90-141: The test function
test_camera_tableis well-structured and covers essential aspects of theCamerasTablefunctionality. It tests the presentation of camcorders and their linked videos, as well as the update mechanism when camcorders are removed. However, consider adding more detailed comments within the test to explain the purpose of each assertion block, especially for future maintainability and readability.tests/gui/widgets/test_docks.py (1)
- 112-146: The test function
test_sessions_dockeffectively validates theSessionsDockfunctionality, including the loading of theCamerasTableand the operation of the "Unlink Video" button. It's comprehensive and covers key aspects of theSessionsDock. To enhance readability and maintainability, consider adding comments to describe the purpose and expected outcome of each major test block, especially for complex interactions like unlinking videos.sleap/gui/widgets/docks.py (5)
- 34-34: The import of
CamerasTableModelis correctly added to support the newSessionsDockfunctionality.- 578-579: The modifications in the
SessionsDockconstructor, including the addition of thecamera_model_typeattribute and the adjustment ofmodel_typeto accept a list, are appropriate for supporting multiple models within the dock.- 617-623: The implementation of the
create_modelsmethod inSessionsDock, returning a dictionary of models, aligns with the discussed approach to accommodate multiple models. Ensure that the type hinting and documentation are updated to reflect this flexibility and that other parts of the application that interact with these models are compatible with this approach.- 625-636: The implementation of the
create_tablesmethod inSessionsDock, returning a dictionary containing thecamera_table, is consistent with the approach to handle multiple models and tables. This flexibility enhances the dock's functionality and allows for more complex interactions.- 638-646: The
create_video_unlink_buttonmethod inSessionsDockcorrectly adds a button for unlinking videos from cameras. This functionality is crucial for managing video-camera assignments within the GUI application.sleap/gui/dataviews.py (5)
- 33-33: The import statement for
CamcorderandRecordingSessionis correctly added and necessary for theCamerasTableModelclass.- 658-664: The class definition and docstring for
CamerasTableModelare clear and informative.- 666-666: The
propertiesdefinition inCamerasTableModelis correctly set up for the intended table columns.- 668-669: The
object_to_itemsmethod inCamerasTableModelis correctly implemented for listing cameras in aRecordingSession.- 671-674: The
item_to_datamethod inCamerasTableModelis correctly implemented for convertingCamcorderitems to a dictionary format suitable for the table model. It appropriately handles cases where a video might not be linked to a camera.sleap/io/cameras.py (2)
- 45-45: Changing the log level from
warningtodebugin theget_videomethod of theCamcorderclass is appropriate for cases where a session not being found is expected behavior.- 465-465: Adjusting the log level from
warningtodebugin theget_videomethod of theRecordingSessionclass is suitable for situations where a camcorder not being linked to a video is part of normal operation.sleap/gui/app.py (4)
- 148-148: Initialization of
sessionstate variable toNoneis consistent with the pattern of state initialization in this section. This change aligns with the PR objectives related to managing recording sessions.- 1098-1098: The variable
has_selected_camcorderis derived from the state ofselected_camera. Please verify the naming convention for consistency across the application, as the PR mentions managing video-camera assignments but usesselected_camcorderhere.- 1153-1153: Enabling the "unlink video" button based on the selection of a camcorder aligns with the PR objectives. Ensure that the naming inconsistency between
selected_cameraandselected_camcorderis addressed for clarity.- 1242-1246: Updating the
SessionsDockto use thesessionstate variable aligns with the PR objectives. The TODO comment suggests a future change to use "selected_session" instead, which should be addressed in subsequent updates.sleap/gui/commands.py (2)
- 85-85: Adding a new
UpdateTopicenum value forsessionsis appropriate for signaling updates related to session changes. This ensures that any UI components listening for session updates can react accordingly.- 659-662: The method
unlink_video_from_camerain theCommandContextclass provides a clear and concise way to execute theUnlinkVideocommand. This method enhances the modularity of the command execution process.
| class UnlinkVideo(EditCommand): | ||
| topics = [UpdateTopic.sessions] | ||
|
|
||
| @staticmethod | ||
| def do_action(context: CommandContext, params: dict): | ||
| camcorder = context.state["selected_camera"] | ||
| # TODO(LM): replace with state["selected_session"] | ||
| recording_session = context.state["session"] | ||
|
|
||
| video = camcorder.get_video(recording_session) | ||
|
|
||
| if video is not None and recording_session is not None: | ||
| recording_session.remove_video(video) | ||
|
|
||
| # Reset the selected camera | ||
| context.state["selected_camera"] = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The UnlinkVideo class correctly implements the do_action method to unlink a video from a camera within a recording session. However, there are a few areas for improvement:
- The method assumes that
context.state["selected_camera"]andcontext.state["session"]are always set and valid. It would be safer to add checks to ensure these values are notNonebefore proceeding with the unlink operation. - After unlinking the video, it resets
context.state["selected_camera"]toNone. This action might not be intuitive for the user, especially if they expect the selected camera to remain selected after the operation. Consider whether this behavior aligns with the user interface expectations. - There's a TODO comment regarding replacing
context.state["session"]withstate["selected_session"]. It's important to address this TODO to ensure consistency in how sessions are referenced throughout the application.
@staticmethod
def do_action(context: CommandContext, params: dict):
+ if "selected_camera" not in context.state or "session" not in context.state:
+ logger.error("Selected camera or session not set.")
+ return
camcorder = context.state["selected_camera"]
recording_session = context.state["session"]
+ if camcorder is None or recording_session is None:
+ logger.error("Selected camera or session is None.")
+ return
video = camcorder.get_video(recording_session)
if video is not None:
recording_session.remove_video(video)
- # Reset the selected camera
- context.state["selected_camera"] = NoneCommittable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.
| class UnlinkVideo(EditCommand): | |
| topics = [UpdateTopic.sessions] | |
| @staticmethod | |
| def do_action(context: CommandContext, params: dict): | |
| camcorder = context.state["selected_camera"] | |
| # TODO(LM): replace with state["selected_session"] | |
| recording_session = context.state["session"] | |
| video = camcorder.get_video(recording_session) | |
| if video is not None and recording_session is not None: | |
| recording_session.remove_video(video) | |
| # Reset the selected camera | |
| context.state["selected_camera"] = None | |
| class UnlinkVideo(EditCommand): | |
| topics = [UpdateTopic.sessions] | |
| @staticmethod | |
| def do_action(context: CommandContext, params: dict): | |
| if "selected_camera" not in context.state or "session" not in context.state: | |
| logger.error("Selected camera or session not set.") | |
| return | |
| camcorder = context.state["selected_camera"] | |
| recording_session = context.state["session"] | |
| if camcorder is None or recording_session is None: | |
| logger.error("Selected camera or session is None.") | |
| return | |
| video = camcorder.get_video(recording_session) | |
| if video is not None: | |
| recording_session.remove_video(video) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review Status
Actionable comments generated: 1
Configuration used: CodeRabbit UI
Files selected for processing (2)
- tests/gui/test_dataviews.py (2 hunks)
- tests/gui/widgets/test_docks.py (2 hunks)
Files skipped from review as they are similar to previous changes (1)
- tests/gui/test_dataviews.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review Status
Actionable comments generated: 1
Configuration used: CodeRabbit UI
Files selected for processing (7)
- sleap/gui/app.py (5 hunks)
- sleap/gui/commands.py (3 hunks)
- sleap/gui/dataviews.py (2 hunks)
- sleap/gui/widgets/docks.py (5 hunks)
- sleap/io/cameras.py (2 hunks)
- tests/gui/test_dataviews.py (1 hunks)
- tests/gui/widgets/test_docks.py (2 hunks)
Files skipped from review as they are similar to previous changes (6)
- sleap/gui/app.py
- sleap/gui/commands.py
- sleap/gui/dataviews.py
- sleap/gui/widgets/docks.py
- sleap/io/cameras.py
- tests/gui/test_dataviews.py
| def test_sessions_dock_cameras_table(qtbot, multiview_min_session_labels): | ||
| labels = multiview_min_session_labels | ||
| session = labels.sessions[0] | ||
| camcorders = session.camera_cluster.cameras | ||
| main_window = MainWindow(labels=labels) | ||
| assert main_window.state["session"] == session | ||
|
|
||
| dock = main_window.sessions_dock | ||
| table = dock.camera_table | ||
|
|
||
| # Testing if cameras_table is loaded correctly | ||
|
|
||
| # Test if all comcorders are presented in the correct row | ||
| for i, cam in enumerate(camcorders): | ||
| table.selectRow(i) | ||
|
|
||
| # Check first column | ||
| assert table.getSelectedRowItem() == cam | ||
| assert table.model().data(table.currentIndex()) == cam.name | ||
|
|
||
| # Check second column | ||
| index = table.model().index(i, 1) | ||
| linked_video_filename = cam.get_video(session).filename | ||
| assert table.model().data(index) == linked_video_filename | ||
|
|
||
| # Test if a comcorder change is reflected | ||
| idxs_to_remove = [1, 2, 7] | ||
| for idx in idxs_to_remove: | ||
| main_window.state["selected_camera"] = camcorders[idx] | ||
| main_window._buttons["unlink video"].click() | ||
|
|
||
| for i, cam in enumerate(camcorders): | ||
| table.selectRow(i) | ||
|
|
||
| # Check first column | ||
| assert table.getSelectedRowItem() == camcorders[i] | ||
| assert table.model().data(table.currentIndex()) == camcorders[i].name | ||
|
|
||
| # Check second column | ||
| index = table.model().index(i, 1) | ||
| linked_video = camcorders[i].get_video(session) | ||
| if i in idxs_to_remove: | ||
| assert table.model().data(index) == "" | ||
| else: | ||
| linked_video_filename = linked_video.filename | ||
| assert table.model().data(index) == linked_video_filename |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test function test_sessions_dock_cameras_table is well-structured and covers important aspects of the SessionsDock cameras table functionality. However, there are a few areas where improvements can be made:
-
Typographical Error: There's a minor typographical error in the comments. The word "comcorders" should be corrected to "camcorders" for clarity and correctness. This occurs in lines 134 and 147.
-
Test Coverage for Unlink Video: The test covers the scenario where videos are unlinked from camcorders, but it might be beneficial to also verify that the
UnlinkVideocommand is correctly updating the underlying data model. This could involve checking that thesessionobject reflects the changes made by theUnlinkVideocommand. -
Magic Numbers: The test uses magic numbers (e.g.,
idxs_to_remove = [1, 2, 7]) without explanation. It would improve readability and maintainability to either document why these specific indices are chosen or to derive these indices programmatically based on the test setup. -
Error Handling: Consider adding tests for error scenarios, such as attempting to unlink a video from a camcorder that doesn't have a linked video. This would ensure robustness in handling edge cases.
- # Test if all comcorders are presented in the correct row
+ # Test if all camcorders are presented in the correct rowConsider enhancing the test to verify that the UnlinkVideo command updates the underlying data model as expected.
Document the rationale behind the choice of indices in idxs_to_remove or derive them programmatically to avoid magic numbers.
Would you like assistance in adding tests for error scenarios, such as handling unlink operations for camcorders without linked videos?
Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.
| def test_sessions_dock_cameras_table(qtbot, multiview_min_session_labels): | |
| labels = multiview_min_session_labels | |
| session = labels.sessions[0] | |
| camcorders = session.camera_cluster.cameras | |
| main_window = MainWindow(labels=labels) | |
| assert main_window.state["session"] == session | |
| dock = main_window.sessions_dock | |
| table = dock.camera_table | |
| # Testing if cameras_table is loaded correctly | |
| # Test if all comcorders are presented in the correct row | |
| for i, cam in enumerate(camcorders): | |
| table.selectRow(i) | |
| # Check first column | |
| assert table.getSelectedRowItem() == cam | |
| assert table.model().data(table.currentIndex()) == cam.name | |
| # Check second column | |
| index = table.model().index(i, 1) | |
| linked_video_filename = cam.get_video(session).filename | |
| assert table.model().data(index) == linked_video_filename | |
| # Test if a comcorder change is reflected | |
| idxs_to_remove = [1, 2, 7] | |
| for idx in idxs_to_remove: | |
| main_window.state["selected_camera"] = camcorders[idx] | |
| main_window._buttons["unlink video"].click() | |
| for i, cam in enumerate(camcorders): | |
| table.selectRow(i) | |
| # Check first column | |
| assert table.getSelectedRowItem() == camcorders[i] | |
| assert table.model().data(table.currentIndex()) == camcorders[i].name | |
| # Check second column | |
| index = table.model().index(i, 1) | |
| linked_video = camcorders[i].get_video(session) | |
| if i in idxs_to_remove: | |
| assert table.model().data(index) == "" | |
| else: | |
| linked_video_filename = linked_video.filename | |
| assert table.model().data(index) == linked_video_filename | |
| def test_sessions_dock_cameras_table(qtbot, multiview_min_session_labels): | |
| labels = multiview_min_session_labels | |
| session = labels.sessions[0] | |
| camcorders = session.camera_cluster.cameras | |
| main_window = MainWindow(labels=labels) | |
| assert main_window.state["session"] == session | |
| dock = main_window.sessions_dock | |
| table = dock.camera_table | |
| # Testing if cameras_table is loaded correctly | |
| # Test if all camcorders are presented in the correct row | |
| for i, cam in enumerate(camcorders): | |
| table.selectRow(i) | |
| # Check first column | |
| assert table.getSelectedRowItem() == cam | |
| assert table.model().data(table.currentIndex()) == cam.name | |
| # Check second column | |
| index = table.model().index(i, 1) | |
| linked_video_filename = cam.get_video(session).filename | |
| assert table.model().data(index) == linked_video_filename | |
| # Test if a comcorder change is reflected | |
| idxs_to_remove = [1, 2, 7] | |
| for idx in idxs_to_remove: | |
| main_window.state["selected_camera"] = camcorders[idx] | |
| main_window._buttons["unlink video"].click() | |
| for i, cam in enumerate(camcorders): | |
| table.selectRow(i) | |
| # Check first column | |
| assert table.getSelectedRowItem() == camcorders[i] | |
| assert table.model().data(table.currentIndex()) == camcorders[i].name | |
| # Check second column | |
| index = table.model().index(i, 1) | |
| linked_video = camcorders[i].get_video(session) | |
| if i in idxs_to_remove: | |
| assert table.model().data(index) == "" | |
| else: | |
| linked_video_filename = linked_video.filename | |
| assert table.model().data(index) == linked_video_filename |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review Status
Actionable comments generated: 3
Configuration used: CodeRabbit UI
Files selected for processing (5)
- sleap/gui/app.py (5 hunks)
- sleap/gui/commands.py (3 hunks)
- sleap/gui/dataviews.py (2 hunks)
- sleap/gui/widgets/docks.py (5 hunks)
- tests/gui/widgets/test_docks.py (2 hunks)
Files skipped from review as they are similar to previous changes (4)
- sleap/gui/app.py
- sleap/gui/commands.py
- sleap/gui/dataviews.py
- sleap/gui/widgets/docks.py
Additional comments: 1
tests/gui/widgets/test_docks.py (1)
- 125-170: The test function
test_sessions_dock_cameras_tablecovers important aspects of theSessionsDockcameras table functionality. However, consider enhancing the test to verify that theUnlinkVideocommand updates the underlying data model as expected.Consider adding assertions to verify that the
sessionobject reflects the changes made by theUnlinkVideocommand.
|
|
||
| # Testing if cameras_table is loaded correctly | ||
|
|
||
| # Test if all comcorders are presented in the correct row |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a typographical error in the comment: "comcorders" should be corrected to "camcorders" for clarity and correctness.
- # Test if all comcorders are presented in the correct row
+ # Test if all camcorders are presented in the correct rowCommittable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.
| # Test if all comcorders are presented in the correct row | |
| # Test if all camcorders are presented in the correct row |
| linked_video_filename = cam.get_video(session).filename | ||
| assert table.model().data(index) == linked_video_filename | ||
|
|
||
| # Test if a comcorder change is reflected |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another typographical error: "comcorder" should be corrected to "camcorder".
- # Test if a comcorder change is reflected
+ # Test if a camcorder change is reflectedCommittable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.
| # Test if a comcorder change is reflected | |
| # Test if a camcorder change is reflected |
| assert table.model().data(index) == linked_video_filename | ||
|
|
||
| # Test if a comcorder change is reflected | ||
| idxs_to_remove = [1, 2, 7] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of magic numbers in idxs_to_remove = [1, 2, 7] should be documented or derived programmatically to improve readability and maintainability.
Consider documenting the rationale behind the choice of indices in idxs_to_remove or deriving them programmatically to avoid magic numbers.
Description
The overall goal is to add
Camcorder/Videoassignment table to theSessionsDockthat is linked to theSessionsTablecreated above. ThisCamerasTablewill have two columns. One column will display theCamcorder.names of allCamcorders in theRecordingSession.cameras: List[Camcorder]list. The second column will show whichVideos are assigned to the adjacentCamcorder.nameby displaying the returned value ofRecordingSession.get_video(Camcorder)(or""ifNone).GenericTableModelto create a new model calledCamerasTableModelwithproperties = ("camera", "video")CamerasTableModel.object_to_items(self, obj: RecordingSession)which returns the list ofCamcorders inobj.cameras: List[Camcorder]CamerasTableModel.item_to_data(self, obj: RecordingSession, item: Camcorder)which returns the following dictionary:{camera: item.name, video: obj.get_video(item)}cameras_model_type = CamerasTableModelattribute toSessionsDock(similar toSkeletonDock.nodes_model_type) and pass into themodel_typelist forsuper().__init__SessionsDock.create_modelsmethod (or append to it if it already exists) that sets aSessionsDock.cameras_model = SessionsDock.sessions_model_type(items=main_window.state["selected_session"], context=main_window.commands)and returns a list of all models created (seeSkeletonDock.create_models)SessionsDock.create_tablesmethod (or append to it if it already exists) that sets aSessionsDock.cameras_table = GenericTableView(state=..., row_name="camera", ...)and returns a list of all tables created (seeSkeletonDock.create_tables)CommandContext.unlink_video_from_cameramethod to un-link aVideofrom aCamcorderCommandContext.unlink_video_from_cameramethodCamcorderfor theCamerasTableSessionsDock.lay_everything_outmethod, add theSessionsDock.cameras_tableto theSessionsDock.wgt_layout: QVBoxLayout(see theInstancesDock.lay_everything_out).sessionsoption toUpdateTopic: Enumif not already thereMainWindow.on_data_updateadd (or append to) theif _has_topic(UpdateTopic.sessions)that updates the items to display in theCamerasTable(seevideo_dock.tableupdate)GuiStatefor the"session"key to the firstRecordingSessioninlabels.sessions: List[RecordingSession]when loading aLabelsproject (similar to how we set the state for"video")Test the
CamerasTableTests for the
CamerasTablewill be added totests/gui/test_dataviews.py. These tests will only test the functionality of the table as it's own entity.Camcorderis returned when we select a specific row of the table (see existing test for inspiration)Camcorderinlabels.sessions, theCamcorderin the table is updated as well (i.e. if we unlink aVideofrom aCamcorder, the data in theVideocell should be empty)Test the
CamerasTableas part of theSessionsDockTests for the
SessionsDockwill be added totests/gui/widgets/test_docks.py.GuiState(see existing test)Fig 1: Depiction of Cameras Table layout.
Types of changes
Does this address any currently open issues?
[list open issues here]
Outside contributors checklist
Thank you for contributing to SLEAP!
❤️
Summary by CodeRabbit
CamerasTableModelfor managing camera and video pairs in recording sessions.SessionsDockwith new methods and attributes to support camera management and video unlinking.SessionsDockfunctionality, including camera table presentation and video unlinking.