Django custom admin action for FeinCMS actions column - python

I'm making an admin panel for a Django-Mptt tree structure using the FeinCMS TreeEditor interface. This interface provides an 'actions column' per-node for things like adding or moving nodes quickly without using the typical Django admin action select box.
What I am trying to do is add a custom admin action to this collection which passes the pk of the node to a celery task which will then add a collection of nodes as children. Existing functions are simply href links to the URL for that task(add/delete/move), so thus far I have simply mimicked this.
My solution currently involves:
Define the action as a function on the model
Create a view which uses this function and redirects back to the changelist
Add this view to the admin URLs
Super the TreeEditor actions column into the ModelAdmin class
Add an action to the collection which calls this URL
Surely there must be a better method than this? It works, but it feels massively convoluted and un-DRY, and I'm sure it'll break in odd ways.
Unfortunately I'm only a month or two into working with Django so there's probably some obvious functions I could be using. I suspect that I might be able to do something with get_urls() and defining the function directly in the ModelAdmin, or use a codeblock within the injected HTML to call the function directly, though I'm not sure how and whether it's considered a better option.
Code:
I've renamed everything to a simpler library <> books example to remove the unrelated functionality from the above example image.
models.py
class Library(models.Model):
def get_books(self):
# Celery task; file omitted for brevity
get_books_in_library.delay(self.pk)
views.py
def get_books_in_library(request, library_id):
this_library = Library.objects.get(pk=library_id)
this_library.get_books_in_library()
messages.add_message(request, messages.SUCCESS, 'Library "{0}" books requested.'.format(this_library.name))
redirect_url = urlresolvers.reverse('admin:myapp_library_changelist')
return HttpResponseRedirect(redirect_url)
urls.py
urlpatterns = [
url(r'^admin/myapp/library/(?P<library_id>[0-9]+)/get_books/$', get_books_in_library, name='get books in library'),
url(r'^admin/', include(admin.site.urls)),
]
admin.py
class LibraryAdmin(TreeEditor):
model = Library
def _actions_column(self, obj):
actions = super(LibraryAdmin, self)._actions_column(obj)
actions.insert(
0, u'<a title="{0}" href="{1}/get_books"><img src="{2}admin/img/icon_addlink.gif" alt="{0}" /></a>'.format(
_('Get Books'),
obj.pk,
settings.STATIC_URL
)
)
return actions
Note that I may have broken something in renaming things and removing the extraneous cruft if you try to execute this code, I think it should adequately illustrate what I'm trying to do here however.

After digging around today and simply trying various other solutions, I've put together one that uses get_urls and a view defined directly into the admin interface which feels tidier though it's effectively just moving the code from multiple django files into the admin interface - though it does make use of the admin wrapper to stop unauthenticated users, which is an improvement.
I'll leave a copy of the working code here for anyone who finds this in future, as I've seen very few examples of TreeEditor et al. being used in newer versions of Django.
class NodeAdmin(TreeEditor):
model = Node
# < ... > Other details removed for brevity
def get_urls(self):
urls = super(NodeAdmin, self).get_urls()
my_urls = [
url(r'^(?P<node_id>[0-9]+)/get_suggestions/$', self.admin_site.admin_view(self.get_suggestions)),
]
return my_urls + urls
def get_suggestions(self, request, node_id):
this_node = Node.objects.get(pk=node_id)
get_suggestions(this_node.pk)
messages.add_message(request, messages.SUCCESS, 'Requested suggestions for {0}'.format(this_node.term))
redirect_url = urlresolvers.reverse('admin:trinket_node_changelist')
return HttpResponseRedirect(redirect_url)
def _actions_column(self, obj):
actions = super(NodeAdmin, self)._actions_column(obj)
# Adds an 'get suggestions' action to the Node editor using a search icon
actions.insert(
0, u'<a title="{0}" href="{1}/get_suggestions"><img src="{2}admin/img/selector-search.gif" alt="{0}" /></a>'.format(
_('Get Suggestions'),
obj.pk,
settings.STATIC_URL,
)
)
# Adds an 'add child' action to the Node editor using a plus icon
actions.insert(
0, u'<a title="{0}" href="add/?{1}={2}"><img src="{3}admin/img/icon_addlink.gif" alt="{0}" /></a>'.format(
_('Add child'),
getattr(self.model._meta,'parent_attr', 'parent'),
obj.pk,
settings.STATIC_URL
)
)
return actions

Related

Wagtail filter page-childs-elements on the basis of logged-in user's permissions

