Skip to content
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

Merged
merged 27 commits into from
Mar 18, 2024

Conversation

roomrys
Copy link
Collaborator

@roomrys roomrys commented Jan 26, 2024

Description

The overall goal is to add Camcorder/Video assignment table to the SessionsDock that is linked to the SessionsTable created above. This CamerasTable will have two columns. One column will display the Camcorder.names of all Camcorders in the RecordingSession.cameras: List[Camcorder] list. The second column will show which Videos are assigned to the adjacent Camcorder.name by displaying the returned value of RecordingSession.get_video(Camcorder) (or "" if None).

  • Subclass GenericTableModel to create a new model called CamerasTableModel with properties = ("camera", "video")
  • Implement the method CamerasTableModel.object_to_items(self, obj: RecordingSession) which returns the list of Camcorders in obj.cameras: List[Camcorder]
  • Implement the method CamerasTableModel.item_to_data(self, obj: RecordingSession, item: Camcorder) which returns the following dictionary: {camera: item.name, video: obj.get_video(item)}
  • Add a cameras_model_type = CamerasTableModel attribute to SessionsDock (similar to SkeletonDock.nodes_model_type) and pass into the model_type list for super().__init__
  • Add a SessionsDock.create_models method (or append to it if it already exists) that sets a SessionsDock.cameras_model = SessionsDock.sessions_model_type(items=main_window.state["selected_session"], context=main_window.commands) and returns a list of all models created (see SkeletonDock.create_models)
  • Add a SessionsDock.create_tables method (or append to it if it already exists) that sets a SessionsDock.cameras_table = GenericTableView(state=..., row_name="camera", ...) and returns a list of all tables created (see SkeletonDock.create_tables)
  • Add a command class and CommandContext.unlink_video_from_camera method to un-link a Video from a Camcorder
  • Add a button to "Unlink Video" and connect it to the CommandContext.unlink_video_from_camera method
  • Disable button to "Unlink Video" when there is no selected Camcorder for the CamerasTable
  • In the SessionsDock.lay_everything_out method, add the SessionsDock.cameras_table to the SessionsDock.wgt_layout: QVBoxLayout (see the InstancesDock.lay_everything_out).
  • Add a sessions option to UpdateTopic: Enum if not already there
  • In MainWindow.on_data_update add (or append to) the if _has_topic(UpdateTopic.sessions) that updates the items to display in the CamerasTable (see video_dock.table update)
  • Set the GuiState for the "session" key to the first RecordingSession in labels.sessions: List[RecordingSession] when loading a Labels project (similar to how we set the state for "video")

Test the CamerasTable

Tests for the CamerasTable will be added to tests/gui/test_dataviews.py. These tests will only test the functionality of the table as it's own entity.

  • Test that the expected Camcorder is returned when we select a specific row of the table (see existing test for inspiration)
  • Test that when we update a Camcorder in labels.sessions, the Camcorder in the table is updated as well (i.e. if we unlink a Video from a Camcorder, the data in the Video cell should be empty)

Test the CamerasTable as part of the SessionsDock

Tests for the SessionsDock will be added to tests/gui/widgets/test_docks.py.

Fig 1: Depiction of Cameras Table layout.

Types of changes

  • Bugfix
  • New feature
  • Refactor / Code style update (no logical changes)
  • Build / CI changes
  • Documentation Update
  • Other (explain)

Does this address any currently open issues?

[list open issues here]

Outside contributors checklist

  • Review the guidelines for contributing to this repository
  • Read and sign the CLA and add yourself to the authors list
  • Make sure you are making a pull request against the develop branch (not main). Also you should start your branch off develop
  • Add tests that prove your fix is effective or that your feature works
  • Add necessary documentation (if appropriate)

Thank you for contributing to SLEAP!

❤️

Summary by CodeRabbit

  • New Features
    • Introduced session management in the GUI, including state initialization and updates.
    • Added functionality to unlink videos from cameras within sessions.
    • Implemented a new CamerasTableModel for managing camera and video pairs in recording sessions.
    • Enhanced SessionsDock with new methods and attributes to support camera management and video unlinking.
  • Tests
    • Updated and added tests for GUI data views and SessionsDock functionality, including camera table presentation and video unlinking.

@roomrys roomrys added the MultiView Stack This PR is part of the MultView stacked PRs. label Jan 26, 2024
Copy link

coderabbitai bot commented Jan 26, 2024

Walkthrough

The 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

Files Change Summary
sleap/gui/app.py Added session state, selected_camera handling, button enablement, and updated session handling
sleap/gui/commands.py Introduced method to unlink video from camera; updated do_action for session handling
sleap/gui/dataviews.py Added Camcorder, RecordingSession imports; new CamerasTableModel class
sleap/gui/widgets/docks.py Updated with CamerasTableModel, new methods for model and table creation, video unlink button
tests/gui/test_dataviews.py
tests/gui/widgets/test_docks.py
Enhanced tests for camera table functionality, SessionsDock, and camera unlinking

