Add html to page edit view within wagtail admin - admin

how can I add some text or html within the "page edit view" in wagtail admin? I want to give them some more information's about a page they are working at.
(Editors can use $variables$ in there text and I want to show them all available variables for a specific page.)
Example how it should look like

It's possible to define a custom panel for the editing interface. Wagtail has the FormSubmissionsPanel panel (see the source code) which displays a number of form submission while editing a form page. You can use it as a starting point.
If you just want to render a static template with some additional information for editors your panel definition will looks similar to this:
class BaseInfoPanel(EditHandler):
template = "path/to/your/template.html"
def render(self):
return mark_safe(render_to_string(self.template))
class InfoPanel(object):
def __init__(self, heading=None):
self.heading = heading
def bind_to_model(self, model):
return type(str('_InfoPanel'), (BaseInfoPanel,), {
'model': model,
'heading': self.heading or "Additional info",
})
In your page model you would be able to use it as
class MyPage(Page):
# fields...
content_panels = Page.content_panels + [
InfoPanel(),
# or InfoPanel("My info panel") to specify heading for a panel.
# other panels ...
]

Related

How to use two ckeditor in same template in Django?

I am trying to develop a ckeditor comment box in Django. I successfully added the comment box. But now I am facing the issue in editing the comment part. What I am trying to do is when the user click on the edit button near to the comment, a modal popup appears and editing should happens there. The issue is the ckeditor is not showing in the modal. Instead of ckeditor a normal text field with data from the database is showing. In the console it showing "editor-element-conflict" error. Is there any solution for this?
I've figured out!
It happens because you have a few ckeditor fields on the page, and they have the same ID, and CKEditor gets confused because it finds a few elements with the same ID.
Solution: change IDs dynamically when the page is being generated.
I don't know the structure of your model, but I can assume that your form is defined like this:
class Comment(models.Model):
text = models.TextField()
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = “__all__”
Then you need to change it like this:
from ckeditor.widgets import CKEditorWidget
class CommentForm(forms.ModelForm):
base_textarea_id = "id_text"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.textarea_id_counter = 0
self.fields['text'].widget = CKEditorWidget(attrs={'id': self.get_textarea_next_id})
def get_textarea_next_id(self):
result = self.base_textarea_id + str(self.textarea_id_counter)
self.textarea_id_counter += 1
return result
class Meta:
model = Comment
fields = “__all__”
Of course, it’s based on very simplified model example, but I hope you’ve got the point.

Is there any way to make the wagtail body field readonly?

