Compare commits

..

No commits in common. "main" and "v0.13" have entirely different histories.
main ... v0.13

58 changed files with 450 additions and 1742 deletions

View File

@ -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"]

View File

@ -1,2 +0,0 @@
web: flask db upgrade; flask translate compile; gunicorn microblog:app
worker: rq worker microblog-tasks

7
Vagrantfile vendored
View File

@ -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

View File

@ -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

View File

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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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())

View File

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

View File

@ -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))

View File

@ -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)

View File

@ -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')

View File

@ -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))

13
app/errors.py Normal file
View File

@ -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

View File

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

View File

@ -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

View File

@ -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'))

View File

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

View File

@ -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'))

View File

@ -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])

View File

@ -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

200
app/routes.py Normal file
View File

@ -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'))

View File

@ -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

View File

@ -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())

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -1,7 +0,0 @@
Dear {{ user.username }},
Please find attached the archive of your posts that you requested.
Sincerely,
The Microblog Team

View File

@ -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>

View File

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

View File

@ -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 %}

View File

@ -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">&larr;</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">&rarr;</span>
</a>
</li>
</ul>
</nav>
{% endblock %}

View File

@ -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">&larr;</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">&rarr;</span>
</a>
</li>
</ul>
</nav>
{% endblock %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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']

View File

@ -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
View File

@ -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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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

View File

@ -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()