Select base template per site - wagtail

I am building a setup that will contain a main site and a number of microsites. Each microsite is going to have a separate branding but use the same page types.
Given Wagtail already has a Site object which links to an appropriate Page tree, is there also built in functionality to configure the template loaders to choose an appropriate base.html or will I have to write a custom template loader?

Wagtail doesn't have any built-in functionality for this, as it doesn't make any assumptions about how your templates are put together. However, you could probably implement this yourself fairly easily using the wagtail.contrib.settings module, which provides the ability to attach custom properties to individual sites. For example, you could define a TemplateSettings model with a base_template field - your templates can then check this setting and dynamically extend the appropriate template, using something like:
{% load wagtailsettings_tags %}
{% get_settings %}
{% extends settings.my_app.TemplateSettings.base_template %}

I extended the above answer to provide an override of the {% extends ... %} tag to use a template_dir parameter.
myapp.models:
from wagtail.contrib.settings.models import BaseSetting, register_setting
#register_setting
class SiteSettings(BaseSetting):
"""Site settings for each microsite."""
# Database fields
template_dir = models.CharField(max_length=255,
help_text="Directory for base template.")
# Configuration
panels = ()
myapp.templatetags.local:
from django import template
from django.core.exceptions import ImproperlyConfigured
from django.template.loader_tags import ExtendsNode
from django.template.exceptions import TemplateSyntaxError
register = template.Library()
class SiteExtendsNode(ExtendsNode):
"""
An extends node that takes a site.
"""
def find_template(self, template_name, context):
try:
template_dir = \
context['settings']['cms']['SiteSettings'].template_dir
except KeyError:
raise ImproperlyConfigured(
"'settings' not in template context. "
"Did you forget the context_processor?"
)
return super().find_template('%s/%s' % (template_dir, template_name),
context)
#register.tag
def siteextends(parser, token):
"""
Inherit a parent template using the appropriate site.
"""
bits = token.split_contents()
if len(bits) != 2:
raise TemplateSyntaxError("'%s' takes one argument" % bits[0])
parent_name = parser.compile_filter(bits[1])
nodelist = parser.parse()
if nodelist.get_nodes_by_type(ExtendsNode):
raise TemplateSyntaxError(
"'%s' cannot appear more than once in the same template" % bits[0])
return SiteExtendsNode(nodelist, parent_name)
myproject.settings:
TEMPLATES = [
{
...
'OPTIONS': {
...
'builtins': ['myapp.templatetags.local'],
},
},
]

Related

Wagtail CMS - display children in reverse chronological order

