1
1
from __future__ import annotations
2
2
3
+ import typing
3
4
from dataclasses import dataclass
4
5
from functools import partial
5
6
from typing import Any
6
7
7
8
from django .conf import settings
9
+ from django .contrib import admin
8
10
from django .contrib import messages
9
11
from django .contrib .admin .options import BaseModelAdmin
10
12
from django .contrib .admin .templatetags .admin_urls import add_preserved_filters
11
13
from django .core .exceptions import FieldDoesNotExist
12
14
from django .http import HttpRequest
13
15
from django .http import HttpResponse
16
+ from django .http import HttpResponseBadRequest
14
17
from django .http import HttpResponseRedirect
18
+ from django .shortcuts import redirect
19
+ from django .shortcuts import render
20
+ from django .urls import path
21
+ from django .urls import reverse
22
+ from django .utils .module_loading import import_string
15
23
from django .utils .translation import gettext_lazy as _
16
24
17
25
import django_fsm as fsm
18
26
27
+ if typing .TYPE_CHECKING :
28
+ from django .forms import Form
29
+
19
30
try :
20
31
import django_fsm_log # noqa: F401
21
32
except ModuleNotFoundError :
24
35
FSM_LOG_ENABLED = True
25
36
26
37
38
+
39
+
27
40
@dataclass
28
41
class FSMObjectTransition :
29
42
fsm_field : str
@@ -42,14 +55,26 @@ class FSMAdminMixin(BaseModelAdmin):
42
55
fsm_context_key = "fsm_object_transitions"
43
56
fsm_post_param = "_fsm_transition_to"
44
57
default_disallow_transition = not getattr (settings , "FSM_ADMIN_FORCE_PERMIT" , False )
58
+ fsm_transition_form_template = "django_fsm/fsm_admin_transition_form.html"
59
+
60
+ def get_urls (self ):
61
+ meta = self .model ._meta
62
+ return [
63
+ path (
64
+ "<path:object_id>/transition/<str:transition_name>/" ,
65
+ self .admin_site .admin_view (self .fsm_transition_view ),
66
+ name = f"{ meta .app_label } _{ meta .model_name } _transition" ,
67
+ ),
68
+ * super ().get_urls (),
69
+ ]
45
70
46
71
def get_fsm_field_instance (self , fsm_field_name : str ) -> fsm .FSMField | None :
47
72
try :
48
73
return self .model ._meta .get_field (fsm_field_name )
49
74
except FieldDoesNotExist :
50
75
return None
51
76
52
- def get_readonly_fields (self , request : HttpRequest , obj : Any = None ) -> tuple [str ]:
77
+ def get_readonly_fields (self , request : HttpRequest , obj : typing . Any = None ) -> tuple [str ]:
53
78
read_only_fields = super ().get_readonly_fields (request , obj )
54
79
55
80
for fsm_field_name in self .fsm_fields :
@@ -65,7 +90,7 @@ def get_readonly_fields(self, request: HttpRequest, obj: Any = None) -> tuple[st
65
90
def get_fsm_block_label (fsm_field_name : str ) -> str :
66
91
return f"Transition ({ fsm_field_name } )"
67
92
68
- def get_fsm_object_transitions (self , request : HttpRequest , obj : Any ) -> list [FSMObjectTransition ]:
93
+ def get_fsm_object_transitions (self , request : HttpRequest , obj : typing . Any ) -> list [FSMObjectTransition ]:
69
94
fsm_object_transitions = []
70
95
71
96
for field_name in sorted (self .fsm_fields ):
@@ -82,12 +107,18 @@ def get_fsm_object_transitions(self, request: HttpRequest, obj: Any) -> list[FSM
82
107
83
108
return fsm_object_transitions
84
109
110
+ def get_fsm_transition_form (self , transition : fsm .Transition ) -> Form | None :
111
+ form = transition .custom .get ("form" )
112
+ if isinstance (form , str ):
113
+ form = import_string (form )
114
+ return form
115
+
85
116
def change_view (
86
117
self ,
87
118
request : HttpRequest ,
88
119
object_id : str ,
89
120
form_url : str = "" ,
90
- extra_context : dict [str , Any ] | None = None ,
121
+ extra_context : dict [str , typing . Any ] | None = None ,
91
122
) -> HttpResponse :
92
123
_context = extra_context or {}
93
124
_context [self .fsm_context_key ] = self .get_fsm_object_transitions (
@@ -102,10 +133,10 @@ def change_view(
102
133
extra_context = _context ,
103
134
)
104
135
105
- def get_fsm_redirect_url (self , request : HttpRequest , obj : Any ) -> str :
136
+ def get_fsm_redirect_url (self , request : HttpRequest , obj : typing . Any ) -> str :
106
137
return request .path
107
138
108
- def get_fsm_response (self , request : HttpRequest , obj : Any ) -> HttpResponse :
139
+ def get_fsm_response (self , request : HttpRequest , obj : typing . Any ) -> HttpResponse :
109
140
redirect_url = self .get_fsm_redirect_url (request = request , obj = obj )
110
141
redirect_url = add_preserved_filters (
111
142
context = {
@@ -116,7 +147,7 @@ def get_fsm_response(self, request: HttpRequest, obj: Any) -> HttpResponse:
116
147
)
117
148
return HttpResponseRedirect (redirect_to = redirect_url )
118
149
119
- def response_change (self , request : HttpRequest , obj : Any ) -> HttpResponse :
150
+ def response_change (self , request : HttpRequest , obj : typing . Any ) -> HttpResponse :
120
151
if self .fsm_post_param in request .POST :
121
152
try :
122
153
transition_name = request .POST [self .fsm_post_param ]
@@ -134,6 +165,20 @@ def response_change(self, request: HttpRequest, obj: Any) -> HttpResponse:
134
165
obj = obj ,
135
166
)
136
167
168
+ # NOTE: if a form is defined on the transition, we redirect to the form view
169
+ if transition_func ._django_fsm .get_transition (
170
+ source = transition_func ._django_fsm .field .get_state (obj ),
171
+ ).custom .get ("form" ):
172
+ return redirect (
173
+ reverse (
174
+ f"admin:{ self .model ._meta .app_label } _{ self .model ._meta .model_name } _transition" ,
175
+ kwargs = {
176
+ "object_id" : obj .pk ,
177
+ "transition_name" : transition_name ,
178
+ },
179
+ )
180
+ )
181
+
137
182
try :
138
183
if FSM_LOG_ENABLED :
139
184
for fn in [
@@ -179,3 +224,69 @@ def response_change(self, request: HttpRequest, obj: Any) -> HttpResponse:
179
224
)
180
225
181
226
return super ().response_change (request = request , obj = obj )
227
+
228
+ @staticmethod
229
+ def _get_transition_title (transition ):
230
+ return getattr (transition .custom , "label" , None ) or transition .name
231
+
232
+ def fsm_transition_view (self , request , * args , ** kwargs ):
233
+ transition_name = kwargs ["transition_name" ]
234
+ obj = self .get_object (request , kwargs ["object_id" ])
235
+
236
+ transition_method = getattr (obj , transition_name )
237
+ if not hasattr (transition_method , "_django_fsm" ):
238
+ return HttpResponseBadRequest (f"{ transition_name } is not a transition method" )
239
+
240
+ transitions = transition_method ._django_fsm .transitions
241
+ if isinstance (transitions , dict ):
242
+ transitions = list (transitions .values ())
243
+ transition = transitions [0 ]
244
+
245
+ if TransitionForm := self .get_fsm_transition_form (transition ):
246
+ if request .method == "POST" :
247
+ transition_form = TransitionForm (data = request .POST , instance = obj )
248
+ if transition_form .is_valid ():
249
+ transition_method (** transition_form .cleaned_data )
250
+ obj .save ()
251
+ else :
252
+ return render (
253
+ request ,
254
+ self .fsm_transition_form_template ,
255
+ context = admin .site .each_context (request )
256
+ | {
257
+ "opts" : self .model ._meta ,
258
+ "original" : obj ,
259
+ "transition" : transition ,
260
+ "transition_form" : transition_form ,
261
+ },
262
+ )
263
+ else :
264
+ transition_form = TransitionForm (instance = obj )
265
+ return render (
266
+ request ,
267
+ self .fsm_transition_form_template ,
268
+ context = admin .site .each_context (request )
269
+ | {
270
+ "opts" : self .model ._meta ,
271
+ "original" : obj ,
272
+ "transition" : transition ,
273
+ "transition_form" : transition_form ,
274
+ },
275
+ )
276
+ else :
277
+ try :
278
+ transition_method ()
279
+ except fsm .TransitionNotAllowed :
280
+ self .message_user (
281
+ request ,
282
+ _ ("Transition %(transition)s is not allowed" ) % {"transition" : self ._get_transition_title (transition )},
283
+ messages .ERROR ,
284
+ )
285
+ else :
286
+ obj .save ()
287
+ self .message_user (
288
+ request ,
289
+ _ ("Transition %(transition)s applied" ) % {"transition" : self ._get_transition_title (transition )},
290
+ messages .SUCCESS ,
291
+ )
292
+ return redirect (f"admin:{ self .model ._meta .app_label } _{ self .model ._meta .model_name } _change" , object_id = obj .id )
0 commit comments