How to access #property variable in django template [duplicate] - django

I'm using Wagtail 2.0 with a custom Block that has the following code:
class LinkButtonBlock(blocks.StructBlock):
label = blocks.CharBlock()
URL = blocks.CharBlock()
styling = blocks.ChoiceBlock(
choices=[
('btn-primary', 'Primary button'),
('btn-secondary', 'Secondary button'),
('btn-success', 'Success button'),
('btn-info', 'Info button'),
('btn-warning', 'Warning button'),
('btn-error', 'Error button'),
],
default='btn-info',
)
outline = blocks.BooleanBlock(
default=False
)
#property
def css(self):
btn_class = self.styling
if self.outline is True:
btn_class = btn_class.replace('btn-', 'btn-outline-')
return btn_class
class Meta:
icon = 'link'
template = 'testapp/blocks/link_button_block.html'
If I then try to access this css "property" in my template, nothing seems to happen. Putting a print(self) as first line inside the css def also shows nothing on the console suggesting the function never even gets called.
Using the following template:
{% load wagtailcore_tags %}
<a class="btn {{ block.value.css }}" href="{{ block.value.URL }}">{{ block.value.label }}</a>
Simply yields:
<a class="btn " href="actual.url.from.instance">actual.label.from.instance</a>
Also, block.value.styling and block.value.outline on their own work just fine, so... what am I doing wrong here?

The thing that's tripping you up is that the value objects you get when iterating over a StreamField are not instances of StructBlock. Block objects such as StructBlock and CharBlock act as converters between different data representations; they don't hold on to the data themselves. In this respect, they work a lot like Django's form field objects; for example, Django's forms.CharField and Wagtail's CharBlock both define how to render a string as a form field, and how to retrieve a string from a form submission.
Note that CharBlock works with string objects - not instances of CharBlock. Likewise, the values returned from StructBlock are not instances of StructBlock - they are a dict-like object of type StructValue, and this is what you need to subclass to implement your css property. There's an example of doing this in the docs: http://docs.wagtail.io/en/v2.0/topics/streamfield.html#custom-value-class-for-structblock. Applied to your code, this would become:
class LinkButtonValue(blocks.StructValue):
#property
def css(self):
# Note that StructValue is a dict-like object, so `styling` and `outline`
# need to be accessed as dictionary keys
btn_class = self['styling']
if self['outline'] is True:
btn_class = btn_class.replace('btn-', 'btn-outline-')
return btn_class
class LinkButtonBlock(blocks.StructBlock):
label = blocks.CharBlock()
URL = blocks.CharBlock()
styling = blocks.ChoiceBlock(choices=[...])
outline = blocks.BooleanBlock(default=False)
class Meta:
icon = 'link'
template = 'testapp/blocks/link_button_block.html'
value_class = LinkButtonValue

Related

Getting current user in blocks class in Wagtail

Using Wagtail 2.9, I am trying to create a block that has a function that generates URL. To generate the URL I need the current logged in user.
class Look(blocks.StructBlock):
title = blocks.CharBlock(required=True, help_text='Add your look title')
id = blocks.CharBlock(required=True, help_text='Enter the Id')
class Meta:
template = "looker/looker_block.html"
value_class = LookStructValue
the value class has the get_url() definition which looks like:
class LookStructValue(blocks.StructValue):
def url(self):
id = self.get('id')
user = User(15,
first_name='This is where is need the current user First name',
last_name='and last name',
permissions=['some_permission'],
models=['some_model'],
group_ids=[2],
external_group_id='awesome_engineers',
user_attributes={"test": "test",
"test_count": "1"},
access_filters={})
url_path = "/embed/looks/" + id
url = URL(user,url_path, force_logout_login=True)
return "https://" + url.to_string()
Can i get the current user inside the LookStructValue class?
You can access the parent's context (parent_context) using the blocks.Structblock's get_context method.
Be sure you render the block with {% include_block %}.
The parent_context keyword argument is available when the block is rendered through an {% include_block %} tag, and is a dict of variables passed from the calling template.
You'd have to rethink how you were creating the user's URL, instead moving it to a method (example: create_custom_url) on the User model.
# Basic Example, to point you the right way.
class DemoBlock(blocks.StructBlock):
title = blocks.CharBlock()
def get_context(self, value, parent_context=None):
"""Add a user's unique URL to the block's context."""
context = super().get_context(value, parent_context=parent_context)
user = parent_context.get('request').user
context['url'] = user.create_custom_url()
return context

