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

Removing everything related to camera in favour of camera plugin #2610

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from

Conversation

DraKen0009
Copy link
Contributor

@DraKen0009 DraKen0009 commented Nov 20, 2024

Proposed Changes

  • Removing everything related to camera from care and using dynamic approaches to load plugin data if present

Associated Issue

Merge Checklist

  • Tests added/fixed
  • Linting Complete

Only PR's with test cases included and passing lint and test pipelines will be reviewed

@ohcnetwork/care-backend-maintainers @ohcnetwork/care-backend-admins

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced dynamic asset class handling and validation in asset management.
    • Added new asset availability and service records viewsets.
    • Implemented a mutable enumeration system for asset classes.
  • Bug Fixes

    • Enhanced error handling for asset operations and validation processes.
  • Chores

    • Removed deprecated camera preset functionalities and associated viewsets.
    • Updated plugin configuration to include a new camera plugin.
  • Tests

    • Adjusted test cases to reflect changes in asset class handling and validation.

Copy link

coderabbitai bot commented Nov 20, 2024

📝 Walkthrough

Walkthrough

The changes in this pull request involve significant updates to various serializers, viewsets, and models related to asset management within the application. Key modifications include the addition of an asset_class field in the AssetSerializer, dynamic handling of asset classes in multiple serializers, and the removal of the CameraPresetSerializer and related viewsets. Additionally, a new migration alters the asset model's fields, enhancing the validation mechanisms. The router configuration has also been updated to reflect these changes, ensuring proper routing for the new asset functionalities.

Changes

File Change Summary
care/facility/api/serializers/asset.py Added asset_class field in AssetSerializer, updated validation logic, and refactored AssetActionSerializer for dynamic action choices.
care/facility/api/serializers/bed.py Updated AssetBedSerializer and ConsultationBedSerializer for dynamic asset class validation and exclusion logic.
care/facility/api/serializers/camera_preset.py Removed CameraPresetSerializer.
care/facility/api/viewsets/asset.py Enhanced filtering logic in AssetFilter and AssetLocationFilter, updated operate_assets method for better error handling.
care/facility/api/viewsets/camera_preset.py Removed AssetBedCameraPresetViewSet and CameraPresetViewSet.
care/facility/migrations/0468_alter_asset_asset_class_alter_asset_meta.py Migration to alter asset_class and meta fields in the asset model.
care/facility/models/asset.py Updated asset_class handling and added dynamic asset class choices method.
care/facility/models/bed.py Removed delete method from AssetBed.
care/facility/models/json_schema/asset.py Introduced AssetMetaRegistry for dynamic asset schema registration.
care/facility/tasks/asset_monitor.py Simplified check_asset_status function by removing specific handling for "ONVIF".
care/facility/tests/test_asset_api.py Removed tests for OnvifAsset and added a test for invalid asset class creation.
care/facility/tests/test_assetbed_api.py Updated test setup to remove references to unused assets.
care/utils/assetintegration/asset_classes.py Changed AssetClasses from enum.Enum to MutableEnum and registered asset classes dynamically.
care/utils/assetintegration/base.py Added new methods for expected behaviors in BaseAssetIntegration.
care/utils/assetintegration/hl7monitor.py Added new methods for action choices and asset status retrieval in HL7MonitorAsset.
care/utils/assetintegration/onvif.py Removed OnvifAsset class and associated logic.
care/utils/assetintegration/utils.py Introduced a mutable enumeration system.
care/utils/assetintegration/ventilator.py Added new methods for action choices and asset status retrieval in VentilatorAsset.
care/utils/models/validators.py Added DynamicJSONFieldSchemaValidator for dynamic schema validation.
config/api_router.py Removed camera preset viewsets and added new registrations for asset-related viewsets.
plug_config.py Introduced a new camera plugin configuration.

Possibly related PRs

  • fix asset class initialization #2611: Modifications to the AssetViewSet, particularly in the operate_assets method, which relates to the handling of asset classes and their initialization, directly connecting to the updates made in the AssetSerializer and AssetActionSerializer.

Suggested labels

bug

Suggested reviewers

  • vigneshhari
  • sainak
  • rithviknishad

"In the code where assets reside,
New fields and logic now abide.
Serializers dance, viewsets play,
Camera presets have gone away.
Migrations shift the model's core,
Dynamic choices open the door!" 🎉


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>, please review it.
    • Generate unit testing code 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 testing code 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 gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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 using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • 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/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@DraKen0009 DraKen0009 marked this pull request as ready for review November 22, 2024 17:29
@DraKen0009 DraKen0009 requested a review from a team as a code owner November 22, 2024 17:29
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.

Actionable comments posted: 16

🧹 Outside diff range and nitpick comments (22)
care/utils/assetintegration/asset_classes.py (1)

6-7: Add documentation to explain the empty class pattern.

While I'm sure you had your reasons, this empty class could use a docstring explaining the dynamic registration pattern. It would help future maintainers understand why we're using this... interesting approach.

 class AssetClasses(MutableEnum):
+    """
+    Dynamic registry for asset classes.
+    Asset types are registered at runtime using MutableEnum.register() 
+    instead of being defined as static enum members.
+    """
     pass
plug_config.py (1)

18-25: Consider a phased transition approach

If there's a need to maintain backward compatibility while transitioning away from camera functionality, perhaps we should:

  1. Document the deprecation timeline
  2. Add migration guides for existing users
  3. Consider feature flags to gradually phase out the camera functionality

