Chapter 15: A Better Application Structure (v0.15)

This commit is contained in:
Miguel Grinberg 2017-10-09 00:09:22 -07:00
parent b00c6902f1
commit a1d4f2800e
No known key found for this signature in database
32 changed files with 622 additions and 525 deletions

View File

@ -1,7 +1,7 @@
import logging
from logging.handlers import SMTPHandler, RotatingFileHandler
import os
from flask import Flask, request
from flask import Flask, request, current_app
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
@ -12,47 +12,73 @@ from config import Config
def get_locale():
return request.accept_languages.best_match(app.config['LANGUAGES'])
return request.accept_languages.best_match(current_app.config['LANGUAGES'])
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
login = LoginManager(app)
login.login_view = 'login'
db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
login.login_view = 'auth.login'
login.login_message = _l('Please log in to access this page.')
mail = Mail(app)
moment = Moment(app)
babel = Babel(app, locale_selector=get_locale)
if not app.debug:
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/microblog.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('Microblog startup')
mail = Mail()
moment = Moment()
babel = Babel()
from app import routes, models, errors
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)
babel.init_app(app, locale_selector=get_locale)
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)
from app.cli import bp as cli_bp
app.register_blueprint(cli_bp)
if not app.debug and not app.testing:
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/microblog.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('Microblog startup')
return app
from app import models

5
app/auth/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('auth', __name__)
from app.auth import routes

14
app/auth/email.py Normal file
View File

@ -0,0 +1,14 @@
from flask import render_template, current_app
from flask_babel import _
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))

View File

@ -1,9 +1,7 @@
from flask_wtf import FlaskForm
from flask_babel import _, lazy_gettext as _l
from wtforms import StringField, PasswordField, BooleanField, SubmitField, \
TextAreaField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, \
Length
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
import sqlalchemy as sa
from app import db
from app.models import User
@ -49,31 +47,3 @@ class ResetPasswordForm(FlaskForm):
_l('Repeat Password'), validators=[DataRequired(),
EqualTo('password')])
submit = SubmitField(_l('Request Password Reset'))
class EditProfileForm(FlaskForm):
username = StringField(_l('Username'), validators=[DataRequired()])
about_me = TextAreaField(_l('About me'),
validators=[Length(min=0, max=140)])
submit = SubmitField(_l('Submit'))
def __init__(self, original_username, *args, **kwargs):
super().__init__(*args, **kwargs)
self.original_username = original_username
def validate_username(self, username):
if username.data != self.original_username:
user = db.session.scalar(sa.select(User).where(
User.username == username.data))
if user is not None:
raise ValidationError(_('Please use a different username.'))
class EmptyForm(FlaskForm):
submit = SubmitField('Submit')
class PostForm(FlaskForm):
post = TextAreaField(_l('Say something'), validators=[
DataRequired(), Length(min=1, max=140)])
submit = SubmitField(_l('Submit'))

85
app/auth/routes.py Normal file
View File

