11from __future__ import annotations
22
3- import contextlib
43import typing
54from dataclasses import dataclass
6- from functools import partial
7- from functools import wraps
85
9- from django .contrib .contenttypes .models import ContentType
106from django .db import models
117
8+ from . import FSMLogDescriptor
129from .models import StateLog
1310from .models import TransitionLogBase
1411from .signals import post_transition
2219__all__ = [
2320 "StateLog" ,
2421 "TransitionLogBase" ,
25- "fsm_log_by" ,
26- "fsm_log_description" ,
2722 "track" ,
2823]
2924
3025
3126@dataclass (frozen = True )
3227class TrackConfig :
33- log_model : type [TransitionLogBase ] | None
34- relation_field : str | None
28+ log_model : type [TransitionLogBase ]
29+ relation_field : str
3530
3631
3732_registry : dict [type [models .Model ], TrackConfig ] = {}
33+
3834NOTSET = object ()
3935
4036
@@ -47,7 +43,10 @@ def decorator(model_cls: type[models.Model]) -> type[models.Model]:
4743 if model_cls ._meta .abstract :
4844 raise TypeError ("django_fsm.track cannot be used with abstract models" )
4945
50- _registry [model_cls ] = TrackConfig (log_model = log_model , relation_field = relation_field )
46+ _registry [model_cls ] = TrackConfig (
47+ log_model = log_model or StateLog ,
48+ relation_field = relation_field or "content_object" ,
49+ )
5150
5251 post_transition .connect (
5352 _log_transition ,
@@ -60,63 +59,6 @@ def decorator(model_cls: type[models.Model]) -> type[models.Model]:
6059 return decorator
6160
6261
63- class FSMLogDescriptor :
64- ATTR_PREFIX = "__django_fsm_log_attr_"
65-
66- def __init__ (self , instance : models .Model , attribute : str , value : typing .Any = NOTSET ):
67- self .instance = instance
68- self .attribute = attribute
69- if value is not NOTSET :
70- self .set (value )
71-
72- def get (self ) -> typing .Any :
73- return getattr (self .instance , self .ATTR_PREFIX + self .attribute )
74-
75- def set (self , value : typing .Any ) -> None :
76- setattr (self .instance , self .ATTR_PREFIX + self .attribute , value )
77-
78- def __enter__ (self ) -> typing .Self :
79- return self
80-
81- def __exit__ (self , * args : object ) -> None :
82- with contextlib .suppress (AttributeError ):
83- delattr (self .instance , self .ATTR_PREFIX + self .attribute )
84-
85-
86- def fsm_log_by (func : _TransitionFunc ) -> _TransitionFunc :
87- @wraps (func )
88- def wrapped (instance : models .Model , * args : typing .Any , ** kwargs : typing .Any ) -> typing .Any :
89- if "by" in kwargs :
90- author = kwargs .pop ("by" )
91- else :
92- return func (instance , * args , ** kwargs )
93-
94- with FSMLogDescriptor (instance , "by" , author ):
95- return func (instance , * args , ** kwargs )
96-
97- return wrapped
98-
99-
100- def fsm_log_description (
101- func : _TransitionFunc | None = None ,
102- * ,
103- description : str | None = None ,
104- ) -> _TransitionFunc :
105- if func is None :
106- return partial (fsm_log_description , description = description )
107-
108- @wraps (func )
109- def wrapped (instance : models .Model , * args : typing .Any , ** kwargs : typing .Any ) -> typing .Any :
110- with FSMLogDescriptor (instance , "description" ) as descriptor :
111- if "description" in kwargs :
112- descriptor .set (kwargs .pop ("description" ))
113- else :
114- descriptor .set (description )
115- return func (instance , * args , ** kwargs )
116-
117- return wrapped
118-
119-
12062def _log_transition (
12163 sender : type [models .Model ],
12264 instance : models .Model ,
@@ -131,44 +73,17 @@ def _log_transition(
13173 return
13274
13375 log_model = config .log_model or StateLog
134- log_kwargs : dict [str , typing .Any ] = {
135- "transition" : name ,
136- "state_field" : field .name ,
137- "source_state" : _coerce_state (source ),
138- "state" : _coerce_state (target ),
139- "by" : _extract_log_value (instance , "by" ),
140- "description" : _extract_log_value (instance , "description" ),
141- }
142-
143- if issubclass (log_model , StateLog ):
144- log_kwargs ["content_type" ] = ContentType .objects .get_for_model (sender )
145- log_kwargs ["object_id" ] = str (instance .pk )
146- else :
147- relation_field = config .relation_field or _resolve_relation_field (log_model , sender )
148- log_kwargs [relation_field ] = instance
149-
150- log_model ._default_manager .using (instance ._state .db ).create (** log_kwargs )
151-
152-
153- def _resolve_relation_field (
154- log_model : type [TransitionLogBase ], model_cls : type [models .Model ]
155- ) -> str :
156- relation_fields = [
157- field .name
158- for field in log_model ._meta .fields
159- if isinstance (field , models .ForeignKey )
160- and _matches_model (field .remote_field .model , model_cls )
161- ]
162- if len (relation_fields ) == 1 :
163- return relation_fields [0 ]
164-
165- if not relation_fields :
166- raise ValueError (
167- f"{ log_model .__name__ } does not define a ForeignKey to { model_cls .__name__ } "
168- )
169- raise ValueError (
170- f"{ log_model .__name__ } has multiple ForeignKey fields to { model_cls .__name__ } ; "
171- "set relation_field when calling track()"
76+
77+ log_model ._default_manager .using (instance ._state .db ).create (
78+ ** {
79+ "transition" : name ,
80+ "state_field" : field .name ,
81+ "source_state" : _coerce_state (source ),
82+ "state" : _coerce_state (target ),
83+ "by" : _extract_log_value (instance , "by" ),
84+ "description" : _extract_log_value (instance , "description" ),
85+ config .relation_field : instance ,
86+ }
17287 )
17388
17489
@@ -180,16 +95,5 @@ def _coerce_state(value: _StateValue | None) -> _StateValue | None:
18095 return value
18196
18297
183- def _matches_model (remote_model : typing .Any , model_cls : type [models .Model ]) -> bool :
184- if remote_model == model_cls :
185- return True
186- if isinstance (remote_model , str ):
187- return remote_model == model_cls .__name__ or remote_model .endswith (f".{ model_cls .__name__ } " )
188- return False
189-
190-
19198def _extract_log_value (instance : models .Model , attribute : str ) -> typing .Any :
192- try :
193- return FSMLogDescriptor (instance , attribute ).get ()
194- except AttributeError :
195- return None
99+ return getattr (instance , f"{ FSMLogDescriptor .ATTR_PREFIX } { attribute } " , None )
0 commit comments