Django user inactivity - django

Is there any way to see if a user is inactive for a certain amount of time? For example Twitter sends out an email to it's users after a certain amount of time of inactivity. I would like to implement a similar feature where if a user has been inactive for 30 days, an email is sent "Hello User, check out what your friends having been posting" How can I implement this?

Well, django.contrib.auth.models.User has a last_login field which might be useful for you.
Just wherever you want, check the last_login date of the User and you'll now how long he's been away of your site.
Hope this helps!

You can write a management command which checks for the last time user logged in and if the days are greater than 30, send an email. (You could implement this as a cron that runs every day)
import datetime
from django.core.management.base import BaseCommand
def compute_inactivity():
inactive_users = User.objects.filter(last_login__lt=datetime.datetime.now() - datetime.timedelta(months=1))
#send out emails to these users
class Command(BaseCommand):
def handle(self, **options):
compute_inactivity()
If you have any other criteria which defines "activity", you can filter your queryset based on that.

After reading karthikr's answer and Aidas Bendoraitis' suggestion, I've written the correction solution below. It is very similar to Karthikr's answer except instead of using the __lt rich comparison operator, use the __eq operator:
import datetime
from django.core.management.base import BaseCommand
def compute_inactivity():
inactive_users = User.objects.filter(last_login__eq=datetime.datetime.now() - datetime.timedelta(months=1))
#send out emails to these users
class Command(BaseCommand):
def handle(self, **options):
compute_inactivity()

My approach would be to send notifications to users exactly when 30 days pass since their last login. For this, you will need to create a management command and run it as a cron job daily.
import datetime
from django.core.management.base import BaseCommand
def compute_inactivity():
a_month_ago = datetime.datetime.now() - datetime.timedelta(days=30)
inactive_users = User.objects.filter(
last_login__year=a_month_ago.year,
last_login__month=a_month_ago.month,
last_login__day=a_month_ago.day,
)
#send out emails to these users
class Command(BaseCommand):
def handle(self, **options):
compute_inactivity()

Related

Use Throttling to restrict the number of times a certain request can be made globally

I am using Django Throttling, and want to add a behavior that will throttle users from calling a certain request more than X times in rate - globally.
Using AnonRateThrottle or UserRateThrottle is not good enough for me, because it checks the number of times that a certain User or IP address made the request. I want to limit the global calls to a certain api_view, no matter who made the request.
For example, if the rate is 1/min and user X made a request, than every other user will be throttled for the next minute.
EDIT:
Thanks to Kaushal's answer below, I found a way to make this work by adding:
def get_cache_key(self, request, view):
return request.method + request.get_full_path()
To apply throttle globally for view you can use same key.
here idea is use same key per view. that mean for all request it'll use same key to get request count data
from rest_framework import throttling
class MyViewRateThrottle(throttling.SimpleRateThrottle):
rate = '3/m'
def get_cache_key(self, request, view):
return view.__class__.__name__
this will throttle per view. same as you're looking for
Here I created Throttling for specific user
throttling.py
from rest_framework.throttling import UserRateThrottle
class RockyRateThrottle(UserRateThrottle):
scope = 'rocky'
settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES':{
'anon': '2/day', # For Anonymous user
'user': '5/hour', # For Registred user
'rocky': '3/minute' # For register but specific user
}
}
views.py
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
from api.throttling import RockyRateThrottle
class StudentModelViewSet(viewsets.ModelViewSet):
queryset = Student.objects.all()
serializer_class = StudentSerializer
authentication_classes=[SessionAuthentication]
permission_classes=[IsAuthenticatedOrReadOnly]
# throttle_classes = [AnonRateThrottle, UserRateThrottle]
throttle_classes = [AnonRateThrottle, RockyRateThrottle] # this is working for 'anon' and 'Rocky'

Require second login for users on Django based site

