Compare commits
	
		
			No commits in common. "main" and "v0.13" have entirely different histories.
		
	
	
		
							
								
								
									
										23
									
								
								Dockerfile
								
								
								
								
							
							
						
						
									
										23
									
								
								Dockerfile
								
								
								
								
							|  | @ -1,23 +0,0 @@ | ||||||
| FROM python:slim |  | ||||||
| 
 |  | ||||||
| RUN useradd microblog |  | ||||||
| 
 |  | ||||||
| WORKDIR /home/microblog |  | ||||||
| 
 |  | ||||||
| COPY requirements.txt requirements.txt |  | ||||||
| RUN python -m venv venv |  | ||||||
| RUN venv/bin/pip install -r requirements.txt |  | ||||||
| RUN venv/bin/pip install gunicorn pymysql cryptography |  | ||||||
| 
 |  | ||||||
| COPY app app |  | ||||||
| COPY migrations migrations |  | ||||||
| COPY microblog.py config.py boot.sh ./ |  | ||||||
| RUN chmod a+x boot.sh |  | ||||||
| 
 |  | ||||||
| ENV FLASK_APP microblog.py |  | ||||||
| 
 |  | ||||||
| RUN chown -R microblog:microblog ./ |  | ||||||
| USER microblog |  | ||||||
| 
 |  | ||||||
| EXPOSE 5000 |  | ||||||
| ENTRYPOINT ["./boot.sh"] |  | ||||||
							
								
								
									
										2
									
								
								Procfile
								
								
								
								
							
							
						
						
									
										2
									
								
								Procfile
								
								
								
								
							|  | @ -1,2 +0,0 @@ | ||||||
| web: flask db upgrade; flask translate compile; gunicorn microblog:app |  | ||||||
| worker: rq worker microblog-tasks |  | ||||||
|  | @ -1,7 +0,0 @@ | ||||||
| Vagrant.configure("2") do |config| |  | ||||||
|   config.vm.box = "ubuntu/focal64" |  | ||||||
|   config.vm.network "private_network", ip: "192.168.33.10" |  | ||||||
|   config.vm.provider "virtualbox" do |vb| |  | ||||||
|     vb.memory = "2048" |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import logging | import logging | ||||||
| from logging.handlers import SMTPHandler, RotatingFileHandler | from logging.handlers import SMTPHandler, RotatingFileHandler | ||||||
| import os | import os | ||||||
| from flask import Flask, request, current_app | from flask import Flask, request | ||||||
| from flask_sqlalchemy import SQLAlchemy | from flask_sqlalchemy import SQLAlchemy | ||||||
| from flask_migrate import Migrate | from flask_migrate import Migrate | ||||||
| from flask_login import LoginManager | from flask_login import LoginManager | ||||||
|  | @ -9,56 +9,25 @@ from flask_mail import Mail | ||||||
| from flask_bootstrap import Bootstrap | from flask_bootstrap import Bootstrap | ||||||
| from flask_moment import Moment | from flask_moment import Moment | ||||||
| from flask_babel import Babel, lazy_gettext as _l | from flask_babel import Babel, lazy_gettext as _l | ||||||
| from elasticsearch import Elasticsearch |  | ||||||
| from redis import Redis |  | ||||||
| import rq |  | ||||||
| from config import Config | from config import Config | ||||||
| 
 | 
 | ||||||
| db = SQLAlchemy() | app = Flask(__name__) | ||||||
| migrate = Migrate() | app.config.from_object(Config) | ||||||
| login = LoginManager() | db = SQLAlchemy(app) | ||||||
| login.login_view = 'auth.login' | migrate = Migrate(app, db) | ||||||
|  | login = LoginManager(app) | ||||||
|  | login.login_view = 'login' | ||||||
| login.login_message = _l('Please log in to access this page.') | login.login_message = _l('Please log in to access this page.') | ||||||
| mail = Mail() | mail = Mail(app) | ||||||
| bootstrap = Bootstrap() | bootstrap = Bootstrap(app) | ||||||
| moment = Moment() | moment = Moment(app) | ||||||
| babel = Babel() | babel = Babel(app) | ||||||
| 
 | 
 | ||||||
| 
 | if not app.debug: | ||||||
| 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) |  | ||||||
|     bootstrap.init_app(app) |  | ||||||
|     moment.init_app(app) |  | ||||||
|     babel.init_app(app) |  | ||||||
|     app.elasticsearch = Elasticsearch([app.config['ELASTICSEARCH_URL']]) \ |  | ||||||
|         if app.config['ELASTICSEARCH_URL'] else None |  | ||||||
|     app.redis = Redis.from_url(app.config['REDIS_URL']) |  | ||||||
|     app.task_queue = rq.Queue('microblog-tasks', connection=app.redis) |  | ||||||
| 
 |  | ||||||
|     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.api import bp as api_bp |  | ||||||
|     app.register_blueprint(api_bp, url_prefix='/api') |  | ||||||
| 
 |  | ||||||
|     if not app.debug and not app.testing: |  | ||||||
|     if app.config['MAIL_SERVER']: |     if app.config['MAIL_SERVER']: | ||||||
|         auth = None |         auth = None | ||||||
|         if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']: |         if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']: | ||||||
|                 auth = (app.config['MAIL_USERNAME'], |             auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD']) | ||||||
|                         app.config['MAIL_PASSWORD']) |  | ||||||
|         secure = None |         secure = None | ||||||
|         if app.config['MAIL_USE_TLS']: |         if app.config['MAIL_USE_TLS']: | ||||||
|             secure = () |             secure = () | ||||||
|  | @ -70,30 +39,22 @@ def create_app(config_class=Config): | ||||||
|         mail_handler.setLevel(logging.ERROR) |         mail_handler.setLevel(logging.ERROR) | ||||||
|         app.logger.addHandler(mail_handler) |         app.logger.addHandler(mail_handler) | ||||||
| 
 | 
 | ||||||
|         if app.config['LOG_TO_STDOUT']: |  | ||||||
|             stream_handler = logging.StreamHandler() |  | ||||||
|             stream_handler.setLevel(logging.INFO) |  | ||||||
|             app.logger.addHandler(stream_handler) |  | ||||||
|         else: |  | ||||||
|     if not os.path.exists('logs'): |     if not os.path.exists('logs'): | ||||||
|         os.mkdir('logs') |         os.mkdir('logs') | ||||||
|             file_handler = RotatingFileHandler('logs/microblog.log', |     file_handler = RotatingFileHandler('logs/microblog.log', maxBytes=10240, | ||||||
|                                                maxBytes=10240, backupCount=10) |                                        backupCount=10) | ||||||
|     file_handler.setFormatter(logging.Formatter( |     file_handler.setFormatter(logging.Formatter( | ||||||
|                 '%(asctime)s %(levelname)s: %(message)s ' |         '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')) | ||||||
|                 '[in %(pathname)s:%(lineno)d]')) |  | ||||||
|     file_handler.setLevel(logging.INFO) |     file_handler.setLevel(logging.INFO) | ||||||
|     app.logger.addHandler(file_handler) |     app.logger.addHandler(file_handler) | ||||||
| 
 | 
 | ||||||
|     app.logger.setLevel(logging.INFO) |     app.logger.setLevel(logging.INFO) | ||||||
|     app.logger.info('Microblog startup') |     app.logger.info('Microblog startup') | ||||||
| 
 | 
 | ||||||
|     return app |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| @babel.localeselector | @babel.localeselector | ||||||
| def get_locale(): | def get_locale(): | ||||||
|     return request.accept_languages.best_match(current_app.config['LANGUAGES']) |     return request.accept_languages.best_match(app.config['LANGUAGES']) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| from app import models | from app import routes, models, errors | ||||||
|  |  | ||||||
|  | @ -1,5 +0,0 @@ | ||||||
| from flask import Blueprint |  | ||||||
| 
 |  | ||||||
| bp = Blueprint('api', __name__) |  | ||||||
| 
 |  | ||||||
| from app.api import users, errors, tokens |  | ||||||
|  | @ -1,28 +0,0 @@ | ||||||
| from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth |  | ||||||
| from app.models import User |  | ||||||
| from app.api.errors import error_response |  | ||||||
| 
 |  | ||||||
| basic_auth = HTTPBasicAuth() |  | ||||||
| token_auth = HTTPTokenAuth() |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @basic_auth.verify_password |  | ||||||
| def verify_password(username, password): |  | ||||||
|     user = User.query.filter_by(username=username).first() |  | ||||||
|     if user and user.check_password(password): |  | ||||||
|         return user |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @basic_auth.error_handler |  | ||||||
| def basic_auth_error(status): |  | ||||||
|     return error_response(status) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @token_auth.verify_token |  | ||||||
| def verify_token(token): |  | ||||||
|     return User.check_token(token) if token else None |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @token_auth.error_handler |  | ||||||
| def token_auth_error(status): |  | ||||||
|     return error_response(status) |  | ||||||
|  | @ -1,15 +0,0 @@ | ||||||
| from flask import jsonify |  | ||||||
| from werkzeug.http import HTTP_STATUS_CODES |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def error_response(status_code, message=None): |  | ||||||
|     payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')} |  | ||||||
|     if message: |  | ||||||
|         payload['message'] = message |  | ||||||
|     response = jsonify(payload) |  | ||||||
|     response.status_code = status_code |  | ||||||
|     return response |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def bad_request(message): |  | ||||||
|     return error_response(400, message) |  | ||||||
|  | @ -1,20 +0,0 @@ | ||||||
| from flask import jsonify |  | ||||||
| from app import db |  | ||||||
| from app.api import bp |  | ||||||
| from app.api.auth import basic_auth, token_auth |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.route('/tokens', methods=['POST']) |  | ||||||
| @basic_auth.login_required |  | ||||||
| def get_token(): |  | ||||||
|     token = basic_auth.current_user().get_token() |  | ||||||
|     db.session.commit() |  | ||||||
|     return jsonify({'token': token}) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.route('/tokens', methods=['DELETE']) |  | ||||||
| @token_auth.login_required |  | ||||||
| def revoke_token(): |  | ||||||
|     token_auth.current_user().revoke_token() |  | ||||||
|     db.session.commit() |  | ||||||
|     return '', 204 |  | ||||||
|  | @ -1,80 +0,0 @@ | ||||||
| from flask import jsonify, request, url_for, abort |  | ||||||
| from app import db |  | ||||||
| from app.models import User |  | ||||||
| from app.api import bp |  | ||||||
| from app.api.auth import token_auth |  | ||||||
| from app.api.errors import bad_request |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.route('/users/<int:id>', methods=['GET']) |  | ||||||
| @token_auth.login_required |  | ||||||
| def get_user(id): |  | ||||||
|     return jsonify(User.query.get_or_404(id).to_dict()) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.route('/users', methods=['GET']) |  | ||||||
| @token_auth.login_required |  | ||||||
| def get_users(): |  | ||||||
|     page = request.args.get('page', 1, type=int) |  | ||||||
|     per_page = min(request.args.get('per_page', 10, type=int), 100) |  | ||||||
|     data = User.to_collection_dict(User.query, page, per_page, 'api.get_users') |  | ||||||
|     return jsonify(data) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.route('/users/<int:id>/followers', methods=['GET']) |  | ||||||
| @token_auth.login_required |  | ||||||
| def get_followers(id): |  | ||||||
|     user = User.query.get_or_404(id) |  | ||||||
|     page = request.args.get('page', 1, type=int) |  | ||||||
|     per_page = min(request.args.get('per_page', 10, type=int), 100) |  | ||||||
|     data = User.to_collection_dict(user.followers, page, per_page, |  | ||||||
|                                    'api.get_followers', id=id) |  | ||||||
|     return jsonify(data) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.route('/users/<int:id>/followed', methods=['GET']) |  | ||||||
| @token_auth.login_required |  | ||||||
| def get_followed(id): |  | ||||||
|     user = User.query.get_or_404(id) |  | ||||||
|     page = request.args.get('page', 1, type=int) |  | ||||||
|     per_page = min(request.args.get('per_page', 10, type=int), 100) |  | ||||||
|     data = User.to_collection_dict(user.followed, page, per_page, |  | ||||||
|                                    'api.get_followed', id=id) |  | ||||||
|     return jsonify(data) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.route('/users', methods=['POST']) |  | ||||||
| def create_user(): |  | ||||||
|     data = request.get_json() or {} |  | ||||||
|     if 'username' not in data or 'email' not in data or 'password' not in data: |  | ||||||
|         return bad_request('must include username, email and password fields') |  | ||||||
|     if User.query.filter_by(username=data['username']).first(): |  | ||||||
|         return bad_request('please use a different username') |  | ||||||
|     if User.query.filter_by(email=data['email']).first(): |  | ||||||
|         return bad_request('please use a different email address') |  | ||||||
|     user = User() |  | ||||||
|     user.from_dict(data, new_user=True) |  | ||||||
|     db.session.add(user) |  | ||||||
|     db.session.commit() |  | ||||||
|     response = jsonify(user.to_dict()) |  | ||||||
|     response.status_code = 201 |  | ||||||
|     response.headers['Location'] = url_for('api.get_user', id=user.id) |  | ||||||
|     return response |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.route('/users/<int:id>', methods=['PUT']) |  | ||||||
| @token_auth.login_required |  | ||||||
| def update_user(id): |  | ||||||
|     if token_auth.current_user().id != id: |  | ||||||
|         abort(403) |  | ||||||
|     user = User.query.get_or_404(id) |  | ||||||
|     data = request.get_json() or {} |  | ||||||
|     if 'username' in data and data['username'] != user.username and \ |  | ||||||
|             User.query.filter_by(username=data['username']).first(): |  | ||||||
|         return bad_request('please use a different username') |  | ||||||
|     if 'email' in data and data['email'] != user.email and \ |  | ||||||
|             User.query.filter_by(email=data['email']).first(): |  | ||||||
|         return bad_request('please use a different email address') |  | ||||||
|     user.from_dict(data, new_user=False) |  | ||||||
|     db.session.commit() |  | ||||||
|     return jsonify(user.to_dict()) |  | ||||||
|  | @ -1,5 +0,0 @@ | ||||||
| from flask import Blueprint |  | ||||||
| 
 |  | ||||||
