Skip to content

Commit

Permalink
Merge pull request #4 from Savage-Aim/notifications
Browse files Browse the repository at this point in the history
Notifications
  • Loading branch information
freyamade authored Feb 1, 2022
2 parents 011baae + 9ef438c commit 8d05b2f
Show file tree
Hide file tree
Showing 56 changed files with 1,471 additions and 285 deletions.
22 changes: 22 additions & 0 deletions backend/api/management/commands/notification_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from api import models


class Command(BaseCommand):
help = 'Set / Update the initial values of the notification details for every user.'

def handle(self, *args, **options):
# Add the tiers
for user in User.objects.all():
try:
for key in models.Settings.NOTIFICATIONS:
if key not in user.settings.notifications:
user.settings.notifications[key] = True
user.settings.save()
except models.Settings.DoesNotExist:
models.Settings.objects.create(
user=user,
theme='beta',
notifications={key: True for key in models.Settings.NOTIFICATIONS},
)
23 changes: 23 additions & 0 deletions backend/api/migrations/0015_settings_updates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.11 on 2022-01-27 13:57

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0014_user_settings'),
]

operations = [
migrations.AddField(
model_name='settings',
name='notifications',
field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='settings',
name='theme',
field=models.CharField(max_length=24),
),
]
31 changes: 31 additions & 0 deletions backend/api/migrations/0016_notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 3.2.11 on 2022-01-28 10:26

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('api', '0015_settings_updates'),
]

operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('link', models.TextField()),
('read', models.BooleanField(default=False)),
('text', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
('type', models.TextField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-timestamp'],
},
),
]
3 changes: 3 additions & 0 deletions backend/api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .gear import Gear
from .job import Job
from .loot import Loot
from .notification import Notification
from .settings import Settings
from .team import Team
from .team_member import TeamMember
Expand All @@ -19,6 +20,8 @@

'Loot',

'Notification',

'Settings',

'Team',
Expand Down
17 changes: 17 additions & 0 deletions backend/api/models/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.contrib.auth.models import User
from django.db import models


class Notification(models.Model):
link = models.TextField()
read = models.BooleanField(default=False)
text = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
type = models.TextField() # Type field maintains the internal notification type, used to send updates via ws
user = models.ForeignKey(User, on_delete=models.CASCADE)

def __str__(self):
return f'Notification #{self.id} for {self.user.username}'

class Meta:
ordering = ['-timestamp']
22 changes: 9 additions & 13 deletions backend/api/models/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,14 @@


class Settings(models.Model):
BETA = 'beta'
BLUE = 'blue'
GREEN = 'green'
PURPLE = 'purple'
RED = 'red'
THEMES = (
(BETA, BETA),
(BLUE, BLUE),
(GREEN, GREEN),
(PURPLE, PURPLE),
(RED, RED),
)
NOTIFICATIONS = {
'loot_tracker_update',
'team_join',
'team_lead',
'verify_fail',
'verify_success',
}

theme = models.CharField(max_length=24, choices=THEMES)
notifications = models.JSONField(default=dict)
theme = models.CharField(max_length=24)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='settings')
2 changes: 1 addition & 1 deletion backend/api/models/team_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class TeamMember(models.Model):
# TODO - Gotta warn about leaving teams or deleting BISLists (which can't be done yet)
bis_list = models.ForeignKey('BISList', on_delete=models.PROTECT)
character = models.ForeignKey('Character', on_delete=models.CASCADE)
# TODO - Can't delete if you're the raid lead / move raidlead to someone else if character is deleted
# TODO - Can't delete if you're the team lead / move teamlead to someone else if character is deleted
lead = models.BooleanField(default=False)
team = models.ForeignKey('Team', on_delete=models.CASCADE, related_name='members')

Expand Down
63 changes: 63 additions & 0 deletions backend/api/notifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
Notifier contains a series of functions to create notifications for every different
type used in the system, without having to add messy code elsewhere
Also will handle sending info to websockets when we get there
"""
from django.contrib.auth.models import User
from . import models


def _create_notif(user: User, text: str, link: str, type: str):
"""
Actually does the work of creating a Notification (and sending it down the websockets later)
Also is where the notification settings are checked, we won't save notifications that the User doesn't want
"""
# First we ensure that the User is set up to receive the notification type
try:
send = user.settings.notifications[type]
except (AttributeError, models.Settings.DoesNotExist, KeyError):
send = True

if not send:
return

# If we make it to this point, create the object and then push updates down the web socket
models.Notification.objects.create(user=user, text=text, link=link, type=type)
# TODO - Websocket stuff


def loot_tracker_update(bis: models.BISList, team: models.Team):
char = bis.owner
text = f'"{char}"\'s {bis.job.id} BIS List was updated via "{team.name}"\'s Loot Tracker!'
link = f'/characters/{char.id}/bis_list/{bis.id}/'
user = char.user
_create_notif(user, text, link, 'loot_tracker_update')


def team_join(char: models.Character, team: models.Team):
text = f'{char} has joined {team.name}!'
link = f'/team/{team.id}/'
user = team.members.get(lead=True).character.user
_create_notif(user, text, link, 'team_join')


def team_lead(char: models.Character, team: models.Team):
text = f'{char} has been made the Team Leader of {team.name}!'
link = f'/team/{team.id}/'
user = char.user
_create_notif(user, text, link, 'team_lead')


def verify_fail(char: models.Character, error: str):
text = f'The verification of {char} has failed! Reason: {error}'
link = f'/characters/{char.id}/'
user = char.user
_create_notif(user, text, link, 'verify_fail')


def verify_success(char: models.Character):
text = f'The verification of {char} has succeeded!'
link = f'/characters/{char.id}/'
user = char.user
_create_notif(user, text, link, 'verify_success')
3 changes: 3 additions & 0 deletions backend/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .gear import GearSerializer
from .job import JobSerializer
from .loot import LootSerializer, LootCreateSerializer, LootCreateWithBISSerializer
from .notification import NotificationSerializer
from .settings import SettingsSerializer
from .team import (
TeamSerializer,
Expand All @@ -28,6 +29,8 @@
'LootCreateSerializer',
'LootCreateWithBISSerializer',

'NotificationSerializer',

'SettingsSerializer',

'TeamSerializer',
Expand Down
17 changes: 17 additions & 0 deletions backend/api/serializers/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
Serializer for Notification entries
"""
# lib
from rest_framework import serializers
# local
from api.models import Notification

