Chapter 15: A Better Application Structure (v0.15)
This commit is contained in:
		
							parent
							
								
									5040abda19
								
							
						
					
					
						commit
						3096f0042e
					
				
							
								
								
									
										106
									
								
								app/__init__.py
								
								
								
								
							
							
						
						
									
										106
									
								
								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 | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -0,0 +1,5 @@ | |||
| from flask import Blueprint | ||||
| 
 | ||||
| bp = Blueprint('auth', __name__) | ||||
| 
 | ||||
| from app.auth import routes | ||||
|  | @ -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)) | ||||
|  | @ -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().__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')) | ||||
|  | @ -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/<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) | ||||
|  | @ -1,14 +1,15 @@ | |||
| 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 | ||||
| 
 | ||||
| 
 | ||||
| @translate.command() | ||||
| @click.argument('lang') | ||||
| def init(lang): | ||||
|  | @ -20,7 +21,6 @@ def init(lang): | |||
|         raise RuntimeError('init command failed') | ||||
|     os.remove('messages.pot') | ||||
| 
 | ||||
| 
 | ||||
| @translate.command() | ||||
| def update(): | ||||
|     """Update all languages.""" | ||||
|  | @ -30,7 +30,6 @@ def update(): | |||
|         raise RuntimeError('update command failed') | ||||
|     os.remove('messages.pot') | ||||
| 
 | ||||
| 
 | ||||
| @translate.command() | ||||
| def compile(): | ||||
|     """Compile all languages.""" | ||||
|  |  | |||
							
								
								
									
										19
									
								
								app/email.py
								
								
								
								
							
							
						
						
									
										19
									
								
								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() | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -0,0 +1,5 @@ | |||
| from flask import Blueprint | ||||
| 
 | ||||
| bp = Blueprint('errors', __name__) | ||||
| 
 | ||||
| from app.errors import handlers | ||||
|  | @ -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 | ||||
|  | @ -0,0 +1,5 @@ | |||
| from flask import Blueprint | ||||
| 
 | ||||
| bp = Blueprint('main', __name__) | ||||
| 
 | ||||
| from app.main import routes | ||||
|  | @ -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().__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')) | ||||
| 
 | ||||
|  | @ -0,0 +1,147 @@ | |||
| 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.following_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/<username>') | ||||
| @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/<username>', 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/<username>', 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(): | ||||
|     data = request.get_json() | ||||
|     return {'text': translate(data['text'], | ||||
|                               data['source_language'], | ||||
|                               data['dest_language'])} | ||||
|  | @ -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( | ||||
|  | @ -69,12 +70,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 | ||||
|  |  | |||
							
								
								
									
										216
									
								
								app/routes.py
								
								
								
								
							
							
						
						
									
										216
									
								
								app/routes.py
								
								
								
								
							|  | @ -1,216 +0,0 @@ | |||
| from datetime import datetime | ||||
| from flask import render_template, flash, redirect, url_for, request, g | ||||
| 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.following_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/<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 = 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/<username>', 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/<username>', 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(): | ||||
|     data = request.get_json() | ||||
|     return {'text': translate(data['text'], | ||||
|                               data['source_language'], | ||||
|                               data['dest_language'])} | ||||
|  | @ -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 %} | ||||
|  |  | |||
|  | @ -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 %} | ||||
|  | @ -13,30 +13,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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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. | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 %} | ||||
|  | @ -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 %} | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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': 'westus' | ||||
|     } | ||||
|     r = requests.post( | ||||
|  |  | |||
|  | @ -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!" | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| 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(): | ||||
|  |  | |||
|  | @ -0,0 +1,37 @@ | |||
| aiosmtpd==1.4.4.post2 | ||||
| alembic==1.10.4 | ||||
| atpublic==3.1.1 | ||||
| attrs==23.1.0 | ||||
| Babel==2.12.1 | ||||
| blinker==1.6.2 | ||||
| certifi==2022.12.7 | ||||
| charset-normalizer==3.1.0 | ||||
| click==8.1.3 | ||||
| dnspython==2.3.0 | ||||
| email-validator==2.0.0.post2 | ||||
| Flask==2.3.2 | ||||
| flask-babel==3.1.0 | ||||
| Flask-Login==0.6.2 | ||||
| Flask-Mail==0.9.1 | ||||
| Flask-Migrate==4.0.4 | ||||
| Flask-Moment==1.0.5 | ||||
| Flask-SQLAlchemy==3.0.3 | ||||
| Flask-WTF==1.1.1 | ||||
| greenlet==2.0.2 | ||||
| idna==3.4 | ||||
| itsdangerous==2.1.2 | ||||
| Jinja2==3.1.2 | ||||
| langdetect==1.0.9 | ||||
| Mako==1.2.4 | ||||
| MarkupSafe==2.1.2 | ||||
| packaging==23.1 | ||||
| PyJWT==2.6.0 | ||||
| python-dotenv==1.0.0 | ||||
| pytz==2023.3 | ||||
| requests==2.29.0 | ||||
| six==1.16.0 | ||||
| SQLAlchemy==2.0.10 | ||||
| typing_extensions==4.5.0 | ||||
| urllib3==1.26.15 | ||||
| Werkzeug==2.3.3 | ||||
| WTForms==3.0.1 | ||||
							
								
								
									
										15
									
								
								tests.py
								
								
								
								
							
							
						
						
									
										15
									
								
								tests.py
								
								
								
								
							|  | @ -1,15 +1,20 @@ | |||
| import os | ||||
| os.environ['DATABASE_URL'] = 'sqlite://' | ||||
| 
 | ||||
| #!/usr/bin/env python | ||||
| 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() | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue