Nasma commited on
Commit
e612d7f
·
verified ·
1 Parent(s): 24bf390

Upload 81 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .flaskenv +2 -0
  2. __pycache__/config.cpython-311.pyc +0 -0
  3. __pycache__/config.cpython-312.pyc +0 -0
  4. __pycache__/main.cpython-311.pyc +0 -0
  5. app.db +0 -0
  6. app/__init__.py +87 -0
  7. app/__pycache__/__init__.cpython-311.pyc +0 -0
  8. app/__pycache__/__init__.cpython-312.pyc +0 -0
  9. app/__pycache__/cli.cpython-311.pyc +0 -0
  10. app/__pycache__/cli.cpython-312.pyc +0 -0
  11. app/__pycache__/email.cpython-311.pyc +0 -0
  12. app/__pycache__/email.cpython-312.pyc +0 -0
  13. app/__pycache__/models.cpython-311.pyc +0 -0
  14. app/__pycache__/models.cpython-312.pyc +0 -0
  15. app/__pycache__/search.cpython-311.pyc +0 -0
  16. app/__pycache__/search.cpython-312.pyc +0 -0
  17. app/__pycache__/translate.cpython-311.pyc +0 -0
  18. app/__pycache__/translate.cpython-312.pyc +0 -0
  19. app/auth/__init__.py +5 -0
  20. app/auth/__pycache__/__init__.cpython-311.pyc +0 -0
  21. app/auth/__pycache__/__init__.cpython-312.pyc +0 -0
  22. app/auth/__pycache__/email.cpython-311.pyc +0 -0
  23. app/auth/__pycache__/email.cpython-312.pyc +0 -0
  24. app/auth/__pycache__/forms.cpython-311.pyc +0 -0
  25. app/auth/__pycache__/forms.cpython-312.pyc +0 -0
  26. app/auth/__pycache__/routes.cpython-311.pyc +0 -0
  27. app/auth/__pycache__/routes.cpython-312.pyc +0 -0
  28. app/auth/email.py +14 -0
  29. app/auth/forms.py +49 -0
  30. app/auth/routes.py +85 -0
  31. app/cli.py +40 -0
  32. app/email.py +17 -0
  33. app/errors/__init__.py +5 -0
  34. app/errors/__pycache__/__init__.cpython-311.pyc +0 -0
  35. app/errors/__pycache__/__init__.cpython-312.pyc +0 -0
  36. app/errors/__pycache__/handlers.cpython-311.pyc +0 -0
  37. app/errors/__pycache__/handlers.cpython-312.pyc +0 -0
  38. app/errors/handlers.py +14 -0
  39. app/main/__init__.py +5 -0
  40. app/main/__pycache__/__init__.cpython-311.pyc +0 -0
  41. app/main/__pycache__/__init__.cpython-312.pyc +0 -0
  42. app/main/__pycache__/forms.cpython-311.pyc +0 -0
  43. app/main/__pycache__/forms.cpython-312.pyc +0 -0
  44. app/main/__pycache__/routes.cpython-311.pyc +0 -0
  45. app/main/__pycache__/routes.cpython-312.pyc +0 -0
  46. app/main/forms.py +47 -0
  47. app/main/routes.py +169 -0
  48. app/models.py +174 -0
  49. app/search.py +28 -0
  50. 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