Wagtail: Dynamically Choosing Template With a Default - python

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)

Related

Render fields conditionally with Django-Filters

I'm working on my Django SAAS app in which I want to allow the user to have some custom settings, like disable or enable certain filters. For that I'm using django-user-setttings combined with django-filters and simple forms with boolean fields:
class PropertyFilterSetting(forms.Form):
filter_by_loans = forms.BooleanField(required=False)
filter_by_tenants = forms.BooleanField(required=False)
The issue is that when trying to apply those settings, I keep running into serious spaghetti code:
views.py
class PropertyListView(LoginRequiredMixin, FilterView):
template_name = 'app/property_list.html'
context_object_name = 'properties'
def get_filterset_class(self):
print(get_user_setting('filter_by_tenants', request=self.request))
return PropertyFilterWithoutTenant if not get_user_setting('filter_by_tenants', request=self.request)['value'] else PropertyFilter
filter.py
class PropertyFilter(django_filter.FilterSet):
...
class PropertyFilterWithoutTenant(PropertyFilter):
...
and I'd have to do the same thing with the rest of the features. Is there any better way to implement this?
You can create methods in your User model, or a new class which acts as a store for all the methods. Each method will give you the relevant filterset class based on the value of corresponding user setting.
Something like:
class UserFilterset:
def __init__(self, request):
self.request = request
def get_property_filterset(self):
if not get_user_setting('filter_by_tenants', request=self.request)['value']:
return PropertyFilterWithoutTenant
return PropertyFilter
... # add more such methods for each user setting
Now you can use this method to get the relevant filterset class
class PropertyListView(LoginRequiredMixin, FilterView):
template_name = 'app/property_list.html'
context_object_name = 'properties'
def get_filterset_class(self):
return UserFilterset(self.request).get_property_filterset()
So even if in future you want to add some more logic, you can just update the relevant method, it would be cleaner and manageable.
I'm not sure how MVT stucture will exactly respond to this one but i use a custom generic class in REST structure to add custom filter fields in any viewset that i want
class ListAPIViewWithFilter(ListAPIView):
def get_kwargs_for_filtering(self):
filtering_kwargs = {}
if self.my_filter_fields is not []:
# iterate over the filter fields
for field in self.my_filter_fields:
# get the value of a field from request query parameter
field_value = self.request.query_params.get(field)
if field_value:
filtering_kwargs[field] = field_value
return filtering_kwargs
def get_queryset(self):
queryset = super(ListAPIViewWithFilter, self).get_queryset()
filtering_kwargs = self.get_kwargs_for_filtering()
if filtering_kwargs != {}:
# filter the queryset based on 'filtering_kwargs'
queryset = queryset.filter(**filtering_kwargs)
self.pagination_class = None
else:
return queryset
return queryset[:self.filter_results_number_limit]
changing origional get_queryset function in views.py should be the key to solving your problem (it works in django rest).
try checking what function gets the data then just identify the field wanted from it.

How to limit the inline ForeignKey queryset to the instance itself in Django admin

I have 5 models and their relations are as follows:
class A(models.Model):
pass
class B(models.Model):
a = models.ForeignKey(A)
class C(models.Model):
b = models.ManyToManyField(B)
class D(models.Model):
pass
class I(models.Model):
a = models.ForeignKey(A)
b = models.ForeignKey(B)
c = models.ForeignKey(C)
d = models.ForeignKey(D)
I decide to use the django admin
class IAdminInline(admin.TabularInline):
pass
class DAdmin(admin.ModelAdmin):
inlines = [IAdminInline, ]
The admin page makes a lot queries,if too many I instances are related to D, which is time consuming. So I disable the Django default actions by setting the formfield_for_foreignkey:
def formfield_for_foreignkey(self, db_field, request, **kwargs):
field = super().formfield_for_foreignkey(db_field, request, **kwargs)
field.choices = [] # django will not make any queries if set choices
Instead, I use ajax to get the corresponding data, and use javascript to render the select widgets and bind actions , which make it easier to add data since these widgets are related to each other. Page loads faster but problem is that, the above code would clear I instances initial values that are apparently already existing in the change view page.
I want to ask how can I render the existing inline object select widgets to their own values? Does Django provide any functions to handle this?
I haven't find any solutions other than using ajax to render the apparently existing values all by myself.

Django Grapelli add autocomplete lookups on InlineAdmin