Wagtail form file upload

I have an issue when I add file upload field to Wagtail Forms Builder I get this error:
Exception Type: TypeError
Exception Value: Object of type InMemoryUploadedFile is not JSON serializable
This is my code:
class FormField(AbstractFormField):
CHOICES = FORM_FIELD_CHOICES + (('fileupload', 'File Upload'),)
page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')
field_type = models.CharField(
verbose_name='field type',
max_length=16,
# use the choices tuple defined above
choices=CHOICES
)
api_fields = [
APIField('page'),
]
class CustomFormBuilder(FormBuilder):
def create_fileupload_field(self, field, options):
return forms.FileField(**options)
class FormPage(AbstractEmailForm):
form_builder = CustomFormBuilder
intro = RichTextField(blank=True)
thank_you_text = RichTextField(blank=True)
content_panels = AbstractEmailForm.content_panels + [
FieldPanel('intro', classname="full"),
InlinePanel('form_fields', label="Form fields"),
FieldPanel('thank_you_text', classname="full"),
MultiFieldPanel([
FieldRowPanel([
FieldPanel('from_address', classname="col6"),
FieldPanel('to_address', classname="col6"),
]),
FieldPanel('subject'),
], "Email"),
]
# Export fields over the API
api_fields = [
APIField('intro'),
APIField('thank_you_text'),
]
This is my template:
{% load wagtailcore_tags %}
<html>
<head>
<title>{{ page.title }}</title>
</head>
<body>
<h1>{{ page.title }}</h1>
{{ page.intro|richtext }}
<form action="{% pageurl page %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<input type="submit">
</form>
</body>
</html>
Wagtail version 2.8.1
Django version 3.0.5
any idea with this issue ?
The core issue here is that you are attempting to store an uploaded file as JSON. Wagtail's FormBuilder does not store the submission data parts as their own DB models but instead bundles is up as json (e.g. {'field-a': 'value'}) and stores that as a string in the database.
The reason for this is that the data stored is flexible on a per page basis and can change over time based on the page's settings.
So, to fully implement a file upload field, you need to store those files somewhere, plus solve a few other problems.
1. Where to store the file
Depending on your Django setup, you will need to get a basic understanding of how to Store files in Django
You will need to create a new model that will store these files, see FormUploadedFile in the example below
Depending on your use case, you will need to consider multiple files uploaded in each form submission, as the FormPage UI enables users to create multiple of any field type, hence it might be good to keep a reference to the field name it is stored under.
2. What to save in the JSON as a reference to the file
This could be a simple pk (primary key) reference, as per the code example below.
You may want to add some more advanced linking between the file upload model and the FormSubmission model for better data integrity
You will need to override the process_form_submission on your FormPage model, you can see the original code here https://github.com/wagtail/wagtail/blob/master/wagtail/contrib/forms/models.py#L195
3. Reading the file and what to represent as this file in the form submissions list
You may want to modify the get_data output from the FormSubmission records, you can do this by adding a custom FormSubmission model (see code below), however this will be in place of your existing model (so your existing submissions will no longer be visible without some sort of migration or other workaround).
You can see the original get_data method here https://github.com/wagtail/wagtail/blob/master/wagtail/contrib/forms/models.py#L48
The Wagatil docs section has a good part about customising the submissions list
Example Code
Here is a rough working POC to get you started, hope this helps.
import json
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django import forms
from modelcluster.fields import ParentalKey
from wagtail.contrib.forms.models import (
AbstractEmailForm, AbstractFormField, AbstractFormSubmission, FORM_FIELD_CHOICES)
from wagtail.contrib.forms.forms import FormBuilder
from wagtail.contrib.forms.views import SubmissionsListView
class FormField(AbstractFormField):
page = ParentalKey('FormPage', related_name='form_fields', on_delete=models.CASCADE)
field_type = models.CharField(
verbose_name='field type',
max_length=16,
choices=FORM_FIELD_CHOICES + (('fileupload', 'File Upload'),)
)
class CustomFormBuilder(FormBuilder):
def create_fileupload_field(self, field, options):
return forms.FileField(**options)
class CustomSubmissionsListView(SubmissionsListView):
"""
further customisation of submission list can be done here
"""
pass
class CustomFormSubmission(AbstractFormSubmission):
# important - adding this custom model will make existing submissions unavailable
# can be resolved with a custom migration
def get_data(self):
"""
Here we hook in to the data representation that the form submission returns
Note: there is another way to do this with a custom SubmissionsListView
However, this gives a bit more granular control
"""
file_form_fields = [
field.clean_name for field in self.page.specific.get_form_fields()
if field.field_type == 'fileupload'
]
data = super().get_data()
for field_name, field_vale in data.items():
if field_name in file_form_fields:
# now we can update the 'representation' of this value
# we could query the FormUploadedFile based on field_vale (pk)
# then return the filename etc.
pass
return data
class FormUploadedFile(models.Model):
file = models.FileField(upload_to="files/%Y/%m/%d")
field_name = models.CharField(blank=True, max_length=254)
class FormPage(AbstractEmailForm):
form_builder = CustomFormBuilder
submissions_list_view_class = CustomSubmissionsListView
# ... other fields (image, body etc)
content_panels = AbstractEmailForm.content_panels + [
# ...
]
def get_submission_class(self):
"""
Returns submission class.
Important: will make your existing data no longer visible, only needed if you want to customise
the get_data call on the form submission class, but might come in handy if you do it early
You can override this method to provide custom submission class.
Your class must be inherited from AbstractFormSubmission.
"""
return CustomFormSubmission
def process_form_submission(self, form):
"""
Accepts form instance with submitted data, user and page.
Creates submission instance.
You can override this method if you want to have custom creation logic.
For example, if you want to save reference to a user.
"""
file_form_fields = [field.clean_name for field in self.get_form_fields() if field.field_type == 'fileupload']
for (field_name, field_value) in form.cleaned_data.items():
if field_name in file_form_fields:
uploaded_file = FormUploadedFile.objects.create(
file=field_value,
field_name=field_name
)
# store a reference to the pk (as this can be converted to JSON)
form.cleaned_data[field_name] = uploaded_file.pk
return self.get_submission_class().objects.create(
form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
page=self,
)