Would you like help creating a deprecation plan or implementing feature flags?

care/utils/assetintegration/ventilator.py (2)

43-47: I suppose we could make this more... efficient

While the implementation works, we could skip the intermediate list creation and directly return the comprehension. Not that it matters much, but you know, every microsecond counts. 😊

@classmethod
def get_action_choices(cls):
-    choices = []
-    choices += [(e.value, e.name) for e in cls.VentilatorActions]
-    return choices
+    return [(e.value, e.name) for e in cls.VentilatorActions]

49-59: Would it kill you to add some docstrings?

These static methods could really use some documentation explaining the reasoning behind these hardcoded values. Future maintainers might appreciate knowing why ventilators can be linked to consultation beds but not to asset beds. Just saying... 🤷

@staticmethod
def is_movable():
+    """
+    Indicates whether ventilator assets can be physically relocated.
+    Returns True as ventilators are portable medical devices.
+    """
    return True

@staticmethod
def can_be_linked_to_consultation_bed():
+    """
+    Indicates whether ventilator can be associated with consultation beds.
+    Returns True as ventilators are commonly used during consultations.
+    """
    return True

@staticmethod
def can_be_linked_to_asset_bed():
+    """
+    Indicates whether ventilator can be associated with asset beds.
+    Returns False as ventilators have their own asset management workflow.
+    """
    return False
care/utils/assetintegration/hl7monitor.py (1)

Line range hint 13-19: Would it kill you to add some documentation about required metadata?

While the error handling is... adequate, it would be so nice if we could document what metadata keys are actually required. You know, for those of us who might want to use this class someday.

Consider adding a docstring like:

def __init__(self, meta):
    """Initialize HL7MonitorAsset with required metadata.
    
    Args:
        meta (dict): Required metadata including:
            - host: The device host address
            - id: The asset identifier
            [add other required keys...]
    
    Raises:
        ValidationError: If any required metadata keys are missing
    """
care/utils/assetintegration/base.py (2)

85-99: Oh look, someone's discovered static methods!

The implementation is mostly fine, but there are a few things that could make it even better:

  1. Those parentheses around NotImplementedError at line 98 are just showing off.
  2. These methods could really use some return type hints to help implementers.
     @staticmethod
-    def can_be_linked_to_consultation_bed():
+    def can_be_linked_to_consultation_bed() -> bool:
         error = "'can_be_linked_to_consultation_bed()' method is not implemented"
         raise NotImplementedError(error)

     @staticmethod
-    def can_be_linked_to_asset_bed():
+    def can_be_linked_to_asset_bed() -> bool:
         error = "'can_be_linked_to_asset_bed()' method is not implemented"
         raise NotImplementedError(error)

     @staticmethod
-    def is_movable():
+    def is_movable() -> bool:
         error = "'is_movable()' method is not implemented"
-        raise (NotImplementedError(error))
+        raise NotImplementedError(error)

Camera-related code still exists and needs attention

It seems the camera functionality hasn't been completely removed as intended. There are several remaining references that should probably be addressed:

  • Active camera-related code in:
    • plug_config.py: Still references camera plugin and its GitHub repository
    • care/facility/models/camera_preset.py: Contains complete camera preset model
    • care/utils/assetintegration/schema.py: Contains camera-specific schema validations
    • care/facility/api/serializers/asset.py: Has camera access key handling
    • care/facility/api/viewsets/patient_consultation.py: Contains preset name logic

While the base integration class has been cleaned up, these lingering camera-specific implementations suggest the migration to a plugin system isn't quite complete yet. Perhaps we should consider moving these to the camera plugin repository?

🔗 Analysis chain

Line range hint 1-102: Verify complete removal of camera-related code

The changes align well with moving towards a dynamic plugin system. However, let's verify that all camera-related code has been properly removed.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining camera-related code
echo "Searching for camera-related code..."
rg -i "camera|onvif|preset|ptz" --type py

echo "Searching for camera-related imports..."
ast-grep --pattern 'import $_ from "camera"'
ast-grep --pattern 'from camera import $_'

Length of output: 9863

care/facility/tasks/asset_monitor.py (3)

Line range hint 20-35: Consider filtering out inactive assets... if you want to be efficient.

