ramifice 0.4.11__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ from pymongo.asynchronous.command_cursor import AsyncCommandCursor
7
7
  from pymongo.asynchronous.database import AsyncDatabase
8
8
 
9
9
  from ..utils import globals, translations
10
+ from .tools import correct_mongo_filter
10
11
 
11
12
 
12
13
  class GeneralMixin:
@@ -25,7 +26,7 @@ class GeneralMixin:
25
26
  if field is None:
26
27
  continue
27
28
  if field.field_type == "TextField":
28
- field.value = data.get(lang, "") if data is not None else None
29
+ field.value = data.get(lang, "- -") if data is not None else None
29
30
  elif field.group == "pass":
30
31
  field.value = None
31
32
  else:
@@ -58,7 +59,10 @@ class GeneralMixin:
58
59
  """Gets an estimate of the count of documents in a collection using collection metadata."""
59
60
  # Get collection for current model.
60
61
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
61
- #
62
+ # Correcting filter.
63
+ if filter is not None:
64
+ filter = correct_mongo_filter(cls, filter)
65
+
62
66
  return await collection.count_documents(
63
67
  filter=filter,
64
68
  session=session,
@@ -78,7 +82,10 @@ class GeneralMixin:
78
82
  """Runs an aggregation framework pipeline."""
79
83
  # Get collection for current model.
80
84
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
81
- #
85
+ # Correcting filter.
86
+ if pipeline is not None:
87
+ pipeline = correct_mongo_filter(cls, pipeline)
88
+
82
89
  return await collection.aggregate(
83
90
  pipeline=pipeline,
84
91
  session=session,
@@ -103,7 +110,10 @@ class GeneralMixin:
103
110
  """
104
111
  # Get collection for current model.
105
112
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
106
- #
113
+ # Correcting filter.
114
+ if filter is not None:
115
+ filter = correct_mongo_filter(cls, filter)
116
+
107
117
  return await collection.distinct(
108
118
  key=key,
109
119
  filter=filter,
ramifice/commons/many.py CHANGED
@@ -7,9 +7,9 @@ from pymongo.asynchronous.collection import AsyncCollection
7
7
  from pymongo.asynchronous.cursor import AsyncCursor, CursorType
8
8
  from pymongo.results import DeleteResult
9
9
 
10
- from ..utils import globals
10
+ from ..utils import globals, translations
11
11
  from ..utils.errors import PanicError
12
- from .tools import mongo_doc_to_raw_doc, password_to_none
12
+ from .tools import correct_mongo_filter, mongo_doc_to_raw_doc, password_to_none
13
13
 
14
14
 
15
15
  class ManyMixin:
@@ -43,6 +43,9 @@ class ManyMixin:
43
43
  """Find documents."""
44
44
  # Get collection for current model.
45
45
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
46
+ # Correcting filter.
47
+ if filter is not None:
48
+ filter = correct_mongo_filter(cls, filter)
46
49
  # Get documents.
47
50
  doc_list: list[dict[str, Any]] = []
48
51
  cursor: AsyncCursor = collection.find(
@@ -108,6 +111,9 @@ class ManyMixin:
108
111
  """
109
112
  # Get collection for current model.
110
113
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
114
+ # Correcting filter.
115
+ if filter is not None:
116
+ filter = correct_mongo_filter(cls, filter)
111
117
  # Get documents.
112
118
  doc_list: list[dict[str, Any]] = []
113
119
  cursor: AsyncCursor = collection.find(
@@ -133,9 +139,18 @@ class ManyMixin:
133
139
  session=session,
134
140
  allow_disk_use=allow_disk_use,
135
141
  )
136
- field_name_and_type = cls.META["field_name_and_type"]
142
+ inst_model_dict = {
143
+ key: val for key, val in cls().__dict__.items() if not callable(val) and not val.ignored
144
+ }
145
+ lang = translations.CURRENT_LOCALE
137
146
  async for mongo_doc in cursor:
138
- doc_list.append(mongo_doc_to_raw_doc(field_name_and_type, mongo_doc))
147
+ doc_list.append(
148
+ mongo_doc_to_raw_doc(
149
+ inst_model_dict,
150
+ mongo_doc,
151
+ lang,
152
+ )
153
+ )
139
154
  return doc_list
140
155
 
141
156
  @classmethod
@@ -166,6 +181,9 @@ class ManyMixin:
166
181
  """Find documents and convert to a json string."""
167
182
  # Get collection for current model.
168
183
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
184
+ # Correcting filter.
185
+ if filter is not None:
186
+ filter = correct_mongo_filter(cls, filter)
169
187
  # Get documents.
170
188
  doc_list: list[dict[str, Any]] = []
171
189
  cursor: AsyncCursor = collection.find(
@@ -191,9 +209,18 @@ class ManyMixin:
191
209
  session=session,
192
210
  allow_disk_use=allow_disk_use,
193
211
  )
194
- field_name_and_type = cls.META["field_name_and_type"]
212
+ inst_model_dict = {
213
+ key: val for key, val in cls().__dict__.items() if not callable(val) and not val.ignored
214
+ }
215
+ lang = translations.CURRENT_LOCALE
195
216
  async for mongo_doc in cursor:
196
- doc_list.append(mongo_doc_to_raw_doc(field_name_and_type, mongo_doc))
217
+ doc_list.append(
218
+ mongo_doc_to_raw_doc(
219
+ inst_model_dict,
220
+ mongo_doc,
221
+ lang,
222
+ )
223
+ )
197
224
  return json.dumps(doc_list) if len(doc_list) > 0 else None
198
225
 
199
226
  @classmethod
@@ -217,6 +244,9 @@ class ManyMixin:
217
244
  raise PanicError(msg)
218
245
  # Get collection for current model.
219
246
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
247
+ # Correcting filter.
248
+ if filter is not None:
249
+ filter = correct_mongo_filter(cls, filter)
220
250
  # Delete documents.
221
251
  result: DeleteResult = await collection.delete_many(
222
252
  filter=filter,
ramifice/commons/one.py CHANGED
@@ -5,9 +5,9 @@ from typing import Any
5
5
  from pymongo.asynchronous.collection import AsyncCollection
6
6
  from pymongo.results import DeleteResult
7
7
 
8
- from ..utils import globals
8
+ from ..utils import globals, translations
9
9
  from ..utils.errors import PanicError
10
- from .tools import mongo_doc_to_raw_doc, password_to_none
10
+ from .tools import correct_mongo_filter, mongo_doc_to_raw_doc, password_to_none
11
11
 
12
12
 
13
13
  class OneMixin:
@@ -23,6 +23,9 @@ class OneMixin:
23
23
  """Find a single document."""
24
24
  # Get collection for current model.
25
25
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
26
+ # Correcting filter.
27
+ if filter is not None:
28
+ filter = correct_mongo_filter(cls, filter)
26
29
  # Get document.
27
30
  mongo_doc = await collection.find_one(filter, *args, **kwargs)
28
31
  if mongo_doc is not None:
@@ -42,13 +45,20 @@ class OneMixin:
42
45
  """Find a single document and converting to raw document."""
43
46
  # Get collection for current model.
44
47
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
48
+ # Correcting filter.
49
+ if filter is not None:
50
+ filter = correct_mongo_filter(cls, filter)
45
51
  # Get document.
46
52
  raw_doc = None
47
53
  mongo_doc = await collection.find_one(filter, *args, **kwargs)
54
+ inst_model_dict = {
55
+ key: val for key, val in cls().__dict__.items() if not callable(val) and not val.ignored
56
+ }
48
57
  if mongo_doc is not None:
49
58
  raw_doc = mongo_doc_to_raw_doc(
50
- cls.META["field_name_and_type"],
59
+ inst_model_dict,
51
60
  mongo_doc,
61
+ translations.CURRENT_LOCALE,
52
62
  )
53
63
  return raw_doc
54
64
 
@@ -62,6 +72,9 @@ class OneMixin:
62
72
  """Find a single document and convert it to a Model instance."""
63
73
  # Get collection for current model.
64
74
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
75
+ # Correcting filter.
76
+ if filter is not None:
77
+ filter = correct_mongo_filter(cls, filter)
65
78
  # Get document.
66
79
  inst_model = None
67
80
  mongo_doc = await collection.find_one(filter, *args, **kwargs)
@@ -80,12 +93,15 @@ class OneMixin:
80
93
  """Find a single document and convert it to a JSON string."""
81
94
  # Get collection for current model.
82
95
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
96
+ # Correcting filter.
97
+ if filter is not None:
98
+ filter = correct_mongo_filter(cls, filter)
83
99
  # Get document.
84
100
  json_str: str | None = None
85
101
  mongo_doc = await collection.find_one(filter, *args, **kwargs)
86
102
  if mongo_doc is not None:
87
103
  # Convert document to Model instance.
88
- inst_model = cls.from_mongo_doc( mongo_doc)
104
+ inst_model = cls.from_mongo_doc(mongo_doc)
89
105
  json_str = inst_model.to_json()
90
106
  return json_str
91
107
 
@@ -110,6 +126,9 @@ class OneMixin:
110
126
  raise PanicError(msg)
111
127
  # Get collection for current model.
112
128
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
129
+ # Correcting filter.
130
+ if filter is not None:
131
+ filter = correct_mongo_filter(cls, filter)
113
132
  # Get document.
114
133
  result: DeleteResult = await collection.delete_one(
115
134
  filter=filter,
@@ -144,6 +163,9 @@ class OneMixin:
144
163
  raise PanicError(msg)
145
164
  # Get collection for current model.
146
165
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
166
+ # Correcting filter.
167
+ if filter is not None:
168
+ filter = correct_mongo_filter(cls, filter)
147
169
  # Get document.
148
170
  mongo_doc: dict[str, Any] | None = await collection.find_one_and_delete(
149
171
  filter=filter,
ramifice/commons/tools.py CHANGED
@@ -1,12 +1,29 @@
1
1
  """Tool of Commons - A set of auxiliary methods."""
2
2
 
3
+ import json
3
4
  from typing import Any
4
5
 
5
6
  from babel.dates import format_date, format_datetime
7
+ from bson import json_util
6
8
 
7
9
  from ..utils import translations
8
10
 
9
11
 
12
+ def correct_mongo_filter(cls_model: Any, filter: Any) -> Any:
13
+ """Correcting filter of request.
14
+
15
+ Corrects `TextField` fields that require localization of translation.
16
+ """
17
+ lang: str = translations.CURRENT_LOCALE
18
+ filter_json: str = json_util.dumps(filter)
19
+ filter_json = (
20
+ cls_model.META["regex_mongo_filter"]
21
+ .sub(rf'\g<field>.{lang}":', filter_json)
22
+ .replace('":.', ".")
23
+ )
24
+ return json_util.loads(filter_json)
25
+
26
+
10
27
  def password_to_none(
11
28
  field_name_and_type: dict[str, str],
12
29
  mongo_doc: dict[str, Any],
@@ -19,8 +36,9 @@ def password_to_none(
19
36
 
20
37
 
21
38
  def mongo_doc_to_raw_doc(
22
- field_name_and_type: dict[str, str],
39
+ inst_model_dict: dict[str, Any],
23
40
  mongo_doc: dict[str, Any],
41
+ lang: str,
24
42
  ) -> dict[str, Any]:
25
43
  """Convert the Mongo document to the raw document.
26
44
 
@@ -31,14 +49,14 @@ def mongo_doc_to_raw_doc(
31
49
  datetime to str
32
50
  """
33
51
  doc: dict[str, Any] = {}
34
- lang: str = translations.CURRENT_LOCALE
35
- for f_name, t_name in field_name_and_type.items():
52
+ for f_name, f_data in inst_model_dict.items():
53
+ field_type = f_data.field_type
36
54
  value = mongo_doc[f_name]
37
55
  if value is not None:
38
- if t_name == "TextField":
39
- value = value.get(lang, "") if value is not None else None
40
- elif "Date" in t_name:
41
- if "Time" in t_name:
56
+ if field_type == "TextField" and f_data.multi_language:
57
+ value = value.get(lang, "- -") if value is not None else None
58
+ elif "Date" in field_type:
59
+ if "Time" in field_type:
42
60
  value = format_datetime(
43
61
  datetime=value,
44
62
  format="short",
@@ -50,9 +68,9 @@ def mongo_doc_to_raw_doc(
50
68
  format="short",
51
69
  locale=lang,
52
70
  )
53
- elif t_name == "IDField":
71
+ elif field_type == "IDField":
54
72
  value = str(value)
55
- elif t_name == "PasswordField":
73
+ elif field_type == "PasswordField":
56
74
  value = None
57
75
  doc[f_name] = value
58
76
  return doc
@@ -7,7 +7,7 @@ from typing import Any
7
7
 
8
8
  from pymongo.asynchronous.collection import AsyncCollection
9
9
 
10
- from ..utils import globals
10
+ from ..utils import globals, translations
11
11
  from ..utils.errors import PanicError
12
12
  from ..utils.unit import Unit
13
13
 
@@ -34,59 +34,52 @@ class UnitMixin:
34
34
  # Check the presence of a Model state.
35
35
  if model_state is None:
36
36
  raise PanicError("Error: Model State - Not found!")
37
- # Get the dynamic field type.
38
- field_type = model_state["field_name_and_type"][unit.field]
39
37
  # Get dynamic field data.
40
- choices: dict[str, float | int | str] | None = model_state["data_dynamic_fields"][
41
- unit.field
42
- ]
43
- # Check whether the type of value is valid for the type of field.
44
- if not (
45
- ("ChoiceFloat" in field_type and isinstance(unit.value, float))
46
- or ("ChoiceInt" in field_type and isinstance(unit.value, int))
47
- or ("ChoiceText" in field_type and isinstance(unit.value, str))
48
- ):
49
- msg = (
50
- "Error: Method: `unit_manager(unit: Unit)` => unit.value - "
51
- + f"The type of value `{type(unit.value)}` "
52
- + f"does not correspond to the type of field `{field_type}`!"
53
- )
54
- raise PanicError(msg)
38
+ choices: list | None = model_state["data_dynamic_fields"][unit.field]
39
+ # Get Title.
40
+ title = unit.title
41
+ title = {lang: title.get(lang, "- -") for lang in translations.LANGUAGES}
42
+ main_lang = translations.DEFAULT_LOCALE
43
+ main_title = title[main_lang]
55
44
  # Add Unit to Model State.
56
45
  if not unit.is_delete:
57
46
  if choices is not None:
58
- choices = {**choices, **{unit.title: unit.value}}
47
+ choices.append({"title": title, "value": unit.value})
59
48
  else:
60
- choices = {unit.title: unit.value}
49
+ choices = [{"title": title, "value": unit.value}]
61
50
  model_state["data_dynamic_fields"][unit.field] = choices
62
- # Delete Unit from Model State.
63
51
  else:
52
+ # Delete Unit from Model State.
64
53
  if choices is None:
65
54
  msg = (
66
55
  "Error: It is not possible to delete Unit."
67
- + f"Unit `{unit.title}: {unit.value}` not exists!"
56
+ + f"Title `{main_title}` not exists!"
68
57
  )
69
58
  raise PanicError(msg)
70
- is_key_exists: bool = unit.title in choices.keys()
59
+ is_key_exists: bool = False
60
+ for item in choices:
61
+ if main_title == item["title"][main_lang]:
62
+ is_key_exists = True
63
+ break
71
64
  if not is_key_exists:
72
65
  msg = (
73
66
  "Error: It is not possible to delete Unit."
74
- + f"Unit `{unit.title}: {unit.value}` not exists!"
67
+ + f"Title `{main_title}` not exists!"
75
68
  )
76
69
  raise PanicError(msg)
77
- del choices[unit.title]
70
+ choices = [item for item in choices if item["title"][main_lang] != main_title]
78
71
  model_state["data_dynamic_fields"][unit.field] = choices or None
79
- # Update the state of the Model in the super collection.
72
+ # Update state of current Model in super collection.
80
73
  await super_collection.replace_one(
81
74
  filter={"collection_name": model_state["collection_name"]},
82
75
  replacement=model_state,
83
76
  )
84
- # Update metadata of the current Model.
77
+ # Update metadata of current Model.
85
78
  cls.META["data_dynamic_fields"][unit.field] = choices or None
86
79
  # Update documents in the collection of the current Model.
87
80
  if unit.is_delete:
88
81
  unit_field: str = unit.field
89
- unit_value: float | int | str = unit.value
82
+ unit_value: float | int | str | None = unit.value
90
83
  collection: AsyncCollection = globals.MONGO_DATABASE[cls.META["collection_name"]]
91
84
  async for mongo_doc in collection.find():
92
85
  field_value = mongo_doc[unit_field]
@@ -23,6 +23,8 @@ class TextField(Field, JsonMixin):
23
23
  readonly: bool = False,
24
24
  unique: bool = False,
25
25
  maxlength: int = 256,
26
+ # Support for several language.
27
+ multi_language: bool = False,
26
28
  ):
27
29
  if globals.DEBUG:
28
30
  if not isinstance(maxlength, int):
@@ -55,6 +57,8 @@ class TextField(Field, JsonMixin):
55
57
  raise AssertionError("Parameter `use_editor` - Not а `bool` type!")
56
58
  if not isinstance(maxlength, int):
57
59
  raise AssertionError("Parameter `maxlength` - Not а `int` type!")
60
+ if not isinstance(multi_language, bool):
61
+ raise AssertionError("Parameter `multi_language` - Not а `int` type!")
58
62
 
59
63
  Field.__init__(
60
64
  self,
@@ -78,6 +82,8 @@ class TextField(Field, JsonMixin):
78
82
  self.textarea = textarea
79
83
  self.use_editor = use_editor
80
84
  self.maxlength = maxlength
85
+ # Support for several language.
86
+ self.multi_language = multi_language
81
87
 
82
88
  def __len__(self) -> int:
83
89
  """Return length of field `value`."""
@@ -1,16 +1,17 @@
1
1
  """Decorator for converting Python classes into Ramifice models."""
2
2
 
3
3
  import os
4
+ import re
4
5
  from typing import Any
5
6
 
6
7
  from ..commons import QCommonsMixin
7
8
  from ..fields import DateTimeField, IDField
8
9
  from ..paladins import CheckMixin, QPaladinsMixin, ValidationMixin
9
10
  from ..utils.errors import DoesNotMatchRegexError, PanicError
11
+ from ..utils.globals import REGEX
10
12
  from ..utils.mixins.add_valid import AddValidMixin
11
13
  from ..utils.mixins.hooks import HooksMixin
12
14
  from ..utils.mixins.indexing import IndexMixin
13
- from ..utils.globals import REGEX
14
15
  from .model import Model
15
16
  from .pseudo import PseudoModel
16
17
 
@@ -103,7 +104,7 @@ def model(
103
104
  def caching(cls: Any, service_name: str, is_migrate_model: bool) -> dict[str, Any]:
104
105
  """Add additional metadata to `Model.META`."""
105
106
  metadata: dict[str, Any] = {}
106
- model_name = cls.__name__
107
+ model_name: str = cls.__name__
107
108
  if REGEX["model_name"].match(model_name) is None:
108
109
  raise DoesNotMatchRegexError("^[A-Z][a-zA-Z0-9]{0,24}$")
109
110
  #
@@ -118,9 +119,12 @@ def caching(cls: Any, service_name: str, is_migrate_model: bool) -> dict[str, An
118
119
  # Build data migration storage for dynamic fields.
119
120
  data_dynamic_fields: dict[str, dict[str, str | int | float] | None] = {}
120
121
  # Count all fields.
121
- count_all_fields = 0
122
+ count_all_fields: int = 0
122
123
  # Count fields for migrating.
123
- count_fields_no_ignored = 0
124
+ count_fields_no_ignored: int = 0
125
+ # List of fields that support localization of translates.
126
+ # Hint: `TextField`
127
+ supported_lang_fields: list[str] = []
124
128
 
125
129
  raw_model = cls()
126
130
  raw_model.fields()
@@ -130,9 +134,9 @@ def caching(cls: Any, service_name: str, is_migrate_model: bool) -> dict[str, An
130
134
  default_fields["created_at"] = DateTimeField()
131
135
  default_fields["updated_at"] = DateTimeField()
132
136
  fields = {**raw_model.__dict__, **default_fields}
133
- for f_name, f_type in fields.items():
134
- if not callable(f_type):
135
- f_type_str = f_type.__class__.__name__
137
+ for f_name, f_data in fields.items():
138
+ if not callable(f_data):
139
+ f_type_str = f_data.__class__.__name__
136
140
  # Count all fields.
137
141
  count_all_fields += 1
138
142
  # Get attributes value for fields of Model: id, name.
@@ -141,14 +145,14 @@ def caching(cls: Any, service_name: str, is_migrate_model: bool) -> dict[str, An
141
145
  "name": f_name,
142
146
  }
143
147
  #
144
- if not f_type.ignored:
148
+ if not f_data.ignored:
145
149
  # Count fields for migrating.
146
150
  if is_migrate_model:
147
151
  count_fields_no_ignored += 1
148
152
  # Get a dictionary of field names and types.
149
153
  field_name_and_type[f_name] = f_type_str
150
154
  # Build data migration storage for dynamic fields.
151
- if "Dyn" in f_type.field_type:
155
+ if "Dyn" in f_data.field_type:
152
156
  if not is_migrate_model:
153
157
  msg = (
154
158
  f"Model: `{cls.__module__}.{model_name}` > "
@@ -157,11 +161,16 @@ def caching(cls: Any, service_name: str, is_migrate_model: bool) -> dict[str, An
157
161
  )
158
162
  raise PanicError(msg)
159
163
  data_dynamic_fields[f_name] = None
164
+ if f_data.field_type == "TextField" and f_data.multi_language:
165
+ supported_lang_fields.append(f_name)
160
166
 
161
167
  metadata["field_name_and_type"] = field_name_and_type
162
168
  metadata["field_attrs"] = field_attrs
163
169
  metadata["data_dynamic_fields"] = data_dynamic_fields
164
170
  metadata["count_all_fields"] = count_all_fields
165
171
  metadata["count_fields_no_ignored"] = count_fields_no_ignored
172
+ metadata["regex_mongo_filter"] = re.compile(
173
+ rf'(?P<field>"(?:{"|".join(supported_lang_fields)})":)'
174
+ )
166
175
 
167
176
  return metadata
ramifice/models/model.py CHANGED
@@ -68,6 +68,7 @@ class Model(metaclass=ABCMeta):
68
68
  """Injecting metadata from Model.META in params of fields."""
69
69
  metadata = self.__class__.META
70
70
  if bool(metadata):
71
+ lang = translations.CURRENT_LOCALE
71
72
  field_attrs = metadata["field_attrs"]
72
73
  data_dynamic_fields = metadata["data_dynamic_fields"]
73
74
  for f_name, f_type in self.__dict__.items():
@@ -75,7 +76,13 @@ class Model(metaclass=ABCMeta):
75
76
  f_type.id = field_attrs[f_name]["id"]
76
77
  f_type.name = field_attrs[f_name]["name"]
77
78
  if "Dyn" in f_type.field_type:
78
- f_type.choices = data_dynamic_fields[f_name]
79
+ dyn_data = data_dynamic_fields[f_name]
80
+ if dyn_data is not None:
81
+ f_type.choices = {
82
+ item["title"].get(lang, "- -"): item["value"] for item in dyn_data
83
+ }
84
+ else:
85
+ f_type.choices = None
79
86
 
80
87
  # Complect of methods for converting Model to JSON and back.
81
88
  # --------------------------------------------------------------------------
@@ -27,11 +27,11 @@ class TextGroupMixin:
27
27
  field = params["field_data"]
28
28
  field_name = field.name
29
29
  field_type: str = field.field_type
30
- is_text_field: bool = "TextField" == field_type
30
+ is_multi_language: bool = (field_type == "TextField") and field.multi_language
31
31
  # Get current value.
32
32
  value = field.value or field.__dict__.get("default")
33
33
 
34
- if is_text_field:
34
+ if is_multi_language:
35
35
  if not isinstance(value, (str, dict, type(None))):
36
36
  panic_type_error("str | dict | None", params)
37
37
  else:
@@ -55,7 +55,7 @@ class TextGroupMixin:
55
55
  value,
56
56
  params,
57
57
  field_name,
58
- is_text_field,
58
+ is_multi_language,
59
59
  ):
60
60
  err_msg = translations._("Is not unique !")
61
61
  accumulate_error(err_msg, params)
@@ -85,19 +85,19 @@ class TextGroupMixin:
85
85
  accumulate_error(err_msg, params)
86
86
  # Insert result.
87
87
  if params["is_save"]:
88
- if is_text_field:
88
+ if is_multi_language:
89
89
  mult_lang_text: dict[str, str] = (
90
90
  params["curr_doc"][field_name]
91
91
  if params["is_update"]
92
92
  else (
93
93
  {lang: value for lang in translations.LANGUAGES}
94
94
  if isinstance(value, str)
95
- else {lang: value.get(lang, "") for lang in translations.LANGUAGES}
95
+ else {lang: value.get(lang, "- -") for lang in translations.LANGUAGES}
96
96
  )
97
97
  )
98
98
  if isinstance(value, dict):
99
99
  for lang in translations.LANGUAGES:
100
- mult_lang_text[lang] = value.get(lang, "")
100
+ mult_lang_text[lang] = value.get(lang, "- -")
101
101
  else:
102
102
  mult_lang_text[translations.CURRENT_LOCALE] = value
103
103
  value = mult_lang_text
@@ -15,10 +15,11 @@ def ignored_fields_to_none(inst_model: Any) -> None:
15
15
  def refresh_from_mongo_doc(inst_model: Any, mongo_doc: dict[str, Any]) -> None:
16
16
  """Update object instance from Mongo document."""
17
17
  lang: str = translations.CURRENT_LOCALE
18
+ model_dict = inst_model.__dict__
18
19
  for name, data in mongo_doc.items():
19
- field = inst_model.__dict__[name]
20
- if field.field_type == "TextField":
21
- field.value = data.get(lang, "") if data is not None else None
20
+ field = model_dict[name]
21
+ if field.field_type == "TextField" and field.multi_language:
22
+ field.value = data.get(lang, "- -") if data is not None else None
22
23
  elif field.group == "pass":
23
24
  field.value = None
24
25
  else:
@@ -54,13 +55,13 @@ async def check_uniqueness(
54
55
  value: str | int | float,
55
56
  params: dict[str, Any],
56
57
  field_name: str | None = None,
57
- is_text_field: bool = False,
58
+ is_multi_language: bool = False,
58
59
  ) -> bool:
59
60
  """Check the uniqueness of the value in the collection."""
60
61
  if not params["is_migrate_model"]:
61
62
  return True
62
63
  q_filter = None
63
- if is_text_field:
64
+ if is_multi_language:
64
65
  lang_filter = [{f"{field_name}.{lang}": value} for lang in translations.LANGUAGES]
65
66
  q_filter = {
66
67
  "$and": [
ramifice/utils/unit.py CHANGED
@@ -1,5 +1,7 @@
1
1
  """Unit - Data management in dynamic fields."""
2
2
 
3
+ from typing import Any
4
+
3
5
  from .errors import PanicError
4
6
  from .mixins.json_converter import JsonMixin
5
7
 
@@ -17,19 +19,22 @@ class Unit(JsonMixin):
17
19
  def __init__( # noqa: D107
18
20
  self,
19
21
  field: str,
20
- title: str,
21
- value: float | int | str,
22
+ title: dict[str, str], # Example: {"en": "Title", "ru": "Заголовок"}
23
+ value: float | int | str | None = None, # None for is_delete=True
22
24
  is_delete: bool = False,
23
25
  ):
24
26
  # Check the match of types.
25
27
  if not isinstance(field, str):
26
28
  msg = "Class: `Unit` > Field: `field` => Not а `str` type!"
27
29
  raise PanicError(msg)
28
- if not isinstance(title, str):
29
- msg = "Class: `Unit` > Field: `title` => Not а `str` type!"
30
+ if not isinstance(title, dict):
31
+ msg = (
32
+ "Class: `Unit` > Field: `title` => Not а `str` type! "
33
+ + 'Example: {"en": "Title", "ru": "Заголовок"}'
34
+ )
30
35
  raise PanicError(msg)
31
- if not isinstance(value, (float, int, str)):
32
- msg = "Class: `Unit` > Field: `value` => Not а `float | int | str` type!"
36
+ if not isinstance(value, (float, int, str, type(None))):
37
+ msg = "Class: `Unit` > Field: `value` => Not а `float | int | str | None` type!"
33
38
  raise PanicError(msg)
34
39
  if not isinstance(is_delete, bool):
35
40
  msg = "Class: `Unit` > Field: `is_delete` => Not а `bool` type!"
@@ -42,10 +47,10 @@ class Unit(JsonMixin):
42
47
  self.value = value
43
48
  self.is_delete = is_delete
44
49
 
45
- self.check_empty_arguments()
50
+ self.check_value_arguments()
46
51
 
47
- def check_empty_arguments(self) -> None:
48
- """Error: If any of the arguments in the Unit is empty.
52
+ def check_value_arguments(self) -> None:
53
+ """Check if the values correspond to the arguments.
49
54
 
50
55
  Returns:
51
56
  `None` or raised exception `PanicError`.
@@ -56,6 +61,14 @@ class Unit(JsonMixin):
56
61
  field_name = "field"
57
62
  elif len(self.title) == 0:
58
63
  field_name = "title"
64
+ elif self.value is None and self.is_delete == False:
65
+ msg = (
66
+ "Method: `unit_manager` > "
67
+ + "Argument: `unit` > "
68
+ + f"Field: `{field_name}` => "
69
+ + "For `value` = None, `is_delete` should be True!"
70
+ )
71
+ raise PanicError(msg)
59
72
  elif isinstance(self.value, str) and len(self.value) == 0:
60
73
  field_name = "value"
61
74
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ramifice
3
- Version: 0.4.11
3
+ Version: 0.5.0
4
4
  Summary: ORM-like API MongoDB for Python language.
5
5
  Project-URL: Homepage, https://github.com/kebasyaty/ramifice
6
6
  Project-URL: Documentation, https://kebasyaty.github.io/ramifice/
@@ -177,7 +177,10 @@ class User:
177
177
  required=True,
178
178
  unique=True,
179
179
  )
180
- self.first_name = TextField(label=gettext("First name"), required=True)
180
+ self.first_name = TextField(
181
+ label=gettext("First name"),
182
+ required=True,
183
+ )
181
184
  self.last_name = TextField(
182
185
  label=gettext("Last name"),
183
186
  required=True,
@@ -187,8 +190,17 @@ class User:
187
190
  required=True,
188
191
  unique=True,
189
192
  )
190
- self.birthday = DateField(label=gettext("Birthday"))
191
- self.password = PasswordField(label=gettext("Password"))
193
+ self.birthday = DateField(
194
+ label=gettext("Birthday"),
195
+ )
196
+ self.description = TextField(
197
+ label=gettext("About yourself"),
198
+ # Support for several language.
199
+ multi_language=True,
200
+ )
201
+ self.password = PasswordField(
202
+ label=gettext("Password"),
203
+ )
192
204
  self.сonfirm_password = PasswordField(
193
205
  label=gettext("Confirm password"),
194
206
  # If true, the value of this field is not saved in the database.
@@ -251,8 +263,7 @@ async def main():
251
263
  print("User details:")
252
264
  user_details = await User.find_one_to_raw_doc(
253
265
  # {"_id": user.id.value}
254
- # For `TextField`.
255
- {f"username.{translations.CURRENT_LOCALE}": user.username.value}
266
+ {f"username": user.username.value}
256
267
  )
257
268
  if user_details is not None:
258
269
  pprint.pprint(user_details)
@@ -1,12 +1,12 @@
1
1
  ramifice/__init__.py,sha256=IuI20h84QWyFftykvte_HLft2iVxwpu0K592NypMwfo,759
2
2
  ramifice/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  ramifice/commons/__init__.py,sha256=FTG78sTU0ay0rvmrq5fUmVjL-I-S7wgQq3BLcyqnjrQ,433
4
- ramifice/commons/general.py,sha256=yi52I9t8R1XePZdyrPmhXJxLc_YkY72Ea7_dBvsKfXI,4984
4
+ ramifice/commons/general.py,sha256=SmtJge18Yjz7cpc-KXo238rQl465WtTqzNnuRjHl8qs,5361
5
5
  ramifice/commons/indexes.py,sha256=hAcWKZ9MMgLbRtoQ6Af0b8r-PY0dbEYmMBl8z_d1DRo,3646
6
- ramifice/commons/many.py,sha256=KGt7licVJmLADvyPtH7-GbXHig-EnGFbnk7QKu3mfvQ,8214
7
- ramifice/commons/one.py,sha256=MwUlU9getNtwTOL6-lROdzViBsChWgMIMppCWoeAnFU,5815
8
- ramifice/commons/tools.py,sha256=s4CdHb9h8aqR371py0KVsBnNBJSW8-7AVjbTmuFAeeg,1793
9
- ramifice/commons/unit_manager.py,sha256=IkWqXu1PHHal0aGfx6zme81iXPeygxPqEWO-u_8RXoI,4356
6
+ ramifice/commons/many.py,sha256=jkah1kO1CXWPB0xW0dbMelCFBSirRYkX9a333EcPE_M,9202
7
+ ramifice/commons/one.py,sha256=uZsgwg_GgA1VMZDPqhRFMmCdtvcv6xsL4DWbCOwGV3w,6730
8
+ ramifice/commons/tools.py,sha256=ectaILVYCSwNbB0VAys6nIhdsJbwuxAupNvcQfweC88,2366
9
+ ramifice/commons/unit_manager.py,sha256=o8Ux2y95nddo762hf1YDyp07QwvQyotR5iVoG5FZaZI,4001
10
10
  ramifice/fields/__init__.py,sha256=yRfX7Tvpuh27Ggcx5u9e1RRYK7Wu59EVJYxxetmuP1w,1290
11
11
  ramifice/fields/bool_field.py,sha256=WWubSwsFJZu8b3MviDd2zXnNYOQaYw8toklFNSTVFLA,2072
12
12
  ramifice/fields/choice_float_dyn_field.py,sha256=Yrd9cNKLm0mhU-P3IojwFRLSbuxdp7f2NMGJhrRWsf8,3093
@@ -34,7 +34,7 @@ ramifice/fields/ip_field.py,sha256=tsxdwKd-MhkhnE1F2FmFg67jwh9Q-RvXECv1aG8PitE,3
34
34
  ramifice/fields/password_field.py,sha256=m3n_qJRRHCtgyBC7nDB0dYBO_82Lp1Ws48d1_jxHA8o,3216
35
35
  ramifice/fields/phone_field.py,sha256=5qZ_rR82M9cuW7Dv-D5d9jDaw7DJp9y60lRB4GEmi4w,3615
36
36
  ramifice/fields/slug_field.py,sha256=9rH1VdxYPoK60QCKHRB91bA3ZmIeIuFmf6WttzB1yTI,2685
37
- ramifice/fields/text_field.py,sha256=OrZJ337C9YUT7LOzIZR5xRBxL7dSb7ERddiJWQaT3zI,3797
37
+ ramifice/fields/text_field.py,sha256=pa5J1nTBC56JDU3mDyjy1Qofv8FLFUgACkNcZB6cUGQ,4107
38
38
  ramifice/fields/url_field.py,sha256=DTy4B22juav9dzIrqKy3NzM2vuICHvDzIDbzkkyF1C0,3993
39
39
  ramifice/fields/general/__init__.py,sha256=5OE0TwPQjvpB3lBPuEwKrhjR_1ehOWxB98OJP_n32MA,20
40
40
  ramifice/fields/general/choice_group.py,sha256=TBJblwH8mB71wd1z2jcSs28H-zx3JZVBfkk4YCE1-pI,916
@@ -44,8 +44,8 @@ ramifice/fields/general/file_group.py,sha256=n45KfPzFI_l5hXoKkPDG0Q-0mdC2obExV-3
44
44
  ramifice/fields/general/number_group.py,sha256=AqlCY-t6JHZ2QVBe7mk5nPt6z8M4VJ_RARRlSBoIxms,715
45
45
  ramifice/fields/general/text_group.py,sha256=6GD2Fe6Toz6zZjAAlcy5rPVCAGh6Yn1ltdIrFg9RF18,1057
46
46
  ramifice/models/__init__.py,sha256=h_QQ5rSJNZ-7kmx7wIFd8E8RmUS94b_x4jdwMbq8zm4,15
47
- ramifice/models/decorator.py,sha256=jOEn-0v0m_tM7mow45-tM-8LADUmwlG2DWZsEuC9S_w,6834
48
- ramifice/models/model.py,sha256=ZvgIbcuSXuAkBl_FofOZXGwWMwlqKB-PwTMyXiqK-Fo,6610
47
+ ramifice/models/decorator.py,sha256=U1ukWWq2voEwvKonQAzihqGYVsoyPrOQ2jDXJZ-UcMc,7251
48
+ ramifice/models/model.py,sha256=3426PGlJpb9ojc7gP80fd0cz5zsenvCyfgDaXAVk6FM,6963
49
49
  ramifice/models/pseudo.py,sha256=PhLQM4zXdaOtTSmNFzwN4twlUmdvA1D_-YOMJtaOIwM,6754
50
50
  ramifice/paladins/__init__.py,sha256=PIP3AXI2KBRXNcLJUF0d7ygJ7VLOAxlhb4HRKQ9MGYY,516
51
51
  ramifice/paladins/check.py,sha256=ennDiVAoCZIS3aKBK0eA-Vt8VJQnbIv90be5IFYraFg,7026
@@ -53,7 +53,7 @@ ramifice/paladins/delete.py,sha256=tw50E98D5eFZ7gHGnh_8ztUB1LeTeWWKZvIcQqlgbF8,3
53
53
  ramifice/paladins/password.py,sha256=w1XWh3bsncH1VTVjCLxyKI2waxMvltwcsPWW3V9Ib84,3027
54
54
  ramifice/paladins/refrash.py,sha256=fw-9x_NKGzreipBt_F9KF6FTsYm9hSzfq4ubi1FHYrQ,1065
55
55
  ramifice/paladins/save.py,sha256=EG0_v-imCQPax2pJIAXPoQcSL99g8abY2EeGJMvXBW4,3864
56
- ramifice/paladins/tools.py,sha256=QxxwZILyBLcAiDUN48KE99c_cHVjG-VYilEsIinRhEU,2641
56
+ ramifice/paladins/tools.py,sha256=Xm1RhkV1GX5B6GbyGmJMBzFyDQcenBxcRjlUamIz5pA,2706
57
57
  ramifice/paladins/validation.py,sha256=gcEJXIEPu1g7Z54vl14YTs5rCmxOEYsgQH1usFfyy7k,1730
58
58
  ramifice/paladins/groups/__init__.py,sha256=hpqmWLsYAMvZHAbmMXluQSqLhkHOSTUAgLHyTM1LTYI,472
59
59
  ramifice/paladins/groups/bool_group.py,sha256=oJc9mw9KGrnK_Pj7uXixYYQAJphcXLr_xSQv3PMUlcU,792
@@ -65,7 +65,7 @@ ramifice/paladins/groups/img_group.py,sha256=RoD_QnW0F0DAzrGQmRi8jMZnWy2IomlFn6A
65
65
  ramifice/paladins/groups/num_group.py,sha256=6jT7nfIiVoQTlI2UdkcQOlHIDYjllFqSH7Nb87uJgzg,2274
66
66
  ramifice/paladins/groups/pass_group.py,sha256=YU5a-NwohEutoEx2N5JmGfg8uPiYiW0XJ8XYsOih6eA,1859
67
67
  ramifice/paladins/groups/slug_group.py,sha256=joBB5litljbv2h5JKEMzF71s_DKMWH6nzgThLiLZx2E,2307
68
- ramifice/paladins/groups/text_group.py,sha256=rUWNaAPkTMMlRf6A-6nQAa4HCj6NBTjPKIYaEKSOUL4,4305
68
+ ramifice/paladins/groups/text_group.py,sha256=EiHGuiJKm-A4UrsV4DHoI7whL-LdQCIEvW6jX4YjfCo,4354
69
69
  ramifice/utils/__init__.py,sha256=xixHoOX4ja5jIUZemem1qn4k4aonv3G3Q76azQK_pkU,43
70
70
  ramifice/utils/errors.py,sha256=iuhq7fzpUmsOyeXeg2fJjta8yAuqlXLKsZVMpfUhtHE,1901
71
71
  ramifice/utils/fixtures.py,sha256=qsv9cg06lc82XaRlhI1j5vftmOQTTwOcDXCg_lnpK04,3159
@@ -73,13 +73,13 @@ ramifice/utils/globals.py,sha256=uR20um3Qg_1SG1t7WyWbpq8kQD-9Mslyr_c1yh5Hw9w,175
73
73
  ramifice/utils/migration.py,sha256=ol8ysNGBr0wuOwo00ZbbN0U8YKiiYVgyRYja0axdiHM,11059
74
74
  ramifice/utils/tools.py,sha256=sOKzwnvf6vdTNf9r6PKAdw6aB4undat2Z8tzS3M1GnQ,2733
75
75
  ramifice/utils/translations.py,sha256=GNGE0ULAA0aOY5pTxUd3MQW-nVaKvp6BeXWEcsR0s0o,4048
76
- ramifice/utils/unit.py,sha256=qJ2SpClsFcMRcwB_ZA-QlrB5T9OinCBiWx5KqQ9vH_A,2266
76
+ ramifice/utils/unit.py,sha256=cFnhXt8sO0X-wiuiSU7_YzmRkpdNFcf2qxfR8YPsuiE,2861
77
77
  ramifice/utils/mixins/__init__.py,sha256=-UxYokZSlEaqoIs0aPVGtzK2CgN88mTzyhryHEtt0Yo,181
78
78
  ramifice/utils/mixins/add_valid.py,sha256=TLOObedzXNA9eCylfAVbVCqIKE5sV-P5AdIN7avj-GA,407
79
79
  ramifice/utils/mixins/hooks.py,sha256=33jvJRhfnJeL2Hd_YFXk3M_7wjqHaByU2wRjKyboL6s,914
80
80
  ramifice/utils/mixins/indexing.py,sha256=Z0427HoaVRyNmSNN8Fx0mSICgAKV-gDdu3iR5qYUEbs,329
81
81
  ramifice/utils/mixins/json_converter.py,sha256=WhigXyDAV-FfILaZuwvRFRIk0D90Rv3dG5t-mv5fVyc,1107
82
- ramifice-0.4.11.dist-info/METADATA,sha256=wgz9BoK-X9H0JftAf608T2qd8nBKBnoC1Q4mlb8rygs,21785
83
- ramifice-0.4.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
84
- ramifice-0.4.11.dist-info/licenses/LICENSE,sha256=LrEL0aTZx90HDwFUQCJutORiDjJL9AnuVvCtspXIqt4,1095
85
- ramifice-0.4.11.dist-info/RECORD,,
82
+ ramifice-0.5.0.dist-info/METADATA,sha256=up068qtq-ezkO2-Y5M1-oiZoHgcA9OPdc-FXFGV5jls,21964
83
+ ramifice-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
84
+ ramifice-0.5.0.dist-info/licenses/LICENSE,sha256=LrEL0aTZx90HDwFUQCJutORiDjJL9AnuVvCtspXIqt4,1095
85
+ ramifice-0.5.0.dist-info/RECORD,,