Wagtail: How to overwrite create/edit template for PageModel - python

I want to overwrite create.html and edit.html used for models derived from Wagtails 'PageModel'.
If I understand the docs correctly it should be as simple as specifying the attributes:
class MyAdmin(ModelAdmin):
model = MyPage
create_template_name = "myapp/create.html"
edit_template_name = "myapp/edit.html"
My templates are located at projectroot/templates/myapp. It works fine if my model is a Django model but for a PageModel based model the create view still uses wagtailadmin/pages/create.html. I also tried the other location patterns mentioned in the docs w/o success.
Is it possible to change the edit and create templates for a PageModel? Or do the same limitations as for views apply, i.e. only index.html and inspect.html can be overwritten?

ModelAdmin does not provide create, edit, or delete functionality for Page models, as per the documentation note.
NOTE: modeladmin only provides ‘create’, ‘edit’ and ‘delete’ functionality for non page type models (i.e. models that do not extend wagtailcore.models.Page). If your model is a ‘page type’ model, customising any of the following will not have any effect.
It can be a bit confusing as the ModelAdmin system would seem to work for page models also, but there are some other ways to modify how your page can be edited. These will not be scoped to the ModelAdmin area though.
Option 1 - customise the generated form for your MyPage model
If you only want to customise how the edit page form that gets generated you can modify the base_form_class on your page model.
Wagtail has documentation about how to create a custom page form.
Note: WagtailAdminPageForm extends Django's ModelFormMetaClass
Example
from django import forms
from django.db import models
from wagtail.admin.forms import WagtailAdminPageForm
from wagtail.core.models import Page
class EventPageForm(WagtailAdminPageForm):
# ...
class MyPage(Page):
# ...
base_form_class = MyPageForm
Option 2 - customise the view via hooks
To customise the create & edit views for the normal (e.g. clicking edit page on the Wagtail user bar or explorer) page editing interface, you will need to use Wagtail hooks. Here you have access to the request so you will likely be able to determine if you are in the ModelAdmin area.
Create a file called wagtail_hooks.py in your app folder and provide a hook that will return a custom response (this will need to be rendered by your custom view.).
There are separate hooks for before_create_page and before_edit_page
Example from before_create_page docs below.
from wagtail.core import hooks
from .models import AwesomePage
from .admin_views import edit_awesome_page
#hooks.register('before_create_page')
def before_create_page(request, parent_page, page_class):
# Use a custom create view for the AwesomePage model
if page_class == AwesomePage:
return create_awesome_page(request, parent_page)
```python

Related

Wagtail: Redirect after saving Django model

I'm trying to redirect the user to a custom HTML page after saving a BaseSiteSetting model in Wagtail 4.1.1
I'm not sure how to accomplish this, the BaseSiteSetting inherits from Django models.Model which means it's possible to override the save() function but how would I do the actual redirect without having access to the request?
Another acceptable solution would be to add an extra button in the CMS by overriding the default BaseSiteSetting HTML template but I can't seem to get that working either, except for ModelAdmin templates. I've opened a StackOverflow question about that here.
My view of the custom HTML page:
def sync(request):
return render(request, "import.html", {"WS_PROTOCOL": settings.WS_PROTOCOL})
My BaseSiteSetting model:
#register_setting
class AnonymousSuccessStoryImportSetting(BaseSiteSetting):
"""
Setting for importing anonymous success stories.
"""
file = models.FileField(
upload_to="success_story_imports/%Y/%m/%d/",
validators=[validate_file_extension],
help_text="Upload a CSV file, then click 'Save' afterwards",
blank=True,
null=True,
)
date = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Importer"
I've looked around for possible solutions and found some wagtail hooks but these only apply to the Wagtail Page model, for example, after_publish_page.
It's unfortunate that there's no hook for standard Django models.
It looks to me like BaseSiteSettings is one of the models that has not been converted to use class-based views. So I think you need to monkey patch this edit method to change the 'after save' redirect here: https://github.com/wagtail/wagtail/blob/main/wagtail/contrib/settings/views.py#L121

Wagtail 'View live' button provides wrong url after page creation while using id as slug

I have a case that uses Page ID as a slug, after creating a new Page, Wagtail provides us a "View live" button but when we click on that button, it provides a wrong URL
The right URL should be ".../property-list/<property-id>"
I have searched on stack overflow, found this thread but the answer is still a mystery:
Wrong 'View live' url in Wagtail admin message after page creation when using id as slug
I have followed the Wagtail official document, using Wagtail Hooks to manipulate data. However, no success yet. This is my code:
#hooks.register('after_create_page')
def set_number_and_slug_after_property_page_created(request, page):
page.number = page.slug = str(page.id)
page.save()
new_revision = page.save_revision()
if page.live:
new_revision.publish()
Please help me, thank you.
The after_create_page hook doesn't work for this, as it's one of the last things to run during the Wagtail admin's page creation view, and the confirmation message (including the old URL) has already been constructed by that point. This can be remedied by using the post_save signal provided by Django instead - being a more low-level mechanism, it's more closely tied to the act of saving the database record, without Wagtail's admin logic getting in the way.
(It's also a bit more future-proof: Wagtail's after_create_page hook is designed to only be called when a user goes through the page creation area of the Wagtail admin, so that it can be used to customise that user's path through the admin if appropriate. If there's ever any other route by which a page might be created - like, say, a data import, or using translation tools for a multi-language site - then after_create_page will be bypassed, but the post_save signal will still be triggered.)
Assuming your project has a properties app where you're defining a PropertyPage model, your code can be rewritten to use post_save as follows - in properties/signals.py:
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import PropertyPage
#receiver(post_save)
def set_number_and_slug_after_property_page_created(sender, instance, created, **kwargs):
if issubclass(sender, PropertyPage) and created:
page = instance
page.number = page.slug = str(page.id)
page.save()
new_revision = page.save_revision()
if page.live:
new_revision.publish()
Then, to connect this signal up on server startup, create a properties/apps.py containing the following (or just add / edit the ready method if you have an AppConfig class there already):
from django.apps import AppConfig
class PropertiesConfig(AppConfig):
name = 'properties'
def ready(self):
from . import signals

Convert (and validate) file added in django admin

I'm working on a Django project that utilizes customized greetings (like in voicemail). The whole functionality is implemented, i have created a custom model:
class Greeting(models.Model):
audio_file = models.FileField(upload_to='greetings/')
description = models.CharField(max_length=128)
uploaded_at = models.DateTimeField(auto_now_add=True)
The next thing that i wanted to do is to make sure that the uploaded file has all the expected properties (is a WAV file, has one channel, has low bitrate etc). But i don't even know where to start. These files will be only added via django admin. In regular FormView i would utilize server-sided validation in View, and only then add it to model. How to do it in django admin?
To summarize what i expect my app to do:
1) Add file to a model in django admin
2) Server checks file properties, and if requirements are not met, tries to convert it to proper format 3) If the file is in proper format, only then it saves the object.
You need to register a ModelAdmin with a custom form.
ModelAdmin has a form property which is by default set to forms.ModelForm class, you can replace that by assigining that property to your Admin class.
# app_dir/admin.py
from django.contrib import admin
from .forms import GreetingAdminForm
from .models import Greeting
#admin.register(models.Greeting)
class GreetingAdmin(admin.ModelAdmin):
form = GreetingAdminForm
readonly_fields = ['uploaded_at']
Than you need to define your GreetingAdminForm in forms.py. with custom validation Logic.
The way I would do it is add a ModelForm with overridden audo_file field with added validators. You can check the django documentation for writing your validation logic here
Probaly you want to use file extension validation, and add a clean_{fieldname} method on the form.
The clean_{fieldname} method does not take any arguments but the return value of this method must replace the existing value in cleaned_data. You will need an external library that suits your needs, accepts audio formats that you intend to allow, and outputs processed file in desired format. Docs on cleaning specific attribiutes are here
# app_dir/forms.py
from django import forms
from django.core.exceptions import ValidationError
from .validators import validate_file_extension
from .models import Greeting
class GreetingAdminForm(forms.ModelForm):
audio_file = forms.FileField(validators=[validate_file_extension])
def clean_audio_file(self):
data = self.cleaned_data
processed_audio_file = None
# audio file processing logic goes here,
if not processed_audio_file:
raise ValidationError('error message')
data['audio_file'] = processed_audio_file
return data
class Meta:
model = Greeting
fields = [
'audio_file',
'description'
]
# app_dir/validators.py
def validate_file_extension(value):
# validate file extension logic here,
you can find a example of file extension validation
here
Another angle to approach this could be also
- writing a custom form field which subclasses the FileField,you can find documentation on writing your own field here, this class should override w methods validate() - which handles validation logic, and to python where you would prepare output to be available in your python code

Django add dropdown filter for a view with no class / model

I have a method mapped to a url in urls.py:
urlpatterns = [
url(r'^mydir/statistics', statistics_view, name="Statistics")
]
Then in mydir/statistics/views.py I have the method:
def statistics_view(request):
list_mystuff = Mytable.objects.all() #Mytable is defined in models
....
#lots of code here to assign "enriched_models" data structure
enriched_models = {bunch of stuff}
......
return render(request, 'statistics.html', {"statistics_enriched_models": enriched_models})
It all works fine and everything is rendered in using statistics.html template.
Now I need to implement a dropdown filter by one of the fields of Mytable.
With classes I would have to create class for Statistics in models.py, StatisticsAdmin as a proxy model in admin.py, register them both, and have
list_filter=["myfield",] set in StatisticsAdmin
Is there a way to set up a dropdown filter (with list_filters or whatever) and have the dropdown rendered directly without creating the classes, just with what I already have (a method that populates the template)?
UPDATE
At the end I used django-filter tool: https://django-filter.readthedocs.io/en/master/index.html
The filter to use for the dropdown control is django_filters.ChoiceFilter .
Worked fine.

How to reference a ModelView in flask-admin

What's the right way to get the URL for a flask-admin ModelView?
Here's a very simple example:
my_admin_view.py
from flask.ext.admin.contrib.sqla import ModelView
from common.flask_app import app
from models import db, User, Role
admin = Admin(app, name="Boost Admin")
admin.add_view(ModelView(User, db.session, category="model"))
admin.add_view(ModelView(Role, db.session, category="model"))
my_admin_template.html
...
<p>Check out my user admin link:</p>
User view link
{# ______________
what argument to pass in here?? #}
...
What's the correct argument to pass to url_for(...)?
I've tried modelview.user, my_admin_view.modelview.user, etc. None of them seem to resolve correctly, and I'd like to avoid hardcoding the link.
thanks!
OK I figured it out after reading the source code for ModelView.
First, make sure that endpoints are named (it's possible to do it without named endpoints, but this makes the code much clearer):
from flask.ext.admin.contrib.sqla import ModelView
from models import db, User, Role
admin = Admin(app, name="Boost Admin")
admin.add_view(ModelView(User,db.session,category="model", endpoint="model_view_user"))
admin.add_view(ModelView(Role,db.session,category="model", endpoint="model_view_role"))
...now in the template, you can reference the basic model view as follows:
URL for User model default view is: {{model_view_user.index_view}}
URL for Role model default view is: {{model_view_role.index_view}}
The index_view function is defined here, and implements the default view for a flask admin ModelView.
See the section Generating URLs in the Flask-Admin introduction.
It says to "use the lowercase name of the model as the prefix". Add a dot, and the name of the view.
index_view for the overview list.
create_view for creating a new row.
edit_view for modifying an existing row.
So you can easily do:
url_for('user.index_view')
url_for('role.create_view')
url_for('user.edit_view', id=1)
It should be
url_for('admin.user')
If you read the flask-admin docs here, for generating URLs, it clearly says:
If you want to generate a URL for a particular view method from outside, the following rules apply:
....
3. For model-based views the rules differ - the model class name should be used if an endpoint name is not provided.

Categories

Resources