I need to create a reusable element (cta button) that I can include in many places throughout the page.
These cta buttons are used ~8 times throughout the design. How can I do this without copy-pasting?
Similar to this: Ways to create reusable sets of fields in Wagtail? Except I must be able to use the set several times on a single page.
This is what I am trying to do:
class HomePage(Page):
template = "home/home_page.html"
hero_heading = models.CharField(max_length=50)
hero_subheading = models.CharField(max_length=255)
hero_cta1 = HeroCTA1() # abstract reusable model
hero_cta2 = HeroCTA2()
content_panels = Page.content_panels + [
FieldPanel("hero_heading"),
FieldPanel("hero_subheading"),
hero_cta1.panels,
hero_cta2.panels,
]
My attempt at a reusable CTAButton class:
class CTAButton(models.Model):
text = RichTextField(max_length=25, features=["bold"])
url = models.URLField(null=True, blank=True)
page = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="%(app_label)s_%(class)s_page",
)
panels = MultiFieldPanel(
[
FieldPanel("text"),
FieldPanel("url"),
PageChooserPanel("page"),
],
heading="CTA Button Fields",
classname="collapsible",
)
class Meta:
abstract = True
class HeroCTA1(CTAButton):
pass
class HeroCTA2(CTAButton):
pass
Except this doesn't work :/
I am encountering "HomePage has no field named 'page'"
Shouldn't this break on 'text' since it's before 'page'?
Any advice on how to proceed?
checkout django documentation for model inheritance there are three method you can achieve model inheritance in django wagtail is made from djanog so you can also use abstract model inheritance in that for more information checkout this documentation
https://docs.djangoproject.com/en/3.0/topics/db/models/#model-inheritance
If your page design has 8 places where a CTA button can go, then perhaps it's more useful to treat it as a flexible sequence of elements where CTA buttons can be freely mixed in with other types of content chosen by the page author, rather than a fixed layout with specific elements at specific points. Wagtail's StreamField provides that kind of flexible layout: https://docs.wagtail.io/en/stable/topics/streamfield.html
I followed the suggestion of #gasman and broke the template out into blocks and used a mixin to keep it DRY.
This solution extracts the href function and allows it to be used in multiple CTA blocks, but it hardcodes the naming convention. Future readers can probably find a smarter way to do this. To use multiple elements, such as TwoCTAMixin, I just extend the base mixin and add cta1_text, etc.
CTAStructValue is required to access the value as you would expect with an #property variable. More info here.
def get_cta_href_value(cta, varname="cta"):
"""Function that returns the href link with a value provided.
The value is returned in this order: (url > page > #)."""
url_name = f"{varname}_url"
page_name = f"{varname}_page"
if cta[url_name] and cta[url_name] != "":
return cta[url_name]
elif cta[page_name] and cta[url_name] != "":
return cta[page_name]
return "#" # broken link, return something non-volatile
class CTAStructValue(blocks.StructValue):
"""Calculated properties for CTAMixin."""
#property
def cta_href(self):
return get_cta_href_value(self)
class CTAMixin(blocks.StructBlock):
"""Mixin that includes a single CTA element."""
cta_text = blocks.CharBlock(required=True, help_text="Text to display on the button")
cta_url = blocks.URLBlock(required=False, help_text="URL the button directs to")
cta_page = blocks.PageChooserBlock(required=False, help_text="Page the button directs to")
class Meta:
abstract = True
class SomeSectionBlock(CTAMixin):
section_heading = blocks.CharBlock()
class Meta:
template = "blocks/some_section_block.html"
some_section_block.html
<section>
<div class="wrapper some-section">
<h2>{{ self.section_heading }}</h2>
<a href="{{ self.cta_href }}">
<button>{{ self.cta_text }}</button>
</a>
</div>
</section>
Related
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)
To keep my project cleaner I decided (maybe wrongly) to split my one Django app into two. One app for the management of information, the other for display. And for this I thought using Django Proxy Models in the display App would be the best way. However, I've come across a problem with the ForeignKey fields within certain models and forcing those foreign keys to use a proxy-model, instead of its originating model.
Here's some examples to make it clearer:
App_1-model.py
class Recipe(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
...
class Ingredient(models.Model):
name = models.CharField(max_length=200)
recipe = models.ForeignKey(Recipe)
weight = models.IntegerField()
App_2-model.py (Imports App_1 models)
class RecipeDisplayProxy(Recipe):
class Meta:
proxy = True
#property
def total_weight(self):
# routine to calculate total weight
return '100g'
class IngredientDisplayProxy(Ingredient):
class Meta:
proxy = True
#property
def weight_lbs(self):
# routine to convert the original weight (grams) to lbs
return '2lb'
App_2.views.py
def display_recipe(request, slug):
recipe = get_object_or_404(RecipeDisplayProxy, slug=slug)
return render(
request,
'display_recipe/recipe.html',
{'recipe': recipe}
)
App_2-template.html
<h2 class="display-4">{{ recipe.name }}</h2>
<p>{{ recipe.total_weight }}</p> <!-- This works fine, as expected //-->
<ul>
{% for recipe_ingredient in recipe.ingredient_set.all %}
<li>{{recipe_ingredient.ingredient}} –
{{recipe_ingredient.weight_lbs}}</li>
<!--
The above line doesn't return anything as the 'Ingredient' model, not the "IngredientDisplayProxy' is being returned. (As expected)
-->
{% endfor %}
</ul>
What's happening here is that I'm successfully returning the RecipeDisplayProxy model as specified in the view, but when I access ingredient_set it returns the Ingredient model, rather than the IngredientDisplayProxy (as expected).
So how do I force ingredient_set to return IngredientDisplayProxy models instead?
I tried implementing the code found here:
Django proxy model and ForeignKey
But had no luck. I then started digging into the init() method for RecipeDisplayProxy - to see if I could overwrite the models used in the ingredient_set, but couldn't find anything that would give me the right response.
So any ideas?
Or, am I just taking this down a bad path - and should be considering a different design altogether?
From the view you are returning the recipe instance, but in the template you are accessing the ingredient through the recipe, but it should be the other way round, from ingredient you can access the recipe.Now for the proxy model, better read this documentation
Looks like I Was doing some things wrong, and so on the advice of fips I went back and did the following:
class RecipeDisplayProxy(Recipe):
class Meta:
proxy = True
#property
def total_weight(self):
# routine to calculate total weight
return '100g'
#property
def ingredient_set(self):
qs = super(RecipeDisplayProxy, self).ingredient_set
qs.model = IngredientDisplayProxy
return qs
It was that simple :'( so thank you for the help and suggestions.
I have an app that I want to simply display all the URL links a page has associated with it when that page is visited.
It's similar to reddit in that there are many userpages (aka subreddits) and each page has an infinite possible amount of submitted links associated with it. The newlinkposts records are associated with a certain page via a ForeignKey.
Given a page, wow can I get all the related newlinkpost objects (including their corresponding likes, link comment, and post date) returned, in order to display them in a template?
My newlinkpost object is defined as follows:
class newlinkpost(models.Model):
newlink_tag = models.ForeignKey('userpagename') #tags link to which userpage it belongs to
link_comment = models.CharField(max_length=128) #comment to go along with post
post_date = models.DateField(auto_now=True, auto_now_add=False, null=False) #submission datestamp. later make it track editable posts if edit function is implemented
url = models.URLField(max_length = 1024, null=False) #actual submitted link
link_likes = models.IntegerField(null=False, default=0) #need to later create like button which will +1 the value
def __unicode__(self):
return self.url
When you add a ForeignKey within a model, as well as creating an attribute in the source model (in your case, newlinkpost) allowing you to find the one associated object, Django also creates a corresponding attribute inside the target model (in your case apparently userpagename).
By default this attribute is named after the source table, so in your case it will be newlinkpost_set.
That allows you to ask the question you're looking to answer: which newlinkpost objects have this userpagename?:
all_links = userpagename_instance.newlinkpost_set.all()
If you wish to apply additional filters, you can use the filter method instead:
some_links = userpagename_instance.newlinkpost_set.filter(...)
The newlinkpost_set attribute contains a RelatedManager object, which is a subtype of Manager, allowing you to use the same set of methods you could use on newlinkpost.objects, along with some additional methods allowing you to create new related objects.
Here's an example view using this technique: (this assumes you've got the model classes imported into the views module):
from django.shortcuts import render
def user_page(request, user_id):
page = userpagename.get(pk=user_id)
links = page.newlinkpost_set.all()
return render(
request,
"myapp/user_page.html",
{
page: page,
links: links,
}
)
...and here's an example of using that "links" variable in the template:
<ul>
{% for link in links %}
<li><a href="{{ link.url }}">{{ link.link_comment }} - {{ link.link_likes }} likes</li>
{% endfor %}
</ul>
You just use the reverse relationship.
my_userpagename.newlinkpost_set.all()
https://github.com/AnthonyBRoberts/fcclincoln/blob/master/apps/story/views.py
I'm a little embarrassed to admit that this is mine. But it is.
class FrontpageView(DetailView):
template_name = "welcome_content.html"
def get_object(self):
return get_object_or_404(Article, slug="front-page")
def get_context_data(self, **kwargs):
context = super(FrontpageView, self).get_context_data(**kwargs)
context['slug'] = "front-page"
events = Article.objects.filter(slug="events")
context['events'] = events
return context
So this is a pretty normal class-based detail view in Django.
It's assigning a template, getting an Article object, and adding some things to the context_data.
Then I copied this class 17 times. Each time, there's a different template, and a different slug, and different stuff added to the context_data.
The idea is that there's a WYSIWYG editor for administrators to change the web content, and a user authentication system, to allow multiple people access to the site content. Basically, a super-simple CMS, so no one has to edit html to update the site.
But I really wish I could refactor this so I don't have these nearly identical 18 classes. Any suggestions on where I should start on this would be most welcome.
Squash all of your classes down to a single class that inherits from TemplateResponseMixin, as DetailView does, (also check out the SingleObjectTemplateResponseMixin) and override its get_template_names() method to return the template appropriate for the current situation.
A beautiful example of this being used is in the django-blog-zinnia project
def get_template_names(self):
"""
Return a list of template names to be used for the view.
"""
model_type = self.get_model_type()
model_name = self.get_model_name()
templates = [
'zinnia/%s/%s/entry_list.html' % (model_type, model_name),
'zinnia/%s/%s_entry_list.html' % (model_type, model_name),
'zinnia/%s/entry_list.html' % model_type,
'zinnia/entry_list.html']
if self.template_name is not None:
templates.insert(0, self.template_name)
return templates
Django will take that list of names and try each item to see if it exists in the templates folder. If it does, that template is used.
Update
After looking at your code a little more closely, perhaps something like this:
In your main urls.py
# convert each url
url(r'^$', FrontpageView.as_view()),
url(r'^history/$', HistoryView.as_view()),
url(r'^calendar/$', CalendarView.as_view()),
url(r'^news/$', NewsView.as_view()),
url(r'^visitors/$', VisitorsView.as_view()),
...
# to just
url(r'^(?P<slug>[\w\d/-]+)/$', SuperSpecialAwesomeView.as_view()),
# but, put this at the end of urls list after any routes that don't use this view
DetailView, after setting the class attribute model, will check to see if slug is in the url's kwargs and if it is, it will use the slug to do a model lookup just like what you are already doing: Article.ojects.get(slug=self.kwargs['slug'])
models.py
You could add a type field to your Article model. The type will specify what type of article it is. For example, your ChildrenView, YouthView, and AdultView could all have a type of music (since the templates are all music, I'm assuming that's how they are related).
ARTICLE_TYPE_CHOICES = (
(0, 'music'),
(1, 'weddings'),
(2, 'outreach'),
...
)
class Article(models.Model):
...
type = models.IntegerField(choices=ARTICLE_TYPE_CHOICES)
...
Then, in your views.py
class SuperSpecialAwesomeView(DetailView):
template_name = None
model = Article
def get_template_names(self):
slug = self.kwargs.get('slug', '')
templates = [
# create a template based on just the slug
'{0}.html'.format(slug),
# create a template based on the model's type
'{0}.html'.format(self.object.get_type_display()),
]
# Allow for template_name overrides in subclasses
if self.template_name is not None:
templates.insert(0, self.template_name)
return templates
Given an article instance with a type of music and a slug of ministry/children, Django will look for a template named ministry/children.html and a template named music.html.
And if you need to do some special stuff for other views (like you will probably need to for SermonsView), then subclass SuperSpecialAwesomeView
class SermonsView(SuperSpecialAwesomeView):
paginate_by = 2
queryset = Article.objects.order_by('-publish_date')
A quick approach I would think:
Add a template field in the model with a list of predefined template choices (those can be created dynamically).
Override the default DetailView methods, override the get_template_names method to assign the proper template to the view (if not available fallback, that can be done through a try: except:).
Apart from that you can alter the View behaviour with any kind of model flags.
This way you can have a single entry point for a model, rather than defining repeatable views all over the place.
I tend to keep a FrontPageView independent from other views though, for easiness and because it serves a different purpose.
If you need repeatable context entries, consider a context processor, if you need repeatable context entries for specific views consider Mixins.
Rarely I can find a places I need to use CBD.
You can refactor it like this:
def editable_page(slug):
return {
'context': {
'slug': slug
}
'template': 'mysupertemplates/{0}.html'.format(slug)
}
def frontpage(req):
return editable_page('frontpage')
def chat(req):
return editable_page('char')
def about(req):
return editable_page('about')
I am using Django Haystack for search.
I only want to target the title field of my model when searching for results.
At present however, it returns results if the search term is in any of the fields in my model.
For example: searching xyz gives results where xyz is in the bio field.
This should not happen, I only want to return results where xyz is in the title field. Totally ignoring all other fields other than Artist.title for searching on.
artists/models.py :
class Artist(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=100)
strapline = models.CharField(max_length=255)
image = models.ImageField(upload_to=get_file_path, storage=s3, max_length=500)
bio = models.TextField()
artists/search_indexes.py
from haystack import indexes
from app.artists.models import Artist
class ArtistIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True, model_attr='title')
def get_model(self):
return Artist
I guess thinking of it like a SQL query:
SELECT * FROM artists WHERE title LIKE '%{search_term}%'
UPDATE
Following suggestion to remove use_template=True, my search_indexes.py now looks like:
from haystack import indexes
from app.artists.models import Artist
class ArtistIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, model_attr='title')
title = indexes.CharField(model_attr='title')
def get_model(self):
return Artist
But I am having the same problem. (Have tried python manage.py rebuild_index)
This is my Haystack settings if that makes any difference:
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.simple_backend.SimpleEngine',
},
}
model_attr and use_template don't work together. In this case, as you're querying for a single model attribute there's no need to use a template. Templates in search indexes are purely meant to group data.
Thus, you end up with:
class ArtistIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, model_attr='title')
def get_model(self):
return Artist
If you don't have any other use case for your index (ie searches that should match terms elsewhere) you just have to not use_template at all (set the use_template param to False and just ditch your search template) and you'll be done. FWIW note that when passing True for use_template the model_attr param is ignored. Also, you may not have a use for a full text search engine then, you could possibly just use Django's standard QuerySet lookup API, ie Artist.objects.filter(title__icontains=searchterm).
Else - if you still need a 'full' document index for other searches and only want to restrict this one search to the title you can as well add another index.CharField (with document=False, model_attr='title') for the title and only search on this field. How to do so is fully documented in Haystack's SearchQuerySet API doc.
From the Docs
Additionally, we’re providing use_template=True on the text field. This allows us to use a data template (rather than error prone concatenation) to build the document the search engine will use in searching. You’ll need to create a new template inside your template directory called search/indexes/myapp/note_text.txt and place the following inside:
{{ object.title }}
{{ object.user.get_full_name }}
{{ object.body }}
So I guess in this template you can declare which fields should be indexed/ searched upon
Other way is to override the def prepare(self, object) of Index class and explicitly define fields that need to be indexed/ searched upon.
OR just use model_attr
Basically your search_indexes.py file is written wrong. It should be like:-
from haystack import indexes
from app.artists.models import Artist
class ArtistIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
title= indexes.CharField(model_attr='title',null=True)
def get_model(self):
return Artist
def index_queryset(self, using=None):
return self.get_model().objects.all()
Then you have to create a template in your app. The directory structure would be like:
templates/search/indexes/artists/artist_text.txt
and add the following code to the artist_text.txt file:
{{ object.title }}
Now do python manage.py rebuild_index.
Now It will return result only for title.