The query could benefit from excluding inactive or deleted assets (if such flags exist) to avoid unnecessary processing. Just a thought. 🤷

    assets = (
        Asset.objects.exclude(Q(asset_class=None) | Q(asset_class=""))
+       .filter(is_active=True)  # assuming this field exists
        .select_related(
            "current_location",
            "current_location__facility",
        )

Line range hint 82-119: This nested logic is... interesting.

The status processing logic could use some refactoring for clarity. Also, those dictionary accesses without .get() are living dangerously, don't you think?

Consider extracting the status determination logic:

+ def determine_asset_status(status_record, local_ip_address):
+     if not status_record.get("status"):
+         return AvailabilityStatus.DOWN
+     
+     asset_status = status_record.get("status", {}).get(local_ip_address, "down")
+     
+     status_map = {
+         "up": AvailabilityStatus.OPERATIONAL,
+         "maintenance": AvailabilityStatus.UNDER_MAINTENANCE,
+         "down": AvailabilityStatus.DOWN
+     }
+     return status_map.get(asset_status, AvailabilityStatus.DOWN)

  # In the main loop:
- if asset.meta.get("local_ip_address") in status_record.get("status", {}):
-     asset_status = status_record["status"][asset.meta.get("local_ip_address")]
- else:
-     asset_status = "down"
-
- if asset_status == "up":
-     new_status = AvailabilityStatus.OPERATIONAL
- elif asset_status == "maintenance":
-     new_status = AvailabilityStatus.UNDER_MAINTENANCE
+ new_status = determine_asset_status(
+     status_record,
+     asset.meta.get("local_ip_address")
+ )

Camera-related code still exists and needs attention

It seems we've found quite a few camera-related components that are still lingering around, perhaps waiting for someone to notice them. Key areas requiring cleanup:

  • plug_config.py: Contains camera plugin configuration and GitHub repository reference
  • config/settings/base.py: Has a commented-out camera plugin
  • care/facility/models/camera_preset.py: Complete file dedicated to camera presets
  • care/utils/assetintegration/schema.py: Contains ONVIF and camera access key schema definitions
  • care/facility/api/serializers/asset.py: Includes camera access key handling logic
  • care/facility/tasks/plausible_stats.py: Contains camera-related analytics events

While some occurrences in migration files can be safely ignored, the active code files still contain camera functionality that should be removed as part of this cleanup.

🔗 Analysis chain

Line range hint 20-119: Verify complete removal of camera-related code.

The ONVIF-specific handling has been removed, but let's ensure no other camera-related code remains.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for any remaining camera-related code
rg -i "camera|onvif" --type py

Length of output: 9069

config/api_router.py (1)

Several camera-related files and code still need attention

I see you've been selectively thorough with the camera removal. There are still quite a few camera-related files and code snippets that need addressing:

  • care/facility/models/camera_preset.py - The entire camera preset model file
  • care/facility/migrations/0466_camera_presets.py - Migration for camera presets
  • plug_config.py - Contains camera plugin configuration
  • care/facility/tasks/plausible_stats.py - Has camera-related analytics events
  • care/utils/assetintegration/schema.py - Contains camera access key schema definitions
  • care/facility/api/serializers/asset.py - Has camera access key handling logic
  • care/facility/api/viewsets/patient_consultation.py - Contains preset name filtering

Would be lovely if we could clean these up to maintain consistency with the stated goal of "removing everything related to camera for plugin". 😊

🔗 Analysis chain

Line range hint 224-229: New routes look good, but let's verify camera removal is complete

The addition of availability and service record routes aligns well with the dynamic approach. However, I notice you've been quite thorough in removing the camera preset routes... almost too thorough. 😏

Let's make absolutely sure we haven't missed any camera-related code:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for any remaining camera-related code

# Search for any remaining camera-related files
echo "Checking for camera-related files..."
fd -t f -e py camera

# Search for any remaining camera-related code
echo "Checking for camera-related code..."
rg -i "camera|preset" --type py

# Search for any orphaned camera-related migrations
echo "Checking for camera-related migrations..."
fd -t f -e py . "migrations" | xargs rg -i "camera|preset"

Length of output: 18410

care/utils/models/validators.py (1)

54-60: The docstring could be a tiny bit more helpful, don't you think?

While the docstring explains what the class does, it would be even better if it included:

  • Example usage scenarios
  • Sample code snippet
  • Explanation of when to prefer this over the base class
     """
     A dynamic JSONField schema validator that generates the schema at runtime.
     Inherits from JSONFieldSchemaValidator for reusability.
+    
+    Example usage:
+    ```python
+    def generate_schema():
+        return {"type": "object", "properties": {"dynamic_field": {"type": "string"}}}
+    
+    field = models.JSONField(validators=[DynamicJSONFieldSchemaValidator(generate_schema)])
+    ```
+    
+    Use this validator when the schema needs to be determined at validation time rather
+    than at class definition time.
     """
care/facility/tests/test_assetbed_api.py (3)

80-80: Maybe consider capitalizing API for consistency?

The docstring uses "api" while the rest of the codebase typically uses "API".

-        Constructs the url for assetbed api
+        Constructs the URL for AssetBed API

91-91: Would be nice to add a comment explaining the assertion count

The test expects exactly 2 assetbeds, but it's not immediately clear which ones are being counted. A brief comment would help future maintainers understand the test's expectations.

-        self.assertEqual(response.data["count"], 2)
+        # Expects 2 assetbeds: bed1-asset1 and bed3-asset3
+        self.assertEqual(response.data["count"], 2)

Also applies to: 214-217


Line range hint 255-291: Perhaps we could reduce some duplication in the test cases?

The update and patch test cases share quite a bit of similar setup and validation logic. Consider extracting common test setup into helper methods.

For example:

def _assert_invalid_asset_update(self, method, data):
    """Helper method for testing invalid asset updates"""
    response = method(
        self.get_url(external_id=self.assetbed.external_id),
        data,
        format="json"
    )
    self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def _verify_assetbed_state(self, bed, asset, meta=None):
    """Helper method to verify assetbed state"""
    self.assertEqual(self.assetbed.bed.external_id, bed.external_id)
    self.assertEqual(self.assetbed.asset.external_id, asset.external_id)
    if meta is not None:
        self.assertEqual(self.assetbed.meta, meta)

This would help reduce code duplication between test_update_assetbed and test_patch_assetbed.

Also applies to: 307-343

care/facility/tests/test_asset_api.py (1)

Line range hint 1-450: Consider adding migration test coverage

Since we're removing camera functionality, it would be really nice if we had tests to verify that existing camera assets are handled gracefully during migration. You know, just to be thorough... 😊

Consider adding a test case like:

def test_migration_of_existing_camera_assets(self):
    # Create an asset with old camera configuration
    old_asset = self.create_asset(
        self.asset_location,
        meta={"legacy_camera_config": "some_value"}
    )
    
    # Verify the asset is migrated correctly
    response = self.client.get(f"/api/v1/asset/{old_asset.external_id}/")
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    self.assertNotIn("legacy_camera_config", response.data["meta"])
care/facility/api/viewsets/asset.py (2)

204-210: Consider caching the non-movable assets list

The list comprehension is executed on every filter call, which might not be the most efficient approach. Perhaps we could cache this list since it only changes when asset classes change?

+ # At module level
+ IMMOVABLE_ASSETS = [member.name for member in AssetClasses.all() if not member.is_movable]

  def filter_is_permanent(self, queryset, _, value):
      if value not in EMPTY_VALUES:
-         movable_assets = [
-             member.name for member in AssetClasses.all() if not member.is_movable
-         ]
          if value:
-             queryset = queryset.filter(asset_class__in=movable_assets)
+             queryset = queryset.filter(asset_class__in=IMMOVABLE_ASSETS)
          else:
-             queryset = queryset.exclude(asset_class__in=movable_assets)
+             queryset = queryset.exclude(asset_class__in=IMMOVABLE_ASSETS)

475-477: Improve method naming as suggested by the comment

The comment suggests that the method name needs improvement. Perhaps we could help with that?

Would you like me to suggest some alternative names for can_be_linked_to_asset_bed()? Here are a few options:

  • is_bed_linkable()
  • supports_bed_association()
  • is_bed_compatible()

Let me know if you'd like me to create an issue to track this naming improvement.

care/utils/assetintegration/utils.py (2)

25-27: Ensure consistent error handling in __getitem__ method

The __getitem__ method currently raises a KeyError if the name is not found in _registry. For consistency with __getattr__, consider raising an AttributeError with a descriptive message.

Apply this diff to improve error handling:

 def __getitem__(cls, name):
-    return cls._registry[name]
+    try:
+        return cls._registry[name]
+    except KeyError:
+        error = f"{name} not found in {cls.__name__}"
+        raise AttributeError(error)

49-56: Consider thread safety when modifying _registry

If MutableEnum is used in a multithreaded context, concurrent modifications to _registry could lead to race conditions. It might be wise to add thread-safe mechanisms, such as threading locks, to ensure safe member registration.

Here's how you might implement it:

import threading

class MutableEnum(metaclass=MutableEnumMeta):
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls._registry = {}
+       cls._lock = threading.Lock()

    @classmethod
    def register(cls, name, value):
+       with cls._lock:
            if name in cls._registry:
                error = f"{name} is already registered."
                raise ValueError(error)
            cls._registry[name] = MutableEnumMember(name, value)
care/facility/models/json_schema/asset.py (1)

1-23: Thread safety considerations for AssetMetaRegistry

Not that it's a big concern, but using a mutable class-level attribute like _registry might cause issues in multi-threaded environments. You might want to ensure thread safety if concurrent access is expected.

care/facility/api/serializers/asset.py (1)

236-238: Consider renaming can_be_linked_to_asset_bed for clarity.

The inline comment suggests the current naming might not be ideal. Renaming it to something more descriptive could improve code readability.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between d23cbcb and 90e8259.

📒 Files selected for processing (23)
  • care/facility/api/serializers/asset.py (3 hunks)
  • care/facility/api/serializers/bed.py (3 hunks)
  • care/facility/api/serializers/camera_preset.py (0 hunks)
  • care/facility/api/viewsets/asset.py (3 hunks)
  • care/facility/api/viewsets/camera_preset.py (0 hunks)
  • care/facility/migrations/0468_alter_asset_asset_class_alter_asset_meta.py (1 hunks)
  • care/facility/models/__init__.py (0 hunks)
  • care/facility/models/asset.py (4 hunks)
  • care/facility/models/bed.py (0 hunks)
  • care/facility/models/json_schema/asset.py (1 hunks)
  • care/facility/tasks/asset_monitor.py (2 hunks)
  • care/facility/tests/test_asset_api.py (1 hunks)
  • care/facility/tests/test_assetbed_api.py (10 hunks)
  • care/utils/assetintegration/asset_classes.py (1 hunks)
  • care/utils/assetintegration/base.py (1 hunks)
  • care/utils/assetintegration/hl7monitor.py (1 hunks)
  • care/utils/assetintegration/onvif.py (0 hunks)
  • care/utils/assetintegration/utils.py (1 hunks)
  • care/utils/assetintegration/ventilator.py (1 hunks)
  • care/utils/models/validators.py (2 hunks)
  • config/api_router.py (1 hunks)
  • config/settings/base.py (1 hunks)
  • plug_config.py (1 hunks)
💤 Files with no reviewable changes (5)
  • care/facility/api/serializers/camera_preset.py
  • care/facility/api/viewsets/camera_preset.py
  • care/facility/models/init.py
  • care/facility/models/bed.py
  • care/utils/assetintegration/onvif.py
✅ Files skipped from review due to trivial changes (1)
  • config/settings/base.py
🧰 Additional context used
🪛 Ruff (0.7.0)
care/facility/migrations/0468_alter_asset_asset_class_alter_asset_meta.py

11-11: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)


