-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 2f29780
Showing
80 changed files
with
10,030 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
FLASK_APP=blog.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
__pycache__ | ||
notebooks/ | ||
.vscode | ||
.env | ||
.venv |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
Oops, something went wrong.