Flask-Blogging is a Flask extension for adding Markdown based blog support to your site. It provides a flexible mechanism to store the data in the database of your choice. It is meant to work with the authentication provided by packages such as Flask-Login or Flask-Security.
The philosophy behind this extension is to provide a lean app based on Markdown to provide blog support to your existing web application. If you already have a web app and you need to have a blog to communicate with your user or to promote your site through content based marketing, then Flask-Blogging would help you quickly get a blog up and running.
Out of the box, Flask-Blogging has support for the following:
- Bootstrap based site
- Markdown based blog editor
- Upload and manage static assets for the blog
- Models to store blog
- Authentication of User's choice
- Sitemap, ATOM support
- Disqus support for comments
- Google analytics for usage tracking
- Open Graph meta tags
- Permissions enabled to control which users can create/edit blogs
- Integrated Flask-Cache based caching for optimization
- Well documented, tested, and extensible design
- DynamoDB storage for use in AWS
- Google Cloud Datastore support
from flask import Flask, render_template_string, redirect
from sqlalchemy import create_engine, MetaData
from flask_login import UserMixin, LoginManager, login_user, logout_user
from flask_blogging import SQLAStorage, BloggingEngine
app = Flask(__name__)
app.config["SECRET_KEY"] = "secret" # for WTF-forms and login
app.config["BLOGGING_URL_PREFIX"] = "/blog"
app.config["BLOGGING_DISQUS_SITENAME"] = "test"
app.config["BLOGGING_SITEURL"] = "http://localhost:8000"
app.config["BLOGGING_SITENAME"] = "My Site"
app.config["BLOGGING_KEYWORDS"] = ["blog", "meta", "keywords"]
app.config["FILEUPLOAD_LOCALSTORAGE_IMG_FOLDER"] = "img_upload"
app.config["FILEUPLOAD_PREFIX"] = "/fileupload"
app.config["FILEUPLOAD_ALLOWED_EXTENSIONS"] = ["png", "jpg", "jpeg", "gif"]
# extensions
engine = create_engine('sqlite:////tmp/blog.db')
meta = MetaData()
sql_storage = SQLAStorage(engine, metadata=meta)
blog_engine = BloggingEngine(app, sql_storage)
login_manager = LoginManager(app)
meta.create_all(bind=engine)
class User(UserMixin):
def __init__(self, user_id):
self.id = user_id
def get_name(self):
return "Paul Dirac" # typically the user's name
@login_manager.user_loader
@blog_engine.user_loader
def load_user(user_id):
return User(user_id)
index_template = """
<!DOCTYPE html>
<html>
<head> </head>
<body>
{% if current_user.is_authenticated %}
<a href="/logout/"> Logout </a>
{% else %}
<a href="/#/"> Login </a>
{% endif %}
  <a href="/blog/"> Blog </a>
  <a href="/blog/sitemap.xml">Sitemap</a>
  <a href="/blog/feeds/all.atom.xml">ATOM</a>
  <a href="/fileupload/">FileUpload</a>
</body>
</html>
"""
@app.route("/")
def index():
return render_template_string(index_template)
@app.route("/#/")
def login():
user = User("testuser")
login_user(user)
return redirect("/blog")
@app.route("/logout/")
def logout():
logout_user()
return redirect("/")
if __name__ == "__main__":
app.run(debug=True, port=8000, use_reloader=True)
The key components required to get the blog hooked is explained below. Please note
that as of Flask-Login 0.3.0 the is_authenticated
attribute in the UserMixin
is a property and not a method. Please use the appropriate option based on your
Flask-Login version. You can find more examples here in the
Flask-Blogging github project page.
The BloggingEngine
class is the gateway to configure blogging support
to your web app. You should create the BloggingEngine
instance like this:
blogging_engine = BloggingEngine() blogging_engine.init_app(app, storage)
You also need to pick the storage
for blog. That can be done as:
from sqlalchemy import create_engine, MetaData engine = create_engine("sqlite:////tmp/sqlite.db") meta = MetaData() storage = SQLAStorage(engine, metadata=meta) meta.create_all(bind=engine)
Here we have created the storage, and created all the tables
in the metadata. Once you have created the blogging engine,
storage, and all the tables in the storage, you can connect
with your app using the init_app
method as shown below:
blogging_engine.init_app(app, storage)
If you are using Flask-Sqlalchemy
, you can do the following:
from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy(app) storage = SQLAStorage(db=db) db.create_all()
One of the changes in version 0.3.1 is the ability for the user
to provide the metadata
object. This has the benefit of the
table creation being passed to the user. Also, this gives the user
the ability to use the common metadata object, and hence helps
with the tables showing up in migrations while using Alembic.
As of version 0.5.2, support for the multi database scenario
under Flask-SQLAlchemy was added. When we have a multiple database
scenario, one can use the bind
keyword in SQLAStorage
to
specify the database to bind to, as shown below:
# config value SQLALCHEMY_BINDS = { 'blog': "sqlite:////tmp/blog.db"), 'security': "sqlite:////tmp/security.db") }
The storage can be initialised as:
db = SQLAlchemy(app) storage = SQLAStorage(db=db, bind="blog") db.create_all()
As of version 0.4.0, Flask-Cache integration is supported. In order
to use caching in the blogging engine, you need to pass the Cache
instance to the BloggingEngine
as:
from flask_caching import Cache from flask_blogging import BloggingEngine blogging_engine = BloggingEngine(app, storage, cache)
Flask-Blogging lets the developer pick the authentication that is suitable, and hence requires her to provide a way to load user information. You will need to provide a BloggingEngine.user_loader callback. This callback is used to load the user from the user_id that is stored for each blog post. Just as in Flask-Login, it should take the unicode user_id of a user, and return the corresponding user object. For example:
@blogging_engine.user_loader def load_user(userid): return User.get(userid)
For the blog to have a readable display name, the User
class must
implement either the get_name
method or the __str__
method.
The BloggingEngine
accepts an optional extensions
argument. This is a list
of Markdown
extensions objects to be used during the markdown processing step.
As of version 0.6.0, a plugin interface is available to add new functionality.
Custom processes can be added to the posts
by subscribing to the
post_process_before
and post_process_after
signals, and adding
new functionality to it.
The BloggingEngine
also accepts post_processor
argument, which can be
used to provide a custom post processor object to handle the processing
of Markdown text. One way to do this would be to inherit the default
PostProcessor
object and override process
method.
In version 0.4.1 and onwards, the BloggingEngine
object can be accessed
from your app
as follows:
engine = app.extensions["blogging"]
The engine method also exposes a get_posts
method to get the recent posts
for display of posts in other views.
In earlier versions the same can be done using the key
FLASK_BLOGGING_ENGINE
instead of blogging
. The use of
FLASK_BLOGGING_ENGINE
key will be deprecated moving forward.
SQLAlchemy ORM models for the SQLAStorage can be accessed after configuration of the SQLAStorage object. Here is a quick example:
storage = SQLAStorage(db=db) from flask_blogging.sqlastorage import Post, Tag # Has to be after SQLAStorage initialization
These ORM models can be extremely convenient to use with Flask-Admin.
One can provide additional MarkDown extensions to the blogging engine.
One example usage is adding the codehilite
MarkDown extension. Additional
extensions should be passed as a list while initializing the BlogggingEngine
as shown:
from markdown.extensions.codehilite import CodeHiliteExtension extn1 = CodeHiliteExtension({}) blogging_engine = BloggingEngine(app, storage,extensions=[extn1])
This allows for the MarkDown to be processed using CodeHilite along with
the default extensions. Please note that one would also need to include
necessary static files in the view
, such as for code highlighting to work.
Let's say you want to include a summary for your blog post, and have it show up along with the post. You don't need to modify the database or the models to accomplish this. This is in fact supported by default by way of Markdown metadata syntax. In your blog post, you can include metadata, as shown below:
Summary: This is a short summary of the blog post Keywords: Blog, specific, keywords This is the much larger blog post. There are lot of things to discuss here.
In the template page.html
this metadata can be accessed as, post.meta.summary
and can be populated in the way it is desired. The same metadata for each post
is also available in other template views like index.html
.
If included, the first summary will be used as the page's meta description
,
and Open Graph og:description
.
The (optional) blog post specific keywords are included in the page's meta
keywords in addition to BLOGGING_KEYWORDS
(if configured). Any tags are also
added as meta keywords.
The plugin framework is a very powerful way to modify the behavior of the blogging engine. Lets say you want to show the top 10 most popular tag in the post. Lets show how one can do that using the plugins framework. As a first step, we create our plugin:
# plugins/tag_cloud/__init__.py from flask_blogging import signals from flask_blogging.sqlastorage import SQLAStorage import sqlalchemy as sqla from sqlalchemy import func def get_tag_data(sqla_storage): engine = sqla_storage.engine with engine.begin() as conn: tag_posts_table = sqla_storage.tag_posts_table tag_table = sqla_storage.tag_table tag_cloud_stmt = sqla.select([ tag_table.c.text,func.count(tag_posts_table.c.tag_id)]).group_by( tag_posts_table.c.tag_id ).where(tag_table.c.id == tag_posts_table.c.tag_id).limit(10) tag_cloud = conn.execute(tag_cloud_stmt).fetchall() return tag_cloud def get_tag_cloud(app, engine, posts, meta): if isinstance(engine.storage, SQLAStorage): tag_cloud = get_tag_data(engine.storage) meta["tag_cloud"] = tag_cloud else: raise RuntimeError("Plugin only supports SQLAStorage. Given storage" "not supported") return def register(app): signals.index_posts_fetched.connect(get_tag_cloud) return
The register
method is what is invoked in order to register the plugin. We have
connected this plugin to the index_posts_fetched
signal. So when the posts are
fetched to show on the index page, this signal will be fired, and this plugin will
be invoked. The plugin basically queries the table that stores the tags, and returns
the tag text and the number of times it is referenced. The data about the tag cloud
we are storing in the meta["tag_cloud"]
which corresponds to the metadata variable.
Now in the index.html template, one can reference the meta.tag_cloud
to access this
data for display. The plugin can be registered by setting the config variable as shown:
app.config["BLOGGING_PLUGINS"] = ["plugins.tag_cloud"]
The Flask-Blogging extension can be configured by setting the following app config variables. These arguments are passed to all the views. The keys that are currently supported include:
BLOGGING_SITENAME
(str): The name of the blog to be used as the brand name. This is also used in the feed heading andog:site_name
meta tag. (default "Flask-Blogging")BLOGGING_SITEURL
(str): The url of the site. This is also used in theog:publisher
meta tag.BLOGGING_BRANDURL
(str): The url of the site brand.BLOGGING_TWITTER_USERNAME
(str): @name to tag social sharing link with.BLOGGING_RENDER_TEXT
(bool): Value to specify if the raw text (markdown) should be rendered to HTML. (defaultTrue
)BLOGGING_DISQUS_SITENAME
(str): Disqus sitename for comments. ANone
value will disable comments. (defaultNone
)BLOGGING_GOOGLE_ANALYTICS
(str): Google analytics code for usage tracking. ANone
value will disable google analytics. (defaultNone
)BLOGGING_URL_PREFIX
(str) : The prefix for the URL of blog posts. ANone
value will have no prefix. (defaultNone
)BLOGGING_FEED_LIMIT
(int): The number of posts to limit to in the feed. IfNone
, then all are shown, else will be limited to this number. (defaultNone
)BLOGGING_PERMISSIONS
(bool): IfTrue
, this will enable permissions for the blogging engine. With permissions enabled, the user will need to have "blogger"Role
to edit or create blog posts. Other authenticated users will not have blog editing permissions. The concepts here derive fromFlask-Principal
. (defaultFalse
)BLOGGING_PERMISSIONNAME
(str): The role name used for permissions. It is effective, if "BLOGGING_PERMISSIONS" is "True". (default "blogger")BLOGGING_POSTS_PER_PAGE
(int): The default number of posts per index page. to be displayed per page. (default 10)BLOGGING_CACHE_TIMEOUT
(int): The timeout in seconds used to cache. the blog pages. (default 60)BLOGGING_PLUGINS
(list): A list of plugins to register.BLOGGING_KEYWORDS
(list): A list of meta keywords to include on each page.BLOGGING_ALLOW_FILEUPLOAD
(bool): Allow static file uploadsflask_fileupload
BLOGGING_ESCAPE_MARKDOWN
(bool): Escape input markdown text input. This isFalse
by default. Set this toTrue
to forbid embedding HTML in markdown.
There are various views that are exposed through Flask-Blogging. The URL for the various views are:
url_for('blogging.index')
(GET): The index blog posts with the first page of articles. Themeta
variable passed into the view holds values for the keysis_user_blogger
,count
andpage
.url_for('blogging.page_by_id', post_id=<post_id>)
(GET): The blog post corresponding to thepost_id
is retrieved. Themeta
variable passed into the view holds values for the keysis_user_blogger
,post_id
andslug
.url_for('blogging.posts_by_tag', tag=<tag_name>)
(GET): The list of blog posts corresponding totag_name
is returned. Themeta
variable passed into the view holds values for the keysis_user_blogger
,tag
,count
andpage
.url_for('blogging.posts_by_author', user_id=<user_id>)
(GET): The list of blog posts written by the authoruser_id
is returned. Themeta
variable passed into the view holds values for the keysis_user_blogger
,count
,user_id
andpages
.url_for('blogging.editor')
(GET, POST): The blog editor is shown. This view needs authentication and permissions (if enabled).url_for('blogging.delete', post_id=<post_id>)
(POST): The blog post given bypost_id
is deleted. This view needs authentication and permissions (if enabled).url_for('blogging.sitemap')
(GET): The sitemap with a link to all the posts is returned.url_for('blogging.feed')
(GET): Returns ATOM feed URL.
The view can be easily customised by the user by overriding with their own templates. The template pages that need to be customized are:
blogging/index.html
: The blog index page used to serve index of posts, posts by tag, and posts by authorblogging/editor.html
: The blog editor page.blogging/page.html
: The page that shows the given article.blogging/sitemap.xml
: The sitemap for the blog posts.
In version 0.3.0 Flask-Blogging, enables permissions based on Flask-Principal.
This addresses the issue of controlling which of the authenticated users can
have access to edit or create blog posts. Permissions are enabled by setting
BLOGGING_PERMISSIONS
to True
. Only users that have access to
Role
"blogger" will have permissions to create or edit blog posts.
Migrations with Alembic: (Applies to versions 0.3.0 and earlier) If you have migrations part of your project using Alembic, or extensions such as
Flask-Migrate
which uses Alembic, then you have to modify theAlembic
configuration in order for it to ignore theFlask-Blogging
related tables. If you don't set these modifications, then every time you run migrations,Alembic
will not recognize the tables and mark them for deletion. And if you happen toupgrade
by mistake then all your blog tables will be deleted. What we will do here is ask Alembic toexclude
the tables used byFlask-Blogging
. In youralembic.ini
file, add a line:[alembic:exclude] tables = tag, post, tag_posts, user_posts
If you have a value set for
table_prefix
argument while creating theSQLAStorage
, then the table names will contain that prefix in their names. In which case, you have to use appropriate names in the table names.And in your
env.py
, we have to mark these tables as the ones to be ignored.def exclude_tables_from_config(config_): tables_ = config_.get("tables", None) if tables_ is not None: tables = tables_.split(",") return tables exclude_tables = exclude_tables_from_config(config.get_section('alembic:exclude')) def include_object(object, name, type_, reflected, compare_to): if type_ == "table" and name in exclude_tables: return False else: return True def run_migrations_online(): """Run migrations in 'online' mode. In this scenario we need to create an Engine and associate a connection with the context. """ engine = engine_from_config( config.get_section(config.config_ini_section), prefix='sqlalchemy.', poolclass=pool.NullPool) connection = engine.connect() context.configure( connection=connection, target_metadata=target_metadata, include_object=include_object, compare_type=True ) try: with context.begin_transaction(): context.run_migrations() finally: connection.close()
In the above, we are using
include_object
incontext.configure(...)
to be specified based on theinclude_object
function.
.. automodule:: flask_blogging :members: :undoc-members: :show-inheritance:
.. automodule:: flask_blogging.engine :members: :undoc-members: :show-inheritance:
.. autoclass:: flask_blogging.processor.PostProcessor :members: :undoc-members: :show-inheritance:
.. automodule:: flask_blogging.sqlastorage :members: :undoc-members: :show-inheritance:
.. automodule:: flask_blogging.storage :members: :undoc-members: :show-inheritance:
.. automodule:: flask_blogging.views :members: :undoc-members: :show-inheritance:
.. automodule:: flask_blogging.forms :members: :undoc-members:
.. automodule:: flask_blogging.signals :members: engine_initialised
.. autodata:: flask_blogging.signals
.. autodata:: flask_blogging.signals.engine_initialised
.. autodata:: flask_blogging.signals.post_processed
.. autodata:: flask_blogging.signals.page_by_id_fetched
.. autodata:: flask_blogging.signals.page_by_id_processed
.. autodata:: flask_blogging.signals.posts_by_tag_fetched
.. autodata:: flask_blogging.signals.posts_by_tag_processed
.. autodata:: flask_blogging.signals.posts_by_author_fetched
.. autodata:: flask_blogging.signals.posts_by_author_processed
.. autodata:: flask_blogging.signals.index_posts_fetched
.. autodata:: flask_blogging.signals.index_posts_processed
.. autodata:: flask_blogging.signals.feed_posts_fetched
.. autodata:: flask_blogging.signals.feed_posts_processed
.. autodata:: flask_blogging.signals.sitemap_posts_fetched
.. autodata:: flask_blogging.signals.sitemap_posts_processed
.. autodata:: flask_blogging.signals.editor_post_saved
.. autodata:: flask_blogging.signals.editor_get_fetched
.. autodata:: flask_blogging.signals.post_deleted
.. autodata:: flask_blogging.signals.blueprint_created
.. autodata:: flask_blogging.signals.sqla_initialized