3

I'd like to create a relationship for Django ORM in which I can add objects from a Set, with data associated in that relationship, but only add each item once to any given container. I mean to use the term Set, defined as follows:

A set is a well defined collection of distinct objects.

Each item in the set, SetItem, is unique within the set. I am ensuring that they are unique in this case by having its fields defined with the unique=True kwarg at the class definition. The container for these items, SetItemContainer, has a relationship to the SetItem which permit the container to associate some data with the SetItemRelationship.

It is a ManyToMany Relationship, but with a twist that there can only be one of each SetItem A, B, C, or D in any individual SetContainer. Using the Pizza and Toppings analogy, Each Pizza can have any amount of Toppings, Every Topping can as many Pizzas as want it, but in this case No Topping can be added to any individual Pizza more than once, i.e. You can't add two "Anchovies" Toppings on any individual Pizza (That's what the "data" in the SetItemRelationship is for).

You can see a proposed pattern in this app's models.py

from django.db import models

class SetItem(models.Model):
    text = models.CharField(max_length=32, unique=True)

    def __unicode__(self):
        return self.text

class SetContainer(models.Model):
    label = models.CharField(max_length=64)
    set_items = models.ManyToManyField(SetItem, through='SetItemRelationship')

    def __unicode__(self):
        return self.label

class SetItemRelationship(models.Model):
    container = models.ForeignKey(SetContainer)
    item = models.ForeignKey(SetItem)
    data = models.PositiveSmallIntegerField()

    def __unicode__(self):
        return "{:s} contains {:s}".format(self.container.label, self.item.text)

This model relationship permits me to create multiple SetContainer objects, each of which have their own instances of the SetItem objects with data associated with them. However, I would like to be limited to only adding one instance of each relationship. I've attempted to figure this out, using the following admin.py:

from django.contrib import admin
from .models import SetItem, SetContainer, SetItemRelationship

class SetInline(admin.TabularInline):
    model = SetItemRelationship

class SetContainerAdmin(admin.ModelAdmin):
    inlines = [SetInline]

admin.site.register(SetContainer, SetContainerAdmin)
admin.site.register(SetItem)

As you can see in the following screen cap, I am capable of adding two SetItemRelationships to the Unique SetItem which has SetItem.text == A

How can I set up a relationship to prevent that addition, and ideally, to prevent the drop down from containing any of the previously used SetItems when adding a new SetItemRelationship.

django-admin showing multiple instances of a SetItem in a SetContainer

Edit: I added an explanatory paragraph including the Pizza and Toppings analogy, modified to help explain this relationship.

OYRM
  • 1,395
  • 10
  • 29
  • you can do one way. make `SetItem` model as a parent of `SetContainer` by define this way `class SetContainer(SetItem):` also you can remove the extra foreign key from `SetItemRelationship` model. The inheritance concept can be applied. – MegaBytes May 13 '15 at 15:56
  • Which of the two `ForeignKey` fields in `SetItemRelationship` are you saying can be removed ? I've tried each without success, the makemigrations process fails. What's more, it seems to be a very strange idiom to subclass `SetItem` as `SetContainer`. Perhaps you can expand on the concept – OYRM May 13 '15 at 16:24
  • you can remove `item = models.ForeignKey(SetItem)` from `SetItemRelationship` class and `set_items = models.ManyToManyField(SetItem, through='SetItemRelationship')` from `SetContainer` class . As I understood, you want to create a `SetItemRelationship` relationship which associate with only single `SetContainer` container of single item of `SetItem`. is it right or am I interpreting wrong? – MegaBytes May 14 '15 at 05:00
  • @MegaBytes No, that's a misinterpretation. I'd like to be able to add multiple SetItems, as in the image, however, only one of each type. So, if I have a total population of SetItems `{A, B, C, D}`, I can add each one, through a SetItemRelationship once and only once. Each SetItemRelationship includes additional data as well, a number associated with the specific SetItemRelationship, containing the SetItem within the SetContainer. It is a ManyToMany Relationship, but with a twist that there can only be one of each SetItem `A`, `B`, `C`, or `D` in any individual SetContainer – OYRM May 14 '15 at 11:34

2 Answers2

3

It seems to me what you need is to make container and item be "unique together" so that the same container cannot be related to the same item more than once. You do this with a Meta class:

class SetItemRelationship(models.Model):
    container = models.ForeignKey(SetContainer)
    item = models.ForeignKey(SetItem)
    data = models.PositiveSmallIntegerField()

    class Meta(object):
        unique_together = (("container", "item"), )

    def __unicode__(self):
        return "{:s} contains {:s}".format(self.container.label, self.item.text)
Louis
  • 146,715
  • 28
  • 274
  • 320
  • that is the idea, but I have a follow up question and an observation. The observation first, for the sake of documentation. In changing the META class in this way, it turns out that I needed to migrate your data in order to execute the schema migration successfully, I edited my database's setexperiment_setrelationships table to ensure that there were no rows violating the unique_together constraint. The question. Is it possible to prune the SetItem list to exclude previously used SetItem(s) when adding a SetItemRelationship. The validation system prevents saving, but this is clunky – OYRM May 17 '15 at 16:36
1

Some ideas:

# signals.py
from django.db.models.signals import pre_save
from django.dispatch import receiver

from app.models import SetItemRelationship

@receiver(pre_save, sender=SetItemRelationship)
def update_relationship_if_exists(sender, **kwargs):
    new_relationship = kwargs['instance']
    old_relationship = (SetItemRelationship.objects
                        .filter(container=instance.container, item=instance.item)
                        .first())
    if old_relationship:
        # force update
        new_relationship.id = old_relationship.id

Where to place signals.py: link.

Community
  • 1
  • 1
f43d65
  • 2,264
  • 11
  • 15
  • This works, however, see the answer from @Louis which seems to be to be simpler and takes advantage of functionality built into the framework seemingly for this purpose. – OYRM May 18 '15 at 01:54