From 8d05a6edfe8c2766b2b90a951aa265bfd7f8099c Mon Sep 17 00:00:00 2001 From: mseng10 Date: Sun, 26 May 2024 12:15:22 -0500 Subject: [PATCH 1/7] BUG: Resolve dependency issues --- server/app.py | 8 +++++--- server/requirements.txt | 12 ++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/server/app.py b/server/app.py index 81131f6..d29474d 100644 --- a/server/app.py +++ b/server/app.py @@ -1,5 +1,6 @@ -from colorama import init, Fore +# from colorama import init, Fore from flask import Flask, request, jsonify +from flask_cors import CORS from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.engine import URL @@ -7,7 +8,7 @@ import logging # Initialize Colorama -init(autoreset=True) +# init(autoreset=True) # Load database configuration from JSON file with open("db.json") as json_data_file: @@ -27,6 +28,7 @@ # Create Flask app app = Flask(__name__) +CORS(app) # Configure logging logging.basicConfig(level=logging.INFO) @@ -62,7 +64,7 @@ def get_plants(): # Example: Convert plants to JSON format # plants_json = [plant.to_json() for plant in plants] # Return JSON response - return jsonify([]) # Placeholder response + return jsonify([{"gangser": "asdfasdf"}]) # Placeholder response if __name__ == "__main__": # Run the Flask app diff --git a/server/requirements.txt b/server/requirements.txt index 8d07afe..8e4f1d3 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,11 +1,19 @@ blinker==1.7.0 click==8.1.7 Flask==3.0.1 -Flask-Cors==4.0.0 +Flask-Cors==4.0.1 importlib-metadata==7.0.1 itsdangerous==2.1.2 Jinja2==3.1.3 MarkupSafe==2.1.4 +numpy==1.26.4 +pandas==2.2.2 +psycopg2-binary==2.9.9 +python-dateutil==2.9.0.post0 +pytz==2024.1 +six==1.16.0 +SQLAlchemy==2.0.30 +typing_extensions==4.12.0 +tzdata==2024.1 Werkzeug==3.0.1 zipp==3.17.0 -pandas==2.2.2 From 7ed9c71285463eeb8ddbcc2850dd647b5aa892e1 Mon Sep 17 00:00:00 2001 From: mseng10 Date: Sun, 26 May 2024 13:46:55 -0500 Subject: [PATCH 2/7] ENH: Support get plants endpoint --- server/app.py | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/server/app.py b/server/app.py index d29474d..926de77 100644 --- a/server/app.py +++ b/server/app.py @@ -6,6 +6,7 @@ from sqlalchemy.engine import URL import json import logging +from models.plant import Plant, Base # Importing the Plant model # Initialize Colorama # init(autoreset=True) @@ -24,6 +25,10 @@ port=db_config["port"], ) engine = create_engine(url) +#TODO: Temp, remove when all is working +Base.metadata.drop_all(engine) +Base.metadata.create_all(engine) + Session = sessionmaker(bind=engine) # Create Flask app @@ -37,34 +42,33 @@ @app.route("/plants", methods=["POST"]) def add_plant(): # Get JSON data from request - new_plant = request.get_json() - # Log the request - logger.info("Received request to add a new plant: %s", new_plant) - # Perform database operations here - # Example: Add new plant to the database - # session = Session() - # session.add(new_plant) - # session.commit() - # session.close() + new_plant_data = request.get_json() + # Create a new Plant object + new_plant = Plant( + cost=new_plant_data["cost"], + size=new_plant_data["size"], + watering=new_plant_data["watering"], + ) + # Add the new plant object to the session + session = Session() + session.add(new_plant) + session.commit() + session.close() # Return response - return jsonify(new_plant), 201 + return jsonify({"message": "Plant added successfully"}), 201 # Example route to get all plants @app.route("/plants", methods=["GET"]) def get_plants(): # Log the request logger.info("Received request to retrieve all plants") - print("HELLO") - # Perform database query to retrieve all plants - # Example: Query all plants from the database - # session = Session() - # plants = session.query(Plant).all() - # session.close() + session = Session() + plants = session.query(Plant).all() + session.close() # Transform plants to JSON format - # Example: Convert plants to JSON format - # plants_json = [plant.to_json() for plant in plants] + plants_json = [{"id": plant.id, "cost": plant.cost, "size": plant.size, "watering": plant.watering} for plant in plants] # Return JSON response - return jsonify([{"gangser": "asdfasdf"}]) # Placeholder response + return jsonify(plants_json) if __name__ == "__main__": # Run the Flask app From 0e3940ddbd82fe8ff6ead93e7f623fca79657477 Mon Sep 17 00:00:00 2001 From: mseng10 Date: Sun, 26 May 2024 15:52:49 -0500 Subject: [PATCH 3/7] BUG: Use HTTP for now... --- client/react/src/App.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/react/src/App.js b/client/react/src/App.js index 193099c..5f5846c 100644 --- a/client/react/src/App.js +++ b/client/react/src/App.js @@ -54,13 +54,13 @@ const App = () => { useEffect(() => { // Fetch plant data from the server - fetch('https://localhost/plants') + fetch('http://127.0.0.1:5000/plants') .then((response) => response.json()) .then((data) => setPlants(data)) .catch((error) => console.error('Error fetching plant data:', error)); const system = {temperature: 20, humidity: 80}; // Fetch plant data from the server - fetch('https://localhost/system') + fetch('http://localhost:5000/system') .then((response) => response.json()) .then(() => setSystem(system)) .catch(() => setSystem(system)); From f50b8cd75637ad60d600802fa5da3a6143a13455 Mon Sep 17 00:00:00 2001 From: mseng10 Date: Sun, 26 May 2024 15:53:42 -0500 Subject: [PATCH 4/7] ENH: Rework NewPlantForm.js to query species and only specify plant information --- client/react/src/forms/NewPlantForm.js | 74 ++++++++------------------ 1 file changed, 23 insertions(+), 51 deletions(-) diff --git a/client/react/src/forms/NewPlantForm.js b/client/react/src/forms/NewPlantForm.js index 8103493..fdfd839 100644 --- a/client/react/src/forms/NewPlantForm.js +++ b/client/react/src/forms/NewPlantForm.js @@ -10,13 +10,6 @@ import CheckSharpIcon from '@mui/icons-material/CheckSharp'; import CloseSharpIcon from '@mui/icons-material/CloseSharp'; const NewPlantForm = ({ isOpen, onRequestClose, onSave }) => { - // TODO: Turn to server side - const typesToGenus = new Map([ - ["Succulent", ["Echeveria"]], - ["Cactus", ["Old Man"]], - ["Philodendron", ["Pink Princess", "White Princess"]], - ["Monstera", ["Albo", "Thai Constelation"]] - ]) const stages = [ "Leaf", @@ -25,28 +18,28 @@ const NewPlantForm = ({ isOpen, onRequestClose, onSave }) => { "Senior" ]; - useEffect(() => { // Fetch plant data from the server - fetch('https://localhost/types') + fetch('http://127.0.0.1:5000/species') .then((response) => response.json()) - .then((data) => setTypes(data? Array.from(typesToGenus.keys()) : Array.from(typesToGenus.keys()))) + .then((data) => setAllSpecies(data)) .catch((error) => console.error('Error fetching plant data:', error)); }, []); const [name, setName] = useState(''); - const [type, setType] = useState('Succulent'); - const [genus, setGenus] = useState('Echeveria'); - const [stage, setStage] = useState('Senior') - const [wateringFrequency, setWateringFrequency] = useState(14); - const [submitted, setSubmit] = useState(false); + const [stage, setStage] = useState('Senior'); + const [size, setSize] = useState(0); + const [cost, setCost] = useState(0); - const [types, setTypes] = useState(Array.from(typesToGenus.keys())) + const [allSpecies, setAllSpecies] = useState([]); + + const [submitted, setSubmit] = useState(false); const handleSubmit = (event) => { setSubmit(true); event.preventDefault(); - onSave({ name, type, wateringFrequency, genus, stage }); + console.log(allSpecies); + onSave({ name, stage }); clearForm(); onRequestClose(); }; @@ -58,9 +51,6 @@ const NewPlantForm = ({ isOpen, onRequestClose, onSave }) => { const clearForm = () => { setName(''); - setType('Succulent'); - setGenus('Echeveria'); - setWateringFrequency(''); }; return ( @@ -100,34 +90,6 @@ const NewPlantForm = ({ isOpen, onRequestClose, onSave }) => { variant="standard" onChange={(event) => setName(event.target.value)} /> - setType(event.target.value)} - variant="standard" - > - {types.map((ty) => ( - {ty} - ))} - - setGenus(event.target.value)} - variant="standard" - > - {typesToGenus.get(type).map((gen) => ( - {gen} - ))} - { fullWidth required type="number" - label="Watering Frequency" - value={wateringFrequency} - onChange={(event) => setWateringFrequency(event.target.value)} + label="Size (inches)" + value={size} + onChange={(event) => setSize(event.target.value)} + variant="standard" + /> + setCost(event.target.value)} variant="standard" /> From 6f6ffb9dcf2929d96343f18e0ba3a83b23b222c8 Mon Sep 17 00:00:00 2001 From: mseng10 Date: Sun, 26 May 2024 17:20:41 -0500 Subject: [PATCH 5/7] ENH: Species endpoints --- server/app.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/server/app.py b/server/app.py index 926de77..67d43af 100644 --- a/server/app.py +++ b/server/app.py @@ -6,7 +6,7 @@ from sqlalchemy.engine import URL import json import logging -from models.plant import Plant, Base # Importing the Plant model +from models.plant import Plant, Base, Species, Genus # Importing the Plant model # Initialize Colorama # init(autoreset=True) @@ -41,13 +41,13 @@ @app.route("/plants", methods=["POST"]) def add_plant(): + logger.info("Attemping create plant") # Get JSON data from request new_plant_data = request.get_json() # Create a new Plant object new_plant = Plant( cost=new_plant_data["cost"], - size=new_plant_data["size"], - watering=new_plant_data["watering"], + size=new_plant_data["size"] ) # Add the new plant object to the session session = Session() @@ -70,6 +70,55 @@ def get_plants(): # Return JSON response return jsonify(plants_json) +@app.route("/species", methods=["POST"]) +def add_species(): + logger.info("Attempting create species") + + new_species_data = request.get_json() + + genus_id = new_species_data["genus_id"] + + # I'm electing not to have a dedicated endpoint to creating genuses... sue me + # We can just embed it on the species object for now + new_genus = None + if not genus_id: + if not new_species_data["genus"]: + return jsonify({"message": "Did not specify genus when creating species"}), 400 + + new_genus_data = new_species_data["genus"] + new_genus = Genus( + name=new_genus_data["name"], + watering=new_genus_data["watering"] + ) + + # Create a new Type object + new_species = Species( + name=new_species_data["name"], + genus_id=genus_id + ) + + # Add the new species (and genus if applicable) object to the session + session = Session() + if new_genus: + session.add(new_genus) + session.add(new_type) + session.commit() + session.close() + + return jsonify({"message": "Species added successfully", "id": new_species.id}), 201 + +@app.route("/species", methods=["GET"]) +def get_species(): + logger.info("Received request to retrieve all plant species") + + session = Session() + species = session.query(Species).all() + session.close() + # Transform species to JSON format + species_json = [{"id": _species.id, "name": _species.name} for _species in species] + # Return JSON response + return jsonify(species_json) + if __name__ == "__main__": # Run the Flask app app.run(debug=True) From 3537b06c24406ecc0f0fca0f4564056219167cff Mon Sep 17 00:00:00 2001 From: mseng10 Date: Sun, 26 May 2024 17:22:19 -0500 Subject: [PATCH 6/7] ENH: GET all genuses endpoint --- server/app.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/app.py b/server/app.py index 67d43af..e3740b7 100644 --- a/server/app.py +++ b/server/app.py @@ -119,6 +119,18 @@ def get_species(): # Return JSON response return jsonify(species_json) +@app.route("/genus", methods=["GET"]) +def get_genuses(): + logger.info("Received request to retrieve all plant genuses") + + session = Session() + genuses = session.query(Genus).all() + session.close() + # Transform genuses to JSON format + genuses_json = [{"id": genus.id, "name": genus.name, "watering": genus.watering} for genus in genuses] + # Return JSON response + return jsonify(genuses_json) + if __name__ == "__main__": # Run the Flask app app.run(debug=True) From bffef0b4dff61e4072d611e2128ea5e8d074c082 Mon Sep 17 00:00:00 2001 From: mseng10 Date: Sun, 26 May 2024 23:10:33 -0500 Subject: [PATCH 7/7] ENH: Genus - Species - Plant mappings --- server/models/plant.py | 69 ++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 43 deletions(-) diff --git a/server/models/plant.py b/server/models/plant.py index 54befe9..bc26044 100644 --- a/server/models/plant.py +++ b/server/models/plant.py @@ -9,17 +9,17 @@ Base = declarative_base() # class DeathCause(Enum, str): - # TOO_LITTLE_WATER = "too little water" - # TOO_MUCH_WATER = "too much water" - # TOO_LITTLE_HUMIDITY = "too little humidity" - # TOO_MUCH_HUMIDITY = "too much humidity" - # TOO_LITTLE_SUN = "too little sun" - # TOO_MUCH_SUN = "too much sun" - # PROPAGATION = "propagation" - # PESTS = "pests" - # MOLD = "mold" - # NEGLECT = "neglect" - # UNKNOWN = "unknown" +# TOO_LITTLE_WATER = "too little water" +# TOO_MUCH_WATER = "too much water" +# TOO_LITTLE_HUMIDITY = "too little humidity" +# TOO_MUCH_HUMIDITY = "too much humidity" +# TOO_LITTLE_SUN = "too little sun" +# TOO_MUCH_SUN = "too much sun" +# PROPAGATION = "propagation" +# PESTS = "pests" +# MOLD = "mold" +# NEGLECT = "neglect" +# UNKNOWN = "unknown" class Plant(Base): @@ -30,67 +30,50 @@ class Plant(Base): # Created at specs id = Column(Integer(), primary_key=True) created_on = Column(DateTime(), default=datetime.now) - cost = Column(Integer()) - size = Column(Integer()) # inches - type_id: Mapped[int] = mapped_column(ForeignKey("type.id")) # Type of Plant + cost = Column(Integer(), default=0, nullable=False) + size = Column(Integer(), default=0, nullable=False) # inches + species_id: Mapped[int] = mapped_column(ForeignKey("species.id", ondelete='CASCADE')) # Species of Plant updated_on = Column(DateTime(), default=datetime.now, onupdate=datetime.now) - # Batch - batch_id: Mapped[int] = mapped_column(ForeignKey("batch.id")) - batch: Mapped["Batch"] = relationship(back_populates="plants") - # Water Info watered_on = Column(DateTime(), default=datetime.now) - watering = Column(Integer()) # days # Death Info dead = Column(Boolean, default=False, nullable=False) # dead_cause = Column(Enum(DeathCause), nullable=True) dead_on = Column(DateTime(), default=None, nullable=True) - def __repr__(self) -> str: - return f"{self.name} ({self.type}/{self.genus})" - - -class Type(Base): - """Type of genus.""" +class Species(Base): + """Species of genus.""" - __tablename__ = "type" + __tablename__ = "species" id = Column(Integer(), primary_key=True) created_on = Column(DateTime(), default=datetime.now) - name = Column(String(100), nullable=False) + name = Column(String(100), nullable=False, unique=True) - plants: Mapped[List["Plant"]] = relationship() + # Available plants of this species + plants: Mapped[List["Plant"]] = relationship('Plant', backref='plant', passive_deletes=True) - # TODO: Best lighting and soil mixes + # Genus of this species of plant + genus_id: Mapped[int] = mapped_column(ForeignKey("genus.id", ondelete='CASCADE')) def __repr__(self) -> str: return f"{self.name}" - class Genus(Base): - """Genus of species.""" + """Genus of plant.""" __tablename__ = "genus" id = Column(Integer(), primary_key=True) created_on = Column(DateTime(), default=datetime.now) name = Column(String(100), nullable=False, unique=True) + watering = Column(Integer()) # days - def __repr__(self) -> str: - return f"{self.name}" - - -class Species(Base): - """Species of plants.""" - - __tablename__ = "species" - - id = Column(Integer(), primary_key=True) - created_on = Column(DateTime(), default=datetime.now) - name = Column(String(100), nullable=False, unique=True) + # Available species of this genus + species: Mapped[List["Species"]] = relationship('Species', backref='species', passive_deletes=True) def __repr__(self) -> str: return f"{self.name}"