more tests for excluding parent fields, finished docs
This commit is contained in:
@ -50,6 +50,26 @@ You can pass a static value or a Callable (function etc.)
|
||||
|
||||
Used both in sql and pydantic.
|
||||
|
||||
Sample usage:
|
||||
|
||||
```python
|
||||
# note the distinction between passing a value and Callable pointer
|
||||
|
||||
# value
|
||||
name: str = ormar.String(max_length=200, default="Name")
|
||||
|
||||
# note that when you call a function it's not a pointer to Callable
|
||||
# a definition like this will call the function at startup and assign
|
||||
# the result of the function to the default, so it will be constant value for all instances
|
||||
created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now())
|
||||
|
||||
# if you want to pass Callable reference (note that it cannot have arguments)
|
||||
# note lack of the parenthesis -> ormar will call this function for you on each model
|
||||
created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
|
||||
|
||||
# Callable can be a function, builtin, class etc.
|
||||
```
|
||||
|
||||
## server default
|
||||
|
||||
`server_default`: `Any` = `None` -> defaults to None.
|
||||
|
||||
@ -461,3 +461,113 @@ abstract parent model you may lose your data on through table if not careful.
|
||||
|
||||
That means that each time you define a Child model you need to either manually create
|
||||
the table in the database, or run a migration (with alembic).
|
||||
|
||||
## exclude_parent_fields
|
||||
|
||||
Ormar allows you to skip certain fields in inherited model that are coming from a parent model.
|
||||
|
||||
!!!Note
|
||||
Note that the same behaviour can be achieved by splitting the model into more abstract models and mixins - which is a preferred way in normal circumstances.
|
||||
|
||||
To skip certain fields from a child model, list all fields that you want to skip in `model.Meta.exclude_parent_fields` parameter like follows:
|
||||
|
||||
```python
|
||||
metadata = sa.MetaData()
|
||||
db = databases.Database(DATABASE_URL)
|
||||
|
||||
|
||||
class AuditModel(ormar.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
created_by: str = ormar.String(max_length=100)
|
||||
updated_by: str = ormar.String(max_length=100, default="Sam")
|
||||
|
||||
|
||||
class DateFieldsModel(ormar.Model):
|
||||
class Meta(ormar.ModelMeta):
|
||||
abstract = True
|
||||
metadata = metadata
|
||||
database = db
|
||||
|
||||
created_date: datetime.datetime = ormar.DateTime(
|
||||
default=datetime.datetime.now, name="creation_date"
|
||||
)
|
||||
updated_date: datetime.datetime = ormar.DateTime(
|
||||
default=datetime.datetime.now, name="modification_date"
|
||||
)
|
||||
|
||||
|
||||
class Category(DateFieldsModel, AuditModel):
|
||||
class Meta(ormar.ModelMeta):
|
||||
tablename = "categories"
|
||||
# set fields that should be skipped
|
||||
exclude_parent_fields = ["updated_by", "updated_date"]
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=50, unique=True, index=True)
|
||||
code: int = ormar.Integer()
|
||||
|
||||
# Note that now the update fields in Category are gone in all places -> ormar fields, pydantic fields and sqlachemy table columns
|
||||
# so full list of available fileds in Category is: ["created_by", "created_date", "id", "name", "code"]
|
||||
```
|
||||
|
||||
Note how you simply need to provide field names and it will exclude the parent field regardless of from which parent model the field is coming from.
|
||||
|
||||
!!!Note
|
||||
Note that if you want to overwrite a field in child model you do not have to exclude it, simpy overwrite the field declaration in child model with same field name.
|
||||
|
||||
!!!Warning
|
||||
Note that this kind of behavior can confuse mypy and static type checkers, yet accessing the non existing fields will fail at runtime. That's why splitting the base classes is preferred.
|
||||
|
||||
The same effect can be achieved by splitting base classes like:
|
||||
|
||||
```python
|
||||
metadata = sa.MetaData()
|
||||
db = databases.Database(DATABASE_URL)
|
||||
|
||||
|
||||
class AuditCreateModel(ormar.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
created_by: str = ormar.String(max_length=100)
|
||||
|
||||
|
||||
class AuditUpdateModel(ormar.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
updated_by: str = ormar.String(max_length=100, default="Sam")
|
||||
|
||||
class CreateDateFieldsModel(ormar.Model):
|
||||
class Meta(ormar.ModelMeta):
|
||||
abstract = True
|
||||
metadata = metadata
|
||||
database = db
|
||||
|
||||
created_date: datetime.datetime = ormar.DateTime(
|
||||
default=datetime.datetime.now, name="creation_date"
|
||||
)
|
||||
|
||||
class UpdateDateFieldsModel(ormar.Model):
|
||||
class Meta(ormar.ModelMeta):
|
||||
abstract = True
|
||||
metadata = metadata
|
||||
database = db
|
||||
|
||||
updated_date: datetime.datetime = ormar.DateTime(
|
||||
default=datetime.datetime.now, name="modification_date"
|
||||
)
|
||||
|
||||
|
||||
class Category(CreateDateFieldsModel, AuditCreateModel):
|
||||
class Meta(ormar.ModelMeta):
|
||||
tablename = "categories"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=50, unique=True, index=True)
|
||||
code: int = ormar.Integer()
|
||||
```
|
||||
|
||||
That way you can inherit from both create and update classes if needed, and only one of them otherwise.
|
||||
@ -2,11 +2,16 @@
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Add `get_pydantic` flag that allows you to auto generate equivalent pydantic models tree from ormar.Model. This newly generated model tree can be used in requests and responses to exclude fields you do not want to include in the data.
|
||||
* Add [`get_pydantic`](https://collerek.github.io/ormar/models/methods/#get_pydantic) flag that allows you to auto generate equivalent pydantic models tree from ormar.Model. This newly generated model tree can be used in requests and responses to exclude fields you do not want to include in the data.
|
||||
* Add [`exclude_parent_fields`](https://collerek.github.io/ormar/models/inheritance/#exclude_parent_fields) parameter to model Meta that allows you to exclude fields from parent models during inheritance. Note that best practice is to combine models and mixins but if you have many similar models and just one that differs it might be useful tool to achieve that.
|
||||
|
||||
## 🐛 Fixes
|
||||
|
||||
* Fix is null filter with pagination and relations (by @erichaydel) [#214](https://github.com/collerek/ormar/issues/214)
|
||||
|
||||
## 💬 Other
|
||||
|
||||
* Expand fastapi part of the documentation to show samples of using ormar in requests and responses in fastapi.
|
||||
* Expand [fastapi](https://collerek.github.io/ormar/fastapi) part of the documentation to show samples of using ormar in requests and responses in fastapi.
|
||||
|
||||
# 0.10.9
|
||||
|
||||
|
||||
@ -119,7 +119,7 @@ def get_potential_fields(attrs: Dict) -> Dict:
|
||||
}
|
||||
|
||||
|
||||
def remove_excluded_parent_fields(model: Type["Model"]):
|
||||
def remove_excluded_parent_fields(model: Type["Model"]) -> None:
|
||||
"""
|
||||
Removes pydantic fields that should be excluded from parent models
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import uuid
|
||||
from typing import Callable, Collection, Dict, Optional, Set, TYPE_CHECKING, cast
|
||||
from typing import Callable, Collection, Dict, List, Optional, Set, TYPE_CHECKING, cast
|
||||
|
||||
import ormar
|
||||
from ormar.exceptions import ModelPersistenceError
|
||||
@ -275,9 +275,7 @@ class SavePrepareMixin(RelationMixin, AliasMixin):
|
||||
:rtype: int
|
||||
"""
|
||||
for field in fields_list:
|
||||
values = getattr(self, field.name) or []
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
values = self._get_field_values(name=field.name)
|
||||
for value in values:
|
||||
if follow:
|
||||
update_count = await value.save_related(
|
||||
@ -299,3 +297,17 @@ class SavePrepareMixin(RelationMixin, AliasMixin):
|
||||
update_count=update_count,
|
||||
)
|
||||
return update_count
|
||||
|
||||
def _get_field_values(self, name: str) -> List:
|
||||
"""
|
||||
Extract field values and ensures it is a list.
|
||||
|
||||
:param name: name of the field
|
||||
:type name: str
|
||||
:return: list of values
|
||||
:rtype: List
|
||||
"""
|
||||
values = getattr(self, name) or []
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
return values
|
||||
|
||||
@ -107,7 +107,7 @@ class Model(ModelRow):
|
||||
await self.signals.post_save.send(sender=self.__class__, instance=self)
|
||||
return self
|
||||
|
||||
async def save_related( # noqa: CCR001
|
||||
async def save_related( # noqa: CCR001, CFQ002
|
||||
self,
|
||||
follow: bool = False,
|
||||
save_all: bool = False,
|
||||
|
||||
@ -552,7 +552,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
|
||||
return items.get("__all__")
|
||||
return items
|
||||
|
||||
def _extract_nested_models( # noqa: CCR001
|
||||
def _extract_nested_models( # noqa: CCR001, CFQ002
|
||||
self,
|
||||
relation_map: Dict,
|
||||
dict_instance: Dict,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
import databases
|
||||
import pytest
|
||||
@ -7,8 +6,6 @@ import sqlalchemy as sa
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
import ormar
|
||||
from ormar import ModelDefinitionError, property_field
|
||||
from ormar.exceptions import ModelError
|
||||
from tests.settings import DATABASE_URL
|
||||
|
||||
metadata = sa.MetaData()
|
||||
@ -16,6 +13,24 @@ db = databases.Database(DATABASE_URL)
|
||||
engine = create_engine(DATABASE_URL)
|
||||
|
||||
|
||||
class User(ormar.Model):
|
||||
class Meta(ormar.ModelMeta):
|
||||
tablename = "users"
|
||||
metadata = metadata
|
||||
database = db
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=50, unique=True, index=True)
|
||||
|
||||
|
||||
class RelationalAuditModel(ormar.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
created_by: User = ormar.ForeignKey(User, nullable=False)
|
||||
updated_by: User = ormar.ForeignKey(User, nullable=False)
|
||||
|
||||
|
||||
class AuditModel(ormar.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
@ -48,6 +63,26 @@ class Category(DateFieldsModel, AuditModel):
|
||||
code: int = ormar.Integer()
|
||||
|
||||
|
||||
class Item(DateFieldsModel, AuditModel):
|
||||
class Meta(ormar.ModelMeta):
|
||||
tablename = "items"
|
||||
exclude_parent_fields = ["updated_by", "updated_date"]
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=50, unique=True, index=True)
|
||||
code: int = ormar.Integer()
|
||||
updated_by: str = ormar.String(max_length=100, default="Bob")
|
||||
|
||||
|
||||
class Gun(RelationalAuditModel, DateFieldsModel):
|
||||
class Meta(ormar.ModelMeta):
|
||||
tablename = "guns"
|
||||
exclude_parent_fields = ["updated_by"]
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=50)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="module")
|
||||
def create_test_database():
|
||||
metadata.create_all(engine)
|
||||
@ -65,3 +100,48 @@ def test_model_definition():
|
||||
assert "updated_date" not in model_fields
|
||||
assert "updated_date" not in sqlalchemy_columns
|
||||
assert "updated_date" not in pydantic_columns
|
||||
|
||||
assert "updated_by" not in Gun.Meta.model_fields
|
||||
assert "updated_by" not in Gun.Meta.table.c
|
||||
assert "updated_by" not in Gun.__fields__
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_works_as_expected():
|
||||
async with db:
|
||||
async with db.transaction(force_rollback=True):
|
||||
test = await Category(name="Cat", code=2, created_by="Joe").save()
|
||||
assert test.created_date is not None
|
||||
|
||||
test2 = await Category.objects.get(pk=test.pk)
|
||||
assert test2.name == "Cat"
|
||||
assert test2.created_by == "Joe"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exclude_with_redefinition():
|
||||
async with db:
|
||||
async with db.transaction(force_rollback=True):
|
||||
test = await Item(name="Item", code=3, created_by="Anna").save()
|
||||
assert test.created_date is not None
|
||||
assert test.updated_by == "Bob"
|
||||
|
||||
test2 = await Item.objects.get(pk=test.pk)
|
||||
assert test2.name == "Item"
|
||||
assert test2.code == 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exclude_with_relation():
|
||||
async with db:
|
||||
async with db.transaction(force_rollback=True):
|
||||
user = await User(name="Michaił Kałasznikow").save()
|
||||
test = await Gun(name="AK47", created_by=user).save()
|
||||
assert test.created_date is not None
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
assert test.updated_by
|
||||
|
||||
test2 = await Gun.objects.select_related("created_by").get(pk=test.pk)
|
||||
assert test2.name == "AK47"
|
||||
assert test2.created_by.name == "Michaił Kałasznikow"
|
||||
|
||||
@ -68,8 +68,10 @@ async def test_is_null():
|
||||
assert tolkien.books[0].year is None
|
||||
assert tolkien.books[0].title == "The Hobbit"
|
||||
|
||||
tolkien = await Author.objects.select_related("books").paginate(1, 10).get(
|
||||
books__year__isnull=True
|
||||
tolkien = (
|
||||
await Author.objects.select_related("books")
|
||||
.paginate(1, 10)
|
||||
.get(books__year__isnull=True)
|
||||
)
|
||||
assert len(tolkien.books) == 1
|
||||
assert tolkien.books[0].year is None
|
||||
|
||||
Reference in New Issue
Block a user