Is there a way to show images in a Wagtail Model Admin Record Listing page? - wagtail

I have reviewed the question on Is there any way to show a field on a listing page in Wagtail admin? but my situation seems to similar but also different enough that that particular solution won't work for me. Instead of on the Page listing I wish to achieve a similar thing on the Model Admin listing and I would think this should be such a common requirement that I am picking that someone must have done this before I have attempted it.
I haven't really figured out how to even try anything to get started but what I have looked at is the modeladmin template tags under wagtail.contrib.modeladmin on GitHub but I am completely guessing.
Can anyone point me to which templates I need to modify and whether I need to modify any of the template tags and how to override anything I need to override?

There's no need to override templates for this - this is standard functionality in ModelAdmin. Adding extra fields to the listing is done by setting list_display on the ModelAdmin class:
class BookAdmin(ModelAdmin):
model = Book
list_display = ('title', 'author')
For displaying images, ModelAdmin provides ThumbnailMixin:
from wagtail.contrib.modeladmin.mixins import ThumbnailMixin
from wagtail.contrib.modeladmin.options import ModelAdmin
class BookAdmin(ThumbnailMixin, ModelAdmin):
model = Book
thumb_image_field_name = 'cover_image'
list_display = ('title', 'author', 'admin_thumb')
('admin_thumb' is a special-purpose field name provided by ThumbnailMixin, and should be used rather than the actual image field on your model - cover_image in this example.)

Related

Template and model reuse in Wagtail

I am building a fairly basic Wagtail site and have run into an issue regarding the reuse of models and templates.
Say my site has two kinds of entries:
blog posts and
events.
Both pages look the same and share many model fields (e.g., author, category, intro, etc.). However, there are some model fields that only make sense for the event entry type (e.g., event_date, event_venue).
What would be the ideal way of creating templates and models for this use-case without repeating myself in the code?
Right now, both blog and event entries use the same HTML template and the same model. However, when the user creates a blog post in the Wagtail admin, he or she has to "ignore" the event-specific fields (which may become even more in the future).
Do I have to create two separate template files and two separate models despite both blogs and events being 95% the same code? What would be the correct way to solve this in Wagtail?
If you want to maintain it the way it is, contained within one model and template, you could create separate model admins for each pseudo-type (Blogs and Events), and override the queryset function to make each separate modeladmin only show the ones you're looking for, and then edit the panels that are shown on create/edit/delete.
class EventAdmin(ModelAdmin):
...
panels = [
FieldPanel('your_field'),
...
]
def get_queryset(self, request):
qs = super().get_queryset(request)
events = qs.filter(your_field__isnull=False)
return events
More information at https://docs.wagtail.io/en/stable/reference/contrib/modeladmin/index.html

Is it posible to use django_select2 widgets with a Wagtail ParentalManyToManyField

I need a complex selection widget because there are a lot of options in a multiple select widget. But I see HeavySelect2MultipleWidget needs views and urls to use it. I think there is not possible it in Wagtail by default.
This is the code:
class Resource(Page):
authors = ParentalManyToManyField('Authors', blank=True)
content_panels = Page.content_panels + [
FieldPanel('authors', widget=forms.CheckboxSelectMultiple)
]
It would be nice to use
FieldPanel('authors', widget=HeavySelect2MultipleWidget)
but it raises a
You must ether specify "data_view" or "data_url".
According to the Django-Select2 documentation you have to initiate (call) the widget with the attributes first.
As the error says - data_url or data_view has not been provided to the widget.
It will be up to you to generate that view or url specific to your use case. You could override the serve method of your page model to serve the data and provide the appropriate url pattern or simply create a different view altogether as documented here (see Heavy Components section).
For example:
FieldPanel(
'authors',
widget=HeavySelect2MultipleWidget(
data_url='url/to/json/resonse'
)
)
HeavySelect2MultipleWidget extends HeavySelect2Widget - see docs for more detail:
http://django-select2.readthedocs.io/en/latest/django_select2.html#django_select2.forms.HeavySelect2Widget
I have not used this specific widget but have used Django-Select2 in Wagtail in a similar set up and it has worked well.

wagtail modeladmin: is it possible to add "explore child pages" column?

I've been using wagtail-modeladmin to create custom lists of certain page types. That gives me the ability to edit those pages. But I'd also like to be able to click through somehow to the "normal" admin explorer version of those pages, and be able to view/add child pages to them.
essentially giving myself a column with the little arrows in on the right, just like in the normal wagtail admin page explorer...
OK I know it's bad form to answer your own question, but I've got this working by using a custom method on the model admin object that reverse wagtails admin urls:
class MySpecialPageModelAdmin(ModelAdmin):
def view_children(self, obj):
url = reverse('wagtailadmin_explore', args=[obj.id])
return format_html(f'View Children')
list_display = ('title', 'live', 'view_children')
but actually, I think I'm not going to end up using this, just replacing this particular modeladmin with a direct link to the right place in the explorer.

Single piece of content, multiple URLs?

