示例#1
0
def test_set_property_from_data_int():
    component = FakeComponent(component_name="test", component_id="12345678")
    assert 99 == component.integer

    set_property_from_data(component, "integer", 100)

    assert 100 == component.integer
示例#2
0
def test_set_property_from_data_datetime():
    component = FakeComponent(component_name="test", component_id="12345678")
    assert datetime(2020, 1, 1) == component.datetime

    set_property_from_data(component, "datetime", datetime(2020, 1, 2))

    assert datetime(2020, 1, 2) == component.datetime
示例#3
0
def test_set_property_from_data_empty_queryset():
    component = FakeComponent(component_name="test", component_id="12345678")
    assert len(component.queryset) == 0

    set_property_from_data(component, "queryset", [{"name": "test-qs"}])

    component_queryset_field_asserts(component, "queryset")
示例#4
0
def test_set_property_from_data_str():
    component = FakeComponent(component_name="test", component_id="12345678")
    assert "property_view" == component.string

    set_property_from_data(component, "string", "property_view_updated")

    assert "property_view_updated" == component.string
示例#5
0
def test_set_property_from_data_model():
    component = ModelPropertyView(component_name="test",
                                  component_id="12345678")
    assert "fake_model" == component.model.name

    set_property_from_data(component, "model", {"name": "fake_model_updated"})

    assert "fake_model_updated" == component.model.name
示例#6
0
def test_set_property_from_data_model():
    component = FakeComponent(component_name="test", component_id="12345678")
    assert component.model.name == "test-initial"

    set_property_from_data(component, "model", {"name": "test-test"})

    assert isinstance(component.model, Model)
    assert isinstance(component.model, Flavor)
    assert component.model.name == "test-test"
def test_set_property_from_data_dict():
    component = DictPropertyView(component_name="test",
                                 component_id="12345678")
    assert "dictionary" == component.dictionary.get("name")

    set_property_from_data(component, "dictionary",
                           {"name": "dictionary_updated"})

    assert "dictionary_updated" == component.dictionary.get("name")
示例#8
0
def test_set_property_from_data_list():
    """
    Prevent attempting to instantiate `List[]` type-hint doesn't throw TypeError
    """
    component = FakeComponent(component_name="test", component_id="12345678")
    assert component.array == []

    set_property_from_data(component, "array", ["string"])

    assert ["string"] == component.array
示例#9
0
def test_set_property_from_data_queryset():
    component = FakeDbComponent(component_name="test", component_id="12345678")
    assert len(component.queryset_with_data) == 1

    set_property_from_data(component, "queryset_with_data", [{
        "pk": 1,
        "name": "test-qs"
    }])

    component_queryset_field_asserts(component, "queryset_with_data")
示例#10
0
def test_set_property_from_data_queryset_parent():
    component = FakeQuerySetComponent(component_name="test",
                                      component_id="12345678")
    assert component.queryset_with_typehint is None

    set_property_from_data(component, "queryset_with_typehint",
                           [{
                               "name": "test-qs"
                           }])

    component_queryset_field_asserts(component, "queryset_with_typehint")
示例#11
0
def test_queryset_values():
    test_component = FakeComponent(component_name="test", component_id="asdf")
    assert test_component.flavors.count() == 0

    flavor = Flavor(name="values-first-flavor")
    flavor.save()

    flavors = Flavor.objects.filter(name="values-first-flavor").values("uuid")
    str_data = dumps({"flavors": flavors})
    data = loads(str_data)
    flavors_data = data["flavors"]

    set_property_from_data(test_component, "flavors", flavors_data)

    assert test_component.flavors.count() == 1
    assert test_component.flavors[0].uuid == str(flavor.uuid)
    assert test_component.flavors[0].id is None
示例#12
0
def test_set_property_from_data_all_querysets():
    component = FakeAllQuerySetComponent(component_name="test",
                                         component_id="12345678")

    set_property_from_data(component, "queryset_with_empty_list",
                           [{
                               "name": "test-qs"
                           }])
    set_property_from_data(component, "queryset_with_none", [{
        "name": "test-qs"
    }])
    set_property_from_data(component, "queryset_with_empty_queryset",
                           [{
                               "name": "test-qs"
                           }])
    set_property_from_data(component, "queryset_with_no_typehint",
                           [{
                               "name": "test-qs"
                           }])

    component_queryset_field_asserts(component, "queryset_with_empty_list")
    component_queryset_field_asserts(component, "queryset_with_none")
    component_queryset_field_asserts(component, "queryset_with_empty_queryset")
    component_queryset_field_asserts(component, "queryset_with_no_typehint")
