Compare commits
	
		
			20 Commits
		
	
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 3472187155 | |
|  | bb2273349f | |
|  | a25d6f2f19 | |
|  | 7f32336d21 | |
|  | b577aa6b62 | |
|  | 5cfb75def5 | |
|  | 94b40cf4b7 | |
|  | 868ad4b410 | |
|  | f4a401e320 | |
|  | 7f7c8ef5d7 | |
|  | 8a56c34ce2 | |
|  | a650e6f989 | |
|  | 542fa42312 | |
|  | 14f6f99ea9 | |
|  | fdeb27d052 | |
|  | d1363e0326 | |
|  | 8f43aadd41 | |
|  | f9ba2d28cf | |
|  | e7b1227a1c | |
|  | 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