├── .env_example ├── .env_template ├── .gitignore ├── README.md ├── assets ├── 1.png ├── 2.png ├── 3.png └── 4.png ├── blog ├── Dockerfile ├── __init__.py ├── about │ ├── __init__.py │ └── about_controller.py ├── api │ ├── __init__.py │ └── api_controller.py ├── app.py ├── contact │ ├── __init__.py │ └── contact_controller.py ├── posts │ ├── __init__.py │ ├── posts_controller.py │ └── service │ │ ├── __init__.py │ │ └── posts_blog_service.py ├── static │ ├── css │ │ ├── clean-blog.css │ │ ├── clean-blog.min.css │ │ ├── semantic-ui-comment.css │ │ └── timeline.css │ ├── img │ │ ├── about-bg.jpg │ │ ├── contact-bg.jpg │ │ ├── home-bg.jpg │ │ ├── post-bg.jpg │ │ ├── post-sample-image.jpg │ │ └── subash_prakash.jpeg │ ├── js │ │ ├── app.js │ │ ├── clean-blog.js │ │ ├── clean-blog.min.js │ │ ├── comments.js │ │ ├── contact_me.js │ │ └── jqBootstrapValidation.js │ ├── scss │ │ ├── _bootstrap-overrides.scss │ │ ├── _contact.scss │ │ ├── _footer.scss │ │ ├── _global.scss │ │ ├── _masthead.scss │ │ ├── _mixins.scss │ │ ├── _navbar.scss │ │ ├── _post.scss │ │ ├── _variables.scss │ │ └── clean-blog.scss │ └── vendor │ │ ├── bootstrap │ │ ├── css │ │ │ ├── bootstrap-grid.css │ │ │ ├── bootstrap-grid.css.map │ │ │ ├── bootstrap-grid.min.css │ │ │ ├── bootstrap-grid.min.css.map │ │ │ ├── bootstrap-reboot.css │ │ │ ├── bootstrap-reboot.css.map │ │ │ ├── bootstrap-reboot.min.css │ │ │ ├── bootstrap-reboot.min.css.map │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.css.map │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.css.map │ │ └── js │ │ │ ├── bootstrap.bundle.js │ │ │ ├── bootstrap.bundle.js.map │ │ │ ├── bootstrap.bundle.min.js │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ ├── bootstrap.js │ │ │ ├── bootstrap.js.map │ │ │ ├── bootstrap.min.js │ │ │ └── bootstrap.min.js.map │ │ ├── fontawesome-free │ │ ├── css │ │ │ ├── all.css │ │ │ ├── all.min.css │ │ │ ├── brands.css │ │ │ ├── brands.min.css │ │ │ ├── fontawesome.css │ │ │ ├── fontawesome.min.css │ │ │ ├── regular.css │ │ │ ├── regular.min.css │ │ │ ├── solid.css │ │ │ ├── solid.min.css │ │ │ ├── svg-with-js.css │ │ │ ├── svg-with-js.min.css │ │ │ ├── v4-shims.css │ │ │ └── v4-shims.min.css │ │ └── webfonts │ │ │ ├── fa-brands-400.eot │ │ │ ├── fa-brands-400.svg │ │ │ ├── fa-brands-400.ttf │ │ │ ├── fa-brands-400.woff │ │ │ ├── fa-brands-400.woff2 │ │ │ ├── fa-regular-400.eot │ │ │ ├── fa-regular-400.svg │ │ │ ├── fa-regular-400.ttf │ │ │ ├── fa-regular-400.woff │ │ │ ├── fa-regular-400.woff2 │ │ │ ├── fa-solid-900.eot │ │ │ ├── fa-solid-900.svg │ │ │ ├── fa-solid-900.ttf │ │ │ ├── fa-solid-900.woff │ │ │ └── fa-solid-900.woff2 │ │ └── jquery │ │ ├── jquery.js │ │ ├── jquery.min.js │ │ ├── jquery.min.map │ │ ├── jquery.slim.js │ │ ├── jquery.slim.min.js │ │ └── jquery.slim.min.map └── templates │ └── blog │ ├── about.html │ ├── base.html │ ├── contact.html │ ├── index.html │ └── post.html ├── blog_admin ├── Dockerfile ├── __init__.py ├── api │ ├── __init__.py │ └── api_controller.py ├── app.py ├── auth │ ├── __init__.py │ └── auth_controller.py ├── posts │ ├── __init__.py │ ├── comments_controller.py │ ├── posts_controller.py │ └── service │ │ ├── __init__.py │ │ └── _posts_service.py ├── static │ ├── css │ │ └── amsify.suggestags.css │ └── js │ │ ├── app.js │ │ ├── comments_app.js │ │ ├── jquery.amsify.suggestags.js │ │ └── posts_app.js └── templates │ ├── auth │ ├── login.html │ └── login_intrim.html │ └── dashboard │ ├── add_post.html │ ├── comments_moderation.html │ ├── dashboard.html │ ├── edit_post.html │ ├── overview.html │ └── posts.html ├── common ├── __init__.py ├── controller │ ├── __init__.py │ ├── comments_api_controller.py │ ├── images_api_controller.py │ └── posts_api_controller.py ├── models │ ├── __init__.py │ ├── comments_model.py │ ├── images_model.py │ ├── posts_model.py │ ├── reply_model.py │ ├── tags_model.py │ └── users_model.py └── services │ ├── __init__.py │ ├── comment_state_enums.py │ ├── comments_service.py │ ├── posts_service.py │ ├── reply_service.py │ ├── tags_service.py │ ├── users_service.py │ └── utility.py ├── docker-compose-mysql.yml ├── docker-compose.yml ├── instance ├── __init__.py └── config.py ├── requirements.txt └── tests ├── __init__.py └── blog ├── __init__.py └── unit ├── __init__.py └── test_blog_post_service.py /.env_example: -------------------------------------------------------------------------------- 1 | # dev, test, prod are the options 2 | APP_CONFIG=prod 3 | FLASK_BLOG_PORT=9080 4 | FLASK_ADMIN_PORT=5001 5 | FLASK_HOST=0.0.0.0 6 | # change according to the env 7 | DB_USER=root 8 | # change according to the env 9 | DB_PASSWORD=root123 10 | # change according to the env 11 | #DB_DATABASE_NAME=blog 12 | DB_DRIVER=mysql 13 | # use while on docker 14 | DB_HOST=mysql_db 15 | #Local computer running DB 16 | #DB_HOST=192.168.12.99 17 | #DB_HOST=localhost 18 | DB_NAME=blog 19 | #DB_NAME=test_blog 20 | DB_PORT=3306 21 | #ReCaptcha Config 22 | RECAPTCHA_SITE_KEY=6LfHcysUAAAAALzasd123yxcyxcasase123b 23 | RECAPTCHA_SITE_SECRET=6LfHcysUAAA123cxcr5151asacas1hVuO 24 | 25 | #SMTP Configs 26 | MAIL_USERNAME=example@example.com 27 | MAIL_PASSWORD=1234 28 | 29 | # Caching app config 30 | CACHE_TYPE=simple 31 | CACHE_DEFAULT_TIMEOUT=300 32 | SECRET_KEY=a1123123129a17ea21d12312aysxasdasda12326adfebc12312aycyx932e22312dea 33 | UPLOAD_FOLDER=uploads 34 | 35 | # The setting for admin user/password 36 | ADMIN_USERNAME=admin 37 | PASSWORD=1234 38 | # The first name, accessible for posting 39 | F_NAME=Name 40 | EMAIL=example@example.com 41 | post_init_limit=10 42 | 43 | #Blog content details 44 | blog_header=Name Blog 45 | blog_subheader=Blog on my daily life 46 | social_git=https://example.com 47 | social_linkedin=https://example.com 48 | social_stack=https://example.com -------------------------------------------------------------------------------- /.env_template: -------------------------------------------------------------------------------- 1 | # dev, test, prod are the options 2 | APP_CONFIG=dev 3 | FLASK_BLOG_PORT=9090 4 | FLASK_ADMIN_PORT=5005 5 | FLASK_HOST=0.0.0.0 6 | # change according to the env 7 | DB_USER=root 8 | # change according to the env 9 | DB_PASSWORD=strongpassword 10 | # change according to the env 11 | DB_DRIVER=mysql 12 | # use while on docker. Cannot change the container name 13 | DB_HOST=mysql_db 14 | #Local computer running DB (If running db separetly) 15 | #DB_HOST=localhost 16 | # Database name of your wish 17 | DB_NAME=yourblogdb 18 | # Change to test db when working with testing 19 | #DB_NAME=test_blog 20 | # Default mysql port specification 21 | DB_PORT=3306 22 | 23 | #ReCaptcha Config 24 | RECAPTCHA_SITE_KEY=YOURRECAPTCHA_SITE_KEY 25 | RECAPTCHA_SITE_SECRET=YOURRECAPTCHA_SECRET 26 | 27 | #SMTP Configs (Email and password of the blog email generated) 28 | MAIL_USERNAME=admin@xyz.com 29 | MAIL_PASSWORD=verystrongpassword 30 | 31 | # Caching app config. (Leave it default for basic caching) 32 | CACHE_TYPE=simple 33 | CACHE_DEFAULT_TIMEOUT=300 34 | 35 | # Upload of image folder 36 | SECRET_KEY=yoursecret 37 | UPLOAD_FOLDER=uploads 38 | 39 | # The setting for admin user/password. This will be anyway changed 40 | ADMIN_USERNAME=youradminusername 41 | PASSWORD=useradminpassword 42 | # The first name, accessible for posting 43 | F_NAME=AuthorFirstName 44 | EMAIL=AuthorEmailId 45 | 46 | #Default post limit leave it to 10 47 | post_init_limit=10 48 | 49 | #Blog content details 50 | blog_header=CHANGETHEBLOGTITLE 51 | blog_subheader=CHANGETHEBLOGSUBTITLE 52 | social_git=LINK1 53 | social_linkedin=LINK2 54 | social_stack=LINK3 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | #instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | .vscode/ 113 | .vscode/* 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # Uploading content 133 | uploads/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create your blogging website from groundup full configured :notebook: 2 | 3 | ## Motivation: 4 | Everybody is motivated to create a blog website to write on their activities, or share something with their friend and family. But, developing something on own is time consuming and often people resort to the blogging sites which would read your content. Keeping this in mind, this web app is created with basic functionality to bring up docker containers on a purchased VM (aws, digitalocean, heroku etc..) and quickly launch with few commands. 5 | 6 | ## Setup: 7 | Important part of the setup is the centered with env_template, let us look that in details: 8 | | Configuration | Description | Values | 9 | | ------------- |:-------------:| -----:| 10 | | APP_CONFIG | Config for the app, ideally will point to different database | dev, test, prod | 11 | | FLASK_BLOG_PORT | PORT to run the blog app | Default:9090 | 12 | | FLASK_ADMIN_PORT | PORT to run blog admin | Default:5005 | 13 | | FLASK_HOST | FLASK listener | Defaults:0.0.0.0 and do not change | 14 | | DB_USER | Database Username | String username | 15 | | DB_PASSWORD | Database Password | String password which should be strong | 16 | | DB_DRIVER | Database driver | Defaults: mysql (Do not change, until using another db) | 17 | | DB_HOST | Your database host | Docker:mysql_db, Others: Hostname of the VM where DB is present | 18 | | DB_NAME | Name of the database | String database name | 19 | | DB_PORT | Mysql PORT | Defaults: 3306 | 20 | | RECAPTCHA_SITE_KEY | Generated RECAPTCHA key | String key needed to handle while commenting | 21 | | RECAPTCHA_SITE_SECRET | Generated RECAPTCHA Secret | String Secret | 22 | | MAIL_USERNAME | Admin email id | Any created email id as String | 23 | | MAIL_PASSWORD | Admin email id password | Any created email password | 24 | | CACHE_TYPE | Default Flask caching | Defaults:simple | 25 | | CACHE_DEFAULT_TIMEOUT | Default caching timeout | Default:300s | 26 | | SECRET_KEY | Flask Secret | Strong secret | 27 | | UPLOAD_FOLDER | Folder to hold the uploaded content | Defaults:uploads | 28 | | ADMIN_USERNAME | root username for blog admin | username as String | 29 | | PASSWORD | root user initial password | password as String | 30 | | F_NAME | root user name, for posting | String | 31 | | EMAIL | root user email for replies | String | 32 | | post_init_limit | Limit of number of post | Defaults:10 | 33 | | blog_header | Name of your blog | String blog name | 34 | | blog_subheader | Subheader for your blog | Subname | 35 | | social_git | Git link | String | 36 | | social_linkedin | Linkedin link | String | 37 | | social_stack | Stackoverflow Link | String | 38 | 39 | 40 | Once, after creating the environments, rename .env_template to .env (This file will not be shared or push to github). Also, I have added and .env_example to fasten the process. 41 | 42 | ## Recaptcha setup: 43 | Please follow the google recaptcha setup here: https://developers.google.com/recaptcha/docs/v3 and add the RECAPTCHA_SITE_KEY and RECAPTCHA_SITE_SECRET. 44 | 45 | ## Building and running Database: 46 | `docker-compose -f docker-compose-mysql.yml up -d` 47 | 48 | ## Building and running blog_admin: 49 | `docker-compose build blog-admin-app` 50 | `docker-compose up -d blog-admin-app` 51 | 52 | ## Building and running blog_app: 53 | `docker-compose build blog-app` 54 | `docker-compose up -d blog-app` 55 | 56 | ## Run locally: 57 | Running blog_admin locally: 58 | 1. First start the mysql database as above and point the same configurations in .env file. 59 | 2. Then export the FLASK_APP from the root directory of the project which is SimplisticBlogger in this case as export `FLASK_APP=blog_admin/app`. 60 | 3. To run, then use flask run --port 5000 61 | 62 | Running only blog locally: 63 | 1. First start the admin so that the author for the blog is created and create a post if you wish to. 64 | 2. First start the mysql database as above and point the same configurations in .env file. 65 | 3. Then export the FLASK_APP from the root directory of the project which is SimplisticBlogger in this case as export `FLASK_APP=blog/app`. 66 | 4. To run, then use flask run --port 5001 67 | 68 | ## Some screenshots: 69 | 1. Blog Admin: 70 | ![Admin login](assets/1.png) 71 | ![Admin Dashboard](assets/2.png) 72 | ![Admin Posting](assets/3.png) 73 | 1. Blog App: 74 | ![Blog App](assets/4.png) 75 | 76 | ## Extensions :pencil2:: 77 | 1. Currently the app uses Summernote editor for making a blog post, future look to integrate markdown editors. 78 | 2. Improving comments and replies. (The current implementation only considers a simple way to do stuff, this can be improved) 79 | 3. About me page can be attempted to be made dynamic. 80 | 81 | ## Contact for any information/pull: 82 | [Subash Prakash](https://github.com/prakass1) 83 | 84 | ### Give a :star: if this project has been useful to you in any ways. 85 | 86 | ### Acknowledgements: 87 | 1. [bootstrap clean blog](https://startbootstrap.com/theme/clean-blog) 88 | 2. [Summernote](https://summernote.org/) 89 | -------------------------------------------------------------------------------- /assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/assets/1.png -------------------------------------------------------------------------------- /assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/assets/2.png -------------------------------------------------------------------------------- /assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/assets/3.png -------------------------------------------------------------------------------- /assets/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/assets/4.png -------------------------------------------------------------------------------- /blog/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.9-slim 2 | RUN apt-get update && apt-get install -y gcc && apt-get install -y default-libmysqlclient-dev 3 | 4 | COPY SimplisticBlogger/blog /app/blog 5 | COPY SimplisticBlogger/requirements.txt /app/blog 6 | COPY SimplisticBlogger/common /app/common 7 | COPY SimplisticBlogger/instance /app/instance 8 | COPY SimplisticBlogger/.env /app/blog 9 | WORKDIR /app/blog 10 | 11 | RUN pip install --upgrade pip 12 | 13 | RUN pip install -r requirements.txt 14 | 15 | CMD flask run --host ${FLASK_HOST} --port ${FLASK_BLOG_PORT} 16 | 17 | #CMD ["flask", "run", "--port ${FLASK_BLOG_PORT}"] 18 | -------------------------------------------------------------------------------- /blog/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from os import environ 3 | from dotenv import load_dotenv, find_dotenv 4 | from flask_wtf.csrf import CSRFProtect 5 | from instance.config import app_config 6 | from common import db, cache, mail 7 | 8 | load_dotenv(find_dotenv()) 9 | 10 | blog_header = environ.get("blog_header") 11 | blog_subheader = environ.get("blog_subheader") 12 | social_git = "#" if environ.get( 13 | "social_git") == "" else environ.get("social_git") 14 | social_linkedin = "#" if environ.get( 15 | "social_linkedin") == "" else environ.get("social_linkedin") 16 | social_stack = "#" if environ.get( 17 | "social_stack") == "" else environ.get("social_stack") 18 | 19 | resp = {"blog_header": blog_header, "blog_subheader": blog_subheader, 20 | "social_git": social_git, "social_linkedin": social_linkedin, "social_stack": social_stack} 21 | 22 | 23 | csrf_protect = CSRFProtect() 24 | 25 | def create_app(config_name): 26 | # More on DB init here... 27 | ''' Create Flask app ''' 28 | app = Flask(__name__, instance_relative_config=True) 29 | app.config.from_object(app_config[config_name]) 30 | app.config.from_pyfile('config.py') 31 | 32 | # init sql-alchemy 33 | db.init_app(app) 34 | 35 | # csrf protection 36 | csrf_protect.init_app(app) 37 | 38 | # init cache to app 39 | cache.init_app(app) 40 | 41 | # Email 42 | mail.init_app(app) 43 | 44 | with app.app_context(): 45 | # Module imports 46 | from blog.api import api_controller 47 | from blog.about import about_controller 48 | from blog.contact import contact_controller 49 | from blog.posts import posts_controller 50 | from common.controller import posts_api_controller 51 | from common.controller import images_api_controller 52 | from common.controller import comments_api_controller 53 | 54 | # Clear cache 55 | cache.clear() 56 | 57 | # create db 58 | db.create_all() 59 | 60 | # from common.models import users_model 61 | 62 | # Register blueprints 63 | app.register_blueprint(api_controller.api_bp) 64 | app.register_blueprint(about_controller.about_bp) 65 | app.register_blueprint(contact_controller.contact_bp) 66 | app.register_blueprint(posts_controller.posts_bp, url_prefix="/blog") 67 | app.register_blueprint(posts_api_controller.posts_api_bp) 68 | app.register_blueprint(images_api_controller.image_bp) 69 | app.register_blueprint(comments_api_controller.comments_bp) 70 | 71 | return app -------------------------------------------------------------------------------- /blog/about/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/about/__init__.py -------------------------------------------------------------------------------- /blog/about/about_controller.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | from blog import resp 3 | 4 | about_bp = Blueprint("about", __name__) 5 | 6 | @about_bp.route("/about") 7 | def about(): 8 | return render_template("blog/about.html", resp=resp) -------------------------------------------------------------------------------- /blog/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/api/__init__.py -------------------------------------------------------------------------------- /blog/api/api_controller.py: -------------------------------------------------------------------------------- 1 | from flask import redirect, url_for, render_template 2 | from flask import Blueprint 3 | 4 | api_bp = Blueprint("api", __name__) 5 | 6 | @api_bp.route("/", methods=["GET"]) 7 | def index(): 8 | return redirect(url_for('posts.blog')) -------------------------------------------------------------------------------- /blog/app.py: -------------------------------------------------------------------------------- 1 | from blog import create_app 2 | from os import environ 3 | 4 | config_name = environ.get("APP_CONFIG") 5 | app = create_app(config_name) 6 | 7 | if __name__ == '__main__': 8 | app.run(port=str(environ.get("FLASK_ADMIN_PORT")), host="0.0.0.0") 9 | -------------------------------------------------------------------------------- /blog/contact/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/contact/__init__.py -------------------------------------------------------------------------------- /blog/contact/contact_controller.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | from blog import resp 3 | 4 | contact_bp = Blueprint("contact", __name__) 5 | 6 | @contact_bp.route("/contact") 7 | def contact(): 8 | return render_template("blog/contact.html", resp=resp) -------------------------------------------------------------------------------- /blog/posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/posts/__init__.py -------------------------------------------------------------------------------- /blog/posts/posts_controller.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Blueprint, render_template, request, redirect, url_for 3 | from common.services.posts_service import PostService 4 | from common.services import tags_service, comments_service 5 | from blog import resp 6 | from blog import cache 7 | from blog.posts.service import posts_blog_service 8 | 9 | posts_bp = Blueprint( 10 | "posts", __name__) 11 | 12 | 13 | @posts_bp.route("/", methods=["GET"]) 14 | def blog(): 15 | post_obj = PostService() 16 | prev_limit = request.args.get("prev_limit") 17 | posts = post_obj.get_all_posts(order_by=True) 18 | print(cache.get("all-posts-ordered")) 19 | if not prev_limit and posts: 20 | post_len = len(posts) 21 | prev_limit = os.environ.get("post_init_limit") 22 | posts_data = posts[:int(prev_limit)] 23 | elif prev_limit and posts: 24 | post_len = len(posts) 25 | new_limit = int(prev_limit) + int(os.environ.get("post_init_limit")) 26 | posts_data = posts[int(prev_limit):new_limit] 27 | posts_serialized = [posts_blog_service.serialize( 28 | post) for post in posts_data] 29 | print(cache.get("more_posts")) 30 | return {"posts_resp": posts_serialized, 31 | "posts_html_reponse": posts_blog_service.get_posts_html_resp(posts_serialized, len(posts_data), new_limit), 32 | "prev_limit": new_limit, 33 | "load_more": True, 34 | "post_len": post_len} 35 | else: 36 | posts_data = False 37 | post_len = 0 38 | 39 | return render_template("blog/index.html", 40 | posts_data=posts_data, 41 | tags_count=tags_service.get_tags_count(), 42 | resp=resp, prev_limit=prev_limit, 43 | post_len=post_len) 44 | 45 | 46 | @posts_bp.route("/post/") 47 | def get_post_title(blog_title): 48 | post_data = PostService().get_post_by_title(blog_title) 49 | # load all comments under_moderation 50 | comments = comments_service.CommentService.get_comments( 51 | post_db_obj=post_data[0], is_admin=False) 52 | print(comments) 53 | if len(post_data) > 0: 54 | data_resp = {"post_data": post_data[0], 55 | "tags": post_data[1], "comments": comments} 56 | else: 57 | data_resp = {"post_data": False, "tags": False, "comments": comments} 58 | 59 | return render_template("blog/post.html", data_resp=data_resp, resp=resp, site_key=os.environ.get("RECAPTCHA_SITE_KEY")) 60 | 61 | 62 | @posts_bp.route("/post/category/") 63 | def get_post_tag(tag): 64 | prev_limit = request.args.get("prev_limit") 65 | posts = tags_service.get_post_tags(tag) 66 | if not prev_limit and posts: 67 | post_len = len(posts) 68 | prev_limit = os.environ.get("post_init_limit") 69 | posts_data = posts[:int(prev_limit)] 70 | elif prev_limit and posts: 71 | post_len = len(posts) 72 | new_limit = int(prev_limit) + int(os.environ.get("post_init_limit")) 73 | posts_data = posts[int(prev_limit):new_limit] 74 | posts_serialized = [posts_blog_service.serialize( 75 | post) for post in posts_data] 76 | return {"posts_resp": posts_serialized, 77 | "posts_html_reponse": posts_blog_service.get_posts_html_resp(posts_serialized, len(posts_data), new_limit), 78 | "prev_limit": new_limit, 79 | "load_more": True, 80 | "post_len": post_len} 81 | else: 82 | return redirect(url_for("posts.blog")) 83 | 84 | return render_template("blog/index.html", 85 | posts_data=posts_data,tags_count=tags_service.get_tags_count(), resp=resp, prev_limit=prev_limit, post_len=post_len) 86 | -------------------------------------------------------------------------------- /blog/posts/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/posts/service/__init__.py -------------------------------------------------------------------------------- /blog/posts/service/posts_blog_service.py: -------------------------------------------------------------------------------- 1 | from flask import url_for 2 | from blog import cache 3 | 4 | 5 | def serialize(obj): 6 | return { 7 | "content": obj.content, 8 | "posted_date": obj.posted_date.strftime('%B %d, %Y'), 9 | "title": obj.title, 10 | "author": obj.author 11 | } 12 | 13 | def get_posts_html_resp(serialized_obj, post_len, new_limit): 14 | str_concat = "" 15 | for post_obj in serialized_obj: 16 | if cache.get(str(post_len)): 17 | str_concat = cache.get(str(post_len)) 18 | else: 19 | str_concat += "" + "

