commit 155f90340a845c9fc4519ca7fb45a18648be56ec Author: Ben Mosley Date: Mon Apr 14 11:31:32 2025 -0500 First Commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..38d284a Binary files /dev/null and b/.DS_Store differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..c51a074 --- /dev/null +++ b/app.py @@ -0,0 +1,320 @@ +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) \ No newline at end of file diff --git a/templates/about.html b/templates/about.html new file mode 100644 index 0000000..4dc52fe --- /dev/null +++ b/templates/about.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block title %}About{% endblock %} + +{% block content %} + + +
+

About Me

+

Learn more about who I am, what I do, and what drives me.

+
+ + +
+
+ + +
+

Who Am I?

+

+ I'm a passionate developer and tech enthusiast focused on building useful, secure, and accessible digital experiences. + Whether it’s creating full-stack web applications, designing clean UIs, or managing network infrastructure — I love what I do. +

+
+ + +
+

Skills & Tools

+
    +
  • Python & Flask
  • +
  • HTML CSS and Javascript
  • +
  • SQL (MYSQL, SQLite)
  • +
  • Linux system administration
  • +
  • Networking & Security (WireGuard, Samba, DNS-Server)
  • +
  • Git & Deployment
  • +
  • API integrations
  • +
+
+ + +
+

What I'm About

+

+ I believe technology should empower people — not confuse them. I enjoy creating tools that are intuitive, helpful, and secure. + I'm always learning, whether it’s diving into cybersecurity, mastering backend frameworks, or exploring automation and DevOps. +

+
+ +
+
+ +{% endblock %} diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..41eca6f --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,100 @@ +{% extends "base.html" %} + +{% block title %}Admin{% endblock %} + +{% block content %} + +
+

Admin Dashboard

+

Manage your blog posts and projects here.

+
+ +
+ + +
+
+

Project Options

+ + New Project +
+ +
+ + + + + + + + + + {% for projectpost in projectpost %} + + + + + + {% endfor %} + +
TitleCategoryActions
{{ projectpost.title }}{{ projectpost.category }} + View + Edit +
+ +
+
+
+
+ + +
+
+

Blog Options

+ + New Blog Post +
+ +
+ + + + + + + + + + {% for blogpost in blogpost %} + + + + + + {% endfor %} + +
TitleCategoryActions
{{ blogpost.title }}{{ blogpost.category }} + View + Edit +
+ +
+
+
+
+ +
+ +{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..bc12ccb --- /dev/null +++ b/templates/base.html @@ -0,0 +1,85 @@ + + + + + + + + + + {% block title %}Benjamin Mosley{% endblock %} + + + + + + + + + + + +
+
+ +

+ Benjamin Mosley +

+ + + + + + +
+ + +
+ +
+
+ + + + + +
+ {% block content %}{% endblock %} +
+ + + + + \ No newline at end of file diff --git a/templates/blog.html b/templates/blog.html new file mode 100644 index 0000000..e06edae --- /dev/null +++ b/templates/blog.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} + +{% block title %}Blog{% endblock %} + +{% block content %} + + +
+

Blog

+

Insights, tutorials, and personal thoughts — straight from my mind to the page.

+
+ + +
+
+
+ {% for blogpost in blogpost %} +
+

+ {{ blogpost.title }} +

+

{{ blogpost.category }}

+ +
+ {% for tag in blogpost.tags.split(',') %} + + #{{ tag.strip() }} + + {% endfor %} +
+
+ {% endfor %} +
+
+
+ +{% endblock %} diff --git a/templates/blog_tag_results.html b/templates/blog_tag_results.html new file mode 100644 index 0000000..b4add6a --- /dev/null +++ b/templates/blog_tag_results.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block title %}Tag Results{% endblock %} + +{% block content %} + + +
+

Posts Tagged: #{{ tag }}

+

Here are all blog posts with this tag.

