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.
- django_genfkadmin-0.1.0/LICENSE +21 -0
- django_genfkadmin-0.1.0/PKG-INFO +77 -0
- django_genfkadmin-0.1.0/README.md +54 -0
- django_genfkadmin-0.1.0/django_genfkadmin.egg-info/PKG-INFO +77 -0
- django_genfkadmin-0.1.0/django_genfkadmin.egg-info/SOURCES.txt +15 -0
- django_genfkadmin-0.1.0/django_genfkadmin.egg-info/dependency_links.txt +1 -0
- django_genfkadmin-0.1.0/django_genfkadmin.egg-info/requires.txt +1 -0
- django_genfkadmin-0.1.0/django_genfkadmin.egg-info/top_level.txt +1 -0
- django_genfkadmin-0.1.0/genfkadmin/__init__.py +2 -0
- django_genfkadmin-0.1.0/genfkadmin/admin.py +90 -0
- django_genfkadmin-0.1.0/genfkadmin/fields.py +52 -0
- django_genfkadmin-0.1.0/genfkadmin/forms.py +82 -0
- django_genfkadmin-0.1.0/pyproject.toml +59 -0
- django_genfkadmin-0.1.0/setup.cfg +4 -0
- django_genfkadmin-0.1.0/tests/test_admin.py +80 -0
- django_genfkadmin-0.1.0/tests/test_fields.py +49 -0
- django_genfkadmin-0.1.0/tests/test_forms.py +68 -0
|
@@ -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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
django>=5
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
genfkadmin
|
|
@@ -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,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
|