Been trying to make this work all day. I have a main model which is entry. An entry can include several shifts. This is part of an app to store working hours. Here's the view for creating an entry:
class EntryCreateView(FormView):
template_name = 'entry/create.html'
form_class = AddWorkDay
success_url = reverse_lazy('time_manager:index')
def get(self, request, ordinal=None, *args, **kwargs):
""" Initiates with a blank form or will populate the day field with the day represented by the passed
ordinal. """
if ordinal:
day = datetime.datetime.fromordinal(int(ordinal))
form = AddWorkDay(initial={'day': day})
else:
form = AddWorkDay()
formset = ShiftFormSet()
return render(request, self.template_name, {'form': form, 'formset': formset})
def post(self, request, ordinal=None, *args, **kwargs):
form = AddWorkDay(data=request.POST)
formset = ShiftFormSet(data=request.POST)
errors = []
shifts = []
if form.is_valid() and formset.is_valid():
# Build entry.
entry = form.save(commit=False)
entry.owner = request.user
errors.extend(entry.validate(request.user))
# Build shift.
for form in formset:
shift = form.save(commit=False)
shift.entry = entry
shifts.append(shift)
errors.extend(shift.validate(request.user))
if len(errors) == 0:
entry.save()
for shift in shifts:
shift.save()
return HttpResponseRedirect(reverse('time_manager:index'))
return render(request, self.template_name, {'form': form, 'formset': formset, 'errors': errors, 'shifts': shifts, 'entry': entry})
When I try to enter an entry with a shift and press save, it terminates saying:
"IntegrityError at /time_manager/entry/create/
NOT NULL constraint failed: shift_shift.entry_id". I tried to figure out what was wrong with the shifts, so I commented the block out where the shift is saved (from "if len(errors)" to "return HttpResponseRedirect.") so that it would return to the view with the form. I then put {{ shifts }} into my template, to see what is in there. When I do that, it terminates with: "NoReverseMatch at /time_manager/entry/create/
Reverse for 'edit' with arguments '()' and keyword arguments '{'pk': None}' not found. 1 pattern(s) tried: ['time_manager/entry/shift/edit/(?P(\d+))/$']" as if I were trying to use the {% url %} tag referencing a view that doesn't exist. So I'm guessing that something goes wrong when I try to save the forms of the formset. However, I read the django documentation again and again and this is the way to store the forms of a formset, is it not?
Ok, with a fresh head I looked at my code, and then it occurred to me. I wanted to make sure that the entry and each shift are built correctly first, and then only save them if they don't violate any rules. So as you can see above: I'm saving both with commit=False. That means however that entry has not been assigned a primary key yet. The primary key is what the ForeignKeyField on my shift model needs. That's why Django failed saving it.
I changed the order of the method somewhat. This is the working code:
def post(self, request, ordinal=None, *args, **kwargs):
form = AddWorkDay(data=request.POST)
formset = ShiftFormSet(data=request.POST)
errors = []
shifts = []
if form.is_valid() and formset.is_valid():
# Build entry.
entry = form.save(commit=False)
entry.owner = request.user
errors.extend(entry.validate(request.user))
# Build shift.
for form in formset:
shift = form.save(commit=False)
shifts.append(shift)
errors.extend(shift.validate(request.user))
# If there are no errors, save the entry ans it's shifts.
if len(errors) == 0:
entry.save()
for shift in shifts:
shift.entry = entry
shift.save()
return HttpResponseRedirect(reverse('time_manager:index'))
return render(request, self.template_name, {'form': form, 'formset': formset, 'errors': errors, 'shifts': shifts, 'entry': entry})
Notice how entry is saved for the second time (without commit=False) and then assigned to shift.
Related
I'm currently building a website with Django and I've gotten to a point where I need to print data on the screen(ListView) and update(UpdateView) data on the same page(template). From what I've found I cant do this easily with the Django generic views so I rewrote my view as a function-based view. This current piece of code updates what I need perfectly with some changes to the HTML.
def DocPostNewView(request, pk):
context = {}
obj = get_object_or_404(DocPost, id=pk)
form = GeeksForm(request.POST or None, instance=obj)
if form.is_valid():
form.save()
return HttpResponseRedirect("/" + id)
context["form"] = form
posts = DocPost.objects.all()
return render(request, "my_app/use_template.html", context)
... And this following piece of code lists objects perfectly with some changes to the HTML.
def DocPostNewView(request, pk):
context = {}
obj = get_object_or_404(DocPost, id=pk)
form = GeeksForm(request.POST or None, instance=obj)
if form.is_valid():
form.save()
return HttpResponseRedirect("/" + id)
context["form"] = form
posts = DocPost.objects.all()
return render(request, 'my_app/use_template.html', context={'posts': posts})
I just need to combine these and the only real difference is the return command at the bottom of both(the same) functions. HOW CAN I COMBINE THE RETURNS SO I CAN LIST DATA AND UPDATE DATA ON THE SAME PAGE???
Thanks
does this work for you ?
def DocPostNewView(request, pk=None):
posts = DocPost.objects.all()
obj = get_object_or_404(DocPost, id=pk)
form = GeeksForm(request.POST or None, instance=obj)
if form.is_valid():
form.save()
return HttpResponseRedirect("/" + id)
context = {"form":form, "posts":posts}
return render(request, "my_app/use_template.html", context)
in your templates you could use "posts" to list every post,
and your can use "form" to update that specific post,
the thing is for every update you make you will always be seeing the whole list of posts, if it is that what you want to achieve
I've created a CreateView with a modelformset, added some js to add or remove additional forms. It is a view to create a booking from a bank booking entry. And as the 'from_account' is obvious (the booking is launched from a certain bank booking entry and handing over the pk from the bank booking in the url), I do not want to show this (selection-) field in the form. So I put the field as a hidden field in the formset with the objective to fill it in the post.
All this works as expected until submit.
Now, in the post() method, I see in formset.error correctly the errors for the missing fields 'bank_account_ta' and 'from_account'. Since I don't know the total_forms number (it might have been changed using js), I cannot prefill it while instantiating the formset class.
Working around in the template with js is possible, but not the proper way...
I would rather using the formset.clean() method but whatever I try, the formset.error remains as before which leads to not formset.is_valid().
My questions: What is the easiest way to complete data (cleaned_data) of a formset in post()? Another point which is still hurting me: Is it necessary or helpful to define self.object in a formset and if yes, how? I put it to None as I receive a missing object error if I leave it undefined.
Thank you in advance.
views.py
class TransactionMulitCreateView(LoginRequiredMixin, CreateView):
model = Transaction
template_name = 'accounting/transaction_formset.html'
extra_context = {'title': model._meta.verbose_name, }
success_url = reverse_lazy('finance:ta_groups')
TaFormset = modelformset_factory(Transaction,
formset=TransactionBaseFormset,
exclude=('validity_period_start', 'validity_period_end', 'ta_group'),
extra=1)
def get(self, request, *args, **kwargs):
self.object = None
ba = BankTransaction.objects.get(pk=self.kwargs['bank_ta_pk'])
form = self.TaFormset(queryset=Transaction.objects.none(), initial=[
{
# commented out, as it was the js approach.
# 'bank_account_ta': ba,
# 'from_account': ba.account.ca_account,
'posting_text': ba.booking_string(),
'valuta': ba.valuta,
'entry_date': ba.entry_date,
'value': ba.amount,
},
], ba=ba)
helper = TransactionMultiFormsetHelper()
return self.render_to_response(self.get_context_data(form=form, helper=helper, bank_ta=ba))
def post(self, request, *args, **kwargs):
self.object = None
ba = BankTransaction.objects.get(pk=self.kwargs['bank_ta_pk'])
# pass in initial to be able to detect changes, see https://docs.djangoproject.com/en/3.1/topics/forms/formsets/
formset = self.TaFormset(request.POST, request.FILES, initial=[
{
# commented out, as it was the js approach.
# 'bank_account_ta': ba,
# 'from_account': ba.account.ca_account,
'posting_text': ba.booking_string(),
'valuta': ba.valuta,
'entry_date': ba.entry_date,
'value': ba.amount,
},
], ba=ba)
formset.clean()
# formset = self.TaFormset()
if formset.is_valid():
instances = formset.save(commit=False)
split = TransactionGroup.objects.create(create_by=self.request.user, update_by=self.request.user)
for instance in instances:
instance.ta_group = split
instance.save()
# BankTransaction.objects.filter(pk=self.kwargs['bank_ta_pk']).update(is_booked=True)
ba.is_booked = True
ba.save()
return HttpResponseRedirect(reverse_lazy('finance:bank_tas'))
# return self.form_valid(formset)
else:
helper = TransactionMultiFormsetHelper()
return self.render_to_response(self.get_context_data(form=formset, helper=helper, bank_ta=ba))
# return self.form_invalid(formset)
forms.py
class TransactionBaseFormset(forms.BaseModelFormSet):
def __init__(self, *args, **kwargs):
# self.ba_pk = kwargs.pop('ba_pk')
try:
self.ba = kwargs.pop('ba')
except KeyError:
self.ba = None
super().__init__(*args, **kwargs)
def clean(self):
data = None
for form in self.forms:
data = form.cleaned_data
data['bank_account_ta'] = self.ba
data['from_account'] = self.ba.account.ca_account
return data
Got it!
The simple solution is to exclude all enforced fields and fill them either in the clean() or in the is_valid() function.
TaFormset = modelformset_factory(Transaction,
formset=TransactionBaseFormset,
exclude=(
'validity_period_start',
'validity_period_end',
'ta_group',
'bank_account_ta',
'from_account',
),
extra=1)
However, I would still like to know which role plays (or might play) self.object in a formset (or modelformset)?
I have forms that can be dynamically added and deleted from a formset. On the front end, I mark deletion by checking the "delete" checkbox included in the form. form-0-DELETE gets added to the POST automatically.
https://docs.djangoproject.com/en/2.2/topics/forms/formsets/#dealing-with-ordering-and-deletion-of-forms
Deleting form from django formset
def edit(request):
Formset = formset_factory(EditForm, extra=1, can_delete=True)
if request.method == 'POST':
formset = Formset(request.POST)
if formset.is_valid():
cleaned_data = formset.cleaned_data
print(cleaned_data)
return render(request, '/edit.html', {'test': 'Post'})
else:
formset = Formset()
return render(request, '/edit.html', {'formset': formset})
Here is part of the return formset:
data:
{u'form-MAX_NUM_FORMS': [u'1000'], u'form-0-test': [u'72'], u'form-0-DELETE': [u'on'], u'form-TOTAL_FORMS': [u'0'], u'form-INITIAL_FORMS': [u'0'], u'form-0-weight': [u'1']}
deleted_forms:
[]
'test' and 'weight' are input values in the forms. You can see 'form-0-DELETE' there. Meaning the delete event was added to the POST with the default value 'on.' One of my links said to change this value to something that could be evaluated to true, setting it to True, true, and 1 didn't change anything.
TOTAL-FORMS is correctly 0 as I keep track of additions and deletions, but I would expected deleted_forms to be populated with form-0. This is a problem when I have more than 0 forms, as I don't know what will be in cleaned data since deleted forms aren't recorded.
The solution was on the front end. TOTAL_FORMS is the absolute total, deleted forms included. By keeping that to the correct amount, instead of subtracting -1 when deleting a form, deleted_forms was populated correctly.
I want to create a text on my website, where the gaps, in which the user can input a word (like the c-tests for learning language) are dynamically created depending on the text that the function gets from the database (not yet implemented).
My idea was it to create a formset in which each label is different depending on a variable I give it.
Here is my views.py
def ctest(request):
c_test_tokens, gaps, tokenindexe = generate_c_test(beispieltext())
# EXAMPLE
# NOT WORKING ON POST REQUEST YET
# if this is a POST request we need to process the form data
if request.method == 'POST':
# create a form instance and populate it with data from the request:
form = CTestform(request.POST)
# check whether it's valid:
if form.is_valid():
# process the data in form.cleaned_data as required
# ...
# redirect to a new URL:
return HttpResponseRedirect('/thanks/')
# if a GET (or any other method) we'll create a blank form
else:
ctestformset = formset_factory(CTestform, extra=len(gaps))
return render(request, 'ctest.html', {'form': ctestformset})
Here is my forms.py
class CTestform(forms.Form):
hello = forms.CharField(widget=forms.TextInput(attrs={'size': '5'}),
required=False, label='hello', label_suffix='')
Is this approach fine and how do I give the form a list with each element being the label for each gap, or should I search for an alternative?
EDIT:
Ok I tried the things on this site Variable number of inputs with Django forms possible?
My forms.py now has the function
def creategaps(self, c_test_tokens, gaps, tokenindexes):
wordsbeforegap = ''
gaps = {}
iteratorforgaps = 0
for i in (0, len(c_test_tokens)-1):
if '#GAP#' not in c_test_tokens[i]:
wordsbeforegap = wordsbeforegap + c_test_tokens[i]
else:
gaps[iteratorforgaps] = forms.CharField(widget=forms.TextInput(attrs={'size': '5'}),
required=False, label=wordsbeforegap, label_suffix='')
wordsbeforegap = ''
return type('test', (forms.BaseForm,), {'base_fields': gaps})
I tried to call the function from views.py but it doesnt create any form.
Here is the else statement in views.py
else:
form = CTestform.creategaps("test", c_test_tokens, gaps, tokenindexe)
return render(request, 'ctest.html', {'form': form})
Why doesnt it work?
This questions addresses my question genearally, but I am looking for a more specific explanation.
I would like a user to update a a group of model objects, however, the queryset for these objects will need to be retrieved first. My plan is to do this in two seperate URs/views, getting the query set info from the first, then displaying the model formset to be updated next.
My first view gives a list of all the the "Project"s (One of my models), and retrieves the id of the project selected.
Here is the form:
class ProjectLookupForm(forms.Form):
Project_Name = chosenforms.ChosenModelChoiceField(queryset=Project.objects.all())
and here is the view:
def update_project_filter(request):
project_form = ProjectLookupForm(request.POST or None)
if request.method == 'POST':
if project_form.is_valid():
context = {"project_form":project_form}
# Get project here and share it with the next view.
selected_project_id = project_form.cleaned_data["Project_Name"].id
# Add a new return statement here?
# Or call update project view from here?
# Add a redirect button to html?
else:
errors = project_form.errors
context = {"errors":errors, "project_form":project_form}
else:
context = {"project_form":project_form}
return render(request, 'filter_update_project_form.html', context)
As one can see, I have included some comments brainstorming what my possibilities are. My goal is to send the selected_project_id to this next view, so that it can use that id as a model form query set.
def update_project(request):
UpdateFormset = modelformset_factory(Sample, fields=("sample_name", "extraction_date",
"project", "order", "notebook", "notebook_page"))
if request.method == 'POST':
formset = UpdateFormset(request.POST, request.FILES)
if formset.is_valid():
formset.save()
context = {"formset": formset, "project_form":project_form}
else:
errors = formset.errors
context = {"formset":formset, "errors":errors, "project_form":project_form}
else:
formset = UpdateFormset(queryset=Sample.objects.filter(project=2))
context = {"formset":formset, "project_form":project_form}
return render(request, 'update_project_form.html', context)
One can see here that I have hard coded the queryset like so:
queryset=Sample.objects.filter(project=2)
How can I set "project=" to my selected_project_id? Do I pass this info to the view as an input parameter? Or do I send it to the next URL and take it from there?
Assuming you've activated django.contrib.sessions.middleware.SessionMiddleware; you can pass data between views using request.session dictionary as follows:
def update_project_filter(request):
...
selected_project_id = project_form.cleaned_data["Project_Name"].id
request.session['selected_project_id'] = selected_project_id
...
def update_project(request):
...
selected_project_id = request.session.get('selected_project_id')
...