clean responses messages; create doc

This commit is contained in:
yann 2025-06-10 15:33:30 +02:00
parent e8c62e9a83
commit a4c876132e
7 changed files with 433 additions and 66 deletions

301
README.md
View File

@ -53,12 +53,307 @@ python manage.py migrate
``` ```
python manage.py runserver python manage.py runserver
``` ```
## Use ___
## Usage
URL is : http://127.0.0.1:8000 **URL:** http://127.0.0.1:8000
Endpoints and methods are coming soon
**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 ## Author
YaL <yann@needsome.coffee> YaL <yann@needsome.coffee>

View File

@ -6,6 +6,7 @@ from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework import status from rest_framework import status
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from django.core.exceptions import PermissionDenied
from authentication.models import User from authentication.models import User
from authentication.serializers import (UserSerializer, from authentication.serializers import (UserSerializer,
@ -18,29 +19,22 @@ class UserCreateView(APIView):
""" """
Allow user registration for anyone 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): def post(self, request):
""" """
User subscription Creates a new user
Args: 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) serializer = UserRegisterSerializer(data=request.data)
if serializer.is_valid(raise_exception=True): if serializer.is_valid(raise_exception=True):
serializer.save() serializer.save()
response = { response = {
"message": "User created successfully", "detail": "User created successfully",
"data": serializer.data "data": serializer.data
} }
return Response(data=response, return Response(data=response, status=status.HTTP_201_CREATED)
status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
class PasswordUpdateView(APIView): class PasswordUpdateView(APIView):
@ -56,10 +50,11 @@ class PasswordUpdateView(APIView):
user.set_password(serializer.data.get('new_password')) user.set_password(serializer.data.get('new_password'))
user.save() user.save()
update_session_auth_hash(request, user) update_session_auth_hash(request, user)
return Response(serializer.errors, response = {
status=status.HTTP_204_NO_CONTENT) "detail": "Password updated successfully."
return Response(serializer.errors, }
status=status.HTTP_400_BAD_REQUEST) return Response(response, status=status.HTTP_204_NO_CONTENT)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class UserView(APIView): class UserView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@ -67,16 +62,18 @@ class UserView(APIView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return Response(UserSerializer(request.user).data) return Response(UserSerializer(request.user).data)
def put(self, request): def patch(self, request):
user = request.user user = request.user
serializer = UserUpdateSerializer(user, data=request.data) serializer = UserUpdateSerializer(user, data=request.data)
print(serializer.initial_data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response("Data updated", response = {
status=status.HTTP_201_CREATED) "detail": "Data updated",
return Response("Error", "data": serializer.data
status=status.HTTP_400_BAD_REQUEST) }
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): def delete(self, request):
user = request.user user = request.user
@ -84,13 +81,9 @@ class UserView(APIView):
if 'user' in request.data: if 'user' in request.data:
if username == request.data['user']: if username == request.data['user']:
user.delete() user.delete()
return Response(f"User {username} deleted.", response = {"detail": f"User {username} deleted."}
status=status.HTTP_204_NO_CONTENT) return Response(response, status=status.HTTP_204_NO_CONTENT)
return Response("Token's owner and user provided don't match", raise PermissionDenied()
status=status.HTTP_400_BAD_REQUEST) response = {"detail": "Username to delete must be given in data"}
return Response("Username to delete must be given in data", return Response(response, status=status.HTTP_400_BAD_REQUEST)
status=status.HTTP_400_BAD_REQUEST)

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -17,7 +17,7 @@ class Project(models.Model):
active = models.BooleanField(default=True) active = models.BooleanField(default=True)
description = models.CharField(max_length=4000) description = models.CharField(max_length=4000)
author = models.ForeignKey(settings.AUTH_USER_MODEL, author = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.DO_NOTHING, on_delete=models.SET_NULL,
related_name='project_author', null=True) related_name='project_author', null=True)
contributors = models.ManyToManyField(settings.AUTH_USER_MODEL, contributors = models.ManyToManyField(settings.AUTH_USER_MODEL,
@ -29,7 +29,7 @@ class Project(models.Model):
class ProjectContributor(models.Model): class ProjectContributor(models.Model):
contributor = models.ForeignKey(settings.AUTH_USER_MODEL, contributor = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.DO_NOTHING) on_delete=models.CASCADE)
active = models.BooleanField(default=True) active = models.BooleanField(default=True)
project = models.ForeignKey('Project', project = models.ForeignKey('Project',
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -76,7 +76,7 @@ class Issue(models.Model):
on_delete=models.CASCADE) on_delete=models.CASCADE)
author = models.ForeignKey(settings.AUTH_USER_MODEL, author = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.DO_NOTHING, on_delete=models.DO_NOTHING,
related_name='issue_author', null=True) related_name='issue_author', blank=True, null=True)
class Comment(models.Model): class Comment(models.Model):

View File

@ -87,21 +87,25 @@ class ProjectViewSet(ModelViewSet):
if not request.user in Project.objects.get(id=pk).contributors.all(): if not request.user in Project.objects.get(id=pk).contributors.all():
raise PermissionDenied() raise PermissionDenied()
if request.data is None or not 'contributor' in request.data: if request.data is None or not 'contributor' in request.data:
return Response(f"Key error;`contributor` is expected", response = {"detail": "Key error;`contributor` is expected"}
status=status.HTTP_400_BAD_REQUEST) return Response(response, status=status.HTTP_400_BAD_REQUEST)
requested_contributor = request.data['contributor']
#get the user's instance #get the user's instance
contributor = User.objects.get(username=request.data['contributor']) try:
data = {'contributor': contributor.id, 'project': int(pk)} user = User.objects.get(username=requested_contributor)
data = {'contributor': user.id, 'project': int(pk)}
serializer = ContributorSerializer(data=data) serializer = ContributorSerializer(data=data)
project = Project.objects.get(id=pk) project = Project.objects.get(id=pk)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(f"User {contributor} " response = {"detail": f"User {user}"
f"added to project {project}", f"added to project ''{project}''"}
status=status.HTTP_202_ACCEPTED) return Response(response, status=status.HTTP_202_ACCEPTED)
response = {'message': 'This user is already contributing'} response = {"detail": "This user is already contributing"}
return Response(response, return Response(response, status=status.HTTP_226_IM_USED)
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): class IssueViewSet(ModelViewSet):
@ -125,7 +129,8 @@ class IssueViewSet(ModelViewSet):
id=project_id).contributors.all(): id=project_id).contributors.all():
raise PermissionDenied() raise PermissionDenied()
return Issue.objects.filter(project=project_id) 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 #query on a list
return Issue.objects.filter(project__in=projects) return Issue.objects.filter(project__in=projects)
@ -143,7 +148,8 @@ class IssueViewSet(ModelViewSet):
username=self.request.data['author']) username=self.request.data['author'])
serializer.save(author=requested_author) serializer.save(author=requested_author)
return Response(serializer.data) 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']) @action(detail=True, methods=['get'])
def contributors(self, request, pk): def contributors(self, request, pk):
@ -152,8 +158,11 @@ class IssueViewSet(ModelViewSet):
of the contributors to the issue's project or raise unauthorized of the contributors to the issue's project or raise unauthorized
""" """
issue = Issue.objects.get(id=pk) issue = Issue.objects.get(id=pk)
if ProjectContributor.objects.filter(project=issue.project).filter(contributor=request.user): if (ProjectContributor.objects.
return Response(UserListSerializer(issue.project.contributors.all(), many=True).data) filter(project=issue.project).
filter(contributor=request.user)):
return Response(UserListSerializer(
issue.project.contributors.all(), many=True).data)
else: else:
raise PermissionDenied() raise PermissionDenied()
@ -164,12 +173,14 @@ class IssueViewSet(ModelViewSet):
project = Project.objects.get(id=request.data['project']) project = Project.objects.get(id=request.data['project'])
serializer = IssueSerializer(data=request.data) serializer = IssueSerializer(data=request.data)
if self.request.user not in project.contributors.all(): 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 = { 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 "data": serializer.data
} }
return Response(response, status = status.HTTP_201_CREATED) return Response(response, status = status.HTTP_201_CREATED)
@ -210,9 +221,9 @@ class CommentViewSet(ModelViewSet):
serializer = CommentDetailSerializer(data=request.data) serializer = CommentDetailSerializer(data=request.data)
if serializer.is_valid(raise_exception=True): if serializer.is_valid(raise_exception=True):
serializer.save(author=user) serializer.save(author=user)
response = {"message": "comment created", response = {"detail": "comment created",
"data": serializer.data} "data": serializer.data}
return Response(response, status=status.HTTP_201_CREATED) return Response(response, status=status.HTTP_201_CREATED)
return Response("Not allowed; " response = {"detail": f"{user} isn't contributor for '{project}'"}
f"{user} isn't contributor for project {project}", return Response(response, status=status.HTTP_403_FORBIDDEN)
status=status.HTTP_403_FORBIDDEN)