diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index ecaadc4..202cf54 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -8,6 +8,13 @@ on: jobs: build: + services: + redis: + image: bitnami/redis:latest + ports: + - 6379:6379 + env: + REDIS_PASSWORD: "password" runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index 7b38d83..8681d7f 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -2,7 +2,7 @@ name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI on: push: - branches: [ main ] + branches: [ main, release-* ] pull_request: branches: [ main, release-* ] release: diff --git a/docs/source/reference/change_log.rst b/docs/source/reference/change_log.rst index 1fd1013..b1fb43d 100644 --- a/docs/source/reference/change_log.rst +++ b/docs/source/reference/change_log.rst @@ -2,6 +2,17 @@ Change Log ========== +1.1.1 +----- + +Release date: 9 Dec, 2021 + +- **ADDED** Support for all session engines (not only DB engine) See docs for :docs:`settings_reference` + +.. warning:: + This version **requires migration** after upgrade from older version + + 1.1.0 ----- diff --git a/docs/source/reference/settings_reference.rst b/docs/source/reference/settings_reference.rst index d136511..848a950 100644 --- a/docs/source/reference/settings_reference.rst +++ b/docs/source/reference/settings_reference.rst @@ -6,18 +6,31 @@ MESSAGES_USE_SESSIONS ~~~~~~~~~~~~~~~~~~~~~ | Type ``bool``; Default to ``False``; Not Required. -| Use session context to store messages. +| Use session context to query messages. -Store and query messages for current session only. -When is set to ``True`` messages are added only to the current session, and is shown only to throughout the session. +Query messages for current session only. +When is set to ``True``, only messages created from the **same session** will be shown. -By default, messages are stored with user context. -That means the user can see all their messages everywhere. +By default (``False``), messages are queried only by the **authenticated user**. +That means the user can see all their messages from **all sessions**. + +Relating messages to session is different according to your configured `Session Engine `_. +For the most part, the ``session_key`` string is used to filter the query. +When is available, the ``Session`` model object is used as `ForeignKey` and is also used to filter the query. .. note:: - Using user context messages can **support authentication backends** other then ``SessionAuthentication``, - while session messages is better for showing messages **only where they are relevant** and - **automatic cleaning** stale messages as the session deletes on expire or logoff. + When using a session engine that works with db ``Session`` model, you unlock extra functionality that **automatically + clears out messages** after user logout or `clearsessions `_ command. + +Tested session engines: + +* ``django.contrib.sessions.backends.db`` (uses db) +* ``django.contrib.sessions.backends.file`` +* ``django.contrib.sessions.backends.cache`` +* ``django.contrib.sessions.backends.cached_db`` (uses db) +* ``django.contrib.sessions.backends.signed_cookies`` +* ``redis_sessions.session`` (`django-redis-sessions `_) + MESSAGES_ALLOW_DELETE_UNREAD ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/drf_messages/__init__.py b/drf_messages/__init__.py index fb6db0c..0696557 100644 --- a/drf_messages/__init__.py +++ b/drf_messages/__init__.py @@ -4,7 +4,7 @@ __title__ = "DRF Messages" -__version__ = "1.1.0" +__version__ = "1.1.1" __author__ = "Dan Yishai" __license__ = "BSD 3-Clause" diff --git a/drf_messages/migrations/0002_message_session_key.py b/drf_messages/migrations/0002_message_session_key.py new file mode 100644 index 0000000..371b9c6 --- /dev/null +++ b/drf_messages/migrations/0002_message_session_key.py @@ -0,0 +1,29 @@ +# pylint: disable=invalid-name, line-too-long +# Generated by Django 3.2.10 on 2021-12-08 20:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('drf_messages', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='message', + name='session_key', + field=models.CharField(blank=True, help_text='The session key where the message was submitted to.', max_length=40, null=True), + ), + migrations.AlterField( + model_name='message', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='messagetag', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/drf_messages/models.py b/drf_messages/models.py index 6396db4..a7ec806 100644 --- a/drf_messages/models.py +++ b/drf_messages/models.py @@ -56,7 +56,11 @@ def with_context(self, request): user=request.user if hasattr(request, "user") and request.user.is_authenticated else None ) if messages_settings.MESSAGES_USE_SESSIONS and hasattr(request, "session"): - return queryset.filter(Q(session__session_key=request.session.session_key) | Q(session__isnull=True)) + return queryset.filter( + Q(session_key=request.session.session_key) + | Q(session__session_key=request.session.session_key) + | Q(session__isnull=True) + ) else: return queryset @@ -85,14 +89,18 @@ def create_message(self, request, message, level, extra_tags=None): :return: Message object. """ # extract session - if hasattr(request, "session") and request.session.session_key: - session = Session.objects.get(session_key=request.session.session_key) + if hasattr(request, "session"): + session_key = request.session.session_key else: - session = None + session_key = None + + session = Session.objects.filter(session_key=session_key).first() + # create message message_obj = self.create( user=request.user, session=session, + session_key=session_key, view=request.resolver_match.view_name if request.resolver_match else '', message=message, level=level, @@ -141,6 +149,8 @@ class Message(models.Model): user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name="messages") session = models.ForeignKey(Session, on_delete=models.CASCADE, null=True, blank=True, default=None, related_name="messages", help_text="The session where the message was submitted to.") + session_key = models.CharField(max_length=40, null=True, blank=True, + help_text="The session key where the message was submitted to.") view = models.CharField(max_length=64, blank=True, default="", help_text="The view where the message was submitted from.") diff --git a/setup.cfg b/setup.cfg index 34df890..11e3f2e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = drf-messages -version = 1.1.0 +version = 1.1.1 description = Use Django's Messages Framework with Django Rest Framework project. long_description = file: README.rst url = https://github.com/danyi1212/drf-messages @@ -15,6 +15,7 @@ classifiers = Framework :: Django :: 2.2 Framework :: Django :: 3.0 Framework :: Django :: 3.1 + Framework :: Django :: 3.2 Intended Audience :: Developers License :: OSI Approved :: BSD License Operating System :: OS Independent diff --git a/testproj/demo/tests.py b/testproj/demo/tests.py index 28c11cc..90bdb0b 100644 --- a/testproj/demo/tests.py +++ b/testproj/demo/tests.py @@ -1,7 +1,10 @@ # pylint: disable=missing-function-docstring, protected-access, no-member, not-context-manager +from typing import Tuple, List + from django.contrib import messages from django.contrib.messages import get_messages, set_level from django.contrib.messages.storage.base import Message as DjangoMessage +from django.db.models import F from django.test import override_settings, modify_settings, TestCase, TransactionTestCase from django.urls import reverse from rest_framework import status @@ -207,6 +210,7 @@ def test_inside_template(self): def test_first_session(self): # check from database session_key = self.client.session.session_key + self.assertEqual(Message.objects.filter(session_key=session_key).count(), 1) self.assertEqual(Message.objects.filter(session__session_key=session_key).count(), 1) # check through storage storage: DBStorage = get_messages(self.request) @@ -224,6 +228,7 @@ def test_alt_session(self): self.assertEqual(len(response.data.get("results")), 0) # check from database using different session session_key = self.alt_client.session.session_key + self.assertEqual(Message.objects.filter(session_key=session_key).count(), 0) self.assertEqual(Message.objects.filter(session__session_key=session_key).count(), 0) # check storage for different session alt_storage: DBStorage = get_messages(response.wsgi_request) @@ -402,7 +407,7 @@ def test_contains_invalid_type(self): self.assertFalse(storage.used) -class MessageModelTestCase(TestCase): +class MessageModelTestCase(TransactionTestCase): def setUp(self): self.user = UserFactory() @@ -416,10 +421,12 @@ def test_created_message_view(self): self.assertEqual(self.message.view, "demo:index") def test_message_session(self): + self.assertEqual(Message.objects.filter(session_key=self.session_key).count(), 1) self.assertEqual(Message.objects.filter(session__session_key=self.session_key).count(), 1) def test_session_clear_on_logout(self): self.client.logout() + self.assertEqual(Message.objects.filter(session_key=self.session_key).count(), 0) self.assertEqual(Message.objects.filter(session__session_key=self.session_key).count(), 0) self.client.force_login(self.user) @@ -458,3 +465,42 @@ def test_parse_django_message_extra_tags(self): django_message = self.message.get_django_message() self.assertEqual(django_message.extra_tags, "test tag0 tag1 tag2") self.assertEqual(django_message.tags, "test tag0 tag1 tag2 info") + + +class SessionEngineTestCase(TestCase): + + @classmethod + def setUpTestData(cls): + cls.user = UserFactory() + + def subtest_message_with_session_engine(self, session_key_count: int, session_obj_count: int): + client = self.client_class() # recreate SessionMiddleware to refresh engine + client.force_login(self.user) # login to create session + self.assertTrue(client.session.session_key, msg="Failed to log in using session engine") + response = client.get(reverse('demo:test')) + session_key = response.wsgi_request.session.session_key + self.assertEqual(Message.objects.filter(session_key=session_key).count(), session_key_count, + msg="Session key was not set correctly") + self.assertEqual(Message.objects.filter(session__isnull=True).count(), session_key_count - session_obj_count, + msg="Session relation was not set correctly") + self.assertEqual(Message.objects.filter(session__session_key=session_key).count(), session_obj_count, + msg="Session relation was not set correctly") + self.assertFalse( + Message.objects.filter(session__isnull=False).exclude(session__session_key=F("session_key")).exists(), + msg="Session key and session object are different", + ) + + def test_session_engines(self): + non_db_engines: List[Tuple[str, bool]] = [ + ('django.contrib.sessions.backends.db', True), + ('django.contrib.sessions.backends.file', False), + ('django.contrib.sessions.backends.cache', False), + ('django.contrib.sessions.backends.cached_db', True), + ('django.contrib.sessions.backends.signed_cookies', False), + ('redis_sessions.session', False), + ] + for engine, is_db in non_db_engines: + with self.subTest(engine): + with self.settings(SESSION_ENGINE=engine): + Message.objects.all().delete() + self.subtest_message_with_session_engine(1, 1 if is_db else 0) diff --git a/testproj/requirements-github.txt b/testproj/requirements-github.txt index 2d1335d..2ce8425 100644 --- a/testproj/requirements-github.txt +++ b/testproj/requirements-github.txt @@ -2,4 +2,5 @@ drf-yasg django-debug-toolbar django-filter -factory-boy \ No newline at end of file +factory-boy +django-redis-sessions diff --git a/testproj/requirements.txt b/testproj/requirements.txt index cf6fcd9..02b820c 100644 --- a/testproj/requirements.txt +++ b/testproj/requirements.txt @@ -10,9 +10,11 @@ colorama==0.4.4 coreapi==2.3.3 coreschema==0.0.4 coverage==6.1.1 -Django==3.2.9 +Deprecated==1.2.13 +Django==3.2.10 django-debug-toolbar==3.2.2 django-filter==21.1 +django-redis-sessions==0.6.2 django-stubs-ext==0.3.1 djangorestframework==3.12.4 docutils==0.16 @@ -47,9 +49,11 @@ python-dateutil==2.8.2 pytz==2020.5 pywin32-ctypes==0.2.0 readme-renderer==28.0 +redis==4.0.2 requests==2.25.1 requests-toolbelt==0.9.1 rfc3986==1.4.0 +rstcheck==3.3.1 ruamel.yaml==0.16.12 ruamel.yaml.clib==0.2.2 six==1.15.0 diff --git a/testproj/testproj/settings.py b/testproj/testproj/settings.py index cf1f8de..051a7e5 100644 --- a/testproj/testproj/settings.py +++ b/testproj/testproj/settings.py @@ -193,4 +193,14 @@ '127.0.0.1', ] +SESSION_REDIS = { + 'host': 'localhost', + 'port': 6379, + 'db': 0, + 'password': 'password', + 'prefix': 'session', + 'socket_timeout': 1, + 'retry_on_timeout': False +} + MESSAGE_STORAGE = "drf_messages.storage.DBStorage"