1

I have a ModelForm and I'm trying to display it with some of the fields disabled. In this case, process_id. The important part of my Model looks like this:

models.py

class Process(models.Model):
    related_processes = models.ManyToManyField('self', blank=True, symmetrical=False)
    process_id = models.CharField(max_length=20, primary_key=True)
    normal_field = models.CharField(max_length=20)
    # a lot of fields here...

So basically I have a Process that can have zero or more related processes. This is what I have on forms.py:

forms.py

class ProcessForm(ModelForm):
    class Meta:
        model = Process
        fields = '__all__'


class EditProcessForm(ProcessForm):
    readonly_fields = ('process_id', )

    def __init__(self, *args, **kwargs):
        super(EditProcessForm, self).__init__(*args, **kwargs)
        for field in (field for name, field in self.fields.items() if name in self.readonly_fields):
            field.widget.attrs['disabled'] = 'true'
            field.required = False

    def clean(self):
        for f in self.readonly_fields:
            self.cleaned_data.pop(f, None)
        return super(EditProcessForm, self).clean()


class NewVersionProcessForm(EditProcessForm):
    readonly_fields = ('process_id', )

    def __init__(self, *args, **kwargs):
        super(NewVersionProcessForm, self).__init__(*args, **kwargs)
        for field in (field for name, field in self.fields.items() if name in self.readonly_fields):
            # field.widget.attrs['disabled'] = 'true'
            # Remember this line ^
            field.required = False

The first time the client is filling the form, I want all fields to be editable, so I use ProcessForm. But when the client is editing the Process, I want some fields to be read-only. I found this nice solution here on stackoverflow (unfortunately I couldn't find it again) and it works perfectly when I'm editing a process. The difference between EditProcessForm and NewVersionProcessForm is their Views and the commented line on __init__.

views.py

class ProcessFormView(FormView):
    template_name = 'my_app/fill_form.html'
    form_class = ProcessForm

    def form_valid(self, form):
        form.save()
        return redirect('my_app:show_process_page', form.cleaned_data.get('process_id'))


class EditProcessView(UpdateView):
    model = Process
    form_class = EditProcessForm
    template_name = 'my_app/edit_form.html'
    pk_url_kwarg = 'process_id'

    def post(self, request, process_id):
        # This is a little hack I found here: https://stackoverflow.com/a/21262262/3773461
        # to edit an immutable QueryDict.
        mutable = request.POST._mutable
        request.POST._mutable = True
        request.POST['process_id'] = process_id
        request.POST._mutable = mutable
        return super().post(request)

    def form_valid(self, form):
        form.save()
        return redirect('my_app:show_process_page', self.kwargs['process_id'])

class NewVersionProcessView(EditProcessView):
    template_name = 'my_app/new_version_form.html'
    form_class = NewVersionProcessForm

    def get(self, request, process_id):
        try:
            Process.objects.get(process_id=process_id)
            return redirect('my_app:process_already_exists_page')
        except Process.DoesNotExist:
            self.object = Process(process_id=process_id)
            last_id = self.object.get_last_version_id()
            last_process = Process.objects.get(process_id=last_id)
            last_process_dict = last_process.__dict__
            related_processes = last_process.related_processes.all()[:]
            exclude = ['_state', 'process_id']
            for key, value in last_process_dict.items():
                if key not in exclude:
                    self.object.__dict__[key] = value

            # self.object.related_processes.add(*related_processes)
            # Remember this line too ^

            form_class = self.get_form_class()
            form = self.get_form(form_class)
            return self.form_invalid(form)

    def post(self, request, process_id):
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def form_valid(self, form):
        form.save()
        return redirect('my_app:show_process_page', form.cleaned_data['process_id'])

The first view is a simple FormView with ProcessForm. The second view is an UpdateView. On post I just add the field that is disabled on the form (and therefore is not passed ahead normally). On form_valid I just save the form on its model (with the just added field).

The third view is my current problem. Keep in mind that I can do 3 things with a Process: create a new one, edit it, or create a new version of an existing process. This third view intends to do the third one. On get, I check if I'm trying to create a process that already exists. If it doesn't, I create the Process and copy all useful data on the base process to it, except the ManyToMany field (the second "remember this line"), because I couldn't figure out a way to do that without saving the Model first. On post, I'm checking if the form is valid, and on form_valid, I'm saving the process.

What works

I can create and edit processes just fine. The read-only fields when editing works perfectly. The model is saved as intended. Also, I can create a new process using another existing process as a base (NewVersionProcessView) if the first "remember this line" is commented, that is, if all the fields are editable.

What doesn't work

If the first "remember this line" is uncommented, that is, if some of the fields are read-only, my view crashes on post. Most specifically, on form_invalid. The only thing I can think of is that somehow there's not a Process object linked to my NewVersionProcessView. I can't understand why though, because I always assign something to self.object on get. I cannot get the object from the database on post because it is not saved yet. Also, I cannot understand why a field being enabled or disabled affects self.object. Remember, the only thing that changes is that field.widget.attrs['disabled'] = 'true' line. Here is the log:

Request Method: POST
Request URL: http://127.0.0.1:8000/my_app/new_version/999-11/

Django Version: 2.0.7
Python Version: 3.4.4
Installed Applications:
['my_app.apps.MyAppConfig',
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']


Traceback:

File "C:\Python34\lib\site-packages\django\core\handlers\exception.py" in inner
  35.             response = get_response(request)

File "C:\Python34\lib\site-packages\django\core\handlers\base.py" in _get_response
  128.                 response = self.process_exception_by_middleware(e, request)

File "C:\Python34\lib\site-packages\django\core\handlers\base.py" in _get_response
  126.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "C:\Python34\lib\site-packages\django\views\generic\base.py" in view
  69.             return self.dispatch(request, *args, **kwargs)

File "C:\Python34\lib\site-packages\django\views\generic\base.py" in dispatch
  89.         return handler(request, *args, **kwargs)

File "C:\Users\***\Desktop\my_app\views.py" in post
  124.          return self.form_invalid(form)

File "C:\Python34\lib\site-packages\django\views\generic\edit.py" in form_invalid
  61.         return self.render_to_response(self.get_context_data(form=form))

File "C:\Python34\lib\site-packages\django\views\generic\edit.py" in get_context_data
  67.         return super().get_context_data(**kwargs)

File "C:\Python34\lib\site-packages\django\views\generic\detail.py" in get_context_data
  93.         if self.object:

Exception Type: AttributeError at /my_app/new_version/999-11/
Exception Value: 'NewVersionProcessView' object has no attribute 'object'

I can provide the urls.py and the htmls if needed. Any help is appreciated.

Hisu
  • 117
  • 9
  • https://stackoverflow.com/questions/34460708/checkoutview-object-has-no-attribute-object – rafaelc Aug 07 '18 at 21:59
  • My process_id is just something like this: "-". When I do... `self.object = Process(process_id=process_id)` `last_id = self.object.get_last_version_id()` ... I'm creating a process with this new id, assigning to self.object, and getting the base process id (that is minus one) just to copy data. I believe this is working as expected. – Hisu Aug 07 '18 at 22:02
  • @RafaelC I cannot do `self.get_object()` because the object is not saved yet on the database. It just gives me a 404. I thought that if I assigned an object to self.object on get, it would still be acessible on post. I'm mistaken? – Hisu Aug 07 '18 at 22:09

0 Answers0