+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/templates/main/home.html b/app/templates/main/home.html
new file mode 100644
index 0000000..2bffa08
--- /dev/null
+++ b/app/templates/main/home.html
@@ -0,0 +1,22 @@
+{% extends "layouts/base.html" %}
+{% block content %}
+
+
+ {% if current_user.is_anonymous %}
+
Microblog App
+ {% else %}
+
Hi {{ current_user.first_name }}
+ {% endif %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/templates/main/search.html b/app/templates/main/search.html
new file mode 100644
index 0000000..2def720
--- /dev/null
+++ b/app/templates/main/search.html
@@ -0,0 +1,29 @@
+{% extends "layouts/base.html" %}
+{% set active_page = "search" %}
+
+{% block content %}
+
+ Search Results
+
+
+
+ {% for post in posts %}
+ {% include 'main/_post.html' %}
+ {% endfor %}
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/templates/main/user.html b/app/templates/main/user.html
new file mode 100644
index 0000000..2e4da8d
--- /dev/null
+++ b/app/templates/main/user.html
@@ -0,0 +1,28 @@
+{% extends "layouts/base.html" %}
+{% block content %}
+
+
+
+ {% include 'main/_userprofile.html' %}
+
+
+
+
+
+ User Posts
+
+
+
+ {% for post in posts.items %}
+ {% include 'main/_post.html' %}
+ {% endfor %}
+
+
+ {% include 'main/_pagination.html' %}
+
+
+
+{% endblock %}
+
+{% block script_footer %}
+{% endblock %}
\ No newline at end of file
diff --git a/blog.py b/blog.py
new file mode 100644
index 0000000..58a78fb
--- /dev/null
+++ b/blog.py
@@ -0,0 +1,8 @@
+from app import create_app, db
+from app.models import User, Posts
+
+app = create_app()
+
+@app.shell_context_processor
+def make_shell_context():
+ return {'db': db, 'User': User, 'Posts': Posts}
\ No newline at end of file
diff --git a/boot.sh b/boot.sh
new file mode 100644
index 0000000..e1ea631
--- /dev/null
+++ b/boot.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+source venv/bin/activate
+flask db upgrade
+exec gunicorn -b :5000 --access-logfile - --error-logfile - portfolio:app
\ No newline at end of file
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..e2b12f6
--- /dev/null
+++ b/config.py
@@ -0,0 +1,23 @@
+import os
+import dotenv
+basedir = os.path.abspath(os.path.dirname(__file__))
+dotenv.load_dotenv(dotenv.find_dotenv())
+
+class Config(object):
+ # SECRET_KEY = secrets.token_urlsafe(64)
+ SECRET_KEY = os.getenv('SECRET_KEY') or 'difficult-to-guess-string'
+ POSTS_PER_PAGE = 3
+
+ SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL') or \
+ 'sqlite:///' + os.path.join(basedir, 'app.db')
+ SQL_ALCHEMY_TRACKMODIFICATIONS = False
+
+ MAIL_SERVER = os.getenv('MAIL_SERVER')
+ MAIL_PORT = int(os.getenv('MAIL_PORT'))
+ MAIL_USE_TLS = os.getenv('MAIL_USE_TLS') is not None
+ MAIL_USE_SSL = os.getenv('MAIL_USE_SSL') is not None
+ MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER')
+ MAIL_USERNAME = os.getenv('MAIL_USERNAME')
+ MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
+ ADMINS = [''] # enter email 'your-email@example.com'
+ ELASTICSEARCH_URL = os.getenv('ELASTICSEARCH_URL')
\ No newline at end of file
diff --git a/migrations/README b/migrations/README
new file mode 100644
index 0000000..0e04844
--- /dev/null
+++ b/migrations/README
@@ -0,0 +1 @@
+Single-database configuration for Flask.
diff --git a/migrations/alembic.ini b/migrations/alembic.ini
new file mode 100644
index 0000000..ec9d45c
--- /dev/null
+++ b/migrations/alembic.ini
@@ -0,0 +1,50 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic,flask_migrate
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[logger_flask_migrate]
+level = INFO
+handlers =
+qualname = flask_migrate
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/migrations/env.py b/migrations/env.py
new file mode 100644
index 0000000..55e9df9
--- /dev/null
+++ b/migrations/env.py
@@ -0,0 +1,97 @@
+from __future__ import with_statement
+
+import logging
+from logging.config import fileConfig
+
+from flask import current_app
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+logger = logging.getLogger('alembic.env')
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+config.set_main_option(
+ 'sqlalchemy.url',
+ str(current_app.extensions['migrate'].db.get_engine().url).replace(
+ '%', '%%'))
+target_db = current_app.extensions['migrate'].db
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def get_metadata():
+ if hasattr(target_db, 'metadatas'):
+ return target_db.metadatas[None]
+ return target_db.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url, target_metadata=get_metadata(), literal_binds=True
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+
+ # this callback is used to prevent an auto-migration from being generated
+ # when there are no changes to the schema
+ # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
+ def process_revision_directives(context, revision, directives):
+ if getattr(config.cmd_opts, 'autogenerate', False):
+ script = directives[0]
+ if script.upgrade_ops.is_empty():
+ directives[:] = []
+ logger.info('No changes in schema detected.')
+
+ connectable = current_app.extensions['migrate'].db.get_engine()
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection,
+ target_metadata=get_metadata(),
+ process_revision_directives=process_revision_directives,
+ **current_app.extensions['migrate'].configure_args
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/migrations/script.py.mako b/migrations/script.py.mako
new file mode 100644
index 0000000..2c01563
--- /dev/null
+++ b/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/migrations/versions/534f7d23c2a2_posts_table.py b/migrations/versions/534f7d23c2a2_posts_table.py
new file mode 100644
index 0000000..23794b3
--- /dev/null
+++ b/migrations/versions/534f7d23c2a2_posts_table.py
@@ -0,0 +1,41 @@
+"""posts table
+
+Revision ID: 534f7d23c2a2
+Revises: f77114abe39f
+Create Date: 2023-03-05 09:22:45.037980
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '534f7d23c2a2'
+down_revision = 'f77114abe39f'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('posts',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('body', sa.String(length=140), nullable=True),
+ sa.Column('timestamp', sa.DateTime(), nullable=True),
+ sa.Column('user_id', sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ with op.batch_alter_table('posts', schema=None) as batch_op:
+ batch_op.create_index(batch_op.f('ix_posts_timestamp'), ['timestamp'], unique=False)
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('posts', schema=None) as batch_op:
+ batch_op.drop_index(batch_op.f('ix_posts_timestamp'))
+
+ op.drop_table('posts')
+ # ### end Alembic commands ###
diff --git a/migrations/versions/55c53aab4611_.py b/migrations/versions/55c53aab4611_.py
new file mode 100644
index 0000000..0f35f9e
--- /dev/null
+++ b/migrations/versions/55c53aab4611_.py
@@ -0,0 +1,34 @@
+"""empty message
+
+Revision ID: 55c53aab4611
+Revises: 725fa9b0c66e
+Create Date: 2023-02-25 20:27:01.715544
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '55c53aab4611'
+down_revision = '725fa9b0c66e'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('user', schema=None) as batch_op:
+ batch_op.drop_index('ix_user_username')
+ batch_op.drop_column('username')
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('user', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('username', sa.VARCHAR(length=64), nullable=True))
+ batch_op.create_index('ix_user_username', ['username'], unique=False)
+
+ # ### end Alembic commands ###
diff --git a/migrations/versions/725fa9b0c66e_.py b/migrations/versions/725fa9b0c66e_.py
new file mode 100644
index 0000000..80531a0
--- /dev/null
+++ b/migrations/versions/725fa9b0c66e_.py
@@ -0,0 +1,32 @@
+"""empty message
+
+Revision ID: 725fa9b0c66e
+Revises: 8f932fae97c8
+Create Date: 2023-02-23 19:34:30.935097
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '725fa9b0c66e'
+down_revision = '8f932fae97c8'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('user', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('avatar', sa.String(length=120), nullable=True))
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('user', schema=None) as batch_op:
+ batch_op.drop_column('avatar')
+
+ # ### end Alembic commands ###
diff --git a/migrations/versions/7916a7adcb89_users_table.py b/migrations/versions/7916a7adcb89_users_table.py
new file mode 100644
index 0000000..6b6b930
--- /dev/null
+++ b/migrations/versions/7916a7adcb89_users_table.py
@@ -0,0 +1,42 @@
+"""users table
+
+Revision ID: 7916a7adcb89
+Revises:
+Create Date: 2023-02-20 19:22:37.340516
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '7916a7adcb89'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('user',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('username', sa.String(length=64), nullable=True),
+ sa.Column('email', sa.String(length=120), nullable=True),
+ sa.Column('password_hash', sa.String(length=128), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
+ with op.batch_alter_table('user', schema=None) as batch_op:
+ batch_op.create_index(batch_op.f('ix_user_email'), ['email'], unique=True)
+ batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True)
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('user', schema=None) as batch_op:
+ batch_op.drop_index(batch_op.f('ix_user_username'))
+ batch_op.drop_index(batch_op.f('ix_user_email'))
+
+ op.drop_table('user')
+ # ### end Alembic commands ###
diff --git a/migrations/versions/8f932fae97c8_.py b/migrations/versions/8f932fae97c8_.py
new file mode 100644
index 0000000..ecb7548
--- /dev/null
+++ b/migrations/versions/8f932fae97c8_.py
@@ -0,0 +1,38 @@
+"""empty message
+
+Revision ID: 8f932fae97c8
+Revises: 7916a7adcb89
+Create Date: 2023-02-21 21:02:22.728424
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '8f932fae97c8'
+down_revision = '7916a7adcb89'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('user', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('first_name', sa.String(length=64), nullable=True))
+ batch_op.add_column(sa.Column('last_name', sa.String(length=64), nullable=True))
+ batch_op.add_column(sa.Column('tagline', sa.String(length=120), nullable=True))
+ batch_op.add_column(sa.Column('about_me', sa.String(length=3500), nullable=True))
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('user', schema=None) as batch_op:
+ batch_op.drop_column('about_me')
+ batch_op.drop_column('tagline')
+ batch_op.drop_column('last_name')
+ batch_op.drop_column('first_name')
+
+ # ### end Alembic commands ###
diff --git a/migrations/versions/dc2ee4208976_followers.py b/migrations/versions/dc2ee4208976_followers.py
new file mode 100644
index 0000000..8289246
--- /dev/null
+++ b/migrations/versions/dc2ee4208976_followers.py
@@ -0,0 +1,33 @@
+"""followers
+
+Revision ID: dc2ee4208976
+Revises: 534f7d23c2a2
+Create Date: 2023-03-05 09:37:44.630003
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'dc2ee4208976'
+down_revision = '534f7d23c2a2'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('followers',
+ sa.Column('follower_id', sa.Integer(), nullable=True),
+ sa.Column('followed_id', sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(['followed_id'], ['user.id'], ),
+ sa.ForeignKeyConstraint(['follower_id'], ['user.id'], )
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('followers')
+ # ### end Alembic commands ###
diff --git a/migrations/versions/f77114abe39f_new_fields_in_user_model.py b/migrations/versions/f77114abe39f_new_fields_in_user_model.py
new file mode 100644
index 0000000..d3b123a
--- /dev/null
+++ b/migrations/versions/f77114abe39f_new_fields_in_user_model.py
@@ -0,0 +1,34 @@
+"""new fields in user model
+
+Revision ID: f77114abe39f
+Revises: 55c53aab4611
+Create Date: 2023-02-28 20:11:39.487884
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'f77114abe39f'
+down_revision = '55c53aab4611'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('user', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('last_seen', sa.DateTime(), nullable=True))
+ batch_op.drop_column('avatar')
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('user', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('avatar', sa.VARCHAR(length=120), nullable=True))
+ batch_op.drop_column('last_seen')
+
+ # ### end Alembic commands ###
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..e9fda5d
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,44 @@
+alembic==1.6.5
+blinker==1.4
+certifi==2021.5.30
+chardet==4.0.0
+click==8.0.1
+dnspython==2.1.0
+dominate==2.6.0
+elasticsearch==7.13.3
+email-validator==1.1.3
+Flask
+Flask-HTTPAuth==4.4.0
+Flask-Login==0.5.0
+Flask-Mail==0.9.1
+Flask-Migrate==3.0.1
+Flask-Moment==1.0.1
+Flask-SQLAlchemy==2.5.1
+Flask-WTF==0.15.1
+httpie==2.4.0
+idna==2.10
+itsdangerous==2.0.1
+Jinja2==3.0.1
+Mako==1.1.4
+MarkupSafe==2.0.1
+Pygments==2.9.0
+PyJWT==2.1.0
+PySocks==1.7.1
+python-dateutil==2.8.1
+python-dotenv==0.18.0
+python-editor==1.0.4
+pytz==2021.1
+redis==3.5.3
+requests==2.25.1
+requests-toolbelt==0.9.1
+rq==1.9.0
+six==1.16.0
+SQLAlchemy==1.4.20
+urllib3==1.26.6
+visitor==0.1.3
+Werkzeug==2.0.1
+WTForms==2.3.3
+
+# requirements for Heroku
+#psycopg2==2.9.1
+#gunicorn==20.1.0
diff --git a/tests.py b/tests.py
new file mode 100644
index 0000000..ea08372
--- /dev/null
+++ b/tests.py
@@ -0,0 +1,101 @@
+from datetime import datetime, timedelta
+import unittest
+from app import create_app, db
+from app.models import User, Posts
+from config import Config
+
+class TestConfig(Config):
+ TESTING = True
+ SQLALCHEMY_DATABASE_URI = 'sqlite://'
+
+class UserModelCase(unittest.TestCase):
+ def setUp(self):
+ self.app = create_app(TestConfig)
+ self.app_context = self.app.app_context()
+ self.app_context.push()
+ db.create_all()
+
+ def tearDown(self):
+ db.session.remove()
+ db.drop_all()
+ self.app_context.pop()
+
+ def test_password_hashing(self):
+ u = User(email='robert@example.com', first_name='robert')
+ u.set_password('cat')
+ self.assertFalse(u.check_password('dog'))
+ self.assertTrue(u.check_password('cat'))
+
+ def test_avatar(self):
+ u = User(email='john@example.com', first_name='john')
+ self.assertEqual(u.avatar(128), ('https://www.gravatar.com/avatar/'
+ 'd4c74594d841139328695756648b6bd6'
+ '?d=identicon&s=128'))
+
+ def test_follow(self):
+ u1 = User(first_name='john', email='john@example.com')
+ u2 = User(first_name='susan', email='susan@example.com')
+ db.session.add(u1)
+ db.session.add(u2)
+ db.session.commit()
+ self.assertEqual(u1.followed.all(), [])
+ self.assertEqual(u1.followers.all(), [])
+
+ u1.follow(u2)
+ db.session.commit()
+ self.assertTrue(u1.is_following(u2))
+ self.assertEqual(u1.followed.count(), 1)
+ self.assertEqual(u1.followed.first().first_name, 'susan')
+ self.assertEqual(u2.followers.count(), 1)
+ self.assertEqual(u2.followers.first().first_name, 'john')
+
+ u1.unfollow(u2)
+ db.session.commit()
+ self.assertFalse(u1.is_following(u2))
+ self.assertEqual(u1.followed.count(), 0)
+ self.assertEqual(u2.followers.count(), 0)
+
+ def test_follow_posts(self):
+ # create four users
+ u1 = User(first_name='john', email='john@example.com')
+ u2 = User(first_name='susan', email='susan@example.com')
+ u3 = User(first_name='mary', email='mary@example.com')
+ u4 = User(first_name='david', email='david@example.com')
+ db.session.add_all([u1, u2, u3, u4])
+
+ # create four posts
+ now = datetime.utcnow()
+ p1 = Posts(body="post from john",
+ author=u1,
+ timestamp=now + timedelta(seconds=1))
+ p2 = Posts(body="post from susan",
+ author=u2,
+ timestamp=now + timedelta(seconds=4))
+ p3 = Posts(body="post from mary",
+ author=u3,
+ timestamp=now + timedelta(seconds=3))
+ p4 = Posts(body="post from david",
+ author=u4,
+ timestamp=now + timedelta(seconds=2))
+ db.session.add_all([p1, p2, p3, p4])
+ db.session.commit()
+
+ #setup followers
+ u1.follow(u2) # john follows susan
+ u1.follow(u4) # john follows david
+ u2.follow(u3) # susan follows mary
+ u3.follow(u4) # mary follows david
+ db.session.commit()
+
+ # check the followed posts of each user
+ f1 = u1.followed_posts().all()
+ f2 = u2.followed_posts().all()
+ f3 = u3.followed_posts().all()
+ f4 = u4.followed_posts().all()
+ self.assertEqual(f1, [p2, p4, p1])
+ self.assertEqual(f2, [p2, p3])
+ self.assertEqual(f3, [p3, p4])
+ self.assertEqual(f4, [p4])
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
\ No newline at end of file