Add preliminairy working JavaScript to render polymorphic inlines

fix_request_path_info
Diederik van der Boor 2016-08-09 01:20:40 +02:00
parent 1f0ddd8436
commit 7330a4f099
7 changed files with 552 additions and 1 deletions

View File

@ -2,4 +2,5 @@ include README.rst
include LICENSE include LICENSE
include DOCS.rst include DOCS.rst
include CHANGES.rst include CHANGES.rst
recursive-include polymorphic/static *.js *.css
recursive-include polymorphic/templates * recursive-include polymorphic/templates *

View File

@ -3,8 +3,13 @@ Rendering utils for admin forms;
This makes sure that admin fieldsets/layout settings are exported to the template. This makes sure that admin fieldsets/layout settings are exported to the template.
""" """
import json
import django import django
from django.contrib.admin.helpers import InlineAdminFormSet, InlineAdminForm, AdminField from django.contrib.admin.helpers import InlineAdminFormSet, InlineAdminForm, AdminField
from django.utils.encoding import force_text
from django.utils.text import capfirst
from django.utils.translation import ugettext
from polymorphic.formsets import BasePolymorphicModelFormSet from polymorphic.formsets import BasePolymorphicModelFormSet
@ -14,6 +19,9 @@ class PolymorphicInlineAdminForm(InlineAdminForm):
Expose the admin configuration for a form Expose the admin configuration for a form
""" """
def polymorphic_ctype_field(self):
return AdminField(self.form, 'polymorphic_ctype', False)
class PolymorphicInlineAdminFormSet(InlineAdminFormSet): class PolymorphicInlineAdminFormSet(InlineAdminFormSet):
""" """
@ -71,6 +79,30 @@ class PolymorphicInlineAdminFormSet(InlineAdminFormSet):
fields.update(child_inline.get_prepopulated_fields(self.request, self.obj)) fields.update(child_inline.get_prepopulated_fields(self.request, self.obj))
return fields return fields
# The polymorphic template follows the same method like all other inlines do in Django 1.10.
# This method is added for compatibility with older Django versions.
def inline_formset_data(self):
"""
A JavaScript data structure for the JavaScript code
"""
verbose_name = self.opts.verbose_name
return json.dumps({
'name': '#%s' % self.formset.prefix,
'options': {
'prefix': self.formset.prefix,
'addText': ugettext('Add another %(verbose_name)s') % {
'verbose_name': capfirst(verbose_name),
},
'childTypes': [
{
'type': model._meta.model_name,
'name': force_text(model._meta.verbose_name)
} for model in self.formset.child_forms.keys()
],
'deleteText': ugettext('Remove'),
}
})
class PolymorphicInlineSupportMixin(object): class PolymorphicInlineSupportMixin(object):
""" """

View File

@ -22,8 +22,22 @@ class PolymorphicInlineModelAdmin(InlineModelAdmin):
* Permissions are only checked on the base model. * Permissions are only checked on the base model.
* The child inlines can't override the base model fields, only this parent inline can do that. * The child inlines can't override the base model fields, only this parent inline can do that.
""" """
formset = BasePolymorphicInlineFormSet formset = BasePolymorphicInlineFormSet
#: The extra media to add for the polymorphic inlines effect.
#: This can be redefined for subclasses.
polymorphic_media = Media(
js=(
'polymorphic/js/jquery.django-inlines.js',
),
css={
'all': (
'polymorphic/css/polymorphic_inlines.css',
)
}
)
#: The extra forms to show #: The extra forms to show
#: By default there are no 'extra' forms as the desired type is unknown. #: By default there are no 'extra' forms as the desired type is unknown.
#: Instead, add each new item using JavaScript that first offers a type-selection. #: Instead, add each new item using JavaScript that first offers a type-selection.

View File

