diff --git a/backend/api/managers.py b/backend/api/managers.py index f680ddd0..cdf04f03 100644 --- a/backend/api/managers.py +++ b/backend/api/managers.py @@ -3,22 +3,22 @@ from django.db.models import Count, Manager class AnnotationManager(Manager): - def calc_label_distribution(self, examples, users, labels): + def calc_label_distribution(self, examples, members, labels): """Calculate label distribution. Args: examples: example queryset. - users: user queryset. + members: user queryset. labels: label queryset. Returns: label distribution per user. Examples: - >>> self.calc_label_distribution(examples, users, labels) + >>> self.calc_label_distribution(examples, members, labels) {'admin': {'positive': 10, 'negative': 5}} """ - distribution = {user.username: {label.text: 0 for label in labels} for user in users} + distribution = {member.username: {label.text: 0 for label in labels} for member in members} items = self.filter(example_id__in=examples)\ .values('user__username', 'label__text')\ .annotate(count=Count('label__text')) diff --git a/backend/api/migrations/0029_auto_20220119_2333.py b/backend/api/migrations/0029_auto_20220119_2333.py new file mode 100644 index 00000000..424cb322 --- /dev/null +++ b/backend/api/migrations/0029_auto_20220119_2333.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.11 on 2022-01-19 23:33 + +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', '0028_auto_20220111_0655'), + ] + + operations = [ + migrations.RemoveField( + model_name='project', + name='users', + ), + migrations.AddField( + model_name='project', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index 9ba82249..28e12a18 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -33,7 +33,11 @@ class Project(PolymorphicModel): guideline = models.TextField(default='', blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - users = models.ManyToManyField(User, related_name='projects') + created_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + ) project_type = models.CharField(max_length=30, choices=PROJECT_CHOICES) random_order = models.BooleanField(default=False) collaborative_annotation = models.BooleanField(default=False) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 904fbbda..c86333a3 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -159,10 +159,10 @@ class ProjectSerializer(serializers.ModelSerializer): 'name', 'description', 'guideline', - 'users', 'project_type', 'updated_at', 'random_order', + 'created_by', 'collaborative_annotation', 'single_class_classification', 'is_text_project', @@ -174,7 +174,6 @@ class ProjectSerializer(serializers.ModelSerializer): ) read_only_fields = ( 'updated_at', - 'users', 'is_text_project', 'can_define_label', 'can_define_relation', diff --git a/backend/api/tests/api/utils.py b/backend/api/tests/api/utils.py index 01138075..4cf9e59f 100644 --- a/backend/api/tests/api/utils.py +++ b/backend/api/tests/api/utils.py @@ -73,8 +73,8 @@ def make_project( project = mommy.make( _model=project_model, project_type=task, - users=users, collaborative_annotation=collaborative_annotation, + created_by=users[0], **kwargs ) diff --git a/backend/api/tests/test_models.py b/backend/api/tests/test_models.py index c4ac06b5..7ad54a22 100644 --- a/backend/api/tests/test_models.py +++ b/backend/api/tests/test_models.py @@ -231,7 +231,7 @@ class TestLabelDistribution(TestCase): mommy.make('Span', example=self.example, start_offset=10, end_offset=15, user=self.user, label=label_b) distribution = Span.objects.calc_label_distribution( examples=self.project.item.examples.all(), - users=self.project.item.users.all(), + members=self.project.users, labels=SpanType.objects.all() ) expected = {user.username: {label.text: 0 for label in SpanType.objects.all()} for user in self.project.users} diff --git a/backend/api/views/project.py b/backend/api/views/project.py index 7e789f5a..da7ee6f1 100644 --- a/backend/api/views/project.py +++ b/backend/api/views/project.py @@ -22,10 +22,10 @@ class ProjectList(generics.ListCreateAPIView): return super().get_permissions() def get_queryset(self): - return self.request.user.projects + return Project.objects.filter(role_mappings__user=self.request.user) def perform_create(self, serializer): - serializer.save(users=[self.request.user]) + serializer.save(created_by=self.request.user) def delete(self, request, *args, **kwargs): delete_ids = request.data['ids'] diff --git a/backend/members/apps.py b/backend/members/apps.py index 5379ddf5..c0b5c1ca 100644 --- a/backend/members/apps.py +++ b/backend/members/apps.py @@ -1,6 +1,7 @@ import importlib from django.apps import AppConfig +from django.db.models.signals import post_save class MembersConfig(AppConfig): @@ -9,3 +10,9 @@ class MembersConfig(AppConfig): def ready(self): importlib.import_module('members.signals') + from api.models import Project + from .signals import add_administrator_on_project_creation + + # Registering signals with the subclasses of project. + for project in Project.__subclasses__(): + post_save.connect(add_administrator_on_project_creation, project) diff --git a/backend/members/models.py b/backend/members/models.py index 2ce7c378..99ad5192 100644 --- a/backend/members/models.py +++ b/backend/members/models.py @@ -61,5 +61,9 @@ class Member(models.Model): message = 'This user is already assigned to a role in this project.' raise ValidationError(message) + @property + def username(self): + return self.user.username + class Meta: unique_together = ('user', 'project') diff --git a/backend/members/signals.py b/backend/members/signals.py index 8245b2df..134b3570 100644 --- a/backend/members/signals.py +++ b/backend/members/signals.py @@ -1,48 +1,16 @@ from django.conf import settings -from django.contrib.auth.models import User -from django.db.models.signals import m2m_changed, post_save, pre_delete -from django.dispatch import receiver from api.models import Project from roles.models import Role from .models import Member -@receiver(post_save, sender=Member) -def add_linked_project(sender, instance, created, **kwargs): - if not created: - return - userInstance = instance.user - projectInstance = instance.project - if userInstance and projectInstance: - user = User.objects.get(pk=userInstance.pk) - project = Project.objects.get(pk=projectInstance.pk) - user.projects.add(project) - user.save() - - -@receiver(m2m_changed, sender=Project.users.through) -def remove_mapping_on_remove_user_from_project(sender, instance, action, reverse, **kwargs): - # if reverse is True, pk_set is project_ids and instance is user. - # else, pk_set is user_ids and instance is project. - user_ids = kwargs['pk_set'] - if action.startswith('post_remove') and not reverse: - Member.objects.filter(user__in=user_ids, project=instance).delete() - elif action.startswith('post_add') and not reverse: +def add_administrator_on_project_creation(sender, instance: Project, created: bool, **kwargs): + # In the case of creating a project. + if created: admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN) - Member.objects.bulk_create( - [Member(role=admin_role, project=instance, user_id=user) - for user in user_ids - if not Member.objects.filter(project=instance, user_id=user).exists()] + Member.objects.create( + project=instance, + user=instance.created_by, + role=admin_role, ) - - -@receiver(pre_delete, sender=Member) -def delete_linked_project(sender, instance, using, **kwargs): - userInstance = instance.user - projectInstance = instance.project - if userInstance and projectInstance: - user = User.objects.get(pk=userInstance.pk) - project = Project.objects.get(pk=projectInstance.pk) - user.projects.remove(project) - user.save() diff --git a/backend/metrics/views.py b/backend/metrics/views.py index d6628e8b..b184a26f 100644 --- a/backend/metrics/views.py +++ b/backend/metrics/views.py @@ -1,12 +1,12 @@ import abc -from django.shortcuts import get_object_or_404 from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from api.models import Example, ExampleState, Project, Annotation, Label, Category, CategoryType, Span, SpanType +from api.models import Example, ExampleState, Annotation, Label, Category, CategoryType, Span, SpanType +from members.models import Member from members.permissions import IsInProjectReadOnlyOrAdmin @@ -24,9 +24,9 @@ class MemberProgressAPI(APIView): permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin] def get(self, request, *args, **kwargs): - project = get_object_or_404(Project, pk=self.kwargs['project_id']) examples = Example.objects.filter(project=self.kwargs['project_id']).values('id') - data = ExampleState.objects.measure_member_progress(examples, project.users.all()) + members = Member.objects.filter(project=self.kwargs['project_id']) + data = ExampleState.objects.measure_member_progress(examples, members) return Response(data=data, status=status.HTTP_200_OK) @@ -36,10 +36,10 @@ class LabelDistribution(abc.ABC, APIView): label_type = Label def get(self, request, *args, **kwargs): - project = get_object_or_404(Project, pk=self.kwargs['project_id']) labels = self.label_type.objects.filter(project=self.kwargs['project_id']) examples = Example.objects.filter(project=self.kwargs['project_id']).values('id') - data = self.model.objects.calc_label_distribution(examples, project.users.all(), labels) + members = Member.objects.filter(project=self.kwargs['project_id']) + data = self.model.objects.calc_label_distribution(examples, members, labels) return Response(data=data, status=status.HTTP_200_OK)