Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
rdarneal committed Mar 30, 2023
0 parents commit 2f29780
Show file tree
Hide file tree
Showing 80 changed files with 10,030 additions and 0 deletions.
1 change: 1 addition & 0 deletions .flaskenv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FLASK_APP=blog.py
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__pycache__
notebooks/
.vscode
.env
.venv
23 changes: 23 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM python:slim

RUN useradd portfolio

WORKDIR /home/portfolio

COPY requirements.txt requirements.txt
RUN python -m venv venv
RUN venv/bin/pip install -r requirements.txt
RUN venv/bin/pip install gunicorn

COPY app app
COPY migrations migrations
COPY portfolio.py config.py boot.sh ./
RUN chmod +x boot.sh

ENV FLASK_APP portfolio.py

RUN chown -R portfolio:portfolio ./
USER portfolio

EXPOSE 5000
ENTRYPOINT ["./boot.sh"]
208 changes: 208 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
<!-- PROJECT LOGO -->
<br />
<div align="center">
<a href="https://github.com/rdarneal/flask-blog">
<img src="app/statics/ico/../../static/ico/android-chrome-192x192.png" alt="Logo" width="80" height="80">
</a>

<h3 align="center">flask-blog</h3>

<p align="center">
Flask blog template integrated with Bootstrap 5.3 and custom css styling. Based on the tutorial by <a href="https://github.com/miguelgrinberg/microblog/">Miguel Grinberg</a>
<br />
</p>
</div>



<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
<ol>
<li>
<a href="#about-the-project">About The Project</a>
<ul>
<li><a href="#built-with">Built With</a></li>
</ul>
</li>
<li>
<a href="#getting-started">Getting Started</a>
<ul>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#installation">Installation</a></li>
</ul>
</li>
<li><a href="#contact">Contact</a></li>
<li><a href="#acknowledgments">Acknowledgments</a></li>
</ol>
</details>



<!-- ABOUT THE PROJECT -->
## About The Project

This project includes a flask blueprint application divided primarily into 3 parts with various features:
- Blog
- Post
- Explore
- View Users
- Follow/Unfollow Users
- Search Posts
- Profile management
- Authentication
- Registration
- Login
- Password Reset
- Error handling