+
+ + +
+
+ {% if blogpost %} +
+ {% for blogpost in blogpost %} +
+

+ + {{ blogpost.title }} + +

+

{{ blogpost.category }}

+
+ {% endfor %} +
+ {% else %} +

No posts found with this tag.

+ {% endif %} +
+
+ +{% endblock %} diff --git a/templates/contact.html b/templates/contact.html new file mode 100644 index 0000000..f976ac2 --- /dev/null +++ b/templates/contact.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block title %}Contact{% endblock %} + +{% block content %} + +
+

Contact Me

+

Looking to connect? Here's where you can reach me!

+
+ +
+
+
+ + +
+

Get in Touch

+

Feel free to drop me a message via the form or through any of the channels below:

+ +
+ + + + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+
+ +{% endblock %} diff --git a/templates/edit_blog.html b/templates/edit_blog.html new file mode 100644 index 0000000..5307d60 --- /dev/null +++ b/templates/edit_blog.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block title %}Edit Blog{% endblock %} + +{% block content %} + +
+ + + + + + + + + + + + + + + + + +
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/edit_project.html b/templates/edit_project.html new file mode 100644 index 0000000..d2623cb --- /dev/null +++ b/templates/edit_project.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block title %}Edit Project{% endblock %} + +{% block content %} +
+ + + + + + + + + + + + + + + + + +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..67c0f26 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} + +{% block title %}Benjamin Mosley{% endblock %} + +{% block content %} + +
+

Benjamin Mosley's Personal Portfolio

+Benjamin Mosley +

I am Benjamin Mosley, a Computer Information Systems student with +a passion for building, creating, and learning.

+ +
+ +
+
+

Contact Me

+

Have any questions? Reach out to me here

+ +
+ +Learn More +
+ +
+

My Projects

+

Check out my latest projects!

+ +
+ +Learn More +
+ +
+

My Blog

+

Read my latest Blog-Posts!

+ +
+ +Learn More +
+ +
+ +{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..28db1d7 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,66 @@ + + + + + + + Login + + + + + + \ No newline at end of file diff --git a/templates/new_blog.html b/templates/new_blog.html new file mode 100644 index 0000000..1c582fe --- /dev/null +++ b/templates/new_blog.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} + +{% block title %}New Blog Post{% endblock %} + +{% block content %} + +
+

Create a New Blog Post

+

Share your thoughts, tutorials, or experiences with the world.

+
+ +
+
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + +
+ +
+
+ + +
+
+ + + + +{% endblock %} diff --git a/templates/new_project.html b/templates/new_project.html new file mode 100644 index 0000000..3db3169 --- /dev/null +++ b/templates/new_project.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} + +{% block title %}New Project Post{% endblock %} + +{% block content %} + +
+

Create a New Project Post

+

Share your projects with the world.

+
+ +
+
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + +
+ +
+
+ + +
+
+ + + + +{% endblock %} diff --git a/templates/project.html b/templates/project.html new file mode 100644 index 0000000..43b0969 --- /dev/null +++ b/templates/project.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} + +{% block title %}Projects{% endblock %} + +{% block content %} + + +
+

My Projects

+

Here’s a showcase of the things I’ve built or contributed to.

+
+ + + +
+
+
+ {% for projectpost in projectpost %} +
+

+ {{ projectpost.title }} +

+

{{ projectpost.category }}

+
+ {% for tag in projectpost.tags.split(',') %} + + #{{ tag.strip() }} + + {% endfor %} +
+
+ {% endfor %} +
+
+
+ +{% endblock %} diff --git a/templates/project_tag_results.html b/templates/project_tag_results.html new file mode 100644 index 0000000..6351bbf --- /dev/null +++ b/templates/project_tag_results.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block title %}Tag Results{% endblock %} + +{% block content %} + + +
+

Posts Tagged: #{{ tag }}

+

Here are all Project Posts with this tag.