| bp = Blueprint('auth', __name__) |  | ||||||
| 
 |  | ||||||
| from app.auth import routes |  | ||||||
|  | @ -1,14 +0,0 @@ | ||||||
| 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,82 +0,0 @@ | ||||||
| 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) |  | ||||||
							
								
								
									
										23
									
								
								app/cli.py
								
								
								
								
							
							
						
						
									
										23
									
								
								app/cli.py
								
								
								
								
							|  | @ -1,16 +1,17 @@ | ||||||
| import os | import os | ||||||
| import click | import click | ||||||
|  | from app import app | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def register(app): | @app.cli.group() | ||||||
|     @app.cli.group() | def translate(): | ||||||
|     def translate(): |  | ||||||
|     """Translation and localization commands.""" |     """Translation and localization commands.""" | ||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
|     @translate.command() | 
 | ||||||
|     @click.argument('lang') | @translate.command() | ||||||
|     def init(lang): | @click.argument('lang') | ||||||
|  | def init(lang): | ||||||
|     """Initialize a new language.""" |     """Initialize a new language.""" | ||||||
|     if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): |     if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): | ||||||
|         raise RuntimeError('extract command failed') |         raise RuntimeError('extract command failed') | ||||||
|  | @ -19,8 +20,9 @@ def register(app): | ||||||
|         raise RuntimeError('init command failed') |         raise RuntimeError('init command failed') | ||||||
|     os.remove('messages.pot') |     os.remove('messages.pot') | ||||||
| 
 | 
 | ||||||
|     @translate.command() | 
 | ||||||
|     def update(): | @translate.command() | ||||||
|  | def update(): | ||||||
|     """Update all languages.""" |     """Update all languages.""" | ||||||
|     if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): |     if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): | ||||||
|         raise RuntimeError('extract command failed') |         raise RuntimeError('extract command failed') | ||||||
|  | @ -28,8 +30,9 @@ def register(app): | ||||||
|         raise RuntimeError('update command failed') |         raise RuntimeError('update command failed') | ||||||
|     os.remove('messages.pot') |     os.remove('messages.pot') | ||||||
| 
 | 
 | ||||||
|     @translate.command() | 
 | ||||||
|     def compile(): | @translate.command() | ||||||
|  | def compile(): | ||||||
|     """Compile all languages.""" |     """Compile all languages.""" | ||||||
|     if os.system('pybabel compile -d app/translations'): |     if os.system('pybabel compile -d app/translations'): | ||||||
|         raise RuntimeError('compile command failed') |         raise RuntimeError('compile command failed') | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								app/email.py
								
								
								
								
							
							
						
						
									
										28
									
								
								app/email.py
								
								
								
								
							|  | @ -1,7 +1,8 @@ | ||||||
| from threading import Thread | from threading import Thread | ||||||
| from flask import current_app | from flask import render_template | ||||||
| from flask_mail import Message | from flask_mail import Message | ||||||
| from app import mail | from flask_babel import _ | ||||||
|  | from app import app, mail | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def send_async_email(app, msg): | def send_async_email(app, msg): | ||||||
|  | @ -9,16 +10,19 @@ def send_async_email(app, msg): | ||||||
|         mail.send(msg) |         mail.send(msg) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def send_email(subject, sender, recipients, text_body, html_body, | def send_email(subject, sender, recipients, text_body, html_body): | ||||||
|                attachments=None, sync=False): |  | ||||||
|     msg = Message(subject, sender=sender, recipients=recipients) |     msg = Message(subject, sender=sender, recipients=recipients) | ||||||
|     msg.body = text_body |     msg.body = text_body | ||||||
|     msg.html = html_body |     msg.html = html_body | ||||||
|     if attachments: |     Thread(target=send_async_email, args=(app, msg)).start() | ||||||
|         for attachment in attachments: | 
 | ||||||
|             msg.attach(*attachment) | 
 | ||||||
|     if sync: | def send_password_reset_email(user): | ||||||
|         mail.send(msg) |     token = user.get_reset_password_token() | ||||||
|     else: |     send_email(_('[Microblog] Reset Your Password'), | ||||||
|         Thread(target=send_async_email, |                sender=app.config['ADMINS'][0], | ||||||
|             args=(current_app._get_current_object(), msg)).start() |                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)) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,13 @@ | ||||||
|  | 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 | ||||||
|  | @ -1,5 +0,0 @@ | ||||||
| from flask import Blueprint |  | ||||||
| 
 |  | ||||||
| bp = Blueprint('errors', __name__) |  | ||||||
| 
 |  | ||||||
| from app.errors import handlers |  | ||||||
|  | @ -1,24 +0,0 @@ | ||||||
| from flask import render_template, request |  | ||||||
| from app import db |  | ||||||
| from app.errors import bp |  | ||||||
| from app.api.errors import error_response as api_error_response |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def wants_json_response(): |  | ||||||
|     return request.accept_mimetypes['application/json'] >= \ |  | ||||||
|         request.accept_mimetypes['text/html'] |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.app_errorhandler(404) |  | ||||||
| def not_found_error(error): |  | ||||||
|     if wants_json_response(): |  | ||||||
|         return api_error_response(404) |  | ||||||
|     return render_template('errors/404.html'), 404 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.app_errorhandler(500) |  | ||||||
| def internal_error(error): |  | ||||||
|     db.session.rollback() |  | ||||||
|     if wants_json_response(): |  | ||||||
|         return api_error_response(500) |  | ||||||
|     return render_template('errors/500.html'), 500 |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| from flask_wtf import FlaskForm | from flask_wtf import FlaskForm | ||||||
| from wtforms import StringField, PasswordField, BooleanField, SubmitField | from wtforms import StringField, PasswordField, BooleanField, SubmitField, \ | ||||||
| from wtforms.validators import ValidationError, DataRequired, Email, EqualTo |     TextAreaField | ||||||
|  | from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, \ | ||||||
|  |     Length | ||||||
| from flask_babel import _, lazy_gettext as _l | from flask_babel import _, lazy_gettext as _l | ||||||
| from app.models import User | from app.models import User | ||||||
| 
 | 
 | ||||||
|  | @ -43,3 +45,29 @@ class ResetPasswordForm(FlaskForm): | ||||||
|         _l('Repeat Password'), validators=[DataRequired(), |         _l('Repeat Password'), validators=[DataRequired(), | ||||||
|                                            EqualTo('password')]) |                                            EqualTo('password')]) | ||||||
|     submit = SubmitField(_l('Request Password Reset')) |     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')) | ||||||
|  | @ -1,5 +0,0 @@ | ||||||
| from flask import Blueprint |  | ||||||
| 
 |  | ||||||
| bp = Blueprint('main', __name__) |  | ||||||
| 
 |  | ||||||
| from app.main import routes |  | ||||||
|  | @ -1,49 +0,0 @@ | ||||||
| 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')) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class SearchForm(FlaskForm): |  | ||||||
|     q = StringField(_l('Search'), validators=[DataRequired()]) |  | ||||||
| 
 |  | ||||||
