diff --git a/.travis.yml b/.travis.yml index a57a1fec..4fba622a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,15 @@ language: python + python: - "3.5" - "3.6" - "3.7" - "3.8" + env: - DJANGO=2.2 - DJANGO=3.0 + install: # command to install dependencies - "pip install coveralls" @@ -14,12 +17,17 @@ install: - pip install -q Django==$DJANGO - "pip install ." # command to run tests + script: - - coverage run --branch --source=notifications manage.py test + - export SAMPLE_APP=1; coverage run --branch --source=notifications manage.py test + - unset SAMPLE_APP; coverage run --branch --source=notifications manage.py test + matrix: exclude: - python: "3.5" env: DJANGO=3.0 + - python: "3.5" + env: SAMPLE_APP=1 DJANGO=3.0 after_success: diff --git a/README.rst b/README.rst index 21aef058..e05badd6 100644 --- a/README.rst +++ b/README.rst @@ -440,6 +440,19 @@ Email Notification Sending email to users has not been integrated into this library. So for now you need to implement it if needed. There is a reserved field `Notification.emailed` to make it easier. +Sample App +---------- +A sample app has been implemented in ``notifications/tests/sample_notifications`` that extends ``django-notifications`` with the sole purpose of testing its extensibility. +You can run the SAMPLE APP by setting the environment variable ``SAMPLE_APP`` as follows + +.. code-block:: shell + + export SAMPLE_APP=1 + # Run the Django development server with sample_notifications app installed + python manage.py runserver + # Unset SAMPLE_APP to remove sample_notifications app from list of INSTALLED_APPS + unset SAMPLE_APP + ``django-notifications`` Team ============================== diff --git a/notifications/admin.py b/notifications/admin.py index a2b60bf0..3d9c172c 100644 --- a/notifications/admin.py +++ b/notifications/admin.py @@ -1,12 +1,13 @@ ''' Django notifications admin file ''' # -*- coding: utf-8 -*- from django.contrib import admin +from notifications.base.admin import AbstractNotificationAdmin from swapper import load_model Notification = load_model('notifications', 'Notification') -class NotificationAdmin(admin.ModelAdmin): +class NotificationAdmin(AbstractNotificationAdmin): raw_id_fields = ('recipient',) list_display = ('recipient', 'actor', 'level', 'target', 'unread', 'public') diff --git a/notifications/base/admin.py b/notifications/base/admin.py new file mode 100644 index 00000000..29c20c24 --- /dev/null +++ b/notifications/base/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin + + +class AbstractNotificationAdmin(admin.ModelAdmin): + raw_id_fields = ('recipient',) + list_display = ('recipient', 'actor', + 'level', 'target', 'unread', 'public') + list_filter = ('level', 'unread', 'public', 'timestamp',) + + def get_queryset(self, request): + qs = super(AbstractNotificationAdmin, self).get_queryset(request) + return qs.prefetch_related('actor') diff --git a/notifications/tests/sample_notifications/__init__.py b/notifications/tests/sample_notifications/__init__.py new file mode 100644 index 00000000..37499c55 --- /dev/null +++ b/notifications/tests/sample_notifications/__init__.py @@ -0,0 +1 @@ +default_app_config = 'notifications.tests.sample_notifications.apps.SampleNotificationsConfig' diff --git a/notifications/tests/sample_notifications/admin.py b/notifications/tests/sample_notifications/admin.py new file mode 100644 index 00000000..79438c9b --- /dev/null +++ b/notifications/tests/sample_notifications/admin.py @@ -0,0 +1,10 @@ +import swapper +from django.contrib import admin +from notifications.base.admin import AbstractNotificationAdmin + +Notification = swapper.load_model('notifications', 'Notification') + + +@admin.register(Notification) +class NotificationAdmin(AbstractNotificationAdmin): + pass diff --git a/notifications/tests/sample_notifications/apps.py b/notifications/tests/sample_notifications/apps.py new file mode 100644 index 00000000..e54fa20b --- /dev/null +++ b/notifications/tests/sample_notifications/apps.py @@ -0,0 +1,6 @@ +from notifications.apps import Config as NotificationConfig + + +class SampleNotificationsConfig(NotificationConfig): + name = 'notifications.tests.sample_notifications' + label = 'sample_notifications' diff --git a/notifications/tests/sample_notifications/migrations/0001_initial.py b/notifications/tests/sample_notifications/migrations/0001_initial.py new file mode 100644 index 00000000..e67dd044 --- /dev/null +++ b/notifications/tests/sample_notifications/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 3.0.5 on 2020-04-11 12:15 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import jsonfield.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Notification', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('level', models.CharField(choices=[('success', 'success'), ('info', 'info'), ('warning', 'warning'), ('error', 'error')], default='info', max_length=20)), + ('unread', models.BooleanField(db_index=True, default=True)), + ('actor_object_id', models.CharField(max_length=255)), + ('verb', models.CharField(max_length=255)), + ('description', models.TextField(blank=True, null=True)), + ('target_object_id', models.CharField(blank=True, max_length=255, null=True)), + ('action_object_object_id', models.CharField(blank=True, max_length=255, null=True)), + ('timestamp', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), + ('public', models.BooleanField(db_index=True, default=True)), + ('deleted', models.BooleanField(db_index=True, default=False)), + ('emailed', models.BooleanField(db_index=True, default=False)), + ('data', jsonfield.fields.JSONField(blank=True, null=True)), + ('details', models.CharField(blank=True, max_length=64, null=True)), + ('action_object_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notify_action_object', to='contenttypes.ContentType')), + ('actor_content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notify_actor', to='contenttypes.ContentType')), + ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)), + ('target_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notify_target', to='contenttypes.ContentType')), + ], + options={ + 'ordering': ('-timestamp',), + 'abstract': False, + 'index_together': {('recipient', 'unread')}, + }, + ), + ] diff --git a/notifications/tests/sample_notifications/migrations/__init__.py b/notifications/tests/sample_notifications/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/notifications/tests/sample_notifications/models.py b/notifications/tests/sample_notifications/models.py new file mode 100644 index 00000000..ddf8c03f --- /dev/null +++ b/notifications/tests/sample_notifications/models.py @@ -0,0 +1,9 @@ +from django.db import models +from notifications.base.models import AbstractNotification + + +class Notification(AbstractNotification): + details = models.CharField(max_length=64, blank=True, null=True) + + class Meta(AbstractNotification.Meta): + abstract = False diff --git a/notifications/tests/sample_notifications/templatetags/__init__.py b/notifications/tests/sample_notifications/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/notifications/tests/sample_notifications/templatetags/notifications_tags.py b/notifications/tests/sample_notifications/templatetags/notifications_tags.py new file mode 100644 index 00000000..6b30bc2d --- /dev/null +++ b/notifications/tests/sample_notifications/templatetags/notifications_tags.py @@ -0,0 +1 @@ +from notifications.templatetags.notifications_tags import register diff --git a/notifications/tests/sample_notifications/tests.py b/notifications/tests/sample_notifications/tests.py new file mode 100644 index 00000000..87529bfc --- /dev/null +++ b/notifications/tests/sample_notifications/tests.py @@ -0,0 +1,22 @@ +import os +from unittest import skipUnless + +import swapper +from notifications.tests.tests import AdminTest as BaseAdminTest +from notifications.tests.tests import NotificationTest as BaseNotificationTest + +Notification = swapper.load_model('notifications', 'Notification') + + +@skipUnless(os.environ.get('SAMPLE_APP', False), 'Running tests on standard django-notifications models') +class AdminTest(BaseAdminTest): + + @classmethod + def setUpClass(cls): + super().setUpClass() + BaseAdminTest.app_name = 'sample_notifications' + + +@skipUnless(os.environ.get('SAMPLE_APP', False), 'Running tests on standard django-notifications models') +class NotificationTest(BaseNotificationTest): + pass diff --git a/notifications/tests/settings.py b/notifications/tests/settings.py index c5e084bb..598b1750 100644 --- a/notifications/tests/settings.py +++ b/notifications/tests/settings.py @@ -4,7 +4,7 @@ BASE_DIR = os.path.abspath(os.path.dirname(__file__)) -SECRET_KEY = 'secret_key' # noqa +SECRET_KEY = 'secret_key' # noqa DEBUG = True TESTING = True @@ -26,7 +26,7 @@ # Django >= 2.0 MIDDLEWARE = MIDDLEWARE_CLASSES -INSTALLED_APPS = ( +INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -35,7 +35,7 @@ 'django.contrib.sessions', 'notifications.tests', 'notifications', -) +] ROOT_URLCONF = 'notifications.tests.urls' STATIC_URL = '/static/' @@ -48,8 +48,11 @@ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], - 'APP_DIRS': True, 'OPTIONS': { + 'loaders' : [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ], 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', @@ -68,3 +71,9 @@ 'USE_JSONFIELD': True, } USE_TZ = True + +if os.environ.get('SAMPLE_APP', False): + INSTALLED_APPS.remove('notifications') + INSTALLED_APPS.append('notifications.tests.sample_notifications') + NOTIFICATIONS_NOTIFICATION_MODEL = 'sample_notifications.Notification' + TEMPLATES[0]['DIRS'] += [os.path.join(BASE_DIR, '../templates')] diff --git a/notifications/tests/tests.py b/notifications/tests/tests.py index ac369567..b9a87963 100644 --- a/notifications/tests/tests.py +++ b/notifications/tests/tests.py @@ -18,7 +18,7 @@ from django.test.utils import CaptureQueriesContext from django.utils import timezone from django.utils.timezone import localtime, utc -from notifications.models import notify_handler +from notifications.base.models import notify_handler from notifications.signals import notify from notifications.utils import id2slug from swapper import load_model @@ -519,6 +519,7 @@ def test_has_notification(self): class AdminTest(TestCase): + app_name = "notifications" def setUp(self): self.message_count = 10 self.from_user = User.objects.create_user(username="from", password="pwd", email="example@example.com") @@ -538,7 +539,7 @@ def test_list(self): self.client.login(username='to', password='pwd') with CaptureQueriesContext(connection=connection) as context: - response = self.client.get(reverse('admin:notifications_notification_changelist')) + response = self.client.get(reverse('admin:{0}_notification_changelist'.format(self.app_name))) self.assertLessEqual(len(context), 6) self.assertEqual(response.status_code, 200, response.content)