django-genfkadmin 0.1.0__py3-none-any.whl

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,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,9 @@
1
+ django_genfkadmin-0.1.0.dist-info/licenses/LICENSE,sha256=Y-5z4chfgv5qhkHMNGH6dldXHzvqelRsjbE5MGLOkTo,1073
2
+ genfkadmin/__init__.py,sha256=hdYh7PI60aPJY8BkokR04Mxo2TCZUOUlA6Zzt765na0,91
3
+ genfkadmin/admin.py,sha256=nlEGMqvFd5tneS_an04Ws6Dp8lXoZ8N0zPwdAo5KD5I,3393
4
+ genfkadmin/fields.py,sha256=pJeoZsAq1L6HvrTgShoZmPMJJiddZ9-RSqc0neeTJTI,1711
5
+ genfkadmin/forms.py,sha256=w_-xK0m-2NFgplOBqnQ3SvHlJs78Y_dpmyVlmzyHhHU,3390
6
+ django_genfkadmin-0.1.0.dist-info/METADATA,sha256=Cyr7c32vs-IeBPVuQCu0kQc228NKIMmlGZayowjg20M,2199
7
+ django_genfkadmin-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ django_genfkadmin-0.1.0.dist-info/top_level.txt,sha256=AD-AF3VAfg620J8v6cGkBScRdx9o3qAj_lX-BbL0SV4,11
9
+ django_genfkadmin-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ genfkadmin
genfkadmin/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ FIELD_ID_FORMAT = "{app_label}${model_name}[{pk}]"
2
+ GENERIC_FIELD_NAME = "{field_name}_gfk"
genfkadmin/admin.py ADDED
@@ -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
+ ]
genfkadmin/fields.py ADDED
@@ -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
+ ]
genfkadmin/forms.py ADDED
@@ -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
+ ]