Geodjango OSMGeoAdmin: how can add different layers

I noticed that maps in the Geodjango admin have a menu on the right.
How can I add more layers than just one representing the model field the map is related in admin?
This is my model (model.py):
class Foresta(models.Model):
nome = models.CharField("Nome", blank = False, max_length = 255)
descrizione = tinymce_models.HTMLField("Descrizione", blank = True, help_text='Inserire una descrizione del bosco')
slug = models.SlugField("Slug", blank = True)
published = models.BooleanField("Pubblicato")
...
coord = models.PointField("Coordinata punto foresta", blank = False)
# GeoDjango-specific: a geometry field (MultiPolygonField), and
# overriding the default manager with a GeoManager instance.
mpoly = models.MultiPolygonField("Mappa foresta (poligono)", blank = False)
objects = models.GeoManager()
This is my admin model (admin.py):
class ForestaAdmin(admin.OSMGeoAdmin):
default_lon= 1308296
default_lat= 5714101
default_zoom= 9
overlays = ('coord', 'mpoly')
I tried with the 'overlays' option but without success!
You can find the default OSMGeoAdmin settings here. Unfortunately, it doesn't look like you can accomplish this with something as simple as specifying an overlays tuple. Instead, it looks like you should create a custom map template and then override the map_template option in your ForestaAdmin class with the path to your template. Start by copying the default openlayers template and customize the javascript to add your other layers as additional OpenLayers Vector layers.
I had to add extra readonly multipolygon on the map. I redefined osm.html (?just changed osm.js file path cause django used default "gis/admin/osm.js")
{% extends "gis/admin/openlayers.html" %}
{% block openlayers %}{% include "gis/osm.js" %}{% endblock %}
osm.js:
{% extends "gis/admin/osm.js" %}
{% block extra_layers %}
{% if extra_wkt %}
var extraLayer = new OpenLayers.Layer.Vector("extra_layer");
var extraGeometry = new OpenLayers.Feature.Vector(
new OpenLayers.Geometry.fromWKT('{{ extra_wkt }}'),
{}, // attrs
{
fillColor: "#8a8a8a",
fillOpacity: 0.4,
strokeColor: "#000000",
strokeOpacity: 0.6,
strokeWidth: 1,
}
)
extraLayer.addFeatures(extraGeometry);
{{ module }}.map.addLayer(extraLayer);
{% endif %}
{% endblock %}
There is I use extra_wkt value which passed as context.
(mpoly - name of geodjango multipolygon field)
from django.contrib.gis.admin import OSMGeoAdmin, OpenLayersWidget
class OSMLayersWidget(OpenLayersWidget):
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
if name == "mpoly":
extra_mpoly = MultiPolygon([..., ...])
if extra_mpoly:
srid = self.params['srid']
if extra_mpoly.srid != srid:
try:
extra_mpoly.transform(srid)
extra_wkt = extra_mpoly.wkt
except GDALException as err:
logger.error(
"Error creating geometry from value '%s' (%s)",
extra_mpoly,
err,
)
extra_wkt = ''
else:
extra_wkt = extra_mpoly.wkt
context["extra_wkt"] = extra_wkt
return context
admin.py:
class ServiceZoneAdmin(OSMGeoAdmin):
map_template = 'gis/osm.html'
widget = OSMLayersWidget
Result:
You can use olwidget http://docs.olwidget.org/en/latest/django-olwidget.html. It allows you to edit and show different layers (inside and outside Django admin). However, you might find it difficult to use depending on which Django version are you running.
Finally, I managed to do it as #garnetb said by modifying the openlayers.js file found in (my case) here:
/usr/local/lib/python2.7/dist-packages/django/contrib/gis/templates/gis/admin/openlayers.js
If you can't find it just look for it in the osm.html and/or openlayers.html. So, this is the way I managed to add a second layer. My objective was to add a "visual" only layer, but you can easily modify the controls if you want to edit it. So, let's assume you have a model like this one:
class Lines(gis_models.Model):
name = gis_models.CharField(max_length=10)
geom = gis_models.MultiLineStringField(srid=4326)
geom_points = gis_models.MultiPointField(srid=4326, null=True)
objects = gis_models.GeoManager()
def __unicode__(self):
return self.name
So you see, there are 2 elements being load, the first one are lines and the second one are points. So my objective is to add the lines as a visual reference for when I'm working with the points. To be able to do that, go to openlayers.js and look for the section where the Base Layer is being defined and add something like this.
// Base Layer
{% if field_name != "geom" %}
geom_layer = new OpenLayers.Layer.Vector("visual");
{{ module }}.map.addLayer(geom_layer);
var wkt_vis = document.getElementById('id_geom').value;
if (wkt_vis){
var features = {{ module }}.read_wkt(wkt_vis);
geom_layer.addFeatures(features);
}
else {
alert("no wkt id field");
}
{% endif %}
if ({{ module }}.is_point) {
var style = new OpenLayers.Style({
pointRadius : 4,
strokeColor : 'red',
strokeWidth : 2,
strokeOpacity : 1,
fillColor : 'white',
fillOpacity : 1
});
var layer_style = new OpenLayers.StyleMap({
'default' : style,
});
{{ module }}.layers.vector = new OpenLayers.Layer.Vector(" {{ field_name }}", {styleMap : layer_style});
}
else {
{{ module }}.layers.vector = new OpenLayers.Layer.Vector(" {{ field_name }}");
}
As you can see, the whole idea is that openlayers.js is loaded for every layer, but you can still access other objects. This configuration left me with a handy admin where I'm showing two maps. In the first one, I show the lines and I'm able to modify them. In the second one, I show the lines as a reference and the points are editable. The second part shows you how to set up the points style (just in case...) This is just a simplified example. If you need more details just let me know. I also managed to add a "delete feature" control. I only tried this with GeoDjango base on Django 1.6, but it should work with other versions as far as they use openlayers.js in the same way.

