pythondjangocreate-view

How can I add an instruction after an object creation in Django CreateView?


On my generic class based CreateView, I would like to perform an instruction which creates new objects from another model related to the current created object.

Example: Collection is a model which is related to 3 differents Element objects. When I create a MyCollection object, which is related to a Collection and a User, 3 MyElement object must be created too and related to the newly created MyCollection object. One for each Element related to Collection.

# models.py

from django.contrib.auth.models import User
from django.db import models


class Collection(models.Model):
    # attributes and methods...


class Element(models.Model):
    collection = models.ForeignKey(Collection, # more arguments...)
    # more attributes and methods


class MyCollection(models.Model):
    user = models.ForeignKey(User, # more arguments...)
    collection = = models.ForeignKey(Collection, # more arguments...)
    # more attributes and methods

    def get_absolute_url(self):
        reverse(# some url with the object pk)


class MyElement(models.Model):
    element = models.ForeignKey(Element, # more arguments...)
    my_collection = models.ForeignKey(MyCollection, # more arguments...)
    # more attributes and methods

I'm using the generic CreateView from Django and after a little bit of research, I saw that it was possible to perform additional actions in the CreateView by overriding the get_success_url() method.

In my example, I did something similar:

# views.py

from django.views.generic import CreateView

from .utils import create_myelements_for_mycollection


class MyCollectionCreateView(CreateView):
    model = MyCollection
    # more attributes and methods...

    def get_success_url(self):
        create_myelements_for_mycollection(self.get_object())  # Here is where the bug occurs, works fine without this line

        return super().get_success_url()
# utils.py

from .models import Collection, Element, MyCollection, MyElement


def create_myelements_for_mycollection(my_collection):
    for element in my_collection.collection.elements_set.all():
        MyElement.objects.create(
            element=element,
            my_collection=my_collection,
        )
# urls.py

from django.urls import re_path

from . import views


urlpatterns = [
    re_path(
        r"^myelement/l/$",
        views.MyElementListView.as_view(),
    ),
    re_path(
        r"^myelement/r/(?P<pk>[0-9]+)/$",
        views.MyElementDetailView.as_view(),
    ),
    re_path(
        r"^myelement/c/(?P<element_pk>\d+)/$",
        views.MyElementCreateView.as_view(),
    ),

    re_path(
        r"^mycollection/l/$",
        views.MyCollectionListView.as_view(),
    ),
    re_path(
        r"^mycollection/r/(?P<pk>[0-9]+)/$",
        views.MyCollectionDetailView.as_view(),
    ),
    re_path(
        r"^mycollection/c/(?P<collection_pk>\d+)/$",
        views.MyCollectionCreateView.as_view(),
    ),
]

When I create a new MyCollection object, all the MyElements are successfully created in the database but I got this error:

AttributeError: Generic detail view MyCollectionCreateView must be called with either an object pk or a slug in the URLconf.

I don't understand why.

My CreateView url doesn't have any pk because when you create a new object, it doesn't have a pk yet. Also, MyCollection has it's own get_absolute_url. Without that specific instruction, I works fine.

Can someone explain me what causes the error and if there is a better way to perform an instruction like this after the object creation ?

Thank you for your help.

FYI:

Django 3.1
Pyhton 3.8

EDIT

I tried to use a post_save signal instead (which is actually way cleaner to write), but I'm stuck with exactly the same error.

# models.py

from django.db.models.signals import post_save
from django.dispatch import receiver

from .utils import create_myelements_for_mycollection


# ...models

@receiver(post_save, sender=MyCollection)
def create_myelements_for_mycollection(sender, instance, **kwargs):

    if instance is created:  # ? (Didn't look at how to write this condition in a signal yet)
        create_myelements_for_mycollection(my_collection)

AttributeError: Generic detail view MyCollectionCreateView must be called with either an object pk or a slug in the URLconf.

EDIT 2

By removing the get_absolute_url() override from the MyCollectionCreateView class, it actually works. It's great but I would still like to know what the issue was. Probably something stupid I didn't see, if this solved the issue.

# models.py

# ... previous code

class MyCollectionCreateView(CreateView):
    model = MyCollection
    # more attributes and methods...

    # Removing this solved the issue
    # def get_success_url(self):
    #     create_myelements_for_mycollection(self.get_object())
    #
    #     return super().get_success_url()

# more code ...

Solution

  • get_absolute_url() is used when a new instance is made for a model, as django must know where to go when a new post is created or a new instance is created.

    From the error

    AttributeError: Generic detail view MyCollectionCreateView must be called with either an object pk or a slug in the URLconf.

    It tells you that it must be called with an object pk or a slug, as such your construction of the URL is where the problem lies. I am not sure if the method create_myelements_for_mycollection() caters that need.

    Ideally what you need is something like, e.g.

    def get_absolute_url(self):
        return f"/mycollection/{self.slug}/"
    

    to generate the URL in the above pattern to be used for example in an anchor tag.