__all__ = [
'NotificationSerializer',
]


class NotificationSerializer(serializers.ModelSerializer):
class Meta:
exclude = ['user']
model = Notification
35 changes: 34 additions & 1 deletion backend/api/serializers/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Serializer for a request user's information
"""
# stdlib
from typing import Dict
# lib
from rest_framework import serializers
# local
Expand All @@ -10,9 +12,40 @@
'SettingsSerializer',
]

NOTIFICATION_VALUES = {True, False}

THEMES = {
'beta',
'blue',
'green',
'purple',
'red',
'trans',
}


class SettingsSerializer(serializers.ModelSerializer):

class Meta:
model = Settings
fields = ['theme']
fields = ['notifications', 'theme']

def validate_notifications(self, notifications: Dict[str, bool]) -> Dict[str, bool]:
"""
Ensure that the notifications dict sent by the user only contains valid keys
"""
for key, value in notifications.items():
# Check that the key is in the allowed strings, and the value is a valid bool
if key not in Settings.NOTIFICATIONS:
raise serializers.ValidationError(f'"{key}" is not a valid choice.')
if value not in NOTIFICATION_VALUES:
raise serializers.ValidationError(f'"{key}" does not have a boolean for a value.')
return notifications

def validate_theme(self, theme: str) -> str:
"""
Ensure the theme is in the set of allowed themes
"""
if theme not in THEMES:
raise serializers.ValidationError(f'"{theme}" is not a valid choice.')
return theme
16 changes: 8 additions & 8 deletions backend/api/serializers/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ class Meta:


class TeamUpdateSerializer(serializers.ModelSerializer):
raid_lead = serializers.IntegerField()
team_lead = serializers.IntegerField()
tier_id = serializers.IntegerField()

class Meta:
model = Team
fields = ['name', 'tier_id', 'raid_lead']
write_only_fields = ['raid_lead']
fields = ['name', 'tier_id', 'team_lead']
write_only_fields = ['team_lead']

def validate_tier_id(self, tier_id: int) -> int:
"""
Expand All @@ -45,16 +45,16 @@ def validate_tier_id(self, tier_id: int) -> int:

return tier_id

def validate_raid_lead(self, raid_lead_id: int) -> int:
def validate_team_lead(self, team_lead_id: int) -> int:
"""
Ensure that the raid lead id is a valid int and refers to a valid member of the team.
The sent raid lead id will be the id of the character, the returned value the id of the TM object.
Ensure that the team lead id is a valid int and refers to a valid member of the team.
The sent team lead id will be the id of the character, the returned value the id of the TM object.
"""
# Ensure it corresponds with a member of this team
try:
member = self.instance.members.get(character__pk=raid_lead_id)
member = self.instance.members.get(character__pk=team_lead_id)
except TeamMember.DoesNotExist:
raise serializers.ValidationError('Please select a member of the Team to be the new raid lead.')
raise serializers.ValidationError('Please select a member of the Team to be the new team lead.')

return member.id

Expand Down
17 changes: 17 additions & 0 deletions backend/api/serializers/user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Serializer for a request user's information
"""
# stdlib
from typing import Dict
# lib
from rest_framework import serializers
# local
Expand All @@ -14,6 +16,7 @@
class UserSerializer(serializers.Serializer):
avatar_url = serializers.SerializerMethodField()
id = serializers.IntegerField()
notifications = serializers.SerializerMethodField()
theme = serializers.SerializerMethodField()
username = serializers.CharField()

Expand All @@ -25,6 +28,20 @@ def get_avatar_url(self, obj) -> str:
return obj.socialaccount_set.first().get_avatar_url()
return ''

def get_notifications(self, obj) -> Dict[str, bool]:
"""
Populate a full dictionary of notifications, filling defaults in as needed
"""
defaults = {key: True for key in Settings.NOTIFICATIONS}

try:
defaults.update(obj.settings.notifications)
except (AttributeError, Settings.DoesNotExist):
# Don't have to do anything here
pass

return defaults

def get_theme(self, obj) -> str:
"""
Get the theme the user has set. Defaults to beta if there's no settings instance
Expand Down
Loading

0 comments on commit 8d05b2f

Please sign in to comment.