@ -0,0 +1,85 @@
from flask import render_template, redirect, url_for, flash, request
from urllib.parse import urlsplit
from flask_login import login_user, logout_user, current_user
from flask_babel import _
import sqlalchemy as sa
from app import db
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm, \
ResetPasswordRequestForm, ResetPasswordForm
from app.models import User
from app.auth.email import send_password_reset_email
@bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = db.session.scalar(
sa.select(User).where(User.username == form.username.data))
if user is None or not user.check_password(form.password.data):
flash(_('Invalid username or password'))
return redirect(url_for('auth.login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or urlsplit(next_page).netloc != '':
next_page = url_for('main.index')
return redirect(next_page)
return render_template('auth/login.html', title=_('Sign In'), form=form)
@bp.route('/logout')
def logout():
logout_user()
return redirect(url_for('main.index'))
@bp.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash(_('Congratulations, you are now a registered user!'))
return redirect(url_for('auth.login'))
return render_template('auth/register.html', title=_('Register'),
form=form)
@bp.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = ResetPasswordRequestForm()
if form.validate_on_submit():
user = db.session.scalar(
sa.select(User).where(User.email == form.email.data))
if user:
send_password_reset_email(user)
flash(
_('Check your email for the instructions to reset your password'))
return redirect(url_for('auth.login'))
return render_template('auth/reset_password_request.html',
title=_('Reset Password'), form=form)
@bp.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
if current_user.is_authenticated:
return redirect(url_for('main.index'))
user = User.verify_reset_password_token(token)
if not user:
return redirect(url_for('main.index'))
form = ResetPasswordForm()
if form.validate_on_submit():
user.set_password(form.password.data)
db.session.commit()
flash(_('Your password has been reset.'))
return redirect(url_for('auth.login'))
return render_template('auth/reset_password.html', form=form)

View File

@ -1,9 +1,11 @@
import os
from flask import Blueprint
import click
from app import app
bp = Blueprint('cli', __name__, cli_group=None)
@app.cli.group()
@bp.cli.group()
def translate():
"""Translation and localization commands."""
pass

View File

@ -1,8 +1,7 @@
from threading import Thread
from flask import render_template
from flask import current_app
from flask_mail import Message
from flask_babel import _
from app import app, mail
from app import mail
def send_async_email(app, msg):
@ -14,15 +13,5 @@ def send_email(subject, sender, recipients, text_body, html_body):
msg = Message(subject, sender=sender, recipients=recipients)
msg.body = text_body
msg.html = html_body
Thread(target=send_async_email, args=(app, msg)).start()
def send_password_reset_email(user):
token = user.get_reset_password_token()
send_email(_('[Microblog] Reset Your Password'),
sender=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))
Thread(target=send_async_email,
args=(current_app._get_current_object(), msg)).start()

View File

@ -1,13 +0,0 @@
from flask import render_template
from app import app, db
@app.errorhandler(404)
def not_found_error(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('500.html'), 500

5
app/errors/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('errors', __name__)
from app.errors import handlers

14
app/errors/handlers.py Normal file
View File

@ -0,0 +1,14 @@
from flask import render_template
from app import db
from app.errors import bp
@bp.app_errorhandler(404)
def not_found_error(error):
return render_template('errors/404.html'), 404
@bp.app_errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('errors/500.html'), 500

5
app/main/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('main', __name__)
from app.main import routes

35
app/main/forms.py Normal file
View File

@ -0,0 +1,35 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField
from wtforms.validators import ValidationError, DataRequired, Length
import sqlalchemy as sa
from flask_babel import _, lazy_gettext as _l
from app import db
from app.models import User
class EditProfileForm(FlaskForm):
username = StringField(_l('Username'), validators=[DataRequired()])
about_me = TextAreaField(_l('About me'),
validators=[Length(min=0, max=140)])
submit = SubmitField(_l('Submit'))
def __init__(self, original_username, *args, **kwargs):
super().__init__(*args, **kwargs)
self.original_username = original_username
def validate_username(self, username):
if username.data != self.original_username:
user = db.session.scalar(sa.select(User).where(
User.username == username.data))
if user is not None:
raise ValidationError(_('Please use a different username.'))
class EmptyForm(FlaskForm):
submit = SubmitField('Submit')
class PostForm(FlaskForm):
post = TextAreaField(_l('Say something'), validators=[
DataRequired(), Length(min=1, max=140)])
submit = SubmitField(_l('Submit'))

152
app/main/routes.py Normal file
View File

@ -0,0 +1,152 @@
from datetime import datetime, timezone
from flask import render_template, flash, redirect, url_for, request, g, \
current_app
from flask_login import current_user, login_required
from flask_babel import _, get_locale
import sqlalchemy as sa
from langdetect import detect, LangDetectException
from app import db
from app.main.forms import EditProfileForm, EmptyForm, PostForm
from app.models import User, Post
from app.translate import translate
from app.main import bp
@bp.before_app_request
def before_request():
if current_user.is_authenticated:
current_user.last_seen = datetime.now(timezone.utc)
db.session.commit()
g.locale = str(get_locale())
@bp.route('/', methods=['GET', 'POST'])
@bp.route('/index', methods=['GET', 'POST'])
@login_required
def index():
form = PostForm()
if form.validate_on_submit():
try:
language = detect(form.post.data)
except LangDetectException:
language = ''
post = Post(body=form.post.data, author=current_user,
language=language)
db.session.add(post)
db.session.commit()
flash(_('Your post is now live!'))
return redirect(url_for('main.index'))
page = request.args.get('page', 1, type=int)
posts = db.paginate(current_user.following_posts(), page=page,
per_page=current_app.config['POSTS_PER_PAGE'],
error_out=False)
next_url = url_for('main.index', page=posts.next_num) \
if posts.has_next else None
prev_url = url_for('main.index', page=posts.prev_num) \
if posts.has_prev else None
return render_template('index.html', title=_('Home'), form=form,
posts=posts.items, next_url=next_url,
prev_url=prev_url)
@bp.route('/explore')
@login_required
def explore():
page = request.args.get('page', 1, type=int)
query = sa.select(Post).order_by(Post.timestamp.desc())
posts = db.paginate(query, page=page,
per_page=current_app.config['POSTS_PER_PAGE'],
error_out=False)
next_url = url_for('main.explore', page=posts.next_num) \
if posts.has_next else None
prev_url = url_for('main.explore', page=posts.prev_num) \
if posts.has_prev else None
return render_template('index.html', title=_('Explore'),
posts=posts.items, next_url=next_url,
prev_url=prev_url)
@bp.route('/user/<username>')
@login_required
def user(username):
user = db.first_or_404(sa.select(User).where(User.username == username))
page = request.args.get('page', 1, type=int)
query = user.posts.select().order_by(Post.timestamp.desc())
posts = db.paginate(query, page=page,
per_page=current_app.config['POSTS_PER_PAGE'],
error_out=False)
next_url = url_for('main.user', username=user.username,
page=posts.next_num) if posts.has_next else None
prev_url = url_for('main.user', username=user.username,
page=posts.prev_num) if posts.has_prev else None
form = EmptyForm()
return render_template('user.html', user=user, posts=posts.items,
next_url=next_url, prev_url=prev_url, form=form)
@bp.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = EditProfileForm(current_user.username)
if form.validate_on_submit():
current_user.username = form.username.data
current_user.about_me = form.about_me.data
db.session.commit()
flash(_('Your changes have been saved.'))
return redirect(url_for('main.edit_profile'))
elif request.method == 'GET':
form.username.data = current_user.username
form.about_me.data = current_user.about_me
return render_template('edit_profile.html', title=_('Edit Profile'),
form=form)
@bp.route('/follow/<username>', methods=['POST'])
@login_required
def follow(username):
form = EmptyForm()
if form.validate_on_submit():
user = db.session.scalar(
sa.select(User).where(User.username == username))
if user is None:
flash(_('User %(username)s not found.', username=username))
return redirect(url_for('main.index'))
if user == current_user:
flash(_('You cannot follow yourself!'))
return redirect(url_for('main.user', username=username))
current_user.follow(user)
db.session.commit()
flash(_('You are following %(username)s!', username=username))
return redirect(url_for('main.user', username=username))
else:
return redirect(url_for('main.index'))
@bp.route('/unfollow/<username>', methods=['POST'])
@login_required
def unfollow(username):
form = EmptyForm()
if form.validate_on_submit():
user = db.session.scalar(
sa.select(User).where(User.username == username))
if user is None:
flash(_('User %(username)s not found.', username=username))
return redirect(url_for('main.index'))
if user == current_user:
flash(_('You cannot unfollow yourself!'))
return redirect(url_for('main.user', username=username))
current_user.unfollow(user)
db.session.commit()
flash(_('You are not following %(username)s.', username=username))
return redirect(url_for('main.user', username=username))
else:
return redirect(url_for('main.index'))
@bp.route('/translate', methods=['POST'])
@login_required
def translate_text():
data = request.get_json()
return {'text': translate(data['text'],
data['source_language'],
data['dest_language'])}

View File

@ -4,10 +4,11 @@ from time import time
from typing import Optional
import sqlalchemy as sa
import sqlalchemy.orm as so
from flask import current_app
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
import jwt
from app import app, db, login
from app import db, login
followers = sa.Table(
@ -95,12 +96,12 @@ class User(UserMixin, db.Model):
def get_reset_password_token(self, expires_in=600):
return jwt.encode(
{'reset_password': self.id, 'exp': time() + expires_in},
app.config['SECRET_KEY'], algorithm='HS256')
current_app.config['SECRET_KEY'], algorithm='HS256')
@staticmethod
def verify_reset_password_token(token):
try:
id = jwt.decode(token, app.config['SECRET_KEY'],
id = jwt.decode(token, current_app.config['SECRET_KEY'],
algorithms=['HS256'])['reset_password']
except Exception:
return

View File

@ -1,224 +0,0 @@
from datetime import datetime, timezone
from urllib.parse import urlsplit
from flask import render_template, flash, redirect, url_for, request, g
from flask_login import login_user, logout_user, current_user, login_required
from flask_babel import _, get_locale
import sqlalchemy as sa
from langdetect import detect, LangDetectException
from app import app, db
from app.forms import LoginForm, RegistrationForm, EditProfileForm, \
EmptyForm, PostForm, ResetPasswordRequestForm, ResetPasswordForm
from app.models import User, Post
from app.email import send_password_reset_email
from app.translate import translate
@app.before_request
def before_request():
if current_user.is_authenticated:
current_user.last_seen = datetime.now(timezone.utc)
db.session.commit()
g.locale = str(get_locale())
@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
@login_required
def index():
form = PostForm()
if form.validate_on_submit():
try:
language = detect(form.post.data)
except LangDetectException:
language = ''
post = Post(body=form.post.data, author=current_user,
language=language)
db.session.add(post)
db.session.commit()
flash(_('Your post is now live!'))
return redirect(url_for('index'))
page = request.args.get('page', 1, type=int)
posts = db.paginate(current_user.following_posts(), page=page,
per_page=app.config['POSTS_PER_PAGE'], error_out=False)
next_url = url_for('index', page=posts.next_num) \
if posts.has_next else None
prev_url = url_for('index', page=posts.prev_num) \
if posts.has_prev else None
return render_template('index.html', title=_('Home'), form=form,
posts=posts.items, next_url=next_url,
prev_url=prev_url)
@app.route('/explore')
@login_required
def explore():
page = request.args.get('page', 1, type=int)
query = sa.select(Post).order_by(Post.timestamp.desc())
posts = db.paginate(query, page=page,
per_page=app.config['POSTS_PER_PAGE'], error_out=False)
next_url = url_for('explore', page=posts.next_num) \
if posts.has_next else None
prev_url = url_for('explore', page=posts.prev_num) \
if posts.has_prev else None
return render_template('index.html', title=_('Explore'),
posts=posts.items, next_url=next_url,
prev_url=prev_url)
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
user = db.session.scalar(
sa.select(User).where(User.username == form.username.data))
if user is None or not user.check_password(form.password.data):
flash(_('Invalid username or password'))
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or urlsplit(next_page).netloc != '':
next_page = url_for('index')
return redirect(next_page)
return render_template('login.html', title=_('Sign In'), form=form)
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash(_('Congratulations, you are now a registered user!'))
return redirect(url_for('login'))
return render_template('register.html', title=_('Register'), form=form)
@app.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = ResetPasswordRequestForm()
if form.validate_on_submit():
user = db.session.scalar(
sa.select(User).where(User.email == form.email.data))
if user:
send_password_reset_email(user)
flash(
_('Check your email for the instructions to reset your password'))
return redirect(url_for('login'))
return render_template('reset_password_request.html',
title=_('Reset Password'), form=form)
@app.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
if current_user.is_authenticated:
return redirect(url_for('index'))
user = User.verify_reset_password_token(token)
if not user:
return redirect(url_for('index'))
form = ResetPasswordForm()
if form.validate_on_submit():
user.set_password(form.password.data)
db.session.commit()
flash(_('Your password has been reset.'))
return redirect(url_for('login'))
return render_template('reset_password.html', form=form)
@app.route('/user/<username>')
@login_required
def user(username):
user = db.first_or_404(sa.select(User).where(User.username == username))
page = request.args.get('page', 1, type=int)
query = user.posts.select().order_by(Post.timestamp.desc())
posts = db.paginate(query, page=page,
per_page=app.config['POSTS_PER_PAGE'],
error_out=False)
next_url = url_for('user', username=user.username, page=posts.next_num) \
if posts.has_next else None
prev_url = url_for('user', username=user.username, page=posts.prev_num) \
if posts.has_prev else None
form = EmptyForm()
return render_template('user.html', user=user, posts=posts.items,
next_url=next_url, prev_url=prev_url, form=form)
@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = EditProfileForm(current_user.username)
if form.validate_on_submit():
current_user.username = form.username.data
current_user.about_me = form.about_me.data
db.session.commit()
flash(_('Your changes have been saved.'))
return redirect(url_for('edit_profile'))
elif request.method == 'GET':
form.username.data = current_user.username
form.about_me.data = current_user.about_me
return render_template('edit_profile.html', title=_('Edit Profile'),
form=form)
@app.route('/follow/<username>', methods=['POST'])
@login_required
def follow(username):
form = EmptyForm()
if form.validate_on_submit():
user = db.session.scalar(
sa.select(User).where(User.username == username))
if user is None:
flash(_('User %(username)s not found.', username=username))
return redirect(url_for('index'))
if user == current_user:
flash(_('You cannot follow yourself!'))
return redirect(url_for('user', username=username))
current_user.follow(user)
db.session.commit()
flash(_('You are following %(username)s!', username=username))
return redirect(url_for('user', username=username))
else:
return redirect(url_for('index'))
@app.route('/unfollow/<username>', methods=['POST'])
@login_required
def unfollow(username):
form = EmptyForm()
if form.validate_on_submit():
user = db.session.scalar(
sa.select(User).where(User.username == username))
if user is None:
flash(_('User %(username)s not found.', username=username))
return redirect(url_for('index'))
if user == current_user:
flash(_('You cannot unfollow yourself!'))
return redirect(url_for('user', username=username))
current_user.unfollow(user)
db.session.commit()
flash(_('You are not following %(username)s.', username=username))
return redirect(url_for('user', username=username))
else:
return redirect(url_for('index'))
@app.route('/translate', methods=['POST'])
@login_required
def translate_text():
data = request.get_json()
return {'text': translate(data['text'],
data['source_language'],
data['dest_language'])}

View File

@ -1,13 +1,13 @@
<table class="table table-hover">
<tr>
<td width="70px">
<a href="{{ url_for('user', username=post.author.username) }}">
<a href="{{ url_for('main.user', username=post.author.username) }}">
<img src="{{ post.author.avatar(70) }}" />
</a>
</td>
<td>
{% set user_link %}
<a href="{{ url_for('user', username=post.author.username) }}">
<a href="{{ url_for('main.user', username=post.author.username) }}">
{{ post.author.username }}
</a>
{% endset %}

View File

@ -4,9 +4,9 @@
{% block content %}
<h1>{{ _('Sign In') }}</h1>
{{ wtf.quick_form(form) }}
<p>{{ _('New User?') }} <a href="{{ url_for('register') }}">{{ _('Click to Register!') }}</a></p>
<p>{{ _('New User?') }} <a href="{{ url_for('auth.register') }}">{{ _('Click to Register!') }}</a></p>
<p>
{{ _('Forgot Your Password?') }}
<a href="{{ url_for('reset_password_request') }}">{{ _('Click to Reset It') }}</a>
<a href="{{ url_for('auth.reset_password_request') }}">{{ _('Click to Reset It') }}</a>
</p>
{% endblock %}

View File

@ -17,30 +17,30 @@
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">Microblog</a>
<a class="navbar-brand" href="{{ url_for('main.index') }}">Microblog</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ url_for('index') }}">{{ _('Home') }}</a>
<a class="nav-link" aria-current="page" href="{{ url_for('main.index') }}">{{ _('Home') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ url_for('explore') }}">{{ _('Explore') }}</a>
<a class="nav-link" aria-current="page" href="{{ url_for('main.explore') }}">{{ _('Explore') }}</a>
</li>
</ul>
<ul class="navbar-nav mb-2 mb-lg-0">
{% if current_user.is_anonymous %}
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ url_for('login') }}">{{ _('Login') }}</a>
<a class="nav-link" aria-current="page" href="{{ url_for('auth.login') }}">{{ _('Login') }}</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ url_for('user', username=current_user.username) }}">{{ _('Profile') }}</a>
<a class="nav-link" aria-current="page" href="{{ url_for('main.user', username=current_user.username) }}">{{ _('Profile') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ url_for('logout') }}">{{ _('Logout') }}</a>
<a class="nav-link" aria-current="page" href="{{ url_for('auth.logout') }}">{{ _('Logout') }}</a>
</li>
{% endif %}
</ul>

