Compare commits
1 Commits
3472187155
...
8b4993d2fc
Author | SHA1 | Date |
---|---|---|
gbrochar | 8b4993d2fc |
1
Procfile
1
Procfile
|
@ -1,2 +1 @@
|
||||||
web: flask db upgrade; flask translate compile; gunicorn microblog:app
|
web: flask db upgrade; flask translate compile; gunicorn microblog:app
|
||||||
worker: rq worker microblog-tasks
|
|
||||||
|
|
|
@ -10,8 +10,6 @@ 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 elasticsearch import Elasticsearch
|
||||||
from redis import Redis
|
|
||||||
import rq
|
|
||||||
from config import Config
|
from config import Config
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
@ -38,8 +36,6 @@ def create_app(config_class=Config):
|
||||||
babel.init_app(app)
|
babel.init_app(app)
|
||||||
app.elasticsearch = Elasticsearch([app.config['ELASTICSEARCH_URL']]) \
|
app.elasticsearch = Elasticsearch([app.config['ELASTICSEARCH_URL']]) \
|
||||||
if app.config['ELASTICSEARCH_URL'] else None
|
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
|
from app.errors import bp as errors_bp
|
||||||
app.register_blueprint(errors_bp)
|
app.register_blueprint(errors_bp)
|
||||||
|
@ -50,9 +46,6 @@ def create_app(config_class=Config):
|
||||||
from app.main import bp as main_bp
|
from app.main import bp as main_bp
|
||||||
app.register_blueprint(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 not app.debug and not app.testing:
|
||||||
if app.config['MAIL_SERVER']:
|
if app.config['MAIL_SERVER']:
|
||||||
auth = None
|
auth = None
|
||||||
|
|
|
@ -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())
|
|
13
app/email.py
13
app/email.py
|
@ -9,16 +9,9 @@ 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,
|
||||||
for attachment in attachments:
|
args=(current_app._get_current_object(), msg)).start()
|
||||||
msg.attach(*attachment)
|
|
||||||
if sync:
|
|
||||||
mail.send(msg)
|
|
||||||
else:
|
|
||||||
Thread(target=send_async_email,
|
|
||||||
args=(current_app._get_current_object(), msg)).start()
|
|
||||||
|
|
|
@ -1,24 +1,14 @@
|
||||||
from flask import render_template, request
|
from flask import render_template
|
||||||
from app import db
|
from app import db
|
||||||
from app.errors import bp
|
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)
|
@bp.app_errorhandler(404)
|
||||||
def not_found_error(error):
|
def not_found_error(error):
|
||||||
if wants_json_response():
|
|
||||||
return api_error_response(404)
|
|
||||||
return render_template('errors/404.html'), 404
|
return render_template('errors/404.html'), 404
|
||||||
|
|
||||||
|
|
||||||
@bp.app_errorhandler(500)
|
@bp.app_errorhandler(500)
|
||||||
def internal_error(error):
|
def internal_error(error):
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
if wants_json_response():
|
|
||||||
return api_error_response(500)
|
|
||||||
return render_template('errors/500.html'), 500
|
return render_template('errors/500.html'), 500
|
||||||
|
|
|
@ -65,6 +65,9 @@ def explore():
|
||||||
posts=posts.items, next_url=next_url,
|
posts=posts.items, next_url=next_url,
|
||||||
prev_url=prev_url)
|
prev_url=prev_url)
|
||||||
|
|
||||||
|
@bp.route('/about')
|
||||||
|
def about_page():
|
||||||
|
return render_template('about.html', title=_('About me'))
|
||||||
|
|
||||||
@bp.route('/user/<username>')
|
@bp.route('/user/<username>')
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -208,17 +211,6 @@ def messages():
|
||||||
next_url=next_url, prev_url=prev_url)
|
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')
|
@bp.route('/notifications')
|
||||||
@login_required
|
@login_required
|
||||||
def notifications():
|
def notifications():
|
||||||
|
|
118
app/models.py
118
app/models.py
|
@ -1,15 +1,11 @@
|
||||||
import base64
|
from datetime import datetime
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
from time import time
|
from time import time
|
||||||
from flask import current_app, url_for
|
from flask import current_app
|
||||||
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
|
|
||||||
import rq
|
|
||||||
from app import db, login
|
from app import db, login
|
||||||
from app.search import add_to_index, remove_from_index, query_index
|
from app.search import add_to_index, remove_from_index, query_index
|
||||||
|
|
||||||
|
@ -57,31 +53,6 @@ db.event.listen(db.session, 'before_commit', SearchableMixin.before_commit)
|
||||||
db.event.listen(db.session, 'after_commit', SearchableMixin.after_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(
|
||||||
'followers',
|
'followers',
|
||||||
db.Column('follower_id', db.Integer, db.ForeignKey('user.id')),
|
db.Column('follower_id', db.Integer, db.ForeignKey('user.id')),
|
||||||
|
@ -89,7 +60,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,8 +68,6 @@ 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),
|
||||||
|
@ -113,7 +82,6 @@ class User(UserMixin, PaginatedAPIMixin, db.Model):
|
||||||
last_message_read_time = db.Column(db.DateTime)
|
last_message_read_time = db.Column(db.DateTime)
|
||||||
notifications = db.relationship('Notification', backref='user',
|
notifications = db.relationship('Notification', backref='user',
|
||||||
lazy='dynamic')
|
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)
|
||||||
|
@ -173,67 +141,6 @@ class User(UserMixin, PaginatedAPIMixin, db.Model):
|
||||||
db.session.add(n)
|
db.session.add(n)
|
||||||
return 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):
|
||||||
|
@ -272,22 +179,3 @@ class Notification(db.Model):
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
return json.loads(str(self.payload_json))
|
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
|
|
||||||
|
|
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())
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "base.html" %} {% block app_content %}
|
||||||
|
|
||||||
|
<h1>A propos</h1>
|
||||||
|
<p>
|
||||||
|
Bonjour, je m'appelle Gaëtan et j'ai modifié
|
||||||
|
<a href="https://github.com/miguelgrinberg/microblog"
|
||||||
|
>l'application Microblog de Miguel Grinberg pour ce TP</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
|
@ -20,6 +20,7 @@
|
||||||
<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('main.index') }}">{{ _('Home') }}</a></li>
|
||||||
<li><a href="{{ url_for('main.explore') }}">{{ _('Explore') }}</a></li>
|
<li><a href="{{ url_for('main.explore') }}">{{ _('Explore') }}</a></li>
|
||||||
|
<li><a href="{{ url_for('main.about_page') }}">{{ _('About') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% if g.search_form %}
|
{% if g.search_form %}
|
||||||
<form class="navbar-form navbar-left" method="get" action="{{ url_for('main.search') }}">
|
<form class="navbar-form navbar-left" method="get" action="{{ url_for('main.search') }}">
|
||||||
|
@ -53,18 +54,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 %}
|
||||||
|
@ -141,9 +130,6 @@
|
||||||
$('#message_count').text(n);
|
$('#message_count').text(n);
|
||||||
$('#message_count').css('visibility', n ? 'visible' : 'hidden');
|
$('#message_count').css('visibility', n ? 'visible' : 'hidden');
|
||||||
}
|
}
|
||||||
function set_task_progress(task_id, progress) {
|
|
||||||
$('#' + task_id + '-progress').text(progress);
|
|
||||||
}
|
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
$(function() {
|
$(function() {
|
||||||
var since = 0;
|
var since = 0;
|
||||||
|
@ -151,15 +137,8 @@
|
||||||
$.ajax('{{ url_for('main.notifications') }}?since=' + since).done(
|
$.ajax('{{ url_for('main.notifications') }}?since=' + since).done(
|
||||||
function(notifications) {
|
function(notifications) {
|
||||||
for (var i = 0; i < notifications.length; i++) {
|
for (var i = 0; i < notifications.length; i++) {
|
||||||
switch (notifications[i].name) {
|
if (notifications[i].name == 'unread_message_count')
|
||||||
case 'unread_message_count':
|
set_message_count(notifications[i].data);
|
||||||
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;
|
since = notifications[i].timestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
|
@ -13,9 +13,6 @@
|
||||||
<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('main.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('main.follow', username=user.username) }}" method="post">
|
||||||
|
|
|
@ -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-11-25 18:26-0800\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"
|
||||||
|
@ -18,7 +18,7 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.5.1\n"
|
"Generated-By: Babel 2.5.1\n"
|
||||||
|
|
||||||
#: app/__init__.py:20
|
#: app/__init__.py:18
|
||||||
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."
|
||||||
|
|
||||||
|
@ -153,14 +153,6 @@ msgstr "Tu mensaje ha sido enviado."
|
||||||
msgid "Send Message"
|
msgid "Send Message"
|
||||||
msgstr "Enviar Mensaje"
|
msgstr "Enviar Mensaje"
|
||||||
|
|
||||||
#: app/main/routes.py:197
|
|
||||||
msgid "An export task is currently in progress"
|
|
||||||
msgstr "Una tarea de exportación esta en progreso"
|
|
||||||
|
|
||||||
#: app/main/routes.py:199
|
|
||||||
msgid "Exporting posts..."
|
|
||||||
msgstr "Exportando artículos..."
|
|
||||||
|
|
||||||
#: app/templates/_post.html:16
|
#: app/templates/_post.html:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s said %(when)s"
|
msgid "%(username)s said %(when)s"
|
||||||
|
@ -198,7 +190,7 @@ msgstr "Perfil"
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr "Salir"
|
msgstr "Salir"
|
||||||
|
|
||||||
#: app/templates/base.html:95
|
#: app/templates/base.html:83
|
||||||
msgid "Error: Could not contact server."
|
msgid "Error: Could not contact server."
|
||||||
msgstr "Error: el servidor no pudo ser contactado."
|
msgstr "Error: el servidor no pudo ser contactado."
|
||||||
|
|
||||||
|
@ -207,11 +199,11 @@ msgstr "Error: el servidor no pudo ser contactado."
|
||||||
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:34
|
||||||
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:39
|
||||||
msgid "Older posts"
|
msgid "Older posts"
|
||||||
msgstr "Artículos previos"
|
msgstr "Artículos previos"
|
||||||
|
|
||||||
|
@ -262,19 +254,15 @@ msgstr "siguiendo a %(count)d"
|
||||||
msgid "Edit your profile"
|
msgid "Edit your profile"
|
||||||
msgstr "Editar tu perfil"
|
msgstr "Editar tu perfil"
|
||||||
|
|
||||||
#: app/templates/user.html:17
|
#: app/templates/user.html:17 app/templates/user_popup.html:14
|
||||||
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 app/templates/user_popup.html:16
|
||||||
msgid "Unfollow"
|
msgid "Unfollow"
|
||||||
msgstr "Dejar de seguir"
|
msgstr "Dejar de seguir"
|
||||||
|
|
||||||
#: app/templates/user.html:25
|
#: app/templates/user.html:22
|
||||||
msgid "Send private message"
|
msgid "Send private message"
|
||||||
msgstr "Enviar mensaje privado"
|
msgstr "Enviar mensaje privado"
|
||||||
|
|
||||||
|
|
|
@ -21,5 +21,4 @@ class Config(object):
|
||||||
LANGUAGES = ['en', 'es']
|
LANGUAGES = ['en', 'es']
|
||||||
MS_TRANSLATOR_KEY = os.environ.get('MS_TRANSLATOR_KEY')
|
MS_TRANSLATOR_KEY = os.environ.get('MS_TRANSLATOR_KEY')
|
||||||
ELASTICSEARCH_URL = os.environ.get('ELASTICSEARCH_URL')
|
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,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,5 +1,5 @@
|
||||||
from app import create_app, db, cli
|
from app import create_app, db, cli
|
||||||
from app.models import User, Post, Message, Notification, Task
|
from app.models import User, Post, Message, Notification
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
cli.register(app)
|
cli.register(app)
|
||||||
|
@ -8,4 +8,4 @@ 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, 'Message': Message,
|
||||||
'Notification': Notification, 'Task': Task}
|
'Notification': Notification}
|
||||||
|
|
|
@ -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 ###
|
|
|
@ -11,7 +11,6 @@ email-validator==1.1.3
|
||||||
Flask==2.0.1
|
Flask==2.0.1
|
||||||
Flask-Babel==2.0.0
|
Flask-Babel==2.0.0
|
||||||
Flask-Bootstrap==3.3.7.1
|
Flask-Bootstrap==3.3.7.1
|
||||||
Flask-HTTPAuth==4.4.0
|
|
||||||
Flask-Login==0.5.0
|
Flask-Login==0.5.0
|
||||||
Flask-Mail==0.9.1
|
Flask-Mail==0.9.1
|
||||||
Flask-Migrate==3.0.1
|
Flask-Migrate==3.0.1
|
||||||
|
@ -19,24 +18,18 @@ Flask-Moment==1.0.1
|
||||||
Flask-SQLAlchemy==2.5.1
|
Flask-SQLAlchemy==2.5.1
|
||||||
Flask-WTF==0.15.1
|
Flask-WTF==0.15.1
|
||||||
greenlet==1.1.0
|
greenlet==1.1.0
|
||||||
httpie==2.4.0
|
|
||||||
idna==2.10
|
idna==2.10
|
||||||
itsdangerous==2.0.1
|
itsdangerous==2.0.1
|
||||||
Jinja2==3.0.1
|
Jinja2==3.0.1
|
||||||
langdetect==1.0.9
|
langdetect==1.0.9
|
||||||
Mako==1.1.4
|
Mako==1.1.4
|
||||||
MarkupSafe==2.0.1
|
MarkupSafe==2.0.1
|
||||||
Pygments==2.9.0
|
|
||||||
PyJWT==2.1.0
|
PyJWT==2.1.0
|
||||||
PySocks==1.7.1
|
|
||||||
python-dateutil==2.8.1
|
python-dateutil==2.8.1
|
||||||
python-dotenv==0.18.0
|
python-dotenv==0.18.0
|
||||||
python-editor==1.0.4
|
python-editor==1.0.4
|
||||||
pytz==2021.1
|
pytz==2021.1
|
||||||
redis==3.5.3
|
|
||||||
requests==2.25.1
|
requests==2.25.1
|
||||||
requests-toolbelt==0.9.1
|
|
||||||
rq==1.9.0
|
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
SQLAlchemy==1.4.20
|
SQLAlchemy==1.4.20
|
||||||
urllib3==1.26.6
|
urllib3==1.26.6
|
||||||
|
|
Loading…
Reference in New Issue