示例#13
0
def test_set_property_from_data_many_to_many():
    component = FakeComponent(component_name="test", component_id="12345678")
    component.model.pk = 1

    # No `TypeError: Direct assignment to the reverse side of a many-to-many set is prohibited.` error gets raised
    set_property_from_data(component.model, "taste_set", [])
def handle(component_request: ComponentRequest, component: UnicornView,
           payload: Dict):
    call_method_name = payload.get("name", "")
    assert call_method_name, "Missing 'name' key for callMethod"

    (method_name, args, kwargs) = parse_call_method_name(call_method_name)
    return_data = Return(method_name, args, kwargs)
    setter_method = {}

    is_refresh_called = False
    is_reset_called = False
    validate_all_fields = False

    if "=" in call_method_name:
        try:
            setter_method = parse_kwarg(call_method_name,
                                        raise_if_unparseable=True)
        except InvalidKwarg:
            pass

    if setter_method:
        property_name = list(setter_method.keys())[0]
        property_value = setter_method[property_name]

        set_property_value(component, property_name, property_value)
        return_data = Return(property_name, [property_value])
    else:
        if method_name == "$refresh":
            # Handle the refresh special action
            component = UnicornView.create(
                component_id=component_request.id,
                component_name=component_request.name,
                request=component_request.request,
            )

            # Set component properties based on request data
            for (
                    property_name,
                    property_value,
            ) in component_request.data.items():
                set_property_from_data(component, property_name,
                                       property_value)
            component.hydrate()

            is_refresh_called = True
        elif method_name == "$reset":
            # Handle the reset special action
            component = UnicornView.create(
                component_id=component_request.id,
                component_name=component_request.name,
                request=component_request.request,
                use_cache=False,
            )

            #  Explicitly remove all errors and prevent validation from firing before render()
            component.errors = {}
            is_reset_called = True
        elif method_name == "$toggle":
            for property_name in args:
                property_value = _get_property_value(component, property_name)
                property_value = not property_value

                set_property_value(component, property_name, property_value)
        elif method_name == "$validate":
            # Handle the validate special action
            validate_all_fields = True
        else:
            component.calling(method_name, args)
            return_data.value = _call_method_name(component, method_name, args,
                                                  kwargs)
            component.called(method_name, args)

    return (
        component,
        is_refresh_called,
        is_reset_called,
        validate_all_fields,
        return_data,
    )
