diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..788d6b186a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +hbnbv3_venv/ +hbnb_venv/ +*.pyc diff --git a/AUTHORS b/AUTHORS index 64b26acdc14..587b943fc30 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,7 @@ # This file lists all individuals having contributed content to the repository. +Lenson Mutugi Jennifer Huang <133@holbertonschool.com> Alexa Orrico <210@holbertonschool.com> Joann Vuong <130@holbertonschool.com> diff --git a/README.md b/README.md index f1d72de6355..d92ddd9dadc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ -# AirBnB Clone - The Console -The console is the first segment of the AirBnB project at Holberton School that will collectively cover fundamental concepts of higher level programming. The goal of AirBnB project is to eventually deploy our server a simple copy of the AirBnB Website(HBnB). A command interpreter is created in this segment to manage objects for the AirBnB(HBnB) website. +# AirBnB Clone + +The goal of AirBnB project is to eventually deploy our server a simple copy of the AirBnB Website(HBnB). +A command interpreter is created to manage objects for the AirBnB(HBnB) website. +A fron-end is built using HTML and CSS +A database is created and mapped to objects using sqlalchemy +Flask is used as the first method to expose the backend to the frontend +A RESTful API is built as a second methos to expose the backend to the frontend #### Functionalities of this command interpreter: * Create a new object (ex: a new User or a new Place) @@ -154,6 +160,7 @@ EOF all create destroy help quit show update No known bugs at this time. ## Authors +Lenson Mutugi - [Github](https://github.com/lensonlovescode) / [Twitter](https://github.com/alexaorrico) Alexa Orrico - [Github](https://github.com/alexaorrico) / [Twitter](https://twitter.com/alexa_orrico) Jennifer Huang - [Github](https://github.com/jhuang10123) / [Twitter](https://twitter.com/earthtojhuang) diff --git a/api/README.md b/api/README.md new file mode 100644 index 00000000000..d2048f96ac5 --- /dev/null +++ b/api/README.md @@ -0,0 +1 @@ +This Directory contins restful api work for the AirBnB clone project diff --git a/api/__init__.py b/api/__init__.py new file mode 100755 index 00000000000..e69de29bb2d diff --git a/api/v1/README.md b/api/v1/README.md new file mode 100644 index 00000000000..d3cd075259b --- /dev/null +++ b/api/v1/README.md @@ -0,0 +1,2 @@ +This Directory contins restful api work for the AirBnB clone project +An app that comprises of flask blueprints for managing AirBnB assets diff --git a/api/v1/__init__.py b/api/v1/__init__.py new file mode 100755 index 00000000000..e69de29bb2d diff --git a/api/v1/app.py b/api/v1/app.py new file mode 100755 index 00000000000..bb9ce2dce89 --- /dev/null +++ b/api/v1/app.py @@ -0,0 +1,36 @@ +#!/usr/bin/python3 +""" +Creates a flask app comprising many flask blueprints +as part of api building +""" +from flask import Flask, jsonify +from models import storage +from api.v1.views import app_views +from os import getenv + + +app = Flask(__name__) +app.register_blueprint(app_views) + + +@app.teardown_appcontext +def close_db_session(exception=None): + """ + Closes the database session + """ + storage.close() + + +@app.errorhandler(404) +def not_found(error): + """ + Handle 404 errors and return JSON response + """ + return jsonify({"error": "Not found"}), 404 + + +if __name__ == '__main__': + + host = getenv('HBNB_API_HOST', default='0.0.0.0') + port = int(getenv('HBNB_API_PORT', default=5000)) + app.run(host=host, port=port, threaded=True) diff --git a/api/v1/views/.gitignore b/api/v1/views/.gitignore new file mode 100644 index 00000000000..29aab1e7223 --- /dev/null +++ b/api/v1/views/.gitignore @@ -0,0 +1,2 @@ +*~ +__pycache__/ diff --git a/api/v1/views/README.md b/api/v1/views/README.md new file mode 100644 index 00000000000..d2048f96ac5 --- /dev/null +++ b/api/v1/views/README.md @@ -0,0 +1 @@ +This Directory contins restful api work for the AirBnB clone project diff --git a/api/v1/views/__init__.py b/api/v1/views/__init__.py new file mode 100755 index 00000000000..98053b10f0e --- /dev/null +++ b/api/v1/views/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/python3 +""" +Contains a python script that creates a flask blueprint +named app_views as part of a larger flask application +contains the folllowing views: index, states, cities +amenities, uusers and places +""" +from flask import Blueprint + +app_views = Blueprint('app_views', __name__, url_prefix='/api/v1') + +from api.v1.views.index import * +from api.v1.views.states import * +from api.v1.views.cities import * +from api.v1.views.users import * +from api.v1.views.places import * +from api.v1.views.places_reviews import * diff --git a/api/v1/views/amenities.py b/api/v1/views/amenities.py new file mode 100755 index 00000000000..9bb2ec67f2f --- /dev/null +++ b/api/v1/views/amenities.py @@ -0,0 +1,100 @@ +#!/usr/bin/python3 +""" +Creates API endpoints for acessing, creating, deleting and +updating Amenity resources, it uses the app_views blueprint +""" +from api.v1.views import app_views +from models import storage +from models.amenity import Amenity +from flask import jsonify, request, abort + + +@app_views.route('/amenities', strict_slashes=False, + methods=['GET']) +def get_amenities(): + """ + Retrieves all amenity objects and returns the JSON + representation of the dictionary + """ + amenities = [] + all_amenities = storage.all(Amenity) + + for amenity in all_amenities.values(): + amenities.append(amenity.to_dict()) + return (jsonify(amenities)) + + +@app_views.route('/amenities/', strict_slashes=False, + methods=['GET']) +def get_an_amenity(amenity_id): + """ + Retrieves a specific amenity based on it's id + 404 error if the amenity does not exist + """ + amenity = storage.get(Amenity, amenity_id) + + if amenity is not None: + return (jsonify(amenity.to_dict())) + abort(404) + + +@app_views.route('/amenities/', strict_slashes=False, + methods=['DELETE']) +def delete_amenity(amenity_id): + """ + Deletes an amenity based on it's id + Returns an empty dictionary plus 200ok status on success + or 404 error page on failure + """ + amenity = storage.get(Amenity, amenity_id) + if amenity is None: + abort(404) + storage.delete(amenity) + storage.save() + + return (jsonify({})), 200 + + +@app_views.route('/amenities', strict_slashes=False, methods=['POST']) +def create_amenity(): + """ + Creates an amenity and returns the dictionary representation of the + amenity, or an error dictionary + """ + data = request.get_json() + if not isinstance(data, dict): + return (jsonify({'error': 'Not a JSON'})) + if 'name' not in data.keys(): + return (jsonify({'error': 'Missing name'})) + + name = data.get('name') + amenity = Amenity(name=f'{data.get('name')}') + storage.new(amenity) + storage.save() + return (jsonify(amenity.to_dict())), 201 + + +@app_views.route('/amenities/', strict_slashes=False, + methods=['PUT']) +def update_amenity(amenity_id): + """ + Updates an amenity based on the id, returns a code 201 + on success or an error dictionary + """ + data = request.get_json() + + if not isinstance(data, dict): + return (jsonify({'error': 'Not a JSON'})) + + amenity = storage.get(Amenity, amenity_id) + + if amenity is None: + abort(404) + + exclude = ['id', 'created_at', 'updated_at'] + for k, v in data.items(): + if k not in exclude: + setattr(amenity, k, v,) + + amenity.save() + return (jsonify(amenity.to_dict())), 200 diff --git a/api/v1/views/cities.py b/api/v1/views/cities.py new file mode 100755 index 00000000000..9c9dfbf965e --- /dev/null +++ b/api/v1/views/cities.py @@ -0,0 +1,103 @@ +#!/usr/bin/python3 +""" +Cities API view for handling RESTful actions +""" + +from flask import jsonify, request, abort +from api.v1.views import app_views +from models import storage +from models.city import City +from models.state import State + + +@app_views.route( + '/states//cities', + methods=['GET'], + strict_slashes=False + ) +def get_cities(state_id): + """ + Retrieve all cities of a given state + """ + state = storage.get(State, state_id) + if not state: + abort(404) + return jsonify([city.to_dict() for city in state.cities]) + + +@app_views.route('/cities/', methods=['GET'], strict_slashes=False) +def get_city(city_id): + """ + Retrieve a single city identified by ID + """ + city = storage.get(City, city_id) + if not city: + abort(404) + return jsonify(city.to_dict()) + + +@app_views.route( + '/states//cities', + methods=['POST'], + strict_slashes=False + ) +def create_city(state_id): + """ + Create a new City in a given State + """ + state = storage.get(State, state_id) + if not state: + abort(404) + + if request.content_type != 'application/json': + return jsonify({"error": "Not a JSON"}), 400 + + data = request.get_json() + if not data: + abort(400, description="Not a JSON") + + if 'name' not in data: + abort(400, description="Missing name") + + new_city = City(name=request.json['name'], state_id=state_id) + new_city.save() + return jsonify(new_city.to_dict()), 201 + + +@app_views.route('/cities/', methods=['PUT'], strict_slashes=False) +def update_city(city_id): + """ + Update a City object idenfified by valid city id + """ + city = storage.get(City, city_id) + if not city: + abort(404) + + if request.content_type != 'application/json': + return jsonify({"error": "Not a JSON"}), 400 + + data = request.get_json() + if not data: + return jsonify({"error": "Not a JSON"}), 400 + + ignored_keys = ["id", "created_at", "updated_at"] + for key, value in data.items(): + if key not in ignored_keys: + setattr(city, key, value) + + city.save() + return jsonify(city.to_dict()), 200 + + +@app_views.route('/cities/', methods=['DELETE'], strict_slashes=False) +def delete_city(city_id): + """ + Delete a City object identified by city id + """ + city = storage.get(City, city_id) + if not city: + abort(404) + + storage.delete(city) + storage.save() + return jsonify({}), 200 diff --git a/api/v1/views/index.py b/api/v1/views/index.py new file mode 100755 index 00000000000..4707cbcad9c --- /dev/null +++ b/api/v1/views/index.py @@ -0,0 +1,39 @@ +#!/usr/bin/python3 +""" +Contains app route for /status in the blueprint app_views +It returns an okay status code for the api +""" +from flask import jsonify +from api.v1.views import app_views +from models import storage +from models.amenity import Amenity +from models.city import City +from models.place import Place +from models.review import Review +from models.state import State +from models.user import User + + +@app_views.route('/status', methods=['GET']) +def status(): + """ + Returns the status of the api + """ + return (jsonify({'status': 'OK'})) + + +@app_views.route("/stats", methods=["GET"]) +def stats(): + """ + Returns the number of each object type + """ + classes = { + "amenities": Amenity, + "cities": City, + "places": Place, + "reviews": Review, + "states": State, + "users": User + } + counts = {key: storage.count(value) for key, value in classes.items()} + return jsonify(counts) diff --git a/api/v1/views/places.py b/api/v1/views/places.py new file mode 100755 index 00000000000..0e98926286c --- /dev/null +++ b/api/v1/views/places.py @@ -0,0 +1,133 @@ +#!/usr/bin/python3 +""" +Handles all default RESTful API actions for Place objects. +""" +from flask import jsonify, abort, request +from api.v1.views import app_views +from models import storage +from models.state import State +from models.place import Place +from models.city import City +from models.user import User + + +@app_views.route('/cities//places', + methods=['GET'], strict_slashes=False) +def get_places_by_city(city_id): + """Retrieves the list of all Place objects in a City""" + city = storage.get(City, city_id) + if not city: + abort(404) + places = [place.to_dict() for place in city.places] + return jsonify(places) + + +@app_views.route('/places/', + methods=['GET'], strict_slashes=False) +def get_place(place_id): + """Retrieves a specific Place object by ID""" + place = storage.get(Place, place_id) + if not place: + abort(404) + return jsonify(place.to_dict()) + + +@app_views.route('/places/', + methods=['DELETE'], strict_slashes=False) +def delete_place(place_id): + """Deletes a Place object""" + place = storage.get(Place, place_id) + if not place: + abort(404) + storage.delete(place) + storage.save() + return jsonify({}), 200 + + +@app_views.route('/cities//places', + methods=['POST'], strict_slashes=False) +def create_place(city_id): + """Creates a new Place object""" + city = storage.get(City, city_id) + if not city: + abort(404) + + data = request.get_json() + if not data: + return jsonify({"error": "Not a JSON"}), 400 + if "user_id" not in data: + return jsonify({"error": "Missing user_id"}), 400 + user = storage.get(User, data["user_id"]) + if not user: + abort(404) + if "name" not in data: + return jsonify({"error": "Missing name"}), 400 + + new_place = Place(city_id=city_id, **data) + storage.new(new_place) + storage.save() + return jsonify(new_place.to_dict()), 201 + + +@app_views.route('/places/', methods=['PUT'], strict_slashes=False) +def update_place(place_id): + """Updates a Place object""" + place = storage.get(Place, place_id) + if not place: + abort(404) + + data = request.get_json() + if not data: + return jsonify({"error": "Not a JSON"}), 400 + + ignored_keys = ["id", "user_id", "city_id", "created_at", "updated_at"] + for key, value in data.items(): + if key not in ignored_keys: + setattr(place, key, value) + + place.save() + return jsonify(place.to_dict()), 200 + + +@app_views.route('/places_search', methods=['POST'], strict_slashes=False) +def search_places(): + """ + Retrieves Place objects based on JSON filters (states, cities, amenities) + """ + data = request.get_json(silent=True) + if data is None: + return jsonify({"error": "Not a JSON"}), 400 + + if not data or ( + not data.get("states") and + not data.get("cities") and + not data.get("amenities") + ): + return jsonify( + [place.to_dict() for place in storage.all(Place).values()] + ) + + places = set() + + state_ids = data.get("states", []) + for state_id in state_ids: + state = storage.get(State, state_id) + if state: + for city in state.cities: + places.update(city.places) + + city_ids = data.get("cities", []) + for city_id in city_ids: + city = storage.get(City, city_id) + if city: + places.update(city.places) + + amenity_ids = data.get("amenities", []) + if amenity_ids: + places = [place for place in places if all( + storage.get(Amenity, amenity_id) in place.amenities + for amenity_id in amenity_ids + ) + ] + + return jsonify([place.to_dict() for place in places]) diff --git a/api/v1/views/places_amenities.py b/api/v1/views/places_amenities.py new file mode 100644 index 00000000000..813b740a95e --- /dev/null +++ b/api/v1/views/places_amenities.py @@ -0,0 +1,35 @@ +#!/usr/bin/python3 +""" +Create a new view for the link between Place objects and Amenity +objects that handles all default RESTFul API actions +""" +from api.v1.views import app_views +from flask import abort, jsonify, request +from models import storage +from models.place import Place +from models.amenity import Amenity + + +@app_views.route('/places//amenities', methods=['GET'], + strict_slashes=False) +def retrieve_amenity(place_id): + """ + Retrieves the list of all Amenity objects of a Place + If the place_id is not linked to any Place object, raise a 404 error + """ + + +@app_views.route('/places//amenities/', + methods=['DELETE'], strict_slashes=False) +def delete_amenity(place_id, amenity_id): + """ + Deletes a Amenity object to a Place + """ + +@app_views('/places//amenities/', methods=['POST']. + strict_slashes=False) + +def link_place(place_id, amenity_id): + """ + Links an amenity object to a Place: + """ diff --git a/api/v1/views/places_reviews.py b/api/v1/views/places_reviews.py new file mode 100644 index 00000000000..a6d68dc0cbb --- /dev/null +++ b/api/v1/views/places_reviews.py @@ -0,0 +1,100 @@ +#!/usr/bin/python3 +""" +Handles all default RESTful API actions for Review objects +""" +from flask import jsonify, abort, request +from api.v1.views import app_views +from models import storage +from models.review import Review +from models.place import Place +from models.user import User + + +@app_views.route('/places//reviews', + methods=['GET'], strict_slashes=False) +def get_reviews(place_id): + """ + Retrieves the list of all Review objects of a Place + """ + place = storage.get(Place, place_id) + if place is None: + abort(404) + return jsonify([review.to_dict() for review in place.reviews]) + + +@app_views.route('/reviews/', methods=['GET'], strict_slashes=False) +def get_review(review_id): + """ + Retrieves a Review object by review id + """ + review = storage.get(Review, review_id) + if review is None: + abort(404) + return jsonify(review.to_dict()) + + +@app_views.route('/reviews/', + methods=['DELETE'], strict_slashes=False) +def delete_review(review_id): + """ + Deletes a Review object by review id + """ + review = storage.get(Review, review_id) + if review is None: + abort(404) + storage.delete(review) + storage.save() + return jsonify({}), 200 + + +@app_views.route('/places//reviews', + methods=['POST'], strict_slashes=False) +def create_review(place_id): + """ + Creates a new Review for a place object + """ + place = storage.get(Place, place_id) + if place is None: + abort(404) + + data = request.get_json() + if not isinstance(data, dict): + return jsonify({'error': 'Not a JSON'}), 400 + + if 'user_id' not in data: + return jsonify({'error': 'Missing user_id'}), 400 + + user = storage.get(User, data['user_id']) + if user is None: + abort(404) + + if 'text' not in data: + return jsonify({'error': 'Missing text'}), 400 + + new_review = Review(**data) + new_review.place_id = place_id + storage.new(new_review) + storage.save() + return jsonify(new_review.to_dict()), 201 + + +@app_views.route('/reviews/', methods=['PUT'], strict_slashes=False) +def update_review(review_id): + """ + Updates a Review object identified by a review id + """ + review = storage.get(Review, review_id) + if review is None: + abort(404) + + data = request.get_json() + if not isinstance(data, dict): + return jsonify({'error': 'Not a JSON'}), 400 + + exclude = ['id', 'user_id', 'place_id', 'created_at', 'updated_at'] + for key, value in data.items(): + if key not in exclude: + setattr(review, key, value) + + storage.save() + return jsonify(review.to_dict()), 200 diff --git a/api/v1/views/states.py b/api/v1/views/states.py new file mode 100755 index 00000000000..b8eae974b99 --- /dev/null +++ b/api/v1/views/states.py @@ -0,0 +1,84 @@ +#!/usr/bin/python3 +"""API endpoints for State objects""" +from flask import jsonify, request, abort +from api.v1.views import app_views +from models import storage +from models.state import State + + +@app_views.route('/states', methods=['GET'], strict_slashes=False) +def get_states(): + """ + Retrieves the list of all State objects + """ + states = storage.all(State).values() + return jsonify([state.to_dict() for state in states]) + + +@app_views.route('/states/', methods=['GET'], strict_slashes=False) +def get_state(state_id): + """ + Retrieves a specific State object identified by ID + """ + state = storage.get(State, state_id) + if not state: + abort(404) + return jsonify(state.to_dict()) + + +@app_views.route('/states/', methods=['DELETE'], + strict_slashes=False) +def delete_state(state_id): + """ + Deletes a State object identified by ID + """ + state = storage.get(State, state_id) + if not state: + abort(404) + storage.delete(state) + storage.save() + return jsonify({}), 200 + + +@app_views.route('/states', methods=['POST'], strict_slashes=False) +def create_state(): + """ + Creates a new State object + """ + if request.content_type != 'application/json': + return jsonify({"error": "Not a JSON"}), 400 + data = request.get_json(silent=True) + if not data: + return jsonify({"error": "Not a JSON"}), 400 + if "name" not in data: + return jsonify({"error": "Missing name"}), 400 + + new_state = State(**data) + storage.new(new_state) + storage.save() + return jsonify(new_state.to_dict()), 201 + + +@app_views.route('/states/', methods=['PUT'], strict_slashes=False) +def update_state(state_id): + """ + Updates a specific State object by ID + """ + state = storage.get(State, state_id) + if not state: + abort(404) + + if request.content_type != 'application/json': + return jsonify({"error": "Content-Type must be application/json"}), 400 + + data = request.get_json() + if not data: + return jsonify({"error": "Not a JSON"}), 400 + + ignored_keys = ["id", "created_at", "updated_at"] + for key, value in data.items(): + if key not in ignored_keys: + setattr(state, key, value) + + state.save() + return jsonify(state.to_dict()), 200 diff --git a/api/v1/views/users.py b/api/v1/views/users.py new file mode 100755 index 00000000000..c7d200892c6 --- /dev/null +++ b/api/v1/views/users.py @@ -0,0 +1,99 @@ +#!/usr/bin/python3 +""" +Creates API endpoints for acessing, creating, deleting and +updating Users, it uses the app_views blueprint +""" +from api.v1.views import app_views +from models import storage +from models.user import User +from flask import jsonify, request, abort + + +@app_views.route('/users', methods=['GET']) +def get_users(): + """ + Retrieves all user objects and returns the JSON + representation of the dictionary + """ + users = [] + all_users = storage.all(User) + + for user in all_users.values(): + users.append(user.to_dict()) + return (jsonify(users)) + + +@app_views.route('/users/', methods=['GET']) +def get_a_user(user_id): + """ + Retrieves a specific User based on their id + 404 error if the user does not exist + """ + user = storage.get(User, user_id) + + if user is not None: + return (jsonify(user.to_dict())), 200 + abort(404) + + +@app_views.route('/users/', methods=['DELETE']) +def delete_user(user_id): + """ + Deletes a user based on their id + Returns an empty dictionary plus 200ok status on success + or 404 error page on failure + """ + user = storage.get(User, user_id) + if user is None: + abort(404) + storage.delete(user) + storage.save() + + return (jsonify({})), 200 + + +@app_views.route('/users', methods=['POST']) +def create_user(): + """ + Creates a user and returns the dictionary representation of the + user, or an error dictionary + """ + data = request.get_json() + if not data: + return (jsonify({'error': 'Not a JSON'})) + if 'email' not in data.keys(): + return (jsonify({'error': 'Missing email'})) + if 'password' not in data.keys(): + return (jsonify({'error': 'Missing password'})) + + email = data.get('email') + password = data.get('password') + user = User(email=email, password=password) + storage.new(user) + storage.save() + return (jsonify(user.to_dict())), 201 + + +@app_views.route('/users/', methods=['PUT']) +def update_user(user_id): + """ + Updates a user based on the id, returns a code 201 + on success or an error dictionary + """ + data = request.get_json() + + if not isinstance(data, dict): + return (jsonify({'error': 'Not a JSON'})) + + user = storage.get(User, user_id) + + if user is None: + abort(404) + + exclude = ['id', 'created_at', 'updated_at'] + for k, v in data.items(): + if k not in exclude: + setattr(user, k, v) + + user.save() + return (jsonify(user.to_dict())), 200 diff --git a/file.json b/file.json new file mode 100644 index 00000000000..b68251c2c2d --- /dev/null +++ b/file.json @@ -0,0 +1 @@ +{"Place.2e36148d-5152-413e-a2de-0bf6790dada3": {"id": "2e36148d-5152-413e-a2de-0bf6790dada3", "created_at": "2025-02-15T11:22:44.658444", "updated_at": "2025-02-15T11:22:44.658685", "__class__": "Place"}, "Review.630c39d3-591e-4b15-93fa-c1d798b63ae5": {"id": "630c39d3-591e-4b15-93fa-c1d798b63ae5", "created_at": "2025-02-15T11:22:49.886735", "updated_at": "2025-02-15T11:22:49.886910", "__class__": "Review"}, "User.fc9ef1ff-ae26-44f9-9a0e-0c08d3347837": {"id": "fc9ef1ff-ae26-44f9-9a0e-0c08d3347837", "created_at": "2025-02-15T11:22:54.759173", "updated_at": "2025-02-15T11:22:54.759323", "__class__": "User"}, "Amenity.781ee56e-8787-4f54-bbc9-81ef13724230": {"id": "781ee56e-8787-4f54-bbc9-81ef13724230", "created_at": "2025-02-15T11:23:01.124620", "updated_at": "2025-02-15T11:23:01.125098", "__class__": "Amenity"}, "City.deb949a9-c887-4356-ae77-b1dea24b7097": {"id": "deb949a9-c887-4356-ae77-b1dea24b7097", "created_at": "2025-02-15T11:23:05.333698", "updated_at": "2025-02-15T11:23:05.334112", "__class__": "City"}, "State.7b001d63-4ac6-42ce-9498-451c214c1eed": {"id": "7b001d63-4ac6-42ce-9498-451c214c1eed", "created_at": "2025-02-15T11:23:10.384902", "updated_at": "2025-02-15T11:23:10.385237", "__class__": "State"}, "Place.9937d56d-a88e-4b23-aa97-803bf82ab3fc": {"id": "9937d56d-a88e-4b23-aa97-803bf82ab3fc", "created_at": "2025-02-15T11:32:07.900909", "updated_at": "2025-02-15T11:32:07.900909", "__class__": "Place"}, "Place.74e76bec-22b5-4147-a2ac-380e2f15597b": {"id": "74e76bec-22b5-4147-a2ac-380e2f15597b", "created_at": "2025-02-15T11:35:48.920571", "updated_at": "2025-02-15T11:35:57.721183", "__class__": "Place"}} \ No newline at end of file diff --git a/models/engine/db_storage.py b/models/engine/db_storage.py index b8e7d291e6f..b3eea715223 100755 --- a/models/engine/db_storage.py +++ b/models/engine/db_storage.py @@ -74,3 +74,22 @@ def reload(self): def close(self): """call remove() method on the private session attribute""" self.__session.remove() + + def get(self, cls, id): + """ + Retrieves one object + """ + obj = self.__session.query(cls).filter_by(id=id).first() + return (obj) + + def count(self, cls=None): + """ + Counts the number of objects + """ + if cls is None: + length = 0 + for k, v in classes.items(): + length += self.__session.query(v).count() + return (length) + else: + return (self.__session.query(cls).count()) diff --git a/models/engine/file_storage.py b/models/engine/file_storage.py index c8cb8c1764d..8466d4c5339 100755 --- a/models/engine/file_storage.py +++ b/models/engine/file_storage.py @@ -55,7 +55,7 @@ def reload(self): jo = json.load(f) for key in jo: self.__objects[key] = classes[jo[key]["__class__"]](**jo[key]) - except: + except Exception: pass def delete(self, obj=None): @@ -66,5 +66,32 @@ def delete(self, obj=None): del self.__objects[key] def close(self): - """call reload() method for deserializing the JSON file to objects""" + """ + call reload() method for deserializing the JSON file to objects + """ self.reload() + + def get(self, cls, id): + """ + Retrieves one object + """ + if cls is not None and cls in classes: + for key, value in self.__objects.items(): + if value.__class__ == cls and value.id == id: + return value + else: + return (None) + + def count(self, cls=None): + """ + Counts the number of objects + """ + count = 0 + if cls is not None: + for key, value in self.__objects.items(): + if value.__class__ == cls or cls == value.__class__.__name__: + count += 1 + return (count) + + else: + return (len(self.__objects.items())) diff --git a/places_amenities.py b/places_amenities.py new file mode 100644 index 00000000000..2edc3cfb65c --- /dev/null +++ b/places_amenities.py @@ -0,0 +1,109 @@ +#!/usr/bin/python3 +""" +Creates a view for Review objects that Handles all default RESTFul API +actions +""" +from api.v1.views import app_views +from flask import abort, jsonify, request +from models import storage +from models.place import Place +from models.user import User +from models.review import Review + + +@app_views.route('/places//reviews', methods=['GET'], + strict_slashes=False) +def get_reviews(place_id): + """ + Returns all reviews for a place based on id + """ + place = storage.get(Place, place_id) + if not place: + abort(404) + + + obj_list = [] + all_objs = storage.all(Review) + for obj in all_obj.values(): + if obj.place_id == place_id: + obj_list.append(obj.to_dict()) + return jsonify(obj_list), 200 + +@app_views.route('/places//reviews', methods=['POST'], + strict_slashes=False) +def post_reviews(place_id): + """ + Creates a new review for a place based on place id + """ + place = storage.get(Place, place_id) + if not place: + abort(404) + data = request.get_json() + if not data: + abort(400, "Not a JSON") + elif 'user_id' not in data.keys(): + abort(400, 'Missing user_id') + elif 'text' not in data.keys(): + abort(400, 'Missing text') + + user = storage.get(User, data['user_id']) + if not user: + abort(404) + + obj = Review(place_id=place_id, **data) + storage.new(obj) + storage.save() + + return jsonify(obj.to_dict()), 201 + + +@app_views.route('/reviews/', methods=['GET'], + strict_slashes=False) +def method_reviews(review_id): + """ + Retrieves a review based on it's id + """ + review = storage.get(Review, review_id) + if review is None or not review: + abort(404) + + return jsonify(review.to_dict()), 200 + + +@app_views.route('/reviews/', methods=['DELETE'], + strict_slashes=False) +def method_reviews(review_id): + """ + Deletes a review based on it's id + """ + review = storage.get(Review, review_id) + if review is None or not review: + abort(404) + + review.delete() + storage.save() + return jsonify({}), 200 + + +@app_views.route('/reviews/', methods=['PUT'], + strict_slashes=False) +def method_reviews(review_id): + """ + Updates a review based on its id + """ + review = storage.get(Review, review_id) + if review is None or not review: + abort(404) + + + data = request.get_json() + if not data: + abort(404, "Not a JSON") + + skip = ['id', 'created_at', 'updated_at', 'user_id', 'place_id'] + for key, value in data.items(): + if key not in skip: + setattr(review, key, value) + + storage.save() + return jsonify(review.to_dict()), 200 diff --git a/test_get_count.py b/test_get_count.py new file mode 100755 index 00000000000..df7851b273a --- /dev/null +++ b/test_get_count.py @@ -0,0 +1,7 @@ +#!/usr/bin/python3 + +from models import storage +from models.state import State + +my_obj = State() +storage.get(my_obj, my_obj.id) diff --git a/tests/test_api/README.md b/tests/test_api/README.md new file mode 100644 index 00000000000..6142a949bed --- /dev/null +++ b/tests/test_api/README.md @@ -0,0 +1 @@ +Contains unittests for the api modules diff --git a/tests/test_api/test_v1/README.md b/tests/test_api/test_v1/README.md new file mode 100644 index 00000000000..a994ced1631 --- /dev/null +++ b/tests/test_api/test_v1/README.md @@ -0,0 +1 @@ +Contains unit tests for the version 1 of our REST API diff --git a/tests/test_api/test_v1/test_views/README.md b/tests/test_api/test_v1/test_views/README.md new file mode 100644 index 00000000000..3aa9f6d9f41 --- /dev/null +++ b/tests/test_api/test_v1/test_views/README.md @@ -0,0 +1,2 @@ +Contains Unit tests for all flask blueprints for the +flask application app diff --git a/tests/test_models/test_engine/test_db_storage.py b/tests/test_models/test_engine/test_db_storage.py index 766e625b5af..1a81a705ea9 100755 --- a/tests/test_models/test_engine/test_db_storage.py +++ b/tests/test_models/test_engine/test_db_storage.py @@ -2,7 +2,6 @@ """ Contains the TestDBStorageDocs and TestDBStorage classes """ - from datetime import datetime import inspect import models @@ -24,21 +23,29 @@ class TestDBStorageDocs(unittest.TestCase): - """Tests to check the documentation and style of DBStorage class""" + """ + Tests to check the documentation and style of DBStorage class + """ @classmethod def setUpClass(cls): - """Set up for the doc tests""" + """ + Set up for the doc tests + """ cls.dbs_f = inspect.getmembers(DBStorage, inspect.isfunction) def test_pep8_conformance_db_storage(self): - """Test that models/engine/db_storage.py conforms to PEP8.""" + """ + Test that models/engine/db_storage.py conforms to PEP8. + """ pep8s = pep8.StyleGuide(quiet=True) result = pep8s.check_files(['models/engine/db_storage.py']) self.assertEqual(result.total_errors, 0, "Found code style errors (and warnings).") def test_pep8_conformance_test_db_storage(self): - """Test tests/test_models/test_db_storage.py conforms to PEP8.""" + """ + Test tests/test_models/test_db_storage.py conforms to PEP8. + """ pep8s = pep8.StyleGuide(quiet=True) result = pep8s.check_files(['tests/test_models/test_engine/\ test_db_storage.py']) @@ -46,21 +53,27 @@ def test_pep8_conformance_test_db_storage(self): "Found code style errors (and warnings).") def test_db_storage_module_docstring(self): - """Test for the db_storage.py module docstring""" + """ + Test for the db_storage.py module docstring + """ self.assertIsNot(db_storage.__doc__, None, "db_storage.py needs a docstring") self.assertTrue(len(db_storage.__doc__) >= 1, "db_storage.py needs a docstring") def test_db_storage_class_docstring(self): - """Test for the DBStorage class docstring""" + """ + Test for the DBStorage class docstring + """ self.assertIsNot(DBStorage.__doc__, None, "DBStorage class needs a docstring") self.assertTrue(len(DBStorage.__doc__) >= 1, "DBStorage class needs a docstring") def test_dbs_func_docstrings(self): - """Test for the presence of docstrings in DBStorage methods""" + """ + Test for the presence of docstrings in DBStorage methods + """ for func in self.dbs_f: self.assertIsNot(func[1].__doc__, None, "{:s} method needs a docstring".format(func[0])) @@ -68,21 +81,58 @@ def test_dbs_func_docstrings(self): "{:s} method needs a docstring".format(func[0])) -class TestFileStorage(unittest.TestCase): - """Test the FileStorage class""" - @unittest.skipIf(models.storage_t != 'db', "not testing db storage") +class TestDBStorage(unittest.TestCase): + """ + Test the DBStorage class + """ + @unittest.skipIf(models.storage_t != 'fs', "not testing db storage") def test_all_returns_dict(self): - """Test that all returns a dictionaty""" + """ + Test that all returns a dictionary + """ self.assertIs(type(models.storage.all()), dict) - @unittest.skipIf(models.storage_t != 'db', "not testing db storage") + @unittest.skipIf(models.storage_t != 'fs', "not testing db storage") def test_all_no_class(self): - """Test that all returns all rows when no class is passed""" + """ + Test that all returns all rows when no class is passed + """ - @unittest.skipIf(models.storage_t != 'db', "not testing db storage") + @unittest.skipIf(models.storage_t != 'fs', "not testing db storage") def test_new(self): - """test that new adds an object to the database""" + """ + test that new adds an object to the database + """ - @unittest.skipIf(models.storage_t != 'db', "not testing db storage") + @unittest.skipIf(models.storage_t != 'fs', "not testing db storage") def test_save(self): - """Test that save properly saves objects to file.json""" + """ + Test that save properly saves objects to file.json + """ + + @unittest.skipIf(models.storage_t != 'fs', "not testing db storage") + def test_get(self): + """ + Tests the method + """ + place = Place() + storage.new(place) + storage.save() + self.assertIs( + storage.get(Place, place.id), place, + "Get did not return the same object" + ) + self.assertIn( + type(storage.get(Place, place.id)), classes, + "Object returned is not in valid classes" + ) + + @unittest.skipIf(models.storage_t != 'fs', "not testing db storage") + def test_count(self): + """ + Tests the count method + """ + self.assertIs( + type(storage.count(Place)), int, + "Get did not return an integer" + ) diff --git a/url b/url new file mode 100644 index 00000000000..c31d9899c33 --- /dev/null +++ b/url @@ -0,0 +1,4 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. +