Poem

In the land of code, where the bits do hop,
A rabbit worked, with no intent to stop. 🐇💻
"Let cameras unlink, let sessions flow,"
It said with a wink, making the GUI glow.
With every push and pull, it danced with glee,
"For a more intuitive GUI, trust in me!"

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?

Share

Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>.
    • Generate unit-tests for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit tests for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai generate interesting stats about this repository and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit tests.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger a review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai help to get help.

Additionally, you can add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Configration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • The JSON schema for the configuration file is available here.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/coderabbit-overrides.v2.json

CodeRabbit Discord Community

Join our Discord Community to get help, request features, and share feedback.

@roomrys
Copy link
Collaborator Author

roomrys commented Jan 26, 2024

  • Cameras Table is displayed on opening GUI
    image

  • Cameras Table should be updated based on the GUIState "sessions"
    image

Copy link
Collaborator Author

@roomrys roomrys left a 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
@@ -1236,6 +1236,12 @@ def _has_topic(topic_list):
if _has_topic([UpdateTopic.frame, UpdateTopic.project_instances]):
self.state["last_interacted_frame"] = self.state["labeled_frame"]

if _has_topic([UpdateTopic.sessions]):
self.sessions_dock.table.model().items = self.labels.videos
Copy link
Collaborator Author

@roomrys roomrys Jan 26, 2024

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).

self.table = self.create_tables()

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'

Comment on lines 3911 to 3917
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:
Copy link
Collaborator Author

@roomrys roomrys Jan 26, 2024

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:

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:

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":

self.camera_table = GenericTableView(
state=main_window.state,
row_name="camera",
model=self.camera_model,
)

Copy link
Collaborator Author

@roomrys roomrys left a 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.

sleap/gui/app.py Outdated Show resolved Hide resolved
Comment on lines 3909 to 3910
class UnlinkVideo(EditCommand):
@staticmethod
Copy link
Collaborator Author

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.

Suggested change
class UnlinkVideo(EditCommand):
@staticmethod
class UnlinkVideo(EditCommand):
topics = [UpdateTopic.sessions]
@staticmethod

@roomrys
Copy link
Collaborator Author

roomrys commented Feb 14, 2024

  • Cameras Table automatically loads
image
  • Unlink video button works
image image

@roomrys roomrys marked this pull request as ready for review February 14, 2024 17:36
Copy link

@coderabbitai coderabbitai bot left a 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

Commits Files that changed from the base of the PR and between cc78c91 and 548caf5.
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 SessionsDock constructor initializes model_type with a list containing self.camera_model_type, but DockWidget's constructor seems to expect either a single model type or None. Verify if passing a list is intended and correctly handled in the superclass.
sleap/gui/dataviews.py (1)
  • 669-670: The method object_to_items uses obj.linked_cameras, which implies that RecordingSession has a linked_cameras attribute. Ensure that this attribute exists and correctly returns a list of Camcorder objects paired with their respective Video objects. If linked_cameras is 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_camcorder checks the presence of a selected camera in the table and uses this to enable the "Unlink Video" button. Ensure that the state selected_camera_table is 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.

sleap/gui/dataviews.py Outdated Show resolved Hide resolved
sleap/gui/dataviews.py Outdated Show resolved Hide resolved
sleap/gui/app.py Outdated Show resolved Hide resolved
sleap/gui/commands.py Outdated Show resolved Hide resolved
Comment on lines 89 to 122
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
Copy link

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.

Suggested change
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 Show resolved Hide resolved
Comment on lines 121 to 122
assert table.getSelectedRowItem() == camcorders[i]
assert table.model().data(table.currentIndex()) == removed_camcorder[i].name
Copy link

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.

Comment on lines 112 to 146
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
Copy link

@coderabbitai coderabbitai bot Feb 14, 2024

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.

Copy link
Collaborator Author

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.

Copy link

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:

  1. 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.
  2. After selecting a camcorder (e.g., main_window.sessions_dock.camera_table.selectRow(i)), assert that the "unlink video" button becomes enabled.
  3. 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.

Comment on lines 617 to 623
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}
Copy link

@coderabbitai coderabbitai bot Feb 14, 2024

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_model

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.

Suggested change
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

Copy link
Collaborator Author

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]]:

Copy link

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.

Comment on lines 626 to 636
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}
Copy link

@coderabbitai coderabbitai bot Feb 14, 2024

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_table

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.

Suggested change
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

Copy link
Collaborator Author

@roomrys roomrys Feb 14, 2024

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]]:

