1import datetime
2import re
3from collections import namedtuple
4
5from plain.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT
6
7FieldReference = namedtuple("FieldReference", "to through")
8
9COMPILED_REGEX_TYPE = type(re.compile(""))
10
11
12class RegexObject:
13 def __init__(self, obj):
14 self.pattern = obj.pattern
15 self.flags = obj.flags
16
17 def __eq__(self, other):
18 if not isinstance(other, RegexObject):
19 return NotImplemented
20 return self.pattern == other.pattern and self.flags == other.flags
21
22
23def get_migration_name_timestamp():
24 return datetime.datetime.now().strftime("%Y%m%d_%H%M")
25
26
27def resolve_relation(model, package_label=None, model_name=None):
28 """
29 Turn a model class or model reference string and return a model tuple.
30
31 package_label and model_name are used to resolve the scope of recursive and
32 unscoped model relationship.
33 """
34 if isinstance(model, str):
35 if model == RECURSIVE_RELATIONSHIP_CONSTANT:
36 if package_label is None or model_name is None:
37 raise TypeError(
38 "package_label and model_name must be provided to resolve "
39 "recursive relationships."
40 )
41 return package_label, model_name
42 if "." in model:
43 package_label, model_name = model.split(".", 1)
44 return package_label, model_name.lower()
45 if package_label is None:
46 raise TypeError(
47 "package_label must be provided to resolve unscoped model relationships."
48 )
49 return package_label, model.lower()
50 return model._meta.package_label, model._meta.model_name
51
52
53def field_references(
54 model_tuple,
55 field,
56 reference_model_tuple,
57 reference_field_name=None,
58 reference_field=None,
59):
60 """
61 Return either False or a FieldReference if `field` references provided
62 context.
63
64 False positives can be returned if `reference_field_name` is provided
65 without `reference_field` because of the introspection limitation it
66 incurs. This should not be an issue when this function is used to determine
67 whether or not an optimization can take place.
68 """
69 remote_field = field.remote_field
70 if not remote_field:
71 return False
72 references_to = None
73 references_through = None
74 if resolve_relation(remote_field.model, *model_tuple) == reference_model_tuple:
75 to_fields = getattr(field, "to_fields", None)
76 if (
77 reference_field_name is None
78 or
79 # Unspecified to_field(s).
80 to_fields is None
81 or
82 # Reference to primary key.
83 (
84 None in to_fields
85 and (reference_field is None or reference_field.primary_key)
86 )
87 or
88 # Reference to field.
89 reference_field_name in to_fields
90 ):
91 references_to = (remote_field, to_fields)
92 through = getattr(remote_field, "through", None)
93 if through and resolve_relation(through, *model_tuple) == reference_model_tuple:
94 through_fields = remote_field.through_fields
95 if (
96 reference_field_name is None
97 or
98 # Unspecified through_fields.
99 through_fields is None
100 or
101 # Reference to field.
102 reference_field_name in through_fields
103 ):
104 references_through = (remote_field, through_fields)
105 if not (references_to or references_through):
106 return False
107 return FieldReference(references_to, references_through)
108
109
110def get_references(state, model_tuple, field_tuple=()):
111 """
112 Generator of (model_state, name, field, reference) referencing
113 provided context.
114
115 If field_tuple is provided only references to this particular field of
116 model_tuple will be generated.
117 """
118 for state_model_tuple, model_state in state.models.items():
119 for name, field in model_state.fields.items():
120 reference = field_references(
121 state_model_tuple, field, model_tuple, *field_tuple
122 )
123 if reference:
124 yield model_state, name, field, reference
125
126
127def field_is_referenced(state, model_tuple, field_tuple):
128 """Return whether `field_tuple` is referenced by any state models."""
129 return next(get_references(state, model_tuple, field_tuple), None) is not None