├── genericadmin ├── __init__.py ├── admin.py └── static │ └── genericadmin │ └── js │ └── genericadmin.js ├── MANIFEST.in ├── .gitignore ├── LICENCE ├── setup.py ├── README.markdown └── README.txt /genericadmin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include genericadmin/static/genericadmin/js/* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .project 3 | .pydevproject 4 | dist/* 5 | *~ 6 | MANIFEST 7 | *egg* 8 | *.bak 9 | *.tmproj 10 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Weston Nielson (wnielson@gmail.com) 2 | 2010, Jan Schrewe (jschrewe@googlemail.com) 3 | 2017, Arthur Hanson (worldnomad@gmail.com) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | from subprocess import call 5 | 6 | def convert_readme(): 7 | try: 8 | call(["pandoc", "-f", "markdown_github", "-t", "rst", "-o", "README.txt", "README.markdown"]) 9 | except OSError: 10 | pass 11 | return open('README.txt').read() 12 | 13 | setup( 14 | name='django-genericadmin', 15 | version='0.7.0', 16 | description="Adds support for generic relations within Django's admin interface.", 17 | author='Weston Nielson, Jan Schrewe, Arthur Hanson', 18 | author_email='wnielson@gmail.com, jschrewe@googlemail.com, worldnomad@gmail.com', 19 | url='https://github.com/arthanson/django-genericadmin', 20 | packages = ['genericadmin'], 21 | # package_data={'genericadmin': ['static/genericadmin/js/genericadmin.js']}, 22 | classifiers=[ 23 | 'Development Status :: 3 - Alpha', 24 | 'Environment :: Web Environment', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: BSD License', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python', 29 | 'Framework :: Django', 30 | ], 31 | long_description=convert_readme(), 32 | include_package_data=True, 33 | zip_safe=False, 34 | ) 35 | -------------------------------------------------------------------------------- /genericadmin/admin.py: -------------------------------------------------------------------------------- 1 | import json 2 | from functools import update_wrapper 3 | 4 | from django.contrib import admin 5 | from django.conf.urls import url 6 | from django.conf import settings 7 | try: 8 | from django.contrib.contenttypes.generic import GenericForeignKey, GenericTabularInline, GenericStackedInline 9 | except ImportError: 10 | from django.contrib.contenttypes.admin import GenericStackedInline, GenericTabularInline 11 | from django.contrib.contenttypes.fields import GenericForeignKey 12 | 13 | from django.contrib.contenttypes.models import ContentType 14 | try: 15 | from django.utils.encoding import force_text 16 | except ImportError: 17 | from django.utils.encoding import force_unicode as force_text 18 | from django.utils.text import capfirst 19 | from django.contrib.admin.widgets import url_params_from_lookup_dict 20 | from django.http import HttpResponse, HttpResponseNotAllowed, Http404 21 | try: 22 | from django.contrib.admin.views.main import IS_POPUP_VAR 23 | except ImportError: 24 | from django.contrib.admin.options import IS_POPUP_VAR 25 | from django.core.exceptions import ObjectDoesNotExist 26 | 27 | JS_PATH = getattr(settings, 'GENERICADMIN_JS', 'genericadmin/js/') 28 | 29 | class BaseGenericModelAdmin(object): 30 | class Media: 31 | js = () 32 | 33 | content_type_lookups = {} 34 | generic_fk_fields = [] 35 | content_type_blacklist = [] 36 | content_type_whitelist = [] 37 | 38 | def __init__(self, model, admin_site): 39 | try: 40 | media = list(self.Media.js) 41 | except: 42 | media = [] 43 | media.append(JS_PATH + 'genericadmin.js') 44 | self.Media.js = tuple(media) 45 | 46 | self.content_type_whitelist = [s.lower() for s in self.content_type_whitelist] 47 | self.content_type_blacklist = [s.lower() for s in self.content_type_blacklist] 48 | 49 | super(BaseGenericModelAdmin, self).__init__(model, admin_site) 50 | 51 | def get_generic_field_list(self, request, prefix=''): 52 | if hasattr(self, 'ct_field') and hasattr(self, 'ct_fk_field'): 53 | exclude = [self.ct_field, self.ct_fk_field] 54 | else: 55 | exclude = [] 56 | 57 | field_list = [] 58 | if hasattr(self, 'generic_fk_fields') and self.generic_fk_fields: 59 | for fields in self.generic_fk_fields: 60 | if fields['ct_field'] not in exclude and \ 61 | fields['fk_field'] not in exclude: 62 | fields['inline'] = prefix != '' 63 | fields['prefix'] = prefix 64 | field_list.append(fields) 65 | else: 66 | for field in self.model._meta.virtual_fields: 67 | if isinstance(field, GenericForeignKey) and \ 68 | field.ct_field not in exclude and field.fk_field not in exclude: 69 | field_list.append({ 70 | 'ct_field': field.ct_field, 71 | 'fk_field': field.fk_field, 72 | 'inline': prefix != '', 73 | 'prefix': prefix, 74 | }) 75 | 76 | if hasattr(self, 'inlines') and len(self.inlines) > 0: 77 | for FormSet, inline in zip(self.get_formsets_with_inlines(request), self.get_inline_instances(request)): 78 | if hasattr(inline, 'get_generic_field_list'): 79 | prefix = FormSet.get_default_prefix() 80 | field_list = field_list + inline.get_generic_field_list(request, prefix) 81 | 82 | return field_list 83 | 84 | def get_urls(self): 85 | def wrap(view): 86 | def wrapper(*args, **kwargs): 87 | return self.admin_site.admin_view(view)(*args, **kwargs) 88 | return update_wrapper(wrapper, view) 89 | 90 | custom_urls = [ 91 | url(r'^obj-data/$', wrap(self.generic_lookup), name='admin_genericadmin_obj_lookup'), 92 | url(r'^genericadmin-init/$', wrap(self.genericadmin_js_init), name='admin_genericadmin_init'), 93 | ] 94 | return custom_urls + super(BaseGenericModelAdmin, self).get_urls() 95 | 96 | def genericadmin_js_init(self, request): 97 | if request.method == 'GET': 98 | obj_dict = {} 99 | for c in ContentType.objects.all(): 100 | val = force_text('%s/%s' % (c.app_label, c.model)) 101 | params = self.content_type_lookups.get('%s.%s' % (c.app_label, c.model), {}) 102 | params = url_params_from_lookup_dict(params) 103 | if self.content_type_whitelist: 104 | if val in self.content_type_whitelist: 105 | obj_dict[c.id] = (val, params) 106 | elif val not in self.content_type_blacklist: 107 | obj_dict[c.id] = (val, params) 108 | 109 | data = { 110 | 'url_array': obj_dict, 111 | 'fields': self.get_generic_field_list(request), 112 | 'popup_var': IS_POPUP_VAR, 113 | } 114 | resp = json.dumps(data, ensure_ascii=False) 115 | return HttpResponse(resp, content_type='application/json') 116 | return HttpResponseNotAllowed(['GET']) 117 | 118 | def generic_lookup(self, request): 119 | if request.method != 'GET': 120 | return HttpResponseNotAllowed(['GET']) 121 | 122 | if 'content_type' in request.GET and 'object_id' in request.GET: 123 | content_type_id = request.GET['content_type'] 124 | object_id = request.GET['object_id'] 125 | 126 | obj_dict = { 127 | 'content_type_id': content_type_id, 128 | 'object_id': object_id, 129 | } 130 | 131 | content_type = ContentType.objects.get(pk=content_type_id) 132 | obj_dict["content_type_text"] = capfirst(force_text(content_type)) 133 | 134 | try: 135 | obj = content_type.get_object_for_this_type(pk=object_id) 136 | obj_dict["object_text"] = capfirst(force_text(obj)) 137 | except ObjectDoesNotExist: 138 | raise Http404 139 | 140 | resp = json.dumps(obj_dict, ensure_ascii=False) 141 | else: 142 | resp = '' 143 | return HttpResponse(resp, content_type='application/json') 144 | 145 | 146 | 147 | class GenericAdminModelAdmin(BaseGenericModelAdmin, admin.ModelAdmin): 148 | """Model admin for generic relations. """ 149 | 150 | 151 | class GenericTabularInline(BaseGenericModelAdmin, GenericTabularInline): 152 | """Model admin for generic tabular inlines. """ 153 | 154 | 155 | class GenericStackedInline(BaseGenericModelAdmin, GenericStackedInline): 156 | """Model admin for generic stacked inlines. """ 157 | 158 | 159 | class TabularInlineWithGeneric(BaseGenericModelAdmin, admin.TabularInline): 160 | """"Normal tabular inline with a generic relation""" 161 | 162 | 163 | class StackedInlineWithGeneric(BaseGenericModelAdmin, admin.StackedInline): 164 | """"Normal stacked inline with a generic relation""" 165 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # django-genericadmin 2 | 3 | A simple django app to make the lookup of generic models easier. 4 | 5 | ## Installation 6 | 7 | Run the usual 8 | 9 | ```pip install django-genericadmin``` 10 | 11 | and add it to your `INSTALLED_APPS` in your project's `settings.py`. There is no need to run `manage.py syncdb` or `manage.py migrate` because _django-genericadmin_ does not have any models. 12 | 13 | ```python 14 | INSTALLED_APPS = ( 15 | ... 16 | 'genericadmin', 17 | ... 18 | ) 19 | ``` 20 | 21 | If you are using the staticfiles app, then run `manage.py collectstatic` and you should be good to go. 22 | 23 | If you don't know what I'm talking about or your django version < 1.3, then you should link or copy `genericadmin/media/js/` to your asset directory and set `GENERICADMIN_JS` to a the relative destination of your just copied files. 24 | 25 | ## Usage 26 | 27 | To use _django-genericadmin_ your model admin class must inherit from `GenericAdminModelAdmin`. 28 | 29 | So a model admin like 30 | 31 | ```python 32 | class NavBarEntryAdmin(admin.ModelAdmin): 33 | pass 34 | 35 | admin.site.register(NavBarEntry, NavBarEntryAdmin) 36 | ``` 37 | 38 | becomes 39 | 40 | ```python 41 | from genericadmin.admin import GenericAdminModelAdmin 42 | 43 | class NavBarEntryAdmin(GenericAdminModelAdmin): 44 | pass 45 | 46 | admin.site.register(NavBarEntry, NavBarEntryAdmin) 47 | ``` 48 | 49 | That's it. 50 | 51 | ## Provided admin classes 52 | 53 | A short overview of the admin classes and their uses provided by _django-genericadmin_. 54 | 55 | * __GenericAdminModelAdmin__ — The admin for a standard Django model that has at least one generic foreign relation. 56 | 57 | * __TabularInlineWithGeneric__ and __StackedInlineWithGeneric__ — Normal inline admins for models that have a generic relation and are edited inline. 58 | 59 | 60 | * __GenericTabularInline__ and __GenericStackedInline__ — Used to provide _True Polymorphic Relationships_ (see below) and generic relations in the admin. Also see the Django docs [here](https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#generic-relations-in-forms-and-admin). 61 | 62 | 63 | ## Inline Usage 64 | 65 | To use _django-genericadmin_ with admin inlines, your models must inherit from `GenericAdminModelAdmin` as described above: 66 | 67 | ```python 68 | from genericadmin.admin import GenericAdminModelAdmin 69 | 70 | class NavBarEntryAdmin(GenericAdminModelAdmin): 71 | pass 72 | 73 | admin.site.register(NavBarEntry, NavBarEntryAdmin) 74 | ``` 75 | 76 | Additionally the inline classes must inherit from either `StackedInlineWithGeneric` or `TabularInlineWithGeneric`: 77 | 78 | ```python 79 | from genericadmin.admin import GenericAdminModelAdmin, TabularInlineWithGeneric 80 | 81 | class PagesInline(TabularInlineWithGeneric): 82 | model = ... 83 | 84 | class NavBarEntryAdmin(GenericAdminModelAdmin): 85 | inlines = [PagesInline, ] 86 | 87 | ... 88 | ``` 89 | 90 | Note that you can't mix and match. If you're going to use a generic inline, the class using it must inherit from `GenericAdminModelAdmin`. 91 | 92 | ## Specifying which fields are handled 93 | 94 | In most cases _django-genericadmin_ will correctly figure out which fields on your model are generic foreign keys and just do the right thing. If you want to specify the fields yourself (Control your own destiny and all that) you can use the `generic_fk_fields` attribute on the admin class. Note that you can specify the fields on each admin class for inline admins. So, for the above mentioned inline admin, you would do it like so: 95 | 96 | ```python 97 | class PagesInline(TabularInlineWithGeneric): 98 | model = AReallyCoolPage 99 | generic_fk_fields = [{ 100 | 'ct_field': , 101 | 'fk_field': , 102 | }] 103 | ``` 104 | 105 | If you want to use more then one field pair, you can just add more dicts to the list. 106 | 107 | If you use the `ct_field` and `ct_fk_field` attributes _django-genericadmin_ will always just ignore those fields and not even try to use them. 108 | 109 | ## Blacklisting Content Types 110 | 111 | Specific content types can be removed from the content type select list. Example: 112 | 113 | ```python 114 | class NavBarEntryAdmin(GenericAdminModelAdmin): 115 | content_type_blacklist = ('auth/group', 'auth/user', ) 116 | ``` 117 | 118 | ## Whitelisting Content Types 119 | 120 | Specific content types that can be display from the content type select list. Example: 121 | 122 | ```python 123 | class NavBarEntryAdmin(GenericAdminModelAdmin): 124 | content_type_whitelist = ('auth/message', ) 125 | ``` 126 | 127 | Note that this only happens on the client; there is no enforcement of the blacklist at the model level. 128 | 129 | ## Lookup parameters by Content Type 130 | 131 | Supply extra lookup parameters per content type similar to how limit_choices_to works with raw id fields. Example: 132 | 133 | ```python 134 | class NavBarEntryAdmin(GenericAdminModelAdmin): 135 | content_type_lookups = {'app.model': {'field': 'value'} 136 | ``` 137 | 138 | ## True Polymorphic Relationships 139 | 140 | `django-genericadmin` also provides a UI to easily manage a particularly useful model that, when used as an inline on another model, enables relations from any entry of any model to any other entry of any other model. And, because it has a generic relationship moving in both directions, it means it can be attached as an inline _to any model_ without having to create unique, individual foreign keys for each model you want to use it on. 141 | 142 | Here's an example of a polymorphic model: 143 | 144 | ```python 145 | from django.db import models 146 | from django.contrib.contenttypes.models import ContentType 147 | from django.contrib.contenttypes import generic 148 | 149 | class RelatedContent(models.Model): 150 | """ 151 | Relates any one entry to another entry irrespective of their individual models. 152 | """ 153 | content_type = models.ForeignKey(ContentType) 154 | object_id = models.PositiveIntegerField() 155 | content_object = generic.GenericForeignKey('content_type', 'object_id') 156 | 157 | parent_content_type = models.ForeignKey(ContentType, related_name="parent_test_link") 158 | parent_object_id = models.PositiveIntegerField() 159 | parent_content_object = generic.GenericForeignKey('parent_content_type', 'parent_object_id') 160 | 161 | def __unicode__(self): 162 | return "%s: %s" % (self.content_type.name, self.content_object) 163 | ``` 164 | 165 | And here's how you'd set up your admin.py: 166 | 167 | ```python 168 | from whateverapp.models import RelatedContent 169 | from genericadmin.admin import GenericAdminModelAdmin, GenericTabularInline 170 | 171 | class RelatedContentInline(GenericTabularInline): 172 | model = RelatedContent 173 | ct_field = 'parent_content_type' # See below (1). 174 | ct_fk_field = 'parent_object_id' # See below (1). 175 | 176 | class WhateverModelAdmin(GenericAdminModelAdmin): # Super important! See below (2). 177 | content_type_whitelist = ('app/model', 'app2/model2' ) # Add white/black lists on this class 178 | inlines = [RelatedContentInline,] 179 | ``` 180 | 181 | (1) By default `ct_field` and `ct_fk_field` will default to `content_type` and `object_id` respectively. `ct_field` and `ct_fk_field` are used to create the parent link from the inline to the model you are attaching it to (similar to how Django does this attachment using foreign keys with more conventional inlines). You could also leave this configuration out of your inline classes but, if you do that, I encourage you to change the model attributes from `parent_content_type` & `parent_object_id` to `child_content_type` & `child_object_id`. I say this because, when it comes time to make queries, you'll want to know which direction you're 'traversing' in. 182 | 183 | (2) Make sure that whatever the admin classes are utilizing these inlines are subclasses of `GenericAdminModelAdmin` from `django-genericadmin` or else the handy-dandy javascript-utilizing interface won't work as intended. 184 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | django-genericadmin 2 | =================== 3 | 4 | A simple django app to make the lookup of generic models easier. 5 | 6 | Installation 7 | ------------ 8 | 9 | To install add it to your ``INSTALLED_APPS`` setting. There is no need 10 | to run ``manage.py syncdb`` because *django-genericadmin* does not have 11 | any models. 12 | 13 | .. code:: python 14 | 15 | INSTALLED_APPS = ( 16 | ... 17 | 'genericadmin', 18 | ... 19 | ) 20 | 21 | If you are using the staticfiles app, then run 22 | ``manage.py collectstatic`` and you should be good to go. 23 | 24 | If you don't know what I'm talking about or your django version < 1.3, 25 | then you should link or copy ``genericadmin/media/js/`` to your asset 26 | directory and set ``GENERICADMIN_JS`` to a the relative destination of 27 | your just copied files. 28 | 29 | Usage 30 | ----- 31 | 32 | To use *django-genericadmin* your model admin class must inherit from 33 | ``GenericAdminModelAdmin``. 34 | 35 | So a model admin like 36 | 37 | .. code:: python 38 | 39 | class NavBarEntryAdmin(admin.ModelAdmin): 40 | pass 41 | 42 | admin.site.register(NavBarEntry, NavBarEntryAdmin) 43 | 44 | becomes 45 | 46 | .. code:: python 47 | 48 | from genericadmin.admin import GenericAdminModelAdmin 49 | 50 | class NavBarEntryAdmin(GenericAdminModelAdmin): 51 | pass 52 | 53 | admin.site.register(NavBarEntry, NavBarEntryAdmin) 54 | 55 | That's it. 56 | 57 | Provided admin classes 58 | ---------------------- 59 | 60 | A short overview of the admin classes and their uses provided by 61 | *django-genericadmin*. 62 | 63 | - **GenericAdminModelAdmin** — The admin for a standard Django model 64 | that has at least one generic foreign relation. 65 | 66 | - **TabularInlineWithGeneric** and **StackedInlineWithGeneric** — 67 | Normal inline admins for models that have a generic relation and are 68 | edited inline. 69 | 70 | - **GenericTabularInline** and **GenericStackedInline** — Used to 71 | provide *True Polymorphic Relationships* (see below) and generic 72 | relations in the admin. Also see the Django docs 73 | `here `__. 74 | 75 | Inline Usage 76 | ------------ 77 | 78 | To use *django-genericadmin* with admin inlines, your models must 79 | inherit from ``GenericAdminModelAdmin`` as described above: 80 | 81 | .. code:: python 82 | 83 | from genericadmin.admin import GenericAdminModelAdmin 84 | 85 | class NavBarEntryAdmin(GenericAdminModelAdmin): 86 | pass 87 | 88 | admin.site.register(NavBarEntry, NavBarEntryAdmin) 89 | 90 | Additionally the inline classes must inherit from either 91 | ``StackedInlineWithGeneric`` or ``TabularInlineWithGeneric``: 92 | 93 | .. code:: python 94 | 95 | from genericadmin.admin import GenericAdminModelAdmin, TabularInlineWithGeneric 96 | 97 | class PagesInline(TabularInlineWithGeneric): 98 | model = ... 99 | 100 | class NavBarEntryAdmin(GenericAdminModelAdmin): 101 | inlines = [PagesInline, ] 102 | 103 | ... 104 | 105 | Note that you can't mix and match. If you're going to use a generic 106 | inline, the class using it must inherit from ``GenericAdminModelAdmin``. 107 | 108 | Specifying which fields are handled 109 | ----------------------------------- 110 | 111 | In most cases *django-genericadmin* will correctly figure out which 112 | fields on your model are generic foreign keys and just do the right 113 | thing. If you want to specify the fields yourself (Control your own 114 | destiny and all that) you can use the ``generic_fk_fields`` attribute on 115 | the admin class. Note that you can specify the fields on each admin 116 | class for inline admins. So, for the above mentioned inline admin, you 117 | would do it like so: 118 | 119 | .. code:: python 120 | 121 | class PagesInline(TabularInlineWithGeneric): 122 | model = AReallyCoolPage 123 | generic_fk_fields = [{ 124 | 'ct_field': , 125 | 'fk_field': , 126 | }] 127 | 128 | If you want to use more then one field pair, you can just add more dicts 129 | to the list. 130 | 131 | If you use the ``ct_field`` and ``ct_fk_field`` attributes 132 | *django-genericadmin* will always just ignore those fields and not even 133 | try to use them. 134 | 135 | Blacklisting Content Types 136 | -------------------------- 137 | 138 | Specific content types can be removed from the content type select list. 139 | Example: 140 | 141 | .. code:: python 142 | 143 | class NavBarEntryAdmin(GenericAdminModelAdmin): 144 | content_type_blacklist = ('auth/group', 'auth/user', ) 145 | 146 | Whitelisting Content Types 147 | -------------------------- 148 | 149 | Specific content types that can be display from the content type select 150 | list. Example: 151 | 152 | .. code:: python 153 | 154 | class NavBarEntryAdmin(GenericAdminModelAdmin): 155 | content_type_whitelist = ('auth/message', ) 156 | 157 | Note that this only happens on the client; there is no enforcement of 158 | the blacklist at the model level. 159 | 160 | Lookup parameters by Content Type 161 | --------------------------------- 162 | 163 | Supply extra lookup parameters per content type similar to how 164 | limit\_choices\_to works with raw id fields. Example: 165 | 166 | .. code:: python 167 | 168 | class NavBarEntryAdmin(GenericAdminModelAdmin): 169 | content_type_lookups = {'app.model': {'field': 'value'} 170 | 171 | True Polymorphic Relationships 172 | ------------------------------ 173 | 174 | ``django-genericadmin`` also provides a UI to easily manage a 175 | particularly useful model that, when used as an inline on another model, 176 | enables relations from any entry of any model to any other entry of any 177 | other model. And, because it has a generic relationship moving in both 178 | directions, it means it can be attached as an inline *to any model* 179 | without having to create unique, individual foreign keys for each model 180 | you want to use it on. 181 | 182 | Here's an example of a polymorphic model: 183 | 184 | .. code:: python 185 | 186 | from django.db import models 187 | from django.contrib.contenttypes.models import ContentType 188 | from django.contrib.contenttypes import generic 189 | 190 | class RelatedContent(models.Model): 191 | """ 192 | Relates any one entry to another entry irrespective of their individual models. 193 | """ 194 | content_type = models.ForeignKey(ContentType) 195 | object_id = models.PositiveIntegerField() 196 | content_object = generic.GenericForeignKey('content_type', 'object_id') 197 | 198 | parent_content_type = models.ForeignKey(ContentType, related_name="parent_test_link") 199 | parent_object_id = models.PositiveIntegerField() 200 | parent_content_object = generic.GenericForeignKey('parent_content_type', 'parent_object_id') 201 | 202 | def __unicode__(self): 203 | return "%s: %s" % (self.content_type.name, self.content_object) 204 | 205 | And here's how you'd set up your admin.py: 206 | 207 | .. code:: python 208 | 209 | from whateverapp.models import RelatedContent 210 | from genericadmin.admin import GenericAdminModelAdmin, GenericTabularInline 211 | 212 | class RelatedContentInline(GenericTabularInline): 213 | model = RelatedContent 214 | ct_field = 'parent_content_type' # See below (1). 215 | ct_fk_field = 'parent_object_id' # See below (1). 216 | 217 | class WhateverModelAdmin(GenericAdminModelAdmin): # Super important! See below (2). 218 | content_type_whitelist = ('app/model', 'app2/model2' ) # Add white/black lists on this class 219 | inlines = [RelatedContentInline,] 220 | 221 | (1) By default ``ct_field`` and ``ct_fk_field`` will default to 222 | ``content_type`` and ``object_id`` respectively. ``ct_field`` and 223 | ``ct_fk_field`` are used to create the parent link from the inline to 224 | the model you are attaching it to (similar to how Django does this 225 | attachment using foreign keys with more conventional inlines). You could 226 | also leave this configuration out of your inline classes but, if you do 227 | that, I encourage you to change the model attributes from 228 | ``parent_content_type`` & ``parent_object_id`` to ``child_content_type`` 229 | & ``child_object_id``. I say this because, when it comes time to make 230 | queries, you'll want to know which direction you're 'traversing' in. 231 | 232 | (2) Make sure that whatever the admin classes are utilizing these 233 | inlines are subclasses of ``GenericAdminModelAdmin`` from 234 | ``django-genericadmin`` or else the handy-dandy javascript-utilizing 235 | interface won't work as intended. 236 | -------------------------------------------------------------------------------- /genericadmin/static/genericadmin/js/genericadmin.js: -------------------------------------------------------------------------------- 1 | /* 2 | genericadmin - Weston Nielson (wnielson@gmail.com) 3 | 4 | updated by Jan Schrewe (jschrewe@googlemail.com) 5 | 6 | updated by Troy Melhase (troy.melhase@gmail.com) 7 | 8 | updated by Jonathan Ellenberger (jon@respondcreate.com) 9 | 10 | */ 11 | (function($) { 12 | var GenericAdmin = { 13 | url_array: null, 14 | fields: null, 15 | obj_url: "../obj-data/", 16 | admin_media_url: window.__admin_media_prefix__, 17 | popup: '_popup', 18 | 19 | prepareSelect: function(select) { 20 | var that = this, 21 | opt_keys = [], 22 | opt_dict = {}, 23 | no_value, 24 | opt_group_css = 'style="font-style:normal; font-weight:bold; color:#999; padding-left: 2px;"'; 25 | 26 | // polish the look of the select 27 | select.find('option').each(function() { 28 | var key, opt; 29 | 30 | if (this.value) { 31 | if (that.url_array[this.value]) { 32 | key = that.url_array[this.value][0].split('/')[0]; 33 | 34 | opt = $(this).clone(); 35 | opt.text(that.capFirst(opt.text())); 36 | if ($.inArray(key, opt_keys) < 0) { 37 | opt_keys.push(key); 38 | // if it's the first time in array 39 | // it's the first time in dict 40 | opt_dict[key] = [opt]; 41 | } else { 42 | opt_dict[key].push(opt); 43 | } 44 | } 45 | } else { 46 | no_value = $(this).clone(); 47 | } 48 | }); 49 | select.empty().append(no_value); 50 | 51 | opt_keys = opt_keys.sort(); 52 | 53 | $.each(opt_keys, function(index, key) { 54 | var opt_group = $(''); 55 | $.each(opt_dict[key], function(index, value) { 56 | opt_group.append(value).css('color', '#000'); 57 | }); 58 | select.append(opt_group); 59 | }); 60 | 61 | return select; 62 | }, 63 | 64 | getLookupUrlParams: function(cID) { 65 | var q = this.url_array[cID][1] || {}, 66 | str = []; 67 | for(var p in q) { 68 | str.push(encodeURIComponent(p) + "=" + encodeURIComponent(q[p])); 69 | } 70 | x = str.join("&"); 71 | url = x ? ("?" + x) : ""; 72 | return url; 73 | }, 74 | 75 | getLookupUrl: function(cID) { 76 | return '../../../' + this.url_array[cID][0] + '/' + this.getLookupUrlParams(cID); 77 | }, 78 | 79 | getFkId: function() { 80 | if (this.fields.inline === false) { 81 | return 'id_' + this.fields.fk_field; 82 | } else { 83 | return ['id_', this.fields.prefix, '-', this.fields.number, '-', this.fields.fk_field].join(''); 84 | } 85 | }, 86 | 87 | getCtId: function() { 88 | if (this.fields.inline === false) { 89 | return 'id_' + this.fields.ct_field; 90 | } else { 91 | return ['id_', this.fields.prefix, '-', this.fields.number, '-', this.fields.ct_field].join(''); 92 | } 93 | }, 94 | 95 | capFirst: function(string) { 96 | return string.charAt(0).toUpperCase() + string.slice(1); 97 | }, 98 | 99 | hideLookupLink: function() { 100 | var this_id = this.getFkId(); 101 | $('#lookup_' + this_id).unbind().remove(); 102 | $('#lookup_text_' + this_id + ' a').remove(); 103 | $('#lookup_text_' + this_id + ' span').remove(); 104 | }, 105 | 106 | showLookupLink: function() { 107 | var that = this, 108 | url = this.getLookupUrl(this.cID), 109 | id = 'lookup_' + this.getFkId(), 110 | link = ' '; 111 | 112 | link = link + ''; 113 | 114 | // insert link html after input element 115 | this.object_input.after(link); 116 | 117 | return id; 118 | }, 119 | 120 | pollInputChange: function(window) { 121 | var that = this, 122 | interval_id = setInterval(function() { 123 | if (window.closed === true) { 124 | clearInterval(interval_id); 125 | that.updateObjectData()(); 126 | return true; 127 | } 128 | }, 129 | 150); 130 | }, 131 | 132 | popRelatedObjectLookup: function(link) { 133 | var name = id_to_windowname(this.getFkId()), 134 | url_parts = [], 135 | href, 136 | win; 137 | 138 | if (link.href.search(/\?/) >= 0) { 139 | url_parts[0] = '&'; 140 | //href = link.href + '&pop=1'; 141 | } else { 142 | url_parts[0] = '?'; 143 | //href = link.href + '?pop=1'; 144 | } 145 | url_parts[1] = this.popup; 146 | url_parts[2] = '=1'; 147 | href = link.href + url_parts.join(''); 148 | 149 | var left = window.screen.width / 2 - 400, 150 | top = window.screen.height / 2 - 250; 151 | win = window.open(href, name, 'left='+left+',top='+top+',height=500,width=800,resizable=yes,scrollbars=yes'); 152 | 153 | // wait for popup to be closed and load object data 154 | this.pollInputChange(win); 155 | 156 | win.focus(); 157 | return false; 158 | }, 159 | 160 | updateObjectData: function() { 161 | var that = this; 162 | return function() { 163 | var value = that.object_input.val(); 164 | 165 | if (!value) { 166 | return 167 | } 168 | //var this_id = that.getFkId(); 169 | $('#lookup_text_' + that.getFkId() + ' span').text('loading...'); 170 | $.ajax({ 171 | url: that.obj_url, 172 | dataType: 'json', 173 | data: { 174 | object_id: value, 175 | content_type: that.cID 176 | }, 177 | success: function(item) { 178 | if (item && item.content_type_text && item.object_text) { 179 | var url = that.getLookupUrl(that.cID); 180 | $('#lookup_text_' + that.getFkId() + ' a') 181 | .text(item.content_type_text + ': ' + item.object_text) 182 | .attr('href', url + item.object_id); 183 | 184 | // run a callback to do other stuff like prepopulating url fields 185 | // can't be done with normal django admin prepopulate 186 | if (that.updateObjectDataCallback) { 187 | that.updateObjectDataCallback(item); 188 | } 189 | } 190 | $('#lookup_text_' + that.getFkId() + ' span').text(''); 191 | }, 192 | error: function(xhr, status, error) { 193 | $('#lookup_text_' + that.getFkId() + ' span').text('') 194 | .html('Error: ' + xhr.status + ' – ' + that.capFirst(xhr.statusText.toLowerCase())); 195 | if (xhr.status === 404) { 196 | that.object_input.val(''); 197 | } else { 198 | $('#lookup_text_' + that.getFkId() + ' span').css('color', '#f00'); 199 | } 200 | } 201 | }); 202 | }; 203 | }, 204 | 205 | install: function(fields, url_array, popup_var) { 206 | var that = this; 207 | 208 | this.url_array = url_array; 209 | this.fields = fields; 210 | this.popup = popup_var || this.popup; 211 | 212 | // store the base element 213 | this.object_input = $("#" + this.getFkId()); 214 | 215 | // find the select we need to change 216 | this.object_select = this.prepareSelect($("#" + this.getCtId())); 217 | 218 | // install event handler for select 219 | this.object_select.change(function() { 220 | // reset the object input to the associated select (this one) 221 | var link_id; 222 | 223 | //(this).css('color', 'red'); // uncomment for testing 224 | that.hideLookupLink(); 225 | // Set our objectId when the content_type is changed 226 | if (this.value) { 227 | that.cID = this.value; 228 | link_id = that.showLookupLink(); 229 | $('#' + link_id).click(function(e) { 230 | e.preventDefault(); 231 | that.popRelatedObjectLookup(this); 232 | }); 233 | } 234 | }); 235 | 236 | // fire change event if something is already selected 237 | if (this.object_select.val()) { 238 | this.object_select.trigger('change'); 239 | } 240 | 241 | // Bind to the onblur of the object_id input. 242 | this.object_input.blur(that.updateObjectData()); 243 | 244 | // Fire once for initial link. 245 | this.updateObjectData()(); 246 | } 247 | }; 248 | 249 | var InlineAdmin = { 250 | sub_admins: null, 251 | url_array: null, 252 | fields: null, 253 | popup: '_popup', 254 | 255 | install: function(fields, url_array, popup_var) { 256 | var inline_count = $('#id_' + fields.prefix + '-TOTAL_FORMS').val(), 257 | admin; 258 | 259 | this.url_array = url_array; 260 | this.fields = fields; 261 | this.sub_admins = []; 262 | this.popup = popup_var || this.popup; 263 | 264 | for (var j = 0; j < inline_count; j++) { 265 | f = $.extend({}, this.fields); 266 | f.number = j; 267 | admin = $.extend({}, GenericAdmin); 268 | admin.install(f, this.url_array, popup_var); 269 | this.sub_admins.push(admin); 270 | } 271 | $('#' + this.fields.prefix + '-group .add-row a').click(this.addHandler()); 272 | }, 273 | addHandler: function() { 274 | var that = this; 275 | return function(e) { 276 | e.preventDefault(); 277 | var added_fields = $.extend({}, that.fields), 278 | admin = $.extend({}, GenericAdmin); 279 | added_fields.number = ($('#id_' + that.fields.prefix + '-TOTAL_FORMS').val() - 1); 280 | admin.install(added_fields, that.url_array, that.popup); 281 | that.sub_admins.push(admin); 282 | 283 | $('#' + that.fields.prefix + '-' + added_fields.number + ' .inline-deletelink').click( 284 | that.removeHandler(that) 285 | ); 286 | } 287 | }, 288 | removeHandler: function(that) { 289 | return function(e) { 290 | var parent_id, 291 | deleted_num, 292 | sub_admin; 293 | 294 | e.preventDefault(); 295 | parent_id = $(e.currentTarget).parents('.dynamic-' + that.fields.prefix).first().attr('id'); 296 | deleted_num = parseInt(parent_id.charAt(parent_id.length - 1), 10); 297 | for (var i = (that.sub_admins.length - 1); i >= 0; i--) { 298 | sub_admin = that.sub_admins[i]; 299 | if (sub_admin.fields.number === deleted_num) { 300 | that.sub_admins.splice(i, 1); 301 | } else if (sub_admin.fields.number > deleted_num) { 302 | sub_admin.fields.number = sub_admin.fields.number - 1; 303 | } 304 | } 305 | } 306 | } 307 | }; 308 | 309 | $(document).ready(function() { 310 | $.ajax({ 311 | url: '../genericadmin-init/', 312 | dataType: 'json', 313 | success: function(data) { 314 | var url_array = data.url_array, 315 | ct_fields = data.fields, 316 | popup_var = data.popup_var, 317 | fields; 318 | 319 | for (var i = 0; i < ct_fields.length; i++) { 320 | fields = ct_fields[i]; 321 | if (fields.inline === false) { 322 | $.extend({}, GenericAdmin).install(fields, url_array, popup_var); 323 | } else { 324 | $.extend({}, InlineAdmin).install(fields, url_array, popup_var); 325 | } 326 | } 327 | } 328 | }); 329 | }); 330 | } (django.jQuery)); 331 | --------------------------------------------------------------------------------