The app is configured to use a mysql or sqllite database, and the repo is set-up for running via [Docker](https://www.docker.com/).

The search posts capability is provided via [Elasticsearch](https://www.elastic.co/) and is also primarily done through docker-container registration.



<br />

![Flask-microblog](/app/static/img/demo-1-landing.png)

<p align="right">(<a href="#readme-top">back to top</a>)</p>


## Built With
* ![Python](https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54)
* ![Flask](https://img.shields.io/badge/flask-%23000.svg?style=for-the-badge&logo=flask&logoColor=white)
* [![Bootstrap][Bootstrap.com]][Bootstrap-url]
* [![JQuery][JQuery.com]][JQuery-url]
* ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)
* ![ElasticSearch](https://img.shields.io/badge/-ElasticSearch-005571?style=for-the-badge&logo=elasticsearch)
* ![MySQL](https://img.shields.io/badge/mysql-%2300f.svg?style=for-the-badge&logo=mysql&logoColor=white)

<p align="right">(<a href="#readme-top">back to top</a>)</p>



<!-- GETTING STARTED -->
## Getting Started

To get a local copy up and running follow these steps.

### Prerequisites

To fully try this project on your machine you will need at minimum:
* [Python](https://www.python.org/downloads/)
* [Docker](https://www.docker.com/)
* [VSCode](https://code.visualstudio.com/)

### Installation

1. Clone the repo
```sh
git clone https://github.com/rdarneal/flask-blog.git
```
2. Navigate to the new folder and open with VS Code
```sh
cd flask-blog
code .
```
3. Create a new virtual environemnt folder `venv`
```sh
python -m venv venv
```
4. Activate the virtual environment
```sh
venv/bin/activate
```
5. Install Python packages
```sh
pip install requirements.txt
```
6. Set the `.flaskenv` file to match your top level `blog.py` filename
```sh
FLASK_APP=blog.py
```
7. Create a new `.env` file with vs code. IMPORTANT: Include `.env` in your `.gitignore` file! Don't share secrets.
```sh
SECRET_KEY='yoursecretstring'
MAIL_SERVER='your.smtp.mailserver.com'
MAIL_PORT= 443
MAIL_USERNAME = 'mailusername'
MAIL_PASSWORD = 'mailpassword'
MAIL_USE_TLS = 1
ELASTICSEARCH_URL = 'http://localhost:9200'
```
8. Run the app locally to test it:
```sh
flask --debug run
```
9. Note that when running with this method, search funtionality will only work if you have an activate Elasticsearch docker image. See docker method below for the command to launch a elasticsearch container.

<br />

### Deploying via Docker
The application is also set-up to be fully deployable via docker containers.

To use the docker method you must first build the application
```sh
docker build -t blog:latest .
```

Three containers are required: MySQL, Elasticsearch, and the blog applciation.
1. Each container can be launched sequentially
- Mysql
```sh
docker run --name mysql -d -e MYSQL_RANDOM_ROOT_PASSWORD=yes \
-e MYSQL_DATABASE=flask-blog -e MYSQL_USER=flask-blog \
-e MYSQL_PASSWORD=<database-password> \
mysql/mysql-server:latest
```
- Elasticsearch
```sh
docker run --name elasticsearch -d \
-p 9200:9200 -p 9300:9300 \
--rm -e "discovery.type=single-node" \
docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2
```
- The app (-e sets the environment variables for the docker container)
```sh
docker run --name flask-blog -d -p 8000:5000 --rm -e SECRET_KEY=my-secret-key \
-e MAIL_SERVER=smtp.email.com -e MAIL_PORT=587 -e MAIL_USE_TLS=true \
-e MAIL_USERNAME=<your-email-username> -e MAIL_PASSWORD=<your-email-password> \
--link mysql:dbserver \
-e DATABASE_URL=mysql+pymysql://flask-blog:<database-password>@dbserver/flask-blog \
--link elasticsearch:elasticsearch \
-e ELASTICSEARCH_URL=http://elasticsearch:9200 \
flask-blog:latest
```

<p align="right">(<a href="#readme-top">back to top</a>)</p>


<!-- CONTACT -->
## Contact

Robert Darneal - python-dev@robertdarneal.com

Project Link: [https://github.com/rdarneal/flask-blog](https://github.com/rdarneal/flask-blog)

<p align="right">(<a href="#readme-top">back to top</a>)</p>



<!-- ACKNOWLEDGMENTS -->
## Acknowledgments

* [Miguel Grinberg's Microblog Tutorial](https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world)
* [Stackoverflow](https://stackoverflow.com/)
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white
[Bootstrap-url]: https://getbootstrap.com
[JQuery.com]: https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery&logoColor=white
[JQuery-url]: https://jquery.com
Binary file added app.db
Binary file not shown.
78 changes: 78 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import os
import logging
from flask import Flask, request, current_app
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_mail import Mail
from flask_moment import Moment
from logging.handlers import RotatingFileHandler, SMTPHandler
from elasticsearch import Elasticsearch

db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
login.login_view = 'auth.login'
login.login_message = ('Please log in to access this page', 'info')
mail = Mail()
moment = Moment()


def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)

db.init_app(app)
migrate.init_app(app, db)
login.init_app(app)
mail.init_app(app)
moment.init_app(app)
app.elasticsearch = Elasticsearch([app.config['ELASTICSEARCH_URL']]) \
if app.config['ELASTICSEARCH_URL'] else None

from app.errors import bp as errors_bp
app.register_blueprint(errors_bp)

from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')

from app.main import bp as main_bp
app.register_blueprint(main_bp)

if not app.debug and not app.testing:
# Mail Handling
if app.config['MAIL_SERVER']:
auth = None
if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
secure = None
if app.config['MAIL_USE_TLS']:
secure = ()
mail_handler = SMTPHandler(
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
fromaddr='no-reply@' + app.config['MAIL_SERVER'],
toaddrs=app.config['ADMINS'],
subject='Microblog Failure',
credentials=auth,
secure=secure
)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)

if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler('logs/app.log', maxBytes=10240,
backupCount=10)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)

app.logger.setLevel(logging.INFO)
app.logger.info('Startup')

return app

from app import models
5 changes: 5 additions & 0 deletions app/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from flask import Blueprint

bp = Blueprint('auth', __name__)

from app.auth import routes
15 changes: 15 additions & 0 deletions app/auth/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from flask import render_template, current_app
from app.email import send_email

def send_password_reset_email(user):
token = user.get_reset_password_token()
send_email('[Microblog] Reset Your Password',
sender=current_app.config['ADMINS'][0],
recipients=[user.email],
text_body=render_template('email/reset_password.txt',
user=user,
token=token
),
html_body=render_template('email/reset_password.html',
user=user,
token=token))
35 changes: 35 additions & 0 deletions app/auth/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, ValidationError, Email, EqualTo, Length
from app.models import User

class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In')


class RegistrationForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
first_name = StringField('First Name', validators=[DataRequired()])
last_name = StringField('Last Name')
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField('Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')

def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('This email address is already registered')


class ResetPasswordRequestForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
submit = SubmitField('Request Password Reset')


class ResetPasswordForm(FlaskForm):
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField('Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Request Password Reset')
Loading

0 comments on commit 2f29780

Please sign in to comment.