Initial Commit
This commit is contained in:
240
app.py
Normal file
240
app.py
Normal file
@@ -0,0 +1,240 @@
|
||||
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/<slug>')
|
||||
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/<slug>')
|
||||
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)
|
||||
Reference in New Issue
Block a user