I have a use case that I could use some advice on.
We publish multiple products, each of which has it's own subtree on the site. Generally, a piece of content gets published to just a single product, e.g. a news article gets published to product A and can be accessed at one URL.
However, sometimes we have content that we want to publish to multiple products, e.g. a single news article gets published to products A, B, and C and will be available at 3 different URLs.
With our current CMS we end up doing this by copying and pasting the content, which is a hassle for editors, especially if the content needs to be updated.
An ideal scenario would be where and editor edits the content in one place, specifies the products to publish to, and the content is served by more than one URL and with a template that is product-specific.
It seems that RoutablePageMixin could be useful here, but I'm not sure how to handle letting the editor specify the destination products and making the routing aware of that choice.
Has anyone solved a similar problem using Wagtail?
I have solved a similar problem in Wagtail, the RoutablePageMixin is the key to solving this problem.
If you have /blog/A/slug-product/, /blog/B/slug-product/, /blog/C/slug-product/ , then you can get the slug value slug-product here, then use this value to search the distinct content in your db.
class BlogPage(RoutablePageMixin, Page):
def get_posts(self):
return PostPage.objects.descendant_of(self).live()
#route(r'^(\d{4})/(\d{2})/(\d{2})/(.+)/$')
def post_by_date_slug(self, request, year, month, day, slug, *args, **kwargs):
post_page = self.get_posts().filter(slug=slug).first()
return Page.serve(post_page, request, *args, **kwargs)
As you can see, I did not use the date info of the url but the slug value to get the blog post object, you can follow the pattern here to use regex to match the url you want.
If the slug values in urls are also different, this solution might not work very well, but in most cases, this solution can work fine.
I have written a blog post talking about how to use RoutablePageMixin to make the page routable, you can check this link if you wang to get more about RoutablePageMixin.
Routable Page
Rather than thinking of your news articles as being child objects of one or more products, it might help to think of them as one big pool of news articles which are categorised by product. Your product page will then effectively be a filtered index page of news articles.
Here's how I'd model it:
If you want your news articles to exist at a canonical URL that's independent of any particular category, or you want to make use of page moderation and/or previewing, then define NewsArticle as a page model; otherwise, define it as a snippet or a ModelAdmin-managed model.
On the NewsArticle model, have an InlinePanel where editors can associate as many related products as required:
class NewsArticle(Page):
body = RichTextField()
date = models.DateField()
content_panels = Page.content_panels + [
FieldPanel('body'),
FieldPanel('date'),
InlinePanel('related_products', label="Related products"),
]
class NewsArticleRelatedProduct(Orderable):
news_article = ParentalKey(NewsArticle, related_name='related_products')
product = models.ForeignKey(ProductPage, on_delete=models.CASCADE, related_name='news_articles')
panels = [
PageChooserPanel('product'),
]
On your ProductPage model, add a method that returns a queryset of news items, filtered and sorted appropriately:
class ProductPage(Page):
# ...
def get_news_articles(self):
return self.news_articles.live().order_by('-date')
You can then loop over the news articles in your product page template, using a tag like {% for news_article in page.get_news_articles %}.

Dynamically passing a model name to a CakePHP Plugin

I'm having trouble wording my problem, so it's been tough to search for an answer. Hopefully you'll know how to help.
I am creating a CakePHP 2.1 Plugin that will interact with a series of its own Models:
- Friend
- Group
- User
Friend and Group are models that are created specifically for the Plugin, and they function within the plugin normally. However, the User model is really just an alias for some other table in the parent app.
So, if "My Awesome Application" decides to use "My Awesome Plugin", it will have to have its own "users" table (though it may called something else). Let's say "My Awesome Application" has a Model called MyUser. "My Awesome Plugin" wants to dynamically tell its internal User model to $useTable = "my_users".
My question is, how do I pass that data to the Plugin? How do I configure "My Awesome Plugin" to understand that User should $useTable "my_users";
As I understand you would like a Model in a PlugIn to use a table that would typically belong to a Model in your Application - by the conventions. Have you tried statically setting:
public $useTable = "my_users";
in the plugin? All plugins usually get initialized when Cake starts up, so all configurations should be loaded then. Why do you need this - it does really restrict you a lot? Will the table being used by the Plugin model change runtime?
The Model class also has some goodies you may find useful:
$this->User->table;
holds the table name for the model - the table that is currently being used that is**.
Also you can set the source table for the Model (inside a Controller) with:
$this->User->setSource('table_name);
** I am not sure if this applies when you use Model::setSource(). It would be interesting to check out what $this->User->table; holds after a Model::setSource() call.
I've figured out a way to accomplish this, but it might not work in all scenarios for all people.
I created a Component in my Plugin, and then I call the Component in my Controller. I pass the name of the users Model through the Component. This way, I can get information about the users Model, and I can set it as the useTable to my Plugin for use in the Plugin.
Of course, this method restricts me to using the Component to utilize the Plugin, but that's probably for the best.
Here's an example of how I did it:
// in the AppController
public $components = array(
'MyPlugin.MyPluginComponent' => array('userModel'=>'UserModelName')
);
// in the Plugin's Component
class MyPluginComponent extends Component {
function initialize($controller) {
//get the base user model
$this->UserModel = ClassRegistry::init($this->settings['userModel']);
//set the useTable for our plugin's user model
$this->PluginUser = ClassRegistry::init('MyPlugin.PluginUser');
//set the useTable value for this model
$this->PleaseUser->setSource($this->UserModel->useTable);
}
That seems to work for me. Hope this helps someone else.

Resources