├── README ├── MANIFEST.in ├── setup.cfg ├── screenshot.png ├── MANIFEST ├── .gitignore ├── relatedwidget ├── templates │ └── relatedwidget │ │ └── widget.html ├── static │ └── relatedwidget │ │ └── js │ │ └── relatedwidget.js └── __init__.py ├── README.md └── setup.py /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | MANIFEST -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjaoming/django-relatedadminwidget/HEAD/screenshot.png -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include setup.cfg 4 | include MANIFEST 5 | # include requirements.txt 6 | recursive-include relatedwidget *.html *.txt *.png *.js *.css *.gif *.less *.mo *.po *.otf *.svg *.woff *.eot *.ttf 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by http://gitignore.org/ 2 | 3 | # 4 | # Linux 5 | # 6 | 7 | # Ignore "backup" files which vim and emacs commonly create 8 | *~ 9 | 10 | # 11 | # Python 12 | # 13 | 14 | dist 15 | *egg-info 16 | 17 | # Compiled Python file 18 | *.pyc 19 | 20 | # 21 | # Subversion 22 | # 23 | 24 | # Subversion data folders 25 | .svn/ 26 | 27 | -------------------------------------------------------------------------------- /relatedwidget/templates/relatedwidget/widget.html: -------------------------------------------------------------------------------- 1 | {{ widget }} 2 | {% if can_change_related %} 3 | 4 | {{ change_help_text }} 5 | 6 | {% endif %} 7 | {% if can_add_related %} 8 | 9 | {{ add_help_text }} 10 | 11 | {% endif %} 12 | {% if can_delete_related %} 13 | 14 | {{ delete_help_text }} 15 | 16 | {% endif %} 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | django-relatedadminwidget 2 | ========================= 3 | 4 | Widget for displaying edit and delete links alongside foreign key admin widgets 5 | 6 | ![Flowers](https://github.com/benjaoming/django-relatedadminwidget/raw/master/screenshot.png) 7 | 8 | Also see this project: [django-admin-enhancer](https://github.com/charettes/django-admin-enhancer) 9 | 10 | Installation: 11 | 12 | 1. **pip install django-relatedadminwidget** 13 | 2. Add "relatedwidget" to settings.INSTALLED_APPS 14 | 3. You may want to run your project's ./manage.py collectstatic 15 | 4. In your applications' admin.py, let the model admins inherit from RelatedWidgetWrapperBase like in this example: 16 | 17 | from django.contrib import admin 18 | from relatedwidget import RelatedWidgetWrapperBase 19 | 20 | class MyModelAdmin(RelatedWidgetWrapperBase, admin.ModelAdmin): 21 | pass 22 | 23 | admin.site.register(MyModel, MyModelAdmin) 24 | 25 | It also works with TabularInline and StackedInline! Remember the order of inheritence, always put RelatedWidgetWrapperBase first! 26 | 27 | Troubleshooting 28 | --------------- 29 | 30 | If you get a `TemplateDoesNotExist` error on 'relatedwidget/widget.html', you might have to add `django.template.loaders.eggs.Loader` to your `settings.TEMPLATE_LOADERS`. 31 | 32 | Credits 33 | ------- 34 | 35 | User [nasp](http://djangosnippets.org/users/nasp/) did most of the work, I just updated it for Django 1.4 and packed it as an app. 36 | -------------------------------------------------------------------------------- /relatedwidget/static/relatedwidget/js/relatedwidget.js: -------------------------------------------------------------------------------- 1 | function dismissEditRelatedPopup(win, objId, newRepr) { 2 | objId = html_unescape(objId); 3 | newRepr = html_unescape(newRepr); 4 | var name = windowname_to_id(win.name).replace(/^edit_/, '');; 5 | var elem = document.getElementById(name); 6 | if (elem) { 7 | var opts = elem.options, 8 | l = opts.length; 9 | for (var i = 0; i < l; i++) { 10 | if (opts[i] && opts[i].value == objId) { 11 | opts[i].innerHTML = newRepr; 12 | } 13 | } 14 | } 15 | win.close(); 16 | }; 17 | 18 | if (!dismissAddAnotherPopup.original) { 19 | var originalDismissAddAnotherPopup = dismissAddAnotherPopup; 20 | dismissAddAnotherPopup = function(win, newId, newRepr) { 21 | originalDismissAddAnotherPopup(win, newId, newRepr); 22 | newId = html_unescape(newId); 23 | newRepr = html_unescape(newRepr); 24 | var id = windowname_to_id(win.name); 25 | $('#' + id).trigger('change'); 26 | }; 27 | dismissAddAnotherPopup.original = originalDismissAddAnotherPopup; 28 | } 29 | 30 | django.jQuery(document).ready(function() { 31 | 32 | var $ = $ || jQuery || django.jQuery, 33 | relatedWidgetCSSSelector = '.related-widget-wrapper-change-link, .related-widget-wrapper-delete-link', 34 | hrefTemplateAttr = 'data-href-template'; 35 | 36 | $('body').on('change', '.related-widget-wrapper', function(){ 37 | var siblings = $(this).nextAll(relatedWidgetCSSSelector); 38 | if (!siblings.length) return; 39 | if (this.value) { 40 | var val = this.value; 41 | siblings.each(function(){ 42 | var elm = $(this); 43 | elm.attr('href', interpolate(elm.attr(hrefTemplateAttr), [val])); 44 | }); 45 | } 46 | else { 47 | siblings.removeAttr('href'); 48 | } 49 | }); 50 | 51 | $('body').on('click', '.related-widget-wrapper-link', function(){ 52 | if (this.href) { 53 | return showAddAnotherPopup(this); 54 | } else return false; 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from setuptools import setup, find_packages 5 | 6 | # Utility function to read the README file. 7 | # Used for the long_description. It's nice, because now 1) we have a top level 8 | # README file and 2) it's easier to type in the README file than to put a raw 9 | # string in below ... 10 | def read(fname): 11 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 12 | 13 | def build_media_pattern(base_folder, file_extension): 14 | return ["%s/%s*.%s" % (base_folder, "*/"*x, file_extension) if base_folder else "%s*.%s" % ("*/"*x, file_extension) for x in range(10)] 15 | 16 | media_patterns = ( build_media_pattern("templates", "html") + 17 | build_media_pattern("static", "js") + 18 | build_media_pattern("static", "css") + 19 | build_media_pattern("static", "png") + 20 | build_media_pattern("static", "jpeg") + 21 | build_media_pattern("static", "gif") 22 | ) 23 | 24 | packages = find_packages() 25 | 26 | package_data = dict( 27 | (package_name, media_patterns) 28 | for package_name in packages 29 | ) 30 | 31 | setup( 32 | name = "django-relatedadminwidget", 33 | version = "0.1", 34 | url = "https://github.com/benjaoming/django-relatedadminwidget", 35 | author = "Benjamin Bach", 36 | author_email = "benjamin@overtag.dk", 37 | description = ("Get edit and delete links in your django admin. A utility class to let your model admins inherit from."), 38 | license = "BSD", 39 | keywords = "django admin", 40 | packages=find_packages(), 41 | long_description=read('README.md'), 42 | zip_safe = False, 43 | install_requires=[ 44 | 'Django>=2.0', 45 | ], 46 | classifiers=[ 47 | 'Development Status :: 4 - Beta', 48 | 'Topic :: Utilities', 49 | 'License :: OSI Approved :: BSD License', 50 | 'Environment :: Web Environment', 51 | 'Framework :: Django', 52 | 'Intended Audience :: Developers', 53 | 'License :: OSI Approved :: BSD License', 54 | 'Operating System :: OS Independent', 55 | 'Programming Language :: Python', 56 | ], 57 | include_package_data=True, 58 | package_data=package_data, 59 | ) 60 | -------------------------------------------------------------------------------- /relatedwidget/__init__.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.admin.widgets import RelatedFieldWidgetWrapper 3 | from django.urls import reverse 4 | from django.template.loader import render_to_string 5 | from django.utils.safestring import mark_safe 6 | from django.utils.http import urlquote 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | from django.contrib import admin 10 | from django.forms.widgets import SelectMultiple 11 | from django.http import HttpResponse 12 | from django.utils.html import escape, escapejs 13 | 14 | class RelatedFieldWidgetWrapper(RelatedFieldWidgetWrapper): 15 | 16 | class Media: 17 | js = ("%srelatedwidget/js/relatedwidget.js" % settings.STATIC_URL,) 18 | 19 | def __init__(self, *args, **kwargs): 20 | self.can_change_related = kwargs.pop('can_change_related', None) 21 | self.can_delete_related = kwargs.pop('can_delete_related', None) 22 | super(RelatedFieldWidgetWrapper, self).__init__(*args, **kwargs) 23 | 24 | @classmethod 25 | def from_contrib_wrapper(cls, wrapper, can_change_related, can_delete_related): 26 | return cls(wrapper.widget, wrapper.rel, wrapper.admin_site, 27 | can_add_related=wrapper.can_add_related, 28 | can_change_related=can_change_related, 29 | can_delete_related=can_delete_related) 30 | 31 | def get_related_url(self, rel_to, info, action, args=[]): 32 | return reverse("admin:%s_%s_%s" % (info + (action,)), current_app=self.admin_site.name, args=args) 33 | 34 | def get_related_url_template(self, rel_to, info, action): 35 | template = self.get_related_url(rel_to, info, action, ['%s']) 36 | format_substring = urlquote('%s') 37 | # Replace urlquoted '%s' if it exists (on Django 1.6+) with %s so that 38 | # URL template is a correct format string. 39 | return template.replace(format_substring, '%s') 40 | 41 | def render(self, name, value, attrs={}, *args, **kwargs): 42 | rel_to = self.rel.to 43 | info = (rel_to._meta.app_label, rel_to._meta.object_name.lower()) 44 | self.widget.choices = self.choices 45 | attrs['class'] = ' '.join((attrs.get('class', ''), 'related-widget-wrapper')) 46 | context = {'widget': self.widget.render(name, value, attrs, *args, **kwargs), 47 | 'name': name, 48 | 'STATIC_URL': settings.STATIC_URL, 49 | 'can_change_related': self.can_change_related, 50 | 'can_add_related': self.can_add_related, 51 | 'can_delete_related': self.can_delete_related} 52 | if self.can_change_related: 53 | if value: 54 | context['change_url'] = self.get_related_url(rel_to, info, 'change', [value]) 55 | template = self.get_related_url_template(rel_to, info, 'change') 56 | context.update({ 57 | 'change_url_template': template, 58 | 'change_help_text': _('Change related model') 59 | }) 60 | if self.can_add_related: 61 | context.update({ 62 | 'add_url': self.get_related_url(rel_to, info, 'add'), 63 | 'add_help_text': _('Add Another') 64 | }) 65 | if self.can_delete_related: 66 | if value: 67 | context['delete_url'] = self.get_related_url(rel_to, info, 'delete', [value]) 68 | template = self.get_related_url_template(rel_to, info, 'delete') 69 | context.update({ 70 | 'delete_url_template': template, 71 | 'delete_help_text': _('Delete related model') 72 | }) 73 | 74 | return mark_safe(render_to_string('relatedwidget/widget.html', context)) 75 | 76 | 77 | class RelatedWidgetWrapperBase(object): 78 | 79 | def formfield_for_dbfield(self, db_field, **kwargs): 80 | formfield = super(RelatedWidgetWrapperBase, self).formfield_for_dbfield(db_field, **kwargs) 81 | if (formfield and 82 | isinstance(formfield.widget, admin.widgets.RelatedFieldWidgetWrapper) and 83 | not isinstance(formfield.widget.widget, SelectMultiple)): 84 | request = kwargs.pop('request', None) 85 | related_modeladmin = self.admin_site._registry.get(db_field.rel.to) 86 | can_change_related = bool(related_modeladmin and 87 | related_modeladmin.has_change_permission(request)) 88 | can_delete_related = bool(related_modeladmin and 89 | related_modeladmin.has_delete_permission(request)) 90 | widget = RelatedFieldWidgetWrapper.from_contrib_wrapper(formfield.widget, 91 | can_change_related, 92 | can_delete_related) 93 | formfield.widget = widget 94 | return formfield 95 | 96 | def response_change(self, request, obj): 97 | if '_popup' in request.REQUEST: 98 | pk_value = obj._get_pk_val() 99 | return HttpResponse('' % \ 100 | # escape() calls force_unicode. 101 | (escape(pk_value), escapejs(obj))) 102 | else: 103 | return super(RelatedWidgetWrapperBase, self).response_change(request, obj) 104 | 105 | class RelatedWidgetWrapperAdmin(RelatedWidgetWrapperBase, admin.ModelAdmin): 106 | pass 107 | 108 | class RelatedWidgetWrapperTabularInline(RelatedWidgetWrapperBase, admin.TabularInline): 109 | pass 110 | 111 | class RelatedWidgetWrapperStackedInline(RelatedWidgetWrapperBase, admin.StackedInline): 112 | pass 113 | --------------------------------------------------------------------------------