1. from django import forms
    
  2. from django.contrib.contenttypes.forms import generic_inlineformset_factory
    
  3. from django.contrib.contenttypes.models import ContentType
    
  4. from django.db import models
    
  5. from django.test import TestCase
    
  6. from django.test.utils import isolate_apps
    
  7. 
    
  8. from .models import (
    
  9.     Animal,
    
  10.     ForProxyModelModel,
    
  11.     Gecko,
    
  12.     Mineral,
    
  13.     ProxyRelatedModel,
    
  14.     TaggedItem,
    
  15. )
    
  16. 
    
  17. 
    
  18. class CustomWidget(forms.TextInput):
    
  19.     pass
    
  20. 
    
  21. 
    
  22. class TaggedItemForm(forms.ModelForm):
    
  23.     class Meta:
    
  24.         model = TaggedItem
    
  25.         fields = "__all__"
    
  26.         widgets = {"tag": CustomWidget}
    
  27. 
    
  28. 
    
  29. class GenericInlineFormsetTests(TestCase):
    
  30.     def test_output(self):
    
  31.         GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1)
    
  32.         formset = GenericFormSet()
    
  33.         self.assertHTMLEqual(
    
  34.             "".join(form.as_p() for form in formset.forms),
    
  35.             """
    
  36.             <p><label
    
  37.                 for="id_generic_relations-taggeditem-content_type-object_id-0-tag">
    
  38.             Tag:</label>
    
  39.             <input id="id_generic_relations-taggeditem-content_type-object_id-0-tag"
    
  40.                 type="text"
    
  41.                 name="generic_relations-taggeditem-content_type-object_id-0-tag"
    
  42.                 maxlength="50"></p>
    
  43.             <p><label
    
  44.                 for="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">
    
  45.             Delete:</label>
    
  46.             <input type="checkbox"
    
  47.                 name="generic_relations-taggeditem-content_type-object_id-0-DELETE"
    
  48.                 id="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">
    
  49.             <input type="hidden"
    
  50.                 name="generic_relations-taggeditem-content_type-object_id-0-id"
    
  51.                 id="id_generic_relations-taggeditem-content_type-object_id-0-id"></p>
    
  52.             """,
    
  53.         )
    
  54.         formset = GenericFormSet(instance=Animal())
    
  55.         self.assertHTMLEqual(
    
  56.             "".join(form.as_p() for form in formset.forms),
    
  57.             """
    
  58.             <p><label
    
  59.                 for="id_generic_relations-taggeditem-content_type-object_id-0-tag">
    
  60.             Tag:</label>
    
  61.             <input id="id_generic_relations-taggeditem-content_type-object_id-0-tag"
    
  62.                 type="text"
    
  63.                 name="generic_relations-taggeditem-content_type-object_id-0-tag"
    
  64.                 maxlength="50"></p>
    
  65.             <p><label
    
  66.                 for="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">
    
  67.             Delete:</label>
    
  68.             <input type="checkbox"
    
  69.                 name="generic_relations-taggeditem-content_type-object_id-0-DELETE"
    
  70.                 id="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">
    
  71.             <input type="hidden"
    
  72.                 name="generic_relations-taggeditem-content_type-object_id-0-id"
    
  73.                 id="id_generic_relations-taggeditem-content_type-object_id-0-id"></p>
    
  74.             """,
    
  75.         )
    
  76.         platypus = Animal.objects.create(
    
  77.             common_name="Platypus",
    
  78.             latin_name="Ornithorhynchus anatinus",
    
  79.         )
    
  80.         platypus.tags.create(tag="shiny")
    
  81.         GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1)
    
  82.         formset = GenericFormSet(instance=platypus)
    
  83.         tagged_item_id = TaggedItem.objects.get(tag="shiny", object_id=platypus.id).id
    
  84.         self.assertHTMLEqual(
    
  85.             "".join(form.as_p() for form in formset.forms),
    
  86.             """
    
  87.             <p><label
    
  88.                 for="id_generic_relations-taggeditem-content_type-object_id-0-tag">
    
  89.             Tag:</label>
    
  90.             <input id="id_generic_relations-taggeditem-content_type-object_id-0-tag"
    
  91.                 type="text"
    
  92.                 name="generic_relations-taggeditem-content_type-object_id-0-tag"
    
  93.                 value="shiny" maxlength="50"></p>
    
  94.             <p><label
    
  95.                 for="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">
    
  96.             Delete:</label>
    
  97.             <input type="checkbox"
    
  98.                 name="generic_relations-taggeditem-content_type-object_id-0-DELETE"
    
  99.                 id="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">
    
  100.             <input type="hidden"
    
  101.                 name="generic_relations-taggeditem-content_type-object_id-0-id"
    
  102.                 value="%s"
    
  103.                 id="id_generic_relations-taggeditem-content_type-object_id-0-id"></p>
    
  104.             <p><label
    
  105.                 for="id_generic_relations-taggeditem-content_type-object_id-1-tag">
    
  106.             Tag:</label>
    
  107.             <input id="id_generic_relations-taggeditem-content_type-object_id-1-tag"
    
  108.                 type="text"
    
  109.                 name="generic_relations-taggeditem-content_type-object_id-1-tag"
    
  110.                 maxlength="50"></p>
    
  111.             <p><label
    
  112.                 for="id_generic_relations-taggeditem-content_type-object_id-1-DELETE">
    
  113.             Delete:</label>
    
  114.             <input type="checkbox"
    
  115.                 name="generic_relations-taggeditem-content_type-object_id-1-DELETE"
    
  116.                 id="id_generic_relations-taggeditem-content_type-object_id-1-DELETE">
    
  117.             <input type="hidden"
    
  118.                 name="generic_relations-taggeditem-content_type-object_id-1-id"
    
  119.                 id="id_generic_relations-taggeditem-content_type-object_id-1-id"></p>
    
  120.             """
    
  121.             % tagged_item_id,
    
  122.         )
    
  123.         lion = Animal.objects.create(common_name="Lion", latin_name="Panthera leo")
    
  124.         formset = GenericFormSet(instance=lion, prefix="x")
    
  125.         self.assertHTMLEqual(
    
  126.             "".join(form.as_p() for form in formset.forms),
    
  127.             """
    
  128.             <p><label for="id_x-0-tag">Tag:</label>
    
  129.             <input id="id_x-0-tag" type="text" name="x-0-tag" maxlength="50"></p>
    
  130.             <p><label for="id_x-0-DELETE">Delete:</label>
    
  131.             <input type="checkbox" name="x-0-DELETE" id="id_x-0-DELETE">
    
  132.             <input type="hidden" name="x-0-id" id="id_x-0-id"></p>
    
  133.             """,
    
  134.         )
    
  135. 
    
  136.     def test_options(self):
    
  137.         TaggedItemFormSet = generic_inlineformset_factory(
    
  138.             TaggedItem,
    
  139.             can_delete=False,
    
  140.             exclude=["tag"],
    
  141.             extra=3,
    
  142.         )
    
  143.         platypus = Animal.objects.create(
    
  144.             common_name="Platypus", latin_name="Ornithorhynchus anatinus"
    
  145.         )
    
  146.         harmless = platypus.tags.create(tag="harmless")
    
  147.         mammal = platypus.tags.create(tag="mammal")
    
  148.         # Works without a queryset.
    
  149.         formset = TaggedItemFormSet(instance=platypus)
    
  150.         self.assertEqual(len(formset.forms), 5)
    
  151.         self.assertHTMLEqual(
    
  152.             formset.forms[0].as_p(),
    
  153.             '<input type="hidden" '
    
  154.             'name="generic_relations-taggeditem-content_type-object_id-0-id" '
    
  155.             'value="%s" '
    
  156.             'id="id_generic_relations-taggeditem-content_type-object_id-0-id">'
    
  157.             % harmless.pk,
    
  158.         )
    
  159.         self.assertEqual(formset.forms[0].instance, harmless)
    
  160.         self.assertEqual(formset.forms[1].instance, mammal)
    
  161.         self.assertIsNone(formset.forms[2].instance.pk)
    
  162.         # A queryset can be used to alter display ordering.
    
  163.         formset = TaggedItemFormSet(
    
  164.             instance=platypus, queryset=TaggedItem.objects.order_by("-tag")
    
  165.         )
    
  166.         self.assertEqual(len(formset.forms), 5)
    
  167.         self.assertEqual(formset.forms[0].instance, mammal)
    
  168.         self.assertEqual(formset.forms[1].instance, harmless)
    
  169.         self.assertIsNone(formset.forms[2].instance.pk)
    
  170.         # A queryset that omits items.
    
  171.         formset = TaggedItemFormSet(
    
  172.             instance=platypus,
    
  173.             queryset=TaggedItem.objects.filter(tag__startswith="harm"),
    
  174.         )
    
  175.         self.assertEqual(len(formset.forms), 4)
    
  176.         self.assertEqual(formset.forms[0].instance, harmless)
    
  177.         self.assertIsNone(formset.forms[1].instance.pk)
    
  178. 
    
  179.     def test_get_queryset_ordering(self):
    
  180.         """
    
  181.         BaseGenericInlineFormSet.get_queryset() adds default ordering, if
    
  182.         needed.
    
  183.         """
    
  184.         inline_formset = generic_inlineformset_factory(TaggedItem, exclude=("tag",))
    
  185.         formset = inline_formset(instance=Gecko.objects.create())
    
  186.         self.assertIs(formset.get_queryset().ordered, True)
    
  187. 
    
  188.     def test_initial(self):
    
  189.         quartz = Mineral.objects.create(name="Quartz", hardness=7)
    
  190.         GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1)
    
  191.         ctype = ContentType.objects.get_for_model(quartz)
    
  192.         initial_data = [
    
  193.             {
    
  194.                 "tag": "lizard",
    
  195.                 "content_type": ctype.pk,
    
  196.                 "object_id": quartz.pk,
    
  197.             }
    
  198.         ]
    
  199.         formset = GenericFormSet(initial=initial_data)
    
  200.         self.assertEqual(formset.forms[0].initial, initial_data[0])
    
  201. 
    
  202.     def test_meta_widgets(self):
    
  203.         """TaggedItemForm has a widget defined in Meta."""
    
  204.         Formset = generic_inlineformset_factory(TaggedItem, TaggedItemForm)
    
  205.         form = Formset().forms[0]
    
  206.         self.assertIsInstance(form["tag"].field.widget, CustomWidget)
    
  207. 
    
  208.     @isolate_apps("generic_relations")
    
  209.     def test_incorrect_content_type(self):
    
  210.         class BadModel(models.Model):
    
  211.             content_type = models.PositiveIntegerField()
    
  212. 
    
  213.         msg = (
    
  214.             "fk_name 'generic_relations.BadModel.content_type' is not a ForeignKey to "
    
  215.             "ContentType"
    
  216.         )
    
  217.         with self.assertRaisesMessage(Exception, msg):
    
  218.             generic_inlineformset_factory(BadModel, TaggedItemForm)
    
  219. 
    
  220.     def test_save_new_uses_form_save(self):
    
  221.         class SaveTestForm(forms.ModelForm):
    
  222.             def save(self, *args, **kwargs):
    
  223.                 self.instance.saved_by = "custom method"
    
  224.                 return super().save(*args, **kwargs)
    
  225. 
    
  226.         Formset = generic_inlineformset_factory(
    
  227.             ForProxyModelModel, fields="__all__", form=SaveTestForm
    
  228.         )
    
  229.         instance = ProxyRelatedModel.objects.create()
    
  230.         data = {
    
  231.             "form-TOTAL_FORMS": "1",
    
  232.             "form-INITIAL_FORMS": "0",
    
  233.             "form-MAX_NUM_FORMS": "",
    
  234.             "form-0-title": "foo",
    
  235.         }
    
  236.         formset = Formset(data, instance=instance, prefix="form")
    
  237.         self.assertTrue(formset.is_valid())
    
  238.         new_obj = formset.save()[0]
    
  239.         self.assertEqual(new_obj.saved_by, "custom method")
    
  240. 
    
  241.     def test_save_new_for_proxy(self):
    
  242.         Formset = generic_inlineformset_factory(
    
  243.             ForProxyModelModel, fields="__all__", for_concrete_model=False
    
  244.         )
    
  245.         instance = ProxyRelatedModel.objects.create()
    
  246.         data = {
    
  247.             "form-TOTAL_FORMS": "1",
    
  248.             "form-INITIAL_FORMS": "0",
    
  249.             "form-MAX_NUM_FORMS": "",
    
  250.             "form-0-title": "foo",
    
  251.         }
    
  252.         formset = Formset(data, instance=instance, prefix="form")
    
  253.         self.assertTrue(formset.is_valid())
    
  254.         (new_obj,) = formset.save()
    
  255.         self.assertEqual(new_obj.obj, instance)
    
  256. 
    
  257.     def test_save_new_for_concrete(self):
    
  258.         Formset = generic_inlineformset_factory(
    
  259.             ForProxyModelModel, fields="__all__", for_concrete_model=True
    
  260.         )
    
  261.         instance = ProxyRelatedModel.objects.create()
    
  262.         data = {
    
  263.             "form-TOTAL_FORMS": "1",
    
  264.             "form-INITIAL_FORMS": "0",
    
  265.             "form-MAX_NUM_FORMS": "",
    
  266.             "form-0-title": "foo",
    
  267.         }
    
  268.         formset = Formset(data, instance=instance, prefix="form")
    
  269.         self.assertTrue(formset.is_valid())
    
  270.         (new_obj,) = formset.save()
    
  271.         self.assertNotIsInstance(new_obj.obj, ProxyRelatedModel)
    
  272. 
    
  273.     def test_initial_count(self):
    
  274.         GenericFormSet = generic_inlineformset_factory(TaggedItem)
    
  275.         data = {
    
  276.             "form-TOTAL_FORMS": "3",
    
  277.             "form-INITIAL_FORMS": "3",
    
  278.             "form-MAX_NUM_FORMS": "",
    
  279.         }
    
  280.         formset = GenericFormSet(data=data, prefix="form")
    
  281.         self.assertEqual(formset.initial_form_count(), 3)
    
  282.         formset = GenericFormSet(data=data, prefix="form", save_as_new=True)
    
  283.         self.assertEqual(formset.initial_form_count(), 0)
    
  284. 
    
  285.     def test_save_as_new(self):
    
  286.         """
    
  287.         The save_as_new parameter creates new items that are associated with
    
  288.         the object.
    
  289.         """
    
  290.         lion = Animal.objects.create(common_name="Lion", latin_name="Panthera leo")
    
  291.         yellow = lion.tags.create(tag="yellow")
    
  292.         hairy = lion.tags.create(tag="hairy")
    
  293.         GenericFormSet = generic_inlineformset_factory(TaggedItem)
    
  294.         data = {
    
  295.             "form-TOTAL_FORMS": "3",
    
  296.             "form-INITIAL_FORMS": "2",
    
  297.             "form-MAX_NUM_FORMS": "",
    
  298.             "form-0-id": str(yellow.pk),
    
  299.             "form-0-tag": "hunts",
    
  300.             "form-1-id": str(hairy.pk),
    
  301.             "form-1-tag": "roars",
    
  302.         }
    
  303.         formset = GenericFormSet(data, instance=lion, prefix="form", save_as_new=True)
    
  304.         self.assertTrue(formset.is_valid())
    
  305.         tags = formset.save()
    
  306.         self.assertEqual([tag.tag for tag in tags], ["hunts", "roars"])
    
  307.         hunts, roars = tags
    
  308.         self.assertSequenceEqual(
    
  309.             lion.tags.order_by("tag"), [hairy, hunts, roars, yellow]
    
  310.         )
    
  311. 
    
  312.     def test_absolute_max(self):
    
  313.         GenericFormSet = generic_inlineformset_factory(TaggedItem, absolute_max=1500)
    
  314.         data = {
    
  315.             "form-TOTAL_FORMS": "1501",
    
  316.             "form-INITIAL_FORMS": "0",
    
  317.             "form-MAX_NUM_FORMS": "0",
    
  318.         }
    
  319.         formset = GenericFormSet(data=data, prefix="form")
    
  320.         self.assertIs(formset.is_valid(), False)
    
  321.         self.assertEqual(len(formset.forms), 1500)
    
  322.         self.assertEqual(
    
  323.             formset.non_form_errors(),
    
  324.             ["Please submit at most 1000 forms."],
    
  325.         )
    
  326. 
    
  327.     def test_absolute_max_with_max_num(self):
    
  328.         GenericFormSet = generic_inlineformset_factory(
    
  329.             TaggedItem,
    
  330.             max_num=20,
    
  331.             absolute_max=100,
    
  332.         )
    
  333.         data = {
    
  334.             "form-TOTAL_FORMS": "101",
    
  335.             "form-INITIAL_FORMS": "0",
    
  336.             "form-MAX_NUM_FORMS": "0",
    
  337.         }
    
  338.         formset = GenericFormSet(data=data, prefix="form")
    
  339.         self.assertIs(formset.is_valid(), False)
    
  340.         self.assertEqual(len(formset.forms), 100)
    
  341.         self.assertEqual(
    
  342.             formset.non_form_errors(),
    
  343.             ["Please submit at most 20 forms."],
    
  344.         )
    
  345. 
    
  346.     def test_can_delete_extra(self):
    
  347.         GenericFormSet = generic_inlineformset_factory(
    
  348.             TaggedItem,
    
  349.             can_delete=True,
    
  350.             can_delete_extra=True,
    
  351.             extra=2,
    
  352.         )
    
  353.         formset = GenericFormSet()
    
  354.         self.assertEqual(len(formset), 2)
    
  355.         self.assertIn("DELETE", formset.forms[0].fields)
    
  356.         self.assertIn("DELETE", formset.forms[1].fields)
    
  357. 
    
  358.     def test_disable_delete_extra(self):
    
  359.         GenericFormSet = generic_inlineformset_factory(
    
  360.             TaggedItem,
    
  361.             can_delete=True,
    
  362.             can_delete_extra=False,
    
  363.             extra=2,
    
  364.         )
    
  365.         formset = GenericFormSet()
    
  366.         self.assertEqual(len(formset), 2)
    
  367.         self.assertNotIn("DELETE", formset.forms[0].fields)
    
  368.         self.assertNotIn("DELETE", formset.forms[1].fields)