import os import json import uuid from flask import Flask, render_template, request, redirect, url_for, flash from flask_sqlalchemy import SQLAlchemy from dotenv import load_dotenv from werkzeug.utils import secure_filename from slugify import slugify load_dotenv() app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///info.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'static', 'uploads') app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'dev-secret-key-change-me') os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) db = SQLAlchemy(app) ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} def allowed_image(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_IMAGE_EXTENSIONS def unique_slug(base_title, model): base = slugify(base_title) slug = base suffix = 2 while model.query.filter_by(slug=slug).first() is not None: slug = f'{base}-{suffix}' suffix += 1 return slug class Goal(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(255), nullable=False) description = db.Column(db.Text, nullable=False) images = db.Column(db.Text, nullable=True) goal_amount = db.Column(db.Integer, nullable=False) completed = db.Column(db.Boolean, default=False, nullable=False) slug = db.Column(db.String(255), nullable=False, unique=True) babysteps = db.relationship('BabyStep', backref='goal', lazy=True, cascade='all, delete-orphan') @property def image_list(self): return json.loads(self.images) if self.images else [] @property def saved_amount(self): return sum(step.saved_amount for step in self.babysteps) @property def progress_percent(self): if not self.goal_amount: return 0 return min(100, round(self.saved_amount / self.goal_amount * 100)) class BabyStep(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(255), nullable=False) description = db.Column(db.Text, nullable=False) monthly_amount = db.Column(db.Integer, nullable=False) completed = db.Column(db.Boolean, default=False, nullable=False) slug = db.Column(db.String(255), nullable=False, unique=True) goal_id = db.Column(db.Integer, db.ForeignKey('goal.id'), nullable=False) payments = db.relationship('Payment', backref='babystep', lazy=True, cascade='all, delete-orphan') @property def saved_amount(self): return sum(payment.amount for payment in self.payments) @property def progress_percent(self): if not self.monthly_amount: return 0 return min(100, round(self.saved_amount / self.monthly_amount * 100)) class Payment(db.Model): id = db.Column(db.Integer, primary_key=True) description = db.Column(db.Text, nullable=False) amount = db.Column(db.Integer, nullable=False) received_at = db.Column(db.DateTime, server_default=db.func.now()) babystep_id = db.Column(db.Integer, db.ForeignKey('baby_step.id'), nullable=False) @app.route('/') def index(): goals = Goal.query.order_by(Goal.id.desc()).all() return render_template('index.html', goals=goals) @app.route('/goals') def view_goals(): goals = Goal.query.order_by(Goal.id.desc()).all() return render_template('goals.html', goals=goals) @app.route('/goals/') def view_goal(slug): goal = Goal.query.filter_by(slug=slug).first_or_404() return render_template('goal_detail.html', goal=goal) @app.route('/babysteps') def view_babysteps(): babysteps = BabyStep.query.order_by(BabyStep.id.desc()).all() return render_template('babysteps.html', babysteps=babysteps) @app.route('/babysteps/') def view_babystep(slug): babystep = BabyStep.query.filter_by(slug=slug).first_or_404() return render_template('babystep_detail.html', babystep=babystep) @app.route('/payments') def view_payments(): payments = Payment.query.order_by(Payment.id.desc()).all() return render_template('payments.html', payments=payments) @app.route('/newgoal', methods=['GET', 'POST']) def new_goal(): if request.method == 'POST': title = request.form.get('title', '').strip() description = request.form.get('description', '').strip() goal_amount = request.form.get('goal_amount', type=int) completed = 'completed' in request.form images = request.files.getlist('images') if not title or goal_amount is None: flash('Title and a valid goal amount are required.') return render_template('new_goal.html') image_filenames = [] for image in images: if image.filename and allowed_image(image.filename): filename = f'{uuid.uuid4().hex}_{secure_filename(image.filename)}' image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) image_filenames.append(filename) goal = Goal( title=title, description=description, images=json.dumps(image_filenames), goal_amount=goal_amount, completed=completed, slug=unique_slug(title, Goal), ) db.session.add(goal) db.session.commit() flash(f'Goal "{goal.title}" created.') return redirect(url_for('view_goal', slug=goal.slug)) return render_template('new_goal.html') @app.route('/newbabystep', methods=['GET', 'POST']) def new_babystep(): goals = Goal.query.order_by(Goal.title).all() if request.method == 'POST': title = request.form.get('title', '').strip() description = request.form.get('description', '').strip() monthly_amount = request.form.get('monthly_amount', type=int) goal_id = request.form.get('goal_id', type=int) completed = 'completed' in request.form goal = db.session.get(Goal, goal_id) if goal_id else None if not title or monthly_amount is None or goal is None: flash('Title, a valid monthly amount, and a goal are required.') return render_template('new_babystep.html', goals=goals) babystep = BabyStep( title=title, description=description, monthly_amount=monthly_amount, completed=completed, slug=unique_slug(title, BabyStep), goal_id=goal.id, ) db.session.add(babystep) db.session.commit() flash(f'Babystep "{babystep.title}" added to "{goal.title}".') return redirect(url_for('view_babystep', slug=babystep.slug)) return render_template('new_babystep.html', goals=goals) @app.route('/newpayment', methods=['GET', 'POST']) def new_payment(): babysteps = BabyStep.query.order_by(BabyStep.title).all() if request.method == 'POST': description = request.form.get('description', '').strip() amount = request.form.get('amount', type=int) babystep_id = request.form.get('babystep_id', type=int) babystep = db.session.get(BabyStep, babystep_id) if babystep_id else None if not description or amount is None or babystep is None: flash('Description, a valid amount, and a babystep are required.') return render_template('new_payment.html', babysteps=babysteps) payment = Payment( description=description, amount=amount, babystep_id=babystep.id, ) db.session.add(payment) db.session.commit() flash(f'Payment of {amount} applied to "{babystep.title}".') return redirect(url_for('view_babystep', slug=babystep.slug)) return render_template('new_payment.html', babysteps=babysteps) def init_db(): with app.app_context(): db.create_all() if __name__ == '__main__': init_db() app.run(debug=False)