Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
MorganeAD committed Mar 7, 2023
1 parent 88c26bc commit 6c6e1aa
Show file tree
Hide file tree
Showing 13 changed files with 424 additions and 3 deletions.
4 changes: 3 additions & 1 deletion src/backend/joanie/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,4 +404,6 @@ class CourseWishAdmin(admin.ModelAdmin):
"course",
"owner",
)
readonly_fields = ("id",)
readonly_fields = (
"id",
)
15 changes: 15 additions & 0 deletions src/backend/joanie/core/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ def ready(self):
sender=models.CourseRun,
dispatch_uid="save_course_run",
)
post_save.connect(
signals.post_save_course_run_notification,
sender=models.CourseRun,
dispatch_uid="post_save_course_run_notification",
)
post_save.connect(
signals.post_save_product_target_course_relation_notification,
sender=models.ProductTargetCourseRelation,
dispatch_uid="post_save_product_target_course_relation_notification",
)
m2m_changed.connect(
signals.post_add_m2m_product_target_course_notification,
sender=models.Product.target_courses.through,
dispatch_uid="post_add_m2m_product_target_course_notification",
)
post_save.connect(
signals.on_save_product_target_course_relation,
sender=models.ProductTargetCourseRelation,
Expand Down
17 changes: 17 additions & 0 deletions src/backend/joanie/core/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone as django_timezone

import factory.fuzzy
Expand Down Expand Up @@ -400,3 +401,19 @@ class Meta:

course = factory.SubFactory(CourseFactory)
owner = factory.SubFactory(UserFactory)


class NotificationFactory(factory.django.DjangoModelFactory):
"""A factory to create an user wish"""

class Meta:
model = models.Notification
exclude = ['notif_subject, notif_object']

owner = factory.SubFactory(UserFactory)

notif_subject_id = factory.SelfAttribute('notif_subject.id')
notif_subject_ctype = factory.LazyAttribute(lambda o: ContentType.objects.get_for_model(o.notif_subject))

notif_object_id = factory.SelfAttribute('notif_object.id')
notif_object_ctype = factory.LazyAttribute(lambda o: ContentType.objects.get_for_model(o.notif_object))
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 4.0.10 on 2023-02-24 16:37

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


class Migration(migrations.Migration):

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('core', '0002_coursewish'),
]

