├── 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 |
5 |
6 | {% endif %}
7 | {% if can_add_related %}
8 |
9 |
10 |
11 | {% endif %}
12 | {% if can_delete_related %}
13 |
14 |
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 | 
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 |
--------------------------------------------------------------------------------