From 97cb9c870374aeb75532881d2a00be93e2098cf5 Mon Sep 17 00:00:00 2001 From: Kobe Date: Thu, 22 May 2025 21:22:15 +0200 Subject: [PATCH] lol --- app.py | 240 +++- config.py | 8 + .../dd9a310269e3_initial_migration.py | 130 ++ ...d55cc5aa7_increase_password_hash_length.py | 38 + requirements.txt | 6 +- templates/create_plant.html | 11 +- templates/manage_plants.html | 77 +- .../__pycache__/__init__.cpython-310.pyc | Bin 1250 -> 1250 bytes .../__pycache__/__main__.cpython-310.pyc | Bin 317 -> 317 bytes .../dotenv/__pycache__/cli.cpython-310.pyc | Bin 5979 -> 5979 bytes .../__pycache__/ipython.cpython-310.pyc | Bin 1475 -> 1475 bytes .../dotenv/__pycache__/main.cpython-310.pyc | Bin 10149 -> 10255 bytes .../dotenv/__pycache__/parser.cpython-310.pyc | Bin 6144 -> 6144 bytes .../__pycache__/variables.cpython-310.pyc | Bin 3533 -> 3533 bytes .../__pycache__/version.cpython-310.pyc | Bin 188 -> 188 bytes venv/Lib/site-packages/dotenv/main.py | 28 +- venv/Lib/site-packages/dotenv/version.py | 2 +- .../flask-2.3.3.dist-info/INSTALLER | 1 - .../flask-2.3.3.dist-info/LICENSE.rst | 28 - .../flask-2.3.3.dist-info/METADATA | 116 -- .../flask-2.3.3.dist-info/RECORD | 53 - .../flask-2.3.3.dist-info/REQUESTED | 0 .../site-packages/flask-2.3.3.dist-info/WHEEL | 4 - .../flask-2.3.3.dist-info/entry_points.txt | 3 - venv/Lib/site-packages/flask/__init__.py | 68 +- .../__pycache__/__init__.cpython-310.pyc | Bin 2828 -> 2282 bytes .../__pycache__/__main__.cpython-310.pyc | Bin 207 -> 207 bytes .../flask/__pycache__/app.cpython-310.pyc | Bin 68325 -> 49877 bytes .../__pycache__/blueprints.cpython-310.pyc | Bin 22516 -> 3418 bytes .../flask/__pycache__/cli.cpython-310.pyc | Bin 27019 -> 29081 bytes .../flask/__pycache__/config.cpython-310.pyc | Bin 12864 -> 13416 bytes .../flask/__pycache__/ctx.cpython-310.pyc | Bin 14674 -> 14848 bytes .../__pycache__/debughelpers.cpython-310.pyc | Bin 6000 -> 6586 bytes .../flask/__pycache__/globals.cpython-310.pyc | Bin 2950 -> 1575 bytes .../flask/__pycache__/helpers.cpython-310.pyc | Bin 24252 -> 21602 bytes .../flask/__pycache__/logging.cpython-310.pyc | Bin 2533 -> 2542 bytes .../__pycache__/scaffold.cpython-310.pyc | Bin 26488 -> 0 bytes .../__pycache__/sessions.cpython-310.pyc | Bin 13251 -> 13284 bytes .../flask/__pycache__/signals.cpython-310.pyc | Bin 1246 -> 805 bytes .../__pycache__/templating.cpython-310.pyc | Bin 7037 -> 7079 bytes .../flask/__pycache__/testing.cpython-310.pyc | Bin 9691 -> 9842 bytes .../flask/__pycache__/typing.cpython-310.pyc | Bin 1716 -> 1829 bytes .../flask/__pycache__/views.cpython-310.pyc | Bin 5461 -> 5539 bytes .../__pycache__/wrappers.cpython-310.pyc | Bin 5193 -> 5233 bytes venv/Lib/site-packages/flask/app.py | 1061 +++----------- venv/Lib/site-packages/flask/blueprints.py | 667 +-------- venv/Lib/site-packages/flask/cli.py | 139 +- venv/Lib/site-packages/flask/config.py | 45 +- venv/Lib/site-packages/flask/ctx.py | 29 +- venv/Lib/site-packages/flask/debughelpers.py | 38 +- venv/Lib/site-packages/flask/globals.py | 45 - venv/Lib/site-packages/flask/helpers.py | 106 +- venv/Lib/site-packages/flask/json/__init__.py | 2 +- .../json/__pycache__/__init__.cpython-310.pyc | Bin 5989 -> 5989 bytes .../json/__pycache__/provider.cpython-310.pyc | Bin 7664 -> 7681 bytes .../json/__pycache__/tag.cpython-310.pyc | Bin 11096 -> 11639 bytes venv/Lib/site-packages/flask/json/provider.py | 13 +- venv/Lib/site-packages/flask/json/tag.py | 24 +- venv/Lib/site-packages/flask/logging.py | 9 +- venv/Lib/site-packages/flask/scaffold.py | 873 ------------ venv/Lib/site-packages/flask/sessions.py | 28 +- venv/Lib/site-packages/flask/signals.py | 16 - venv/Lib/site-packages/flask/templating.py | 25 +- venv/Lib/site-packages/flask/testing.py | 13 +- venv/Lib/site-packages/flask/typing.py | 18 +- venv/Lib/site-packages/flask/views.py | 9 +- venv/Lib/site-packages/flask/wrappers.py | 9 +- .../python_dotenv-1.0.0.dist-info/INSTALLER | 1 - .../python_dotenv-1.0.0.dist-info/LICENSE | 27 - .../python_dotenv-1.0.0.dist-info/METADATA | 667 --------- .../python_dotenv-1.0.0.dist-info/RECORD | 26 - .../python_dotenv-1.0.0.dist-info/REQUESTED | 0 .../python_dotenv-1.0.0.dist-info/WHEEL | 5 - .../entry_points.txt | 2 - .../top_level.txt | 1 - .../werkzeug-2.3.7.dist-info/INSTALLER | 1 - .../werkzeug-2.3.7.dist-info/LICENSE.rst | 28 - .../werkzeug-2.3.7.dist-info/METADATA | 118 -- .../werkzeug-2.3.7.dist-info/RECORD | 126 -- .../werkzeug-2.3.7.dist-info/REQUESTED | 0 .../werkzeug-2.3.7.dist-info/WHEEL | 4 - venv/Lib/site-packages/werkzeug/__init__.py | 21 +- .../__pycache__/__init__.cpython-310.pyc | Bin 344 -> 928 bytes .../__pycache__/_internal.cpython-310.pyc | Bin 9959 -> 7050 bytes .../__pycache__/_reloader.cpython-310.pyc | Bin 12620 -> 12620 bytes .../__pycache__/exceptions.cpython-310.pyc | Bin 27320 -> 27320 bytes .../__pycache__/formparser.cpython-310.pyc | Bin 14761 -> 12642 bytes .../werkzeug/__pycache__/http.cpython-310.pyc | Bin 42949 -> 38885 bytes .../__pycache__/local.cpython-310.pyc | Bin 20916 -> 20916 bytes .../__pycache__/security.cpython-310.pyc | Bin 5731 -> 5347 bytes .../__pycache__/serving.cpython-310.pyc | Bin 30351 -> 30443 bytes .../werkzeug/__pycache__/test.cpython-310.pyc | Bin 45091 -> 42885 bytes .../__pycache__/testapp.cpython-310.pyc | Bin 6389 -> 6389 bytes .../werkzeug/__pycache__/urls.cpython-310.pyc | Bin 39968 -> 6441 bytes .../__pycache__/user_agent.cpython-310.pyc | Bin 1856 -> 1856 bytes .../__pycache__/utils.cpython-310.pyc | Bin 22274 -> 22274 bytes .../werkzeug/__pycache__/wsgi.cpython-310.pyc | Bin 26180 -> 19948 bytes venv/Lib/site-packages/werkzeug/_internal.py | 124 +- .../__pycache__/__init__.cpython-310.pyc | Bin 1550 -> 1550 bytes .../__pycache__/accept.cpython-310.pyc | Bin 10642 -> 10642 bytes .../__pycache__/auth.cpython-310.pyc | Bin 15696 -> 10465 bytes .../__pycache__/cache_control.cpython-310.pyc | Bin 6609 -> 6609 bytes .../__pycache__/csp.cpython-310.pyc | Bin 4145 -> 4145 bytes .../__pycache__/etag.cpython-310.pyc | Bin 3909 -> 3909 bytes .../__pycache__/file_storage.cpython-310.pyc | Bin 6058 -> 6058 bytes .../__pycache__/headers.cpython-310.pyc | Bin 18770 -> 17740 bytes .../__pycache__/mixins.cpython-310.pyc | Bin 9045 -> 9045 bytes .../__pycache__/range.cpython-310.pyc | Bin 6012 -> 6012 bytes .../__pycache__/structures.cpython-310.pyc | Bin 35929 -> 35929 bytes .../werkzeug/datastructures/auth.py | 202 +-- .../werkzeug/datastructures/headers.py | 65 +- .../__pycache__/__init__.cpython-310.pyc | Bin 14360 -> 14360 bytes .../debug/__pycache__/console.cpython-310.pyc | Bin 8314 -> 8314 bytes .../debug/__pycache__/repr.cpython-310.pyc | Bin 9065 -> 9065 bytes .../debug/__pycache__/tbtools.cpython-310.pyc | Bin 11796 -> 11796 bytes venv/Lib/site-packages/werkzeug/formparser.py | 150 +- venv/Lib/site-packages/werkzeug/http.py | 230 +-- .../__pycache__/__init__.cpython-310.pyc | Bin 181 -> 181 bytes .../__pycache__/dispatcher.cpython-310.pyc | Bin 2830 -> 2830 bytes .../__pycache__/http_proxy.cpython-310.pyc | Bin 6923 -> 6923 bytes .../__pycache__/lint.cpython-310.pyc | Bin 12984 -> 12984 bytes .../__pycache__/profiler.cpython-310.pyc | Bin 5065 -> 5622 bytes .../__pycache__/proxy_fix.cpython-310.pyc | Bin 6023 -> 6023 bytes .../__pycache__/shared_data.cpython-310.pyc | Bin 9275 -> 9275 bytes .../werkzeug/middleware/profiler.py | 19 +- .../__pycache__/__init__.cpython-310.pyc | Bin 4619 -> 4619 bytes .../__pycache__/converters.cpython-310.pyc | Bin 9414 -> 9146 bytes .../__pycache__/exceptions.cpython-310.pyc | Bin 5631 -> 5631 bytes .../routing/__pycache__/map.cpython-310.pyc | Bin 31556 -> 30921 bytes .../__pycache__/matcher.cpython-310.pyc | Bin 5124 -> 5124 bytes .../routing/__pycache__/rules.cpython-310.pyc | Bin 27712 -> 27654 bytes .../werkzeug/routing/converters.py | 13 +- .../Lib/site-packages/werkzeug/routing/map.py | 41 +- .../site-packages/werkzeug/routing/rules.py | 9 +- .../__pycache__/__init__.cpython-310.pyc | Bin 177 -> 177 bytes .../sansio/__pycache__/http.cpython-310.pyc | Bin 4700 -> 4127 bytes .../__pycache__/multipart.cpython-310.pyc | Bin 7426 -> 7487 bytes .../__pycache__/request.cpython-310.pyc | Bin 19644 -> 17292 bytes .../__pycache__/response.cpython-310.pyc | Bin 25300 -> 24511 bytes .../sansio/__pycache__/utils.cpython-310.pyc | Bin 4616 -> 4616 bytes .../Lib/site-packages/werkzeug/sansio/http.py | 37 +- .../werkzeug/sansio/multipart.py | 10 +- .../site-packages/werkzeug/sansio/request.py | 133 +- .../site-packages/werkzeug/sansio/response.py | 48 +- venv/Lib/site-packages/werkzeug/security.py | 25 +- venv/Lib/site-packages/werkzeug/serving.py | 10 +- venv/Lib/site-packages/werkzeug/test.py | 149 +- venv/Lib/site-packages/werkzeug/urls.py | 1247 +---------------- .../__pycache__/__init__.cpython-310.pyc | Bin 296 -> 296 bytes .../__pycache__/request.cpython-310.pyc | Bin 21706 -> 21644 bytes .../__pycache__/response.cpython-310.pyc | Bin 28485 -> 28414 bytes .../werkzeug/wrappers/request.py | 13 +- .../werkzeug/wrappers/response.py | 12 +- venv/Lib/site-packages/werkzeug/wsgi.py | 262 +--- venv/Scripts/dotenv.exe | Bin 108407 -> 108407 bytes venv/Scripts/flask.exe | Bin 108403 -> 108403 bytes 156 files changed, 1205 insertions(+), 6603 deletions(-) create mode 100644 config.py create mode 100644 migrations/versions/dd9a310269e3_initial_migration.py create mode 100644 migrations/versions/f83d55cc5aa7_increase_password_hash_length.py delete mode 100644 venv/Lib/site-packages/flask-2.3.3.dist-info/INSTALLER delete mode 100644 venv/Lib/site-packages/flask-2.3.3.dist-info/LICENSE.rst delete mode 100644 venv/Lib/site-packages/flask-2.3.3.dist-info/METADATA delete mode 100644 venv/Lib/site-packages/flask-2.3.3.dist-info/RECORD delete mode 100644 venv/Lib/site-packages/flask-2.3.3.dist-info/REQUESTED delete mode 100644 venv/Lib/site-packages/flask-2.3.3.dist-info/WHEEL delete mode 100644 venv/Lib/site-packages/flask-2.3.3.dist-info/entry_points.txt delete mode 100644 venv/Lib/site-packages/flask/__pycache__/scaffold.cpython-310.pyc delete mode 100644 venv/Lib/site-packages/flask/scaffold.py delete mode 100644 venv/Lib/site-packages/python_dotenv-1.0.0.dist-info/INSTALLER delete mode 100644 venv/Lib/site-packages/python_dotenv-1.0.0.dist-info/LICENSE delete mode 100644 venv/Lib/site-packages/python_dotenv-1.0.0.dist-info/METADATA delete mode 100644 venv/Lib/site-packages/python_dotenv-1.0.0.dist-info/RECORD delete mode 100644 venv/Lib/site-packages/python_dotenv-1.0.0.dist-info/REQUESTED delete mode 100644 venv/Lib/site-packages/python_dotenv-1.0.0.dist-info/WHEEL delete mode 100644 venv/Lib/site-packages/python_dotenv-1.0.0.dist-info/entry_points.txt delete mode 100644 venv/Lib/site-packages/python_dotenv-1.0.0.dist-info/top_level.txt delete mode 100644 venv/Lib/site-packages/werkzeug-2.3.7.dist-info/INSTALLER delete mode 100644 venv/Lib/site-packages/werkzeug-2.3.7.dist-info/LICENSE.rst delete mode 100644 venv/Lib/site-packages/werkzeug-2.3.7.dist-info/METADATA delete mode 100644 venv/Lib/site-packages/werkzeug-2.3.7.dist-info/RECORD delete mode 100644 venv/Lib/site-packages/werkzeug-2.3.7.dist-info/REQUESTED delete mode 100644 venv/Lib/site-packages/werkzeug-2.3.7.dist-info/WHEEL diff --git a/app.py b/app.py index 7d2517e..f540115 100644 --- a/app.py +++ b/app.py @@ -5,14 +5,10 @@ from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.utils import secure_filename import os +from config import Config app = Flask(__name__) -app.config['SECRET_KEY'] = os.urandom(24) -# Use DATABASE_URL from environment if available, else default to SQLite -app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///site.db') -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -app.config['UPLOAD_FOLDER'] = 'static/uploads' -app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024 # 2MB max file size +app.config.from_object(Config) os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) @@ -23,7 +19,7 @@ migrate = Migrate(app, db) class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) - password_hash = db.Column(db.String(128), nullable=False) + password_hash = db.Column(db.String(256), nullable=False) def set_password(self, password): self.password_hash = generate_password_hash(password) @@ -270,6 +266,14 @@ def manage_environments(): description = request.form['description'] icon_file = request.files.get('icon') icon_filename = None + if not name: + flash('Name is required!', 'danger') + print('No name provided') + return redirect(url_for('manage_environments')) + if Environment.query.filter_by(name=name).first(): + flash('Environment with this name already exists!', 'danger') + print('Duplicate environment name') + return redirect(url_for('manage_environments')) if icon_file and icon_file.filename: icon_filename = secure_filename(icon_file.filename) icon_path = os.path.join('static/icons', icon_filename) @@ -278,11 +282,18 @@ def manage_environments(): icon_file.save(icon_path) except Exception as e: print('Error saving icon:', e) + flash(f'Error saving icon: {e}', 'danger') + try: environment = Environment(name=name, description=description, icon=icon_filename) db.session.add(environment) db.session.commit() flash('Environment added successfully!', 'success') - return redirect(url_for('manage_environments')) + print('Environment added:', name) + except Exception as e: + db.session.rollback() + print('Error adding environment:', e) + flash(f'Error adding environment: {e}', 'danger') + return redirect(url_for('manage_environments')) environments = Environment.query.all() return render_template('manage_environments.html', environments=environments, @@ -339,6 +350,14 @@ def manage_climates(): description = request.form['description'] icon_file = request.files.get('icon') icon_filename = None + if not name: + flash('Name is required!', 'danger') + print('No name provided') + return redirect(url_for('manage_climates')) + if Climate.query.filter_by(name=name).first(): + flash('Climate with this name already exists!', 'danger') + print('Duplicate climate name') + return redirect(url_for('manage_climates')) if icon_file and icon_file.filename: icon_filename = secure_filename(icon_file.filename) icon_path = os.path.join('static/icons', icon_filename) @@ -347,11 +366,18 @@ def manage_climates(): icon_file.save(icon_path) except Exception as e: print('Error saving icon:', e) + flash(f'Error saving icon: {e}', 'danger') + try: climate = Climate(name=name, description=description, icon=icon_filename) db.session.add(climate) db.session.commit() flash('Climate added successfully!', 'success') - return redirect(url_for('manage_climates')) + print('Climate added:', name) + except Exception as e: + db.session.rollback() + print('Error adding climate:', e) + flash(f'Error adding climate: {e}', 'danger') + return redirect(url_for('manage_climates')) climates = Climate.query.all() return render_template('manage_climates.html', climates=climates, @@ -470,30 +496,59 @@ def new_plant(): picture_file = request.files.get('picture') picture_filename = None + # Required field checks + if not name: + flash('Name is required!', 'danger') + print('No name provided') + return redirect(url_for('new_plant')) + if not climate_id: + flash('Climate is required!', 'danger') + print('No climate provided') + return redirect(url_for('new_plant')) + if not environment_id: + flash('Environment is required!', 'danger') + print('No environment provided') + return redirect(url_for('new_plant')) + if Plant.query.filter_by(name=name).first(): + flash('A plant with this name already exists!', 'danger') + print('Duplicate plant name') + return redirect(url_for('new_plant')) + if picture_file and picture_file.filename: filename = secure_filename(picture_file.filename) picture_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) - picture_file.save(picture_path) - picture_filename = filename + try: + picture_file.save(picture_path) + picture_filename = filename + except Exception as e: + print('Error saving plant picture:', e) + flash(f'Error saving plant picture: {e}', 'danger') - plant = Plant( - name=name, - picture=picture_filename, - climate_id=climate_id, - environment_id=environment_id, - light_id=light_id, - toxicity_id=toxicity_id, - size_id=size_id, - care_difficulty_id=care_difficulty_id, - growth_rate_id=growth_rate_id, - products=','.join(product_ids), - description=description, - care_guide=care_guide - ) - db.session.add(plant) - db.session.commit() - flash('Your plant has been added!', 'success') - return redirect(url_for('home')) + try: + plant = Plant( + name=name, + picture=picture_filename, + climate_id=climate_id, + environment_id=environment_id, + light_id=light_id, + toxicity_id=toxicity_id, + size_id=size_id, + care_difficulty_id=care_difficulty_id, + growth_rate_id=growth_rate_id, + products=','.join(product_ids), + description=description, + care_guide=care_guide + ) + db.session.add(plant) + db.session.commit() + flash('Your plant has been added!', 'success') + print('Plant added:', name) + return redirect(url_for('manage_plants', add=1)) + except Exception as e: + db.session.rollback() + print('Error adding plant:', e) + flash(f'Error adding plant: {e}', 'danger') + return redirect(url_for('new_plant')) climates = Climate.query.all() environments = Environment.query.all() @@ -591,20 +646,37 @@ def manage_plants(): climates = Climate.query.all() environments = Environment.query.all() products = Product.query.all() + lights = Light.query.order_by(Light.name).all() + toxicities = Toxicity.query.order_by(Toxicity.name).all() + sizes = Size.query.order_by(Size.name).all() + difficulties = CareDifficulty.query.order_by(CareDifficulty.name).all() + growth_rates = GrowthRate.query.order_by(GrowthRate.name).all() + + # Create dictionaries for easy lookup + climates_dict = {climate.id: climate.name for climate in climates} + environments_dict = {env.id: env.name for env in environments} + return render_template('manage_plants.html', plants=plants, - climates=climates, - environments=environments, + climates=climates, # list for form + environments=environments, # list for form + climates_dict=climates_dict, # dict for table + environments_dict=environments_dict, # dict for table products=products, + lights=lights, + toxicities=toxicities, + sizes=sizes, + difficulties=difficulties, + growth_rates=growth_rates, plant_count=len(plants), climate_count=len(climates), environment_count=len(environments), product_count=len(products), - light_count=len(Light.query.all()), - toxicity_count=len(Toxicity.query.all()), - size_count=len(Size.query.all()), - difficulty_count=len(CareDifficulty.query.all()), - rate_count=len(GrowthRate.query.all())) + light_count=len(lights), + toxicity_count=len(toxicities), + size_count=len(sizes), + difficulty_count=len(difficulties), + rate_count=len(growth_rates)) @app.route('/admin/attributes') def manage_attributes(): @@ -634,6 +706,14 @@ def manage_lights(): description = request.form['description'] icon_file = request.files.get('icon') icon_filename = None + if not name: + flash('Name is required!', 'danger') + print('No name provided') + return redirect(url_for('manage_lights')) + if Light.query.filter_by(name=name).first(): + flash('Light requirement with this name already exists!', 'danger') + print('Duplicate light name') + return redirect(url_for('manage_lights')) if icon_file and icon_file.filename: icon_filename = secure_filename(icon_file.filename) icon_path = os.path.join('static/icons', icon_filename) @@ -642,11 +722,18 @@ def manage_lights(): icon_file.save(icon_path) except Exception as e: print('Error saving icon:', e) + flash(f'Error saving icon: {e}', 'danger') + try: light = Light(name=name, description=description, icon=icon_filename) db.session.add(light) db.session.commit() flash('Light requirement added successfully!', 'success') - return redirect(url_for('manage_lights')) + print('Light added:', name) + except Exception as e: + db.session.rollback() + print('Error adding light:', e) + flash(f'Error adding light: {e}', 'danger') + return redirect(url_for('manage_lights')) lights = Light.query.all() return render_template('manage_lights.html', lights=lights, @@ -703,6 +790,14 @@ def manage_toxicities(): description = request.form['description'] icon_file = request.files.get('icon') icon_filename = None + if not name: + flash('Name is required!', 'danger') + print('No name provided') + return redirect(url_for('manage_toxicities')) + if Toxicity.query.filter_by(name=name).first(): + flash('Toxicity with this name already exists!', 'danger') + print('Duplicate toxicity name') + return redirect(url_for('manage_toxicities')) if icon_file and icon_file.filename: icon_filename = secure_filename(icon_file.filename) icon_path = os.path.join('static/icons', icon_filename) @@ -711,11 +806,18 @@ def manage_toxicities(): icon_file.save(icon_path) except Exception as e: print('Error saving icon:', e) + flash(f'Error saving icon: {e}', 'danger') + try: toxicity = Toxicity(name=name, description=description, icon=icon_filename) db.session.add(toxicity) db.session.commit() flash('Toxicity level added successfully!', 'success') - return redirect(url_for('manage_toxicities')) + print('Toxicity added:', name) + except Exception as e: + db.session.rollback() + print('Error adding toxicity:', e) + flash(f'Error adding toxicity: {e}', 'danger') + return redirect(url_for('manage_toxicities')) toxicities = Toxicity.query.all() return render_template('manage_toxicities.html', toxicities=toxicities, @@ -772,6 +874,14 @@ def manage_sizes(): description = request.form['description'] icon_file = request.files.get('icon') icon_filename = None + if not name: + flash('Name is required!', 'danger') + print('No name provided') + return redirect(url_for('manage_sizes')) + if Size.query.filter_by(name=name).first(): + flash('Size with this name already exists!', 'danger') + print('Duplicate size name') + return redirect(url_for('manage_sizes')) if icon_file and icon_file.filename: icon_filename = secure_filename(icon_file.filename) icon_path = os.path.join('static/icons', icon_filename) @@ -780,11 +890,18 @@ def manage_sizes(): icon_file.save(icon_path) except Exception as e: print('Error saving icon:', e) + flash(f'Error saving icon: {e}', 'danger') + try: size = Size(name=name, description=description, icon=icon_filename) db.session.add(size) db.session.commit() flash('Size category added successfully!', 'success') - return redirect(url_for('manage_sizes')) + print('Size added:', name) + except Exception as e: + db.session.rollback() + print('Error adding size:', e) + flash(f'Error adding size: {e}', 'danger') + return redirect(url_for('manage_sizes')) sizes = Size.query.all() return render_template('manage_sizes.html', sizes=sizes, @@ -841,6 +958,14 @@ def manage_care_difficulties(): description = request.form['description'] icon_file = request.files.get('icon') icon_filename = None + if not name: + flash('Name is required!', 'danger') + print('No name provided') + return redirect(url_for('manage_care_difficulties')) + if CareDifficulty.query.filter_by(name=name).first(): + flash('Care difficulty with this name already exists!', 'danger') + print('Duplicate care difficulty name') + return redirect(url_for('manage_care_difficulties')) if icon_file and icon_file.filename: icon_filename = secure_filename(icon_file.filename) icon_path = os.path.join('static/icons', icon_filename) @@ -849,11 +974,18 @@ def manage_care_difficulties(): icon_file.save(icon_path) except Exception as e: print('Error saving icon:', e) + flash(f'Error saving icon: {e}', 'danger') + try: difficulty = CareDifficulty(name=name, description=description, icon=icon_filename) db.session.add(difficulty) db.session.commit() flash('Care difficulty level added successfully!', 'success') - return redirect(url_for('manage_care_difficulties')) + print('Care difficulty added:', name) + except Exception as e: + db.session.rollback() + print('Error adding care difficulty:', e) + flash(f'Error adding care difficulty: {e}', 'danger') + return redirect(url_for('manage_care_difficulties')) difficulties = CareDifficulty.query.all() return render_template('manage_care_difficulties.html', difficulties=difficulties, @@ -910,6 +1042,14 @@ def manage_growth_rates(): description = request.form['description'] icon_file = request.files.get('icon') icon_filename = None + if not name: + flash('Name is required!', 'danger') + print('No name provided') + return redirect(url_for('manage_growth_rates')) + if GrowthRate.query.filter_by(name=name).first(): + flash('Growth rate with this name already exists!', 'danger') + print('Duplicate growth rate name') + return redirect(url_for('manage_growth_rates')) if icon_file and icon_file.filename: icon_filename = secure_filename(icon_file.filename) icon_path = os.path.join('static/icons', icon_filename) @@ -918,11 +1058,18 @@ def manage_growth_rates(): icon_file.save(icon_path) except Exception as e: print('Error saving icon:', e) + flash(f'Error saving icon: {e}', 'danger') + try: rate = GrowthRate(name=name, description=description, icon=icon_filename) db.session.add(rate) db.session.commit() flash('Growth rate category added successfully!', 'success') - return redirect(url_for('manage_growth_rates')) + print('Growth rate added:', name) + except Exception as e: + db.session.rollback() + print('Error adding growth rate:', e) + flash(f'Error adding growth rate: {e}', 'danger') + return redirect(url_for('manage_growth_rates')) rates = GrowthRate.query.all() return render_template('manage_growth_rates.html', rates=rates, @@ -1147,4 +1294,15 @@ def seed_db(): db.session.commit() if __name__ == "__main__": + with app.app_context(): + db.create_all() # Create all tables + # Create admin user if it doesn't exist + admin = User.query.filter_by(username='admin').first() + if not admin: + admin = User(username='admin') + admin.set_password('admin123') # Set a default password + db.session.add(admin) + db.session.commit() + print("Admin user created with username: admin and password: admin123") + seed_db() # Seed initial data app.run(debug=True) \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..5282060 --- /dev/null +++ b/config.py @@ -0,0 +1,8 @@ +import os + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(24) + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'postgresql://postgres:1253@localhost:5432/verpotjelot' + SQLALCHEMY_TRACK_MODIFICATIONS = False + UPLOAD_FOLDER = 'static/uploads' + MAX_CONTENT_LENGTH = 2 * 1024 * 1024 # 2MB max file size \ No newline at end of file diff --git a/migrations/versions/dd9a310269e3_initial_migration.py b/migrations/versions/dd9a310269e3_initial_migration.py new file mode 100644 index 0000000..e267f2b --- /dev/null +++ b/migrations/versions/dd9a310269e3_initial_migration.py @@ -0,0 +1,130 @@ +"""Initial migration + +Revision ID: dd9a310269e3 +Revises: +Create Date: 2025-05-22 20:59:13.076749 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'dd9a310269e3' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('care_difficulty', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('icon', sa.String(length=200), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('climate', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('icon', sa.String(length=200), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('environment', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('icon', sa.String(length=200), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('growth_rate', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('icon', sa.String(length=200), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('light', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('icon', sa.String(length=200), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('product', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('size', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('icon', sa.String(length=200), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('toxicity', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('icon', sa.String(length=200), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=80), nullable=False), + sa.Column('password_hash', sa.String(length=128), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('username') + ) + op.create_table('plant', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('picture', sa.String(length=200), nullable=True), + sa.Column('climate_id', sa.Integer(), nullable=True), + sa.Column('environment_id', sa.Integer(), nullable=True), + sa.Column('light_id', sa.Integer(), nullable=True), + sa.Column('toxicity_id', sa.Integer(), nullable=True), + sa.Column('size_id', sa.Integer(), nullable=True), + sa.Column('care_difficulty_id', sa.Integer(), nullable=True), + sa.Column('growth_rate_id', sa.Integer(), nullable=True), + sa.Column('products', sa.Text(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('date_added', sa.DateTime(), nullable=False), + sa.Column('care_guide', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['care_difficulty_id'], ['care_difficulty.id'], ), + sa.ForeignKeyConstraint(['climate_id'], ['climate.id'], ), + sa.ForeignKeyConstraint(['environment_id'], ['environment.id'], ), + sa.ForeignKeyConstraint(['growth_rate_id'], ['growth_rate.id'], ), + sa.ForeignKeyConstraint(['light_id'], ['light.id'], ), + sa.ForeignKeyConstraint(['size_id'], ['size.id'], ), + sa.ForeignKeyConstraint(['toxicity_id'], ['toxicity.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('plant') + op.drop_table('user') + op.drop_table('toxicity') + op.drop_table('size') + op.drop_table('product') + op.drop_table('light') + op.drop_table('growth_rate') + op.drop_table('environment') + op.drop_table('climate') + op.drop_table('care_difficulty') + # ### end Alembic commands ### diff --git a/migrations/versions/f83d55cc5aa7_increase_password_hash_length.py b/migrations/versions/f83d55cc5aa7_increase_password_hash_length.py new file mode 100644 index 0000000..8f6f1ae --- /dev/null +++ b/migrations/versions/f83d55cc5aa7_increase_password_hash_length.py @@ -0,0 +1,38 @@ +"""Increase password hash length + +Revision ID: f83d55cc5aa7 +Revises: dd9a310269e3 +Create Date: 2025-05-22 21:01:20.068388 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f83d55cc5aa7' +down_revision = 'dd9a310269e3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.alter_column('password_hash', + existing_type=sa.VARCHAR(length=128), + type_=sa.String(length=256), + existing_nullable=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.alter_column('password_hash', + existing_type=sa.String(length=256), + type_=sa.VARCHAR(length=128), + existing_nullable=False) + + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index 29c4fa0..27f7956 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ -Flask==2.3.3 +Flask==3.0.2 Flask-SQLAlchemy==3.1.1 Flask-Login==0.6.2 Flask-WTF==1.2.1 Flask-Migrate==4.0.5 SQLAlchemy==2.0.23 -Werkzeug==2.3.7 +Werkzeug==3.0.1 WTForms==3.1.1 -python-dotenv==1.0.0 +python-dotenv==1.0.1 psycopg2-binary==2.9.9 gunicorn==21.2.0 \ No newline at end of file diff --git a/templates/create_plant.html b/templates/create_plant.html index e03c527..4f0f673 100644 --- a/templates/create_plant.html +++ b/templates/create_plant.html @@ -99,7 +99,16 @@ - +
+ + +
+
+ +
diff --git a/templates/manage_plants.html b/templates/manage_plants.html index 2b6d097..86694d0 100644 --- a/templates/manage_plants.html +++ b/templates/manage_plants.html @@ -9,12 +9,12 @@