View File

@ -4,12 +4,12 @@
<p>Dear {{ user.username }},</p>
<p>
To reset your password
<a href="{{ url_for('reset_password', token=token, _external=True) }}">
<a href="{{ url_for('auth.reset_password', token=token, _external=True) }}">
click here
</a>.
</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('reset_password', token=token, _external=True) }}</p>
<p>{{ url_for('auth.reset_password', token=token, _external=True) }}</p>
<p>If you have not requested a password reset simply ignore this message.</p>
<p>Sincerely,</p>
<p>The Microblog Team</p>

View File

@ -2,7 +2,7 @@ Dear {{ user.username }},
To reset your password click on the following link:
{{ url_for('reset_password', token=token, _external=True) }}
{{ url_for('auth.reset_password', token=token, _external=True) }}
If you have not requested a password reset simply ignore this message.

View File

@ -2,5 +2,5 @@
{% block content %}
<h1>{{ _('Not Found') }}</h1>
<p><a href="{{ url_for('index') }}">{{ _('Back') }}</a></p>
<p><a href="{{ url_for('main.index') }}">{{ _('Back') }}</a></p>
{% endblock %}

View File

@ -3,5 +3,5 @@
{% block content %}
<h1>{{ _('An unexpected error has occurred') }}</h1>
<p>{{ _('The administrator has been notified. Sorry for the inconvenience!') }}</p>
<p><a href="{{ url_for('index') }}">{{ _('Back') }}</a></p>
<p><a href="{{ url_for('main.index') }}">{{ _('Back') }}</a></p>
{% endblock %}

