Compare commits
20 Commits
Author | SHA1 | Date |
---|---|---|
Miguel Grinberg | 3472187155 | |
Miguel Grinberg | bb2273349f | |
Miguel Grinberg | a25d6f2f19 | |
Miguel Grinberg | 7f32336d21 | |
Miguel Grinberg | b577aa6b62 | |
Miguel Grinberg | 5cfb75def5 | |
Miguel Grinberg | 94b40cf4b7 | |
Miguel Grinberg | 868ad4b410 | |
Miguel Grinberg | f4a401e320 | |
Miguel Grinberg | 7f7c8ef5d7 | |
Miguel Grinberg | 8a56c34ce2 | |
Miguel Grinberg | a650e6f989 | |
Miguel Grinberg | 542fa42312 | |
Miguel Grinberg | 14f6f99ea9 | |
Miguel Grinberg | fdeb27d052 | |
Miguel Grinberg | d1363e0326 | |
Miguel Grinberg | 8f43aadd41 | |
Miguel Grinberg | f9ba2d28cf | |
Miguel Grinberg | e7b1227a1c | |
Miguel Grinberg | 68368ea5c5 |
13
Dockerfile
13
Dockerfile
|
@ -1,8 +1,13 @@
|
||||||
FROM python:slim
|
FROM python:slim
|
||||||
|
|
||||||
|
RUN useradd microblog
|
||||||
|
|
||||||
|
WORKDIR /home/microblog
|
||||||
|
|
||||||
COPY requirements.txt requirements.txt
|
COPY requirements.txt requirements.txt
|
||||||
RUN pip install -r requirements.txt
|
RUN python -m venv venv
|
||||||
RUN pip install gunicorn pymysql cryptography
|
RUN venv/bin/pip install -r requirements.txt
|
||||||
|
RUN venv/bin/pip install gunicorn pymysql cryptography
|
||||||
|
|
||||||
COPY app app
|
COPY app app
|
||||||
COPY migrations migrations
|
COPY migrations migrations
|
||||||
|
@ -10,7 +15,9 @@ COPY microblog.py config.py boot.sh ./
|
||||||
RUN chmod a+x boot.sh
|
RUN chmod a+x boot.sh
|
||||||
|
|
||||||
ENV FLASK_APP microblog.py
|
ENV FLASK_APP microblog.py
|
||||||
RUN flask translate compile
|
|
||||||
|
RUN chown -R microblog:microblog ./
|
||||||
|
USER microblog
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
ENTRYPOINT ["./boot.sh"]
|
ENTRYPOINT ["./boot.sh"]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
Vagrant.configure("2") do |config|
|
Vagrant.configure("2") do |config|
|
||||||
config.vm.box = "ubuntu/jammy64"
|
config.vm.box = "ubuntu/focal64"
|
||||||
config.vm.network "private_network", ip: "192.168.56.10"
|
config.vm.network "private_network", ip: "192.168.33.10"
|
||||||
config.vm.provider "virtualbox" do |vb|
|
config.vm.provider "virtualbox" do |vb|
|
||||||
vb.memory = "2048"
|
vb.memory = "2048"
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,7 @@ 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
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
|
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
|
||||||
|
@ -13,17 +14,13 @@ from redis import Redis
|
||||||
import rq
|
import rq
|
||||||
from config import Config
|
from config import Config
|
||||||
|
|
||||||
|
|
||||||
def get_locale():
|
|
||||||
return request.accept_languages.best_match(current_app.config['LANGUAGES'])
|
|
||||||
|
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
migrate = Migrate()
|
migrate = Migrate()
|
||||||
login = LoginManager()
|
login = LoginManager()
|
||||||
login.login_view = 'auth.login'
|
login.login_view = 'auth.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()
|
||||||
|
bootstrap = Bootstrap()
|
||||||
moment = Moment()
|
moment = Moment()
|
||||||
babel = Babel()
|
babel = Babel()
|
||||||
|
|
||||||
|
@ -36,8 +33,9 @@ def create_app(config_class=Config):
|
||||||
migrate.init_app(app, db)
|
migrate.init_app(app, db)
|
||||||
login.init_app(app)
|
login.init_app(app)
|
||||||
mail.init_app(app)
|
mail.init_app(app)
|
||||||
|
bootstrap.init_app(app)
|
||||||
moment.init_app(app)
|
moment.init_app(app)
|
||||||
babel.init_app(app, locale_selector=get_locale)
|
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.redis = Redis.from_url(app.config['REDIS_URL'])
|
||||||
|
@ -52,9 +50,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.cli import bp as cli_bp
|
|
||||||
app.register_blueprint(cli_bp)
|
|
||||||
|
|
||||||
from app.api import bp as api_bp
|
from app.api import bp as api_bp
|
||||||
app.register_blueprint(api_bp, url_prefix='/api')
|
app.register_blueprint(api_bp, url_prefix='/api')
|
||||||
|
|
||||||
|
@ -96,4 +91,9 @@ def create_app(config_class=Config):
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@babel.localeselector
|
||||||
|
def get_locale():
|
||||||
|
return request.accept_languages.best_match(current_app.config['LANGUAGES'])
|
||||||
|
|
||||||
|
|
||||||
from app import models
|
from app import models
|
||||||
|
|
58
app/cli.py
58
app/cli.py
|
@ -1,37 +1,35 @@
|
||||||
import os
|
import os
|
||||||
from flask import Blueprint
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
bp = Blueprint('cli', __name__, cli_group=None)
|
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
@app.cli.group()
|
||||||
|
def translate():
|
||||||
|
"""Translation and localization commands."""
|
||||||
|
pass
|
||||||
|
|
||||||
@bp.cli.group()
|
@translate.command()
|
||||||
def translate():
|
@click.argument('lang')
|
||||||
"""Translation and localization commands."""
|
def init(lang):
|
||||||
pass
|
"""Initialize a new language."""
|
||||||
|
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
|
||||||
|
raise RuntimeError('extract command failed')
|
||||||
|
if os.system(
|
||||||
|
'pybabel init -i messages.pot -d app/translations -l ' + lang):
|
||||||
|
raise RuntimeError('init command failed')
|
||||||
|
os.remove('messages.pot')
|
||||||
|
|
||||||
@translate.command()
|
@translate.command()
|
||||||
@click.argument('lang')
|
def update():
|
||||||
def init(lang):
|
"""Update all languages."""
|
||||||
"""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')
|
if os.system('pybabel update -i messages.pot -d app/translations'):
|
||||||
if os.system(
|
raise RuntimeError('update command failed')
|
||||||
'pybabel init -i messages.pot -d app/translations -l ' + lang):
|
os.remove('messages.pot')
|
||||||
raise RuntimeError('init command failed')
|
|
||||||
os.remove('messages.pot')
|
|
||||||
|
|
||||||
@translate.command()
|
@translate.command()
|
||||||
def update():
|
def compile():
|
||||||
"""Update all languages."""
|
"""Compile all languages."""
|
||||||
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
|
if os.system('pybabel compile -d app/translations'):
|
||||||
raise RuntimeError('extract command failed')
|
raise RuntimeError('compile command failed')
|
||||||
if os.system('pybabel update -i messages.pot -d app/translations'):
|
|
||||||
raise RuntimeError('update command failed')
|
|
||||||
os.remove('messages.pot')
|
|
||||||
|
|
||||||
@translate.command()
|
|
||||||
def compile():
|
|
||||||
"""Compile all languages."""
|
|
||||||
if os.system('pybabel compile -d app/translations'):
|
|
||||||
raise RuntimeError('compile command failed')
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ class EditProfileForm(FlaskForm):
|
||||||
submit = SubmitField(_l('Submit'))
|
submit = SubmitField(_l('Submit'))
|
||||||
|
|
||||||
def __init__(self, original_username, *args, **kwargs):
|
def __init__(self, original_username, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super(EditProfileForm, self).__init__(*args, **kwargs)
|
||||||
self.original_username = original_username
|
self.original_username = original_username
|
||||||
|
|
||||||
def validate_username(self, username):
|
def validate_username(self, username):
|
||||||
|
|
|
@ -38,7 +38,7 @@ def index():
|
||||||
flash(_('Your post is now live!'))
|
flash(_('Your post is now live!'))
|
||||||
return redirect(url_for('main.index'))
|
return redirect(url_for('main.index'))
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
posts = current_user.following_posts().paginate(
|
posts = current_user.followed_posts().paginate(
|
||||||
page=page, per_page=current_app.config['POSTS_PER_PAGE'],
|
page=page, per_page=current_app.config['POSTS_PER_PAGE'],
|
||||||
error_out=False)
|
error_out=False)
|
||||||
next_url = url_for('main.index', page=posts.next_num) \
|
next_url = url_for('main.index', page=posts.next_num) \
|
||||||
|
@ -151,10 +151,9 @@ def unfollow(username):
|
||||||
@bp.route('/translate', methods=['POST'])
|
@bp.route('/translate', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def translate_text():
|
def translate_text():
|
||||||
data = request.get_json()
|
return jsonify({'text': translate(request.form['text'],
|
||||||
return {'text': translate(data['text'],
|
request.form['source_language'],
|
||||||
data['source_language'],
|
request.form['dest_language'])})
|
||||||
data['dest_language'])}
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/search')
|
@bp.route('/search')
|
||||||
|
|
|
@ -24,7 +24,7 @@ class SearchableMixin(object):
|
||||||
for i in range(len(ids)):
|
for i in range(len(ids)):
|
||||||
when.append((ids[i], i))
|
when.append((ids[i], i))
|
||||||
return cls.query.filter(cls.id.in_(ids)).order_by(
|
return cls.query.filter(cls.id.in_(ids)).order_by(
|
||||||
db.case(*when, value=cls.id)), total
|
db.case(when, value=cls.id)), total
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def before_commit(cls, session):
|
def before_commit(cls, session):
|
||||||
|
@ -84,10 +84,8 @@ class PaginatedAPIMixin(object):
|
||||||
|
|
||||||
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')),
|
||||||
primary_key=True),
|
db.Column('followed_id', db.Integer, db.ForeignKey('user.id'))
|
||||||
db.Column('followed_id', db.Integer, db.ForeignKey('user.id'),
|
|
||||||
primary_key=True)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,32 +94,26 @@ class User(UserMixin, PaginatedAPIMixin, db.Model):
|
||||||
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)
|
||||||
password_hash = db.Column(db.String(128))
|
password_hash = db.Column(db.String(128))
|
||||||
posts = db.relationship('Post', back_populates='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 = db.Column(db.String(32), index=True, unique=True)
|
||||||
token_expiration = db.Column(db.DateTime)
|
token_expiration = db.Column(db.DateTime)
|
||||||
following = 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),
|
||||||
lazy='dynamic', back_populates='followers')
|
backref=db.backref('followers', lazy='dynamic'), lazy='dynamic')
|
||||||
followers = db.relationship(
|
|
||||||
'User', secondary=followers,
|
|
||||||
primaryjoin=(followers.c.followed_id == id),
|
|
||||||
secondaryjoin=(followers.c.follower_id == id),
|
|
||||||
lazy='dynamic', back_populates='following')
|
|
||||||
messages_sent = db.relationship('Message',
|
messages_sent = db.relationship('Message',
|
||||||
foreign_keys='Message.sender_id',
|
foreign_keys='Message.sender_id',
|
||||||
lazy='dynamic', back_populates='author')
|
backref='author', lazy='dynamic')
|
||||||
messages_received = db.relationship('Message',
|
messages_received = db.relationship('Message',
|
||||||
foreign_keys='Message.recipient_id',
|
foreign_keys='Message.recipient_id',
|
||||||
lazy='dynamic',
|
backref='recipient', lazy='dynamic')
|
||||||
back_populates='recipient')
|
|
||||||
last_message_read_time = db.Column(db.DateTime)
|
last_message_read_time = db.Column(db.DateTime)
|
||||||
notifications = db.relationship('Notification', lazy='dynamic',
|
notifications = db.relationship('Notification', backref='user',
|
||||||
back_populates='user')
|
lazy='dynamic')
|
||||||
tasks = db.relationship('Task', lazy='dynamic', back_populates='user')
|
tasks = db.relationship('Task', backref='user', lazy='dynamic')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<User {}>'.format(self.username)
|
return '<User {}>'.format(self.username)
|
||||||
|
@ -134,25 +126,27 @@ class User(UserMixin, PaginatedAPIMixin, db.Model):
|
||||||
|
|
||||||
def avatar(self, size):
|
def avatar(self, size):
|
||||||
digest = md5(self.email.lower().encode('utf-8')).hexdigest()
|
digest = md5(self.email.lower().encode('utf-8')).hexdigest()
|
||||||
return f'https://www.gravatar.com/avatar/{digest}?d=identicon&s={size}'
|
return 'https://www.gravatar.com/avatar/{}?d=identicon&s={}'.format(
|
||||||
|
digest, size)
|
||||||
|
|
||||||
def follow(self, user):
|
def follow(self, user):
|
||||||
if not self.is_following(user):
|
if not self.is_following(user):
|
||||||
self.following.append(user)
|
self.followed.append(user)
|
||||||
|
|
||||||
def unfollow(self, user):
|
def unfollow(self, user):
|
||||||
if self.is_following(user):
|
if self.is_following(user):
|
||||||
self.following.remove(user)
|
self.followed.remove(user)
|
||||||
|
|
||||||
def is_following(self, user):
|
def is_following(self, user):
|
||||||
return user in self.following
|
return self.followed.filter(
|
||||||
|
followers.c.followed_id == user.id).count() > 0
|
||||||
|
|
||||||
def following_posts(self):
|
def followed_posts(self):
|
||||||
following = Post.query.join(
|
followed = Post.query.join(
|
||||||
followers, (followers.c.followed_id == Post.user_id)).filter(
|
followers, (followers.c.followed_id == Post.user_id)).filter(
|
||||||
followers.c.follower_id == self.id)
|
followers.c.follower_id == self.id)
|
||||||
own = Post.query.filter_by(user_id=self.id)
|
own = Post.query.filter_by(user_id=self.id)
|
||||||
return following.union(own).order_by(Post.timestamp.desc())
|
return followed.union(own).order_by(Post.timestamp.desc())
|
||||||
|
|
||||||
def get_reset_password_token(self, expires_in=600):
|
def get_reset_password_token(self, expires_in=600):
|
||||||
return jwt.encode(
|
return jwt.encode(
|
||||||
|
@ -251,9 +245,8 @@ class Post(SearchableMixin, db.Model):
|
||||||
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'), index=True)
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
language = db.Column(db.String(5))
|
language = db.Column(db.String(5))
|
||||||
author = db.relationship('User', back_populates='posts')
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Post {}>'.format(self.body)
|
return '<Post {}>'.format(self.body)
|
||||||
|
@ -261,14 +254,10 @@ class Post(SearchableMixin, db.Model):
|
||||||
|
|
||||||
class Message(db.Model):
|
class Message(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
|
sender_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
recipient_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
|
recipient_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
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)
|
||||||
author = db.relationship('User', foreign_keys='Message.sender_id',
|
|
||||||
back_populates='messages_sent')
|
|
||||||
recipient = db.relationship('User', foreign_keys='Message.recipient_id',
|
|
||||||
back_populates='messages_received')
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Message {}>'.format(self.body)
|
return '<Message {}>'.format(self.body)
|
||||||
|
@ -277,10 +266,9 @@ class Message(db.Model):
|
||||||
class Notification(db.Model):
|
class Notification(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(128), index=True)
|
name = db.Column(db.String(128), index=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
timestamp = db.Column(db.Float, index=True, default=time)
|
timestamp = db.Column(db.Float, index=True, default=time)
|
||||||
payload_json = db.Column(db.Text)
|
payload_json = db.Column(db.Text)
|
||||||
user = db.relationship('User', back_populates='notifications')
|
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
return json.loads(str(self.payload_json))
|
return json.loads(str(self.payload_json))
|
||||||
|
@ -292,7 +280,6 @@ class Task(db.Model):
|
||||||
description = db.Column(db.String(128))
|
description = db.Column(db.String(128))
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
complete = db.Column(db.Boolean, default=False)
|
complete = db.Column(db.Boolean, default=False)
|
||||||
user = db.relationship('User', back_populates='tasks')
|
|
||||||
|
|
||||||
def get_rq_job(self):
|
def get_rq_job(self):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -7,7 +7,7 @@ def add_to_index(index, model):
|
||||||
payload = {}
|
payload = {}
|
||||||
for field in model.__searchable__:
|
for field in model.__searchable__:
|
||||||
payload[field] = getattr(model, field)
|
payload[field] = getattr(model, field)
|
||||||
current_app.elasticsearch.index(index=index, id=model.id, document=payload)
|
current_app.elasticsearch.index(index=index, id=model.id, body=payload)
|
||||||
|
|
||||||
|
|
||||||
def remove_from_index(index, model):
|
def remove_from_index(index, model):
|
||||||
|
@ -21,8 +21,7 @@ def query_index(index, query, page, per_page):
|
||||||
return [], 0
|
return [], 0
|
||||||
search = current_app.elasticsearch.search(
|
search = current_app.elasticsearch.search(
|
||||||
index=index,
|
index=index,
|
||||||
query={'multi_match': {'query': query, 'fields': ['*']}},
|
body={'query': {'multi_match': {'query': query, 'fields': ['*']}},
|
||||||
from_=(page - 1) * per_page,
|
'from': (page - 1) * per_page, 'size': per_page})
|
||||||
size=per_page)
|
|
||||||
ids = [int(hit['_id']) for hit in search['hits']['hits']]
|
ids = [int(hit['_id']) for hit in search['hits']['hits']]
|
||||||
return ids, search['hits']['total']['value']
|
return ids, search['hits']['total']['value']
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% set user_link %}
|
{% set user_link %}
|
||||||
<a class="user_popup" href="{{ url_for('main.user', username=post.author.username) }}">
|
<span class="user_popup">
|
||||||
{{ post.author.username }}
|
<a href="{{ url_for('main.user', username=post.author.username) }}">
|
||||||
</a>
|
{{ post.author.username }}
|
||||||
|
</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()) }}
|
||||||
|
@ -19,8 +21,8 @@
|
||||||
<br><br>
|
<br><br>
|
||||||
<span id="translation{{ post.id }}">
|
<span id="translation{{ post.id }}">
|
||||||
<a href="javascript:translate(
|
<a href="javascript:translate(
|
||||||
'post{{ post.id }}',
|
'#post{{ post.id }}',
|
||||||
'translation{{ post.id }}',
|
'#translation{{ post.id }}',
|
||||||
'{{ post.language }}',
|
'{{ post.language }}',
|
||||||
'{{ g.locale }}');">{{ _('Translate') }}</a>
|
'{{ g.locale }}');">{{ _('Translate') }}</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% import "bootstrap_wtf.html" as wtf %}
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
{% block content %}
|
{% block app_content %}
|
||||||
<h1>{{ _('Sign In') }}</h1>
|
<h1>{{ _('Sign In') }}</h1>
|
||||||
{{ wtf.quick_form(form) }}
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
{{ wtf.quick_form(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
<p>{{ _('New User?') }} <a href="{{ url_for('auth.register') }}">{{ _('Click to Register!') }}</a></p>
|
<p>{{ _('New User?') }} <a href="{{ url_for('auth.register') }}">{{ _('Click to Register!') }}</a></p>
|
||||||
<p>
|
<p>
|
||||||
{{ _('Forgot Your Password?') }}
|
{{ _('Forgot Your Password?') }}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% import "bootstrap_wtf.html" as wtf %}
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
{% block content %}
|
{% block app_content %}
|
||||||
<h1>{{ _('Register') }}</h1>
|
<h1>{{ _('Register') }}</h1>
|
||||||
{{ wtf.quick_form(form) }}
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
{{ wtf.quick_form(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% import "bootstrap_wtf.html" as wtf %}
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
{% block content %}
|
{% block app_content %}
|
||||||
<h1>{{ _('Reset Your Password') }}</h1>
|
<h1>{{ _('Reset Your Password') }}</h1>
|
||||||
{{ wtf.quick_form(form) }}
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
{{ wtf.quick_form(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% import "bootstrap_wtf.html" as wtf %}
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
{% block content %}
|
{% block app_content %}
|
||||||
<h1>{{ _('Reset Password') }}</h1>
|
<h1>{{ _('Reset Password') }}</h1>
|
||||||
{{ wtf.quick_form(form) }}
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
{{ wtf.quick_form(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,173 +1,171 @@
|
||||||
<!doctype html>
|
{% extends 'bootstrap/base.html' %}
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
{% if title %}
|
|
||||||
<title>{{ title }} - Microblog</title>
|
|
||||||
{% else %}
|
|
||||||
<title>{{ _('Welcome to Microblog') }}</title>
|
|
||||||
{% endif %}
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
|
||||||
<div class="container">
|
|
||||||
<a class="navbar-brand" href="{{ url_for('main.index') }}">Microblog</a>
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" aria-current="page" href="{{ url_for('main.index') }}">{{ _('Home') }}</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" aria-current="page" href="{{ url_for('main.explore') }}">{{ _('Explore') }}</a>
|
|
||||||
</li>
|
|
||||||
{% 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>
|
|
||||||
<ul class="navbar-nav mb-2 mb-lg-0">
|
|
||||||
{% if current_user.is_anonymous %}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" aria-current="page" href="{{ url_for('auth.login') }}">{{ _('Login') }}</a>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" aria-current="page" href="{{ url_for('main.messages') }}">{{ _('Messages') }}
|
|
||||||
{% set new_messages = current_user.new_messages() %}
|
|
||||||
<span id="message_count" class="badge text-bg-danger"
|
|
||||||
style="visibility: {% if new_messages %}visible
|
|
||||||
{% else %}hidden{% endif %};">
|
|
||||||
{{ new_messages }}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" aria-current="page" href="{{ url_for('main.user', username=current_user.username) }}">{{ _('Profile') }}</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" aria-current="page" href="{{ url_for('auth.logout') }}">{{ _('Logout') }}</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<div class="container mt-3">
|
|
||||||
{% 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() %}
|
{% block title %}
|
||||||
{% if messages %}
|
{% if title %}{{ title }} - Microblog{% else %}{{ _('Welcome to Microblog') }}{% endif %}
|
||||||
{% for message in messages %}
|
{% endblock %}
|
||||||
<div class="alert alert-info" role="alert">{{ message }}</div>
|
|
||||||
{% endfor %}
|
{% block navbar %}
|
||||||
{% endif %}
|
<nav class="navbar navbar-default">
|
||||||
{% endwith %}
|
<div class="container">
|
||||||
{% block content %}{% endblock %}
|
<div class="navbar-header">
|
||||||
|
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
|
||||||
|
<span class="sr-only">Toggle navigation</span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>
|
||||||
|
<a class="navbar-brand" href="{{ url_for('main.index') }}">Microblog</a>
|
||||||
|
</div>
|
||||||
|
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
|
||||||
|
<ul class="nav navbar-nav">
|
||||||
|
<li><a href="{{ url_for('main.index') }}">{{ _('Home') }}</a></li>
|
||||||
|
<li><a href="{{ url_for('main.explore') }}">{{ _('Explore') }}</a></li>
|
||||||
|
</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">
|
||||||
|
{% if current_user.is_anonymous %}
|
||||||
|
<li><a href="{{ url_for('auth.login') }}">{{ _('Login') }}</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('main.messages') }}">{{ _('Messages') }}
|
||||||
|
{% 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 %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<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() %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-info" role="alert">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{# application content needs to be provided in the app_content block #}
|
||||||
|
{% block app_content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{{ super() }}
|
||||||
{{ moment.include_moment() }}
|
{{ moment.include_moment() }}
|
||||||
{{ moment.lang(g.locale) }}
|
{{ moment.lang(g.locale) }}
|
||||||
<script>
|
<script>
|
||||||
async function translate(sourceElem, destElem, sourceLang, destLang) {
|
function translate(sourceElem, destElem, sourceLang, destLang) {
|
||||||
document.getElementById(destElem).innerHTML =
|
$(destElem).html('<img src="{{ url_for('static', filename='loading.gif') }}">');
|
||||||
'<img src="{{ url_for('static', filename='loading.gif') }}">';
|
$.post('/translate', {
|
||||||
const response = await fetch('/translate', {
|
text: $(sourceElem).text(),
|
||||||
method: 'POST',
|
source_language: sourceLang,
|
||||||
headers: {'Content-Type': 'application/json; charset=utf-8'},
|
dest_language: destLang
|
||||||
body: JSON.stringify({
|
}).done(function(response) {
|
||||||
text: document.getElementById(sourceElem).innerText,
|
$(destElem).text(response['text'])
|
||||||
source_language: sourceLang,
|
}).fail(function() {
|
||||||
dest_language: destLang
|
$(destElem).text("{{ _('Error: Could not contact server.') }}");
|
||||||
})
|
});
|
||||||
})
|
|
||||||
const data = await response.json();
|
|
||||||
document.getElementById(destElem).innerText = data.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initialize_popovers() {
|
|
||||||
const popups = document.getElementsByClassName('user_popup');
|
|
||||||
for (let i = 0; i < popups.length; i++) {
|
|
||||||
const popover = new bootstrap.Popover(popups[i], {
|
|
||||||
content: 'Loading...',
|
|
||||||
trigger: 'hover focus',
|
|
||||||
placement: 'right',
|
|
||||||
html: true,
|
|
||||||
sanitize: false,
|
|
||||||
delay: {show: 500, hide: 0},
|
|
||||||
container: popups[i],
|
|
||||||
customClass: 'd-inline',
|
|
||||||
});
|
|
||||||
popups[i].addEventListener('show.bs.popover', async (ev) => {
|
|
||||||
if (ev.target.popupLoaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const response = await fetch('/user/' + ev.target.innerText.trim() + '/popup');
|
|
||||||
const data = await response.text();
|
|
||||||
const popover = bootstrap.Popover.getInstance(ev.target);
|
|
||||||
if (popover && data) {
|
|
||||||
ev.target.popupLoaded = true;
|
|
||||||
popover.setContent({'.popover-body': data});
|
|
||||||
flask_moment_render_all();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
$(function () {
|
||||||
document.addEventListener('DOMContentLoaded', initialize_popovers);
|
var timer = null;
|
||||||
|
var xhr = null;
|
||||||
function set_message_count(n) {
|
$('.user_popup').hover(
|
||||||
const count = document.getElementById('message_count');
|
function(event) {
|
||||||
count.innerText = n;
|
// mouse in event handler
|
||||||
count.style.visibility = n ? 'visible' : 'hidden';
|
var elem = $(event.currentTarget);
|
||||||
}
|
timer = setTimeout(function() {
|
||||||
|
timer = null;
|
||||||
function set_task_progress(task_id, progress) {
|
xhr = $.ajax(
|
||||||
const progressElement = document.getElementById(task_id + '-progress');
|
'/user/' + elem.first().text().trim() + '/popup').done(
|
||||||
if (progressElement) {
|
function(data) {
|
||||||
progressElement.innerText = progress;
|
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 initialize_notifications() {
|
{% if current_user.is_authenticated %}
|
||||||
let since = 0;
|
$(function() {
|
||||||
setInterval(async function() {
|
var since = 0;
|
||||||
const response = await fetch('{{ url_for('main.notifications') }}?since=' + since);
|
setInterval(function() {
|
||||||
const notifications = await response.json();
|
$.ajax('{{ url_for('main.notifications') }}?since=' + since).done(
|
||||||
for (let i = 0; i < notifications.length; i++) {
|
function(notifications) {
|
||||||
switch (notifications[i].name) {
|
for (var i = 0; i < notifications.length; i++) {
|
||||||
case 'unread_message_count':
|
switch (notifications[i].name) {
|
||||||
set_message_count(notifications[i].data);
|
case 'unread_message_count':
|
||||||
break;
|
set_message_count(notifications[i].data);
|
||||||
case 'task_progress':
|
break;
|
||||||
set_task_progress(notifications[i].data.task_id,
|
case 'task_progress':
|
||||||
notifications[i].data.progress);
|
set_task_progress(notifications[i].data.task_id,
|
||||||
break;
|
notifications[i].data.progress);
|
||||||
}
|
break;
|
||||||
since = notifications[i].timestamp;
|
}
|
||||||
}
|
since = notifications[i].timestamp;
|
||||||
}, 10000);
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener('DOMContentLoaded', initialize_notifications);
|
);
|
||||||
{% endif %}
|
}, 10000);
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
{% endblock %}
|
||||||
</html>
|
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
{% macro form_field(field, autofocus) %}
|
|
||||||
{%- if field.type == 'BooleanField' %}
|
|
||||||
<div class="form-check mb-3">
|
|
||||||
{{ field(class='form-check-input') }}
|
|
||||||
{{ field.label(class='form-check-label') }}
|
|
||||||
</div>
|
|
||||||
{%- elif field.type == 'RadioField' %}
|
|
||||||
{{ field.label(class='form-label') }}
|
|
||||||
{%- for item in field %}
|
|
||||||
<div class="form-check{% if loop.last %} mb-3{% endif %}">
|
|
||||||
{{ item(class='form-check-input') }}
|
|
||||||
{{ item.label(class='form-check-label') }}
|
|
||||||
</div>
|
|
||||||
{%- endfor %}
|
|
||||||
{%- elif field.type == 'SelectField' %}
|
|
||||||
{{ field.label(class='form-label') }}
|
|
||||||
{{ field(class='form-select mb-3') }}
|
|
||||||
{%- elif field.type == 'TextAreaField' %}
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ field.label(class='form-label') }}
|
|
||||||
{% if autofocus %}
|
|
||||||
{{ field(class='form-control' + (' is-invalid' if field.errors else ''), autofocus=True) }}
|
|
||||||
{% else %}
|
|
||||||
{{ field(class='form-control' + (' is-invalid' if field.errors else '')) }}
|
|
||||||
{% endif %}
|
|
||||||
{%- for error in field.errors %}
|
|
||||||
<div class="invalid-feedback">{{ error }}</div>
|
|
||||||
{%- endfor %}
|
|
||||||
</div>
|
|
||||||
{%- elif field.type == 'SubmitField' %}
|
|
||||||
{{ field(class='btn btn-primary mb-3') }}
|
|
||||||
{%- else %}
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ field.label(class='form-label') }}
|
|
||||||
{% if autofocus %}
|
|
||||||
{{ field(class='form-control' + (' is-invalid' if field.errors else ''), autofocus=True) }}
|
|
||||||
{% else %}
|
|
||||||
{{ field(class='form-control' + (' is-invalid' if field.errors else '')) }}
|
|
||||||
{% endif %}
|
|
||||||
{%- for error in field.errors %}
|
|
||||||
<div class="invalid-feedback">{{ error }}</div>
|
|
||||||
{%- endfor %}
|
|
||||||
</div>
|
|
||||||
{%- endif %}
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro quick_form(form, action="", method="post", id="", novalidate=False) %}
|
|
||||||
<form novalidate
|
|
||||||
{%- if action != None %} action="{{ action }}"{% endif -%}
|
|
||||||
{%- if method %} method="{{ method }}"{% endif %}
|
|
||||||
{%- if id %} id="{{ id }}"{% endif -%}
|
|
||||||
{%- if novalidate %} novalidate{% endif -%}>
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
{%- for field, errors in form.errors.items() %}
|
|
||||||
{%- if form[field].widget.input_type == 'hidden' %}
|
|
||||||
{%- for error in errors %}
|
|
||||||
<div class="invalid-feedback">{{ error }}</div>
|
|
||||||
{%- endfor %}
|
|
||||||
{%- endif %}
|
|
||||||
{%- endfor %}
|
|
||||||
|
|
||||||
{% set ns = namespace(first_field=true) %}
|
|
||||||
{%- for field in form %}
|
|
||||||
{% if field.widget.input_type != 'hidden' -%}
|
|
||||||
{{ form_field(field, ns.first_field) }}
|
|
||||||
{% set ns.first_field = false %}
|
|
||||||
{%- endif %}
|
|
||||||
{%- endfor %}
|
|
||||||
</form>
|
|
||||||
{% endmacro %}
|
|
|
@ -1,7 +1,11 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% import "bootstrap_wtf.html" as wtf %}
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
{% block content %}
|
{% block app_content %}
|
||||||
<h1>{{ _('Edit Profile') }}</h1>
|
<h1>{{ _('Edit Profile') }}</h1>
|
||||||
{{ wtf.quick_form(form) }}
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
{{ wtf.quick_form(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
<!doctype html>
|
<p>Dear {{ user.username }},</p>
|
||||||
<html>
|
<p>
|
||||||
<body>
|
To reset your password
|
||||||
<p>Dear {{ user.username }},</p>
|
<a href="{{ url_for('auth.reset_password', token=token, _external=True) }}">
|
||||||
<p>
|
click here
|
||||||
To reset your password
|
</a>.
|
||||||
<a href="{{ url_for('auth.reset_password', token=token, _external=True) }}">
|
</p>
|
||||||
click here
|
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
|
||||||
</a>.
|
<p>{{ url_for('auth.reset_password', token=token, _external=True) }}</p>
|
||||||
</p>
|
<p>If you have not requested a password reset simply ignore this message.</p>
|
||||||
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
|
<p>Sincerely,</p>
|
||||||
<p>{{ url_for('auth.reset_password', token=token, _external=True) }}</p>
|
<p>The Microblog Team</p>
|
||||||
<p>If you have not requested a password reset simply ignore this message.</p>
|
|
||||||
<p>Sincerely,</p>
|
|
||||||
<p>The Microblog Team</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block 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('main.index') }}">{{ _('Back') }}</a></p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block 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('main.index') }}">{{ _('Back') }}</a></p>
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% import "bootstrap_wtf.html" as wtf %}
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
{% block content %}
|
{% block app_content %}
|
||||||
<h1>{{ _('Hi, %(username)s!', username=current_user.username) }}</h1>
|
<h1>{{ _('Hi, %(username)s!', username=current_user.username) }}</h1>
|
||||||
{% if form %}
|
{% if form %}
|
||||||
{{ wtf.quick_form(form) }}
|
{{ wtf.quick_form(form) }}
|
||||||
|
<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
{% include '_post.html' %}
|
{% include '_post.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<nav aria-label="Post navigation">
|
<nav aria-label="...">
|
||||||
<ul class="pagination">
|
<ul class="pager">
|
||||||
<li class="page-item{% if not prev_url %} disabled{% endif %}">
|
<li class="previous{% if not prev_url %} disabled{% endif %}">
|
||||||
<a class="page-link" href="#">
|
<a href="{{ prev_url or '#' }}">
|
||||||
<span aria-hidden="true">←</span> {{ _('Newer posts') }}
|
<span aria-hidden="true">←</span> {{ _('Newer posts') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item{% if not next_url %} disabled{% endif %}">
|
<li class="next{% if not next_url %} disabled{% endif %}">
|
||||||
<a class="page-link" href="#">
|
<a href="{{ next_url or '#' }}">
|
||||||
{{ _('Older posts') }} <span aria-hidden="true">→</span>
|
{{ _('Older posts') }} <span aria-hidden="true">→</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block app_content %}
|
||||||
<h1>{{ _('Messages') }}</h1>
|
<h1>{{ _('Messages') }}</h1>
|
||||||
{% for post in messages %}
|
{% for post in messages %}
|
||||||
{% include '_post.html' %}
|
{% include '_post.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<nav aria-label="Post navigation">
|
<nav aria-label="...">
|
||||||
<ul class="pagination">
|
<ul class="pager">
|
||||||
<li class="page-item{% if not prev_url %} disabled{% endif %}">
|
<li class="previous{% if not prev_url %} disabled{% endif %}">
|
||||||
<a class="page-link" href="#">
|
<a href="{{ prev_url or '#' }}">
|
||||||
<span aria-hidden="true">←</span> {{ _('Newer messages') }}
|
<span aria-hidden="true">←</span> {{ _('Newer messages') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item{% if not next_url %} disabled{% endif %}">
|
<li class="next{% if not next_url %} disabled{% endif %}">
|
||||||
<a class="page-link" href="#">
|
<a href="{{ next_url or '#' }}">
|
||||||
{{ _('Older messages') }} <span aria-hidden="true">→</span>
|
{{ _('Older messages') }} <span aria-hidden="true">→</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -1,20 +1,20 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block app_content %}
|
||||||
<h1>{{ _('Search Results') }}</h1>
|
<h1>{{ _('Search Results') }}</h1>
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
{% include '_post.html' %}
|
{% include '_post.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<nav aria-label="Post navigation">
|
<nav aria-label="...">
|
||||||
<ul class="pagination">
|
<ul class="pager">
|
||||||
<li class="page-item{% if not prev_url %} disabled{% endif %}">
|
<li class="previous{% if not prev_url %} disabled{% endif %}">
|
||||||
<a class="page-link" href="#">
|
<a href="{{ prev_url or '#' }}">
|
||||||
<span aria-hidden="true">←</span> {{ _('Newer posts') }}
|
<span aria-hidden="true">←</span> {{ _('Previous results') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item{% if not next_url %} disabled{% endif %}">
|
<li class="next{% if not next_url %} disabled{% endif %}">
|
||||||
<a class="page-link" href="#">
|
<a href="{{ next_url or '#' }}">
|
||||||
{{ _('Older posts') }} <span aria-hidden="true">→</span>
|
{{ _('Next results') }} <span aria-hidden="true">→</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% import "bootstrap_wtf.html" as wtf %}
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
{% block content %}
|
{% block app_content %}
|
||||||
<h1>{{ _('Send Message to %(recipient)s', recipient=recipient) }}</h1>
|
<h1>{{ _('Send Message to %(recipient)s', recipient=recipient) }}</h1>
|
||||||
{{ wtf.quick_form(form) }}
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
{{ wtf.quick_form(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block app_content %}
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<tr>
|
<tr>
|
||||||
<td width="256px"><img src="{{ user.avatar(256) }}"></td>
|
<td width="256px"><img src="{{ user.avatar(256) }}"></td>
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
{% if user.last_seen %}
|
{% if user.last_seen %}
|
||||||
<p>{{ _('Last seen on') }}: {{ moment(user.last_seen).format('LLL') }}</p>
|
<p>{{ _('Last seen on') }}: {{ moment(user.last_seen).format('LLL') }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>{{ _('%(count)d followers', count=user.followers.count()) }}, {{ _('%(count)d following', count=user.following.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') %}
|
{% if not current_user.get_task_in_progress('export_posts') %}
|
||||||
|
@ -20,14 +20,14 @@
|
||||||
<p>
|
<p>
|
||||||
<form action="{{ url_for('main.follow', username=user.username) }}" method="post">
|
<form action="{{ url_for('main.follow', username=user.username) }}" method="post">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{{ form.submit(value=_('Follow'), class_='btn btn-primary') }}
|
{{ 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('main.unfollow', username=user.username) }}" method="post">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{{ form.submit(value=_('Unfollow'), class_='btn btn-primary') }}
|
{{ form.submit(value=_('Unfollow'), class_='btn btn-default') }}
|
||||||
</form>
|
</form>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -40,15 +40,15 @@
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
{% include '_post.html' %}
|
{% include '_post.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<nav aria-label="Post navigation">
|
<nav aria-label="...">
|
||||||
<ul class="pagination">
|
<ul class="pager">
|
||||||
<li class="page-item{% if not prev_url %} disabled{% endif %}">
|
<li class="previous{% if not prev_url %} disabled{% endif %}">
|
||||||
<a class="page-link" href="#">
|
<a href="{{ prev_url or '#' }}">
|
||||||
<span aria-hidden="true">←</span> {{ _('Newer posts') }}
|
<span aria-hidden="true">←</span> {{ _('Newer posts') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item{% if not next_url %} disabled{% endif %}">
|
<li class="next{% if not next_url %} disabled{% endif %}">
|
||||||
<a class="page-link" href="#">
|
<a href="{{ next_url or '#' }}">
|
||||||
{{ _('Older posts') }} <span aria-hidden="true">→</span>
|
{{ _('Older posts') }} <span aria-hidden="true">→</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,27 +1,32 @@
|
||||||
<div>
|
<table class="table">
|
||||||
<img src="{{ user.avatar(64) }}" style="margin: 5px; float: left">
|
<tr>
|
||||||
<p><a href="{{ url_for('main.user', username=user.username) }}">{{ user.username }}</a></p>
|
<td width="64" style="border: 0px;"><img src="{{ user.avatar(64) }}"></td>
|
||||||
{% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
|
<td style="border: 0px;">
|
||||||
<div class="clearfix"></div>
|
<p><a href="{{ url_for('main.user', username=user.username) }}">{{ user.username }}</a></p>
|
||||||
{% if user.last_seen %}
|
<small>
|
||||||
<p>{{ _('Last seen on') }}: {{ moment(user.last_seen).format('lll') }}</p>
|
{% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
|
||||||
{% endif %}
|
{% if user.last_seen %}
|
||||||
<p>{{ _('%(count)d followers', count=user.followers.count()) }}, {{ _('%(count)d following', count=user.following.count()) }}</p>
|
<p>{{ _('Last seen on') }}: {{ moment(user.last_seen).format('lll') }}</p>
|
||||||
{% if user != current_user %}
|
{% endif %}
|
||||||
{% if not current_user.is_following(user) %}
|
<p>{{ _('%(count)d followers', count=user.followers.count()) }}, {{ _('%(count)d following', count=user.followed.count()) }}</p>
|
||||||
<p>
|
{% if user != current_user %}
|
||||||
<form action="{{ url_for('main.follow', username=user.username) }}" method="post">
|
{% if not current_user.is_following(user) %}
|
||||||
{{ form.hidden_tag() }}
|
<p>
|
||||||
{{ form.submit(value=_('Follow'), class_='btn btn-outline-primary btn-sm') }}
|
<form action="{{ url_for('main.follow', username=user.username) }}" method="post">
|
||||||
</form>
|
{{ form.hidden_tag() }}
|
||||||
</p>
|
{{ form.submit(value=_('Follow'), class_='btn btn-default btn-sm') }}
|
||||||
{% else %}
|
</form>
|
||||||
<p>
|
</p>
|
||||||
<form action="{{ url_for('main.unfollow', username=user.username) }}" method="post">
|
{% else %}
|
||||||
{{ form.hidden_tag() }}
|
<p>
|
||||||
{{ form.submit(value=_('Unfollow'), class_='btn btn-outline-primary btn-sm') }}
|
<form action="{{ url_for('main.unfollow', username=user.username) }}" method="post">
|
||||||
</form>
|
{{ form.hidden_tag() }}
|
||||||
</p>
|
{{ form.submit(value=_('Unfollow'), class_='btn btn-default btm-sm') }}
|
||||||
{% endif %}
|
</form>
|
||||||
{% endif %}
|
</p>
|
||||||
</div>
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
|
@ -10,8 +10,7 @@ def translate(text, source_language, dest_language):
|
||||||
return _('Error: the translation service is not configured.')
|
return _('Error: the translation service is not configured.')
|
||||||
auth = {
|
auth = {
|
||||||
'Ocp-Apim-Subscription-Key': current_app.config['MS_TRANSLATOR_KEY'],
|
'Ocp-Apim-Subscription-Key': current_app.config['MS_TRANSLATOR_KEY'],
|
||||||
'Ocp-Apim-Subscription-Region': 'westus'
|
'Ocp-Apim-Subscription-Region': 'westus2'}
|
||||||
}
|
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
'https://api.cognitive.microsofttranslator.com'
|
'https://api.cognitive.microsofttranslator.com'
|
||||||
'/translate?api-version=3.0&from={}&to={}'.format(
|
'/translate?api-version=3.0&from={}&to={}'.format(
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
[python: app/**.py]
|
[python: app/**.py]
|
||||||
[jinja2: app/templates/**.html]
|
[jinja2: app/templates/**.html]
|
||||||
|
extensions=jinja2.ext.autoescape,jinja2.ext.with_
|
||||||
|
|
1
boot.sh
1
boot.sh
|
@ -9,4 +9,5 @@ while true; do
|
||||||
echo Deploy command failed, retrying in 5 secs...
|
echo Deploy command failed, retrying in 5 secs...
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
||||||
|
flask translate compile
|
||||||
exec gunicorn -b :5000 --access-logfile - --error-logfile - microblog:app
|
exec gunicorn -b :5000 --access-logfile - --error-logfile - microblog:app
|
||||||
|
|
|
@ -10,6 +10,7 @@ class Config(object):
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', '').replace(
|
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', '').replace(
|
||||||
'postgres://', 'postgresql://') or \
|
'postgres://', 'postgresql://') or \
|
||||||
'sqlite:///' + os.path.join(basedir, 'app.db')
|
'sqlite:///' + os.path.join(basedir, 'app.db')
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
LOG_TO_STDOUT = os.environ.get('LOG_TO_STDOUT')
|
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)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from app import create_app, db
|
from app import create_app, db, cli
|
||||||
from app.models import User, Post, Message, Notification, Task
|
from app.models import User, Post, Message, Notification, Task
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
cli.register(app)
|
||||||
|
|
||||||
|
|
||||||
@app.shell_context_processor
|
@app.shell_context_processor
|
||||||
|
|
|
@ -18,15 +18,11 @@ depends_on = None
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
op.add_column('post', sa.Column('language', sa.String(length=5), nullable=True))
|
||||||
batch_op.add_column(sa.Column('language', sa.String(length=5), nullable=True))
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
op.drop_column('post', 'language')
|
||||||
batch_op.drop_column('language')
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
|
@ -18,17 +18,13 @@ depends_on = None
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
op.add_column('user', sa.Column('about_me', sa.String(length=140), nullable=True))
|
||||||
batch_op.add_column(sa.Column('about_me', sa.String(length=140), nullable=True))
|
op.add_column('user', sa.Column('last_seen', sa.DateTime(), nullable=True))
|
||||||
batch_op.add_column(sa.Column('last_seen', sa.DateTime(), nullable=True))
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
op.drop_column('user', 'last_seen')
|
||||||
batch_op.drop_column('last_seen')
|
op.drop_column('user', 'about_me')
|
||||||
batch_op.drop_column('about_me')
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
|
@ -26,18 +26,12 @@ def upgrade():
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id')
|
||||||
)
|
)
|
||||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
op.create_index(op.f('ix_post_timestamp'), 'post', ['timestamp'], unique=False)
|
||||||
batch_op.create_index(batch_op.f('ix_post_timestamp'), ['timestamp'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_post_user_id'), ['user_id'], unique=False)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
op.drop_index(op.f('ix_post_timestamp'), table_name='post')
|
||||||
batch_op.drop_index(batch_op.f('ix_post_user_id'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_post_timestamp'))
|
|
||||||
|
|
||||||
op.drop_table('post')
|
op.drop_table('post')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
|
@ -19,11 +19,10 @@ depends_on = None
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('followers',
|
op.create_table('followers',
|
||||||
sa.Column('follower_id', sa.Integer(), nullable=False),
|
sa.Column('follower_id', sa.Integer(), nullable=True),
|
||||||
sa.Column('followed_id', sa.Integer(), nullable=False),
|
sa.Column('followed_id', sa.Integer(), nullable=True),
|
||||||
sa.ForeignKeyConstraint(['followed_id'], ['user.id'], ),
|
sa.ForeignKeyConstraint(['followed_id'], ['user.id'], ),
|
||||||
sa.ForeignKeyConstraint(['follower_id'], ['user.id'], ),
|
sa.ForeignKeyConstraint(['follower_id'], ['user.id'], )
|
||||||
sa.PrimaryKeyConstraint('follower_id', 'followed_id')
|
|
||||||
)
|
)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
|
@ -28,26 +28,14 @@ def upgrade():
|
||||||
sa.ForeignKeyConstraint(['sender_id'], ['user.id'], ),
|
sa.ForeignKeyConstraint(['sender_id'], ['user.id'], ),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id')
|
||||||
)
|
)
|
||||||
|
op.create_index(op.f('ix_message_timestamp'), 'message', ['timestamp'], unique=False)
|
||||||
with op.batch_alter_table('message', schema=None) as batch_op:
|
op.add_column('user', sa.Column('last_message_read_time', sa.DateTime(), nullable=True))
|
||||||
batch_op.create_index(batch_op.f('ix_message_recipient_id'), ['recipient_id'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_message_sender_id'), ['sender_id'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_message_timestamp'), ['timestamp'], unique=False)
|
|
||||||
|
|
||||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('last_message_read_time', sa.DateTime(), nullable=True))
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
op.drop_column('user', 'last_message_read_time')
|
||||||
batch_op.drop_column('last_message_read_time')
|
op.drop_index(op.f('ix_message_timestamp'), table_name='message')
|
||||||
|
|
||||||
with op.batch_alter_table('notification', schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f('ix_notification_timestamp'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_message_sender_id'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_message_recipient_id'))
|
|
||||||
|
|
||||||
op.drop_table('message')
|
op.drop_table('message')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
|
@ -25,18 +25,14 @@ def upgrade():
|
||||||
sa.Column('password_hash', sa.String(length=128), nullable=True),
|
sa.Column('password_hash', sa.String(length=128), nullable=True),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id')
|
||||||
)
|
)
|
||||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
|
||||||
batch_op.create_index(batch_op.f('ix_user_email'), ['email'], unique=True)
|
op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True)
|
||||||
batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
op.drop_index(op.f('ix_user_username'), table_name='user')
|
||||||
batch_op.drop_index(batch_op.f('ix_user_username'))
|
op.drop_index(op.f('ix_user_email'), table_name='user')
|
||||||
batch_op.drop_index(batch_op.f('ix_user_email'))
|
|
||||||
|
|
||||||
op.drop_table('user')
|
op.drop_table('user')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
|
@ -27,20 +27,14 @@ def upgrade():
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id')
|
||||||
)
|
)
|
||||||
|
op.create_index(op.f('ix_notification_name'), 'notification', ['name'], unique=False)
|
||||||
with op.batch_alter_table('notification', schema=None) as batch_op:
|
op.create_index(op.f('ix_notification_timestamp'), 'notification', ['timestamp'], unique=False)
|
||||||
batch_op.create_index(batch_op.f('ix_notification_name'), ['name'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_notification_timestamp'), ['timestamp'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_notification_user_id'), ['user_id'], unique=False)
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
with op.batch_alter_table('notification', schema=None) as batch_op:
|
op.drop_index(op.f('ix_notification_timestamp'), table_name='notification')
|
||||||
batch_op.drop_index(batch_op.f('ix_notification_user_id'))
|
op.drop_index(op.f('ix_notification_name'), table_name='notification')
|
||||||
batch_op.drop_index(batch_op.f('ix_notification_timestamp'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_notification_name'))
|
|
||||||
|
|
||||||
op.drop_table('notification')
|
op.drop_table('notification')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
|
@ -1,56 +1,49 @@
|
||||||
aiosmtpd==1.4.4.post2
|
alembic==1.6.5
|
||||||
alembic==1.10.4
|
Babel==2.9.1
|
||||||
async-timeout==4.0.2
|
blinker==1.4
|
||||||
atpublic==3.1.1
|
certifi==2021.5.30
|
||||||
attrs==23.1.0
|
chardet==4.0.0
|
||||||
Babel==2.12.1
|
click==8.0.1
|
||||||
blinker==1.6.2
|
dnspython==2.1.0
|
||||||
certifi==2022.12.7
|
dominate==2.6.0
|
||||||
charset-normalizer==3.1.0
|
elasticsearch==7.13.3
|
||||||
click==8.1.3
|
email-validator==1.1.3
|
||||||
defusedxml==0.7.1
|
Flask==2.0.1
|
||||||
dnspython==2.3.0
|
Flask-Babel==2.0.0
|
||||||
elastic-transport==8.4.0
|
Flask-Bootstrap==3.3.7.1
|
||||||
elasticsearch==8.7.0
|
Flask-HTTPAuth==4.4.0
|
||||||
email-validator==2.0.0.post2
|
Flask-Login==0.5.0
|
||||||
Flask==2.3.2
|
|
||||||
flask-babel==3.1.0
|
|
||||||
Flask-HTTPAuth==4.8.0
|
|
||||||
Flask-Login==0.6.2
|
|
||||||
Flask-Mail==0.9.1
|
Flask-Mail==0.9.1
|
||||||
Flask-Migrate==4.0.4
|
Flask-Migrate==3.0.1
|
||||||
Flask-Moment==1.0.5
|
Flask-Moment==1.0.1
|
||||||
Flask-SQLAlchemy==3.0.3
|
Flask-SQLAlchemy==2.5.1
|
||||||
Flask-WTF==1.1.1
|
Flask-WTF==0.15.1
|
||||||
greenlet==2.0.2
|
greenlet==1.1.0
|
||||||
httpie==3.2.1
|
httpie==2.4.0
|
||||||
idna==3.4
|
idna==2.10
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.0.1
|
||||||
Jinja2==3.1.2
|
Jinja2==3.0.1
|
||||||
langdetect==1.0.9
|
langdetect==1.0.9
|
||||||
Mako==1.2.4
|
Mako==1.1.4
|
||||||
markdown-it-py==2.2.0
|
MarkupSafe==2.0.1
|
||||||
MarkupSafe==2.1.2
|
Pygments==2.9.0
|
||||||
mdurl==0.1.2
|
PyJWT==2.1.0
|
||||||
multidict==6.0.4
|
|
||||||
packaging==23.1
|
|
||||||
Pygments==2.15.1
|
|
||||||
PyJWT==2.6.0
|
|
||||||
PySocks==1.7.1
|
PySocks==1.7.1
|
||||||
python-dotenv==1.0.0
|
python-dateutil==2.8.1
|
||||||
pytz==2023.3
|
python-dotenv==0.18.0
|
||||||
redis==4.5.4
|
python-editor==1.0.4
|
||||||
requests==2.29.0
|
pytz==2021.1
|
||||||
requests-toolbelt==1.0.0
|
redis==3.5.3
|
||||||
rich==13.3.5
|
requests==2.25.1
|
||||||
rq==1.14.0
|
requests-toolbelt==0.9.1
|
||||||
|
rq==1.9.0
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
SQLAlchemy==2.0.10
|
SQLAlchemy==1.4.20
|
||||||
typing_extensions==4.5.0
|
urllib3==1.26.6
|
||||||
urllib3==1.26.15
|
visitor==0.1.3
|
||||||
Werkzeug==2.3.3
|
Werkzeug==2.0.1
|
||||||
WTForms==3.0.1
|
WTForms==2.3.3
|
||||||
|
|
||||||
# requirements for Heroku
|
# requirements for Heroku
|
||||||
#psycopg2-binary==2.9.6
|
#psycopg2==2.9.1
|
||||||
#gunicorn==20.1.0
|
#gunicorn==20.1.0
|
||||||
|
|
18
tests.py
18
tests.py
|
@ -42,21 +42,21 @@ class UserModelCase(unittest.TestCase):
|
||||||
db.session.add(u1)
|
db.session.add(u1)
|
||||||
db.session.add(u2)
|
db.session.add(u2)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
self.assertEqual(u1.following.all(), [])
|
self.assertEqual(u1.followed.all(), [])
|
||||||
self.assertEqual(u1.followers.all(), [])
|
self.assertEqual(u1.followers.all(), [])
|
||||||
|
|
||||||
u1.follow(u2)
|
u1.follow(u2)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
self.assertTrue(u1.is_following(u2))
|
self.assertTrue(u1.is_following(u2))
|
||||||
self.assertEqual(u1.following.count(), 1)
|
self.assertEqual(u1.followed.count(), 1)
|
||||||
self.assertEqual(u1.following.first().username, 'susan')
|
self.assertEqual(u1.followed.first().username, 'susan')
|
||||||
self.assertEqual(u2.followers.count(), 1)
|
self.assertEqual(u2.followers.count(), 1)
|
||||||
self.assertEqual(u2.followers.first().username, 'john')
|
self.assertEqual(u2.followers.first().username, 'john')
|
||||||
|
|
||||||
u1.unfollow(u2)
|
u1.unfollow(u2)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
self.assertFalse(u1.is_following(u2))
|
self.assertFalse(u1.is_following(u2))
|
||||||
self.assertEqual(u1.following.count(), 0)
|
self.assertEqual(u1.followed.count(), 0)
|
||||||
self.assertEqual(u2.followers.count(), 0)
|
self.assertEqual(u2.followers.count(), 0)
|
||||||
|
|
||||||
def test_follow_posts(self):
|
def test_follow_posts(self):
|
||||||
|
@ -87,11 +87,11 @@ class UserModelCase(unittest.TestCase):
|
||||||
u3.follow(u4) # mary follows david
|
u3.follow(u4) # mary follows david
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# check the following posts of each user
|
# check the followed posts of each user
|
||||||
f1 = u1.following_posts().all()
|
f1 = u1.followed_posts().all()
|
||||||
f2 = u2.following_posts().all()
|
f2 = u2.followed_posts().all()
|
||||||
f3 = u3.following_posts().all()
|
f3 = u3.followed_posts().all()
|
||||||
f4 = u4.following_posts().all()
|
f4 = u4.followed_posts().all()
|
||||||
self.assertEqual(f1, [p2, p4, p1])
|
self.assertEqual(f1, [p2, p4, p1])
|
||||||
self.assertEqual(f2, [p2, p3])
|
self.assertEqual(f2, [p2, p3])
|
||||||
self.assertEqual(f3, [p3, p4])
|
self.assertEqual(f3, [p3, p4])
|
||||||
|
|
Loading…
Reference in New Issue