django floppyforms BaseGMapWidget - default-zoom and lat/long

Does someone know a simple way to set the default zoom and latitude/longitude with floppyforms.gis.BaseGMapWidget ? And additionally, when creating an entry set panning as default tool.
In the form i define:
class PointWidget(floppyforms.gis.PointWidget, floppyforms.gis.BaseGMapWidget):
map_width = 690
map_height = 300
class EventForm(ModelForm):
class Meta:
model = Event
widgets = {
'coordinates': PointWidget(),
}
This perfectly shows the map widget and i can set a point. It also nicely centers & zooms the point when loading the form for an existing entry.
But when displaying the form to create a new entry it centers at the west-cost of africa..
Is there a way to achieve this (preferably by defining in the form/widget-class and not using additional javascript).
First, override get_context_data() on your widget class and point it
to a custom template:
class PointWidget(floppyforms.gis.BaseGMapWidget, floppyforms.gis.PointWidget):
template_name = 'custom_lonlat.html'
default_lon = 'something'
default_lat = 'something'
def get_context_data(self):
ctx = super(PointWidget, self).get_context_data()
ctx.update({
'lon': self.default_lon,
'lat': self.default_lat,
})
return ctx
Then create the custom_lonlat.html template:
{% extends "floppyforms/gis/google.html" %}
{% block options %}{{ block.super }}
options['default_lon'] = {{ lon }};
options['default_lat'] = {{ lat }};
{% endblock %}
If you want to make the default lon/lat dynamic, you can set them directly on the form instance from your views code:
form = EventForm()
form.fields['coordinates'].widget.default_lon = 'something'
form.fields['coordinates'].widget.default_lat = 'something else'
I'm the author of floppyforms and I've seen this question a couple of times… I'll add this answer to the official docs soon.

