Skip to content

Commit

Permalink
add mixin for models search, project goals
Browse files Browse the repository at this point in the history
  • Loading branch information
githubering182 committed Aug 19, 2024
1 parent b91d9c1 commit 8e6c972
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 112 deletions.
35 changes: 35 additions & 0 deletions backend-app/api/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from django.db.models import Model
from typing import Any, Callable
from rest_framework.status import HTTP_404_NOT_FOUND


def with_model_assertion(
model: Model,
accessor: str,
class_based=True,
**kw
) -> Callable:
def decorator(callback: Callable) -> Callable:
def inner(*args, **kwargs) -> Any:
pk_index = int(class_based)

try:
_model = model.objects \
.select_related(*kw.get("select", [])) \
.prefetch_related(*kw.get("prefetch", [])) \
.filter(**kw.get("filter", {})) \
.get(**{accessor: args[pk_index]})

arg_payload = list(args)
arg_payload[pk_index] = _model

return callback(*arg_payload, **kwargs)

except model.DoesNotExist: return (
{"message": "Query model does not exist"},
HTTP_404_NOT_FOUND
)

return inner

return decorator
50 changes: 21 additions & 29 deletions backend-app/project/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
TextField,
DateTimeField,
BooleanField,
ManyToManyField
ManyToManyField,
ForeignKey,
CASCADE,
IntegerField
)
from attribute.models import Level, Attribute
from typing import Any
Expand All @@ -16,34 +19,13 @@ class Project(Model):
created_at: DateTimeField = DateTimeField(auto_now_add=True)
visible: BooleanField = BooleanField(default=True)

user_visible: ManyToManyField = ManyToManyField(
to="user.CustomUser",
related_name="project_visible"
)
user_upload: ManyToManyField = ManyToManyField(
to="user.CustomUser",
related_name="project_upload"
)
user_view: ManyToManyField = ManyToManyField(
to="user.CustomUser",
related_name="project_view"
)
user_validate: ManyToManyField = ManyToManyField(
to="user.CustomUser",
related_name="project_validate"
)
user_stats: ManyToManyField = ManyToManyField(
to="user.CustomUser",
related_name="project_stats"
)
user_download: ManyToManyField = ManyToManyField(
to="user.CustomUser",
related_name="project_download"
)
user_edit: ManyToManyField = ManyToManyField(
to="user.CustomUser",
related_name="project_edit"
)
user_visible = ManyToManyField(to="user.CustomUser", related_name="project_visible")
user_upload = ManyToManyField(to="user.CustomUser", related_name="project_upload")
user_view = ManyToManyField(to="user.CustomUser", related_name="project_view")
user_validate = ManyToManyField(to="user.CustomUser", related_name="project_validate")
user_stats = ManyToManyField(to="user.CustomUser", related_name="project_stats")
user_download = ManyToManyField(to="user.CustomUser", related_name="project_download")
user_edit = ManyToManyField(to="user.CustomUser", related_name="project_edit")

class Meta:
db_table = "project"
Expand Down Expand Up @@ -122,3 +104,13 @@ def _create_attributes(

if children: self._create_attributes(children, rest, ATTRIBUTE, LEVEL)
elif rest: self._create_attributes([], rest, None, LEVEL)


class ProjectGoals(Model):
amount = IntegerField(default=0)

attribute = ForeignKey("attribute.Attribute", on_delete=CASCADE)
project = ForeignKey("project.Project", on_delete=CASCADE)

class Meta:
db_table = "project_goal"
2 changes: 1 addition & 1 deletion backend-app/project/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class ProjectPermission(BasePermission):
def has_permission(self, request: Request, view: APIView) -> bool | None:
if request.user.is_superuser or request.method == "GET": return True

if request.method in {"PATCH", "DELETE"}:
if request.method in {"PATCH", "DELETE", "POST"}:
return bool(request.user.project_edit.filter(id=view.kwargs["pk"]))


Expand Down
23 changes: 20 additions & 3 deletions backend-app/project/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from rest_framework.views import Request
from typing import Any
from attribute.serializers import LevelSerializer
from .models import Project
from .models import Project, ProjectGoals


class ProjectsSerializer(ModelSerializer):
Expand All @@ -16,8 +16,8 @@ def add_attributes(self) -> None:


class ProjectSerializer(ProjectsSerializer):
attributes: SerializerMethodField = SerializerMethodField()
permissions: SerializerMethodField = SerializerMethodField()
attributes= SerializerMethodField()
permissions= SerializerMethodField()

class Meta(ProjectsSerializer.Meta):
fields = ("id", "name", "description", "attributes", "permissions")
Expand All @@ -43,3 +43,20 @@ def get_permissions(self, instance: Project) -> dict[str, bool]:
"download": user_id in {user.id for user in instance.user_download.all()},
"edit": user_id in {user.id for user in instance.user_edit.all()},
}


