I have multiple formsets on different steps in my form wizard. I've run into an issue where one formset will not render. Specifically, it returns the error:
'NoneType' object does not support item assignment
I've determined that the error is that I'm trying to modify the initial formset data based on a previous step in the form wizard, but the data is not being passed to the form wizard. Instead, it is passing None
, which causes the wizard to render a regular form, rather than a formset.
Looking at the code, you can see two formsets. One formset collaborators
works totally fine. The other jobs
is broken.
I can find no reason for the formset to pass None
initial data. Aside from (unsuccessfully) tracing back to determine how the data variable is generated, I've tried numerous combinations of placing forms, formset classes, and more on the collaborators
step.
The thing I've noticed using this methodology, is that the problem seems to be tied to the step. If I place all formset data from jobs
in my FORMS ordered dictionary over to collaborators
, the formset renders. Turning off all extra functionality tied to the jobs
step does not suddenly allow the formset to render, however.
Hoping it's something stupid. Thank you in advance for your help!
Here's the relevant code:
app/views.py
FORMS = [
('info', project_forms.NewProjectNameForm),
('incomesources', project_forms.NewProjectSourcesForm),
('collaborators', forms.modelformset_factory(
models.UserCollaborator,
form=project_forms.NewProjectCollaboratorsForm)),
('jobs', forms.modelformset_factory(
models.Job,
form=project_forms.NewProjectJobForm,
formset=project_forms.JobFormset)),
('checkingaccount', project_forms.NewProjectWithdrawalBankForm),
('review', project_forms.NewProjectReviewForm),
TEMPLATES = {
'info': 'project/name.html',
'incomesources': 'project/sources.html',
'collaborators': 'project/collaborators.html',
'jobs': 'project/jobs.html',
'checkingaccount': 'project/checkingaccount.html',
'review': 'project/review.html',
}
TEMPLATE_NAMES = {
'info': "Basic Info",
'incomesources': "Income Sources",
'collaborators': "Collaborators",
'jobs': "Jobs",
'checkingaccount': "Checking Account",
'review': "Review",
class ProjectWizard(NamedUrlSessionWizardView):
def get_template_names(self):
"""
Gets form template names to be rendered in page title.
:return: Template Name
"""
return [TEMPLATES[self.steps.current]]
def get_collaborators_as_users(self):
collaborators = []
collaborators_data = self.storage.get_step_data('collaborators')
if collaborators_data:
total_emails = int(collaborators_data.get('collaborators-TOTAL_FORMS'))
for email in range(total_emails):
# Gather a list of users.
collaborator_key = 'collaborators-' + str(email) + '-email'
email = collaborators_data.get(collaborator_key)
collaborators.append(User.objects.get(email__iexact=email))
return collaborators
def get_owner_collaborators_as_names(self, collaborators):
collaborators_names = [self.request.user.get_full_name() + ' (Owner)']
for collaborator in collaborators:
if not collaborator.get_full_name():
collaborators_names.append(collaborator.email)
else:
collaborators_names.append(collaborator.get_full_name())
return collaborators_names
def get_context_data(self, form, **kwargs):
"""
Overrides class method.
:param form: Current Form
:param kwargs: Derived from class
:return: Context is returned
"""
context = super(ProjectWizard, self).get_context_data(form=form, **kwargs)
step = self.steps.current
if step == 'incomesources':
youtube_credentials = models.YouTubeCredentials.objects.filter(user_id=self.request.user.id)
context.update(
{'youtube_credentials': youtube_credentials})
elif step == 'jobs':
collaborators = self.get_collaborators_as_users()
collaborators_names = self.get_owner_collaborators_as_names(collaborators)
context.update({'collaborator_names': collaborators_names})
elif step == 'checkingaccount':
accounts = StripeCredentials.objects.filter(id=self.request.user.id)
if accounts.exists():
context.update(({
'accounts_exist': True,
'stripe_public_key': settings.STRIPE_PUBLIC_KEY,
}))
else:
context.update(({'accounts_exist': False}))
elif step == 'review':
# Get raw step data
step_info = self.storage.get_step_data('info')
step_income_sources = self.storage.get_step_data('incomesources')
step_jobs = self.storage.get_step_data('jobs')
step_checking_account = self.storage.get_step_data('checkingaccount')
# Process collaborator objects to names
collaborators = self.get_collaborators_as_users()
collaborators_names = self.get_owner_collaborators_as_names(collaborators)
# Process jobs
total_jobs = step_jobs['jobs-TOTAL_FORMS']
jobs = []
for job in range(int(total_jobs)):
# Gather a list of users.
job_key = 'jobs-' + str(job) + '-job'
job = step_jobs.get(job_key)
jobs.append(job)
print(step_checking_account)
context.update({
'project_name': step_info['info-name'],
'video_id': step_income_sources['incomesources-value'],
'collaborators_names': collaborators_names,
})
context.update(
{'page_title': "New Project – " + TEMPLATE_NAMES[self.steps.current]})
return context
def get_form(self, step=None, data=None, files=None):
"""
Overrides class method.
Constructs the form for a given `step`. If no `step` is defined, the
current step will be determined automatically.
The form will be initialized using the `data` argument to prefill the
new form. If needed, instance or queryset (for `ModelForm` or
`ModelFormSet`) will be added too.
"""
if step is None:
step = self.steps.current
form_class = self.form_list[step]
# Prepare the kwargs for the form instance.
kwargs = self.get_form_kwargs(step)
if self.request.method == 'GET':
if step == 'collaborators':
assert data != None, "Formset not rendering. Data returned as: {}".format(data)
if step == 'jobs':
assert data != None, "Formset not rendering. Data returned as: {}".format(data)
# Define number of forms to be registered based on previous step.
collaborators_data = self.storage.get_step_data('collaborators')
data['jobs-TOTAL_FORMS'] = str(int(collaborators_data['collaborators-TOTAL_FORMS']) + 1)
kwargs.update({
'data': data,
'files': files,
'prefix': self.get_form_prefix(step, form_class),
'initial': self.get_form_initial(step),
})
if issubclass(form_class, (forms.ModelForm, forms.models.BaseInlineFormSet)):
# If the form is based on ModelForm or InlineFormSet,
# add instance if available and not previously set.
kwargs.setdefault('instance', self.get_form_instance(step))
elif issubclass(form_class, forms.models.BaseModelFormSet):
# If the form is based on ModelFormSet, add queryset if available
# and not previous set.
kwargs.setdefault('queryset', self.get_form_instance(step))
return form_class(**kwargs)
def post(self, *args, **kwargs):
"""
This method handles POST requests.
The wizard will render either the current step (if form validation
wasn't successful), the next step (if the current step was stored
successful) or the done view (if no more steps are available)
"""
# Look for a wizard_goto_step element in the posted data which
# contains a valid step name. If one was found, render the requested
# form. (This makes stepping back a lot easier).
wizard_goto_step = self.request.POST.get('wizard_goto_step', None)
if wizard_goto_step and wizard_goto_step in self.get_form_list():
return self.render_goto_step(wizard_goto_step)
# Check if form was refreshed
management_form = ManagementForm(self.request.POST, prefix=self.prefix)
if not management_form.is_valid():
raise ValidationError(
_('ManagementForm data is missing or has been tampered.'),
code='missing_management_form',
)
form_current_step = management_form.cleaned_data['current_step']
if (form_current_step != self.steps.current and
self.storage.current_step is not None):
# form refreshed, change current step
self.storage.current_step = form_current_step
# get the form for the current step
form = self.get_form(data=self.request.POST, files=self.request.FILES)
# and try to validate
if form.is_valid():
# if the form is valid, store the cleaned data and files.
self.storage.set_step_data(self.steps.current, self.process_step(form))
self.storage.set_step_files(self.steps.current, self.process_step_files(form))
# Interact with Stripe
if self.steps.current == 'checkingaccount':
stripe.api_key = settings.STRIPE_SECRET_KEY
stripe_token = self.request.POST.get('stripe_token')
email = self.request.user.email
description = 'Customer for ' + email
customer = stripe.Customer.create(
description=description,
source=stripe_token
)
customer_id = customer.id
stripe_credentials, created = StripeCredentials.objects.get_or_create(
owner_id=self.request.user)
if created:
stripe_credentials.save()
# check if the current step is the last step
if self.steps.current == self.steps.last:
# no more steps, render done view
return self.render_done(form, **kwargs)
else:
# proceed to the next step
return self.render_next_step(form)
return self.render(form)
app/urls.py
project_wizard = project_views.ProjectWizard.as_view(project_views.FORMS,
url_name='project_step', done_step_name='finished')
urlpatterns = [
path('new/<step>', project_wizard, name='project_step'),
path('new', project_wizard, name='new_project'),
]
Reading the comments from the class I overrided:
if issubclass(form_class, (forms.ModelForm, forms.models.BaseInlineFormSet)):
# If the form is based on ModelForm or InlineFormSet,
# add instance if available and not previously set.
kwargs.setdefault('instance', self.get_form_instance(step))
elif issubclass(form_class, forms.models.BaseModelFormSet):
# If the form is based on ModelFormSet, add queryset if available
# and not previous set.
kwargs.setdefault('queryset', self.get_form_instance(step))
I believe those comments mean that data will only pass one time..."add queryset if available and not previous set.
That said, I figured exactly when the formset is called: app/views.py
...
form_class = self.form_list[step]
...
That returns modifiable data which ultimately passes along to the front end. I modified the total forms based on the previous formset's total forms, and was finally able to achieve my desired results.