" + post_obj["title"] + \ 20 | "

" + "
" 22 | cache.set(str(post_len), str_concat, timeout=50) 23 | return str_concat 24 | #if post_len > limit: 25 | # # Add load more content 26 | # str_concat += "
" + "" + \ 27 | # "" 28 | #return str_concat 29 | -------------------------------------------------------------------------------- /blog/static/css/clean-blog.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - Clean Blog v5.0.10 (https://startbootstrap.com/theme/clean-blog) 3 | * Copyright 2013-2020 Start Bootstrap 4 | * Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-clean-blog/blob/master/LICENSE) 5 | */body{font-size:20px;color:#212529;font-family:Lora,'Times New Roman',serif}p{line-height:1.5;margin:30px 0}p a{text-decoration:underline}h1,h2,h3,h4,h5,h6{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif}a{color:#212529;transition:all .2s}a:focus,a:hover{color:#0085a1}blockquote{font-style:italic;color:#868e96}.section-heading{font-size:36px;font-weight:700;margin-top:60px}.caption{font-size:14px;font-style:italic;display:block;margin:0;padding:10px;text-align:center;border-bottom-right-radius:5px;border-bottom-left-radius:5px}::-moz-selection{color:#fff;background:#0085a1;text-shadow:none}::selection{color:#fff;background:#0085a1;text-shadow:none}img::-moz-selection{color:#fff;background:0 0}img::selection{color:#fff;background:0 0}img::-moz-selection{color:#fff;background:0 0}#mainNav{position:absolute;border-bottom:1px solid #e9ecef;background-color:#fff;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif}#mainNav .navbar-brand{font-weight:800;color:#343a40}#mainNav .navbar-toggler{font-size:12px;font-weight:800;padding:13px;text-transform:uppercase;color:#343a40}#mainNav .navbar-nav>li.nav-item>a{font-size:12px;font-weight:800;letter-spacing:1px;text-transform:uppercase}@media only screen and (min-width:992px){#mainNav{border-bottom:1px solid transparent;background:0 0}#mainNav .navbar-brand{padding:10px 20px;color:#fff}#mainNav .navbar-brand:focus,#mainNav .navbar-brand:hover{color:rgba(255,255,255,.8)}#mainNav .navbar-nav>li.nav-item>a{padding:10px 20px;color:#fff}#mainNav .navbar-nav>li.nav-item>a:focus,#mainNav .navbar-nav>li.nav-item>a:hover{color:rgba(255,255,255,.8)}}@media only screen and (min-width:992px){#mainNav{transition:background-color .2s;transform:translate3d(0,0,0);-webkit-backface-visibility:hidden;backface-visibility:hidden}#mainNav.is-fixed{position:fixed;top:-67px;transition:transform .2s;border-bottom:1px solid #fff;background-color:rgba(255,255,255,.9)}#mainNav.is-fixed .navbar-brand{color:#212529}#mainNav.is-fixed .navbar-brand:focus,#mainNav.is-fixed .navbar-brand:hover{color:#0085a1}#mainNav.is-fixed .navbar-nav>li.nav-item>a{color:#212529}#mainNav.is-fixed .navbar-nav>li.nav-item>a:focus,#mainNav.is-fixed .navbar-nav>li.nav-item>a:hover{color:#0085a1}#mainNav.is-visible{transform:translate3d(0,100%,0)}}header.masthead{margin-bottom:50px;background:no-repeat center center;background-color:#868e96;background-attachment:scroll;position:relative;background-size:cover}header.masthead .overlay{position:absolute;top:0;left:0;height:100%;width:100%;background-color:#212529;opacity:.5}header.masthead .page-heading,header.masthead .post-heading,header.masthead .site-heading{padding:200px 0 150px;color:#fff}@media only screen and (min-width:768px){header.masthead .page-heading,header.masthead .post-heading,header.masthead .site-heading{padding:200px 0}}header.masthead .page-heading,header.masthead .site-heading{text-align:center}header.masthead .page-heading h1,header.masthead .site-heading h1{font-size:50px;margin-top:0}header.masthead .page-heading .subheading,header.masthead .site-heading .subheading{font-size:24px;font-weight:300;line-height:1.1;display:block;margin:10px 0 0;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif}@media only screen and (min-width:768px){header.masthead .page-heading h1,header.masthead .site-heading h1{font-size:80px}}header.masthead .post-heading h1{font-size:35px}header.masthead .post-heading .meta,header.masthead .post-heading .subheading{line-height:1.1;display:block}header.masthead .post-heading .subheading{font-size:24px;font-weight:600;margin:10px 0 30px;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif}header.masthead .post-heading .meta{font-size:20px;font-weight:300;font-style:italic;font-family:Lora,'Times New Roman',serif}header.masthead .post-heading .meta a{color:#fff}@media only screen and (min-width:768px){header.masthead .post-heading h1{font-size:55px}header.masthead .post-heading .subheading{font-size:30px}}.post-preview>a{color:#212529}.post-preview>a:focus,.post-preview>a:hover{text-decoration:none;color:#0085a1}.post-preview>a>.post-title{font-size:30px;margin-top:30px;margin-bottom:10px}.post-preview>a>.post-subtitle{font-weight:300;margin:0 0 10px}.post-preview>.post-meta{font-size:18px;font-style:italic;margin-top:0;color:#868e96}.post-preview>.post-meta>a{text-decoration:none;color:#212529}.post-preview>.post-meta>a:focus,.post-preview>.post-meta>a:hover{text-decoration:underline;color:#0085a1}@media only screen and (min-width:768px){.post-preview>a>.post-title{font-size:36px}}.floating-label-form-group{font-size:14px;position:relative;margin-bottom:0;padding-bottom:.5em;border-bottom:1px solid #dee2e6}.floating-label-form-group input,.floating-label-form-group textarea{font-size:1.5em;position:relative;z-index:1;padding:0;resize:none;border:none;border-radius:0;background:0 0;box-shadow:none!important;font-family:Lora,'Times New Roman',serif}.floating-label-form-group input::-webkit-input-placeholder,.floating-label-form-group textarea::-webkit-input-placeholder{color:#868e96;font-family:Lora,'Times New Roman',serif}.floating-label-form-group label{font-size:.85em;line-height:1.764705882em;position:relative;z-index:0;top:2em;display:block;margin:0;transition:top .3s ease,opacity .3s ease;opacity:0}.floating-label-form-group .help-block{margin:15px 0}.floating-label-form-group-with-value label{top:0;opacity:1}.floating-label-form-group-with-focus label{color:#0085a1}form .form-group:first-child .floating-label-form-group{border-top:1px solid #dee2e6}footer{padding:50px 0 65px}footer .list-inline{margin:0;padding:0}footer .copyright{font-size:14px;margin-bottom:0;text-align:center}.btn{font-size:14px;font-weight:800;padding:15px 25px;letter-spacing:1px;text-transform:uppercase;border-radius:0;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif}.btn-primary{background-color:#0085a1;border-color:#0085a1}.btn-primary:active,.btn-primary:focus,.btn-primary:hover{color:#fff;background-color:#00657b!important;border-color:#00657b!important}.btn-lg{font-size:16px;padding:25px 35px} -------------------------------------------------------------------------------- /blog/static/css/semantic-ui-comment.css: -------------------------------------------------------------------------------- 1 | /*-------------- 2 | Comment 3 | ---------------*/ 4 | 5 | .ui.comments .comment { 6 | position: relative; 7 | background: none; 8 | margin: 0.5em 0em 0em; 9 | padding: 0.5em 0em 0em; 10 | border: none; 11 | border-top: none; 12 | line-height: 1.2; 13 | } 14 | 15 | .ui.comments .comment:first-child { 16 | margin-top: 0em; 17 | padding-top: 0em; 18 | } 19 | 20 | /*-------------------- 21 | Nested Comments 22 | ---------------------*/ 23 | 24 | .ui.comments .comment .comments { 25 | margin: 0em 0em 0.5em 0.5em; 26 | padding: 1em 0em 1em 1em; 27 | } 28 | 29 | .ui.comments .comment .comments:before { 30 | position: absolute; 31 | top: 0px; 32 | left: 0px; 33 | } 34 | 35 | .ui.comments .comment .comments .comment { 36 | border: none; 37 | border-top: none; 38 | background: none; 39 | } 40 | 41 | /*-------------- 42 | Avatar 43 | ---------------*/ 44 | 45 | .ui.comments .comment .avatar { 46 | display: block; 47 | width: 2.5em; 48 | height: auto; 49 | float: left; 50 | margin: 0.2em 0em 0em; 51 | } 52 | 53 | .ui.comments .comment img.avatar, 54 | .ui.comments .comment .avatar img { 55 | display: block; 56 | margin: 0em auto; 57 | width: 100%; 58 | height: 100%; 59 | border-radius: 0.25rem; 60 | } 61 | 62 | /*-------------- 63 | Content 64 | ---------------*/ 65 | 66 | .ui.comments .comment > .content { 67 | display: block; 68 | } 69 | 70 | /* If there is an avatar move content over */ 71 | 72 | .ui.comments .comment > .avatar ~ .content { 73 | margin-left: 3.5em; 74 | } 75 | 76 | /*-------------- 77 | Author 78 | ---------------*/ 79 | 80 | .ui.comments .comment .author { 81 | font-size: 1em; 82 | color: rgba(0, 0, 0, 0.87); 83 | font-weight: bold; 84 | } 85 | 86 | .ui.comments .comment a.author { 87 | cursor: pointer; 88 | } 89 | 90 | .ui.comments .comment a.author:hover { 91 | color: #1e70bf; 92 | } 93 | 94 | /*-------------- 95 | Metadata 96 | ---------------*/ 97 | 98 | .ui.comments .comment .metadata { 99 | display: inline-block; 100 | margin-left: 0.5em; 101 | color: rgba(0, 0, 0, 0.4); 102 | font-size: 0.875em; 103 | } 104 | 105 | .ui.comments .comment .metadata > * { 106 | display: inline-block; 107 | margin: 0em 0.5em 0em 0em; 108 | } 109 | 110 | .ui.comments .comment .metadata > :last-child { 111 | margin-right: 0em; 112 | } 113 | 114 | /*-------------------- 115 | Comment Text 116 | ---------------------*/ 117 | 118 | .ui.comments .comment .text { 119 | margin: 0.25em 0em 0.5em; 120 | font-size: 1em; 121 | word-wrap: break-word; 122 | color: rgba(0, 0, 0, 0.87); 123 | line-height: 1.3; 124 | } 125 | 126 | /*-------------------- 127 | User Actions 128 | ---------------------*/ 129 | 130 | .ui.comments .comment .actions { 131 | font-size: 0.875em; 132 | } 133 | 134 | .ui.comments .comment .actions a { 135 | cursor: pointer; 136 | display: inline-block; 137 | margin: 0em 0.75em 0em 0em; 138 | color: rgba(0, 0, 0, 0.4); 139 | } 140 | 141 | .ui.comments .comment .actions a:last-child { 142 | margin-right: 0em; 143 | } 144 | 145 | .ui.comments .comment .actions a.active, 146 | .ui.comments .comment .actions a:hover { 147 | color: rgba(0, 0, 0, 0.8); 148 | } 149 | 150 | /*-------------------- 151 | Reply Form 152 | ---------------------*/ 153 | 154 | .ui.comments > .reply.form { 155 | margin-top: 1em; 156 | } 157 | 158 | .ui.comments .comment .reply.form { 159 | width: 100%; 160 | margin-top: 1em; 161 | } 162 | 163 | .ui.comments .reply.form textarea { 164 | font-size: 1em; 165 | height: 12em; 166 | } 167 | 168 | /******************************* 169 | State 170 | *******************************/ 171 | 172 | .ui.collapsed.comments, 173 | .ui.comments .collapsed.comments, 174 | .ui.comments .collapsed.comment { 175 | display: none; 176 | } 177 | 178 | /******************************* 179 | Variations 180 | *******************************/ 181 | 182 | /*-------------------- 183 | Threaded 184 | ---------------------*/ 185 | 186 | .ui.threaded.comments .comment .comments { 187 | margin: -1.5em 0 -1em 1.25em; 188 | padding: 3em 0em 2em 2.25em; 189 | -webkit-box-shadow: -1px 0px 0px rgba(34, 36, 38, 0.15); 190 | box-shadow: -1px 0px 0px rgba(34, 36, 38, 0.15); 191 | } 192 | 193 | /*-------------------- 194 | Minimal 195 | ---------------------*/ 196 | 197 | .ui.minimal.comments .comment .actions { 198 | opacity: 0; 199 | position: absolute; 200 | top: 0px; 201 | right: 0px; 202 | left: auto; 203 | -webkit-transition: opacity 0.2s ease; 204 | transition: opacity 0.2s ease; 205 | -webkit-transition-delay: 0.1s; 206 | transition-delay: 0.1s; 207 | } 208 | 209 | .ui.minimal.comments .comment > .content:hover > .actions { 210 | opacity: 1; 211 | } 212 | 213 | /*------------------- 214 | Sizes 215 | --------------------*/ 216 | 217 | .ui.mini.comments { 218 | font-size: 0.78571429rem; 219 | } 220 | 221 | .ui.tiny.comments { 222 | font-size: 0.85714286rem; 223 | } 224 | 225 | .ui.small.comments { 226 | font-size: 0.92857143rem; 227 | } 228 | 229 | .ui.comments { 230 | font-size: 1rem; 231 | } 232 | 233 | .ui.large.comments { 234 | font-size: 1.14285714rem; 235 | } 236 | 237 | .ui.big.comments { 238 | font-size: 1.28571429rem; 239 | } 240 | 241 | .ui.huge.comments { 242 | font-size: 1.42857143rem; 243 | } 244 | 245 | .ui.massive.comments { 246 | font-size: 1.71428571rem; 247 | } 248 | 249 | 250 | .ui.comments:first-child { 251 | margin-top: 0; 252 | } -------------------------------------------------------------------------------- /blog/static/css/timeline.css: -------------------------------------------------------------------------------- 1 | /* ================ The Timeline ================ */ 2 | 3 | .timeline { 4 | position: relative; 5 | width: 660px; 6 | margin: 0 auto; 7 | margin-top: 20px; 8 | padding: 1em 0; 9 | list-style-type: none; 10 | } 11 | 12 | .timeline:before { 13 | position: absolute; 14 | left: 50%; 15 | top: 0; 16 | content: ' '; 17 | display: block; 18 | width: 6px; 19 | height: 100%; 20 | margin-left: -3px; 21 | background: rgb(80,80,80); 22 | background: -moz-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%); 23 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(30,87,153,1)), color-stop(100%,rgba(125,185,232,1))); 24 | background: -webkit-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%); 25 | background: -o-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%); 26 | background: -ms-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%); 27 | background: linear-gradient(to bottom, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%); 28 | 29 | z-index: 5; 30 | } 31 | 32 | .timeline li { 33 | padding: 1em 0; 34 | } 35 | 36 | .timeline li:after { 37 | content: ""; 38 | display: block; 39 | height: 0; 40 | clear: both; 41 | visibility: hidden; 42 | } 43 | 44 | .direction-l { 45 | position: relative; 46 | width: 300px; 47 | float: left; 48 | text-align: right; 49 | } 50 | 51 | .direction-r { 52 | position: relative; 53 | width: 300px; 54 | float: right; 55 | } 56 | 57 | .flag-wrapper { 58 | position: relative; 59 | display: inline-block; 60 | 61 | text-align: center; 62 | } 63 | 64 | .flag { 65 | position: static; 66 | display: inline; 67 | background: rgb(248,248,248); 68 | padding: 6px 10px; 69 | border-radius: 5px; 70 | 71 | font-weight: 600; 72 | text-align: left; 73 | } 74 | 75 | .direction-l .flag { 76 | -webkit-box-shadow: -1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15); 77 | -moz-box-shadow: -1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15); 78 | box-shadow: -1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15); 79 | } 80 | 81 | .direction-r .flag { 82 | -webkit-box-shadow: 1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15); 83 | -moz-box-shadow: 1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15); 84 | box-shadow: 1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15); 85 | } 86 | 87 | .direction-l .flag:before, 88 | .direction-r .flag:before { 89 | position: absolute; 90 | top: 50%; 91 | right: -40px; 92 | content: ' '; 93 | display: block; 94 | width: 12px; 95 | height: 12px; 96 | margin-top: -10px; 97 | background: #fff; 98 | border-radius: 10px; 99 | border: 4px solid #0085a1; 100 | z-index: 10; 101 | } 102 | 103 | .direction-r .flag:before { 104 | left: -40px; 105 | } 106 | 107 | .direction-l .flag:after { 108 | content: ""; 109 | position: absolute; 110 | left: 100%; 111 | top: 50%; 112 | height: 0; 113 | width: 0; 114 | margin-top: -8px; 115 | border: solid transparent; 116 | border-left-color: rgb(248,248,248); 117 | border-width: 8px; 118 | pointer-events: none; 119 | } 120 | 121 | .direction-r .flag:after { 122 | content: ""; 123 | position: absolute; 124 | right: 100%; 125 | top: 50%; 126 | height: 0; 127 | width: 0; 128 | margin-top: -8px; 129 | border: solid transparent; 130 | border-right-color: rgb(248,248,248); 131 | border-width: 8px; 132 | pointer-events: none; 133 | } 134 | 135 | .time-wrapper { 136 | display: inline; 137 | 138 | line-height: 1em; 139 | font-size: 0.66666em; 140 | color: #0085a1; 141 | vertical-align: middle; 142 | } 143 | 144 | .direction-l .time-wrapper { 145 | float: left; 146 | } 147 | 148 | .direction-r .time-wrapper { 149 | float: right; 150 | } 151 | 152 | .time { 153 | display: inline-block; 154 | padding: 4px 6px; 155 | background: rgb(248,248,248); 156 | } 157 | 158 | .desc { 159 | margin: 1em 0.75em 0 0; 160 | 161 | font-size: 0.77777em; 162 | font-style: italic; 163 | line-height: 1.5em; 164 | } 165 | 166 | .direction-r .desc { 167 | margin: 1em 0 0 0.75em; 168 | } 169 | 170 | .flag-old { 171 | color: #808080; 172 | } 173 | /* ================ Timeline Media Queries ================ */ 174 | 175 | @media screen and (max-width: 660px) { 176 | 177 | .timeline { 178 | width: 100%; 179 | padding: 4em 0 1em 0; 180 | } 181 | 182 | .timeline li { 183 | padding: 2em 0; 184 | } 185 | 186 | .direction-l, 187 | .direction-r { 188 | float: none; 189 | width: 100%; 190 | 191 | text-align: center; 192 | } 193 | 194 | .flag-wrapper { 195 | text-align: center; 196 | } 197 | 198 | .flag { 199 | background: rgb(255,255,255); 200 | z-index: 15; 201 | } 202 | 203 | .direction-l .flag:before, 204 | .direction-r .flag:before { 205 | position: absolute; 206 | top: -30px; 207 | left: 50%; 208 | content: ' '; 209 | display: block; 210 | width: 12px; 211 | height: 12px; 212 | margin-left: -9px; 213 | background: #fff; 214 | border-radius: 10px; 215 | border: 4px solid #0085a1; 216 | z-index: 10; 217 | } 218 | 219 | .direction-l .flag:after, 220 | .direction-r .flag:after { 221 | content: ""; 222 | position: absolute; 223 | left: 50%; 224 | top: -8px; 225 | height: 0; 226 | width: 0; 227 | margin-left: -8px; 228 | border: solid transparent; 229 | border-bottom-color: rgb(255,255,255); 230 | border-width: 8px; 231 | pointer-events: none; 232 | } 233 | 234 | .time-wrapper { 235 | display: block; 236 | position: relative; 237 | margin: 4px 0 0 0; 238 | z-index: 14; 239 | } 240 | 241 | .direction-l .time-wrapper { 242 | float: none; 243 | } 244 | 245 | .direction-r .time-wrapper { 246 | float: none; 247 | } 248 | 249 | .desc { 250 | position: relative; 251 | margin: 1em 0 0 0; 252 | padding: 1em; 253 | background: rgb(245,245,245); 254 | -webkit-box-shadow: 0 0 1px rgba(0,0,0,0.20); 255 | -moz-box-shadow: 0 0 1px rgba(0,0,0,0.20); 256 | box-shadow: 0 0 1px rgba(0,0,0,0.20); 257 | 258 | z-index: 15; 259 | } 260 | 261 | .direction-l .desc, 262 | .direction-r .desc { 263 | position: relative; 264 | margin: 1em 1em 0 1em; 265 | padding: 1em; 266 | 267 | z-index: 15; 268 | } 269 | 270 | } 271 | 272 | @media screen and (min-width: 400px) and (max-width: 660px) { 273 | 274 | .direction-l .desc, 275 | .direction-r .desc { 276 | margin: 1em 4em 0 4em; 277 | } 278 | 279 | } -------------------------------------------------------------------------------- /blog/static/img/about-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/img/about-bg.jpg -------------------------------------------------------------------------------- /blog/static/img/contact-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/img/contact-bg.jpg -------------------------------------------------------------------------------- /blog/static/img/home-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/img/home-bg.jpg -------------------------------------------------------------------------------- /blog/static/img/post-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/img/post-bg.jpg -------------------------------------------------------------------------------- /blog/static/img/post-sample-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/img/post-sample-image.jpg -------------------------------------------------------------------------------- /blog/static/img/subash_prakash.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/img/subash_prakash.jpeg -------------------------------------------------------------------------------- /blog/static/js/app.js: -------------------------------------------------------------------------------- 1 | import { add_comment } from './comments.js'; 2 | 3 | $(document).ready(function () { 4 | 5 | $("#load_more").on("click", function (e) { 6 | e.preventDefault(); 7 | var prev_limit = $("#prev_limit").val(); 8 | // GET more 9 | load_more(prev_limit); 10 | }); 11 | 12 | function success_state(data_response) { 13 | $(".display-posts").append(data_response.posts_html_reponse); 14 | if (data_response.prev_limit > data_response.post_len) { 15 | $(".clearfix").remove(); 16 | } 17 | else { 18 | $("input[name=prev_limit]").val(data_response.prev_limit); 19 | } 20 | 21 | } 22 | 23 | function load_more(prev_limit) { 24 | $.ajax({ 25 | url: "/blog" + "?prev_limit=" + prev_limit, 26 | cache: false, 27 | processData: false, 28 | content: "application/json", 29 | type: "GET", 30 | success: function (data_response) { 31 | if (data_response.load_more) { 32 | //Load the content of the post. 33 | localStorage.setItem("response", data_response); 34 | success_state(data_response); 35 | } 36 | }, 37 | error: function (data_response) { 38 | //catch xhrs here 39 | if (data_response.load_more === false) { 40 | console.log("loaded all posts"); 41 | } 42 | } 43 | }); 44 | } 45 | 46 | window.onpopstate = function (e) { 47 | var response = localStorage.getItem('response'); 48 | success_state(response); 49 | } 50 | 51 | //Add Comment 52 | $("#add-form-comment").on("submit", function (e) { 53 | e.preventDefault(); 54 | 55 | if ($("#log > strong").length > 0) { 56 | $("#log > strong").remove(); 57 | } 58 | else { 59 | $("#add-comment").prepend( 60 | "" 64 | ); 65 | } 66 | 67 | var author_name = $("#nameFormInput").val(); 68 | var author_email = $("#emailFormInput").val(); 69 | var author_comment = $('#commentForm').val(); 70 | var g_recaptcha = $("#g-recaptcha-response").val(); 71 | var blog_title = $(".post-heading > h1").text(); 72 | 73 | add_comment(author_name, author_email, author_comment, g_recaptcha, blog_title); 74 | }); 75 | 76 | 77 | // End of file 78 | }); -------------------------------------------------------------------------------- /blog/static/js/clean-blog.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; // Start of use strict 3 | 4 | // Floating label headings for the contact form 5 | $("body").on("input propertychange", ".floating-label-form-group", function(e) { 6 | $(this).toggleClass("floating-label-form-group-with-value", !!$(e.target).val()); 7 | }).on("focus", ".floating-label-form-group", function() { 8 | $(this).addClass("floating-label-form-group-with-focus"); 9 | }).on("blur", ".floating-label-form-group", function() { 10 | $(this).removeClass("floating-label-form-group-with-focus"); 11 | }); 12 | 13 | // Show the navbar when the page is scrolled up 14 | var MQL = 992; 15 | 16 | //primary navigation slide-in effect 17 | if ($(window).width() > MQL) { 18 | var headerHeight = $('#mainNav').height(); 19 | $(window).on('scroll', { 20 | previousTop: 0 21 | }, 22 | function() { 23 | var currentTop = $(window).scrollTop(); 24 | //check if user is scrolling up 25 | if (currentTop < this.previousTop) { 26 | //if scrolling up... 27 | if (currentTop > 0 && $('#mainNav').hasClass('is-fixed')) { 28 | $('#mainNav').addClass('is-visible'); 29 | } else { 30 | $('#mainNav').removeClass('is-visible is-fixed'); 31 | } 32 | } else if (currentTop > this.previousTop) { 33 | //if scrolling down... 34 | $('#mainNav').removeClass('is-visible'); 35 | if (currentTop > headerHeight && !$('#mainNav').hasClass('is-fixed')) $('#mainNav').addClass('is-fixed'); 36 | } 37 | this.previousTop = currentTop; 38 | }); 39 | } 40 | 41 | })(jQuery); // End of use strict 42 | -------------------------------------------------------------------------------- /blog/static/js/clean-blog.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - Clean Blog v5.0.10 (https://startbootstrap.com/theme/clean-blog) 3 | * Copyright 2013-2020 Start Bootstrap 4 | * Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-clean-blog/blob/master/LICENSE) 5 | */ 6 | 7 | !function(o){"use strict";o("body").on("input propertychange",".floating-label-form-group",function(i){o(this).toggleClass("floating-label-form-group-with-value",!!o(i.target).val())}).on("focus",".floating-label-form-group",function(){o(this).addClass("floating-label-form-group-with-focus")}).on("blur",".floating-label-form-group",function(){o(this).removeClass("floating-label-form-group-with-focus")});if(992this.previousTop&&(o("#mainNav").removeClass("is-visible"),sThe author name must be greater than 3 characters").prependTo("#log"); 30 | $("#log").show().fadeOut(25000, "linear"); 31 | this.abort(); 32 | } 33 | 34 | else if (author_comment === "") { 35 | $("The comment cannot be empty!").prependTo("#log"); 36 | $("#log").show().fadeOut(25000, "linear"); 37 | this.abort(); 38 | } 39 | 40 | else if (comment_arr.length <=3){ 41 | $("The comment should atleast contain 3 or more words").prependTo("#log"); 42 | $("#log").show().fadeOut(25000, "linear"); 43 | this.abort(); 44 | } 45 | 46 | }, 47 | success: function (response) { 48 | $('' + response + '').prependTo("#log"); 49 | $("#log").show().fadeOut(25000,"linear"); 50 | $('#add-form-comment')[0].reset(); 51 | }, 52 | error: function (data) { 53 | console.log(data); 54 | $('' + data + '').prependTo("#log"); 55 | $("#log").show().fadeOut(25000,"linear"); 56 | $('#add-form-comment')[0].reset(); 57 | } 58 | }); 59 | } 60 | 61 | 62 | //Export functions to be imported in main wrapper 63 | export { add_comment }; -------------------------------------------------------------------------------- /blog/static/js/contact_me.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | $("#contactForm input,#contactForm textarea").jqBootstrapValidation({ 4 | preventSubmit: true, 5 | submitError: function($form, event, errors) { 6 | // additional error messages or events 7 | }, 8 | submitSuccess: function($form, event) { 9 | event.preventDefault(); // prevent default submit behaviour 10 | // get values from FORM 11 | var name = $("input#name").val(); 12 | var email = $("input#email").val(); 13 | var phone = $("input#phone").val(); 14 | var message = $("textarea#message").val(); 15 | var firstName = name; // For Success/Failure Message 16 | // Check for white space in name for Success/Fail message 17 | if (firstName.indexOf(' ') >= 0) { 18 | firstName = name.split(' ').slice(0, -1).join(' '); 19 | } 20 | $this = $("#sendMessageButton"); 21 | $this.prop("disabled", true); // Disable submit button until AJAX call is complete to prevent duplicate messages 22 | $.ajax({ 23 | url: "././mail/contact_me.php", 24 | type: "POST", 25 | data: { 26 | name: name, 27 | phone: phone, 28 | email: email, 29 | message: message 30 | }, 31 | cache: false, 32 | success: function() { 33 | // Success message 34 | $('#success').html("
"); 35 | $('#success > .alert-success').html(""); 37 | $('#success > .alert-success') 38 | .append("Your message has been sent. "); 39 | $('#success > .alert-success') 40 | .append('
'); 41 | //clear all fields 42 | $('#contactForm').trigger("reset"); 43 | }, 44 | error: function() { 45 | // Fail message 46 | $('#success').html("
"); 47 | $('#success > .alert-danger').html(""); 49 | $('#success > .alert-danger').append($("").text("Sorry " + firstName + ", it seems that my mail server is not responding. Please try again later!")); 50 | $('#success > .alert-danger').append('
'); 51 | //clear all fields 52 | $('#contactForm').trigger("reset"); 53 | }, 54 | complete: function() { 55 | setTimeout(function() { 56 | $this.prop("disabled", false); // Re-enable submit button when AJAX call is complete 57 | }, 1000); 58 | } 59 | }); 60 | }, 61 | filter: function() { 62 | return $(this).is(":visible"); 63 | }, 64 | }); 65 | 66 | $("a[data-toggle=\"tab\"]").click(function(e) { 67 | e.preventDefault(); 68 | $(this).tab("show"); 69 | }); 70 | }); 71 | 72 | /*When clicking on Full hide fail/success boxes */ 73 | $('#name').focus(function() { 74 | $('#success').html(''); 75 | }); 76 | -------------------------------------------------------------------------------- /blog/static/scss/_bootstrap-overrides.scss: -------------------------------------------------------------------------------- 1 | // Bootstrap overrides for this template 2 | .btn { 3 | font-size: 14px; 4 | font-weight: 800; 5 | padding: 15px 25px; 6 | letter-spacing: 1px; 7 | text-transform: uppercase; 8 | border-radius: 0; 9 | @include sans-serif-font; 10 | } 11 | 12 | .btn-primary { 13 | background-color: $primary; 14 | border-color: $primary; 15 | &:hover, 16 | &:focus, 17 | &:active { 18 | color: $white; 19 | background-color: darken($primary, 7.5) !important; 20 | border-color: darken($primary, 7.5) !important; 21 | } 22 | } 23 | 24 | .btn-lg { 25 | font-size: 16px; 26 | padding: 25px 35px; 27 | } 28 | -------------------------------------------------------------------------------- /blog/static/scss/_contact.scss: -------------------------------------------------------------------------------- 1 | // Styling for the contact page 2 | .floating-label-form-group { 3 | font-size: 14px; 4 | position: relative; 5 | margin-bottom: 0; 6 | padding-bottom: 0.5em; 7 | border-bottom: 1px solid $gray-300; 8 | input, 9 | textarea { 10 | font-size: 1.5em; 11 | position: relative; 12 | z-index: 1; 13 | padding: 0; 14 | resize: none; 15 | border: none; 16 | border-radius: 0; 17 | background: none; 18 | box-shadow: none !important; 19 | @include serif-font; 20 | &::-webkit-input-placeholder { 21 | color: $gray-600; 22 | @include serif-font; 23 | } 24 | } 25 | label { 26 | font-size: 0.85em; 27 | line-height: 1.764705882em; 28 | position: relative; 29 | z-index: 0; 30 | top: 2em; 31 | display: block; 32 | margin: 0; 33 | -webkit-transition: top 0.3s ease, opacity 0.3s ease; 34 | -moz-transition: top 0.3s ease, opacity 0.3s ease; 35 | -ms-transition: top 0.3s ease, opacity 0.3s ease; 36 | transition: top 0.3s ease, opacity 0.3s ease; 37 | opacity: 0; 38 | } 39 | .help-block { 40 | margin: 15px 0; 41 | } 42 | } 43 | 44 | .floating-label-form-group-with-value { 45 | label { 46 | top: 0; 47 | opacity: 1; 48 | } 49 | } 50 | 51 | .floating-label-form-group-with-focus { 52 | label { 53 | color: $primary; 54 | } 55 | } 56 | form .form-group:first-child .floating-label-form-group { 57 | border-top: 1px solid $gray-300; 58 | } 59 | -------------------------------------------------------------------------------- /blog/static/scss/_footer.scss: -------------------------------------------------------------------------------- 1 | // Styling for the footer 2 | footer { 3 | padding: 50px 0 65px; 4 | .list-inline { 5 | margin: 0; 6 | padding: 0; 7 | } 8 | .copyright { 9 | font-size: 14px; 10 | margin-bottom: 0; 11 | text-align: center; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /blog/static/scss/_global.scss: -------------------------------------------------------------------------------- 1 | // Global styling for this template 2 | body { 3 | font-size: 20px; 4 | color: $gray-900; 5 | @include serif-font; 6 | } 7 | 8 | p { 9 | line-height: 1.5; 10 | margin: 30px 0; 11 | a { 12 | text-decoration: underline; 13 | } 14 | } 15 | 16 | h1, 17 | h2, 18 | h3, 19 | h4, 20 | h5, 21 | h6 { 22 | font-weight: 800; 23 | @include sans-serif-font; 24 | } 25 | 26 | a { 27 | color: $gray-900; 28 | @include transition-all; 29 | &:focus, 30 | &:hover { 31 | color: $primary; 32 | } 33 | } 34 | 35 | blockquote { 36 | font-style: italic; 37 | color: $gray-600; 38 | } 39 | 40 | .section-heading { 41 | font-size: 36px; 42 | font-weight: 700; 43 | margin-top: 60px; 44 | } 45 | 46 | .caption { 47 | font-size: 14px; 48 | font-style: italic; 49 | display: block; 50 | margin: 0; 51 | padding: 10px; 52 | text-align: center; 53 | border-bottom-right-radius: 5px; 54 | border-bottom-left-radius: 5px; 55 | } 56 | 57 | ::-moz-selection { 58 | color: $white; 59 | background: $primary; 60 | text-shadow: none; 61 | } 62 | 63 | ::selection { 64 | color: $white; 65 | background: $primary; 66 | text-shadow: none; 67 | } 68 | 69 | img::selection { 70 | color: $white; 71 | background: transparent; 72 | } 73 | 74 | img::-moz-selection { 75 | color: $white; 76 | background: transparent; 77 | } 78 | -------------------------------------------------------------------------------- /blog/static/scss/_masthead.scss: -------------------------------------------------------------------------------- 1 | // Styling for the masthead 2 | header.masthead { 3 | // TIP: Background images are set within the HTML using inline CSS! 4 | margin-bottom: 50px; 5 | background: no-repeat center center; 6 | background-color: $gray-600; 7 | background-attachment: scroll; 8 | position: relative; 9 | @include background-cover; 10 | .overlay { 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | height: 100%; 15 | width: 100%; 16 | background-color: $gray-900; 17 | opacity: 0.5; 18 | } 19 | .page-heading, 20 | .post-heading, 21 | .site-heading { 22 | padding: 200px 0 150px; 23 | color: white; 24 | @media only screen and (min-width: 768px) { 25 | padding: 200px 0; 26 | } 27 | } 28 | .page-heading, 29 | .site-heading { 30 | text-align: center; 31 | h1 { 32 | font-size: 50px; 33 | margin-top: 0; 34 | } 35 | .subheading { 36 | font-size: 24px; 37 | font-weight: 300; 38 | line-height: 1.1; 39 | display: block; 40 | margin: 10px 0 0; 41 | @include sans-serif-font; 42 | } 43 | @media only screen and (min-width: 768px) { 44 | h1 { 45 | font-size: 80px; 46 | } 47 | } 48 | } 49 | .post-heading { 50 | h1 { 51 | font-size: 35px; 52 | } 53 | .meta, 54 | .subheading { 55 | line-height: 1.1; 56 | display: block; 57 | } 58 | .subheading { 59 | font-size: 24px; 60 | font-weight: 600; 61 | margin: 10px 0 30px; 62 | @include sans-serif-font; 63 | } 64 | .meta { 65 | font-size: 20px; 66 | font-weight: 300; 67 | font-style: italic; 68 | @include serif-font; 69 | a { 70 | color: $white; 71 | } 72 | } 73 | @media only screen and (min-width: 768px) { 74 | h1 { 75 | font-size: 55px; 76 | } 77 | .subheading { 78 | font-size: 30px; 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /blog/static/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // Bootstrap Button Variant 3 | @mixin button-variant($color, $background, $border) { 4 | color: $color; 5 | border-color: $border; 6 | background-color: $background; 7 | &.focus, 8 | &:focus { 9 | color: $color; 10 | border-color: darken($border, 25%); 11 | background-color: darken($background, 10%); 12 | } 13 | &:hover { 14 | color: $color; 15 | border-color: darken($border, 12%); 16 | background-color: darken($background, 10%); 17 | } 18 | &.active, 19 | &:active, 20 | .open > &.dropdown-toggle { 21 | color: $color; 22 | border-color: darken($border, 12%); 23 | background-color: darken($background, 10%); 24 | &.focus, 25 | &:focus, 26 | &:hover { 27 | color: $color; 28 | border-color: darken($border, 25%); 29 | background-color: darken($background, 17%); 30 | } 31 | } 32 | &.active, 33 | &:active, 34 | .open > &.dropdown-toggle { 35 | background-image: none; 36 | } 37 | &.disabled, 38 | &[disabled], 39 | fieldset[disabled] & { 40 | &.focus, 41 | &:focus, 42 | &:hover { 43 | border-color: $border; 44 | background-color: $background; 45 | } 46 | } 47 | .badge { 48 | color: $background; 49 | background-color: $color; 50 | } 51 | } 52 | @mixin transition-all() { 53 | -webkit-transition: all 0.2s; 54 | -moz-transition: all 0.2s; 55 | transition: all 0.2s; 56 | } 57 | @mixin background-cover() { 58 | -webkit-background-size: cover; 59 | -moz-background-size: cover; 60 | -o-background-size: cover; 61 | background-size: cover; 62 | } 63 | @mixin serif-font() { 64 | font-family: 'Lora', 'Times New Roman', serif; 65 | } 66 | @mixin sans-serif-font() { 67 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 68 | } 69 | -------------------------------------------------------------------------------- /blog/static/scss/_navbar.scss: -------------------------------------------------------------------------------- 1 | // Styling for the navbar 2 | #mainNav { 3 | position: absolute; 4 | border-bottom: 1px solid $gray-200; 5 | background-color: white; 6 | @include sans-serif-font; 7 | .navbar-brand { 8 | font-weight: 800; 9 | color: $gray-800; 10 | } 11 | .navbar-toggler { 12 | font-size: 12px; 13 | font-weight: 800; 14 | padding: 13px; 15 | text-transform: uppercase; 16 | color: $gray-800; 17 | } 18 | .navbar-nav { 19 | > li.nav-item { 20 | > a { 21 | font-size: 12px; 22 | font-weight: 800; 23 | letter-spacing: 1px; 24 | text-transform: uppercase; 25 | } 26 | } 27 | } 28 | @media only screen and (min-width: 992px) { 29 | border-bottom: 1px solid transparent; 30 | background: transparent; 31 | .navbar-brand { 32 | padding: 10px 20px; 33 | color: $white; 34 | &:focus, 35 | &:hover { 36 | color: fade-out($white, .2); 37 | } 38 | } 39 | .navbar-nav { 40 | > li.nav-item { 41 | > a { 42 | padding: 10px 20px; 43 | color: $white; 44 | &:focus, 45 | &:hover { 46 | color: fade-out($white, .2); 47 | } 48 | } 49 | } 50 | } 51 | } 52 | @media only screen and (min-width: 992px) { 53 | -webkit-transition: background-color 0.2s; 54 | -moz-transition: background-color 0.2s; 55 | transition: background-color 0.2s; 56 | /* Force Hardware Acceleration in WebKit */ 57 | -webkit-transform: translate3d(0, 0, 0); 58 | -moz-transform: translate3d(0, 0, 0); 59 | -ms-transform: translate3d(0, 0, 0); 60 | -o-transform: translate3d(0, 0, 0); 61 | transform: translate3d(0, 0, 0); 62 | -webkit-backface-visibility: hidden; 63 | backface-visibility: hidden; 64 | &.is-fixed { 65 | /* when the user scrolls down, we hide the header right above the viewport */ 66 | position: fixed; 67 | top: -67px; 68 | -webkit-transition: -webkit-transform 0.2s; 69 | -moz-transition: -moz-transform 0.2s; 70 | transition: transform 0.2s; 71 | border-bottom: 1px solid darken($white, .05); 72 | background-color: fade-out($white, .1); 73 | .navbar-brand { 74 | color: $gray-900; 75 | &:focus, 76 | &:hover { 77 | color: $primary; 78 | } 79 | } 80 | .navbar-nav { 81 | > li.nav-item { 82 | > a { 83 | color: $gray-900; 84 | &:focus, 85 | &:hover { 86 | color: $primary; 87 | } 88 | } 89 | } 90 | } 91 | } 92 | &.is-visible { 93 | /* if the user changes the scrolling direction, we show the header */ 94 | -webkit-transform: translate3d(0, 100%, 0); 95 | -moz-transform: translate3d(0, 100%, 0); 96 | -ms-transform: translate3d(0, 100%, 0); 97 | -o-transform: translate3d(0, 100%, 0); 98 | transform: translate3d(0, 100%, 0); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /blog/static/scss/_post.scss: -------------------------------------------------------------------------------- 1 | // Styling for the post page 2 | .post-preview { 3 | > a { 4 | color: $gray-900; 5 | &:focus, 6 | &:hover { 7 | text-decoration: none; 8 | color: $primary; 9 | } 10 | > .post-title { 11 | font-size: 30px; 12 | margin-top: 30px; 13 | margin-bottom: 10px; 14 | } 15 | > .post-subtitle { 16 | font-weight: 300; 17 | margin: 0 0 10px; 18 | } 19 | } 20 | > .post-meta { 21 | font-size: 18px; 22 | font-style: italic; 23 | margin-top: 0; 24 | color: $gray-600; 25 | > a { 26 | text-decoration: none; 27 | color: $gray-900; 28 | &:focus, 29 | &:hover { 30 | text-decoration: underline; 31 | color: $primary; 32 | } 33 | } 34 | } 35 | @media only screen and (min-width: 768px) { 36 | > a { 37 | > .post-title { 38 | font-size: 36px; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /blog/static/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | 3 | $white: #fff !default; 4 | $gray-100: #f8f9fa !default; 5 | $gray-200: #e9ecef !default; 6 | $gray-300: #dee2e6 !default; 7 | $gray-400: #ced4da !default; 8 | $gray-500: #adb5bd !default; 9 | $gray-600: #868e96 !default; 10 | $gray-700: #495057 !default; 11 | $gray-800: #343a40 !default; 12 | $gray-900: #212529 !default; 13 | $black: #000 !default; 14 | 15 | $blue: #007bff !default; 16 | $indigo: #6610f2 !default; 17 | $purple: #6f42c1 !default; 18 | $pink: #e83e8c !default; 19 | $red: #dc3545 !default; 20 | $orange: #fd7e14 !default; 21 | $yellow: #ffc107 !default; 22 | $green: #28a745 !default; 23 | $teal: #0085A1 !default; 24 | $cyan: #17a2b8 !default; 25 | 26 | $primary: $teal !default; 27 | $secondary: $gray-600 !default; 28 | $success: $green !default; 29 | $info: $cyan !default; 30 | $warning: $yellow !default; 31 | $danger: $red !default; 32 | $light: $gray-100 !default; 33 | $dark: $gray-800 !default; 34 | -------------------------------------------------------------------------------- /blog/static/scss/clean-blog.scss: -------------------------------------------------------------------------------- 1 | @import "variables.scss"; 2 | @import "mixins.scss"; 3 | @import "global.scss"; 4 | @import "navbar.scss"; 5 | @import "masthead.scss"; 6 | @import "post.scss"; 7 | @import "contact.scss"; 8 | @import "footer.scss"; 9 | @import "bootstrap-overrides.scss"; 10 | -------------------------------------------------------------------------------- /blog/static/vendor/bootstrap/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors 4 | * Copyright 2011-2020 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 28 | font-size: 1rem; 29 | font-weight: 400; 30 | line-height: 1.5; 31 | color: #212529; 32 | text-align: left; 33 | background-color: #fff; 34 | } 35 | 36 | [tabindex="-1"]:focus:not(:focus-visible) { 37 | outline: 0 !important; 38 | } 39 | 40 | hr { 41 | box-sizing: content-box; 42 | height: 0; 43 | overflow: visible; 44 | } 45 | 46 | h1, h2, h3, h4, h5, h6 { 47 | margin-top: 0; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | p { 52 | margin-top: 0; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | abbr[title], 57 | abbr[data-original-title] { 58 | text-decoration: underline; 59 | -webkit-text-decoration: underline dotted; 60 | text-decoration: underline dotted; 61 | cursor: help; 62 | border-bottom: 0; 63 | -webkit-text-decoration-skip-ink: none; 64 | text-decoration-skip-ink: none; 65 | } 66 | 67 | address { 68 | margin-bottom: 1rem; 69 | font-style: normal; 70 | line-height: inherit; 71 | } 72 | 73 | ol, 74 | ul, 75 | dl { 76 | margin-top: 0; 77 | margin-bottom: 1rem; 78 | } 79 | 80 | ol ol, 81 | ul ul, 82 | ol ul, 83 | ul ol { 84 | margin-bottom: 0; 85 | } 86 | 87 | dt { 88 | font-weight: 700; 89 | } 90 | 91 | dd { 92 | margin-bottom: .5rem; 93 | margin-left: 0; 94 | } 95 | 96 | blockquote { 97 | margin: 0 0 1rem; 98 | } 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | small { 106 | font-size: 80%; 107 | } 108 | 109 | sub, 110 | sup { 111 | position: relative; 112 | font-size: 75%; 113 | line-height: 0; 114 | vertical-align: baseline; 115 | } 116 | 117 | sub { 118 | bottom: -.25em; 119 | } 120 | 121 | sup { 122 | top: -.5em; 123 | } 124 | 125 | a { 126 | color: #007bff; 127 | text-decoration: none; 128 | background-color: transparent; 129 | } 130 | 131 | a:hover { 132 | color: #0056b3; 133 | text-decoration: underline; 134 | } 135 | 136 | a:not([href]):not([class]) { 137 | color: inherit; 138 | text-decoration: none; 139 | } 140 | 141 | a:not([href]):not([class]):hover { 142 | color: inherit; 143 | text-decoration: none; 144 | } 145 | 146 | pre, 147 | code, 148 | kbd, 149 | samp { 150 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 151 | font-size: 1em; 152 | } 153 | 154 | pre { 155 | margin-top: 0; 156 | margin-bottom: 1rem; 157 | overflow: auto; 158 | -ms-overflow-style: scrollbar; 159 | } 160 | 161 | figure { 162 | margin: 0 0 1rem; 163 | } 164 | 165 | img { 166 | vertical-align: middle; 167 | border-style: none; 168 | } 169 | 170 | svg { 171 | overflow: hidden; 172 | vertical-align: middle; 173 | } 174 | 175 | table { 176 | border-collapse: collapse; 177 | } 178 | 179 | caption { 180 | padding-top: 0.75rem; 181 | padding-bottom: 0.75rem; 182 | color: #6c757d; 183 | text-align: left; 184 | caption-side: bottom; 185 | } 186 | 187 | th { 188 | text-align: inherit; 189 | text-align: -webkit-match-parent; 190 | } 191 | 192 | label { 193 | display: inline-block; 194 | margin-bottom: 0.5rem; 195 | } 196 | 197 | button { 198 | border-radius: 0; 199 | } 200 | 201 | button:focus { 202 | outline: 1px dotted; 203 | outline: 5px auto -webkit-focus-ring-color; 204 | } 205 | 206 | input, 207 | button, 208 | select, 209 | optgroup, 210 | textarea { 211 | margin: 0; 212 | font-family: inherit; 213 | font-size: inherit; 214 | line-height: inherit; 215 | } 216 | 217 | button, 218 | input { 219 | overflow: visible; 220 | } 221 | 222 | button, 223 | select { 224 | text-transform: none; 225 | } 226 | 227 | [role="button"] { 228 | cursor: pointer; 229 | } 230 | 231 | select { 232 | word-wrap: normal; 233 | } 234 | 235 | button, 236 | [type="button"], 237 | [type="reset"], 238 | [type="submit"] { 239 | -webkit-appearance: button; 240 | } 241 | 242 | button:not(:disabled), 243 | [type="button"]:not(:disabled), 244 | [type="reset"]:not(:disabled), 245 | [type="submit"]:not(:disabled) { 246 | cursor: pointer; 247 | } 248 | 249 | button::-moz-focus-inner, 250 | [type="button"]::-moz-focus-inner, 251 | [type="reset"]::-moz-focus-inner, 252 | [type="submit"]::-moz-focus-inner { 253 | padding: 0; 254 | border-style: none; 255 | } 256 | 257 | input[type="radio"], 258 | input[type="checkbox"] { 259 | box-sizing: border-box; 260 | padding: 0; 261 | } 262 | 263 | textarea { 264 | overflow: auto; 265 | resize: vertical; 266 | } 267 | 268 | fieldset { 269 | min-width: 0; 270 | padding: 0; 271 | margin: 0; 272 | border: 0; 273 | } 274 | 275 | legend { 276 | display: block; 277 | width: 100%; 278 | max-width: 100%; 279 | padding: 0; 280 | margin-bottom: .5rem; 281 | font-size: 1.5rem; 282 | line-height: inherit; 283 | color: inherit; 284 | white-space: normal; 285 | } 286 | 287 | progress { 288 | vertical-align: baseline; 289 | } 290 | 291 | [type="number"]::-webkit-inner-spin-button, 292 | [type="number"]::-webkit-outer-spin-button { 293 | height: auto; 294 | } 295 | 296 | [type="search"] { 297 | outline-offset: -2px; 298 | -webkit-appearance: none; 299 | } 300 | 301 | [type="search"]::-webkit-search-decoration { 302 | -webkit-appearance: none; 303 | } 304 | 305 | ::-webkit-file-upload-button { 306 | font: inherit; 307 | -webkit-appearance: button; 308 | } 309 | 310 | output { 311 | display: inline-block; 312 | } 313 | 314 | summary { 315 | display: list-item; 316 | cursor: pointer; 317 | } 318 | 319 | template { 320 | display: none; 321 | } 322 | 323 | [hidden] { 324 | display: none !important; 325 | } 326 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /blog/static/vendor/bootstrap/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors 4 | * Copyright 2011-2020 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/css/brands.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Brands'; 7 | font-style: normal; 8 | font-weight: 400; 9 | font-display: block; 10 | src: url("../webfonts/fa-brands-400.eot"); 11 | src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } 12 | 13 | .fab { 14 | font-family: 'Font Awesome 5 Brands'; 15 | font-weight: 400; } 16 | -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/css/brands.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands";font-weight:400} -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/css/regular.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 400; 9 | font-display: block; 10 | src: url("../webfonts/fa-regular-400.eot"); 11 | src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } 12 | 13 | .far { 14 | font-family: 'Font Awesome 5 Free'; 15 | font-weight: 400; } 16 | -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/css/regular.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:"Font Awesome 5 Free";font-weight:400} -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/css/solid.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 900; 9 | font-display: block; 10 | src: url("../webfonts/fa-solid-900.eot"); 11 | src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } 12 | 13 | .fa, 14 | .fas { 15 | font-family: 'Font Awesome 5 Free'; 16 | font-weight: 900; } 17 | -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/css/solid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900} -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/css/svg-with-js.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | .svg-inline--fa,svg:not(:root).svg-inline--fa{overflow:visible}.svg-inline--fa{display:inline-block;font-size:inherit;height:1em;vertical-align:-.125em}.svg-inline--fa.fa-lg{vertical-align:-.225em}.svg-inline--fa.fa-w-1{width:.0625em}.svg-inline--fa.fa-w-2{width:.125em}.svg-inline--fa.fa-w-3{width:.1875em}.svg-inline--fa.fa-w-4{width:.25em}.svg-inline--fa.fa-w-5{width:.3125em}.svg-inline--fa.fa-w-6{width:.375em}.svg-inline--fa.fa-w-7{width:.4375em}.svg-inline--fa.fa-w-8{width:.5em}.svg-inline--fa.fa-w-9{width:.5625em}.svg-inline--fa.fa-w-10{width:.625em}.svg-inline--fa.fa-w-11{width:.6875em}.svg-inline--fa.fa-w-12{width:.75em}.svg-inline--fa.fa-w-13{width:.8125em}.svg-inline--fa.fa-w-14{width:.875em}.svg-inline--fa.fa-w-15{width:.9375em}.svg-inline--fa.fa-w-16{width:1em}.svg-inline--fa.fa-w-17{width:1.0625em}.svg-inline--fa.fa-w-18{width:1.125em}.svg-inline--fa.fa-w-19{width:1.1875em}.svg-inline--fa.fa-w-20{width:1.25em}.svg-inline--fa.fa-pull-left{margin-right:.3em;width:auto}.svg-inline--fa.fa-pull-right{margin-left:.3em;width:auto}.svg-inline--fa.fa-border{height:1.5em}.svg-inline--fa.fa-li{width:2em}.svg-inline--fa.fa-fw{width:1.25em}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers-text{left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-counter{background-color:#ff253a;border-radius:1em;-webkit-box-sizing:border-box;box-sizing:border-box;color:#fff;height:1.5em;line-height:1;max-width:5em;min-width:1.5em;overflow:hidden;padding:.25em;right:0;text-overflow:ellipsis;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-bottom-right{bottom:0;right:0;top:auto;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:bottom right;transform-origin:bottom right}.fa-layers-bottom-left{bottom:0;left:0;right:auto;top:auto;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:bottom left;transform-origin:bottom left}.fa-layers-top-right{right:0;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-top-left{left:0;right:auto;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top left;transform-origin:top left}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2.5em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.svg-inline--fa.fa-stack-1x{height:1em;width:1.25em}.svg-inline--fa.fa-stack-2x{height:2em;width:2.5em}.fa-inverse{color:#fff}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.svg-inline--fa .fa-primary{fill:var(--fa-primary-color,currentColor);opacity:1;opacity:var(--fa-primary-opacity,1)}.svg-inline--fa .fa-secondary{fill:var(--fa-secondary-color,currentColor)}.svg-inline--fa .fa-secondary,.svg-inline--fa.fa-swap-opacity .fa-primary{opacity:.4;opacity:var(--fa-secondary-opacity,.4)}.svg-inline--fa.fa-swap-opacity .fa-secondary{opacity:1;opacity:var(--fa-primary-opacity,1)}.svg-inline--fa mask .fa-primary,.svg-inline--fa mask .fa-secondary{fill:#000}.fad.fa-inverse{color:#fff} -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/vendor/fontawesome-free/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/vendor/fontawesome-free/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/vendor/fontawesome-free/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/vendor/fontawesome-free/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/vendor/fontawesome-free/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/vendor/fontawesome-free/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/vendor/fontawesome-free/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/vendor/fontawesome-free/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/vendor/fontawesome-free/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/vendor/fontawesome-free/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/vendor/fontawesome-free/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /blog/static/vendor/fontawesome-free/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog/static/vendor/fontawesome-free/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /blog/templates/blog/about.html: -------------------------------------------------------------------------------- 1 | {% extends "blog/base.html" %} 2 | {% block content %} 3 | 4 |
5 |
6 |
7 |   8 | 20x20 10 |

