parent
0f0f148418
commit
37415e93e3
Binary file not shown.
Binary file not shown.
@ -0,0 +1,44 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_bcrypt import Bcrypt
|
||||
from flask_login import LoginManager
|
||||
from flask_mail import Mail
|
||||
from minibase.config import Config
|
||||
|
||||
# (DATABASE) Definition
|
||||
db = SQLAlchemy()
|
||||
|
||||
# (PASSWORD) Hashign Program to save paswords safely
|
||||
bcrypt = Bcrypt()
|
||||
|
||||
# (LOGIN) Login manage plugin configuration
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = 'users.login'
|
||||
login_manager.login_message_category = 'info' #Boostrap Info Message
|
||||
|
||||
# (EMAIL AGENT) Definition
|
||||
mail = Mail()
|
||||
|
||||
def create_minibase(config_class=Config):
|
||||
# (FLASK) Main Flask Application
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
# (DATABASE) Initialisation
|
||||
db.init_app(app)
|
||||
# (PASSWORD) Initialisation
|
||||
bcrypt.init_app(app)
|
||||
# (LOGIN) Initialisation
|
||||
login_manager.init_app(app)
|
||||
# (EMAIL AGENT) Initialisation
|
||||
mail.init_app(app)
|
||||
# (FLASK) Importing adn then registering blueprints
|
||||
from minibase.users.routes import users
|
||||
from minibase.posts.routes import posts
|
||||
from minibase.main.routes import main
|
||||
from minibase.errors.handlers import errors
|
||||
app.register_blueprint(users)
|
||||
app.register_blueprint(posts)
|
||||
app.register_blueprint(main)
|
||||
app.register_blueprint(errors)
|
||||
# Retunr The ccreated app
|
||||
return app
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,20 @@
|
||||
import os
|
||||
|
||||
class themeMinibase():
|
||||
image="rounded-circle account-img"
|
||||
|
||||
|
||||
class Config:
|
||||
# (FLASK) Sectret key wich will be used to secure some requests an connections
|
||||
SECRET_KEY = os.environ.get('MINIBASE_SECRET_KEY')
|
||||
|
||||
# (SQLALCHEMY) COnfiguration
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('MINIBASE_SQLALCHEMY_DATABASE_URI')
|
||||
|
||||
# (MAIL AGENT) Configure mail Server to send EMails.
|
||||
MAIL_SERVER = os.environ.get('MINIBASE_MAIL_SERVER')
|
||||
MAIL_USERNAME = os.environ.get('MINIBASE_MAIL_USERNAME')
|
||||
MAIL_PASSWORD = os.environ.get('MINIBASE_MAIL_PASSWORD')
|
||||
MAIL_PORT = 465
|
||||
MAIL_USE_TLS = False
|
||||
MAIL_USE_SSL = True
|
Binary file not shown.
@ -0,0 +1,16 @@
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
errors = Blueprint('errors', __name__)
|
||||
|
||||
@errors.app_errorhandler(404)
|
||||
def error_404(error):
|
||||
return render_template('errors/404.html'), 404
|
||||
|
||||
@errors.app_errorhandler(403)
|
||||
def error_403(error):
|
||||
return render_template('errors/403.html'), 403
|
||||
|
||||
@errors.app_errorhandler(500)
|
||||
def error_500(error):
|
||||
return render_template('errors/500.html'), 500
|
||||
|
Binary file not shown.
@ -0,0 +1,21 @@
|
||||
from flask import render_template, request, Blueprint
|
||||
from minibase.models import Post
|
||||
|
||||
# Declaring a blueprint
|
||||
main = Blueprint('main', __name__)
|
||||
|
||||
#Redirect from / and also /home routes to the /
|
||||
@main.route("/")
|
||||
@main.route("/home")
|
||||
def home():
|
||||
# (PAGINATION) Defines the page number that we will start with
|
||||
page = request.args.get('page', 1, type=int)
|
||||
# (POSTS) Query posts usin SQLAlchemy
|
||||
posts = Post.query.order_by(Post.date_posted.asc()).paginate(per_page=2)
|
||||
# (HTML) Renders the template for templates/home.html
|
||||
return render_template('home.html', posts=posts)
|
||||
|
||||
|
||||
@main.route("/about")
|
||||
def about():
|
||||
return render_template('about.html', title='About')
|
@ -0,0 +1,46 @@
|
||||
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
|
||||
from datetime import datetime
|
||||
from minibase import db, login_manager
|
||||
from flask_login import UserMixin
|
||||
from flask import url_for, current_app
|
||||
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(20), unique=True, nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
|
||||
password = db.Column(db.String(60), nullable=False)
|
||||
posts = db.relationship('Post', backref='author', lazy=True)
|
||||
|
||||
def get_reset_token(self, expires_sec=1800):
|
||||
s = Serializer(current_app.config['SECRET_KEY'], expires_sec)
|
||||
return s.dumps({'user_id': self.id}).decode('utf-8')
|
||||
|
||||
@staticmethod
|
||||
def verify_reset_token(token):
|
||||
s = Serializer(current_app.config['SECRET_KEY'])
|
||||
try:
|
||||
user_id = s.loads(token)['user_id']
|
||||
except:
|
||||
return none
|
||||
return User.query.get(user_id)
|
||||
|
||||
def __repr__(self):
|
||||
return f"User('{self.username}', '{self.email}', '{self.image_file}')"
|
||||
|
||||
class Post(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"User('{self.title}', '{self.date_posted}')"
|
||||
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1,10 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, TextAreaField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
class postForm(FlaskForm):
|
||||
title = StringField('Title',
|
||||
validators=[DataRequired()])
|
||||
content = TextAreaField('Content',
|
||||
validators=[DataRequired()])
|
||||
submit = SubmitField('Post')
|
@ -0,0 +1,57 @@
|
||||
from flask import (render_template, url_for, flash,
|
||||
redirect, request, abort, Blueprint)
|
||||
from flask_login import current_user, login_required
|
||||
from minibase import db
|
||||
from minibase.models import Post
|
||||
from minibase.posts.forms import postForm
|
||||
from minibase.config import themeMinibase
|
||||
|
||||
|
||||
# Declaring a blueprint
|
||||
posts = Blueprint('posts', __name__)
|
||||
|
||||
@posts.route("/post/new", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def new_post():
|
||||
form = postForm()
|
||||
if form.validate_on_submit():
|
||||
post = Post(title=form.title.data, content=form.content.data, author=current_user)
|
||||
db.session.add(post)
|
||||
db.session.commit()
|
||||
flash('Your post has been created', 'success')
|
||||
return redirect(url_for('main.home'))
|
||||
return render_template('create_post.html', title='Create Post', legend='Create Post', form=form)
|
||||
|
||||
@posts.route("/post/<int:post_id>")
|
||||
def post(post_id):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
return render_template('post.html', title=post.title, post=post, theme=themeMinibase)
|
||||
|
||||
@posts.route("/post/<int:post_id>/update", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def post_update(post_id):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
if post.author != current_user:
|
||||
abort(403)
|
||||
form = postForm()
|
||||
if form.validate_on_submit():
|
||||
post.title = form.title.data
|
||||
post.content = form.content.data
|
||||
db.session.commit()
|
||||
flash('Your post has been updated', 'success')
|
||||
return redirect(url_for('posts.post', post_id=post.id))
|
||||
elif request.method == 'GET':
|
||||
form.title.data = post.title
|
||||
form.content.data = post.content
|
||||
return render_template('create_post.html', title='Update Post', legend='Update Post', form=form)
|
||||
|
||||
@posts.route("/post/<int:post_id>/delete", methods=['POST'])
|
||||
@login_required
|
||||
def post_delete(post_id):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
if post.author != current_user:
|
||||
abort(403)
|
||||
db.session.delete(post)
|
||||
db.session.commit()
|
||||
flash('Your post has been deleted', 'success')
|
||||
return redirect(url_for('main.home'))
|
Binary file not shown.
@ -0,0 +1,80 @@
|
||||
body {
|
||||
background: #fafafa;
|
||||
color: #333333;
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #444444;
|
||||
}
|
||||
|
||||
.bg-steel {
|
||||
background-color: #191717
|
||||
}
|
||||
|
||||
.site-header .navbar-nav .nav-link {
|
||||
color: #cbd5db;
|
||||
}
|
||||
|
||||
.site-header .navbar-nav .nav-link:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.site-header .navbar-nav .nav-link.active {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
background: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border: 1px solid #dddddd;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
color: #444444;
|
||||
}
|
||||
|
||||
a.article-title:hover {
|
||||
color: #428bca;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.article-content {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.article-img {
|
||||
height: 65px;
|
||||
width: 65px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.article-metadata {
|
||||
padding-bottom: 1px;
|
||||
margin-bottom: 4px;
|
||||
border-bottom: 1px solid #e3e3e3
|
||||
}
|
||||
|
||||
.article-metadata a:hover {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.article-svg {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.account-img {
|
||||
height: 125px;
|
||||
width: 125px;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.account-heading {
|
||||
font-size: 2.5rem;
|
||||
}
|
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 364 KiB |
After Width: | Height: | Size: 11 KiB |
@ -0,0 +1,5 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>About Page</h1>
|
||||
{% endblock content %}
|
@ -0,0 +1,60 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<div class="content-section">
|
||||
<div class="media">
|
||||
<img class="rounded-circle account-img" src="{{ image_file }}">
|
||||
<div class="media-body">
|
||||
<h2 class="account-heading">{{ current_user.username }}</h2>
|
||||
<p class="text-secondary">{{ current_user.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
<fieldset class="form-group">
|
||||
<legend class="border-bottom mb-4"> Account Info </legend>
|
||||
<div class="form-group">
|
||||
{{ form.username.label(class="form-control-label") }}
|
||||
|
||||
{% if form.username.errors %}
|
||||
{{ form.username(class="form-control form-control-lg is-invalid") }}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.username.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form.username(class="form-control form-control-lg") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</br>
|
||||
<div class="form-group">
|
||||
{{ form.email.label(class="form-control-label") }}
|
||||
{% if form.email.errors %}
|
||||
{{ form.email(class="form-control form-control-lg is-invalid") }}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.email.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form.email(class="form-control form-control-lg") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</br>
|
||||
<div class="form-group">
|
||||
{{ form.picture.label() }} </br>
|
||||
{{ form.picture(class="form-control-file") }}</br>
|
||||
{% if form.picture.errors %}
|
||||
{% for error in form.picture.errors %}
|
||||
<span class="text-danger">{{ error }}</span></br>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</br>
|
||||
</fieldset>
|
||||
<div class="form-group">
|
||||
{{ form.submit(class="btn btn-outline-info") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -0,0 +1,40 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<div class="content-section">
|
||||
<form method="POST" action="">
|
||||
{{ form.hidden_tag() }}
|
||||
<fieldset class="form-group">
|
||||
<legend class="border-bottom mb-4">{{ legend }}</legend>
|
||||
<div class="form-group">
|
||||
{{ form.title.label(class="form-control-label") }}
|
||||
{% if form.title.errors %}
|
||||
{{ form.title(class="form-control form-control-lg is-invalid") }}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.title.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form.title(class="form-control form-control-lg") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.content.label(class="form-control-label") }}
|
||||
{% if form.content.errors %}
|
||||
{{ form.content(class="form-control form-control-lg is-invalid") }}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.content.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form.content(class="form-control form-control-lg") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group">
|
||||
{{ form.submit(class="btn btn-outline-info") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -0,0 +1,9 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content-section">
|
||||
<h1>Your dont have permission to do that (403)</h1>
|
||||
<p>PLease Check your account and try again</p>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
@ -0,0 +1,9 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content-section">
|
||||
<h1>Ooops :S Page Not Found (404)</h1>
|
||||
<p>That page doens not exist. Please try another one</p>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
@ -0,0 +1,9 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
cat: templates/errors/500.html: No such file or directory
|
||||
<div class="content-section">
|
||||
<h1>Something Went Wrong (500)</h1>
|
||||
<p>There Was an Error from our side please give us a moment and try again later</p>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
@ -0,0 +1,28 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% for post in posts.items %}
|
||||
<article class="media content-section">
|
||||
<img class="rounded-circle article-img" src="{{ url_for('static', filename='pics/' + post.author.image_file) }}">
|
||||
<div class="media-body">
|
||||
<div class="article-metadata">
|
||||
<a class="mr-2" href="{{ url_for('users.user_posts', username=post.author.username) }} ">{{ post.author.username }}</a>
|
||||
<small class="text-muted">{{ post.date_posted.strftime('%Y-%m-%d') }}</small>
|
||||
</div>
|
||||
<h2><a class="article-title" href="{{ url_for('posts.post', post_id=post.id) }}">{{ post.title }}</a></h2>
|
||||
<p class="article-content">{{ post.content }}</p>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
{% for page_num in posts.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
|
||||
{% if page_num %}
|
||||
{% if posts.page == page_num %}
|
||||
<a class="btn btn-info mb-4" href="{{ url_for('main.home', page=page_num) }}">{{ page_num }}</a>
|
||||
{% else %}
|
||||
<a class="btn btn-outline-info mb-4" href="{{ url_for('main.home', page=page_num) }}">{{ page_num }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
...
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endblock content %}
|
@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
|
||||
|
||||
{% if title %}
|
||||
<title> MiniBase - {{ title }} </title>
|
||||
{% else %}
|
||||
<title> MiniBase </title>
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-sm navbar-dark bg-steel fixed-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand mr-4" href="{{ url_for('main.home') }}">MiniBase</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarToggle" aria-controls="navbarToggle" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarToggle">
|
||||
<div class="navbar-nav mr-auto">
|
||||
<a class="nav-item nav-link" href="{{ url_for('main.home') }}">Home</a>
|
||||
<a class="nav-item nav-link" href="{{ url_for('main.about') }}">About</a>
|
||||
</div>
|
||||
<!-- Navbar Right Side -->
|
||||
<div class="navbar-nav">
|
||||
{% if current_user.is_authenticated %}
|
||||
<a class="nav-item nav-link" href="{{ url_for('posts.new_post') }}">New Post</a>
|
||||
<a class="nav-item nav-link" href="{{ url_for('users.logout') }}">Logout</a>
|
||||
<a class="nav-item nav-link" href="{{ url_for('users.account') }}">Account</a>
|
||||
{% else %}
|
||||
<a class="nav-item nav-link" href="{{ url_for('users.login') }}">Login</a>
|
||||
<a class="nav-item nav-link" href="{{ url_for('users.register') }}">Register</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<main role="main" class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="content-section">
|
||||
<h3>Our Sidebar</h3>
|
||||
<p class='text-muted'>You can put any information here you'd like.
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item list-group-item-light">Latest Posts</li>
|
||||
<li class="list-group-item list-group-item-light">Announcements</li>
|
||||
<li class="list-group-item list-group-item-light">Calendars</li>
|
||||
<li class="list-group-item list-group-item-light">etc</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,52 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<div class="content-section">
|
||||
<form method="POST" action="">
|
||||
{{ form.hidden_tag() }}
|
||||
<fieldset class="form-group">
|
||||
<legend class="border-bottom mb-4">Log In</legend>
|
||||
<div class="form-group">
|
||||
{{ form.email.label(class="form-control-label") }}
|
||||
{% if form.email.errors %}
|
||||
{{ form.email(class="form-control form-control-lg is-invalid") }}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.email.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form.email(class="form-control form-control-lg") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.password.label(class="form-control-label") }}
|
||||
{% if form.password.errors %}
|
||||
{{ form.password(class="form-control form-control-lg is-invalid") }}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.password.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form.password(class="form-control form-control-lg") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-check">
|
||||
{{ form.remember(class="form-check-input") }}
|
||||
{{ form.remember.label(class="form-check-label") }}
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group">
|
||||
{{ form.submit(class="btn btn-outline-info") }}
|
||||
</div>
|
||||
<small class="text-muted ml-2">
|
||||
<a href="{{ url_for('users.reset_request') }}">Forgot Password?</a>
|
||||
</small>
|
||||
</form>
|
||||
</div>
|
||||
<div class="border-top pt-3">
|
||||
<small class="text-muted">
|
||||
Need An Account? <a class="ml-2" href="{{ url_for('users.register') }}">Sign Up Now</a>
|
||||
</small>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -0,0 +1,40 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article class="media content-section">
|
||||
<img class="{{ theme.image }} article-img" src="{{ url_for('static', filename='pics/' + post.author.image_file) }}">
|
||||
<div class="media-body">
|
||||
<div class="article-metadata">
|
||||
<a class="mr-2" href="{{ url_for('users.user_posts', username=post.author.username) }}" >{{ post.author.username }}</a>
|
||||
<small class="text-muted">{{ post.date_posted.strftime('%Y-%m-%d') }}</small>
|
||||
{% if post.author == current_user %}
|
||||
<div>
|
||||
<a class="btn btn-secondary btn-sm m-1" href="{{ url_for('posts.post_update', post_id=post.id) }}">Update</a>
|
||||
<button type="button" class="btn btn-danger btn-sm m-1" data-bs-toggle="modal" data-bs-target="#staticBackdrop">Delete</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h2 class="article-title">{{ post.title }}</h2>
|
||||
<p class="article-content">{{ post.content }}</p>
|
||||
</div>
|
||||
</article>
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="staticBackdropLabel">Modal title</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Your last chance, are you Sure?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<form action="{{ url_for('posts.post_delete', post_id=post.id) }}" method="POST">
|
||||
<input class="btn btn-danger" type="submit" value="Delete">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -0,0 +1,72 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<div class="content-section">
|
||||
<form method="POST" action="">
|
||||
{{ form.hidden_tag() }}
|
||||
<fieldset class="form-group">
|
||||
<legend class="border-bottom mb-4">Join Today</legend>
|
||||
<div class="form-group">
|
||||
{{ form.username.label(class="form-control-label") }}
|
||||
|
||||
{% if form.username.errors %}
|
||||
{{ form.username(class="form-control form-control-lg is-invalid") }}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.username.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form.username(class="form-control form-control-lg") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.email.label(class="form-control-label") }}
|
||||
{% if form.email.errors %}
|
||||
{{ form.email(class="form-control form-control-lg is-invalid") }}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.email.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form.email(class="form-control form-control-lg") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.password.label(class="form-control-label") }}
|
||||
{% if form.password.errors %}
|
||||
{{ form.password(class="form-control form-control-lg is-invalid") }}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.password.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form.password(class="form-control form-control-lg") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.password_confirm.label(class="form-control-label") }}
|
||||
{% if form.password_confirm.errors %}
|
||||
{{ form.password_confirm(class="form-control form-control-lg is-invalid") }}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.password_confirm.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form.password_confirm(class="form-control form-control-lg") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group">
|
||||
{{ form.submit(class="btn btn-outline-info") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="border-top pt-3">
|
||||
<small class="text-muted">
|
||||
Already Have An Account? <a class="ml-2" href="{{ url_for('users.login') }}">Sign In</a>
|
||||
</small>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -0,0 +1,27 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<div class="content-section">
|
||||
<form method="POST" action="">
|
||||
{{ form.hidden_tag() }}
|
||||
<fieldset class="form-group">
|
||||
<legend class="border-bottom mb-4">Reset Password</legend>
|
||||
<div class="form-group">
|
||||
{{ form.email.label(class="form-control-label") }}
|
||||
{% if form.email.errors %}
|
||||
{{ form.email(class="form-control form-control-lg is-invalid") }}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.email.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form.email(class="form-control form-control-lg") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group">
|
||||
{{ form.submit(class="btn btn-outline-info") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -0,0 +1,40 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<div class="content-section">
|
||||
<form method="POST" action="">
|
||||
{{ form.hidden_tag() }}
|
||||
<fieldset class="form-group">
|
||||
<legend class="border-bottom mb-4">Reset Password</legend>
|
||||
<div class="form-group">
|
||||
{{ form.password.label(class="form-control-label") }}
|
||||
{% if form.password.errors %}
|
||||
{{ form.password(class="form-control form-control-lg is-invalid") }}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.password.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form.password(class="form-control form-control-lg") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.password_confirm.label(class="form-control-label") }}
|
||||
{% if form.password_confirm.errors %}
|
||||
{{ form.password_confirm(class="form-control form-control-lg is-invalid") }}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.password_confirm.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form.password_confirm(class="form-control form-control-lg") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group">
|
||||
{{ form.submit(class="btn btn-outline-info") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -0,0 +1,29 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="mb-3"> Posts By {{ user.username }} ({{ posts.total }}) </h1>
|
||||
{% for post in posts.items %}
|
||||
<article class="media content-section">
|
||||
<img class="rounded-circle article-img" src="{{ url_for('static', filename='pics/' + post.author.image_file) }}">
|
||||
<div class="media-body">
|
||||
<div class="article-metadata">
|
||||
<a class="mr-2" href="{{ url_for('users.user_posts', username=post.author.username) }} ">{{ post.author.username }}</a>
|
||||
<small class="text-muted">{{ post.date_posted.strftime('%Y-%m-%d') }}</small>
|
||||
</div>
|
||||
<h2><a class="article-title" href="{{ url_for('posts.post', post_id=post.id) }}">{{ post.title }}</a></h2>
|
||||
<p class="article-content">{{ post.content }}</p>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
{% for page_num in posts.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
|
||||
{% if page_num %}
|
||||
{% if posts.page == page_num %}
|
||||
<a class="btn btn-info mb-4" href="{{ url_for('users.user_posts', username=user.username, page=page_num) }}">{{ page_num }}</a>
|
||||
{% else %}
|
||||
<a class="btn btn-outline-info mb-4" href="{{ url_for('users.user_posts', username=user.username, page=page_num) }}">{{ page_num }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
...
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endblock content %}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,74 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed
|
||||
from wtforms import StringField, PasswordField, SubmitField, BooleanField
|
||||
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
|
||||
from flask_login import current_user
|
||||
from minibase.models import User
|
||||
|
||||
class registrationForm(FlaskForm):
|
||||
username = StringField('User Name',
|
||||
validators=[DataRequired(),Length(min = 3, max = 20)])
|
||||
email = StringField('Email',
|
||||
validators=[DataRequired(),Email()])
|
||||
password = PasswordField('Password',
|
||||
validators=[DataRequired()])
|
||||
password_confirm = PasswordField('Confirm Password',
|
||||
validators=[DataRequired(), EqualTo('password')])
|
||||
submit = SubmitField('Sing Up')
|
||||
|
||||
def validate_username(self, username):
|
||||
user = User.query.filter_by(username=username.data).first()
|
||||
if user:
|
||||
raise ValidationError('That username is taken please choose another one')
|
||||
|
||||
def validate_email(self, email):
|
||||
email = User.query.filter_by(email=email.data).first()
|
||||
if email:
|
||||
raise ValidationError('That email is taken do you have an acocunt ?')
|
||||
|
||||
class loginForm(FlaskForm):
|
||||
email = StringField('Email',
|
||||
validators=[DataRequired(),Email()])
|
||||
password = PasswordField('Password',
|
||||
validators=[DataRequired()])
|
||||
remember = BooleanField('Remember Me')
|
||||
submit = SubmitField('Log In')
|
||||
|
||||
class updateAccountForm(FlaskForm):
|
||||
username = StringField('User Name',
|
||||
validators=[DataRequired(),Length(min = 3, max = 20)])
|
||||
email = StringField('Email',
|
||||
validators=[DataRequired(),Email()])
|
||||
picture = FileField('Update Profile Picture',
|
||||
validators=[FileAllowed(['jpg','png'])])
|
||||
submit = SubmitField('Update')
|
||||
|
||||
def validate_username(self, username):
|
||||
if username.data != current_user.username:
|
||||
user = User.query.filter_by(username=username.data).first()
|
||||
if user:
|
||||
raise ValidationError('That username is taken please choose another one')
|
||||
|
||||
def validate_email(self, email):
|
||||
if email.data != current_user.email:
|
||||
email = User.query.filter_by(email=email.data).first()
|
||||
if email:
|
||||
raise ValidationError('That email is taken do you have an acocunt ?')
|
||||
|
||||
class requestResetForm(FlaskForm):
|
||||
email = StringField('Email',
|
||||
validators=[DataRequired(),Email()])
|
||||
submit = SubmitField('Request Password Reset')
|
||||
|
||||
def validate_email(self, email):
|
||||
email = User.query.filter_by(email=email.data).first()
|
||||
if email is None:
|
||||
raise ValidationError('There is no Account with this email your must register first.')
|
||||
|
||||
class resetPasswordForm(FlaskForm):
|
||||
password = PasswordField('Password',
|
||||
validators=[DataRequired()])
|
||||
password_confirm = PasswordField('Confirm Password',
|
||||
validators=[DataRequired(), EqualTo('password')])
|
||||
submit = SubmitField('Reset Password')
|
||||
|
@ -0,0 +1,106 @@
|
||||
from flask import render_template, url_for, flash, redirect, request, Blueprint
|
||||
from flask_login import login_user, current_user, logout_user, login_required
|
||||
from minibase import db, bcrypt
|
||||
from minibase.models import User, Post
|
||||
from minibase.users.forms import registrationForm, loginForm, updateAccountForm, requestResetForm, resetPasswordForm
|
||||
from minibase.posts.forms import postForm
|
||||
from minibase.users.utils import save_picture, send_reset_email
|
||||
|
||||
|
||||
# Declaring a blueprint
|
||||
users = Blueprint('users', __name__)
|
||||
|
||||
#Route is the file that is going to be generated
|
||||
@users.route("/register", methods=['GET', 'POST'])
|
||||
def register():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.home'))
|
||||
|
||||
form = registrationForm()
|
||||
if form.validate_on_submit():
|
||||
hashed_pw = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
|
||||
user = User(username=form.username.data, email=form.email.data, password=hashed_pw)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash(f'Your account has benn created you can now log in!','success')
|
||||
return redirect(url_for('users.login'))
|
||||
|
||||
return render_template('register.html', title='Register', form=form)
|
||||
|
||||
@users.route("/login", methods=['GET', 'POST'])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.home'))
|
||||
form = loginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(email=form.email.data).first()
|
||||
if user and bcrypt.check_password_hash(user.password, form.password.data):
|
||||
login_user(user,remember=form.remember.data)
|
||||
next_page = request.args.get('next')
|
||||
return redirect(next_page) if next_page else redirect(url_for('main.home'))
|
||||
else:
|
||||
flash('Login unsuccessful. Please chek your Email and Password!','danger')
|
||||
return render_template('login.html', title='Login', form=form)
|
||||
|
||||
@users.route("/logout")
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for('main.home'))
|
||||
|
||||
|
||||
@users.route("/account", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def account():
|
||||
form = updateAccountForm()
|
||||
if form.validate_on_submit():
|
||||
if form.picture.data:
|
||||
picture_file = save_picture(form.picture.data)
|
||||
current_user.image_file = picture_file
|
||||
current_user.username = form.username.data
|
||||
current_user.email = form.email.data
|
||||
db.session.commit()
|
||||
flash('Your account has been updated!', 'success')
|
||||
return redirect(url_for('users.account'))
|
||||
elif request.method == 'GET':
|
||||
form.username.data = current_user.username
|
||||
form.email.data = current_user.email
|
||||
image_file = url_for('static', filename='pics/'+ current_user.image_file)
|
||||
return render_template('account.html', title='Account', image_file = image_file, form=form)
|
||||
|
||||
|
||||
@users.route("/user/<string:username>")
|
||||
def user_posts(username):
|
||||
user = User.query.filter_by(username=username).first_or_404()
|
||||
page = request.args.get('page', 1, type=int)
|
||||
posts = Post.query.filter_by(author=user)\
|
||||
.order_by(Post.date_posted.asc())\
|
||||
.paginate(page=page, per_page=2)
|
||||
return render_template('user_posts.html', posts=posts, user=user)
|
||||
|
||||
@users.route("/reset_password", methods=['GET', 'POST'])
|
||||
def reset_request():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.home'))
|
||||
form = requestResetForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(email=form.email.data).first()
|
||||
send_reset_email(user)
|
||||
flash('An Email has benn sent with instruction to reset your password', 'warning')
|
||||
return render_template('reset_request.html', title='Reset Password', form=form)
|
||||
|
||||
@users.route("/reset_password/<token>", methods=['GET', 'POST'])
|
||||
def reset_token(token):
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.home'))
|
||||
user = User.verify_reset_token(token)
|
||||
if user is None:
|
||||
flash('That is an invalid or expired token', 'warning')
|
||||
return redirect(url_for('users.reset_request'))
|
||||
form = resetPasswordForm()
|
||||
if form.validate_on_submit():
|
||||
hashed_pw = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
|
||||
user.password = hashed_pw
|
||||
db.session.commit()
|
||||
flash(f'Your password has benn udated','success')
|
||||
return redirect(url_for('users.login'))
|
||||
return render_template('reset_token.html', title='Reset Password', form=form)
|
@ -0,0 +1,28 @@
|
||||
import os
|
||||
import secrets
|
||||
from PIL import Image
|
||||
from flask import url_for, current_app
|
||||
from flask_mail import Message
|
||||
from minibase import mail
|
||||
|
||||
def save_picture(form_picture):
|
||||
random_hex = secrets.token_hex(8)
|
||||
_, f_ext =os.path.splitext(form_picture.filename)
|
||||
picture_fn = random_hex + f_ext
|
||||
picture_path = os.path.join(current_app.root_path, 'static/pics', picture_fn)
|
||||
output_size = (125,125)
|
||||
i = Image.open(form_picture)
|
||||
i.thumbnail(output_size)
|
||||
i.save(picture_path)
|
||||
return picture_fn
|
||||
|
||||
def send_reset_email(user):
|
||||
token = user.get_reset_token()
|
||||
msg = Message('Password Reset Request',
|
||||
sender='noreply@demo.com',
|
||||
recipients=[user.email])
|
||||
msg.body = f'''To reset your password, visit the following link:
|
||||
{url_for('reset_token', token=token, _external=True)}
|
||||
If you didn't make this request, then simply ingnore this email and no chancges will be made.
|
||||
'''
|
||||
mail.send(msg)
|
@ -0,0 +1,7 @@
|
||||
from minibase import create_minibase
|
||||
#Enable debug option even if run directly form Python3
|
||||
|
||||
app=create_minibase()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,16 @@
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
errors = Blueprint('errors', __name__)
|
||||
|
||||
@errors.app_errorhandler(404)
|
||||
def error_404(error):
|
||||
return render_template('errors/404.html'), 404
|
||||
|
||||
@errors.app_errorhandler(403)
|
||||
def error_403(error):
|
||||
return render_template('errors/403.html'), 403
|
||||
|
||||
@errors.app_errorhandler(500)
|
||||
def error_500(error):
|
||||
return render_template('errors/500.html'), 500
|
||||
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1,9 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content-section">
|
||||
<h1>Your dont have permission to do that (403)</h1>
|
||||
<p>PLease Check your account and try again</p>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
@ -0,0 +1,9 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content-section">
|
||||
<h1>Ooops :S Page Not Found (404)</h1>
|
||||
<p>That page doens not exist. Please try another one</p>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
@ -0,0 +1,9 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
cat: templates/errors/500.html: No such file or directory
|
||||
<div class="content-section">
|
||||
<h1>Something Went Wrong (500)</h1>
|
||||
<p>There Was an Error from our side please give us a moment and try again later</p>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
Loading…
Reference in new issue