I have a web site running Wagtail CMS 2.6.1. Users have created an "Updates" page and a number of news/updates articles under it. The problem is on the "Updates" page the children are shown in alphabetical order, instead of reverse chronological order.
Can this be changed somehow from the admin interface? If not, what is the fastest way to do it in Python?
This is the the Python model (I believe):
class ArticleIndexPage(Page):
intro = models.CharField(max_length=250, blank=True, null=True)
content_panels = Page.content_panels + [
FieldPanel('intro', classname='full')
]
def get_context(self, request, *args, **kwargs):
context = super(ArticleIndexPage, self)\
.get_context(request, *args, **kwargs)
children = ArticlePage.objects.live()\
.child_of(self).not_type(ArticleIndexPage).order_by('-date')
siblings = ArticleIndexPage.objects.live()\
.sibling_of(self).order_by('title')
child_groups = ArticleIndexPage.objects.live()\
.child_of(self).type(ArticleIndexPage).order_by('title')
child_groups_for_layout = convert_list_to_matrix(child_groups)
context['children'] = children
context['siblings'] = siblings
context['child_groups'] = child_groups_for_layout
return context
You can manually reorder pages (Reordering pages in the Editor's guide), but for them to be automatically ordered by date, you need to do this in code.
If your 'Updates page' only contains ArticlePage instances as children, then you can add the children sorted by date to the Updates page template context. See Customising template context. It might look like
class BlogIndexPage(Page):
...
def get_context(self, request):
context = super().get_context(request)
context['children'] = ArticlePage.objects.child_of(self).live().order_by('-date')
return context
Then in the template, you can use it as
{% for child in children %}
{{ child.title }}
{{ child.date }}
{% endfor %}
(This makes assumptions about your models' and variables' naming and templates. Feel free to change the details.)

How to prevent duplicates when using ModelChoiceFilter in Django Filter and Wagtail

I am trying to use filters with a Wagtail Page model and a Orderable model. But I get duplicates in my filter now. How can I solve something like this?
My code:
class FieldPosition(Orderable):
page = ParentalKey('PlayerDetailPage', on_delete=models.CASCADE, related_name='field_position_relationship')
field_position = models.CharField(max_length=3, choices=FIELD_POSITION_CHOICES, null=True)
panels = [
FieldPanel('field_position')
]
def __str__(self):
return self.get_field_position_display()
class PlayerDetailPage(Page):
content_panels = Page.content_panels + [
InlinePanel('field_position_relationship', label="Field position", max_num=3),
]
class PlayerDetailPageFilter(FilterSet):
field_position_relationship = filters.ModelChoiceFilter(queryset=FieldPosition.objects.all())
class Meta:
model = PlayerDetailPage
fields = []
So what I am trying to do is create a filter which uses the entries from FIELD_POSITION_CHOICES to filter out any page that has this position declared in the inline panel in Wagtail.
As you can see in the picture down below, the filters are coming through and the page is being rendered. (These are 2 pages with a list of 3 field positions).
So Page 1 and Page 2 both have a "Left Winger" entry, so this is double in the dropdown. The filtering works perfectly fine.
What can I do to prevent this?
The solution should be something like this (Credits to Harris for this):
I basically have one FieldPosition object per page-field position, so it's listing all of the objects correctly. I suspect I should not use the model chooser there, but a list of the hard coded values in FIELD_POSITION_CHOICES and then a filter to execute a query that looks something like PlayerDetailPage.objects.filter(field_position_relationship__field_position=str_field_position_choice). But what is the Django Filter way of doing this?
Raf
In my limited simplistic view it looks like
class PlayerDetailPageFilter(FilterSet):
field_position_relationship = filters.ModelChoiceFilter(queryset=FieldPosition.objects.all())
is going to return all the objects from FieldPosition and if you have 2 entries for 'left wing' in here (one for page 1 and one for page 2) then it would make sense that this is duplicating in your list. So have you tried to filter this list queryset with a .distinct? Perhaps something like
class PlayerDetailPageFilter(FilterSet):
field_position_relationship = filters.ModelChoiceFilter(queryset=FieldPosition.objects.values('field_position').distinct())
I found the solution after some trial and error:
The filter:
class PlayerDetailPageFilter(FilterSet):
field_position_relationship__field_position = filters.ChoiceFilter(choices=FIELD_POSITION_CHOICES)
class Meta:
model = PlayerDetailPage
fields = []
And then the view just like this:
context['filter_page'] = PlayerDetailPageFilter(request.GET, queryset=PlayerDetailPage.objects.all()
By accessing the field_position through the related name of the ParentalKey field_position_relationship with __.
Then using the Django Filter ChoiceFilter I get all the hard-coded entries now from the choice list and compare them against the entries inside the PlayerDetailPage query set.
In the template I can get the list using the Django Filter method and then just looping through the query set:
<form action="" method="get">
{{ filter_page.form.as_p }}
<input type="submit" />
</form>
{% for obj in filter_page.qs %}
{{ obj }} >
{% endfor %}

Wagtail Custom Pages?

I'm totally new to Wagtail/ Django.
Here's what I am trying to achieve:
I'd like to have an ability in the backend of my Wagtail CMS install to create 'pages' or 'posts' that follow a strict template.
The template would have custom fields like 'header' and aim content' etc.
I'm sure that this is possible, I'd just be interested to know how I'd go about achieving this?
For example, does anyone know if Wagtail has a plugin or other to enable this?
Thanks for all help/ direction.
You want to create pages and posts that follow a strict template: that's exactly what Django and Wagtail let you do. But there's one catch: Wagtail takes this a step further and lets you move entire sections of a page — these are called Streamfields. It's an amazing feature, to be honest.
Here's an example to get you started (note: this is untested and not linted)
# -*- coding: utf-8 -*-
"""Basic Page model."""
from django.db import models
from wagtail.admin.edit_handlers import FieldPanel, MultiFieldPanel, StreamFieldPanel
from wagtail.core.fields import StreamField
from wagtail.core.models import Page
from your_custom_app.streams import streamfields
class BasicPage(Page):
"""A basic page class."""
template = "templates/pages/basic_page.html"
parent_page_type = ["pages.HomePage", "pages.BasicPage"]
subpage_types = ["pages.BasicPage"]
header = models.CharField(max_length=100)
content = StreamField(
('streamfield_name', streamfields.CustomStreamfield()),
# ... More streams
null=True,
blank=True,
)
# Other additional fields you want on your page.
# Panels are how you lay out your pages in the /admin/
content_panels = [
FieldPanel("title", classname="full title"),
FieldPanel("header"),
# FieldPanel("other_fields"),
StreamFieldPanel("content"),
]
settings_panels = Page.settings_panels + [] # Custom settings panel
promote_panels = Page.promote_panels + [] # Custom promote panel
class Meta:
"""Meta information."""
verbose_name = "Basic Page"
verbose_name_plural = "Basic Pages"
You can also download and setup the Wagtail Bakery Demo, it has a lot of great examples in it.

Django Dynamic model register in admin

I'm using django 1.11 and I tried to to create django dynamic models by referring this link https://code.djangoproject.com/wiki/DynamicModels , by executing each and every step it runs without any issue, but How can I see this created table in django admin panel?
action.py
from django.db import models
from django.contrib import admin
def create_model(name, fields=None, app_label='', module='', options=None, admin_opts=None):
"""
Create specified model
"""
class Meta:
# Using type('Meta', ...) gives a dictproxy error during model creation
pass
if app_label:
# app_label must be set using the Meta inner class
setattr(Meta, 'app_label', app_label)
# Update Meta with any options that were provided
if options is not None:
for key, value in options.iteritems():
setattr(Meta, key, value)
# Set up a dictionary to simulate declarations within a class
attrs = {'__module__': module, 'Meta': Meta}
# Add in any fields that were provided
if fields:
attrs.update(fields)
# Create the class, which automatically triggers ModelBase processing
model = type(name, (models.Model,), attrs)
# Create an Admin class if admin options were provided
if admin_opts is not None:
print admin_opts
class Admin(admin.ModelAdmin):
pass
for key, value in admin_opts:
setattr(Admin, key, value)
admin.site.register(model, Admin)
return model
In Console:
from action import create_model
from django.db import models
fields = {
'first_name': models.CharField(max_length=255),
'last_name': models.CharField(max_length=255),
'__str__': lambda self: '%s %s' (self.first_name, self.last_name),
}
options = {
'ordering': ['last_name', 'first_name'],
'verbose_name': 'valued customer',
}
admin_opts = {}
model = create_model('Person', fields,
options=options,
admin_opts=admin_opts,
app_label='form',
module='project.app.model',
)
I can see no. of fields by
len(model._meta.fields)
But I have no idea of, how to register the created model in admin, and what parameter will come inside admin_opts = {} , how can i do makemigrations and migrate,how can I access this model in views.py, from where i will import this model .Can you guys please help me for this , it will be very useful for me and Thanks in advance.
with connection.schema_editor() as editor:
editor.create_model(Model)
This is from github source code , try it instead of sql_model_create and I try to success in my project,and it's true..
I have worked hard for a long time because I don't find django-dynamic-model in "django 1.10".
I think you forgot to execute this function.
def install(model):
from django.core.management import sql, color
from django.db import connection
# Standard syncdb expects models to be in reliable locations,
# so dynamic models need to bypass django.core.management.syncdb.
# On the plus side, this allows individual models to be installed
# without installing the entire project structure.
# On the other hand, this means that things like relationships and
# indexes will have to be handled manually.
# This installs only the basic table definition.
# disable terminal colors in the sql statements
style = color.no_style()
cursor = connection.cursor()
statements, pending = sql.sql_model_create(model, style)
for sql in statements:
cursor.execute(sql)

django model/modelForm - How to get dynamic choices in choiceField?

i'm experimenting with django and the builtin admin interface.
I basically want to have a field that is a drop down in the admin UI. The drop down choices should be all the directories available in a specified directory.
If i define a field like this:
test_folder_list = models.FilePathField(path=/some/file/path)
it shows me all the files in the directory, but not the directories.
Does anyone know how i can display the folders?
also i tried doing
test_folder_list = models.charField(max_length=100, choices=SOME_LIST)
where SOME_LIST is a list i populate using some custom code to read the folders in a directory. This works but it doesn't refresh. i.e. the choice list is limited to a snapshot of whatever was there when running the app for the first time.
thanks in advance.
update:
after some thinking and research i discovered what i want may be to either
1. create my own widget that is based on forms.ChoiceField
or
2. pass my list of folders to the choice list when it is rendered to the client
for 1. i tried a custom widget.
my model looks like
class Test1(models.Model):
test_folder_ddl = models.CharField(max_length=100)
then this is my custom widget:
class FolderListDropDown(forms.Select):
def __init__(self, attrs=None, target_path):
target_folder = '/some/file/path'
dir_contents = os.listdir(target_folder)
directories = []
for item in dir_contents:
if os.path.isdir(''.join((target_folder,item,))):
directories.append((item, item),)
folder_list = tuple(directories)
super(FolderListDropDown, self).__init__(attrs=attrs, choices=folder_list)
then i did this in my modelForm
class test1Form(ModelForm):
test_folder_ddl = forms.CharField(widget=FolderListDropDown())
and it didn't seem to work.What i mean by that is django didn't want to use my widget and instead rendered the default textinput you get when you use a CharField.
for 2. I tried this in my ModelForm
class test1Form(ModelForm):
test_folder_ddl = forms.CharField(widget=FolderListDropDown())
test_folder_ddl.choices = {some list}
I also tried
class test1Form(ModelForm):
test_folder_ddl = forms.ChoiceField(choices={some list})
and it would still render the default char field widget.
Anyone know what i'm doing wrong?
Yay solved. after beating my head all day and going through all sorts of examples by people i got this to work.
basically i had the right idea with #2. The steps are
- Create a ModelForm of our model
- override the default form field user for a models.CharField. i.e. we want to explcitly say use a choiceField.
- Then we have to override how the form is instantiated so that we call the thing we want to use to generate our dynamic list of choices
- then in our ModelAdmin make sure we explicitly tell the admin to use our ModelForm
class Test1(models.Model):
test_folder_ddl = models.CharField(max_length=100)
class Test1Form(ModelForm):
test_folder_ddl = forms.choiceField()
def __init__(self, *args, **kwargs):
super(Test1Form, self).__init__(*args, **kwargs)
self.fields['test_folder_ddl'].choices = utility.get_folder_list()
class Test1Admin(admin.ModelAdmin):
form = Test1Form
I use a generator:
see git://gist.github.com/1118279.git
import pysvn
class SVNChoices(DynamicChoice):
"""
Generate a choice from somes files in a svn repo
""""
SVNPATH = 'http://xxxxx.com/svn/project/trunk/choices/'
def generate(self):
def get_login( realm, username, may_save ):
return True, 'XXX', 'xxxxx', True
client = pysvn.Client()
client.callback_get_login = get_login
return [os.path.basename(sql[0].repos_path) for sql in client.list(self.SVNPATH)[1:]]

Resources