Skip to content

Commit

Permalink
📦 Release 1.1.1 (#20)
Browse files Browse the repository at this point in the history
* 🤖 Improved publish CI/CD

* Support for session engines (#19)

* ⚙️ Added support for session engines other than db

* 📖 Improved docs for MESSAGES_USE_SESSIONS

* 📦 Release 1.1.1
  • Loading branch information
danyi1212 authored Jan 1, 2022
1 parent e9bb188 commit c44fd99
Show file tree
Hide file tree
Showing 12 changed files with 150 additions and 18 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/django.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-to-test-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions docs/source/reference/change_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----

Expand Down
29 changes: 21 additions & 8 deletions docs/source/reference/settings_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://docs.djangoproject.com/en/dev/ref/settings/#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 <https://docs.djangoproject.com/en/3.2/topics/http/sessions/#clearing-the-session-store>`_ 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 <https://github.com/martinrusev/django-redis-sessions>`_)


MESSAGES_ALLOW_DELETE_UNREAD
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
2 changes: 1 addition & 1 deletion drf_messages/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


__title__ = "DRF Messages"
__version__ = "1.1.0"
__version__ = "1.1.1"
__author__ = "Dan Yishai"
__license__ = "BSD 3-Clause"

Expand Down
29 changes: 29 additions & 0 deletions drf_messages/migrations/0002_message_session_key.py
Original file line number Diff line number Diff line change
@@ -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'),
),
]
18 changes: 14 additions & 4 deletions drf_messages/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.")

Expand Down
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
48 changes: 47 additions & 1 deletion testproj/demo/tests.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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)

Expand Down Expand Up @@ -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)
3 changes: 2 additions & 1 deletion testproj/requirements-github.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
drf-yasg
django-debug-toolbar
django-filter
factory-boy
factory-boy
django-redis-sessions
6 changes: 5 additions & 1 deletion testproj/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions testproj/testproj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

0 comments on commit c44fd99

Please sign in to comment.