Coverage for jutil/model.py: 70%
85 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-07 16:40 -0500
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-07 16:40 -0500
1from datetime import timedelta, datetime
2from django.db.models.fields import Field
3from django.utils.encoding import force_str
4from time import sleep
5from typing import Type, List, Tuple, Any, Optional, Sequence
6from django.db.models import Model
7from django.utils.timezone import now
8from jutil.format import choices_label
11def get_object_or_none(cls: Any, **kwargs) -> Any:
12 """
13 Returns model instance or None if not found.
14 :param cls: Class or queryset
15 :param kwargs: Filters for get() call
16 :return: Object or None
17 """
18 try:
19 qs = cls._default_manager.all() if hasattr(cls, "_default_manager") else cls # pylint: disable=protected-access
20 return qs.get(**kwargs)
21 except Exception:
22 return None
25def wait_object_or_none(cls: Any, timeout: float = 5.0, sleep_interval: float = 1.0, **kwargs) -> Any:
26 """
27 Returns model instance or None if not found after specified timeout.
28 Waits timeout before returning if no object available.
29 Waiting is done by sleeping specified intervals.
30 :param cls: Class or queryset
31 :param timeout: Timeout in seconds
32 :param sleep_interval: Sleep interval in seconds
33 :param kwargs: Filters for get() call
34 :return: Object or None
35 """
36 t0: Optional[datetime] = None
37 t1: Optional[datetime] = None
38 qs0 = cls._default_manager if hasattr(cls, "_default_manager") else cls # pylint: disable=protected-access
39 while t0 is None or t0 < t1: # type: ignore
40 obj = qs0.all().filter(**kwargs).first()
41 if obj is not None:
42 return obj
43 t0 = now()
44 if t1 is None:
45 t1 = t0 + timedelta(seconds=timeout)
46 sleep(sleep_interval)
47 return qs0.all().filter(**kwargs).first()
50def get_model_field_or_none(instance, field_name: str) -> Optional[Field]:
51 """
52 Returns model field.
53 :param instance: Model instance
54 :param field_name: Model attribute name
55 :return: Optional[Field]
56 """
57 for f in instance._meta.fields:
58 if f.attname == field_name:
59 return f
60 return None
63def get_model_field(instance, field_name: str) -> Field:
64 """
65 Returns model field.
66 :param instance: Model instance
67 :param field_name: Model attribute name
68 :return: Field
69 """
70 f = get_model_field_or_none(instance, field_name)
71 if f is None:
72 raise Exception(f"Field {field_name} not found from {instance}")
73 return f
76def get_model_field_label(instance, field_name: str) -> str:
77 """
78 Returns model field label.
79 Resolves also function / property short_description meta fields.
80 :param instance: Model instance
81 :param field_name: Model attribute name
82 :return: label str
83 """
84 for f in instance._meta.fields: 84 ↛ 87line 84 didn't jump to line 87, because the loop on line 84 didn't complete
85 if field_name in (f.name, f.attname):
86 return f.verbose_name
87 try:
88 return getattr(getattr(getattr(instance.__class__, field_name), "fget"), "short_description")
89 except AttributeError:
90 pass
91 try:
92 return getattr(getattr(instance.__class__, field_name), "short_description")
93 except AttributeError:
94 pass
95 return field_name
98def get_model_field_label_and_value(instance, field_name: str) -> Tuple[str, str]:
99 """
100 Returns model field label and value as str.
101 :param instance: Model instance
102 :param field_name: Model attribute name
103 :return: (label, value) tuple
104 """
105 label = get_model_field_label(instance, field_name)
106 value = str(getattr(instance, field_name)) if hasattr(instance, field_name) else None
107 for f in instance._meta.fields: 107 ↛ 112line 107 didn't jump to line 112, because the loop on line 107 didn't complete
108 if field_name in (f.attname, f.name):
109 if hasattr(f, "choices") and f.choices: 109 ↛ 110line 109 didn't jump to line 110, because the condition on line 109 was never true
110 value = choices_label(f.choices, value)
111 break
112 if value is None: 112 ↛ 113line 112 didn't jump to line 113, because the condition on line 112 was never true
113 value = ""
114 val = force_str(value)
115 return label, val
118def is_model_field_changed(instance, field_name: str) -> bool:
119 """
120 Compares model instance field value to value stored in DB.
121 If object has not been stored in DB (yet) field is considered unchanged.
122 :param instance: Model instance
123 :param field_name: Model attribute name
124 :return: True if field value has been changed compared to value stored in DB.
125 """
126 assert hasattr(instance, field_name)
127 if not hasattr(instance, "pk") or instance.pk is None: 127 ↛ 128line 127 didn't jump to line 128, because the condition on line 127 was never true
128 return False
129 qs = instance.__class__.objects.all()
130 params = {"pk": instance.pk, field_name: getattr(instance, field_name)}
131 return not qs.filter(**params).exists()
134def get_model_field_names(instance, cls: Optional[Type[Model]] = None, exclude_fields: Sequence[str] = ("id",), base_class_suffix: str = "_ptr") -> List[str]:
135 if cls is None: 135 ↛ 136line 135 didn't jump to line 136, because the condition on line 135 was never true
136 cls = instance.__class__
137 return [f.name for f in cls._meta.fields if f.name not in exclude_fields and not f.name.endswith(base_class_suffix)]
140def clone_model(
141 instance, cls: Optional[Type[Model]] = None, commit: bool = True, exclude_fields: Sequence[str] = ("id",), base_class_suffix: str = "_ptr", **kw
142):
143 """
144 Assigns model fields to new object. Ignores exclude_fields list and
145 attributes ending with pointer suffix (default '_ptr')
146 :param instance: Instance to copy
147 :param cls: Class (opt)
148 :param commit: Save or not
149 :param exclude_fields: List of fields to exclude
150 :param base_class_suffix: End of name for base class pointers, e.g. model Car(Vehicle) has implicit vehicle_ptr
151 :return: New instance
152 """
153 if cls is None: 153 ↛ 155line 153 didn't jump to line 155, because the condition on line 153 was never false
154 cls = instance.__class__
155 keys = get_model_field_names(instance, cls=cls, exclude_fields=exclude_fields, base_class_suffix=base_class_suffix)
156 new_instance = cls()
157 for k in keys:
158 setattr(new_instance, k, getattr(instance, k))
159 for k, v in kw.items(): 159 ↛ 160line 159 didn't jump to line 160, because the loop on line 159 never started
160 setattr(new_instance, k, v)
161 if commit: 161 ↛ 163line 161 didn't jump to line 163, because the condition on line 161 was never false
162 new_instance.save()
163 return new_instance