I am working on a small site using Wagtail. This site is all about a "mainpage" and several "subpages". So far it is pretty simple! But, depending on what group the user (not admin) is in, the right subpages should show up!
See the following setup (minimized), to get an idea of what I am talking about.
If I set permissions on ToolKitPart (like requiring explicit user-login and group-membership), then the following is happening:
when going to the page using the fully qualified path, the user is requested to login and, in the case of insufficient rights, the user will not see the content!
when going to the ToolkitIndex-Page, all children are displayed, including the ones the user never should see, without the need to be logged in or being a member of a certain group.
class ToolkitIndex(Page):
def get_context(self, request):
# Update context to include only published posts, ordered by reverse-chron
context = super().get_context(request)
blogpages = self.get_children().live().order_by('-first_published_at')
context['pages'] = blogpages
return context
class ToolkitPart(Page):
body = StreamField([
('staff', MpStaff()),
('news', MpNews()),
('stuff', MpStuff()),
('teditor', blocks.RichTextBlock()),
('reditor', blocks.RawHTMLBlock()),
], blank=True)
content_panels = Page.content_panels + [
StreamFieldPanel('body'),
]
class MpNews(blocks.StructBlock):
head = blocks.TextBlock(required=True, help_text='Schlagzeile')
lead = blocks.TextBlock(required=False, help_text='Einleitung')
body = blocks.RichTextBlock(required=True, help_text='Inhalt')
image = ImageChooserBlock(required=False)
type = blocks.ChoiceBlock(
choices=[('default', 'Standard'),
('highlight', 'Hervorgehoben'),
], required=True)
class Meta:
template = 'home/mparts/toolkit_news.html'
icon = 'code'
Any idea how to solve this?
Assuming you've set these permissions up using Wagtail's private pages feature, these are stored in the PageViewRestriction model. Unfortunately Wagtail doesn't currently provide a way to apply these permission checks against anything other than the current page request, so you'd have to recreate this logic yourself to filter a queryset to the user's view permissions. This would be something like (untested):
from django.db.models import Q
class ToolkitIndex(Page):
def get_context(self, request):
context = super().get_context(request)
blogpages = self.get_children().live().order_by('-first_published_at')
if not request.user.is_authenticated:
blogpages = blogpages.public() # only pages with no view restrictions at all
else:
blogpages = blogpages.filter(
# pages with no view restrictions
Q(view_restrictions__isnull=True)
# pages restricted to any logged-in user
| Q(view_restrictions__restriction_type='login')
# pages restricted by group
| Q(view_restrictions__restriction_type='groups', view_restrictions__groups__in=request.user.groups.all())
)
Disclaimers:
This doesn't account for pages that are protected by a shared password
To be fully correct, we'd need to account for the fact that view restrictions propagate down the tree (and so a subpage may still be restricted even if it doesn't have a view restriction record directly attached to it); however, we're only looking at immediate children of the current page (which they evidently do have access to...) so that issue doesn't come up here.
PageViewRestriction is not a public Wagtail API and may change in future releases - in particular, see RFC 32 for a proposed change that may happen in the fairly near future.

Overwrite django view with custom context (Django 1.11, Viewflow)