I have this 3 models:
class MyFile(models.Model):
file = models.FileField(upload_to="files/%Y/%m/%d")
def __unicode__(self):
"""."""
return "%s" % (
self.file.name)
class ExampleModel(models.Model):
attached_files =models.ManyToManyField(MyFile)
main_model = models.ForeignKey(MainModel)
class MainModel(models.Model):
attached_files =models.ManyToManyField(MyFile)
And my admin.py as follows:
class ExampleModelAdminInline(admin.TabularInline):
model = ExampleModel
extra = 2
class MainModelAdmin(admin.ModelAdmin):
inlines = [ExampleModelAdminInline]
Im using django-grapelli because it offer autocomplete lookups for many to many fields. However, Im not sure how to add this autocomplete lookup to a TabularInline admin. Can anyone explain me how to set up the attached_files field to have autocomplete lookups?
First you need to set the static method autocomplete_search_fields() in the Model you want to search from, in your case MyFile. From the docs we get:
class MyFile(models.Model):
#your variable declaration...
#staticmethod
def autocomplete_search_fields():
return ("id__iexact", "name__icontains",) #the fields you want here
You can also define GRAPPELLI_AUTOCOMPLETE_SEARCH_FIELDS instead of declaring the static method, like:
GRAPPELLI_AUTOCOMPLETE_SEARCH_FIELDS = {
"myapp": {
"MyFile": ("id__iexact", "name__icontains",)
}
}
Then you should add the lookup and raw fields to your desired admin class, considering its related Model (say, your ExampleModel) which is the one that has a ManyToManyField. You can also handle ForeignKey in a similar way. Also from the mentioned docs:
class ExampleModel(models.Model):
main_model = models.ForeignKey(MainModel) #some FK to other Model related
attached_files =models.ManyToManyField(MyFile) #the one with static decl
class MainModelAdmin(admin.ModelAdmin):
#your variable declaration...
# define raw fields
raw_id_fields = ('main_model','attached_files',)
# define the autocomplete_lookup_fields
autocomplete_lookup_fields = {
'fk': ['main_model'],
'm2m': ['attached_files'],
}
Remember to register both ends (your models) of the relationship to your admin.site, like this:
#the one with the m2m and the one with the lookup
admin.site.register(ExampleModel, MainModelAdmin)
You can also check this question to understand better.

Refactoring Django class-based views, clean up 18 repetitive classes.

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')

Django admin foreign key dropdown with custom value

I have 3 Django models:
class Test(models.Model):
pass
class Page(models.Model):
test = models.ForeignKey(Test)
class Question(model.Model):
page = models.ForeignKey(Page)
If I register the Question model to the admin, I get a dropdown with the desired Page. Now, what do I have to modify to display the desired Page plus that page's corresponding Test?
Say, if I have three pages created, the dropdown will contain these values: Page1, Page2, Page3. I would like to see: Page1 - Test1, Page2 - Test1, Page3 - Test1
2 Options.
Option 1:
Create a new field, copy forms.ModelChoiceField and override label_from_instance.
# From the source
class PageModelChoiceField(forms.ModelChoiceField():
def label_from_instance(self, obj):
"""
This method is used to convert objects into strings; it's used to
generate the labels for the choices presented by this object. Subclasses
can override this method to customize the display of the choices.
"""
# Then return what you'd like to display
return "Page{0} - Test{1}".format(obj.pk, obj.test.pk)
This will only change the text for that particular dropdown field. As you are accessing the Test object for each item in the list, you may want to ensure the queryset you pass to the PageModelChoiceField has select_related('test'), otherwise it will make a DB hit for each item on the list.
I've not tested this exact code but the logic is there. Will try it later when I can
class QuestionForm(forms.ModelForm):
page = PageModelChoiceField(
queryset=Page.objects.select_related('test').all()
)
class Meta:
model = Page
class QuestionAdmin(ModelAdmin):
class Meta:
model = Question
form = QuestionForm
Option B.
Change the unicode() representation of Page.
class Page(models.Model):
test = models.ForeignKey(Test)
def __unicode__(self):
return "Page{0} - Test{1}".format(obj.pk, obj.test.pk)
This will change how Pages are displayed everywhere you print a page object, print(page_object), {{ page_object }}.
Personally I prefer Option 1
For some reason Option B in the accepted answer didn't work for me so I figured I'd update this page with what worked well for me.
You can overload the __str__ function for the Page model to get what you're wanting. So, something along the lines of this
class Page(models.Model):
test = models.ForeignKey(Test)
def __str__(self):
return f'Page{self.pk} - Test{self.test.pk}'

Categories

Resources