1

It's been days I am trying to split up a simple django form with some charfield and dropdowns to sub forms using django-formtools. In the first step form I left 2 dropdowns field and in the second step form, 2 char fields and one image file. Once I change dropdowns and go to the second form then after step back, the values and image of the second step disappear so I need to refill the second step of form before submitting(and values of dropdown in the first step saved in the storage after changing the step):

Here is the view.py code:

 FORMS = [('step_first', step_first_form),
     ('step_second', step_second_form)]

 TEMPLATES = {'step_first': 'myapp/step_first.html',
        'step_second': 'myapp/step_second.html'}



class NewWizard(NamedUrlSessionWizardView):
      file_storage = FileSystemStorage(location=os.path.join(settings.MEDIA_ROOT, 'photos'))

      def done(self, form_list, **kwargs):
              .....

      def get_form(self, step=None, data=None, files=None):
           # maintaining the files in session when changing steps
           if self.steps.current == 'step_first':
              step_files = self.storage.get_step_files(self.steps.current)
          else:
             step_files = self.storage.current_step_files

          if step_files and files:
            for key, value in step_files.items():
                if files in key and files[key] is not None:
                    step_files[key] = files[key]
          elif files:
              step_files = files

         return super(NewWizard, self).get_form(step, data,step_files)


      def get_template_names(self):
           return [TEMPLATES[self.steps.current]]

and my second template:

<form id="dropdownForm" method="POST" action="" enctype="multipart/form-data">{% csrf_token %}
      {{ wizard.management_form }}
      {% if wizard.form.forms %}
            {{ wizard.form.management_form }}
            {% for form in wizard.form.forms %}
                 {{ form }}
            {% endfor %}
      {% else %}
          <div class="content-section mt-4 text-center">
                    <div class="row justify-content-center">

                        {% if wizard.steps.prev %}
                            <button class="btn btn-sm btn-outline-primary" name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" formnovalidate>{% trans "prev step" %}</button>
                        {% endif %}
                        <div class="form-group ml-2">
                            <input class="btn btn-sm btn-outline-primary"  type="submit" value="{% trans 'submit' %}"/>
                        </div>
                    </div>
                </div>

      {% endif %}

in url.py:

create_wizard = login_required(NewWizard.as_view(FORMS, url_name='step_first', done_step_name='finished'))
urlpatterns = [
re_path('myapp/new/create-(?P<step>.+)', create_wizard, name='step_first'),
]

I guess the prev step does not submit the form!

Any assistance you can provide would be greatly appreciated.

Braiano
  • 335
  • 6
  • 26
  • This code has multiple references to things that are not defined. As such it can't easily be run to start debugging the problem. See [How to create a Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example). –  Dec 04 '20 at 18:47
  • I don't agree with you on this. This code is part of `django-formtools` document with few changes which should work but is not. I tried to make it as simple as possible. I think the problem is not in the question, it's more in the document which is very generic and is not very well explained. – Braiano Dec 04 '20 at 19:12
  • done() causes syntax errors, both forms (step_first_form,..) are not defined, all imports are missing, form tag isn't closed in template, suggesting there's missing code... If I pip install django-formtools and paste the code into views.py, create the template, I should be able to have a working setup. Not fix several issues before getting starting. Again, read the linked help document. Specifically, the complete part. –  Dec 04 '20 at 20:29

1 Answers1

2

The default behavior of form wizard is that if you go back and come back to the current form, you will lose all the data(non-file) and files. The reason is that the prev button is associated with render_goto_step method. In the doc about render_goto_step, it says:

This method is called when the step should be changed to something else than the next step. By default, this method just stores the requested step goto_step in the storage and then renders the new step. If you want to store the entered data of the current step before rendering the next step, you can overwrite this method.

The following will solve part of your problems.

class NewWizard(NamedUrlSessionWizardView):
    file_storage = FileSystemStorage(location=os.path.join(settings.MEDIA_ROOT, 'photos'))

    def done(self, form_list, form_dict, **kwargs):

        return render(self.request, 'simpletest/done.html', {
            'form_data':[form.cleaned_data for form in form_list]
        })

    def get_template_names(self):
        return [TEMPLATES[self.steps.current]]

    def render_goto_step(self, goto_step, **kwargs):

        print('under render_goto_step')
        print(self.storage.current_step)

        form1 = self.get_form(self.storage.current_step, data=self.request.POST,files=self.request.FILES)
        if form1.is_valid:
            print("form.is valid")
            self.storage.set_step_data(self.storage.current_step, self.process_step(form1))
            print('after set_step_data')
            self.storage.set_step_files(self.storage.current_step, self.process_step_files(form1))
            print('after set_step_files')
        else:
            print('under form.errors')
            print(form1.errors)

        ######### this is from render_goto_step method
        self.storage.current_step = goto_step

        form = self.get_form(
            data=self.storage.get_step_data(self.steps.current),
            files=self.storage.get_step_files(self.steps.current))

        return redirect(self.get_step_url(goto_step))

Unfortunately, it cannot solve the image preview problem. I am not entirely sure about it, but this seems not related to the render_goto_step function per se because even the ones saved by postmethod to the session storage cannot be rendered. For example, if you add an image in form2, hit submit, and go to form3, and hit prev, you will see that image in form2 is gone, although the value(title) is there.

It seems that django and form wizard just do not render these files because they are dictionaries not files themselves. They are either <UploadedFile> or <InMemoryUploadedFile>objects.

What to do about image preview?

