From adb398cd6cab4884a90e26ffba50f93b39467d43 Mon Sep 17 00:00:00 2001 From: prince rusweka Date: Thu, 17 Apr 2025 22:43:11 -0400 Subject: [PATCH 1/3] Saving progress before switching to main --- backend/db.py | 43 ++++ backend/main.py | 506 +++++++++++++++++++----------------------------- 2 files changed, 241 insertions(+), 308 deletions(-) create mode 100644 backend/db.py diff --git a/backend/db.py b/backend/db.py new file mode 100644 index 0000000..93f618f --- /dev/null +++ b/backend/db.py @@ -0,0 +1,43 @@ +import sqlite3 +import os + +def init_db(): + """Initialize the database, creating tables and inserting initial data.""" + db_path = os.path.join(os.path.dirname(__file__), 'animal_shelter.db') + + if os.path.exists(db_path): + print("Database already initialized.") + return + + # Connect to SQLite database (it will create the file if it doesn't exist) + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Create tables + cursor.execute('''CREATE TABLE IF NOT EXISTS animals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + species TEXT NOT NULL, + breed TEXT, + age INTEGER, + personality TEXT, + image_path TEXT, + adoption_status TEXT DEFAULT 'Available' + )''') + + cursor.execute('''CREATE TABLE IF NOT EXISTS adopters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + email TEXT UNIQUE, + join_date TEXT DEFAULT CURRENT_TIMESTAMP + )''') + + # Insert initial data (e.g., a test animal) + cursor.execute('''INSERT INTO animals (name, species, breed, age, personality, image_path) + VALUES (?, ?, ?, ?, ?, ?)''', + ('TestDog', 'Dog', 'Mixed', 2, 'Friendly', '/images/test.jpg')) + + conn.commit() + conn.close() + print("Database initialized and initial data inserted.") diff --git a/backend/main.py b/backend/main.py index 183a3af..ad707dd 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,342 +1,232 @@ -from flask import Flask, jsonify, request, send_from_directory -from flask_cors import CORS +from flask import Flask, jsonify, request from flasgger import Swagger +import sqlite3 import os -from pets import add_pet, update_pet, delete_pet, search_pets, reset_pets +# Initialize Flask app app = Flask(__name__) -app.secret_key = "test_secret_key" -CORS(app) -# Set up Swagger documentation -swagger_config = { - "title": "Animal Shelter API", - "version": "1.0.0", - "description": "API for animal adoption management" -} -# swagger = Swagger(app, config=swagger_config) - -# Mock data for testing -ANIMALS = [ - { - "id": 1, - "name": "Luna", - "species": "Dog", - "breed": "Labrador Mix", - "age": 2, - "personality": "Playful and energetic", - "image_path": "/images/luna.jpg", - "adoption_status": "Available" - }, - { - "id": 2, - "name": "Oliver", - "species": "Cat", - "breed": "Tabby", - "age": 4, - "personality": "Independent but affectionate", - "image_path": "/images/oliver.jpg", - "adoption_status": "Available" - }, - { - "id": 3, - "name": "Max", - "species": "Dog", - "breed": "German Shepherd", - "age": 3, - "personality": "Loyal and intelligent", - "image_path": "/images/max.jpg", - "adoption_status": "Available" - } -] - -USERS = [ - { - "id": 1, - "username": "admin", - # In a real app, this would be hashed, but for testing it's plain text - "password": "admin123", - "email": "admin@example.com" - } -] +# Initialize Swagger (Flasgger) +Swagger(app) + +# SQLite database file +DATABASE = 'animal_shelter.db' + +# Helper function to get a database connection +def get_db(): + conn = sqlite3.connect(DATABASE) + conn.row_factory = sqlite3.Row # Allows row access by column name + return conn + +# Database initialization function to create tables and initial data +def init_db(): + """Create tables and add initial data if they don't exist.""" + if not os.path.exists(DATABASE): + conn = get_db() + cursor = conn.cursor() + + # Create the animals table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS animals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + species TEXT NOT NULL, + breed TEXT, + age INTEGER, + personality TEXT, + image_path TEXT, + adoption_status TEXT DEFAULT 'Available' + ) + ''') + + # Insert initial pet data for testing + pets = [ + ('Buddy', 'Dog', 'Golden Retriever', 3, 'Friendly', '/images/buddy.jpg', 'Available'), + ('Whiskers', 'Cat', 'Siamese', 2, 'Independent', '/images/whiskers.jpg', 'Available'), + ('Rex', 'Dog', 'German Shepherd', 4, 'Protective', '/images/rex.jpg', 'Available'), + ('Max', 'Dog', 'Labrador Retriever', 5, 'Playful', '/images/max.jpg', 'Adopted'), + ('Mittens', 'Cat', 'Persian', 1, 'Lazy', '/images/mittens.jpg', 'Available'), + ('Bella', 'Dog', 'Bulldog', 2, 'Gentle', '/images/bella.jpg', 'Available') + ] + + # Insert data into the animals table + cursor.executemany(''' + INSERT INTO animals (name, species, breed, age, personality, image_path, adoption_status) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', pets) + + conn.commit() + conn.close() -# User registration endpoint -@app.route('/api/register', methods=['POST']) -def register_user(): - """ - Register a new adopter +# API Routes +@app.route('/api/pets', methods=['GET']) +def get_pets(): + """Retrieve all pets --- - parameters: - - in: body - name: user - description: User registration details - schema: - type: object - required: - - username - - password - - email - properties: - username: - type: string - password: - type: string - email: - type: string responses: - 201: - description: User registered successfully - 400: - description: Missing required fields - 409: - description: Username already exists + 200: + description: A list of pets + schema: + type: array + items: + type: object + properties: + id: + type: integer + name: + type: string + species: + type: string + breed: + type: string + age: + type: integer + personality: + type: string + image_path: + type: string + adoption_status: + type: string """ - data = request.get_json() - username = data.get('username') - password = data.get('password') - email = data.get('email') + try: + conn = get_db() + cursor = conn.cursor() + cursor.execute("SELECT * FROM animals") + pets = cursor.fetchall() - if not username or not password or not email: - return jsonify({'error': 'All fields are required'}), 400 + # Convert rows to a list of dictionaries + pet_list = [dict(pet) for pet in pets] + conn.close() - # Check if username already exists - if any(user['username'] == username for user in USERS): - return jsonify({'error': 'Username already exists'}), 409 + return jsonify(pet_list), 200 + except Exception as e: + print(f"An error occurred: {e}") + return jsonify({'error': 'Internal Server Error'}), 500 - # Check if email already exists - if any(user['email'] == email for user in USERS): - return jsonify({'error': 'Email already registered'}), 409 +@app.route('/', methods=['GET']) +def home(): + return jsonify({'message': 'Welcome to the Pet API! Visit /apidocs for the Swagger documentation.'}) - # Create new user - new_user = { - "id": len(USERS) + 1, - "username": username, - "password": password, # In real app, would be hashed - "email": email - } - USERS.append(new_user) - - return jsonify({'message': 'Registration successful!', 'user_id': new_user['id']}), 201 -# User login endpoint -@app.route('/api/login', methods=['POST']) -def authenticate_user(): - """ - User login + +@app.route('/api/pets/', methods=['GET']) +def get_pet(pet_id): + """Retrieve a single pet by ID --- parameters: - - in: body - name: credentials - description: Login credentials - schema: - type: object - required: - - username - - password - properties: - username: - type: string - password: - type: string + - name: pet_id + in: path + type: integer + required: true + description: The unique ID of the pet responses: 200: - description: Login successful - 401: - description: Invalid credentials + description: A pet + schema: + type: object + properties: + id: + type: integer + name: + type: string + species: + type: string + breed: + type: string + age: + type: integer + personality: + type: string + image_path: + type: string + adoption_status: + type: string + 404: + description: Pet not found + 500: + description: Internal Server Error """ - data = request.get_json() - username = data.get('username') - password = data.get('password') + try: + conn = get_db() + cursor = conn.cursor() + cursor.execute("SELECT * FROM animals WHERE id = ?", (pet_id,)) + pet = cursor.fetchone() - if not username or not password: - return jsonify({'error': 'Username and password required'}), 400 + if pet is None: + return jsonify({'error': 'Pet not found'}), 404 - # Find user by username - user = next((user for user in USERS if user['username'] == username), None) - - if not user or user['password'] != password: - return jsonify({'error': 'Invalid username or password'}), 401 - - return jsonify({ - 'message': 'Login successful', - 'user_id': user['id'], - 'username': user['username'], - 'email': user['email'] - }), 200 + # Convert the SQLite row object to a dictionary + pet_dict = {key: pet[key] for key in pet.keys()} + conn.close() -# Animal routes -@app.route('/api/animals', methods=['GET']) -def list_animals(): - """ - Get all available animals - --- - responses: - 200: - description: List of available animals - """ - # Filter only available animals - available_animals = [animal for animal in ANIMALS if animal['adoption_status'] == 'Available'] - return jsonify(available_animals), 200 - -@app.route('/api/animals/', methods=['GET']) -def animal_details(animal_id): - """ - Get details for a specific animal - --- - parameters: - - name: animal_id - in: path - type: integer - required: true - responses: - 200: - description: Animal details - 404: - description: Animal not found - """ - animal = next((animal for animal in ANIMALS if animal['id'] == animal_id), None) - - if not animal: - return jsonify({'error': 'Animal not found'}), 404 - - return jsonify(animal), 200 + return jsonify(pet_dict), 200 + except Exception as e: + print(f"An error occurred: {e}") + return jsonify({'error': 'Internal Server Error'}), 500 -@app.route('/api/animals//adopt', methods=['POST']) -def adopt_animal(animal_id): - """ - Adopt an animal +@app.route('/api/pets', methods=['POST']) +def add_pet(): + """Add a new pet --- parameters: - - name: animal_id - in: path - type: integer - required: true - - in: body - name: adoption - schema: - type: object - required: - - user_id - properties: - user_id: - type: integer + - name: pet + in: body + required: true + schema: + type: object + properties: + name: + type: string + species: + type: string + breed: + type: string + age: + type: integer + personality: + type: string + image_path: + type: string + adoption_status: + type: string responses: - 200: - description: Adoption successful - 404: - description: Animal not found + 201: + description: Pet created successfully 400: - description: Animal not available for adoption + description: Invalid pet data + 500: + description: Internal Server Error """ - data = request.get_json() - user_id = data.get('user_id') - - if not user_id: - return jsonify({'error': 'User ID required'}), 400 - - # Find the animal - animal_index = next((i for i, animal in enumerate(ANIMALS) if animal['id'] == animal_id), None) - - if animal_index is None: - return jsonify({'error': 'Animal not found'}), 404 - - # Check if animal is available - if ANIMALS[animal_index]['adoption_status'] != 'Available': - return jsonify({'error': 'Animal not available for adoption'}), 400 - - # Update status to adopted - ANIMALS[animal_index]['adoption_status'] = 'Adopted' - - return jsonify({ - 'message': 'Adoption successful', - 'animal': ANIMALS[animal_index] - }), 200 + try: + pet_data = request.get_json() -@app.route('/api/animals/search', methods=['GET']) -def search_animals(): - """ - Search for animals with filters - --- - parameters: - - name: species - in: query - type: string - required: false - - name: min_age - in: query - type: integer - required: false - - name: max_age - in: query - type: integer - required: false - responses: - 200: - description: Search results - """ - species = request.args.get('species') - min_age = request.args.get('min_age') - max_age = request.args.get('max_age') - - results = ANIMALS.copy() - - # Apply filters - if species: - results = [animal for animal in results if animal['species'].lower() == species.lower()] + # Validate required fields + required_fields = ['name', 'species', 'breed', 'age', 'personality'] + for field in required_fields: + if field not in pet_data: + return jsonify({'error': f'Missing required field: {field}'}), 400 + + conn = get_db() + cursor = conn.cursor() - if min_age: - results = [animal for animal in results if animal['age'] >= int(min_age)] + cursor.execute(''' + INSERT INTO animals (name, species, breed, age, personality, image_path, adoption_status) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', (pet_data['name'], pet_data['species'], pet_data['breed'], pet_data['age'], + pet_data['personality'], pet_data.get('image_path', '/images/default.jpg'), 'Available')) - if max_age: - results = [animal for animal in results if animal['age'] <= int(max_age)] - - return jsonify(results), 200 - -# Serve images of animals -@app.route('/images/') -def serve_image(filename): - return send_from_directory(os.path.join(app.root_path, 'static/images'), filename) - -@app.route('/') -def homepage(): - return """ -

