From 3636d4a72b402b86c6989e20fc856dd717c45fe3 Mon Sep 17 00:00:00 2001 From: yann Date: Fri, 30 May 2025 10:24:01 +0200 Subject: [PATCH] working on permission is_contributor; checkpoint --- softdesk/authentication/serializers.py | 3 +- softdesk/softdesk/settings.py | 2 +- softdesk/softdesk/urls.py | 5 +- softdesk/support/admin.py | 16 +++- ...ue_priority_alter_issue_status_and_more.py | 28 +++++++ ...alter_issue_priority_alter_issue_status.py | 23 ++++++ .../migrations/0013_alter_issue_project.py | 19 +++++ .../migrations/0014_alter_issue_project.py | 19 +++++ softdesk/support/models.py | 18 ++--- softdesk/support/permissions.py | 16 +++- softdesk/support/serializers.py | 73 +++++++++++++++-- softdesk/support/views.py | 78 +++++++++++++++++-- 12 files changed, 268 insertions(+), 32 deletions(-) create mode 100644 softdesk/support/migrations/0011_alter_issue_priority_alter_issue_status_and_more.py create mode 100644 softdesk/support/migrations/0012_alter_issue_priority_alter_issue_status.py create mode 100644 softdesk/support/migrations/0013_alter_issue_project.py create mode 100644 softdesk/support/migrations/0014_alter_issue_project.py diff --git a/softdesk/authentication/serializers.py b/softdesk/authentication/serializers.py index 6fd6e7a..d71346e 100644 --- a/softdesk/authentication/serializers.py +++ b/softdesk/authentication/serializers.py @@ -1,4 +1,4 @@ -from rest_framework.serializers import ModelSerializer, SerializerMethodField, ValidationError +from rest_framework.serializers import ModelSerializer, ValidationError from rest_framework import serializers from authentication.models import User @@ -50,7 +50,6 @@ class UserRegisterSerializer(ModelSerializer): """ Create and return a new `User` instance, given the validated data. """ - #if self.validate(validated_data): user = User.objects.create_user( username=validated_data['username'], email=validated_data['email'], diff --git a/softdesk/softdesk/settings.py b/softdesk/softdesk/settings.py index 679a9e9..dcf617e 100644 --- a/softdesk/softdesk/settings.py +++ b/softdesk/softdesk/settings.py @@ -133,6 +133,6 @@ REST_FRAMEWORK = { } SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(days=10), + 'ACCESS_TOKEN_LIFETIME': timedelta(days=30), 'REFRESH_TOKEN_LIFETIME': timedelta(days=30), } diff --git a/softdesk/softdesk/urls.py b/softdesk/softdesk/urls.py index 071337e..a70983b 100644 --- a/softdesk/softdesk/urls.py +++ b/softdesk/softdesk/urls.py @@ -17,7 +17,7 @@ Including another URLconf from django.contrib import admin from django.urls import path, include from authentication.views import (UserView, UserCreateView, PasswordUpdateView) -from support.views import ProjectViewSet +from support.views import ProjectViewSet, IssueViewSet, CommentViewSet, ContributorViewSet from rest_framework import routers from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView @@ -25,6 +25,9 @@ from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView router = routers.SimpleRouter() #router.register('user', UserViewSet, basename='user') router.register('project', ProjectViewSet, basename='project') +router.register('issue', IssueViewSet, basename='issue') +router.register('comment', CommentViewSet, basename='comment') +router.register('contributors', ContributorViewSet) urlpatterns = [ path('admin/', admin.site.urls), diff --git a/softdesk/support/admin.py b/softdesk/support/admin.py index e084721..d7c3623 100644 --- a/softdesk/support/admin.py +++ b/softdesk/support/admin.py @@ -2,12 +2,20 @@ from django.contrib import admin from support.models import Project, Issue, Comment, ProjectContributor from authentication.models import User -class AdminUser: - pass +class AdminProject(admin.ModelAdmin): + list_display = ('id', 'title', 'author', 'contributors') + + @admin.display(description='contributors') + def contributors(self, obj): + return obj.contributor + +class AdminIssue(admin.ModelAdmin): + list_display = ('id', 'title', 'author', 'project') + admin.site.register(User) -admin.site.register(Project) -admin.site.register(Issue) +admin.site.register(Project, AdminProject) +admin.site.register(Issue, AdminIssue) admin.site.register(Comment) admin.site.register(ProjectContributor) diff --git a/softdesk/support/migrations/0011_alter_issue_priority_alter_issue_status_and_more.py b/softdesk/support/migrations/0011_alter_issue_priority_alter_issue_status_and_more.py new file mode 100644 index 0000000..59bda9c --- /dev/null +++ b/softdesk/support/migrations/0011_alter_issue_priority_alter_issue_status_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.1 on 2025-05-26 18:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('support', '0010_alter_comment_author_alter_issue_author_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='issue', + name='priority', + field=models.CharField(choices=[('L', 'Low'), ('M', 'Medium'), ('H', 'High')], max_length=15), + ), + migrations.AlterField( + model_name='issue', + name='status', + field=models.CharField(choices=[('ToDo', 'Todo'), ('InProgress', 'Inprogress'), ('Finished', 'Finished')], max_length=15), + ), + migrations.AlterField( + model_name='issue', + name='tag', + field=models.CharField(choices=[('Bug', 'Bug'), ('Feature', 'Feature'), ('Task', 'Task')], max_length=15), + ), + ] diff --git a/softdesk/support/migrations/0012_alter_issue_priority_alter_issue_status.py b/softdesk/support/migrations/0012_alter_issue_priority_alter_issue_status.py new file mode 100644 index 0000000..7621910 --- /dev/null +++ b/softdesk/support/migrations/0012_alter_issue_priority_alter_issue_status.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.1 on 2025-05-26 18:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('support', '0011_alter_issue_priority_alter_issue_status_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='issue', + name='priority', + field=models.CharField(choices=[('Low', 'Low'), ('Medium', 'Medium'), ('High', 'High')], max_length=15), + ), + migrations.AlterField( + model_name='issue', + name='status', + field=models.CharField(choices=[('ToDo', 'Todo'), ('In Progress', 'Inprogress'), ('Finished', 'Finished')], max_length=15), + ), + ] diff --git a/softdesk/support/migrations/0013_alter_issue_project.py b/softdesk/support/migrations/0013_alter_issue_project.py new file mode 100644 index 0000000..ca71cbf --- /dev/null +++ b/softdesk/support/migrations/0013_alter_issue_project.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.1 on 2025-05-26 19:06 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('support', '0012_alter_issue_priority_alter_issue_status'), + ] + + operations = [ + migrations.AlterField( + model_name='issue', + name='project', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='support.project'), + ), + ] diff --git a/softdesk/support/migrations/0014_alter_issue_project.py b/softdesk/support/migrations/0014_alter_issue_project.py new file mode 100644 index 0000000..b459358 --- /dev/null +++ b/softdesk/support/migrations/0014_alter_issue_project.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.1 on 2025-05-27 09:30 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('support', '0013_alter_issue_project'), + ] + + operations = [ + migrations.AlterField( + model_name='issue', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='support.project'), + ), + ] diff --git a/softdesk/support/models.py b/softdesk/support/models.py index 588a3b9..4aa4f76 100644 --- a/softdesk/support/models.py +++ b/softdesk/support/models.py @@ -45,14 +45,14 @@ class ProjectContributor(models.Model): class Issue(models.Model): class Priority(models.TextChoices): - LOW = 'L' - MEDIUM = 'M' - HIGH = 'H' + LOW = 'Low' + MEDIUM = 'Medium' + HIGH = 'High' class Status(models.TextChoices): TODO = 'ToDo' - INPROGRESS = 'InProgress' + INPROGRESS = 'In Progress' FINISHED = 'Finished' @@ -65,13 +65,11 @@ class Issue(models.Model): title = models.CharField(max_length=255, verbose_name='title') date_created = models.DateTimeField(auto_now_add=True) description = models.TextField() - status = models.CharField(Status.choices, max_length=15) - priority = models.CharField(Priority.choices, max_length=15) - tag = models.CharField(Tag.choices, max_length=15) + status = models.CharField(choices=Status.choices, max_length=15) + priority = models.CharField(choices=Priority.choices, max_length=15) + tag = models.CharField(choices=Tag.choices, max_length=15) project = models.ForeignKey(Project, - null=True, - on_delete=models.CASCADE, - blank=True) + on_delete=models.CASCADE) author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, related_name='issue_author', null=True) diff --git a/softdesk/support/permissions.py b/softdesk/support/permissions.py index d4b4293..3dc9f42 100644 --- a/softdesk/support/permissions.py +++ b/softdesk/support/permissions.py @@ -1,9 +1,19 @@ from rest_framework.permissions import BasePermission - +from support.models import Project class IsAuthor(BasePermission): - def has_object_permission(self, request, view, project): + def has_object_permission(self, request, view, object): return bool(request.user and request.user.is_authenticated - and request.user==project.author) \ No newline at end of file + and request.user == object.author + ) + + +class IsContributor(BasePermission): + + def has_object_permission(self, request, view, object): + return bool(request.user + and request.user.is_authenticated + and request.user in object.contributors.all() + ) \ No newline at end of file diff --git a/softdesk/support/serializers.py b/softdesk/support/serializers.py index 431bf95..596da25 100644 --- a/softdesk/support/serializers.py +++ b/softdesk/support/serializers.py @@ -1,6 +1,8 @@ from rest_framework.serializers import (ModelSerializer, StringRelatedField, - SlugRelatedField) + SlugRelatedField, + SerializerMethodField, + ValidationError) from support.models import Project, ProjectContributor, Issue, Comment @@ -11,26 +13,85 @@ class ContributorSerializer(ModelSerializer): fields = ['contributor', 'project', 'data'] +class ContributorListSerializer(ModelSerializer): + + class Meta: + model = ProjectContributor + fields = ['contributor'] + + + class ProjectSerializer(ModelSerializer): + author = StringRelatedField(many=False) + contributors = SlugRelatedField(many=True, + read_only='True', + slug_field='username') + class Meta: + model = Project + fields = ['id', 'author', 'contributors', 'title', 'type', 'date_created'] + + def validate_title(self, value): + if Project.objects.filter(title=value).exists(): + raise ValidationError("Project already exists.") + return value + +class ProjectDetailSerializer(ModelSerializer): + contributors = SlugRelatedField(many=True, read_only='True', slug_field='username') author = StringRelatedField(many=False) + issues = SerializerMethodField() class Meta: model = Project - fields = ['id', 'title', 'date_created', 'type', 'description', 'author', - 'contributors'] + fields = ['title', + 'date_created', 'type', + 'author', 'contributors', 'description', 'issues'] + + def get_issues(self, instance): + queryset = Issue.objects.filter(project=instance.pk) + serializer = IssueSerializer(queryset, many=True) + return serializer.data -class ProjectDetailSerializer(ModelSerializer): - pass class IssueSerializer(ModelSerializer): + author = StringRelatedField(many=False) + + class Meta: model = Issue - fields = ['title', 'date_created', 'priority', 'tag', 'status', 'author'] + fields = ['id', 'title', 'project', 'date_created', 'priority', + 'tag', 'status', 'author'] + read_only_field = ['author'] + def validate_author(self, data): + if Project.objects.filter(contributors=data.author).exists(): + raise ValidationError("Requestor isn't contributor") + return data + def validate_project(self, data): + # if data['user'] not in data['project'].contributors: + # raise ValidationError("User must be a contributor to the project") + #print(data.project) + #if self.context['request'].user not in data.contributors: + # raise ValidationError("User must be a contributor to the project") + #print(self.get_contributors(data)) + + return data + + +class CommentListSerializer(ModelSerializer): + + class Meta: + model = Comment + fields = ['title', 'date_created', 'author'] + +class CommentDetailSerializer(ModelSerializer): + + class Meta: + model = Comment + fields = ['title', 'date_created', 'author', 'description'] \ No newline at end of file diff --git a/softdesk/support/views.py b/softdesk/support/views.py index 6481296..3d8891c 100644 --- a/softdesk/support/views.py +++ b/softdesk/support/views.py @@ -3,21 +3,33 @@ from rest_framework.serializers import raise_errors_on_nested_writes from rest_framework.viewsets import ModelViewSet from support.models import Project, ProjectContributor, Issue, Comment from authentication.models import User -from support.serializers import ProjectSerializer, ContributorSerializer +from support.serializers import (ProjectSerializer, + ProjectDetailSerializer, + ContributorSerializer, + IssueSerializer, + CommentListSerializer, + CommentDetailSerializer) from rest_framework.response import Response from rest_framework import status from rest_framework.permissions import (IsAuthenticated, IsAuthenticatedOrReadOnly) -from support.permissions import IsAuthor +from support.permissions import IsAuthor, IsContributor from rest_framework.decorators import action class ProjectViewSet(ModelViewSet): - permission_classes=[IsAuthenticatedOrReadOnly] + permission_classes = [IsAuthenticatedOrReadOnly] serializer_class = ProjectSerializer + detail_serializer_class = ProjectDetailSerializer + queryset = Project.objects.filter(active=True) + def get_serializer_class(self): + if self.action == 'retrieve': + return self.detail_serializer_class + return super().get_serializer_class() + def perform_create(self, serializer): """set authenticated user as author and contributor on creation""" test = serializer.save(author=self.request.user) @@ -27,7 +39,7 @@ class ProjectViewSet(ModelViewSet): contributor_serializer.save() @action(detail=True, methods=['patch'], - permission_classes=[IsAuthor], + permission_classes=[IsContributor], basename='add_contributor') def add_contributor(self, request, pk): """Create the user/project contributor's relation""" @@ -47,4 +59,60 @@ class ProjectViewSet(ModelViewSet): class IssueViewSet(ModelViewSet): - serializer = \ No newline at end of file + permission_classes = [IsContributor] + + serializer_class = IssueSerializer + + + def get_queryset(self): + project_id = int(self.request.GET.get('project')) + project = Project.objects.get(id=project_id) + self.check_object_permissions(self.request, project) + return Issue.objects.filter(project=project_id) + + + def get_contributors(self, project): + queryset = ProjectContributor.objects.filter(project=project) + contributors_serializer = ContributorSerializer(queryset, many=True) + return contributors_serializer.data + + + def create(self, request, *args, **kwargs): + print(request.data['project']) + project = Project.objects.get(id=request.data['project']) + serializer = IssueSerializer(data=request.data) + + + print(request.data['project'], type(request.data['project'])) + print(self.get_contributors(request.data['project'])) + + if self.request.user in project.contributors: + if serializer.is_valid(raise_exception=True): + serializer.author = self.request.user + serializer.save() + response = { + "message": f"Issue created for project {project}", + "data": serializer.data + } + return Response(response, status = status.HTTP_201_CREATED) + + #def perform_create(self, serializer): + # """set authenticated user as author and contributor on creation""" + # serializer.save(author=self.request.user) + + +class ContributorViewSet(ModelViewSet): + serializer_class = ContributorSerializer + queryset = ProjectContributor.objects.all() + + +class CommentViewSet(ModelViewSet): + serializer_class = CommentListSerializer + detail_serializer_class = CommentDetailSerializer + + queryset = Comment.objects.all() + + def get_serializer_class(self): + if self.action == 'retrieve': + return self.detail_serializer_class + return super().get_serializer_class()