| 1 | from django.contrib.contenttypes.generic import generic_inlineformset_factory |
| 2 | from django.contrib.contenttypes.models import ContentType |
| 3 | from django.test import TestCase |
| 4 | |
| 5 | from models import TaggedItem, ValuableTaggedItem, Comparison, Animal, Vegetable, Mineral |
| 6 | |
| 7 | |
| 8 | class GenericRelationsTests(TestCase): |
| 9 | def test_generic_relations(self): |
| 10 | # Create the world in 7 lines of code... |
| 11 | lion = Animal(common_name="Lion", latin_name="Panthera leo") |
| 12 | platypus = Animal(common_name="Platypus", latin_name="Ornithorhynchus anatinus") |
| 13 | eggplant = Vegetable(name="Eggplant", is_yucky=True) |
| 14 | bacon = Vegetable(name="Bacon", is_yucky=False) |
| 15 | quartz = Mineral(name="Quartz", hardness=7) |
| 16 | for o in (platypus, lion, eggplant, bacon, quartz): |
| 17 | o.save() |
| 18 | |
| 19 | # Objects with declared GenericRelations can be tagged directly -- the API |
| 20 | # mimics the many-to-many API. |
| 21 | bacon.tags.create(tag="fatty") |
| 22 | bacon.tags.create(tag="salty") |
| 23 | lion.tags.create(tag="yellow") |
| 24 | lion.tags.create(tag="hairy") |
| 25 | platypus.tags.create(tag="fatty") |
| 26 | self.assertQuerysetEqual(lion.tags.all(), ["<TaggedItem: hairy>", "<TaggedItem: yellow>"]) |
| 27 | self.assertQuerysetEqual(bacon.tags.all(), ["<TaggedItem: fatty>", "<TaggedItem: salty>"]) |
| 28 | |
| 29 | # You can easily access the content object like a foreign key. |
| 30 | t = TaggedItem.objects.get(tag="salty") |
| 31 | self.assertEqual(repr(t.content_object), "<Vegetable: Bacon>") |
| 32 | |
| 33 | # Recall that the Mineral class doesn't have an explicit GenericRelation |
| 34 | # defined. That's OK, because you can create TaggedItems explicitly. |
| 35 | tag1 = TaggedItem(content_object=quartz, tag="shiny") |
| 36 | tag2 = TaggedItem(content_object=quartz, tag="clearish") |
| 37 | tag1.save() |
| 38 | tag2.save() |
| 39 | |
| 40 | # However, excluding GenericRelations means your lookups have to be a bit more |
| 41 | # explicit. |
| 42 | ctype = ContentType.objects.get_for_model(quartz) |
| 43 | q = TaggedItem.objects.filter(content_type__pk=ctype.id, object_id=quartz.id) |
| 44 | self.assertQuerysetEqual(q, ["<TaggedItem: clearish>", "<TaggedItem: shiny>"]) |
| 45 | |
| 46 | # You can set a generic foreign key in the way you'd expect. |
| 47 | tag1.content_object = platypus |
| 48 | tag1.save() |
| 49 | self.assertQuerysetEqual(platypus.tags.all(), ["<TaggedItem: fatty>", "<TaggedItem: shiny>"]) |
| 50 | q = TaggedItem.objects.filter(content_type__pk=ctype.id, object_id=quartz.id) |
| 51 | self.assertQuerysetEqual(q, ["<TaggedItem: clearish>"]) |
| 52 | |
| 53 | # Queries across generic relations respect the content types. Even though there are two TaggedItems with a tag of "fatty", this query only pulls out the one with the content type related to Animals. |
| 54 | self.assertQuerysetEqual(Animal.objects.order_by('common_name'), [ |
| 55 | "<Animal: Lion>", |
| 56 | "<Animal: Platypus>" |
| 57 | ] |
| 58 | ) |
| 59 | self.assertQuerysetEqual(Animal.objects.filter(tags__tag='fatty'), ["<Animal: Platypus>"]) |
| 60 | self.assertQuerysetEqual(Animal.objects.exclude(tags__tag='fatty'), ["<Animal: Lion>"]) |
| 61 | |
| 62 | # If you delete an object with an explicit Generic relation, the related |
| 63 | # objects are deleted when the source object is deleted. |
| 64 | # Original list of tags: |
| 65 | comp_func = lambda obj: (obj.tag, repr(obj.content_type), obj.object_id) |
| 66 | self.assertQuerysetEqual(TaggedItem.objects.all(), [ |
| 67 | (u'clearish', "<ContentType: mineral>", 1), |
| 68 | (u'fatty', "<ContentType: vegetable>", 2), |
| 69 | (u'fatty', "<ContentType: animal>", 1), |
| 70 | (u'hairy', "<ContentType: animal>", 2), |
| 71 | (u'salty', "<ContentType: vegetable>", 2), |
| 72 | (u'shiny', "<ContentType: animal>", 1), |
| 73 | (u'yellow', "<ContentType: animal>", 2) |
| 74 | ], |
| 75 | comp_func |
| 76 | ) |
| 77 | lion.delete() |
| 78 | self.assertQuerysetEqual(TaggedItem.objects.all(), [ |
| 79 | (u'clearish', "<ContentType: mineral>", 1), |
| 80 | (u'fatty', "<ContentType: vegetable>", 2), |
| 81 | (u'fatty', "<ContentType: animal>", 1), |
| 82 | (u'salty', "<ContentType: vegetable>", 2), |
| 83 | (u'shiny', "<ContentType: animal>", 1) |
| 84 | ], |
| 85 | comp_func |
| 86 | ) |
| 87 | |
| 88 | # If Generic Relation is not explicitly defined, any related objects |
| 89 | # remain after deletion of the source object. |
| 90 | quartz.delete() |
| 91 | self.assertQuerysetEqual(TaggedItem.objects.all(), [ |
| 92 | (u'clearish', "<ContentType: mineral>", 1), |
| 93 | (u'fatty', "<ContentType: vegetable>", 2), |
| 94 | (u'fatty', "<ContentType: animal>", 1), |
| 95 | (u'salty', "<ContentType: vegetable>", 2), |
| 96 | (u'shiny', "<ContentType: animal>", 1) |
| 97 | ], |
| 98 | comp_func |
| 99 | ) |
| 100 | # If you delete a tag, the objects using the tag are unaffected |
| 101 | # (other than losing a tag) |
| 102 | tag = TaggedItem.objects.get(id=1) |
| 103 | tag.delete() |
| 104 | self.assertQuerysetEqual(bacon.tags.all(), ["<TaggedItem: salty>"]) |
| 105 | self.assertQuerysetEqual(TaggedItem.objects.all(), [ |
| 106 | (u'clearish', "<ContentType: mineral>", 1), |
| 107 | (u'fatty', "<ContentType: animal>", 1), |
| 108 | (u'salty', "<ContentType: vegetable>", 2), |
| 109 | (u'shiny', "<ContentType: animal>", 1) |
| 110 | ], |
| 111 | comp_func |
| 112 | ) |
| 113 | TaggedItem.objects.filter(tag='fatty').delete() |
| 114 | ctype = ContentType.objects.get_for_model(lion) |
| 115 | self.assertQuerysetEqual(Animal.objects.filter(tags__content_type=ctype), ["<Animal: Platypus>"]) |
| 116 | |
| 117 | |
| 118 | def test_multiple_gfk(self): |
| 119 | # Simple tests for multiple GenericForeignKeys |
| 120 | # only uses one model, since the above tests should be sufficient. |
| 121 | tiger = Animal(common_name="tiger") |
| 122 | cheetah = Animal(common_name="cheetah") |
| 123 | bear = Animal(common_name="bear") |
| 124 | for o in [tiger, cheetah, bear]: |
| 125 | o.save() |
| 126 | |
| 127 | # Create directly |
| 128 | Comparison(first_obj=cheetah, other_obj=tiger, comparative="faster").save() |
| 129 | Comparison(first_obj=tiger, other_obj=cheetah, comparative="cooler").save() |
| 130 | |
| 131 | # Create using GenericRelation |
| 132 | tiger.comparisons.create(other_obj=bear, comparative="cooler") |
| 133 | tiger.comparisons.create(other_obj=cheetah, comparative="stronger") |
| 134 | self.assertQuerysetEqual(cheetah.comparisons.all(), ["<Comparison: cheetah is faster than tiger>"]) |
| 135 | |
| 136 | # Filtering works |
| 137 | self.assertQuerysetEqual(tiger.comparisons.filter(comparative="cooler"), [ |
| 138 | "<Comparison: tiger is cooler than cheetah>", |
| 139 | "<Comparison: tiger is cooler than bear>" |
| 140 | ] |
| 141 | ) |
| 142 | |
| 143 | # Filtering and deleting works |
| 144 | subjective = ["cooler"] |
| 145 | tiger.comparisons.filter(comparative__in=subjective).delete() |
| 146 | self.assertQuerysetEqual(Comparison.objects.all(), [ |
| 147 | "<Comparison: cheetah is faster than tiger>", |
| 148 | "<Comparison: tiger is stronger than cheetah>" |
| 149 | ] |
| 150 | ) |
| 151 | |
| 152 | # If we delete cheetah, Comparisons with cheetah as 'first_obj' will be deleted |
| 153 | # since Animal has an explicit GenericRelation to Comparison through first_obj. |
| 154 | # Comparisons with cheetah as 'other_obj' will not be deleted. |
| 155 | cheetah.delete() |
| 156 | self.assertQuerysetEqual(Comparison.objects.all(), ["<Comparison: tiger is stronger than None>"]) |
| 157 | |
| 158 | def test_gfk_subclasses(self): |
| 159 | # GenericForeignKey should work with subclasses (see #8309) |
| 160 | quartz = Mineral.objects.create(name="Quartz", hardness=7) |
| 161 | valuedtag = ValuableTaggedItem(content_object=quartz, tag="shiny", value=10) |
| 162 | valuedtag.save() |
| 163 | self.assertEqual(repr(valuedtag.content_object), "<Mineral: Quartz>") |
| 164 | |
| 165 | def test_generic_inline_formsets(self): |
| 166 | GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1) |
| 167 | formset = GenericFormSet() |
| 168 | self.assertEqual(u''.join([form.as_p() for form in formset.forms]), u"""<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-0-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-0-tag" maxlength="50" /></p> |
| 169 | <p><label for="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-0-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-0-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-0-id" id="id_generic_relations-taggeditem-content_type-object_id-0-id" /></p>""") |
| 170 | |
| 171 | formset = GenericFormSet(instance=Animal()) |
| 172 | self.assertEqual(u''.join([form.as_p() for form in formset.forms]), u"""<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-0-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-0-tag" maxlength="50" /></p> |
| 173 | <p><label for="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-0-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-0-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-0-id" id="id_generic_relations-taggeditem-content_type-object_id-0-id" /></p>""") |
| 174 | |
| 175 | platypus = Animal(common_name="Platypus", latin_name="Ornithorhynchus anatinus") |
| 176 | platypus.save() |
| 177 | platypus.tags.create(tag="shiny") |
| 178 | GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1) |
| 179 | formset = GenericFormSet(instance=platypus) |
| 180 | tagged_item_id = TaggedItem.objects.get(tag='shiny', object_id=platypus.id).id |
| 181 | self.assertEqual(u''.join([form.as_p() for form in formset.forms]), u"""<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-0-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-0-tag" value="shiny" maxlength="50" /></p> |
| 182 | <p><label for="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-0-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-0-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-0-id" value="%s" id="id_generic_relations-taggeditem-content_type-object_id-0-id" /></p><p><label for="id_generic_relations-taggeditem-content_type-object_id-1-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-1-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-1-tag" maxlength="50" /></p> |
| 183 | <p><label for="id_generic_relations-taggeditem-content_type-object_id-1-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-1-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-1-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-1-id" id="id_generic_relations-taggeditem-content_type-object_id-1-id" /></p>""" % tagged_item_id) |
| 184 | |
| 185 | lion = Animal(common_name="Lion", latin_name="Panthera leo") |
| 186 | lion.save() |
| 187 | formset = GenericFormSet(instance=lion, prefix='x') |
| 188 | self.assertEqual(u''.join([form.as_p() for form in formset.forms]), u"""<p><label for="id_x-0-tag">Tag:</label> <input id="id_x-0-tag" type="text" name="x-0-tag" maxlength="50" /></p> |
| 189 | <p><label for="id_x-0-DELETE">Delete:</label> <input type="checkbox" name="x-0-DELETE" id="id_x-0-DELETE" /><input type="hidden" name="x-0-id" id="id_x-0-id" /></p>""") |