|     def __init__(self, *args, **kwargs): |  | ||||||
|         if 'formdata' not in kwargs: |  | ||||||
|             kwargs['formdata'] = request.args |  | ||||||
|         if 'meta' not in kwargs: |  | ||||||
|             kwargs['meta'] = {'csrf': False} |  | ||||||
|         super(SearchForm, self).__init__(*args, **kwargs) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class MessageForm(FlaskForm): |  | ||||||
|     message = TextAreaField(_l('Message'), validators=[ |  | ||||||
|         DataRequired(), Length(min=1, max=140)]) |  | ||||||
|     submit = SubmitField(_l('Submit')) |  | ||||||
|  | @ -1,232 +0,0 @@ | ||||||
| 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, SearchForm, \ |  | ||||||
|     MessageForm |  | ||||||
| from app.models import User, Post, Message, Notification |  | ||||||
| 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.search_form = SearchForm() |  | ||||||
|     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/<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('/user/<username>/popup') |  | ||||||
| @login_required |  | ||||||
| def user_popup(username): |  | ||||||
|     user = User.query.filter_by(username=username).first_or_404() |  | ||||||
|     form = EmptyForm() |  | ||||||
|     return render_template('user_popup.html', user=user, 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(): |  | ||||||
|     return jsonify({'text': translate(request.form['text'], |  | ||||||
|                                       request.form['source_language'], |  | ||||||
|                                       request.form['dest_language'])}) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.route('/search') |  | ||||||
| @login_required |  | ||||||
| def search(): |  | ||||||
|     if not g.search_form.validate(): |  | ||||||
|         return redirect(url_for('main.explore')) |  | ||||||
|     page = request.args.get('page', 1, type=int) |  | ||||||
|     posts, total = Post.search(g.search_form.q.data, page, |  | ||||||
|                                current_app.config['POSTS_PER_PAGE']) |  | ||||||
|     next_url = url_for('main.search', q=g.search_form.q.data, page=page + 1) \ |  | ||||||
|         if total > page * current_app.config['POSTS_PER_PAGE'] else None |  | ||||||
|     prev_url = url_for('main.search', q=g.search_form.q.data, page=page - 1) \ |  | ||||||
|         if page > 1 else None |  | ||||||
|     return render_template('search.html', title=_('Search'), posts=posts, |  | ||||||
|                            next_url=next_url, prev_url=prev_url) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.route('/send_message/<recipient>', methods=['GET', 'POST']) |  | ||||||
| @login_required |  | ||||||
| def send_message(recipient): |  | ||||||
|     user = User.query.filter_by(username=recipient).first_or_404() |  | ||||||
|     form = MessageForm() |  | ||||||
|     if form.validate_on_submit(): |  | ||||||
|         msg = Message(author=current_user, recipient=user, |  | ||||||
|                       body=form.message.data) |  | ||||||
|         db.session.add(msg) |  | ||||||
|         user.add_notification('unread_message_count', user.new_messages()) |  | ||||||
|         db.session.commit() |  | ||||||
|         flash(_('Your message has been sent.')) |  | ||||||
|         return redirect(url_for('main.user', username=recipient)) |  | ||||||
|     return render_template('send_message.html', title=_('Send Message'), |  | ||||||
|                            form=form, recipient=recipient) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.route('/messages') |  | ||||||
| @login_required |  | ||||||
| def messages(): |  | ||||||
|     current_user.last_message_read_time = datetime.utcnow() |  | ||||||
|     current_user.add_notification('unread_message_count', 0) |  | ||||||
|     db.session.commit() |  | ||||||
|     page = request.args.get('page', 1, type=int) |  | ||||||
|     messages = current_user.messages_received.order_by( |  | ||||||
|         Message.timestamp.desc()).paginate( |  | ||||||
|             page=page, per_page=current_app.config['POSTS_PER_PAGE'], |  | ||||||
|             error_out=False) |  | ||||||
|     next_url = url_for('main.messages', page=messages.next_num) \ |  | ||||||
|         if messages.has_next else None |  | ||||||
|     prev_url = url_for('main.messages', page=messages.prev_num) \ |  | ||||||
|         if messages.has_prev else None |  | ||||||
|     return render_template('messages.html', messages=messages.items, |  | ||||||
|                            next_url=next_url, prev_url=prev_url) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.route('/export_posts') |  | ||||||
| @login_required |  | ||||||
| def export_posts(): |  | ||||||
|     if current_user.get_task_in_progress('export_posts'): |  | ||||||
|         flash(_('An export task is currently in progress')) |  | ||||||
|     else: |  | ||||||
|         current_user.launch_task('export_posts', _('Exporting posts...')) |  | ||||||
|         db.session.commit() |  | ||||||
|     return redirect(url_for('main.user', username=current_user.username)) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @bp.route('/notifications') |  | ||||||
| @login_required |  | ||||||
| def notifications(): |  | ||||||
|     since = request.args.get('since', 0.0, type=float) |  | ||||||
|     notifications = current_user.notifications.filter( |  | ||||||
|         Notification.timestamp > since).order_by(Notification.timestamp.asc()) |  | ||||||
|     return jsonify([{ |  | ||||||
|         'name': n.name, |  | ||||||
|         'data': n.get_data(), |  | ||||||
|         'timestamp': n.timestamp |  | ||||||
|     } for n in notifications]) |  | ||||||
							
								
								
									
										214
									
								
								app/models.py
								
								
								
								
							
							
						
						
									
										214
									
								
								app/models.py
								
								
								
								
							|  | @ -1,85 +1,10 @@ | ||||||
| import base64 | from datetime import datetime | ||||||
| from datetime import datetime, timedelta |  | ||||||
| from hashlib import md5 | from hashlib import md5 | ||||||
| import json |  | ||||||
| import os |  | ||||||
| from time import time | from time import time | ||||||
| from flask import current_app, url_for |  | ||||||
| from flask_login import UserMixin | from flask_login import UserMixin | ||||||
| from werkzeug.security import generate_password_hash, check_password_hash | from werkzeug.security import generate_password_hash, check_password_hash | ||||||
| import jwt | import jwt | ||||||
| import redis | from app import app, db, login | ||||||
| import rq |  | ||||||
| from app import db, login |  | ||||||
| from app.search import add_to_index, remove_from_index, query_index |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class SearchableMixin(object): |  | ||||||
|     @classmethod |  | ||||||
|     def search(cls, expression, page, per_page): |  | ||||||
|         ids, total = query_index(cls.__tablename__, expression, page, per_page) |  | ||||||
|         if total == 0: |  | ||||||
|             return cls.query.filter_by(id=0), 0 |  | ||||||
|         when = [] |  | ||||||
|         for i in range(len(ids)): |  | ||||||
|             when.append((ids[i], i)) |  | ||||||
|         return cls.query.filter(cls.id.in_(ids)).order_by( |  | ||||||
|             db.case(when, value=cls.id)), total |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def before_commit(cls, session): |  | ||||||
|         session._changes = { |  | ||||||
|             'add': list(session.new), |  | ||||||
|             'update': list(session.dirty), |  | ||||||
|             'delete': list(session.deleted) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def after_commit(cls, session): |  | ||||||
|         for obj in session._changes['add']: |  | ||||||
|             if isinstance(obj, SearchableMixin): |  | ||||||
|                 add_to_index(obj.__tablename__, obj) |  | ||||||
|         for obj in session._changes['update']: |  | ||||||
|             if isinstance(obj, SearchableMixin): |  | ||||||
|                 add_to_index(obj.__tablename__, obj) |  | ||||||
|         for obj in session._changes['delete']: |  | ||||||
|             if isinstance(obj, SearchableMixin): |  | ||||||
|                 remove_from_index(obj.__tablename__, obj) |  | ||||||
|         session._changes = None |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def reindex(cls): |  | ||||||
|         for obj in cls.query: |  | ||||||
|             add_to_index(cls.__tablename__, obj) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| db.event.listen(db.session, 'before_commit', SearchableMixin.before_commit) |  | ||||||
| db.event.listen(db.session, 'after_commit', SearchableMixin.after_commit) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class PaginatedAPIMixin(object): |  | ||||||
|     @staticmethod |  | ||||||
|     def to_collection_dict(query, page, per_page, endpoint, **kwargs): |  | ||||||
|         resources = query.paginate(page=page, per_page=per_page, |  | ||||||
|                                    error_out=False) |  | ||||||
|         data = { |  | ||||||
|             'items': [item.to_dict() for item in resources.items], |  | ||||||
|             '_meta': { |  | ||||||
|                 'page': page, |  | ||||||
|                 'per_page': per_page, |  | ||||||
|                 'total_pages': resources.pages, |  | ||||||
|                 'total_items': resources.total |  | ||||||
|             }, |  | ||||||
|             '_links': { |  | ||||||
|                 'self': url_for(endpoint, page=page, per_page=per_page, |  | ||||||
|                                 **kwargs), |  | ||||||
|                 'next': url_for(endpoint, page=page + 1, per_page=per_page, |  | ||||||
|                                 **kwargs) if resources.has_next else None, |  | ||||||
|                 'prev': url_for(endpoint, page=page - 1, per_page=per_page, |  | ||||||
|                                 **kwargs) if resources.has_prev else None |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return data |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| followers = db.Table( | followers = db.Table( | ||||||
|  | @ -89,7 +14,7 @@ followers = db.Table( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class User(UserMixin, PaginatedAPIMixin, db.Model): | class User(UserMixin, db.Model): | ||||||
|     id = db.Column(db.Integer, primary_key=True) |     id = db.Column(db.Integer, primary_key=True) | ||||||
|     username = db.Column(db.String(64), index=True, unique=True) |     username = db.Column(db.String(64), index=True, unique=True) | ||||||
|     email = db.Column(db.String(120), index=True, unique=True) |     email = db.Column(db.String(120), index=True, unique=True) | ||||||
|  | @ -97,23 +22,11 @@ class User(UserMixin, PaginatedAPIMixin, db.Model): | ||||||
|     posts = db.relationship('Post', backref='author', lazy='dynamic') |     posts = db.relationship('Post', backref='author', lazy='dynamic') | ||||||
|     about_me = db.Column(db.String(140)) |     about_me = db.Column(db.String(140)) | ||||||
|     last_seen = db.Column(db.DateTime, default=datetime.utcnow) |     last_seen = db.Column(db.DateTime, default=datetime.utcnow) | ||||||
|     token = db.Column(db.String(32), index=True, unique=True) |  | ||||||
|     token_expiration = db.Column(db.DateTime) |  | ||||||
|     followed = db.relationship( |     followed = db.relationship( | ||||||
|         'User', secondary=followers, |         'User', secondary=followers, | ||||||
|         primaryjoin=(followers.c.follower_id == id), |         primaryjoin=(followers.c.follower_id == id), | ||||||
|         secondaryjoin=(followers.c.followed_id == id), |         secondaryjoin=(followers.c.followed_id == id), | ||||||
|         backref=db.backref('followers', lazy='dynamic'), lazy='dynamic') |         backref=db.backref('followers', lazy='dynamic'), lazy='dynamic') | ||||||
|     messages_sent = db.relationship('Message', |  | ||||||
|                                     foreign_keys='Message.sender_id', |  | ||||||
|                                     backref='author', lazy='dynamic') |  | ||||||
|     messages_received = db.relationship('Message', |  | ||||||
|                                         foreign_keys='Message.recipient_id', |  | ||||||
|                                         backref='recipient', lazy='dynamic') |  | ||||||
|     last_message_read_time = db.Column(db.DateTime) |  | ||||||
|     notifications = db.relationship('Notification', backref='user', |  | ||||||
|                                     lazy='dynamic') |  | ||||||
|     tasks = db.relationship('Task', backref='user', lazy='dynamic') |  | ||||||
| 
 | 
 | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return '<User {}>'.format(self.username) |         return '<User {}>'.format(self.username) | ||||||
|  | @ -151,143 +64,28 @@ class User(UserMixin, PaginatedAPIMixin, db.Model): | ||||||
|     def get_reset_password_token(self, expires_in=600): |     def get_reset_password_token(self, expires_in=600): | ||||||
|         return jwt.encode( |         return jwt.encode( | ||||||
|             {'reset_password': self.id, 'exp': time() + expires_in}, |             {'reset_password': self.id, 'exp': time() + expires_in}, | ||||||
|             current_app.config['SECRET_KEY'], algorithm='HS256') |             app.config['SECRET_KEY'], algorithm='HS256') | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def verify_reset_password_token(token): |     def verify_reset_password_token(token): | ||||||
|         try: |         try: | ||||||
|             id = jwt.decode(token, current_app.config['SECRET_KEY'], |             id = jwt.decode(token, app.config['SECRET_KEY'], | ||||||
|                             algorithms=['HS256'])['reset_password'] |                             algorithms=['HS256'])['reset_password'] | ||||||
|         except: |         except: | ||||||
|             return |             return | ||||||
|         return User.query.get(id) |         return User.query.get(id) | ||||||
| 
 | 
 | ||||||
|     def new_messages(self): |  | ||||||
|         last_read_time = self.last_message_read_time or datetime(1900, 1, 1) |  | ||||||
|         return Message.query.filter_by(recipient=self).filter( |  | ||||||
|             Message.timestamp > last_read_time).count() |  | ||||||
| 
 |  | ||||||
|     def add_notification(self, name, data): |  | ||||||
|         self.notifications.filter_by(name=name).delete() |  | ||||||
|         n = Notification(name=name, payload_json=json.dumps(data), user=self) |  | ||||||
|         db.session.add(n) |  | ||||||
|         return n |  | ||||||
| 
 |  | ||||||
|     def launch_task(self, name, description, *args, **kwargs): |  | ||||||
|         rq_job = current_app.task_queue.enqueue('app.tasks.' + name, self.id, |  | ||||||
|                                                 *args, **kwargs) |  | ||||||
|         task = Task(id=rq_job.get_id(), name=name, description=description, |  | ||||||
|                     user=self) |  | ||||||
|         db.session.add(task) |  | ||||||
|         return task |  | ||||||
| 
 |  | ||||||
|     def get_tasks_in_progress(self): |  | ||||||
|         return Task.query.filter_by(user=self, complete=False).all() |  | ||||||
| 
 |  | ||||||
|     def get_task_in_progress(self, name): |  | ||||||
|         return Task.query.filter_by(name=name, user=self, |  | ||||||
|                                     complete=False).first() |  | ||||||
| 
 |  | ||||||
|     def to_dict(self, include_email=False): |  | ||||||
|         data = { |  | ||||||
|             'id': self.id, |  | ||||||
|             'username': self.username, |  | ||||||
|             'last_seen': self.last_seen.isoformat() + 'Z', |  | ||||||
|             'about_me': self.about_me, |  | ||||||
|             'post_count': self.posts.count(), |  | ||||||
|             'follower_count': self.followers.count(), |  | ||||||
|             'followed_count': self.followed.count(), |  | ||||||
|             '_links': { |  | ||||||
|                 'self': url_for('api.get_user', id=self.id), |  | ||||||
|                 'followers': url_for('api.get_followers', id=self.id), |  | ||||||
|                 'followed': url_for('api.get_followed', id=self.id), |  | ||||||
|                 'avatar': self.avatar(128) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         if include_email: |  | ||||||
|             data['email'] = self.email |  | ||||||
|         return data |  | ||||||
| 
 |  | ||||||
|     def from_dict(self, data, new_user=False): |  | ||||||
|         for field in ['username', 'email', 'about_me']: |  | ||||||
|             if field in data: |  | ||||||
|                 setattr(self, field, data[field]) |  | ||||||
|         if new_user and 'password' in data: |  | ||||||
|             self.set_password(data['password']) |  | ||||||
| 
 |  | ||||||
|     def get_token(self, expires_in=3600): |  | ||||||
|         now = datetime.utcnow() |  | ||||||
|         if self.token and self.token_expiration > now + timedelta(seconds=60): |  | ||||||
|             return self.token |  | ||||||
|         self.token = base64.b64encode(os.urandom(24)).decode('utf-8') |  | ||||||
|         self.token_expiration = now + timedelta(seconds=expires_in) |  | ||||||
|         db.session.add(self) |  | ||||||
|         return self.token |  | ||||||
| 
 |  | ||||||
|     def revoke_token(self): |  | ||||||
|         self.token_expiration = datetime.utcnow() - timedelta(seconds=1) |  | ||||||
| 
 |  | ||||||
|     @staticmethod |  | ||||||
|     def check_token(token): |  | ||||||
|         user = User.query.filter_by(token=token).first() |  | ||||||
|         if user is None or user.token_expiration < datetime.utcnow(): |  | ||||||
|             return None |  | ||||||
|         return user |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| @login.user_loader | @login.user_loader | ||||||
| def load_user(id): | def load_user(id): | ||||||
|     return User.query.get(int(id)) |     return User.query.get(int(id)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Post(SearchableMixin, db.Model): | class Post(db.Model): | ||||||
|     __searchable__ = ['body'] |  | ||||||
|     id = db.Column(db.Integer, primary_key=True) |     id = db.Column(db.Integer, primary_key=True) | ||||||
|     body = db.Column(db.String(140)) |     body = db.Column(db.String(140)) | ||||||
|     timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) |     timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) | ||||||
|     user_id = db.Column(db.Integer, db.ForeignKey('user.id')) |     user_id = db.Column(db.Integer, db.ForeignKey('user.id')) | ||||||
|     language = db.Column(db.String(5)) |  | ||||||
| 
 | 
 | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return '<Post {}>'.format(self.body) |         return '<Post {}>'.format(self.body) | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Message(db.Model): |  | ||||||
|     id = db.Column(db.Integer, primary_key=True) |  | ||||||
|     sender_id = db.Column(db.Integer, db.ForeignKey('user.id')) |  | ||||||
|     recipient_id = db.Column(db.Integer, db.ForeignKey('user.id')) |  | ||||||
|     body = db.Column(db.String(140)) |  | ||||||
|     timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) |  | ||||||
| 
 |  | ||||||
|     def __repr__(self): |  | ||||||
|         return '<Message {}>'.format(self.body) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Notification(db.Model): |  | ||||||
|     id = db.Column(db.Integer, primary_key=True) |  | ||||||
|     name = db.Column(db.String(128), index=True) |  | ||||||
|     user_id = db.Column(db.Integer, db.ForeignKey('user.id')) |  | ||||||
|     timestamp = db.Column(db.Float, index=True, default=time) |  | ||||||
|     payload_json = db.Column(db.Text) |  | ||||||
| 
 |  | ||||||
|     def get_data(self): |  | ||||||
|         return json.loads(str(self.payload_json)) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Task(db.Model): |  | ||||||
|     id = db.Column(db.String(36), primary_key=True) |  | ||||||
|     name = db.Column(db.String(128), index=True) |  | ||||||
|     description = db.Column(db.String(128)) |  | ||||||
|     user_id = db.Column(db.Integer, db.ForeignKey('user.id')) |  | ||||||
|     complete = db.Column(db.Boolean, default=False) |  | ||||||
| 
 |  | ||||||
|     def get_rq_job(self): |  | ||||||
|         try: |  | ||||||
|             rq_job = rq.job.Job.fetch(self.id, connection=current_app.redis) |  | ||||||
|         except (redis.exceptions.RedisError, rq.exceptions.NoSuchJobError): |  | ||||||
|             return None |  | ||||||
|         return rq_job |  | ||||||
| 
 |  | ||||||
|     def get_progress(self): |  | ||||||
|         job = self.get_rq_job() |  | ||||||
|         return job.meta.get('progress', 0) if job is not None else 100 |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,200 @@ | ||||||
|  | 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 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 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @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(): | ||||||
|  |         post = Post(body=form.post.data, author=current_user) | ||||||
|  |         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/<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')) | ||||||
|  | @ -1,27 +0,0 @@ | ||||||
| from flask import current_app |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def add_to_index(index, model): |  | ||||||
|     if not current_app.elasticsearch: |  | ||||||
|         return |  | ||||||
|     payload = {} |  | ||||||
|     for field in model.__searchable__: |  | ||||||
|         payload[field] = getattr(model, field) |  | ||||||
|     current_app.elasticsearch.index(index=index, id=model.id, body=payload) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def remove_from_index(index, model): |  | ||||||
|     if not current_app.elasticsearch: |  | ||||||
|         return |  | ||||||
|     current_app.elasticsearch.delete(index=index, id=model.id) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def query_index(index, query, page, per_page): |  | ||||||
|     if not current_app.elasticsearch: |  | ||||||
|         return [], 0 |  | ||||||
|     search = current_app.elasticsearch.search( |  | ||||||
|         index=index, |  | ||||||
|         body={'query': {'multi_match': {'query': query, 'fields': ['*']}}, |  | ||||||
|               'from': (page - 1) * per_page, 'size': per_page}) |  | ||||||
|     ids = [int(hit['_id']) for hit in search['hits']['hits']] |  | ||||||
|     return ids, search['hits']['total']['value'] |  | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 673 B | 
							
								
								
									
										51
									
								
								app/tasks.py
								
								
								
								
							
							
						
						
									
										51
									
								
								app/tasks.py
								
								
								
								
							|  | @ -1,51 +0,0 @@ | ||||||
| import json |  | ||||||
| import sys |  | ||||||
| import time |  | ||||||
| from flask import render_template |  | ||||||
| from rq import get_current_job |  | ||||||
| from app import create_app, db |  | ||||||
| from app.models import User, Post, Task |  | ||||||
| from app.email import send_email |  | ||||||
| 
 |  | ||||||
| app = create_app() |  | ||||||
| app.app_context().push() |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def _set_task_progress(progress): |  | ||||||
|     job = get_current_job() |  | ||||||
|     if job: |  | ||||||
|         job.meta['progress'] = progress |  | ||||||
|         job.save_meta() |  | ||||||
|         task = Task.query.get(job.get_id()) |  | ||||||
|         task.user.add_notification('task_progress', {'task_id': job.get_id(), |  | ||||||
|                                                      'progress': progress}) |  | ||||||
|         if progress >= 100: |  | ||||||
|             task.complete = True |  | ||||||
|         db.session.commit() |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def export_posts(user_id): |  | ||||||
|     try: |  | ||||||
|         user = User.query.get(user_id) |  | ||||||
|         _set_task_progress(0) |  | ||||||
|         data = [] |  | ||||||
|         i = 0 |  | ||||||
|         total_posts = user.posts.count() |  | ||||||
|         for post in user.posts.order_by(Post.timestamp.asc()): |  | ||||||
|             data.append({'body': post.body, |  | ||||||
|                          'timestamp': post.timestamp.isoformat() + 'Z'}) |  | ||||||
|             time.sleep(5) |  | ||||||
|             i += 1 |  | ||||||
|             _set_task_progress(100 * i // total_posts) |  | ||||||
| 
 |  | ||||||
|         send_email('[Microblog] Your blog posts', |  | ||||||
|                 sender=app.config['ADMINS'][0], recipients=[user.email], |  | ||||||
|                 text_body=render_template('email/export_posts.txt', user=user), |  | ||||||
|                 html_body=render_template('email/export_posts.html', |  | ||||||
|                                           user=user), |  | ||||||
|                 attachments=[('posts.json', 'application/json', |  | ||||||
|                               json.dumps({'posts': data}, indent=4))], |  | ||||||
|                 sync=True) |  | ||||||
|     except: |  | ||||||
|         _set_task_progress(100) |  | ||||||
|         app.logger.error('Unhandled exception', exc_info=sys.exc_info()) |  | ||||||
|  | @ -2,5 +2,5 @@ | ||||||
| 
 | 
 | ||||||
| {% block app_content %} | {% block app_content %} | ||||||
|     <h1>{{ _('Not Found') }}</h1> |     <h1>{{ _('Not Found') }}</h1> | ||||||
|     <p><a href="{{ url_for('main.index') }}">{{ _('Back') }}</a></p> |     <p><a href="{{ url_for('index') }}">{{ _('Back') }}</a></p> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  | @ -3,5 +3,5 @@ | ||||||
| {% block app_content %} | {% block app_content %} | ||||||
|     <h1>{{ _('An unexpected error has occurred') }}</h1> |     <h1>{{ _('An unexpected error has occurred') }}</h1> | ||||||
|     <p>{{ _('The administrator has been notified. Sorry for the inconvenience!') }}</p> |     <p>{{ _('The administrator has been notified. Sorry for the inconvenience!') }}</p> | ||||||
|     <p><a href="{{ url_for('main.index') }}">{{ _('Back') }}</a></p> |     <p><a href="{{ url_for('index') }}">{{ _('Back') }}</a></p> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  | @ -1,32 +1,20 @@ | ||||||
|     <table class="table table-hover"> |     <table class="table table-hover"> | ||||||
|         <tr> |         <tr> | ||||||
|             <td width="70px"> |             <td width="70px"> | ||||||
|                 <a href="{{ url_for('main.user', username=post.author.username) }}"> |                 <a href="{{ url_for('user', username=post.author.username) }}"> | ||||||
|                     <img src="{{ post.author.avatar(70) }}" /> |                     <img src="{{ post.author.avatar(70) }}" /> | ||||||
|                 </a> |                 </a> | ||||||
|             </td> |             </td> | ||||||
|             <td> |             <td> | ||||||
|                 {% set user_link %} |                 {% set user_link %} | ||||||
|                     <span class="user_popup"> |                     <a href="{{ url_for('user', username=post.author.username) }}"> | ||||||
|                         <a href="{{ url_for('main.user', username=post.author.username) }}"> |  | ||||||
|                         {{ post.author.username }} |                         {{ post.author.username }} | ||||||
|                     </a> |                     </a> | ||||||
|                     </span> |  | ||||||
|                 {% endset %} |                 {% endset %} | ||||||
|                 {{ _('%(username)s said %(when)s', |                 {{ _('%(username)s said %(when)s', | ||||||
|                     username=user_link, when=moment(post.timestamp).fromNow()) }} |                     username=user_link, when=moment(post.timestamp).fromNow()) }} | ||||||
|                 <br> |                 <br> | ||||||
|                 <span id="post{{ post.id }}">{{ post.body }}</span> |                 {{ post.body }} | ||||||
|                 {% if post.language and post.language != g.locale %} |  | ||||||
|                 <br><br> |  | ||||||
|                 <span id="translation{{ post.id }}"> |  | ||||||
|                     <a href="javascript:translate( |  | ||||||
|                                 '#post{{ post.id }}', |  | ||||||
|                                 '#translation{{ post.id }}', |  | ||||||
|                                 '{{ post.language }}', |  | ||||||
|                                 '{{ g.locale }}');">{{ _('Translate') }}</a> |  | ||||||
|                 </span> |  | ||||||
|                 {% endif %} |  | ||||||
|             </td> |             </td> | ||||||
|         </tr> |         </tr> | ||||||
|     </table> |     </table> | ||||||
|  |  | ||||||
|  | @ -14,36 +14,19 @@ | ||||||
|                     <span class="icon-bar"></span> |                     <span class="icon-bar"></span> | ||||||
|                     <span class="icon-bar"></span> |                     <span class="icon-bar"></span> | ||||||
|                 </button> |                 </button> | ||||||
|                 <a class="navbar-brand" href="{{ url_for('main.index') }}">Microblog</a> |                 <a class="navbar-brand" href="{{ url_for('index') }}">Microblog</a> | ||||||
|             </div> |             </div> | ||||||
|             <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> |             <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> | ||||||
|                 <ul class="nav navbar-nav"> |                 <ul class="nav navbar-nav"> | ||||||
|                     <li><a href="{{ url_for('main.index') }}">{{ _('Home') }}</a></li> |                     <li><a href="{{ url_for('index') }}">{{ _('Home') }}</a></li> | ||||||
|                     <li><a href="{{ url_for('main.explore') }}">{{ _('Explore') }}</a></li> |                     <li><a href="{{ url_for('explore') }}">{{ _('Explore') }}</a></li> | ||||||
|                 </ul> |                 </ul> | ||||||
|                 {% if g.search_form %} |  | ||||||
|                 <form class="navbar-form navbar-left" method="get" action="{{ url_for('main.search') }}"> |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         {{ g.search_form.q(size=20, class='form-control', placeholder=g.search_form.q.label.text) }} |  | ||||||
|                     </div> |  | ||||||
|                 </form> |  | ||||||
|                 {% endif %} |  | ||||||
|                 <ul class="nav navbar-nav navbar-right"> |                 <ul class="nav navbar-nav navbar-right"> | ||||||
|                     {% if current_user.is_anonymous %} |                     {% if current_user.is_anonymous %} | ||||||
|                     <li><a href="{{ url_for('auth.login') }}">{{ _('Login') }}</a></li> |                     <li><a href="{{ url_for('login') }}">{{ _('Login') }}</a></li> | ||||||
|                     {% else %} |                     {% else %} | ||||||
|                     <li> |                     <li><a href="{{ url_for('user', username=current_user.username) }}">{{ _('Profile') }}</a></li> | ||||||
|                         <a href="{{ url_for('main.messages') }}">{{ _('Messages') }} |                     <li><a href="{{ url_for('logout') }}">{{ _('Logout') }}</a></li> | ||||||
|                             {% set new_messages = current_user.new_messages() %} |  | ||||||
|                             <span id="message_count" class="badge" |  | ||||||
|                                   style="visibility: {% if new_messages %}visible |  | ||||||
|                                                      {% else %}hidden{% endif %};"> |  | ||||||
|                                 {{ new_messages }} |  | ||||||
|                             </span> |  | ||||||
|                         </a> |  | ||||||
|                     </li> |  | ||||||
|                     <li><a href="{{ url_for('main.user', username=current_user.username) }}">{{ _('Profile') }}</a></li> |  | ||||||
|                     <li><a href="{{ url_for('auth.logout') }}">{{ _('Logout') }}</a></li> |  | ||||||
|                     {% endif %} |                     {% endif %} | ||||||
|                 </ul> |                 </ul> | ||||||
|             </div> |             </div> | ||||||
|  | @ -53,18 +36,6 @@ | ||||||
| 
 | 
 | ||||||
| {% block content %} | {% block content %} | ||||||
|     <div class="container"> |     <div class="container"> | ||||||
|         {% if current_user.is_authenticated %} |  | ||||||
|         {% with tasks = current_user.get_tasks_in_progress() %} |  | ||||||
|         {% if tasks %} |  | ||||||
|             {% for task in tasks %} |  | ||||||
|             <div class="alert alert-success" role="alert"> |  | ||||||
|                 {{ task.description }} |  | ||||||
|                 <span id="{{ task.id }}-progress">{{ task.get_progress() }}</span>% |  | ||||||
|             </div> |  | ||||||
|             {% endfor %} |  | ||||||
|         {% endif %} |  | ||||||
|         {% endwith %} |  | ||||||
|         {% endif %} |  | ||||||
|         {% with messages = get_flashed_messages() %} |         {% with messages = get_flashed_messages() %} | ||||||
|         {% if messages %} |         {% if messages %} | ||||||
|             {% for message in messages %} |             {% for message in messages %} | ||||||
|  | @ -82,90 +53,4 @@ | ||||||
|     {{ super() }} |     {{ super() }} | ||||||
|     {{ moment.include_moment() }} |     {{ moment.include_moment() }} | ||||||
|     {{ moment.lang(g.locale) }} |     {{ moment.lang(g.locale) }} | ||||||
|     <script> |  | ||||||
|         function translate(sourceElem, destElem, sourceLang, destLang) { |  | ||||||
|             $(destElem).html('<img src="{{ url_for('static', filename='loading.gif') }}">'); |  | ||||||
|             $.post('/translate', { |  | ||||||
|                 text: $(sourceElem).text(), |  | ||||||
|                 source_language: sourceLang, |  | ||||||
|                 dest_language: destLang |  | ||||||
|             }).done(function(response) { |  | ||||||
|                 $(destElem).text(response['text']) |  | ||||||
|             }).fail(function() { |  | ||||||
|                 $(destElem).text("{{ _('Error: Could not contact server.') }}"); |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|         $(function () { |  | ||||||
|             var timer = null; |  | ||||||
|             var xhr = null; |  | ||||||
|             $('.user_popup').hover( |  | ||||||
|                 function(event) { |  | ||||||
|                     // mouse in event handler |  | ||||||
|                     var elem = $(event.currentTarget); |  | ||||||
|                     timer = setTimeout(function() { |  | ||||||
|                         timer = null; |  | ||||||
|                         xhr = $.ajax( |  | ||||||
|                             '/user/' + elem.first().text().trim() + '/popup').done( |  | ||||||
|                                 function(data) { |  | ||||||
|                                     xhr = null; |  | ||||||
|                                     elem.popover({ |  | ||||||
|                                         trigger: 'manual', |  | ||||||
|                                         html: true, |  | ||||||
|                                         animation: false, |  | ||||||
|                                         container: elem, |  | ||||||
|                                         content: data |  | ||||||
|                                     }).popover('show'); |  | ||||||
|                                     flask_moment_render_all(); |  | ||||||
|                                 } |  | ||||||
|                             ); |  | ||||||
|                     }, 1000); |  | ||||||
|                 }, |  | ||||||
|                 function(event) { |  | ||||||
|                     // mouse out event handler |  | ||||||
|                     var elem = $(event.currentTarget); |  | ||||||
|                     if (timer) { |  | ||||||
|                         clearTimeout(timer); |  | ||||||
|                         timer = null; |  | ||||||
|                     } |  | ||||||
|                     else if (xhr) { |  | ||||||
|                         xhr.abort(); |  | ||||||
|                         xhr = null; |  | ||||||
|                     } |  | ||||||
|                     else { |  | ||||||
|                         elem.popover('destroy'); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             ); |  | ||||||
|         }); |  | ||||||
|         function set_message_count(n) { |  | ||||||
|             $('#message_count').text(n); |  | ||||||
|             $('#message_count').css('visibility', n ? 'visible' : 'hidden'); |  | ||||||
|         } |  | ||||||
|         function set_task_progress(task_id, progress) { |  | ||||||
|             $('#' + task_id + '-progress').text(progress); |  | ||||||
|         } |  | ||||||
|         {% if current_user.is_authenticated %} |  | ||||||
|         $(function() { |  | ||||||
|             var since = 0; |  | ||||||
|             setInterval(function() { |  | ||||||
|                 $.ajax('{{ url_for('main.notifications') }}?since=' + since).done( |  | ||||||
|                     function(notifications) { |  | ||||||
|                         for (var i = 0; i < notifications.length; i++) { |  | ||||||
|                             switch (notifications[i].name) { |  | ||||||
|                                 case 'unread_message_count': |  | ||||||
|                                     set_message_count(notifications[i].data); |  | ||||||
|                                     break; |  | ||||||
|                                 case 'task_progress': |  | ||||||
|                                     set_task_progress(notifications[i].data.task_id, |  | ||||||
|                                         notifications[i].data.progress); |  | ||||||
|                                     break; |  | ||||||
|                             } |  | ||||||
|                             since = notifications[i].timestamp; |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 ); |  | ||||||
|             }, 10000); |  | ||||||
|         }); |  | ||||||
|         {% endif %} |  | ||||||
|     </script> |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
|  | @ -1,4 +0,0 @@ | ||||||
| <p>Dear {{ user.username }},</p> |  | ||||||
| <p>Please find attached the archive of your posts that you requested.</p> |  | ||||||
| <p>Sincerely,</p> |  | ||||||
| <p>The Microblog Team</p> |  | ||||||
|  | @ -1,7 +0,0 @@ | ||||||
| Dear {{ user.username }}, |  | ||||||
| 
 |  | ||||||
| Please find attached the archive of your posts that you requested. |  | ||||||
| 
 |  | ||||||
| Sincerely, |  | ||||||
| 
 |  | ||||||
| The Microblog Team |  | ||||||
|  | @ -1,12 +1,12 @@ | ||||||
| <p>Dear {{ user.username }},</p> | <p>Dear {{ user.username }},</p> | ||||||
| <p> | <p> | ||||||
|     To reset your password |     To reset your password | ||||||
|     <a href="{{ url_for('auth.reset_password', token=token, _external=True) }}"> |     <a href="{{ url_for('reset_password', token=token, _external=True) }}"> | ||||||
|         click here |         click here | ||||||
|     </a>. |     </a>. | ||||||
| </p> | </p> | ||||||
| <p>Alternatively, you can paste the following link in your browser's address bar:</p> | <p>Alternatively, you can paste the following link in your browser's address bar:</p> | ||||||
| <p>{{ url_for('auth.reset_password', token=token, _external=True) }}</p> | <p>{{ url_for('reset_password', token=token, _external=True) }}</p> | ||||||
| <p>If you have not requested a password reset simply ignore this message.</p> | <p>If you have not requested a password reset simply ignore this message.</p> | ||||||
| <p>Sincerely,</p> | <p>Sincerely,</p> | ||||||
| <p>The Microblog Team</p> | <p>The Microblog Team</p> | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ Dear {{ user.username }}, | ||||||
| 
 | 
 | ||||||
| To reset your password click on the following link: | To reset your password click on the following link: | ||||||
| 
 | 
 | ||||||
| {{ url_for('auth.reset_password', token=token, _external=True) }} | {{ url_for('reset_password', token=token, _external=True) }} | ||||||
| 
 | 
 | ||||||
| If you have not requested a password reset simply ignore this message. | If you have not requested a password reset simply ignore this message. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -9,9 +9,9 @@ | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|     <br> |     <br> | ||||||
|     <p>{{ _('New User?') }} <a href="{{ url_for('auth.register') }}">{{ _('Click to Register!') }}</a></p> |     <p>{{ _('New User?') }} <a href="{{ url_for('register') }}">{{ _('Click to Register!') }}</a></p> | ||||||
|     <p> |     <p> | ||||||
|         {{ _('Forgot Your Password?') }} |         {{ _('Forgot Your Password?') }} | ||||||
|         <a href="{{ url_for('auth.reset_password_request') }}">{{ _('Click to Reset It') }}</a> |         <a href="{{ url_for('reset_password_request') }}">{{ _('Click to Reset It') }}</a> | ||||||
|     </p> |     </p> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  | @ -1,22 +0,0 @@ | ||||||
| {% extends "base.html" %} |  | ||||||
| 
 |  | ||||||
| {% block app_content %} |  | ||||||
|     <h1>{{ _('Messages') }}</h1> |  | ||||||
|     {% for post in messages %} |  | ||||||
|         {% include '_post.html' %} |  | ||||||
|     {% endfor %} |  | ||||||
|     <nav aria-label="..."> |  | ||||||
|         <ul class="pager"> |  | ||||||
|             <li class="previous{% if not prev_url %} disabled{% endif %}"> |  | ||||||
|                 <a href="{{ prev_url or '#' }}"> |  | ||||||
|                     <span aria-hidden="true">←</span> {{ _('Newer messages') }} |  | ||||||
|                 </a> |  | ||||||
|             </li> |  | ||||||
|             <li class="next{% if not next_url %} disabled{% endif %}"> |  | ||||||
|                 <a href="{{ next_url or '#' }}"> |  | ||||||
|                     {{ _('Older messages') }} <span aria-hidden="true">→</span> |  | ||||||
|                 </a> |  | ||||||
|             </li> |  | ||||||
|         </ul> |  | ||||||
|     </nav> |  | ||||||
| {% endblock %} |  | ||||||
|  | @ -1,22 +0,0 @@ | ||||||
| {% extends "base.html" %} |  | ||||||
| 
 |  | ||||||
| {% block app_content %} |  | ||||||
|     <h1>{{ _('Search Results') }}</h1> |  | ||||||
|     {% for post in posts %} |  | ||||||
|         {% include '_post.html' %} |  | ||||||
|     {% endfor %} |  | ||||||
|     <nav aria-label="..."> |  | ||||||
|         <ul class="pager"> |  | ||||||
|             <li class="previous{% if not prev_url %} disabled{% endif %}"> |  | ||||||
|                 <a href="{{ prev_url or '#' }}"> |  | ||||||
|                     <span aria-hidden="true">←</span> {{ _('Previous results') }} |  | ||||||
|                 </a> |  | ||||||
|             </li> |  | ||||||
|             <li class="next{% if not next_url %} disabled{% endif %}"> |  | ||||||
|                 <a href="{{ next_url or '#' }}"> |  | ||||||
|                     {{ _('Next results') }} <span aria-hidden="true">→</span> |  | ||||||
|                 </a> |  | ||||||
|             </li> |  | ||||||
|         </ul> |  | ||||||
|     </nav> |  | ||||||
| {% endblock %} |  | ||||||
|  | @ -1,11 +0,0 @@ | ||||||
| {% extends "base.html" %} |  | ||||||
| {% import 'bootstrap/wtf.html' as wtf %} |  | ||||||
| 
 |  | ||||||
| {% block app_content %} |  | ||||||
|     <h1>{{ _('Send Message to %(recipient)s', recipient=recipient) }}</h1> |  | ||||||
|     <div class="row"> |  | ||||||
|         <div class="col-md-4"> |  | ||||||
|             {{ wtf.quick_form(form) }} |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| {% endblock %} |  | ||||||
|  | @ -12,28 +12,22 @@ | ||||||
|                 {% endif %} |                 {% endif %} | ||||||
|                 <p>{{ _('%(count)d followers', count=user.followers.count()) }}, {{ _('%(count)d following', count=user.followed.count()) }}</p> |                 <p>{{ _('%(count)d followers', count=user.followers.count()) }}, {{ _('%(count)d following', count=user.followed.count()) }}</p> | ||||||
|                 {% if user == current_user %} |                 {% if user == current_user %} | ||||||
|                 <p><a href="{{ url_for('main.edit_profile') }}">{{ _('Edit your profile') }}</a></p> |                 <p><a href="{{ url_for('edit_profile') }}">{{ _('Edit your profile') }}</a></p> | ||||||
|                 {% if not current_user.get_task_in_progress('export_posts') %} |  | ||||||
|                 <p><a href="{{ url_for('main.export_posts') }}">{{ _('Export your posts') }}</a></p> |  | ||||||
|                 {% endif %} |  | ||||||
|                 {% elif not current_user.is_following(user) %} |                 {% elif not current_user.is_following(user) %} | ||||||
|                 <p> |                 <p> | ||||||
|                     <form action="{{ url_for('main.follow', username=user.username) }}" method="post"> |                     <form action="{{ url_for('follow', username=user.username) }}" method="post"> | ||||||
|                         {{ form.hidden_tag() }} |                         {{ form.hidden_tag() }} | ||||||
|                         {{ form.submit(value=_('Follow'), class_='btn btn-default') }} |                         {{ form.submit(value=_('Follow'), class_='btn btn-default') }} | ||||||
|                     </form> |                     </form> | ||||||
|                 </p> |                 </p> | ||||||
|                 {% else %} |                 {% else %} | ||||||
|                 <p> |                 <p> | ||||||
|                     <form action="{{ url_for('main.unfollow', username=user.username) }}" method="post"> |                     <form action="{{ url_for('unfollow', username=user.username) }}" method="post"> | ||||||
|                         {{ form.hidden_tag() }} |                         {{ form.hidden_tag() }} | ||||||
|                         {{ form.submit(value=_('Unfollow'), class_='btn btn-default') }} |                         {{ form.submit(value=_('Unfollow'), class_='btn btn-default') }} | ||||||
|                     </form> |                     </form> | ||||||
|                 </p> |                 </p> | ||||||
|                 {% endif %} |                 {% endif %} | ||||||
|                 {% if user != current_user %} |  | ||||||
|                 <p><a href="{{ url_for('main.send_message', recipient=user.username) }}">{{ _('Send private message') }}</a></p> |  | ||||||
|                 {% endif %} |  | ||||||
|             </td> |             </td> | ||||||
|         </tr> |         </tr> | ||||||
|     </table> |     </table> | ||||||
|  |  | ||||||
|  | @ -1,32 +0,0 @@ | ||||||
| <table class="table"> |  | ||||||
|     <tr> |  | ||||||
|         <td width="64" style="border: 0px;"><img src="{{ user.avatar(64) }}"></td> |  | ||||||
|         <td style="border: 0px;"> |  | ||||||
|             <p><a href="{{ url_for('main.user', username=user.username) }}">{{ user.username }}</a></p> |  | ||||||
|             <small> |  | ||||||
|                 {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %} |  | ||||||
|                 {% if user.last_seen %} |  | ||||||
|                 <p>{{ _('Last seen on') }}: {{ moment(user.last_seen).format('lll') }}</p> |  | ||||||
|                 {% endif %} |  | ||||||
|                 <p>{{ _('%(count)d followers', count=user.followers.count()) }}, {{ _('%(count)d following', count=user.followed.count()) }}</p> |  | ||||||
|                 {% if user != current_user %} |  | ||||||
|                     {% if not current_user.is_following(user) %} |  | ||||||
|                     <p> |  | ||||||
|                         <form action="{{ url_for('main.follow', username=user.username) }}" method="post"> |  | ||||||
|                             {{ form.hidden_tag() }} |  | ||||||
|                             {{ form.submit(value=_('Follow'), class_='btn btn-default btn-sm') }} |  | ||||||
|                         </form> |  | ||||||
|                     </p> |  | ||||||
|                     {% else %} |  | ||||||
|                     <p> |  | ||||||
|                         <form action="{{ url_for('main.unfollow', username=user.username) }}" method="post"> |  | ||||||
|                             {{ form.hidden_tag() }} |  | ||||||
|                             {{ form.submit(value=_('Unfollow'), class_='btn btn-default btm-sm') }} |  | ||||||
|                         </form> |  | ||||||
|                     </p> |  | ||||||
|                     {% endif %} |  | ||||||
|                 {% endif %} |  | ||||||
|             </small> |  | ||||||
|         </td> |  | ||||||
|     </tr> |  | ||||||
| </table> |  | ||||||
|  | @ -1,21 +0,0 @@ | ||||||
| import json |  | ||||||
| import requests |  | ||||||
| from flask import current_app |  | ||||||
| from flask_babel import _ |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def translate(text, source_language, dest_language): |  | ||||||
|     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': current_app.config['MS_TRANSLATOR_KEY'], |  | ||||||
|         'Ocp-Apim-Subscription-Region': 'westus2'} |  | ||||||
|     r = requests.post( |  | ||||||
|         'https://api.cognitive.microsofttranslator.com' |  | ||||||
|         '/translate?api-version=3.0&from={}&to={}'.format( |  | ||||||
|             source_language, dest_language), headers=auth, json=[ |  | ||||||
|                 {'Text': text}]) |  | ||||||
|     if r.status_code != 200: |  | ||||||
|         return _('Error: the translation service failed.') |  | ||||||
|     return r.json()[0]['translations'][0]['text'] |  | ||||||
|  | @ -7,7 +7,7 @@ msgid "" | ||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PROJECT VERSION\n" | "Project-Id-Version: PROJECT VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" | ||||||
| "POT-Creation-Date: 2017-11-25 18:27-0800\n" | "POT-Creation-Date: 2017-10-03 15:49-0700\n" | ||||||
| "PO-Revision-Date: 2017-09-29 23:25-0700\n" | "PO-Revision-Date: 2017-09-29 23:25-0700\n" | ||||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||||
| "Language: es\n" | "Language: es\n" | ||||||
|  | @ -22,154 +22,138 @@ msgstr "" | ||||||
| msgid "Please log in to access this page." | msgid "Please log in to access this page." | ||||||
| msgstr "Por favor ingrese para acceder a esta página." | msgstr "Por favor ingrese para acceder a esta página." | ||||||
| 
 | 
 | ||||||
| #: app/translate.py:10 | #: app/email.py:21 | ||||||
| msgid "Error: the translation service is not configured." |  | ||||||
| msgstr "Error: el servicio de traducciones no está configurado." |  | ||||||
| 
 |  | ||||||
| #: app/translate.py:18 |  | ||||||
| msgid "Error: the translation service failed." |  | ||||||
| msgstr "Error el servicio de traducciones ha fallado." |  | ||||||
| 
 |  | ||||||
| #: app/auth/email.py:8 |  | ||||||
| msgid "[Microblog] Reset Your Password" | msgid "[Microblog] Reset Your Password" | ||||||
| msgstr "[Microblog] Nueva Contraseña" | msgstr "[Microblog] Nueva Contraseña" | ||||||
| 
 | 
 | ||||||
| #: app/auth/forms.py:10 app/auth/forms.py:17 app/main/forms.py:10 | #: app/forms.py:12 app/forms.py:19 app/forms.py:50 | ||||||
| msgid "Username" | msgid "Username" | ||||||
| msgstr "Nombre de usuario" | msgstr "Nombre de usuario" | ||||||
| 
 | 
 | ||||||
| #: app/auth/forms.py:11 app/auth/forms.py:19 app/auth/forms.py:42 | #: app/forms.py:13 app/forms.py:21 app/forms.py:43 | ||||||
| msgid "Password" | msgid "Password" | ||||||
| msgstr "Contraseña" | msgstr "Contraseña" | ||||||
| 
 | 
 | ||||||
| #: app/auth/forms.py:12 | #: app/forms.py:14 | ||||||
| msgid "Remember Me" | msgid "Remember Me" | ||||||
| msgstr "Recordarme" | msgstr "Recordarme" | ||||||
| 
 | 
 | ||||||
| #: app/auth/forms.py:13 app/templates/auth/login.html:5 | #: app/forms.py:15 app/templates/login.html:5 | ||||||
| msgid "Sign In" | msgid "Sign In" | ||||||
| msgstr "Ingresar" | msgstr "Ingresar" | ||||||
| 
 | 
 | ||||||
| #: app/auth/forms.py:18 app/auth/forms.py:37 | #: app/forms.py:20 app/forms.py:38 | ||||||
| msgid "Email" | msgid "Email" | ||||||
| msgstr "Email" | msgstr "Email" | ||||||
| 
 | 
 | ||||||
| #: app/auth/forms.py:21 app/auth/forms.py:44 | #: app/forms.py:23 app/forms.py:45 | ||||||
| msgid "Repeat Password" | msgid "Repeat Password" | ||||||
| msgstr "Repetir Contraseña" | msgstr "Repetir Contraseña" | ||||||
| 
 | 
 | ||||||
| #: app/auth/forms.py:23 app/templates/auth/register.html:5 | #: app/forms.py:24 app/templates/register.html:5 | ||||||
| msgid "Register" | msgid "Register" | ||||||
| msgstr "Registrarse" | msgstr "Registrarse" | ||||||
| 
 | 
 | ||||||
| #: app/auth/forms.py:28 app/main/forms.py:23 | #: app/forms.py:29 app/forms.py:62 | ||||||
| msgid "Please use a different username." | msgid "Please use a different username." | ||||||
| msgstr "Por favor use un nombre de usuario diferente." | msgstr "Por favor use un nombre de usuario diferente." | ||||||
| 
 | 
 | ||||||
| #: app/auth/forms.py:33 | #: app/forms.py:34 | ||||||
| msgid "Please use a different email address." | msgid "Please use a different email address." | ||||||
| msgstr "Por favor use una dirección de email diferente." | msgstr "Por favor use una dirección de email diferente." | ||||||
| 
 | 
 | ||||||
| #: app/auth/forms.py:38 app/auth/forms.py:46 | #: app/forms.py:39 app/forms.py:46 | ||||||
| msgid "Request Password Reset" | msgid "Request Password Reset" | ||||||
| msgstr "Pedir una nueva contraseña" | msgstr "Pedir una nueva contraseña" | ||||||
| 
 | 
 | ||||||
| #: app/auth/routes.py:20 | #: app/forms.py:51 | ||||||
| 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" | msgid "About me" | ||||||
| msgstr "Acerca de mí" | msgstr "Acerca de mí" | ||||||
| 
 | 
 | ||||||
| #: app/main/forms.py:13 app/main/forms.py:28 app/main/forms.py:44 | #: app/forms.py:52 app/forms.py:67 | ||||||
| msgid "Submit" | msgid "Submit" | ||||||
| msgstr "Enviar" | msgstr "Enviar" | ||||||
| 
 | 
 | ||||||
| #: app/main/forms.py:27 | #: app/forms.py:66 | ||||||
| msgid "Say something" | msgid "Say something" | ||||||
| msgstr "Dí algo" | msgstr "Dí algo" | ||||||
| 
 | 
 | ||||||
| #: app/main/forms.py:32 | #: app/forms.py:71 | ||||||
| msgid "Search" | msgid "Search" | ||||||
| msgstr "Buscar" | msgstr "Buscar" | ||||||
| 
 | 
 | ||||||
| #: app/main/forms.py:43 | #: app/routes.py:30 | ||||||
| msgid "Message" |  | ||||||
| msgstr "Mensaje" |  | ||||||
| 
 |  | ||||||
| #: app/main/routes.py:36 |  | ||||||
| msgid "Your post is now live!" | msgid "Your post is now live!" | ||||||
| msgstr "¡Tu artículo ha sido publicado!" | msgstr "¡Tu artículo ha sido publicado!" | ||||||
| 
 | 
 | ||||||
| #: app/main/routes.py:94 | #: app/routes.py:66 | ||||||
|  | msgid "Invalid username or password" | ||||||
|  | msgstr "Nombre de usuario o contraseña inválidos" | ||||||
|  | 
 | ||||||
|  | #: app/routes.py:92 | ||||||
|  | msgid "Congratulations, you are now a registered user!" | ||||||
|  | msgstr "¡Felicitaciones, ya eres un usuario registrado!" | ||||||
|  | 
 | ||||||
|  | #: app/routes.py:107 | ||||||
|  | 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:124 | ||||||
|  | msgid "Your password has been reset." | ||||||
|  | msgstr "Tu contraseña ha sido cambiada." | ||||||
|  | 
 | ||||||
|  | #: app/routes.py:152 | ||||||
| msgid "Your changes have been saved." | msgid "Your changes have been saved." | ||||||
| msgstr "Tus cambios han sido salvados." | msgstr "Tus cambios han sido salvados." | ||||||
| 
 | 
 | ||||||
| #: app/main/routes.py:99 app/templates/edit_profile.html:5 | #: app/routes.py:157 app/templates/edit_profile.html:5 | ||||||
| msgid "Edit Profile" | msgid "Edit Profile" | ||||||
| msgstr "Editar Perfil" | msgstr "Editar Perfil" | ||||||
| 
 | 
 | ||||||
| #: app/main/routes.py:108 app/main/routes.py:124 | #: app/routes.py:166 app/routes.py:182 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "User %(username)s not found." | msgid "User %(username)s not found." | ||||||
| msgstr "El usuario %(username)s no ha sido encontrado." | msgstr "El usuario %(username)s no ha sido encontrado." | ||||||
| 
 | 
 | ||||||
| #: app/main/routes.py:111 | #: app/routes.py:169 | ||||||
| msgid "You cannot follow yourself!" | msgid "You cannot follow yourself!" | ||||||
| msgstr "¡No te puedes seguir a tí mismo!" | msgstr "¡No te puedes seguir a tí mismo!" | ||||||
| 
 | 
 | ||||||
| #: app/main/routes.py:115 | #: app/routes.py:173 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "You are following %(username)s!" | msgid "You are following %(username)s!" | ||||||
| msgstr "¡Ahora estás siguiendo a %(username)s!" | msgstr "¡Ahora estás siguiendo a %(username)s!" | ||||||
| 
 | 
 | ||||||
| #: app/main/routes.py:127 | #: app/routes.py:185 | ||||||
| msgid "You cannot unfollow yourself!" | msgid "You cannot unfollow yourself!" | ||||||
| msgstr "¡No te puedes dejar de seguir a tí mismo!" | msgstr "¡No te puedes dejar de seguir a tí mismo!" | ||||||
| 
 | 
 | ||||||
| #: app/main/routes.py:131 | #: app/routes.py:189 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "You are not following %(username)s." | msgid "You are not following %(username)s." | ||||||
| msgstr "No estás siguiendo a %(username)s." | msgstr "No estás siguiendo a %(username)s." | ||||||
| 
 | 
 | ||||||
| #: app/main/routes.py:170 | #: app/templates/404.html:4 | ||||||
| msgid "Your message has been sent." | msgid "Not Found" | ||||||
| msgstr "Tu mensaje ha sido enviado." | msgstr "Página No Encontrada" | ||||||
| 
 | 
 | ||||||
| #: app/main/routes.py:172 | #: app/templates/404.html:5 app/templates/500.html:6 | ||||||
| msgid "Send Message" | msgid "Back" | ||||||
| msgstr "Enviar Mensaje" | msgstr "Atrás" | ||||||
| 
 | 
 | ||||||
| #: app/main/routes.py:197 | #: app/templates/500.html:4 | ||||||
| msgid "An export task is currently in progress" | msgid "An unexpected error has occurred" | ||||||
| msgstr "Una tarea de exportación esta en progreso" | msgstr "Ha ocurrido un error inesperado" | ||||||
| 
 | 
 | ||||||
| #: app/main/routes.py:199 | #: app/templates/500.html:5 | ||||||
| msgid "Exporting posts..." | msgid "The administrator has been notified. Sorry for the inconvenience!" | ||||||
| msgstr "Exportando artículos..." | msgstr "El administrador ha sido notificado. ¡Lamentamos la inconveniencia!" | ||||||
| 
 | 
 | ||||||
| #: app/templates/_post.html:16 | #: app/templates/_post.html:9 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "%(username)s said %(when)s" | msgid "%(username)s said %(when)s" | ||||||
| msgstr "%(username)s dijo %(when)s" | msgstr "%(username)s dijo %(when)s" | ||||||
| 
 | 
 | ||||||
| #: app/templates/_post.html:27 |  | ||||||
| msgid "Translate" |  | ||||||
| msgstr "Traducir" |  | ||||||
| 
 |  | ||||||
| #: app/templates/base.html:4 | #: app/templates/base.html:4 | ||||||
| msgid "Welcome to Microblog" | msgid "Welcome to Microblog" | ||||||
| msgstr "Bienvenido a Microblog" | msgstr "Bienvenido a Microblog" | ||||||
|  | @ -186,74 +170,77 @@ msgstr "Explorar" | ||||||
| msgid "Login" | msgid "Login" | ||||||
| msgstr "Ingresar" | msgstr "Ingresar" | ||||||
| 
 | 
 | ||||||
| #: app/templates/base.html:36 app/templates/messages.html:4 | #: app/templates/base.html:35 | ||||||
| msgid "Messages" |  | ||||||
| msgstr "Mensajes" |  | ||||||
| 
 |  | ||||||
| #: app/templates/base.html:45 |  | ||||||
| msgid "Profile" | msgid "Profile" | ||||||
| msgstr "Perfil" | msgstr "Perfil" | ||||||
| 
 | 
 | ||||||
| #: app/templates/base.html:46 | #: app/templates/base.html:36 | ||||||
| msgid "Logout" | msgid "Logout" | ||||||
| msgstr "Salir" | msgstr "Salir" | ||||||
| 
 | 
 | ||||||
| #: app/templates/base.html:95 |  | ||||||
| msgid "Error: Could not contact server." |  | ||||||
| msgstr "Error: el servidor no pudo ser contactado." |  | ||||||
| 
 |  | ||||||
| #: app/templates/index.html:5 | #: app/templates/index.html:5 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "Hi, %(username)s!" | msgid "Hi, %(username)s!" | ||||||
| msgstr "¡Hola, %(username)s!" | msgstr "¡Hola, %(username)s!" | ||||||
| 
 | 
 | ||||||
| #: app/templates/index.html:17 app/templates/user.html:37 | #: app/templates/index.html:17 app/templates/user.html:31 | ||||||
| msgid "Newer posts" | msgid "Newer posts" | ||||||
| msgstr "Artículos siguientes" | msgstr "Artículos siguientes" | ||||||
| 
 | 
 | ||||||
| #: app/templates/index.html:22 app/templates/user.html:42 | #: app/templates/index.html:22 app/templates/user.html:36 | ||||||
| msgid "Older posts" | msgid "Older posts" | ||||||
| msgstr "Artículos previos" | msgstr "Artículos previos" | ||||||
| 
 | 
 | ||||||
| #: app/templates/messages.html:12 | #: app/templates/login.html:12 | ||||||
| msgid "Newer messages" | msgid "New User?" | ||||||
| msgstr "Mensajes siguientes" | msgstr "¿Usuario Nuevo?" | ||||||
| 
 | 
 | ||||||
| #: app/templates/messages.html:17 | #: app/templates/login.html:12 | ||||||
| msgid "Older messages" | msgid "Click to Register!" | ||||||
| msgstr "Mensajes previos" | 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 | #: app/templates/search.html:4 | ||||||
| msgid "Search Results" | msgid "Search Results" | ||||||
| msgstr "" | msgstr "Resultados de Búsqueda" | ||||||
| 
 | 
 | ||||||
| #: app/templates/search.html:12 | #: app/templates/search.html:12 | ||||||
| msgid "Previous results" | msgid "Previous results" | ||||||
| msgstr "" | msgstr "Resultados previos" | ||||||
| 
 | 
 | ||||||
| #: app/templates/search.html:17 | #: app/templates/search.html:17 | ||||||
| msgid "Next results" | msgid "Next results" | ||||||
| msgstr "" | msgstr "Resultados próximos" | ||||||
| 
 |  | ||||||
| #: app/templates/send_message.html:5 |  | ||||||
| #, python-format |  | ||||||
| msgid "Send Message to %(recipient)s" |  | ||||||
| msgstr "Enviar Mensaje a %(recipient)s" |  | ||||||
| 
 | 
 | ||||||
| #: app/templates/user.html:8 | #: app/templates/user.html:8 | ||||||
| msgid "User" | msgid "User" | ||||||
| msgstr "Usuario" | msgstr "Usuario" | ||||||
| 
 | 
 | ||||||
| #: app/templates/user.html:11 app/templates/user_popup.html:9 | #: app/templates/user.html:11 | ||||||
| msgid "Last seen on" | msgid "Last seen on" | ||||||
| msgstr "Última visita" | msgstr "Última visita" | ||||||
| 
 | 
 | ||||||
| #: app/templates/user.html:13 app/templates/user_popup.html:11 | #: app/templates/user.html:13 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "%(count)d followers" | msgid "%(count)d followers" | ||||||
| msgstr "%(count)d seguidores" | msgstr "%(count)d seguidores" | ||||||
| 
 | 
 | ||||||
| #: app/templates/user.html:13 app/templates/user_popup.html:11 | #: app/templates/user.html:13 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "%(count)d following" | msgid "%(count)d following" | ||||||
| msgstr "siguiendo a %(count)d" | msgstr "siguiendo a %(count)d" | ||||||
|  | @ -263,58 +250,10 @@ msgid "Edit your profile" | ||||||
| msgstr "Editar tu perfil" | msgstr "Editar tu perfil" | ||||||
| 
 | 
 | ||||||
| #: app/templates/user.html:17 | #: app/templates/user.html:17 | ||||||
| msgid "Export your posts" |  | ||||||
| msgstr "Exportar tus artículos" |  | ||||||
| 
 |  | ||||||
| #: app/templates/user.html:20 app/templates/user_popup.html:14 |  | ||||||
| msgid "Follow" | msgid "Follow" | ||||||
| msgstr "Seguir" | msgstr "Seguir" | ||||||
| 
 | 
 | ||||||
| #: app/templates/user.html:22 app/templates/user_popup.html:16 | #: app/templates/user.html:19 | ||||||
| msgid "Unfollow" | msgid "Unfollow" | ||||||
| msgstr "Dejar de seguir" | msgstr "Dejar de seguir" | ||||||
| 
 | 
 | ||||||
| #: app/templates/user.html:25 |  | ||||||
| msgid "Send private message" |  | ||||||
| msgstr "Enviar mensaje privado" |  | ||||||
| 
 |  | ||||||
| #: 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!" |  | ||||||
| 
 |  | ||||||
|  |  | ||||||
							
								
								
									
										13
									
								
								boot.sh
								
								
								
								
							
							
						
						
									
										13
									
								
								boot.sh
								
								
								
								
							|  | @ -1,13 +0,0 @@ | ||||||
| #!/bin/bash |  | ||||||
| # this script is used to boot a Docker container |  | ||||||
| source venv/bin/activate |  | ||||||
| while true; do |  | ||||||
|     flask db upgrade |  | ||||||
|     if [[ "$?" == "0" ]]; then |  | ||||||
|         break |  | ||||||
|     fi |  | ||||||
|     echo Deploy command failed, retrying in 5 secs... |  | ||||||
|     sleep 5 |  | ||||||
| done |  | ||||||
| flask translate compile |  | ||||||
| exec gunicorn -b :5000 --access-logfile - --error-logfile - microblog:app |  | ||||||
							
								
								
									
										10
									
								
								config.py
								
								
								
								
							
							
						
						
									
										10
									
								
								config.py
								
								
								
								
							|  | @ -1,17 +1,12 @@ | ||||||
| import os | import os | ||||||
| from dotenv import load_dotenv |  | ||||||
| 
 |  | ||||||
| basedir = os.path.abspath(os.path.dirname(__file__)) | basedir = os.path.abspath(os.path.dirname(__file__)) | ||||||
| load_dotenv(os.path.join(basedir, '.env')) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Config(object): | class Config(object): | ||||||
|     SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' |     SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' | ||||||
|     SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', '').replace( |     SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ | ||||||
|         'postgres://', 'postgresql://') or \ |  | ||||||
|         'sqlite:///' + os.path.join(basedir, 'app.db') |         'sqlite:///' + os.path.join(basedir, 'app.db') | ||||||
|     SQLALCHEMY_TRACK_MODIFICATIONS = False |     SQLALCHEMY_TRACK_MODIFICATIONS = False | ||||||
|     LOG_TO_STDOUT = os.environ.get('LOG_TO_STDOUT') |  | ||||||
|     MAIL_SERVER = os.environ.get('MAIL_SERVER') |     MAIL_SERVER = os.environ.get('MAIL_SERVER') | ||||||
|     MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25) |     MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25) | ||||||
|     MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None |     MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None | ||||||
|  | @ -19,7 +14,4 @@ class Config(object): | ||||||
|     MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') |     MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') | ||||||
|     ADMINS = ['your-email@example.com'] |     ADMINS = ['your-email@example.com'] | ||||||
|     LANGUAGES = ['en', 'es'] |     LANGUAGES = ['en', 'es'] | ||||||
|     MS_TRANSLATOR_KEY = os.environ.get('MS_TRANSLATOR_KEY') |  | ||||||
|     ELASTICSEARCH_URL = os.environ.get('ELASTICSEARCH_URL') |  | ||||||
|     REDIS_URL = os.environ.get('REDIS_URL') or 'redis://' |  | ||||||
|     POSTS_PER_PAGE = 25 |     POSTS_PER_PAGE = 25 | ||||||
|  |  | ||||||
|  | @ -1,37 +0,0 @@ | ||||||
| server { |  | ||||||
|     # listen on port 80 (http) |  | ||||||
|     listen 80; |  | ||||||
|     server_name _; |  | ||||||
|     location / { |  | ||||||
|         # redirect any requests to the same URL but on https |  | ||||||
|         return 301 https://$host$request_uri; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| server { |  | ||||||
|     # listen on port 443 (https) |  | ||||||
|     listen 443 ssl; |  | ||||||
|     server_name _; |  | ||||||
| 
 |  | ||||||
|     # location of the self-signed SSL certificate |  | ||||||
|     ssl_certificate /home/ubuntu/microblog/certs/cert.pem; |  | ||||||
|     ssl_certificate_key /home/ubuntu/microblog/certs/key.pem; |  | ||||||
| 
 |  | ||||||
|     # write access and error logs to /var/log |  | ||||||
|     access_log /var/log/microblog_access.log; |  | ||||||
|     error_log /var/log/microblog_error.log; |  | ||||||
| 
 |  | ||||||
|     location / { |  | ||||||
|         # forward application requests to the gunicorn server |  | ||||||
|         proxy_pass http://localhost:8000; |  | ||||||
|         proxy_redirect off; |  | ||||||
|         proxy_set_header Host $host; |  | ||||||
|         proxy_set_header X-Real-IP $remote_addr; |  | ||||||
|         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     location /static { |  | ||||||
|         # handle static files directly, without forwarding to the application |  | ||||||
|         alias /home/ubuntu/microblog/app/static; |  | ||||||
|         expires 30d; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,9 +0,0 @@ | ||||||
| [program:microblog-tasks] |  | ||||||
| command=/home/ubuntu/microblog/venv/bin/rq worker microblog-tasks |  | ||||||
| numprocs=1 |  | ||||||
| directory=/home/ubuntu/microblog |  | ||||||
| user=ubuntu |  | ||||||
| autostart=true |  | ||||||
| autorestart=true |  | ||||||
| stopasgroup=true |  | ||||||
| killasgroup=true |  | ||||||
|  | @ -1,8 +0,0 @@ | ||||||
| [program:microblog] |  | ||||||
| command=/home/ubuntu/microblog/venv/bin/gunicorn -b localhost:8000 -w 4 microblog:app |  | ||||||
| directory=/home/ubuntu/microblog |  | ||||||
| user=ubuntu |  | ||||||
| autostart=true |  | ||||||
| autorestart=true |  | ||||||
| stopasgroup=true |  | ||||||
| killasgroup=true |  | ||||||
							
								
								
									
										10
									
								
								microblog.py
								
								
								
								
							
							
						
						
									
										10
									
								
								microblog.py
								
								
								
								
							|  | @ -1,11 +1,7 @@ | ||||||
| from app import create_app, db, cli | from app import app, db, cli | ||||||
| from app.models import User, Post, Message, Notification, Task | from app.models import User, Post | ||||||
| 
 |  | ||||||
| app = create_app() |  | ||||||
| cli.register(app) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.shell_context_processor | @app.shell_context_processor | ||||||
| def make_shell_context(): | def make_shell_context(): | ||||||
|     return {'db': db, 'User': User, 'Post': Post, 'Message': Message, |     return {'db': db, 'User': User, 'Post': Post} | ||||||
|             'Notification': Notification, 'Task': Task} |  | ||||||
|  |  | ||||||
|  | @ -1,28 +0,0 @@ | ||||||
| """add language to posts |  | ||||||
| 
 |  | ||||||
| Revision ID: 2b017edaa91f |  | ||||||
| Revises: ae346256b650 |  | ||||||
| Create Date: 2017-10-04 22:48:34.494465 |  | ||||||
| 
 |  | ||||||
| """ |  | ||||||
| from alembic import op |  | ||||||
| import sqlalchemy as sa |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # revision identifiers, used by Alembic. |  | ||||||
| revision = '2b017edaa91f' |  | ||||||
| down_revision = 'ae346256b650' |  | ||||||
| branch_labels = None |  | ||||||
| depends_on = None |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def upgrade(): |  | ||||||
|     # ### commands auto generated by Alembic - please adjust! ### |  | ||||||
|     op.add_column('post', sa.Column('language', sa.String(length=5), nullable=True)) |  | ||||||
|     # ### end Alembic commands ### |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def downgrade(): |  | ||||||
|     # ### commands auto generated by Alembic - please adjust! ### |  | ||||||
|     op.drop_column('post', 'language') |  | ||||||
|     # ### end Alembic commands ### |  | ||||||
|  | @ -1,32 +0,0 @@ | ||||||
| """user tokens |  | ||||||
| 
 |  | ||||||
| Revision ID: 834b1a697901 |  | ||||||
| Revises: c81bac34faab |  | ||||||
| Create Date: 2017-11-05 18:41:07.996137 |  | ||||||
| 
 |  | ||||||
| """ |  | ||||||
| from alembic import op |  | ||||||
| import sqlalchemy as sa |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # revision identifiers, used by Alembic. |  | ||||||
| revision = '834b1a697901' |  | ||||||
| down_revision = 'c81bac34faab' |  | ||||||
| branch_labels = None |  | ||||||
| depends_on = None |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def upgrade(): |  | ||||||
|     # ### commands auto generated by Alembic - please adjust! ### |  | ||||||
|     op.add_column('user', sa.Column('token', sa.String(length=32), nullable=True)) |  | ||||||
|     op.add_column('user', sa.Column('token_expiration', sa.DateTime(), nullable=True)) |  | ||||||
|     op.create_index(op.f('ix_user_token'), 'user', ['token'], unique=True) |  | ||||||
|     # ### end Alembic commands ### |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def downgrade(): |  | ||||||
|     # ### commands auto generated by Alembic - please adjust! ### |  | ||||||
|     op.drop_index(op.f('ix_user_token'), table_name='user') |  | ||||||
|     op.drop_column('user', 'token_expiration') |  | ||||||
|     op.drop_column('user', 'token') |  | ||||||
|     # ### end Alembic commands ### |  | ||||||
|  | @ -1,38 +0,0 @@ | ||||||
| """tasks |  | ||||||
| 
 |  | ||||||
| Revision ID: c81bac34faab |  | ||||||
| Revises: f7ac3d27bb1d |  | ||||||
| Create Date: 2017-11-23 10:56:49.599779 |  | ||||||
| 
 |  | ||||||
| """ |  | ||||||
| from alembic import op |  | ||||||
| import sqlalchemy as sa |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # revision identifiers, used by Alembic. |  | ||||||
| revision = 'c81bac34faab' |  | ||||||
| down_revision = 'f7ac3d27bb1d' |  | ||||||
| branch_labels = None |  | ||||||
| depends_on = None |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def upgrade(): |  | ||||||
|     # ### commands auto generated by Alembic - please adjust! ### |  | ||||||
|     op.create_table('task', |  | ||||||
|     sa.Column('id', sa.String(length=36), nullable=False), |  | ||||||
|     sa.Column('name', sa.String(length=128), nullable=True), |  | ||||||
|     sa.Column('description', sa.String(length=128), nullable=True), |  | ||||||
|     sa.Column('user_id', sa.Integer(), nullable=True), |  | ||||||
|     sa.Column('complete', sa.Boolean(), nullable=True), |  | ||||||
|     sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), |  | ||||||
|     sa.PrimaryKeyConstraint('id') |  | ||||||
|     ) |  | ||||||
|     op.create_index(op.f('ix_task_name'), 'task', ['name'], unique=False) |  | ||||||
|     # ### end Alembic commands ### |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def downgrade(): |  | ||||||
|     # ### commands auto generated by Alembic - please adjust! ### |  | ||||||
|     op.drop_index(op.f('ix_task_name'), table_name='task') |  | ||||||
|     op.drop_table('task') |  | ||||||
|     # ### end Alembic commands ### |  | ||||||
|  | @ -1,41 +0,0 @@ | ||||||
| """private messages |  | ||||||
| 
 |  | ||||||
| Revision ID: d049de007ccf |  | ||||||
| Revises: 834b1a697901 |  | ||||||
| Create Date: 2017-11-12 23:30:28.571784 |  | ||||||
| 
 |  | ||||||
| """ |  | ||||||
| from alembic import op |  | ||||||
| import sqlalchemy as sa |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # revision identifiers, used by Alembic. |  | ||||||
| revision = 'd049de007ccf' |  | ||||||
| down_revision = '2b017edaa91f' |  | ||||||
| branch_labels = None |  | ||||||
| depends_on = None |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def upgrade(): |  | ||||||
|     # ### commands auto generated by Alembic - please adjust! ### |  | ||||||
|     op.create_table('message', |  | ||||||
|     sa.Column('id', sa.Integer(), nullable=False), |  | ||||||
|     sa.Column('sender_id', sa.Integer(), nullable=True), |  | ||||||
|     sa.Column('recipient_id', sa.Integer(), nullable=True), |  | ||||||
|     sa.Column('body', sa.String(length=140), nullable=True), |  | ||||||
|     sa.Column('timestamp', sa.DateTime(), nullable=True), |  | ||||||
|     sa.ForeignKeyConstraint(['recipient_id'], ['user.id'], ), |  | ||||||
|     sa.ForeignKeyConstraint(['sender_id'], ['user.id'], ), |  | ||||||
|     sa.PrimaryKeyConstraint('id') |  | ||||||
|     ) |  | ||||||
|     op.create_index(op.f('ix_message_timestamp'), 'message', ['timestamp'], unique=False) |  | ||||||
|     op.add_column('user', sa.Column('last_message_read_time', sa.DateTime(), nullable=True)) |  | ||||||
|     # ### end Alembic commands ### |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def downgrade(): |  | ||||||
|     # ### commands auto generated by Alembic - please adjust! ### |  | ||||||
|     op.drop_column('user', 'last_message_read_time') |  | ||||||
|     op.drop_index(op.f('ix_message_timestamp'), table_name='message') |  | ||||||
|     op.drop_table('message') |  | ||||||
|     # ### end Alembic commands ### |  | ||||||
|  | @ -1,40 +0,0 @@ | ||||||
| """notifications |  | ||||||
| 
 |  | ||||||
| Revision ID: f7ac3d27bb1d |  | ||||||
| Revises: d049de007ccf |  | ||||||
| Create Date: 2017-11-22 19:48:39.945858 |  | ||||||
| 
 |  | ||||||
| """ |  | ||||||
| from alembic import op |  | ||||||
| import sqlalchemy as sa |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # revision identifiers, used by Alembic. |  | ||||||
| revision = 'f7ac3d27bb1d' |  | ||||||
| down_revision = 'd049de007ccf' |  | ||||||
| branch_labels = None |  | ||||||
| depends_on = None |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def upgrade(): |  | ||||||
|     # ### commands auto generated by Alembic - please adjust! ### |  | ||||||
|     op.create_table('notification', |  | ||||||
|     sa.Column('id', sa.Integer(), nullable=False), |  | ||||||
|     sa.Column('name', sa.String(length=128), nullable=True), |  | ||||||
|     sa.Column('user_id', sa.Integer(), nullable=True), |  | ||||||
|     sa.Column('timestamp', sa.Float(), nullable=True), |  | ||||||
|     sa.Column('payload_json', sa.Text(), nullable=True), |  | ||||||
|     sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), |  | ||||||
|     sa.PrimaryKeyConstraint('id') |  | ||||||
|     ) |  | ||||||
|     op.create_index(op.f('ix_notification_name'), 'notification', ['name'], unique=False) |  | ||||||
|     op.create_index(op.f('ix_notification_timestamp'), 'notification', ['timestamp'], unique=False) |  | ||||||
|     # ### end Alembic commands ### |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def downgrade(): |  | ||||||
|     # ### commands auto generated by Alembic - please adjust! ### |  | ||||||
|     op.drop_index(op.f('ix_notification_timestamp'), table_name='notification') |  | ||||||
|     op.drop_index(op.f('ix_notification_name'), table_name='notification') |  | ||||||
|     op.drop_table('notification') |  | ||||||
|     # ### end Alembic commands ### |  | ||||||
|  | @ -1,49 +0,0 @@ | ||||||
| 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 |  | ||||||
| elasticsearch==7.13.3 |  | ||||||
| email-validator==1.1.3 |  | ||||||
| Flask==2.0.1 |  | ||||||
| Flask-Babel==2.0.0 |  | ||||||
| Flask-Bootstrap==3.3.7.1 |  | ||||||
| Flask-HTTPAuth==4.4.0 |  | ||||||
| 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 |  | ||||||
| httpie==2.4.0 |  | ||||||
| idna==2.10 |  | ||||||
| itsdangerous==2.0.1 |  | ||||||
| Jinja2==3.0.1 |  | ||||||
| langdetect==1.0.9 |  | ||||||
| Mako==1.1.4 |  | ||||||
| MarkupSafe==2.0.1 |  | ||||||
| Pygments==2.9.0 |  | ||||||
| PyJWT==2.1.0 |  | ||||||
| PySocks==1.7.1 |  | ||||||
| python-dateutil==2.8.1 |  | ||||||
| python-dotenv==0.18.0 |  | ||||||
| python-editor==1.0.4 |  | ||||||
| pytz==2021.1 |  | ||||||
| redis==3.5.3 |  | ||||||
| requests==2.25.1 |  | ||||||
| requests-toolbelt==0.9.1 |  | ||||||
| rq==1.9.0 |  | ||||||
| six==1.16.0 |  | ||||||
| SQLAlchemy==1.4.20 |  | ||||||
| urllib3==1.26.6 |  | ||||||
| visitor==0.1.3 |  | ||||||
| Werkzeug==2.0.1 |  | ||||||
| WTForms==2.3.3 |  | ||||||
| 
 |  | ||||||
| # requirements for Heroku |  | ||||||
| #psycopg2==2.9.1 |  | ||||||
| #gunicorn==20.1.0 |  | ||||||
							
								
								
									
										15
									
								
								tests.py
								
								
								
								
							
							
						
						
									
										15
									
								
								tests.py
								
								
								
								
							|  | @ -1,21 +1,16 @@ | ||||||
| #!/usr/bin/env python | #!/usr/bin/env python | ||||||
|  | import os | ||||||
|  | os.environ['DATABASE_URL'] = 'sqlite://' | ||||||
|  | 
 | ||||||
| from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||||
| import unittest | import unittest | ||||||
| from app import create_app, db | from app import app, db | ||||||
| from app.models import User, Post | from app.models import User, Post | ||||||
| from config import Config |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class TestConfig(Config): |  | ||||||
|     TESTING = True |  | ||||||
|     SQLALCHEMY_DATABASE_URI = 'sqlite://' |  | ||||||
|     ELASTICSEARCH_URL = None |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class UserModelCase(unittest.TestCase): | class UserModelCase(unittest.TestCase): | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         self.app = create_app(TestConfig) |         self.app_context = app.app_context() | ||||||
|         self.app_context = self.app.app_context() |  | ||||||
|         self.app_context.push() |         self.app_context.push() | ||||||
|         db.create_all() |         db.create_all() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue