From 9773ee0bdb2e147752420df48c062ea891ee1e11 Mon Sep 17 00:00:00 2001 From: Kristof Boucher Charbonneau Date: Wed, 22 Mar 2017 20:43:00 -0400 Subject: [PATCH 1/7] Adding the Notification model and migration --- api/migrations/0004_notification.py | 37 +++++++++++++++++++ api/models.py | 56 +++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 api/migrations/0004_notification.py diff --git a/api/migrations/0004_notification.py b/api/migrations/0004_notification.py new file mode 100644 index 0000000..5779fd3 --- /dev/null +++ b/api/migrations/0004_notification.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2017-03-23 00:41 +from __future__ import unicode_literals + +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', '0003_task_done'), + ] + + operations = [ + migrations.CreateModel( + name='Notification', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('published_date', models.DateField(verbose_name='Published date')), + ('modification_date', models.DateField(blank=True, null=True, verbose_name='Modification Date')), + ('status', models.IntegerField(default=1, verbose_name='Status')), + ('target_type', models.CharField(max_length=50, verbose_name='Target type')), + ('target_id', models.PositiveIntegerField(verbose_name='Target ID')), + ('target_intention', models.CharField(max_length=50, verbose_name='Target intention')), + ('title', models.CharField(max_length=255, verbose_name='Title')), + ('text', models.TextField(verbose_name='Text')), + ('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications_received', to=settings.AUTH_USER_MODEL, verbose_name='Receiver')), + ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_sent', to=settings.AUTH_USER_MODEL, verbose_name='Sender')), + ], + options={ + 'verbose_name_plural': 'Notifications', + }, + ), + ] diff --git a/api/models.py b/api/models.py index dc99797..6be26f0 100644 --- a/api/models.py +++ b/api/models.py @@ -255,3 +255,59 @@ class Meta: def __unicode__(self): return self.name + + +class Notification(models.Model): + class Meta: + verbose_name_plural = "Notifications" + + sender = models.ForeignKey( + User, + verbose_name='Sender', + related_name='notification_sent' + ) + + receiver = models.ForeignKey( + User, + verbose_name='Receiver', + related_name='notifications_received' + ) + + published_date = models.DateField( + verbose_name='Published date' + ) + + modification_date = models.DateField( + verbose_name='Modification Date', + blank=True, + null=True + ) + + status = models.IntegerField( + verbose_name='Status', + default=1 + ) + + target_type = models.CharField( + verbose_name='Target type', + max_length=50 + ) + + target_id = models.PositiveIntegerField( + verbose_name='Target ID' + ) + + target_intention = models.CharField( + verbose_name='Target intention', + max_length=50 + ) + + title = models.CharField( + verbose_name='Title', + max_length=255 + ) + + text = models.TextField( + verbose_name='Text', + ) + From bf0d5dcf30d3ead0decb87c5341c4e6b3b3b5feb Mon Sep 17 00:00:00 2001 From: Kristof Boucher Charbonneau Date: Wed, 22 Mar 2017 21:11:14 -0400 Subject: [PATCH 2/7] Adding the endpoint to the notification --- api/admin.py | 1 + api/serializers.py | 21 +++++++++++++++++++++ api/urls.py | 11 +++++++++++ api/views.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+) diff --git a/api/admin.py b/api/admin.py index c9e75e2..8578524 100644 --- a/api/admin.py +++ b/api/admin.py @@ -9,3 +9,4 @@ admin.site.register(models.Task) admin.site.register(models.Access) admin.site.register(models.Label) +admin.site.register(models.Notification) diff --git a/api/serializers.py b/api/serializers.py index 5f9d514..8e07015 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -189,6 +189,27 @@ class Meta: ) +class NotificationSerializer(serializers.ModelSerializer): + + class Meta: + model = models.Notification + fields = '__all__' + + sender = UserSerializer(read_only=True) + sender_id = serializers.PrimaryKeyRelatedField( + queryset=models.User.objects.all(), + source='sender', + write_only=True + ) + + receiver = UserSerializer(read_only=True) + receiver_id = serializers.PrimaryKeyRelatedField( + queryset=models.User.objects.all(), + source='receiver', + write_only=True + ) + + class FileSerializer(serializers.ModelSerializer): class Meta: diff --git a/api/urls.py b/api/urls.py index ce2c555..32de737 100755 --- a/api/urls.py +++ b/api/urls.py @@ -16,6 +16,17 @@ views.UserRetrieve.as_view(), name='users_detail' ), + # NOTIFICATION + url( + r'^users/(?P\d+)/notification$', + views.NotificationList.as_view(), + name='user_notification_list' + ), + url( + r'^users/(?P\d+)/notification/(?P\d+)$', + views.NotificationRetrieveUpdate.as_view(), + name='user_notification_detail' + ), # PROJECTS url( r'^projects$', diff --git a/api/views.py b/api/views.py index b3dca9f..d7758e9 100755 --- a/api/views.py +++ b/api/views.py @@ -180,6 +180,36 @@ class TaskRetrieveUpdateDestroy(generics.RetrieveUpdateDestroyAPIView): def get_queryset(self): return models.Task.objects.all() +""" +NOTIFICATIONS +""" + + +class NotificationList(generics.ListAPIView): + serializer_class = serializers.NotificationSerializer + + def get_queryset(self): + #Check if user have access to this notification + get_object_or_404( + models.Notification, + receiver__id=self.kwargs['uid'] + ) + + return models.Notification.objects.filter(receiver__id=self.kwargs['uid']) + + +class NotificationRetrieveUpdate(generics.RetrieveUpdateAPIView): + serializer_class = serializers.NotificationSerializer + + def get_queryset(self): + #Check if user have access to this notification + get_object_or_404( + models.Notification, + receiver__id=self.kwargs['uid'] + ) + + return models.Notification.objects.filter(receiver__id=self.kwargs['uid']) + """ LABELS From 2e513bccfff2eaee01beb9498fd9e461370215b3 Mon Sep 17 00:00:00 2001 From: Kristof Boucher Charbonneau Date: Wed, 22 Mar 2017 21:27:44 -0400 Subject: [PATCH 3/7] Returning the user id along with the auth token --- api/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/views.py b/api/views.py index d7758e9..16cc204 100755 --- a/api/views.py +++ b/api/views.py @@ -32,6 +32,7 @@ def post(self, request, *args, **kwargs): content = { 'token': str(token.key), + 'user_id': user.id } return Response(content) From 43fb80b98316bee7aa632a27a0cc3cac8becd49e Mon Sep 17 00:00:00 2001 From: Kristof Boucher Charbonneau Date: Wed, 22 Mar 2017 23:35:20 -0400 Subject: [PATCH 4/7] Auto creation a notification when a task is created --- api/migrations/0005_notification_is_read.py | 20 ++++++++++++++ api/migrations/0006_task_created_by.py | 24 +++++++++++++++++ api/models.py | 30 +++++++++++++++++++++ api/serializers.py | 7 +++++ api/urls.py | 8 +++--- api/views.py | 23 ++++++++-------- 6 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 api/migrations/0005_notification_is_read.py create mode 100644 api/migrations/0006_task_created_by.py diff --git a/api/migrations/0005_notification_is_read.py b/api/migrations/0005_notification_is_read.py new file mode 100644 index 0000000..2e4cd53 --- /dev/null +++ b/api/migrations/0005_notification_is_read.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2017-03-23 01:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0004_notification'), + ] + + operations = [ + migrations.AddField( + model_name='notification', + name='is_read', + field=models.BooleanField(default=0, verbose_name='Is read'), + ), + ] diff --git a/api/migrations/0006_task_created_by.py b/api/migrations/0006_task_created_by.py new file mode 100644 index 0000000..710d184 --- /dev/null +++ b/api/migrations/0006_task_created_by.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2017-03-23 03:03 +from __future__ import unicode_literals + +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', '0005_notification_is_read'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='created_by', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='task_created', to=settings.AUTH_USER_MODEL, verbose_name='Created by'), + preserve_default=False, + ), + ] diff --git a/api/models.py b/api/models.py index 6be26f0..b8f5134 100644 --- a/api/models.py +++ b/api/models.py @@ -4,6 +4,8 @@ from django.db import models from django.contrib.auth.models import User +from django.db.models.signals import post_save +from django.dispatch import receiver RIGHT_ACCESS = ( @@ -107,6 +109,12 @@ class Meta: related_name="tasks" ) + created_by = models.ForeignKey( + User, + verbose_name='Created by', + related_name='task_created' + ) + assigned = models.ForeignKey( User, verbose_name='assigned', @@ -288,6 +296,11 @@ class Meta: default=1 ) + is_read = models.BooleanField( + verbose_name='Is read', + default=0 + ) + target_type = models.CharField( verbose_name='Target type', max_length=50 @@ -311,3 +324,20 @@ class Meta: verbose_name='Text', ) + +# Receivers +@receiver(post_save, sender=Task, dispatch_uid="task_saved") +def task_saved(sender, instance, created, *args, **kwargs): + # If the user creating the task is not the one assigned + if created and instance.assigned is not None and instance.assigned is not instance.created_by: + Notification.objects.create( + sender=instance.created_by, + receiver=instance.assigned, + published_date=instance.creation_date, + target_type="task", + target_id=instance.id, + target_intention="create", + title='You have been assigned to "%s"' % instance.name, + text=instance.description + ) + diff --git a/api/serializers.py b/api/serializers.py index 8e07015..c0d4ade 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -172,6 +172,13 @@ class Meta: labels = BaseLabelSerializer(many=True, read_only=True) + created_by = UserSerializer(read_only=True) + created_by_id = serializers.PrimaryKeyRelatedField( + queryset=models.User.objects.all(), + source='created_by', + write_only=True + ) + assigned = UserSerializer(read_only=True) assigned_id = serializers.PrimaryKeyRelatedField( queryset=models.User.objects.all(), diff --git a/api/urls.py b/api/urls.py index 32de737..111c7cc 100755 --- a/api/urls.py +++ b/api/urls.py @@ -18,14 +18,14 @@ ), # NOTIFICATION url( - r'^users/(?P\d+)/notification$', + r'^notification$', views.NotificationList.as_view(), - name='user_notification_list' + name='notification_list' ), url( - r'^users/(?P\d+)/notification/(?P\d+)$', + r'^notification/(?P\d+)$', views.NotificationRetrieveUpdate.as_view(), - name='user_notification_detail' + name='notification_detail' ), # PROJECTS url( diff --git a/api/views.py b/api/views.py index 16cc204..241b73d 100755 --- a/api/views.py +++ b/api/views.py @@ -7,6 +7,7 @@ import django_filters from django.shortcuts import get_object_or_404 +from django.shortcuts import get_list_or_404 from rest_framework import generics from rest_framework import filters @@ -31,8 +32,7 @@ def post(self, request, *args, **kwargs): token, created = Token.objects.get_or_create(user=user) content = { - 'token': str(token.key), - 'user_id': user.id + 'token': str(token.key) } return Response(content) @@ -171,6 +171,9 @@ class TaskListCreate(generics.ListCreateAPIView): serializer_class = serializers.TaskSerializer filter_class = TaskFilter + def get_initial(self): + return {'created_by': self.request.user} + def get_queryset(self): return models.Task.objects.filter(project__access__user=self.request.user) @@ -178,6 +181,9 @@ def get_queryset(self): class TaskRetrieveUpdateDestroy(generics.RetrieveUpdateDestroyAPIView): serializer_class = serializers.TaskSerializer + def get_initial(self): + return {'created_by': self.request.user} + def get_queryset(self): return models.Task.objects.all() @@ -190,13 +196,7 @@ class NotificationList(generics.ListAPIView): serializer_class = serializers.NotificationSerializer def get_queryset(self): - #Check if user have access to this notification - get_object_or_404( - models.Notification, - receiver__id=self.kwargs['uid'] - ) - - return models.Notification.objects.filter(receiver__id=self.kwargs['uid']) + return models.Notification.objects.filter(receiver__id=self.request.user.id) class NotificationRetrieveUpdate(generics.RetrieveUpdateAPIView): @@ -206,10 +206,11 @@ def get_queryset(self): #Check if user have access to this notification get_object_or_404( models.Notification, - receiver__id=self.kwargs['uid'] + receiver__id=self.request.user.id, + pk=self.kwargs['pk'] ) - return models.Notification.objects.filter(receiver__id=self.kwargs['uid']) + return models.Notification.objects.filter(receiver__id=self.request.user.id,pk=self.kwargs['pk']) """ From 621623a87d1cae2fdc0bd71954538cf28e6a5f80 Mon Sep 17 00:00:00 2001 From: Kristof Boucher Charbonneau Date: Wed, 22 Mar 2017 23:55:52 -0400 Subject: [PATCH 5/7] Non required created at field since it's populated automaticaly --- api/migrations/0007_auto_20170323_0350.py | 22 ++++++++++++++++++++++ api/migrations/0008_auto_20170323_0351.py | 23 +++++++++++++++++++++++ api/migrations/0009_auto_20170323_0352.py | 22 ++++++++++++++++++++++ api/migrations/0010_auto_20170323_0353.py | 23 +++++++++++++++++++++++ api/models.py | 3 ++- api/serializers.py | 4 +++- 6 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 api/migrations/0007_auto_20170323_0350.py create mode 100644 api/migrations/0008_auto_20170323_0351.py create mode 100644 api/migrations/0009_auto_20170323_0352.py create mode 100644 api/migrations/0010_auto_20170323_0353.py diff --git a/api/migrations/0007_auto_20170323_0350.py b/api/migrations/0007_auto_20170323_0350.py new file mode 100644 index 0000000..0ba56ea --- /dev/null +++ b/api/migrations/0007_auto_20170323_0350.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2017-03-23 03:50 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0006_task_created_by'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='task_created', to=settings.AUTH_USER_MODEL, verbose_name='Created by'), + ), + ] diff --git a/api/migrations/0008_auto_20170323_0351.py b/api/migrations/0008_auto_20170323_0351.py new file mode 100644 index 0000000..0e271dd --- /dev/null +++ b/api/migrations/0008_auto_20170323_0351.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2017-03-23 03:51 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0007_auto_20170323_0350'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='created_by', + field=models.ForeignKey(blank=True, default=1, on_delete=django.db.models.deletion.CASCADE, related_name='task_created', to=settings.AUTH_USER_MODEL, verbose_name='Created by'), + preserve_default=False, + ), + ] diff --git a/api/migrations/0009_auto_20170323_0352.py b/api/migrations/0009_auto_20170323_0352.py new file mode 100644 index 0000000..19e6881 --- /dev/null +++ b/api/migrations/0009_auto_20170323_0352.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2017-03-23 03:52 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0008_auto_20170323_0351'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='task_created', to=settings.AUTH_USER_MODEL, verbose_name='Created by'), + ), + ] diff --git a/api/migrations/0010_auto_20170323_0353.py b/api/migrations/0010_auto_20170323_0353.py new file mode 100644 index 0000000..df22bd3 --- /dev/null +++ b/api/migrations/0010_auto_20170323_0353.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2017-03-23 03:53 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0009_auto_20170323_0352'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='created_by', + field=models.ForeignKey(blank=True, default=1, on_delete=django.db.models.deletion.CASCADE, related_name='task_created', to=settings.AUTH_USER_MODEL, verbose_name='Created by'), + preserve_default=False, + ), + ] diff --git a/api/models.py b/api/models.py index b8f5134..235bffb 100644 --- a/api/models.py +++ b/api/models.py @@ -112,7 +112,8 @@ class Meta: created_by = models.ForeignKey( User, verbose_name='Created by', - related_name='task_created' + related_name='task_created', + blank=True ) assigned = models.ForeignKey( diff --git a/api/serializers.py b/api/serializers.py index c0d4ade..cd04d37 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -176,7 +176,9 @@ class Meta: created_by_id = serializers.PrimaryKeyRelatedField( queryset=models.User.objects.all(), source='created_by', - write_only=True + write_only=True, + required=False, + allow_null=True ) assigned = UserSerializer(read_only=True) From b73937939908a5ae89854be0e7619e3a92cc2aa9 Mon Sep 17 00:00:00 2001 From: Kristof Boucher Charbonneau Date: Thu, 23 Mar 2017 21:53:06 -0400 Subject: [PATCH 6/7] Forcing the created_by to be always the current user --- api/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/views.py b/api/views.py index 241b73d..f59f0e8 100755 --- a/api/views.py +++ b/api/views.py @@ -171,8 +171,8 @@ class TaskListCreate(generics.ListCreateAPIView): serializer_class = serializers.TaskSerializer filter_class = TaskFilter - def get_initial(self): - return {'created_by': self.request.user} + def perform_create(self, serializer): + serializer.save(created_by=self.request.user) def get_queryset(self): return models.Task.objects.filter(project__access__user=self.request.user) @@ -181,8 +181,8 @@ def get_queryset(self): class TaskRetrieveUpdateDestroy(generics.RetrieveUpdateDestroyAPIView): serializer_class = serializers.TaskSerializer - def get_initial(self): - return {'created_by': self.request.user} + def perform_update(self, serializer): + serializer.save(created_by=self.request.user) def get_queryset(self): return models.Task.objects.all() From 722f8c13724312f8e34544f9dff973555f4ab091 Mon Sep 17 00:00:00 2001 From: Kristof Boucher Charbonneau Date: Thu, 30 Mar 2017 08:36:55 -0400 Subject: [PATCH 7/7] user id along the token --- api/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/views.py b/api/views.py index f59f0e8..50a21b6 100755 --- a/api/views.py +++ b/api/views.py @@ -32,7 +32,8 @@ def post(self, request, *args, **kwargs): token, created = Token.objects.get_or_create(user=user) content = { - 'token': str(token.key) + 'token': str(token.key), + 'id': user.id } return Response(content)