Skip to content

Commit 78430c7

Browse files
authored
Merge pull request #75 from silverlogic/BA-notification-settings
baseapp-notifications: allow user to enable/disable notifications
2 parents 9c5772d + dd30f5f commit 78430c7

File tree

13 files changed

+553
-23
lines changed

13 files changed

+553
-23
lines changed

baseapp-notifications/README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,15 @@ CELERY_TASK_ROUTES = {
7171
}
7272
```
7373

74-
4 - Make sure that your main `User`'s `DjangoObjectType` implements interface `NotificationsNode`:
74+
4 - Make sure that your main `User`'s `DjangoObjectType` implements interface `NotificationsInterface`:
7575

7676
```python
7777
from baseapp_core.graphql import DjangoObjectType
78-
from baseapp_notifications.graphql.object_types import NotificationsNode
78+
from baseapp_notifications.graphql.object_types import NotificationsInterface
7979

8080
class UserNode(DjangoObjectType):
8181
class Meta:
82-
interfaces = (relay.Node, NotificationsNode)
82+
interfaces = (relay.Node, NotificationsInterface)
8383
```
8484

8585
5 - Then you can expose notification's mutations and subscriptions:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import swapper
2+
from django.contrib import admin
3+
4+
NotificationSetting = swapper.load_model("baseapp_notifications", "NotificationSetting")
5+
6+
7+
@admin.register(NotificationSetting)
8+
class NotificationSettingAdmin(admin.ModelAdmin):
9+
list_display = ["user", "verb", "channel", "is_active", "created"]
10+
list_filter = ["channel", "is_active"]
11+
search_fields = ["user__username", "verb"]

baseapp-notifications/baseapp_notifications/base.py

+26
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
from baseapp_core.graphql.models import RelayModel
2+
from django.conf import settings
3+
from django.db import models
4+
from django.utils.translation import gettext_lazy as _
5+
from model_utils.models import TimeStampedModel
26
from notifications.base.models import AbstractNotification as BaseAbstractNotification
37

48

@@ -27,3 +31,25 @@ def delete(self, *args, **kwargs):
2731
OnNotificationChange.send_delete_notification(
2832
recipient_id=self.recipient_id, notification_id=notification_relay_id
2933
)
34+
35+
36+
class AbstractNotificationSetting(TimeStampedModel, RelayModel):
37+
class Meta:
38+
abstract = True
39+
40+
class NotificationChannelTypes(models.IntegerChoices):
41+
ALL = 0, _("All")
42+
EMAIL = 1, _("Email")
43+
PUSH = 2, _("Push")
44+
IN_APP = 3, _("In-App")
45+
46+
@property
47+
def description(self):
48+
return self.label
49+
50+
user = models.ForeignKey(
51+
settings.AUTH_USER_MODEL, related_name="notifications_settings", on_delete=models.CASCADE
52+
)
53+
channel = models.IntegerField(choices=NotificationChannelTypes.choices)
54+
verb = models.CharField(max_length=255)
55+
is_active = models.BooleanField(default=True)

baseapp-notifications/baseapp_notifications/graphql/mutations.py

+66-3
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
from baseapp_core.graphql.utils import get_pk_from_relay_id
55
from django.utils.translation import gettext_lazy as _
66

7-
from .object_types import NotificationNode, NotificationsNode
7+
from .object_types import (
8+
NotificationChannelTypesEnum,
9+
NotificationNode,
10+
NotificationSettingNode,
11+
NotificationsInterface,
12+
)
813

914
Notification = swapper.load_model("notifications", "Notification")
15+
NotificationSetting = swapper.load_model("baseapp_notifications", "NotificationSetting")
1016

1117

1218
class NotificationsMarkAllAsRead(RelayMutation):
13-
recipient = graphene.Field(NotificationsNode)
19+
recipient = graphene.Field(NotificationsInterface)
1420

1521
class Input:
1622
read = graphene.Boolean(required=True, description=_("Mark as read or unread"))
@@ -31,7 +37,7 @@ def mutate_and_get_payload(cls, root, info, read, **input):
3137

3238

3339
class NotificationsMarkAsRead(RelayMutation):
34-
recipient = graphene.Field(NotificationsNode)
40+
recipient = graphene.Field(NotificationsInterface)
3541
notifications = graphene.List(NotificationNode)
3642

3743
class Input:
@@ -59,6 +65,63 @@ def mutate_and_get_payload(cls, root, info, notification_ids, read, **input):
5965
)
6066

6167

68+
class NotificationSettingToggle(RelayMutation):
69+
notification_setting = graphene.Field(NotificationSettingNode)
70+
71+
class Input:
72+
verb = graphene.String(required=True)
73+
channel = graphene.Field(NotificationChannelTypesEnum, required=True)
74+
75+
@classmethod
76+
@login_required
77+
def mutate_and_get_payload(cls, root, info, verb, channel, **input):
78+
# Determine if a setting other than 'ALL' exists for the given verb
79+
is_channel_all = channel == NotificationSetting.NotificationChannelTypes.ALL
80+
81+
# Create or update the notification setting
82+
notification_setting, created = NotificationSetting.objects.get_or_create(
83+
user=info.context.user,
84+
verb=verb,
85+
channel=channel,
86+
defaults={"is_active": False},
87+
)
88+
89+
if not created:
90+
notification_setting.is_active = not notification_setting.is_active
91+
notification_setting.save(update_fields=["is_active"])
92+
93+
# Update other settings based on 'ALL' channel logic
94+
if is_channel_all:
95+
has_non_active_setting = (
96+
NotificationSetting.objects.filter(
97+
user=info.context.user, verb=verb, is_active=False
98+
)
99+
.exclude(channel=NotificationSetting.NotificationChannelTypes.ALL)
100+
.exists()
101+
)
102+
if has_non_active_setting:
103+
# If a non-active setting exists, update 'ALL' setting to False
104+
NotificationSetting.objects.filter(
105+
user=info.context.user,
106+
verb=verb,
107+
channel=NotificationSetting.NotificationChannelTypes.ALL,
108+
).update(is_active=False)
109+
# Update all settings to match the 'ALL' setting
110+
NotificationSetting.objects.filter(user=info.context.user, verb=verb).update(
111+
is_active=notification_setting.is_active
112+
)
113+
else:
114+
# If the current channel is not 'ALL', ensure 'ALL' settings are updated to match
115+
NotificationSetting.objects.filter(
116+
user=info.context.user,
117+
verb=verb,
118+
channel=NotificationSetting.NotificationChannelTypes.ALL,
119+
).update(is_active=notification_setting.is_active)
120+
121+
return NotificationSettingToggle(notification_setting=notification_setting)
122+
123+
62124
class NotificationsMutations(object):
63125
notifications_mark_as_read = NotificationsMarkAsRead.Field()
64126
notifications_mark_all_as_read = NotificationsMarkAllAsRead.Field()
127+
notification_setting_toggle = NotificationSettingToggle.Field()

baseapp-notifications/baseapp_notifications/graphql/object_types.py

+47-1
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,27 @@
33
import swapper
44
from baseapp_core.graphql import DjangoObjectType
55
from graphene import relay
6+
from graphene_django import DjangoConnectionField
67
from graphene_django.filter import DjangoFilterConnectionField
78

9+
from ..utils import can_user_receive_notification
810
from .filters import NotificationFilter
911

1012
Notification = swapper.load_model("notifications", "Notification")
13+
NotificationSetting = swapper.load_model("baseapp_notifications", "NotificationSetting")
14+
NotificationChannelTypesEnum = graphene.Enum.from_enum(NotificationSetting.NotificationChannelTypes)
1115

1216

13-
class NotificationsNode(relay.Node):
17+
class NotificationsInterface(relay.Node):
1418
notifications_unread_count = graphene.Int()
1519
notifications = DjangoFilterConnectionField(
1620
lambda: NotificationNode, filterset_class=NotificationFilter
1721
)
22+
notification_settings = DjangoConnectionField(lambda: NotificationSettingNode)
23+
is_notification_setting_active = graphene.Boolean(
24+
verb=graphene.String(required=True),
25+
channel=NotificationChannelTypesEnum(required=True),
26+
)
1827

1928
def resolve_notifications_unread_count(self, info):
2029
if self.is_authenticated:
@@ -28,6 +37,16 @@ def resolve_notifications(self, info, **kwargs):
2837
)
2938
return Notification.objects.none()
3039

40+
def resolve_notification_settings(self, info, **kwargs):
41+
if info.context.user.is_authenticated and info.context.user == self:
42+
return NotificationSetting.objects.filter(user=info.context.user)
43+
return NotificationSetting.objects.none()
44+
45+
def resolve_is_notification_setting_active(self, info, verb, channel, **kwargs):
46+
if info.context.user.is_authenticated and info.context.user == self:
47+
return can_user_receive_notification(info.context.user.id, verb, channel)
48+
return False
49+
3150

3251
class NotificationNode(gql_optimizer.OptimizedDjangoObjectType, DjangoObjectType):
3352
actor = graphene.Field(relay.Node)
@@ -56,3 +75,30 @@ def get_queryset(cls, queryset, info):
5675
return queryset.none()
5776

5877
return super().get_queryset(queryset.filter(recipient=info.context.user), info)
78+
79+
80+
class NotificationSettingNode(gql_optimizer.OptimizedDjangoObjectType, DjangoObjectType):
81+
channel = graphene.Field(NotificationChannelTypesEnum)
82+
83+
class Meta:
84+
model = NotificationSetting
85+
fields = "__all__"
86+
interfaces = (relay.Node,)
87+
88+
@classmethod
89+
def get_queryset(cls, queryset, info):
90+
if not info.context.user.is_authenticated:
91+
return queryset.none()
92+
93+
return super().get_queryset(queryset.filter(user=info.context.user), info)
94+
95+
@classmethod
96+
def get_node(cls, info, id):
97+
if not info.context.user.is_authenticated:
98+
return None
99+
100+
try:
101+
queryset = cls.get_queryset(cls._meta.model.objects, info)
102+
return queryset.get(id=id, user=info.context.user)
103+
except cls._meta.model.DoesNotExist:
104+
return None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Generated by Django 4.2.10 on 2024-02-08 13:53
2+
3+
import django.db.models.deletion
4+
import django.utils.timezone
5+
import model_utils.fields
6+
from django.conf import settings
7+
from django.db import migrations, models
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14+
("baseapp_notifications", "0001_initial"),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name="NotificationSetting",
20+
fields=[
21+
(
22+
"id",
23+
models.AutoField(
24+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
25+
),
26+
),
27+
(
28+
"created",
29+
model_utils.fields.AutoCreatedField(
30+
default=django.utils.timezone.now, editable=False, verbose_name="created"
31+
),
32+
),
33+
(
34+
"modified",
35+
model_utils.fields.AutoLastModifiedField(
36+
default=django.utils.timezone.now, editable=False, verbose_name="modified"
37+
),
38+
),
39+
(
40+
"channel",
41+
models.IntegerField(
42+
choices=[(0, "All"), (1, "Email"), (2, "Push"), (3, "In-App")]
43+
),
44+
),
45+
("verb", models.CharField(max_length=255)),
46+
("is_active", models.BooleanField(default=True)),
47+
(
48+
"user",
49+
models.ForeignKey(
50+
on_delete=django.db.models.deletion.CASCADE,
51+
related_name="notifications_settings",
52+
to=settings.AUTH_USER_MODEL,
53+
),
54+
),
55+
],
56+
options={
57+
"swappable": "BASEAPP_NOTIFICATIONS_NOTIFICATIONSETTING_MODEL",
58+
},
59+
),
60+
]
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
from .base import AbstractNotification
1+
import swapper
2+
3+
from .base import AbstractNotification, AbstractNotificationSetting
24

35

46
class Notification(AbstractNotification):
57
class Meta(AbstractNotification.Meta):
68
abstract = False
9+
10+
11+
class NotificationSetting(AbstractNotificationSetting):
12+
class Meta:
13+
swappable = swapper.swappable_setting("baseapp_notifications", "NotificationSetting")

baseapp-notifications/baseapp_notifications/tests/factories.py

+7
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,10 @@ class NotificationFactory(AbstractNotificationFactory):
1818

1919
class Meta:
2020
model = Notification
21+
22+
23+
class NotificationSettingFactory(factory.django.DjangoModelFactory):
24+
user = factory.SubFactory(UserFactory)
25+
26+
class Meta:
27+
model = swapper.load_model("baseapp_notifications", "NotificationSetting")

0 commit comments

Comments
 (0)