I'm developing blog app with wagtail.
main point about blog page:
Writer can write "title", "introduction" and "body".
Firstly, writer can submit title.
After finishing "title task", writer can edit and submit body.
When writer do title task, writer can not edit body field.
Furthermore, when writer do body task, writer can not edit title field.
I want to change permission dynamically for titleField and bodyField(RichTextField) but I could not figure out how to do it.
I thought editing hook about #hooks.register("after_edit_page") in wagtail_hooks.py can reach resolving.
I tried to use PagePermissionHelper, PermissionHelper.
Proposed Solution
The Wagtail documentation regarding Customising generated forms explains the method to override the form that gets generated when editing/creating a new page.
The WagtailAdminPageForm extends the Django ModelForm and you can extend this further to add custom clean/__init__/save etc methods to add essentially any logic you want to both how the form renders and what errors get provided to the user before the save gets applied.
Django ModelForm documentation.
By default you do not have acesss to the request object on form creation, but you do get it on the save method so it would be possible to easily do some user basic logic there.
If you need further customisation, you can dig into Wagtail edit handers (search through the source code) and you can create your own edit handler that can pass in the request to your custom BlogPageForm.
Note: If the eventual goal is to add a full on 'process' based page editing workflow, you may want to look at Wagtails' ModelAdmin and essentially just build the blog workflow completely in isolation of the normal page structure and then restructure permissions so that blog editors cannot access the normal page tree but can only access your custom workflow.
Example Code
This is just a basic example of a custom form for a BlogPage model.
__init__ can be extended to add custom logic to how the form gets generated (e.g. make some fields read only or even 'hide' some fields).
save can be extended to add server side validation to the read only fields and also provide user facing error messaging.
It is possible to add logic for a 'new' page creation along with logic for editing an existing page by checking if the self.instance.pk (primary key) exists.
# other imports
from wagtail.admin.forms import WagtailAdminPageForm
class BlogPageForm(WagtailAdminPageForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if not instance.pk:
# this is a NEW blog entry form - only allow title to be enabled, disable other fields
self.fields['description'].widget.attrs['readonly'] = True
if instance.pk:
# assume title has been entered and saved at this point (required for a new post)
# disable the title field
self.fields['title'].widget.attrs['readonly'] = True
def clean(self):
cleaned_data = super().clean()
instance = getattr(self, 'instance', None)
title = cleaned_data['title']
description = cleaned_data['description']
if not instance.pk:
# this is a NEW blog entry, check that only the title has been entered
if not title:
self.add_error('title', 'title must be edited before all other fields')
return cleaned_data
if description:
self.add_error('description', 'description cannot be entered until title has been completed')
if instance.pk:
# an existing blog entry, do not allow title to be changed
print('title', instance.title, title)
if instance.title != title:
self.add_error('title', 'title cannot be edited after the initial save')
class BlogPage(Page):
# ...fields
base_form_class = BlogPageForm
Can be done easy with additional custom.css file
With insert_global_admin_css Wagtail hook, add path to your custom.css file. Here is link for documentation: https://docs.wagtail.io/en/latest/reference/hooks.html#insert-global-admin-css
Then add classname(eg. "myReadonlyInput") to FieldPanel in Page model. This will add new class to li element with input field.
FieldPanel("field_name", classname="myReadonlyInput"),
In custom.css file add pointer-events:none; to input field that belongs to new class for li element:
li.myReadonlyInput div.input input {
background-color: rgb(239 239 239);
color: rgb(99 99 99);
pointer-events:none;
cursor:text;
}
That way only with adding classname to any field model, input field will be grayed out and not reachable.

Permission for field in Wagtail Page

How can permissions be applied to individual fields of a Wagtail page?
Let's say we have a page like this one:
class HomePage(Page):
body = RichTextField(blank=True)
content_panels = Page.content_panels + [
FieldPanel('body', classname="full"),
]
Everyone should be allowed to edit the title - but only users with a certain permission should be able to alter the body.
I realize this is a really old question now, but just in case people come across it in the future, here's how my code shop does this in Wagtail 2.3. It may or may not work in later versions.
Add the following to the Page subclass you've written:
panels = [
...
MultiFieldPanel(
[
FieldPanel('display_locations', widget=forms.CheckboxSelectMultiple),
StreamFieldPanel('assets'),
],
heading='Admin-only Fields',
# NOTE: The 'admin-only' class is how EventPage.get_edit_handler() identifies this MultiFieldPanel.
classname='collapsible admin-only'
),
...
]
class Meta:
verbose_name = 'Event'
verbose_name_plural = 'Events'
ordering = ['start_date', 'title']
permissions = (
('can_access_admin_fields', 'Can access Event Admin fields'),
)
#classmethod
def get_edit_handler(cls):
"""
We override this method (which is added to the Page class in wagtail.admin.edit_handlers) in order to enforce
our custom field-level permissions.
"""
# Do the same thing that wagtail.admin.edit_handlers.get_edit_handler() would do...
bound_handler = cls.edit_handler.bind_to_model(cls)
# ... then enforce admin-only field permissions on the result.
current_request = get_current_request()
# This method gets called during certain manage.py commands, so we need to be able to gracefully fail if there
# is no current request. Thus, if there is no current request, the admin-only fields are removed.
if current_request is None or not current_request.user.has_perm('calendar_app.can_access_admin_fields'):
# We know for sure that bound_handler.children[0].children is the list of Panels in the Content tab.
# We must search through that list to find the admin-only MultiFieldPanel, and remove it.
# The [:] gets us a copy of the list, so altering the original doesn't change what we're looping over.
for child in bound_handler.children[0].children[:]:
if 'admin-only' in child.classname:
bound_handler.children[0].children.remove(child)
break
return bound_handler
This is, obviously, quite funky and fragile. But it's the only solution I could find.
Wagtail do not support field based permission control. But you can achieve this by enabling/disabling fields of the page's form. If you add a field to a page, you can pass whether that field should be enabled or disabled in the constructor. But how can you do that runtime?
Every HTML page with a form in wagtail is associated with a Django Form. You can change the default form to a your own one like this.
from django.db import models
from wagtail.admin.forms.pages import WagtailAdminPageForm
from wagtail.core.models import Page
from wagtail.admin.edit_handlers import FieldPanel
class HomeForm(WagtailAdminPageForm):
pass
class HomePage(Page):
body = models.CharField(max_length=500, default='', blank=True)
content_panels = Page.content_panels + [
FieldPanel('body'),
]
base_form_class = HomeForm # Tell wagtail to user our form
This way, every time you load the create view or edit view of HomePage, wagtail will create an instance from HomeForm class. Now what you have to do is, to check the user status and enable/disable the required field when creating an instance of the HomeForm.
For this example, I will enable the body field only when the user is a superuser.
class HomeForm(WagtailAdminPageForm):
# Override the constructor to do the things at object creation.
def __init__(self, data=None, files=None, parent_page=None, subscription=None, *args, **kwargs):
super().__init__(data, files, parent_page, subscription, *args, **kwargs)
user = kwargs['for_user'] # Get the user accessing the form
is_superuser = user.is_superuser
body = self.fields.get('body') # Get the body field
body.disabled = not is_superuser # Disable the body field if user is not a superuser
This way, every time a non superuser loads the create or edit page, the body field will be disabled.
But if you want to remove access only from edit page, you need to use self.initial variable. This variable is a dictionary with initial values to be used when showing the form.
You can check the value of a required field (like title) from self.initial and if the field's value is empty, that means the create page is loaded and if there is a value, that means the edit page is loaded.

Can I disable a field in the Rest Framework API browsing view

I am using Django Rest Framework to serialize a model in which I have a foreignkey.
models.py
class Article(models.Model):
author = models.ForeignKey(Author, related_name='articles')
... other fields...
serializers.py
class ArticleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Article
I want to get rid of the 'HTML form' at the bottom of the browsable API view since I get a list with all my articles and retrieving them from the DB takes ages (I have some 100K articles, and each time the html form is displayed, my server does 100K queries).
I have read the answer from How to disable admin-style browsable interface of django-rest-framework? and I am currently displaying the view in JSON. However, I like the html view and would like to find a way to avoid the html form available at the bottom.
I don't want to properly remove the field from the view (I need to use it), but just remove the database queries used to populate the form.
Any idea ?
Making the field read-only also means you cannot modify it, which is probably not wanted in all cases.
Another solution is to override the BrowsableApiRenderer so it won't display the HTML form (which can be indeed really slow with a lot of data).
This is surprisingly easy, just override get_rendered_html_form:
from rest_framework.renderers import BrowsableAPIRenderer
class NoHTMLFormBrowsableAPIRenderer(BrowsableAPIRenderer):
def get_rendered_html_form(self, *args, **kwargs):
"""
We don't want the HTML forms to be rendered because it can be
really slow with large datasets
"""
return ""
then adjust your settings to use this renderer:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'myapp.renderers.NoHTMLFormBrowsableAPIRenderer',
)
}
I answer my own question.
I found in the documentation the solution to my problem. I had to use the read_only attribute.
serializers.py
class ArticleSerializer(serializers.HyperlinkedModelSerializer):
author = serializers.RelatedField(read_only=True)
class Meta:
model = Article
fields = ('author', ...other_fields)
#maerteijn answer will disable all forms: POST, PUT, DELETE and OPTIONS.
If you still want to allow the awesome "OPTIONS" button, you can do something like this
class NoHTMLFormBrowsableAPIRenderer(BrowsableAPIRenderer):
OPTIONS_METHOD = "OPTIONS"
def get_rendered_html_form(self, data, view, method, request):
if method == self.OPTIONS_METHOD:
return super().get_rendered_html_form(data, view, method, request)
else:
"""
We don't want the HTML forms to be rendered because it can be
really slow with large datasets
"""
return ""
And modify settings.py in the same way
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'myapp.renderers.NoHTMLFormBrowsableAPIRenderer',
)
}

