1

I've searched around for a while, but can't seem to find an existing question for this (although it could be an issue of not knowing terminology).

I'm new to Django, and have been attempting to take a design which should be very expandable over time, and make it work with Django's ORM. Essentially, it's a series of many-to-many relationships using a shared junction table.

The design is a generic game crafting system, which says "if you meet [require], you can create [reward] using [cost] as materials." This allows items to be sold from any number of shops using the same system, and is generic enough to support a wide range of mechanics - I've seen it used successfully in the past.

Django doesn't support multiple M2M relationships sharing the same junction table (apparently since it has no way to work out the reverse relationship), so I seem to have these options:

  • Let it create its own junction tables, which ends up being six or more, or
  • Use foreign keys to the junction table in place of a built-in MTM relationship.

The first option is a bit of a mess, since I know I'll eventually have to add additional fields into the junction tables. The second option works pretty well. Unfortunately, because there is no foreign key from the junction table BACK to each of the other tables, I'm constantly fighting the admin system to get it to do what I want.

Here are the affected models:

class Craft(models.Model):
    name        = models.CharField(max_length=30)
    description = models.CharField(max_length=300, blank=True)
    cost        = models.ForeignKey('Container', related_name="craft_cost")
    reward      = models.ForeignKey('Container', related_name="craft_reward")
    require     = models.ForeignKey('Container', related_name="craft_require")

class ShopContent(models.Model):
    shopId      = models.ForeignKey(Shop)
    cost        = models.ForeignKey('Container', related_name="shop_cost")
    reward      = models.ForeignKey('Container', related_name="shop_reward")
    require     = models.ForeignKey('Container', related_name="shop_require")
    description = models.CharField(max_length=300)

class Container(models.Model):
    name        = models.CharField(max_length=30)

class ContainerContent(models.Model):
    containerId = models.ForeignKey(Container, verbose_name="Container")
    itemId      = models.ForeignKey(Item, verbose_name="Item")
    itemMin     = models.PositiveSmallIntegerField(verbose_name=u"min amount")
    itemMax     = models.PositiveSmallIntegerField(verbose_name=u"max amount")
    weight      = models.PositiveSmallIntegerField(null=True, blank=True)
    optionGroup = models.PositiveSmallIntegerField(null=True, blank=True,
                                                   verbose_name=u"option group")

Is there a simpler, likely obvious way to get this working? I'm attempting to allow inline editing of ContainerContent information from each related column on the Craft edit interface.

ThreeHams
  • 125
  • 1
  • 7

2 Answers2

3

It sounds like you have a sort of "Transaction" that has a name, description, and type, and defines a cost, reward, and requirement. You should define that as a single model, not multiple ones (ShopContent, Craft, etc.).

class Transaction(models.Model):
    TYPE_CHOICES = (('Craft', 0),
                    ('Purchase', 1),
                   )
    name        = models.CharField(max_length=30)
    description = models.CharField(max_length=300, blank=True)
    cost        = models.ForeignKey('Container')
    reward      = models.ForeignKey('Container')
    require     = models.ForeignKey('Container')
    type        = models.IntegerField(choices = TYPE_CHOICES)

Now Shop etc. can have a single ManyToManyField to Transaction.

Whether or not you use this particular model, the cost, reward and require relationships should all be in one place -- as above, or in OneToOne relationships with Craft, ShopContent etc. As you guessed, you shouldn't have a whole bunch of complex Many-To-Many through tables that are all really the same.


You mention at the bottom of your post that you're

attempting to allow inline editing of ContainerContent information from each related column on the Craft edit interface.

If you're modeling several levels of relationship, and using the admin app, you'll need to either apply some sort of nested inline patch, or use some sort of linking scheme like the one I use in my recent question, How do I add a link from the Django admin page of one object to the admin page of a related object?

Community
  • 1
  • 1
agf
  • 171,228
  • 44
  • 289
  • 238
  • Another version of the linking: http://stackoverflow.com/questions/10115137/in-the-django-admin-is-there-a-way-to-show-a-list-of-actual-links-to-a-models – agf Apr 12 '12 at 19:56
  • Nested inline information is really useful. I edited the question to show the relationship problem a bit better, though - multiple tables will be going through this one. Still, if I end up doing this, it would cut the number of tables needed by a third! – ThreeHams Apr 12 '12 at 19:57
  • @ThreeHams OK, if you have the same relationship multiple times, you need to only model it once. Either generalize to a single model as in my example, or use either explicit `OneToOne` fields, or implicit `OneToOne` fields with subclassing. – agf Apr 12 '12 at 20:23
  • 1
    The transaction change actually works really well. Hopefully, it'll stand up to changes down the line, but it cuts two tables and three complex relationships from the design. Thanks for the help! As far as inline editing, I've realized that the scope of the CMS I'm planning falls pretty far outside of the built-in admin interface. Considering how many limitations I keep hitting, it's probably best to use the existing one as a guide, and build mine from scratch. – ThreeHams Apr 13 '12 at 01:38
1

I am smelling something is too complicated here, but I might be wrong. As a start, is this any better? (ContainerContent will be figured out later)

class Cost(models.Model):
    name        = models.CharField(max_length=30)

class Reward(models.Model):
    name        = models.CharField(max_length=30)

class Require(models.Model):
    name        = models.CharField(max_length=30)

class Craft(models.Model):
    name        = models.CharField(max_length=30)
    description = models.CharField(max_length=300, blank=True)
    cost        = models.ForeignKey(Cost)
    reward      = models.ForeignKey(Reward)
    require     = models.ForeignKey(Require)

class Shop(models.Model):
    name        = models.CharField(max_length=30)
    crafts      = models.ManyToMany(Craft, blank=True)
Udi
  • 29,222
  • 9
  • 96
  • 129