diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2ba986f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/client/src/components/DogList.svelte b/client/src/components/DogList.svelte index b28bc36..4c4aed5 100644 --- a/client/src/components/DogList.svelte +++ b/client/src/components/DogList.svelte @@ -5,16 +5,47 @@ id: number; name: string; breed: string; + status: string; } export let dogs: Dog[] = []; let loading = true; let error: string | null = null; + let breeds: string[] = []; + let selectedBreed = ''; + let availableOnly = false; + + const fetchBreeds = async () => { + try { + const response = await fetch('/api/breeds'); + if(response.ok) { + breeds = await response.json(); + } else { + console.error('Failed to fetch breeds:', response.status); + } + } catch (err) { + console.error('Error fetching breeds:', err); + } + }; const fetchDogs = async () => { loading = true; try { - const response = await fetch('/api/dogs'); + let url = '/api/dogs'; + const params = new URLSearchParams(); + + if (selectedBreed) { + params.append('breed', selectedBreed); + } + if (availableOnly) { + params.append('available', 'true'); + } + + if (params.toString()) { + url += '?' + params.toString(); + } + + const response = await fetch(url); if(response.ok) { dogs = await response.json(); } else { @@ -28,13 +59,53 @@ }; onMount(() => { + fetchBreeds(); fetchDogs(); }); + + // Reactive statement to refetch dogs when filters change + $: if (selectedBreed !== undefined && availableOnly !== undefined) { + fetchDogs(); + }

Available Dogs

+ +
+
+ +
+ + +
+ + +
+ +
+
+
+ {#if loading}
@@ -72,7 +143,16 @@

{dog.name}

-

{dog.breed}

+

{dog.breed}

+
+ {#if dog.status === 'AVAILABLE'} + Available + {:else if dog.status === 'PENDING'} + Pending Adoption + {:else if dog.status === 'ADOPTED'} + Adopted + {/if} +
View details diff --git a/server/app.py b/server/app.py index b6331cc..576850e 100644 --- a/server/app.py +++ b/server/app.py @@ -1,7 +1,7 @@ import os from typing import Dict, List, Any, Optional -from flask import Flask, jsonify, Response -from models import init_db, db, Dog, Breed +from flask import Flask, jsonify, Response, request +from server.models import init_db, db, Dog, Breed # Get the server directory path base_dir: str = os.path.abspath(os.path.dirname(__file__)) @@ -15,12 +15,26 @@ @app.route('/api/dogs', methods=['GET']) def get_dogs() -> Response: + # Get query parameters for filtering + breed_filter = request.args.get('breed') + available_only = request.args.get('available') == 'true' + query = db.session.query( Dog.id, Dog.name, - Breed.name.label('breed') + Breed.name.label('breed'), + Dog.status ).join(Breed, Dog.breed_id == Breed.id) + # Apply breed filter if provided + if breed_filter: + query = query.filter(Breed.name == breed_filter) + + # Apply availability filter if requested + if available_only: + from server.models.dog import AdoptionStatus + query = query.filter(Dog.status == AdoptionStatus.AVAILABLE) + dogs_query = query.all() # Convert the result to a list of dictionaries @@ -28,7 +42,8 @@ def get_dogs() -> Response: { 'id': dog.id, 'name': dog.name, - 'breed': dog.breed + 'breed': dog.breed, + 'status': dog.status.name if dog.status else 'UNKNOWN' } for dog in dogs_query ] @@ -65,7 +80,14 @@ def get_dog(id: int) -> tuple[Response, int] | Response: return jsonify(dog) -## HERE +@app.route('/api/breeds', methods=['GET']) +def get_breeds() -> Response: + breeds_query = db.session.query(Breed.name).distinct().all() + + # Convert the result to a list of breed names + breeds_list: List[str] = [breed.name for breed in breeds_query] + + return jsonify(breeds_list) if __name__ == '__main__': app.run(debug=True, port=5100) # Port 5100 to avoid macOS conflicts \ No newline at end of file diff --git a/server/test_app.py b/server/test_app.py index adbdf5f..f878ba4 100644 --- a/server/test_app.py +++ b/server/test_app.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import patch, MagicMock import json -from app import app # Changed from relative import to absolute import +from server.app import app # Changed to absolute import for running from project root # filepath: server/test_app.py class TestApp(unittest.TestCase): @@ -12,13 +12,15 @@ def setUp(self): # Turn off database initialization for tests app.config['TESTING'] = True - def _create_mock_dog(self, dog_id, name, breed): + def _create_mock_dog(self, dog_id, name, breed, status='AVAILABLE'): """Helper method to create a mock dog with standard attributes""" - dog = MagicMock(spec=['to_dict', 'id', 'name', 'breed']) + dog = MagicMock(spec=['to_dict', 'id', 'name', 'breed', 'status']) dog.id = dog_id dog.name = name dog.breed = breed - dog.to_dict.return_value = {'id': dog_id, 'name': name, 'breed': breed} + dog.status = MagicMock() + dog.status.name = status + dog.to_dict.return_value = {'id': dog_id, 'name': name, 'breed': breed, 'status': status} return dog def _setup_query_mock(self, mock_query, dogs): @@ -26,10 +28,17 @@ def _setup_query_mock(self, mock_query, dogs): mock_query_instance = MagicMock() mock_query.return_value = mock_query_instance mock_query_instance.join.return_value = mock_query_instance + mock_query_instance.filter.return_value = mock_query_instance mock_query_instance.all.return_value = dogs return mock_query_instance - @patch('app.db.session.query') + def _create_mock_breed(self, name): + """Helper method to create a mock breed""" + breed = MagicMock(spec=['name']) + breed.name = name + return breed + + @patch('server.app.db.session.query') def test_get_dogs_success(self, mock_query): """Test successful retrieval of multiple dogs""" # Arrange @@ -52,16 +61,18 @@ def test_get_dogs_success(self, mock_query): self.assertEqual(data[0]['id'], 1) self.assertEqual(data[0]['name'], "Buddy") self.assertEqual(data[0]['breed'], "Labrador") + self.assertEqual(data[0]['status'], "AVAILABLE") # Verify second dog self.assertEqual(data[1]['id'], 2) self.assertEqual(data[1]['name'], "Max") self.assertEqual(data[1]['breed'], "German Shepherd") + self.assertEqual(data[1]['status'], "AVAILABLE") # Verify query was called mock_query.assert_called_once() - @patch('app.db.session.query') + @patch('server.app.db.session.query') def test_get_dogs_empty(self, mock_query): """Test retrieval when no dogs are available""" # Arrange @@ -75,7 +86,7 @@ def test_get_dogs_empty(self, mock_query): data = json.loads(response.data) self.assertEqual(data, []) - @patch('app.db.session.query') + @patch('server.app.db.session.query') def test_get_dogs_structure(self, mock_query): """Test the response structure for a single dog""" # Arrange @@ -89,7 +100,98 @@ def test_get_dogs_structure(self, mock_query): data = json.loads(response.data) self.assertTrue(isinstance(data, list)) self.assertEqual(len(data), 1) - self.assertEqual(set(data[0].keys()), {'id', 'name', 'breed'}) + self.assertEqual(set(data[0].keys()), {'id', 'name', 'breed', 'status'}) + + @patch('server.app.db.session.query') + def test_get_dogs_breed_filter(self, mock_query): + """Test filtering dogs by breed""" + # Arrange + beagle1 = self._create_mock_dog(1, "Buddy", "Beagle") + beagle2 = self._create_mock_dog(2, "Max", "Beagle") + self._setup_query_mock(mock_query, [beagle1, beagle2]) + + # Act + response = self.app.get('/api/dogs?breed=Beagle') + + # Assert + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertEqual(len(data), 2) + for dog in data: + self.assertEqual(dog['breed'], 'Beagle') + + @patch('server.app.db.session.query') + def test_get_dogs_available_filter(self, mock_query): + """Test filtering dogs by availability""" + # Arrange + available_dog = self._create_mock_dog(1, "Buddy", "Labrador", "AVAILABLE") + self._setup_query_mock(mock_query, [available_dog]) + + # Act + response = self.app.get('/api/dogs?available=true') + + # Assert + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertEqual(len(data), 1) + self.assertEqual(data[0]['status'], 'AVAILABLE') + + @patch('server.app.db.session.query') + def test_get_dogs_combined_filters(self, mock_query): + """Test filtering dogs by both breed and availability""" + # Arrange + available_beagle = self._create_mock_dog(1, "Buddy", "Beagle", "AVAILABLE") + self._setup_query_mock(mock_query, [available_beagle]) + + # Act + response = self.app.get('/api/dogs?breed=Beagle&available=true') + + # Assert + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertEqual(len(data), 1) + self.assertEqual(data[0]['breed'], 'Beagle') + self.assertEqual(data[0]['status'], 'AVAILABLE') + + @patch('server.app.db.session.query') + def test_get_breeds_success(self, mock_query): + """Test successful retrieval of breed list""" + # Arrange + breed1 = self._create_mock_breed("Labrador") + breed2 = self._create_mock_breed("Beagle") + mock_breeds = [breed1, breed2] + + mock_query_instance = MagicMock() + mock_query.return_value = mock_query_instance + mock_query_instance.distinct.return_value = mock_query_instance + mock_query_instance.all.return_value = mock_breeds + + # Act + response = self.app.get('/api/breeds') + + # Assert + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertEqual(len(data), 2) + self.assertIn("Labrador", data) + self.assertIn("Beagle", data) + + @patch('server.app.db.session.query') + def test_get_breeds_empty(self, mock_query): + """Test retrieval when no breeds are available""" + # Arrange + mock_query_instance = MagicMock() + mock_query.return_value = mock_query_instance + mock_query_instance.distinct.return_value = mock_query_instance + mock_query_instance.all.return_value = [] + + # Act + response = self.app.get('/api/breeds') + + # Assert + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertEqual(data, []) if __name__ == '__main__':