Django Admin: Add text at runtime next to a field

I want to add a text next to a field of the django admin interface.
The warning needs to created at runtime inside a python method. I know python and the django ORM well, but I don't know how to get the text next the field.
The text should be a warning. Raising ValidationError in clean() is not a solution, since
the user can't edit the page any more. It should be just a warning message.
You can use custom ModelForm subclass for the admin, adding help_text attribute for the field in question at its initialization, and style it appropriately.
# forms.py
class YourModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(YourModelForm, self).__init__(*args, **kwargs)
self.fields['field_in_question'].help_text = generate_warning()
# admin.py
class YourModelAdmin(admin.ModelAdmin):
form = forms.YourModelForm
# And here you can specify custom CSS / JS which would make
# `help_text` for that particular field look like a warning.
# Or you can make it generic--say, style (and maybe reposition w/js) all tags
# like <span class="warning"> that occur within the help text of any field.
class Media:
css = {"all": ("admin_warning.css", )}
js = ("admin_warning.js", )
If you want to do it in changelist view, you can write in model method, which returns string in format you want, and put name of that method in list_display in admin.
class MyModel(models.Model):
myfield = models.CharField(max_length=100)
def myfield_with_warning(self):
return '%s - %s' % (self.myfield, '<span class="warn">My warning message</p>'
myfield_with_warning.short_description = 'My field verbose name'
myfield_with_warning.allow_tags = True
class MyModelAdmin(models.ModelAdmin):
list_display = ('myfield_with_warning',)
If it's not what you need, write more precisely, where do you want to display warning message.
I think the simplest way would be to override the specific admin page for that model. This is described here in the Django documentation. The template you need to override is probably change_form.html. Within these template displayed object is available in the template variable original.
I would add a method or property to you model, that generates and returns the error message and call this method from the template.
Edit: Have a look at contrib/admin/templates/admin/change_form.html there is a include for includes/fieldset.html that displays the the fields of the admin site. You could put some code there that chckes if the model has some special named attribute and if so it is displayed. You could them simply override that change_form.html for all models with your custom one.