├── .editorconfig ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── .gitignore ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── setup.cfg ├── setup.py ├── tests ├── manage.py └── testapp │ ├── __init__.py │ ├── settings.py │ ├── templates │ ├── 404.html │ └── base.html │ ├── test_messages.py │ └── urls.py ├── tox.ini └── user_messages ├── __init__.py ├── admin.py ├── api.py ├── apps.py ├── context_processors.py ├── locale └── de │ └── LC_MESSAGES │ ├── django.mo │ └── django.po ├── migrations ├── 0001_squashed_0003_alter_message_index_together_and_more.py └── __init__.py └── models.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.py] 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tests: 11 | name: Python ${{ matrix.python-version }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: 17 | - "3.8" 18 | - "3.9" 19 | - "3.10" 20 | - "3.11" 21 | - "3.12" 22 | - "3.13" 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip wheel setuptools tox 33 | - name: Run tox targets for ${{ matrix.python-version }} 34 | run: | 35 | ENV_PREFIX=$(tr -C -d "0-9" <<< "${{ matrix.python-version }}") 36 | TOXENV=$(tox --listenvs | grep "^py$ENV_PREFIX" | tr '\n' ',') python -m tox 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py? 2 | *.sw? 3 | *~ 4 | .coverage 5 | .tox 6 | /*.egg-info 7 | /MANIFEST 8 | build 9 | dist 10 | htmlcov 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: ".yarn/|yarn.lock|\\.min\\.(css|js)$" 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.6.0 5 | hooks: 6 | - id: check-added-large-files 7 | - id: check-builtin-literals 8 | - id: check-executables-have-shebangs 9 | - id: check-merge-conflict 10 | - id: check-toml 11 | - id: check-yaml 12 | - id: detect-private-key 13 | - id: end-of-file-fixer 14 | - id: mixed-line-ending 15 | - id: trailing-whitespace 16 | - repo: https://github.com/adamchainz/django-upgrade 17 | rev: 1.21.0 18 | hooks: 19 | - id: django-upgrade 20 | args: [--target-version, "3.2"] 21 | - repo: https://github.com/MarcoGorelli/absolufy-imports 22 | rev: v0.3.1 23 | hooks: 24 | - id: absolufy-imports 25 | - repo: https://github.com/astral-sh/ruff-pre-commit 26 | rev: "v0.6.4" 27 | hooks: 28 | - id: ruff 29 | - id: ruff-format 30 | - repo: https://github.com/pre-commit/mirrors-prettier 31 | rev: v3.1.0 32 | hooks: 33 | - id: prettier 34 | args: [--list-different, --no-semi] 35 | exclude: "^conf/|.*\\.html$" 36 | - repo: https://github.com/tox-dev/pyproject-fmt 37 | rev: 2.2.3 38 | hooks: 39 | - id: pyproject-fmt 40 | - repo: https://github.com/abravalheri/validate-pyproject 41 | rev: v0.19 42 | hooks: 43 | - id: validate-pyproject 44 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | version: 2 5 | 6 | build: 7 | os: ubuntu-24.04 8 | tools: 9 | python: "3.11" 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | # python: 14 | # install: 15 | # - requirements: docs/requirements.txt 16 | # - method: pip 17 | # path: . 18 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Change log 3 | ========== 4 | 5 | Next version 6 | ============ 7 | 8 | - Added Django 5.2, Python 3.13. 9 | 10 | 11 | 1.1 (2024-09-11) 12 | ================ 13 | 14 | - Added Django 5.0, 5.1 compatibility. 15 | - Squashed migrations to remove uses of ``index_together`` which wasn't liked 16 | by newer versions of Django. 17 | 18 | 19 | `1.0`_ (2022-09-25) 20 | =================== 21 | 22 | .. _1.0: https://github.com/matthiask/django-user-messages/compare/0.8...1.0 23 | 24 | - Added pre-commit. 25 | - Raised the version requirements to Python >= 3.8, Django >= 3.2. 26 | - Replaced the system check with a version which only cares about adding our 27 | context processor *after* the default messages context processor and about 28 | nothing else. 29 | 30 | 31 | `0.8`_ (2022-02-03) 32 | =================== 33 | 34 | - Switched to a declarative setup. 35 | - Switched from Travis CI to GitHub actions. 36 | - Added a composite index to reduce database load when fetching messages. 37 | Thanks @tpatterson! 38 | 39 | 40 | `0.7`_ (2020-01-22) 41 | =================== 42 | 43 | - Fixed a crash because of unicode strings being returned from 44 | ``Message.__str__`` in Python 2. 45 | - Order messages upon retrieval. 46 | - Added Django 3.0 to the Travis CI matrix. 47 | - Replaced ``ugettext*`` with ``gettext*`` to avoid deprecation 48 | warnings. 49 | 50 | 51 | `0.6`_ (2018-09-26) 52 | =================== 53 | 54 | - Reformatted the code using black. 55 | - Added a hint about silencing the messages context processor system 56 | check under Django 2.2. 57 | 58 | 59 | `0.5`_ (2018-03-04) 60 | =================== 61 | 62 | - Added german translations and a nice app name. 63 | - Changed the implementation of keyword-only arguments to be compatible 64 | with Python 2. 65 | 66 | 67 | `0.4`_ (2017-07-19) 68 | =================== 69 | 70 | - **Backwards incompatible** Rebuilt the model to not use Django's 71 | ``JSONField`` at all. This design decision unnecessarily restricted 72 | the areas where django-user-messages was usable. 73 | - Fixed properties to be more forgiving with missing data. 74 | - Added tox configuration for running tests and coding style checks and for 75 | building the docs. 76 | - Improved documentation and test coverage. 77 | 78 | 79 | `0.3`_ (2017-05-18) 80 | =================== 81 | 82 | - Added usage instructions. 83 | - Merge the ``message`` and ``meta`` JSON fields into a single ``data`` 84 | field and imitate the ``Message`` object interface more closely. 85 | 86 | 87 | `0.2`_ (2017-05-18) 88 | =================== 89 | 90 | - Added the possibility to associate additional data with a message py 91 | passing a dictionary as the ``meta`` keyword-only argument to the API. 92 | - Changed the module to import the ``Message`` model as late as possible 93 | so that the API can easily be imported for example in a ``AppConfig`` 94 | module. 95 | 96 | 97 | `0.1`_ (2017-05-17) 98 | =================== 99 | 100 | - Initial public release. 101 | 102 | .. _django-user-messages: https://django-user-messages.readthedocs.io/ 103 | 104 | .. _0.1: https://github.com/matthiask/django-user-messages/commit/3a9c0e329e 105 | .. _0.2: https://github.com/matthiask/django-user-messages/compare/0.1...0.2 106 | .. _0.3: https://github.com/matthiask/django-user-messages/compare/0.2...0.3 107 | .. _0.4: https://github.com/matthiask/django-user-messages/compare/0.3...0.4 108 | .. _0.5: https://github.com/matthiask/django-user-messages/compare/0.4...0.5 109 | .. _0.6: https://github.com/matthiask/django-user-messages/compare/0.5...0.6 110 | .. _0.7: https://github.com/matthiask/django-user-messages/compare/0.6...0.7 111 | .. _0.8: https://github.com/matthiask/django-user-messages/compare/0.7...0.8 112 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 FEINHEIT AG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include MANIFEST.in 3 | include README.rst 4 | recursive-include user_messages *.html *.po *.mo *.js *.css 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================================================ 2 | django-user-messages - Offline addon for django.contrib.messages 3 | ================================================================ 4 | 5 | django-user-messages adds offline messaging support to Django's 6 | messaging framework. It achieves this by allowing to save messages in 7 | the database. The ``user_messages.api.get_messages`` utility and the 8 | ``user_messages.context_processors.messages`` context processor 9 | transparently concatenate Django's messages and our own messages in a single 10 | list, therefore existing code works without any changes and without 11 | causing database writes. django-user-messages' functions have to be 12 | used explicitly. I consider this a feature, not a bug. 13 | 14 | 15 | Installation 16 | ============ 17 | 18 | - Install ``django-user-messages`` using pip into your virtualenv. 19 | - Add ``user_messages`` to ``INSTALLED_APPS`` and run ``migrate``. 20 | - Add the ``user_messages.context_processors.messages`` message processor 21 | somewhere *after* the default messages processor. Django's admin app checks 22 | for the presence of the latter so you cannot simply remove it (except if you 23 | want to silence the ``"admin.E404"`` system check). 24 | - Use ``user_messages.api`` as you would use 25 | ``django.contrib.messages`` except that you pass the user model or ID 26 | as first parameter, not the current request. 27 | 28 | 29 | Usage 30 | ===== 31 | 32 | Pretty much the same as Django's messaging framework:: 33 | 34 | from user_messages import api 35 | 36 | api.info(user, 'Hey there') 37 | api.warning(user, 'Stop this') 38 | api.error(user, 'Not nice!') 39 | 40 | # Passing the ID is also possible; the user instance does not 41 | # have to be instantiated at all: 42 | api.success(user.id, 'Yay!') 43 | 44 | django-user-messages' messages supports two additional features not 45 | available in Django's messages framework: 46 | 47 | - Messages can be delivered more than once by passing 48 | ``deliver_once=False``. These messages have to be acknowledged 49 | explicitly. django-user-messages does not contain any code to do this. 50 | - It is possible to attach additional data by passing a dictionary as 51 | ``meta``:: 52 | 53 | api.debug(user, 'Oww', meta={ 54 | 'url': 'http://example.com', 55 | }) 56 | 57 | For convenience, our messages have the same ``tags`` and ``level_tag`` 58 | properties as Django's messages. Meta properties are also accessible in 59 | templates:: 60 | 61 | {% if messages %} 62 | 71 | {% endif %} 72 | 73 | django-user-messages' messages are also evaluated lazily. 74 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = test 8 | SOURCEDIR = . 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import subprocess 4 | import sys 5 | 6 | 7 | sys.path.append(os.path.abspath("..")) 8 | 9 | project = "django-user-messages" 10 | author = "Feinheit AG" 11 | copyright = "2017," + author 12 | version = __import__("user_messages").__version__ 13 | release = subprocess.check_output( 14 | "git fetch --tags; git describe", shell=True, text=True 15 | ).strip() 16 | language = "en" 17 | 18 | ####################################### 19 | project_slug = re.sub(r"[^a-z]+", "", project) 20 | 21 | extensions = [] 22 | templates_path = ["_templates"] 23 | source_suffix = ".rst" 24 | master_doc = "index" 25 | 26 | exclude_patterns = ["build", "Thumbs.db", ".DS_Store"] 27 | pygments_style = "sphinx" 28 | todo_include_todos = False 29 | 30 | html_theme = "alabaster" 31 | html_static_path = ["_static"] 32 | htmlhelp_basename = project_slug + "doc" 33 | 34 | latex_elements = { 35 | "papersize": "a4", 36 | } 37 | latex_documents = [ 38 | ( 39 | master_doc, 40 | project_slug + ".tex", 41 | project + " Documentation", 42 | author, 43 | "manual", 44 | ) 45 | ] 46 | man_pages = [ 47 | ( 48 | master_doc, 49 | project_slug, 50 | project + " Documentation", 51 | [author], 52 | 1, 53 | ) 54 | ] 55 | texinfo_documents = [ 56 | ( 57 | master_doc, 58 | project_slug, 59 | project + " Documentation", 60 | author, 61 | project_slug, 62 | "", # Description 63 | "Miscellaneous", 64 | ) 65 | ] 66 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | .. include:: ../CHANGELOG.rst 3 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=build 12 | set SPHINXPROJ=test 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django_user_messages 3 | version = attr: user_messages.__version__ 4 | description = Offline addon for django.contrib.messages 5 | long_description = file: README.rst 6 | long_description_content_type = text/x-rst 7 | url = https://github.com/matthiask/django-user-messages/ 8 | author = Matthias Kestenholz 9 | author_email = mk@feinheit.ch 10 | license = MIT-License 11 | license_file = LICENSE 12 | platforms = OS Independent 13 | classifiers = 14 | Development Status :: 5 - Production/Stable 15 | Environment :: Web Environment 16 | Framework :: Django 17 | Intended Audience :: Developers 18 | License :: OSI Approved :: BSD License 19 | Operating System :: OS Independent 20 | Programming Language :: Python 21 | Programming Language :: Python :: 3 22 | Programming Language :: Python :: 3.8 23 | Programming Language :: Python :: 3.9 24 | Programming Language :: Python :: 3.10 25 | Programming Language :: Python :: 3.11 26 | Programming Language :: Python :: 3.12 27 | Topic :: Internet :: WWW/HTTP :: Dynamic Content 28 | Topic :: Software Development 29 | Topic :: Software Development :: Libraries :: Application Frameworks 30 | 31 | [options] 32 | packages = find: 33 | python_requires = >=3.8 34 | include_package_data = True 35 | zip_safe = False 36 | 37 | [options.extras_require] 38 | tests = 39 | coverage 40 | 41 | [options.packages.find] 42 | exclude = 43 | tests 44 | tests.* 45 | 46 | [coverage:run] 47 | branch = True 48 | include = 49 | *user_messages* 50 | omit = 51 | *migrations* 52 | *tests* 53 | *.tox* 54 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from setuptools import setup 3 | 4 | 5 | setup() 6 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from os.path import abspath, dirname 5 | 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") 9 | 10 | sys.path.insert(0, dirname(dirname(abspath(__file__)))) 11 | 12 | from django.core.management import execute_from_command_line 13 | 14 | execute_from_command_line(sys.argv) 15 | -------------------------------------------------------------------------------- /tests/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiask/django-user-messages/de8e834b53bde476795e198d13cba34d755ee6cb/tests/testapp/__init__.py -------------------------------------------------------------------------------- /tests/testapp/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} 5 | 6 | INSTALLED_APPS = [ 7 | "django.contrib.auth", 8 | "django.contrib.contenttypes", 9 | "django.contrib.sessions", 10 | "django.contrib.staticfiles", 11 | "django.contrib.messages", 12 | "django.contrib.admin", 13 | "testapp", 14 | "user_messages", 15 | ] 16 | 17 | MEDIA_ROOT = "/media/" 18 | STATIC_URL = "/static/" 19 | BASEDIR = os.path.dirname(__file__) 20 | MEDIA_ROOT = os.path.join(BASEDIR, "media/") 21 | STATIC_ROOT = os.path.join(BASEDIR, "static/") 22 | SECRET_KEY = "supersikret" 23 | 24 | ROOT_URLCONF = "testapp.urls" 25 | LANGUAGES = (("en", "English"),) 26 | 27 | TEMPLATES = [ 28 | { 29 | "BACKEND": "django.template.backends.django.DjangoTemplates", 30 | "DIRS": [], 31 | "APP_DIRS": True, 32 | "OPTIONS": { 33 | "context_processors": [ 34 | "django.template.context_processors.debug", 35 | "django.template.context_processors.request", 36 | "django.contrib.auth.context_processors.auth", 37 | "user_messages.context_processors.messages", 38 | ] 39 | }, 40 | } 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | "django.middleware.security.SecurityMiddleware", 45 | "django.contrib.sessions.middleware.SessionMiddleware", 46 | "django.middleware.common.CommonMiddleware", 47 | "django.middleware.locale.LocaleMiddleware", 48 | "django.middleware.csrf.CsrfViewMiddleware", 49 | "django.contrib.auth.middleware.AuthenticationMiddleware", 50 | "django.contrib.messages.middleware.MessageMiddleware", 51 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 52 | ] 53 | 54 | SILENCED_SYSTEM_CHECKS = ["admin.E404"] 55 | -------------------------------------------------------------------------------- /tests/testapp/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

Page not found

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /tests/testapp/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}testapp{% endblock %} 5 | 6 | 7 |

Hi, {{ user }}

8 | {% if messages %} 9 | 14 | {% endif %} 15 | {% block content %}{% endblock %} 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/testapp/test_messages.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import Client, TestCase 3 | 4 | from user_messages import api 5 | from user_messages.models import Message 6 | 7 | 8 | def _messages(response): 9 | return [m.message for m in response.context["messages"]] 10 | 11 | 12 | class MessagesTestCase(TestCase): 13 | def test_messages(self): 14 | user = User.objects.create_user("test", "test@example.com", "test") 15 | 16 | client = Client() 17 | client.force_login(user) 18 | 19 | api.error(user, "Hello world") 20 | self.assertContains(client.get("/"), '
  • Hello world
  • ', 1) 21 | self.assertContains(client.get("/"), '
  • Hello world
  • ', 0) 22 | 23 | anonymous = Client() 24 | api.error(user, "Hello world 2") 25 | 26 | self.assertNotContains(anonymous.get("/"), '