✨(organization) add metadata update command
This allows to update the Organization metadata with default values.
This commit is contained in:
1
src/backend/core/management/__init__.py
Normal file
1
src/backend/core/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Management commands for core app."""
|
||||||
1
src/backend/core/management/commands/__init__.py
Normal file
1
src/backend/core/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Management commands for core app."""
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""
|
||||||
|
Management command for filling organization metadata with default values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from core import models
|
||||||
|
from core.utils.json_schema import generate_default_from_schema
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""Management command to fill organization metadata with default values."""
|
||||||
|
|
||||||
|
help = "Fill organization metadata with default values"
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
"""Fill organizations metadata missing values with default values."""
|
||||||
|
organization_metadata_schema = models.get_organization_metadata_schema()
|
||||||
|
|
||||||
|
if not organization_metadata_schema:
|
||||||
|
message = "No organization metadata schema defined."
|
||||||
|
self.stdout.write(self.style.ERROR(message))
|
||||||
|
return
|
||||||
|
|
||||||
|
default_metadata = generate_default_from_schema(organization_metadata_schema)
|
||||||
|
for organization in models.Organization.objects.all():
|
||||||
|
organization.metadata = {**default_metadata, **organization.metadata}
|
||||||
|
|
||||||
|
# Save the organization with the updated metadata
|
||||||
|
# We don't use bulk update because we want to trigger the clean method
|
||||||
|
organization.save(update_fields=["metadata", "updated_at"])
|
||||||
|
|
||||||
|
message = "Organization metadata filled with default values."
|
||||||
|
self.stdout.write(self.style.SUCCESS(message))
|
||||||
1
src/backend/core/tests/management_commands/__init__.py
Normal file
1
src/backend/core/tests/management_commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Test for management commands for core app."""
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
"""Tests for the fill_organization_metadata management command."""
|
||||||
|
|
||||||
|
from io import StringIO
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core import factories
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="command_output")
|
||||||
|
def command_output_fixture():
|
||||||
|
"""Capture command output."""
|
||||||
|
out = StringIO()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_fill_organization_metadata_no_schema(command_output):
|
||||||
|
"""Test command behavior when no schema is available."""
|
||||||
|
organization_1 = factories.OrganizationFactory(
|
||||||
|
name="Org with empty metadata",
|
||||||
|
metadata={},
|
||||||
|
with_registration_id=True,
|
||||||
|
)
|
||||||
|
organization_2 = factories.OrganizationFactory(
|
||||||
|
name="Org with partial metadata",
|
||||||
|
metadata={"existing_key": "existing_value"},
|
||||||
|
with_registration_id=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock the schema function to return None (no schema)
|
||||||
|
with patch("core.models.get_organization_metadata_schema") as mock_get_schema:
|
||||||
|
mock_get_schema.return_value = None
|
||||||
|
|
||||||
|
# Call the command
|
||||||
|
call_command("fill_organization_metadata", stdout=command_output)
|
||||||
|
|
||||||
|
# Check the command output
|
||||||
|
assert "No organization metadata schema defined" in command_output.getvalue()
|
||||||
|
|
||||||
|
organization_1.refresh_from_db()
|
||||||
|
assert organization_1.metadata == {}
|
||||||
|
|
||||||
|
organization_2.refresh_from_db()
|
||||||
|
assert organization_2.metadata == {"existing_key": "existing_value"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"existing_metadata,expected_result",
|
||||||
|
[
|
||||||
|
({}, {"field1": "default_value"}), # Empty metadata gets defaults
|
||||||
|
({"field1": "custom"}, {"field1": "custom"}), # Existing values preserved
|
||||||
|
(
|
||||||
|
{"other_field": "value"},
|
||||||
|
{"other_field": "value", "field1": "default_value"},
|
||||||
|
), # Mixed case
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_metadata_merging_scenarios(existing_metadata, expected_result):
|
||||||
|
"""Test various metadata merging scenarios."""
|
||||||
|
# Create a simple schema with one field
|
||||||
|
simple_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"field1": {"type": "string", "default": "default_value"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create an organization with the specified metadata
|
||||||
|
organization = factories.OrganizationFactory(
|
||||||
|
name="Test organization",
|
||||||
|
metadata=existing_metadata,
|
||||||
|
with_registration_id=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock the schema function to return our simple schema
|
||||||
|
with patch("core.models.get_organization_metadata_schema") as mock_get_schema:
|
||||||
|
mock_get_schema.return_value = simple_schema
|
||||||
|
|
||||||
|
# Call the command
|
||||||
|
call_command("fill_organization_metadata")
|
||||||
|
|
||||||
|
# Refresh from DB and check
|
||||||
|
organization.refresh_from_db()
|
||||||
|
|
||||||
|
# Check that the metadata has been merged correctly
|
||||||
|
for key, value in expected_result.items():
|
||||||
|
assert organization.metadata[key] == value
|
||||||
1
src/backend/core/tests/utils/__init__.py
Normal file
1
src/backend/core/tests/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Tests for the utils module."""
|
||||||
162
src/backend/core/tests/utils/test_json_schema.py
Normal file
162
src/backend/core/tests/utils/test_json_schema.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
"""Tests for the JSON schema `generate_default_from_schema` utility functions."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.utils.json_schema import generate_default_from_schema
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"schema,expected",
|
||||||
|
[
|
||||||
|
# Test empty schema
|
||||||
|
({}, {}),
|
||||||
|
# Test schema with no properties
|
||||||
|
({"type": "object"}, {}),
|
||||||
|
# Test basic property types
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"age": {"type": "integer"},
|
||||||
|
"active": {"type": "boolean"},
|
||||||
|
"data": {"type": "null"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"name": "", "age": None, "active": False, "data": None},
|
||||||
|
),
|
||||||
|
# Test default values
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string", "default": "John Doe"},
|
||||||
|
"age": {"type": "integer", "default": 30},
|
||||||
|
"active": {"type": "boolean", "default": True},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "John Doe",
|
||||||
|
"age": 30,
|
||||||
|
"active": True,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# Test array type
|
||||||
|
(
|
||||||
|
{"properties": {"items": {"type": "array"}, "tags": {"type": "array"}}},
|
||||||
|
{"items": [], "tags": []},
|
||||||
|
),
|
||||||
|
# Test nested object
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"user": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"details": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"age": {"type": "integer"},
|
||||||
|
"active": {"type": "boolean", "default": True},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"user": {"name": "", "details": {"age": None, "active": True}}},
|
||||||
|
),
|
||||||
|
# Test complex schema with multiple types and nesting
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "default": "user-123"},
|
||||||
|
"profile": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"firstName": {"type": "string"},
|
||||||
|
"lastName": {"type": "string"},
|
||||||
|
"age": {"type": "number", "default": 25},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"roles": {"type": "array"},
|
||||||
|
"settings": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"notifications": {"type": "boolean", "default": False},
|
||||||
|
"theme": {"type": "string", "default": "light"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"unknown": {"type": "something-else"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "user-123",
|
||||||
|
"profile": {"firstName": "", "lastName": "", "age": 25},
|
||||||
|
"roles": [],
|
||||||
|
"settings": {"notifications": False, "theme": "light"},
|
||||||
|
"unknown": None,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_generate_default_from_schema(schema, expected):
|
||||||
|
"""Test the generate_default_from_schema function with various schema inputs."""
|
||||||
|
result = generate_default_from_schema(schema)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_invalid_inputs():
|
||||||
|
"""Test the function with invalid inputs to ensure it handles them gracefully."""
|
||||||
|
# pylint: disable=use-implicit-booleaness-not-comparison
|
||||||
|
|
||||||
|
# None input
|
||||||
|
assert generate_default_from_schema(None) == {}
|
||||||
|
|
||||||
|
# Invalid schema type
|
||||||
|
assert generate_default_from_schema([]) == {}
|
||||||
|
assert generate_default_from_schema("not-a-schema") == {}
|
||||||
|
|
||||||
|
# Empty properties
|
||||||
|
assert generate_default_from_schema({"properties": {}}) == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_complex_nested_arrays():
|
||||||
|
"""Test handling of complex schemas with nested arrays and objects."""
|
||||||
|
schema = {
|
||||||
|
"properties": {
|
||||||
|
"users": {
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"features": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"enabledFlags": {"type": "array"},
|
||||||
|
"limits": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"maxUsers": {"type": "integer", "default": 10},
|
||||||
|
"maxStorage": {"type": "integer", "default": 5120},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"users": [],
|
||||||
|
"config": {
|
||||||
|
"features": {
|
||||||
|
"enabledFlags": [],
|
||||||
|
"limits": {"maxUsers": 10, "maxStorage": 5120},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result = generate_default_from_schema(schema)
|
||||||
|
assert result == expected
|
||||||
32
src/backend/core/utils/json_schema.py
Normal file
32
src/backend/core/utils/json_schema.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""Useful functions for working with JSON schemas"""
|
||||||
|
|
||||||
|
|
||||||
|
def generate_default_from_schema(schema: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Generate default values based on a JSON schema
|
||||||
|
"""
|
||||||
|
if not schema or "properties" not in schema:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for prop_name, prop_schema in schema.get("properties", {}).items():
|
||||||
|
prop_type = prop_schema.get("type")
|
||||||
|
|
||||||
|
match prop_type:
|
||||||
|
case "object" if "properties" in prop_schema:
|
||||||
|
result[prop_name] = generate_default_from_schema(prop_schema)
|
||||||
|
case "array":
|
||||||
|
result[prop_name] = []
|
||||||
|
case "string":
|
||||||
|
result[prop_name] = prop_schema.get("default", "")
|
||||||
|
case "number" | "integer":
|
||||||
|
result[prop_name] = prop_schema.get("default", None)
|
||||||
|
case "boolean":
|
||||||
|
result[prop_name] = prop_schema.get("default", False)
|
||||||
|
case "null":
|
||||||
|
result[prop_name] = None
|
||||||
|
case _:
|
||||||
|
result[prop_name] = None
|
||||||
|
|
||||||
|
return result
|
||||||
Reference in New Issue
Block a user