Newer
Older
from enum import Enum
"""
This file defines classes which allow you to compose workflows based on the following structure:
Workflow -> Stage -> Phase -> Action
Current limitations:
* Changing the name of a phase will mean that any object which references it cannot progress. [will
be fixed when streamfield, may require intermediate fix prior to launch]
"""
class UserPermissions(Enum):
STAFF = 1
ADMIN = 2
LEAD = 3
APPLICANT = 4
class Workflow(dict):
def __init__(self, name, admin_name, **data):
self.name = name
self.admin_name = admin_name
super().__init__(**data)
def __str__(self):
return self.name
@property
def stages(self):
stages = []
for phase in self.values():
if phase.stage not in stages:
stages.append(phase.stage)
return stages
class Phase:
def __init__(self, name, display, stage, permissions, step, transitions=dict()):
self.name = name
self.display_name = display
self.stage = stage
self.permissions = permissions
self.step = step
# For building transition methods on the parent
default_permissions = {UserPermissions.STAFF, UserPermissions.ADMIN, UserPermissions.LEAD}
for transition_target, action in transitions.items():
transition = dict()
transition['display'] = action.get('display')
except AttributeError:
transition['display'] = action
transition['permissions'] = default_permissions
else:
transition['method'] = action.get('method')
conditions = action.get('conditions', '')
transition['conditions'] = conditions.split(',') if conditions else []
transition['permissions'] = action.get('permissions', default_permissions)
self.transitions[transition_target] = transition
def __str__(self):
return self.display_name
class Stage:
def __init__(self, name, has_external_review=False):
self.name = name
self.has_external_review = has_external_review
def __str__(self):
return self.name
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
class Permission:
def can_edit(self, user: 'User') -> bool:
return False
def can_staff_review(self, user: 'User') -> bool:
return False
def can_reviewer_review(self, user: 'User') -> bool:
return False
def can_review(self, user: 'User') -> bool:
return self.can_staff_review(user) or self.can_reviewer_review(user)
class StaffReviewPermission(Permission):
def can_staff_review(self, user: 'User') -> bool:
return user.is_apply_staff
class ReviewerReviewPermission(Permission):
def can_reviewer_review(self, user: 'User') -> bool:
return user.is_reviewer
class CanEditPermission(Permission):
def can_edit(self, user: 'User') -> bool:
return True
Request = Stage('Request', False)
Concept = Stage('Concept', False)
Proposal = Stage('Proposal', False)
'rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
'more_info': 'Request More Information',
},
'display': 'Under Discussion',
'stage': Request,
'permissions': Permission(),
'step': 0,
},
'more_info': {
'transitions': {
INITIAL_STATE: {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}, 'method': 'create_revision'},
},
'display': 'More information required',
'stage': Request,
'permissions': CanEditPermission(),
'step': 0,
},
'post_review_discussion': 'Close Review',
},
'display': 'Internal Review',
'stage': Request,
'permissions': StaffReviewPermission(),
'step': 1,
},
'post_review_discussion': {
'transitions': {
'accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
'rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
'post_review_more_info': 'Request More Information',
},
'display': 'Under Discussion',
'stage': Request,
'permissions': Permission(),
'step': 2,
},
'post_review_more_info': {
'transitions': {
'post_review_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}, 'method': 'create_revision'},
},
'display': 'More information required',
'stage': Request,
'permissions': CanEditPermission(),
'step': 2,
},
'accepted': {
'display': 'Accepted',
'stage': Request,
'permissions': Permission(),
'step': 3,
},
'rejected': {
'display': 'Rejected',
'stage': Request,
'permissions': Permission(),
'step': 3,
},
}
'concept_internal_review': 'Open Review',
'concept_rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
'concept_more_info': 'Request More Information',
'permissions': Permission(),
'step': 0,
},
'concept_more_info': {
'transitions': {
INITIAL_STATE: {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}, 'method': 'create_revision'},
},
'display': 'More information required',
'stage': Concept,
'permissions': CanEditPermission(),
'step': 0,
},
'concept_review_discussion': 'Close Review',
},
'display': 'Internal Review',
'stage': Concept,
'permissions': StaffReviewPermission(),
'step': 1,
},
'concept_review_discussion': {
'transitions': {
'invited_to_proposal': {'display': 'Invite to Proposal', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
'concept_rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
'concept_review_more_info': 'Request More Information',
},
'display': 'Under Discussion',
'stage': Concept,
'permissions': Permission(),
'step': 2,
},
'concept_review_more_info': {
'transitions': {
'concept_review_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}, 'method': 'create_revision'},
},
'display': 'More information required',
'stage': Concept,
'permissions': CanEditPermission(),
'step': 2,
},
'invited_to_proposal': {
'draft_proposal': {
'display': 'Progress',
'method': 'progress_application',
'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD},
'conditions': 'not_progressed',
},
},
'stage': Concept,
'permissions': Permission(),
'step': 3,
},
'concept_rejected': {
'display': 'Rejected',
'stage': Concept,
'permissions': Permission(),
'step': 3,
},
'proposal_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}},
},
'display': 'Invited for Proposal',
'stage': Proposal,
'permissions': CanEditPermission(),
'proposal_internal_review': 'Open Review',
'proposal_rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
'proposal_more_info': 'Request More Information',
},
'display': 'Under Discussion',
'stage': Proposal,
'permissions': Permission(),
'proposal_more_info': {
'transitions': {
'proposal_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}, 'method': 'create_revision'},
},
'display': 'More information required',
'stage': Proposal,
'permissions': CanEditPermission(),
'step': 5,
},
'post_proposal_review_discussion': 'Close Review',
},
'display': 'Internal Review',
'stage': Proposal,
'permissions': StaffReviewPermission(),
},
'post_proposal_review_discussion': {
'transitions': {
'external_review': 'Open AC review',
'proposal_rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
'post_proposal_review_more_info': 'Request More Information',
},
'display': 'Under Discussion',
'stage': Proposal,
'permissions': ReviewerReviewPermission(),
'post_proposal_review_more_info': {
'transitions': {
'post_proposal_review_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}, 'method': 'create_revision'},
},
'display': 'More information required',
'stage': Proposal,
'permissions': CanEditPermission(),
'step': 7,
},
'external_review': {
'transitions': {
'post_external_review_discussion': 'Close Review',
},
'display': 'Advisory Council Review',
'stage': Proposal,
'permissions': Permission(),
},
'post_external_review_discussion': {
'transitions': {
'proposal_accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
'proposal_rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
'post_external_review_more_info': 'Request More Information',
},
'display': 'Under Discussion',
'stage': Proposal,
'permissions': Permission(),
'post_external_review_more_info': {
'transitions': {
'post_external_review_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}, 'method': 'create_revision'},
},
'display': 'More information required',
'stage': Proposal,
'permissions': CanEditPermission(),
'step': 9,
},
'proposal_accepted': {
'display': 'Accepted',
'stage': Proposal,
'permissions': Permission(),
},
'proposal_rejected': {
'display': 'Rejected',
'stage': Proposal,
'permissions': Permission(),
Request = Workflow('Request', 'single', **{
phase_name: Phase(phase_name, **phase_data)
for phase_name, phase_data in SingleStageDefinition.items()
ConceptProposal = Workflow('Concept & Proposal', 'double', **{
phase_name: Phase(phase_name, **phase_data)
for phase_name, phase_data in DoubleStageDefinition.items()
WORKFLOWS = {
Request.admin_name: Request,
ConceptProposal.admin_name: ConceptProposal,
}
PHASES = list(itertools.chain.from_iterable(workflow.items() for workflow in WORKFLOWS.values()))
STATUSES[value.display_name].add(key)
if 'accepted' not in status and 'rejected' not in status and 'invited' not in status
reviews = set()
for phase_name, phase in PHASES:
if 'review' in phase_name:
if user is None:
reviews.add(phase_name)
elif phase.permissions.can_review(user):
reviews.add(phase_name)
return reviews
review_statuses = get_review_statuses()
DETERMINATION_PHASES = list(phase_name for phase_name, _ in PHASES if '_discussion' in phase_name)
def get_determination_transitions():
transitions = set()
for phase_name, phase in PHASES:
for transition_name in phase.transitions:
transitions.add(transition_name)
transitions.add(transition_name)
transitions.add(transition_name)
return transitions
DETERMINATION_RESPONSE_TRANSITIONS = get_determination_transitions()