From c73123d0d8749b866f56d4ab42024fb7cde92a5a Mon Sep 17 00:00:00 2001 From: Alexandra Bruckner Date: Fri, 13 Oct 2023 11:22:02 +0200 Subject: [PATCH] SIANXKE-330: add anonymous login relevant properties to User model --- CHANGELOG.md | 5 + README.md | 16 +++ drf_anonymous_login/models.py | 17 +++ tests/core/settings.py | 2 + tests/testapp/migrations/0001_initial.py | 126 ++++++++++++++++++++++- tests/testapp/models.py | 7 ++ tests/testapp/tests/test_api.py | 38 ++++++- 7 files changed, 208 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42d04f0..7663742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- Mixin for User to provide properties `is_anonymous_login` and `anonymous_login` + ## [1.1.0] ### Added diff --git a/README.md b/README.md index 88d417d..12a7555 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,22 @@ OR 2. Directly add the `AnonymousLoginAuthentication` and `IsAuthenticated` to your ViewSet's `authentication_classes` and `permission_classes` as implemented in the [AnonymousLoginAuthenticationModelViewSet](drf_anonymous_login/views.py). +3. Optionally add the `AnonymousLoginUserMixin` to your app's User model in order to access its `is_anonymous_login` + and `anonymous_login` properties: + ``` + # myapp.models.py + + class User(AnonymousLoginUserMixin, AbstractUser): + pass + ``` + + ``` + # settings.py + + AUTH_USER_MODEL = "myapp.User" + ``` + + #### Configure token expiration The tokens will not expire by default (expiration_datetime remains `None`). You can configure the `ANONYMOUS_LOGIN_EXPIRATION` in your application's `settings.py` to define a default expiration in minutes, e.g. diff --git a/drf_anonymous_login/models.py b/drf_anonymous_login/models.py index 1fd5954..d01bce4 100644 --- a/drf_anonymous_login/models.py +++ b/drf_anonymous_login/models.py @@ -33,3 +33,20 @@ def set_default_expiration_datetime(): default_expiration = getattr(settings, "ANONYMOUS_LOGIN_EXPIRATION", None) if default_expiration: return timezone.now() + timedelta(minutes=default_expiration) + + +class AnonymousLoginUserMixin(object): + @property + def is_anonymous_login(self): + return AnonymousLogin.objects.filter(token=self.username).exists() + + @property + def anonymous_login(self): + """ + Returns the "longest" (with the latest expiration) AnonymousLogin element matching the user's username + """ + return ( + AnonymousLogin.objects.filter(token=self.username) + .order_by("-expiration_datetime") + .first() + ) diff --git a/tests/core/settings.py b/tests/core/settings.py index 7990fb6..01cfbc5 100644 --- a/tests/core/settings.py +++ b/tests/core/settings.py @@ -106,3 +106,5 @@ # https://docs.djangoproject.com/en/3.2/howto/static-files/ STATIC_URL = "/static/" + +AUTH_USER_MODEL = "testapp.User" diff --git a/tests/testapp/migrations/0001_initial.py b/tests/testapp/migrations/0001_initial.py index 4e84959..232a5ed 100644 --- a/tests/testapp/migrations/0001_initial.py +++ b/tests/testapp/migrations/0001_initial.py @@ -1,12 +1,19 @@ -# Generated by Django 3.2.18 on 2023-04-03 13:20 +# Generated by Django 4.2.6 on 2023-10-13 09:18 +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone from django.db import migrations, models +import drf_anonymous_login.models + class Migration(migrations.Migration): initial = True - dependencies = [] + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] operations = [ migrations.CreateModel( @@ -27,4 +34,119 @@ class Migration(migrations.Migration): ), ], ), + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + bases=(drf_anonymous_login.models.AnonymousLoginUserMixin, models.Model), + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), ] diff --git a/tests/testapp/models.py b/tests/testapp/models.py index f56ca2a..4e80acd 100644 --- a/tests/testapp/models.py +++ b/tests/testapp/models.py @@ -1,5 +1,8 @@ +from django.contrib.auth.models import AbstractUser from django.db import models +from drf_anonymous_login.models import AnonymousLoginUserMixin + class PublicModel(models.Model): """ @@ -15,3 +18,7 @@ class PrivateModel(models.Model): """ name = models.CharField(max_length=50, primary_key=True) + + +class User(AnonymousLoginUserMixin, AbstractUser): + pass diff --git a/tests/testapp/tests/test_api.py b/tests/testapp/tests/test_api.py index bc412f5..48da10b 100644 --- a/tests/testapp/tests/test_api.py +++ b/tests/testapp/tests/test_api.py @@ -4,7 +4,7 @@ from django.urls import reverse from django.utils import timezone from rest_framework.status import HTTP_200_OK, HTTP_403_FORBIDDEN -from testapp.models import PrivateModel, PublicModel +from testapp.models import PrivateModel, PublicModel, User from drf_anonymous_login.authentication import AUTH_HEADER, AUTH_KEYWORD from drf_anonymous_login.management.commands.cleanup_tokens import Command @@ -103,3 +103,39 @@ def test_anonymous_login_token_cleanup(self): # make sure the token gets deleted cleanup_tokens.handle_tick() self.assertEqual(AnonymousLogin.objects.count(), 0) + + def test_user_is_anonymous_login(self): + """ + Assert that User is correctly identified as AnonymousLogin + :return: + """ + user = User.objects.create( + username=self.anonymous_login.token, password="password" + ) + self.assertTrue(user.is_anonymous_login) + + def test_user_is_not_anonymous_login(self): + """ + Assert that User is correctly identified as no AnonymousLogin + :return: + """ + user = User.objects.create(username="user", password="password") + self.assertFalse(user.is_anonymous_login) + + def test_user_get_anonymous_login(self): + """ + Assert that User can access their AnonymousLogin + :return: + """ + user = User.objects.create( + username=self.anonymous_login.token, password="password" + ) + self.assertEqual(user.anonymous_login, self.anonymous_login) + + def test_user_get_no_anonymous_login(self): + """ + Assert that User can not access an AnonymousLogin + :return: + """ + user = User.objects.create(username="user", password="password") + self.assertIsNone(user.anonymous_login)