Coverage for jutil/model.py: 70%

85 statements  

« 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 

9 

10 

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 

23 

24 

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() 

48 

49 

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 

61 

62 

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 

74 

75 

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 

96 

97 

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 

116 

117 

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() 

132 

133 

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)] 

138 

139 

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