11-11: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)


16-16: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)


17-17: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)


21-21: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)


22-22: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

🔇 Additional comments (16)
care/utils/assetintegration/asset_classes.py (1)

10-11: LGTM! Dynamic registration implemented correctly.

The dynamic registration approach aligns well with the PR objectives, making the system more flexible for future asset types.

care/facility/migrations/0468_alter_asset_asset_class_alter_asset_meta.py (3)

3-12: Dependencies and imports look fine... I suppose.

The migration chain and necessary imports are properly structured.

🧰 Tools
🪛 Ruff (0.7.0)

11-11: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)


11-11: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)


15-19: Verify the impact on existing asset_class values

Making the asset_class field nullable could affect existing records. It would be so nice if we could verify there are no non-null values that might be affected.

✅ Verification successful

Making asset_class nullable is safe and aligned with code changes

The field modification is part of a larger effort to remove camera-related components, and the codebase analysis shows this change is well-handled:

  • The migration is preceded by 0466_camera_presets.py which already handles cleanup of asset_class data
  • The field is already validated in Asset.save() method against dynamic choices from AssetClasses
  • The asset_class field is optional in serializers and properly handled in views with appropriate filters
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for existing asset_class values that might be affected
echo "Checking for existing asset_class values..."
ast-grep --pattern 'class Asset($$$):
  $$$
  asset_class = $$$
  $$$' 