Subash Prakash

11 |

Software Engineer

12 |

XYZ

13 | 14 |
15 |
16 |

17 | Thank you for visiting my profile. I am Subash Prakash. I like to use open source tools and softwares to create 18 | tools for benefitting everyone 19 |

20 |
21 |
22 |

Interests:

23 |
    24 |
  • Helping and collaborating in projects 💻
  • 25 |
  • Discussion of ideas for startups ✏
  • 26 |
  • Learning more of AI 🤖
  • 27 |
28 |
29 |
30 |

Hobbies:

31 |
  • Gaming 🎮
  • 32 |
  • Painting 🖌
  • 33 |
    34 |
    35 |
    36 |
    37 |
    38 | 39 |
    40 | 41 |
    42 | 43 | 53 | 54 | 55 |
    56 |
    57 |
    58 |
    59 |
    60 |
    61 |
    62 |
    Rank Based Detection Algorithm
    63 |

    Rank based outlier detection algorithm implementation in python

    64 | More 65 |
    66 |
    67 |
    68 |
    69 |
    70 |
    71 |
    Traffic Sign Detection
    72 |

    Detecting traffic signs by using traditional Computer Vision approaches

    73 | More 74 |
    75 |
    76 |
    77 |
    78 |
    79 |
    80 |
    Interactive System to identify similar neighbors
    81 |

    A prototype visualization system to explore and interact with the nearest neighbors 82 | and detect outliers within them

    83 | More 84 |
    85 |
    86 |
    87 |
    88 |
    89 |
    90 | 103 |
    104 |
    105 |
    106 |
    107 |
    108 |
      109 | 110 | 121 | 122 |
    • 123 |
      124 |
      125 | OVGU, Magdeburg 126 | 2017-2020 127 |
      128 |
      129 | Masters in Data and Knowledge engineering with the focus towards combining machine learning with 130 | software engineer via building data analysis tools, machine learning prediction tools and 131 | understanding of data engineering pipelines. 132 |
      133 |
      134 |
    • 135 | 136 |
    • 137 |
      138 |
      139 | Torry Harris Integration Solutions, Bangalore 140 | 2013-2017 141 |
      142 |
      Worked as software developer in the area of Telecom domain building REST 143 | apis, data ingestion, and automation activities.
      144 |
      145 |
    • 146 | 147 |
    • 148 |
      149 |
      150 | Sir MVIT 151 | 2009 - 2013 152 |
      153 |
      Bachelor of Engineering in Information Science majoring computer science 154 | related subjects
      155 |
      156 |
    • 157 |
    158 |
    159 |
    160 |
    161 |
    162 |
    163 | 164 | {% endblock %} -------------------------------------------------------------------------------- /blog/templates/blog/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{resp["blog_header"]}} Title 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 99 | 100 | 101 | 102 | 103 | 104 | 128 | {% block content %} 129 | {% endblock %} 130 | 131 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /blog/templates/blog/contact.html: -------------------------------------------------------------------------------- 1 | {% extends "blog/base.html" %} 2 | {% block content %} 3 | 4 |
    5 |
    6 |
    7 |
    8 |
    9 |

    Contact Me

    10 | Have questions? I have answers. 11 |
    12 |
    13 |
    14 |
    15 |
    16 | 17 |
    18 |
    19 |
    20 |

    Want to get in touch? Fill out the form below to send me a message and I will get back to you as soon as 21 | possible!

    22 | 23 | 24 | 25 |
    26 |
    27 |
    28 | 29 | 31 |

    32 |
    33 |
    34 |
    35 |
    36 | 37 | 39 |

    40 |
    41 |
    42 |
    43 |
    44 | 45 | 47 |

    48 |
    49 |
    50 |
    51 |
    52 | 53 | 55 |

    56 |
    57 |
    58 |
    59 |
    60 | 61 |
    62 |
    63 |
    64 |
    65 | 66 |
    67 | 68 | {% endblock %} -------------------------------------------------------------------------------- /blog/templates/blog/index.html: -------------------------------------------------------------------------------- 1 | {% extends "blog/base.html" %} 2 | {% block content %} 3 | 4 |
    5 |
    6 |
    7 |
    8 |
    9 |

    {{resp.blog_header}}

    10 | {{resp.blog_subheader}} 11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    Blog Tags:
    18 | {% if tags_count %} 19 | {% for key, val in tags_count.items() %} 20 | 22 | {{key}} | {{val}} 23 | 24 | {% endfor %} 25 | {% endif %} 26 |
    27 | 28 |
    29 |
    30 |
    31 |
    32 | {% if posts_data %} 33 | {% for post in posts_data %} 34 |
    35 | 36 |

    37 | {{ post.title }} 38 |

    39 |
    40 | 44 |
    45 |
    46 | {% endfor %} 47 | {% else %} 48 |

    No posts to show yet.

    49 | {% endif %} 50 |
    51 |
    52 | 53 | {% if post_len|int >= prev_limit|int %} 54 |
    55 | 56 | 57 |
    58 | {% endif %} 59 |
    60 |
    61 |
    62 | {% endblock %} -------------------------------------------------------------------------------- /blog/templates/blog/post.html: -------------------------------------------------------------------------------- 1 | {% extends "blog/base.html" %} 2 | {% block content %} 3 | 4 |
    5 |
    6 |
    7 |
    8 | {% if data_resp.post_data %} 9 |
    10 |

    {{data_resp.post_data.title}}

    11 |
    12 | {% for item in data_resp.tags %} 13 | 15 | {{item.tag}} 16 | 17 | {% endfor %} 18 |
    19 |
    20 | Posted by 21 | {{data_resp.post_data.author}} 22 | on {{data_resp.post_data.posted_date.strftime('%B %d, %Y')}} 23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 | 30 |
    31 |
    32 |
    33 |
    34 | {{data_resp.post_data.content|safe}} 35 |
    36 |
    37 |
    38 |
    39 | {% else %} 40 |

    No content to show

    41 | {% endif %} 42 |
    43 |

    Comments

    44 |
    45 |
    46 | {% for comment_info in data_resp.comments %} 47 |
    48 | 49 | 50 | 51 |
    52 | {{comment_info.author_name}} 53 | 56 |
    57 | {{comment_info.content|safe}} 58 |
    59 | 62 |
    63 |
    64 | {% endfor %} 65 |
    66 |
    67 |
    68 | 69 |
    70 |
    71 |

    72 |

    Leave a comment on how you felt of the article...
    73 |

    74 |
    75 | 80 |
    81 |
    82 |
    Author Name
    83 | 84 |
    85 |
    86 |
    Author Email
    87 | 88 |
    Note: This email will not be shared to anyone
    89 |
    90 |
    91 |
    Comment
    92 | 93 |
    94 |
    95 |
    96 |
    97 | 98 | 99 |
    100 |
    101 |
    102 |
    103 |
    104 | {% endblock %} -------------------------------------------------------------------------------- /blog_admin/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.9-slim 2 | RUN apt-get update && apt-get install -y gcc && apt-get install -y default-libmysqlclient-dev 3 | 4 | COPY SimplisticBlogger/blog_admin /app/blog_admin 5 | COPY SimplisticBlogger/requirements.txt /app/blog_admin 6 | COPY SimplisticBlogger/common /app/common 7 | COPY SimplisticBlogger/instance /app/instance 8 | COPY SimplisticBlogger/.env /app/blog_admin 9 | WORKDIR /app/blog_admin 10 | 11 | RUN pip install --upgrade pip 12 | 13 | RUN pip install -r requirements.txt 14 | 15 | CMD flask run --host ${FLASK_HOST} --port ${FLASK_ADMIN_PORT} 16 | #CMD ["flask", "run", "--port ${FLASK_ADMIN_PORT}"] 17 | -------------------------------------------------------------------------------- /blog_admin/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from os import environ 3 | from werkzeug.security import generate_password_hash, check_password_hash 4 | from dotenv import load_dotenv, find_dotenv 5 | from instance.config import app_config 6 | from flask_login import LoginManager 7 | from common import db, cache 8 | from flask_wtf.csrf import CSRFProtect 9 | from common import mail 10 | 11 | login_manager = LoginManager() 12 | csrf_protect = CSRFProtect() 13 | 14 | username = environ.get("ADMIN_USERNAME") 15 | password = environ.get("PASSWORD") 16 | f_name = environ.get("F_NAME") 17 | email = environ.get("EMAIL") 18 | 19 | load_dotenv(find_dotenv()) 20 | 21 | 22 | def create_admin_app(config_name): 23 | ''' Create Flask app ''' 24 | print("Initiating admin app for blogging...") 25 | # More on DB init here... 26 | app = Flask(__name__, instance_relative_config=True) 27 | app.config.from_object(app_config[config_name]) 28 | app.config.from_pyfile('config.py') 29 | # init sql-alchemy 30 | db.init_app(app) 31 | # init csrf 32 | csrf_protect.init_app(app) 33 | # init cache 34 | cache.init_app(app) 35 | # init email 36 | mail.init_app(app) 37 | 38 | # Set the login manager 39 | login_manager.login_view = 'auth.admin' 40 | login_manager.init_app(app) 41 | 42 | with app.app_context(): 43 | from common.services.users_service import UserService 44 | 45 | db.create_all() 46 | 47 | 48 | 49 | #Query table to check if admin is already present then simply do not create a user... 50 | 51 | # Create the admin user as per configs 52 | user_obj = UserService() 53 | admin = user_obj.query_single_user(user_name=username) 54 | if admin == -1: 55 | print("Creating the user for the first time") 56 | return_type = user_obj.create_user(username, generate_password_hash(password), f_name, email) 57 | if return_type == -1: 58 | print("There has been an error while creating the admin user. Check logs") 59 | else: 60 | print("Admin user has been created and can be logged in") 61 | 62 | from common.models import users_model 63 | @login_manager.user_loader 64 | def load_user(user_id): 65 | # Query by user_id of the Users table 66 | return users_model.Users.query.get(int(user_id)) 67 | 68 | # Module imports 69 | from blog_admin.api import api_controller 70 | from blog_admin.auth import auth_controller 71 | from blog_admin.posts import posts_controller 72 | from blog_admin.posts import comments_controller 73 | from common.controller import posts_api_controller 74 | from common.controller import images_api_controller 75 | from common.controller import comments_api_controller 76 | 77 | # Register blueprints 78 | app.register_blueprint(api_controller.api_bp) 79 | app.register_blueprint(auth_controller.auth_bp, url_prefix="/admin") 80 | app.register_blueprint(posts_controller.posts_bp, url_prefix="/admin") 81 | app.register_blueprint(comments_controller.comments_bp, url_prefix="/admin") 82 | app.register_blueprint(posts_api_controller.posts_api_bp, url_prefix="/api") 83 | app.register_blueprint(images_api_controller.image_bp) 84 | app.register_blueprint(comments_api_controller.comments_bp) 85 | 86 | return app 87 | -------------------------------------------------------------------------------- /blog_admin/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog_admin/api/__init__.py -------------------------------------------------------------------------------- /blog_admin/api/api_controller.py: -------------------------------------------------------------------------------- 1 | from flask import (Blueprint, redirect, url_for, request, 2 | flash, abort) 3 | from werkzeug.security import check_password_hash, generate_password_hash 4 | from flask_login import login_user, login_required, current_user 5 | from urllib import parse 6 | from common import db 7 | from common.models.users_model import Users 8 | 9 | api_bp = Blueprint("api", __name__) 10 | 11 | # Safe url function handy for redirects 12 | 13 | def is_safe_url(target): 14 | ref_url = parse.urlparse(request.host_url) 15 | test_url = parse.urlparse(parse.urljoin(request.host_url, target)) 16 | return test_url.scheme in ('http', 'https') and \ 17 | ref_url.netloc == test_url.netloc 18 | 19 | 20 | @api_bp.route("/") 21 | def index(): 22 | return redirect(url_for("auth.admin")) 23 | 24 | 25 | @api_bp.route("/login", methods=["POST"]) 26 | def login(): 27 | if current_user.is_authenticated: 28 | return redirect(url_for("posts.dash_posts")) 29 | 30 | user_name = request.form.get("username") 31 | password = request.form.get("password") 32 | user = Users.query.filter_by(user_name=user_name).first() 33 | next_link = request.args.get("next") 34 | if not user or not check_password_hash(user.password, password): 35 | flash("Looks like the provided login credentials are not correct !!!. Please login again") 36 | return redirect(url_for("auth.admin")) 37 | # Login the user into flask-login 38 | login_user(user) 39 | 40 | if not is_safe_url(next_link): 41 | return abort(400) 42 | 43 | if user.changed_pass: 44 | return redirect(url_for("posts.dash_posts")) 45 | 46 | return redirect(next_link or url_for("auth.intrim_login")) 47 | 48 | 49 | @api_bp.route("/change_password", methods=["POST"]) 50 | @login_required 51 | def change_password(): 52 | old_pass = request.form.get("old-password") 53 | new_pass = request.form.get("new-password") 54 | 55 | user = Users.query.filter_by(user_name=current_user.user_name).first() 56 | 57 | if not check_password_hash(user.password, old_pass): 58 | flash("Looks like the provided old password is wrong. Try again !!!") 59 | return redirect(url_for("auth.intrim_login")) 60 | print("Sucess with change_password() call") 61 | user.password = generate_password_hash(new_pass) 62 | user.changed_pass = True 63 | db.session.commit() 64 | flash("Updated your password, Welcome to the Admin Dashboard") 65 | return redirect(url_for("posts.dash_posts")) 66 | 67 | -------------------------------------------------------------------------------- /blog_admin/app.py: -------------------------------------------------------------------------------- 1 | from blog_admin import create_admin_app 2 | from os import environ 3 | 4 | config_name = environ.get("APP_CONFIG") 5 | admin_app = create_admin_app(config_name) 6 | 7 | 8 | if __name__ == '__main__': 9 | admin_app.run(host=str(environ.get("FLASK_HOST")), port=str(environ.get("FLASK_ADMIN_PORT"))) 10 | -------------------------------------------------------------------------------- /blog_admin/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog_admin/auth/__init__.py -------------------------------------------------------------------------------- /blog_admin/auth/auth_controller.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, request, redirect,url_for, current_app 2 | from flask_login import login_required, current_user, logout_user 3 | from common.services import comments_service 4 | 5 | auth_bp = Blueprint("auth", __name__) 6 | 7 | @current_app.context_processor 8 | def inject_data(): 9 | count_comments = comments_service.CommentService.get_comment_count(is_admin=True) 10 | return dict(user=current_user, no_comments=count_comments) 11 | 12 | @auth_bp.route("/", methods=["GET"]) 13 | def admin(): 14 | if current_user.is_authenticated: 15 | return redirect(url_for("posts.dash_posts")) 16 | 17 | return render_template("auth/login.html") 18 | 19 | @auth_bp.route("/intrim_login", methods=["GET"]) 20 | @login_required 21 | def intrim_login(): 22 | if current_user.changed_pass: 23 | return redirect(url_for("posts.dash_posts")) 24 | 25 | return render_template("auth/login_intrim.html") 26 | 27 | @auth_bp.route("/dashboard", methods=["GET"]) 28 | @login_required 29 | def dashboard(): 30 | return render_template("dashboard/dashboard.html") 31 | 32 | 33 | @auth_bp.route("/logout") 34 | @login_required 35 | def logout(): 36 | logout_user() 37 | return redirect(url_for("auth.admin")) 38 | -------------------------------------------------------------------------------- /blog_admin/posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog_admin/posts/__init__.py -------------------------------------------------------------------------------- /blog_admin/posts/comments_controller.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | from flask_login import login_required, current_user 3 | from common.services import comments_service 4 | 5 | comments_bp = Blueprint("comments", __name__) 6 | #count_comments = comments_service.CommentService.get_comment_count(is_admin=True) 7 | 8 | 9 | @comments_bp.route("/comments.html") 10 | @login_required 11 | def dash_comments(): 12 | #load all comments under_moderation 13 | comments = comments_service.CommentService.get_comments(is_admin=True) 14 | return render_template("dashboard/comments_moderation.html", comments=comments) 15 | -------------------------------------------------------------------------------- /blog_admin/posts/posts_controller.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | from flask_login import login_required, current_user 3 | from common.services import posts_service, comments_service 4 | 5 | posts_bp = Blueprint("posts", __name__) 6 | 7 | # Count the comments 8 | #count_comments = comments_service.CommentService.get_comment_count(is_admin=True) 9 | 10 | 11 | @posts_bp.route("/overview.html") 12 | @login_required 13 | def dash_overview(): 14 | return render_template("dashboard/overview.html") 15 | 16 | 17 | @posts_bp.route("/posts.html") 18 | @login_required 19 | def dash_posts(): 20 | #load all posts 21 | posts = posts_service.PostService.get_all_posts(order_by=True, is_admin=True) 22 | return render_template("dashboard/posts.html", posts=posts) 23 | 24 | 25 | @posts_bp.route("/add_post.html") 26 | @login_required 27 | def add_post(): 28 | return render_template("dashboard/add_post.html") 29 | 30 | @posts_bp.route("/edit_post.html/") 31 | @login_required 32 | def edit_post(post_title): 33 | post_data = posts_service.PostService.get_post_by_title(post_title, is_admin=True) 34 | if len(post_data) > 0: 35 | data_resp = {"post_data": post_data[0], "tags": posts_service.PostService.serialize_tags(post_data[1])} 36 | else: 37 | data_resp = {"post_data": False, "tags": False} 38 | 39 | return render_template("dashboard/edit_post.html", data_resp=data_resp) 40 | -------------------------------------------------------------------------------- /blog_admin/posts/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/blog_admin/posts/service/__init__.py -------------------------------------------------------------------------------- /blog_admin/posts/service/_posts_service.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import traceback 3 | from sqlalchemy import exc 4 | from common import db 5 | from common.models import posts_model 6 | from common.models import images_model 7 | from common.models import tags_model 8 | 9 | class PostService(object): 10 | def __init__(self): 11 | pass 12 | 13 | def add_post(self, blog_title, blog_author, blog_content, curr_user, image_list, post_tags_list): 14 | try: 15 | posts = posts_model.Posts(content=blog_content, 16 | posted_date = datetime.datetime.now(), 17 | title = blog_title, 18 | author = blog_author, 19 | users = curr_user 20 | ) 21 | db.session.add(posts) 22 | db.session.commit() 23 | model_image_list = [images_model.Images(image_url = image_url, posts=posts) for image_url in image_list] 24 | #print(model_image_list) 25 | # insert_bulk() 26 | db.session.add_all(model_image_list) 27 | db.session.commit() 28 | # Add the tags 29 | model_tags_list = [tags_model.Tags(tag=tag, posts=posts) for tag in post_tags_list] 30 | db.session.add_all(model_tags_list) 31 | db.session.commit() 32 | return True 33 | except exc.SQLAlchemyError: 34 | traceback.print_exc() 35 | return False 36 | 37 | 38 | def view_post(self): 39 | pass 40 | 41 | def delete_post(self, post): 42 | try: 43 | #get the tags associated with the post 44 | tags = tags_model.Tags.query.filter_by(post=post).all() 45 | if tags: 46 | db.session.delete(tags) 47 | db.session.commit() 48 | return True 49 | except exc.SQLAlchemyError: 50 | traceback.print_exc() 51 | return False 52 | 53 | def cmp_add(self,new_items, db_items, reference, db_type): 54 | for item in new_items: 55 | if item not in db_items: 56 | if db_type == "IMAGES": 57 | db_obj = images_model.Images(image_url = item, posts=reference) 58 | elif db_type=="TAG": 59 | db_obj = tags_model.Tags(tag = item, posts=reference) 60 | db.session.add(db_obj) 61 | db.session.commit() 62 | 63 | def edit_post(self, post, new_title, new_content, new_image_list, new_tags_list): 64 | try: 65 | post.title = new_title 66 | post.content = new_content 67 | db.session.add(post) 68 | db.session.commit() 69 | db_image_obj = images_model.Images.query.filter_by(posts=post).all() 70 | db_img_list = [db_image.image_url for db_image in db_image_obj] 71 | self.cmp_add(new_image_list, db_img_list, post, "IMAGES") 72 | # Add the tags 73 | db_tag_obj = tags_model.Tags.query.filter_by(posts=post).all() 74 | db_tag_list = [db_tag.tag for db_tag in db_tag_obj] 75 | self.cmp_add(new_tags_list, db_tag_list, post, "TAG") 76 | return True 77 | except exc.SQLAlchemyError: 78 | return False 79 | 80 | 81 | @classmethod 82 | def count_post(cls): 83 | post_count = posts_model.Posts.query.count() 84 | return post_count 85 | 86 | @classmethod 87 | def get_all_posts(cls): 88 | posts = posts_model.Posts.query.all() 89 | return posts 90 | 91 | @classmethod 92 | def get_post_by_title(cls, post_title): 93 | post = posts_model.Posts.query.filter_by(title = post_title).first() 94 | return post 95 | -------------------------------------------------------------------------------- /blog_admin/static/css/amsify.suggestags.css: -------------------------------------------------------------------------------- 1 | .amsify-suggestags-area 2 | .amsify-suggestags-input-area-default { 3 | cursor: pointer; 4 | border: 1px solid #cccccc; 5 | min-height: 20px; 6 | padding: 8px 5px; 7 | } 8 | 9 | .amsify-suggestags-area 10 | .amsify-suggestags-input-area { 11 | text-align: left; 12 | height: auto; 13 | } 14 | 15 | .amsify-suggestags-area 16 | .amsify-suggestags-input-area:hover { 17 | cursor: text; 18 | } 19 | 20 | .amsify-suggestags-area 21 | .amsify-suggestags-input-area 22 | .amsify-suggestags-input { 23 | max-width: 200px; 24 | padding: 0px 4px; 25 | border: 0; 26 | } 27 | 28 | .amsify-suggestags-area 29 | .amsify-suggestags-input-area 30 | .amsify-suggestags-input:focus { 31 | outline: 0; 32 | } 33 | 34 | .amsify-focus { 35 | border-color: #66afe9; 36 | outline: 0; 37 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); 38 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); 39 | } 40 | 41 | .amsify-focus-light { 42 | border-color: #cacaca; 43 | outline: 0; 44 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(189, 189, 189, 0.6); 45 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(189, 189, 189, 0.6); 46 | } 47 | 48 | .amsify-suggestags-area 49 | .amsify-suggestags-label { 50 | cursor: pointer; 51 | min-height: 20px; 52 | } 53 | 54 | .amsify-toggle-suggestags { 55 | float: right; 56 | cursor: pointer; 57 | } 58 | 59 | .amsify-suggestags-area .amsify-suggestags-list { 60 | display: none; 61 | position: absolute; 62 | background: white; 63 | border: 1px solid #dedede; 64 | z-index: 1; 65 | } 66 | 67 | .amsify-suggestags-area 68 | .amsify-suggestags-list 69 | ul.amsify-list { 70 | list-style: none; 71 | padding: 3px 0px; 72 | max-height: 150px; 73 | overflow-y: auto; 74 | } 75 | 76 | .amsify-suggestags-area 77 | .amsify-suggestags-list 78 | ul.amsify-list 79 | li.amsify-list-item { 80 | text-align: left; 81 | cursor: pointer; 82 | padding: 0px 10px; 83 | } 84 | 85 | .amsify-suggestags-area 86 | .amsify-suggestags-list 87 | ul.amsify-list 88 | li.amsify-list-item:active { 89 | background: #717171; 90 | color: white; 91 | -moz-box-shadow: inset 0 0 10px #000000; 92 | -webkit-box-shadow: inset 0 0 10px #000000; 93 | box-shadow: inset 0 0 10px #000000; 94 | } 95 | 96 | .amsify-suggestags-area 97 | .amsify-suggestags-list 98 | ul.amsify-list 99 | li.amsify-list-group { 100 | text-align: left; 101 | padding: 0px 10px; 102 | font-weight: bold; 103 | } 104 | 105 | .amsify-suggestags-area 106 | .amsify-suggestags-list 107 | ul.amsify-list 108 | li.amsify-item-pad { 109 | padding-left: 30px; 110 | } 111 | 112 | .amsify-suggestags-area 113 | .amsify-suggestags-list 114 | ul.amsify-list 115 | li.amsify-item-noresult { 116 | display: none; 117 | color: #ff6060; 118 | font-weight: bold; 119 | text-align: center; 120 | } 121 | 122 | .amsify-suggestags-area 123 | .amsify-suggestags-list 124 | .amsify-select-input { 125 | display: none; 126 | } 127 | 128 | .amsify-suggestags-area 129 | .amsify-suggestags-list 130 | ul.amsify-list 131 | li.active { 132 | background: #d9d8d8; 133 | } 134 | 135 | .amsify-suggestags-area 136 | .amsify-suggestags-list 137 | ul.amsify-list 138 | li.amsify-item-pad.active { 139 | font-weight: normal; 140 | } 141 | 142 | .amsify-suggestags-input-area 143 | .amsify-select-tag { 144 | padding: 2px 7px; 145 | margin: 0px 4px 1px 0px; 146 | -webkit-border-radius: 2px; 147 | -moz-border-radius: 2px; 148 | border-radius: 2px; 149 | display: inline-block; 150 | } 151 | 152 | .amsify-suggestags-input-area 153 | .amsify-select-tag.col-bg { 154 | background: #d8d8d8; 155 | color: black; 156 | } 157 | 158 | /*.amsify-suggestags-input-area 159 | .amsify-select-tag:hover { 160 | background: #737373; 161 | color: white; 162 | }*/ 163 | 164 | .amsify-suggestags-input-area 165 | .disabled.amsify-select-tag { 166 | background: #eaeaea; 167 | color: #b9b9b9; 168 | pointer-events: none; 169 | } 170 | 171 | .amsify-suggestags-input-area 172 | .flash.amsify-select-tag { 173 | background-color: #f57f7f; 174 | -webkit-transition: background-color 200ms linear; 175 | -ms-transition: background-color 200ms linear; 176 | transition: background-color 200ms linear; 177 | } 178 | 179 | .amsify-suggestags-input-area 180 | .amsify-remove-tag { 181 | cursor: pointer; 182 | } -------------------------------------------------------------------------------- /blog_admin/static/js/app.js: -------------------------------------------------------------------------------- 1 | import {add_post, delete_post, edit_post} from './posts_app.js'; 2 | import {approve_comment} from './comments_app.js'; 3 | 4 | $(document).ready(function(){ 5 | $(".toast").toast("hide"); 6 | $("#log").hide(); 7 | $("#posts-dtable #comments-dtable").DataTable(); 8 | //Loading the summernote 9 | $('#summernote').summernote({ 10 | placeholder: 'Hello !!!', 11 | tabsize: 2, 12 | height: 300, 13 | minHeight:null, 14 | maxHeight:null, 15 | toolbar: [ 16 | ['style', ['style']], 17 | ['font', ['bold', 'underline', 'clear']], 18 | ['color', ['color']], 19 | ['para', ['ul', 'ol', 'paragraph']], 20 | ['table', ['table']], 21 | ['insert', ['link', 'picture', 'video']], 22 | ['view', ['fullscreen', 'codeview', 'help']] 23 | ], 24 | callbacks: { 25 | onImageUpload: function(image){ 26 | uploadImage(image[0]); 27 | }, 28 | 29 | onMediaDelete: function(file){ 30 | console.log("Filename to be deleted -- ", file[0].src); 31 | deleteImage(file[0].src); 32 | } 33 | } 34 | }); 35 | 36 | // Loading data-tag 37 | $('input[name="categoryFormInput"]').amsifySuggestags({ 38 | tagLimit: 4, 39 | trimValue: true, 40 | dashSpaces: true 41 | }); 42 | 43 | function uploadImage(image){ 44 | var csrftoken = $('meta[name=csrf-token]').attr('content'); 45 | //Upload image via ajax 46 | $("#log").empty(); 47 | $("#log").hide(); 48 | var data = new FormData(); 49 | var blog_title = $("#titleFormInput").val(); 50 | 51 | //console.log(blog_title); 52 | //data = JSON.stringify({"image":image, "blog_title":blog_title}); 53 | data.append("image", image); 54 | // data.append("blog_title", blog_title); 55 | $.ajax({ 56 | url: "/image_upload", 57 | cache: false, 58 | contentType: false, 59 | processData: false, 60 | data: data, 61 | type: "POST", 62 | beforeSend: function(xhr, settings){ 63 | if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { 64 | xhr.setRequestHeader("X-CSRFToken", csrftoken) 65 | } 66 | 67 | if (blog_title === ""){ 68 | $("#log").append("The title of the blog cannot be empty before uploading an image !!"); 69 | $("#log").show(); 70 | this.abort(); 71 | } 72 | 73 | 74 | }, 75 | success: function(filename) 76 | { 77 | $("#log").empty(); 78 | $("#log").append("Successfully uploaded the file"); 79 | $("#log").show(); 80 | var image = $("").attr("src",filename).addClass("img-fluid"); 81 | $("#summernote").summernote("insertNode", image[0]); 82 | }, 83 | error: function(data){ 84 | console.log(data); 85 | $("#log").empty(); 86 | $("#log").append(data); 87 | $("#log").show(); 88 | } 89 | }); 90 | } 91 | 92 | 93 | function deleteImage(image){ 94 | var csrftoken = $('meta[name=csrf-token]').attr('content'); 95 | //Upload image via ajax 96 | $("#log").empty(); 97 | $("#log").hide(); 98 | $.ajax({ 99 | url: "/image_delete", 100 | cache: false, 101 | processData: false, 102 | contentType: "application/json", 103 | data: JSON.stringify({"image_file":image}), 104 | type: "POST", 105 | 106 | beforeSend: function(xhr, settings){ 107 | if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { 108 | xhr.setRequestHeader("X-CSRFToken", csrftoken) 109 | } 110 | }, 111 | success: function(resp) 112 | { 113 | $("#log").empty(); 114 | $("#log").append(resp); 115 | $("#log").show(); 116 | }, 117 | error: function(resp){ 118 | console.log(resp); 119 | $("#log").empty(); 120 | $("#log").append(resp); 121 | $("#log").show(); 122 | } 123 | }); 124 | } 125 | 126 | //Add a post to the database 127 | $("#add-form-post").on("submit", function(e){ 128 | e.preventDefault(); 129 | $("#log").hide(); 130 | var blog_title = $("#titleFormInput").val(); 131 | var blog_author = $("#authorFormInput").val(); 132 | var blog_content = $('#summernote').summernote('code'); 133 | var blog_tags = $("span.amsify-select-tag").get_items("items"); 134 | 135 | add_post(blog_title, blog_author, blog_content, blog_tags); 136 | }); 137 | 138 | //Edit the post and make changes 139 | $("#edit-form-post").on("submit",function(e){ 140 | e.preventDefault(); 141 | $("#log").hide(); 142 | var blog_title = $("#edit_form_title").val(); 143 | var old_title = $("#edit_form_old_title").val(); 144 | var blog_content = $('#summernote').summernote('code'); 145 | var blog_tags = $("span.amsify-select-tag").get_items("items"); 146 | edit_post(blog_title, blog_content, old_title, blog_tags); 147 | }); 148 | 149 | 150 | //delete the post 151 | $("#posts-dtable").on("click", ".delete-blogpost", function(e){ 152 | e.preventDefault(); 153 | var closest_tr = $(this).closest("tr"); 154 | $("#log").hide(); 155 | var blog_title = $(closest_tr).find("#post_id").text(); 156 | //alert(blog_title); 157 | delete_post(blog_title, closest_tr); 158 | }); 159 | 160 | //Change status of the comment 161 | $(".comment-status").on("change", function(e){ 162 | var comment_status = this.value; 163 | var comment_ref_id = $(this).closest("tr").attr("id"); 164 | //Settings 165 | if ($(".commentlog > strong").length > 0){ 166 | $("strong").remove(); 167 | } 168 | //approve comments 169 | approve_comment(comment_status, comment_ref_id); 170 | }); 171 | 172 | //End of document 173 | 174 | }); -------------------------------------------------------------------------------- /blog_admin/static/js/comments_app.js: -------------------------------------------------------------------------------- 1 | function approve_comment(comment_status, comment_ref_id) { 2 | var data = { 3 | "comment_status": comment_status, 4 | "comment_ref_id": comment_ref_id 5 | }; 6 | 7 | var csrftoken = $('meta[name=csrf-token]').attr('content'); 8 | $.ajax({ 9 | url: "/api/comment", 10 | cache: false, 11 | contentType: "application/json", 12 | processData: false, 13 | data: JSON.stringify(data), 14 | type: "PUT", 15 | beforeSend: function (xhr, settings) { 16 | if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { 17 | xhr.setRequestHeader("X-CSRFToken", csrftoken) 18 | } 19 | 20 | if (comment_status === "" || comment_ref_id === "") { 21 | $('' + "Empty payload, aborting request!!" + "").prependTo(".commentlog"); 22 | $(".toast").toast("show"); 23 | this.abort(); 24 | } 25 | 26 | }, 27 | success: function (response) { 28 | if (response.resp){ 29 | $('' + response.message + '').prependTo(".commentlog"); 30 | $(".toast").toast("show"); 31 | $("#" + comment_ref_id).remove(); 32 | temp_val = $("#comment-badge").text(); 33 | new_val = Integer.parseInt(temp_val) - 1; 34 | if (new_val > 0){ 35 | $("#comment-badge").text(new_val); 36 | } 37 | else{ 38 | $("a.notification").remove(); 39 | } 40 | 41 | } 42 | else{ 43 | $('' + response.message + '').prependTo(".commentlog"); 44 | $(".toast").toast("show"); 45 | } 46 | 47 | }, 48 | error: function (response) { 49 | console.log(data); 50 | $('' + response.message + '
    ').prependTo(".commentlog"); 51 | $(".toast").toast("show"); 52 | } 53 | }); 54 | 55 | } 56 | 57 | //Export functions to be imported in main wrapper 58 | export { approve_comment }; -------------------------------------------------------------------------------- /blog_admin/static/js/posts_app.js: -------------------------------------------------------------------------------- 1 | function add_post(blog_title, blog_author, blog_content, blog_tags) { 2 | //Upload image via ajax 3 | $("#log").empty(); 4 | $("#log").hide(); 5 | //var data = new FormData(); 6 | var data = { 7 | "blog_title": blog_title, 8 | "blog_author": blog_author, 9 | "blog_content": blog_content, 10 | "blog_tags": blog_tags 11 | }; 12 | //data.append("blog_title", blog_title); 13 | //data.append("blog_author", blog_author); 14 | //data.append("blog_content", blog_content) 15 | var csrftoken = $('meta[name=csrf-token]').attr('content'); 16 | $.ajax({ 17 | url: "/api/post", 18 | cache: false, 19 | contentType: "application/json", 20 | processData: false, 21 | data: JSON.stringify(data), 22 | type: "POST", 23 | beforeSend: function (xhr, settings) { 24 | if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { 25 | xhr.setRequestHeader("X-CSRFToken", csrftoken) 26 | } 27 | 28 | if (blog_title === "") { 29 | $("#log").append("The title of the blog cannot be empty before uploading an image !!"); 30 | $("#log").show().fadeOut(3000, "linear"); 31 | this.abort(); 32 | } 33 | 34 | }, 35 | success: function (response) { 36 | $("#log").empty(); 37 | //$("#log").append(response); 38 | //$("#log").show(); 39 | window.location.replace(response.redirect_uri) 40 | }, 41 | error: function (data) { 42 | console.log(data); 43 | $("#log").empty(); 44 | $("#log").append(data); 45 | $("#log").show().fadeOut(3000,"linear"); 46 | } 47 | }); 48 | } 49 | 50 | function view_post() { 51 | 52 | 53 | } 54 | 55 | 56 | function delete_post(blog_title, closest_tr) { 57 | //Upload image via ajax 58 | $("#log").empty(); 59 | $("#log").hide(); 60 | //var data = new FormData(); 61 | var data = { 62 | "blog_title": blog_title 63 | }; 64 | 65 | var csrftoken = $('meta[name=csrf-token]').attr('content'); 66 | $.ajax({ 67 | url: "/api/post", 68 | cache: false, 69 | contentType: "application/json", 70 | processData: false, 71 | data: JSON.stringify(data), 72 | type: "DELETE", 73 | beforeSend: function (xhr, settings) { 74 | if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { 75 | xhr.setRequestHeader("X-CSRFToken", csrftoken) 76 | } 77 | 78 | }, 79 | success: function (response) { 80 | closest_tr.remove(); 81 | $("#log").empty(); 82 | $("#log").append(response); 83 | $("#log").show().fadeOut(3000,"linear"); 84 | }, 85 | error: function (data) { 86 | console.log(data); 87 | $("#log").empty(); 88 | $("#log").append(data); 89 | $("#log").show().fadeOut(3000,"linear"); 90 | } 91 | }); 92 | 93 | } 94 | 95 | 96 | function edit_post(blog_title, blog_content, old_title, blog_tags) { 97 | //Upload image via ajax 98 | $("#log").empty(); 99 | $("#log").hide(); 100 | //var data = new FormData(); 101 | var data = { 102 | "blog_title": blog_title, 103 | "blog_content": blog_content, 104 | "old_title": old_title, 105 | "blog_tags": blog_tags 106 | }; 107 | 108 | var csrftoken = $('meta[name=csrf-token]').attr('content'); 109 | $.ajax({ 110 | url: "/api/post", 111 | cache: false, 112 | contentType: "application/json", 113 | processData: false, 114 | data: JSON.stringify(data), 115 | type: "PUT", 116 | beforeSend: function (xhr, settings) { 117 | if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { 118 | xhr.setRequestHeader("X-CSRFToken", csrftoken) 119 | } 120 | 121 | if (blog_title === "" || blog_content === "") { 122 | $("#log").append("The title of the blog cannot be empty!!"); 123 | $("#log").show(); 124 | this.abort(); 125 | } 126 | 127 | }, 128 | success: function (response) { 129 | $("#log").empty(); 130 | $("#log").append(response); 131 | $("#log").show(); 132 | }, 133 | error: function (data) { 134 | console.log(data); 135 | $("#log").empty(); 136 | $("#log").append(data); 137 | $("#log").show(); 138 | } 139 | }); 140 | 141 | } 142 | 143 | //Export functions to be imported in main wrapper 144 | export { add_post, delete_post, view_post, edit_post }; -------------------------------------------------------------------------------- /blog_admin/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | Simplistic Blogger Admin 16 | 49 | 50 | 51 | 52 |

    Blog Admin Panel

    53 | 83 | 84 | 85 | 86 | 87 | 90 | 93 | 94 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /blog_admin/templates/auth/login_intrim.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Simplistic Blogger Admin 12 | 39 | 40 | 41 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 79 | 80 | -------------------------------------------------------------------------------- /blog_admin/templates/dashboard/add_post.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard/dashboard.html" %} 2 | {% block content %} 3 |
    4 | 5 |
    6 |
    7 | 8 | 9 |
    10 |
    11 | 12 | 13 |
    14 |
    15 | 16 | 17 |
    18 |
    19 | 20 | 21 |
    22 |
    23 | 24 | 25 |
    26 |
    27 |
    28 | {% endblock %} -------------------------------------------------------------------------------- /blog_admin/templates/dashboard/comments_moderation.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard/dashboard.html" %} 2 | {% block content %} 3 |
    4 |
    5 |
    6 | 7 | 18 |
    19 | 20 |
    21 |

    Comments Moderation

    22 |
    23 |
    24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% for comment in comments %} 32 | 33 | 60 | 61 | {% endfor %} 62 | 63 |
    Details
    34 |
    35 |
    36 |
    37 |
    Posted on 38 | {{comment.posted_date}}
    39 |
    Name: {{comment.author_name}}
    40 |
    Email: {{comment.author_email}}
    41 |
    42 |
    43 |

    44 |

    Comment Content:
    {{comment.content}} 45 |

    46 |
    47 |

    48 |

    More Info: Visit Post 49 |
    50 | Actions: 51 | 56 |

    57 |
    58 |
    59 |
    64 |
    65 |
    66 |
    67 | {% endblock %} -------------------------------------------------------------------------------- /blog_admin/templates/dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Simplistic Blogger Admin 25 | 98 | 99 | 100 | 101 |
    138 | 139 |
    140 |
    141 | 182 | {% block content %} 183 | {% endblock %} 184 | 185 |
    186 |
    187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /blog_admin/templates/dashboard/edit_post.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard/dashboard.html" %} 2 | {% block content %} 3 |
    4 | 5 |
    6 |
    7 | 8 | 9 | 10 |
    11 |
    12 | 13 | 14 |
    15 |
    16 | 17 | 18 |
    19 |
    20 | 21 | 22 |
    23 |
    24 |
    25 | {% endblock %} -------------------------------------------------------------------------------- /blog_admin/templates/dashboard/overview.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard/dashboard.html" %} 2 | {% block content %} 3 |
    4 |

    Dashboard Overview

    5 |

    COMING SOON

    6 |
    7 | {% endblock %} -------------------------------------------------------------------------------- /blog_admin/templates/dashboard/posts.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard/dashboard.html" %} 2 | {% block content %} 3 |
    4 |

    Posts Overview

    5 | 10 | {% with messages = get_flashed_messages() %} 11 | {% if messages %} 12 | 18 | {% endif %} 19 | {% endwith %} 20 |      32 |
    33 |
    34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {% for post in posts %} 44 | 45 | 46 | 47 | 68 | 69 | {% endfor %} 70 |
    TitlePosted OnActions
    {{post.title}}{{post.posted_date}} 48 | 50 | 53 | 54 | 55 | 56 | Edit 57 | 67 |
    71 |
    72 |
    73 |
    74 | {% endblock %} -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from flask_caching import Cache 3 | from flask_mail import Mail 4 | 5 | 6 | db = SQLAlchemy() 7 | cache = Cache() 8 | mail = Mail() 9 | -------------------------------------------------------------------------------- /common/controller/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/common/controller/__init__.py -------------------------------------------------------------------------------- /common/controller/comments_api_controller.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | from flask import Blueprint, request, jsonify 4 | from common.services.posts_service import PostService 5 | from common.services import comments_service 6 | 7 | comments_bp = Blueprint( 8 | "comments_api_bp", __name__) 9 | 10 | 11 | def verify_recaptcha(recaptcha_response): 12 | url = "https://www.google.com/recaptcha/api/siteverify" 13 | payload = {'secret':os.environ.get("RECAPTCHA_SITE_SECRET"),'response':recaptcha_response} 14 | response = requests.post(url, data=payload) 15 | return response.json() 16 | 17 | 18 | @comments_bp.route("/api/comment", methods=["POST"]) 19 | def add_comment(): 20 | request_body = request.get_json() 21 | author_name = request_body["author_name"] 22 | author_email = request_body["author_email"] 23 | author_comment = request_body["author_comment"] 24 | blog_title = request_body["blog_title"] 25 | g_recaptcha = request_body["g_recaptcha"] 26 | is_admin = False 27 | post_data = PostService().get_post_by_title(blog_title) 28 | 29 | if len(post_data) > 0: 30 | # Ideally there must be a post 31 | comments_service_obj = comments_service.CommentService() 32 | # Verify recaptcha else do not add the comment... 33 | if verify_recaptcha(g_recaptcha)["success"]: 34 | print("Recaptcha is verified, is human") 35 | if author_email == os.environ.get("EMAIL"): 36 | is_admin=True 37 | 38 | if comments_service_obj.add_comment(author_name, author_email, author_comment, post_data[0], is_admin=is_admin): 39 | if is_admin: 40 | c_status = comments_service.CommentService.send_email(author_comment, author_name, post_data[0]) 41 | if c_status: 42 | #Admin and it is a reply 43 | return "Your comment is posted and notification sent to the original commenter" 44 | 45 | #Admin but not reply 46 | return "Your comment is posted" 47 | 48 | # Commented but not admin 49 | return "Your comment is posted and is under moderation" 50 | 51 | #Internal error 52 | return "There has been an error while posting the comment, try after sometime..." 53 | #recaptcha failed 54 | return "Cannot comment this request" 55 | 56 | 57 | @comments_bp.route("/api/comment", methods=["PUT"]) 58 | def approve_comment(): 59 | request_body = request.get_json() 60 | comment_ref_id = request_body["comment_ref_id"] 61 | comment_status = request_body["comment_status"] 62 | if comment_ref_id and comment_status: 63 | #Edit to approve or reject comment 64 | comment_serv_obj = comments_service.CommentService() 65 | resp = comment_serv_obj.edit_comment(comment_ref_id.split("-")[1], comment_status) 66 | print(resp) 67 | return jsonify(resp) 68 | else: 69 | data_resp = {"resp":False, "message": "Payload passed is empty"} 70 | return jsonify(data_resp) 71 | -------------------------------------------------------------------------------- /common/controller/images_api_controller.py: -------------------------------------------------------------------------------- 1 | from flask import (Blueprint,send_from_directory, request, abort) 2 | import os 3 | from flask_login import login_required 4 | import datetime 5 | from werkzeug.utils import secure_filename 6 | import traceback 7 | from common import db 8 | from common.models.images_model import Images 9 | from sqlalchemy import exc 10 | 11 | 12 | image_bp = Blueprint("image_api", __name__) 13 | 14 | # Allowed files to be uploaded 15 | ALLOWED_EXTENSIONS = set(["png", "jpg", "jpeg", "gif"]) 16 | 17 | 18 | def allowed_file(filename): 19 | return '.' in filename and \ 20 | filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS 21 | 22 | 23 | @image_bp.route("/image_upload", methods=["GET", "POST"]) 24 | @login_required 25 | def upload_images(): 26 | if request.method == "POST": 27 | file = request.files["image"] 28 | current_dm = str(datetime.datetime.now()).split(".")[0].split(" ")[0].replace("-","") 29 | 30 | if file.filename == "": 31 | return "error.png" 32 | if file and allowed_file(file.filename): 33 | try: 34 | filename = secure_filename(file.filename) 35 | if not os.path.exists(os.environ.get("UPLOAD_FOLDER") + "/" + current_dm): 36 | os.makedirs(os.environ.get("UPLOAD_FOLDER") + "/" + current_dm) 37 | file.save(os.path.normpath(os.path.join(os.environ.get("UPLOAD_FOLDER"), 38 | current_dm, filename))) 39 | set_url = "".join("/image/" + current_dm + "/" + file.filename) 40 | return set_url 41 | except Exception: 42 | traceback.print_exc() 43 | abort(500) 44 | return "Internal Error, check logs" 45 | 46 | @image_bp.route("/image_delete", methods=["POST"]) 47 | @login_required 48 | def delete_image(): 49 | try: 50 | payload = request.get_json() 51 | image_file = payload["image_file"] 52 | image_path_list = image_file.split("/") 53 | blog_post_val = image_path_list[len(image_path_list) - 2] 54 | image_file_name = image_path_list[len(image_path_list) - 1] 55 | image_rel_url = "/image" + "/" + blog_post_val + "/" + image_file_name 56 | 57 | #file location removal 58 | os.remove(os.environ.get("UPLOAD_FOLDER") + "/" + blog_post_val + "/" + image_file_name) 59 | 60 | image = Images.query.filter_by(image_url=image_rel_url).first() 61 | if image: 62 | #Remove from database 63 | #database 64 | db.session.delete(image) 65 | db.session.commit() 66 | 67 | return "Successfully removed the image file" 68 | 69 | except (FileNotFoundError, exc.SQLAlchemyError): 70 | traceback.print_exc() 71 | return "File cannot be delete, check logs !!!" 72 | 73 | @image_bp.route("/image//", methods=["GET"]) 74 | def get_image(curr_dm, filename): 75 | try: 76 | dir_abs_path = os.path.dirname(os.path.abspath(os.environ.get("UPLOAD_FOLDER"))) 77 | return send_from_directory(dir_abs_path + "\\" + os.environ.get("UPLOAD_FOLDER") + "\\" + str(curr_dm), filename) 78 | except FileNotFoundError: 79 | abort(404) 80 | -------------------------------------------------------------------------------- /common/controller/posts_api_controller.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, jsonify, url_for 2 | from flask_login import login_required, current_user 3 | from bs4 import BeautifulSoup 4 | from common.services import posts_service 5 | 6 | 7 | posts_api_bp = Blueprint("posts_api_bp", __name__) 8 | 9 | 10 | @posts_api_bp.route("/post", methods=["POST"]) 11 | @login_required 12 | def add(): 13 | request_body = request.get_json() 14 | blog_title = request_body["blog_title"] 15 | blog_content = request_body["blog_content"] 16 | blog_author = request_body["blog_author"] 17 | blog_tags_list = request_body["blog_tags"] 18 | 19 | if len(blog_tags_list) == 0: 20 | blog_tags_list.append("uncategorized") 21 | 22 | # Parse the image url and store it for the purposes of logging 23 | html_parsed = BeautifulSoup(blog_content, "html.parser") 24 | img_list = [image["src"] for image in html_parsed.find_all("img")] 25 | print(img_list) 26 | 27 | # Call the post service to add the post 28 | p_service_obj = posts_service.PostService() 29 | rc = p_service_obj.add_post(blog_title, blog_author, blog_content, current_user, img_list, blog_tags_list) 30 | if rc: 31 | data_resp = {"redirect_uri": url_for("posts.dash_posts"), "message": "Successfully inserted the post !!"} 32 | return jsonify(data_resp) 33 | return "There has been some error while inserting the post. Check Logs and retry posting again" 34 | 35 | @posts_api_bp.route("/post", methods=["PUT"]) 36 | @login_required 37 | def edit(): 38 | request_body = request.get_json() 39 | blog_title = request_body["blog_title"] 40 | old_title = request_body["old_title"] 41 | blog_content = request_body["blog_content"] 42 | blog_tags_list = request_body["blog_tags"] 43 | 44 | if len(blog_tags_list) == 0: 45 | blog_tags_list.append("uncategorized") 46 | 47 | 48 | # Parse the image url and store it for the purposes of logging 49 | html_parsed = BeautifulSoup(blog_content, "html.parser") 50 | new_img_list = [image["src"] for image in html_parsed.find_all("img")] 51 | 52 | # Call the post service to add the post 53 | post_data = posts_service.PostService.get_post_by_title(old_title, is_admin=True) 54 | if len(post_data) > 0: 55 | p_obj = posts_service.PostService() 56 | post = post_data[0] 57 | tags = post_data[1] 58 | rc = p_obj.edit_post(post,tags, blog_title, blog_content, new_img_list, blog_tags_list) 59 | if rc: 60 | return "Successfully edited the blog content" 61 | return "Could not edit the content of blog, check logs" 62 | else: 63 | return "No such post to edit" 64 | 65 | @posts_api_bp.route("/post", methods=["DELETE"]) 66 | @login_required 67 | def delete(): 68 | request_body = request.get_json() 69 | blog_title = request_body["blog_title"] 70 | 71 | # Call the post service to add the post 72 | post_data = posts_service.PostService.get_post_by_title(blog_title, is_admin=True) 73 | if len(post_data) > 0: 74 | p_obj = posts_service.PostService() 75 | rc = p_obj.delete_post(post_data[0], post_data[1]) 76 | if rc: 77 | return "Successfully deleted the blog" 78 | return "Could not delete the content of blog, check logs" 79 | 80 | else: 81 | return "No such post to delete, must be a hack !!!" 82 | 83 | # @posts_api_bp.route("/post", methods=["GET"]) 84 | # @login_required 85 | # def get_n_posts(): 86 | # limit = request.params.get("limit") 87 | # posts = posts_service.PostService.get_all_posts() 88 | 89 | # if posts: 90 | # return posts[:limit] 91 | # else: 92 | # return False 93 | 94 | # @posts_api_bp.route("/post/", methods=["GET"]) 95 | # def get_title_post(blog_title): 96 | # # Call the post service to add the post 97 | # post = posts_service.PostService.get_post_by_title(blog_title) 98 | # if post: 99 | # return post 100 | # else: 101 | # return False 102 | -------------------------------------------------------------------------------- /common/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/common/models/__init__.py -------------------------------------------------------------------------------- /common/models/comments_model.py: -------------------------------------------------------------------------------- 1 | from common import db 2 | from common.models.reply_model import Replies 3 | 4 | class Comments(db.Model): 5 | __tablename__="comments" 6 | comment_id = db.Column(db.Integer, primary_key=True, autoincrement=True) 7 | comment_uuid = db.Column(db.String(255)) 8 | author_email = db.Column(db.String(255), nullable=False) 9 | author_name = db.Column(db.String(255), nullable=False) 10 | author_comment = db.Column(db.String(255)) 11 | posted_date = db.Column(db.DateTime) 12 | comment_state = db.Column(db.String(255), nullable=False) 13 | post_id = db.Column(db.Integer, db.ForeignKey("posts.p_id")) 14 | replies = db.relationship("Replies", backref="comments", lazy="dynamic") 15 | -------------------------------------------------------------------------------- /common/models/images_model.py: -------------------------------------------------------------------------------- 1 | from common import db 2 | 3 | class Images(db.Model): 4 | __tablename__="images" 5 | image_id = db.Column(db.Integer, primary_key=True) 6 | image_url = db.Column(db.String(255), nullable=True) 7 | post_id = db.Column(db.Integer, db.ForeignKey("posts.p_id")) 8 | -------------------------------------------------------------------------------- /common/models/posts_model.py: -------------------------------------------------------------------------------- 1 | from common import db 2 | from common.models.images_model import Images 3 | from common.models.tags_model import Tags 4 | from common.models.comments_model import Comments 5 | 6 | class Posts(db.Model): 7 | __tablename__ = "posts" 8 | p_id = db.Column(db.Integer, primary_key=True, autoincrement=True) 9 | content = db.Column(db.Text) 10 | posted_date = db.Column(db.DateTime) 11 | title = db.Column(db.String(255)) 12 | author = db.Column(db.String(255)) 13 | user_id = db.Column(db.Integer, db.ForeignKey("users.id")) 14 | modified_flag = db.Column(db.Boolean, default=False) 15 | images = db.relationship("Images", backref="posts", lazy=True) 16 | category = db.relationship("Tags", backref="posts", lazy="dynamic") 17 | comments = db.relationship("Comments", backref="posts", lazy="dynamic") 18 | -------------------------------------------------------------------------------- /common/models/reply_model.py: -------------------------------------------------------------------------------- 1 | from common import db 2 | 3 | class Replies(db.Model): 4 | __tablename__="replies" 5 | reply_id = db.Column(db.Integer, primary_key=True) 6 | content = db.Column(db.String(255)) 7 | posted_date = db.Column(db.DateTime) 8 | author_email = db.Column(db.String(255), nullable=False) 9 | reply_state = db.Column(db.String(255), nullable=False) 10 | comment_id = db.Column(db.Integer, db.ForeignKey("comments.comment_id")) 11 | -------------------------------------------------------------------------------- /common/models/tags_model.py: -------------------------------------------------------------------------------- 1 | from common import db 2 | 3 | class Tags(db.Model): 4 | __tablename__="tags" 5 | tag_id = db.Column(db.Integer, primary_key=True) 6 | tag = db.Column(db.String(50), nullable=True) 7 | post_id = db.Column(db.Integer, db.ForeignKey("posts.p_id")) 8 | -------------------------------------------------------------------------------- /common/models/users_model.py: -------------------------------------------------------------------------------- 1 | from common import db 2 | from flask_login import UserMixin 3 | from common.models.posts_model import Posts 4 | 5 | class Users(UserMixin, db.Model): 6 | __tablename__ = "users" 7 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 8 | user_name = db.Column(db.String(80), unique=True, nullable=False) 9 | password = db.Column(db.String(255), nullable=False) 10 | changed_pass = db.Column(db.Boolean(), nullable=False, default=False) 11 | f_name = db.Column(db.String(255), nullable=False) 12 | email = db.Column(db.String(255), nullable=False) 13 | profile_pic = db.Column(db.String(255), nullable=True) 14 | l_name = db.Column(db.String(255), nullable=True) 15 | posts = db.relationship("Posts", backref="users", lazy=True) 16 | 17 | def __repr__(self): 18 | return "" % self.user_name 19 | -------------------------------------------------------------------------------- /common/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/common/services/__init__.py -------------------------------------------------------------------------------- /common/services/comment_state_enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class States(enum.Enum): 4 | UNDER_MODERATION = 1 5 | APPROVED = 2 6 | REJECTED = 3 -------------------------------------------------------------------------------- /common/services/comments_service.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import traceback 3 | import uuid 4 | import os 5 | from libgravatar import Gravatar 6 | from sqlalchemy import exc, and_ 7 | from common import db, cache 8 | from common.models import posts_model, comments_model 9 | from common.services.comment_state_enums import States 10 | from common.services.utility import send_email, check_reply 11 | 12 | 13 | class CommentService(): 14 | def __init__(self): 15 | pass 16 | 17 | @classmethod 18 | def serialize_comments(cls, comments_db_obj, is_admin=False): 19 | comments_list = list() 20 | for comment_db_obj in comments_db_obj: 21 | if is_admin: 22 | comments_list.append({ 23 | "author_name": comment_db_obj.author_name, 24 | "author_email": comment_db_obj.author_email, 25 | "comment_ref_id": comment_db_obj.comment_uuid, 26 | "content": comment_db_obj.author_comment, 27 | "posted_date": comment_db_obj.posted_date.strftime('%B %d, %Y'), 28 | "post_link": "http://" + os.environ.get("FLASK_HOST") + ":" + os.environ.get("FLASK_BLOG_PORT") + "/blog/" + comment_db_obj.posts.title 29 | }) 30 | else: 31 | comments_list.append({ 32 | "author_name": comment_db_obj.author_name, 33 | "author_email": comment_db_obj.author_email, 34 | "comment_ref_id": comment_db_obj.comment_uuid, 35 | "content": comment_db_obj.author_comment, 36 | "image_url": Gravatar(comment_db_obj.author_email).get_image(default="robohash"), 37 | "posted_date": comment_db_obj.posted_date.strftime('%B %d, %Y'), 38 | }) 39 | 40 | return comments_list 41 | 42 | def add_comment(self, author_name, author_email, author_comment, post_db_obj, is_admin=False): 43 | try: 44 | if is_admin: 45 | comment_state = States.APPROVED.value 46 | else: 47 | comment_state = States.UNDER_MODERATION.value 48 | 49 | comment_db_obj = comments_model.Comments(author_name=author_name, 50 | author_email=author_email, 51 | author_comment=author_comment, 52 | comment_uuid=str( 53 | uuid.uuid4()).split("-")[0], 54 | posted_date=datetime.datetime.now(), 55 | comment_state= comment_state, 56 | posts=post_db_obj) 57 | db.session.add(comment_db_obj) 58 | db.session.commit() 59 | return True 60 | except exc.SQLAlchemyError: 61 | traceback.print_exc() 62 | return False 63 | 64 | @classmethod 65 | def send_email(cls, author_comment, author_name, post_db_obj): 66 | c_name_tuple, c_status = check_reply(author_comment, post_db_obj) 67 | if c_name_tuple and c_status: 68 | e_status = send_email(author_comment, author_name, c_name_tuple, post_db_obj) 69 | if e_status: 70 | print("The reply email is sent to -- ", c_name_tuple[0]) 71 | return True 72 | else: 73 | print("error while sending the email reply") 74 | return False 75 | else: 76 | return False 77 | 78 | @classmethod 79 | def get_comment_count(cls, is_admin=False): 80 | try: 81 | if is_admin: 82 | count = comments_model.Comments.query.filter_by(comment_state=States.UNDER_MODERATION.value).count() 83 | else: 84 | count = comments_model.Comments.query.filter_by(comment_state=States.APPROVED.value).count() 85 | return count 86 | except exc.SQLAlchemyError: 87 | traceback.print_exc() 88 | return 0 89 | 90 | @classmethod 91 | def get_comments(cls, post_db_obj=None, is_admin=False): 92 | if not post_db_obj and is_admin: 93 | comments = comments_model.Comments.query.filter_by( 94 | comment_state=States.UNDER_MODERATION.value).all() 95 | elif post_db_obj and is_admin: 96 | comments = comments_model.Comments.query.filter_by(posts=post_db_obj).filter_by( 97 | comment_state=States.UNDER_MODERATION.value).all() 98 | elif post_db_obj and not is_admin: 99 | comments = comments_model.Comments.query.filter_by(posts=post_db_obj).filter_by( 100 | comment_state=States.APPROVED.value).all() 101 | 102 | serialized_comments = cls.serialize_comments(comments, is_admin) 103 | return serialized_comments 104 | 105 | def get_comment(self): 106 | pass 107 | 108 | def delete_comment(self): 109 | pass 110 | 111 | def edit_comment(self, comment_ref_id, comment_status): 112 | try: 113 | #If the status is reject delete from db 114 | comment = comments_model.Comments.query.filter_by(comment_uuid=comment_ref_id).first() 115 | if comment: 116 | if int(comment_status) == States.REJECTED.value: 117 | db.session.delete(comment) 118 | db.session.commit() 119 | return {"resp":True, "message": "Deleted Comment"} 120 | elif int(comment_status) == States.APPROVED.value: 121 | # Edit the comment to be accept for posting 122 | comment.comment_state = States.APPROVED.value 123 | db.session.add(comment) 124 | db.session.commit() 125 | return {"resp":True, "message": "Approved Comment"} 126 | else: 127 | return {"resp":False, "message": "Comment does not exist in DB"} 128 | except (exc.SQLAlchemyError, AttributeError): 129 | traceback.print_exc() 130 | return {"resp": False, "message":"Internal System Error has occurred"} -------------------------------------------------------------------------------- /common/services/posts_service.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import traceback 3 | from sqlalchemy import exc 4 | from common import db 5 | from common.models import posts_model 6 | from common.models import images_model 7 | from common.models import tags_model 8 | from common import cache 9 | 10 | class PostService(object): 11 | def __init__(self): 12 | pass 13 | 14 | def add_post(self, blog_title, blog_author, blog_content, curr_user, image_list, post_tags_list): 15 | try: 16 | posts = posts_model.Posts(content=blog_content, 17 | posted_date = datetime.datetime.now(), 18 | title = blog_title, 19 | author = blog_author, 20 | users = curr_user 21 | ) 22 | db.session.add(posts) 23 | db.session.commit() 24 | model_image_list = [images_model.Images(image_url = image_url, posts=posts) for image_url in image_list] 25 | #print(model_image_list) 26 | # insert_bulk() 27 | db.session.add_all(model_image_list) 28 | db.session.commit() 29 | # Add the tags 30 | model_tags_list = [tags_model.Tags(tag=tag, posts=posts) for tag in post_tags_list] 31 | db.session.add_all(model_tags_list) 32 | db.session.commit() 33 | return True 34 | except exc.SQLAlchemyError: 35 | traceback.print_exc() 36 | return False 37 | 38 | def delete_post(self, post, tags): 39 | try: 40 | #get the tags associated with the post 41 | #tags = tags_model.Tags.query.filter_by(post=post).all() 42 | if tags: 43 | for db_tag in tags: 44 | db.session.delete(db_tag) 45 | db.session.commit() 46 | db.session.delete(post) 47 | db.session.commit() 48 | return True 49 | except exc.SQLAlchemyError: 50 | traceback.print_exc() 51 | return False 52 | 53 | def cmp_add(self,new_items, db_items, reference, db_type): 54 | for item in new_items: 55 | if item not in db_items: 56 | if db_type == "IMAGES": 57 | db_obj = images_model.Images(image_url = item, posts=reference) 58 | elif db_type=="TAG": 59 | db_obj = tags_model.Tags(tag = item, posts=reference) 60 | db.session.add(db_obj) 61 | db.session.commit() 62 | 63 | def edit_post(self, post, db_tags, new_title, new_content, new_image_list, new_tags_list): 64 | try: 65 | post.title = new_title 66 | post.content = new_content 67 | post.posted_date = datetime.datetime.now() 68 | db.session.add(post) 69 | db.session.commit() 70 | db_image_obj = images_model.Images.query.filter_by(posts=post).all() 71 | db_img_list = [db_image.image_url for db_image in db_image_obj] 72 | self.cmp_add(new_image_list, db_img_list, post, "IMAGES") 73 | # Add the tags 74 | #db_tag_obj = tags_model.Tags.query.filter_by(posts=post).all() 75 | db_tag_list = [db_tag.tag for db_tag in db_tags] 76 | if ("uncategorized" not in new_tags_list) and ("uncategorized" in db_tag_list): 77 | print("True") 78 | db_tag_list.remove("uncategorized") 79 | tag = tags_model.Tags.query.filter_by(tag="uncategorized", posts=post).first() 80 | db.session.delete(tag) 81 | db.session.commit() 82 | self.cmp_add(new_tags_list, db_tag_list, post, "TAG") 83 | return True 84 | except exc.SQLAlchemyError: 85 | return False 86 | 87 | @classmethod 88 | def count_post(cls): 89 | post_count = posts_model.Posts.query.count() 90 | return post_count 91 | 92 | @classmethod 93 | def get_all_posts(cls, order_by=False, is_admin=False): 94 | if is_admin and order_by: 95 | posts = posts_model.Posts.query.order_by(posts_model.Posts.posted_date.desc()).all() 96 | if is_admin: 97 | posts = posts_model.Posts.query.all() 98 | elif cache.get("all-posts-ordered"): 99 | print("Retreiving the posts from cache") 100 | posts = cache.get("all-posts-ordered") 101 | elif order_by: 102 | posts = posts_model.Posts.query.order_by(posts_model.Posts.posted_date.desc()).all() 103 | print("Not yet cached, caching all posts") 104 | cache.set("all-posts-ordered", posts, timeout=50) 105 | elif cache.get("all-posts"): 106 | print("Retreiving the posts from cache") 107 | posts = cache.get("all-posts") 108 | else: 109 | posts = posts_model.Posts.query.all() 110 | print("Not yet cached, caching all posts") 111 | cache.set("all-posts", posts, timeout=50) 112 | return posts 113 | 114 | @classmethod 115 | def get_post_by_title(cls, post_title, is_admin=False): 116 | if is_admin: 117 | post = posts_model.Posts.query.filter_by(title = post_title).first() 118 | tags = tags_model.Tags.query.filter_by(posts=post).all() 119 | post_set = [post, tags] 120 | elif cache.get(post_title): 121 | print("Retreiving from cache") 122 | post_set = cache.get(post_title) 123 | else: 124 | print("Not yet cached, caching the current post details") 125 | post = posts_model.Posts.query.filter_by(title = post_title).first() 126 | tags = tags_model.Tags.query.filter_by(posts=post).all() 127 | cache.set(post_title, [post, tags], timeout=50) 128 | post_set = [post, tags] 129 | return post_set 130 | 131 | 132 | @classmethod 133 | def get_tags_for_post(cls, post): 134 | db_tags = tags_model.Tags.query.filter_by(posts=post).all() 135 | tags_strings = ",".join(db_tag.tag for db_tag in db_tags) 136 | return tags_strings 137 | 138 | @classmethod 139 | def serialize_tags(cls, db_tags): 140 | tags_strings = ",".join(db_tag.tag for db_tag in db_tags) 141 | return tags_strings 142 | 143 | -------------------------------------------------------------------------------- /common/services/reply_service.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import traceback 3 | from sqlalchemy import exc 4 | from common import db, cache 5 | from common.models import posts_model 6 | 7 | class ReplyService(object): 8 | def __init__(self): 9 | pass 10 | 11 | def add_reply(self): 12 | pass 13 | 14 | def get_comment_replies(self): 15 | pass 16 | 17 | def delete_reply(self): 18 | pass 19 | 20 | def edit_reply(self): 21 | pass 22 | -------------------------------------------------------------------------------- /common/services/tags_service.py: -------------------------------------------------------------------------------- 1 | from common import cache 2 | from common.models import tags_model, posts_model 3 | 4 | 5 | def get_post_tags(tag_name): 6 | if cache.get(tag_name): 7 | print("Retreiving the posts from cache for the tag", tag_name) 8 | posts = cache.get(tag_name) 9 | else: 10 | print("Not yet cached retreiving from db for the tag", tag_name) 11 | tags_db_list = tags_model.Tags.query.filter_by(tag=tag_name).all() 12 | if len(tags_db_list) > 0: 13 | posts = [] 14 | for db_tag in tags_db_list: 15 | print(db_tag.posts.title) 16 | print(db_tag.posts.posted_date) 17 | posts.append(db_tag.posts) 18 | cache.set(tag_name, posts, timeout=50) 19 | else: 20 | return False 21 | 22 | return posts 23 | 24 | def get_tags_count(): 25 | db_tags_all = tags_model.Tags.query.all() 26 | tags_count = dict() 27 | if db_tags_all: 28 | for db_tag in db_tags_all: 29 | if db_tag.tag not in tags_count: 30 | tags_count[db_tag.tag] = 1 31 | else: 32 | tags_count[db_tag.tag] = tags_count[db_tag.tag] + 1 33 | 34 | # Sort by value of the dictionary and return 35 | tags_count = dict(sorted(tags_count.items(), key=lambda x: x[1], reverse=True)) 36 | return tags_count 37 | -------------------------------------------------------------------------------- /common/services/users_service.py: -------------------------------------------------------------------------------- 1 | from common.models.users_model import Users 2 | from common import db 3 | from sqlalchemy import exc 4 | import traceback 5 | 6 | 7 | class UserService: 8 | 9 | def __init__(self): 10 | pass 11 | 12 | def create_user(self, user_name, password, f_name, email, l_name="", changed_pass=False): 13 | try: 14 | user = Users(user_name=user_name, 15 | password=password, 16 | changed_pass=changed_pass, 17 | f_name=f_name, 18 | email=email, 19 | l_name=l_name) 20 | db.session.add(user) 21 | db.session.commit() 22 | return 0 23 | except exc.SQLAlchemyError: 24 | print("Error is -- ", traceback.print_exc()) 25 | return -1 26 | 27 | def query_single_user(self, user_name): 28 | result = Users.query.filter_by(user_name=user_name).first() 29 | if result is None: 30 | return -1 31 | else: 32 | return result 33 | -------------------------------------------------------------------------------- /common/services/utility.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | from common.models import comments_model 3 | from flask_mail import Message 4 | from common import mail 5 | 6 | def check_reply(comment, post_obj): 7 | refer_names = list() 8 | words_list = comment.split(" ") 9 | for word in words_list: 10 | if len(word) >= 3 and "@" in word[0] and ":" in word[len(word) - 1]: 11 | refer_names.append(word.replace("@", "").replace(":","")) 12 | if len(refer_names) > 0: 13 | for name in refer_names: 14 | com_db_obj = comments_model.Comments.query.filter(comments_model.Comments.author_name.like("%" + name + "%"), comments_model.Comments.post_id==post_obj.p_id).first() 15 | if com_db_obj.author_name: 16 | return (com_db_obj.author_name, com_db_obj.author_email), True 17 | return None, False 18 | 19 | 20 | def send_email(author_comment, author_name, c_name_tuple, post_db_obj): 21 | try: 22 | msg = Message('New message from Blog', 23 | sender = environ.get("MAIL_USERNAME"), 24 | recipients = [c_name_tuple[1]]) 25 | msg_body = "

    Hello," + c_name_tuple[0] + "There has been a reply to your comment to " + post_db_obj.title + ".


    " 26 | msg_body += "
    " 27 | msg_body += "
    " + "

    Comment: " + author_comment + "

    " + "

    Author: " + author_name + "

    " + "
    " 28 | msg.html = msg_body 29 | mail.send(msg) 30 | return True 31 | except (UnicodeError, TypeError, AttributeError): 32 | return False 33 | -------------------------------------------------------------------------------- /docker-compose-mysql.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | networks: 3 | backend-network: 4 | services: 5 | mysql_db: 6 | image: "mysql:latest" 7 | restart: "always" 8 | command: "--default-authentication-plugin=mysql_native_password" 9 | container_name: "mysql_db" 10 | env_file: ".env" 11 | volumes: 12 | - mysql-data:/var/lib/mysql 13 | ports: 14 | - "3308:${DB_PORT}" 15 | environment: 16 | - "MYSQL_ROOT_PASSWORD=${DB_PASSWORD}" 17 | - "MYSQL_DATABASE=${DB_NAME}" 18 | networks: 19 | - "backend-network" 20 | 21 | volumes: 22 | mysql-data: -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | networks: 3 | backend-network: 4 | services: 5 | blog-app: 6 | container_name: "blog_app" 7 | restart: "always" 8 | # depends_on: 9 | # - "blog-admin-app" 10 | build: 11 | context: .. 12 | dockerfile: SimplisticBlogger/blog/Dockerfile 13 | env_file: ".env" 14 | ports: 15 | - "9010:${FLASK_BLOG_PORT}" 16 | networks: 17 | - "backend-network" 18 | blog-admin-app: 19 | container_name: "blog_admin" 20 | restart: "always" 21 | # depends_on: 22 | # - "mysql_db" 23 | build: 24 | context: .. 25 | dockerfile: SimplisticBlogger/blog_admin/Dockerfile 26 | env_file: ".env" 27 | ports: 28 | - "9000:${FLASK_ADMIN_PORT}" 29 | networks: 30 | - "backend-network" 31 | -------------------------------------------------------------------------------- /instance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/instance/__init__.py -------------------------------------------------------------------------------- /instance/config.py: -------------------------------------------------------------------------------- 1 | # Adding secret infos here 2 | from os import environ 3 | 4 | class Config(object): 5 | DEBUG = True 6 | SQLALCHEMY_ECHO = False 7 | SQLALCHEMY_TRACK_MODIFICATIONS = True 8 | CSRF_ENABLED = True 9 | CACHE_TYPE = environ.get("CACHE_TYPE") 10 | CACHE_DEFAULT_TIMEOUT = environ.get("CACHE_DEFAULT_TIMEOUT") 11 | SECRET_KEY = environ.get("SECRET_KEY") 12 | DB_DRIVER = environ.get("DB_DRIVER") 13 | DB_HOST = environ.get("DB_HOST") 14 | DB_PORT = environ.get("DB_PORT") 15 | DB_USER = environ.get("DB_USER") 16 | DB_PASSWORD = environ.get("DB_PASSWORD") 17 | DATABASE_NAME = environ.get("DB_NAME") 18 | MAIL_SERVER = "smtp.gmail.com" 19 | MAIL_PORT = 465 20 | MAIL_USE_SSL = True 21 | MAIL_USERNAME = environ.get("MAIL_USERNAME") 22 | MAIL_PASSWORD = environ.get("MAIL_PASSWORD") 23 | MAIL_DEBUG = False 24 | db_uri = DB_DRIVER + "://" + DB_USER + ":" + DB_PASSWORD + "@" + DB_HOST + ":" + DB_PORT + "/" + DATABASE_NAME 25 | 26 | 27 | class DevelopmentConfig(Config): 28 | SQLALCHEMY_DATABASE_URI = Config.db_uri 29 | 30 | 31 | class TestingConfig(Config): 32 | SQLALCHEMY_DATABASE_URI = Config.db_uri 33 | SQLALCHEMY_ECHO = True 34 | 35 | 36 | class ProductionConfig(Config): 37 | SQLALCHEMY_DATABASE_URI = Config.db_uri 38 | SQLALCHEMY_ECHO = True 39 | 40 | 41 | app_config = { 42 | "dev": DevelopmentConfig, 43 | "test": TestingConfig, 44 | "prod": ProductionConfig 45 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==8.1.0 2 | astroid==2.4.2 3 | autoenv==1.0.0 4 | autopep8==1.5.4 5 | beautifulsoup4==4.9.3 6 | blinker==1.4 7 | bs4==0.0.1 8 | certifi==2020.12.5 9 | chardet==4.0.0 10 | click==7.1.2 11 | colorama==0.4.4 12 | Flask==1.1.2 13 | Flask-Caching==1.9.0 14 | Flask-Login==0.5.0 15 | Flask-Mail==0.9.1 16 | Flask-RESTful==0.3.8 17 | Flask-SQLAlchemy==2.4.4 18 | Flask-WTF==0.14.3 19 | idna==2.10 20 | isort==5.6.4 21 | itsdangerous==1.1.0 22 | Jinja2==2.11.3 23 | lazy-object-proxy==1.4.3 24 | libgravatar==0.2.4 25 | MarkupSafe==1.1.1 26 | mccabe==0.6.1 27 | mysqlclient==2.0.1 28 | passlib==1.7.4 29 | pycodestyle==2.6.0 30 | pylint==2.6.0 31 | pylint-flask==0.6 32 | pylint-flask-sqlalchemy==0.2.0 33 | pylint-plugin-utils==0.6 34 | python-dotenv==0.15.0 35 | pytz==2020.4 36 | requests==2.25.1 37 | six==1.15.0 38 | soupsieve==2.0.1 39 | SQLAlchemy==1.3.20 40 | toml==0.10.2 41 | typed-ast==1.4.1 42 | urllib3==1.26.4 43 | Werkzeug==1.0.1 44 | wrapt==1.12.1 45 | WTForms==2.3.3 46 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/tests/__init__.py -------------------------------------------------------------------------------- /tests/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/tests/blog/__init__.py -------------------------------------------------------------------------------- /tests/blog/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prakass1/SimplisticBlogger/cc55cccb47677ae34e3efd1e3406ad8bb60ff06e/tests/blog/unit/__init__.py -------------------------------------------------------------------------------- /tests/blog/unit/test_blog_post_service.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | import datetime 3 | import pytest 4 | from flask import url_for 5 | from common.models import posts_model 6 | from dotenv import load_dotenv, find_dotenv 7 | from blog import create_app 8 | from blog.posts.service import posts_blog_service 9 | 10 | 11 | @pytest.fixture(scope='session', autouse=True) 12 | def load_env(): 13 | load_dotenv(find_dotenv()) 14 | 15 | @pytest.fixture(scope="module") 16 | def test_client(): 17 | app_config = environ.get("APP_CONFIG") 18 | test_app = create_app(app_config) 19 | with test_app.test_client() as testing_client: 20 | yield testing_client 21 | 22 | @pytest.fixture(scope="module") 23 | def test_context(): 24 | app_config = environ.get("APP_CONFIG") 25 | test_app = create_app(app_config) 26 | with test_app.app_context() as testing_context: 27 | yield testing_context 28 | 29 | def test_serialize(test_client): 30 | post_db_obj = posts_model.Posts(content="test content", 31 | posted_date = datetime.datetime.now(), 32 | title = "test title", 33 | author = "test author" 34 | ) 35 | serialized_obj = posts_blog_service.serialize(post_db_obj) 36 | test_stub = { 37 | "content": "test content", 38 | "posted_date": datetime.datetime.now().strftime('%B %d, %Y'), 39 | "title": "test title", 40 | "author": "test author" 41 | } 42 | 43 | assert test_stub == serialized_obj 44 | 45 | def test_get_posts_html_resp(test_context): 46 | test_stub_items = list() 47 | test_stub = { 48 | "content": "test content", 49 | "posted_date": datetime.datetime.now().strftime('%B %d, %Y'), 50 | "title": "test title", 51 | "author": "test author" 52 | } 53 | test_stub_items.append(test_stub) 54 | print(test_stub) 55 | test_html_str = "" + "

    " + test_stub_items[0]["title"] + \ 56 | "

    " + "
    " 58 | html_resp = posts_blog_service.get_posts_html_resp(test_stub_items, 10, None) 59 | print(test_html_str) 60 | print(html_resp) 61 | 62 | assert test_html_str == html_resp 63 | 64 | --------------------------------------------------------------------------------