Copy link

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!

Copy link
Collaborator Author

@roomrys roomrys left a 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.

sleap/gui/app.py Outdated Show resolved Hide resolved
sleap/gui/dataviews.py Outdated Show resolved Hide resolved
Comment on lines 119 to 122
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
Copy link
Collaborator Author

@roomrys roomrys Feb 14, 2024

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.

Comment on lines 123 to 129







Copy link
Collaborator Author

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

Copy link

@coderabbitai coderabbitai bot left a 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

Commits Files that changed from the base of the PR and between 548caf5 and ba0f556.
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_table is well-structured and covers essential aspects of the CamerasTable functionality. 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_dock effectively validates the SessionsDock functionality, including the loading of the CamerasTable and the operation of the "Unlink Video" button. It's comprehensive and covers key aspects of the SessionsDock. 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 CamerasTableModel is correctly added to support the new SessionsDock functionality.
  • 578-579: The modifications in the SessionsDock constructor, including the addition of the camera_model_type attribute and the adjustment of model_type to accept a list, are appropriate for supporting multiple models within the dock.
  • 617-623: The implementation of the create_models method in SessionsDock, 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_tables method in SessionsDock, returning a dictionary containing the camera_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_button method in SessionsDock correctly 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 Camcorder and RecordingSession is correctly added and necessary for the CamerasTableModel class.
  • 658-664: The class definition and docstring for CamerasTableModel are clear and informative.
  • 666-666: The properties definition in CamerasTableModel is correctly set up for the intended table columns.
  • 668-669: The object_to_items method in CamerasTableModel is correctly implemented for listing cameras in a RecordingSession.
  • 671-674: The item_to_data method in CamerasTableModel is correctly implemented for converting Camcorder items 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 warning to debug in the get_video method of the Camcorder class is appropriate for cases where a session not being found is expected behavior.
  • 465-465: Adjusting the log level from warning to debug in the get_video method of the RecordingSession class 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 session state variable to None is 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_camcorder is derived from the state of selected_camera. Please verify the naming convention for consistency across the application, as the PR mentions managing video-camera assignments but uses selected_camcorder here.
  • 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_camera and selected_camcorder is addressed for clarity.
  • 1242-1246: Updating the SessionsDock to use the session state 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 UpdateTopic enum value for sessions is 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_camera in the CommandContext class provides a clear and concise way to execute the UnlinkVideo command. This method enhances the modularity of the command execution process.

Comment on lines 3913 to 3928
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
Copy link

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:

  1. The method assumes that context.state["selected_camera"] and context.state["session"] are always set and valid. It would be safer to add checks to ensure these values are not None before proceeding with the unlink operation.
  2. After unlinking the video, it resets context.state["selected_camera"] to None. 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.
  3. There's a TODO comment regarding replacing context.state["session"] with state["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"] = None

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.

Suggested change
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)

Copy link

@coderabbitai coderabbitai bot left a 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

Commits Files that changed from the base of the PR and between ba0f556 and c493fd6.
Files selected for processing (1)
  • tests/gui/widgets/test_docks.py (2 hunks)

@roomrys roomrys marked this pull request as draft March 18, 2024 18:37
@roomrys roomrys marked this pull request as ready for review March 18, 2024 18:38
Copy link

@coderabbitai coderabbitai bot left a 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

Commits Files that changed from the base of the PR and between c493fd6 and 907f873.
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

Copy link

@coderabbitai coderabbitai bot left a 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

Commits Files that changed from the base of the PR and between 907f873 and cd8662e.
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

Comment on lines +122 to +167
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
Copy link

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:

  1. 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.

  2. 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 UnlinkVideo command is correctly updating the underlying data model. This could involve checking that the session object reflects the changes made by the UnlinkVideo command.

  3. 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.

  4. 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 row

Consider 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.

Suggested change
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

@roomrys roomrys merged commit 145a1d1 into liezl/add-gui-elements-for-sessions Mar 18, 2024
2 of 5 checks passed
@roomrys roomrys deleted the advaith/add-cameras-table branch March 18, 2024 19:53
Copy link

@coderabbitai coderabbitai bot left a 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

Commits Files that changed from the base of the PR and between cd8662e and 77283d0.
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_table covers important aspects of the SessionsDock cameras table functionality. However, consider enhancing the test to verify that the UnlinkVideo command updates the underlying data model as expected.

Consider adding assertions to verify that the session object reflects the changes made by the UnlinkVideo command.


# Testing if cameras_table is loaded correctly

# Test if all comcorders are presented in the correct row
Copy link

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 row

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.

Suggested change
# 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
Copy link

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 reflected

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.

Suggested change
# 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]
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
MultiView Stack This PR is part of the MultView stacked PRs.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants