fill part of queryset docstrings
This commit is contained in:
@ -149,6 +149,7 @@ assert len(tracks) == 1
|
|||||||
* `create(**kwargs): -> Model`
|
* `create(**kwargs): -> Model`
|
||||||
* `get(**kwargs): -> Model`
|
* `get(**kwargs): -> Model`
|
||||||
* `get_or_create(**kwargs) -> Model`
|
* `get_or_create(**kwargs) -> Model`
|
||||||
|
* `first(): -> Model`
|
||||||
* `update(each: bool = False, **kwargs) -> int`
|
* `update(each: bool = False, **kwargs) -> int`
|
||||||
* `update_or_create(**kwargs) -> Model`
|
* `update_or_create(**kwargs) -> Model`
|
||||||
* `bulk_create(objects: List[Model]) -> None`
|
* `bulk_create(objects: List[Model]) -> None`
|
||||||
|
|||||||
@ -149,6 +149,7 @@ assert len(tracks) == 1
|
|||||||
* `create(**kwargs): -> Model`
|
* `create(**kwargs): -> Model`
|
||||||
* `get(**kwargs): -> Model`
|
* `get(**kwargs): -> Model`
|
||||||
* `get_or_create(**kwargs) -> Model`
|
* `get_or_create(**kwargs) -> Model`
|
||||||
|
* `first(): -> Model`
|
||||||
* `update(each: bool = False, **kwargs) -> int`
|
* `update(each: bool = False, **kwargs) -> int`
|
||||||
* `update_or_create(**kwargs) -> Model`
|
* `update_or_create(**kwargs) -> Model`
|
||||||
* `bulk_create(objects: List[Model]) -> None`
|
* `bulk_create(objects: List[Model]) -> None`
|
||||||
|
|||||||
@ -2,14 +2,55 @@ from typing import Dict
|
|||||||
|
|
||||||
import ormar
|
import ormar
|
||||||
from ormar.exceptions import ModelPersistenceError
|
from ormar.exceptions import ModelPersistenceError
|
||||||
|
from ormar.models.mixins import AliasMixin
|
||||||
from ormar.models.mixins.relation_mixin import RelationMixin
|
from ormar.models.mixins.relation_mixin import RelationMixin
|
||||||
|
|
||||||
|
|
||||||
class SavePrepareMixin(RelationMixin):
|
class SavePrepareMixin(RelationMixin, AliasMixin):
|
||||||
"""
|
"""
|
||||||
Used to prepare models to be saved in database
|
Used to prepare models to be saved in database
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _prepare_model_to_save(cls, new_kwargs: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Combines all preparation methods before saving.
|
||||||
|
Removes primary key for if it's nullable or autoincrement pk field,
|
||||||
|
and it's set to None.
|
||||||
|
Substitute related models with their primary key values as fk column.
|
||||||
|
Populates the default values for field with default set and no value.
|
||||||
|
Translate columns into aliases (db names).
|
||||||
|
|
||||||
|
:param new_kwargs: dictionary of model that is about to be saved
|
||||||
|
:type new_kwargs: Dict[str, str]
|
||||||
|
:return: dictionary of model that is about to be saved
|
||||||
|
:rtype: Dict[str, str]
|
||||||
|
"""
|
||||||
|
new_kwargs = cls._remove_pk_from_kwargs(new_kwargs)
|
||||||
|
new_kwargs = cls.substitute_models_with_pks(new_kwargs)
|
||||||
|
new_kwargs = cls.populate_default_values(new_kwargs)
|
||||||
|
new_kwargs = cls.translate_columns_to_aliases(new_kwargs)
|
||||||
|
return new_kwargs
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _remove_pk_from_kwargs(cls, new_kwargs: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Removes primary key for if it's nullable or autoincrement pk field,
|
||||||
|
and it's set to None.
|
||||||
|
|
||||||
|
:param new_kwargs: dictionary of model that is about to be saved
|
||||||
|
:type new_kwargs: Dict[str, str]
|
||||||
|
:return: dictionary of model that is about to be saved
|
||||||
|
:rtype: Dict[str, str]
|
||||||
|
"""
|
||||||
|
pkname = cls.Meta.pkname
|
||||||
|
pk = cls.Meta.model_fields[pkname]
|
||||||
|
if new_kwargs.get(pkname, ormar.Undefined) is None and (
|
||||||
|
pk.nullable or pk.autoincrement
|
||||||
|
):
|
||||||
|
del new_kwargs[pkname]
|
||||||
|
return new_kwargs
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def substitute_models_with_pks(cls, model_dict: Dict) -> Dict: # noqa CCR001
|
def substitute_models_with_pks(cls, model_dict: Dict) -> Dict: # noqa CCR001
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from ormar.models.mixins import (
|
from ormar.models.mixins import (
|
||||||
AliasMixin,
|
|
||||||
ExcludableMixin,
|
ExcludableMixin,
|
||||||
MergeModelMixin,
|
MergeModelMixin,
|
||||||
PrefetchQueryMixin,
|
PrefetchQueryMixin,
|
||||||
@ -8,7 +7,7 @@ from ormar.models.mixins import (
|
|||||||
|
|
||||||
|
|
||||||
class ModelTableProxy(
|
class ModelTableProxy(
|
||||||
PrefetchQueryMixin, MergeModelMixin, AliasMixin, SavePrepareMixin, ExcludableMixin
|
PrefetchQueryMixin, MergeModelMixin, SavePrepareMixin, ExcludableMixin
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Used to combine all mixins with different set of functionalities.
|
Used to combine all mixins with different set of functionalities.
|
||||||
|
|||||||
@ -4,11 +4,23 @@ import sqlalchemy
|
|||||||
|
|
||||||
|
|
||||||
class FilterQuery:
|
class FilterQuery:
|
||||||
|
"""
|
||||||
|
Modifies the select query with given list of where/filter clauses.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, filter_clauses: List, exclude: bool = False) -> None:
|
def __init__(self, filter_clauses: List, exclude: bool = False) -> None:
|
||||||
self.exclude = exclude
|
self.exclude = exclude
|
||||||
self.filter_clauses = filter_clauses
|
self.filter_clauses = filter_clauses
|
||||||
|
|
||||||
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
|
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
|
||||||
|
"""
|
||||||
|
Applies all filter clauses if set.
|
||||||
|
|
||||||
|
:param expr: query to modify
|
||||||
|
:type expr: sqlalchemy.sql.selectable.Select
|
||||||
|
:return: modified query
|
||||||
|
:rtype: sqlalchemy.sql.selectable.Select
|
||||||
|
"""
|
||||||
if self.filter_clauses:
|
if self.filter_clauses:
|
||||||
if len(self.filter_clauses) == 1:
|
if len(self.filter_clauses) == 1:
|
||||||
clause = self.filter_clauses[0]
|
clause = self.filter_clauses[0]
|
||||||
|
|||||||
@ -4,10 +4,22 @@ import sqlalchemy
|
|||||||
|
|
||||||
|
|
||||||
class LimitQuery:
|
class LimitQuery:
|
||||||
|
"""
|
||||||
|
Modifies the select query with limit clause.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, limit_count: Optional[int]) -> None:
|
def __init__(self, limit_count: Optional[int]) -> None:
|
||||||
self.limit_count = limit_count
|
self.limit_count = limit_count
|
||||||
|
|
||||||
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
|
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
|
||||||
|
"""
|
||||||
|
Applies the limit clause.
|
||||||
|
|
||||||
|
:param expr: query to modify
|
||||||
|
:type expr: sqlalchemy.sql.selectable.Select
|
||||||
|
:return: modified query
|
||||||
|
:rtype: sqlalchemy.sql.selectable.Select
|
||||||
|
"""
|
||||||
if self.limit_count:
|
if self.limit_count:
|
||||||
expr = expr.limit(self.limit_count)
|
expr = expr.limit(self.limit_count)
|
||||||
return expr
|
return expr
|
||||||
|
|||||||
@ -4,10 +4,22 @@ import sqlalchemy
|
|||||||
|
|
||||||
|
|
||||||
class OffsetQuery:
|
class OffsetQuery:
|
||||||
|
"""
|
||||||
|
Modifies the select query with offset if set
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, query_offset: Optional[int]) -> None:
|
def __init__(self, query_offset: Optional[int]) -> None:
|
||||||
self.query_offset = query_offset
|
self.query_offset = query_offset
|
||||||
|
|
||||||
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
|
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
|
||||||
|
"""
|
||||||
|
Applies the offset clause.
|
||||||
|
|
||||||
|
:param expr: query to modify
|
||||||
|
:type expr: sqlalchemy.sql.selectable.Select
|
||||||
|
:return: modified query
|
||||||
|
:rtype: sqlalchemy.sql.selectable.Select
|
||||||
|
"""
|
||||||
if self.query_offset:
|
if self.query_offset:
|
||||||
expr = expr.offset(self.query_offset)
|
expr = expr.offset(self.query_offset)
|
||||||
return expr
|
return expr
|
||||||
|
|||||||
@ -4,10 +4,22 @@ import sqlalchemy
|
|||||||
|
|
||||||
|
|
||||||
class OrderQuery:
|
class OrderQuery:
|
||||||
|
"""
|
||||||
|
Modifies the select query with given list of order_by clauses.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, sorted_orders: Dict) -> None:
|
def __init__(self, sorted_orders: Dict) -> None:
|
||||||
self.sorted_orders = sorted_orders
|
self.sorted_orders = sorted_orders
|
||||||
|
|
||||||
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
|
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
|
||||||
|
"""
|
||||||
|
Applies all order_by clauses if set.
|
||||||
|
|
||||||
|
:param expr: query to modify
|
||||||
|
:type expr: sqlalchemy.sql.selectable.Select
|
||||||
|
:return: modified query
|
||||||
|
:rtype: sqlalchemy.sql.selectable.Select
|
||||||
|
"""
|
||||||
if self.sorted_orders:
|
if self.sorted_orders:
|
||||||
for order in list(self.sorted_orders.values()):
|
for order in list(self.sorted_orders.values()):
|
||||||
if order is not None:
|
if order is not None:
|
||||||
|
|||||||
@ -20,6 +20,10 @@ if TYPE_CHECKING: # pragma no cover
|
|||||||
|
|
||||||
|
|
||||||
class QuerySet:
|
class QuerySet:
|
||||||
|
"""
|
||||||
|
Main class to perform database queries, exposed on each model as objects attribute.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__( # noqa CFQ002
|
def __init__( # noqa CFQ002
|
||||||
self,
|
self,
|
||||||
model_cls: Type["Model"] = None,
|
model_cls: Type["Model"] = None,
|
||||||
@ -57,12 +61,24 @@ class QuerySet:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def model_meta(self) -> "ModelMeta":
|
def model_meta(self) -> "ModelMeta":
|
||||||
|
"""
|
||||||
|
Shortcut to model class Meta set on QuerySet model.
|
||||||
|
|
||||||
|
:return: Meta class of the model
|
||||||
|
:rtype: model Meta class
|
||||||
|
"""
|
||||||
if not self.model_cls: # pragma nocover
|
if not self.model_cls: # pragma nocover
|
||||||
raise ValueError("Model class of QuerySet is not initialized")
|
raise ValueError("Model class of QuerySet is not initialized")
|
||||||
return self.model_cls.Meta
|
return self.model_cls.Meta
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model(self) -> Type["Model"]:
|
def model(self) -> Type["Model"]:
|
||||||
|
"""
|
||||||
|
Shortcut to model class set on QuerySet.
|
||||||
|
|
||||||
|
:return: model class
|
||||||
|
:rtype: Type[Model]
|
||||||
|
"""
|
||||||
if not self.model_cls: # pragma nocover
|
if not self.model_cls: # pragma nocover
|
||||||
raise ValueError("Model class of QuerySet is not initialized")
|
raise ValueError("Model class of QuerySet is not initialized")
|
||||||
return self.model_cls
|
return self.model_cls
|
||||||
@ -70,6 +86,16 @@ class QuerySet:
|
|||||||
async def _prefetch_related_models(
|
async def _prefetch_related_models(
|
||||||
self, models: Sequence[Optional["Model"]], rows: List
|
self, models: Sequence[Optional["Model"]], rows: List
|
||||||
) -> Sequence[Optional["Model"]]:
|
) -> Sequence[Optional["Model"]]:
|
||||||
|
"""
|
||||||
|
Performs prefetch query for selected models names.
|
||||||
|
|
||||||
|
:param models: list of already parsed main Models from main query
|
||||||
|
:type models: List[Model]
|
||||||
|
:param rows: database rows from main query
|
||||||
|
:type rows: List[sqlalchemy.engine.result.RowProxy]
|
||||||
|
:return: list of models with prefetch models populated
|
||||||
|
:rtype: List[Model]
|
||||||
|
"""
|
||||||
query = PrefetchQuery(
|
query = PrefetchQuery(
|
||||||
model_cls=self.model,
|
model_cls=self.model,
|
||||||
fields=self._columns,
|
fields=self._columns,
|
||||||
@ -81,6 +107,14 @@ class QuerySet:
|
|||||||
return await query.prefetch_related(models=models, rows=rows) # type: ignore
|
return await query.prefetch_related(models=models, rows=rows) # type: ignore
|
||||||
|
|
||||||
def _process_query_result_rows(self, rows: List) -> Sequence[Optional["Model"]]:
|
def _process_query_result_rows(self, rows: List) -> Sequence[Optional["Model"]]:
|
||||||
|
"""
|
||||||
|
Process database rows and initialize ormar Model from each of the rows.
|
||||||
|
|
||||||
|
:param rows: list of database rows from query result
|
||||||
|
:type rows: List[sqlalchemy.engine.result.RowProxy]
|
||||||
|
:return: list of models
|
||||||
|
:rtype: List[Model]
|
||||||
|
"""
|
||||||
result_rows = [
|
result_rows = [
|
||||||
self.model.from_row(
|
self.model.from_row(
|
||||||
row=row,
|
row=row,
|
||||||
@ -94,24 +128,14 @@ class QuerySet:
|
|||||||
return self.model.merge_instances_list(result_rows) # type: ignore
|
return self.model.merge_instances_list(result_rows) # type: ignore
|
||||||
return result_rows
|
return result_rows
|
||||||
|
|
||||||
def _prepare_model_to_save(self, new_kwargs: dict) -> dict:
|
|
||||||
new_kwargs = self._remove_pk_from_kwargs(new_kwargs)
|
|
||||||
new_kwargs = self.model.substitute_models_with_pks(new_kwargs)
|
|
||||||
new_kwargs = self.model.populate_default_values(new_kwargs)
|
|
||||||
new_kwargs = self.model.translate_columns_to_aliases(new_kwargs)
|
|
||||||
return new_kwargs
|
|
||||||
|
|
||||||
def _remove_pk_from_kwargs(self, new_kwargs: dict) -> dict:
|
|
||||||
pkname = self.model_meta.pkname
|
|
||||||
pk = self.model_meta.model_fields[pkname]
|
|
||||||
if new_kwargs.get(pkname, ormar.Undefined) is None and (
|
|
||||||
pk.nullable or pk.autoincrement
|
|
||||||
):
|
|
||||||
del new_kwargs[pkname]
|
|
||||||
return new_kwargs
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_single_result_rows_count(rows: Sequence[Optional["Model"]]) -> None:
|
def check_single_result_rows_count(rows: Sequence[Optional["Model"]]) -> None:
|
||||||
|
"""
|
||||||
|
Verifies if the result has one and only one row.
|
||||||
|
|
||||||
|
:param rows: one element list of Models
|
||||||
|
:type rows: List[Model]
|
||||||
|
"""
|
||||||
if not rows or rows[0] is None:
|
if not rows or rows[0] is None:
|
||||||
raise NoMatch()
|
raise NoMatch()
|
||||||
if len(rows) > 1:
|
if len(rows) > 1:
|
||||||
@ -119,15 +143,40 @@ class QuerySet:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def database(self) -> databases.Database:
|
def database(self) -> databases.Database:
|
||||||
|
"""
|
||||||
|
Shortcut to models database from Meta class.
|
||||||
|
|
||||||
|
:return: database
|
||||||
|
:rtype: databases.Database
|
||||||
|
"""
|
||||||
return self.model_meta.database
|
return self.model_meta.database
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self) -> sqlalchemy.Table:
|
def table(self) -> sqlalchemy.Table:
|
||||||
|
"""
|
||||||
|
Shortcut to models table from Meta class.
|
||||||
|
|
||||||
|
:return: database table
|
||||||
|
:rtype: sqlalchemy.Table
|
||||||
|
"""
|
||||||
return self.model_meta.table
|
return self.model_meta.table
|
||||||
|
|
||||||
def build_select_expression(
|
def build_select_expression(
|
||||||
self, limit: int = None, offset: int = None, order_bys: List = None,
|
self, limit: int = None, offset: int = None, order_bys: List = None,
|
||||||
) -> sqlalchemy.sql.select:
|
) -> sqlalchemy.sql.select:
|
||||||
|
"""
|
||||||
|
Constructs the actual database query used in the QuerySet.
|
||||||
|
If any of the params is not passed the QuerySet own value is used.
|
||||||
|
|
||||||
|
:param limit: number to limit the query
|
||||||
|
:type limit: int
|
||||||
|
:param offset: number to offset by
|
||||||
|
:type offset: int
|
||||||
|
:param order_bys: list of order-by fields names
|
||||||
|
:type order_bys: List
|
||||||
|
:return: built sqlalchemy select expression
|
||||||
|
:rtype: sqlalchemy.sql.selectable.Select
|
||||||
|
"""
|
||||||
qry = Query(
|
qry = Query(
|
||||||
model_cls=self.model,
|
model_cls=self.model,
|
||||||
select_related=self._select_related,
|
select_related=self._select_related,
|
||||||
@ -145,6 +194,33 @@ class QuerySet:
|
|||||||
return exp
|
return exp
|
||||||
|
|
||||||
def filter(self, _exclude: bool = False, **kwargs: Any) -> "QuerySet": # noqa: A003
|
def filter(self, _exclude: bool = False, **kwargs: Any) -> "QuerySet": # noqa: A003
|
||||||
|
"""
|
||||||
|
Allows you to filter by any `Model` attribute/field
|
||||||
|
as well as to fetch instances, with a filter across an FK relationship.
|
||||||
|
|
||||||
|
You can use special filter suffix to change the filter operands:
|
||||||
|
|
||||||
|
* exact - like `album__name__exact='Malibu'` (exact match)
|
||||||
|
* iexact - like `album__name__iexact='malibu'` (exact match case insensitive)
|
||||||
|
* contains - like `album__name__contains='Mal'` (sql like)
|
||||||
|
* icontains - like `album__name__icontains='mal'` (sql like case insensitive)
|
||||||
|
* in - like `album__name__in=['Malibu', 'Barclay']` (sql in)
|
||||||
|
* gt - like `position__gt=3` (sql >)
|
||||||
|
* gte - like `position__gte=3` (sql >=)
|
||||||
|
* lt - like `position__lt=3` (sql <)
|
||||||
|
* lte - like `position__lte=3` (sql <=)
|
||||||
|
* startswith - like `album__name__startswith='Mal'` (exact start match)
|
||||||
|
* istartswith - like `album__name__istartswith='mal'` (case insensitive)
|
||||||
|
* endswith - like `album__name__endswith='ibu'` (exact end match)
|
||||||
|
* iendswith - like `album__name__iendswith='IBU'` (case insensitive)
|
||||||
|
|
||||||
|
:param _exclude: flag if it should be exclude or filter
|
||||||
|
:type _exclude: bool
|
||||||
|
:param kwargs: fields names and proper value types
|
||||||
|
:type kwargs: Any
|
||||||
|
:return: filtered QuerySet
|
||||||
|
:rtype: QuerySet
|
||||||
|
"""
|
||||||
qryclause = QueryClause(
|
qryclause = QueryClause(
|
||||||
model_cls=self.model,
|
model_cls=self.model,
|
||||||
select_related=self._select_related,
|
select_related=self._select_related,
|
||||||
@ -173,9 +249,43 @@ class QuerySet:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def exclude(self, **kwargs: Any) -> "QuerySet": # noqa: A003
|
def exclude(self, **kwargs: Any) -> "QuerySet": # noqa: A003
|
||||||
|
"""
|
||||||
|
Works exactly the same as filter and all modifiers (suffixes) are the same,
|
||||||
|
but returns a *not* condition.
|
||||||
|
|
||||||
|
So if you use `filter(name='John')` which is `where name = 'John'` in SQL,
|
||||||
|
the `exclude(name='John')` equals to `where name <> 'John'`
|
||||||
|
|
||||||
|
Note that all conditions are joined so if you pass multiple values it
|
||||||
|
becomes a union of conditions.
|
||||||
|
|
||||||
|
`exclude(name='John', age>=35)` will become
|
||||||
|
`where not (name='John' and age>=35)`
|
||||||
|
|
||||||
|
:param kwargs: fields names and proper value types
|
||||||
|
:type kwargs: Any
|
||||||
|
:return: filtered QuerySet
|
||||||
|
:rtype: QuerySet
|
||||||
|
"""
|
||||||
return self.filter(_exclude=True, **kwargs)
|
return self.filter(_exclude=True, **kwargs)
|
||||||
|
|
||||||
def select_related(self, related: Union[List, str]) -> "QuerySet":
|
def select_related(self, related: Union[List, str]) -> "QuerySet":
|
||||||
|
"""
|
||||||
|
Allows to prefetch related models during the same query.
|
||||||
|
|
||||||
|
**With `select_related` always only one query is run against the database**,
|
||||||
|
meaning that one (sometimes complicated) join is generated and later nested
|
||||||
|
models are processed in python.
|
||||||
|
|
||||||
|
To fetch related model use `ForeignKey` names.
|
||||||
|
|
||||||
|
To chain related `Models` relation use double underscores between names.
|
||||||
|
|
||||||
|
:param related: list of relation field names, can be linked by '__' to nest
|
||||||
|
:type related: Union[List, str]
|
||||||
|
:return: QuerySet
|
||||||
|
:rtype: QuerySet
|
||||||
|
"""
|
||||||
if not isinstance(related, list):
|
if not isinstance(related, list):
|
||||||
related = [related]
|
related = [related]
|
||||||
|
|
||||||
@ -195,6 +305,23 @@ class QuerySet:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def prefetch_related(self, related: Union[List, str]) -> "QuerySet":
|
def prefetch_related(self, related: Union[List, str]) -> "QuerySet":
|
||||||
|
"""
|
||||||
|
Allows to prefetch related models during query - but opposite to
|
||||||
|
`select_related` each subsequent model is fetched in a separate database query.
|
||||||
|
|
||||||
|
**With `prefetch_related` always one query per Model is run against the
|
||||||
|
database**, meaning that you will have multiple queries executed one
|
||||||
|
after another.
|
||||||
|
|
||||||
|
To fetch related model use `ForeignKey` names.
|
||||||
|
|
||||||
|
To chain related `Models` relation use double underscores between names.
|
||||||
|
|
||||||
|
:param related: list of relation field names, can be linked by '__' to nest
|
||||||
|
:type related: Union[List, str]
|
||||||
|
:return: QuerySet
|
||||||
|
:rtype: QuerySet
|
||||||
|
"""
|
||||||
if not isinstance(related, list):
|
if not isinstance(related, list):
|
||||||
related = [related]
|
related = [related]
|
||||||
|
|
||||||
@ -213,31 +340,49 @@ class QuerySet:
|
|||||||
limit_raw_sql=self.limit_sql_raw,
|
limit_raw_sql=self.limit_sql_raw,
|
||||||
)
|
)
|
||||||
|
|
||||||
def exclude_fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
|
|
||||||
if isinstance(columns, str):
|
|
||||||
columns = [columns]
|
|
||||||
|
|
||||||
current_excluded = self._exclude_columns
|
|
||||||
if not isinstance(columns, dict):
|
|
||||||
current_excluded = update_dict_from_list(current_excluded, columns)
|
|
||||||
else:
|
|
||||||
current_excluded = update(current_excluded, columns)
|
|
||||||
|
|
||||||
return self.__class__(
|
|
||||||
model_cls=self.model,
|
|
||||||
filter_clauses=self.filter_clauses,
|
|
||||||
exclude_clauses=self.exclude_clauses,
|
|
||||||
select_related=self._select_related,
|
|
||||||
limit_count=self.limit_count,
|
|
||||||
offset=self.query_offset,
|
|
||||||
columns=self._columns,
|
|
||||||
exclude_columns=current_excluded,
|
|
||||||
order_bys=self.order_bys,
|
|
||||||
prefetch_related=self._prefetch_related,
|
|
||||||
limit_raw_sql=self.limit_sql_raw,
|
|
||||||
)
|
|
||||||
|
|
||||||
def fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
|
def fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
|
||||||
|
"""
|
||||||
|
With `fields()` you can select subset of model columns to limit the data load.
|
||||||
|
|
||||||
|
Note that `fields()` and `exclude_fields()` works both for main models
|
||||||
|
(on normal queries like `get`, `all` etc.)
|
||||||
|
as well as `select_related` and `prefetch_related`
|
||||||
|
models (with nested notation).
|
||||||
|
|
||||||
|
You can select specified fields by passing a `str, List[str], Set[str] or
|
||||||
|
dict` with nested definition.
|
||||||
|
|
||||||
|
To include related models use notation
|
||||||
|
`{related_name}__{column}[__{optional_next} etc.]`.
|
||||||
|
|
||||||
|
`fields()` can be called several times, building up the columns to select.
|
||||||
|
|
||||||
|
If you include related models into `select_related()` call but you won't specify
|
||||||
|
columns for those models in fields - implies a list of all fields for
|
||||||
|
those nested models.
|
||||||
|
|
||||||
|
Mandatory fields cannot be excluded as it will raise `ValidationError`,
|
||||||
|
to exclude a field it has to be nullable.
|
||||||
|
|
||||||
|
Pk column cannot be excluded - it's always auto added even if
|
||||||
|
not explicitly included.
|
||||||
|
|
||||||
|
You can also pass fields to include as dictionary or set.
|
||||||
|
|
||||||
|
To mark a field as included in a dictionary use it's name as key
|
||||||
|
and ellipsis as value.
|
||||||
|
|
||||||
|
To traverse nested models use nested dictionaries.
|
||||||
|
|
||||||
|
To include fields at last level instead of nested dictionary a set can be used.
|
||||||
|
|
||||||
|
To include whole nested model specify model related field name and ellipsis.
|
||||||
|
|
||||||
|
:param columns: columns to include
|
||||||
|
:type columns: Union[List, str, Set, Dict]
|
||||||
|
:return: QuerySet
|
||||||
|
:rtype: QuerySet
|
||||||
|
"""
|
||||||
if isinstance(columns, str):
|
if isinstance(columns, str):
|
||||||
columns = [columns]
|
columns = [columns]
|
||||||
|
|
||||||
@ -261,7 +406,88 @@ class QuerySet:
|
|||||||
limit_raw_sql=self.limit_sql_raw,
|
limit_raw_sql=self.limit_sql_raw,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def exclude_fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
|
||||||
|
"""
|
||||||
|
With `exclude_fields()` you can select subset of model columns that will
|
||||||
|
be excluded to limit the data load.
|
||||||
|
|
||||||
|
It's the opposite of `fields()` method so check documentation above
|
||||||
|
to see what options are available.
|
||||||
|
|
||||||
|
Especially check above how you can pass also nested dictionaries
|
||||||
|
and sets as a mask to exclude fields from whole hierarchy.
|
||||||
|
|
||||||
|
Note that `fields()` and `exclude_fields()` works both for main models
|
||||||
|
(on normal queries like `get`, `all` etc.)
|
||||||
|
as well as `select_related` and `prefetch_related` models
|
||||||
|
(with nested notation).
|
||||||
|
|
||||||
|
Mandatory fields cannot be excluded as it will raise `ValidationError`,
|
||||||
|
to exclude a field it has to be nullable.
|
||||||
|
|
||||||
|
Pk column cannot be excluded - it's always auto added even
|
||||||
|
if explicitly excluded.
|
||||||
|
|
||||||
|
:param columns: columns to exclude
|
||||||
|
:type columns: Union[List, str, Set, Dict]
|
||||||
|
:return: QuerySet
|
||||||
|
:rtype: QuerySet
|
||||||
|
"""
|
||||||
|
if isinstance(columns, str):
|
||||||
|
columns = [columns]
|
||||||
|
|
||||||
|
current_excluded = self._exclude_columns
|
||||||
|
if not isinstance(columns, dict):
|
||||||
|
current_excluded = update_dict_from_list(current_excluded, columns)
|
||||||
|
else:
|
||||||
|
current_excluded = update(current_excluded, columns)
|
||||||
|
|
||||||
|
return self.__class__(
|
||||||
|
model_cls=self.model,
|
||||||
|
filter_clauses=self.filter_clauses,
|
||||||
|
exclude_clauses=self.exclude_clauses,
|
||||||
|
select_related=self._select_related,
|
||||||
|
limit_count=self.limit_count,
|
||||||
|
offset=self.query_offset,
|
||||||
|
columns=self._columns,
|
||||||
|
exclude_columns=current_excluded,
|
||||||
|
order_bys=self.order_bys,
|
||||||
|
prefetch_related=self._prefetch_related,
|
||||||
|
limit_raw_sql=self.limit_sql_raw,
|
||||||
|
)
|
||||||
|
|
||||||
def order_by(self, columns: Union[List, str]) -> "QuerySet":
|
def order_by(self, columns: Union[List, str]) -> "QuerySet":
|
||||||
|
"""
|
||||||
|
With `order_by()` you can order the results from database based on your
|
||||||
|
choice of fields.
|
||||||
|
|
||||||
|
You can provide a string with field name or list of strings with fields names.
|
||||||
|
|
||||||
|
Ordering in sql will be applied in order of names you provide in order_by.
|
||||||
|
|
||||||
|
By default if you do not provide ordering `ormar` explicitly orders by
|
||||||
|
all primary keys
|
||||||
|
|
||||||
|
If you are sorting by nested models that causes that the result rows are
|
||||||
|
unsorted by the main model `ormar` will combine those children rows into
|
||||||
|
one main model.
|
||||||
|
|
||||||
|
The main model will never duplicate in the result
|
||||||
|
|
||||||
|
To order by main model field just provide a field name
|
||||||
|
|
||||||
|
To sort on nested models separate field names with dunder '__'.
|
||||||
|
|
||||||
|
You can sort this way across all relation types -> `ForeignKey`,
|
||||||
|
reverse virtual FK and `ManyToMany` fields.
|
||||||
|
|
||||||
|
To sort in descending order provide a hyphen in front of the field name
|
||||||
|
|
||||||
|
:param columns: columns by which models should be sorted
|
||||||
|
:type columns: Union[List, str]
|
||||||
|
:return: QuerySet
|
||||||
|
:rtype: QuerySet
|
||||||
|
"""
|
||||||
if not isinstance(columns, list):
|
if not isinstance(columns, list):
|
||||||
columns = [columns]
|
columns = [columns]
|
||||||
|
|
||||||
@ -281,16 +507,43 @@ class QuerySet:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def exists(self) -> bool:
|
async def exists(self) -> bool:
|
||||||
|
"""
|
||||||
|
Returns a bool value to confirm if there are rows matching the given criteria
|
||||||
|
(applied with `filter` and `exclude` if set).
|
||||||
|
|
||||||
|
:return: result of the check
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
expr = self.build_select_expression()
|
expr = self.build_select_expression()
|
||||||
expr = sqlalchemy.exists(expr).select()
|
expr = sqlalchemy.exists(expr).select()
|
||||||
return await self.database.fetch_val(expr)
|
return await self.database.fetch_val(expr)
|
||||||
|
|
||||||
async def count(self) -> int:
|
async def count(self) -> int:
|
||||||
|
"""
|
||||||
|
Returns number of rows matching the given criteria
|
||||||
|
(applied with `filter` and `exclude` if set before).
|
||||||
|
|
||||||
|
:return: number of rows
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
expr = self.build_select_expression().alias("subquery_for_count")
|
expr = self.build_select_expression().alias("subquery_for_count")
|
||||||
expr = sqlalchemy.func.count().select().select_from(expr)
|
expr = sqlalchemy.func.count().select().select_from(expr)
|
||||||
return await self.database.fetch_val(expr)
|
return await self.database.fetch_val(expr)
|
||||||
|
|
||||||
async def update(self, each: bool = False, **kwargs: Any) -> int:
|
async def update(self, each: bool = False, **kwargs: Any) -> int:
|
||||||
|
"""
|
||||||
|
Updates the model table after applying the filters from kwargs.
|
||||||
|
|
||||||
|
You have to either pass a filter to narrow down a query or explicitly pass
|
||||||
|
each=True flag to affect whole table.
|
||||||
|
|
||||||
|
:param each: flag if whole table should be affected if no filter is passed
|
||||||
|
:type each: bool
|
||||||
|
:param kwargs: fields names and proper value types
|
||||||
|
:type kwargs: Any
|
||||||
|
:return: number of updated rows
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
self_fields = self.model.extract_db_own_fields().union(
|
self_fields = self.model.extract_db_own_fields().union(
|
||||||
self.model.extract_related_names()
|
self.model.extract_related_names()
|
||||||
)
|
)
|
||||||
@ -307,6 +560,19 @@ class QuerySet:
|
|||||||
return await self.database.execute(expr)
|
return await self.database.execute(expr)
|
||||||
|
|
||||||
async def delete(self, each: bool = False, **kwargs: Any) -> int:
|
async def delete(self, each: bool = False, **kwargs: Any) -> int:
|
||||||
|
"""
|
||||||
|
Deletes from the model table after applying the filters from kwargs.
|
||||||
|
|
||||||
|
You have to either pass a filter to narrow down a query or explicitly pass
|
||||||
|
each=True flag to affect whole table.
|
||||||
|
|
||||||
|
:param each: flag if whole table should be affected if no filter is passed
|
||||||
|
:type each: bool
|
||||||
|
:param kwargs: fields names and proper value types
|
||||||
|
:type kwargs: Any
|
||||||
|
:return: number of deleted rows
|
||||||
|
:rtype:int
|
||||||
|
"""
|
||||||
if kwargs:
|
if kwargs:
|
||||||
return await self.filter(**kwargs).delete()
|
return await self.filter(**kwargs).delete()
|
||||||
if not each and not self.filter_clauses:
|
if not each and not self.filter_clauses:
|
||||||
@ -320,6 +586,19 @@ class QuerySet:
|
|||||||
return await self.database.execute(expr)
|
return await self.database.execute(expr)
|
||||||
|
|
||||||
def limit(self, limit_count: int, limit_raw_sql: bool = None) -> "QuerySet":
|
def limit(self, limit_count: int, limit_raw_sql: bool = None) -> "QuerySet":
|
||||||
|
"""
|
||||||
|
You can limit the results to desired number of parent models.
|
||||||
|
|
||||||
|
To limit the actual number of database query rows instead of number of main
|
||||||
|
models use the `limit_raw_sql` parameter flag, and set it to `True`.
|
||||||
|
|
||||||
|
:param limit_raw_sql: flag if raw sql should be limited
|
||||||
|
:type limit_raw_sql: bool
|
||||||
|
:param limit_count: number of models to limit
|
||||||
|
:type limit_count: int
|
||||||
|
:return: QuerySet
|
||||||
|
:rtype: QuerySet
|
||||||
|
"""
|
||||||
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql
|
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql
|
||||||
return self.__class__(
|
return self.__class__(
|
||||||
model_cls=self.model,
|
model_cls=self.model,
|
||||||
@ -336,6 +615,19 @@ class QuerySet:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def offset(self, offset: int, limit_raw_sql: bool = None) -> "QuerySet":
|
def offset(self, offset: int, limit_raw_sql: bool = None) -> "QuerySet":
|
||||||
|
"""
|
||||||
|
You can also offset the results by desired number of main models.
|
||||||
|
|
||||||
|
To offset the actual number of database query rows instead of number of main
|
||||||
|
models use the `limit_raw_sql` parameter flag, and set it to `True`.
|
||||||
|
|
||||||
|
:param limit_raw_sql: flag if raw sql should be offset
|
||||||
|
:type limit_raw_sql: bool
|
||||||
|
:param offset: numbers of models to offset
|
||||||
|
:type offset: int
|
||||||
|
:return: QuerySet
|
||||||
|
:rtype: QuerySet
|
||||||
|
"""
|
||||||
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql
|
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql
|
||||||
return self.__class__(
|
return self.__class__(
|
||||||
model_cls=self.model,
|
model_cls=self.model,
|
||||||
@ -352,6 +644,16 @@ class QuerySet:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def first(self, **kwargs: Any) -> "Model":
|
async def first(self, **kwargs: Any) -> "Model":
|
||||||
|
"""
|
||||||
|
Gets the first row from the db ordered by primary key column ascending.
|
||||||
|
|
||||||
|
:raises: NoMatch if no rows are returned
|
||||||
|
:raises: MultipleMatches if more than 1 row is returned.
|
||||||
|
:param kwargs: fields names and proper value types
|
||||||
|
:type kwargs: Any
|
||||||
|
:return: returned model
|
||||||
|
:rtype: Model
|
||||||
|
"""
|
||||||
if kwargs:
|
if kwargs:
|
||||||
return await self.filter(**kwargs).first()
|
return await self.filter(**kwargs).first()
|
||||||
|
|
||||||
@ -366,6 +668,20 @@ class QuerySet:
|
|||||||
return processed_rows[0] # type: ignore
|
return processed_rows[0] # type: ignore
|
||||||
|
|
||||||
async def get(self, **kwargs: Any) -> "Model":
|
async def get(self, **kwargs: Any) -> "Model":
|
||||||
|
"""
|
||||||
|
Get's the first row from the db meeting the criteria set by kwargs.
|
||||||
|
|
||||||
|
If no criteria set it will return the last row in db sorted by pk.
|
||||||
|
|
||||||
|
Passing a criteria is actually calling filter(**kwargs) method described below.
|
||||||
|
|
||||||
|
:raises: NoMatch if no rows are returned
|
||||||
|
:raises: MultipleMatches if more than 1 row is returned.
|
||||||
|
:param kwargs: fields names and proper value types
|
||||||
|
:type kwargs: Any
|
||||||
|
:return: returned model
|
||||||
|
:rtype: Model
|
||||||
|
"""
|
||||||
if kwargs:
|
if kwargs:
|
||||||
return await self.filter(**kwargs).get()
|
return await self.filter(**kwargs).get()
|
||||||
|
|
||||||
@ -384,12 +700,32 @@ class QuerySet:
|
|||||||
return processed_rows[0] # type: ignore
|
return processed_rows[0] # type: ignore
|
||||||
|
|
||||||
async def get_or_create(self, **kwargs: Any) -> "Model":
|
async def get_or_create(self, **kwargs: Any) -> "Model":
|
||||||
|
"""
|
||||||
|
Combination of create and get methods.
|
||||||
|
|
||||||
|
Tries to get a row meeting the criteria fro kwargs
|
||||||
|
and if `NoMatch` exception is raised
|
||||||
|
it creates a new one with given kwargs.
|
||||||
|
|
||||||
|
:param kwargs: fields names and proper value types
|
||||||
|
:type kwargs: Any
|
||||||
|
:return: returned or created Model
|
||||||
|
:rtype: Model
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return await self.get(**kwargs)
|
return await self.get(**kwargs)
|
||||||
except NoMatch:
|
except NoMatch:
|
||||||
return await self.create(**kwargs)
|
return await self.create(**kwargs)
|
||||||
|
|
||||||
async def update_or_create(self, **kwargs: Any) -> "Model":
|
async def update_or_create(self, **kwargs: Any) -> "Model":
|
||||||
|
"""
|
||||||
|
Updates the model, or in case there is no match in database creates a new one.
|
||||||
|
|
||||||
|
:param kwargs: fields names and proper value types
|
||||||
|
:type kwargs: Any
|
||||||
|
:return: updated or created model
|
||||||
|
:rtype: Model
|
||||||
|
"""
|
||||||
pk_name = self.model_meta.pkname
|
pk_name = self.model_meta.pkname
|
||||||
if "pk" in kwargs:
|
if "pk" in kwargs:
|
||||||
kwargs[pk_name] = kwargs.pop("pk")
|
kwargs[pk_name] = kwargs.pop("pk")
|
||||||
@ -399,6 +735,18 @@ class QuerySet:
|
|||||||
return await model.update(**kwargs)
|
return await model.update(**kwargs)
|
||||||
|
|
||||||
async def all(self, **kwargs: Any) -> Sequence[Optional["Model"]]: # noqa: A003
|
async def all(self, **kwargs: Any) -> Sequence[Optional["Model"]]: # noqa: A003
|
||||||
|
"""
|
||||||
|
Returns all rows from a database for given model for set filter options.
|
||||||
|
|
||||||
|
Passing kwargs is a shortcut and equals to calling `filter(**kwrags).all()`.
|
||||||
|
|
||||||
|
If there are no rows meeting the criteria an empty list is returned.
|
||||||
|
|
||||||
|
:param kwargs: fields names and proper value types
|
||||||
|
:type kwargs: Any
|
||||||
|
:return: list of returned models
|
||||||
|
:rtype: List[Model]
|
||||||
|
"""
|
||||||
if kwargs:
|
if kwargs:
|
||||||
return await self.filter(**kwargs).all()
|
return await self.filter(**kwargs).all()
|
||||||
|
|
||||||
@ -411,9 +759,19 @@ class QuerySet:
|
|||||||
return result_rows
|
return result_rows
|
||||||
|
|
||||||
async def create(self, **kwargs: Any) -> "Model":
|
async def create(self, **kwargs: Any) -> "Model":
|
||||||
|
"""
|
||||||
|
Creates the model instance, saves it in a database and returns the updates model
|
||||||
|
(with pk populated if not passed and autoincrement is set).
|
||||||
|
|
||||||
|
The allowed kwargs are `Model` fields names and proper value types.
|
||||||
|
|
||||||
|
:param kwargs: fields names and proper value types
|
||||||
|
:type kwargs: Any
|
||||||
|
:return: created model
|
||||||
|
:rtype: Model
|
||||||
|
"""
|
||||||
new_kwargs = dict(**kwargs)
|
new_kwargs = dict(**kwargs)
|
||||||
new_kwargs = self._prepare_model_to_save(new_kwargs)
|
new_kwargs = self.model._prepare_model_to_save(new_kwargs)
|
||||||
|
|
||||||
expr = self.table.insert()
|
expr = self.table.insert()
|
||||||
expr = expr.values(**new_kwargs)
|
expr = expr.values(**new_kwargs)
|
||||||
@ -444,10 +802,22 @@ class QuerySet:
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
async def bulk_create(self, objects: List["Model"]) -> None:
|
async def bulk_create(self, objects: List["Model"]) -> None:
|
||||||
|
"""
|
||||||
|
Performs a bulk update in one database session to speed up the process.
|
||||||
|
|
||||||
|
Allows you to create multiple objects at once.
|
||||||
|
|
||||||
|
A valid list of `Model` objects needs to be passed.
|
||||||
|
|
||||||
|
Bulk operations do not send signals.
|
||||||
|
|
||||||
|
:param objects: list of ormar models already initialized and ready to save.
|
||||||
|
:type objects: List[Model]
|
||||||
|
"""
|
||||||
ready_objects = []
|
ready_objects = []
|
||||||
for objt in objects:
|
for objt in objects:
|
||||||
new_kwargs = objt.dict()
|
new_kwargs = objt.dict()
|
||||||
new_kwargs = self._prepare_model_to_save(new_kwargs)
|
new_kwargs = objt._prepare_model_to_save(new_kwargs)
|
||||||
ready_objects.append(new_kwargs)
|
ready_objects.append(new_kwargs)
|
||||||
|
|
||||||
expr = self.table.insert()
|
expr = self.table.insert()
|
||||||
@ -459,6 +829,23 @@ class QuerySet:
|
|||||||
async def bulk_update( # noqa: CCR001
|
async def bulk_update( # noqa: CCR001
|
||||||
self, objects: List["Model"], columns: List[str] = None
|
self, objects: List["Model"], columns: List[str] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Performs bulk update in one database session to speed up the process.
|
||||||
|
|
||||||
|
Allows to update multiple instance at once.
|
||||||
|
|
||||||
|
All `Models` passed need to have primary key column populated.
|
||||||
|
|
||||||
|
You can also select which fields to update by passing `columns` list
|
||||||
|
as a list of string names.
|
||||||
|
|
||||||
|
Bulk operations do not send signals.
|
||||||
|
|
||||||
|
:param objects: list of ormar models
|
||||||
|
:type objects: List[Model]
|
||||||
|
:param columns: list of columns to update
|
||||||
|
:type columns: List[str]
|
||||||
|
"""
|
||||||
ready_objects = []
|
ready_objects = []
|
||||||
pk_name = self.model_meta.pkname
|
pk_name = self.model_meta.pkname
|
||||||
if not columns:
|
if not columns:
|
||||||
|
|||||||
@ -376,7 +376,7 @@ class QuerysetProxy(ormar.QuerySetProtocol):
|
|||||||
Actual call delegated to QuerySet.
|
Actual call delegated to QuerySet.
|
||||||
|
|
||||||
:param related: list of relation field names, can be linked by '__' to nest
|
:param related: list of relation field names, can be linked by '__' to nest
|
||||||
:type related: str
|
:type related: Union[List, str]
|
||||||
:return: QuerysetProxy
|
:return: QuerysetProxy
|
||||||
:rtype: QuerysetProxy
|
:rtype: QuerysetProxy
|
||||||
"""
|
"""
|
||||||
@ -399,7 +399,7 @@ class QuerysetProxy(ormar.QuerySetProtocol):
|
|||||||
Actual call delegated to QuerySet.
|
Actual call delegated to QuerySet.
|
||||||
|
|
||||||
:param related: list of relation field names, can be linked by '__' to nest
|
:param related: list of relation field names, can be linked by '__' to nest
|
||||||
:type related: str
|
:type related: Union[List, str]
|
||||||
:return: QuerysetProxy
|
:return: QuerysetProxy
|
||||||
:rtype: QuerysetProxy
|
:rtype: QuerysetProxy
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user