execute django admin action as a celery task - django

Normal functions can be executed as django admin actions. I want to export data as csv file. Due to the size of data, I am trying to execute this as a celery task. But objects of model, request, queryset etc cannot be passed to a task.
Is there any way to execute a admin action as celery task.

To execute an admin action from a celery task or from anywhere (e.g. a management command):
from celery import shared_task
from django.contrib import admin
from django.test.client import RequestFactory
from django.contrib.auth.models import User
#shared_task
def my_task(pk_of_model):
'''
Task executes a delete_selected admin action.
'''
# the queryset is the set of objects selected from the change list
queryset = MyModel.objects.filter(pk=pk_of_model)
# we use the django request factory to create a bogus request
rf = RequestFactory()
# the post data must reflect as if a user selected the action
# below we use a 'delete' action and specify post:'post' to
# simulate the user confirmed the delete
request = rf.post(
'/admin/app/model', # url of the admin change list
{
'_selected_action': [m.pk for m in queryset],
'action': 'delete_selected',
'post': 'post',
}
)
# the request factory does not use any middlewares so we add our
# system user - some admin user all the tasks and commands run as.
request.user = User.objects.get(username='SYSTEM') # must exist
# the admin site registry holds all the ModelAdmin
# instances where our actions are declared
admin.site._registry[MyModel].delete_selected(request, queryset)
The example above will fail because the delete_selected action relies on the messages middleware and the request factory does not use any. One could wrap the final execution line in a try: ... except MessageFailure: pass but most likely you will be executing your own custom action where you can check if the message middleware is enabled.

Related

Show message in Django without needing a request

