Get parent page on creating new Wagtail Page - python

I need to change some field default value given a value from the parent page when creating a new page. When editing an existing page, this is not a problem, but when creating a new page, I need to set a default value for a field. I have tried overriding the admin form, on init for WagtailAdminPageForm, the parent_page parameter is empty. And if I try to do it on the init method form Page subclass, there is no arg or kwargs with the parent information.
is there a way to get the page for which the new page will be the parent?
This is what I tried on Page constructor
class CMSPage(Page):
.
.
.
def __init__(self, *args, **kwargs):
super(BaseContentMixin, self).__init__(*args, **kwargs)
if hasattr(self.get_parent().specific, 'published_site') and self.get_parent().specific.published_site == 'extranet':
self.published_site = 'extranet'
This works editing a page, for new page, I get that NoneType objects has no attribute specific.
Django version is 1.10, Python version is 3.6, Wagtail version is 1.9

After clarification of the question, here is a second answer that might be more suitable. Note that this requires Wagtail 1.11.x which as at this time is not yet released.
Example Code for Solution
First Create a new class for your custom page form, usually in models.py or wherever the model for your page is.
from wagtail.wagtailadmin.forms import WagtailAdminPageForm
class MyCustomPageForm(WagtailAdminPageForm):
# Override the __init__ function to update 'initial' form values
def __init__(self, data=None, files=None, parent_page=None, *args, **kwargs):
print('parent_page', parent_page, parent_page.id, parent_page.title)
# update the kwargs BEFORE the init of the super form class
instance = kwargs.get('instance')
if not instance.id:
# only update the initial value when creating a new page
kwargs.update(initial={
# 'field': 'value'
'title': parent_page.id # can be anything from the parent page
})
# Ensure you call the super class __init__
super(MyCustomPageForm, self).__init__(data, files, *args, **kwargs)
self.parent_page = parent_page
Second On the page model definition, tell that model to use the form class you have defined
from wagtail.wagtailcore.models import Page
class MyCustomPage(Page):
base_form_class = MyCustomPageForm
Explanation of Solution
Wagtail provides the ability to override the form class (note: not model class) that gets called when building the form for creation and editing in Wagtail Admin.
Documentation: Customising generated forms
Our custom form class will inherit the WagtailAdminPageForm class.
Inside this new form class, we want to define our own __init__ function, this is called when the form is created.
Note the Github code for the default definition of __init__ on WagtailAdminPageForm
To inject custom values, we will override the initial data in kwargs before the calling of the class' super __init__
We only want to do this when a new page is being created, so check that the instance has no id
We can access the parent_page instance and any fields in it
After adding our custom initial data, then we call the super __init__ with the changed kwargs
Note: The reason for the requirement of Wagtail 1.11.x is that previous versions until that point did not add parent_page when calling the class model until pull request 3508

I would recommend using the Wagtail Hooks:
http://docs.wagtail.io/en/v1.10.1/reference/hooks.html
Here is a basic example that works (in the Wagtaildemo app), in this example, I am just getting the parent page's title. It should work the same if you are getting something else from the parent's page specific model. Note that this updates the record after it has been created, not as part of the creation itself.
# my_app/wagtail_hooks.py
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore import hooks
#hooks.register('after_create_page')
def do_after_page_create(request, page):
# get the parent page from the current page
parent = page.get_parent().specific
# get the value from the parent page
parent_title = parent.title
# get a new instance of the page that was just created
new_page = Page.objects.get(id=page.id).specific
# update the value in the page that was just created & save
new_page.parent_title = parent_title
new_page.save()
I have used the Wagtail hooks a lot and they work well, you could further refine this by checking the new_page.content_type == "CMSPage" to ensure this only updates pages of that specific model.
Finally, it might be good to look into how Wagtail sites works, this might be a more specific solution to your issue in the example code. You can add extra settings to your Site Model and have multiple Sites in Wagtail. I am not sure if your example was just a rough example or the exact issue you are facing.

Related

Wagtail: How to overwrite create/edit template for PageModel

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

Wagtail: Dynamically Choosing Template With a Default

