Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
PleatherStarfish committed Dec 1, 2024
1 parent b053753 commit f270d0c
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 137 deletions.
128 changes: 128 additions & 0 deletions backend/inventory/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
Expand Down Expand Up @@ -628,3 +629,130 @@ def test_cross_user_data_isolation(self):

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertDictEqual(response.json(), expected_data)


class UserInventoryViewTest(APITestCase):
def setUp(self):
# Create a test user
self.user = User.objects.create_user(
username="testuser", password="testpassword"
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)

# Create a test component type and manufacturer
self.component_type = Types.objects.create(name="Resistor")
self.manufacturer = ComponentManufacturer.objects.create(
name="Test Manufacturer"
)

# Create a test component
self.component = Component.objects.create(
description="Test Resistor",
type=self.component_type,
manufacturer=self.manufacturer,
manufacturer_part_no="TR-001",
mounting_style="th",
)

def test_get_user_inventory(self):
# Create sample inventory for the user
UserInventory.objects.create(
user=self.user, component=self.component, quantity=10
)

response = self.client.get("/api/inventory/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["quantity"], 10)

def test_post_user_inventory_create(self):
data = {
"quantity": 5,
"location": ["Shelf 1", "Bin A"],
}
response = self.client.post(
f"/api/inventory/{self.component.id}/create-or-update/", data
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["quantity"], 5)
self.assertEqual(response.data["location"], ["Shelf 1", "Bin A"])

def test_post_user_inventory_update(self):
# Create an initial inventory item
inventory = UserInventory.objects.create(
user=self.user, component=self.component, quantity=5, location=["Shelf 1"]
)

data = {
"quantity": 10,
"editMode": True,
"location": ["Shelf 1"],
}
response = self.client.post(
f"/api/inventory/{self.component.id}/create-or-update/", data
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
inventory.refresh_from_db()
self.assertEqual(inventory.quantity, 10)

def test_delete_user_inventory(self):
# Create an inventory item to delete
inventory = UserInventory.objects.create(
user=self.user, component=self.component, quantity=5
)

response = self.client.delete(f"/api/inventory/{inventory.id}/delete/")
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(UserInventory.objects.filter(id=inventory.id).exists())

def test_patch_user_inventory_location_update(self):
# Create an inventory item
inventory = UserInventory.objects.create(
user=self.user, component=self.component, quantity=5, location=["Shelf 1"]
)

data = {
"location": ["Shelf 2"],
}
response = self.client.patch(f"/api/inventory/{inventory.id}/update/", data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
inventory.refresh_from_db()
self.assertEqual(inventory.location, ["Shelf 2"])

def test_patch_user_inventory_duplicate_location(self):
# Create two inventory items with the same user and component
inventory1 = UserInventory.objects.create(
user=self.user, component=self.component, quantity=5, location=["Shelf 1"]
)
UserInventory.objects.create(
user=self.user, component=self.component, quantity=10, location=["Shelf 2"]
)

data = {
"location": ["Shelf 2"],
}
response = self.client.patch(f"/api/inventory/{inventory1.id}/update/", data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("error", response.data)
self.assertEqual(
response.data["error"],
"A user inventory item with this location already exists.",
)

def test_post_user_inventory_invalid_component(self):
invalid_uuid = uuid.uuid4()
data = {
"quantity": 5,
"location": ["Shelf 1", "Bin A"],
}
response = self.client.post(
f"/api/inventory/{invalid_uuid}/create-or-update/", data
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_delete_nonexistent_inventory(self):
non_existent_uuid = uuid.uuid4()
response = self.client.delete(f"/inventory/{non_existent_uuid}/delete/")
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(response.data["detail"], "User inventory not found")
24 changes: 22 additions & 2 deletions backend/inventory/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@
import json
from django.db.models import Q
from django.http import JsonResponse
from rest_framework.exceptions import NotFound


class UserInventoryView(APIView):
"""
Handles GET, POST, DELETE, and PATCH methods for the user's inventory.
"""

# permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticated]

def get(self, request):
user = request.user
Expand All @@ -41,10 +42,24 @@ def post(self, request, component_pk):

# Handle location if it is a list
if isinstance(location, list):
location = ", ".join(location).strip()
location = ", ".join(
location
).strip() # Converts list to a comma-separated string

if isinstance(location, list): # If already a list, use it directly
location_list = location
elif isinstance(location, str): # If a string, split it into a list
location_list = [loc.strip() for loc in location.split(",") if loc.strip()]
else:
location_list = None

location_list = location.split(",") if location else None

try:
component = Component.objects.get(id=component_pk)
except Component.DoesNotExist:
raise NotFound(detail="Component not found", code=status.HTTP_404_NOT_FOUND)

# Filter the user inventory items by user, component_id, and location
if location_list is not None:
# Using __exact lookup for an exact match of the JSON array
Expand Down Expand Up @@ -111,6 +126,11 @@ def patch(self, request, inventory_pk):
# Retrieve the user inventory item by inventory_pk
user_inventory_item = UserInventory.objects.filter(pk=inventory_pk).first()

if not user_inventory_item:
return Response(
{"error": "Inventory item not found."}, status=status.HTTP_404_NOT_FOUND
)

# Check if the location already exists for this user and component
if location_list is not None:

Expand Down
Empty file.
Empty file.
2 changes: 1 addition & 1 deletion backend/static/js/main.bundle.js

Large diffs are not rendered by default.

21 changes: 0 additions & 21 deletions frontend/src/components/ControlledInput.js

This file was deleted.

29 changes: 29 additions & 0 deletions frontend/src/components/ControlledInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { useEffect, useRef, useState } from 'react';

interface ControlledInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
value: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

const ControlledInput: React.FC<ControlledInputProps> = ({ value, onChange, ...rest }) => {
const [cursor, setCursor] = useState<number | null>(null);
const ref = useRef<HTMLInputElement | null>(null);

useEffect(() => {
const input = ref.current;
if (input && cursor !== null) {
input.setSelectionRange(cursor, cursor);
}
}, [ref, cursor, value]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCursor(e.target.selectionStart || 0);
if (onChange) {
onChange(e);
}
};

return <input onChange={handleChange} ref={ref} value={value} {...rest} />;
};

export default ControlledInput;
1 change: 1 addition & 0 deletions frontend/src/components/bom_list/quantity.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const Types = Object.freeze({
const Quantity = ({ useHook, hookArgs, replaceZero = true, classNames = "", hideLoadingTag = false }) => {
const [quantity, setQuantity] = React.useState();
const { data, isLoading, error } = useHook(...Object.values(hookArgs));
console.log(quantity)

useEffect(() => {
setQuantity(data);
Expand Down
78 changes: 78 additions & 0 deletions frontend/src/components/components/AsyncComponentSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { useCallback, useState } from "react";
import AsyncSelect from "react-select/async";
import debounce from "lodash/debounce";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";

interface ComponentOption {
value: string;
label: string;
}

interface AsyncComponentSelectProps {
placeholder?: string;
onChange: (selected: ComponentOption | null) => void;
value?: ComponentOption | null;
}

const fetchComponentOptions = async (inputValue: string): Promise<ComponentOption[]> => {
const response = await axios.get("/api/components-autocomplete/", {
params: { q: inputValue },
xsrfCookieName: "csrftoken",
xsrfHeaderName: "X-CSRFToken",
});
return response.data.results.map((item: { id: string; text: string }) => ({
label: item.text,
value: item.id,
}));
};

export const useComponentAutocomplete = (inputValue: string) => {
return useQuery({
enabled: Boolean(inputValue && inputValue.length >= 2),
queryFn: () => fetchComponentOptions(inputValue),
queryKey: ["components-autocomplete", inputValue],
});
};

const AsyncComponentSelect: React.FC<AsyncComponentSelectProps> = ({
placeholder = "Search components...",
onChange,
value,
}) => {
const [searchTerm, setSearchTerm] = useState("");

const { data: componentOptions = [], isLoading } = useComponentAutocomplete(searchTerm);

const debouncedLoadOptions = useCallback(
debounce((inputValue: string, callback: (options: ComponentOption[]) => void) => {
setSearchTerm(inputValue);
callback(componentOptions);
}, 200),
[componentOptions]
);

return (
<AsyncSelect
cacheOptions
className="w-full h-10"
isLoading={isLoading}
loadOptions={debouncedLoadOptions}
menuPortalTarget={document.body}
menuPosition="fixed"
onChange={onChange}
placeholder={placeholder}
styles={{
menu: (provided) => ({
...provided,
maxHeight: 200,
overflowY: "auto",
}),
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
}}
value={value}
/>
);
};

export default AsyncComponentSelect;
Loading

0 comments on commit f270d0c

Please sign in to comment.