I have a Django 2.2 based project that uses a custom user model, the built-in Auth app and Django-All-Auth for user management. Almost every page on the site is behind a login and we use varying levels of permissions to determine what can be accessed a user.
So far, so good, but now I'm being asked to designate everything behind a specific part of the site as "sensitive", requiring a second login prompt using the same login credentials. What this means is that the client wants to see a login appear when they try to access anything under /top-secret/ the first time in a set time, say 30 mins, regardless of whether they're already logged in or not.
I've dug around on the internet for ideas on how to do this, but so far I've been unable to find a good solution. Has anyone here had any experience with something similar, and if so, could they point me in the right direction?
Figured out how to make this work in my situation. I debated deleting this question, but on the off chance that there's someone else out there that wants to do something similar, here's how I did it.
Create a new middleware with the following:
from allauth.account.adapter import get_adapter
from datetime import datetime
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.shortcuts import redirect
from django.urls import reverse
from django.utils import timezone
class ExtraLoginPromptMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated:
for url in settings.EXTRA_LOGIN_URLS:
if request.path.startswith(url):
last_seen = request.session.get(settings.EXTRA_LOGIN_SESSION_VARIABLE)
if not last_seen:
last_seen = request.user.last_login
else:
last_seen = datetime.fromisoformat(last_seen)
if last_seen + relativedelta(seconds=settings.EXTRA_LOGIN_EXPIRY) < timezone.now():
# NB, this uses allauth, if you want to use django.auth, just
# call django.contrib.auth's logout instead
get_adapter(request).logout(request)
return redirect('{}?next={}'.format(reverse('account_login'), url))
else:
request.session[settings.EXTRA_LOGIN_SESSION_VARIABLE] = timezone.now().isoformat()
return self.get_response(request)
To make use of this, you'll want to add this middleware after SessionMiddleware and AuthenticationMiddleware
You'll also want to define a few variables in your settings:
EXTRA_LOGIN_URLS = ['/top-secret/', 'another-secret']
EXTRA_LOGIN_EXPIRY = 120 # How long before the "extra security" session expires
EXTRA_LOGIN_SESSION_VARIABLE = 'extra-login-last-seen'

Django `update_last_login` always providing lastest time. not last login time

I have logged into my site 1 days ago but today when I login into my site again, it is showing me today's date in user.last_login whereas its should display yesterday's date.
Here is the Django source code:-
https://github.com/django/django/blob/a3ba2662cdaa36183fdfb8a26dfa157e26fca76a/django/contrib/auth/models.py#L20
I have also reported to Django(https://code.djangoproject.com/ticket/28256)
Check this link out.
It says last time not the time before this time. I think you misunderstand what last time means. When you write a comment that is the last comment you wrote, not the one before that. So when you Log in again to check when you last logged in THIS is the time Django shows you.
Test the function with two different users. Log in with user1, Log user1 out again. Then go with user2 in the Admin and check the time of user1.
Hope that helps.
p.s. please take down the Ticket...
For those coming here to figure out how to save the previous login time to the current session, you can make your own signal handler for the user_logged_in signal.
Add previous_login to your User model:
from django.utils.translation import gettext_lazy as _
...
previous_login = models.DateTimeField(_("previous login"), blank=True, null=True)
And then make your own signal handler and disconnect the one offered by django
from django.contrib.auth import user_login_failed, user_logged_in
from django.contrib.auth.models import update_last_login
from django.utils import timezone
def update_last_and_previous_login(sender, user, **kwargs):
user.previous_login = user.last_login
user.last_login = timezone.now()
user.save(update_fields=["previous_login", "last_login"])
user_logged_in.disconnect(update_last_login, dispatch_uid="update_last_login")
user_logged_in.connect(update_last_and_previous_login, dispatch_uid="update_last_and_previous_login")

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 email digest

Is there an existing plug-in to produce daily or weekly digest emails in Django? (We want to combine many small notifications into one email, rather than bother people all the time.)
Django-mailer claims to support this, but I'm told it doesn't really.
There is django-mailer app of which I was not aware till now, so the answer below details my own approach.
The simplest case won't require much:
put this into your app/management/commands/send_email_alerts.py, then set up a cron job to run this command once a week with python manage.py send_email_alerts (all paths must be set in the environment of course for manage.py to pick up your app settings)
from django.core.management.base import NoArgsCommand
from django.db import connection
from django.core.mail import EmailMessage
class Command(NoArgsCommand):
def handle_noargs(self,**options):
try:
self.send_email_alerts()
except Exception, e:
print e
finally:
connection.close()
def send_email_alerts(self):
for user in User.objects.all():
text = 'Hi %s, here the news' % user.username
subject = 'some subject'
msg = EmailMessage(subject, text, settings.DEFAULT_FROM_EMAIL, [user.email])
msg.send()
But if you will need to keep track of what to email each user and how often, some extra code will be needed. Here is a homegrown example. Maybe that's where django-mailer can fill in the gaps.
I've just released the django-digested package to PyPI. It supports instant notifications, daily and weekly digests, and individual preferences for different groups of updates.