django-genfkadmin 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Michael Spallino
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-genfkadmin
3
+ Version: 0.1.0
4
+ Summary: No frills GenericForeignKeys for your Django Admin
5
+ Author-email: Michael Spallino <mikehspallino@gmail.com>
6
+ Maintainer-email: Michael Spallino <mikehspallino@gmail.com>
7
+ Project-URL: Homepage, https://github.com/mikespallino/django-genericfkadmin
8
+ Project-URL: Issues, https://github.com/mikespallino/django-genericfkadmin/issues
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Software Development :: Libraries
12
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Framework :: Django :: 5
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Operating System :: OS Independent
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: django>=5
22
+ Dynamic: license-file
23
+
24
+ # Django GenericFKAdmin
25
+
26
+ Using `GenericForeignKey` in your Django models is cool, the default behavior of
27
+ Django Admin is not. This package allows you to replace the `content_type` and
28
+ `object_id` fields in your admin forms with a single input that is prefilled
29
+ with only the models related through `GenericRelation` fields.
30
+
31
+ ## Setup
32
+
33
+ ### Install
34
+
35
+ ```shell
36
+ pip install django-genfkadmin
37
+ ````
38
+
39
+ ```shell
40
+ uv add django-genfkadmin
41
+ ```
42
+
43
+ ### Usage
44
+
45
+ Using this package is pretty simple.
46
+
47
+ 1. Create a subclass of `GenericFKModelForm` for your model.
48
+ 2. Create a subclass of `GenericFKAdmin` for your model.
49
+ 3. ???
50
+ 4. Profit!
51
+
52
+ e.g. in your `admin.py`
53
+ ```python
54
+ from genfkadmin.admin import GenericFKAdmin
55
+ from genfkadmin.forms import GenericFKModelForm
56
+
57
+
58
+ class PetAdminForm(GenericFKModelForm):
59
+
60
+ class Meta:
61
+ model = Pet
62
+ fields = "__all__"
63
+
64
+
65
+ @admin.register(Pet)
66
+ class PetAdmin(GenericFKAdmin):
67
+ form = PetAdminForm
68
+ ```
69
+
70
+ ![example](docs/screenshots/example_admin.png)
71
+
72
+ A complete example django app exists in this repository at [here](/example)
73
+
74
+
75
+ ## Roadmap
76
+ 1. Implement a filtering callback to further restrict the set of options available in the selector
77
+ 2. Stacked and Tabular Inline support.
@@ -0,0 +1,54 @@
1
+ # Django GenericFKAdmin
2
+
3
+ Using `GenericForeignKey` in your Django models is cool, the default behavior of
4
+ Django Admin is not. This package allows you to replace the `content_type` and
5
+ `object_id` fields in your admin forms with a single input that is prefilled
6
+ with only the models related through `GenericRelation` fields.
7
+
8
+ ## Setup
9
+
10
+ ### Install
11
+
12
+ ```shell
13
+ pip install django-genfkadmin
14
+ ````
15
+
16
+ ```shell
17
+ uv add django-genfkadmin
18
+ ```
19
+
20
+ ### Usage
21
+
22
+ Using this package is pretty simple.
23
+
24
+ 1. Create a subclass of `GenericFKModelForm` for your model.
25
+ 2. Create a subclass of `GenericFKAdmin` for your model.
26
+ 3. ???
27
+ 4. Profit!
28
+
29
+ e.g. in your `admin.py`
30
+ ```python
31
+ from genfkadmin.admin import GenericFKAdmin
32
+ from genfkadmin.forms import GenericFKModelForm
33
+
34
+
35
+ class PetAdminForm(GenericFKModelForm):
36
+
37
+ class Meta:
38
+ model = Pet
39
+ fields = "__all__"
40
+
41
+
42
+ @admin.register(Pet)
43
+ class PetAdmin(GenericFKAdmin):
44
+ form = PetAdminForm
45
+ ```
46
+
47
+ ![example](docs/screenshots/example_admin.png)
48
+
49
+ A complete example django app exists in this repository at [here](/example)
50
+
51
+
52
+ ## Roadmap
53
+ 1. Implement a filtering callback to further restrict the set of options available in the selector
54
+ 2. Stacked and Tabular Inline support.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-genfkadmin
3
+ Version: 0.1.0
4
+ Summary: No frills GenericForeignKeys for your Django Admin
5
+ Author-email: Michael Spallino <mikehspallino@gmail.com>
6
+ Maintainer-email: Michael Spallino <mikehspallino@gmail.com>
7
+ Project-URL: Homepage, https://github.com/mikespallino/django-genericfkadmin
8
+ Project-URL: Issues, https://github.com/mikespallino/django-genericfkadmin/issues
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Software Development :: Libraries
12
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Framework :: Django :: 5
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Operating System :: OS Independent
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: django>=5
22
+ Dynamic: license-file
23
+
24
+ # Django GenericFKAdmin
25
+
26
+ Using `GenericForeignKey` in your Django models is cool, the default behavior of
27
+ Django Admin is not. This package allows you to replace the `content_type` and
28
+ `object_id` fields in your admin forms with a single input that is prefilled
29
+ with only the models related through `GenericRelation` fields.
30
+
31
+ ## Setup
32
+
33
+ ### Install
34
+
35
+ ```shell
36
+ pip install django-genfkadmin
37
+ ````
38
+
39
+ ```shell
40
+ uv add django-genfkadmin
41
+ ```
42
+
43
+ ### Usage
44
+
45
+ Using this package is pretty simple.
46
+
47
+ 1. Create a subclass of `GenericFKModelForm` for your model.
48
+ 2. Create a subclass of `GenericFKAdmin` for your model.
49
+ 3. ???
50
+ 4. Profit!
51
+
52
+ e.g. in your `admin.py`
53
+ ```python
54
+ from genfkadmin.admin import GenericFKAdmin
55
+ from genfkadmin.forms import GenericFKModelForm
56
+
57
+
58
+ class PetAdminForm(GenericFKModelForm):
59
+
60
+ class Meta:
61
+ model = Pet
62
+ fields = "__all__"
63
+
64
+
65
+ @admin.register(Pet)
66
+ class PetAdmin(GenericFKAdmin):
67
+ form = PetAdminForm
68
+ ```
69
+
70
+ ![example](docs/screenshots/example_admin.png)
71
+
72
+ A complete example django app exists in this repository at [here](/example)
73
+
74
+
75
+ ## Roadmap
76
+ 1. Implement a filtering callback to further restrict the set of options available in the selector
77
+ 2. Stacked and Tabular Inline support.
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ django_genfkadmin.egg-info/PKG-INFO
5
+ django_genfkadmin.egg-info/SOURCES.txt
6
+ django_genfkadmin.egg-info/dependency_links.txt
7
+ django_genfkadmin.egg-info/requires.txt
8
+ django_genfkadmin.egg-info/top_level.txt
9
+ genfkadmin/__init__.py
10
+ genfkadmin/admin.py
11
+ genfkadmin/fields.py
12
+ genfkadmin/forms.py
13
+ tests/test_admin.py
14
+ tests/test_fields.py
15
+ tests/test_forms.py
@@ -0,0 +1,2 @@
1
+ FIELD_ID_FORMAT = "{app_label}${model_name}[{pk}]"
2
+ GENERIC_FIELD_NAME = "{field_name}_gfk"
@@ -0,0 +1,90 @@
1
+ import copy
2
+
3
+ from django.contrib import admin
4
+ from django.contrib.contenttypes.fields import GenericForeignKey
5
+
6
+ from genfkadmin import GENERIC_FIELD_NAME
7
+ from genfkadmin.forms import GenericFKModelForm
8
+
9
+
10
+ class GenericFKAdmin(admin.ModelAdmin):
11
+ """
12
+ A ModelAdmin for use with a Model that utilizes GenericForeignKeys.
13
+ """
14
+
15
+ form = None
16
+
17
+ def __init__(self, *args, **kwargs):
18
+ super().__init__(*args, **kwargs)
19
+
20
+ # store a mapping of our GenericForeignKeys to their content_type and
21
+ # foreign_key fields
22
+ self.generic_fields = {}
23
+ self.generic_related_fields = set()
24
+ for field in self.model._meta.private_fields:
25
+ if isinstance(field, GenericForeignKey):
26
+ self.generic_fields[
27
+ GENERIC_FIELD_NAME.format(field_name=field.name)
28
+ ] = {
29
+ "ct_field": field.ct_field,
30
+ "fk_field": field.fk_field,
31
+ }
32
+ self.generic_related_fields.add(field.ct_field)
33
+ self.generic_related_fields.add(field.fk_field)
34
+
35
+ def get_fields(self, *args, **kwargs):
36
+ """
37
+ Overrides get_fields to remove content_type and foreign_key fields for
38
+ the GenericForeignKey and replaces them with the dynamic fields.
39
+ """
40
+ updated_fields_with_generic_keys = []
41
+
42
+ if self.fields:
43
+ # if we have fields already make sure the content type and foreign
44
+ # key fields aren't present and add our generic fields
45
+ updated_fields_with_generic_keys = copy.deepcopy(self.fields)
46
+ for field, generic_related_fields in self.generic_fields.items():
47
+ try:
48
+ updated_fields_with_generic_keys.remove(
49
+ generic_related_fields["ct_field"]
50
+ )
51
+ updated_fields_with_generic_keys.remove(
52
+ generic_related_fields["fk_field"]
53
+ )
54
+ updated_fields_with_generic_keys.append(field)
55
+ except ValueError:
56
+ pass
57
+ else:
58
+ # if we don't have fields generate them ourselves, including the
59
+ # dynamic generic foreign key fields
60
+ for field in self.model._meta.fields:
61
+ if (
62
+ not field.primary_key
63
+ and field.name not in self.generic_related_fields
64
+ ):
65
+ updated_fields_with_generic_keys.append(field.name)
66
+ for generic_field in self.generic_fields:
67
+ updated_fields_with_generic_keys.append(generic_field)
68
+ return updated_fields_with_generic_keys
69
+
70
+ def get_form(self, *args, **kwargs):
71
+ """
72
+ Overrides get_form to return our subclassed GenericFKModelForm.
73
+ """
74
+ # bypass any auto form generation by simply returning the form
75
+ # attribute and do some gut checks to make sure we have an appropriate
76
+ # form to use
77
+ if not self.form:
78
+ raise NotImplementedError(
79
+ "Must define self.form that inherits from "
80
+ )
81
+ if not issubclass(self.form, GenericFKModelForm):
82
+ raise NotImplementedError(
83
+ "self.form must be subclass of GenericFKModelForm"
84
+ )
85
+ return self.form
86
+
87
+
88
+ __all__ = [
89
+ "GenericFKAdmin",
90
+ ]
@@ -0,0 +1,52 @@
1
+ from django import forms
2
+ from django.contrib.contenttypes.fields import GenericRelation
3
+
4
+ from genfkadmin import FIELD_ID_FORMAT
5
+
6
+
7
+ class GenericFKField(forms.ChoiceField):
8
+ """
9
+ A ChoiceField that generates it's set of choices based on the related
10
+ models for the GenericForeignKey Relations.
11
+ """
12
+
13
+ def __init__(self, model, *args, **kwargs):
14
+ """
15
+ Given a model and an optional, initialize the set of
16
+ choices that should be available for this field.
17
+ """
18
+ choices = []
19
+
20
+ # generic relations are stored in _relation_tree, so we can grab
21
+ # the models from those relations and develop a set of choices
22
+ # for the select input. The value of the choice is a formatted string
23
+ # FIELD_ID_FORMAT, that stores the necessary information to parse
24
+ # back out in the form on save to grab the content_type_id and
25
+ # object_id of the selected value.
26
+ for relation in model._meta._relation_tree:
27
+ if isinstance(relation, GenericRelation):
28
+ queryset = relation.model.objects.all()
29
+ choices.extend(
30
+ [
31
+ (
32
+ FIELD_ID_FORMAT.format(
33
+ app_label=i._meta.app_label,
34
+ model_name=i._meta.model_name,
35
+ pk=i.pk,
36
+ ),
37
+ str(i),
38
+ )
39
+ for i in queryset
40
+ ]
41
+ )
42
+
43
+ super().__init__(
44
+ *args,
45
+ choices=choices,
46
+ **kwargs,
47
+ )
48
+
49
+
50
+ __all__ = [
51
+ "GenericFKField",
52
+ ]
@@ -0,0 +1,82 @@
1
+ from django import forms
2
+ from django.contrib.contenttypes.fields import GenericForeignKey
3
+ from django.contrib.contenttypes.models import ContentType
4
+
5
+ from genfkadmin import FIELD_ID_FORMAT, GENERIC_FIELD_NAME
6
+ from genfkadmin.fields import GenericFKField
7
+
8
+
9
+ class GenericFKModelForm(forms.ModelForm):
10
+ """
11
+ A ModelForm that automatically replaces the content type and foreign key
12
+ fields of GenericForeignKeys with a single input supplies options for all
13
+ the models with GenericRelations.
14
+ """
15
+
16
+ def __init__(self, *args, **kwargs):
17
+
18
+ super().__init__(*args, **kwargs)
19
+ self.generic_fields = {}
20
+
21
+ # private_fields has GenericForeignKeys, so we check for those here
22
+ # and inject a fake field into the form. We also remove the
23
+ # content_type and foreign_key fields if they exist on the declared
24
+ # fields of the form so that we only have the generic field.
25
+ for field in self._meta.model._meta.private_fields:
26
+ if isinstance(field, GenericForeignKey):
27
+ # we must name the generic field something other than the
28
+ # original name, because GenericForeignKey are editable=False
29
+ # and won't be allowed in the form
30
+ generic_field_name = GENERIC_FIELD_NAME.format(
31
+ field_name=field.name
32
+ )
33
+ self.generic_fields[generic_field_name] = {
34
+ "ct_field": field.ct_field,
35
+ "fk_field": field.fk_field,
36
+ }
37
+ self.fields.pop(field.ct_field, None)
38
+ self.fields.pop(field.fk_field, None)
39
+ self.fields[generic_field_name] = GenericFKField(
40
+ field.model,
41
+ label=field.name,
42
+ )
43
+
44
+ def get_initial_for_field(self, field, field_name):
45
+ # generate the initial value for any of the generic fields so that
46
+ # the correct choice is auto selected
47
+ if field_name in self.generic_fields:
48
+ target_instance = self.instance.content_object
49
+ if target_instance:
50
+ return FIELD_ID_FORMAT.format(
51
+ app_label=target_instance._meta.app_label,
52
+ model_name=target_instance._meta.model_name,
53
+ pk=target_instance.pk,
54
+ )
55
+ return super().get_initial_for_field(field, field_name)
56
+
57
+ def save(self, commit=True):
58
+ instance = super().save(commit=commit)
59
+
60
+ # for the generic fields, we parse the value out of FIELD_ID_FORMAT
61
+ # which gives us the ability to query for the ContentType and get the
62
+ # primary key of the related field. We use setattr to update these
63
+ # values dynamically
64
+ for generic_field, related_fields in self.generic_fields.items():
65
+ target_model_instance = self.cleaned_data[generic_field]
66
+ app_label, rest = target_model_instance.split("$")
67
+ model_name, dirty_id = rest.split("[")
68
+
69
+ content_type = ContentType.objects.get(
70
+ app_label=app_label, model=model_name
71
+ )
72
+ object_id = dirty_id.strip("[").strip("]")
73
+
74
+ setattr(instance, related_fields["ct_field"], content_type)
75
+ setattr(instance, related_fields["fk_field"], object_id)
76
+
77
+ return instance
78
+
79
+
80
+ __all__ = [
81
+ "GenericFKModelForm",
82
+ ]
@@ -0,0 +1,59 @@
1
+ [project]
2
+ name = "django-genfkadmin"
3
+ version = "0.1.0"
4
+ description = "No frills GenericForeignKeys for your Django Admin"
5
+ readme = "README.md"
6
+ authors = [
7
+ {name = "Michael Spallino", email = "mikehspallino@gmail.com"},
8
+ ]
9
+ maintainers = [
10
+ {name = "Michael Spallino", email = "mikehspallino@gmail.com"},
11
+ ]
12
+ license-files = ["LICENSE"]
13
+ classifiers = [
14
+ "Development Status :: 2 - Pre-Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Topic :: Software Development :: Libraries",
17
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Framework :: Django :: 5",
20
+ "Programming Language :: Python",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Operating System :: OS Independent"
23
+ ]
24
+ requires-python = ">=3.11"
25
+ dependencies = [
26
+ "django>=5",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/mikespallino/django-genericfkadmin"
31
+ Issues = "https://github.com/mikespallino/django-genericfkadmin/issues"
32
+
33
+ [tool.black]
34
+ line-length = 80
35
+
36
+ [tool.isort]
37
+ profile = "black"
38
+
39
+ [tool.uv.sources]
40
+ django-genfkadmin = { workspace = true }
41
+
42
+ [tool.pytest.ini_options]
43
+ minversion = "6.0"
44
+ testpaths = [
45
+ "tests/",
46
+ ]
47
+ addopts = "--create-db --no-migrations"
48
+
49
+ [dependency-groups]
50
+ dev = [
51
+ "autoflake>=2.3.1",
52
+ "black>=25.1.0",
53
+ "factory-boy>=3.3.3",
54
+ "isort>=6.0.1",
55
+ "pre-commit>=4.3.0",
56
+ "pytest>=8.4.2",
57
+ "pytest-django>=4.11.1",
58
+ "twine>=6.2.0",
59
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,80 @@
1
+ import pytest
2
+ from django import forms
3
+
4
+ from genfkadmin.admin import GenericFKAdmin
5
+ from genfkadmin.forms import GenericFKModelForm
6
+ from tests.models import Pet
7
+
8
+
9
+ class BadAdminConfiguration(GenericFKAdmin):
10
+ pass
11
+
12
+
13
+ def test_admin_must_define_form():
14
+ from django.contrib.admin import site
15
+
16
+ with pytest.raises(NotImplementedError):
17
+ admin = BadAdminConfiguration(Pet, site)
18
+ admin.get_form()
19
+
20
+
21
+ class BadForm(forms.ModelForm):
22
+ class Meta:
23
+ model = Pet
24
+ fields = "__all__"
25
+
26
+
27
+ def test_admin_form_must_subclass():
28
+ from django.contrib.admin import site
29
+
30
+ with pytest.raises(NotImplementedError):
31
+ BadAdminConfiguration.form = BadForm
32
+ admin = BadAdminConfiguration(Pet, site)
33
+ admin.get_form()
34
+
35
+
36
+ class GoodForm(GenericFKModelForm):
37
+
38
+ class Meta:
39
+ model = Pet
40
+ fields = "__all__"
41
+
42
+
43
+ class GoodAdminConfiguration(GenericFKAdmin):
44
+ form = GoodForm
45
+
46
+
47
+ def test_admin_stores_generic_fields():
48
+ from django.contrib.admin import site
49
+
50
+ admin = GoodAdminConfiguration(Pet, site)
51
+
52
+ assert "content_object_gfk" in admin.generic_fields
53
+ assert (
54
+ admin.generic_fields["content_object_gfk"]["ct_field"] == "content_type"
55
+ )
56
+ assert admin.generic_fields["content_object_gfk"]["fk_field"] == "object_id"
57
+
58
+
59
+ def test_admin_stores_generic_related_fields():
60
+ from django.contrib.admin import site
61
+
62
+ admin = GoodAdminConfiguration(Pet, site)
63
+ assert admin.generic_related_fields == {"content_type", "object_id"}
64
+
65
+
66
+ def test_admin_default_field_config_removes_generic_related_fields():
67
+ from django.contrib.admin import site
68
+
69
+ admin = GoodAdminConfiguration(Pet, site)
70
+ fields = admin.get_fields()
71
+ assert admin.generic_related_fields & set(fields) == set()
72
+
73
+
74
+ def test_admin_removes_generic_related_fields_when_fields_defined():
75
+ from django.contrib.admin import site
76
+
77
+ admin = GoodAdminConfiguration(Pet, site)
78
+ admin.fields = ["owner", "content_type", "object_id"]
79
+ fields = admin.get_fields()
80
+ assert admin.generic_related_fields & set(fields) == set()
@@ -0,0 +1,49 @@
1
+ import pytest
2
+
3
+ from genfkadmin import FIELD_ID_FORMAT
4
+ from genfkadmin.fields import GenericFKField
5
+ from tests.factories import ElephantFactory
6
+ from tests.models import Pet
7
+
8
+
9
+ @pytest.mark.django_db
10
+ def test_field_has_choices(pets):
11
+ field = GenericFKField(Pet)
12
+ assert field.choices is not None
13
+
14
+
15
+ @pytest.mark.django_db
16
+ def test_field_choices_value_format(pets):
17
+ expected_values = []
18
+ for dog in pets["dogs"]:
19
+ expected_values.append(
20
+ FIELD_ID_FORMAT.format(
21
+ app_label="tests", model_name="dog", pk=dog.pk
22
+ )
23
+ )
24
+ for cat in pets["cats"]:
25
+ expected_values.append(
26
+ FIELD_ID_FORMAT.format(
27
+ app_label="tests", model_name="cat", pk=cat.pk
28
+ )
29
+ )
30
+
31
+ field = GenericFKField(Pet)
32
+ for value, display in field.choices:
33
+ assert value in expected_values
34
+
35
+
36
+ @pytest.mark.django_db
37
+ def test_field_choices_only_linked_models(pets):
38
+ elephants = [ElephantFactory() for _ in range(3)]
39
+
40
+ field = GenericFKField(Pet)
41
+ elephant_choices = [
42
+ FIELD_ID_FORMAT.format(
43
+ app_label="tests", model_name="elephant", pk=el.pk
44
+ )
45
+ for el in elephants
46
+ ]
47
+
48
+ for value, display in field.choices:
49
+ assert value not in elephant_choices
@@ -0,0 +1,68 @@
1
+ import pytest
2
+ from django.contrib.contenttypes.models import ContentType
3
+
4
+ from genfkadmin import FIELD_ID_FORMAT
5
+ from genfkadmin.fields import GenericFKField
6
+ from genfkadmin.forms import GenericFKModelForm
7
+ from tests.models import Cat, Dog, Pet
8
+
9
+
10
+ class PetAdminForm(GenericFKModelForm):
11
+
12
+ class Meta:
13
+ model = Pet
14
+ fields = "__all__"
15
+
16
+
17
+ @pytest.mark.django_db
18
+ def test_form_detects_generic_fields():
19
+ form = PetAdminForm()
20
+
21
+ assert "content_object_gfk" in form.generic_fields
22
+ assert (
23
+ form.generic_fields["content_object_gfk"]["ct_field"] == "content_type"
24
+ )
25
+ assert form.generic_fields["content_object_gfk"]["fk_field"] == "object_id"
26
+
27
+
28
+ @pytest.mark.django_db
29
+ def test_form_removes_content_type_and_fk_fields():
30
+ form = PetAdminForm()
31
+
32
+ assert "content_object_gfk" in form.fields
33
+ assert "content_type" not in form.fields
34
+ assert "object_id" not in form.fields
35
+
36
+
37
+ @pytest.mark.django_db
38
+ def test_form_populates_initial_value(pets):
39
+ instance = pets["pets"][0]
40
+ dog = pets["dogs"][0]
41
+ form = PetAdminForm(instance=instance)
42
+ assert form.get_initial_for_field(
43
+ GenericFKField, "content_object_gfk"
44
+ ) == FIELD_ID_FORMAT.format(app_label="tests", model_name="dog", pk=dog.pk)
45
+
46
+
47
+ @pytest.mark.django_db
48
+ def test_form_save_updates_content_type_and_fk_fields(pets):
49
+ instance = pets["pets"][0]
50
+ assert instance.content_type == ContentType.objects.get_for_model(Dog)
51
+ assert instance.content_object == pets["dogs"][0]
52
+
53
+ cat = pets["cats"][0]
54
+ form = PetAdminForm(
55
+ data={
56
+ "owner": instance.owner,
57
+ "content_object_gfk": FIELD_ID_FORMAT.format(
58
+ app_label="tests", model_name="cat", pk=cat.pk
59
+ ),
60
+ },
61
+ instance=instance,
62
+ )
63
+ updated_instance = form.save()
64
+
65
+ assert updated_instance.content_type == ContentType.objects.get_for_model(
66
+ Cat
67
+ )
68
+ assert updated_instance.content_object == cat