2

I am trying to use Django Generic Class-Based Views to build a CRUD interface to a two-model database. I have a working CRUD interface to the parent model, and am stuck trying to get the child Create working. For consistency with other Django examples, take the parent to be Author and the child to be Book. What is the simplest way to allow users to add Books to an Author?

In HTML terms, I think that I want to make a link on the Author detail page that includes the ID of the Author, have that ID be pre-set on the Book form, and then have the Book form processing use that ID as the PK of the Book. But I don't understand how to use Django to make this happen. I have read through https://docs.djangoproject.com/en/1.6/topics/class-based-views/generic-editing/, How do I use CreateView with a ModelForm, How do I set initial data on a Django class based generic createview with request data, and Set initial value to modelform in class based generic views, each of which seems to answer a slightly different question.

Here is the relevant code:

models.py

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    author = models.ForeignKey(Author)
    title = models.CharField(max_length=500)

views.py

class BookCreate(CreateView):
    form_class = BookForm

    def get_success_url(self):
        return reverse('myapp:author_read',args=(self.object.author.pk))

forms.py

class BookForm(forms.Modelform):
    class Meta:
        model = Book

urls.py

url(r'^(?P<pk>\d+)/$', AuthorRead.as_view(), name='author_read'),
url(r'^book/create/(?P<author_id>\d+)/$', BookCreate.as_view(), name='book_create'),

templates/myapp/author_detail.html

...
<p><a href="{% url 'myapp:book_create' author_id=Author.pk %}">Add a book</a></p>
...

templates/myapp/book_form.html

<form action="" method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Done">
</form>

Questions

1) How do I get the Author ID from the Book page URL to the Author form, and then processed correctly? With the sample code above, the Django debugger shows that it's present in this way:

View function            Arguments      Keyword arguments         URL name
myapp.views.BookCreate   ()             {'author_id': u'1234'}    book_create

but I don't understand how to grab that variable out of the ... context? ... and put it into the form.

1a) Can I make it a url parameter instead of part of the URL itself, i.e., book/create?author=1234 instead of book/create/1234/? Or even make the whole thing a POST so that it's not part of the URL? Which is the best practice, and how is it done?

2) Once the variable is in the form, how can it be present as a hidden input, so that the user doesn't have to see it?

Community
  • 1
  • 1
Joel Aufrecht
  • 442
  • 1
  • 5
  • 16

3 Answers3

2

With the url that you defined in author_detail.html the author_id variable will be accessible in the view as self.kwargs['author_id']

# views.py
class BookCreate(CreateView):
...
def form_valid(self, form):
    book = form.save(commit=False)
    author_id = form.data['author_id']
    author = get_object_or_404(Author, id=author_id)
    book.author = author
    return super(BookCreate, self).form_valid(form)
...
def get_context_data(self, **kwargs):
    context = super(BookCreate, self).get_context_data(**kwargs)
    context['a_id'] = self.kwargs['author_id']
    return context

Then in your form you can add:

class BookForm(forms.Modelform):
    ...
    def __init__(self, *args, **kwargs):
        self.fields["author_id"] = forms.CharField(widget=forms.HiddenInput())
        super(BookForm, self).__init__(self, *args, **kwargs)

Then in the template:

  <input type=hidden name="author_id" value="{{ a_id }}">

The form_valid in the view should retrieve the id, get the appropriate author and set that author as the books author. The commit=False prevents the model getting saved at first while you set the author and calling super will result in form.save(commit=True) being called.