# Look for any references to asset_class in views/serializers
echo "Checking for asset_class references in views and serializers..."
rg "asset_class" --type py

Length of output: 20414

🧰 Tools
🪛 Ruff (0.7.0)

16-16: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)


17-17: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)


20-24: ⚠️ Potential issue

Using dict() as default might not be the best idea...

Using a mutable default value (dict) in Django models can lead to unexpected behavior. While it might work in migrations, it would be slightly better to use a callable instead.

Consider this alternative:

-            field=models.JSONField(blank=True, default=dict, validators=[care.utils.models.validators.DynamicJSONFieldSchemaValidator(care.facility.models.json_schema.asset.get_dynamic_asset_meta)]),
+            field=models.JSONField(blank=True, default=dict, validators=[care.utils.models.validators.DynamicJSONFieldSchemaValidator(care.facility.models.json_schema.asset.get_dynamic_asset_meta)]),

Also, let's verify the schema validator won't break existing data.

🧰 Tools
🪛 Ruff (0.7.0)

21-21: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)


22-22: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

care/utils/assetintegration/hl7monitor.py (2)

43-47: Well, this is actually quite nice.

Clean implementation using enum values for dynamic action choices. I suppose we can't really complain about this one.


49-59: Oh, I see we're playing the "guess what these methods do" game.

While these capability methods are probably self-explanatory, it would be fantastic if we could verify that other asset types implement these consistently.

Consider adding docstrings to explain the purpose and impact of these capability flags:

@staticmethod
def is_movable():
    """
    Indicates if this asset type can be physically relocated.
    Used for asset management workflows.
    """
    return False
care/utils/models/validators.py (1)

2-2: LGTM! The import statement looks perfectly fine.

The addition of Callable to the existing import statement from collections.abc is clean and well-organized.

care/facility/tests/test_assetbed_api.py (1)

Line range hint 1-364: Just checking - are we testing all asset class combinations?

The tests cover basic CRUD operations nicely, but it might be worth adding test cases for different asset class combinations to ensure we're handling all scenarios correctly.

Would you like me to help generate additional test cases for different asset class combinations?

care/facility/tests/test_asset_api.py (1)

Line range hint 1-450: Verify removal of all camera-related test cases

I notice we've removed some camera-related tests, but let's make absolutely sure we haven't missed any lingering camera references in the remaining test cases.

✅ Verification successful

Camera-related test cases have been properly handled... mostly

The only remaining camera references are in the negative test cases for VentilatorAsset and HL7MonitorAsset, which actually verify that camera_access_key is not allowed for these asset types. These test cases are serving their purpose by ensuring camera configurations don't sneak into non-camera assets, so they should stay.

• Lines 130-136: The test cases appropriately validate that camera_access_key is rejected for both ventilator and HL7 monitor assets

I suppose one could say the test coverage is quite thorough, even if it's just making sure things that shouldn't be there... aren't there.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining camera-related test cases or assertions
rg -i "camera|onvif" --type py

Length of output: 9069


Script:

#!/bin/bash
# Let's check if there are any camera-related test cases in the test file that we might have missed
ast-grep --pattern 'def test_$_camera$_$_($_) { $$$ }' care/facility/tests/test_asset_api.py

# Also check for any camera-related assertions or test data
rg -i "camera|onvif" care/facility/tests/test_asset_api.py -A 2 -B 2

Length of output: 991

care/facility/models/asset.py (3)

10-10: Import of get_dynamic_asset_meta is appropriate

Updating the import to get_dynamic_asset_meta correctly reflects the move towards dynamic asset metadata handling.


17-20: Validators are properly imported

Including DynamicJSONFieldSchemaValidator and PhoneNumberValidator ensures that the necessary validators are available for the model fields.


