表单向导中的 Django 模型表单集返回 None
Django Model Formset in Form Wizard returning None
我在表单向导的不同步骤中有多个表单集。我 运行 遇到一个表单集无法呈现的问题。具体来说,它 returns 错误:
'NoneType' object does not support item assignment
我确定错误是我试图根据表单向导中的上一步修改初始表单集数据,但数据没有传递到表单向导。相反,它传递 None
,这会导致向导呈现常规表单,而不是表单集。
查看代码,您可以看到两个表单集。一个 formset collaborators
工作得很好。另一个jobs
坏了
我找不到表单集传递 None
初始数据的原因。除了(未成功)回溯以确定数据变量是如何生成的之外,我还尝试了放置表单、表单集 类 以及更多 collaborators
步骤的多种组合。
我在使用这种方法时注意到的一点是,问题似乎与步骤有关。如果我将我的 FORMS 有序字典中 jobs
的所有表单集数据放置到 collaborators
,表单集将呈现。但是,关闭与 jobs
步骤相关的所有额外功能不会突然允许表单集呈现。
希望这是愚蠢的事情。预先感谢您的帮助!
相关代码如下:
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'),
]
正在阅读来自 class 的评论我覆盖了:
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))
我相信这些评论意味着数据只会通过一次...“如果可用,请添加查询集而不是以前的集合。
就是说,我准确地计算出调用表单集的时间:
app/views.py
...
form_class = self.form_list[step]
...
最终传递到前端的 returns 可修改数据。我在之前的formset的总表的基础上修改了总表,终于达到了我想要的效果
我在表单向导的不同步骤中有多个表单集。我 运行 遇到一个表单集无法呈现的问题。具体来说,它 returns 错误:
'NoneType' object does not support item assignment
我确定错误是我试图根据表单向导中的上一步修改初始表单集数据,但数据没有传递到表单向导。相反,它传递 None
,这会导致向导呈现常规表单,而不是表单集。
查看代码,您可以看到两个表单集。一个 formset collaborators
工作得很好。另一个jobs
坏了
我找不到表单集传递 None
初始数据的原因。除了(未成功)回溯以确定数据变量是如何生成的之外,我还尝试了放置表单、表单集 类 以及更多 collaborators
步骤的多种组合。
我在使用这种方法时注意到的一点是,问题似乎与步骤有关。如果我将我的 FORMS 有序字典中 jobs
的所有表单集数据放置到 collaborators
,表单集将呈现。但是,关闭与 jobs
步骤相关的所有额外功能不会突然允许表单集呈现。
希望这是愚蠢的事情。预先感谢您的帮助!
相关代码如下:
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'),
]
正在阅读来自 class 的评论我覆盖了:
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))
我相信这些评论意味着数据只会通过一次...“如果可用,请添加查询集而不是以前的集合。
就是说,我准确地计算出调用表单集的时间: app/views.py
...
form_class = self.form_list[step]
...
最终传递到前端的 returns 可修改数据。我在之前的formset的总表的基础上修改了总表,终于达到了我想要的效果