示例#15
0
def _process_component_request(
    request: HttpRequest, component_request: ComponentRequest
) -> Dict:
    """
    Process a `ComponentRequest`:
        1. construct a Component view
        2. set all of the properties on the view from the data
        3. execute the type
            - update the properties based on the payload for "syncInput"
            - call the method specified for "callMethod"
        4. validate any fields specified in a Django form
        5. construct a `dict` that will get returned in a `JsonResponse` later on

    Args:
        param request: HttpRequest for the function-based view.
        param: component_request: Component request to process.

    Returns:
        `dict` with the following structure:
        {
            "id": component_id,
            "dom": html,  // re-rendered version of the component after actions in the payload are completed
            "data": {},  // updated data after actions in the payload are completed
            "errors": {},  // form validation errors
            "return": {}, // optional return value from an executed action
            "parent": {},  // optional representation of the parent component
        }
    """
    component = UnicornView.create(
        component_id=component_request.id,
        component_name=component_request.name,
        request=request,
    )

    # Get a deepcopy of the data passed in to determine what fields are updated later
    original_data = copy.deepcopy(component_request.data)

    # Set component properties based on request data
    for (property_name, property_value) in component_request.data.items():
        set_property_from_data(component, property_name, property_value)
    component.hydrate()

    validate_all_fields = False
    is_reset_called = False
    is_refresh_called = False
    return_data = None
    partials = []

    for action in component_request.action_queue:
        if action.partial:
            partials.append(action.partial)
        else:
            partials = action.partials

        if action.action_type == "syncInput":
            sync_input.handle(component_request, component, action.payload)
        elif action.action_type == "callMethod":
            (
                component,
                _is_refresh_called,
                _is_reset_called,
                _validate_all_fields,
                return_data,
            ) = call_method.handle(component_request, component, action.payload)

            is_refresh_called = is_refresh_called | _is_refresh_called
            is_reset_called = is_reset_called | _is_reset_called
            validate_all_fields = validate_all_fields | _validate_all_fields
        else:
            raise UnicornViewError(f"Unknown action_type '{action.action_type}'")

    component.complete()

    # Re-load frontend context variables to deal with non-serializable properties
    component_request.data = orjson.loads(component.get_frontend_context_variables())

    # Get set of attributes that should be marked as `safe`
    safe_fields = []
    if hasattr(component, "Meta") and hasattr(component.Meta, "safe"):
        if isinstance(component.Meta.safe, Sequence):
            for field_name in component.Meta.safe:
                if field_name in component._attributes().keys():
                    safe_fields.append(field_name)

    # Mark safe attributes as such before rendering
    for field_name in safe_fields:
        value = getattr(component, field_name)
        if isinstance(value, str):
            setattr(component, field_name, mark_safe(value))

    # Send back all available data for reset or refresh actions
    updated_data = component_request.data

    if not is_reset_called:
        if not is_refresh_called:
            updated_data = {}

            for key, value in original_data.items():
                if value != component_request.data.get(key):
                    updated_data[key] = component_request.data.get(key)

        if validate_all_fields:
            component.validate()
        else:
            component.validate(model_names=list(updated_data.keys()))

    # Pass the current request so that it can be used inside the component template
    rendered_component = component.render(request=request)
    component.rendered(rendered_component)

    cache = caches[get_cache_alias()]

    try:
        cache.set(component.component_cache_key, get_cacheable_component(component))
    except UnicornCacheError as e:
        logger.warning(e)

    partial_doms = []

    if partials and all(partials):
        soup = BeautifulSoup(rendered_component, features="html.parser")

        for partial in partials:
            partial_found = False
            only_id = False
            only_key = False

            target = partial.get("target")

            if not target:
                target = partial.get("key")

                if target:
                    only_key = True

            if not target:
                target = partial.get("id")

                if target:
                    only_id = True

            assert target, "Partial target is required"

            if not only_id:
                for element in soup.find_all():
                    if (
                        "unicorn:key" in element.attrs
                        and element.attrs["unicorn:key"] == target
                    ):
                        partial_doms.append({"key": target, "dom": str(element)})
                        partial_found = True
                        break

            if not partial_found and not only_key:
                for element in soup.find_all():
                    if "id" in element.attrs and element.attrs["id"] == target:
                        partial_doms.append({"id": target, "dom": str(element)})
                        partial_found = True
                        break

    res = {
        "id": component_request.id,
        "data": updated_data,
        "errors": component.errors,
        "calls": component.calls,
        "checksum": generate_checksum(orjson.dumps(component_request.data)),
    }

    if partial_doms:
        res.update({"partials": partial_doms})
    else:
        hash = generate_checksum(rendered_component)

        if (
            component_request.hash == hash
            and (not return_data or not return_data.value)
            and not component.calls
        ):
            raise RenderNotModified()

        # Make sure that partials with comments or blank lines before the root element only return the root element
        soup = BeautifulSoup(rendered_component, features="html.parser")
        rendered_component = str(get_root_element(soup))

        res.update(
            {
                "dom": rendered_component,
                "hash": hash,
            }
        )

    if return_data:
        res.update(
            {
                "return": return_data.get_data(),
            }
        )

        if return_data.redirect:
            res.update(
                {
                    "redirect": return_data.redirect,
                }
            )

        if return_data.poll:
            res.update(
                {
                    "poll": return_data.poll,
                }
            )

    parent_component = component.parent

    if parent_component:
        # TODO: Should parent_component.hydrate() be called?
        parent_frontend_context_variables = loads(
            parent_component.get_frontend_context_variables()
        )
        parent_checksum = generate_checksum(dumps(parent_frontend_context_variables))

        parent = {
            "id": parent_component.component_id,
            "checksum": parent_checksum,
        }

        if not partial_doms:
            parent_dom = parent_component.render()
            component.parent_rendered(parent_dom)

            try:
                cache.set(
                    parent_component.component_cache_key,
                    get_cacheable_component(parent_component),
                )
            except UnicornCacheError as e:
                logger.warning(e)

            parent.update(
                {
                    "dom": parent_dom,
                    "data": parent_frontend_context_variables,
                    "errors": parent_component.errors,
                }
            )

        res.update({"parent": parent})

    return res