101-103: Dynamic JSON schema validation is well-implemented

Adding DynamicJSONFieldSchemaValidator(get_dynamic_asset_meta) to the meta field enhances validation by ensuring the JSON data conforms to the expected schema.

care/facility/api/serializers/bed.py (3)

124-133: Dynamic asset class validation enhances flexibility

This change allows for dynamic determination of valid asset classes when linking assets to beds, improving maintainability.


323-327: Dynamic exclusion of asset classes in consultation bed creation

By dynamically determining which asset classes cannot be linked to a consultation bed, the code becomes more flexible and maintainable.


343-343: Properly excluding invalid asset classes from assets query

This ensures that only appropriate assets are considered when assigning to a consultation bed.

care/facility/api/serializers/asset.py (1)

144-146: Adding 'asset_class' field aligns with dynamic asset class handling.

This addition correctly integrates asset classes into the serializer.

care/utils/assetintegration/asset_classes.py Show resolved Hide resolved
plug_config.py Show resolved Hide resolved
plug_config.py Show resolved Hide resolved
Comment on lines +61 to +62
def get_asset_status(self):
return self.api_get(self.get_url("devices/status"))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Interesting how we forgot about error handling and timeouts here

The get_asset_status method seems to be missing some rather important features that exist in handle_action:

  1. Error handling for API failures
  2. Timeout parameter to prevent hanging requests
  3. Documentation about the expected response format

Here's a slightly more robust implementation:

-    def get_asset_status(self):
-        return self.api_get(self.get_url("devices/status"))
+    def get_asset_status(self, timeout=None):
+        """
+        Retrieves the current status of the ventilator device.
+        
+        Args:
+            timeout (float, optional): Request timeout in seconds
+            
+        Returns:
+            dict: Device status information
+            
+        Raises:
+            ValidationError: If the API request fails
+        """
+        try:
+            return self.api_get(self.get_url("devices/status"), timeout=timeout)
+        except Exception as e:
+            raise ValidationError({"status": str(e)}) from e
📝 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. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_asset_status(self):
return self.api_get(self.get_url("devices/status"))
def get_asset_status(self, timeout=None):
"""
Retrieves the current status of the ventilator device.
Args:
timeout (float, optional): Request timeout in seconds
Returns:
dict: Device status information
Raises:
ValidationError: If the API request fails
"""
try:
return self.api_get(self.get_url("devices/status"), timeout=timeout)
except Exception as e:
raise ValidationError({"status": str(e)}) from e

Comment on lines +61 to +62
def get_asset_status(self):
return self.api_get(self.get_url("devices/status"))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

I'm sure nothing could possibly go wrong with this implementation.

The get_asset_status method seems a bit... optimistic. It might be worth considering:

  • Error handling for API failures
  • Timeout parameter
  • Documentation about the expected response format

Consider updating it like this:

def get_asset_status(self):
+    """
+    Fetch the current status of the asset from the device.
+    
+    Returns:
+        dict: Status information containing:
+            - state: Current device state
+            - last_seen: Timestamp of last communication
+            [document other returned fields...]
+    
+    Raises:
+        ValidationError: If the device is unreachable or returns invalid status
+    """
-    return self.api_get(self.get_url("devices/status"))
+    try:
+        return self.api_get(self.get_url("devices/status"), timeout=5)
+    except Exception as e:
+        raise ValidationError({"status": f"Failed to fetch device status: {str(e)}"})
📝 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. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_asset_status(self):
return self.api_get(self.get_url("devices/status"))
def get_asset_status(self):
"""
Fetch the current status of the asset from the device.
Returns:
dict: Status information containing:
- state: Current device state
- last_seen: Timestamp of last communication
[document other returned fields...]
Raises:
ValidationError: If the device is unreachable or returns invalid status
"""
try:
return self.api_get(self.get_url("devices/status"), timeout=5)
except Exception as e:
raise ValidationError({"status": f"Failed to fetch device status: {str(e)}"})

Comment on lines +46 to +47
_registry = {}

Copy link

@coderabbitai coderabbitai bot Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Separate _registry per subclass to prevent unintended sharing

Currently, _registry is defined at the base class MutableEnum, meaning all subclasses share the same registry. This could lead to different enums unexpectedly sharing members. To ensure each enum class maintains its own members, initialize _registry in each subclass.

Apply this diff to fix the issue:

 class MutableEnum(metaclass=MutableEnumMeta):
     """
     A base class to mimic enum behavior with mutability.
     """
-    _registry = {}
+    def __init_subclass__(cls, **kwargs):
+        super().__init_subclass__(**kwargs)
+        cls._registry = {}
📝 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. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls._registry = {}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed according to me . Need more input though

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DraKen0009, certainly. The issue arises because _registry is currently defined in the base class MutableEnum, causing all subclasses to share the same _registry dictionary. This means that if you have multiple subclasses of MutableEnum, their members could overlap unintentionally. By initializing _registry in each subclass using __init_subclass__, you ensure that each subclass maintains its own separate registry of members. This prevents any unintended sharing between different enums. Hope this helps clarify!

care/facility/models/json_schema/asset.py Show resolved Hide resolved
care/facility/models/asset.py Show resolved Hide resolved
care/facility/models/asset.py Show resolved Hide resolved
care/facility/api/serializers/asset.py Outdated Show resolved Hide resolved
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.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (6)
care/facility/api/serializers/asset.py (3)