View File

@ -12,17 +12,17 @@
{% endif %}
<p>{{ _('%(count)d followers', count=user.followers_count()) }}, {{ _('%(count)d following', count=user.following_count()) }}</p>
{% if user == current_user %}
<p><a href="{{ url_for('edit_profile') }}">{{ _('Edit your profile') }}</a></p>
<p><a href="{{ url_for('main.edit_profile') }}">{{ _('Edit your profile') }}</a></p>
{% elif not current_user.is_following(user) %}
<p>
<form action="{{ url_for('follow', username=user.username) }}" method="post">
<form action="{{ url_for('main.follow', username=user.username) }}" method="post">
{{ form.hidden_tag() }}
{{ form.submit(value=_('Follow'), class_='btn btn-primary') }}
</form>
</p>
{% else %}
<p>
<form action="{{ url_for('unfollow', username=user.username) }}" method="post">
<form action="{{ url_for('main.unfollow', username=user.username) }}" method="post">
{{ form.hidden_tag() }}
{{ form.submit(value=_('Unfollow'), class_='btn btn-primary') }}
</form>

View File

@ -1,14 +1,14 @@
import requests
from flask import current_app
from flask_babel import _
from app import app
def translate(text, source_language, dest_language):
if 'MS_TRANSLATOR_KEY' not in app.config or \
not app.config['MS_TRANSLATOR_KEY']:
if 'MS_TRANSLATOR_KEY' not in current_app.config or \
not current_app.config['MS_TRANSLATOR_KEY']:
return _('Error: the translation service is not configured.')
auth = {
'Ocp-Apim-Subscription-Key': app.config['MS_TRANSLATOR_KEY'],
'Ocp-Apim-Subscription-Key': current_app.config['MS_TRANSLATOR_KEY'],
'Ocp-Apim-Subscription-Region': 'westus'
}
r = requests.post(

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2017-10-05 15:32-0700\n"
"POT-Creation-Date: 2017-11-25 17:17-0800\n"
"PO-Revision-Date: 2017-09-29 23:25-0700\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es\n"
@ -18,151 +18,131 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.5.1\n"
#: app/__init__.py:20
#: app/__init__.py:17
msgid "Please log in to access this page."
msgstr "Por favor ingrese para acceder a esta página."
#: app/email.py:21
msgid "[Microblog] Reset Your Password"
msgstr "[Microblog] Nueva Contraseña"
#: app/forms.py:12 app/forms.py:19 app/forms.py:50
msgid "Username"
msgstr "Nombre de usuario"
#: app/forms.py:13 app/forms.py:21 app/forms.py:43
msgid "Password"
msgstr "Contraseña"
#: app/forms.py:14
msgid "Remember Me"
msgstr "Recordarme"
#: app/forms.py:15 app/templates/login.html:5
msgid "Sign In"
msgstr "Ingresar"
#: app/forms.py:20 app/forms.py:38
msgid "Email"
msgstr "Email"
#: app/forms.py:23 app/forms.py:45
msgid "Repeat Password"
msgstr "Repetir Contraseña"
#: app/forms.py:24 app/templates/register.html:5
msgid "Register"
msgstr "Registrarse"
#: app/forms.py:29 app/forms.py:62
msgid "Please use a different username."
msgstr "Por favor use un nombre de usuario diferente."
#: app/forms.py:34
msgid "Please use a different email address."
msgstr "Por favor use una dirección de email diferente."
#: app/forms.py:39 app/forms.py:46
msgid "Request Password Reset"
msgstr "Pedir una nueva contraseña"
#: app/forms.py:51
msgid "About me"
msgstr "Acerca de mí"
#: app/forms.py:52 app/forms.py:67
msgid "Submit"
msgstr "Enviar"
#: app/forms.py:66
msgid "Say something"
msgstr "Dí algo"
#: app/forms.py:71
msgid "Search"
msgstr "Buscar"
#: app/routes.py:37
msgid "Your post is now live!"
msgstr "¡Tu artículo ha sido publicado!"
#: app/routes.py:73
msgid "Invalid username or password"
msgstr "Nombre de usuario o contraseña inválidos"
#: app/routes.py:99
msgid "Congratulations, you are now a registered user!"
msgstr "¡Felicitaciones, ya eres un usuario registrado!"
#: app/routes.py:114
msgid "Check your email for the instructions to reset your password"
msgstr "Busca en tu email las instrucciones para crear una nueva contraseña"
#: app/routes.py:131
msgid "Your password has been reset."
msgstr "Tu contraseña ha sido cambiada."
#: app/routes.py:159
msgid "Your changes have been saved."
msgstr "Tus cambios han sido salvados."
#: app/routes.py:164 app/templates/edit_profile.html:5
msgid "Edit Profile"
msgstr "Editar Perfil"
#: app/routes.py:173 app/routes.py:189
#, python-format
msgid "User %(username)s not found."
msgstr "El usuario %(username)s no ha sido encontrado."
#: app/routes.py:176
msgid "You cannot follow yourself!"
msgstr "¡No te puedes seguir a tí mismo!"
#: app/routes.py:180
#, python-format
msgid "You are following %(username)s!"
msgstr "¡Ahora estás siguiendo a %(username)s!"
#: app/routes.py:192
msgid "You cannot unfollow yourself!"
msgstr "¡No te puedes dejar de seguir a tí mismo!"
#: app/routes.py:196
#, python-format
msgid "You are not following %(username)s."
msgstr "No estás siguiendo a %(username)s."
#: app/translate.py:10
msgid "Error: the translation service is not configured."
msgstr "Error: el servicio de traducciones no está configurado."
#: app/translate.py:17
#: app/translate.py:18
msgid "Error: the translation service failed."
msgstr "Error el servicio de traducciones ha fallado."
#: app/templates/404.html:4
msgid "Not Found"
msgstr "Página No Encontrada"
#: app/auth/email.py:8
msgid "[Microblog] Reset Your Password"
msgstr "[Microblog] Nueva Contraseña"
#: app/templates/404.html:5 app/templates/500.html:6
msgid "Back"
msgstr "Atrás"
#: app/auth/forms.py:9 app/auth/forms.py:16 app/main/forms.py:10
msgid "Username"
msgstr "Nombre de usuario"
#: app/templates/500.html:4
msgid "An unexpected error has occurred"
msgstr "Ha ocurrido un error inesperado"
#: app/auth/forms.py:10 app/auth/forms.py:18 app/auth/forms.py:41
msgid "Password"
msgstr "Contraseña"
#: app/templates/500.html:5
msgid "The administrator has been notified. Sorry for the inconvenience!"
msgstr "El administrador ha sido notificado. ¡Lamentamos la inconveniencia!"
#: app/auth/forms.py:11
msgid "Remember Me"
msgstr "Recordarme"
#: app/templates/_post.html:9
#: app/auth/forms.py:12 app/templates/auth/login.html:5
msgid "Sign In"
msgstr "Ingresar"
#: app/auth/forms.py:17 app/auth/forms.py:36
msgid "Email"
msgstr "Email"
#: app/auth/forms.py:20 app/auth/forms.py:43
msgid "Repeat Password"
msgstr "Repetir Contraseña"
#: app/auth/forms.py:22 app/templates/auth/register.html:5
msgid "Register"
msgstr "Registrarse"
#: app/auth/forms.py:27 app/main/forms.py:23
msgid "Please use a different username."
msgstr "Por favor use un nombre de usuario diferente."
#: app/auth/forms.py:32
msgid "Please use a different email address."
msgstr "Por favor use una dirección de email diferente."
#: app/auth/forms.py:37 app/auth/forms.py:45
msgid "Request Password Reset"
msgstr "Pedir una nueva contraseña"
#: app/auth/routes.py:20
msgid "Invalid username or password"
msgstr "Nombre de usuario o contraseña inválidos"
#: app/auth/routes.py:46
msgid "Congratulations, you are now a registered user!"
msgstr "¡Felicitaciones, ya eres un usuario registrado!"
#: app/auth/routes.py:61
msgid "Check your email for the instructions to reset your password"
msgstr "Busca en tu email las instrucciones para crear una nueva contraseña"
#: app/auth/routes.py:78
msgid "Your password has been reset."
msgstr "Tu contraseña ha sido cambiada."
#: app/main/forms.py:11
msgid "About me"
msgstr "Acerca de mí"
#: app/main/forms.py:13 app/main/forms.py:28
msgid "Submit"
msgstr "Enviar"
#: app/main/forms.py:27
msgid "Say something"
msgstr "Dí algo"
#: app/main/routes.py:35
msgid "Your post is now live!"
msgstr "¡Tu artículo ha sido publicado!"
#: app/main/routes.py:86
msgid "Your changes have been saved."
msgstr "Tus cambios han sido salvados."
#: app/main/routes.py:91 app/templates/edit_profile.html:5
msgid "Edit Profile"
msgstr "Editar Perfil"
#: app/main/routes.py:100 app/main/routes.py:116
#, python-format
msgid "User %(username)s not found."
msgstr "El usuario %(username)s no ha sido encontrado."
#: app/main/routes.py:103
msgid "You cannot follow yourself!"
msgstr "¡No te puedes seguir a tí mismo!"
#: app/main/routes.py:107
#, python-format
msgid "You are following %(username)s!"
msgstr "¡Ahora estás siguiendo a %(username)s!"
#: app/main/routes.py:119
msgid "You cannot unfollow yourself!"
msgstr "¡No te puedes dejar de seguir a tí mismo!"
#: app/main/routes.py:123
#, python-format
msgid "You are not following %(username)s."
msgstr "No estás siguiendo a %(username)s."
#: app/templates/_post.html:14
#, python-format
msgid "%(username)s said %(when)s"
msgstr "%(username)s dijo %(when)s"
#: app/templates/_post.html:19
#: app/templates/_post.html:25
msgid "Translate"
msgstr "Traducir"
@ -178,19 +158,19 @@ msgstr "Inicio"
msgid "Explore"
msgstr "Explorar"
#: app/templates/base.html:33
#: app/templates/base.html:26
msgid "Login"
msgstr "Ingresar"
#: app/templates/base.html:35
#: app/templates/base.html:28
msgid "Profile"
msgstr "Perfil"
#: app/templates/base.html:36
#: app/templates/base.html:29
msgid "Logout"
msgstr "Salir"
#: app/templates/base.html:73
#: app/templates/base.html:66
msgid "Error: Could not contact server."
msgstr "Error: el servidor no pudo ser contactado."
@ -207,42 +187,6 @@ msgstr "Artículos siguientes"
msgid "Older posts"
msgstr "Artículos previos"
#: app/templates/login.html:12
msgid "New User?"
msgstr "¿Usuario Nuevo?"
#: app/templates/login.html:12
msgid "Click to Register!"
msgstr "¡Haz click aquí para registrarte!"
#: app/templates/login.html:14
msgid "Forgot Your Password?"
msgstr "¿Te olvidaste tu contraseña?"
#: app/templates/login.html:15
msgid "Click to Reset It"
msgstr "Haz click aquí para pedir una nueva"
#: app/templates/reset_password.html:5
msgid "Reset Your Password"
msgstr "Nueva Contraseña"
#: app/templates/reset_password_request.html:5
msgid "Reset Password"
msgstr "Nueva Contraseña"
#: app/templates/search.html:4
msgid "Search Results"
msgstr "Resultados de Búsqueda"
#: app/templates/search.html:12
msgid "Previous results"
msgstr "Resultados previos"
#: app/templates/search.html:17
msgid "Next results"
msgstr "Resultados próximos"
#: app/templates/user.html:8
msgid "User"
msgstr "Usuario"
@ -273,3 +217,42 @@ msgstr "Seguir"
msgid "Unfollow"
msgstr "Dejar de seguir"
#: app/templates/auth/login.html:12
msgid "New User?"
msgstr "¿Usuario Nuevo?"
#: app/templates/auth/login.html:12
msgid "Click to Register!"
msgstr "¡Haz click aquí para registrarte!"
#: app/templates/auth/login.html:14
msgid "Forgot Your Password?"
msgstr "¿Te olvidaste tu contraseña?"
#: app/templates/auth/login.html:15
msgid "Click to Reset It"
msgstr "Haz click aquí para pedir una nueva"
#: app/templates/auth/reset_password.html:5
msgid "Reset Your Password"
msgstr "Nueva Contraseña"
#: app/templates/auth/reset_password_request.html:5
msgid "Reset Password"
msgstr "Nueva Contraseña"
#: app/templates/errors/404.html:4
msgid "Not Found"
msgstr "Página No Encontrada"
#: app/templates/errors/404.html:5 app/templates/errors/500.html:6
msgid "Back"
msgstr "Atrás"
#: app/templates/errors/500.html:4
msgid "An unexpected error has occurred"
msgstr "Ha ocurrido un error inesperado"
#: app/templates/errors/500.html:5
msgid "The administrator has been notified. Sorry for the inconvenience!"
msgstr "El administrador ha sido notificado. ¡Lamentamos la inconveniencia!"

View File

@ -1,5 +1,8 @@
import os
from dotenv import load_dotenv
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))
class Config:

