diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 3d76a425a4..1f88adb49b 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -2,6 +2,7 @@ import datetime from warnings import warn from django.core.exceptions import ImproperlyConfigured from django.utils.importlib import import_module +from django.contrib.auth.signals import user_logged_in, user_logged_out SESSION_KEY = '_auth_user_id' BACKEND_SESSION_KEY = '_auth_user_backend' @@ -62,8 +63,7 @@ def login(request, user): if user is None: user = request.user # TODO: It would be nice to support different login methods, like signed cookies. - user.last_login = datetime.datetime.now() - user.save() + user_logged_in.send(sender=user.__class__, request=request, user=user) if SESSION_KEY in request.session: if request.session[SESSION_KEY] != user.id: @@ -83,6 +83,13 @@ def logout(request): Removes the authenticated user's ID from the request and flushes their session data. """ + # Dispatch the signal before the user is logged out so the receivers have a + # chance to find out *who* logged out. + user = getattr(request, 'user', None) + if hasattr(user, 'is_authenticated') and not user.is_authenticated(): + user = None + user_logged_out.send(sender=user.__class__, request=request, user=user) + request.session.flush() if hasattr(request, 'user'): from django.contrib.auth.models import AnonymousUser diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 16a8b99d6c..ebf9a8d216 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -2,6 +2,7 @@ import datetime import urllib from django.contrib import auth +from django.contrib.auth.signals import user_logged_in from django.core.exceptions import ImproperlyConfigured from django.db import models from django.db.models.manager import EmptyManager @@ -40,6 +41,15 @@ def check_password(raw_password, enc_password): algo, salt, hsh = enc_password.split('$') return hsh == get_hexdigest(algo, salt, raw_password) +def update_last_login(sender, user, **kwargs): + """ + A signal receiver which updates the last_login date for + the user logging in. + """ + user.last_login = datetime.datetime.now() + user.save() +user_logged_in.connect(update_last_login) + class SiteProfileNotAvailable(Exception): pass diff --git a/django/contrib/auth/signals.py b/django/contrib/auth/signals.py new file mode 100644 index 0000000000..4f0b2c235c --- /dev/null +++ b/django/contrib/auth/signals.py @@ -0,0 +1,4 @@ +from django.dispatch import Signal + +user_logged_in = Signal(providing_args=['request', 'user']) +user_logged_out = Signal(providing_args=['request', 'user']) diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py index 98061a157a..9f8b49f400 100644 --- a/django/contrib/auth/tests/__init__.py +++ b/django/contrib/auth/tests/__init__.py @@ -5,6 +5,7 @@ from django.contrib.auth.tests.forms import UserCreationFormTest, Authentication from django.contrib.auth.tests.remote_user \ import RemoteUserTest, RemoteUserNoCreateTest, RemoteUserCustomTest from django.contrib.auth.tests.models import ProfileTestCase +from django.contrib.auth.tests.signals import SignalTestCase from django.contrib.auth.tests.tokens import TokenGeneratorTest from django.contrib.auth.tests.views \ import PasswordResetTest, ChangePasswordTest, LoginTest, LogoutTest diff --git a/django/contrib/auth/tests/signals.py b/django/contrib/auth/tests/signals.py new file mode 100644 index 0000000000..3806021dcc --- /dev/null +++ b/django/contrib/auth/tests/signals.py @@ -0,0 +1,47 @@ +from django.test import TestCase +from django.contrib.auth import signals + + +class SignalTestCase(TestCase): + urls = 'django.contrib.auth.tests.urls' + fixtures = ['authtestdata.json'] + + def listener_login(self, user, **kwargs): + self.logged_in.append(user) + + def listener_logout(self, user, **kwargs): + self.logged_out.append(user) + + def setUp(self): + """Set up the listeners and reset the logged in/logged out counters""" + self.logged_in = [] + self.logged_out = [] + signals.user_logged_in.connect(self.listener_login) + signals.user_logged_out.connect(self.listener_logout) + + def tearDown(self): + """Disconnect the listeners""" + signals.user_logged_in.disconnect(self.listener_login) + signals.user_logged_out.disconnect(self.listener_logout) + + def test_login(self): + # Only a successful login will trigger the signal. + self.client.login(username='testclient', password='bad') + self.assertEqual(len(self.logged_in), 0) + # Like this: + self.client.login(username='testclient', password='password') + self.assertEqual(len(self.logged_in), 1) + self.assertEqual(self.logged_in[0].username, 'testclient') + + def test_logout_anonymous(self): + # The log_out function will still trigger the signal for anonymous + # users. + self.client.get('/logout/next_page/') + self.assertEqual(len(self.logged_out), 1) + self.assertEqual(self.logged_out[0], None) + + def test_logout(self): + self.client.login(username='testclient', password='password') + self.client.get('/logout/next_page/') + self.assertEqual(len(self.logged_out), 1) + self.assertEqual(self.logged_out[0].username, 'testclient') diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index 4c1db33b87..36ad071a75 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -12,6 +12,9 @@ A list of all the signals that Django sends. The :doc:`comment framework ` sends a :doc:`set of comment-related signals `. + The :ref:`authentication framework ` sends :ref:`signals when + a user is logged in / out `. + Model signals ============= diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index f45c61a94b..748fb89419 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -665,6 +665,44 @@ How to log a user out immediately after logging out, do that *after* calling :func:`django.contrib.auth.logout()`. +.. _topics-auth-signals: + +Login and logout signals +------------------------ + +The auth framework uses two :ref:`signals ` that can be used for +notification when a user logs in or out. + +**:data:`django.contrib.auth.signals.user_logged_in`** + +Sent when a user logs in successfully. + +Arguments sent with this signal: + + ``sender`` + As above: the class of the user that just logged in. + + ``request`` + The current :class:`~django.http.HttpRequest` instance. + + ``user`` + The user instance that just logged in. + +**:data:`django.contrib.auth.signals.user_logged_out`** + +Sent when the logout method is called. + + ``sender`` + As above: the class of the user that just logged out or ``None`` + if the user was not authenticated. + + ``request`` + The current :class:`~django.http.HttpRequest` instance. + + ``user`` + The user instance that just logged out or ``None`` if the + user was not authenticated. + Limiting access to logged-in users ----------------------------------