144-146: LGTM, though some documentation would be lovely.

The dynamic asset class field implementation aligns well with the model's requirements. I see you've made it optional, which is... thoughtful.

A docstring explaining the field's purpose and valid choices would make this even better, but I suppose we can live without it. 🙃


236-238: That inline comment is practically begging for a refactor.

The filtering logic works, but that inline comment about naming is... interesting. Let's make it more explicit.

Consider extracting this into a descriptive method:

-asset_class__in=[
-    member.name
-    for member in AssetClasses.all()
-    if member.value.can_be_linked_to_asset_bed  # need better naming
-],
+asset_class__in=AssetClasses.get_linkable_asset_classes(),

And in AssetClasses:

@classmethod
def get_linkable_asset_classes(cls):
    return [
        member.name
        for member in cls.all()
        if member.value.can_be_linked_to_asset_bed
    ]

415-419: The flattening looks good, but have you considered caching?

While the implementation correctly flattens the choices as suggested, we might want to avoid regenerating this list on every serializer instantiation.

Consider caching the choices:

+@classmethod
+@cached_property
+def get_flattened_action_choices(cls):
+    return [
+        choice
+        for asset_class in AssetClasses.all()
+        for choice in asset_class.value.get_action_choices()
+    ]

 class AssetActionSerializer(Serializer):
     type = ChoiceField(
-        choices=[
-            choice
-            for asset_class in AssetClasses.all()
-            for choice in asset_class.value.get_action_choices()
-        ],
+        choices=get_flattened_action_choices,
         required=True,
     )

I mean, unless you enjoy regenerating the same list repeatedly... 😏

care/facility/tests/test_asset_api.py (1)

138-147: The invalid asset class test is good, but could be even better...

While the test correctly validates rejection of invalid asset classes, it might be nice to add a few more edge cases. You know, just to be thorough... 😏

Consider adding these test variations:

 def test_create_asset_with_invalid_asset_class(self):
-    sample_data = {
-        "name": "Test Asset",
-        "asset_type": 50,
-        "location": self.asset_location.external_id,
-        "asset_class": "INVALID_CLASS",
-    }
-    response = self.client.post("/api/v1/asset/", sample_data)
-    self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+    invalid_classes = [
+        "INVALID_CLASS",
+        "",  # empty string
+        "camera",  # lowercase
+        "CAMERA",  # removed class
+        None,  # null value
+    ]
+    
+    for invalid_class in invalid_classes:
+        with self.subTest(asset_class=invalid_class):
+            sample_data = {
+                "name": "Test Asset",
+                "asset_type": 50,
+                "location": self.asset_location.external_id,
+                "asset_class": invalid_class,
+            }
+            response = self.client.post("/api/v1/asset/", sample_data)
+            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
care/facility/api/viewsets/asset.py (2)

393-401: Consider renaming loop variable for clarity

Using asset as the loop variable in available_asset_classes = [asset.name for asset in AssetClasses.all()] might be a bit confusing since asset is also the variable representing the current asset. Maybe renaming it to member or asset_class could enhance readability.


475-477: Improve method naming for clarity

Not to nitpick, but since the comment mentions "better naming required," perhaps renaming can_be_linked_to_asset_bed() to something like supports_bed_linkage() might make its purpose clearer.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 90e8259 and e7c774e.

📒 Files selected for processing (4)
  • care/facility/api/serializers/asset.py (3 hunks)
  • care/facility/api/viewsets/asset.py (3 hunks)
  • care/facility/tests/test_asset_api.py (1 hunks)
  • care/utils/models/validators.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • care/utils/models/validators.py
🧰 Additional context used
📓 Learnings (1)
care/facility/api/serializers/asset.py (1)
Learnt from: DraKen0009
PR: ohcnetwork/care#2610
File: care/facility/models/asset.py:87-90
Timestamp: 2024-11-22T19:10:46.229Z
Learning: In the `Asset` model (`care/facility/models/asset.py`), the `asset_class` field cannot include the `choices` parameter because the choices are dynamic and are handled through the serializers and the `save` method.
🔇 Additional comments (1)
care/facility/tests/test_asset_api.py (1)

133-133: LGTM! The asset_class field addition looks good.

The explicit "VENTILATOR" asset class is a valid choice and aligns well with the move away from camera-specific implementations.

Comment on lines 204 to 210
movable_assets = [
member.name for member in AssetClasses.all() if not member.is_movable
]
if value:
queryset = queryset.filter(
asset_class__in=[
AssetClasses.ONVIF.name,
AssetClasses.HL7MONITOR.name,
]
)
queryset = queryset.filter(asset_class__in=movable_assets)
else:
queryset = queryset.exclude(
asset_class__in=[
AssetClasses.ONVIF.name,
AssetClasses.HL7MONITOR.name,
]
)
queryset = queryset.exclude(asset_class__in=movable_assets)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Variable naming inconsistency with 'movable_assets'

It appears that movable_assets actually contains asset classes where member.is_movable is False, meaning they are non-movable assets. Perhaps renaming the variable to non_movable_assets would improve clarity.

@@ -4,7 +4,6 @@
from .ambulance import * # noqa
from .asset import * # noqa
from .bed import * # noqa
from .camera_preset import * # noqa
Copy link
Member