+
+ + +
+
+ {% if projectpost %} +
+ {% for projectpost in projectpost %} +
+

+ + {{ projectpost.title }} + +

+

{{ projectpost.category }}

+
+ {% endfor %} +
+ {% else %} +

No posts found with this tag.

+ {% endif %} +
+
+ +{% endblock %} diff --git a/templates/resume.html b/templates/resume.html new file mode 100644 index 0000000..8665403 --- /dev/null +++ b/templates/resume.html @@ -0,0 +1,95 @@ +{% extends "base.html" %} + +{% block title %}About Me{% endblock %} + +{% block content %} + + +
+

Benjamin Mosley

+

Aspiring IT Professional & Web Developer

+
+ + +
+
+ + +
+

Email: ben@bennyshouse.net

+

Portfolio: benjaminmosley.com

+

Business Website: bennyshouse.net

+

LinkedIn: Benjamin Mosley

+

GitHub: github.com/Benny-ui-ux

+
+ + +
+

Education

+
    +
  • High School Graduation: Mother of Divine Grace – June 2022
  • +
  • College: West Texas A&M University
    + Currently pursuing B.S. in Computer Information Systems, graduating Spring 2026 +
  • +
+
+ + +
+

Background

+

+ I’ve been working with computers for as long as I can remember — what started as curiosity has grown into a passion. My technical journey continues to evolve through real-world experience and ongoing education. +

+
+ + +
+

Skills

+
    +
  • Python Programming
  • +
  • Linux Operating Systems
  • +
  • Web Design & Hosting
  • +
  • SQL & Database Management
  • +
  • Leadership & Teamwork
  • +
+
+ + +
+

Work Experience

+
+ +
+

United Supermarkets – Borger, TX

+

Grocery Team Lead | Nov 2019 – Aug 2022

+
    +
  • Stocked shelves and maintained floor organization
  • +
  • Assisted customers and team members
  • +
  • Managed ordering and inventory processes
  • +
+
+ +
+

The Pergola Shop – Canyon, TX

+

Wood Stainer / Cutter | Aug 2022 – Aug 2023

+
    +
  • Prepped and treated wood materials for construction
  • +
+
+ +
+

United Supermarkets – Canyon, TX

+

Service Center / Bookkeeper | Aug 2023 – Present

+
    +
  • Nightly accounting duties and managment of my team
  • +
  • Provided high-quality customer service
  • +
+
+ +
+
+ +
+
+ +{% endblock %} diff --git a/templates/view_blog.html b/templates/view_blog.html new file mode 100644 index 0000000..1c97f79 --- /dev/null +++ b/templates/view_blog.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} + +{% block title %}{{ blogpost.title }}{% endblock %} + +{% block content %} + + +
+

{{ blogpost.title }}

+

Category: {{ blogpost.category }}

+
+ {% for tag in blogpost.tags.split(',') %} + + #{{ tag.strip() }} + + {% endfor %} +
+
+ + +
+
+ + {% if blogpost.images %} +
+

Gallery

+ + +
+ {% endif %} + + + + +
+ {{ blogpost.content | safe }} +
+ + + + +
+
+ +{% endblock %} diff --git a/templates/view_project.html b/templates/view_project.html new file mode 100644 index 0000000..5b05123 --- /dev/null +++ b/templates/view_project.html @@ -0,0 +1,77 @@ +{% extends "base.html" %} + +{% block title %}{{ projectpost.title }}{% endblock %} + +{% block content %} + + +
+

{{ projectpost.title }}

+

Category: {{ projectpost.category }}

+
+ {% for tag in projectpost.tags.split(',') %} + + #{{ tag.strip() }} + + {% endfor %} +
+
+ + +
+
+ + {% if projectpost.images %} +
+

Gallery

+ + +
+ {% endif %} + + + +
+ {{ projectpost.content | safe }} +
+ + + + +
+
+ +{% endblock %}