def test_exclude_field_attributes_invalid_type(): with pytest.raises(AssertionError) as e: serializer.dumps( { "book": { "title": "The Grapes of Wrath", "author": "John Steinbeck" } }, exclude_field_attributes=("book.blob"), )
def test_exclude_field_attributes_invalid_attribute(): with pytest.raises(InvalidFieldAttributeError) as e: serializer.dumps( { "book": { "title": "The Grapes of Wrath", "author": "John Steinbeck" } }, exclude_field_attributes=("book.blob", ), ) assert ( e.exconly() == "django_unicorn.serializer.InvalidFieldAttributeError: Cannot resolve 'blob'. Choices on 'book' are: title, author" )
def test_model_many_to_many(django_assert_num_queries): flavor_one = Flavor(name="name1", label="label1") flavor_one.save() taste1 = Taste(name="Bitter1") taste1.save() taste2 = Taste(name="Bitter2") taste2.save() taste3 = Taste(name="Bitter3") taste3.save() flavor_one.taste_set.add(taste1) flavor_one.taste_set.add(taste2) flavor_one.taste_set.add(taste3) with django_assert_num_queries(2): actual = serializer.dumps(flavor_one) expected = { "name": "name1", "label": "label1", "parent": None, "float_value": None, "decimal_value": None, "uuid": str(flavor_one.uuid), "datetime": None, "date": None, "time": None, "duration": None, "pk": 1, "taste_set": [taste1.pk, taste2.pk, taste3.pk], "origins": [], } assert expected == json.loads(actual)
def test_simple_model(): simple_test_model = SimpleTestModel(id=1, name="abc") expected = '{"simple_test_model":{"name":"abc","pk":1}}' actual = serializer.dumps({"simple_test_model": simple_test_model}) assert expected == actual
def get_data(self): try: serialized_value = loads(dumps(self.value)) serialized_args = loads(dumps(self.args)) serialized_kwargs = loads(dumps(self.kwargs)) return { "method": self.method_name, "args": serialized_args, "kwargs": serialized_kwargs, "value": serialized_value, } except Exception as e: logger.exception(e) return {}
def test_list(): expected = '{"name":["abc","def"]}' actual = serializer.dumps({"name": [ "abc", "def", ]}) assert expected == actual
def test_nested_list_float_less_complicated(): expected = '{"another":[{"great":"1.0","ok":["1.6","0.0",4]}]}' actual = serializer.dumps({ "another": [{ "great": 1.0, "ok": [1.6, 0.0, 4] }], }) assert expected == actual
def test_model_foreign_key(): test_model_one = ComplicatedTestModel(id=1, name="abc") test_model_two = ComplicatedTestModel(id=2, name="def", parent=test_model_one) expected = '{"test_model_two":{"name":"def","parent":1,"pk":2}}' actual = serializer.dumps({"test_model_two": test_model_two}) assert expected == actual
def test_pydantic(): from pydantic import BaseModel class Book(BaseModel): title = "The Grapes of Wrath" author = "John Steinbeck" expected = '{"title":"The Grapes of Wrath","author":"John Steinbeck"}' actual = serializer.dumps(Book()) assert expected == actual
def test_exclude_field_attributes_none(): expected = '{"book":{"title":"The Grapes of Wrath","author":"John Steinbeck"}}' actual = serializer.dumps( {"book": { "title": "The Grapes of Wrath", "author": "John Steinbeck" }}, exclude_field_attributes=None, ) assert expected == actual
def test_exclude_field_attributes_no_fix_floats(): expected = '{"book":{"title":"The Grapes of Wrath"}}' actual = serializer.dumps( {"book": { "title": "The Grapes of Wrath", "author": "John Steinbeck" }}, fix_floats=False, exclude_field_attributes=("book.author", ), ) assert expected == actual
def validate_checksum(self): """ Validates that the checksum in the request matches the data. Returns: Raises `AssertionError` if the checksums don't match. """ checksum = self.body.get("checksum") assert checksum, "Missing checksum" generated_checksum = generate_checksum( dumps(self.data, fix_floats=False)) assert checksum == generated_checksum, "Checksum does not match"
def test_dumps_queryset(db): flavor_one = Flavor(name="name1", label="label1") flavor_one.save() flavor_two = Flavor(name="name2", label="label2", parent=flavor_one) flavor_two.save() flavors = Flavor.objects.all() expected = '{"flavors":[{"name":"name1","label":"label1","parent":null,"float_value":null,"decimal_value":null,"pk":1},{"name":"name2","label":"label2","parent":1,"float_value":null,"decimal_value":null,"pk":2}]}' actual = serializer.dumps({"flavors": flavors}) assert expected == actual
def test_model(): flavor = Flavor(name="first-flavor") flavor.save() str_data = dumps({"flavor": flavor}) data = loads(str_data) flavor_data = data["flavor"] actual = _construct_model(Flavor, flavor_data) assert actual.pk == flavor.id assert actual.name == flavor.name assert actual.parent is None
def test_nested_list_float_complicated(): expected = '{"name":{"blob":[1,2,"0.0"]},"more":["1.9",2,5],"another":[{"great":"1.0","ok":["1.6","0.0",4]}]}' actual = serializer.dumps({ "name": { "blob": [1, 2, 0.0] }, "more": [1.9, 2, 5], "another": [{ "great": 1.0, "ok": [1.6, 0.0, 4] }], }) assert expected == actual
def test_exclude_field_attributes_nested(): expected = '{"classic":{"book":{"title":"The Grapes of Wrath"}}}' actual = serializer.dumps( { "classic": { "book": { "title": "The Grapes of Wrath", "author": "John Steinbeck" } } }, exclude_field_attributes=("classic.book.author", ), ) assert expected == actual
def test_model_foreign_key(): parent = Flavor(name="parent-flavor") parent.save() flavor = Flavor(name="first-flavor", parent=parent) flavor.save() str_data = dumps({"flavor": flavor}) data = loads(str_data) flavor_data = data["flavor"] actual = _construct_model(Flavor, flavor_data) assert actual.pk == flavor.id assert actual.name == flavor.name assert actual.parent.pk == parent.id assert actual.parent.name == parent.name
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
def test_dumps_queryset(db): flavor_one = Flavor(name="name1", label="label1") flavor_one.save() flavor_two = Flavor(name="name2", label="label2", parent=flavor_one) flavor_two.save() flavors = Flavor.objects.all() expected_data = { "flavors": [ { "name": "name1", "label": "label1", "parent": None, "float_value": None, "decimal_value": None, "uuid": str(flavor_one.uuid), "date": None, "datetime": None, "time": None, "duration": None, "pk": 1, "taste_set": [], "origins": [], }, { "name": "name2", "label": "label2", "parent": 1, "float_value": None, "decimal_value": None, "uuid": str(flavor_two.uuid), "date": None, "datetime": None, "time": None, "duration": None, "pk": 2, "taste_set": [], "origins": [], }, ] } actual = serializer.dumps({"flavors": flavors}) assert expected_data == json.loads(actual)
def test_model_many_to_many_with_excludes(django_assert_num_queries): flavor_one = Flavor(name="name1", label="label1") flavor_one.save() taste1 = Taste(name="Bitter1") taste1.save() taste2 = Taste(name="Bitter2") taste2.save() taste3 = Taste(name="Bitter3") taste3.save() flavor_one.taste_set.add(taste1) flavor_one.taste_set.add(taste2) flavor_one.taste_set.add(taste3) flavor_one = Flavor.objects.prefetch_related( "taste_set", "origins").get(pk=flavor_one.pk) # This shouldn't make any database calls because of the prefetch_related with django_assert_num_queries(0): actual = serializer.dumps( {"flavor": flavor_one}, exclude_field_attributes=( "flavor.taste_set", "flavor.origins", ), ) expected = { "flavor": { "name": "name1", "label": "label1", "parent": None, "float_value": None, "decimal_value": None, "uuid": str(flavor_one.uuid), "datetime": None, "date": None, "time": None, "duration": None, "pk": 1, } } assert expected == json.loads(actual)
def test_model_with_duration_as_string(db): duration = "-1 day, 19:00:00" flavor = Flavor(name="name1", duration=duration) expected = { "flavor": { "name": "name1", "label": "", "parent": None, "float_value": None, "decimal_value": None, "uuid": str(flavor.uuid), "date": None, "datetime": None, "time": None, "duration": "-1 19:00:00", "pk": None, } } actual = serializer.dumps({"flavor": flavor}) assert dicts_equal(expected, json.loads(actual))
def test_model_with_datetime_as_string(db): datetime = now().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] flavor = Flavor(name="name1", datetime=datetime) expected = { "flavor": { "name": "name1", "label": "", "parent": None, "float_value": None, "decimal_value": None, "uuid": str(flavor.uuid), "date": None, "datetime": datetime, "time": None, "duration": None, "pk": None, } } actual = serializer.dumps({"flavor": flavor}) assert dicts_equal(expected, json.loads(actual))
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
def test_string(): expected = '{"name":"abc"}' actual = serializer.dumps({"name": "abc"}) assert expected == actual
def test_int(): expected = '{"name":123}' actual = serializer.dumps({"name": 123}) assert expected == actual
def test_nested_list_float(): expected = '{"name":{"blob":[1,2,"0.0"]}}' actual = serializer.dumps({"name": {"blob": [1, 2, 0.0]}}) assert expected == actual
def test_dict_float(): expected = '{"name":{"another":"0.0"}}' actual = serializer.dumps({"name": {"another": 0.0}}) assert expected == actual
def test_float(): expected = '{"name":"0.0"}' actual = serializer.dumps({"name": 0.0}) assert expected == actual
def test_decimal(): expected = '{"name":"123.1"}' actual = serializer.dumps({"name": Decimal("123.1")}) assert expected == actual