operations = [
migrations.AlterField(
model_name='courserun',
name='is_listed',
field=models.BooleanField(default=False, help_text='If checked the course run will be included in the list of course runs available for enrollment on the related course page.', verbose_name='is listed'),
),
migrations.CreateModel(
name='Notification',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_on', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_on', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('notif_subject_id', models.UUIDField()),
('notif_object_id', models.UUIDField()),
('action_type', models.CharField(choices=[('EMAIL', 'Send an email')], default='EMAIL', max_length=10, verbose_name='Type of action')),
('notified_at', models.DateTimeField(blank=True, editable=False, help_text='date and time at which an email has been sent to the user', null=True, verbose_name='Notified at')),
('notif_object_ctype', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='object_ctype_of_notifications', to='contenttypes.contenttype')),
('notif_subject_ctype', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subject_ctype_of_notifications', to='contenttypes.contenttype')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Notification',
'verbose_name_plural': 'Notifications',
'db_table': 'joanie_notification',
'ordering': ('owner', 'notified_at'),
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 4.0.10 on 2023-02-27 15:55

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


class Migration(migrations.Migration):

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('core', '0003_alter_courserun_is_listed_notification'),
]

operations = [
migrations.RenameField(
model_name='notification',
old_name='action_type',
new_name='notif_type',
),
migrations.AlterField(
model_name='notification',
name='notif_object_ctype',
field=models.ForeignKey(limit_choices_to=models.Q(models.Q(('app_label', 'core'), ('model', 'producttargetcourserelation')), models.Q(('app_label', 'core'), ('model', 'courserun')), models.Q(('app_label', 'core'), ('model', 'product')), _connector='OR'), on_delete=django.db.models.deletion.CASCADE, related_name='object_ctype_of_notifications', to='contenttypes.contenttype'),
),
migrations.AlterField(
model_name='notification',
name='notif_subject_ctype',
field=models.ForeignKey(limit_choices_to=models.Q(models.Q(('app_label', 'core'), ('model', 'coursewish')), models.Q(('app_label', 'core'), ('model', 'enrollment')), _connector='OR'), on_delete=django.db.models.deletion.CASCADE, related_name='subject_ctype_of_notifications', to='contenttypes.contenttype'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.0.10 on 2023-02-28 09:42

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0004_rename_action_type_notification_notif_type_and_more'),
]

operations = [
migrations.AddField(
model_name='notification',
name='action',
field=models.CharField(choices=[('ADD', 'added'), ('CREATE', 'created')], default='CREATE', max_length=10, verbose_name='Action on the object of notification'),
),
migrations.AlterField(
model_name='notification',
name='notif_type',
field=models.CharField(choices=[('EMAIL', 'Send an email')], default='EMAIL', max_length=10, verbose_name='Type of notification'),
),
]
1 change: 1 addition & 0 deletions src/backend/joanie/core/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from .courses import *
from .products import *
from .wishlist import *
from .notifications import *
4 changes: 3 additions & 1 deletion src/backend/joanie/core/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import uuid
from itertools import chain

from django.contrib.contenttypes.fields import GenericForeignKey
from django.db import models
from django.utils.translation import gettext_lazy as _

Expand Down Expand Up @@ -52,7 +53,8 @@ def to_dict(self):
opts = self._meta
data = {}
for field in chain(opts.concrete_fields, opts.private_fields):
data[field.name] = field.value_from_object(self)
if not isinstance(field, GenericForeignKey):
data[field.name] = field.value_from_object(self)
for field in opts.many_to_many:
data[field.name] = [related.id for related in field.value_from_object(self)]
return data
126 changes: 126 additions & 0 deletions src/backend/joanie/core/models/notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""
Declare and configure the models for the notification part
"""
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.translation import gettext_lazy as _

from . import User
from .base import BaseModel


class Notification(BaseModel):
"""
Notification model define a notification about an action on a object instance according a
subject instance.
notif_subject: [GenericForeignKey]
Defined using notif_subject_ctype [ContentType] and notif_subject_id [uuid]
It represents the user's subject that he want to be notified about.
It can be a user course wish or a user course enrollment.
notif_object: [GenericForeignKey]
Defined using notif_object_ctype [ContentType] and notif_object_id [uuid]
It represents the object that generate the notification according the user's subject
It can be a course run, a product or the relation between a product and an course.
action: [CharField with NOTIF_ACTIONS choices]
It represents the action done on the object that generate the notification
The action's values are
"ADD": the object has been add in a m2m relationship
"CREATE": the object has been create
notif_type: [CharField with NOTIF_TYPES choices]
It's the type of the notification
It can be an email or a dashboard notification (not implemented yet)
notified_at: [DateTimeField]
It's the DatiTime when the notification has been sent to the owner.
If the value is None, the notification has not been sent yet.
owner: [User]
It's the User that received or will receive the notification.
"""

NOTIF_ACTION_ADD = "ADD"
NOTIF_ACTION_CREATE = "CREATE"

NOTIF_ACTIONS = (
(NOTIF_ACTION_ADD, _("added")),
(NOTIF_ACTION_CREATE, _("created")),
)

NOTIF_TYPE_EMAIL = "EMAIL"

NOTIF_TYPES = (
(NOTIF_TYPE_EMAIL, _("Send an email")),
)

notif_subject_ctype_limit = models.Q(app_label='core', model='coursewish') \
| models.Q(app_label='core', model='enrollment')
notif_subject_ctype = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name="subject_ctype_of_notifications",
limit_choices_to=notif_subject_ctype_limit
)
notif_subject_id = models.UUIDField()
notif_subject = GenericForeignKey('notif_subject_ctype', 'notif_subject_id')

notif_object_ctype_limit = models.Q(app_label='core', model='producttargetcourserelation') \
| models.Q(app_label='core', model='courserun') \
| models.Q(app_label='core', model='product')
notif_object_ctype = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name="object_ctype_of_notifications",
limit_choices_to=notif_object_ctype_limit
)
notif_object_id = models.UUIDField()
notif_object = GenericForeignKey('notif_object_ctype', 'notif_object_id')

action = models.CharField(
_("Action on the object of notification"),
choices=NOTIF_ACTIONS,
max_length=10,
default=NOTIF_ACTION_CREATE
)

notif_type = models.CharField(
_("Type of notification"),
choices=NOTIF_TYPES,
max_length=10,
default=NOTIF_TYPE_EMAIL
)

notified_at = models.DateTimeField(
verbose_name=_("Notified at"),
help_text=_("date and time when the notification has been sent to the owner"),
blank=True,
null=True,
editable=False,
)

owner = models.ForeignKey(User, on_delete=models.PROTECT)

class Meta:
db_table = "joanie_notification"
ordering = ("owner", "notified_at")
verbose_name = _(
"Notification"
)
verbose_name_plural = _(
"Notifications"
)

def __str__(self):
if self.notified_at:
return (
f"'{self.owner}' has been notified about "
f"'{self.notif_object}' according to '{self.notif_subject}' at '{self.notified_at}'"
)
return (
f"'{self.owner}' hasn't been notified about "
f"'{self.notif_object}' according to '{self.notif_subject}' yet"
)
2 changes: 1 addition & 1 deletion src/backend/joanie/core/models/wishlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

class CourseWish(BaseModel):
"""
CourseWish represents and records a user wish to participate at a course
CourseWish represents and records a user wish to participate to a course
"""

owner = models.ForeignKey(
Expand Down
55 changes: 55 additions & 0 deletions src/backend/joanie/core/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
import logging

from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError

from joanie.core import enums, models
Expand Down Expand Up @@ -157,3 +158,57 @@ def on_change_course_product_relation(action, instance, pk_set, **kwargs):
return

webhooks.synchronize_course_runs(serialized_course_runs)


def base_create_notification(instance, course_wishes, action=None):
"""
Create a notification instance to each user who has a course_wish to participate to
the course related to the saved instance
"""
for course_wish in course_wishes:
course_wish_ctype = ContentType.objects.get_for_model(course_wish._meta.model)
instance_ctype = ContentType.objects.get_for_model(instance._meta.model)

notif_data = {
"owner": course_wish.owner,
"notif_subject_ctype": course_wish_ctype,
"notif_subject_id": course_wish.id,
"notif_object_ctype": instance_ctype,
"notif_object_id": instance.id,
}
if action:
notif_data["action"] = action

models.Notification.objects.get_or_create(
**notif_data
)


def post_save_course_run_notification(instance, created, **kwargs):
"""
Create a notification instance to each user who has a course_wish to participate to
the course of the saved course run.
"""
if created:
course_wishes = instance.course.wished_in_wishlists.all()
base_create_notification(instance, course_wishes, models.Notification.NOTIF_ACTION_CREATE)


def post_save_product_target_course_relation_notification(instance, created, **kwargs):
"""
Create a notification instance to each user who has a course_wish to participate to
the course of the saved course run.
"""
if created:
course_wishes = instance.course.wished_in_wishlists.all()
base_create_notification(instance.product, course_wishes, models.Notification.NOTIF_ACTION_CREATE)


def post_add_m2m_product_target_course_notification(action, instance, pk_set, **kwargs):
"""
Create a notification instance to each user who has a course_wish to participate to
a target_course of the saved product.
"""
if action == "post_add":
course_wishes = models.CourseWish.objects.filter(course__id__in=pk_set)
base_create_notification(instance, course_wishes, models.Notification.NOTIF_ACTION_ADD)
Loading

0 comments on commit 6c6e1aa

Please sign in to comment.