Skip to content
This repository has been archived by the owner on Dec 26, 2021. It is now read-only.

Add notifications #51

Merged
merged 61 commits into from
Jun 25, 2021
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
e96455c
start adding notifications from 3.0 package PR
girardinsamuel Feb 21, 2021
d677e39
Merge remote-tracking branch 'origin/master' into feat/32
girardinsamuel Feb 21, 2021
99430c6
Merge remote-tracking branch 'origin/master' into feat/32
girardinsamuel Feb 21, 2021
0eec8e9
add notification class tests
girardinsamuel Feb 21, 2021
c175cfc
continue refactoring
girardinsamuel Feb 21, 2021
0d47a11
refactor vonage driver
girardinsamuel Feb 21, 2021
e70f2ae
Merge remote-tracking branch 'origin/master' into feat/32
girardinsamuel Mar 13, 2021
71e12cc
remove contracts classes
girardinsamuel Mar 13, 2021
1c9c4f2
fix tests
girardinsamuel Mar 13, 2021
3e4d43a
remove need for vonage to be installed if not used
girardinsamuel Mar 13, 2021
17519eb
rewrite ndriver handling and vonage driver
girardinsamuel Mar 13, 2021
ad48dfe
simplify code again
girardinsamuel Mar 13, 2021
2f1811d
simplification
girardinsamuel Mar 13, 2021
e1533d7
clean code
girardinsamuel Mar 13, 2021
3eea238
start adding mock notification class
girardinsamuel Mar 13, 2021
45d228e
add registering of notifications mocking
girardinsamuel Mar 15, 2021
27d851a
update database with phone data in user
girardinsamuel Mar 15, 2021
5ce1cd6
add notification scaffolding command
girardinsamuel Mar 18, 2021
936b3fc
fix notification stub to include name
girardinsamuel Mar 18, 2021
a886d47
add database driver
girardinsamuel Mar 18, 2021
63744a6
update database driver to use builder
girardinsamuel Mar 18, 2021
f55a792
start adding broadcast driver for notifications
girardinsamuel Mar 18, 2021
ed2649b
add broadcast driver
girardinsamuel Mar 18, 2021
0b2654a
add responses dependency for mocking purposes
girardinsamuel Mar 18, 2021
e96a6cb
add missing hashids module
girardinsamuel Mar 18, 2021
73d17f2
fix notification provider by adding boot() back
girardinsamuel Mar 18, 2021
ea578ef
fix linting
girardinsamuel Mar 18, 2021
db5b43a
Merge branch 'master' into feat/32
girardinsamuel Apr 10, 2021
e7aece4
update tests
girardinsamuel Apr 10, 2021
99de0f9
add mocking notifications and tests
girardinsamuel Apr 10, 2021
374690f
add missing dependencies
girardinsamuel Apr 10, 2021
8bd2b51
fix tests
girardinsamuel Apr 12, 2021
bd95d68
fix mock mail tests to have better idempotence
girardinsamuel Apr 12, 2021
2b7016b
finalize adding mock capabilities
girardinsamuel Apr 12, 2021
d5754fe
return notifications sent from list
girardinsamuel Apr 12, 2021
a2be1cb
Apply suggestions from code review
girardinsamuel Apr 27, 2021
c61919c
add f-string
girardinsamuel Apr 27, 2021
74c13eb
make notification template more generic
girardinsamuel Apr 27, 2021
f56cad8
move notification drivers inside notifications feature folder
girardinsamuel Apr 27, 2021
d3a9f9c
Merge branch 'master' into feat/32
josephmancuso May 7, 2021
73b79fe
fixed tests
josephmancuso May 8, 2021
13e022a
formatted
josephmancuso May 8, 2021
2fd9d1f
fixed duplicate key
josephmancuso May 8, 2021
a4c7b83
modified notifications
josephmancuso May 8, 2021
aafc2a9
fixed tests
josephmancuso May 8, 2021
760117c
added database
josephmancuso May 8, 2021
fbafb9a
formatted
josephmancuso May 8, 2021
c358d3a
fixed test
josephmancuso May 8, 2021
b6c76ed
moved notification command into the notifications module
josephmancuso May 8, 2021
a445b1f
formatted
josephmancuso May 8, 2021
b5078e4
fixed the stub
josephmancuso May 8, 2021
7b88c3c
remove unused code
girardinsamuel May 10, 2021
11e83b7
remove queueing feature
girardinsamuel May 10, 2021
383025a
fix sending to multiple recipients
girardinsamuel May 10, 2021
b71eca1
remove unused code
girardinsamuel May 10, 2021
66a9eb8
Merge remote-tracking branch 'origin/master' into feat/32
girardinsamuel May 31, 2021
6ba7c8b
add back sqlite database from master
girardinsamuel May 31, 2021
feece29
update migrations to use big_increments
girardinsamuel May 31, 2021
dbdc446
migrate db to add notifications table
girardinsamuel May 31, 2021
487e570
Merge branch 'master' of https://github.com/MasoniteFramework/masonit…
josephmancuso Jun 25, 2021
3fb268a
formatted
josephmancuso Jun 25, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified database.sqlite3
Binary file not shown.
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ black
cryptography
masonite-orm
python-dotenv
waitress
waitress
responses
slackblocks
hashids
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@
"boto3",
"pusher",
"pymemcache",
"vonage",
"slackblocks",
],
},
# If there are data files included in your packages that need to be
Expand Down
1 change: 1 addition & 0 deletions src/masonite/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
DumpException,
InvalidSecretKey,
InvalidCSRFToken,
NotificationException,
)
4 changes: 4 additions & 0 deletions src/masonite/exceptions/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,7 @@ class ProjectProviderHttpError(Exception):

