├── .codeclimate.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── messages_tests ├── __init__.py ├── base.py ├── test_session.py └── urls.py ├── offline_messages ├── __init__.py ├── admin.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── south_migrations │ ├── 0001_initial.py │ ├── 0002_auto__add_field_offlinemessage_level.py │ ├── 0003_auto__add_field_offlinemessage_tags.py │ ├── 0004_auto__del_field_offlinemessage_tags.py │ ├── 0005_auto__add_field_offlinemessage_read__add_field_offlinemessage_content_.py │ └── __init__.py ├── storage.py └── utils.py ├── setup.py └── tests ├── __init__.py ├── models.py ├── runtests.py ├── settings.py └── tests.py /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # Save as .codeclimate.yml (note leading .) in project root directory 2 | languages: 3 | Python: true 4 | exclude_paths: 5 | - "messages_tests/*" 6 | - "tests/*" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | *.pyc 4 | *~ 5 | *.egg 6 | 7 | build/ 8 | django_offline_messages.egg-info/* 9 | dist/* 10 | venv/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | 8 | env: 9 | - DJANGO="Django>=1.8,<1.9" 10 | - DJANGO="Django>=1.7,<1.8" 11 | - DJANGO="Django>=1.6,<1.7" 12 | - DJANGO="Django>=1.5,<1.6" 13 | - DJANGO="Django>=1.4,<1.5" 14 | 15 | install: 16 | - pip install $DJANGO django-jsonfield 17 | - pip install coveralls 18 | 19 | script: 20 | coverage run --source=offline_messages tests/runtests.py 21 | 22 | after_success: 23 | coveralls 24 | 25 | matrix: 26 | exclude: 27 | - python: "3.3" 28 | env: DJANGO="Django>=1.4,<1.5" 29 | - python: "3.4" 30 | env: DJANGO="Django>=1.4,<1.5" 31 | 32 | sudo: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, oDesk http://www.odesk.com 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of oDesk nor the names of its contributors may 13 | be used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: 2 | https://travis-ci.org/dym/django-offline-messages.png 3 | :target: https://travis-ci.org/dym/django-offline-messages 4 | 5 | .. image:: 6 | https://coveralls.io/repos/dym/django-offline-messages/badge.svg?branch=master&service=github 7 | :target: https://coveralls.io/github/dym/django-offline-messages?branch=master 8 | 9 | .. image:: https://codeclimate.com/github/dym/django-offline-messages/badges/gpa.svg 10 | :target: https://codeclimate.com/github/dym/django-offline-messages 11 | :alt: Code Climate 12 | 13 | ========================= 14 | Installation Instructions 15 | ========================= 16 | 17 | Make changes to your settings: 18 | 19 | * Add 'offline_messages' to INSTALLED_APPS 20 | 21 | * Set MESSAGE_STORAGE to 'offline_messages.storage.OfflineStorageEngine' 22 | 23 | 24 | ========================= 25 | About 26 | ========================= 27 | 28 | This is a slightly modified version of the excellent and simple `offline_messages` package. It 29 | includes generic foreign keys plus extra meta information. This is a specific implementation 30 | for Zapier as we have tons of feedback points, but its easy to confuse the bejesus out of our 31 | customers because important error messages disappear for good. 32 | 33 | So basically this adds: 34 | 35 | 1. Persistent history of messages. 36 | 2. Generic foreign keys to attach messages to specific objects (any model, any record). 37 | 3. The ability to store even more meta data (EG: the parameters that caused the message). 38 | 39 | Enjoy! 40 | 41 | 42 | ========================= 43 | Example Usage 44 | ========================= 45 | 46 | You can continue to use the standard Django message system as desired. Messages created like: 47 | 48 | from django.contrib import messages 49 | 50 | messages.add_message(request, messages.INFO, 'Hello world.') 51 | 52 | Will work just fine. However, if you'd like to create an offline message, do something like this: 53 | 54 | from offline_messages.utils import create_offline_message, constants 55 | 56 | create_offline_message(User.objects.get(id=1), "Hello there!", level=constants.WARNING) 57 | 58 | Or like this: 59 | 60 | from offline_messages.models import OfflineMessage 61 | 62 | OfflineMessage.objects.create(user=User.objects.get(id=1), level=20, message='Hello world.') 63 | 64 | Usage example from the real life:: 65 | 66 | # Iterate through users 67 | for user in User.objects.all(): 68 | already_notified = OfflineMessage.objects.filter(user=user, message=message).exists() 69 | if not already_notified: 70 | create_offline_message(user, message, level=constants.WARNING) 71 | 72 | =========================== 73 | Extra Functionality 74 | =========================== 75 | 76 | The idea behind utils is you can just do: 77 | 78 | from offline_messages import utils as messages 79 | 80 | In place of: 81 | 82 | from django.contrib import messages 83 | 84 | And still have access to boring old `messages.success(request, 'Good job!')` but 85 | also have access to be able to do things like... 86 | 87 | comment = Comment.objects.create(title='A test', message='Thanks!') 88 | 89 | messages.success(request, 'Comment posted!', content_object=comment, meta={'blah': 'blah'}) 90 | 91 | -------------------------------------------------------------------------------- /messages_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dym/django-offline-messages/c56d4dd4c0389531b0960821c4d5d9529403b4a5/messages_tests/__init__.py -------------------------------------------------------------------------------- /messages_tests/base.py: -------------------------------------------------------------------------------- 1 | from django import http 2 | from django.contrib.messages import constants, get_level, set_level, utils 3 | from django.contrib.messages.api import MessageFailure 4 | from django.contrib.messages.constants import DEFAULT_LEVELS 5 | from django.contrib.messages.storage import base, default_storage 6 | from django.contrib.messages.storage.base import Message 7 | from django.core.urlresolvers import reverse 8 | from django.test import modify_settings, override_settings 9 | from django.utils.translation import ugettext_lazy 10 | 11 | 12 | def add_level_messages(storage): 13 | """ 14 | Adds 6 messages from different levels (including a custom one) to a storage 15 | instance. 16 | """ 17 | storage.add(constants.INFO, 'A generic info message') 18 | storage.add(29, 'Some custom level') 19 | storage.add(constants.DEBUG, 'A debugging message', extra_tags='extra-tag') 20 | storage.add(constants.WARNING, 'A warning') 21 | storage.add(constants.ERROR, 'An error') 22 | storage.add(constants.SUCCESS, 'This was a triumph.') 23 | 24 | 25 | class override_settings_tags(override_settings): 26 | def enable(self): 27 | super(override_settings_tags, self).enable() 28 | # LEVEL_TAGS is a constant defined in the 29 | # django.contrib.messages.storage.base module, so after changing 30 | # settings.MESSAGE_TAGS, we need to update that constant too. 31 | self.old_level_tags = base.LEVEL_TAGS 32 | base.LEVEL_TAGS = utils.get_level_tags() 33 | 34 | def disable(self): 35 | super(override_settings_tags, self).disable() 36 | base.LEVEL_TAGS = self.old_level_tags 37 | 38 | 39 | class BaseTests(object): 40 | storage_class = default_storage 41 | levels = { 42 | 'debug': constants.DEBUG, 43 | 'info': constants.INFO, 44 | 'success': constants.SUCCESS, 45 | 'warning': constants.WARNING, 46 | 'error': constants.ERROR, 47 | } 48 | 49 | def setUp(self): 50 | self.settings_override = override_settings_tags( 51 | TEMPLATES=[{ 52 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 53 | 'DIRS': [], 54 | 'APP_DIRS': True, 55 | 'OPTIONS': { 56 | 'context_processors': ( 57 | 'django.contrib.auth.context_processors.auth', 58 | 'django.contrib.messages.context_processors.messages', 59 | ), 60 | }, 61 | }], 62 | ROOT_URLCONF='messages_tests.urls', 63 | MESSAGE_TAGS='', 64 | MESSAGE_STORAGE='%s.%s' % (self.storage_class.__module__, 65 | self.storage_class.__name__), 66 | SESSION_SERIALIZER='django.contrib.sessions.serializers.JSONSerializer', 67 | ) 68 | self.settings_override.enable() 69 | 70 | def tearDown(self): 71 | self.settings_override.disable() 72 | 73 | def get_request(self): 74 | return http.HttpRequest() 75 | 76 | def get_response(self): 77 | return http.HttpResponse() 78 | 79 | def get_storage(self, data=None): 80 | """ 81 | Returns the storage backend, setting its loaded data to the ``data`` 82 | argument. 83 | 84 | This method avoids the storage ``_get`` method from getting called so 85 | that other parts of the storage backend can be tested independent of 86 | the message retrieval logic. 87 | """ 88 | storage = self.storage_class(self.get_request()) 89 | storage._loaded_data = data or [] 90 | return storage 91 | 92 | def test_add(self): 93 | storage = self.get_storage() 94 | self.assertFalse(storage.added_new) 95 | storage.add(constants.INFO, 'Test message 1') 96 | self.assertTrue(storage.added_new) 97 | storage.add(constants.INFO, 'Test message 2', extra_tags='tag') 98 | self.assertEqual(len(storage), 2) 99 | 100 | def test_add_lazy_translation(self): 101 | storage = self.get_storage() 102 | response = self.get_response() 103 | 104 | storage.add(constants.INFO, ugettext_lazy('lazy message')) 105 | storage.update(response) 106 | 107 | storing = self.stored_messages_count(storage, response) 108 | self.assertEqual(storing, 1) 109 | 110 | def test_no_update(self): 111 | storage = self.get_storage() 112 | response = self.get_response() 113 | storage.update(response) 114 | storing = self.stored_messages_count(storage, response) 115 | self.assertEqual(storing, 0) 116 | 117 | def test_add_update(self): 118 | storage = self.get_storage() 119 | response = self.get_response() 120 | 121 | storage.add(constants.INFO, 'Test message 1') 122 | storage.add(constants.INFO, 'Test message 1', extra_tags='tag') 123 | storage.update(response) 124 | 125 | storing = self.stored_messages_count(storage, response) 126 | self.assertEqual(storing, 2) 127 | 128 | def test_existing_add_read_update(self): 129 | storage = self.get_existing_storage() 130 | response = self.get_response() 131 | 132 | storage.add(constants.INFO, 'Test message 3') 133 | list(storage) # Simulates a read 134 | storage.update(response) 135 | 136 | storing = self.stored_messages_count(storage, response) 137 | self.assertEqual(storing, 0) 138 | 139 | def test_existing_read_add_update(self): 140 | storage = self.get_existing_storage() 141 | response = self.get_response() 142 | 143 | list(storage) # Simulates a read 144 | storage.add(constants.INFO, 'Test message 3') 145 | storage.update(response) 146 | 147 | storing = self.stored_messages_count(storage, response) 148 | self.assertEqual(storing, 1) 149 | 150 | @override_settings(MESSAGE_LEVEL=constants.DEBUG) 151 | def test_full_request_response_cycle(self): 152 | """ 153 | With the message middleware enabled, tests that messages are properly 154 | stored and then retrieved across the full request/redirect/response 155 | cycle. 156 | """ 157 | data = { 158 | 'messages': ['Test message %d' % x for x in range(5)], 159 | } 160 | show_url = reverse('show_message') 161 | for level in ('debug', 'info', 'success', 'warning', 'error'): 162 | add_url = reverse('add_message', args=(level,)) 163 | response = self.client.post(add_url, data, follow=True) 164 | self.assertRedirects(response, show_url) 165 | self.assertIn('messages', response.context) 166 | messages = [Message(self.levels[level], msg) for msg in data['messages']] 167 | self.assertEqual(list(response.context['messages']), messages) 168 | for msg in data['messages']: 169 | self.assertContains(response, msg) 170 | 171 | @override_settings(MESSAGE_LEVEL=constants.DEBUG) 172 | def test_with_template_response(self): 173 | data = { 174 | 'messages': ['Test message %d' % x for x in range(5)], 175 | } 176 | show_url = reverse('show_template_response') 177 | for level in self.levels.keys(): 178 | add_url = reverse('add_template_response', args=(level,)) 179 | response = self.client.post(add_url, data, follow=True) 180 | self.assertRedirects(response, show_url) 181 | self.assertIn('messages', response.context) 182 | for msg in data['messages']: 183 | self.assertContains(response, msg) 184 | 185 | # there shouldn't be any messages on second GET request 186 | response = self.client.get(show_url) 187 | for msg in data['messages']: 188 | self.assertNotContains(response, msg) 189 | 190 | def test_context_processor_message_levels(self): 191 | show_url = reverse('show_template_response') 192 | response = self.client.get(show_url) 193 | 194 | self.assertIn('DEFAULT_MESSAGE_LEVELS', response.context) 195 | self.assertEqual(response.context['DEFAULT_MESSAGE_LEVELS'], DEFAULT_LEVELS) 196 | 197 | @override_settings(MESSAGE_LEVEL=constants.DEBUG) 198 | def test_multiple_posts(self): 199 | """ 200 | Tests that messages persist properly when multiple POSTs are made 201 | before a GET. 202 | """ 203 | data = { 204 | 'messages': ['Test message %d' % x for x in range(5)], 205 | } 206 | show_url = reverse('show_message') 207 | messages = [] 208 | for level in ('debug', 'info', 'success', 'warning', 'error'): 209 | messages.extend(Message(self.levels[level], msg) for msg in data['messages']) 210 | add_url = reverse('add_message', args=(level,)) 211 | self.client.post(add_url, data) 212 | response = self.client.get(show_url) 213 | self.assertIn('messages', response.context) 214 | self.assertEqual(list(response.context['messages']), messages) 215 | for msg in data['messages']: 216 | self.assertContains(response, msg) 217 | 218 | @modify_settings( 219 | INSTALLED_APPS={'remove': 'django.contrib.messages'}, 220 | MIDDLEWARE_CLASSES={'remove': 'django.contrib.messages.middleware.MessageMiddleware'}, 221 | ) 222 | @override_settings( 223 | MESSAGE_LEVEL=constants.DEBUG, 224 | TEMPLATES=[{ 225 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 226 | 'DIRS': [], 227 | 'APP_DIRS': True, 228 | }], 229 | ) 230 | def test_middleware_disabled(self): 231 | """ 232 | Tests that, when the middleware is disabled, an exception is raised 233 | when one attempts to store a message. 234 | """ 235 | data = { 236 | 'messages': ['Test message %d' % x for x in range(5)], 237 | } 238 | reverse('show_message') 239 | for level in ('debug', 'info', 'success', 'warning', 'error'): 240 | add_url = reverse('add_message', args=(level,)) 241 | self.assertRaises(MessageFailure, self.client.post, add_url, 242 | data, follow=True) 243 | 244 | @modify_settings( 245 | INSTALLED_APPS={'remove': 'django.contrib.messages'}, 246 | MIDDLEWARE_CLASSES={'remove': 'django.contrib.messages.middleware.MessageMiddleware'}, 247 | ) 248 | @override_settings( 249 | TEMPLATES=[{ 250 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 251 | 'DIRS': [], 252 | 'APP_DIRS': True, 253 | }], 254 | ) 255 | def test_middleware_disabled_fail_silently(self): 256 | """ 257 | Tests that, when the middleware is disabled, an exception is not 258 | raised if 'fail_silently' = True 259 | """ 260 | data = { 261 | 'messages': ['Test message %d' % x for x in range(5)], 262 | 'fail_silently': True, 263 | } 264 | show_url = reverse('show_message') 265 | for level in ('debug', 'info', 'success', 'warning', 'error'): 266 | add_url = reverse('add_message', args=(level,)) 267 | response = self.client.post(add_url, data, follow=True) 268 | self.assertRedirects(response, show_url) 269 | self.assertNotIn('messages', response.context) 270 | 271 | def stored_messages_count(self, storage, response): 272 | """ 273 | Returns the number of messages being stored after a 274 | ``storage.update()`` call. 275 | """ 276 | raise NotImplementedError('This method must be set by a subclass.') 277 | 278 | def test_get(self): 279 | raise NotImplementedError('This method must be set by a subclass.') 280 | 281 | def get_existing_storage(self): 282 | return self.get_storage([Message(constants.INFO, 'Test message 1'), 283 | Message(constants.INFO, 'Test message 2', 284 | extra_tags='tag')]) 285 | 286 | def test_existing_read(self): 287 | """ 288 | Tests that reading the existing storage doesn't cause the data to be 289 | lost. 290 | """ 291 | storage = self.get_existing_storage() 292 | self.assertFalse(storage.used) 293 | # After iterating the storage engine directly, the used flag is set. 294 | data = list(storage) 295 | self.assertTrue(storage.used) 296 | # The data does not disappear because it has been iterated. 297 | self.assertEqual(data, list(storage)) 298 | 299 | def test_existing_add(self): 300 | storage = self.get_existing_storage() 301 | self.assertFalse(storage.added_new) 302 | storage.add(constants.INFO, 'Test message 3') 303 | self.assertTrue(storage.added_new) 304 | 305 | def test_default_level(self): 306 | # get_level works even with no storage on the request. 307 | request = self.get_request() 308 | self.assertEqual(get_level(request), constants.INFO) 309 | 310 | # get_level returns the default level if it hasn't been set. 311 | storage = self.get_storage() 312 | request._messages = storage 313 | self.assertEqual(get_level(request), constants.INFO) 314 | 315 | # Only messages of sufficient level get recorded. 316 | add_level_messages(storage) 317 | self.assertEqual(len(storage), 5) 318 | 319 | def test_low_level(self): 320 | request = self.get_request() 321 | storage = self.storage_class(request) 322 | request._messages = storage 323 | 324 | self.assertTrue(set_level(request, 5)) 325 | self.assertEqual(get_level(request), 5) 326 | 327 | add_level_messages(storage) 328 | self.assertEqual(len(storage), 6) 329 | 330 | def test_high_level(self): 331 | request = self.get_request() 332 | storage = self.storage_class(request) 333 | request._messages = storage 334 | 335 | self.assertTrue(set_level(request, 30)) 336 | self.assertEqual(get_level(request), 30) 337 | 338 | add_level_messages(storage) 339 | self.assertEqual(len(storage), 2) 340 | 341 | @override_settings(MESSAGE_LEVEL=29) 342 | def test_settings_level(self): 343 | request = self.get_request() 344 | storage = self.storage_class(request) 345 | 346 | self.assertEqual(get_level(request), 29) 347 | 348 | add_level_messages(storage) 349 | self.assertEqual(len(storage), 3) 350 | 351 | def test_tags(self): 352 | storage = self.get_storage() 353 | storage.level = 0 354 | add_level_messages(storage) 355 | tags = [msg.tags for msg in storage] 356 | self.assertEqual(tags, 357 | ['info', '', 'extra-tag debug', 'warning', 'error', 358 | 'success']) 359 | 360 | def test_level_tag(self): 361 | storage = self.get_storage() 362 | storage.level = 0 363 | add_level_messages(storage) 364 | tags = [msg.level_tag for msg in storage] 365 | self.assertEqual(tags, 366 | ['info', '', 'debug', 'warning', 'error', 367 | 'success']) 368 | 369 | @override_settings_tags(MESSAGE_TAGS={ 370 | constants.INFO: 'info', 371 | constants.DEBUG: '', 372 | constants.WARNING: '', 373 | constants.ERROR: 'bad', 374 | 29: 'custom', 375 | } 376 | ) 377 | def test_custom_tags(self): 378 | storage = self.get_storage() 379 | storage.level = 0 380 | add_level_messages(storage) 381 | tags = [msg.tags for msg in storage] 382 | self.assertEqual(tags, 383 | ['info', 'custom', 'extra-tag', '', 'bad', 'success']) 384 | -------------------------------------------------------------------------------- /messages_tests/test_session.py: -------------------------------------------------------------------------------- 1 | from django.contrib.messages import constants 2 | from django.contrib.messages.storage.base import Message 3 | from django.contrib.messages.storage.session import SessionStorage 4 | from django.test import TestCase 5 | from django.utils.safestring import SafeData, mark_safe 6 | 7 | from .base import BaseTests 8 | 9 | 10 | def set_session_data(storage, messages): 11 | """ 12 | Sets the messages into the backend request's session and remove the 13 | backend's loaded data cache. 14 | """ 15 | storage.request.session[storage.session_key] = storage.serialize_messages(messages) 16 | if hasattr(storage, '_loaded_data'): 17 | del storage._loaded_data 18 | 19 | 20 | def stored_session_messages_count(storage): 21 | data = storage.deserialize_messages(storage.request.session.get(storage.session_key, [])) 22 | return len(data) 23 | 24 | 25 | class SessionTest(BaseTests, TestCase): 26 | storage_class = SessionStorage 27 | 28 | def get_request(self): 29 | self.session = {} 30 | request = super(SessionTest, self).get_request() 31 | request.session = self.session 32 | return request 33 | 34 | def stored_messages_count(self, storage, response): 35 | return stored_session_messages_count(storage) 36 | 37 | def test_get(self): 38 | storage = self.storage_class(self.get_request()) 39 | # Set initial data. 40 | example_messages = ['test', 'me'] 41 | set_session_data(storage, example_messages) 42 | # Test that the message actually contains what we expect. 43 | self.assertEqual(list(storage), example_messages) 44 | 45 | def test_safedata(self): 46 | """ 47 | Tests that a message containing SafeData is keeping its safe status when 48 | retrieved from the message storage. 49 | """ 50 | storage = self.get_storage() 51 | 52 | message = Message(constants.DEBUG, mark_safe("Hello Django!")) 53 | set_session_data(storage, [message]) 54 | self.assertIsInstance(list(storage)[0].message, SafeData) 55 | -------------------------------------------------------------------------------- /messages_tests/urls.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.conf.urls import url 3 | from django.contrib import messages 4 | from django.contrib.messages.views import SuccessMessageMixin 5 | from django.core.urlresolvers import reverse 6 | from django.http import HttpResponse, HttpResponseRedirect 7 | from django.template import engines 8 | from django.template.response import TemplateResponse 9 | from django.views.decorators.cache import never_cache 10 | from django.views.generic.edit import FormView 11 | 12 | 13 | TEMPLATE = """{% if messages %} 14 |
21 | {% endif %} 22 | """ 23 | 24 | 25 | @never_cache 26 | def add(request, message_type): 27 | # don't default to False here, because we want to test that it defaults 28 | # to False if unspecified 29 | fail_silently = request.POST.get('fail_silently', None) 30 | for msg in request.POST.getlist('messages'): 31 | if fail_silently is not None: 32 | getattr(messages, message_type)(request, msg, 33 | fail_silently=fail_silently) 34 | else: 35 | getattr(messages, message_type)(request, msg) 36 | 37 | show_url = reverse('show_message') 38 | return HttpResponseRedirect(show_url) 39 | 40 | 41 | @never_cache 42 | def add_template_response(request, message_type): 43 | for msg in request.POST.getlist('messages'): 44 | getattr(messages, message_type)(request, msg) 45 | 46 | show_url = reverse('show_template_response') 47 | return HttpResponseRedirect(show_url) 48 | 49 | 50 | @never_cache 51 | def show(request): 52 | template = engines['django'].from_string(TEMPLATE) 53 | return HttpResponse(template.render(request=request)) 54 | 55 | 56 | @never_cache 57 | def show_template_response(request): 58 | template = engines['django'].from_string(TEMPLATE) 59 | return TemplateResponse(request, template) 60 | 61 | 62 | class ContactForm(forms.Form): 63 | name = forms.CharField(required=True) 64 | slug = forms.SlugField(required=True) 65 | 66 | 67 | class ContactFormViewWithMsg(SuccessMessageMixin, FormView): 68 | form_class = ContactForm 69 | success_url = show 70 | success_message = "%(name)s was created successfully" 71 | 72 | 73 | urlpatterns = [ 74 | url('^add/(debug|info|success|warning|error)/$', add, name='add_message'), 75 | url('^add/msg/$', ContactFormViewWithMsg.as_view(), name='add_success_msg'), 76 | url('^show/$', show, name='show_message'), 77 | url('^template_response/add/(debug|info|success|warning|error)/$', 78 | add_template_response, name='add_template_response'), 79 | url('^template_response/show/$', show_template_response, name='show_template_response'), 80 | ] 81 | -------------------------------------------------------------------------------- /offline_messages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dym/django-offline-messages/c56d4dd4c0389531b0960821c4d5d9529403b4a5/offline_messages/__init__.py -------------------------------------------------------------------------------- /offline_messages/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from offline_messages.models import OfflineMessage 4 | 5 | class OfflineMessageAdmin(admin.ModelAdmin): 6 | list_display = [f.name for f in OfflineMessage._meta.fields] 7 | admin.site.register(OfflineMessage, OfflineMessageAdmin) -------------------------------------------------------------------------------- /offline_messages/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import jsonfield.fields 6 | from django.conf import settings 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('contenttypes', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='OfflineMessage', 19 | fields=[ 20 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 21 | ('level', models.IntegerField(default=20)), 22 | ('message', models.CharField(max_length=200)), 23 | ('created', models.DateTimeField(auto_now_add=True)), 24 | ('read', models.BooleanField(default=False)), 25 | ('object_id', models.PositiveIntegerField(null=True, blank=True)), 26 | ('meta', jsonfield.fields.JSONField(default={}, null=True, blank=True)), 27 | ('content_type', models.ForeignKey(blank=True, to='contenttypes.ContentType', null=True)), 28 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 29 | ], 30 | options={ 31 | }, 32 | bases=(models.Model,), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /offline_messages/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dym/django-offline-messages/c56d4dd4c0389531b0960821c4d5d9529403b4a5/offline_messages/migrations/__init__.py -------------------------------------------------------------------------------- /offline_messages/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; mode: python; -*- 2 | 3 | from django.db import models 4 | from django.conf import settings 5 | try: 6 | from django.utils.encoding import force_unicode 7 | except ImportError: 8 | from django.utils.encoding import force_text as force_unicode 9 | from django.contrib.messages import constants 10 | from django.contrib.messages.utils import get_level_tags 11 | 12 | from django.contrib.contenttypes.models import ContentType 13 | try: 14 | from django.contrib.contenttypes.fields import GenericForeignKey 15 | except ImportError: 16 | from django.contrib.contenttypes.generic import GenericForeignKey 17 | 18 | from jsonfield import JSONField 19 | 20 | AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') 21 | 22 | 23 | class OfflineMessageQuerySetManager(models.query.QuerySet): 24 | """ Provide easy to use filters for use in templates 25 | """ 26 | 27 | def info(self): 28 | return self.filter(level=constants.INFO) 29 | 30 | def debug(self): 31 | return self.filter(level=constants.DEBUG) 32 | 33 | def success(self): 34 | return self.filter(level=constants.SUCCESS) 35 | 36 | def warning(self): 37 | return self.filter(level=constants.WARNING) 38 | 39 | def error(self): 40 | return self.filter(level=constants.ERROR) 41 | 42 | def unread(self): 43 | return self.filter(read=False) 44 | 45 | 46 | class OfflineMessageManager(models.Manager): 47 | 48 | def get_queryset(self): 49 | return OfflineMessageQuerySetManager(self.model) 50 | 51 | def __getattr__(self, name): 52 | try: 53 | return getattr(self, name) 54 | except AttributeError: 55 | return getattr(self.get_queryset(), name) 56 | 57 | 58 | class OfflineMessage(models.Model): 59 | user = models.ForeignKey(AUTH_USER_MODEL) 60 | level = models.IntegerField(default=constants.INFO) 61 | message = models.CharField(max_length=200) 62 | created = models.DateTimeField(auto_now_add=True) 63 | 64 | read = models.BooleanField(default=False) 65 | 66 | content_type = models.ForeignKey(ContentType, blank=True, null=True) 67 | object_id = models.PositiveIntegerField(blank=True, null=True) 68 | content_object = GenericForeignKey('content_type', 'object_id') 69 | 70 | meta = JSONField(default={}, blank=True, null=True) 71 | 72 | objects = OfflineMessageManager() 73 | 74 | def __unicode__(self): 75 | return force_unicode(self.message) 76 | 77 | @property 78 | def tags(self): 79 | level_tags = get_level_tags() 80 | return force_unicode(level_tags.get(self.level, ''), strings_only=True) 81 | -------------------------------------------------------------------------------- /offline_messages/south_migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'OfflineMessage' 12 | db.create_table('offline_messages_offlinemessage', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), 15 | ('message', self.gf('django.db.models.fields.CharField')(max_length=200)), 16 | ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), 17 | )) 18 | db.send_create_signal('offline_messages', ['OfflineMessage']) 19 | 20 | 21 | def backwards(self, orm): 22 | 23 | # Deleting model 'OfflineMessage' 24 | db.delete_table('offline_messages_offlinemessage') 25 | 26 | 27 | models = { 28 | 'auth.group': { 29 | 'Meta': {'object_name': 'Group'}, 30 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 31 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 32 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 33 | }, 34 | 'auth.permission': { 35 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 36 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 37 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 38 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 39 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 40 | }, 41 | 'auth.user': { 42 | 'Meta': {'object_name': 'User'}, 43 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 44 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 45 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 46 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 47 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 48 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 49 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 50 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 51 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 52 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 53 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 54 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 55 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 56 | }, 57 | 'contenttypes.contenttype': { 58 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 59 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 60 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 61 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 62 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 63 | }, 64 | 'offline_messages.offlinemessage': { 65 | 'Meta': {'object_name': 'OfflineMessage'}, 66 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 67 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 68 | 'message': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 69 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 70 | } 71 | } 72 | 73 | complete_apps = ['offline_messages'] 74 | -------------------------------------------------------------------------------- /offline_messages/south_migrations/0002_auto__add_field_offlinemessage_level.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'OfflineMessage.level' 12 | db.add_column('offline_messages_offlinemessage', 'level', self.gf('django.db.models.fields.IntegerField')(default=20), keep_default=False) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'OfflineMessage.level' 18 | db.delete_column('offline_messages_offlinemessage', 'level') 19 | 20 | 21 | models = { 22 | 'auth.group': { 23 | 'Meta': {'object_name': 'Group'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 27 | }, 28 | 'auth.permission': { 29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | }, 35 | 'auth.user': { 36 | 'Meta': {'object_name': 'User'}, 37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 50 | }, 51 | 'contenttypes.contenttype': { 52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 57 | }, 58 | 'offline_messages.offlinemessage': { 59 | 'Meta': {'object_name': 'OfflineMessage'}, 60 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'level': ('django.db.models.fields.IntegerField', [], {'default': '20'}), 63 | 'message': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 64 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 65 | } 66 | } 67 | 68 | complete_apps = ['offline_messages'] 69 | -------------------------------------------------------------------------------- /offline_messages/south_migrations/0003_auto__add_field_offlinemessage_tags.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'OfflineMessage.tags' 12 | db.add_column('offline_messages_offlinemessage', 'tags', self.gf('django.db.models.fields.CharField')(max_length=100, null=True, blank=True), keep_default=False) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'OfflineMessage.tags' 18 | db.delete_column('offline_messages_offlinemessage', 'tags') 19 | 20 | 21 | models = { 22 | 'auth.group': { 23 | 'Meta': {'object_name': 'Group'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 27 | }, 28 | 'auth.permission': { 29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | }, 35 | 'auth.user': { 36 | 'Meta': {'object_name': 'User'}, 37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 50 | }, 51 | 'contenttypes.contenttype': { 52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 57 | }, 58 | 'offline_messages.offlinemessage': { 59 | 'Meta': {'object_name': 'OfflineMessage'}, 60 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'level': ('django.db.models.fields.IntegerField', [], {'default': '20'}), 63 | 'message': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 64 | 'tags': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 65 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 66 | } 67 | } 68 | 69 | complete_apps = ['offline_messages'] 70 | -------------------------------------------------------------------------------- /offline_messages/south_migrations/0004_auto__del_field_offlinemessage_tags.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Deleting field 'OfflineMessage.tags' 12 | db.delete_column('offline_messages_offlinemessage', 'tags') 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Adding field 'OfflineMessage.tags' 18 | db.add_column('offline_messages_offlinemessage', 'tags', self.gf('django.db.models.fields.CharField')(max_length=100, null=True, blank=True), keep_default=False) 19 | 20 | 21 | models = { 22 | 'auth.group': { 23 | 'Meta': {'object_name': 'Group'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 27 | }, 28 | 'auth.permission': { 29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | }, 35 | 'auth.user': { 36 | 'Meta': {'object_name': 'User'}, 37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 50 | }, 51 | 'contenttypes.contenttype': { 52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 57 | }, 58 | 'offline_messages.offlinemessage': { 59 | 'Meta': {'object_name': 'OfflineMessage'}, 60 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'level': ('django.db.models.fields.IntegerField', [], {'default': '20'}), 63 | 'message': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 64 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 65 | } 66 | } 67 | 68 | complete_apps = ['offline_messages'] 69 | -------------------------------------------------------------------------------- /offline_messages/south_migrations/0005_auto__add_field_offlinemessage_read__add_field_offlinemessage_content_.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'OfflineMessage.read' 12 | db.add_column('offline_messages_offlinemessage', 'read', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False) 13 | 14 | # Adding field 'OfflineMessage.content_type' 15 | db.add_column('offline_messages_offlinemessage', 'content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'], null=True, blank=True), keep_default=False) 16 | 17 | # Adding field 'OfflineMessage.object_id' 18 | db.add_column('offline_messages_offlinemessage', 'object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True), keep_default=False) 19 | 20 | # Adding field 'OfflineMessage.meta' 21 | db.add_column('offline_messages_offlinemessage', 'meta', self.gf('jsonfield.fields.JSONField')(null=True, blank=True), keep_default=False) 22 | 23 | 24 | def backwards(self, orm): 25 | 26 | # Deleting field 'OfflineMessage.read' 27 | db.delete_column('offline_messages_offlinemessage', 'read') 28 | 29 | # Deleting field 'OfflineMessage.content_type' 30 | db.delete_column('offline_messages_offlinemessage', 'content_type_id') 31 | 32 | # Deleting field 'OfflineMessage.object_id' 33 | db.delete_column('offline_messages_offlinemessage', 'object_id') 34 | 35 | # Deleting field 'OfflineMessage.meta' 36 | db.delete_column('offline_messages_offlinemessage', 'meta') 37 | 38 | 39 | models = { 40 | 'auth.group': { 41 | 'Meta': {'object_name': 'Group'}, 42 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 43 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 44 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 45 | }, 46 | 'auth.permission': { 47 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 48 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 49 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 50 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 51 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 52 | }, 53 | 'auth.user': { 54 | 'Meta': {'object_name': 'User'}, 55 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 56 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 57 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 58 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 59 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 60 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 61 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 62 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 63 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 64 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 65 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 66 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 67 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 68 | }, 69 | 'contenttypes.contenttype': { 70 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 71 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 72 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 73 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 74 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 75 | }, 76 | 'offline_messages.offlinemessage': { 77 | 'Meta': {'object_name': 'OfflineMessage'}, 78 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), 79 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 80 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 81 | 'level': ('django.db.models.fields.IntegerField', [], {'default': '20'}), 82 | 'message': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 83 | 'meta': ('jsonfield.fields.JSONField', [], {'default': '{}', 'null': 'True', 'blank': 'True'}), 84 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 85 | 'read': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 86 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 87 | } 88 | } 89 | 90 | complete_apps = ['offline_messages'] 91 | -------------------------------------------------------------------------------- /offline_messages/south_migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dym/django-offline-messages/c56d4dd4c0389531b0960821c4d5d9529403b4a5/offline_messages/south_migrations/__init__.py -------------------------------------------------------------------------------- /offline_messages/storage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; mode: python; -*- 2 | from django.contrib.messages.storage.session import SessionStorage 3 | 4 | from offline_messages.utils import create_offline_message 5 | from offline_messages.models import OfflineMessage 6 | 7 | 8 | class OfflineStorageEngine(SessionStorage): 9 | """ 10 | Stores messages in the database (offline_messages.OfflineMessage). 11 | """ 12 | 13 | def _get(self, *args, **kwargs): 14 | """ 15 | Get unread offline and all online messages (which are inherently 'unread'). 16 | """ 17 | messages = [] 18 | 19 | if hasattr(self.request, 'user') and self.request.user.is_authenticated(): 20 | offline_messages = OfflineMessage.objects.filter(user=self.request.user, read=False) 21 | 22 | if offline_messages: 23 | messages.extend(offline_messages) 24 | offline_messages.update(read=True) 25 | 26 | online_messages, all_retrieved = super(OfflineStorageEngine, self)._get(*args, **kwargs) 27 | if online_messages: 28 | messages.extend(online_messages) 29 | 30 | return messages, True 31 | 32 | def _store(self, messages, *args, **kwargs): 33 | """ 34 | Store messages. If logged in, store them offline, else, store in session. 35 | """ 36 | if hasattr(self.request, 'user') and self.request.user.is_authenticated(): 37 | for msg in messages: 38 | # just the basics, if you need the extra meta data, do this manually 39 | # and add the extra kwargs 40 | create_offline_message(self.request.user, msg.message, msg.level) 41 | else: 42 | messages = [msg for msg in messages if not isinstance(msg, OfflineMessage)] 43 | return super(OfflineStorageEngine, self)._store(messages, *args, **kwargs) 44 | -------------------------------------------------------------------------------- /offline_messages/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; mode: python; -*- 2 | 3 | try: 4 | from django.contrib.auth import get_user_model 5 | except ImportError: 6 | from django.contrib.auth.models import User 7 | 8 | def get_user_model(): 9 | return User 10 | 11 | from django.contrib.messages import constants 12 | from django.contrib.messages.api import MessageFailure 13 | try: 14 | from django.utils.encoding import force_unicode 15 | except ImportError: 16 | from django.utils.encoding import force_text as force_unicode 17 | from django.contrib.messages.utils import get_level_tags 18 | 19 | from offline_messages.models import OfflineMessage 20 | 21 | __doc__ = """ 22 | 23 | The idea here is you can just do: 24 | 25 | from offline_messages import utils as messages 26 | 27 | In place of: 28 | 29 | from django.contrib import messages 30 | 31 | And still have access to boring old `messages.success(request, 'Good job!')` but 32 | also have access to be able to do things like... 33 | 34 | comment = Comment.objects.create(title='A test', message='Thanks!') 35 | messages.success(request, 'Comment posted!', content_object=comment, meta={'blah': 'blah'}) 36 | 37 | """.strip() 38 | 39 | 40 | def create_offline_message(user, 41 | message, 42 | level=constants.INFO, 43 | read=False, 44 | content_object=None, 45 | meta={}): 46 | 47 | if not isinstance(user, get_user_model()): 48 | user = get_user_model().objects.get(username=user) 49 | 50 | level_tags = get_level_tags() 51 | label_tag = force_unicode(level_tags.get(level, ''), strings_only=True) 52 | 53 | kwargs = dict( 54 | user=user, 55 | level=level, 56 | tags=label_tag, 57 | read=read, 58 | message=message, 59 | meta=dict(meta) 60 | ) 61 | 62 | if content_object: 63 | kwargs['content_object'] = content_object 64 | 65 | return OfflineMessage.objects.create(**kwargs) 66 | 67 | 68 | def add_message(request, level, message, extra_tags='', fail_silently=False, **kwargs): 69 | """ 70 | Attempts to add a message to the request using the 'messages' app, falling 71 | back to the user's message_set if MessageMiddleware hasn't been enabled. 72 | """ 73 | if hasattr(request, 'user') and request.user.is_authenticated(): 74 | # can pass in content_object and meta now 75 | return create_offline_message(request.user, message, level, **kwargs) 76 | if hasattr(request, '_messages'): 77 | return request._messages.add(level, message, extra_tags) 78 | if not fail_silently: 79 | raise MessageFailure('Without the django.contrib.messages ' 80 | 'middleware, messages can only be added to ' 81 | 'authenticated users.') 82 | 83 | 84 | def debug(request, message, **kwargs): 85 | add_message(request, constants.DEBUG, message, **kwargs) 86 | 87 | 88 | def info(request, message, **kwargs): 89 | add_message(request, constants.INFO, message, **kwargs) 90 | 91 | 92 | def success(request, message, **kwargs): 93 | add_message(request, constants.SUCCESS, message, **kwargs) 94 | 95 | 96 | def warning(request, message, **kwargs): 97 | add_message(request, constants.WARNING, message, **kwargs) 98 | 99 | 100 | def error(request, message, **kwargs): 101 | add_message(request, constants.ERROR, message, **kwargs) 102 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; mode: python; -*- 2 | """ 3 | A package that implements offline messages for Django 4 | Web Framework. 5 | 6 | (C) 2011-2014 oDesk www.oDesk.com w/revisions by Zapier.com 7 | """ 8 | 9 | from setuptools import setup 10 | 11 | setup( 12 | name='django-offline-messages', 13 | version='0.3.7', 14 | description='A package that implements offline messages for Django plus more', 15 | long_description='A package that implements offline messages for Django Web Framework', 16 | license='BSD', 17 | keywords='django offline messages', 18 | url='https://github.com/dym/django-offline-messages', 19 | author='oDesk, www.odesk.com', 20 | author_email='developers@odesk.com', 21 | maintainer='Dmitriy Budashny', 22 | maintainer_email='dmitriy.budashny@gmail.com', 23 | packages=['offline_messages', 'offline_messages.migrations', 'offline_messages.south_migrations'], 24 | classifiers=['Development Status :: 3 - Alpha', 25 | 'Environment :: Web Environment', 26 | 'Framework :: Django', 27 | 'Intended Audience :: Developers', 28 | 'License :: OSI Approved :: BSD License', 29 | 'Operating System :: OS Independent', 30 | 'Programming Language :: Python', 31 | 'Topic :: Software Development :: Libraries :: Python Modules', 32 | ], 33 | test_suite='tests.runtests.runtests', 34 | install_requires=['django-jsonfield'], 35 | zip_safe=False 36 | ) 37 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dym/django-offline-messages/c56d4dd4c0389531b0960821c4d5d9529403b4a5/tests/__init__.py -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dym/django-offline-messages/c56d4dd4c0389531b0960821c4d5d9529403b4a5/tests/models.py -------------------------------------------------------------------------------- /tests/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import django 3 | import os 4 | import sys 5 | 6 | project_root = os.path.abspath(os.path.join(__file__, os.pardir, os.pardir)) 7 | sys.path.insert(0, project_root) 8 | 9 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' 10 | 11 | # Fix for Django>=1.7 12 | if hasattr(django, 'setup'): 13 | django.setup() 14 | 15 | 16 | try: 17 | from django_coverage.coverage_runner import CoverageRunner as TestRunner 18 | except ImportError: 19 | try: 20 | from django.test.runner import DiscoverRunner as TestRunner 21 | except ImportError: 22 | from django.test.simple import DjangoTestSuiteRunner as TestRunner 23 | 24 | def runtests(): 25 | test_runner = TestRunner(verbosity=1, interactive=True) 26 | failures = test_runner.run_tests(['tests']) 27 | sys.exit(failures) 28 | 29 | 30 | if __name__ == '__main__': 31 | runtests() 32 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | # django-offline-messages Test Settings 2 | 3 | DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3'}} 4 | 5 | INSTALLED_APPS = ( 6 | 'django.contrib.sessions', 7 | 'django.contrib.auth', 8 | 'django.contrib.contenttypes', 9 | 'django.contrib.messages', 10 | 'offline_messages', 11 | 'tests' 12 | ) 13 | 14 | MIDDLEWARE_CLASSES = ( 15 | 'django.contrib.sessions.middleware.SessionMiddleware', 16 | 'django.contrib.messages.middleware.MessageMiddleware', 17 | ) 18 | 19 | ROOT_URLCONF = '' 20 | 21 | COVERAGE_ADDITIONAL_MODULES = ('offline_messages',) 22 | 23 | SECRET_KEY = 'foobar' 24 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.contrib.messages import constants 3 | try: 4 | from django.contrib.messages.tests import test_session as session_tests 5 | except ImportError: 6 | try: 7 | from django.contrib.messages.tests import session as session_tests 8 | except ImportError: 9 | from messages_tests import test_session as session_tests 10 | 11 | 12 | from offline_messages.models import OfflineMessage 13 | from offline_messages.storage import OfflineStorageEngine 14 | from offline_messages.utils import create_offline_message 15 | 16 | 17 | class OfflineMessagesTests(session_tests.SessionTest): 18 | storage_class = OfflineStorageEngine 19 | test_message = 'This is a test message' 20 | test_level = constants.DEBUG 21 | 22 | def create_user(self): 23 | return User.objects.create(username='test-user') 24 | 25 | def get_request(self, user=None): 26 | request = super(OfflineMessagesTests, self).get_request() 27 | if user: 28 | request.user = user 29 | return request 30 | 31 | def create_offline_message(self, user, message=None, level=None): 32 | if not message: 33 | message = self.test_message 34 | 35 | if not level: 36 | level = self.test_level 37 | 38 | create_offline_message(user, message, level) 39 | 40 | def assert_offline_message(self, user, message=None, level=None): 41 | if not message: 42 | expected_message =self.test_message 43 | 44 | if not level: 45 | expected_level = self.test_level 46 | 47 | offline_message = OfflineMessage.objects.get() 48 | self.assertEqual(offline_message.message, expected_message) 49 | self.assertEqual(offline_message.level, expected_level) 50 | 51 | storage = OfflineStorageEngine(self.get_request(user)) 52 | all_messages = list(storage) 53 | self.assertEqual(len(all_messages), 1) 54 | self.assertEqual(offline_message, all_messages[0]) 55 | 56 | def test_create_offline_storage(self): 57 | user = self.create_user() 58 | self.create_offline_message(user) 59 | self.assert_offline_message(user) 60 | 61 | def test_create_offline_storage_with_username(self): 62 | user = self.create_user() 63 | self.create_offline_message(user.username) 64 | self.assert_offline_message(user) 65 | 66 | def test_offline_message_with_session_messages(self): 67 | user = self.create_user() 68 | self.create_offline_message(user) 69 | # Grab the message before it is delted 70 | offline_message = OfflineMessage.objects.get() 71 | 72 | storage = OfflineStorageEngine(self.get_request(user)) 73 | 74 | test_messages = ['one', 'two'] 75 | session_tests.set_session_data(storage, test_messages) 76 | 77 | all_messages = list(storage) 78 | self.assertEqual(len(all_messages), 3) 79 | test_messages.insert(0, offline_message) 80 | self.assertEqual(all_messages, test_messages) 81 | 82 | def test_offline_message_tags(self): 83 | user = self.create_user() 84 | self.create_offline_message(user) 85 | offline_message = OfflineMessage.objects.get() 86 | expected_tags = constants.DEFAULT_TAGS.get(self.test_level) 87 | self.assertEqual(offline_message.tags, expected_tags) 88 | -------------------------------------------------------------------------------- 19 | {% endfor %} 20 |