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 @@