[Django]-Django Forms – Many to Many relationships

12👍

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

2👍

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.

👤AdamKG

0👍

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()
👤Wagh

Leave a comment