Animal Shelter API

-

Welcome to our animal adoption platform!

- View API Documentation - """ -# Add a new pet -@app.route('/api/pets', methods=['POST']) -def add_new_pet(): - pet_data = request.get_json() - return add_pet(pet_data) + conn.commit() + conn.close() -# Update an existing pet -@app.route('/api/pets/', methods=['PUT']) -def update_existing_pet(pet_id): - pet_data = request.get_json() - return update_pet(pet_id, pet_data) + return jsonify({'message': 'Pet created successfully'}), 201 + except Exception as e: + print(f"An error occurred: {e}") + return jsonify({'error': 'Internal Server Error'}), 500 -# Delete a pet -@app.route('/api/pets/', methods=['DELETE']) -def remove_pet(pet_id): - return delete_pet(pet_id) +# Initialize database on first run +@app.before_first_request +def before_first_request(): + init_db() -# Search for pets -@app.route('/api/pets/search', methods=['GET']) -def search_for_pets(): - return search_pets(request.args) - -# Reset pets data (for testing) -@app.route('/api/pets/reset', methods=['POST']) -def reset_pets_data(): - """ - Reset the pets data to initial state. - --- - responses: - 200: - description: Pets data reset successfully - """ - reset_pets() - return jsonify({'message': 'Pets data reset successfully'}), 200 -if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file +if __name__ == "__main__": + app.run(debug=True) From f1973c49a42b733b352c4816fb2786ad04a5e7ca Mon Sep 17 00:00:00 2001 From: prince rusweka Date: Thu, 17 Apr 2025 22:44:21 -0400 Subject: [PATCH 2/3] Update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index a538de6..1cf1a09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /backend/animal_shelter.db +/backend/test_animal_shelter.db /frontend/node_modules +# ignore sqlite db file +animal_shelter.db From f696da3e0f80ab288e6c2431a5f8b6e4e367f99d Mon Sep 17 00:00:00 2001 From: Sean Dombrofski Date: Thu, 17 Apr 2025 23:03:47 -0400 Subject: [PATCH 3/3] adding test-main.py and merging prince's new db.py file --- backend/test-main.py | 215 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 backend/test-main.py diff --git a/backend/test-main.py b/backend/test-main.py new file mode 100644 index 0000000..3784523 --- /dev/null +++ b/backend/test-main.py @@ -0,0 +1,215 @@ +import pytest +import json +from main import app, ANIMALS, USERS + +@pytest.fixture +def client(): + app.config['TESTING'] = True + with app.test_client() as client: + # Reset data before each test + global ANIMALS, USERS + ANIMALS = [ + { + "id": 1, + "name": "Luna", + "species": "Dog", + "breed": "Labrador Mix", + "age": 2, + "personality": "Playful and energetic", + "image_path": "/images/luna.jpg", + "adoption_status": "Available" + }, + { + "id": 2, + "name": "Oliver", + "species": "Cat", + "breed": "Tabby", + "age": 4, + "personality": "Independent but affectionate", + "image_path": "/images/oliver.jpg", + "adoption_status": "Available" + }, + { + "id": 3, + "name": "Max", + "species": "Dog", + "breed": "German Shepherd", + "age": 3, + "personality": "Loyal and intelligent", + "image_path": "/images/max.jpg", + "adoption_status": "Available" + } + ] + USERS = [ + { + "id": 1, + "username": "admin", + "password": "admin123", + "email": "admin@example.com" + } + ] + yield client + +def test_homepage(client): + """Test that the homepage returns a 200 status code.""" + response = client.get('/') + assert response.status_code == 200 + assert b'Animal Shelter API' in response.data + +def test_register_user(client): + """Test user registration with valid data.""" + response = client.post('/api/register', + json={ + 'username': 'testuser', + 'password': 'password123', + 'email': 'test@example.com' + }) + assert response.status_code == 201 + data = json.loads(response.data) + assert 'Registration successful' in data['message'] + assert data['user_id'] == 2 # Should be the second user + +def test_register_duplicate_username(client): + """Test registration with a duplicate username.""" + # First registration + client.post('/api/register', + json={ + 'username': 'testuser', + 'password': 'password123', + 'email': 'test@example.com' + }) + + # Attempt to register with same username + response = client.post('/api/register', + json={ + 'username': 'testuser', + 'password': 'different', + 'email': 'different@example.com' + }) + assert response.status_code == 409 + data = json.loads(response.data) + assert 'Username already exists' in data['error'] + +def test_register_missing_fields(client): + """Test registration with missing fields.""" + response = client.post('/api/register', + json={ + 'username': 'testuser', + # Missing password and email + }) + assert response.status_code == 400 + data = json.loads(response.data) + assert 'All fields are required' in data['error'] + +def test_login_success(client): + """Test successful login.""" + response = client.post('/api/login', + json={ + 'username': 'admin', + 'password': 'admin123' + }) + assert response.status_code == 200 + data = json.loads(response.data) + assert 'Login successful' in data['message'] + assert data['username'] == 'admin' + +def test_login_incorrect_password(client): + """Test login with incorrect password.""" + response = client.post('/api/login', + json={ + 'username': 'admin', + 'password': 'wrong_password' + }) + assert response.status_code == 401 + data = json.loads(response.data) + assert 'Invalid username or password' in data['error'] + +def test_login_nonexistent_user(client): + """Test login with a username that doesn't exist.""" + response = client.post('/api/login', + json={ + 'username': 'nonexistent', + 'password': 'anything' + }) + assert response.status_code == 401 + data = json.loads(response.data) + assert 'Invalid username or password' in data['error'] + +def test_get_animals(client): + """Test getting all available animals.""" + response = client.get('/api/animals') + assert response.status_code == 200 + data = json.loads(response.data) + assert len(data) == 3 + assert data[0]['name'] == 'Luna' + assert data[1]['name'] == 'Oliver' + assert data[2]['name'] == 'Max' + +def test_get_animal_details(client): + """Test getting details for a specific animal.""" + response = client.get('/api/animals/2') + assert response.status_code == 200 + data = json.loads(response.data) + assert data['name'] == 'Oliver' + assert data['species'] == 'Cat' + assert data['breed'] == 'Tabby' + +def test_get_nonexistent_animal(client): + """Test getting details for an animal that doesn't exist.""" + response = client.get('/api/animals/999') + assert response.status_code == 404 + data = json.loads(response.data) + assert 'Animal not found' in data['error'] + +def test_adopt_animal(client): + """Test adopting an animal.""" + response = client.post('/api/animals/1/adopt', + json={ + 'user_id': 1 + }) + assert response.status_code == 200 + data = json.loads(response.data) + assert 'Adoption successful' in data['message'] + assert data['animal']['adoption_status'] == 'Adopted' + + # Check that the animal is no longer available + response = client.get('/api/animals') + assert response.status_code == 200 + data = json.loads(response.data) + assert len(data) == 2 # Only 2 animals should now be available + +def test_adopt_already_adopted_animal(client): + """Test trying to adopt an animal that's already adopted.""" + # First adoption + client.post('/api/animals/1/adopt', json={'user_id': 1}) + + # Attempt second adoption + response = client.post('/api/animals/1/adopt', json={'user_id': 1}) + assert response.status_code == 400 + data = json.loads(response.data) + assert 'Animal not available for adoption' in data['error'] + +def test_animal_search(client): + """Test searching for animals with filters.""" + # Search by species + response = client.get('/api/animals/search?species=Dog') + assert response.status_code == 200 + data = json.loads(response.data) + assert len(data) == 2 + assert all(animal['species'] == 'Dog' for animal in data) + + # Search by age range + response = client.get('/api/animals/search?min_age=3&max_age=4') + assert response.status_code == 200 + data = json.loads(response.data) + assert len(data) == 2 + assert all(3 <= animal['age'] <= 4 for animal in data) + + # Combined search + response = client.get('/api/animals/search?species=Cat&min_age=3') + assert response.status_code == 200 + data = json.loads(response.data) + assert len(data) == 1 + assert data[0]['name'] == 'Oliver' + assert data[0]['species'] == 'Cat' + assert data[0]['age'] == 4