Search code examples
pythondjangodjango-modelsmany-to-many

Django: How to check if object with ManyToManyField contains the entire contents of other object ManyToManyField?


class Hero(models.Model):
   talents = models.ManyToManyField(Talent)
   (...)

class Talent(models.Model)
   required_talents = models.ManyToManyField('self', symmetrical=False)
   (...)

I would like to create method has_required_talents(self, talent) for Hero, which will check if this Hero has required_talents for chosen Talent.

I tried this:

def has_required_talents(self, talent)
   required_list = talent.talents_required.values_list('id', flat=True)
   hero_talents = self.hero.talents.values_list('id', flat=True)
   if required_list in hero_talents:
      return True
   return False

However, it doesn't work properly when I test it, using these tests:

class HeroTalents(TestCase):
 def setUp(self):
    self.hero=Hero('Duck')
    self.hero.save() 

 def test_has_required_talents(self):
    self.talent1 = Talent(name = "Overpower")
    self.talent1.save()
    self.talent2 = Talent(name = "Overpower2")
    self.talent2.save()
    self.talent2.talents_required.add(self.talent1)
    self.assertFalse(self.hero.has_required_talents(self.talent2), "test1")
    self.hero.talents.add(self.talent1)
    self.assertTrue(self.hero.has_required_talents(self.talent2), "test2")
    self.talent3 = Talent(name = "Overpower3")
    self.talent3.save()
    self.hero.talents.add(self.talent2)
    self.assertTrue(self.hero.has_required_talents(self.talent3), "test3")
    self.talent1.talents_required.add(self.talent3)
    self.assertFalse(self.hero.has_required_talents(self.talent1), "test4")

What needs to be done to make this work?


Solution

  • def has_required_talents(self, talent)
       required_list = talent.talents_required.values_list('id', flat=True)
       hero_talents = self.hero.talents.values_list('id', flat=True)
       for item in required_list:
          if not item in hero_talents:
             return False
       return True
    

    That would be a list-based approach. You can also convert them to sets:

    def has_required_talents(self, talent)
       required_list = set(talent.talents_required.values_list('id', flat=True))
       hero_talents = set(self.hero.talents.values_list('id', flat=True))
       if required_list.issubset(hero_talents):
          return True
       return False
    

    But (I don't know about exact internals, you could run some tests) the first approach should be quicker, because there is no conversion.