From a4c876132e05d609d8628f4a9e5f20d70e362d02 Mon Sep 17 00:00:00 2001 From: yann Date: Tue, 10 Jun 2025 15:33:30 +0200 Subject: [PATCH] clean responses messages; create doc --- README.md | 301 +++++++++++++++++- softdesk/authentication/views.py | 59 ++-- .../migrations/0015_alter_project_author.py | 21 ++ ...alter_issue_author_alter_project_author.py | 26 ++ ...17_alter_projectcontributor_contributor.py | 21 ++ softdesk/support/models.py | 6 +- softdesk/support/views.py | 65 ++-- 7 files changed, 433 insertions(+), 66 deletions(-) create mode 100644 softdesk/support/migrations/0015_alter_project_author.py create mode 100644 softdesk/support/migrations/0016_alter_issue_author_alter_project_author.py create mode 100644 softdesk/support/migrations/0017_alter_projectcontributor_contributor.py diff --git a/README.md b/README.md index 2dd1765..c81a9f7 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,307 @@ python manage.py migrate ``` python manage.py runserver ``` -## Use +___ +## Usage -URL is : http://127.0.0.1:8000 -Endpoints and methods are coming soon +**URL:** http://127.0.0.1:8000 +**Authentication :** +Without authentication (no token): +- you can create a user +- you can get the project's list + +For any other action a token is required +To get details of a project you must be contributor + +To create/get detail of issue or to create/get detail of comment you must be contributor to the project + +User's management: +----- + +### *User create:* + +-> POST /api/user/create/ +<- 201_CREATED ; 400_BAD_REQUEST +``` +params: + +{ + "username": str, + "email": str, + "password": str, + "password2": str, + "age": int, + "can_be_contacted": boolean, + "can_data_be_shared": boolean +} +``` + +### *User info:* +*token required* +-> GET /api/user/ +<- 200_OK with user's info + +### *User update:* +*token required* +-> PATCH /api/user/ +<- 201_CREATED; 400_BAD_REQUEST +``` +params: + +{ + "email": str, + "can_be_contacted": boolean, + "can_data_be_shared": boolean +} +``` + +### *Password update:* + +-> GET /api/user/password-update/ +<- 204_NO_CONTENT; 400_BAD_REQUEST +``` +params: + +{ + "old_password": str, + "new_password": str, +} +``` +### *Delete a user* + +*token required* +-> DELETE /api/user/ +<- 204_NO_CONTENT; 401_UNAUTHORIZED +``` +params: + +{ + "user": str +} +``` + +### *Get token* + +-> POST /api/token/ +<- 200_OK +``` +params +{ + "username": str, + "password": str, +} + +response +{ + "refresh": "xxxxx", + "access": "xxxx" +} +``` +### *Refresh token* + +-> POST /api/token/refresh/ +<- 200_OK +``` +params +{ + "username": str, + "password": str, + "refresh": "xxxxxx" +} + +response +{ + "refresh": "xxxxx", + "access": "xxxx" +} +``` + +Project: +--- +### *Retrieve the list of projects* +-> GET /api/project/ +<- 200_OK / data; 403_FORBIDDEN +``` +querystrings + +?contributor={user} +?author={user} +``` + +### *Create a project* +-> POST /api/project/ +<- 200_OK / data; 403_FORBIDDEN +``` +params: + +{ + "title": str, + "type": + "choices": [ + { + "value": "BackEnd", + }, + { + "value": "FrontEnd", + }, + { + "value": "iOS", + }, + { + "value": "Android", + }, + "description": str, +} +``` +### *Get project's detail* +*token required* +-> GET /api/project/{id}/ +<- 200_OK / data; 403_FORBIDDEN + +### *Update a project* +*token required* +-> PATCH /api/project/{id}/ +<- 200_OK / data; 403_FORBIDDEN +params: + +{ + "title": str, + "type": + "choices": [ + { + "value": "BackEnd", + }, + { + "value": "FrontEnd", + }, + { + "value": "iOS", + }, + { + "value": "Android", + }, + "description": str, +} + +### *Add a contributor to a project* +*token required* + +-> PATCH /api/project/{id}/contributor/ +<- 202_ACCEPTED; 403_FORBIDDEN +``` +params: + +{ + "contributor": {username} +} +``` +### *Delete a project* +*token required* +-> DELETE /api/project/{id}/ +<- 204_NO_CONTENT; 403_FORBIDDEN + +Issue: +--- +### *List issues (where requestor is contributor)* +*token required* +-> GET /api/issue/ +<- 200_OK + +### *Create an issue* +*token required* +-> POST /api/issue/ +<- 201_CREATED / data; 403_FORBIDDEN +``` +params: + +{ + "title": str, + "project": int, + "description": str, + "priority": + "choices": [ + { + "value": "Low", + }, + { + "value": "Medium", + }, + { + "value": "High", + } + ] + "tag": + "choices": [ + { + "value": "Bug", + }, + { + "value": "Feature", + }, + { + "value": "Task", + } + ] + "status": + "choices": [ + { + "value": "ToDo", + }, + { + "value": "In Progress", + }, + { + "value": "Finished",, + } + ] +} +``` +### *Update an issue* +*token required* +-> PATCH /api/issue/{id}/ +<- 200_OK / data; 403_FORBIDDEN + +/!\ Only the author of an issue can affect it +(update to another author) + +### *Delete an issue* +*token required* +-> DELETE /api/issue/{id}/ +<- 204_NO_CONTENT; 403_FORBIDDEN + +### *Retrieve contributors for a given issue* +*token required* +-> GET /api/issue/{id}/contributors/ +<- 200_OK; 403_FORBIDDEN + +Comment: +--- +### *Create a comment* +*token required* +-> POST /api/comment/ +<- 201_CREATED / data; 403_FORBIDDEN +``` +params: + +{ + "title": str, + "issue": int, + "description": str +} +``` + +### *Update a comment* +*token required* +-> PATCH /api/comment/{id}/ +<- 200_OK / data; 403_FORBIDDEN + +### *Delete a comment* +*token required* +-> DELETE /api/comment/{id}/ +<- 204_NO_CONTENT; 403_FORBIDDEN +___ ## Author YaL diff --git a/softdesk/authentication/views.py b/softdesk/authentication/views.py index 451c83d..49cad28 100644 --- a/softdesk/authentication/views.py +++ b/softdesk/authentication/views.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework import status from rest_framework.permissions import IsAuthenticated +from django.core.exceptions import PermissionDenied from authentication.models import User from authentication.serializers import (UserSerializer, @@ -18,29 +19,22 @@ class UserCreateView(APIView): """ Allow user registration for anyone """ - - #TODELETE : for testing purpose - def get(self, request, *args, **kwargs): - user = User.objects.all() - serializer = UserSerializer(user, many=True) - return Response(serializer.data) - def post(self, request): """ - User subscription - Args: + Creates a new user + Requires : + username->str, email->str, password->str, password2->str, age->int, + can_be_contacted->bool, can_data_be_shared->bool """ serializer = UserRegisterSerializer(data=request.data) if serializer.is_valid(raise_exception=True): serializer.save() response = { - "message": "User created successfully", + "detail": "User created successfully", "data": serializer.data } - return Response(data=response, - status=status.HTTP_201_CREATED) - return Response(serializer.errors, - status=status.HTTP_400_BAD_REQUEST) + return Response(data=response, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class PasswordUpdateView(APIView): @@ -56,10 +50,11 @@ class PasswordUpdateView(APIView): user.set_password(serializer.data.get('new_password')) user.save() update_session_auth_hash(request, user) - return Response(serializer.errors, - status=status.HTTP_204_NO_CONTENT) - return Response(serializer.errors, - status=status.HTTP_400_BAD_REQUEST) + response = { + "detail": "Password updated successfully." + } + return Response(response, status=status.HTTP_204_NO_CONTENT) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class UserView(APIView): permission_classes = [IsAuthenticated] @@ -67,16 +62,18 @@ class UserView(APIView): def get(self, request, *args, **kwargs): return Response(UserSerializer(request.user).data) - def put(self, request): + def patch(self, request): user = request.user serializer = UserUpdateSerializer(user, data=request.data) - print(serializer.initial_data) if serializer.is_valid(): serializer.save() - return Response("Data updated", - status=status.HTTP_201_CREATED) - return Response("Error", - status=status.HTTP_400_BAD_REQUEST) + response = { + "detail": "Data updated", + "data": serializer.data + } + return Response(response, status=status.HTTP_201_CREATED) + response = {"detail": "Data error"} + return Response(response, status=status.HTTP_400_BAD_REQUEST) def delete(self, request): user = request.user @@ -84,13 +81,9 @@ class UserView(APIView): if 'user' in request.data: if username == request.data['user']: user.delete() - return Response(f"User {username} deleted.", - status=status.HTTP_204_NO_CONTENT) - return Response("Token's owner and user provided don't match", - status=status.HTTP_400_BAD_REQUEST) - return Response("Username to delete must be given in data", - status=status.HTTP_400_BAD_REQUEST) - - - + response = {"detail": f"User {username} deleted."} + return Response(response, status=status.HTTP_204_NO_CONTENT) + raise PermissionDenied() + response = {"detail": "Username to delete must be given in data"} + return Response(response, status=status.HTTP_400_BAD_REQUEST) diff --git a/softdesk/support/migrations/0015_alter_project_author.py b/softdesk/support/migrations/0015_alter_project_author.py new file mode 100644 index 0000000..c2c041b --- /dev/null +++ b/softdesk/support/migrations/0015_alter_project_author.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.1 on 2025-06-09 09:40 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('support', '0014_alter_issue_project'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='author', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='project_author', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/softdesk/support/migrations/0016_alter_issue_author_alter_project_author.py b/softdesk/support/migrations/0016_alter_issue_author_alter_project_author.py new file mode 100644 index 0000000..17b9b3d --- /dev/null +++ b/softdesk/support/migrations/0016_alter_issue_author_alter_project_author.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.1 on 2025-06-09 15:44 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('support', '0015_alter_project_author'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='issue', + name='author', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='issue_author', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='project', + name='author', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='project_author', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/softdesk/support/migrations/0017_alter_projectcontributor_contributor.py b/softdesk/support/migrations/0017_alter_projectcontributor_contributor.py new file mode 100644 index 0000000..218064a --- /dev/null +++ b/softdesk/support/migrations/0017_alter_projectcontributor_contributor.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.1 on 2025-06-09 15:59 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('support', '0016_alter_issue_author_alter_project_author'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='projectcontributor', + name='contributor', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/softdesk/support/models.py b/softdesk/support/models.py index df020f0..974d841 100644 --- a/softdesk/support/models.py +++ b/softdesk/support/models.py @@ -17,7 +17,7 @@ class Project(models.Model): active = models.BooleanField(default=True) description = models.CharField(max_length=4000) author = models.ForeignKey(settings.AUTH_USER_MODEL, - on_delete=models.DO_NOTHING, + on_delete=models.SET_NULL, related_name='project_author', null=True) contributors = models.ManyToManyField(settings.AUTH_USER_MODEL, @@ -29,7 +29,7 @@ class Project(models.Model): class ProjectContributor(models.Model): contributor = models.ForeignKey(settings.AUTH_USER_MODEL, - on_delete=models.DO_NOTHING) + on_delete=models.CASCADE) active = models.BooleanField(default=True) project = models.ForeignKey('Project', on_delete=models.CASCADE, @@ -76,7 +76,7 @@ class Issue(models.Model): on_delete=models.CASCADE) author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, - related_name='issue_author', null=True) + related_name='issue_author', blank=True, null=True) class Comment(models.Model): diff --git a/softdesk/support/views.py b/softdesk/support/views.py index 4cdeb43..91ea709 100644 --- a/softdesk/support/views.py +++ b/softdesk/support/views.py @@ -87,21 +87,25 @@ class ProjectViewSet(ModelViewSet): if not request.user in Project.objects.get(id=pk).contributors.all(): raise PermissionDenied() if request.data is None or not 'contributor' in request.data: - return Response(f"Key error;`contributor` is expected", - status=status.HTTP_400_BAD_REQUEST) + response = {"detail": "Key error;`contributor` is expected"} + return Response(response, status=status.HTTP_400_BAD_REQUEST) + requested_contributor = request.data['contributor'] #get the user's instance - contributor = User.objects.get(username=request.data['contributor']) - data = {'contributor': contributor.id, 'project': int(pk)} - serializer = ContributorSerializer(data=data) - project = Project.objects.get(id=pk) - if serializer.is_valid(): - serializer.save() - return Response(f"User {contributor} " - f"added to project {project}", - status=status.HTTP_202_ACCEPTED) - response = {'message': 'This user is already contributing'} - return Response(response, - status=status.HTTP_226_IM_USED) + try: + user = User.objects.get(username=requested_contributor) + data = {'contributor': user.id, 'project': int(pk)} + serializer = ContributorSerializer(data=data) + project = Project.objects.get(id=pk) + if serializer.is_valid(): + serializer.save() + response = {"detail": f"User {user}" + f"added to project ''{project}''"} + return Response(response, status=status.HTTP_202_ACCEPTED) + response = {"detail": "This user is already contributing"} + return Response(response, status=status.HTTP_226_IM_USED) + except: + response = {"detail": "User doesn't exist"} + return Response(response, status=status.HTTP_404_NOT_FOUND) class IssueViewSet(ModelViewSet): @@ -125,7 +129,8 @@ class IssueViewSet(ModelViewSet): id=project_id).contributors.all(): raise PermissionDenied() return Issue.objects.filter(project=project_id) - projects = Project.objects.filter(contributors=self.request.user).values('id') + projects = Project.objects.filter( + contributors=self.request.user).values('id') #query on a list return Issue.objects.filter(project__in=projects) @@ -143,7 +148,8 @@ class IssueViewSet(ModelViewSet): username=self.request.data['author']) serializer.save(author=requested_author) return Response(serializer.data) - return Response("Data error", status=status.HTTP_400_BAD_REQUEST) + response = {"detail": "Data error"} + return Response(response, status=status.HTTP_400_BAD_REQUEST) @action(detail=True, methods=['get']) def contributors(self, request, pk): @@ -152,8 +158,11 @@ class IssueViewSet(ModelViewSet): of the contributors to the issue's project or raise unauthorized """ issue = Issue.objects.get(id=pk) - if ProjectContributor.objects.filter(project=issue.project).filter(contributor=request.user): - return Response(UserListSerializer(issue.project.contributors.all(), many=True).data) + if (ProjectContributor.objects. + filter(project=issue.project). + filter(contributor=request.user)): + return Response(UserListSerializer( + issue.project.contributors.all(), many=True).data) else: raise PermissionDenied() @@ -164,12 +173,14 @@ class IssueViewSet(ModelViewSet): project = Project.objects.get(id=request.data['project']) serializer = IssueSerializer(data=request.data) if self.request.user not in project.contributors.all(): - return Response("Requestor isn't contributor for this project", - status=status.HTTP_403_FORBIDDEN) - if serializer.is_valid(raise_exception=True): - serializer.save(author=self.request.user) response = { - "message": f"Issue created for project {project}", + "detail": "Requestor isn't contributor for this project" + } + return Response(response, status=status.HTTP_403_FORBIDDEN) + if serializer.is_valid(raise_exception=True): + issue = serializer.save(author=self.request.user) + response = { + "detail": f"Issue {issue.id} created for project {project}", "data": serializer.data } return Response(response, status = status.HTTP_201_CREATED) @@ -210,9 +221,9 @@ class CommentViewSet(ModelViewSet): serializer = CommentDetailSerializer(data=request.data) if serializer.is_valid(raise_exception=True): serializer.save(author=user) - response = {"message": "comment created", + response = {"detail": "comment created", "data": serializer.data} return Response(response, status=status.HTTP_201_CREATED) - return Response("Not allowed; " - f"{user} isn't contributor for project {project}", - status=status.HTTP_403_FORBIDDEN) + response = {"detail": f"{user} isn't contributor for '{project}'"} + return Response(response, status=status.HTTP_403_FORBIDDEN) +