Coverage for jutil/model.py : 86%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from datetime import timedelta, datetime
2from time import sleep
3from typing import Type, List, Tuple, Any, Optional
4from django.db.models import Model
5from django.utils.encoding import force_text
6from django.utils.timezone import now
7from jutil.format import choices_label
10def get_object_or_none(cls: Any, **kwargs) -> Any:
11 """
12 Returns model instance or None if not found.
13 :param cls: Class or queryset
14 :param kwargs: Filters for get() call
15 :return: Object or None
16 """
17 try:
18 qs = cls._default_manager.all() if hasattr(cls, "_default_manager") else cls # pylint: disable=protected-access
19 return qs.get(**kwargs)
20 except Exception:
21 return None
24def wait_object_or_none(cls: Any, timeout: float = 5.0, sleep_interval: float = 1.0, **kwargs) -> Any:
25 """
26 Returns model instance or None if not found after specified timeout.
27 Waits timeout before returning if no object available.
28 Waiting is done by sleeping specified intervals.
29 :param cls: Class or queryset
30 :param timeout: Timeout in seconds
31 :param sleep_interval: Sleep interval in seconds
32 :param kwargs: Filters for get() call
33 :return: Object or None
34 """
35 t0: Optional[datetime] = None
36 t1: Optional[datetime] = None
37 qs0 = cls._default_manager if hasattr(cls, "_default_manager") else cls # pylint: disable=protected-access
38 while t0 is None or t0 < t1: # type: ignore
39 obj = qs0.all().filter(**kwargs).first()
40 if obj is not None:
41 return obj
42 t0 = now()
43 if t1 is None:
44 t1 = t0 + timedelta(seconds=timeout)
45 sleep(sleep_interval)
46 return qs0.all().filter(**kwargs).first()
49def get_model_field_label_and_value(instance, field_name: str) -> Tuple[str, str]:
50 """
51 Returns model field label and value (as text).
52 :param instance: Model instance
53 :param field_name: Model attribute name
54 :return: (label, value) tuple
55 """
56 label = field_name
57 value = str(getattr(instance, field_name)) if hasattr(instance, field_name) else None
58 for f in instance._meta.fields: 58 ↛ 64line 58 didn't jump to line 64, because the loop on line 58 didn't complete
59 if f.attname == field_name:
60 label = f.verbose_name
61 if hasattr(f, "choices") and f.choices: 61 ↛ 62line 61 didn't jump to line 62, because the condition on line 61 was never true
62 value = choices_label(f.choices, value)
63 break
64 if value is None: 64 ↛ 65line 64 didn't jump to line 65, because the condition on line 64 was never true
65 value = ""
66 val = force_text(value)
67 return label, val
70def is_model_field_changed(instance, field_name: str) -> bool:
71 """
72 Compares model instance field value to value stored in DB.
73 If object has not been stored in DB (yet) field is considered unchanged.
74 :param instance: Model instance
75 :param field_name: Model attribute name
76 :return: True if field value has been changed compared to value stored in DB.
77 """
78 assert hasattr(instance, field_name)
79 if not hasattr(instance, "pk") or instance.pk is None: 79 ↛ 80line 79 didn't jump to line 80, because the condition on line 79 was never true
80 return False
81 qs = instance.__class__.objects.all()
82 params = {"pk": instance.pk, field_name: getattr(instance, field_name)}
83 return not qs.filter(**params).exists()
86def get_model_keys(
87 instance, cls: Optional[Type[Model]] = None, exclude_fields: tuple = ("id",), base_class_suffix: str = "_ptr"
88) -> List[str]:
89 if cls is None: 89 ↛ 90line 89 didn't jump to line 90, because the condition on line 89 was never true
90 cls = instance.__class__
91 return [f.name for f in cls._meta.fields if f.name not in exclude_fields and not f.name.endswith(base_class_suffix)]
94def clone_model(
95 instance,
96 cls: Optional[Type[Model]] = None,
97 commit: bool = True,
98 exclude_fields: tuple = ("id",),
99 base_class_suffix: str = "_ptr",
100 **kw
101):
102 """
103 Assigns model fields to new object. Ignores exclude_fields list and
104 attributes ending with pointer suffix (default '_ptr')
105 :param instance: Instance to copy
106 :param cls: Class (opt)
107 :param commit: Save or not
108 :param exclude_fields: List of fields to exclude
109 :param base_class_suffix: End of name for base class pointers, e.g. model Car(Vehicle) has implicit vehicle_ptr
110 :return: New instance
111 """
112 if cls is None: 112 ↛ 114line 112 didn't jump to line 114, because the condition on line 112 was never false
113 cls = instance.__class__
114 keys = get_model_keys(instance, cls=cls, exclude_fields=exclude_fields, base_class_suffix=base_class_suffix)
115 new_instance = cls()
116 for k in keys:
117 setattr(new_instance, k, getattr(instance, k))
118 for k, v in kw.items(): 118 ↛ 119line 118 didn't jump to line 119, because the loop on line 118 never started
119 setattr(new_instance, k, v)
120 if commit: 120 ↛ 122line 120 didn't jump to line 122, because the condition on line 120 was never false
121 new_instance.save()
122 return new_instance