Completed
Push — master ( 7af861...bb4bfe )
by Chris
01:13
created

MultiStepWizard.remaining()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 4
rs 10
1
"""A simple multi-step wizard that uses the flask application session.
2
3
Creating multi-step forms of arbitrary length is simple and intuitive.
4
5
Example usage:
6
7
```
8
from flask.ext.wtf import Form
9
10
class MultiStepTest1(Form):
11
    field1 = StringField(validators=[validators.DataRequired()],)
12
    field2 = StringField(validators=[validators.DataRequired()],)
13
14
15
class MultiStepTest2(Form):
16
    field3 = StringField(validators=[validators.DataRequired()],)
17
    field4 = StringField(validators=[validators.DataRequired()],)
18
19
20
class MyCoolForm(MultiStepWizard):
21
    __forms__ = [
22
        MultiStepTest1,
23
        MultiStepTest2,
24
    ]
25
```
26
"""
27
28
from flask import session
29
from flask.ext.wtf import Form
30
31
32
class MultiStepWizard(Form):
33
    """Generates a multi-step wizard.
34
35
    The wizard uses the app specified session backend to store both
36
    form data and current step.
37
38
    TODO: make sure all the expected features of the typical form
39
    are exposed here, but automatically determining the active form
40
    and deferring to it. See __iter__ and data for examples.
41
    """
42
43
    __forms__ = []
44
45
    def __iter__(self):
46
        """Get the specific forms' fields for standard WTForm iteration."""
47
        _, form = self.get_active()
48
        return form.__iter__()
49
50
    def __init__(self, *args, **kwargs):
51
        """Do all the required setup for managing the forms."""
52
        super(MultiStepWizard, self).__init__(*args, **kwargs)
53
        # Store the name and session key by a user specified kwarg,
54
        # or fall back to this class name.
55
        self.name = kwargs.get('session_key', self.__class__.__name__)
56
        # Get the sessions' current step if it exists.
57
        curr_step = session.get(self.name, {}).get('curr_step', 1)
58
        # if the user specified a step, we'll use that instead. Form validation
59
        # will still occur, but this is useful for when the user may need
60
        # to go back a step or more.
61
        if 'curr_step' in kwargs:
62
            curr_step = int(kwargs.pop('curr_step'))
63
        if curr_step > len(self.__forms__):
64
            curr_step = 1
65
        self.step = curr_step
66
        # Store forms in a dunder because we want to avoid conflicts
67
        # with any WTForm objects or third-party libs.
68
        self.__forms = []
69
        self._setup_session()
70
        self._populate_forms()
71
        invalid_forms_msg = 'Something happened during form population.'
72
        assert len(self.__forms) == len(self.__forms__), invalid_forms_msg
73
        assert len(self.__forms) > 0, 'Need at least one form!'
74
        self.active_form = self.get_active()[1]
75
        # Inject the required fields for the active form.
76
        # The multiform class will always be instantiated once
77
        # on account of separate POST requests, and so the previous form
78
        # values will no longer be attributes to be concerned with.
79
        self._setfields()
80
81
    def _setfields(self):
82
        """Dynamically set fields for this particular form step."""
83
        _, form = self.get_active()
84
        for name, val in vars(form).items():
85
            if repr(val).startswith('<UnboundField'):
86
                setattr(self, name, val)
87
88
    def alldata(self, combine_fields=False, flush_after=False):
89
        """Get the specific forms data."""
90
        _alldata = dict()
91
        # Get all session data, combine if specified,
92
        # and delete session if specified.
93
        if self.name in session:
94
            _alldata = session[self.name].get('data')
95
            if combine_fields:
96
                combined = dict()
97
                for formname, data in _alldata.items():
98
                    combined.update(data)
99
                _alldata = combined
100
        if flush_after:
101
            self.flush()
102
        return _alldata
103
104
    @property
105
    def data(self):
106
        """Get the specific forms data."""
107
        _, form = self.get_active()
108
        return form.data
109
110
    def _setup_session(self):
111
        """Setup session placeholders for later use."""
112
        # We will populate these values as the form progresses,
113
        # but only if it doesn't already exist from a previous step.
114
        if self.name not in session:
115
            session[self.name] = dict(
116
                curr_step=self.curr_step,
117
                data={f.__name__: None for f in self.__forms__})
118
119
    def _populate_forms(self):
120
        """Populate all forms with existing data for validation.
121
122
        This will only be done if the session data exists for a form.
123
        """
124
        for form in self.__forms__:
125
            data = session[self.name]['data'].get(form.__name__)
126
            init_form = form(**data) if data is not None else form()
127
            self.__forms.append(init_form)
128
129
    def _update_session_formdata(self, form):
130
        """Update session data for a given form key."""
131
        # Add data to session for this current form!
132
        name = form.__class__.__name__
133
        data = form.data
134
        # Update the session data for this particular form step.
135
        # The policy here is to always clobber old data.
136
        session[self.name]['data'][name] = data
137
138
    @property
139
    def active_name(self):
140
        """Return the nice name of this form class."""
141
        return self.active_form.__class__.__name__
142
143
    def next_step(self):
144
        """Set the step number in the session to the next value."""
145
        next_step = session[self.name]['curr_step'] + 1
146
        self.curr_step = next_step
147
        if self.name in session:
148
            session[self.name]['curr_step'] += 1
149
150
    @property
151
    def step(self):
152
        """Get the current step."""
153
        if self.name in session:
154
            return session[self.name]['curr_step']
155
156
    @step.setter
157
    def step(self, step_val):
158
        """Set the step number in the session."""
159
        self.curr_step = step_val
160
        if self.name in session:
161
            session[self.name]['curr_step'] = step_val
162
163
    def validate_on_submit(self, *args, **kwargs):
164
        """Override validator and setup session updates for persistence."""
165
        # Update the step to the next form automagically for the user
166
        step, form = self.get_active()
167
        self._update_session_formdata(form)
168
        if not form.validate_on_submit():
169
            self.step = step - 1
170
            return False
171
        # Update to next form if applicable.
172
        if step - 1 < len(self.__forms):
173
            self.curr_step += 1
174
            self.active_form = self.__forms[self.curr_step - 1]
175
            self.next_step()
176
        # Mark the current step as -1 to indicate it has been
177
        # fully completed, if the current step is the final step.
178
        elif step - 1 == len(self.__forms):
179
            self.step = -1
180
        return True
181
182
    @property
183
    def remaining(self):
184
        """Get the number of steps remaining."""
185
        return len(self.__forms[self.curr_step:]) + 1
186
187
    @property
188
    def total_steps(self):
189
        """Get the number of steps for this form in a (non-zero index)."""
190
        return len(self.__forms) + 1
191
192
    @property
193
    def steps(self):
194
        """Get a list of the steps for iterating in views, html, etc."""
195
        return range(1, self.total_steps)
196
197
    def get_active(self):
198
        """Get active step."""
199
        form_index = self.curr_step - 1 if self.curr_step > 0 else 0
200
        return self.curr_step + 1, self.__forms[form_index]
201
202
    def flush(self):
203
        """Clear data and reset."""
204
        del session[self.name]
205
206
    def is_complete(self):
207
        """Determine if all forms have been completed."""
208
        if self.name not in session:
209
            return False
210
        # Make the current step index something unique for being "complete"
211
        completed = self.step == -1
212
        if completed:
213
            # Reset.
214
            self.curr_step = 1
215
        return completed
216