diff --git a/app/__init__.py b/app/__init__.py index 0a6b530..e92cd70 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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 @@ -11,50 +11,74 @@ from flask_moment import Moment from flask_babel import Babel, lazy_gettext as _l from config import Config -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) -bootstrap = Bootstrap(app) -moment = Moment(app) -babel = Babel(app) +mail = Mail() +bootstrap = Bootstrap() +moment = Moment() +babel = Babel() -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) +def create_app(config_class=Config): + app = Flask(__name__) + app.config.from_object(config_class) - app.logger.setLevel(logging.INFO) - app.logger.info('Microblog startup') + db.init_app(app) + migrate.init_app(app, db) + login.init_app(app) + mail.init_app(app) + bootstrap.init_app(app) + moment.init_app(app) + babel.init_app(app) + + 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: + 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 @babel.localeselector def get_locale(): - return request.accept_languages.best_match(app.config['LANGUAGES']) + return request.accept_languages.best_match(current_app.config['LANGUAGES']) -from app import routes, models, errors +from app import models diff --git a/app/auth/__init__.py b/app/auth/__init__.py new file mode 100644 index 0000000..088b033 --- /dev/null +++ b/app/auth/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('auth', __name__) + +from app.auth import routes diff --git a/app/auth/email.py b/app/auth/email.py new file mode 100644 index 0000000..98755ac --- /dev/null +++ b/app/auth/email.py @@ -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)) diff --git a/app/forms.py b/app/auth/forms.py similarity index 65% rename from app/forms.py rename to app/auth/forms.py index 76a3901..c1dd3eb 100644 --- a/app/forms.py +++ b/app/auth/forms.py @@ -1,8 +1,6 @@ from flask_wtf import FlaskForm -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 from flask_babel import _, lazy_gettext as _l from app.models import User @@ -45,29 +43,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(EditProfileForm, self).__init__(*args, **kwargs) - self.original_username = original_username - - def validate_username(self, username): - if username.data != self.original_username: - user = User.query.filter_by(username=self.username.data).first() - 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()]) - submit = SubmitField(_l('Submit')) diff --git a/app/auth/routes.py b/app/auth/routes.py new file mode 100644 index 0000000..b3f1d72 --- /dev/null +++ b/app/auth/routes.py @@ -0,0 +1,82 @@ +from flask import render_template, redirect, url_for, flash, request +from werkzeug.urls import url_parse +from flask_login import login_user, logout_user, current_user +from flask_babel import _ +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 = User.query.filter_by(username=form.username.data).first() + 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 url_parse(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 = User.query.filter_by(email=form.email.data).first() + 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/', 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) diff --git a/app/cli.py b/app/cli.py index 8c6697a..e9b3d76 100644 --- a/app/cli.py +++ b/app/cli.py @@ -1,38 +1,35 @@ import os import click -from app import app -@app.cli.group() -def translate(): - """Translation and localization commands.""" - pass +def register(app): + @app.cli.group() + def translate(): + """Translation and localization commands.""" + pass + @translate.command() + @click.argument('lang') + def init(lang): + """Initialize a new language.""" + if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): + raise RuntimeError('extract command failed') + if os.system( + 'pybabel init -i messages.pot -d app/translations -l ' + lang): + raise RuntimeError('init command failed') + os.remove('messages.pot') -@translate.command() -@click.argument('lang') -def init(lang): - """Initialize a new language.""" - if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): - raise RuntimeError('extract command failed') - if os.system( - 'pybabel init -i messages.pot -d app/translations -l ' + lang): - raise RuntimeError('init command failed') - os.remove('messages.pot') + @translate.command() + def update(): + """Update all languages.""" + if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): + raise RuntimeError('extract command failed') + if os.system('pybabel update -i messages.pot -d app/translations'): + raise RuntimeError('update command failed') + os.remove('messages.pot') - -@translate.command() -def update(): - """Update all languages.""" - if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): - raise RuntimeError('extract command failed') - if os.system('pybabel update -i messages.pot -d app/translations'): - raise RuntimeError('update command failed') - os.remove('messages.pot') - - -@translate.command() -def compile(): - """Compile all languages.""" - if os.system('pybabel compile -d app/translations'): - raise RuntimeError('compile command failed') + @translate.command() + def compile(): + """Compile all languages.""" + if os.system('pybabel compile -d app/translations'): + raise RuntimeError('compile command failed') diff --git a/app/email.py b/app/email.py index 1f4d46f..ee23da8 100644 --- a/app/email.py +++ b/app/email.py @@ -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() diff --git a/app/errors.py b/app/errors.py deleted file mode 100644 index ed214c4..0000000 --- a/app/errors.py +++ /dev/null @@ -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 diff --git a/app/errors/__init__.py b/app/errors/__init__.py new file mode 100644 index 0000000..5701c1d --- /dev/null +++ b/app/errors/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('errors', __name__) + +from app.errors import handlers diff --git a/app/errors/handlers.py b/app/errors/handlers.py new file mode 100644 index 0000000..4a40ad9 --- /dev/null +++ b/app/errors/handlers.py @@ -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 diff --git a/app/main/__init__.py b/app/main/__init__.py new file mode 100644 index 0000000..3b580b0 --- /dev/null +++ b/app/main/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('main', __name__) + +from app.main import routes diff --git a/app/main/forms.py b/app/main/forms.py new file mode 100644 index 0000000..ce62e6a --- /dev/null +++ b/app/main/forms.py @@ -0,0 +1,33 @@ +from flask import request +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, TextAreaField +from wtforms.validators import ValidationError, DataRequired, Length +from flask_babel import _, lazy_gettext as _l +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(EditProfileForm, self).__init__(*args, **kwargs) + self.original_username = original_username + + def validate_username(self, username): + if username.data != self.original_username: + user = User.query.filter_by(username=self.username.data).first() + 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()]) + submit = SubmitField(_l('Submit')) + diff --git a/app/main/routes.py b/app/main/routes.py new file mode 100644 index 0000000..bce9dce --- /dev/null +++ b/app/main/routes.py @@ -0,0 +1,146 @@ +from datetime import datetime +from flask import render_template, flash, redirect, url_for, request, g, \ + jsonify, current_app +from flask_login import current_user, login_required +from flask_babel import _, get_locale +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.utcnow() + 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 = current_user.followed_posts().paginate( + 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) + posts = Post.query.order_by(Post.timestamp.desc()).paginate( + 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/') +@login_required +def user(username): + user = User.query.filter_by(username=username).first_or_404() + page = request.args.get('page', 1, type=int) + posts = user.posts.order_by(Post.timestamp.desc()).paginate( + 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/', methods=['POST']) +@login_required +def follow(username): + form = EmptyForm() + if form.validate_on_submit(): + user = User.query.filter_by(username=username).first() + 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/', methods=['POST']) +@login_required +def unfollow(username): + form = EmptyForm() + if form.validate_on_submit(): + user = User.query.filter_by(username=username).first() + 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(): + return jsonify({'text': translate(request.form['text'], + request.form['source_language'], + request.form['dest_language'])}) diff --git a/app/models.py b/app/models.py index a4db0be..4ad6ad6 100644 --- a/app/models.py +++ b/app/models.py @@ -1,10 +1,11 @@ from datetime import datetime from hashlib import md5 from time import time +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 = db.Table( @@ -64,12 +65,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: return diff --git a/app/routes.py b/app/routes.py deleted file mode 100644 index 5f1c7a3..0000000 --- a/app/routes.py +++ /dev/null @@ -1,216 +0,0 @@ -from datetime import datetime -from flask import render_template, flash, redirect, url_for, request, g, \ - jsonify -from flask_login import login_user, logout_user, current_user, login_required -from werkzeug.urls import url_parse -from flask_babel import _, get_locale -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.utcnow() - 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 = current_user.followed_posts().paginate( - 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) - posts = Post.query.order_by(Post.timestamp.desc()).paginate( - 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 = User.query.filter_by(username=form.username.data).first() - 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 url_parse(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 = User.query.filter_by(email=form.email.data).first() - 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/', 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/') -@login_required -def user(username): - user = User.query.filter_by(username=username).first_or_404() - page = request.args.get('page', 1, type=int) - posts = user.posts.order_by(Post.timestamp.desc()).paginate( - 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/', methods=['POST']) -@login_required -def follow(username): - form = EmptyForm() - if form.validate_on_submit(): - user = User.query.filter_by(username=username).first() - 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/', methods=['POST']) -@login_required -def unfollow(username): - form = EmptyForm() - if form.validate_on_submit(): - user = User.query.filter_by(username=username).first() - 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(): - return jsonify({'text': translate(request.form['text'], - request.form['source_language'], - request.form['dest_language'])}) diff --git a/app/templates/_post.html b/app/templates/_post.html index 1c13800..895fd47 100644 --- a/app/templates/_post.html +++ b/app/templates/_post.html @@ -1,13 +1,13 @@
- + {% set user_link %} - + {{ post.author.username }} {% endset %} diff --git a/app/templates/login.html b/app/templates/auth/login.html similarity index 61% rename from app/templates/login.html rename to app/templates/auth/login.html index 56b4c03..fe46cce 100644 --- a/app/templates/login.html +++ b/app/templates/auth/login.html @@ -9,9 +9,9 @@
-

{{ _('New User?') }} {{ _('Click to Register!') }}

+

{{ _('New User?') }} {{ _('Click to Register!') }}

{{ _('Forgot Your Password?') }} - {{ _('Click to Reset It') }} + {{ _('Click to Reset It') }}

{% endblock %} diff --git a/app/templates/register.html b/app/templates/auth/register.html similarity index 100% rename from app/templates/register.html rename to app/templates/auth/register.html diff --git a/app/templates/reset_password.html b/app/templates/auth/reset_password.html similarity index 100% rename from app/templates/reset_password.html rename to app/templates/auth/reset_password.html diff --git a/app/templates/reset_password_request.html b/app/templates/auth/reset_password_request.html similarity index 100% rename from app/templates/reset_password_request.html rename to app/templates/auth/reset_password_request.html diff --git a/app/templates/base.html b/app/templates/base.html index 9874556..6a54732 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -14,19 +14,19 @@ - Microblog + Microblog diff --git a/app/templates/email/reset_password.html b/app/templates/email/reset_password.html index a39a9c4..b60907a 100644 --- a/app/templates/email/reset_password.html +++ b/app/templates/email/reset_password.html @@ -1,12 +1,12 @@

Dear {{ user.username }},

To reset your password - + click here .

Alternatively, you can paste the following link in your browser's address bar:

-

{{ 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.

Sincerely,

The Microblog Team

diff --git a/app/templates/email/reset_password.txt b/app/templates/email/reset_password.txt index 5387347..bd107b5 100644 --- a/app/templates/email/reset_password.txt +++ b/app/templates/email/reset_password.txt @@ -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. diff --git a/app/templates/404.html b/app/templates/errors/404.html similarity index 59% rename from app/templates/404.html rename to app/templates/errors/404.html index adc4da2..9cb8d5e 100644 --- a/app/templates/404.html +++ b/app/templates/errors/404.html @@ -2,5 +2,5 @@ {% block app_content %}

{{ _('Not Found') }}

-

{{ _('Back') }}

+

{{ _('Back') }}

{% endblock %} diff --git a/app/templates/500.html b/app/templates/errors/500.html similarity index 75% rename from app/templates/500.html rename to app/templates/errors/500.html index 5975fde..a178ac8 100644 --- a/app/templates/500.html +++ b/app/templates/errors/500.html @@ -3,5 +3,5 @@ {% block app_content %}

{{ _('An unexpected error has occurred') }}

{{ _('The administrator has been notified. Sorry for the inconvenience!') }}

-

{{ _('Back') }}

+

{{ _('Back') }}

{% endblock %} diff --git a/app/templates/user.html b/app/templates/user.html index 34d807e..f264891 100644 --- a/app/templates/user.html +++ b/app/templates/user.html @@ -12,17 +12,17 @@ {% endif %}

{{ _('%(count)d followers', count=user.followers.count()) }}, {{ _('%(count)d following', count=user.followed.count()) }}

{% if user == current_user %} -

{{ _('Edit your profile') }}

+

{{ _('Edit your profile') }}

{% elif not current_user.is_following(user) %}

-

+ {{ form.hidden_tag() }} {{ form.submit(value=_('Follow'), class_='btn btn-default') }}

{% else %}

-

+ {{ form.hidden_tag() }} {{ form.submit(value=_('Unfollow'), class_='btn btn-default') }}
diff --git a/app/translate.py b/app/translate.py index d4a7998..888ed3d 100644 --- a/app/translate.py +++ b/app/translate.py @@ -1,15 +1,15 @@ import json 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': 'westus2'} r = requests.post( 'https://api.cognitive.microsofttranslator.com' diff --git a/app/translations/es/LC_MESSAGES/messages.po b/app/translations/es/LC_MESSAGES/messages.po index d468175..e21f644 100644 --- a/app/translations/es/LC_MESSAGES/messages.po +++ b/app/translations/es/LC_MESSAGES/messages.po @@ -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 \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!" diff --git a/config.py b/config.py index 880701f..cbb7c2c 100644 --- a/config.py +++ b/config.py @@ -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(object): diff --git a/microblog.py b/microblog.py index 637d9a3..20d62e6 100644 --- a/microblog.py +++ b/microblog.py @@ -1,6 +1,9 @@ -from app import app, db, cli +from app import create_app, db, cli from app.models import User, Post +app = create_app() +cli.register(app) + @app.shell_context_processor def make_shell_context(): diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bea8923 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,37 @@ +alembic==1.6.5 +Babel==2.9.1 +blinker==1.4 +certifi==2021.5.30 +chardet==4.0.0 +click==8.0.1 +dnspython==2.1.0 +dominate==2.6.0 +email-validator==1.1.3 +Flask==2.0.1 +Flask-Babel==2.0.0 +Flask-Bootstrap==3.3.7.1 +Flask-Login==0.5.0 +Flask-Mail==0.9.1 +Flask-Migrate==3.0.1 +Flask-Moment==1.0.1 +Flask-SQLAlchemy==2.5.1 +Flask-WTF==0.15.1 +greenlet==1.1.0 +idna==2.10 +itsdangerous==2.0.1 +Jinja2==3.0.1 +langdetect==1.0.9 +Mako==1.1.4 +MarkupSafe==2.0.1 +PyJWT==2.1.0 +python-dateutil==2.8.1 +python-dotenv==0.18.0 +python-editor==1.0.4 +pytz==2021.1 +requests==2.25.1 +six==1.16.0 +SQLAlchemy==1.4.20 +urllib3==1.26.6 +visitor==0.1.3 +Werkzeug==2.0.1 +WTForms==2.3.3 diff --git a/tests.py b/tests.py index ea99d5a..a890e69 100755 --- a/tests.py +++ b/tests.py @@ -1,16 +1,20 @@ #!/usr/bin/env python -import os -os.environ['DATABASE_URL'] = 'sqlite://' - from datetime import datetime, 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()