@ -26,7 +26,7 @@ class PolymorphicFormSetChild(object):
# This is mostly needed for the generic inline formsets # This is mostly needed for the generic inline formsets
self._form_base = form self._form_base = form
self.fields = fields self.fields = fields
self.exclude = exclude self.exclude = exclude or ()
self.formfield_callback = formfield_callback self.formfield_callback = formfield_callback
self.widgets = widgets self.widgets = widgets
self.localized_fields = localized_fields self.localized_fields = localized_fields

View File

@ -0,0 +1,27 @@
.add-row-choice {
position: relative;
}
.add-row-choice a:focus {
text-decoration: none;
}
.add-row .inline-type-choice {
position: absolute;
top: 2.2em;
left: 0.5em;
border: 1px solid #ccc;
border-radius: 4px;
padding: 2px;
background-color: #fff;
}
.add-row .inline-type-choice ul {
padding: 2px;
margin: 0;
}
.add-row .inline-type-choice li {
list-style: none inside none;
padding: 4px 8px;
}

View File

@ -0,0 +1,440 @@
/**
* jQuery plugin for Django inlines
*
* (c) 2011-2016 Diederik van der Boor, Apache 2 Licensed.
*/
(function($){
function DjangoInline(group, options) {
options = $.extend({}, $.fn.djangoInline.defaults, options);
this.group = group;
this.$group = $(group);
this.options = options;
options.prefix = options.prefix || this.$group.attr('id').replace(/-group$/, '');
if( options.formTemplate ) {
this.$form_template = $(options.formTemplate);
} else {
this.$form_template = this.$group.find(this.options.emptyFormSelector); // the extra item to construct new instances.
}
// Create the add button if requested (null/undefined means auto select)
if(options.showAddButton !== false) {
var dominfo = this._getManagementForm();
if (dominfo.max_forms == null || dominfo.max_forms.value === '' || (dominfo.max_forms.value - dominfo.total_forms.value) > 0) {
this.createAddButton();
}
}
}
DjangoInline.prototype = {
/**
* Create the add button
*/
createAddButton: function() {
var $addButton;
var myself = this;
if (this.options.childTypes) {
// Polymorphic inlines!
// The add button opens a menu.
var menu = '<div class="inline-type-choice" style="display: none;"><ul>';
for (var i = 0; i < this.options.childTypes.length; i++) {
var obj = this.options.childTypes[i];
menu += '<li><a href="#" data-type="' + obj.type + '">' + obj.name + '</a></li>';
}
menu += '</ul></div>';
$addButton = $('<div class="' + this.options.addCssClass + ' add-row-choice"><a href="#">' + this.options.addText + "</a>" + menu + "</div>");
this.$group.append($addButton);
$addButton.children('a').click($.proxy(this._onMenuToggle, this));
$addButton.find('li a').click(function(event){ myself._onMenuItemClick(event); });
}
else {
// Normal inlines
$addButton = $('<div class="' + this.options.addCssClass + '"><a href="#">' + this.options.addText + "</a></div>");
this.$group.append($addButton);
$addButton.find('a').click(function(event) { event.preventDefault(); myself.addForm() });
}
},
_onMenuToggle: function(event) {
event.preventDefault();
event.stopPropagation();
var $menu = $(event.target).next('.inline-type-choice');
if(! $menu.is(':visible')) {
function hideMenu() {
$menu.slideUp();
$(document).unbind('click', hideMenu);
}
$(document).click(hideMenu);
}
$menu.slideToggle();
},
_onMenuItemClick: function(event) {
event.preventDefault();
var type = $(event.target).attr('data-type');
var empty_form_selector = this.options.emptyFormSelector + "[data-inline-type=" + type + "]";
this.addForm(empty_form_selector);
},
/**
* The main action, add a new row.
* Allow to select a different form template (for polymorphic inlines)
*/
addForm: function(emptyFormSelector) {
var $form_template;
if(emptyFormSelector) {
$form_template = this.$group.find(emptyFormSelector);
if($form_template.length === 0) {
throw new Error("Form template '" + emptyFormSelector + "' not found")
}
}
else {
if(! this.$form_template || this.$form_template.length === 0) {
throw new Error("No empty form available. Define the 'form_template' setting or add an '.empty-form' element in the '" + this.options.prefix + "' formset group!");
}
$form_template = this.$form_template;
}
// The Django admin/media/js/inlines.js API is not public, or easy to use.
// Recoded the inline model dynamics.
var management_form = this._getManagementForm();
if(! management_form.total_forms) {
throw new Error("Missing '#" + this._getGroupFieldIdPrefix() + "-TOTAL_FORMS' field. Make sure the management form included!");
}
// When a inline is presented in a complex table,
// the newFormTarget can be very useful to direct the output.
var container;
if(this.options.newFormTarget == null) {
container = $form_template.parent();
}
else if($.isFunction(this.options.newFormTarget)) {
container = this.options.newFormTarget.apply(this.group);
}
else {
container = this.$group.find(this.options.newFormTarget);
}
if(container === null || container.length === 0) {
throw new Error("No container found via custom 'newFormTarget' function!");
}
// Clone the item.
var new_index = management_form.total_forms.value;
var item_id = this._getFormId(new_index);
var newhtml = _getOuterHtml($form_template).replace(/__prefix__/g, new_index);
var newitem = $(newhtml).removeClass("empty-form").attr("id", item_id);
// Add it
container.append(newitem);
var formset_item = $("#" + item_id);
if( formset_item.length === 0 ) {
throw new Error("New FormSet item not found: #" + item_id);
}
formset_item.data('djangoInlineIndex', new_index);
if(this.options.onAdd) {
this.options.onAdd.call(this.group, formset_item, new_index, this.options);
}
// Update administration
management_form.total_forms.value++;
return formset_item;
},
getFormAt: function(index) {
return $('#' + this._getFormId(index));
},
_getFormId: function(index) {
// The form container is expected by the numbered as #prefix-NR
return this.options.itemIdTemplate.replace('{prefix}', this.options.prefix).replace('{index}', index);
},
_getGroupFieldIdPrefix: function() {
// typically: #id_modelname
return this.options.autoId.replace('{prefix}', this.options.prefix);
},
/**
* Get the management form data.
*/
_getManagementForm: function() {
var group_id_prefix = this._getGroupFieldIdPrefix();
return {
// management form item
total_forms: $("#" + group_id_prefix + "-TOTAL_FORMS")[0],
max_forms: $("#" + group_id_prefix + "-MAX_NUM_FORMS")[0],
group_id_prefix: group_id_prefix
}
},
_getItemData: function(child_node) {
var formset_item = $(child_node).closest(this.options.itemsSelector);
if( formset_item.length === 0 ) {
return null;
}
// Split the ID, using the id_template pattern.
// note that ^...$ is important, as a '-' char can occur multiple times with generic inlines (inlinetype-id / app-model-ctfield-ctfkfield-id)
var id = formset_item.attr("id");
var cap = (new RegExp('^' + this.options.itemIdTemplate.replace('{prefix}', '(.+?)').replace('{index}', '(\\d+)') + '$')).exec(id);
return {
formset_item: formset_item,
prefix: cap[1],
index: parseInt(cap[2], 0) // or parseInt(formset_item.data('djangoInlineIndex'))
};
},
/**
* Get the meta-data of a single form.
*/
_getItemForm: function(child_node) {
var dominfo = this._getItemData(child_node);
if( dominfo === null ) {
return null;
}
var field_id_prefix = this._getGroupFieldIdPrefix() + "-" + dominfo.index;
return $.extend({}, dominfo, {
// Export settings data
field_id_prefix: field_id_prefix,
field_name_prefix: dominfo.prefix + '-' + dominfo.index,
// Item fields
pk_field: $('#' + field_id_prefix + '-' + this.options.pkFieldName),
delete_checkbox: $("#" + field_id_prefix + "-DELETE")
});
},
/**
* Remove a row
*/
removeForm: function(child_node)
{
// Get dom info
var management_form = this._getManagementForm();
var itemform = this._getItemForm(child_node);
if( itemform === null ) {
throw new Error("No form found for the selector '" + child_node.selector + "'!");
}
var total_count = parseInt(management_form.total_forms.value, 0);
var has_pk_field = itemform.pk_field.length != 0;
if(this.options.onBeforeRemove) {
this.options.onBeforeRemove.call(this.group, itemform.formset_item, this.options);
}
// In case there is a delete checkbox, save it.
if( itemform.delete_checkbox.length )
{
if(has_pk_field)
itemform.pk_field.insertAfter(management_form.total_forms);
itemform.delete_checkbox.attr('checked', true).insertAfter(management_form.total_forms).hide();
}
else if( has_pk_field && itemform.pk_field[0].value )
{
// Construct a delete checkbox on the fly.
itemform.pk_field.insertAfter(management_form.total_forms);
$('<input type="hidden" id="' + itemform.field_id_prefix + '-DELETE" name="' + itemform.field_name_prefix + '-DELETE" value="on">').insertAfter(itemform.total_forms);
}
else
{
// Newly added item, renumber in reverse order
for( var i = itemform.index + 1; i < total_count; i++ )
{
this._renumberItem(this.getFormAt(i), i - 1);
}
management_form.total_forms.value--;
}
// And remove item
itemform.formset_item.remove();
if(this.options.onRemove) {
this.options.onRemove.call(this.group, itemform.formset_item, this.options);
}
return itemform.formset_item;
},
// Based on django/contrib/admin/media/js/inlines.js
_renumberItem: function($formset_item, new_index)
{
var id_regex = new RegExp("(" + this._getFormId('(\\d+|__prefix__)') + ")");
var replacement = this._getFormId(new_index);
$formset_item.data('djangoInlineIndex', new_index);
// Loop through the nodes.
// Getting them all at once turns out to be more efficient, then looping per level.
var nodes = $formset_item.add( $formset_item.find("*") );
for( var i = 0; i < nodes.length; i++ )
{
var node = nodes[i];
var $node = $(node);
var for_attr = $node.attr('for');
if( for_attr && for_attr.match(id_regex) ) {
$node.attr("for", for_attr.replace(id_regex, replacement));
}
if( node.id && node.id.match(id_regex) ) {
node.id = node.id.replace(id_regex, replacement);
}
if( node.name && node.name.match(id_regex) ) {
node.name = node.name.replace(id_regex, replacement);
}
}
},
// Extra query methods for external callers:
getFormIndex: function(child_node) {
var dominfo = this._getItemData(child_node);
return dominfo ? dominfo.index : null;
},
getForms: function() {
// typically: .inline-related:not(.empty-form)
return this.$group.children(this.options.itemsSelector + ":not(" + this.options.emptyFormSelector + ")");
},
getEmptyForm: function() {
// typically: #modelname-group > .empty-form
return this.$form_template;
},
getFieldIdPrefix: function(item_index) {
if(! $.isNumeric(item_index)) {
var dominfo = this._getItemData(item_index);
if(dominfo === null) {
throw new Error("Unexpected element in getFieldIdPrefix, needs to be item_index, or DOM child node.");
}
item_index = dominfo.index;
}
// typically: #id_modelname-NN
return this._getGroupFieldIdPrefix() + "-" + item_index;
},
getFieldsAt: function(index) {
var $form = this.getFormAt(index);
return this.getFields($form);
},
getFields: function(child_node) {
// Return all fields in a simple lookup object, with the prefix stripped.
var dominfo = this._getItemData(child_node);
if(dominfo === null) {
return null;
}
var fields = {};
var $inputs = dominfo.formset_item.find(':input');
var name_prefix = this.prefix + "-" + dominfo.index;
for(var i = 0; i < $inputs.length; i++) {
var name = $inputs[i].name;
if(name.substring(0, name_prefix.length) == name_prefix) {
var suffix = name.substring(name_prefix.length + 1); // prefix-<name>
fields[suffix] = $inputs[i];
}
}
return fields;
},
removeFormAt: function(index) {
return this.removeForm(this.getFormAt(index));
}
};
function _getOuterHtml($node)
{
if( $node.length )
{
if( $node[0].outerHTML ) {
return $node[0].outerHTML;
} else {
return $("<div>").append($node.clone()).html();
}
}
return null;
}
// jQuery plugin definition
// Separated from the main code, as demonstrated by Twitter bootstrap.
$.fn.djangoInline = function(option) {
var args = Array.prototype.splice.call(arguments, 1);
var call_method = (typeof option == 'string');
var plugin_result = (call_method ? undefined : this);
this.filter('.inline-group').each(function() {
var $this = $(this);
var data = $this.data('djangoInline');
if (! data) {
var options = typeof option == 'object' ? option : {};
$this.data('djangoInline', (data = new DjangoInline(this, options)));
}
if (typeof option == 'string') {
plugin_result = data[option].apply(data, args);
}
});
return plugin_result;
};
$.fn.djangoInline.defaults = {
pkFieldName: 'id', // can be `tablename_ptr` for inherited models.
autoId: 'id_{prefix}', // the auto id format used in Django.
prefix: null, // typically the model name in lower case.
newFormTarget: null, // Define where the row should be added; a CSS selector or function.
itemIdTemplate: '{prefix}-{index}', // Format of the ID attribute.
itemsSelector: '.inline-related', // CSS class that each item has
emptyFormSelector: '.empty-form', // CSS class that
formTemplate: null, // Complete HTML of the new form
childTypes: null, // Extra for django-polymorphic, allow a choice between empty-forms.
showAddButton: true,
addText: "add another", // Text for the add link
deleteText: "remove", // Text for the delete link
addCssClass: "add-row" // CSS class applied to the add link
};
// Also expose inner object
$.fn.djangoInline.Constructor = DjangoInline;
// Auto enable inlines
$.fn.ready(function(){
$('.js-jquery-django-inlines').each(function(){
var $this = $(this);
var data = $this.data();
var inlineOptions = data.inlineFormset;
$this.djangoInline(inlineOptions.options)
});
})
})(window.django ? window.django.jQuery : jQuery);

