diff --git a/api/funkwhale_api/common/factories.py b/api/funkwhale_api/common/factories.py index 6919f9c3771ec81c9e4019cfa9e42d4d30e99494..9f46669ba8680f1976ef4713e06822cbad9c49cc 100644 --- a/api/funkwhale_api/common/factories.py +++ b/api/funkwhale_api/common/factories.py @@ -3,6 +3,7 @@ import factory from funkwhale_api.factories import registry, NoUpdateOnCreate from funkwhale_api.federation import factories as federation_factories +from funkwhale_api.users import factories as users_factories @registry.register @@ -23,3 +24,26 @@ class MutationFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory): return self.target = extracted self.save() + + +@registry.register +class ServiceMessageFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory): + code = factory.Faker("word") + title = factory.Faker("paragraph") + content = factory.Faker("paragraph") + expiration_date = None + + class Meta: + model = "common.ServiceMessage" + + +@registry.register +class ServiceMessageNotificationFactory( + NoUpdateOnCreate, factory.django.DjangoModelFactory +): + user = factory.SubFactory(users_factories.UserFactory) + service_message = factory.SubFactory(ServiceMessageFactory) + is_read = False + + class Meta: + model = "common.ServiceMessageNotification" diff --git a/api/funkwhale_api/common/migrations/0004_auto_20190725_1510.py b/api/funkwhale_api/common/migrations/0004_auto_20190725_1510.py new file mode 100644 index 0000000000000000000000000000000000000000..773fe3eb76573e7211aea7a60b4a4714f78ed789 --- /dev/null +++ b/api/funkwhale_api/common/migrations/0004_auto_20190725_1510.py @@ -0,0 +1,78 @@ +# Generated by Django 2.2.3 on 2019-07-25 15:10 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("common", "0003_cit_extension"), + ] + + operations = [ + migrations.CreateModel( + name="ServiceMessage", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "code", + models.CharField( + db_index=True, default=uuid.uuid4, max_length=255, unique=True + ), + ), + ("title", models.CharField(max_length=255, null=True)), + ("content", models.TextField(max_length=255, null=True)), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("expiration_date", models.DateTimeField(null=True)), + ], + ), + migrations.CreateModel( + name="ServiceMessageNotification", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("is_read", models.BooleanField(default=False)), + ( + "service_message", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="notifications", + to="common.ServiceMessage", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="service_message_notifications", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/api/funkwhale_api/common/models.py b/api/funkwhale_api/common/models.py index 52a02cad9433f4e79972b6cfe858534c830cb513..9e0cd638b4aff5fe88febec9838a75753276c104 100644 --- a/api/funkwhale_api/common/models.py +++ b/api/funkwhale_api/common/models.py @@ -150,3 +150,26 @@ class Mutation(models.Model): self.applied_date = timezone.now() self.save(update_fields=["is_applied", "applied_date", "previous_state"]) return previous_state + + +class ServiceMessage(models.Model): + code = models.CharField( + unique=True, db_index=True, default=uuid.uuid4, max_length=255 + ) + title = models.CharField(null=True, max_length=255) + content = models.TextField(null=True, max_length=255) + + creation_date = models.DateTimeField(default=timezone.now) + expiration_date = models.DateTimeField(null=True) + + +class ServiceMessageNotification(models.Model): + service_message = models.ForeignKey( + ServiceMessage, related_name="notifications", on_delete=models.CASCADE + ) + user = models.ForeignKey( + "users.User", + related_name="service_message_notifications", + on_delete=models.CASCADE, + ) + is_read = models.BooleanField(default=False) diff --git a/api/funkwhale_api/common/serializers.py b/api/funkwhale_api/common/serializers.py index 59b513f37aa057d843df6a4a5405381be71c2c8f..a70dbed7fe0eb956a819ef93d273e0d3dc5b9465 100644 --- a/api/funkwhale_api/common/serializers.py +++ b/api/funkwhale_api/common/serializers.py @@ -272,3 +272,20 @@ class APIMutationSerializer(serializers.ModelSerializer): if value not in self.context["registry"]: raise serializers.ValidationError("Invalid mutation type {}".format(value)) return value + + +class ServiceMessageSerializer(serializers.ModelSerializer): + class Meta: + model = models.ServiceMessage + fields = ["code", "title", "content", "creation_date", "expiration_date"] + + +class ServiceMessageNotificationSerializer(serializers.Serializer): + class Meta: + model = models.ServiceMessageNotification + fields = ["is_read"] + + def to_representation(self, instance): + data = ServiceMessageSerializer(instance.service_message).data + data["is_read"] = instance.is_read + return data diff --git a/api/funkwhale_api/common/tasks.py b/api/funkwhale_api/common/tasks.py index 994b0bdfff13a27a5eec0e99a87c72c11b39287d..6f26cd7df864b5f417bdd92baa5dcc0c1151631b 100644 --- a/api/funkwhale_api/common/tasks.py +++ b/api/funkwhale_api/common/tasks.py @@ -4,6 +4,7 @@ from django.dispatch import receiver from funkwhale_api.common import channels from funkwhale_api.taskapp import celery +from funkwhale_api.users.models import User from . import models from . import serializers @@ -57,3 +58,24 @@ def broadcast_mutation_update(mutation, old_is_approved, new_is_approved, **kwar }, }, ) + + +@celery.app.task(name="common.broadcast_service_message") +@transaction.atomic +@celery.require_instance(models.ServiceMessage.objects.all(), "message") +def broadcast_service_message(message, user_ids): + existing_ids = list( + User.objects.filter(pk__in=user_ids).values_list("pk", flat=True) + ) + + notifications = [] + for user_id in existing_ids: + notifications.append( + models.ServiceMessageNotification( + service_message_id=message.pk, user_id=user_id + ) + ) + + models.ServiceMessageNotification.objects.bulk_create( + notifications, batch_size=2000, ignore_conflicts=True + ) diff --git a/api/tests/common/test_serializers.py b/api/tests/common/test_serializers.py index 6d20443af6b97d603f029482816893c5afeab683..8e0e57f5d8186f2e86140309d8640eb98618272a 100644 --- a/api/tests/common/test_serializers.py +++ b/api/tests/common/test_serializers.py @@ -182,3 +182,21 @@ def test_strip_exif_field(): cleaned = PIL.Image.open(field.to_internal_value(uploaded)) assert cleaned._getexif() is None + + +def test_message_notification_serializer(factories, now): + notification = factories["common.ServiceMessageNotification"]( + service_message__expiration_date=now + ) + message = notification.service_message + expected = { + "code": message.code, + "title": message.title, + "content": message.content, + "creation_date": message.creation_date.isoformat().split("+")[0] + "Z", + "expiration_date": message.expiration_date.isoformat().split("+")[0] + "Z", + "is_read": False, + } + serializer = serializers.ServiceMessageNotificationSerializer(notification) + + assert serializer.data == expected diff --git a/api/tests/common/test_tasks.py b/api/tests/common/test_tasks.py index f097c44231af27de220df3889cf7f1db2fe48ea0..54bc5751deaf6fcce984893ba8e0fd3c7a824e65 100644 --- a/api/tests/common/test_tasks.py +++ b/api/tests/common/test_tasks.py @@ -63,3 +63,28 @@ def test_cannot_apply_already_applied_migration(factories): mutation = factories["common.Mutation"](payload={}, is_applied=True) with pytest.raises(mutation.__class__.DoesNotExist): tasks.apply_mutation(mutation_id=mutation.pk) + + +def test_can_broadcast_service_message(factories, now): + message = factories["common.ServiceMessage"]( + code="test", content="Hello world", expiration_date=now + ) + + user1 = factories["users.User"]() + user2 = factories["users.User"]() + factories["users.User"]() + + tasks.broadcast_service_message( + message_id=message.pk, user_ids=[user1.pk, user2.pk] + ) + + assert message.notifications.count() == 2 + + assert ( + user1.service_message_notifications.get(service_message=message).is_read + is False + ) + assert ( + user2.service_message_notifications.get(service_message=message).is_read + is False + )