I'm wondering if there is a way in Wagtail to enter a custom template path via CharField in a base model, and then establish a template in an inherited model that would be the default. For example:
base/models.py
class WebPage(Page):
template_path = models.CharField()
def get_template(self, request):
if self.template_path:
template = template_path
else:
template = *something*
app/models.py
class MyWebPage(WebPage):
template = 'default_template.html'
Ideally, I'd establish the template attribute in the MyWebPage model, and that would act as a default. However, the get_template method in the WebPage base model would supersede it, but only if it's not empty. Is any of this possible?
I was reading through the Wagtail Docs and found this page (http://docs.wagtail.io/en/v2.1.1/advanced_topics/third_party_tutorials.html) and on that page was an article about dynamic templating. This is the page that has it: https://www.coactivate.org/projects/ejucovy/blog/2014/05/10/wagtail-notes-dynamic-templates-per-page/
The idea is to set a CharField and let the user select their template. In the following example they're using a drop down, which might even be better for you.
class CustomPage(Page):
template_string = models.CharField(max_length=255, choices=(
(”myapp/default.html”, “Default Template”),
(”myapp/three_column.html”, “Three Column Template”,
(”myapp/minimal.html”, “Minimal Template”)))
#property
def template(self):
return self.template_string
^ code is from the coactivate.org website, it's not mine to take credit for.
In the template property, you could check if not self.template_string: and set your default path in there.
Edit #1:
Adding Page inheritance.
You can add a parent Page (the Base class) and modify that, then extend any other class with your new Base class. Here's an example:
class BasePage(Page):
"""This is your base Page class. It inherits from Page, and children can extend from this."""
template_string = models.CharField(max_length=255, choices=(
(”myapp/default.html”, “Default Template”),
(”myapp/three_column.html”, “Three Column Template”,
(”myapp/minimal.html”, “Minimal Template”)))
#property
def template(self):
return self.template_string
class CustomPage(BasePage):
"""Your new custom Page."""
#property
def template(self):
"""Overwrite this property."""
return self.template_string
Additionally, you could set the BasePage to be an abstract class so your migrations don't create a database table for BasePage (if it's only used for inheritance)

Django session variables lost when set in form_valid() of class-based views

Using Django, if I set a session variable within the post() method of a django.views.generic.edit.FormView class, that variable is then available for future requests.
e.g.
def post(self, request, *args, **kwargs):
"""
Store useful data in session variable for future requests
"""
if 'useful_data' in request.POST:
request.session['useful_data'] = useful_data
return HttpResponseRedirect(self.get_success_url())
If however, I attempt to set a session variable via the form_valid() method of a django.views.generic.edit.FormView class, the changes I make to the variable seem to disappear before the next request.
e.g.
def form_valid(self, form):
"""
Store useful data in session variable for future requests
"""
# Useful data that I only want to update if the form was validated
self.request.session['useful_data'].update(form.cleaned_data['useful_data'])
return HttpResponseRedirect(self.get_success_url())
So, how can I make persistent changes to session variables from the form_valid() method of a django.views.generic.edit.FormView class?
I suspect the problem could be the update part:
self.request.session['useful_data'].update(form.cleaned_data['useful_data'])
If we take a look at the documentation, the issue could be django does not know that the session has been modified and thus does not change it.
To validate this, make it explicit that session has been modified:
self.request.session['useful_data'].update(form.cleaned_data['useful_data'])
self.request.session.modified = True
Or just assign the value without using update(), just like in the documentation:
self.request.session['useful_data'] = form.cleaned_data['useful_data']
Hope it helps!

Django bootstrap3_datetime widget in admin site doesn't pass form data

I'm trying to replace the standard AdminSplitDateTime widget in my admin site for better functionality (basically I want to display only 'available' dates in my calender which I couldn't find how to do with the default picker).
I decided to use the bootstrap3_datetime widget.
After overriding my field to use the new widget, it doesn't seem to be transferred into the 'clean' method (isn't in self.cleaned_data) for validation.
models.py
publish_time = models.DateTimeField('Date to publish')
admin.py
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
bad_dates = []
#populating bad_dates with application logic
def clean(self):
# This will always return None when using the new widget.
# When working with the default widget, I have the correct value.
publish_time = self.cleaned_data.get('publish_time', None)
publish_time = forms.DateTimeField(widget=DateTimePicker(options=
{"format": "DD-MM-YYYY HH:mm",
"startDate": timezone.now().strftime('%Y-%m-%d'),
"disabledDates": bad_dates,
})
class MyModelAdmin(admin.ModelAdmin):
form = MyForm
admin.site.register(MyModel, MyModelAdmin)
HTML-wise, the widget works well and the text field is populated with the correct date (and with the 'bad_dates' disabled). The problem is that it seems it isn't saved on the form.
I also tried initializing the widget in the init method by doing:
self.fields['publish_time'].widget = DateTimePicker(options=...)
But the result was the same.
What am I missing here?
Is it even possible to modify widgets in the admin site?
Thanks!
Update:
I've analysed the POST request that is sent using each of the widgets.
In the default admin widget, I see that it generates two fields: "publish_time_0" (for date) and "publish_time_1" (for time).
In the bootstrap3 widget, only a single "publish_time" field is sent.
I'm assuming that the admin site understands that the field is a DateTimeField (from models), looks for id_0 and id_1 and that's why it fails.
Does that make sense? Is there anyway around it?

Django REST Framework: creating hierarchical objects using URL arguments

I have a django-rest-framework REST API with hierarchical resources. I want to be able to create subobjects by POSTing to /v1/objects/<pk>/subobjects/ and have it automatically set the foreign key on the new subobject to the pk kwarg from the URL without having to put it in the payload. Currently, the serializer is causing a 400 error, because it expects the object foreign key to be in the payload, but it shouldn't be considered optional either. The URL of the subobjects is /v1/subobjects/<pk>/ (since the key of the parent isn't necessary to identify it), so it is still required if I want to PUT an existing resource.
Should I just make it so that you POST to /v1/subobjects/ with the parent in the payload to add subobjects, or is there a clean way to pass the pk kwarg from the URL to the serializer? I'm using HyperlinkedModelSerializer and ModelViewSet as my respective base classes. Is there some recommended way of doing this? So far the only idea I had was to completely re-implement the ViewSets and make a custom Serializer class whose get_default_fields() comes from a dictionary that is passed in from the ViewSet, populated by its kwargs. This seems quite involved for something that I would have thought is completely run-of-the-mill, so I can't help but think I'm missing something. Every REST API I've ever seen that has writable endpoints has this kind of URL-based argument inference, so the fact that django-rest-framework doesn't seem to be able to do it at all seems strange.
Make the parent object serializer field read_only. It's not optional but it's not coming from the request data either. Instead you pull the pk/slug from the URL in pre_save()...
# Assuming list and detail URLs like:
# /v1/objects/<parent_pk>/subobjects/
# /v1/objects/<parent_pk>/subobjects/<pk>/
def pre_save(self, obj):
parent = models.MainObject.objects.get(pk=self.kwargs['parent_pk'])
obj.parent = parent
Here's what I've done to solve it, although it would be nice if there was a more general way to do it, since it's such a common URL pattern. First I created a mixin for my ViewSets that redefined the create method:
class CreatePartialModelMixin(object):
def initial_instance(self, request):
return None
def create(self, request, *args, **kwargs):
instance = self.initial_instance(request)
serializer = self.get_serializer(
instance=instance, data=request.DATA, files=request.FILES,
partial=True)
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data)
return Response(
serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Mostly it is copied and pasted from CreateModelMixin, but it defines an initial_instance method that we can override in subclasses to provide a starting point for the serializer, which is set up to do a partial deserialization. Then I can do, for example,
class SubObjectViewSet(CreatePartialModelMixin, viewsets.ModelViewSet):
# ....
def initial_instance(self, request):
instance = models.SubObject(owner=request.user)
if 'pk' in self.kwargs:
parent = models.MainObject.objects.get(pk=self.kwargs['pk'])
instance.parent = parent
return instance
(I realize I don't actually need to do a .get on the pk to associate it on the model, but in my case I'm exposing the slug rather than the primary key in the public API)
If you're using ModelSerializer (which is implemented by HyperlinkedModelSerializer) it's as easy as implementing the restore_object() method:
class MySerializer(serializers.ModelSerializer):
def restore_object(self, attrs, instance=None):
if instance is None:
# If `instance` is `None`, it means we're creating
# a new object, so we set the `parent_id` field.
attrs['parent_id'] = self.context['view'].kwargs['parent_pk']
return super(MySerializer, self).restore_object(attrs, instance)
# ...
restore_object() is used to deserialize a dictionary of attributes into an object instance. ModelSerializer implements this method and creates/updates the instance for the model you specified in the Meta class. If the given instance is None it means the object still has to be created, so you just add the parent_id attribute on the attrs argument and call super().
So this way you don't have to specify a read-only field, or have a custom view/serializer.
More information:
http://www.django-rest-framework.org/api-guide/serializers#declaring-serializers
Maybe a bit late, but i guess this drf nested routers library could be helpful for that operation.

Categories

Resources