Skip to content

Conversation

@tharjiha
Copy link

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

  • Volunteers can create item movement requests
  • Admins can view all pending movement requests
  • Admins can approve or reject movement requests
  • ItemHistory records are automatically generated on approval/rejection

Acceptance Criteria Checklist

  • Volunteers can request moves when authorized
  • Admin approvals/rejections update movement request status
  • ItemHistory entries match final request status

Testing

  • Manually tested via Django Admin and API endpoints
  • Verified database updates for:
    • ItemMovementRequest
    • ItemHistory

Copy link

Copilot AI left a 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 ItemMovementRequestViewSet with 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:

  1. Duplicate Model Creation: The PR attempts to create a new ItemHistory model in the requests app, but this model already exists in the inventory app with different field names (event_type vs item_type). This will cause database conflicts and import errors.

  2. Security Vulnerability: The requested_by field in the serializer is not marked as read-only, allowing potential user impersonation during request creation.

  3. 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.

Comment on lines 53 to 54
validated_data["requested_by"] = self.context["request"].user
return super().create(validated_data)
Copy link

Copilot AI Dec 22, 2025

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.

Suggested change
validated_data["requested_by"] = self.context["request"].user
return super().create(validated_data)
return super().create(validated_data)

Copilot uses AI. Check for mistakes.
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import ItemMovementRequest, ItemHistory
Copy link

Copilot AI Dec 22, 2025

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.

Suggested change
from .models import ItemMovementRequest, ItemHistory
from .models import ItemMovementRequest

Copilot uses AI. Check for mistakes.
Comment on lines 66 to 69
# volunteers see only their own requests
user = self.request.user
if user.is_staff:
return ItemMovementRequest.objects.all() # admins see all
Copy link

Copilot AI Dec 22, 2025

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.

Suggested change
# 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

Copilot uses AI. Check for mistakes.
Comment on lines 55 to 89
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)
Copy link

Copilot AI Dec 22, 2025

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.

Copilot uses AI. Check for mistakes.
"created_at",
"updated_at",
]
read_only_fields = ["id", "status", "admin", "admin_comment", "created_at", "updated_at"]
Copy link

Copilot AI Dec 22, 2025

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.

Suggested change
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"]

Copilot uses AI. Check for mistakes.
Comment on lines 83 to 89
@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)
Copy link

Copilot AI Dec 22, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines 92 to 119


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}"
Copy link

Copilot AI Dec 22, 2025

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.

Suggested change
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 uses AI. Check for mistakes.
read_only_fields = ["id", "status", "admin", "admin_comment", "created_at", "updated_at"]

def create(self, validated_data):
# volunteer = logged-in user
Copy link

Copilot AI Dec 22, 2025

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.

Suggested change
# volunteer = logged-in user
# Volunteer is the logged-in user.

Copilot uses AI. Check for mistakes.
Comment on lines 75 to 81
@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)
Copy link

Copilot AI Dec 22, 2025

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.

Copilot uses AI. Check for mistakes.
@@ -1,4 +1,5 @@
from rest_framework import serializers
from .models import ItemMovementRequest, ItemHistory
Copy link

Copilot AI Dec 22, 2025

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.

Suggested change
from .models import ItemMovementRequest, ItemHistory
from .models import ItemMovementRequest

Copilot uses AI. Check for mistakes.
@tharjiha tharjiha requested review from qiuethan and vivjd January 2, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create Endpoints for Item Move Requests and Admin Approval

2 participants