Search code examples
djangodjango-formsformsetinline-formset

Complex multi-model object creation with CreateView in Django


I have a setup where the user can define products (like 'MacBook Pro'). Each Product gets "instantiated" to create a physical item (ProductInstance) which gets a serial number. Pretty simple so far.

I want the user to be able to define other types of unique identifiers, like MAC addresses, VIN, social security number, etc. I call this a UIDType. Each Product can have zero or more of any UIDType. These need to be differentiated somehow by assigning a name/description and linking to both the Product and the UIDType (UIDProductInfo). To actually assign values I need to assign a value to a UIDProductInfo for a specific ProductInstance -- this is done with the UID model.

When the user instantiates a product (creates a ProductInstance), they should be forced to create one and only one UID for each UIDProductInfo of the Product they're instantiating. I'd like to do it all on a single page. Here's the question: Using a CreateView, how do I do this?

I think I need a ModelForm for the ProductInstance and a formset to handle the UIDs. The forms in the UID formset need to create a UID that's bound to each of the UIDProductInfo objects associated with the Product and to the newly created ProductInstance. Unfortunately, I have no idea how to do that. I tried using the initial parameter when creating the formset in the CreateView's get_context_data method, and a few other more kludgey approaches but I just can't get the formset created properly.

Thanks for your help. The models and my sad attempt at the CreateView.get_context_data method are below

models:

# a product, i.e. Toyota Corolla
class Product(models.Model):
    name = models.CharField(max_length = 255)
    identifiers = models.ManyToManyField('UIDType', related_name = 'products', through = 'UIDProductInfo')

# a physical item, an "instantiation" of a Product. Gets a serial number
class ProductInstance(models.Model):
    serial_number = models.CharField(max_length = 255)
    product = models.ForeignKey(Product, related_name = 'instances')

# a type of unique identifier, like a MAC address
class UIDType(models.Model):
    name = models.CharField(max_length = 255, unique = True)

# a description of a UIDType as it applies to a particular Product.
# for example, a computer might get two MAC addresses, 'Host MAC 1' and 'Host MAC 2'
class UIDProductInfo(models.Model):
    name = models.CharField(max_length = 255)
    type = models.ForeignKey(UIDType, related_name = 'info')
    product = models.ForeignKey(Product, related_name = 'product_identifiers')

# a unique identifier for a ProductInstance
class UID(models.Model):
    value = models.CharField(max_length = 255)
    info = models.ForeignKey(UIDProductInfo, related_name = 'values')
    product_instance = models.ForeignKey(ProductInstance, related_name = 'identifiers')

get_context_data:

def get_context_data(self, **kwargs):
    self.object = None
    context = super(ProductInstanceCreate, self).get_context_data(**kwargs)

    product = get_object_or_404(Product, id = self.kwargs.get('product_pk'))

    uid_info = [ ]

    pi = ProductInstance(product = product)

    for info in pi.product.product_identifiers.all():
        uid = UID(productInstance = pi, id_type = info)
        uid_info.append({'info':uid})

    UIDFormset = inlineformset_factory(ProductInstance, UID,
            fields = ('identifier', 'info',), can_delete = False,
            widgets = { 'info' : HiddenInput() },
            labels = {'identifier' : ''}, extra = 3)

    uid_formset = UIDFormset(
            instance = pi,
            initial = uid_info
            )

    context['identifiers_formset'] = uid_formset

    return context

Solution

  • I've solved this problem using the technique described here rather than using a formset:

    https://collingrady.wordpress.com/2008/02/18/editing-multiple-objects-in-django-with-newforms/