From e6c96b9d09718023998d183cced39722d28af16c Mon Sep 17 00:00:00 2001 From: Python3pkg Date: Wed, 17 May 2017 22:22:18 -0700 Subject: [PATCH] Convert to python3 --- ahem/backends.py | 2 +- ahem/backends.py.bak | 98 +++++++++++++++ ahem/loader.py | 4 +- ahem/loader.py.bak | 52 ++++++++ ahem/migrations/0001_initial.py | 2 +- ahem/migrations/0001_initial.py.bak | 45 +++++++ ahem/models.py | 2 +- ahem/models.py.bak | 42 +++++++ ahem/notification.py | 2 +- ahem/notification.py.bak | 106 ++++++++++++++++ ahem/scopes.py | 2 +- ahem/scopes.py.bak | 56 +++++++++ ahem/settings.py | 2 +- ahem/settings.py.bak | 6 + ahem/tasks.py | 2 +- ahem/tasks.py.bak | 73 +++++++++++ ahem/tests/test_backends.py | 2 +- ahem/tests/test_backends.py.bak | 151 +++++++++++++++++++++++ ahem/tests/test_models.py | 2 +- ahem/tests/test_models.py.bak | 18 +++ ahem/tests/test_notification.py | 2 +- ahem/tests/test_notification.py.bak | 184 ++++++++++++++++++++++++++++ ahem/tests/test_scopes.py | 2 +- ahem/tests/test_scopes.py.bak | 115 +++++++++++++++++ ahem/tests/test_triggers.py | 2 +- ahem/tests/test_triggers.py.bak | 121 ++++++++++++++++++ ahem/tests/test_utils.py | 2 +- ahem/tests/test_utils.py.bak | 32 +++++ ahem/triggers.py | 2 +- ahem/triggers.py.bak | 69 +++++++++++ ahem/utils.py | 2 +- ahem/utils.py.bak | 47 +++++++ example/example/__init__.py | 2 +- example/example/__init__.py.bak | 4 + example/example/celery.py | 2 +- example/example/celery.py.bak | 12 ++ testsettings.py | 2 +- testsettings.py.bak | 58 +++++++++ 38 files changed, 1309 insertions(+), 20 deletions(-) create mode 100644 ahem/backends.py.bak create mode 100644 ahem/loader.py.bak create mode 100644 ahem/migrations/0001_initial.py.bak create mode 100644 ahem/models.py.bak create mode 100644 ahem/notification.py.bak create mode 100644 ahem/scopes.py.bak create mode 100644 ahem/settings.py.bak create mode 100644 ahem/tasks.py.bak create mode 100644 ahem/tests/test_backends.py.bak create mode 100644 ahem/tests/test_models.py.bak create mode 100644 ahem/tests/test_notification.py.bak create mode 100644 ahem/tests/test_scopes.py.bak create mode 100644 ahem/tests/test_triggers.py.bak create mode 100644 ahem/tests/test_utils.py.bak create mode 100644 ahem/triggers.py.bak create mode 100644 ahem/utils.py.bak create mode 100644 example/example/__init__.py.bak create mode 100644 example/example/celery.py.bak create mode 100644 testsettings.py.bak diff --git a/ahem/backends.py b/ahem/backends.py index 43c49dd..0ad40d7 100644 --- a/ahem/backends.py +++ b/ahem/backends.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + import logging diff --git a/ahem/backends.py.bak b/ahem/backends.py.bak new file mode 100644 index 0000000..43c49dd --- /dev/null +++ b/ahem/backends.py.bak @@ -0,0 +1,98 @@ +from __future__ import unicode_literals + +import logging + +from django.core.exceptions import ObjectDoesNotExist +from django.core.mail import send_mail +from django.conf import settings as django_settings + +from ahem.models import UserBackendRegistry + + +class BaseBackend(object): + """ + VARIABLES + name - A unique string that identifies the backend + across the project + required_settings - A list of setings required to register + a user in the backend + + METHODS + send_notification - Custom method for each backend that + sends the desired notification + """ + + required_settings = [] + + @classmethod + def register_user(cls, user, **settings): + required_settings = [] + if hasattr(cls, 'required_settings'): + required_settings = cls.required_settings + + if not set(required_settings).issubset(set(settings.keys())): + raise Exception("Missing backend settings.") # TODO: change to custom exception + + try: + registry = UserBackendRegistry.objects.get( + user=user, backend=cls.name) + except ObjectDoesNotExist: + registry = UserBackendRegistry( + user=user, backend=cls.name) + + registry.settings = settings + registry.save() + + return registry + + def send_notification(self, user, notification, context={}, settings={}): + raise NotImplementedError + + +class EmailBackend(BaseBackend): + """ + CONTEXT PARAMS + subject - An subject for the email. + from_email - The email the message will be sent from, + default is DEFAULT_FROM_EMAIL. + use_html - If true, the email will be sent with html content type. + """ + name = 'email' + + def send_notification(self, user, notification, context={}, settings={}): + body = notification.render_template(user, self.name, context=context) + subject = context.get('subject', '') + from_email = context.get('from_email', django_settings.DEFAULT_FROM_EMAIL) + recipient_list = [user.email] + use_html = context.get('use_html', False) + + email_params = {} + if use_html: + email_params['html_message'] = body + + send_mail(subject, body, from_email, recipient_list, **email_params) + + +class LoggingBackend(BaseBackend): + """ + CONTEXT PARAMS + logger - logger name + logging_level - the level of the logger + """ + name = 'logging' + + def get_logger(self, logger_name): + if logger_name: + logger = logging.getLogger(logger_name) + else: + logger = logging + + return logger + + def send_notification(self, user, notification, context={}, settings={}): + text = notification.render_template(user, self.name, context=context) + + logging_level = context.get('logging_level', 'info') + logger = self.get_logger(context.get('logger', None)) + + getattr(logger, logging_level)(text) diff --git a/ahem/loader.py b/ahem/loader.py index 7c418e0..763fb38 100644 --- a/ahem/loader.py +++ b/ahem/loader.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + import inspect from importlib import import_module @@ -41,7 +41,7 @@ def register_notifications(): def get_celery_beat_schedule(): register_notifications() schedule = {} - for _, notification in notification_registry.items(): + for _, notification in list(notification_registry.items()): if notification.is_periodic(): schedule['ahem_' + notification.name] = { 'task': 'ahem.tasks.dispatch_to_users', diff --git a/ahem/loader.py.bak b/ahem/loader.py.bak new file mode 100644 index 0000000..7c418e0 --- /dev/null +++ b/ahem/loader.py.bak @@ -0,0 +1,52 @@ +from __future__ import unicode_literals + +import inspect +from importlib import import_module + +from django.conf import settings + + +notification_registry = {} + + +def add_to_registry(notification): + notification_registry[notification.name] = notification + + +def load_notifications(module): + """ + Loads notifications from the module. + """ + from ahem.notification import Notification + + for name, notification in inspect.getmembers(module): + if (inspect.isclass(notification) and issubclass(notification, Notification) + and not notification is Notification): + add_to_registry(notification) + + +def register_notifications(): + """ + Registers all notifications in the application. + """ + for app_path in settings.INSTALLED_APPS: + if app_path != 'ahem': + try: + module = import_module('%s.notifications' % app_path) + load_notifications(module) + except ImportError: + pass + + +def get_celery_beat_schedule(): + register_notifications() + schedule = {} + for _, notification in notification_registry.items(): + if notification.is_periodic(): + schedule['ahem_' + notification.name] = { + 'task': 'ahem.tasks.dispatch_to_users', + 'schedule': notification.trigger.crontab, + 'args': (notification.name,) + } + + return schedule diff --git a/ahem/migrations/0001_initial.py b/ahem/migrations/0001_initial.py index c12c9ac..4759e09 100644 --- a/ahem/migrations/0001_initial.py +++ b/ahem/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations import jsonfield.fields diff --git a/ahem/migrations/0001_initial.py.bak b/ahem/migrations/0001_initial.py.bak new file mode 100644 index 0000000..c12c9ac --- /dev/null +++ b/ahem/migrations/0001_initial.py.bak @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import jsonfield.fields +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='DeferredNotification', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('notification', models.CharField(max_length=255)), + ('context', jsonfield.fields.JSONField(default={})), + ('task_id', models.CharField(max_length=255)), + ('ran_at', models.DateTimeField(null=True, blank=True)), + ('created_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='UserBackendRegistry', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('backend', models.CharField(max_length=255)), + ('settings', jsonfield.fields.JSONField(default={})), + ('user', models.ForeignKey(related_name='ahem_backends', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='deferrednotification', + name='user_backend', + field=models.ForeignKey(to='ahem.UserBackendRegistry'), + ), + migrations.AlterUniqueTogether( + name='userbackendregistry', + unique_together=set([('user', 'backend')]), + ), + ] diff --git a/ahem/models.py b/ahem/models.py index 20a7aa3..a65593c 100644 --- a/ahem/models.py +++ b/ahem/models.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + import json diff --git a/ahem/models.py.bak b/ahem/models.py.bak new file mode 100644 index 0000000..20a7aa3 --- /dev/null +++ b/ahem/models.py.bak @@ -0,0 +1,42 @@ +from __future__ import unicode_literals + +import json + +from django.db import models +from django.conf import settings + +from jsonfield import JSONField + +from ahem.loader import register_notifications + + +class UserBackendRegistry(models.Model): + """ + Availble backends for a given recipient + """ + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='ahem_backends') + backend = models.CharField(max_length=255) + + settings = JSONField(default={}) + + class Meta: + unique_together = (('user','backend'),) + + +class DeferredNotification(models.Model): + """ + A notification that has been deferred until a future time. + """ + notification = models.CharField(max_length=255) + user_backend = models.ForeignKey(UserBackendRegistry) + + context = JSONField(default={}) + + task_id = models.CharField(max_length=255) + + ran_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now=True) + + +# Notification loader +register_notifications() diff --git a/ahem/notification.py b/ahem/notification.py index cd907fe..400be9d 100644 --- a/ahem/notification.py +++ b/ahem/notification.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from django.utils import timezone from django.template.loader import get_template diff --git a/ahem/notification.py.bak b/ahem/notification.py.bak new file mode 100644 index 0000000..cd907fe --- /dev/null +++ b/ahem/notification.py.bak @@ -0,0 +1,106 @@ +from __future__ import unicode_literals + +from django.utils import timezone +from django.template.loader import get_template +from django.template import Context, Template +from django.db.models.query import EmptyQuerySet +from django.contrib.auth.models import AnonymousUser + +from ahem.tasks import dispatch_to_users +from ahem.utils import celery_is_available + + +class Notification(object): + """ + Base notification class. Notifications extend this class. + + VARIABLES + name - A unique string that identifies the notification + across the project + backends - A list of supported backends for this notification + templates - A dictionary with a backend name as the key and + a path for a template as value. Must have a 'default'. + scope - the scope of the notification. + trigger - A trigger class that specifies when the + notification will be sent. + + METHODS + get_context_data - returns a dictionary containing context variables + schedule - schedules tasks according to notification configuration + and passed arguments + filter_scope - can be used to perform context based filters. Returns + a list of users. + """ + + def get_scope(self, backend): + return self.scope + + def get_users(self, backend, context): + scope = self.get_scope(backend) + queryset = scope.get_users_queryset(context) + + if queryset == EmptyQuerySet: + users = [AnonymousUser()] + elif hasattr(self, 'filter_scope'): + users = self.filter_scope(queryset, context) + else: + users = queryset.all() + + return users + + def get_next_run_eta(self, last_run_at=None): + return self.trigger.next_run_eta(last_run_at) + + @classmethod + def is_periodic(cls): + return cls.trigger.is_periodic + + def render_template(self, user, backend_name, context={}, **kwargs): + template_path = self.templates.get(backend_name) + if not template_path: + template_path = self.templates.get('default') + + if not template_path: + raise Exception("""A template for the specified backend could not be found. +Please define a 'default' template for the notification""") + + template = get_template(template_path) + + return template.render(Context(context)) + + def get_context_data(self, user, backend_name, **kwargs): + kwargs['user'] = user + return kwargs + + def get_task_eta(self, delay_timedelta, eta): + run_eta = None + if delay_timedelta is None and eta is None: + run_eta = self.get_next_run_eta() + elif delay_timedelta: + run_eta = timezone.now() + delay_timedelta + elif eta: + run_eta = eta + + return run_eta + + def get_task_backends(self, restrict_backends): + if restrict_backends: + return list(set(self.backends).intersection(set(restrict_backends))) + return self.backends + + def schedule(self, context={}, delay_timedelta=None, eta=None, backends=None): + run_eta = self.get_task_eta(delay_timedelta, eta) + backends = self.get_task_backends(backends) + + if celery_is_available(): + dispatch_to_users.delay( + self.name, + eta=run_eta, + context=context, + backends=backends) + else: + dispatch_to_users( + self.name, + eta=run_eta, + context=context, + backends=backends) diff --git a/ahem/scopes.py b/ahem/scopes.py index 3f271bd..5a5b764 100644 --- a/ahem/scopes.py +++ b/ahem/scopes.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from django.contrib.auth import get_user_model from django.db.models.query import EmptyQuerySet diff --git a/ahem/scopes.py.bak b/ahem/scopes.py.bak new file mode 100644 index 0000000..3f271bd --- /dev/null +++ b/ahem/scopes.py.bak @@ -0,0 +1,56 @@ +from __future__ import unicode_literals + +from django.contrib.auth import get_user_model +from django.db.models.query import EmptyQuerySet + + +class Scope(object): + """ + Base scope class. A scope identifies the recipients for notifications. + Subclasses implement different methods of defining recipients. + """ + + def get_users_queryset(self, context): + return self.get_queryset(context) + + def get_queryset(self, context): + return self.queryset + + +class AnonymousUserScope(Scope): + """ + Returns an empty queryset + """ + def get_queryset(self, context): + return EmptyQuerySet + + +class QuerySetScope(Scope): + """ + Returns a queryset. + """ + def __init__(self, queryset=None): + self.queryset = queryset + + def get_queryset(self, context): + if self.queryset is None: + user_model = get_user_model() + self.queryset = user_model.objects.get_queryset() + return self.queryset + + +class ContextFilterScope(Scope): + """ + A specific user. + """ + def __init__(self, context_key=None, lookup_field=None): + self.queryset = get_user_model().objects + self.lookup_field = lookup_field + self.context_key = context_key + + def get_queryset(self, context): + """ + Gets the specific user. + """ + value = context[self.context_key] + return self.queryset.filter(**{ self.lookup_field: value }) diff --git a/ahem/settings.py b/ahem/settings.py index 1b90e0f..6a53fee 100644 --- a/ahem/settings.py +++ b/ahem/settings.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + AHEM_BACKENDS = ( 'ahem.backends.EmailBackend', diff --git a/ahem/settings.py.bak b/ahem/settings.py.bak new file mode 100644 index 0000000..1b90e0f --- /dev/null +++ b/ahem/settings.py.bak @@ -0,0 +1,6 @@ +from __future__ import unicode_literals + +AHEM_BACKENDS = ( + 'ahem.backends.EmailBackend', + 'ahem.backends.LoggingBackend', +) diff --git a/ahem/tasks.py b/ahem/tasks.py index c878857..b1da7d8 100644 --- a/ahem/tasks.py +++ b/ahem/tasks.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from django.utils import timezone from django.contrib.auth.models import AnonymousUser diff --git a/ahem/tasks.py.bak b/ahem/tasks.py.bak new file mode 100644 index 0000000..c878857 --- /dev/null +++ b/ahem/tasks.py.bak @@ -0,0 +1,73 @@ +from __future__ import unicode_literals + +from django.utils import timezone +from django.contrib.auth.models import AnonymousUser + +from ahem.utils import get_notification, get_backend, celery_is_available +from ahem.models import DeferredNotification, UserBackendRegistry + +if celery_is_available(): + from celery import shared_task +else: + def shared_task(func): + return func + + +@shared_task +def dispatch_to_users(notification_name, eta=None, context={}, backends=None, **kwargs): + notification = get_notification(notification_name) + task_backends = notification.get_task_backends(backends) + + for backend in task_backends: + users = notification.get_users(backend, context) + for user in users: + if isinstance(user, AnonymousUser): + send_anonymous_notification.apply_async( + (notification_name, backend, context), + eta=eta) + else: + user_backend = UserBackendRegistry.objects.filter( + user=user, backend=backend).first() + if user_backend: + deferred = DeferredNotification.objects.create( + notification=notification_name, + user_backend=user_backend, + context=context) + + if celery_is_available(): + task_id = send_notification.apply_async( + (deferred.id,), + eta=eta) + deferred.task_id = task_id + deferred.save() + else: + send_notification(deferred.id) + +@shared_task +def send_anonymous_notification(notification_name, backend_name, context): + notification = get_notification(notification_name) + backend = get_backend(backend_name) + + backend.send_notification( + AnonymousUser(), notification, context=context) + + +@shared_task +def send_notification(deferred_id): + deferred = ((DeferredNotification.objects + .select_related('user_backend', 'user_backend__user')) + .get(id=deferred_id)) + user = deferred.user_backend.user + backend_settings = deferred.user_backend.settings + + backend = get_backend(deferred.user_backend.backend) + notification = get_notification(deferred.notification) + + context = notification.get_context_data(user, backend.name, **deferred.context) + + backend.send_notification( + user, notification, context=context, settings=backend_settings) + + deferred.ran_at = timezone.now() + deferred.save() + diff --git a/ahem/tests/test_backends.py b/ahem/tests/test_backends.py index e0998cb..fc7388a 100644 --- a/ahem/tests/test_backends.py +++ b/ahem/tests/test_backends.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from datetime import timedelta diff --git a/ahem/tests/test_backends.py.bak b/ahem/tests/test_backends.py.bak new file mode 100644 index 0000000..e0998cb --- /dev/null +++ b/ahem/tests/test_backends.py.bak @@ -0,0 +1,151 @@ +from __future__ import unicode_literals + +from datetime import timedelta + +from mock import patch + +from django.test import TestCase +from django.test.utils import override_settings +from django.core import mail +from django.contrib.auth.models import AnonymousUser + +from model_mommy import mommy + +from ahem.models import UserBackendRegistry +from ahem.backends import BaseBackend +from ahem.notification import Notification +from ahem.scopes import QuerySetScope +from ahem.triggers import DelayedTrigger +from ahem.backends import ( + BaseBackend, EmailBackend, LoggingBackend) + + +class TestBackend(BaseBackend): + name = 'test_backend' + + +class TestNotification(Notification): + name = 'delayed_trigger_notification' + backends = ['test_backend'] + + scope = QuerySetScope() + trigger = DelayedTrigger(timedelta(days=2)) + + templates = { + 'default': 'ahem/tests/test_template.html'} + + +class Test2Notification(Notification): + name = 'delayed_trigger_notification' + backends = ['test_backend'] + + scope = QuerySetScope() + trigger = DelayedTrigger(timedelta(days=2)) + + templates = { + 'default': 'ahem/tests/test_template_backend.html'} + + +class TestBackend(BaseBackend): + name = 'test_backend' + required_settings = ['username', 'id'] + + +class BaseBackendTests(TestCase): + + def setUp(self): + self.user = mommy.make('auth.User') + + def test_raises_exception_if_required_settings_are_not_passed(self): + user = self.user + + with self.assertRaises(Exception): + TestBackend.register_user(user, username='username') + + def test_does_not_raises_exception_if_all_settings_are_passed(self): + user = self.user + + TestBackend.register_user(user, + username='username', id='test_id') + + def test_returns_true_if_is_successfull(self): + user = self.user + + result = TestBackend.register_user(user, + username='username', id='test_id') + self.assertTrue(result) + + def test_settings_are_correctly_saved(self): + user = self.user + TestBackend.register_user(user, + username='user_name', id='test_id') + + registry = UserBackendRegistry.objects.get(user=user, backend=TestBackend.name) + + self.assertEqual(registry.settings['username'], 'user_name') + self.assertEqual(registry.settings['id'], 'test_id') + + def test_resgistering_user_again_updates_settings(self): + user = self.user + registry = TestBackend.register_user(user, + username='username', id='test_id') + + registry = UserBackendRegistry.objects.get(user=user, backend=TestBackend.name) + self.assertEqual(registry.settings['username'], 'username') + + TestBackend.register_user(user, + username='new_username', id='test_id') + + registry = UserBackendRegistry.objects.get(user=user, backend=TestBackend.name) + self.assertEqual(registry.settings['username'], 'new_username') + + +class EmailBackendTests(TestCase): + + def setUp(self): + self.user = mommy.make('auth.User') + + self.backend = EmailBackend() + self.notification = TestNotification() + + def test_sends_email(self): + self.backend.send_notification( + self.user, self.notification) + + self.assertEqual(len(mail.outbox), 1) + + def test_adds_subject_if_passed_on_context(self): + self.backend.send_notification( + self.user, self.notification, + context={'subject': 'Test Subject'}) + + self.assertEqual(mail.outbox[0].subject, 'Test Subject') + + def test_uses_context_from_email_if_provided(self): + self.backend.send_notification( + self.user, self.notification, + context={'from_email': 'from@email.com'}) + + self.assertEqual(mail.outbox[0].from_email, 'from@email.com') + + def test_recipient_list_is_user_email(self): + user = self.user + + self.backend.send_notification( + self.user, self.notification) + + self.assertEqual(mail.outbox[0].to[0], user.email) + + +class LoggingBackendTests(TestCase): + + def setUp(self): + self.backend = LoggingBackend() + self.notification = Test2Notification() + + def test_send_message(self): + with patch.object(self.backend.get_logger(None), 'error') as mock_logger: + self.backend.send_notification( + AnonymousUser(), self.notification, + context={'logging_level': 'error'}) + mock_logger.assert_called_once_with('backend template') diff --git a/ahem/tests/test_models.py b/ahem/tests/test_models.py index a1e8223..f67c843 100644 --- a/ahem/tests/test_models.py +++ b/ahem/tests/test_models.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from django.test import TestCase from django.db import IntegrityError diff --git a/ahem/tests/test_models.py.bak b/ahem/tests/test_models.py.bak new file mode 100644 index 0000000..a1e8223 --- /dev/null +++ b/ahem/tests/test_models.py.bak @@ -0,0 +1,18 @@ +from __future__ import unicode_literals + +from django.test import TestCase +from django.db import IntegrityError + +from model_mommy import mommy + +from ahem.models import UserBackendRegistry + + +class UserBackendRegistryTests(TestCase): + + def test_user_and_backend_unique_together(self): + mommy.make('ahem.UserBackendRegistry', + user__id=1, backend='test_backend') + with self.assertRaises(IntegrityError): + mommy.make('ahem.UserBackendRegistry', + user__id=1, backend='test_backend') diff --git a/ahem/tests/test_notification.py b/ahem/tests/test_notification.py index 6f88fae..a24d2d8 100644 --- a/ahem/tests/test_notification.py +++ b/ahem/tests/test_notification.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from datetime import timedelta diff --git a/ahem/tests/test_notification.py.bak b/ahem/tests/test_notification.py.bak new file mode 100644 index 0000000..6f88fae --- /dev/null +++ b/ahem/tests/test_notification.py.bak @@ -0,0 +1,184 @@ +from __future__ import unicode_literals + +from datetime import timedelta + +from django.test import TestCase +from django.utils import timezone +from django.test.utils import override_settings + +from model_mommy import mommy + +from ahem.backends import BaseBackend +from ahem.notification import Notification +from ahem.scopes import QuerySetScope +from ahem.triggers import DelayedTrigger +from ahem.loader import add_to_registry +from ahem.models import DeferredNotification + + +class TestBackend(BaseBackend): + name = 'test_backend' + + def send_notification(self, user, notification, context={}, settings={}): + pass + + +class OtherBackend(BaseBackend): + name = 'other_backend' + + def send_notification(self, user, notification, context={}, settings={}): + pass + + +class TestNotification(Notification): + name = 'test_notification' + backends = ['test_backend', 'other_backend'] + + scope = QuerySetScope() + trigger = DelayedTrigger(timedelta(days=1)) + + def filter_scope(self, queryset, context): + username_filter = context.get('username_filter', None) + if username_filter: + return queryset.filter(username=username_filter).all() + return queryset.all() + + def get_context_data(self, user, backend_name, **kwargs): + context = super(TestNotification, self).get_context_data( + user, backend_name, **kwargs) + context['test'] = 'test works' + return context + + templates = { + 'default': 'ahem/tests/test_template.html', + 'test_backend': 'ahem/tests/test_template_backend.html'} + + +@override_settings( + AHEM_BACKENDS=('ahem.tests.test_notification.TestNotification',)) +class NotificationTemplateTests(TestCase): + + def setUp(self): + self.user = mommy.make('auth.User') + self.notification = TestNotification() + + def test_template_rendering(self): + user = self.user + + body = self.notification.render_template(user, 'test_backend') + self.assertEqual(body, 'backend template') + + def test_should_fetch_default_template_if_backend_specific_is_not_provided(self): + user = self.user + + body = self.notification.render_template(user, 'other_backend') + self.assertIn('default template', body) + + def test_context_variable_should_be_available_in_template(self): + user = self.user + + body = self.notification.render_template(user, 'other_backend', context={'context_variable': 'IT_WORKED'}) + self.assertIn('IT_WORKED', body) + + +@override_settings( + AHEM_BACKENDS=('ahem.tests.test_notification.TestBackend', + 'ahem.tests.test_notification.OtherBackend',)) +class NotificationScheduleTests(TestCase): + + def setUp(self): + self.user = mommy.make('auth.User') + + add_to_registry(TestNotification) + self.notification = TestNotification() + + mommy.make('ahem.UserBackendRegistry', + user=self.user, backend=TestBackend.name) + + def test_uses_delay_timedelta_if_is_passed(self): + expected = timezone.now() + timedelta(days=2) + eta = self.notification.get_task_eta(timedelta(days=2), None) + + self.assertEqual(eta.day, expected.day) + + def test_uses_eta_if_is_passed(self): + expected = timezone.now() + timedelta(days=5) + eta = self.notification.get_task_eta(None, expected) + + self.assertEqual(eta, expected) + + def test_uses_trigger_default_if_no_everride_is_passed(self): + expected = timezone.now() + timedelta(days=1) + eta = self.notification.get_task_eta(None, None) + + self.assertEqual(eta.day, expected.day) + + def test_uses_default_backends_if_none_is_passed(self): + backends = self.notification.get_task_backends(None) + + self.assertEqual(set(self.notification.backends), set(backends)) + + def test_uses_only_intersection_of_passed_backends_and_notification_ones(self): + backends = self.notification.get_task_backends(['test_backend', 'not_allowed_backend']) + + self.assertEqual(set(['test_backend']), set(backends)) + + def test_schedule_creates_deferred_notification_instances(self): + self.notification.schedule() + + deferreds = DeferredNotification.objects.all() + + self.assertEqual(len(deferreds), 1) + + def test_schedule_creates_deferred_notifications_for_each_backend(self): + mommy.make('ahem.UserBackendRegistry', + user=self.user, backend=OtherBackend.name) + self.notification.schedule() + + deferreds = DeferredNotification.objects.all() + + self.assertEqual(len(deferreds), 2) + + def test_schedule_creates_notification_for_each_user(self): + other_user = mommy.make('auth.User') + mommy.make('ahem.UserBackendRegistry', + user=other_user, backend=TestBackend.name) + + self.notification.schedule() + + deferreds = DeferredNotification.objects.all() + + self.assertEqual(len(deferreds), 2) + + def test_schedule_only_creates_deferred_notification_for_user_existent_backends(self): + other_user = mommy.make('auth.User') + + self.notification.schedule() + + deferreds = DeferredNotification.objects.all() + + self.assertEqual(len(deferreds), 1) + + +class NotificationMethodsTests(TestCase): + + def setUp(self): + self.user = mommy.make('auth.User') + + add_to_registry(TestNotification) + self.notification = TestNotification() + + def test_get_users(self): + user = self.user + + users = self.notification.get_users('test_backend', {}) + + self.assertEqual(users[0], user) + + def test_filter_scope(self): + other_user = mommy.make('auth.User', username='username') + + users = self.notification.get_users('test_backend', context={'username_filter': 'username'}) + + self.assertEqual(len(users), 1) + self.assertEqual(users[0], other_user) diff --git a/ahem/tests/test_scopes.py b/ahem/tests/test_scopes.py index 5b791d0..ae13e6b 100644 --- a/ahem/tests/test_scopes.py +++ b/ahem/tests/test_scopes.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from django.test import TestCase from django.contrib.auth.models import User, AnonymousUser diff --git a/ahem/tests/test_scopes.py.bak b/ahem/tests/test_scopes.py.bak new file mode 100644 index 0000000..5b791d0 --- /dev/null +++ b/ahem/tests/test_scopes.py.bak @@ -0,0 +1,115 @@ +from __future__ import unicode_literals + +from django.test import TestCase +from django.contrib.auth.models import User, AnonymousUser + +from model_mommy import mommy + +from ahem.backends import BaseBackend +from ahem.notification import Notification +from ahem.scopes import ( + QuerySetScope, ContextFilterScope, AnonymousUserScope) + + +class TestBackend(BaseBackend): + name = 'test_backend' + required_settings = ['username', 'id'] + + +class OtherBackend(BaseBackend): + name = 'other_backend' + + +class AnonymousUserNotification(Notification): + name = 'anonymous_user_notification' + backends = ['test_backend'] + + scope = AnonymousUserScope() + + templates = { + 'default': 'ahem/tests/test_template.html'} + + +class AnonymousUserScopeTests(TestCase): + + def test_returns_list_with_anonymous_user(self): + notification = AnonymousUserNotification() + + notification_users = notification.get_users('test_backend', {}) + self.assertEqual(len(notification_users), 1) + self.assertTrue(isinstance(notification_users[0], AnonymousUser)) + + +class QuerySetNotification(Notification): + name = 'query_set_notification' + backends = ['test_backend', 'other_backend'] + + scope = QuerySetScope() + + templates = { + 'default': 'ahem/tests/test_template.html'} + + +class CustomQuerySetNotification(Notification): + name = 'custom_query_set_notification' + backends = ['test_backend', 'other_backend'] + + scope = QuerySetScope(User.objects.filter(is_staff=True)) + + templates = { + 'default': 'ahem/tests/test_template.html'} + + +class QuerySetScopeTests(TestCase): + + def setUp(self): + self.users = mommy.make('auth.User', _quantity=3) + + def test_gets_all_users_if_no_query_scope_is_set(self): + users = self.users + notification = QuerySetNotification() + + notification_users = notification.get_users('test_backend', {}) + self.assertEqual(len(users), len(notification_users)) + + def test_a_queryset_can_be_passed_to_the_scope(self): + staffs = mommy.make('auth.User', is_staff=True, _quantity=2) + + notification = CustomQuerySetNotification() + + notification_users = notification.get_users('test_backend', {}) + self.assertEqual(len(staffs), len(notification_users)) + + +class ContextFilterScopeNotification(Notification): + name = 'context_filter_scope_notification' + + backends = ['test_backend', 'other_backend'] + + scope = ContextFilterScope(context_key='user_is_staff', lookup_field='is_staff') + + templates = { + 'default': 'ahem/tests/test_template.html'} + + +class ContextFilterScopeTests(TestCase): + + def setUp(self): + self.users = mommy.make('auth.User', _quantity=3) + self.staffs = mommy.make('auth.User', is_staff=True, _quantity=2) + + self.notification = ContextFilterScopeNotification() + + def test_returns_only_non_staffs(self): + users = self.users + + notification_users = self.notification.get_users( + 'test_backend', {'user_is_staff': False}) + self.assertEqual(len(users), len(notification_users)) + + def test_returns_only_staffs(self): + staffs = self.staffs + + notification_users = self.notification.get_users( + 'test_backend', {'user_is_staff': True}) + self.assertEqual(len(staffs), len(notification_users)) diff --git a/ahem/tests/test_triggers.py b/ahem/tests/test_triggers.py index 0b6a19d..8b92fc0 100644 --- a/ahem/tests/test_triggers.py +++ b/ahem/tests/test_triggers.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + import pytz diff --git a/ahem/tests/test_triggers.py.bak b/ahem/tests/test_triggers.py.bak new file mode 100644 index 0000000..0b6a19d --- /dev/null +++ b/ahem/tests/test_triggers.py.bak @@ -0,0 +1,121 @@ +from __future__ import unicode_literals + +import pytz + +from datetime import timedelta, datetime + +from django.conf import settings +from django.utils import timezone +from django.test import TestCase +from django.test.utils import override_settings +from django.contrib.auth.models import User + +from model_mommy import mommy + +from ahem.backends import BaseBackend +from ahem.notification import Notification +from ahem.scopes import QuerySetScope +from ahem.triggers import DelayedTrigger, CalendarTrigger +from ahem.utils import celery_is_available + + +if celery_is_available(): + from celery.schedules import crontab +else: + crontab = None + + +class TestBackend(BaseBackend): + name = 'test_backend' + + +class DelayedTriggerNotification(Notification): + name = 'delayed_trigger_notification' + backends = ['test_backend'] + + scope = QuerySetScope() + trigger = DelayedTrigger(timedelta(days=2), at_hour=11, at_minute=0) + + templates = { + 'default': 'ahem/tests/test_template.html'} + + +class DelayedTriggerTests(TestCase): + + def setUp(self): + self.users = mommy.make('auth.User') + + self.notification = DelayedTriggerNotification() + + def test_is_not_periodic(self): + self.assertFalse(self.notification.is_periodic()) + + def test_eta_is_two_days_from_now(self): + eta = self.notification.get_next_run_eta() + + expected_day = (timezone.now() + timedelta(days=2)).day + self.assertEqual(expected_day, eta.day) + + def test_at_hour_and_at_minute_are_considered(self): + eta = self.notification.get_next_run_eta() + + self.assertEqual(eta.hour, 11) + self.assertEqual(eta.minute, 0) + + +if crontab: + class CalendarTriggerNotification(Notification): + name = 'calendar_trigger_notification' + backends = ['test_backend'] + + scope = QuerySetScope() + trigger = CalendarTrigger(crontab(hour=23, minute=45)) + + templates = { + 'default': 'ahem/tests/test_template.html'} + + + class CalendarTriggerTests(TestCase): + + def setUp(self): + self.users = mommy.make('auth.User') + + self.notification = CalendarTriggerNotification() + + def test_is_periodic(self): + self.assertTrue(self.notification.is_periodic) + + def test_next_eta(self): + # crontab schedules in the Celery timezone + # eta is therefore referent to Celery timezone + eta = self.notification.get_next_run_eta() + eta = eta.astimezone(pytz.timezone(settings.CELERY_TIMEZONE)) + + self.assertEqual(eta.hour, 23) + self.assertEqual(eta.minute, 45) + + @override_settings(USE_TZ=False) + def test_next_eta_use_tz_false(self): + eta = self.notification.get_next_run_eta() + eta = timezone.make_aware(eta, timezone.get_current_timezone()) + eta = eta.astimezone(pytz.timezone(settings.CELERY_TIMEZONE)) + + self.assertEqual(eta.hour, 23) + self.assertEqual(eta.minute, 45) + + @override_settings(TIME_ZONE='America/Sao_Paulo') + def test_next_eta_other_time_zone(self): + eta = self.notification.get_next_run_eta() + eta = eta.astimezone(pytz.timezone(settings.CELERY_TIMEZONE)) + + self.assertEqual(eta.hour, 23) + self.assertEqual(eta.minute, 45) + + @override_settings(USE_TZ=False, TIME_ZONE='America/Sao_Paulo') + def test_next_eta_use_tz_false_and_other_time_zone(self): + eta = self.notification.get_next_run_eta() + eta = timezone.make_aware(eta, timezone.get_current_timezone()) + eta = eta.astimezone(pytz.timezone(settings.CELERY_TIMEZONE)) + + self.assertEqual(eta.hour, 23) + self.assertEqual(eta.minute, 45) diff --git a/ahem/tests/test_utils.py b/ahem/tests/test_utils.py index f632c9a..8810373 100644 --- a/ahem/tests/test_utils.py +++ b/ahem/tests/test_utils.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from django.test import TestCase diff --git a/ahem/tests/test_utils.py.bak b/ahem/tests/test_utils.py.bak new file mode 100644 index 0000000..f632c9a --- /dev/null +++ b/ahem/tests/test_utils.py.bak @@ -0,0 +1,32 @@ +from __future__ import unicode_literals + +from django.test import TestCase + +from ahem.notification import Notification +from ahem.loader import add_to_registry, notification_registry +from ahem.utils import get_notification + + +class TestNotification(Notification): + name = 'test_notification' + + +class OtherNotification(Notification): + name = 'other_notification' + + +class GetNotificationTests(TestCase): + + def setUp(self): + add_to_registry(TestNotification) + add_to_registry(OtherNotification) + + def test_get_test_notification(self): + notification = get_notification(TestNotification.name) + + self.assertEqual(notification.__class__, TestNotification) + + def test_get_other_notification(self): + notification = get_notification(OtherNotification.name) + + self.assertEqual(notification.__class__, OtherNotification) diff --git a/ahem/triggers.py b/ahem/triggers.py index 4d5c4c5..e51bfc5 100644 --- a/ahem/triggers.py +++ b/ahem/triggers.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + import pytz import math diff --git a/ahem/triggers.py.bak b/ahem/triggers.py.bak new file mode 100644 index 0000000..4d5c4c5 --- /dev/null +++ b/ahem/triggers.py.bak @@ -0,0 +1,69 @@ +from __future__ import unicode_literals + +import pytz +import math + +from django.utils import timezone +from django.conf import settings + +from datetime import timedelta + + +class NotificationTrigger(object): + """ + Base class for notification triggers + """ + is_periodic = False + + def next_run_eta(self, last_run_at=None): + raise NotImplementedError + + +class DelayedTrigger(NotificationTrigger): + """ + A trigger that will only run one time + """ + def __init__(self, delay_timedelta=None, at_hour=None, at_minute=None): + """ + delay_timedelta will be added to the current time. + If an at_hour or at_minute is passed, the resulting + date will have it's hour and minute updated. + """ + self.delay_timedelta = delay_timedelta + self.at_hour = at_hour + self.at_minute = at_minute + + def next_run_eta(self, last_run_at=None): + if self.delay_timedelta is None: + return None + + eta = timezone.now() + self.delay_timedelta + + if self.at_hour is not None: + eta = eta.replace(hour=self.at_hour) + if self.at_minute is not None: + eta = eta.replace(minute=self.at_minute) + + if timezone.is_aware(eta): + eta = eta.astimezone(pytz.UTC) + + return eta + + +class CalendarTrigger(NotificationTrigger): + """ + A trigger that schedules itself again after is run + """ + is_periodic = True + + def __init__(self, crontab): + self.crontab = crontab + + def next_run_eta(self, last_run_at=None): + if not last_run_at: + last_run_at = timezone.now() + + is_due, next_time_to_run = self.crontab.is_due(last_run_at) + eta = last_run_at + timedelta(seconds=math.ceil(next_time_to_run)) + + return eta diff --git a/ahem/utils.py b/ahem/utils.py index 50bc66e..9a72eff 100644 --- a/ahem/utils.py +++ b/ahem/utils.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from importlib import import_module diff --git a/ahem/utils.py.bak b/ahem/utils.py.bak new file mode 100644 index 0000000..50bc66e --- /dev/null +++ b/ahem/utils.py.bak @@ -0,0 +1,47 @@ +from __future__ import unicode_literals + +from importlib import import_module + +from django.conf import settings + +from ahem.loader import notification_registry +from ahem.settings import AHEM_BACKENDS + + +def get_notification(notification_name): + return notification_registry[notification_name]() + + +def get_backend(backend_name): + if hasattr(settings, 'AHEM_BACKENDS'): + backend_paths = settings.AHEM_BACKENDS + else: + backend_paths = AHEM_BACKENDS + + for path in backend_paths: + module, backend_class = path.rsplit(".", 1) + module = import_module(module) + backend = getattr(module, backend_class) + if backend.name == backend_name: + return backend() + + raise Exception("The specifyed backend is not registered. Add it to AHEM_BACKENDS.") + + +def celery_is_available(): + try: + import celery + except ImportError: + return False + else: + return True + + +def register_user(backend_name, user, **settings): + backend = get_backend(backend_name) + backend.register_user(user, **settings) + + +def schedule_notification(notification_name, **params): + notification = get_notification(notification_name) + notification.schedule(**params) diff --git a/example/example/__init__.py b/example/example/__init__.py index 365e955..48c2ab2 100644 --- a/example/example/__init__.py +++ b/example/example/__init__.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import + from .celery import app as celery_app diff --git a/example/example/__init__.py.bak b/example/example/__init__.py.bak new file mode 100644 index 0000000..365e955 --- /dev/null +++ b/example/example/__init__.py.bak @@ -0,0 +1,4 @@ + +from __future__ import absolute_import + +from .celery import app as celery_app diff --git a/example/example/celery.py b/example/example/celery.py index acd20d0..5dff578 100644 --- a/example/example/celery.py +++ b/example/example/celery.py @@ -1,5 +1,5 @@ -from __future__ import absolute_import + import os from celery import Celery diff --git a/example/example/celery.py.bak b/example/example/celery.py.bak new file mode 100644 index 0000000..acd20d0 --- /dev/null +++ b/example/example/celery.py.bak @@ -0,0 +1,12 @@ + +from __future__ import absolute_import + +import os +from celery import Celery +from django.conf import settings + +os.environ.setdefault('DJANGO_SETTINGS_MODULE','example.settings') + +app = Celery('ahem') +app.config_from_object('django.conf:settings') +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) diff --git a/testsettings.py b/testsettings.py index 52ed0c7..fd5ff52 100644 --- a/testsettings.py +++ b/testsettings.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import + DATABASES = { 'default': { diff --git a/testsettings.py.bak b/testsettings.py.bak new file mode 100644 index 0000000..52ed0c7 --- /dev/null +++ b/testsettings.py.bak @@ -0,0 +1,58 @@ +from __future__ import absolute_import + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + }, +} + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + + 'ahem', +) + +import django +from distutils.version import LooseVersion + +if (LooseVersion(django.get_version()) >= LooseVersion('1.5') + and LooseVersion(django.get_version()) < LooseVersion('1.6')): + INSTALLED_APPS += ('discover_runner',) + TEST_RUNNER = 'discover_runner.DiscoverRunner' + +SECRET_KEY = 'abcde12345' + +if LooseVersion(django.get_version()) >= LooseVersion('1.7'): + MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware' + ) + +TIME_ZONE = 'America/Sao_Paulo' + +USE_TZ = True + +CELERY_TIMEZONE = 'UTC' +CELERY_EAGER_PROPAGATES_EXCEPTIONS = True +CELERY_ALWAYS_EAGER = True + +# Configuring celery to be able to run tasks +import os +from django.conf import settings + +try: + from celery import Celery +except ImportError: + Celery = None + +if Celery: + app = Celery('ahem') + app.config_from_object('django.conf:settings') +# end celery configuration