I have a Django 1.11 project using Viewflow - https://github.com/viewflow/viewflow - that I've incorporated. It's been very helpful, but a lot of stuff is kind of "magic", and being my first serious Django project, I'm running into an issue I'm not sure of how to solve, or the best way.
I have a generic template that expects a lot of context. I have a function that adds this context to all of my views:
def add_general_context(context, MOC, MOC_enabled_fields = (), MOC_status = None):
context['MOC'] = MOC
context['current_date'] = timezone.now().strftime("%D")
context['MOC_form'] = forms.MOCForm(prefix="MOC_form", MOC_enabled_fields=MOC_enabled_fields, instance=MOC)
context['MOCAttachments'] = models.MOCAttachment.objects.filter(MOC=MOC)
context['MOCAttachment_form'] = forms.MOCAttachmentForm(prefix="MOCAttachment_form")
context['MOCApprovals'] = models.MOCApproval.objects.filter(MOC=MOC)
context['MOCTasks'] = models.MOCTask.objects.filter(MOC=MOC)
context['MOC_status'] = MOC_status
context['MOCConversation'] = models.MOCConversation.objects.filter(MOC=MOC)
# Add comments to the conversation
for conversation in context['MOCConversation']:
conversation.comments = models.MOCComment.objects.filter(conversation=conversation)
context['MOCComment_form'] = forms.MOCCommentForm(MOC=MOC)
context['MOCCommentReply_form'] = forms.MOCCommentReplyForm()
I basically need to add this context to a view that is inside viewflow - namely, AssignTaskView - https://github.com/viewflow/viewflow/blob/f50accb3cde5d53f1d4db0debf5936867712c3bd/viewflow/flow/views/task.py#L109
I've tried a few things to overwrite/add to the context, but none seem to work.
Attempt 1: Overwrite the URL and use extra_context (SO suggested this)
- The issue is that the urls are "magic", my urlpatterns is very simply:
from material.frontend import urls as frontend_urls
urlpatterns = [
url(r'^MOC/', include('MOC.urls')),
url(r'', include(frontend_urls)),
]
Overwriting the urls themselves was WAY over my head, I dug into it for a while, but it uses really generic functions to pull in modules, etc. I didn't have a clue how to really even attempt it.
Attempt 2: Overwrite the view itself and the get_context_data function
I think this would be possible, but it just doesn't seem to work. My attempts looked similar to this (the lastest one):
from viewflow.flow.views.task import AssignTaskView as CoreAssignTaskView
class AssignTaskView(CoreAssignTaskView):
def get_context_data(self, **kwargs):
context = super(AssignTaskView, self).get_context_data(**kwargs)
print("Did it work?")
return context
This is in my views.py - however, it simply doesn't run. I'm probably missing something, but I can't figure out how to actually force it to use my view instead of the one built in to viewflow.
I've successfully overwritten Viewflow's templates without issue, but overwriting anything else is beyond me. Any suggestions?
Yes you can actually override a view url by putting it on top of url_patterns
urlpatterns = [
url(
r'^/workflow/appname/flowname/(?P<process_pk>\d+)/taskname/(?P<task_pk>\d+)/assign/$',
YouCustomView.as_view(),
{'flow_task': FlowClass.taskname},
name="{}__assign".format(self.name)))
),
url(r'', include(frontend_urls)),
]
But it's simpler just to create a custom flow.View subclass and set you own Assign View
https://github.com/viewflow/viewflow/blob/master/viewflow/flow/nodes.py#L306
from viewflow import flow
class MyView(flow.View):
assign_view_class = MyAssignTaskView
flows.py:
class MyFlow(Flow):
...
taskname = MyView(UpdateProcessView).next(this.end)
That's how you can override any of the built-in views.
Viewflow designed to provide all knobs within your codebase. You can customize any behavior by subclassing Flow or flow Node classes.
The custom node example could be helpful
https://github.com/viewflow/viewflow/blob/master/demo/customnode/nodes.py

How to set visible all methods for non-authenticated users in Django build-in documentation?

When I open built-in documentation in Django there aren't visible any methods. When I authenticate as a user or admin they become visible. How can I make it available without logging?
Solution
I deleted from settings
# 'DEFAULT_PERMISSION_CLASSES': (
# 'rest_framework.permissions.IsAuthenticated',
# ),
and then I created own permissions and added decorators to generic views. Everything is working properly now.
You can simply create a view that is mapped to a url which is hit whenever Authentication option is set to None(or changed); using javascript; in the left panel.
class GetPermission(permissions.BasePermission):
def has_permission(self, request, view):
if request.user.is_authenticated():
#will only get called for authenticated users.
if request.GET.get("is_authentication_set_to_null"):
request.visible = True
else:
request.visible = False
return True
Now you can simply use the variable request.visible inside the views or the function of a generic view which has the request object.
In any view:
#permission_classes([GetPermission])
def someview(request):
if request.visible = True:
#Show those objects that you want to.
#For example, Update [Path Parameters, Request Body]
else:
#Show the restricted/less things you want to show.
#Show options like: Install the command line client
Does this help you?

Django two-factor authentication, require 2FA on specific views

I am implementing Django two-factor-auth on my website and I would love to have some views protected by two-FA, and some other not.
In order to do so, I use the decorator #otp_required which works great, but unfortunately asks the users to input their credentials again (to handle user sessions, I use the registration module).
Would you be able to give me a good to way to hack the form in order to just ask the user to input the token (skipping a step of the form, basically) ?
Thanks a lot,
For those who care, I found a way to do it that is quite clean.
The trick was to override the LoginView class in the core.py module of the two_factor_authentication module.
In order to do so, go to your views and insert the following code:
class CustomLoginView(LoginView):
form_list = (
('token', AuthenticationTokenForm),
('backup', BackupTokenForm),
)
def get_user(self):
self.request.user.backend = 'django.contrib.auth.backends.ModelBackend'
return self.request.user
Basically, I erase the 'auth' step and override the method get_user() in order to return the current user.
The backend must be specified otherwise Django raises an error.
Now, to make that class be used instead of the LoginView, go to your urls and insert the following line BEFORE including the two_factor.urls.
url(r'^account/login/$', tradingviews.CustomLoginView.as_view(), name='login'),
That's it!

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

Categories

Resources