View File

@ -0,0 +1,37 @@
{% load i18n admin_urls static %}
<div class="js-jquery-django-inlines inline-group"
id="{{ inline_admin_formset.formset.prefix }}-group"
data-inline-type="stacked"
data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<fieldset class="module {{ inline_admin_formset.classes }}">
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }}
{% for inline_admin_form in inline_admin_formset %}
<div class="inline-related inline-{{ inline_admin_form.model_admin.opts.model_name }}{% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if not inline_admin_form.original.pk %} empty-form {% endif %}{% if forloop.last %} last-related{% endif %}"
id="{{ inline_admin_formset.formset.prefix }}-{% if inline_admin_form.original.pk %}{{ forloop.counter0 }}{% else %}empty{% endif %}"
data-inline-type="{{ inline_admin_form.model_admin.opts.model_name }}">
<h3><b>{{ inline_admin_form.model_admin.opts.verbose_name|capfirst }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} <a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="inlinechangelink">{% trans "Change" %}</a>{% endif %}
{% else %}#{{ forloop.counter }}{% endif %}</span>
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %}
{% if inline_admin_form.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
</h3>
{% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %}
{% for fieldset in inline_admin_form %}
{% include "admin/includes/fieldset.html" %}
{% endfor %}
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{{ inline_admin_form.fk_field.field }}
{{ inline_admin_form.polymorphic_ctype_field.field }}
</div>
{% endfor %}
</fieldset>
</div>