2424from django .db .models .expressions import OrderBy , RawSQL
2525from django .db .models .fields .json import KeyTextTransform
2626from django .db .models .functions import Cast
27- from django .dispatch import receiver
2827from django .urls import reverse
2928from django .utils import timezone
3029from django .utils .functional import cached_property
3130from django .utils .html import strip_tags
3231from django .utils .text import slugify
3332from django .utils .translation import gettext_lazy as _
34- from django_fsm import RETURN_VALUE , FSMField , can_proceed , transition
35- from django_fsm .signals import post_transition
33+ from viewflow .fsm import State
3634from wagtail .contrib .forms .models import AbstractFormSubmission
3735from wagtail .fields import StreamField
3836
@@ -273,7 +271,7 @@ def can_transition(instance, user):
273271
274272def wrap_method (func ):
275273 def wrapped (* args , ** kwargs ):
276- # Provides a new function that can be wrapped with the django_fsm method
274+ # Provides a new function that can be wrapped with the viewflow-fsm method
277275 # Without this using the same method for multiple transitions fails as
278276 # the fsm wrapping is overwritten
279277 return func (* args , ** kwargs )
@@ -286,8 +284,18 @@ def transition_id(target, phase):
286284 return "__" .join ([transition_prefix , phase .stage .name .lower (), phase .name , target ])
287285
288286
287+ def get_all_possible_states ():
288+ all_states = set ()
289+ for workflow in WORKFLOWS .values ():
290+ for phase_name , data in workflow .items ():
291+ all_states .add ((phase_name , data .display_name ))
292+ return all_states
293+
294+
289295class AddTransitions (models .base .ModelBase ):
290296 def __new__ (cls , name , bases , attrs , ** kwargs ):
297+ status_field = attrs .get ("status_field" )
298+
291299 for workflow in WORKFLOWS .values ():
292300 for phase_name , data in workflow .items ():
293301 for transition_name , action in data .transitions .items ():
@@ -296,43 +304,55 @@ def __new__(cls, name, bases, attrs, **kwargs):
296304 permission_func = make_permission_check (action ["permissions" ])
297305
298306 # Get the method defined on the parent or default to a NOOP
299- transition_state = wrap_method (
300- attrs .get (action .get ("method" ), lambda * args , ** kwargs : None )
301- )
307+ method = action .get ("method" )
308+
309+ if method in attrs :
310+ transition_m = attrs [method ]
311+ elif method and hasattr (cls , method ):
312+ transition_m = getattr (cls , method )
313+ else :
314+ # Create a bound noop method that updates status
315+ def make_noop (target_state ):
316+ def noop_method (instance , * args , ** kwargs ):
317+ return True
318+
319+ return noop_method
320+
321+ transition_m = make_noop (transition_name )
322+
323+ transition_state = wrap_method (transition_m )
302324 # Provide a neat name for graph viz display
303325 transition_state .__name__ = slugify (action ["display" ])
304326
305327 conditions = [
306328 attrs [condition ] for condition in action .get ("conditions" , [])
307329 ]
308- # Wrap with transition decorator
309- transition_func = transition (
310- attrs [ "status" ],
330+
331+ # Create the transition
332+ transition_func = status_field . transition (
311333 source = phase_name ,
312334 target = transition_name ,
313335 permission = permission_func ,
314336 conditions = conditions ,
315337 custom = action .get ("custom" , {}),
316- )( transition_state )
338+ )
317339
318- # Attach to new class
319- attrs [method_name ] = transition_func
340+ # Bind the transition method
341+ attrs [method_name ] = transition_func ( transition_state )
320342 attrs [permission_name ] = permission_func
321343
322344 def get_transition (self , transition ):
323345 try :
324346 return getattr (self , transition_id (transition , self .phase ))
325- except TypeError :
326- # Defined on the class
327- return None
328- except AttributeError :
329- # For the other workflow
347+ except (TypeError , AttributeError ):
330348 return None
331349
332350 attrs ["get_transition" ] = get_transition
333351
334352 def get_actions_for_user (self , user ):
335- transitions = self .get_available_user_status_transitions (user )
353+ transitions = type (self ).status_field .get_available_transitions (
354+ self , self .status , user
355+ )
336356 actions = [
337357 (
338358 transition .target ,
@@ -346,15 +366,19 @@ def get_actions_for_user(self, user):
346366 attrs ["get_actions_for_user" ] = get_actions_for_user
347367
348368 def perform_transition (self , action , user , request = None , ** kwargs ):
349- transition = self .get_transition (action )
350- if not transition :
369+ transition_method = self .get_transition (action )
370+ if not transition_method :
351371 raise PermissionDenied (f'Invalid "{ action } " transition' )
352- if not can_proceed (transition ):
372+
373+ if not transition_method .can_proceed ():
353374 action = self .phase .transitions [action ]
354375 raise PermissionDenied (f'You do not have permission to "{ action } "' )
355376
356- transition (by = user , request = request , ** kwargs )
357- self .save (update_fields = ["status" ])
377+ # Execute the transition
378+ result = transition_method (by = user , request = request , ** kwargs )
379+ if result :
380+ self .status = action
381+ self .save (update_fields = ["status" ])
358382
359383 self .progress_stage_when_possible (user , request , ** kwargs )
360384
@@ -472,7 +496,15 @@ class ApplicationSubmission(
472496 search_document = SearchVectorField (null = True )
473497
474498 # Workflow inherited from WorkflowHelpers
475- status = FSMField (default = INITIAL_STATE , protected = True )
499+ status = models .CharField (
500+ max_length = 100 ,
501+ choices = get_all_possible_states (),
502+ default = INITIAL_STATE ,
503+ )
504+ status_field = State (
505+ default = INITIAL_STATE ,
506+ states = get_all_possible_states (),
507+ )
476508
477509 screening_statuses = models .ManyToManyField (
478510 "funds.ScreeningStatus" , related_name = "submissions" , blank = True
@@ -513,6 +545,10 @@ class Meta:
513545 def is_draft (self ):
514546 return self .status == DRAFT_STATE
515547
548+ @status_field .getter ()
549+ def _get_object_status (self ):
550+ return self .status
551+
516552 @property
517553 def title_text_display (self ):
518554 """Return the title text for display across the site.
@@ -532,10 +568,9 @@ def title_text_display(self):
532568 def not_progressed (self ):
533569 return not self .next
534570
535- @transition (
536- status ,
571+ @status_field .transition (
537572 source = "*" ,
538- target = RETURN_VALUE ( INITIAL_STATE , "draft_proposal" , "invited_to_proposal" ) ,
573+ target = [ INITIAL_STATE , "draft_proposal" , "invited_to_proposal" ] ,
539574 permission = make_permission_check ({UserPermissions .ADMIN }),
540575 )
541576 def restart_stage (self , ** kwargs ):
@@ -636,8 +671,8 @@ def is_determination_form_attached(self):
636671 def progress_application (self , ** kwargs ):
637672 target = None
638673 for phase in STAGE_CHANGE_ACTIONS :
639- transition = self .get_transition (phase )
640- if can_proceed (transition ):
674+ transition_method = self .get_transition (phase )
675+ if transition_method and transition_method . can_proceed ():
641676 # We convert to dict as not concerned about transitions from the first phase
642677 # See note in workflow.py
643678 target = dict (PHASES )[phase ].stage
@@ -662,6 +697,7 @@ def progress_application(self, **kwargs):
662697
663698 submission_in_db .next = self
664699 submission_in_db .save ()
700+ return True
665701
666702 def from_draft (self ) -> Self :
667703 """Sets current `form_data` to the `form_data` from the draft revision.
@@ -745,8 +781,7 @@ def create_revision(
745781 self .save (skip_custom = True )
746782
747783 return revision
748-
749- return None
784+ return True
750785
751786 def clean_submission (self ):
752787 self .process_form_data ()
@@ -1000,48 +1035,49 @@ def get_yes_screening_status(self):
10001035 def get_no_screening_status (self ):
10011036 return self .screening_statuses .filter (yes = False ).first ()
10021037
1038+ @status_field .on_success ()
1039+ def log_status_update (self , descriptor , source , target , ** kwargs ):
1040+ instance = self
1041+ old_phase = self .workflow [source ]
10031042
1004- @receiver (post_transition , sender = ApplicationSubmission )
1005- def log_status_update (sender , ** kwargs ):
1006- instance = kwargs ["instance" ]
1007- old_phase = instance .workflow [kwargs ["source" ]]
1043+ by = kwargs ["by" ]
1044+ request = kwargs ["request" ]
1045+ notify = kwargs .get ("notify" , True )
10081046
1009- by = kwargs ["method_kwargs" ]["by" ]
1010- request = kwargs ["method_kwargs" ]["request" ]
1011- notify = kwargs ["method_kwargs" ].get ("notify" , True )
1047+ if request and notify :
1048+ if source == DRAFT_STATE :
1049+ # remove task from applicant dashboard for this instance
1050+ remove_tasks_for_user (
1051+ code = SUBMISSION_DRAFT , user = by , related_obj = instance
1052+ )
1053+ # notify for a new submission
1054+ messenger (
1055+ MESSAGES .NEW_SUBMISSION ,
1056+ request = request ,
1057+ user = by ,
1058+ source = instance ,
1059+ )
1060+ else :
1061+ messenger (
1062+ MESSAGES .TRANSITION ,
1063+ user = by ,
1064+ request = request ,
1065+ source = instance ,
1066+ related = old_phase ,
1067+ )
10121068
1013- if request and notify :
1014- if kwargs ["source" ] == DRAFT_STATE :
1015- # remove task from applicant dashboard for this instance
1016- remove_tasks_for_user (code = SUBMISSION_DRAFT , user = by , related_obj = instance )
1017- # notify for a new submission
1018- messenger (
1019- MESSAGES .NEW_SUBMISSION ,
1020- request = request ,
1021- user = by ,
1022- source = instance ,
1023- )
1024- else :
1025- messenger (
1026- MESSAGES .TRANSITION ,
1027- user = by ,
1028- request = request ,
1029- source = instance ,
1030- related = old_phase ,
1031- )
1069+ if instance .status in review_statuses :
1070+ messenger (
1071+ MESSAGES .READY_FOR_REVIEW ,
1072+ user = by ,
1073+ request = request ,
1074+ source = instance ,
1075+ )
10321076
1033- if instance .status in review_statuses :
1077+ if instance .status in STAGE_CHANGE_ACTIONS :
10341078 messenger (
1035- MESSAGES .READY_FOR_REVIEW ,
1036- user = by ,
1079+ MESSAGES .INVITED_TO_PROPOSAL ,
10371080 request = request ,
1081+ user = by ,
10381082 source = instance ,
10391083 )
1040-
1041- if instance .status in STAGE_CHANGE_ACTIONS :
1042- messenger (
1043- MESSAGES .INVITED_TO_PROPOSAL ,
1044- request = request ,
1045- user = by ,
1046- source = instance ,
1047- )
0 commit comments