This snippet adds support for OSM maps for GeometryField in Admin TabularInlines.
The one possible issue with this snippet is that the OSMGeoInlineForm has to know about the parent ModelAdmin which it does through the code
model_admin_instance = admin.sites.site._registry[self.parent_model]
which won't work if you don't use the default model admin to register the model admin. I'll try and come up with a work around and edit the snippet.
Due to the need to mess around with inline style sheets and IE not playing ball with just copying the innerHTML I've settled on using the jQuery.Rule plugin which I've included here as the last version published on the site was incompatible with jQuery 1.4.2 and I found a pathced version online, I also had to modify it due to the Django admin using the compatibility mode of jQuery so there is no global jQuery variable it's django.jQuery instead.
-
Create an osmgeo_inline.py file in your app and copy the top code into it.
-
Create the template file in a directory called admin within a template directory for your app, the template file must be called osmgeo_tabular_inline.html, and copy the middle code into it.
-
Create the jquery rule plugin file in your media or admin-media js directory and copy the bottom code into it. Don't forget to change the OSMGeoInlineForm's class Media's js = ('.....',) to the correct path to the file if need be.
-
In your admin.py you can create inline models using OSMGeoTabularInline just as you would TabularInline.
Examples all based on the following in models.py
from django.contrib.gis.db import models
class MyModel(models.Model):
name = models.CharField(max_length=64)
route = models.LineStringField(srid=settings.WGS_SRID)
class MySubModel(models.Model):
mymodel = models.ForeignKey(MyModel)
name = models.CharField(max_length=64)
location = models.PointField(srid=settings.WGS_SRID)
Example 1 - basic usage (admin.py):
from django.contrib.gis.admin import OSMGeoAdmin
from myapp.osmgeo_inline import OSMGeoTabularInline
from myapp.models import MyModel, MySubModel
class MySubModelInline(OSMGeoTabularInline):
model = MySubModel
class MyModelAdmin(OSMGeoAdmin):
inlines = [MySubModelInline]
admin.site.register(MyModel, MyModelAdmin)
Example 2 - overriding the default map widget params and setting the inline map's centre point to match the main models map centre (admin.py):
from django.contrib.gis.admin import OSMGeoAdmin
from myapp.osmgeo_inline import OSMGeoTabularInline
from myapp.models import MyModel, MySubModel
class MySubModelInline(OSMGeoTabularInline):
model = MySubModel
class MyModelAdmin(OSMGeoAdmin):
inlines = [MySubModelInline]
params = {'map_width: 200, 'map_height': 200]
def get_formset(self, request, obj=None, **kwargs):
centre = None
if obj is not None:
centre = obj.route.centroid.transform(900913, clone=True)
self.params['default_lon'] = centre.coords[0]
self.params['default_lat'] = centre.coords[1]
self.params['default_zoom'] = 12
return super(TrailSubmissionInlineBase, self).get_formset(request, obj, **kwargs)
admin.site.register(MyModel, MyModelAdmin)
I've not looked at StackedInlines because I don't use them but all that would be needed is to add a class OSMGeoStackedInline(StackedInline) that was a copy of OSMGeoTabularInline but with a template based on the StackedInline template - the javascript code in var initMap = function(row) would probably have to be adapted though.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | in osmgeo_inline.py:
from django.forms.models import ModelForm
from django.contrib.admin.options import TabularInline
from django.contrib import admin
from django.contrib.gis.forms.fields import GeometryField
class OSMGeoInlineForm(ModelForm):
"""
A form to go along with the OSMGeoTabularInline to set GeometryField's widgets to the correct map widget
A subclass can set a params option, a dict that will override the map widget's params. The keys of the dict are the names of the map options to override.
eg. params = {'map_width': 300, 'map_height': 300, 'default_zoom': 10}
see GeoModelAdmin for the available options.
"""
def __init__(self, *args, **kwargs):
super(OSMGeoInlineForm, self).__init__(*args, **kwargs)
model_admin_instance = admin.sites.site._registry[self.parent_model]
for field in self.fields:
if isinstance(self.fields[field], GeometryField):
model_field = self._meta.model._meta.get_field(field)
self.fields[field].widget = model_admin_instance.get_map_widget(model_field)()
if hasattr(self, 'params'):
for param in self.params:
self.fields[field].widget.params[param] = self.params[param]
class Media:
js = ('admin-media/js/jquery.rule-1.0.1.1-min.js',) # Change this if you store the jquery rule file elsewhere
class OSMGeoTabularInline(TabularInline):
"""
A subclass of TabularInline that allows OSMGeoAdmin map widgets to be used for GeometryFields
You must subclass this just the same as TabularInline and provide the model option
You can also set a params option, a dict that will be passed to OSMGeoInlineForm to override the map widgets params. See OSMGeoInlineForm for details.
"""
template = 'admin/osmgeo_tabular_inline.html'
form = OSMGeoInlineForm
def __init__(self, parent_model, admin_site):
self.form.parent_model = parent_model
if hasattr(self, 'params'):
self.form.params = self.params
super(OSMGeoTabularInline, self).__init__(parent_model, admin_site)
-----------------------
in template/admin/osmgeo_tabular_inline.html:
{% load i18n adminmedia %}
<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
<div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
{{ inline_admin_formset.formset.management_form }}
<fieldset class="module">
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.non_form_errors }}
<table>
<thead><tr>
{% for field in inline_admin_formset.fields %}
{% if not field.widget.is_hidden %}
<th{% if forloop.first %} colspan="2"{% endif %}{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}</th>
{% endif %}
{% endfor %}
{% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
</tr></thead>
<tbody>
{% for inline_admin_form in inline_admin_formset %}
{% if inline_admin_form.form.non_field_errors %}
<tr><td colspan="{{ inline_admin_form.field_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
{% endif %}
<tr class="{% cycle "row1" "row2" %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last %} empty-form{% endif %}"
id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
<td class="original">
{% if inline_admin_form.original or inline_admin_form.show_url %}<p>
{% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %}
{% if inline_admin_form.show_url %}<a href="../../../r/{{ inline_admin_form.original_content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a>{% endif %}
</p>{% endif %}
{% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{{ inline_admin_form.fk_field.field }}
{% spaceless %}
{% for fieldset in inline_admin_form %}
{% for line in fieldset %}
{% for field in line %}
{% if field.is_hidden %} {{ field.field }} {% endif %}
{% endfor %}
{% endfor %}
{% endfor %}
{% endspaceless %}
</td>
{% for fieldset in inline_admin_form %}
{% for line in fieldset %}
{% for field in line %}
<td class="{{ field.field.name }}">
{% if field.is_readonly %}
<p>{{ field.contents }}</p>
{% else %}
{{ field.field.errors.as_ul }}
{{ field.field }}
{% endif %}
</td>
{% endfor %}
{% endfor %}
{% endfor %}
{% if inline_admin_formset.formset.can_delete %}
<td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</fieldset>
</div>
</div>
<script type="text/javascript">
(function($) {
$(document).ready(function($) {
var rows = "#{{ inline_admin_formset.formset.prefix }}-group .tabular.inline-related tbody tr";
var alternatingRows = function(row) {
$(rows).not(".add-row").removeClass("row1 row2")
.filter(":even").addClass("row1").end()
.filter(rows + ":odd").addClass("row2");
}
var reinitDateTimeShortCuts = function() {
// Reinitialize the calendar and clock widgets by force
if (typeof DateTimeShortcuts != "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
}
var updateSelectFilter = function() {
// If any SelectFilter widgets are a part of the new form,
// instantiate a new SelectFilter instance for it.
if (typeof SelectFilter != "undefined"){
$(".selectfilter").each(function(index, value){
var namearr = value.name.split('-');
SelectFilter.init(value.id, namearr[namearr.length-1], false, "{% admin_media_prefix %}");
})
$(".selectfilterstacked").each(function(index, value){
var namearr = value.name.split('-');
SelectFilter.init(value.id, namearr[namearr.length-1], true, "{% admin_media_prefix %}");
})
}
}
var initPrepopulatedFields = function(row) {
row.find('.prepopulated_field').each(function() {
var field = $(this);
var input = field.find('input, select, textarea');
var dependency_list = input.data('dependency_list') || [];
var dependencies = row.find(dependency_list.join(',')).find('input, select, textarea');
if (dependencies.length) {
input.prepopulate(dependencies, input.attr('maxlength'));
}
});
}
var initMap = function(row) {
var id = row.attr('id');
var number = id.substr(id.lastIndexOf('-') + 1);
var emptyRow = row.parent().children('.empty-form');
var map_tds = [];
row.find('.olMap').each(function() {
map_tds.push($(this).closest('td'));
});
var empty_map_tds = [];
emptyRow.find('.olMap').each(function() {
empty_map_tds.push($(this).closest('td'));
});
row.find('.olMapViewport').each(function() {
$(this).remove();
});
var num_maps = map_tds.length;
for(var i = 0; i < num_maps; i++) {
var map_td = map_tds[i];
var empty_map_td = empty_map_tds[i];
map_td.children('span').children('a').each(function() {
$(this).attr('href', $(this).attr('href').replace('__prefix__', number));
});
map_td.children('style').each(function() {
var styles = this.innerHTML.replace(/__prefix__/g, number).replace(/\n\s*/g,'').split('}');
var length = styles.length;
for(var i = 0; i < length; i++) {
var styletext = styles[i];
if(styletext) {
$.rule(styletext + '}').appendTo('link');
}
}
}).remove();
empty_map_td.find('script').each(function() {
var script = document.createElement("script");
script.type = "text/javascript";
script.text = this.innerHTML.replace(/__prefix__/g, number);
$("head").append(script);
});
}
}
$(rows).formset({
prefix: "{{ inline_admin_formset.formset.prefix }}",
addText: "{% blocktrans with inline_admin_formset.opts.verbose_name|title as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}",
formCssClass: "dynamic-{{ inline_admin_formset.formset.prefix }}",
deleteCssClass: "inline-deletelink",
deleteText: "{% trans "Remove" %}",
emptyCssClass: "empty-form",
removed: alternatingRows,
added: (function(row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
alternatingRows(row);
initMap(row);
})
});
});
})(django.jQuery);
</script>
-----------------------
in media/admin-media/js/jquery.rule-1.0.1.1-min.js:
/*
jQuery.Rule - Css Rules manipulation, the jQuery way.
Copyright (c) 2007-2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
Dual licensed under MIT and GPL.
Date: 02/27/2008
Compatible with jQuery 1.2.x, tested on FF 2, Opera 9, Safari 3, and IE 6, on Windows.
(version 1.0.1.1 adds support for jQuery 1.4.2 to version 1.0.1)
*/
(function(c){var i=c('<style rel="alternate stylesheet" type="text/css" />').appendTo("head")[0],j=i.sheet?"sheet":"styleSheet",f=i[j],g=f.rules?"rules":"cssRules",m=f.deleteRule?"deleteRule":"removeRule";i=f.ownerNode?"ownerNode":"owningElement";var l=/^([^{]+)\{([^}]*)\}/m,n=/([^:]+):([^;}]+)/;f.disabled=true;var d=c.rule=function(a,b){if(!(this instanceof d))return new d(a,b);this.sheets=d.sheets(b);if(a&&l.test(a))a=d.clean(a);if(typeof a=="object"&&!a.exec)return this.setArray(a.get?a.get():
a.splice?a:[a]);this.setArray(this.sheets.cssRules().get());return a?this.filter(a):this};c.extend(d,{sheets:function(a){var b=a;if(typeof b!="object")b=c.makeArray(document.styleSheets);b=c(b).not(f);if(typeof a=="string")b=b.ownerNode().filter(a).sheet();return b},rule:function(a){if(a.selectorText)return["",a.selectorText,a.style.cssText];return l.exec(a)},appendTo:function(a,b,e){switch(typeof b){case "string":b=this.sheets(b);case "object":if(b[0])b=b[0];if(b[j])b=b[j];if(b[g])break;default:if(typeof a==
"object")return a;b=f}var h;if(!e&&(h=this.parent(a)))a=this.remove(a,h);a=this.rule(a);if(b.addRule)b.addRule(a[1],a[2]||";");else b.insertRule&&b.insertRule(a[1]+"{"+a[2]+"}",b[g].length);return b[g][b[g].length-1]},remove:function(a,b){b=b||this.parent(a);if(b!=f){var e=b?c.inArray(a,b[g]):-1;if(e!=-1){a=this.appendTo(a,0,true);b[m](e)}}return a},clean:function(a){return c.map(a.split("}"),function(b){if(b)return d.appendTo(b+"}")})},parent:function(a){if(typeof a=="string"||!c.browser.msie)return a.parentStyleSheet;
var b;this.sheets().each(function(){if(c.inArray(a,this[g])!=-1){b=this;return false}});return b},outerText:function(a){return!a?"":[a.selectorText+"{","\t"+a.style.cssText,"}"].join("\n").toLowerCase()},text:function(a,b){if(b!==undefined)a.style.cssText=b;return!a?"":a.style.cssText.toLowerCase()}});d.fn=d.prototype={pushStack:function(a,b){a=d(a,b||this.sheets);a.prevObject=this;return a},end:function(){return this.prevObject||d(0,[])},filter:function(a){var b;a||(a=/./);if(a.split){b=c.trim(a).toLowerCase().split(/\s*,\s*/);
a=function(){return!!c.grep(this.selectorText.toLowerCase().split(/\s*,\s*/),function(e){return c.inArray(e,b)!=-1}).length}}else if(a.exec){b=a;a=function(){return b.test(this.selectorText)}}return this.pushStack(c.grep(this,function(e,h){return a.call(e,h)}))},add:function(a,b){return this.pushStack(c.merge(this.get(),d(a,b)))},is:function(a){return!!(a&&this.filter(a).length)},not:function(a,b){a=d(a,b);return this.filter(function(){return c.inArray(this,a)==-1})},append:function(a){var b=this,
e;c.each(a.split(/\s*;\s*/),function(h,k){if(e=n.exec(k))b.css(e[1],e[2])});return this},text:function(a){return!arguments.length?d.text(this[0]):this.each(function(){d.text(this,a)})},outerText:function(){return d.outerText(this[0])}};c.each({ownerNode:i,sheet:j,cssRules:g},function(a,b){var e=b==g;c.fn[a]=function(){return this.map(function(){return e?c.makeArray(this[b]):this[b]})}});c.fn.cssText=function(){return this.filter("link,style").eq(0).sheet().cssRules().map(function(){return d.outerText(this)}).get().join("\n")};
c.each("remove,appendTo,parent".split(","),function(a,b){d.fn[b]=function(){var e=c.makeArray(arguments),h=this;e.unshift(0);return this.each(function(k){e[0]=this;h[k]=d[b].apply(d,e)||h[k]})}});c.each("each,index,get,size,eq,slice,map,attr,andSelf,css,show,hide,toggle,queue,dequeue,stop,animate,fadeIn,fadeOut,fadeTo".split(","),function(a,b){d.fn[b]=c.fn[b]});d.fn.setArray=function(a){this.length=0;Array.prototype.push.apply(this,a);return this};var o=c.curCSS;c.curCSS=function(a,b){return"selectorText"in
a?a.style[b]||c.prop(a,b=="opacity"?1:0,"curCSS",0,b):o.apply(this,arguments)};d.cache={};i=function(a){return function(b){var e=b.selectorText;if(e)arguments[0]=d.cache[e]=d.cache[e]||{};return a.apply(c,arguments)}};c.data=i(c.data);c.removeData=i(c.removeData);c(window).unload(function(){c(f).cssRules().remove()})})(django.jQuery);
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 1 week ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 2 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 6 months ago
Comments
I forgot to test in IE!
Damn browser won't allow the inline stylesheet to be copied so I've had to use the jQuery.Rules plugin for the css.
I've edited the snippet to work cross browser now.
#
This will incredibly helpful - thanks for sharing it. BUT, i can't get it to work... The admin interface runs properly, displaying the MyModel fields, but the inlines are not displayed. Any idea why?
I'm lost because i can't see any errors anywhere.
#
Please login first before commenting.