class GoalSerializer(ModelSerializer):
project = SerializerMethodField()
attribute = SerializerMethodField()
complete = SerializerMethodField()

class Meta:
model = ProjectGoals
fields = "__all__"

def get_project(self, instance: ProjectGoals) -> int: return instance.project.id

def get_attribute(self, instance: ProjectGoals) -> str: return instance.attribute.name

def get_complete(self, instance: ProjectGoals) -> int:
return instance.attribute.attributegroup_set.count()
132 changes: 54 additions & 78 deletions backend-app/project/services.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from rest_framework.views import Request
from rest_framework.status import (
HTTP_400_BAD_REQUEST,
HTTP_404_NOT_FOUND,
HTTP_201_CREATED,
HTTP_200_OK,
HTTP_202_ACCEPTED
Expand All @@ -11,17 +10,10 @@
from typing import Any
from user.models import CustomUser
from .serializers import ProjectSerializer, Project, ProjectsSerializer
from api.mixins import with_model_assertion


class ViewSetServices:
project_prefetch = (
"user_upload",
"user_validate",
"user_stats",
"user_download",
"user_edit"
)

def _get_available_projects(self, user: CustomUser) -> QuerySet:
projects: QuerySet = Project.objects.order_by("id").filter(visible=True)

Expand All @@ -35,97 +27,81 @@ def _create_project(
request_data: dict[str, Any]
) -> tuple[dict[str, Any], int]:
new_project: ProjectsSerializer = ProjectsSerializer(data=request_data)
try:
assert new_project.is_valid(), new_project.errors

if valid := new_project.is_valid():
try:
with transaction.atomic():
new_project.save()
new_project.add_attributes()
except Exception: valid = False
with transaction.atomic():
new_project.save()
new_project.add_attributes()

response_data = {"ok": valid}
return {"ok": True}, HTTP_201_CREATED

if not valid: response_data["errors"] = new_project.errors or "Unexpected error"
except Exception as e: return {"errors": str(e)}, HTTP_400_BAD_REQUEST

@with_model_assertion(
Project,
"id",
filter={"visible": True},
prefetch=("user_upload", "user_validate", "user_stats", "user_download", "user_edit")
)
def _get_project(self, project, request: Request) -> tuple[dict[str, Any], int]:
return (
response_data,
HTTP_201_CREATED if valid else HTTP_400_BAD_REQUEST
ProjectSerializer(project, context={"request": request}).data,
HTTP_200_OK
)

def _get_project(
self,
pk: int,
request: Request
) -> tuple[dict[str, Any], int]:
response: dict[str, Any] = {}
status: int = HTTP_200_OK

try:
project = Project.objects \
.prefetch_related(*self.project_prefetch) \
.filter(visible=True) \
.get(id=pk)
response = ProjectSerializer(project, context={"request": request}).data

except Project.DoesNotExist:
response["message"] = "query project does not exist"
status = HTTP_404_NOT_FOUND

return response, status

@with_model_assertion(Project, "id", filter={"visible": True}, prefetch=("attribute_set",))
def _patch_project(
self,
pk: int,
project: Project,
request_data: dict[str, Any]
) -> tuple[dict[str, Any], int]:
project = Project.objects \
.prefetch_related("attribute_set") \
.get(id=pk)

updated = ProjectSerializer(
project,
data=request_data,
partial=True
)
updated = ProjectSerializer(project, data=request_data, partial=True)

if valid := updated.is_valid():
try:
with transaction.atomic():
updated.save()
updated.add_attributes()
except Exception: valid = False
try:
assert updated.is_valid(), updated.errors

response: dict[str, Any] = {'ok': valid}
with transaction.atomic():
updated.save()
updated.add_attributes()

if not valid: response['errors'] = updated.errors
return {"ok": True}, HTTP_202_ACCEPTED

return (
response,
HTTP_202_ACCEPTED if valid else HTTP_400_BAD_REQUEST
)
except Exception as e: return {"errors": str(e)}, HTTP_400_BAD_REQUEST

@with_model_assertion(Project, "id", filter={"visible": True})
def _delete_project(
self,
pk: int,
project: Project,
request_data: dict[str, Any]
) -> tuple[dict[str, Any], int]:
response: dict[str, Any] = {}
status: int = HTTP_200_OK

try:
project: Project = Project.objects.filter(visible=True).get(id=pk)
assert request_data.get("approval") == project.name
project.visible = False
project.save()

if request_data.get('approval') == project.name:
project.visible = False
project.save()
response["deleted"] = True
return {"deleted": True}, HTTP_200_OK

except AssertionError: return (
{"message": "approval text differs from the actual name"},
HTTP_400_BAD_REQUEST,
)


class GoalViewServices:
@with_model_assertion(
Project,
"id",
filter={"visible": True},
prefetch=("projectgoals_set__attribute__attributegroup_set",)
)
def _get_goals(self, project: Project) -> tuple[list, int]: ...

else:
response["message"] = 'approval text differs from the actual name'
status = HTTP_400_BAD_REQUEST
@with_model_assertion(Project, "id", filter={"visible": True})
def _create_goal(self, project: Project, request_data: dict[str, Any]): ...

except Project.DoesNotExist:
response["message"] = 'query project does not exist'
status = HTTP_404_NOT_FOUND
@with_model_assertion(Project, "id", filter={"visible": True})
def _update_goal(self, project: Project, goal_pk: int): ...

return response, status
@with_model_assertion(Project, "id", filter={"visible": True})
def _delete_goal(self, project: Project, goal_pk: int): ...
23 changes: 22 additions & 1 deletion backend-app/project/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework.views import Response, APIView, Request
from rest_framework.permissions import IsAuthenticated
from .permissions import ProjectsPermission, ProjectPermission, ProjectViewPermission
from .services import ViewSetServices
from .services import ViewSetServices, GoalViewServices


class ProjectsViewSet(APIView, ViewSetServices):
Expand Down Expand Up @@ -31,3 +31,24 @@ def patch(self, request: Request, pk: int) -> Response:
def delete(self, request: Request, pk: int) -> Response:
response, status = self._delete_project(pk, request.data)
return Response(response, status=status)


class GoalViewSet(APIView, GoalViewServices):
http_method_names = ("get", "post", "patch", "delete")
permission_classes = (IsAuthenticated, ProjectPermission, ProjectViewPermission)

def get(self, pk: int) -> Response:
response, status = self._get_goals(pk)
return Response(response, status=status)

def post(self, request: Request, pk: int) -> Response:
response, status = self._create_goal(pk, request.data)
return Response(response, status=status)

def patch(self, request: Request, pk: int, goal_pk: int) -> Response:
response, status = self._update_goal(pk, goal_pk)
return Response(response, status=status)

def delete(self, pk: int, goal_pk: int) -> Response:
response, status = self._delete_goal(pk, goal_pk)
return Response(response, status=status)

0 comments on commit 8e6c972

Please sign in to comment.