Verified Commit 2588927b authored by Raoul Snyman's avatar Raoul Snyman
Browse files

Initial commit

This diff is collapsed.
include LICENSE
recursive-include cornerstone *.py *.html *.json
CornerstoneCMS is a really simple content management system for churches. It has only two components to it, pages and
sermons. Sermons are hosted on Simplecast.
Install and update using `pip`_::
$ pip install -U CornerstoneCMS
Set up
To set up CornerstoneCMS for your site, you can either manually create a configuration file, or run a configuration
Configuration wizard
CornerstoneCMS comes with a short configuration wizard which will create a configuration file for you::
$ python -m cornerstone.conf
Manual configuration
Set up CornerstoneCMS by creating a configuration file like ``cornerstone.conf``:
.. code-block:: ini
secret_key = <create a secret for sessions etc>
database_uri = sqlite:///cornerstone.sqlite
title = My Church Name
Deploying to Production
CornerstoneCMS is a WSGI application, and needs to be deployed to a WSGI server. Create a file called ```` and
point your WSGI server to the file.
.. code-block:: python
from import create_app
application = create_app('/path/to/yourfile.conf')
* Website:
* Documentation:
* License:
* Issue tracker:
secret_key = ${secretkey}
database_uri = mysql://${dbuser}:${dbpass}@${dbhost}/${dbname}
title = ${sitename}
__version__ = '0.1.0'
import re
from unidecode import unidecode
from flask import redirect, url_for, request
from flask_admin import Admin, AdminIndexView, BaseView, expose
from flask_admin.contrib.sqla import ModelView
from flask_user import current_user
from wtforms import TextAreaField, PasswordField
from wtforms.fields.html5 import IntegerField
from wtforms.widgets import TextArea
from cornerstone.models import User, Page, Sermon, Preacher, session
from cornerstone.settings import get_all_settings, has_setting, save_setting
def _create_slug(title):
Convert the title to a slug
return re.sub(r'\W+', '-', unidecode(title).lower()).strip('-')
class CKTextAreaWidget(TextArea):
def __call__(self, field, **kwargs):
if kwargs.get('class'):
kwargs['class'] += ' ckeditor'
kwargs.setdefault('class', 'ckeditor')
return super(CKTextAreaWidget, self).__call__(field, **kwargs)
class CKTextAreaField(TextAreaField):
widget = CKTextAreaWidget()
class AuthorizedMixin(object):
def is_accessible(self):
return current_user.is_active and current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs):
if current_user.is_authenticated:
return redirect(url_for('/'))
return redirect(url_for('user.login', next=request.url))
class AuthorizedAdminIndexView(AuthorizedMixin, AdminIndexView):
class AuthorizedModelView(AuthorizedMixin, ModelView):
extra_js = ['//']
column_exclude_list = ('password',)
column_descriptions = {
'weight': 'Use this to order items in the menu'
form_excluded_columns = ('slug',)
form_overrides = {
'password': PasswordField,
'weight': IntegerField
def on_model_change(self, form, model, is_create):
if isinstance(model, Page):
model.slug = _create_slug(model.title)
class SettingsView(AuthorizedMixin, BaseView):
@expose('/', methods=['GET'])
def index(self):
settings = get_all_settings()
return self.render('admin/settings.html', settings=settings)
@expose('/', methods=['POST'])
def index_post(self):
for key, value in request.form.items():
if has_setting(key):
save_setting(key, value)
return redirect(self.get_url('settings.index'))
admin = Admin(name='CornerstoneCMS', template_mode='bootstrap4', index_view=AuthorizedAdminIndexView())
admin.add_view(AuthorizedModelView(Page, session, name='Pages'))
admin.add_view(AuthorizedModelView(Sermon, session, name='Sermons'))
admin.add_view(AuthorizedModelView(Preacher, session, name='Preachers'))
admin.add_view(AuthorizedModelView(User, session, name='Users'))
admin.add_view(SettingsView(name='Settings', endpoint='settings'))
import os
from flask import Flask
from import MenuLink
from flask_themes2 import Themes, packaged_themes_loader, theme_paths_loader
from flask_user import UserManager
from cornerstone.admin import admin
from cornerstone.config import config_from_file
from cornerstone.models import User, db, setup_db
from cornerstone.theming import get_themes_loader
from cornerstone.views.home import home
from cornerstone.views.pages import pages
from cornerstone.views.sermons import sermons
from cornerstone.views.uploads import uploads
def _resolve_themes_directory(config_file):
Resolve the themes directory from the config file
return os.path.join(os.path.dirname(os.path.abspath(config_file)), 'themes')
def create_app(config_file):
app = Flask('cornerstone')
config_from_file(app, config_file)
# Set up themes, making sure to use a local themes folder if it exists
loaders = [packaged_themes_loader, theme_paths_loader]
themes_directory = _resolve_themes_directory(config_file)
if os.path.exists(themes_directory):
local_themes_loader = get_themes_loader(themes_directory)
loaders.insert(0, local_themes_loader)
Themes(app, app_identifier='cornerstone', loaders=loaders)
# Initialise various other parts of the application
UserManager(app, db, User)
# Register blueprints
with app.app_context():
# Set up menu shortcuts
admin.add_link(MenuLink('Back to main site', '/'))
admin.add_link(MenuLink('Logout', '/user/sign-out'))
return app
import json
from pathlib import Path
from configparser import ConfigParser
import yaml
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
BOOLEAN_VALUES = ['yes', 'true', 'on', 'no', 'false', 'off']
# SQLAlchemy
'SQLALCHEMY_DATABASE_URI': 'sqlite:///cornerstonecms.sqlite',
# User settings
# CornerstoneCMS settings
'email': '',
'name': 'Superuser',
'password': 'P@ssw0rd'
def _load_from_ini_file(filename):
Load from a config file
config = {}
parser = ConfigParser()
for section in parser.sections():
for option in parser.options(section):
# Get the value, skip it if it is blank
string_value = parser.get(section, option)
if not string_value:
# Try to figure out what type it is
if string_value.isnumeric() and '.' in string_value:
value = parser.getfloat(section, option)
elif string_value.isnumeric():
value = parser.getint(section, option)
elif string_value.lower() in BOOLEAN_VALUES:
value = parser.getboolean(section, option)
elif string_value.startswith('{'):
# Try to load string values beginning with '{' as JSON
value = json.loads(string_value)
except ValueError:
# If this is not JSON, just use the string
value = string_value
value = string_value
# Set up the configuration key
if section == 'flask':
# Options in the flask section don't need FLASK_*
key = option.upper()
key = '{}_{}'.format(section, option).upper()
# Save this into our config dictionary
config[key] = value
return config
def _load_from_yaml_file(filename):
Load the configuration from a yaml file
with as yaml_file:
config = yaml.load(, Loader=Loader)
return config
def _load_from_json_file(filename):
Load the Flask configuration from a config file
with as json_file:
config = json.loads(
return config
def config_from_file(app, filename):
Load configuration from a file
# Get the default configuration
config = dict(**DEFAULT_CONFIG)
# Set up the filename
filename = Path(filename)
if not filename.exists:
# log a warning
print('No config file found')
# Load from the file based on the extension
if filename.suffix in ['.ini', '.conf', '.cfg']:
elif filename.suffix in ['.yaml', '.yml']:
elif filename.suffix in ['.json']:
# Set the app name everywhere it is needed so that the deployer doesn't need to
from flask_sqlalchemy import SQLAlchemy
from flask_user import UserMixin
db = SQLAlchemy()
session = db.session
def get_or_create(session, model, **kwargs):
instance = model.query.filter_by(**kwargs).first()
if not instance:
instance = model(**kwargs)
return instance
sermons_topics = db.Table(
db.Column('sermon_id', db.Integer, db.ForeignKey(''), primary_key=True),
db.Column('topic_id', db.Integer, db.ForeignKey(''), primary_key=True)
class User(db.Model, UserMixin):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
email = db.Column(db.String(255), nullable=False, unique=True)
password = db.Column(db.String(255), nullable=False, server_default='')
active = db.Column('is_active', db.Boolean(), nullable=False, server_default='1')
def __repr__(self):
class Sermon(db.Model):
__tablename__ = 'sermons'
id = db.Column(db.Integer, primary_key=True)
preacher_id = db.Column(db.Integer, db.ForeignKey(''))
title = db.Column(db.String(255), nullable=False)
scripture = db.Column(db.String(255), nullable=False)
simplecast_id = db.Column(db.String(50))
date = db.Column(db.Date, nullable=False)
preacher = db.relationship('Preacher', lazy='subquery', backref=db.backref('sermons', lazy=True))
topics = db.relationship('Topic', secondary=sermons_topics, lazy='subquery',
backref=db.backref('sermons', lazy=True))
def __repr__(self):
return self.title
class Preacher(db.Model):
__tablename__ = 'preachers'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
def __repr__(self):
class Topic(db.Model):
__tablename__ = 'topics'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255), nullable=False)
class Page(db.Model):
__tablename__ = 'pages'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255), nullable=False)
slug = db.Column(db.String(255), nullable=False)
body = db.Column(db.Text)
weight = db.Column(db.Integer, default=0)
def __repr__(self):
return self.title
class Setting(db.Model):
__tablename__ = 'settings'
key = db.Column(db.String(255), primary_key=True)
title = db.Column(db.String(255), nullable=False)
group = db.Column(db.String(255), default='core')
value = db.Column(db.Text)
type = db.Column(db.String(20), nullable=False)
allowed_values = db.Column(db.Text, default='None')
class MenuLink(db.Model):
__tablename__ = 'menulinks'
id = db.Column(db.Integer, primary_key=True, )
title = db.Column(db.String(255), nullable=False)
url = db.Column(db.String(255), nullable=False)
weight = db.Column(db.Integer, default=0)
def setup_db(app):
Set up the database
# Need this to prevent a circular import
from cornerstone.settings import add_setting, has_setting, save_setting
# Create the tables
# Optionally create a superuser
if app.config.get('CORNERSTONE_SUPERUSER', None) and app.config['CORNERSTONE_SUPERUSER'].get('email', None):
superuser = app.config['CORNERSTONE_SUPERUSER']
if not User.query.filter( == superuser['email']).first():
user = User(
name=superuser.get('name', 'Superuser'),
password=app.user_manager.hash_password(superuser.get('password', 'Password1')),
# Create the home page, if it doesn't exist
index_page = get_or_create(db.session, Page, slug='home')
if not index_page.title and not index_page.body:
index_page.title = 'Home'
index_page.body = '<h1>Home</h1><p>This is the home page. Edit it and replace this content with your own.</p>'
# Add some settings, if they don't already exist
if not has_setting('sermons-on-home-page'):
add_setting('Show sermons on the home page', 'sermons-on-home-page', 'bool', 'home page')
save_setting('sermons-on-home-page', False)
add_setting('Number of sermons to show on the home page', 'sermons-home-page-count', 'int', 'home page')
save_setting('sermons-home-page-count', 10)
if not has_setting('pages-in-menu'):
add_setting('Include pages in menu automatically', 'pages-in-menu', 'bool', 'menu')
save_setting('pages-in-menu', True)
if not has_setting('theme'):
add_setting('Theme', 'theme', 'str', 'theme')
save_setting('theme', 'bootstrap4')
from serpent import dumps, loads
from cornerstone.models import Setting, db
def has_setting(key):
Check if a setting exists
:param key: The key of the setting
return Setting.query.get(key) is not None
def add_setting(title, key, type_, group='core', allowed_values=None):
Add a setting
:param title: The visible title of the setting
:param key: The unique key used to look up the setting in the database
:param type_: The type of this setting. Can be one of "bool", "int", "str".
:param allowed_values: Restrict values to only those in this list (renders as a dropdown)
setting = Setting(title=title, key=key, type=type_, group=group, allowed_values=dumps(allowed_values))
return setting
def get_all_settings():
Get all the settings
grouped_settings = {}
settings = Setting.query.all()
for setting in settings:
setting.value = loads(setting.value if isinstance(setting.value, bytes) else setting.value.encode('utf8'))
setting.allowed_values = loads(setting.allowed_values if isinstance(setting.allowed_values, bytes)
else setting.allowed_values.encode('utf8'))
except KeyError:
grouped_settings[] = [setting]
return grouped_settings
def get_setting(key, default=None):
Get a setting
setting = Setting.query.get(key)
if not setting:
return default
return loads(setting.value if isinstance(setting.value, bytes) else setting.value.encode('utf8'))
def save_setting(key, value):
setting = Setting.query.get(key)
if not setting:
raise Exception('Cannot save setting without running add_setting: {}'.format(key))
setting.value = dumps(value)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
{% extends 'admin/master.html' %}
{% block body %}
<article class="container" style="margin-top: 10px;">
<h2 class="h2">Settings</h2>
<form role="form" method="post">
{% for group, group_settings in settings.items() %}
<div class="card mb-3">
<div class="card-header">{{ group.title() }}</div>
<div class="card-body">
{% for setting in group_settings %}
<div class="form-group {% if setting.type == 'bool' %}custom-control custom-switch{% endif %}">
{% if setting.type == 'bool' %}
<input type="checkbox" class="custom-control-input" id="input-{{ setting.key }}" name="{{ setting.key }}" {% if setting.value %}checked{% endif %}>
<label for="input-{{ setting.key }}" class="custom-control-label">{{ setting.title }}</label>
{% else %}
<label for="input-{{ setting.key }}">{{ setting.title }}</label>
{% if setting.allowed_values %}
<select name="{{ setting.key }}" id="input-{{ setting.key }}" class="form-control">
{% for val in setting.allowed_values %}
<option value="{{ val }}" {% if val == setting.value %}selected{% endif %}>{{ val }}</option>
{% endfor %}
{% elif setting.type == "int" %}
<input type="number" name="{{ setting.key }}" id="input-{{ setting.key }}" class="form-control" value="{{ setting.value }}">
{% else %}
<input type="text" name="{{ setting.key }}" id="input-{{ setting.key }}" class="form-control" value="{{ setting.value }}">
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
<div class="mt-3">
<button type="submit" class="btn btn-primary">Save</button>
{% endblock %}
<!DOCTYPE html>
<html lang="en" class="h-100">
<meta charset="utf-8">
<title>{% if page %}{{ page.title }} | {% endif %}{{config.get('CORNERSTONE_TITLE', 'CornerstoneCMS')}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="stylesheet" href="" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
main {
padding-top: 70px;
padding-bottom: 15px;
.footer {
background-color: #f5f5f5;