View File

@ -1,8 +1,10 @@
import sqlalchemy as sa
import sqlalchemy.orm as so
from app import app, db, cli
from app import create_app, db
from app.models import User, Post
app = create_app()
@app.shell_context_processor
def make_shell_context():

38
requirements.txt Normal file
View File

@ -0,0 +1,38 @@
aiosmtpd==1.4.4.post2
alembic==1.12.1
atpublic==4.0
attrs==23.1.0
Babel==2.13.1
blinker==1.7.0
certifi==2023.11.17
charset-normalizer==3.3.2
click==8.1.7
dnspython==2.4.2
email-validator==2.1.0.post1
Flask==3.0.0
flask-babel==4.0.0
Flask-Login==0.6.3
Flask-Mail==0.9.1
Flask-Migrate==4.0.5
Flask-Moment==1.0.5
Flask-SQLAlchemy==3.1.1
Flask-WTF==1.2.1
greenlet==3.0.1
idna==3.4
itsdangerous==2.1.2
Jinja2==3.1.2
langdetect==1.0.9
Mako==1.3.0
MarkupSafe==2.1.3
packaging==23.2
PyJWT==2.8.0
python-dotenv==1.0.0
pytz==2023.3.post1
requests==2.31.0
setuptools==68.2.2
six==1.16.0
SQLAlchemy==2.0.23
typing_extensions==4.8.0
urllib3==2.1.0
Werkzeug==3.0.1
WTForms==3.1.1

View File

@ -1,15 +1,20 @@
import os
os.environ['DATABASE_URL'] = 'sqlite://'
#!/usr/bin/env python
from datetime import datetime, timezone, timedelta
import unittest
from app import app, db
from app import create_app, db
from app.models import User, Post
from config import Config
class TestConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite://'
class UserModelCase(unittest.TestCase):
def setUp(self):
self.app_context = app.app_context()
self.app = create_app(TestConfig)
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()