Adding a button to Wagtail Dashboard - django

Is it possible to add a additional button(buttons) on the top panel as shown in the picture?
I did't find anything in Google and here.

It is not clear on the screenshot whether you use modeladmin or the snippets to expose this model to the user but I'll assume the former.
I don't know about a hook which allows you to add buttons directly to the header, however the template uses blocks which should allow us to overwrite just this part.
We can take advantage of the resolution order of the templates and create templates/modeladmin/app-name/model-name/index.html which will take precedence over /modeladmin/index.html. So given with your app called feedler and a model called Entry, create /modeladmin/feedler/entry/index.html with the following content:
{% extends "modeladmin/index.html" %}
{% block header_extra %}
My New Button
{{ block.super }}{% comment %}Display the original buttons {% endcomment %}
{% endblock %}
Right now your button doesn't do much. To create an action which will interact with that model admin, you'll need to create some button/url/permission helpers and a view.
Let's say the action is exporting the objects to a CSV file. Brace yourself, there's quite a bit of code ahead.
In /feedler/admin.py, create the button/url/permission helpers and view:
from django.contrib.auth.decorators import login_required
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _
from wagtail.contrib.modeladmin.helpers import AdminURLHelper, ButtonHelper
from wagtail.contrib.modeladmin.views import IndexView
class ExportButtonHelper(ButtonHelper):
"""
This helper constructs all the necessary attributes to create a button.
There is a lot of boilerplate just for the classnames to be right :(
"""
export_button_classnames = ['icon', 'icon-download']
def export_button(self, classnames_add=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
classnames_exclude = []
classnames = self.export_button_classnames + classnames_add
cn = self.finalise_classname(classnames, classnames_exclude)
text = _('Export {}'.format(self.verbose_name_plural.title()))
return {
'url': self.url_helper.get_action_url('export', query_params=self.request.GET),
'label': text,
'classname': cn,
'title': text,
}
class ExportAdminURLHelper(AdminURLHelper):
"""
This helper constructs the different urls.
This is mostly just to overwrite the default behaviour
which consider any action other than 'create', 'choose_parent' and 'index'
as `object specific` and will try to add the object PK to the url
which is not what we want for the `export` option.
In addition, it appends the filters to the action.
"""
non_object_specific_actions = ('create', 'choose_parent', 'index', 'export')
def get_action_url(self, action, *args, **kwargs):
query_params = kwargs.pop('query_params', None)
url_name = self.get_action_url_name(action)
if action in self.non_object_specific_actions:
url = reverse(url_name)
else:
url = reverse(url_name, args=args, kwargs=kwargs)
if query_params:
url += '?{params}'.format(params=query_params.urlencode())
return url
def get_action_url_pattern(self, action):
if action in self.non_object_specific_actions:
return self._get_action_url_pattern(action)
return self._get_object_specific_action_url_pattern(action)
class ExportView(IndexView):
"""
A Class Based View which will generate
"""
def export_csv(self):
data = self.queryset.all()
response = ...
return response
#method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
super().dispatch(request, *args, **kwargs)
return self.export_csv()
class ExportModelAdminMixin(object):
"""
A mixin to add to your model admin which hooks the different helpers, the view
and register the new urls.
"""
button_helper_class = ExportButtonHelper
url_helper_class = ExportAdminURLHelper
export_view_class = ExportView
def get_admin_urls_for_registration(self):
urls = super().get_admin_urls_for_registration()
urls += (
url(
self.url_helper.get_action_url_pattern('export'),
self.export_view,
name=self.url_helper.get_action_url_name('export')
),
)
return urls
def export_view(self, request):
kwargs = {'model_admin': self}
view_class = self.export_view_class
return view_class.as_view(**kwargs)(request)
In /feedler/wagtail_hooks.py, create and register the ModelAdmin:
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from .admin import ExportModelAdminMixin
from .models import Entry
class EntryModelAdmin(ExportModelAdminMixin, ModelAdmin):
model = Entry
# ...
modeladmin_register(EntryModelAdmin)
With all that setup, you should be able to use {% include 'modeladmin/includes/button.html' with button=view.button_helper.export_button %} in the template created above.

Watch out, if you are listing Pages you will need to subclass from PageAdminURLHelper, not from AdminURLHelper (FilterableAdminURLHelper doesn't seem to be a standard implementation).
Otherwise, you will have some strange redirections taking place.

Related

django : url change depends on model exist

I'd like to make user access to page depends on model exist. and I use CBV.
Do I have to control views? or urls?
Is FBV only way to control url?
How can I control user access url? hope kindly help me.
i'd like to control for example:(as you know, this is invalid syntax. hope you know what i'm saying.)
from django.urls import path
from . import views, models
app_name = "papers"
urlpatterns = [
path(
"memberagreement/<int:preassociation_pk>/",
{% if models.MemberAgreement.association.get(pk=preassociaion_pk) is not NULL %}
views.member_agreement_detail_vc.as_view(),
{% else %}
views.member_agreement_create_vc.as_view(),
{% endif %}
name="member_agreement_vc",
)
]
I add my views.py:(it works when models is exist, but if model does not exist, i can't load my template...)
def member_agreement_vc(request, preassociation_pk):
preassociation = preassociation_models.Preassociation.objects.get(
pk=preassociation_pk
)
try:
member_agreement = models.MemberAgreement.objects.get(pk=1)
return render(
request,
"papers/member_agreement/detail.html",
{"member_agreement": member_agreement},
)
except models.MemberAgreement.DoesNotExist:
form_class = forms.CreateMemberAgreementFormVC
template_name = "papers/member_agreement/create.html"
def form_valid(self, form):
pk = self.kwargs.get("preassociation_pk")
member_agreement = form.save()
# content
# association
# writer
# participants
# category
# is_business
# is_general
# number_of_investment_account
# name
# resident_registration_number
# address
# contact
member_agreement.writer = self.request.user
member_agreement.association = preassociation_models.Preassociation.objects.get(
pk=pk
)
member_agreement.category = "member_agreement"
member_agreement.is_business = True
member_agreement.is_general = False
member_agreement.save()
form.save()
return redirect(
reverse(
"preassociations:paper",
kwargs={"pk": member_agreement.association.pk},
)
)
What you are asking, I think is not possible. URL is just a path, or a way you will do nothing in there except walking. Just like you are new to a road, you will just walk, then when you have to decide where to go, either you will ask google to do so, or the nearby person. So, road is like url and google map is like views where you will decide where to go.
I am going to try it the views ways then,
// urls.py
// Add your path
// views.py
from .models import YourModel // import model
def decideWhereToGo(request):
modelExist = YourModel.objects.filter(someField=someValue).exists()
if modelExist:
// Do sthg
else:
// redirect to the url where you want to send if model does not exists

Django : rendering a TemplateView in another

Using Django, I have multiple template files (A, B and C) that could be rendered in the same TemplateView called GenericView.
A, B and C uses the same View (let's call it DynamicView), therefore I need to call the rendering method of this DynamicView from GenericView's get_context_data`.
Is there a way I can render easily DynamicView's template within GenericView's template?
EDIT : I am using class based view coding
EDIT : Added some code to make my question clearer :
Here is my GenericView :
class GenericView(DetailView):
model = SimpleModel
template_name = "template.html"
def get_context_data(self, **kwargs):
context = super(GenericView, self).get_context_data(**kwargs)
tests = DynamicModel.objects.filter(test=context['object'].pk)
context['tests'] = tests
print("tests : ", tests[0]) # each test contains a field called "template_path", I would like to instanciate a DynamicView so that I can include the rendered page in context
return context
class DynamicView(TemplateView):
template_name = "dummy.html"
model = DynamicModel
def render_to_response(self, context, **kwargs):
absolute_path = get_object_or_404(DynamicModel, pk=self.kwargs['pk'])
page = render(self.request, absolute_path, context, content_type=None, status=None, using=None) # here the page is rendered
You could override DynamicView's get_template_names method.
Before doing that though, what is being considered in making the decision on what template to render? It might be better to make 3 different views, use inheritance to let them share code and not rework any of that view routing.
Yes, it can be done with the Django template tag. You need to call render function inside of this tag and put it inside of needed template.
EDIT after code updates.
from django import template
from django.shortcuts import get_object_or_404
from myapp.models import DynamicModel
register = template.Library()
#register.inclusion_tag('dummy.html', takes_context=True)
def render_dynamic_model(context, **kwargs):
absolute_path = get_object_or_404(DynamicModel, pk=kwargs['pk'])
return {
'absolute_path': absolute_path,
}
And put it into template template.html
{% render_dynamic_model pk %}
This code like a general idea, if you need some variables from request, you can pass it in the template and update template tag code respectively.
{% render_dynamic_model pk request.user %}

Django Admin Actions on single object

The admin actions seem to work on several items selected in the list view of django admin interface:
In my case I would like to have a simple action button on the change (one item) view.
Is there a way to make the django admin actions available there?
I know that I can walk around this problem by going to the list view, and select one item there. But it would be more nice to have it directly available.
Create a template for your model in your app.
templates/admin/<yourapp>/<yourmodel>/change_form.html
With this example content to add a button when changing an existing object.
{% extends "admin/change_form.html" %}
{% block submit_buttons_bottom %}
{{ block.super }}
{% if original %} {# Only show if changing #}
<div class="submit-row">
<a href="{% url 'custom-model-action' original.pk %}">
Another action
</a>
</div>
{% endif %}
{% endblock %}
Link that action to any url and redirect back to your model change object view. More information about extending admin templates.
Update: Added complete common use case for custom action on existing object
urls.py
urlpatterns = [
url(r'^custom_model_action/(?P<object_pk>\d+)/$',
core_views.custom_model_action, name='custom-model-action')
]
views.py
from django.urls import reverse
from django.contrib import messages
from django.http import HttpResponse, HttpResponseRedirect
def custom_model_action(request, object_pk):
messages.info(request, 'Performed custom action!')
return HttpResponseRedirect(
reverse('admin:<yourapp>_<yourmodel>_change', args=[object_pk])
)
If you realy need per-single object, I suggest you to use this solution, eg:
class Gallery(TimeStampedModel):
title = models.CharField(max_length=200)
attachment = models.FileField(upload_to='gallery/attachment/%Y/%m/%d')
def __str__(self):
return self.title
def process_button(self):
return ('<button id="%(id)s class="btn btn-default process_btn" '
'data-value="%(value)s>Process</button>' % {'id': self.pk, 'value': self.attachment.url})
process_button.short_description = 'Action'
process_button.allow_tags = True
In your admin.py, insert process_button into list_display;
class GalleryAdmin(admin.ModelAdmin):
list_display = ['title', 'process_button', 'created']
search_fields = ['title', 'pk']
....
class Media:
js = ('path/to/yourfile.js', )
Then, inside yourfile.js, you can also process it..
$('.process_btn').click(function(){
var id = $(this).attr('id'); // single object id
var value = $(this).data('value'); // single object value
...
});
Hope it helpful..
Not the same as the topic starter asked, but this snippet allows to have Single Object action from on the list page with minimum amount of code
BaseAction code
class AdminActionError(Exception):
pass
class AdminObjectAction:
"""Base class for Django Admin actions for single object"""
short_description = None
exp_obj_state = {}
def __init__(self, modeladmin, request, queryset):
self.admin = modeladmin
self.request = request
self.queryset = queryset
self.__call__()
def validate_qs(self):
count = self.queryset.count()
if count != 1:
self.error("You must select one object for this action.")
if self.exp_obj_state:
if self.queryset.filter(**self.exp_obj_state).count() != 1:
self.error(f'Selected object does not meet the requirements: {self.exp_obj_state}')
def error(self, msg):
raise AdminActionError(msg)
def get_object(self):
return self.queryset.get()
def process_object_action(self, obj):
pass
def validate_obj(self, obj):
pass
def __call__(self, *args, **kwargs):
try:
self.validate_qs()
obj = self.get_object()
self.validate_obj(obj)
except AdminActionError as e:
self.admin.message_user(self.request, f"Failed: {e}", level=messages.ERROR)
else:
with transaction.atomic():
result = self.process_object_action(obj)
self.admin.message_user(self.request, f"Success: {self.short_description}, {result}")
Custom Action [minimum amount of code]
class RenewSubscriptionAction(AdminObjectAction):
short_description = 'Renew subscription'
exp_obj_state = {
'child': None,
'active_status': True,
}
def process_object_action(self, obj):
manager = RenewManager(user=obj.user, subscription=obj)
return manager.process()
AdminClass
class SomeAdmin(admin.ModelAdmin):
actions = [RenewSubscriptionAction]
The built-in admin actions operate on a queryset.
You can use a calable for the action you whant or to show something else:
class ProductAdmin(admin.ModelAdmin):
list_display ('name' )
readonly_fields('detail_url)
def detail_url(self, instance):
url = reverse('product_detail', kwargs={'pk': instance.slug})
response = format_html("""{0}""", product_detail)
return response
or using forms
class ProductForm(forms.Form):
name = forms.Charfield()
def form_action(self, product, user):
return Product.value(
id=product.pk,
user= user,
.....
)
#admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
# render buttons and links to
def product_actions(self, obj):
return format_html(
'<a class="button" href="{}">Action1</a> '
'<a class="button" href="{}">Action 2</a>',
reverse('admin:product-action-1', args=[obj.pk]),
reverse('admin:aproduct-action-3', args=[obj.pk]),
)
for more details about using forms

Can Function based views be reused in several apps without duplicating the code?

I have bunch of function based views that have similar functionality
For example create, list, edit view and search companies and contacts of: Customers , Vendors and Manufactures.
I can reuse the same model with few boolean flags for all 3 of them.
But I was wondering how to deal with view and templates in a best way to avoid code duplications. (currently I am thinking but massive usage of control-H but it doesn't feel right)
(I regret now not using the Class based views but it is too late)
There is a lot you can do by passing in url parameters to achieve reuse. Your imagination and consistency will help a lot here:
For a simple example:
urlpatterns = [
url(r'^add-customer/$', create, {'template':'customer.html'}, name='create_customer'),
url(r'^add-vendor/$', create, {'template':'vendor.html'}, name='create_vendor'),
url(r'^add-manufacturer/$', create, {'template':'manufacturer.html'}, name='create_manufacturer'),
...
and the view:
def create(request, template):
#use template as you like, coming from url conf.
return render(request, template)
...
That's probably normal code when you think about it. A little more interesting example:
from someapp.forms import *
from someapp.models import *
urlpatterns = [
url(r'^customer/$', create, {'template':'customer.html', 'form_class':customer_form}, name='create_customer'),
url(r'^vendor/$', create, {'template':'vendor.html', 'form_class':vendor_form}, name='create_vendor'),
url(r'^manufacturer/$', create, {'template':'manufacturer.html', 'form_class':manufacturer_form}, name='create_manufacturer'),
...
and the view:
def create(request, template, form_class):
#use template as you like, coming from url conf.
form = form_class()#
return render(request, template)
...
Still pretty much normal code. You can get funky and generate the urls dynamically:
In your urls.py:
for form_class in (CustomerForm, VendorForm, ManufacturerForm): #model forms
model = form_class._meta.model
model_name = model._meta.module_name
urlpatterns += url(r'^create/$', create, {'template':'%s.html' % model_name, 'form_class':form_class, 'model':model}, name='create_%s' % model_name),
and in the view:
def create(request, template, form_class, model):
#use template as you like, coming from url conf.
form = form_class()#
return render(request, template)
You can pass in pks to models and write codes like:
def create(request, form_class, model, id=None):
instance = get_object_or_404(model, pk=id) if id else None
edited = True if instance else False
if request.method == 'POST':
form = form_class(data=request.POST, files=request.FILES, instance=instance)
if form.is_valid():
instance = form.save()
...
else:
form = form_class(instance=instance)
return render(request, template_name, {'form': form, 'instance': instance})
Hopefully you have the stomach for it.
The answer, of course, depends on what actually is different between the models. But if you can abstract your view functions "with a few boolean flags" I would suggest to put them in a central place, probably a common file in your app or project, e.g. abstract_views.py.
Then you will only need to dispatch the view functions in a meaningful way:
import my_app.abstract_views
from .models import Model1, Model2
def model1_create_view(request):
abstract_views.create(request, model=Model1)
def model2_create_view(request):
abstract_views.create(request, model=Model2)
In this hypthetical example create would be the abstract view function for creating an instance and it would require a parameter model with the actual model to operate on.
ADDED:
Alternatively, use boolean flags, as requested:
import my_app.abstract_views
def model1_create_view(request):
abstract_views.create(request, is_customer=True)
def model2_create_view(request):
abstract_views.create(request, is_vendor=True)
When using boolean flags, remember to define what happens if someone is both, a customer and a vendor...
The definition of the abstract view then reads something like:
def abstract_view(request, is_customer=False, is_vendor=False):
context = do_something (is_customer, is_vendor)
return(render(request, 'app/template.html', context))
Hope that helps.

What's the best way to render multiple views in the same template?

Scenario
I need to render 2 separate views from a third party app, in the same View. The views in question are for login and signup.
The template for each view then simply includes an inclusion tag to render a generic form.
Solution
The solution i've come up with is to register a tag for each view that creates a template.Node to render each one.
from django import template
from third_party_app import LoginView, SignupView
register = template.Library()
#register.tag
def login_form(parser, token):
return ViewNode(LoginView, template_name=get_template_name(token))
#register.tag
def signup_form(parser, token):
return ViewNode(SignupView, template_name=get_template_name(token))
class ViewNode(template.Node):
def __init__(self, view_class, **kwargs):
self.view_class = view_class
self.kwargs = kwargs
def render(self, context):
request = context['request']
self.kwargs['request'] = request
view = self.view_class(**self.kwargs)
response = view.get(request)
response.render()
return response.content
def get_template_name(token):
tag_name, template = token.split_contents()
return str(template[1:-1])
And the template for the main View looks like this:
<div>
{% login_form 'account/login.html' %}
</div>
... some other html ...
<div>
{% signup_form 'account/signup.html' %}
</div>
The template for each of the individual login and signup views only contains an inclusion tag to render another template for a generic form.
So accounts/login.html is simply this:
{% render_login_form %}
and the inclusion tag looks like this
#register.inclusion_tag('account/snippets/form.html', takes_context=True)
def render_login_form(context):
return {'form': context['form'],
'primary_btn_label': 'Sign In',
'secondary_btn_label': 'Forgot Password?',
'tertiary_btn_label': 'Sign Up?',
'col_offset': '3',
'col_width': '9'}
Question
It works, but i'm wondering 2 things.
Is this the best way to render 2 Views in the same View?
It feels like there are two many steps to achieve this. Is there a simpler way to solve this problem?
I have does something similar this way:
First two templates login.html and signup.html. In these templates I access context variables like form, primary_btn_label and other stuff you have in your render_login_form inclusion tag.
But then I have a custom context processor (See the Django documentation on context processors) that initializes these variables (like form and so on)
The context processor (save in myapp/context_processors.py) looks something like this:
from .forms import LoginForm, RegisterForm
def login_register_form(request):
login_popup_form = LoginFormForPopup()
register_popup_form = RegisterFormForPopup()
context = {
'login_popup_form': login_popup_form,
'register_popup_form': register_popup_form,
}
return context
The custom context processor is enabled in your Django settings file by adding it to the TEMPLATE_CONTEXT_PROCESSORS variable:
TEMPLATE_CONTEXT_PROCESSORS = (
...
'myapp.context_processors.login_register_form',
)
Hope this helps!