I have a view which uses threading e.g
from .utils import my_heavy_function
def my_view(request):
if request.method == "POST":
form = my_model_form()
if form.is_valid():
#create thread
thr = threading.Thread(target=my_heavy_function,args=(form,))
thr.start()
messages.success(request, "Processing ...")
return redirect("my_template")
else:
form = my_model_form()
return render(request, "my_app/my_template.html")
and it works like a charm; it process the my_heavy_function in the background while making the user able to continue using the webpage. I just need a way to show a message when my_heavy_function is done.
Is there a way to make Django display a message even when a new-request is not called but based on some other condition? E.g on a page when a file is done loading etc.
(I have on purposed not used Django-Q, Celery or back-ground-tasks for this threading since I find it being overkill)
It's not very clear what do you want to do. If you want to notify user without it doing an HTTP request, your only bet in web technologies is to setup a WebSocket so you can push things from server.
If it's ok for user to get the message next time they open a page, you can put something in the DB when your heavy task is done. And on each request you check if you have something in DB, you'll do messages.add_message and remove that row from DB.
If you want to show the message with a result of an action, that has finished after a response to the request has been returned, you have this option to show it synchronically.
The way it works is that next time the user requests a resource, this implementation will check for any messages for this user, that has been created meanwhile (eg. after your async code finished execution and added a message.
This solution uses a superstructure to the synchronous Django messaging framework with a simple Memcache container.
Install memcached as your cache backend.
docker run -p 11211:11211 --name local-memcache -d memcached memcached -m 64
Then go and pip install django-async-messages django-pymemcache
Add this to your middleware in settings.py file:
'async_messages.middleware.AsyncMiddleware'
Ensure it comes after 'django.contrib.messages.middleware.MessageMiddleware'
Then add this to your settings.py file:
CACHES = {
'default': {
'BACKEND': 'djpymemcache.backend.PyMemcacheCache',
'LOCATION': [
'127.0.0.1:11211',
],
},
}
Where you want to use this async messaging, go from async_messages import message_user
Substitute your classical messages.add_message(... for
message_user(request.user, "your message") where first agrument is a user object
go to the django-async-messages package because it is slightly obsolete and needs a small update.
Locate the middleware.py file in the package (likely in venv/Lib/site-packages/async_messages/middleware.py)
Change it from this
from django.contrib import messages
from async_messages import get_messages
class AsyncMiddleware(object):
def process_response(self, request, response):
"""
Check for messages for this user and, if it exists,
call the messages API with it
"""
if hasattr(request, "session") and hasattr(request, "user") and request.user.is_authenticated():
msgs = get_messages(request.user)
if msgs:
for msg, level in msgs:
messages.add_message(request, level, msg)
return response
to this:
from django.contrib import messages
from async_messages import get_messages
from django.utils.deprecation import MiddlewareMixin
class AsyncMiddleware(MiddlewareMixin):
def process_response(self, request, response):
"""
Check for messages for this user and, if it exists,
call the messages API with it
"""
if hasattr(request, "session") and hasattr(request, "user") and request.user.is_authenticated:
msgs = get_messages(request.user)
if msgs:
for msg, level in msgs:
messages.add_message(request, level, msg)
return response
That is it - you have asynchronous messaging!
Ps - because you just edited a package which would change when deploying - I exctracted the package and with above changes I included it directly in my project structure.

Flask Admin custom permissions for items in the nav

I'm having trouble figuring out how to add custom permissions to nav items, such that certain items will show up when the user is logged in / logged out, or other parameters are met (such as the user is part of a certain organization).
Any help or examples would be greatly appreciated.
With flask_security package you are able to set roles to users and check them in flask_admin views as follows:
from http import HTTPStatus
from flask_admin.contrib.sqla import ModelView
from flask_security import current_user, url_for_security
from flask import abort, redirect, request
class AdminSecurityMixin:
allowed_roles = []
def is_accessible(self):
return current_user.is_active and current_user.is_authenticated and \
(current_user.has_role('admin') or any(current_user.has_role(r) for r in self.allowed_roles))
def _handle_view(self, name, **kwargs):
if not self.is_accessible():
# if user is logged in, but can't view the admin, reject access
if current_user.is_authenticated:
abort(HTTPStatus.FORBIDDEN)
# otherwise redirect to the admin login view
# the next parameter is so we can redirect them after they'ved
# logged in to where they wanted to go originally
return redirect(url_for_security('login', next=request.url))
return None
class SecuredModelView(AdminSecurityMixin, ModelView):
allowed_roles = ['custom_role']
Now model registered with SecuredModelView will be accessible in admin panel only to users who are logged in and have assigned custom_role or admin role.
Roles can be created and added to existing users with following commands:
$ flask roles create custom_role
$ flask roles add <your user email> custom_role
I guess you already have a database with at least a user table.
I think you just want to design templates.
I invite you to follow this Flask tutorial about templates. It will introduce you Jinja, a templating language.

How to limit number of concurrent users logging in to same account in Django

My site is a digital marketplace website written in Django.
Digital content(text, images, videos) on the site is 'locked' by default. Only users who bought those content can view it.
There's a story that certain user(who bought the content) give away username/password for free to many people(1,000+ people in Facebook groups, for example). Those 1,000 users can then login using that single username/password and view the 'locked' digital content without paying a cent.
Is it possible to limit number of concurrent login to the same account?
I've found this package:
https://github.com/pcraston/django-preventconcurrentlogins
but what it does is logging previous user out when someone logged in using the same username/password. That would not help because each user only need to type in username/password each time to access 'locked' content.
To limit the concurrent users, keep an eye on the existing sessions.
In your current approach, when a user logs in, a new session is created. That new session co-exists with the older sessions, so you have N concurrent sessions at the same time.
You want to allow a single session. The easiest approach would be to invalidate older session when a new login happens:
detect/extend the login event (use the "user_logged_in" signal)
for each login, remove the other existing sessions from the same user (see "Clearing the session store")
Other (more complete, but more complex) approaches would be using Two-factor authentication, blocking per IP, throttling the login event, requiring email confirmation, etc...
Store User-Session mapping in another model.
from django.conf import settings
from django.contrib.sessions.models import Session
from django.db import models
class UserSessions(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='user_sessions')
session = models.OneToOneField(Session, related_name='user_sessions',
on_delete=models.CASCADE)
def __str__(self):
return '%s - %s' % (self.user, self.session.session_key)
If you have your own login view, you can update this model yourself:
from django.contrib.auth.views import login as auth_login
def login(request):
auth_login(request)
if request.user.is_authenticated():
session = Session.objects.get(session_key=request.session.session_key)
user_session = UserSession.objects.create(user=request.user, session=session)
no_of_logins = request.user.user_sessions.count()
if no_of_logins > 1: # whatever your limit is
request.SESSION['EXTRA_LOGIN'] = True
# Do your stuff here
Other option is to use Signal. Django provides signals: user_logged_in, user_login_failed and user_logged_out, if you use the Django login view, that is.
# signals.py
from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver
#receiver(user_logged_in)
def concurrent_logins(sender, **kwargs):
user = kwargs.get('user')
request = kwargs.get('request')
if user is not None and request is not None:
session = Session.objects.get(session_key=request.session.session_key)
UserSessions.objects.create(user=user, session=session)
if user is not None:
request.session['LOGIN_COUNT'] = user.user_sessions.count()
# your login view
def login(request):
auth_login(request)
if request.user.is_authenticated() and request.session['LOGIN_COUNT'] > 1:
# 'LOGIN_COUNT' populated by signal
request.session['EXTRA_LOGIN'] = True
# Do your stuff
If EXTRA_LOGIN is True, you can list the previous sessions, and ask the user to choose which sessions to logout. (Don't stop him from logging in, else he might be locked out - if he doesn't have access to his previous sessions now)
1 In your users/profiles app add a management command file
To add managment command, follow this guide:
https://docs.djangoproject.com/en/1.10/howto/custom-management-commands/
2 The management command code:
kills all sessions from users that have more then 10 sessions, you may change that to 1K if needed, or send this value as a param to the management command
from django.core.management.base import BaseCommand, CommandError
from django.contrib.sessions.models import Session
from django.contrib.auth.models import User
class Command(BaseCommand):
def handle(self, *args, **options):
session_user_dict = {}
# users with more than 10 sessions - del all
for ses in Session.objects.all():
data = ses.get_decoded()
user_owner = User.objects.filter(pk = data.get('_auth_user_id', None))
if int(data.get('_auth_user_id', None)) in session_user_dict:
session_user_dict[int(data.get('_auth_user_id', None))] += 1
else:
session_user_dict[int(data.get('_auth_user_id', None))] = 1
for k,v in session_user_dict.iteritems():
if v > 10:
for ses in Session.objects.all():
data = ses.get_decoded()
if str(k) == data.get('_auth_user_id', None):
ses.delete()
3 Optional password change-
after killing the bad-users sessions -change the bad users password to a diff one.
To do that change the last loop in the above code
for k,v in session_user_dict.iteritems():
if v > 10:
for ses in Session.objects.all():
data = ses.get_decoded()
if str(k) == data.get('_auth_user_id', None):
ses.delete()
theuser = User.objects.filter(pk=k)
#maybe use uuid to pick a password ...
theuser.set_password('new_unknown_password')
4 Add a django management command to crontab every minute / hour or whenever use this guide:
https://www.cyberciti.biz/faq/how-do-i-add-jobs-to-cron-under-linux-or-unix-oses/
if you are using a virtual env , remember that a management command that runs from cron needs to enter to the virtual env first, you may do it with a .sh script, ask for help if needed

django-allauth: Only allow users from a specific google apps domain

I am a newbie at Django. Using django-allauth I have set up single click sign in. I obtained my domain credentials ( client_id and secret_key) from google api console. But the problem is django-allauth is letting me login from any google account while I want the email addresses to be restricted to my domain ( #example.com instead of #gmail.com)
django-social-auth has the white listed domains parameter for this, how do I include this information in allauth?
I found django-allauth much easier to set up after spending hours on django-social-auth
Any help would be much appreciated.
Answering my own question-
What you want to do is stall the login after a user has been authenticated by a social account provider and before they can proceed to their profile page. You can do this with the
pre_social_login method of the DefaultSocialAccountAdapter class in allauth/socialaccount/adaptor.py
Invoked just after a user successfully authenticates via a
social provider, but before the login is actually processed
(and before the pre_social_login signal is emitted).
You can use this hook to intervene, e.g. abort the login by
raising an ImmediateHttpResponse
Why both an adapter hook and the signal? Intervening in
e.g. the flow from within a signal handler is bad -- multiple
handlers may be active and are executed in undetermined order.
Do something like
from allauth.socialaccount.adaptor import DefaultSocialAccountAdapter
class MySocialAccount(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
u = sociallogin.account.user
if not u.email.split('#')[1] == "example.com"
raise ImmediateHttpResponse(render_to_response('error.html'))
This is not an exact implementation but something like this works.
Here's an alternate solution:
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class CustomAccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
return False # No email/password signups allowed
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
def is_open_for_signup(self, request, sociallogin):
u = sociallogin.user
# Optionally, set as staff now as well.
# This is useful if you are using this for the Django Admin login.
# Be careful with the staff setting, as some providers don't verify
# email address, so that could be considered a security flaw.
#u.is_staff = u.email.split('#')[1] == "customdomain.com"
return u.email.split('#')[1] == "customdomain.com"
This code can live anywhere, but assuming it's in mysite/adapters.py, you'll also need the following in your settings.py:
ACCOUNT_ADAPTER = 'mysite.adapters.CustomAccountAdapter'
SOCIALACCOUNT_ADAPTER = 'mysite.adapters.CustomSocialAccountAdapter'
You could do something in the line of overriding allauth's allauth.socialaccount.forms.SignupForm and checking the domain during the signup process.
Discalmer: this is all written without testing, but something in the line of that should work.
# settings.py
# not necesarry, but it would be a smart way to go instead of hardcoding it
ALLOWED_DOMAIN = 'example.com'
.
# forms.py
from django.conf import settings
from allauth.socialaccount.forms import SignupForm
class MySignupForm(SignupForm):
def clean_email(self):
data = self.cleaned_data['email']
if data.split('#')[1].lower() == settings.ALLOWED_DOMAIN:
raise forms.ValidationError(_(u'domena!'))
return data
in your urls override allauth defaults (put this before the include of django-allauth)
# urls.py
from allauth.socialaccount.views import SignupView
from .forms import MySignupForm
urlpatterns = patterns('',
# ...
url(r"^social/signup/$", SignupView.as_view(form_class=MySignupForm), name="account_signup"),
# ...
)
I'm not sure for the "^social/signup/$", recheck that.

django multiple admin instances and locking down access to a particular instance

I have several admin instances running on a site - one for each country, that the site supports.
However, if a user logs into one admin, they are automatically able to access other instances.
I need to make the auth code aware of which admin the user has logged into and prevent access to other admin systems.
Any ideas how this can be done?
You can use middleware to check for user permissions to access certain areas of admin site. Checkout this snippet. (You might want to know more about handling custom permissions in Django.)
If you need something more universal, you can use the code example below. The idea is simple: it uses custom functions to find out about user permissions and to give an appropriate response:
#coding: utf-8
# Note that RESTRICTED_URLS tuple takes three parameters: url regex, function to check
# whether user has certain permission, and a function to redirect the user to a certain
# page if he doesn't have sufficient rights.
import re
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.http import HttpResponseRedirect
from django.contrib import messages
from backend.models import Professional
from django.contrib.auth.decorators import permission_required
def calculate_forbidden_response(request, view_func,view_args,view_kwargs):
if not request.user.is_authenticated():
return permission_required('')(view_func)(request,*view_args,**view_kwargs)
elif request.user.has_perm('backend.p_add_professional'):
messages.error(request, _('You need permission Spam to enter this cabinet.'))
return HttpResponseRedirect('/some_help_page_about_permissions.html')
def check_professional_permission(request):
return request.user.has_perm('backend.p_access_professional_cabinet')
RESTRICTED_URLS = (
(r'/professional/(.*)$', check_professional_permission, calculate_forbidden_response),
)
RESTRICTED_URLS_EXCEPTIONS = ()
class CheckPermissionMiddleware(object):
def __init__(self):
self.restricted = tuple([(re.compile(url[0]), url[1], url[2]) for url in RESTRICTED_URLS])
self.exceptions = tuple([re.compile(url) for url in RESTRICTED_URLS_EXCEPTIONS])
def process_view(self,request,view_func,view_args,view_kwargs):
if request.user.is_superuser:
return None
for path in self.exceptions:
if path.match(request.path): return None
for rule in self.restricted:
url, permission = rule[0], rule[1]
calculated_response = rule[2]
if url.match(request.path):
if not permission(request):
return calculated_response(request, view_func,view_args,view_kwargs)
else:
return None
return None