class ProjectTargetNotEmpty(Exception):
pass


class NotificationException(Exception):
pass
4 changes: 4 additions & 0 deletions src/masonite/foundation/Kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ def register_framework(self):

self.application.bind("base_url", "http://localhost:8000")

self.application.bind(
"notifications.location", "tests/integrations/notifications"
)

self.application.bind(
"router",
Router(),
Expand Down
39 changes: 39 additions & 0 deletions src/masonite/notification/AnonymousNotifiable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Anonymous Notifiable mixin"""

from .Notifiable import Notifiable


class AnonymousNotifiable(Notifiable):
"""Anonymous notifiable allowing to send notification without having
a notifiable entity.

Usage:
self.notification.route("sms", "+3346474764").send(WelcomeNotification())
"""

def __init__(self, application=None):
self.application = application
self._routes = {}

def route(self, driver, recipient):
"""Define which driver using to route the notification."""
if driver == "database":
raise ValueError(
"The database driver does not support on-demand notifications."
)
self._routes[driver] = recipient
return self

def route_notification_for(self, driver):
try:
return self._routes[driver]
except KeyError:
raise ValueError(
"Routing has not been defined for the driver {}".format(driver)
)

def send(self, notification, dry=False, fail_silently=False):
"""Send the given notification."""
return self.application.make("notification").send(
self, notification, self._routes, dry, fail_silently
)
38 changes: 38 additions & 0 deletions src/masonite/notification/DatabaseNotification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""DatabaseNotification Model."""
import pendulum
from masoniteorm.relationships import morph_to
from masoniteorm.models import Model


class DatabaseNotification(Model):
girardinsamuel marked this conversation as resolved.
Show resolved Hide resolved
"""DatabaseNotification Model allowing notifications to be stored in database."""

__fillable__ = ["id", "type", "data", "read_at", "notifiable_id", "notifiable_type"]
__table__ = "notifications"

@morph_to("notifiable_type", "notifiable_id")
def notifiable(self):
"""Get the notifiable entity that the notification belongs to."""
return

def mark_as_read(self):
"""Mark the notification as read."""
if not self.read_at:
self.read_at = pendulum.now()
return self.save(query=True)

def mark_as_unread(self):
"""Mark the notification as unread."""
if self.read_at:
self.read_at = None
return self.save(query=True)

@property
def is_read(self):
"""Determine if a notification has been read."""
return self.read_at is not None

@property
def is_unread(self):
"""Determine if a notification has not been read yet."""
return self.read_at is None
120 changes: 120 additions & 0 deletions src/masonite/notification/MockNotification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from .NotificationManager import NotificationManager
from .AnonymousNotifiable import AnonymousNotifiable
from .Notification import Notification


class NotificationWithAsserts(Notification):
def assertSentVia(self, *drivers):
sent_via = self.via(self.notifiable)
for driver in drivers:
assert (
driver in sent_via
), f"notification sent via {sent_via}, not {driver}."
return self

def assertEqual(self, value, reference):
assert value == reference, "{value} not equal to {reference}."
return self

def assertNotEqual(self, value, reference):
assert value != reference, "{value} equal to {reference}."
return self

def assertIn(self, value, container):
assert value in container, "{value} not in {container}."
return self

@classmethod
def patch(cls, target):
for k in cls.__dict__:
obj = getattr(cls, k)
if not k.startswith("_") and callable(obj):
setattr(target, k, obj)


class MockNotification(NotificationManager):
def __init__(self, application, *args, **kwargs):
super().__init__(application, *args, **kwargs)
self.count = 0
self.last_notifiable = None
self.last_notification = None

def send(
self, notifiables, notification, drivers=[], dry=False, fail_silently=False
):
_notifiables = []
for notifiable in self._format_notifiables(notifiables):
if isinstance(notifiable, AnonymousNotifiable):
_notifiables.extend(notifiable._routes.values())
else:
_notifiables.append(notifiable)

notification_key = notification.type()
NotificationWithAsserts.patch(notification.__class__)
for notifiable in _notifiables:
notification.notifiable = notifiable # for asserts
old_notifs = self.sent_notifications.get(notifiable, {})
old_notifs.update(
{
notification_key: old_notifs.get(notification_key, [])
+ [notification]
}
)
self.sent_notifications.update({notifiable: old_notifs})
self.count += 1
self.last_notification = notification
self.last_notifiable = notifiable
return self

def resetCount(self):
"""Reset sent notifications count."""
self.count = 0
self.sent_notifications = {}
self.last_notifiable = None
self.last_notification = None
return self

def assertNothingSent(self):
assert self.count == 0, f"{self.count} notifications have been sent."
return self

def assertCount(self, count):
assert (
self.count == count
), f"{self.count} notifications have been sent, not {count}."
return self

def assertSentTo(
self, notifiable, notification_class, callable_assert=None, count=None
):
notification_key = notification_class.__name__
notifiable_notifications = self.sent_notifications.get(notifiable, [])
assert notification_key in notifiable_notifications
if count:
sent_count = len(notifiable_notifications.get(notification_key, []))
assert (
sent_count == count
), f"{notification_key} has been sent to {notifiable} {sent_count} times"
if callable_assert:
# assert last notification sent for this notifiable
notification = notifiable_notifications.get(notification_key)[-1]
assert callable_assert(notifiable, notification)
return self

def last(self):
"""Get last sent mocked notification if any."""
return self.last_notification

def assertLast(self, callable_assert):
if not self.last_notifiable or not self.last_notification:
raise AssertionError("No notification has been sent.")
assert callable_assert(self.last_notifiable, self.last_notification)
return self

def assertNotSentTo(self, notifiable, notification_class):
notification_key = notification_class.__name__
notifiable_notifications = self.sent_notifications.get(notifiable, [])
assert (
notification_key not in notifiable_notifications
), f"{notification_key} has been sent to {notifiable}."
return self
63 changes: 63 additions & 0 deletions src/masonite/notification/Notifiable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Notifiable mixin"""
from masoniteorm.relationships import has_many

from .DatabaseNotification import DatabaseNotification
from ..exceptions.exceptions import NotificationException


class Notifiable:
"""Notifiable mixin allowing to send notification to a model. It's often used with the
User model.

Usage:
user.notify(WelcomeNotification())
"""

def notify(self, notification, drivers=[], dry=False, fail_silently=False):
"""Send the given notification."""
from wsgi import application
josephmancuso marked this conversation as resolved.
Show resolved Hide resolved

return application.make("notification").send(
self, notification, drivers, dry, fail_silently
)

def route_notification_for(self, driver):
"""Get the notification routing information for the given driver. If method has not been
defined on the model: for mail driver try to use 'email' field of model."""
# check if routing has been specified on the model
method_name = "route_notification_for_{0}".format(driver)

try:
method = getattr(self, method_name)
return method()
except AttributeError:
# if no method is defined on notifiable use default
if driver == "database":
# with database channel, notifications are saved to database
pass
elif driver == "mail":
return self.email
else:
raise NotificationException(
"Notifiable model does not implement {}".format(method_name)
)

@has_many("id", "notifiable_id")
def notifications(self):
"""Get all notifications sent to the model instance. Only for 'database'
notifications."""
return DatabaseNotification.where("notifiable_type", "users").order_by(
"created_at", direction="DESC"
)

@property
def unread_notifications(self):
"""Get the model instance unread notifications. Only for 'database'
notifications."""
return self.notifications.where("read_at", "==", None)

@property
def read_notifications(self):
"""Get the model instance read notifications. Only for 'database'
notifications."""
return self.notifications.where("read_at", "!=", None)
42 changes: 42 additions & 0 deletions src/masonite/notification/Notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Base Notification facade."""


class Notification:
def via(self, notifiable):
"""Defines the notification's delivery channels."""
raise NotImplementedError("via() method should be implemented.")

def should_send(self):
return True

def ignore_errors(self):
return False

def broadcast_on(self):
return "broadcast"

@classmethod
def type(cls):
"""Get notification type defined with class name."""
return cls.__name__

def dry(self):
"""Sets whether the notification should be sent or not.

Returns:
self
"""
self._dry = True
return self

def fail_silently(self):
"""Sets whether the notification can fail silently (without raising exceptions).

Returns:
self
"""
self._fail_silently = True
return self

def text(self, text):
return Sms().text(text)
Loading