-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement item movement request workflow with admin approval #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…izers, views, and URLs for move request workflow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements a workflow for item movement requests where volunteers can request item moves and admins can approve or reject them. However, there are critical issues that need to be addressed before merging, particularly around duplicate model creation and security concerns.
Key Changes:
- Adds
ItemMovementRequestViewSetwith approve/reject actions for admin users - Implements URL routing for movement request endpoints
- Creates serializers for movement request data handling
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/requests/models.py | Attempts to add ItemHistory model (duplicates existing inventory model) |
| backend/requests/serializers.py | Adds ItemMovementRequestSerializer with username fields |
| backend/requests/views.py | Implements viewset with create, list, approve, and reject actions |
| backend/requests/urls.py | Registers movement-requests router endpoint |
| backend/requests/migrations/0003_itemhistory.py | Migration to create ItemHistory (conflicts with existing model) |
| backend/core/urls.py | Integrates requests app URLs into main routing |
Critical Issues Found:
-
Duplicate Model Creation: The PR attempts to create a new
ItemHistorymodel in the requests app, but this model already exists in the inventory app with different field names (event_typevsitem_type). This will cause database conflicts and import errors. -
Security Vulnerability: The
requested_byfield in the serializer is not marked as read-only, allowing potential user impersonation during request creation. -
Status Validation: The approve/reject actions don't validate whether a request has already been processed, potentially creating duplicate history entries.
Comments suppressed due to low confidence (1)
backend/requests/views.py:4
- Import of 'ItemHistory' is not used.
from .models import ItemMovementRequest, ItemHistory
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
backend/requests/serializers.py
Outdated
| validated_data["requested_by"] = self.context["request"].user | ||
| return super().create(validated_data) |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'requested_by' field is being set redundantly. It's already set in the viewset's perform_create method (line 73 in views.py), so this assignment here is unnecessary and will be overwritten. Remove this line and let the viewset handle setting the requested_by field.
| validated_data["requested_by"] = self.context["request"].user | |
| return super().create(validated_data) | |
| return super().create(validated_data) |
backend/requests/views.py
Outdated
| from rest_framework import viewsets, permissions, status | ||
| from rest_framework.decorators import action | ||
| from rest_framework.response import Response | ||
| from .models import ItemMovementRequest, ItemHistory |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ItemHistory is imported but never used in this file. Since ItemHistory operations are handled within the model's approve/reject methods, this import should be removed.
| from .models import ItemMovementRequest, ItemHistory | |
| from .models import ItemMovementRequest |
backend/requests/views.py
Outdated
| # volunteers see only their own requests | ||
| user = self.request.user | ||
| if user.is_staff: | ||
| return ItemMovementRequest.objects.all() # admins see all |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment should start with a capital letter: "Admins see all". Following consistent capitalization in comments improves code readability.
| # volunteers see only their own requests | |
| user = self.request.user | |
| if user.is_staff: | |
| return ItemMovementRequest.objects.all() # admins see all | |
| # Volunteers see only their own requests | |
| user = self.request.user | |
| if user.is_staff: | |
| return ItemMovementRequest.objects.all() # Admins see all |
| class ItemMovementRequestViewSet(viewsets.ModelViewSet): | ||
| """ | ||
| viewSet for managing item movement requests. | ||
| supports creation, listing, retrieval, approval, and rejection. | ||
| """ | ||
|
|
||
| queryset = ItemMovementRequest.objects.all() | ||
| serializer_class = ItemMovementRequestSerializer | ||
| permission_classes = [permissions.IsAuthenticated] | ||
|
|
||
| def get_queryset(self): | ||
| # volunteers see only their own requests | ||
| user = self.request.user | ||
| if user.is_staff: | ||
| return ItemMovementRequest.objects.all() # admins see all | ||
| return ItemMovementRequest.objects.filter(requested_by=user) | ||
|
|
||
| def perform_create(self, serializer): | ||
| serializer.save(requested_by=self.request.user) | ||
|
|
||
| @action(detail=True, methods=["post"], permission_classes=[permissions.IsAdminUser]) | ||
| def approve(self, request, pk=None): | ||
| move_request = self.get_object() | ||
| comment = request.data.get("comment", "") | ||
| move_request.approve(admin_user=request.user, comment=comment) | ||
| serializer = self.get_serializer(move_request) | ||
| return Response(serializer.data, status=status.HTTP_200_OK) | ||
|
|
||
| @action(detail=True, methods=["post"], permission_classes=[permissions.IsAdminUser]) | ||
| def reject(self, request, pk=None): | ||
| move_request = self.get_object() | ||
| comment = request.data.get("comment", "") | ||
| move_request.reject(admin_user=request.user, comment=comment) | ||
| serializer = self.get_serializer(move_request) | ||
| return Response(serializer.data, status=status.HTTP_200_OK) |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new ItemMovementRequestViewSet and its approve/reject actions lack test coverage. Since the users app has pytest tests, similar test coverage should be added for the movement request workflow, including testing creation, approval, rejection, permission checks, and the resulting ItemHistory entries.
backend/requests/serializers.py
Outdated
| "created_at", | ||
| "updated_at", | ||
| ] | ||
| read_only_fields = ["id", "status", "admin", "admin_comment", "created_at", "updated_at"] |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'requested_by' field should be included in the read_only_fields list since it's automatically set by the viewset's perform_create method. Without this protection, a malicious user could potentially set this field to another user's ID during request creation, impersonating other users.
| read_only_fields = ["id", "status", "admin", "admin_comment", "created_at", "updated_at"] | |
| read_only_fields = ["id", "requested_by", "status", "admin", "admin_comment", "created_at", "updated_at"] |
| @action(detail=True, methods=["post"], permission_classes=[permissions.IsAdminUser]) | ||
| def reject(self, request, pk=None): | ||
| move_request = self.get_object() | ||
| comment = request.data.get("comment", "") | ||
| move_request.reject(admin_user=request.user, comment=comment) | ||
| serializer = self.get_serializer(move_request) | ||
| return Response(serializer.data, status=status.HTTP_200_OK) |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reject action doesn't validate the current status of the movement request before processing. A request that has already been approved or rejected could be processed again, potentially creating duplicate ItemHistory entries. Consider adding a check to ensure the request is in "WAITING_APPROVAL" status before proceeding, and return an appropriate error response if not.
backend/requests/models.py
Outdated
|
|
||
|
|
||
| class ItemHistory(models.Model): | ||
| """ | ||
| Records actions performed on items, including moves. | ||
| """ | ||
|
|
||
| ITEM_CHOICES = [ | ||
| ("MOVE_REQUESTED", "Move Requested"), | ||
| ("MOVE_APPROVED", "Move Approved"), | ||
| ("MOVE_REJECTED", "Move Rejected"), | ||
| ("MOVE_CANCELLED", "Move Cancelled"), | ||
| ] | ||
|
|
||
| item = models.ForeignKey(CollectionItem, on_delete=models.CASCADE) | ||
| item_type = models.CharField(max_length=50, choices=ITEM_CHOICES) | ||
| from_location = models.ForeignKey(Location, on_delete=models.SET_NULL, null=True, blank=True, related_name="+") | ||
| to_location = models.ForeignKey(Location, on_delete=models.SET_NULL, null=True, blank=True, related_name="+") | ||
| movement_request = models.ForeignKey("ItemMovementRequest", on_delete=models.SET_NULL, null=True, blank=True) | ||
| acted_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True) | ||
| notes = models.TextField(blank=True) | ||
| timestamp = models.DateTimeField(auto_now_add=True) | ||
|
|
||
| class Meta: | ||
| ordering = ["-timestamp"] | ||
|
|
||
| def __str__(self): | ||
| return f"{self.item_type} for {self.item.item_code} at {self.timestamp}" |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ItemHistory model already exists in inventory/models.py with an 'event_type' field. Creating a duplicate model here with 'item_type' will cause conflicts and database schema issues. The approve() and reject() methods in ItemMovementRequest already correctly import ItemHistory from inventory.models and use 'event_type'. This duplicate model definition should be removed entirely.
| class ItemHistory(models.Model): | |
| """ | |
| Records actions performed on items, including moves. | |
| """ | |
| ITEM_CHOICES = [ | |
| ("MOVE_REQUESTED", "Move Requested"), | |
| ("MOVE_APPROVED", "Move Approved"), | |
| ("MOVE_REJECTED", "Move Rejected"), | |
| ("MOVE_CANCELLED", "Move Cancelled"), | |
| ] | |
| item = models.ForeignKey(CollectionItem, on_delete=models.CASCADE) | |
| item_type = models.CharField(max_length=50, choices=ITEM_CHOICES) | |
| from_location = models.ForeignKey(Location, on_delete=models.SET_NULL, null=True, blank=True, related_name="+") | |
| to_location = models.ForeignKey(Location, on_delete=models.SET_NULL, null=True, blank=True, related_name="+") | |
| movement_request = models.ForeignKey("ItemMovementRequest", on_delete=models.SET_NULL, null=True, blank=True) | |
| acted_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True) | |
| notes = models.TextField(blank=True) | |
| timestamp = models.DateTimeField(auto_now_add=True) | |
| class Meta: | |
| ordering = ["-timestamp"] | |
| def __str__(self): | |
| return f"{self.item_type} for {self.item.item_code} at {self.timestamp}" |
backend/requests/serializers.py
Outdated
| read_only_fields = ["id", "status", "admin", "admin_comment", "created_at", "updated_at"] | ||
|
|
||
| def create(self, validated_data): | ||
| # volunteer = logged-in user |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment should start with a capital letter and be more descriptive: "Volunteer is the logged-in user". This makes the code's intent clearer.
| # volunteer = logged-in user | |
| # Volunteer is the logged-in user. |
| @action(detail=True, methods=["post"], permission_classes=[permissions.IsAdminUser]) | ||
| def approve(self, request, pk=None): | ||
| move_request = self.get_object() | ||
| comment = request.data.get("comment", "") | ||
| move_request.approve(admin_user=request.user, comment=comment) | ||
| serializer = self.get_serializer(move_request) | ||
| return Response(serializer.data, status=status.HTTP_200_OK) |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approve and reject actions don't validate the current status of the movement request before processing. A request that has already been approved or rejected could be processed again, potentially creating duplicate ItemHistory entries. Consider adding a check to ensure the request is in "WAITING_APPROVAL" status before proceeding, and return an appropriate error response if not.
backend/requests/serializers.py
Outdated
| @@ -1,4 +1,5 @@ | |||
| from rest_framework import serializers | |||
| from .models import ItemMovementRequest, ItemHistory | |||
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import of 'ItemHistory' is not used.
| from .models import ItemMovementRequest, ItemHistory | |
| from .models import ItemMovementRequest |
Summary
This PR implements the full workflow for item movement requests, allowing volunteers to request item moves and admins to approve or reject them. Admin decisions are reflected in both the movement request records and item history.
Related Issue
Fixes #12
Changes
Acceptance Criteria Checklist
Testing