1. I was able to solve this problem by saving the file data into Model. Override post method and render_goto_step method to make sure that the image is saved to model both when you hit submit (post) and hitprev, first--- render_goto_step.

In addition, in order to render the image in your template, override get_context_data method and pass mypostinstance.

Please note that: in the following code, I simplified the save to modelportion by just save to pk=1 object. You have to change it accordingly.

class NewWizard(NamedUrlSessionWizardView):
    file_storage = FileSystemStorage(location=os.path.join(settings.MEDIA_ROOT, 'photos'))

    def done(self, form_list, form_dict, **kwargs):

        return render(self.request, 'simpletest/done.html', {
            'form_data':[form.cleaned_data for form in form_list]
        })

    def get_template_names(self):
        return [TEMPLATES[self.steps.current]]

    def render_goto_step(self, goto_step, **kwargs):

        print('under render_goto_step')
        print(self.storage.current_step)

        form1 = self.get_form(self.storage.current_step, data=self.request.POST,files=self.request.FILES)
        if form1.is_valid:
            print("form.is valid")
            self.storage.set_step_data(self.storage.current_step, self.process_step(form1))
            print('after set_step_data')
            self.storage.set_step_files(self.storage.current_step, self.process_step_files(form1))
            print('after set_step_files')

            ############ check if it is step_second, save file to model.
            if self.steps.current =='step_second':
                print('under render_goto_step step_second')

                if 'imagefile' in self.request.FILES.keys():

                    f = self.request.FILES['imagefile']
                    print(f)
                    if f: 
                        mypost = MyPost.objects.get(pk=1) 
                        mypost.image = f
                        mypost.save()
                        print('saved')
        else:
            print('under form.errors')
            print(form1.errors)

        ######### this is from render_goto_step method
        self.storage.current_step = goto_step

        form = self.get_form(
            data=self.storage.get_step_data(self.steps.current),
            files=self.storage.get_step_files(self.steps.current))

        return redirect(self.get_step_url(goto_step))


    def post(self, *args, **kwargs):
        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)

        print('wizard_goto_step')
        print(wizard_goto_step)
        print('current')
        print(self.steps.current)
         # 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():

            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))

            ############ check if it is step_second, save file to model.
            if self.steps.current =='step_second':
                print('under step_second')
                f = self.request.FILES['imagefile']
                print(f)
                if f: 
                    mypost = MyPost.objects.get(pk=1) 
                    mypost.image = f
                    mypost.save()
                    print('saved')

                return self.render_next_step(form)
            else: 
                # 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)
        return super(NewWizard, self).post(*args, **kwargs)

    def get_context_data(self, form, **kwargs):
        context = super().get_context_data(form=form, **kwargs)
        mypost = MyPost.objects.get(pk=1)

        context.update({'mypost': mypost})
        return context    

and in your template, use image.url like below:

<div class="preview">%
                           {% if mypost.image %}
                           <img id="fileip-preview" src="{{mypost.image.url}}" alt="....">
                           {% else %}
                           <img id="fileip-preview" src="" alt="....">
                          {% endif %}
                         </div>

2. Another viable way is to first read the file and pass it to the template in views. I did not try to implement this. But this post might provide you an idea about how to implement it.

3. Another seemingly viable way is to use history API and localStorage. The idea is to mimic the browser back and forward buttons. As you can see when you use the browser to go back and come back to current, you can see that all your info is retained. It seemed that so many things that users do can affect history states, such as using back/forward rather than prev, submit; refreshing pages, back/forward from refreshed pages, etc. I tried this approach and felt like it should be a winner, but abandoned it eventually.

ha-neul
  • 3,058
  • 9
  • 24
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/225436/discussion-on-answer-by-ha-neul-why-in-django-wizard-form-values-do-not-get-save). – Machavity Dec 02 '20 at 21:08
  • @ha-neul I appreciate your time and effort. I will try to fix it and will confirm the answer as soon as fit it to my code. – Braiano Dec 04 '20 at 23:59
  • Thank you! Please first see if the non-file part works (without images). And then check if overriding ```post``` method works by checking your database about updated images. And then incorporate ```image``` part from ```render_goto_step```. The last part, I have not extensively validated... – ha-neul Dec 05 '20 at 00:20
  • I also updated ```render_goto_step```'s image part. add an ```if``` statement to check if the image is updated or not ... anyway. use print() generously to check if every step is working. I had spend a lot time debugging, I think it will take some time for you too. – ha-neul Dec 05 '20 at 00:25
  • @ha-neul Hi, I just tried first solution, related to just `render_goto_step`, and is working correctly. But in the second part about image preview and in `render_goto_step`, still struggling to save the data. I don't know how you could run the code `MyPost.objects.get(pk=1) ` because still form is not submitted to database and shouldn't have any `id` in model to get/retrieve. Do you know how to solve it? – Braiano Dec 05 '20 at 15:04
  • and when I add `post()` and `get_context_data` in the second page after submit it ridirect to the first page!(maybe I need to debug this) – Braiano Dec 05 '20 at 15:06
  • Great that the first part worked out for you. For second and third, try to first generate an instance of ```pk=``` in your database (without image, make sure image can be null=True, blank=True), and just use my code to see if the ```image preview``` works or not. And then you can work on the how to get the MyPost generated... – ha-neul Dec 05 '20 at 15:07
  • besides generating a database instance, try step 2 first; means you will keep the first step's ```render_goto_step``` intact, just add ```post``` method. And you will check image if image is saved in the database by hit ``` submit```. Only after you worked out this step, then go to next step with adding the image part to ```render_goto_step```. – ha-neul Dec 05 '20 at 15:10