How can i customize the html output of a widget in Django?

I couldn't find this in the docs, but think it must be possible. I'm talking specifically of the ClearableFileInput widget. From a project in django 1.2.6 i have this form:
# the profile picture upload form
class ProfileImageUploadForm(forms.ModelForm):
"""
simple form for uploading an image. only a filefield is provided
"""
delete = forms.BooleanField(required=False,widget=forms.CheckboxInput())
def save(self):
# some stuff here to check if "delete" is checked
# and then delete the file
# 8 lines
def is_valid(self):
# some more stuff here to make the form valid
# allthough the file input field is empty
# another 8 lines
class Meta:
model = SocialUserProfile
fields = ('image',)
which i then rendered using this template code:
<form action="/profile/edit/" method="post" enctype="multipart/form-data">
Delete your image:
<label> {{ upload_form.delete }} Ok, delete </label>
<button name="delete_image" type="submit" value="Save">Delete Image</button>
Or upload a new image:
{{ upload_form.image }}
<button name="upload_image" type="submit" value="Save">Start Upload</button>
{% csrf_token %}
</form>
As Django 1.3.1 now uses ClearableFileInput as the default widget, i'm pretty sure i can skip the 16 lines of my form.save and just shorten the form code like so:
# the profile picture upload form
class ProfileImageUploadForm(forms.ModelForm):
"""
simple form for uploading an image. only a filefield is provided
"""
class Meta:
model = SocialUserProfile
fields = ('image',)
That would give me the good feeling that i have less customized formcode, and can rely on the Django builtins.
I would, of course, like to keep the html-output the same as before. When just use the existing template code, such things like "Currently: somefilename.png" pop up at places where i do not want them.
Splitting the formfield further, like {{ upload_form.image.file }} does not seem to work. The next thing coming to my mind was to write a custom widget. Which would work exactly against my efforts to remove as many customized code as possible.
Any ideas what would be the most simple thing to do in this scenario?
Firstly, create a widgets.py file in an app. For my example, I'll be making you an AdminImageWidget class that extends AdminFileWidget. Essentially, I want a image upload field that shows the currently uploaded image in an <img src="" /> tag instead of just outputting the file's path.
Put the following class in your widgets.py file:
from django.contrib.admin.widgets import AdminFileWidget
from django.utils.translation import ugettext as _
from django.utils.safestring import mark_safe
import os
import Image
class AdminImageWidget(AdminFileWidget):
def render(self, name, value, attrs=None):
output = []
if value and getattr(value, "url", None):
image_url = value.url
file_name=str(value)
# defining the size
size='100x100'
x, y = [int(x) for x in size.split('x')]
try :
# defining the filename and the miniature filename
filehead, filetail = os.path.split(value.path)
basename, format = os.path.splitext(filetail)
miniature = basename + '_' + size + format
filename = value.path
miniature_filename = os.path.join(filehead, miniature)
filehead, filetail = os.path.split(value.url)
miniature_url = filehead + '/' + miniature
# make sure that the thumbnail is a version of the current original sized image
if os.path.exists(miniature_filename) and os.path.getmtime(filename) > os.path.getmtime(miniature_filename):
os.unlink(miniature_filename)
# if the image wasn't already resized, resize it
if not os.path.exists(miniature_filename):
image = Image.open(filename)
image.thumbnail([x, y], Image.ANTIALIAS)
try:
image.save(miniature_filename, image.format, quality=100, optimize=1)
except:
image.save(miniature_filename, image.format, quality=100)
output.append(u' <div><img src="%s" alt="%s" /></div> %s ' % \
(miniature_url, miniature_url, miniature_filename, _('Change:')))
except:
pass
output.append(super(AdminFileWidget, self).render(name, value, attrs))
return mark_safe(u''.join(output))
Ok, so what's happening here?
I import an existing widget (you may be starting from scratch, but should probably be able to extend ClearableFileInput if that's what you are starting with)
I only want to change the output/presentation of the widget, not the underlying logic. So, I override the widget's render function.
in the render function I build the output I want as an array output = [] you don't have to do this, but it saves some concatenation. 3 key lines:
output.append(u' <div><img src="%s" alt="%s" /></div> %s ' % (miniature_url, miniature_url, miniature_filename, _('Change:'))) Adds an img tag to the output
output.append(super(AdminFileWidget, self).render(name, value, attrs)) adds the parent's output to my widget
return mark_safe(u''.join(output)) joins my output array with empty strings AND exempts it from escaping before display
How do I use this?
class SomeModelForm(forms.ModelForm):
"""Author Form"""
photo = forms.ImageField(
widget = AdminImageWidget()
)
class Meta:
model = SomeModel
OR
class SomeModelForm(forms.ModelForm):
"""Author Form"""
class Meta:
model = SomeModel
widgets = {'photo' : AdminImageWidget(),}
Which gives us: