from flask import Flask, request, redirect, url_for, render_template, session, flash from flask_sqlalchemy import SQLAlchemy from sqlalchemy import func from dotenv import load_dotenv import os from werkzeug.utils import secure_filename from flask_login import LoginManager, login_required, logout_user, UserMixin, login_user from flask_mail import Mail, Message import json from slugify import slugify load_dotenv() app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///posts.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['UPLOAD_FOLDER'] = 'static/uploads' app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY') app.config['MAIL_SERVER'] = 'mail.bennyshouse.net' app.config['MAIL_PORT'] = 587 app.config['MAIL_USE_TLS'] = True app.config['MAIL_USERNAME'] = os.getenv("MAIL_USERNAME") app.config['MAIL_PASSWORD'] = os.getenv("MAIL_PASSWORD") app.config['MAIL_DEFAULT_SENDER'] = os.getenv("MAIL_DEFAULT_SENDER") mail = Mail(app) db = SQLAlchemy(app) login_manager = LoginManager(app) login_manager.login_view = 'login' USERNAME = os.getenv('FLASK_LOGIN_USER') PASSWORD = os.getenv('FLASK_LOGIN_PASSWORD') if not os.path.exists(app.config['UPLOAD_FOLDER']): os.makedirs(app.config['UPLOAD_FOLDER']) class User(UserMixin): id = 1 @login_manager.user_loader def load_user(user_id): if user_id == "1": return User() return None class BlogPost(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(255), nullable=False) slug = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) images = db.Column(db.Text, nullable=True) category = db.Column(db.String(100), nullable=True) tags = db.Column(db.Text, nullable=True) pinned = db.Column(db.Boolean, default=False) class ProjectPost(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(255), nullable=False) slug = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) images = db.Column(db.Text, nullable=True) category = db.Column(db.String(100), nullable=True) tags = db.Column(db.Text, nullable=True) pinned = db.Column(db.Boolean, default=False) with app.app_context(): db.create_all() @app.template_filter('from_json') def from_json(value): try: return json.loads(value) except (TypeError, json.JSONDecodeError): return [] @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": username = request.form["username"] password = request.form["password"] if username == USERNAME and password == PASSWORD: user = User() login_user(user) session.permanent = True return redirect(url_for("admin_panel")) flash("Invalid email or password!", "danger") return render_template("login.html") @app.route("/logout") @login_required def logout(): logout_user() return redirect(url_for("index")) @app.route('/admin') @login_required def admin_panel(): blogpost = BlogPost.query.order_by(BlogPost.id.desc()).all() projectpost = ProjectPost.query.order_by(ProjectPost.id.desc()).all() return render_template('admin.html', blogpost=blogpost, projectpost=projectpost) @app.route('/newblog', methods=['GET', 'POST']) @login_required def new_blog(): if request.method == 'POST': title = request.form['title'] content = request.form['content'].replace("\n", "
") category = request.form['category'] tags = request.form['tags'] pinned = 'pinned' in request.form images = request.files.getlist('images') slug = slugify(title) image_filenames = [secure_filename(image.filename) for image in images if image.filename] for image, filename in zip(images, image_filenames): image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) new_post = BlogPost( title=title, slug=slug, content=content, category=category, tags=tags, pinned=pinned, images=json.dumps(image_filenames) ) db.session.add(new_post) db.session.commit() return redirect(url_for('blog')) return render_template('new_blog.html') @app.route('/newproject', methods=['GET', 'POST']) @login_required def new_project(): if request.method == 'POST': title = request.form['title'] content = request.form['content'].replace("\n", "
") category = request.form['category'] tags = request.form['tags'] pinned = 'pinned' in request.form images = request.files.getlist('images') slug = slugify(title) image_filenames = [secure_filename(image.filename) for image in images if image.filename] for image, filename in zip(images, image_filenames): image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) new_post = ProjectPost( title=title, slug=slug, content=content, category=category, tags=tags, pinned=pinned, images=json.dumps(image_filenames) ) db.session.add(new_post) db.session.commit() return redirect(url_for('projects')) return render_template('new_project.html') @app.route('/edit-blog/', methods=['GET', 'POST']) @login_required def edit_blog(slug): blogpost = BlogPost.query.filter_by(slug=slug).first_or_404() if request.method == 'POST': blogpost.title = request.form['title'] blogpost.slug = slugify(blogpost.title) blogpost.content = request.form['content'].replace("\n", "
") blogpost.category = request.form['category'] blogpost.tags = request.form['tags'] blogpost.pinned = 'pinned' in request.form images = request.files.getlist('images') image_filenames = json.loads(blogpost.images) if blogpost.images else [] for image in images: if image.filename: filename = secure_filename(image.filename) image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) image_filenames.append(filename) blogpost.images = json.dumps(image_filenames) db.session.commit() return redirect(url_for('view_blog', slug=blogpost.slug)) return render_template('edit_blog.html', blogpost=blogpost) @app.route('/edit-project/', methods=['GET', 'POST']) @login_required def edit_project(slug): projectpost = ProjectPost.query.filter_by(slug=slug).first_or_404() if request.method == 'POST': projectpost.title = request.form['title'] projectpost.slug = slugify(projectpost.title) projectpost.content = request.form['content'].replace("\n", "
") projectpost.category = request.form['category'] projectpost.tags = request.form['tags'] projectpost.pinned = 'pinned' in request.form images = request.files.getlist('images') image_filenames = json.loads(projectpost.images) if projectpost.images else [] for image in images: if image.filename: filename = secure_filename(image.filename) image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) image_filenames.append(filename) projectpost.images = json.dumps(image_filenames) db.session.commit() return redirect(url_for('view_project', slug=projectpost.slug)) return render_template('edit_project.html', projectpost=projectpost) @app.route('/delete-blog/', methods=['POST']) @login_required def delete_blog(slug): blogpost = BlogPost.query.filter_by(slug=slug).first_or_404() if blogpost.images: for filename in json.loads(blogpost.images): image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) if os.path.exists(image_path): os.remove(image_path) db.session.delete(blogpost) db.session.commit() return redirect(url_for('admin_panel')) @app.route('/delete-project/', methods=['POST']) @login_required def delete_project(slug): projectpost = ProjectPost.query.filter_by(slug=slug).first_or_404() if projectpost.images: for filename in json.loads(projectpost.images): image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) if os.path.exists(image_path): os.remove(image_path) db.session.delete(projectpost) db.session.commit() return redirect(url_for('admin_panel')) @app.route('/blogtag/') def view_tag(tag): tag_lower = tag.lower() # Normalize case blogpost = BlogPost.query.filter(func.lower(BlogPost.tags).contains(tag_lower)).order_by(BlogPost.id.desc()).all() return render_template('blog_tag_results.html', blogpost=blogpost, tag=tag) @app.route('/projecttag/') def view_project_tag(tag): tag_lower = tag.lower() # Normalize case projectpost = ProjectPost.query.filter(func.lower(ProjectPost.tags).contains(tag_lower)).order_by(ProjectPost.id.desc()).all() return render_template('project_tag_results.html', projectpost=projectpost, tag=tag) @app.route('/blog') def blog(): blogpost = BlogPost.query.order_by(BlogPost.id.desc()).all() return render_template('blog.html', blogpost=blogpost) @app.route('/projects') def projects(): projectpost = ProjectPost.query.order_by(ProjectPost.id.desc()).all() return render_template('project.html', projectpost=projectpost) @app.route('/blog/') def view_blog(slug): blogpost = BlogPost.query.filter_by(slug=slug).first_or_404() return render_template('view_blog.html', blogpost=blogpost) @app.route('/project/') def view_project(slug): projectpost = ProjectPost.query.filter_by(slug=slug).first_or_404() return render_template('view_project.html', projectpost=projectpost) @app.route("/contact", methods=["GET", "POST"]) def contact(): if request.method == "POST": name = request.form.get("name") email = request.form.get("email") message = request.form.get("message") # Compose the email msg = Message(subject=f"New Contact Message from {name}", sender=("Portfolio Updates", "ben@bennyshouse.net"), recipients=["ben@bennyshouse.net"], body=f"Name: {name}\nEmail: {email}\n\n{message}") try: mail.send(msg) flash("Your message has been sent!", "success") except Exception as e: flash("Something went wrong. Please try again later.", "error") print(f"Email error: {e}") return redirect(url_for("contact")) return render_template("contact.html") @app.route('/resume') def resume(): return render_template('resume.html') @app.route('/about') def about(): return render_template('about.html') @app.route('/') def index(): return render_template('index.html') if __name__ == '__main__': app.run(debug=True)