This repository has been archived by the owner on Jun 6, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
core.py
210 lines (163 loc) · 6.43 KB
/
core.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
class SMException(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return self.value
class InvalidState(SMException):
pass
class InvalidTransition(SMException):
pass
class InvalidStateMachine(SMException):
pass
class State(object):
verbose_name = None
transitions = {}
permissions = (
('view', 'Can View'),
('edit', 'Can Edit'),
('delete', 'Can Delete'),
)
def __init__(self, instance=None, **kwargs):
self.instance = instance
self.extra_args = kwargs
def set_state(self, new_state):
"""
A method that can be overridden for custom state processing.
By default this method looks for a ``state_field`` on the instance
and just updates that field.
"""
if self.instance:
state_field = self.extra_args.get('state_field', 'state')
setattr(self.instance, state_field, new_state)
return new_state
def transition(self, action, **kwargs):
"""
Performs a transition based on ``action`` and returns a
instance for the next State
"""
try:
new_state = self.transitions[action]
except KeyError:
raise InvalidTransition(
'%s is not a valid action. Valid actions are: %s' % (
action, [k for k in self.transitions]))
# Try to run a custom method if it exists
if hasattr(self, action):
getattr(self, action)(**kwargs)
return self.set_state(new_state)
class StateMachine(object):
state_map = {}
initial_state = ''
def __init__(self, instance=None, **kwargs):
"""
The entry point for our statemachine.
``kwargs`` is extra arguments that the developer can pass through
to the statemachine and it's States.
This can then be used in the custom action methods for those states.
"""
self.instance = instance
self.extra_args = kwargs
self.process_state()
self.verify_statemachine()
def set_state(self, state):
self._state = state
def get_state(self):
return self.state_map[self._state].verbose_name
state = property(get_state)
def get_actions(self):
return [i for i in self.get_state_instance().transitions]
actions = property(get_actions)
@classmethod
def get_choices(cls):
"""
Returns a standard django tuple containing a list of States, in
the format, ``(<state_value>, '<verbose_name>')``.
This is a handy helper for using in django choices fields etc.
"""
choices = ()
for k in cls.state_map:
choices += (
(k, cls.state_map[k].verbose_name or cls.state_map[k].__name__),
)
return choices
def process_state(self):
"""
Our default state processor. This method can be overridden
if the state is determined by more than just a field on the
instance.
If you override this method, make sure to call set_state() to
set the state on the instance.
"""
self.state_field = self.extra_args.get('state_field', 'state')
state = self.extra_args.get('state', None)
if not state:
state = getattr(self.instance, self.state_field, None) or self.initial_state
if state not in self.state_map:
state = self.initial_state
self.set_state(state)
def get_state_instance(self):
""" Returns a single instance for the current state """
return self.state_map[self._state](
instance=self.instance, **self.extra_args)
def take_action(self, action, **kwargs):
self._state = self.get_state_instance().transition(action, **kwargs)
def action_result(self, action):
"""
Determines what the resulting state for would be if ``action`` is
transitioned.
"""
try:
return self.get_state_instance().transitions[action]
except KeyError:
raise InvalidTransition('%s, is not a valid action.' % action)
def verify_statemachine(self):
"""
Verify that the ``initial_state`` and ``state_map`` does not
contain any invalid states.
"""
# First verify if the initial state is a valid state
if self.initial_state not in self.state_map:
raise InvalidStateMachine(
'"%s" is not a valid state for %s. Valid states are %s' % (
self._state, self.__class__.__name__,
[i for i in self.state_map.keys()]
))
# Now cycle through every state in the state_map and make sure that
# actions are valid and there are no "hanging states"
state_keys = self.state_map.keys() # Hold on to these for testing
for key in self.state_map:
state_cl = self.state_map[key]
targets = state_cl.transitions.values()
for t in targets:
if t not in state_keys:
raise InvalidState(
"%s contains an invalid action target, %s." %
(state_cl.__name__, t))
@classmethod
def get_permissions(cls, prefix, verbose_prefix=""):
"""
Returns the permissions for the different states and transitions
as tuples, the same as what django's permission system expects.
``prefix`` is required so that we can specify on which model
the permission applies.
"""
perms = ()
for k, v in cls.state_map.iteritems():
for perm in v.permissions:
# permission codename format: "<state>_<action>_<prefix>"
perms += ((
'%s_%s_%s' % (v.__name__.lower(), perm[0], prefix),
'[%s] %s %s' % (v.verbose_name, perm[1], verbose_prefix or prefix),
),)
# Now add the transition permissions
for t in v.transitions:
perm = (
'can_%s_%s' % (t, prefix),
'Can %s %s' % (t.capitalize(), verbose_prefix or prefix),
)
if perm not in perms: # Dont add it if it already exists
perms += (perm,)
return perms
class IntegerStateMachine(StateMachine):
def set_state(self, state):
super(IntegerStateMachine, self).set_state(int(state))