Spaces:
Runtime error
Runtime error
Upload 81 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .flaskenv +2 -0
- __pycache__/config.cpython-311.pyc +0 -0
- __pycache__/config.cpython-312.pyc +0 -0
- __pycache__/main.cpython-311.pyc +0 -0
- app.db +0 -0
- app/__init__.py +87 -0
- app/__pycache__/__init__.cpython-311.pyc +0 -0
- app/__pycache__/__init__.cpython-312.pyc +0 -0
- app/__pycache__/cli.cpython-311.pyc +0 -0
- app/__pycache__/cli.cpython-312.pyc +0 -0
- app/__pycache__/email.cpython-311.pyc +0 -0
- app/__pycache__/email.cpython-312.pyc +0 -0
- app/__pycache__/models.cpython-311.pyc +0 -0
- app/__pycache__/models.cpython-312.pyc +0 -0
- app/__pycache__/search.cpython-311.pyc +0 -0
- app/__pycache__/search.cpython-312.pyc +0 -0
- app/__pycache__/translate.cpython-311.pyc +0 -0
- app/__pycache__/translate.cpython-312.pyc +0 -0
- app/auth/__init__.py +5 -0
- app/auth/__pycache__/__init__.cpython-311.pyc +0 -0
- app/auth/__pycache__/__init__.cpython-312.pyc +0 -0
- app/auth/__pycache__/email.cpython-311.pyc +0 -0
- app/auth/__pycache__/email.cpython-312.pyc +0 -0
- app/auth/__pycache__/forms.cpython-311.pyc +0 -0
- app/auth/__pycache__/forms.cpython-312.pyc +0 -0
- app/auth/__pycache__/routes.cpython-311.pyc +0 -0
- app/auth/__pycache__/routes.cpython-312.pyc +0 -0
- app/auth/email.py +14 -0
- app/auth/forms.py +49 -0
- app/auth/routes.py +85 -0
- app/cli.py +40 -0
- app/email.py +17 -0
- app/errors/__init__.py +5 -0
- app/errors/__pycache__/__init__.cpython-311.pyc +0 -0
- app/errors/__pycache__/__init__.cpython-312.pyc +0 -0
- app/errors/__pycache__/handlers.cpython-311.pyc +0 -0
- app/errors/__pycache__/handlers.cpython-312.pyc +0 -0
- app/errors/handlers.py +14 -0
- app/main/__init__.py +5 -0
- app/main/__pycache__/__init__.cpython-311.pyc +0 -0
- app/main/__pycache__/__init__.cpython-312.pyc +0 -0
- app/main/__pycache__/forms.cpython-311.pyc +0 -0
- app/main/__pycache__/forms.cpython-312.pyc +0 -0
- app/main/__pycache__/routes.cpython-311.pyc +0 -0
- app/main/__pycache__/routes.cpython-312.pyc +0 -0
- app/main/forms.py +47 -0
- app/main/routes.py +169 -0
- app/models.py +174 -0
- app/search.py +28 -0
- app/static/loading.gif +0 -0
.flaskenv
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
FLASK_APP=main.py
|
2 |
+
FLASK_DEBUG=1
|
__pycache__/config.cpython-311.pyc
ADDED
Binary file (1.97 kB). View file
|
|
__pycache__/config.cpython-312.pyc
ADDED
Binary file (1.91 kB). View file
|
|
__pycache__/main.cpython-311.pyc
ADDED
Binary file (907 Bytes). View file
|
|
app.db
ADDED
Binary file (20.5 kB). View file
|
|
app/__init__.py
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
from logging.handlers import SMTPHandler, RotatingFileHandler
|
3 |
+
import os
|
4 |
+
from flask import Flask, request, current_app
|
5 |
+
from flask_sqlalchemy import SQLAlchemy
|
6 |
+
from flask_migrate import Migrate
|
7 |
+
from flask_login import LoginManager
|
8 |
+
from flask_mail import Mail
|
9 |
+
from flask_moment import Moment
|
10 |
+
from flask_babel import Babel, lazy_gettext as _l
|
11 |
+
from elasticsearch import Elasticsearch
|
12 |
+
from config import Config
|
13 |
+
|
14 |
+
|
15 |
+
def get_locale():
|
16 |
+
return request.accept_languages.best_match(current_app.config['LANGUAGES'])
|
17 |
+
|
18 |
+
|
19 |
+
db = SQLAlchemy()
|
20 |
+
migrate = Migrate()
|
21 |
+
login = LoginManager()
|
22 |
+
login.login_view = 'auth.login'
|
23 |
+
login.login_message = _l('Please log in to access this page.')
|
24 |
+
mail = Mail()
|
25 |
+
moment = Moment()
|
26 |
+
babel = Babel()
|
27 |
+
|
28 |
+
|
29 |
+
def create_app(config_class=Config):
|
30 |
+
app = Flask(__name__)
|
31 |
+
app.config.from_object(config_class)
|
32 |
+
|
33 |
+
db.init_app(app)
|
34 |
+
migrate.init_app(app, db)
|
35 |
+
login.init_app(app)
|
36 |
+
mail.init_app(app)
|
37 |
+
moment.init_app(app)
|
38 |
+
babel.init_app(app, locale_selector=get_locale)
|
39 |
+
app.elasticsearch = Elasticsearch([app.config['ELASTICSEARCH_URL']]) \
|
40 |
+
if app.config['ELASTICSEARCH_URL'] else None
|
41 |
+
|
42 |
+
from app.errors import bp as errors_bp
|
43 |
+
app.register_blueprint(errors_bp)
|
44 |
+
|
45 |
+
from app.auth import bp as auth_bp
|
46 |
+
app.register_blueprint(auth_bp, url_prefix='/auth')
|
47 |
+
|
48 |
+
from app.main import bp as main_bp
|
49 |
+
app.register_blueprint(main_bp)
|
50 |
+
|
51 |
+
from app.cli import bp as cli_bp
|
52 |
+
app.register_blueprint(cli_bp)
|
53 |
+
|
54 |
+
if not app.debug and not app.testing:
|
55 |
+
if app.config['MAIL_SERVER']:
|
56 |
+
auth = None
|
57 |
+
if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
|
58 |
+
auth = (app.config['MAIL_USERNAME'],
|
59 |
+
app.config['MAIL_PASSWORD'])
|
60 |
+
secure = None
|
61 |
+
if app.config['MAIL_USE_TLS']:
|
62 |
+
secure = ()
|
63 |
+
mail_handler = SMTPHandler(
|
64 |
+
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
|
65 |
+
fromaddr='no-reply@' + app.config['MAIL_SERVER'],
|
66 |
+
toaddrs=app.config['ADMINS'], subject='Microblog Failure',
|
67 |
+
credentials=auth, secure=secure)
|
68 |
+
mail_handler.setLevel(logging.ERROR)
|
69 |
+
app.logger.addHandler(mail_handler)
|
70 |
+
|
71 |
+
if not os.path.exists('logs'):
|
72 |
+
os.mkdir('logs')
|
73 |
+
file_handler = RotatingFileHandler('logs/microblog.log',
|
74 |
+
maxBytes=10240, backupCount=10)
|
75 |
+
file_handler.setFormatter(logging.Formatter(
|
76 |
+
'%(asctime)s %(levelname)s: %(message)s '
|
77 |
+
'[in %(pathname)s:%(lineno)d]'))
|
78 |
+
file_handler.setLevel(logging.INFO)
|
79 |
+
app.logger.addHandler(file_handler)
|
80 |
+
|
81 |
+
app.logger.setLevel(logging.INFO)
|
82 |
+
app.logger.info('Microblog startup')
|
83 |
+
|
84 |
+
return app
|
85 |
+
|
86 |
+
|
87 |
+
from app import models
|
app/__pycache__/__init__.cpython-311.pyc
ADDED
Binary file (5.06 kB). View file
|
|
app/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (4.53 kB). View file
|
|
app/__pycache__/cli.cpython-311.pyc
ADDED
Binary file (2.41 kB). View file
|
|
app/__pycache__/cli.cpython-312.pyc
ADDED
Binary file (2.17 kB). View file
|
|
app/__pycache__/email.cpython-311.pyc
ADDED
Binary file (1.37 kB). View file
|
|
app/__pycache__/email.cpython-312.pyc
ADDED
Binary file (1.14 kB). View file
|
|
app/__pycache__/models.cpython-311.pyc
ADDED
Binary file (14 kB). View file
|
|
app/__pycache__/models.cpython-312.pyc
ADDED
Binary file (13.4 kB). View file
|
|
app/__pycache__/search.cpython-311.pyc
ADDED
Binary file (1.89 kB). View file
|
|
app/__pycache__/search.cpython-312.pyc
ADDED
Binary file (1.69 kB). View file
|
|
app/__pycache__/translate.cpython-311.pyc
ADDED
Binary file (1.44 kB). View file
|
|
app/__pycache__/translate.cpython-312.pyc
ADDED
Binary file (1.34 kB). View file
|
|
app/auth/__init__.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Blueprint
|
2 |
+
|
3 |
+
bp = Blueprint('auth', __name__)
|
4 |
+
|
5 |
+
from app.auth import routes
|
app/auth/__pycache__/__init__.cpython-311.pyc
ADDED
Binary file (376 Bytes). View file
|
|
app/auth/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (347 Bytes). View file
|
|
app/auth/__pycache__/email.cpython-311.pyc
ADDED
Binary file (1.13 kB). View file
|
|
app/auth/__pycache__/email.cpython-312.pyc
ADDED
Binary file (986 Bytes). View file
|
|
app/auth/__pycache__/forms.cpython-311.pyc
ADDED
Binary file (4.64 kB). View file
|
|
app/auth/__pycache__/forms.cpython-312.pyc
ADDED
Binary file (3.74 kB). View file
|
|
app/auth/__pycache__/routes.cpython-311.pyc
ADDED
Binary file (6.76 kB). View file
|
|
app/auth/__pycache__/routes.cpython-312.pyc
ADDED
Binary file (6.01 kB). View file
|
|
app/auth/email.py
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import render_template, current_app
|
2 |
+
from flask_babel import _
|
3 |
+
from app.email import send_email
|
4 |
+
|
5 |
+
|
6 |
+
def send_password_reset_email(user):
|
7 |
+
token = user.get_reset_password_token()
|
8 |
+
send_email(_('[Microblog] Reset Your Password'),
|
9 |
+
sender=current_app.config['ADMINS'][0],
|
10 |
+
recipients=[user.email],
|
11 |
+
text_body=render_template('email/reset_password.txt',
|
12 |
+
user=user, token=token),
|
13 |
+
html_body=render_template('email/reset_password.html',
|
14 |
+
user=user, token=token))
|
app/auth/forms.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask_wtf import FlaskForm
|
2 |
+
from flask_babel import _, lazy_gettext as _l
|
3 |
+
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
4 |
+
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
|
5 |
+
import sqlalchemy as sa
|
6 |
+
from app import db
|
7 |
+
from app.models import User
|
8 |
+
|
9 |
+
|
10 |
+
class LoginForm(FlaskForm):
|
11 |
+
username = StringField(_l('Username'), validators=[DataRequired()])
|
12 |
+
password = PasswordField(_l('Password'), validators=[DataRequired()])
|
13 |
+
remember_me = BooleanField(_l('Remember Me'))
|
14 |
+
submit = SubmitField(_l('Sign In'))
|
15 |
+
|
16 |
+
|
17 |
+
class RegistrationForm(FlaskForm):
|
18 |
+
username = StringField(_l('Username'), validators=[DataRequired()])
|
19 |
+
email = StringField(_l('Email'), validators=[DataRequired(), Email()])
|
20 |
+
password = PasswordField(_l('Password'), validators=[DataRequired()])
|
21 |
+
password2 = PasswordField(
|
22 |
+
_l('Repeat Password'), validators=[DataRequired(),
|
23 |
+
EqualTo('password')])
|
24 |
+
submit = SubmitField(_l('Register'))
|
25 |
+
|
26 |
+
def validate_username(self, username):
|
27 |
+
user = db.session.scalar(sa.select(User).where(
|
28 |
+
User.username == username.data))
|
29 |
+
if user is not None:
|
30 |
+
raise ValidationError(_('Please use a different username.'))
|
31 |
+
|
32 |
+
def validate_email(self, email):
|
33 |
+
user = db.session.scalar(sa.select(User).where(
|
34 |
+
User.email == email.data))
|
35 |
+
if user is not None:
|
36 |
+
raise ValidationError(_('Please use a different email address.'))
|
37 |
+
|
38 |
+
|
39 |
+
class ResetPasswordRequestForm(FlaskForm):
|
40 |
+
email = StringField(_l('Email'), validators=[DataRequired(), Email()])
|
41 |
+
submit = SubmitField(_l('Request Password Reset'))
|
42 |
+
|
43 |
+
|
44 |
+
class ResetPasswordForm(FlaskForm):
|
45 |
+
password = PasswordField(_l('Password'), validators=[DataRequired()])
|
46 |
+
password2 = PasswordField(
|
47 |
+
_l('Repeat Password'), validators=[DataRequired(),
|
48 |
+
EqualTo('password')])
|
49 |
+
submit = SubmitField(_l('Request Password Reset'))
|
app/auth/routes.py
ADDED
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import render_template, redirect, url_for, flash, request
|
2 |
+
from urllib.parse import urlsplit
|
3 |
+
from flask_login import login_user, logout_user, current_user
|
4 |
+
from flask_babel import _
|
5 |
+
import sqlalchemy as sa
|
6 |
+
from app import db
|
7 |
+
from app.auth import bp
|
8 |
+
from app.auth.forms import LoginForm, RegistrationForm, \
|
9 |
+
ResetPasswordRequestForm, ResetPasswordForm
|
10 |
+
from app.models import User
|
11 |
+
from app.auth.email import send_password_reset_email
|
12 |
+
|
13 |
+
|
14 |
+
@bp.route('/login', methods=['GET', 'POST'])
|
15 |
+
def login():
|
16 |
+
if current_user.is_authenticated:
|
17 |
+
return redirect(url_for('main.index'))
|
18 |
+
form = LoginForm()
|
19 |
+
if form.validate_on_submit():
|
20 |
+
user = db.session.scalar(
|
21 |
+
sa.select(User).where(User.username == form.username.data))
|
22 |
+
if user is None or not user.check_password(form.password.data):
|
23 |
+
flash(_('Invalid username or password'))
|
24 |
+
return redirect(url_for('auth.login'))
|
25 |
+
login_user(user, remember=form.remember_me.data)
|
26 |
+
next_page = request.args.get('next')
|
27 |
+
if not next_page or urlsplit(next_page).netloc != '':
|
28 |
+
next_page = url_for('main.index')
|
29 |
+
return redirect(next_page)
|
30 |
+
return render_template('auth/login.html', title=_('Sign In'), form=form)
|
31 |
+
|
32 |
+
|
33 |
+
@bp.route('/logout')
|
34 |
+
def logout():
|
35 |
+
logout_user()
|
36 |
+
return redirect(url_for('main.index'))
|
37 |
+
|
38 |
+
|
39 |
+
@bp.route('/register', methods=['GET', 'POST'])
|
40 |
+
def register():
|
41 |
+
if current_user.is_authenticated:
|
42 |
+
return redirect(url_for('main.index'))
|
43 |
+
form = RegistrationForm()
|
44 |
+
if form.validate_on_submit():
|
45 |
+
user = User(username=form.username.data, email=form.email.data)
|
46 |
+
user.set_password(form.password.data)
|
47 |
+
db.session.add(user)
|
48 |
+
db.session.commit()
|
49 |
+
flash(_('Congratulations, you are now a registered user!'))
|
50 |
+
return redirect(url_for('auth.login'))
|
51 |
+
return render_template('auth/register.html', title=_('Register'),
|
52 |
+
form=form)
|
53 |
+
|
54 |
+
|
55 |
+
@bp.route('/reset_password_request', methods=['GET', 'POST'])
|
56 |
+
def reset_password_request():
|
57 |
+
if current_user.is_authenticated:
|
58 |
+
return redirect(url_for('main.index'))
|
59 |
+
form = ResetPasswordRequestForm()
|
60 |
+
if form.validate_on_submit():
|
61 |
+
user = db.session.scalar(
|
62 |
+
sa.select(User).where(User.email == form.email.data))
|
63 |
+
if user:
|
64 |
+
send_password_reset_email(user)
|
65 |
+
flash(
|
66 |
+
_('Check your email for the instructions to reset your password'))
|
67 |
+
return redirect(url_for('auth.login'))
|
68 |
+
return render_template('auth/reset_password_request.html',
|
69 |
+
title=_('Reset Password'), form=form)
|
70 |
+
|
71 |
+
|
72 |
+
@bp.route('/reset_password/<token>', methods=['GET', 'POST'])
|
73 |
+
def reset_password(token):
|
74 |
+
if current_user.is_authenticated:
|
75 |
+
return redirect(url_for('main.index'))
|
76 |
+
user = User.verify_reset_password_token(token)
|
77 |
+
if not user:
|
78 |
+
return redirect(url_for('main.index'))
|
79 |
+
form = ResetPasswordForm()
|
80 |
+
if form.validate_on_submit():
|
81 |
+
user.set_password(form.password.data)
|
82 |
+
db.session.commit()
|
83 |
+
flash(_('Your password has been reset.'))
|
84 |
+
return redirect(url_for('auth.login'))
|
85 |
+
return render_template('auth/reset_password.html', form=form)
|
app/cli.py
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from flask import Blueprint
|
3 |
+
import click
|
4 |
+
|
5 |
+
bp = Blueprint('cli', __name__, cli_group=None)
|
6 |
+
|
7 |
+
|
8 |
+
@bp.cli.group()
|
9 |
+
def translate():
|
10 |
+
"""Translation and localization commands."""
|
11 |
+
pass
|
12 |
+
|
13 |
+
|
14 |
+
@translate.command()
|
15 |
+
@click.argument('lang')
|
16 |
+
def init(lang):
|
17 |
+
"""Initialize a new language."""
|
18 |
+
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
|
19 |
+
raise RuntimeError('extract command failed')
|
20 |
+
if os.system(
|
21 |
+
'pybabel init -i messages.pot -d app/translations -l ' + lang):
|
22 |
+
raise RuntimeError('init command failed')
|
23 |
+
os.remove('messages.pot')
|
24 |
+
|
25 |
+
|
26 |
+
@translate.command()
|
27 |
+
def update():
|
28 |
+
"""Update all languages."""
|
29 |
+
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
|
30 |
+
raise RuntimeError('extract command failed')
|
31 |
+
if os.system('pybabel update -i messages.pot -d app/translations'):
|
32 |
+
raise RuntimeError('update command failed')
|
33 |
+
os.remove('messages.pot')
|
34 |
+
|
35 |
+
|
36 |
+
@translate.command()
|
37 |
+
def compile():
|
38 |
+
"""Compile all languages."""
|
39 |
+
if os.system('pybabel compile -d app/translations'):
|
40 |
+
raise RuntimeError('compile command failed')
|
app/email.py
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from threading import Thread
|
2 |
+
from flask import current_app
|
3 |
+
from flask_mail import Message
|
4 |
+
from app import mail
|
5 |
+
|
6 |
+
|
7 |
+
def send_async_email(app, msg):
|
8 |
+
with app.app_context():
|
9 |
+
mail.send(msg)
|
10 |
+
|
11 |
+
|
12 |
+
def send_email(subject, sender, recipients, text_body, html_body):
|
13 |
+
msg = Message(subject, sender=sender, recipients=recipients)
|
14 |
+
msg.body = text_body
|
15 |
+
msg.html = html_body
|
16 |
+
Thread(target=send_async_email,
|
17 |
+
args=(current_app._get_current_object(), msg)).start()
|
app/errors/__init__.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Blueprint
|
2 |
+
|
3 |
+
bp = Blueprint('errors', __name__)
|
4 |
+
|
5 |
+
from app.errors import handlers
|
app/errors/__pycache__/__init__.cpython-311.pyc
ADDED
Binary file (384 Bytes). View file
|
|
app/errors/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (355 Bytes). View file
|
|
app/errors/__pycache__/handlers.cpython-311.pyc
ADDED
Binary file (1.03 kB). View file
|
|
app/errors/__pycache__/handlers.cpython-312.pyc
ADDED
Binary file (949 Bytes). View file
|
|
app/errors/handlers.py
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import render_template
|
2 |
+
from app import db
|
3 |
+
from app.errors import bp
|
4 |
+
|
5 |
+
|
6 |
+
@bp.app_errorhandler(404)
|
7 |
+
def not_found_error(error):
|
8 |
+
return render_template('errors/404.html'), 404
|
9 |
+
|
10 |
+
|
11 |
+
@bp.app_errorhandler(500)
|
12 |
+
def internal_error(error):
|
13 |
+
db.session.rollback()
|
14 |
+
return render_template('errors/500.html'), 500
|
app/main/__init__.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Blueprint
|
2 |
+
|
3 |
+
bp = Blueprint('main', __name__)
|
4 |
+
|
5 |
+
from app.main import routes
|
app/main/__pycache__/__init__.cpython-311.pyc
ADDED
Binary file (376 Bytes). View file
|
|
app/main/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (347 Bytes). View file
|
|
app/main/__pycache__/forms.cpython-311.pyc
ADDED
Binary file (4.03 kB). View file
|
|
app/main/__pycache__/forms.cpython-312.pyc
ADDED
Binary file (3.31 kB). View file
|
|
app/main/__pycache__/routes.cpython-311.pyc
ADDED
Binary file (12.6 kB). View file
|
|
app/main/__pycache__/routes.cpython-312.pyc
ADDED
Binary file (11.6 kB). View file
|
|
app/main/forms.py
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import request
|
2 |
+
from flask_wtf import FlaskForm
|
3 |
+
from wtforms import StringField, SubmitField, TextAreaField
|
4 |
+
from wtforms.validators import ValidationError, DataRequired, Length
|
5 |
+
import sqlalchemy as sa
|
6 |
+
from flask_babel import _, lazy_gettext as _l
|
7 |
+
from app import db
|
8 |
+
from app.models import User
|
9 |
+
|
10 |
+
|
11 |
+
class EditProfileForm(FlaskForm):
|
12 |
+
username = StringField(_l('Username'), validators=[DataRequired()])
|
13 |
+
about_me = TextAreaField(_l('About me'),
|
14 |
+
validators=[Length(min=0, max=140)])
|
15 |
+
submit = SubmitField(_l('Submit'))
|
16 |
+
|
17 |
+
def __init__(self, original_username, *args, **kwargs):
|
18 |
+
super().__init__(*args, **kwargs)
|
19 |
+
self.original_username = original_username
|
20 |
+
|
21 |
+
def validate_username(self, username):
|
22 |
+
if username.data != self.original_username:
|
23 |
+
user = db.session.scalar(sa.select(User).where(
|
24 |
+
User.username == self.username.data))
|
25 |
+
if user is not None:
|
26 |
+
raise ValidationError(_('Please use a different username.'))
|
27 |
+
|
28 |
+
|
29 |
+
class EmptyForm(FlaskForm):
|
30 |
+
submit = SubmitField('Submit')
|
31 |
+
|
32 |
+
|
33 |
+
class PostForm(FlaskForm):
|
34 |
+
post = TextAreaField(_l('Say something'), validators=[
|
35 |
+
DataRequired(), Length(min=1, max=140)])
|
36 |
+
submit = SubmitField(_l('Submit'))
|
37 |
+
|
38 |
+
|
39 |
+
class SearchForm(FlaskForm):
|
40 |
+
q = StringField(_l('Search'), validators=[DataRequired()])
|
41 |
+
|
42 |
+
def __init__(self, *args, **kwargs):
|
43 |
+
if 'formdata' not in kwargs:
|
44 |
+
kwargs['formdata'] = request.args
|
45 |
+
if 'meta' not in kwargs:
|
46 |
+
kwargs['meta'] = {'csrf': False}
|
47 |
+
super(SearchForm, self).__init__(*args, **kwargs)
|
app/main/routes.py
ADDED
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime, timezone
|
2 |
+
from flask import render_template, flash, redirect, url_for, request, g, \
|
3 |
+
current_app
|
4 |
+
from flask_login import current_user, login_required
|
5 |
+
from flask_babel import _, get_locale
|
6 |
+
import sqlalchemy as sa
|
7 |
+
from langdetect import detect, LangDetectException
|
8 |
+
from app import db
|
9 |
+
from app.main.forms import EditProfileForm, EmptyForm, PostForm, SearchForm
|
10 |
+
from app.models import User, Post
|
11 |
+
from app.translate import translate
|
12 |
+
from app.main import bp
|
13 |
+
|
14 |
+
|
15 |
+
@bp.before_app_request
|
16 |
+
def before_request():
|
17 |
+
if current_user.is_authenticated:
|
18 |
+
current_user.last_seen = datetime.now(timezone.utc)
|
19 |
+
db.session.commit()
|
20 |
+
g.search_form = SearchForm()
|
21 |
+
g.locale = str(get_locale())
|
22 |
+
|
23 |
+
|
24 |
+
@bp.route('/', methods=['GET', 'POST'])
|
25 |
+
@bp.route('/index', methods=['GET', 'POST'])
|
26 |
+
@login_required
|
27 |
+
def index():
|
28 |
+
form = PostForm()
|
29 |
+
if form.validate_on_submit():
|
30 |
+
try:
|
31 |
+
language = detect(form.post.data)
|
32 |
+
except LangDetectException:
|
33 |
+
language = ''
|
34 |
+
post = Post(body=form.post.data, author=current_user,
|
35 |
+
language=language)
|
36 |
+
db.session.add(post)
|
37 |
+
db.session.commit()
|
38 |
+
flash(_('Your post is now live!'))
|
39 |
+
return redirect(url_for('main.index'))
|
40 |
+
page = request.args.get('page', 1, type=int)
|
41 |
+
posts = db.paginate(current_user.following_posts(), page=page,
|
42 |
+
per_page=current_app.config['POSTS_PER_PAGE'],
|
43 |
+
error_out=False)
|
44 |
+
next_url = url_for('main.index', page=posts.next_num) \
|
45 |
+
if posts.has_next else None
|
46 |
+
prev_url = url_for('main.index', page=posts.prev_num) \
|
47 |
+
if posts.has_prev else None
|
48 |
+
return render_template('index.html', title=_('Home'), form=form,
|
49 |
+
posts=posts.items, next_url=next_url,
|
50 |
+
prev_url=prev_url)
|
51 |
+
|
52 |
+
|
53 |
+
@bp.route('/explore')
|
54 |
+
@login_required
|
55 |
+
def explore():
|
56 |
+
page = request.args.get('page', 1, type=int)
|
57 |
+
query = sa.select(Post).order_by(Post.timestamp.desc())
|
58 |
+
posts = db.paginate(query, page=page,
|
59 |
+
per_page=current_app.config['POSTS_PER_PAGE'],
|
60 |
+
error_out=False)
|
61 |
+
next_url = url_for('main.explore', page=posts.next_num) \
|
62 |
+
if posts.has_next else None
|
63 |
+
prev_url = url_for('main.explore', page=posts.prev_num) \
|
64 |
+
if posts.has_prev else None
|
65 |
+
return render_template('index.html', title=_('Explore'),
|
66 |
+
posts=posts.items, next_url=next_url,
|
67 |
+
prev_url=prev_url)
|
68 |
+
|
69 |
+
|
70 |
+
@bp.route('/user/<username>')
|
71 |
+
@login_required
|
72 |
+
def user(username):
|
73 |
+
user = db.first_or_404(sa.select(User).where(User.username == username))
|
74 |
+
page = request.args.get('page', 1, type=int)
|
75 |
+
query = user.posts.select().order_by(Post.timestamp.desc())
|
76 |
+
posts = db.paginate(query, page=page,
|
77 |
+
per_page=current_app.config['POSTS_PER_PAGE'],
|
78 |
+
error_out=False)
|
79 |
+
next_url = url_for('main.user', username=user.username,
|
80 |
+
page=posts.next_num) if posts.has_next else None
|
81 |
+
prev_url = url_for('main.user', username=user.username,
|
82 |
+
page=posts.prev_num) if posts.has_prev else None
|
83 |
+
form = EmptyForm()
|
84 |
+
return render_template('user.html', user=user, posts=posts.items,
|
85 |
+
next_url=next_url, prev_url=prev_url, form=form)
|
86 |
+
|
87 |
+
|
88 |
+
@bp.route('/edit_profile', methods=['GET', 'POST'])
|
89 |
+
@login_required
|
90 |
+
def edit_profile():
|
91 |
+
form = EditProfileForm(current_user.username)
|
92 |
+
if form.validate_on_submit():
|
93 |
+
current_user.username = form.username.data
|
94 |
+
current_user.about_me = form.about_me.data
|
95 |
+
db.session.commit()
|
96 |
+
flash(_('Your changes have been saved.'))
|
97 |
+
return redirect(url_for('main.edit_profile'))
|
98 |
+
elif request.method == 'GET':
|
99 |
+
form.username.data = current_user.username
|
100 |
+
form.about_me.data = current_user.about_me
|
101 |
+
return render_template('edit_profile.html', title=_('Edit Profile'),
|
102 |
+
form=form)
|
103 |
+
|
104 |
+
|
105 |
+
@bp.route('/follow/<username>', methods=['POST'])
|
106 |
+
@login_required
|
107 |
+
def follow(username):
|
108 |
+
form = EmptyForm()
|
109 |
+
if form.validate_on_submit():
|
110 |
+
user = db.session.scalar(
|
111 |
+
sa.select(User).where(User.username == username))
|
112 |
+
if user is None:
|
113 |
+
flash(_('User %(username)s not found.', username=username))
|
114 |
+
return redirect(url_for('main.index'))
|
115 |
+
if user == current_user:
|
116 |
+
flash(_('You cannot follow yourself!'))
|
117 |
+
return redirect(url_for('main.user', username=username))
|
118 |
+
current_user.follow(user)
|
119 |
+
db.session.commit()
|
120 |
+
flash(_('You are following %(username)s!', username=username))
|
121 |
+
return redirect(url_for('main.user', username=username))
|
122 |
+
else:
|
123 |
+
return redirect(url_for('main.index'))
|
124 |
+
|
125 |
+
|
126 |
+
@bp.route('/unfollow/<username>', methods=['POST'])
|
127 |
+
@login_required
|
128 |
+
def unfollow(username):
|
129 |
+
form = EmptyForm()
|
130 |
+
if form.validate_on_submit():
|
131 |
+
user = db.session.scalar(
|
132 |
+
sa.select(User).where(User.username == username))
|
133 |
+
if user is None:
|
134 |
+
flash(_('User %(username)s not found.', username=username))
|
135 |
+
return redirect(url_for('main.index'))
|
136 |
+
if user == current_user:
|
137 |
+
flash(_('You cannot unfollow yourself!'))
|
138 |
+
return redirect(url_for('main.user', username=username))
|
139 |
+
current_user.unfollow(user)
|
140 |
+
db.session.commit()
|
141 |
+
flash(_('You are not following %(username)s.', username=username))
|
142 |
+
return redirect(url_for('main.user', username=username))
|
143 |
+
else:
|
144 |
+
return redirect(url_for('main.index'))
|
145 |
+
|
146 |
+
|
147 |
+
@bp.route('/translate', methods=['POST'])
|
148 |
+
@login_required
|
149 |
+
def translate_text():
|
150 |
+
data = request.get_json()
|
151 |
+
return {'text': translate(data['text'],
|
152 |
+
data['source_language'],
|
153 |
+
data['dest_language'])}
|
154 |
+
|
155 |
+
|
156 |
+
@bp.route('/search')
|
157 |
+
@login_required
|
158 |
+
def search():
|
159 |
+
if not g.search_form.validate():
|
160 |
+
return redirect(url_for('main.explore'))
|
161 |
+
page = request.args.get('page', 1, type=int)
|
162 |
+
posts, total = Post.search(g.search_form.q.data, page,
|
163 |
+
current_app.config['POSTS_PER_PAGE'])
|
164 |
+
next_url = url_for('main.search', q=g.search_form.q.data, page=page + 1) \
|
165 |
+
if total > page * current_app.config['POSTS_PER_PAGE'] else None
|
166 |
+
prev_url = url_for('main.search', q=g.search_form.q.data, page=page - 1) \
|
167 |
+
if page > 1 else None
|
168 |
+
return render_template('search.html', title=_('Search'), posts=posts,
|
169 |
+
next_url=next_url, prev_url=prev_url)
|
app/models.py
ADDED
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime, timezone
|
2 |
+
from hashlib import md5
|
3 |
+
from time import time
|
4 |
+
from typing import Optional
|
5 |
+
import sqlalchemy as sa
|
6 |
+
import sqlalchemy.orm as so
|
7 |
+
from flask import current_app
|
8 |
+
from flask_login import UserMixin
|
9 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
10 |
+
import jwt
|
11 |
+
from app import db, login
|
12 |
+
from app.search import add_to_index, remove_from_index, query_index
|
13 |
+
|
14 |
+
|
15 |
+
class SearchableMixin:
|
16 |
+
@classmethod
|
17 |
+
def search(cls, expression, page, per_page):
|
18 |
+
ids, total = query_index(cls.__tablename__, expression, page, per_page)
|
19 |
+
if total == 0:
|
20 |
+
return [], 0
|
21 |
+
when = []
|
22 |
+
for i in range(len(ids)):
|
23 |
+
when.append((ids[i], i))
|
24 |
+
query = sa.select(cls).where(cls.id.in_(ids)).order_by(
|
25 |
+
db.case(*when, value=cls.id))
|
26 |
+
return db.session.scalars(query), total
|
27 |
+
|
28 |
+
@classmethod
|
29 |
+
def before_commit(cls, session):
|
30 |
+
session._changes = {
|
31 |
+
'add': list(session.new),
|
32 |
+
'update': list(session.dirty),
|
33 |
+
'delete': list(session.deleted)
|
34 |
+
}
|
35 |
+
|
36 |
+
@classmethod
|
37 |
+
def after_commit(cls, session):
|
38 |
+
for obj in session._changes['add']:
|
39 |
+
if isinstance(obj, SearchableMixin):
|
40 |
+
add_to_index(obj.__tablename__, obj)
|
41 |
+
for obj in session._changes['update']:
|
42 |
+
if isinstance(obj, SearchableMixin):
|
43 |
+
add_to_index(obj.__tablename__, obj)
|
44 |
+
for obj in session._changes['delete']:
|
45 |
+
if isinstance(obj, SearchableMixin):
|
46 |
+
remove_from_index(obj.__tablename__, obj)
|
47 |
+
session._changes = None
|
48 |
+
|
49 |
+
@classmethod
|
50 |
+
def reindex(cls):
|
51 |
+
for obj in db.session.scalars(sa.select(cls)):
|
52 |
+
add_to_index(cls.__tablename__, obj)
|
53 |
+
|
54 |
+
|
55 |
+
db.event.listen(db.session, 'before_commit', SearchableMixin.before_commit)
|
56 |
+
db.event.listen(db.session, 'after_commit', SearchableMixin.after_commit)
|
57 |
+
|
58 |
+
|
59 |
+
followers = sa.Table(
|
60 |
+
'followers',
|
61 |
+
db.metadata,
|
62 |
+
sa.Column('follower_id', sa.Integer, sa.ForeignKey('user.id'),
|
63 |
+
primary_key=True),
|
64 |
+
sa.Column('followed_id', sa.Integer, sa.ForeignKey('user.id'),
|
65 |
+
primary_key=True)
|
66 |
+
)
|
67 |
+
|
68 |
+
|
69 |
+
class User(UserMixin, db.Model):
|
70 |
+
id: so.Mapped[int] = so.mapped_column(primary_key=True)
|
71 |
+
username: so.Mapped[str] = so.mapped_column(sa.String(64), index=True,
|
72 |
+
unique=True)
|
73 |
+
email: so.Mapped[str] = so.mapped_column(sa.String(120), index=True,
|
74 |
+
unique=True)
|
75 |
+
password_hash: so.Mapped[Optional[str]] = so.mapped_column(sa.String(256))
|
76 |
+
about_me: so.Mapped[Optional[str]] = so.mapped_column(sa.String(140))
|
77 |
+
last_seen: so.Mapped[Optional[datetime]] = so.mapped_column(
|
78 |
+
default=lambda: datetime.now(timezone.utc))
|
79 |
+
|
80 |
+
posts: so.WriteOnlyMapped['Post'] = so.relationship(
|
81 |
+
back_populates='author')
|
82 |
+
following: so.WriteOnlyMapped['User'] = so.relationship(
|
83 |
+
secondary=followers, primaryjoin=(followers.c.follower_id == id),
|
84 |
+
secondaryjoin=(followers.c.followed_id == id),
|
85 |
+
back_populates='followers')
|
86 |
+
followers: so.WriteOnlyMapped['User'] = so.relationship(
|
87 |
+
secondary=followers, primaryjoin=(followers.c.followed_id == id),
|
88 |
+
secondaryjoin=(followers.c.follower_id == id),
|
89 |
+
back_populates='following')
|
90 |
+
|
91 |
+
def __repr__(self):
|
92 |
+
return '<User {}>'.format(self.username)
|
93 |
+
|
94 |
+
def set_password(self, password):
|
95 |
+
self.password_hash = generate_password_hash(password)
|
96 |
+
|
97 |
+
def check_password(self, password):
|
98 |
+
return check_password_hash(self.password_hash, password)
|
99 |
+
|
100 |
+
def avatar(self, size):
|
101 |
+
digest = md5(self.email.lower().encode('utf-8')).hexdigest()
|
102 |
+
return f'https://www.gravatar.com/avatar/{digest}?d=identicon&s={size}'
|
103 |
+
|
104 |
+
def follow(self, user):
|
105 |
+
if not self.is_following(user):
|
106 |
+
self.following.add(user)
|
107 |
+
|
108 |
+
def unfollow(self, user):
|
109 |
+
if self.is_following(user):
|
110 |
+
self.following.remove(user)
|
111 |
+
|
112 |
+
def is_following(self, user):
|
113 |
+
query = self.following.select().where(User.id == user.id)
|
114 |
+
return db.session.scalar(query) is not None
|
115 |
+
|
116 |
+
def followers_count(self):
|
117 |
+
query = sa.select(sa.func.count()).select_from(
|
118 |
+
self.followers.select().subquery())
|
119 |
+
return db.session.scalar(query)
|
120 |
+
|
121 |
+
def following_count(self):
|
122 |
+
query = sa.select(sa.func.count()).select_from(
|
123 |
+
self.following.select().subquery())
|
124 |
+
return db.session.scalar(query)
|
125 |
+
|
126 |
+
def following_posts(self):
|
127 |
+
Author = so.aliased(User)
|
128 |
+
Follower = so.aliased(User)
|
129 |
+
return (
|
130 |
+
sa.select(Post)
|
131 |
+
.join(Post.author.of_type(Author))
|
132 |
+
.join(Author.followers.of_type(Follower), isouter=True)
|
133 |
+
.where(sa.or_(
|
134 |
+
Follower.id == self.id,
|
135 |
+
Author.id == self.id,
|
136 |
+
))
|
137 |
+
.group_by(Post)
|
138 |
+
.order_by(Post.timestamp.desc())
|
139 |
+
)
|
140 |
+
|
141 |
+
def get_reset_password_token(self, expires_in=600):
|
142 |
+
return jwt.encode(
|
143 |
+
{'reset_password': self.id, 'exp': time() + expires_in},
|
144 |
+
current_app.config['SECRET_KEY'], algorithm='HS256')
|
145 |
+
|
146 |
+
@staticmethod
|
147 |
+
def verify_reset_password_token(token):
|
148 |
+
try:
|
149 |
+
id = jwt.decode(token, current_app.config['SECRET_KEY'],
|
150 |
+
algorithms=['HS256'])['reset_password']
|
151 |
+
except Exception:
|
152 |
+
return
|
153 |
+
return db.session.get(User, id)
|
154 |
+
|
155 |
+
|
156 |
+
@login.user_loader
|
157 |
+
def load_user(id):
|
158 |
+
return db.session.get(User, int(id))
|
159 |
+
|
160 |
+
|
161 |
+
class Post(SearchableMixin, db.Model):
|
162 |
+
__searchable__ = ['body']
|
163 |
+
id: so.Mapped[int] = so.mapped_column(primary_key=True)
|
164 |
+
body: so.Mapped[str] = so.mapped_column(sa.String(140))
|
165 |
+
timestamp: so.Mapped[datetime] = so.mapped_column(
|
166 |
+
index=True, default=lambda: datetime.now(timezone.utc))
|
167 |
+
user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id),
|
168 |
+
index=True)
|
169 |
+
language: so.Mapped[Optional[str]] = so.mapped_column(sa.String(5))
|
170 |
+
|
171 |
+
author: so.Mapped[User] = so.relationship(back_populates='posts')
|
172 |
+
|
173 |
+
def __repr__(self):
|
174 |
+
return '<Post {}>'.format(self.body)
|
app/search.py
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import current_app
|
2 |
+
|
3 |
+
|
4 |
+
def add_to_index(index, model):
|
5 |
+
if not current_app.elasticsearch:
|
6 |
+
return
|
7 |
+
payload = {}
|
8 |
+
for field in model.__searchable__:
|
9 |
+
payload[field] = getattr(model, field)
|
10 |
+
current_app.elasticsearch.index(index=index, id=model.id, document=payload)
|
11 |
+
|
12 |
+
|
13 |
+
def remove_from_index(index, model):
|
14 |
+
if not current_app.elasticsearch:
|
15 |
+
return
|
16 |
+
current_app.elasticsearch.delete(index=index, id=model.id)
|
17 |
+
|
18 |
+
|
19 |
+
def query_index(index, query, page, per_page):
|
20 |
+
if not current_app.elasticsearch:
|
21 |
+
return [], 0
|
22 |
+
search = current_app.elasticsearch.search(
|
23 |
+
index=index,
|
24 |
+
query={'multi_match': {'query': query, 'fields': ['*']}},
|
25 |
+
from_=(page - 1) * per_page,
|
26 |
+
size=per_page)
|
27 |
+
ids = [int(hit['_id']) for hit in search['hits']['hits']]
|
28 |
+
return ids, search['hits']['total']['value']
|
app/static/loading.gif
ADDED
![]() |