David
David

Reputation: 2620

Django Forms - Many to Many relationships

relatively new to Django and trying to piece together standard practice for dealing with M2M relationships in a form. I already have the model and db squared away.

For this example, I've written an app in my project for Articles, and I'm attempting to add Categories. To keep it simple an Article has a title, body, timestamp (not included in form), and Categories. I prefer checkboxes to represent 1 or more categories that an Article can belong to.

So far I have:

models.py

class Category(models.Model):
    category = models.CharField(max_length=100)

    def __unicode__(self):
        return self.category


class Article(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    pub_date = models.DateTimeField(auto_now_add=True)
    category = models.ManyToManyField(Category)

    def __unicode__(self):
        return self.title

views.py

def article_index(request):
    return render_to_response('article_index.html', {'articles': Article.objects.all()})

def article_detail(request, article_id=1):
    return render_to_response('article_detail.html', {'article': Article.objects.get(id=article_id)} )

def article_create(request):
    if request.method == 'POST': # If the form has been submitted...
        form = ArticleForm(request.POST) # A form bound to the POST data
        if form.is_valid(): # All validation rules pass
            article = Article.objects.create(
                title=form.cleaned_data['title'],
                body=form.cleaned_data['body'],
                category=form.cleaned_data['category']
            )
            return redirect('article_index') # Redirect after POST
    else:
        form = ArticleForm() # An unbound form

    return render(request, 'article_form.html', { 'form': form })

forms.py

class ArticleForm(forms.Form):
    title = forms.CharField(required=True)
    body = forms.CharField(required=True, widget=forms.Textarea)
    category = forms.MultipleChoiceField(Category.objects.all(), widget=forms.CheckboxSelectMultiple)

The two item's I'm currently stuck on are:

1) in the view 'article_create', I'm not sure how to create the category(ies) as part of the Article object. In the shell, I had to create the Article with a call to save(), then add each category after that. Do I need to do something similar here, e.g. create the article then iterate through each category? Example code is appreciated.

2) Haven't coded 'article_edit' yet, assuming it will be highly similar to create, but I'm not sure if or how I need to handle the logic for comparing previously selected categories to the current submission. Or, should I just delete all category entries for the article being edited, and re-enter them based on the current submission? That's probably the easiest. Again, sample code for this would help.

Thanks!

Upvotes: 3

Views: 15058

Answers (3)

Mike DeSimone
Mike DeSimone

Reputation: 42825

Comments per file...

models.py

class Category(models.Model):
    category = models.CharField(max_length=100)

The category's name should be named name. A field named category I'd expect to be something like models.ForeignKey("Category").

class Article(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    pub_date = models.DateTimeField(auto_now_add=True)
    category = models.ManyToManyField(Category)

As Adam pointed out, this should be named categories. Further, its reverse (the field in Category that links back to Article) should be named articles. So we get:

    categories = models.ManyToManyField(Category, related_name="articles")

So now you can get a queryset with all the articles in a category with something like:

get_object_or_404(Category, id=int(cat_id, 10)).articles.all()

views.py

def article_detail(request, article_id=1):

Don't use a default here. There's nothing special about the ID 1, and if someone forgets the ID, it should be an error.

def article_create(request):
    if request.method == 'POST': # If the form has been submitted...
        form = ArticleForm(request.POST) # A form bound to the POST data
        if form.is_valid(): # All validation rules pass
            article = Article.objects.create(
                title=form.cleaned_data['title'],
                body=form.cleaned_data['body'],
                category=form.cleaned_data['category']
            )

By using a ModelForm, this is simplified to:

def article_create(request):
    if request.method == 'POST': # If the form has been submitted...
        form = ArticleForm(request.POST) # A form bound to the POST data
        if form.is_valid(): # All validation rules pass
            form.save()
        return redirect('article_index') # Redirect after POST
    else:
        form = ArticleForm() # An unbound form

    return render(request, 'article_form.html', {'form': form})

forms.py

class ArticleForm(forms.Form):

You really should be using ModelForm instead (docs here):

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ["title", "body", "category"]
        widgets = {
            'body': forms.Textarea(),
            'category': forms.CheckboxSelectMultiple()
        }

On to your questions:

1) in the view 'article_create', I'm not sure how to create the category(ies) as part of the Article object. In the shell, I had to create the Article with a call to save(), then add each category after that. Do I need to do something similar here, e.g. create the article then iterate through each category? Example code is appreciated.

IIRC, ModelForm.save() will take care of this for you.

2) Haven't coded 'article_edit' yet, assuming it will be highly similar to create, but I'm not sure if or how I need to handle the logic for comparing previously selected categories to the current submission. Or, should I just delete all category entries for the article being edited, and re-enter them based on the current submission? That's probably the easiest. Again, sample code for this would help.

Editing is almost exactly like creating. All you have to do is associate the original object with the form. (Typically, you figure out what the original object is from the URL.) So something like:

def article_edit(request, article_id):
    article = get_object_or_404(Article, id=int(article_id, 10))

    if request.method == 'POST': # If the form has been submitted...
        form = ArticleForm(request.POST, instance=article)
        if form.is_valid(): # All validation rules pass
            form.save()
        return redirect('article_index') # Redirect after POST
    else:
        form = ArticleForm(instance=article)

    return render(request, 'article_form.html', {'form': form})

EDIT: As jheld comments below, you can combine article_create and article_edit into one view method:

def article_modify(request, article_id=None):
    if article_id is not None:
        article = get_object_or_404(Article, id=int(article_id, 10))
    else:
        article = None

    if request.method == 'POST': # If the form has been submitted...
        form = ArticleForm(request.POST, instance=article)
        if form.is_valid(): # All validation rules pass
            form.save()
        return redirect('article_index') # Redirect after POST
    else:
        form = ArticleForm(instance=article)

    return render(request, 'article_form.html', {'form': form})

Then the URLs are easy:

url(r"^/article/edit/(?P<article_id>[0-9]+)$", "app.views.article_modify", name="edit"),
url(r"^/article/new$", "app.views.article_modify", name="new"),

Upvotes: 12

Wagh
Wagh

Reputation: 4306

You can do like this for example:

  if todo_list_form.is_valid():
                todo_list = todo_list_form.save(commit=False)
                todo_list.save()
                todo_list_form.save_m2m()

Upvotes: 0

AdamKG
AdamKG

Reputation: 14091

I'd start by renaming category in the model to categories, and updating the related code accordingly - the singular naming is just going to be a continuous headache.

At that point, you're pretty close. In your success branch when submitting an article, assign the categories as a separate statement.

article = Article.objects.create(
    title=form.cleaned_data['title'],
    body=form.cleaned_data['body']
)
# note changed plural name on the m2m attr & form field
article.categories.add(*form.cleaned_data['categories'])
# alternately
# for cat in form.cleaned_data['categories']:
#     article.categories.add(cat)
return redirect('article_index') # Redirect after POST

Oh, and, kudos on avoiding ModelForm. It's muuch easier to hook up the form-instance plumbing yourself, this question would be much more complicated with ModelForm involved.

For the edit view, yes, clear & re-adding is easiest. There are more efficient ways, but nothing that's worth the complexity until it's actually a problem. The method call to clear will be article.categories.clear(), re-adding is same as above.

Upvotes: 2

Related Questions