tomwalker
  • 338
  • 3
  • 8
  • I have implemented this, which as I understand it passes the author_id along until the form is processed after submission by AuthorCreate, which then applies author_id to set book.author before sending the book object to the database. However, the book form renders with the author dropdown input, which shows ---- by default, and when I submit, the form is invalid and reloads because "This field is required." How do I hide or skip the author field so that the form can be submitted? – Joel Aufrecht Jul 27 '14 at 18:01
  • add `fields = ['title']` under `class Meta:` [https://docs.djangoproject.com/en/1.6/topics/forms/modelforms/#selecting-the-fields-to-use](https://docs.djangoproject.com/en/1.6/topics/forms/modelforms/#selecting-the-fields-to-use) – tomwalker Jul 27 '14 at 18:21
  • Great - that solved the form problem. However, now it stops on the line `author_id = form.author_id` with the error `'BookForm' object has no attribute 'author_id'`. I can still see the author_id in the Request debugging info as described in my question; why isn't it going through? Is it because get_initial doesn't execute on the submit? I'm also not sure how to debug the form to see if, in the template, it's receiving author_id. `{{ author_id }}` comes back empty, as does `{{ form.author_id }}`. – Joel Aufrecht Jul 27 '14 at 18:44
  • I changed the answer to make it a bit more straight forward – tomwalker Jul 27 '14 at 19:09
  • The form now loads, the field is hidden, but the same error happens, on the same line, `author_id = form.author_id`: `'BookForm' object has no attribute 'author_id'`. I can see author_id in the POST data in the debug toolbar, so I'm trying to figure out how to grab it from within the `form_valid` function. Also, it took me a little while to figure out that the __init__ function needs to be within the `Meta` subclass of the Form. – Joel Aufrecht Jul 27 '14 at 19:37
  • Still digging with the debugger; author_id is not in form.fields but it is in form.data. – Joel Aufrecht Jul 27 '14 at 20:43
  • I had to make one more change, and now it works! The change is, instead of `author_id = form.author_id`, `author_id = form.data['author_id']`. – Joel Aufrecht Jul 27 '14 at 23:29
  • Great, I updated the answer as well so that it can help anyone with a similar problem. Glad I could help and good luck with the rest of your project. – tomwalker Jul 28 '14 at 10:13
0

You could pass the author id to the form, here's some directions:

class BookForm(forms.Modelform):
    author = None

    class Meta:
        model = Book

    def __init__(self, *args, **kwargs):
        self.author = kwargs.pop('author')
        super(BookForm, self).__init__(*args, **kwargs)

    def save(self, commit=True):
        # your custom save (returns book)


class BookCreate(CreateView):
    form_class = BookForm

    def get_form_kwargs(self):
        kwargs = super(BookCreate, self).get_form_kwargs()
        kwargs['author'] = # pass your author object

        return kwargs
Hedde van der Heide
  • 21,841
  • 13
  • 71
  • 100
0

I had a similar situation and, when doing the accepted answer steps I encountered 2 errors (I'm using Python 2.7):

  1. object has no attribute 'fields' which was fixed by using answer to a similar question: https://stackoverflow.com/a/8928501/2097023 from @scotchandsoda:

...self.fields should be placed before calling super(...)

def __init__(self, users_list, **kw):
    super(BaseWriteForm, self).__init__(**kw)
    self.fields['recipients'].queryset = User.objects.filter(pk__in=users_list)
  1. object has no attribute 'get' which was fixed using answer: https://stackoverflow.com/a/36951830/2097023 from @luke_dupin:

...this error can also be generated by incorrectly passing arguments in the init of a form, which is used for an admin model.

Example:

class MyForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(MyForm, self).__init__(self, *args, **kwargs)

Notice the double passing of self? It should be:

class MyForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(MyForm, self).__init__(*args, **kwargs)
Community
  • 1
  • 1
  • Links to a solution are welcome, but please ensure your answer is useful without it: [add context around the link](http://meta.stackexchange.com/a/8259) so your fellow users will have some idea what it is and why it’s there, then quote the most relevant part of the page you're linking to in case the target page is unavailable. [Answers that are little more than a link may be deleted](http://stackoverflow.com/help/deleted-answers). Link-only answers can become invalid if the linked page changes. – mrun Apr 23 '17 at 05:36