@rithviknishad rithviknishad Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing this now will cause a pending make migration as 0 references are there to the old CameraPreset model now. lets remove it in the second part PR where we would be deleting the old model.

image

Copy link

codecov bot commented Nov 27, 2024

Codecov Report

Attention: Patch coverage is 69.44444% with 44 lines in your changes missing coverage. Please review.

Project coverage is 69.43%. Comparing base (f217f3a) to head (6d4bf27).

Files with missing lines Patch % Lines
care/utils/assetintegration/utils.py 61.11% 12 Missing and 2 partials ⚠️
care/utils/models/validators.py 52.63% 7 Missing and 2 partials ⚠️
care/facility/api/viewsets/asset.py 0.00% 6 Missing ⚠️
care/utils/assetintegration/base.py 60.00% 6 Missing ⚠️
care/facility/models/asset.py 72.72% 2 Missing and 1 partial ⚠️
care/utils/assetintegration/hl7monitor.py 87.50% 2 Missing ⚠️
care/utils/assetintegration/ventilator.py 87.50% 2 Missing ⚠️
care/facility/models/json_schema/asset.py 92.30% 1 Missing ⚠️
care/facility/tasks/asset_monitor.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #2610      +/-   ##
===========================================
- Coverage    69.58%   69.43%   -0.15%     
===========================================
  Files          211      209       -2     
  Lines        11875    11873       -2     
  Branches      1201     1197       -4     
===========================================
- Hits          8263     8244      -19     
- Misses        3246     3258      +12     
- Partials       366      371       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

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.

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (3)
care/facility/api/serializers/asset.py (3)

144-144: Consider enhancing asset_class validation with explicit choices

While the validation works, it might be slightly more robust to define the choices directly on the field rather than just in the validate method. You know, just in case someone forgets to check the validation in the future.

-    asset_class = serializers.CharField(required=False)
+    asset_class = serializers.CharField(
+        required=False,
+        allow_null=True,
+        allow_blank=True,
+        choices=Asset.get_asset_class_choices
+    )

Also applies to: 167-173


241-243: That comment is practically begging for attention

The comment # need better naming is quite right. The predicate can_be_linked_to_asset_bed could be more descriptive of its actual purpose in IP address validation.

Consider renaming to something more specific like requires_unique_ip_address or is_network_device.


Line range hint 359-371: Looks like we missed a spot with the camera removal

The to_representation method still contains camera-specific logic for handling camera_access_key. Since we're removing everything related to cameras, shouldn't this be removed as well?

     def to_representation(self, instance: Asset):
         data = super().to_representation(instance)
         data["ip_address"] = instance.meta.get("local_ip_address")
-        if camera_access_key := instance.meta.get("camera_access_key"):
-            values = camera_access_key.split(":")
-            if len(values) == 3:  # noqa: PLR2004
-                data["username"], data["password"], data["access_key"] = values
         return data
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between e7c774e and 19c2b74.

📒 Files selected for processing (1)
  • care/facility/api/serializers/asset.py (4 hunks)
🔇 Additional comments (1)
care/facility/api/serializers/asset.py (1)

420-424: Nice implementation of the flattened choices

The list comprehension elegantly handles the flattening of choices from all asset classes. This addresses the previous feedback perfectly.

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.

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (3)
care/facility/api/serializers/asset.py (3)

144-144: Consider moving validation to field level

The asset_class validation could be more elegantly handled at the field level using ChoiceField instead of CharField. You know, just like how it's done for other choice fields in this file...

-    asset_class = serializers.CharField(required=False)
+    asset_class = ChoiceField(
+        choices=Asset.get_asset_class_choices,
+        required=False
+    )

167-173: Enhance error message with valid choices

The error message could be more helpful by including the list of valid choices. I mean, if we're going to tell users they're wrong, we might as well tell them how to be right...

-            error = f"{attrs['asset_class']} is not a valid asset class"
+            valid_choices = ", ".join(Asset.get_asset_class_choices())
+            error = f"{attrs['asset_class']} is not a valid asset class. Valid choices are: {valid_choices}"

241-243: Improve method naming and readability

The comment "need better naming" is quite right. The method name can_be_linked_to_asset_bed is a bit... mysterious. Also, this list comprehension could be more readable.

Consider:

  1. Renaming the method to something more descriptive like supports_ip_address_linking
  2. Making the list comprehension more readable:
-                            member.name
-                            for member in AssetClasses.all()
-                            if member.value.can_be_linked_to_asset_bed()
+                            member.name
+                            for member in AssetClasses.all()
+                            if member.value.supports_ip_address_linking()  # renamed for clarity
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 19c2b74 and 8d4581f.

📒 Files selected for processing (2)
  • care/facility/api/serializers/asset.py (4 hunks)
  • care/facility/api/viewsets/asset.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • care/facility/api/viewsets/asset.py
🔇 Additional comments (1)
care/facility/api/serializers/asset.py (1)

420-424: Structure choices properly for ChoiceField

This looks suspiciously similar to an issue that was flagged before...

The current structure might cause issues with ChoiceField. Consider flattening the choices as previously suggested:

-        choices=[
-            choice
-            for asset_class in AssetClasses.all()
-            for choice in asset_class.value.get_action_choices()
-        ],
+        choices=lambda: [
+            choice for asset_class in AssetClasses.all()
+            for choice in asset_class.value.get_action_choices()
+        ],

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Move Camera Integrations to a Plug
2 participants