Move to new settings system

pull/2/head
Noah 2014-12-04 15:06:44 -08:00
parent 023d5f91df
commit ff75921129
20 changed files with 348 additions and 111 deletions

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
# Don't check in site specific settings. # Don't check in site specific settings.
config.py settings.ini
# Compiled Python # Compiled Python
*.pyc *.pyc

180
defaults.ini Normal file
View File

@ -0,0 +1,180 @@
# Default configuration settings for Rophako - DO NOT EDIT THIS FILE!
#
# To configure your site, create a new file named "settings.yml" and override
# settings defined in this file. Your settings.yml is masked on top of the
# settings in defaults.yml.
#
# String values can substitute the following special variables:
# %(_basedir): The absolute path to the root of this git repository, such that
# ./rophako/app.py exists.
# %(_year): inserts the current year (for the RSS feed copyright setting)
# Constants that may be useful in this file.
[DEFAULT]
_admin_email = root@localhost
_date_format = %A, %B %d %Y @ %I:%M:%S %p
# "Weekday, Month dd yyyy @ hh:mm:ss AM"
#------------------------------------------------------------------------------#
# General Website Settings #
#------------------------------------------------------------------------------#
[site]
# Debug mode for development only!
debug = false
# Unique name of your site, e.g. "kirsle.net"
site_name = example.com
# Path to your site's HTML root. Whenever Rophako tries to render a
# template, it will check in your site's root for the template first before
# defaulting to the default fallback pages in the rophako/www folder. All
# of the core Rophako pages, e.g. for account, blog, photo albums and so on,
# have templates in the default site. You can override those templates by
# creating files with the same paths in your site's HTML folder.
site_root = %(_basedir)s/site/www
# E-mail address for site notifications (e.g. new comments and exceptions)
notify_address = %(_admin_email)s
# Where to save temp files for photo uploads etc.
tempdir = /tmp
#------------------------------------------------------------------------------#
# Database settings #
#------------------------------------------------------------------------------#
[db]
# Rophako uses a flat file JSON database system, and a Redis server sits
# between Rophako and the filesystem. The db_root is the path on the
# filesystem to store documents in (can be relative, default "./db")
db_root = db
redis_host = localhost
redis_port = 6379
redis_db = 0
redis_prefix = rophako:
#------------------------------------------------------------------------------#
# Security Settings #
#------------------------------------------------------------------------------#
[security]
# Set this value to true to force SSL/TLS use on your web app. Turning
# this on will do the following:
# - Send HTTP Strict-Transport-Security header
# - Use secure session cookies
force_ssl = false
# Secret key used for session cookie signing. Make this long and hard to
# guess.
#
# Tips for creating a strong secret key:
# $ python
# >>> import os
# >>> os.urandom(24)
# '\xfd{H\xe5<\x95\xf9\xe3\x96.5\xd1\x01O<!\xd5\xa2\xa0\x9fR"\xa1\xa8'
#
# Then take that whole quoted string and paste it right in as the secret
# key! Do NOT use that one. It was just an example! Make your own.
secret_key = for the love of Arceus, change this key!
# Password strength: number of iterations for bcrypt password.
bcrypt_iterations = 12
#------------------------------------------------------------------------------#
# Mail Settings #
#------------------------------------------------------------------------------#
[mail]
# method = smtp or sendmail (not yet implemented)
method = smtp
server = localhost
port = 25
sender = Rophako CMS <no-reply@rophako.kirsle.net>
#------------------------------------------------------------------------------#
# Plugin Configurations #
#------------------------------------------------------------------------------#
###
# Emoticons
###
# Emoticon theme used for blog posts and comments. Should exist at the URL
# "/static/smileys" from your document root, and have a file named
# "emoticons.json" inside. If you add a custom theme to your private site
# folder, then also change EMOTICON_ROOT_PRIVATE to look there instead.
[emoticons]
theme = tango
root_private = %(_basedir)s/rophako/www/static/smileys
###
# Blog
###
[blog]
default_category = Uncategorized
default_privacy = public
time_format = %(_date_format)s
allow_comments = true
entries_per_page = 5
# RSS feed settings.
title = Rophako CMS Blog
link = http://rophako.kirsle.net/
language = en
description = The web blog of the Rophako CMS.
copyright = Copyright %(_year)s
webmaster = %(_admin_email)s
image_title = Rophako CMS Blog
image_url = //www.kirsle.net/static/avatars/default.png
image_width = 100
image_height = 100
image_description = Rophako CMS
entries_per_feed = 5
###
# Photo
###
[photo]
# The path to where uploaded photos will be stored.
# The PRIVATE path is from the perspective of the server file system.
# The PUBLIC path is from the perspective of the web browser via HTTP.
root_private = %(_basedir)s/site/www/static/photos
root_public = /static/photos
default_album = My Photos
time_format = %(_date_format)s
# Max widths for photo sizes
width_large = 800
width_thumb = 256
width_avatar = 96
###
# Comment
###
[comment]
time_format = %(_date_format)s
# We use Gravatar for comments if the user provides an e-mail address.
# Specify the URL to a fallback image to use in case they don't have
# a gravatar.
default_avatar =
#------------------------------------------------------------------------------#
# List of Enabled Plugins #
#------------------------------------------------------------------------------#
[plugins]
# Which plugins to enable? List each plugin by module name. The plugins
# will be assumed to be blueprints that can be attached to the main app
# object. If you instead want to load an arbitrary Python module (i.e. to
# define custom routes at the app layer, not in a blueprint) list those
# under the "custom" section (remove the empty array [] and list them
# like shown in the plugins section).
blueprints =
rophako.modules.blog
rophako.modules.photo
rophako.modules.comment
rophako.modules.emoticons
rophako.modules.contact
rophako.modules.tracking
custom =

View File

@ -7,7 +7,7 @@ import re
import os import os
import json import json
import config from rophako.settings import Config
from rophako.app import app from rophako.app import app
from rophako.utils import template, login_required from rophako.utils import template, login_required
import rophako.model.blog as Blog import rophako.model.blog as Blog
@ -109,6 +109,6 @@ def ssl_test():
}, },
"App Configuration": { "App Configuration": {
"Session cookies secure": app.config["SESSION_COOKIE_SECURE"], "Session cookies secure": app.config["SESSION_COOKIE_SECURE"],
"config.FORCE_SSL": config.FORCE_SSL, "config.FORCE_SSL": Config.security.force_ssl,
}, },
})) }))

View File

@ -5,4 +5,4 @@ bcrypt
pillow pillow
requests requests
Markdown Markdown
Pygments Pygments

View File

@ -23,17 +23,20 @@ app = Flask(__name__,
# jinja2.ChoiceLoader. # jinja2.ChoiceLoader.
BLUEPRINT_PATHS = [] BLUEPRINT_PATHS = []
import config from rophako.settings import Config
Config.load_settings()
Config.load_plugins()
from rophako import __version__ from rophako import __version__
from rophako.plugin import load_plugin from rophako.plugin import load_plugin
import rophako.model.tracking as Tracking import rophako.model.tracking as Tracking
import rophako.utils import rophako.utils
app.DEBUG = config.DEBUG app.DEBUG = Config.site.debug == "true"
app.secret_key = config.SECRET_KEY app.secret_key = Config.security.secret_key.decode("string_escape")
# Security? # Security?
if config.FORCE_SSL: if Config.security.force_ssl == "true":
app.config['SESSION_COOKIE_SECURE'] = True app.config['SESSION_COOKIE_SECURE'] = True
sslify = SSLify(app) sslify = SSLify(app)
@ -44,8 +47,8 @@ load_plugin("rophako.modules.account")
# Custom Jinja handler to support custom- and default-template folders for # Custom Jinja handler to support custom- and default-template folders for
# rendering templates. # rendering templates.
template_paths = [ template_paths = [
config.SITE_ROOT, # Site specific. Config.site.site_root, # Site specific.
"rophako/www", # Default/fall-back "rophako/www", # Default/fall-back
] ]
template_paths.extend(BLUEPRINT_PATHS) template_paths.extend(BLUEPRINT_PATHS)
app.jinja_loader = jinja2.ChoiceLoader([ jinja2.FileSystemLoader(x) for x in template_paths]) app.jinja_loader = jinja2.ChoiceLoader([ jinja2.FileSystemLoader(x) for x in template_paths])
@ -70,7 +73,7 @@ def before_request():
"version": __version__, "version": __version__,
"python_version": "{}.{}".format(sys.version_info.major, sys.version_info.minor), "python_version": "{}.{}".format(sys.version_info.major, sys.version_info.minor),
"author": "Noah Petherbridge", "author": "Noah Petherbridge",
"photo_url": config.PHOTO_ROOT_PUBLIC, "photo_url": Config.photo.root_public,
}, },
"uri": request.path, "uri": request.path,
"session": { "session": {
@ -125,7 +128,7 @@ def catchall(path):
otherwise we give the 404 error page.""" otherwise we give the 404 error page."""
# Search for this file. # Search for this file.
for root in [config.SITE_ROOT, "rophako/www"]: for root in [Config.site.site_root, "rophako/www"]:
abspath = os.path.abspath("{}/{}".format(root, path)) abspath = os.path.abspath("{}/{}".format(root, path))
if os.path.isfile(abspath): if os.path.isfile(abspath):
return send_file(abspath) return send_file(abspath)

View File

@ -12,7 +12,7 @@ import redis
import json import json
import time import time
import config from rophako.settings import Config
from rophako.log import logger from rophako.log import logger
redis_client = None redis_client = None
@ -111,7 +111,7 @@ def mkpath(document):
if document.endswith(".json"): if document.endswith(".json"):
# Let's not do that. # Let's not do that.
raise Exception("mkpath: document path already includes .json extension!") raise Exception("mkpath: document path already includes .json extension!")
return "{}/{}.json".format(config.DB_ROOT, str(document)) return "{}/{}.json".format(Config.db.db_root, str(document))
def read_json(path): def read_json(path):
@ -137,6 +137,7 @@ def read_json(path):
data = json.loads(text) data = json.loads(text)
except: except:
logger.error("Couldn't decode JSON data from {}".format(path)) logger.error("Couldn't decode JSON data from {}".format(path))
logger.error(text)
data = None data = None
return data return data
@ -179,16 +180,16 @@ def get_redis():
global redis_client global redis_client
if not redis_client: if not redis_client:
redis_client = redis.StrictRedis( redis_client = redis.StrictRedis(
host = config.REDIS_HOST, host = Config.db.redis_host,
port = config.REDIS_PORT, port = Config.db.redis_port,
db = config.REDIS_DB, db = Config.db.redis_db,
) )
return redis_client return redis_client
def set_cache(key, value, expires=None): def set_cache(key, value, expires=None):
"""Set a key in the Redis cache.""" """Set a key in the Redis cache."""
key = config.REDIS_PREFIX + key key = Config.db.redis_prefix + key
try: try:
client = get_redis() client = get_redis()
client.set(key, json.dumps(value)) client.set(key, json.dumps(value))
@ -202,7 +203,7 @@ def set_cache(key, value, expires=None):
def get_cache(key): def get_cache(key):
"""Get a cached item.""" """Get a cached item."""
key = config.REDIS_PREFIX + key key = Config.db.redis_prefix + key
value = None value = None
try: try:
client = get_redis() client = get_redis()
@ -217,6 +218,6 @@ def get_cache(key):
def del_cache(key): def del_cache(key):
"""Delete a cached item.""" """Delete a cached item."""
key = config.REDIS_PREFIX + key key = Config.db.redis_prefix + key
client = get_redis() client = get_redis()
client.delete(key) client.delete(key)

View File

@ -7,7 +7,7 @@ from __future__ import print_function
from flask import g, request from flask import g, request
import logging import logging
import config from rophako.settings import Config
class LogHandler(logging.Handler): class LogHandler(logging.Handler):
"""A custom logging handler.""" """A custom logging handler."""
@ -29,7 +29,7 @@ handler.setFormatter(logging.Formatter("[%(asctime)s] [%(levelname)s] $prefix$%(
logger.addHandler(handler) logger.addHandler(handler)
# Log level. # Log level.
if config.DEBUG: if Config.site.debug == "true":
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
else: else:
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)

View File

@ -8,7 +8,7 @@ import re
import glob import glob
import os import os
import config from rophako.settings import Config
import rophako.jsondb as JsonDB import rophako.jsondb as JsonDB
from rophako.log import logger from rophako.log import logger
@ -206,7 +206,7 @@ def list_avatars():
# Load avatars from both locations. We check the built-in set first, # Load avatars from both locations. We check the built-in set first,
# so if you have matching names in your local site those will override. # so if you have matching names in your local site those will override.
"rophako/www/static/avatars/*.*", "rophako/www/static/avatars/*.*",
os.path.join(config.SITE_ROOT, "static", "avatars", "*.*"), os.path.join(Config.site.site_root, "static", "avatars", "*.*"),
] ]
for path in paths: for path in paths:
for filename in glob.glob(path): for filename in glob.glob(path):
@ -228,4 +228,4 @@ def get_next_id(index):
# Sanity check! # Sanity check!
if next_id in index: if next_id in index:
raise Exception("Failed to get_next_id for the blog. Chosen ID is still in the index!") raise Exception("Failed to get_next_id for the blog. Chosen ID is still in the index!")
return next_id return next_id

View File

@ -2,7 +2,7 @@
"""Commenting models.""" """Commenting models."""
from flask import g, url_for from flask import url_for
import time import time
import hashlib import hashlib
import urllib import urllib
@ -10,7 +10,7 @@ import random
import re import re
import sys import sys
import config from rophako.settings import Config
import rophako.jsondb as JsonDB import rophako.jsondb as JsonDB
import rophako.model.user as User import rophako.model.user as User
import rophako.model.emoticons as Emoticons import rophako.model.emoticons as Emoticons
@ -58,7 +58,7 @@ def add_comment(thread, uid, name, subject, message, url, time, ip, image=None):
# Send the e-mail to the site admins. # Send the e-mail to the site admins.
send_email( send_email(
to=config.NOTIFY_ADDRESS, to=Config.site.notify_address,
subject="New comment: {}".format(subject), subject="New comment: {}".format(subject),
message="""{name} has left a comment on: {subject} message="""{name} has left a comment on: {subject}
@ -230,7 +230,7 @@ def gravatar(email):
"""Generate a Gravatar link for an email address.""" """Generate a Gravatar link for an email address."""
if "@" in email: if "@" in email:
# Default avatar? # Default avatar?
default = config.COMMENT_DEFAULT_AVATAR default = Config.comment.default_avatar
# Construct the URL. # Construct the URL.
params = { params = {

View File

@ -2,14 +2,12 @@
"""Emoticon models.""" """Emoticon models."""
from flask import g, url_for
import os import os
import codecs import codecs
import json import json
import re import re
import config from rophako.settings import Config
import rophako.jsondb as JsonDB
from rophako.log import logger from rophako.log import logger
@ -18,7 +16,7 @@ _cache = {}
def load_theme(): def load_theme():
"""Pre-load and cache the emoticon theme. This happens on startup.""" """Pre-load and cache the emoticon theme. This happens on startup."""
theme = config.EMOTICON_THEME theme = Config.emoticons.theme
global _cache global _cache
# Cached? # Cached?
@ -26,13 +24,13 @@ def load_theme():
return _cache return _cache
# Only if the theme file exists. # Only if the theme file exists.
settings = os.path.join(config.EMOTICON_ROOT_PRIVATE, theme, "emoticons.json") settings = os.path.join(Config.emoticons.root_private, theme, "emoticons.json")
if not os.path.isfile(settings): if not os.path.isfile(settings):
logger.error("Failed to load smiley theme {}: not found!") logger.error("Failed to load smiley theme {}: not found!")
# Try the default (tango). # Try the default (tango).
theme = "tango" theme = "tango"
settings = os.path.join(config.EMOTICON_ROOT_PRIVATE, theme, "emoticons.json") settings = os.path.join(Config.emoticons.root_private, theme, "emoticons.json")
if os.path.isfile(settings): if os.path.isfile(settings):
logger.info("Falling back to default theme: tango") logger.info("Falling back to default theme: tango")
else: else:
@ -71,11 +69,11 @@ def render(message):
if trigger in message: if trigger in message:
# Substitute it. # Substitute it.
sub = """<img src="{url}" alt="{trigger}" title="{trigger}">""".format( sub = """<img src="{url}" alt="{trigger}" title="{trigger}">""".format(
url="/static/smileys/{}/{}".format(config.EMOTICON_THEME, img), url="/static/smileys/{}/{}".format(Config.emoticons.theme, img),
trigger=trigger, trigger=trigger,
) )
pattern = r'([^A-Za-z0-9:\-]|^){}([^A-Za-z0-9:\-]|$)'.format(re.escape(trigger)) pattern = r'([^A-Za-z0-9:\-]|^){}([^A-Za-z0-9:\-]|$)'.format(re.escape(trigger))
result = r'\1{}\2'.format(sub) result = r'\1{}\2'.format(sub)
message = re.sub(pattern, result, message) message = re.sub(pattern, result, message)
return message return message

View File

@ -10,16 +10,16 @@ from PIL import Image
import hashlib import hashlib
import random import random
import config from rophako.settings import Config
import rophako.jsondb as JsonDB import rophako.jsondb as JsonDB
from rophako.utils import sanitize_name, remote_addr from rophako.utils import sanitize_name, remote_addr
from rophako.log import logger from rophako.log import logger
# Maps the friendly names of photo sizes with their pixel values from config. # Maps the friendly names of photo sizes with their pixel values from config.
PHOTO_SCALES = dict( PHOTO_SCALES = dict(
large=config.PHOTO_WIDTH_LARGE, large=int(Config.photo.width_large),
thumb=config.PHOTO_WIDTH_THUMB, thumb=int(Config.photo.width_thumb),
avatar=config.PHOTO_WIDTH_AVATAR, avatar=int(Config.photo.width_avatar),
) )
@ -57,7 +57,6 @@ def list_albums():
def get_album(name): def get_album(name):
"""Get details about an album.""" """Get details about an album."""
index = get_index() index = get_index()
result = []
if not name in index["albums"]: if not name in index["albums"]:
return None return None
@ -79,7 +78,7 @@ def rename_album(old_name, new_name):
Returns True on success, False if the new name conflicts with another Returns True on success, False if the new name conflicts with another
album's name.""" album's name."""
old_name = sanitize_name(old_name) old_name = sanitize_name(old_name)
newname = sanitize_name(new_name) new_name = sanitize_name(new_name)
index = get_index() index = get_index()
# New name is unique? # New name is unique?
@ -196,7 +195,7 @@ def get_photo(key):
def get_image_dimensions(pic): def get_image_dimensions(pic):
"""Use PIL to get the image's true dimensions.""" """Use PIL to get the image's true dimensions."""
filename = os.path.join(config.PHOTO_ROOT_PRIVATE, pic["large"]) filename = os.path.join(Config.photo.root_private, pic["large"])
img = Image.open(filename) img = Image.open(filename)
return img.size return img.size
@ -233,11 +232,11 @@ def crop_photo(key, x, y, length):
for size in ["thumb", "avatar"]: for size in ["thumb", "avatar"]:
pic = index["albums"][album][key][size] pic = index["albums"][album][key][size]
logger.debug("Delete {} size: {}".format(size, pic)) logger.debug("Delete {} size: {}".format(size, pic))
os.unlink(os.path.join(config.PHOTO_ROOT_PRIVATE, pic)) os.unlink(os.path.join(Config.photo.root_private, pic))
# Regenerate all the thumbnails. # Regenerate all the thumbnails.
large = index["albums"][album][key]["large"] large = index["albums"][album][key]["large"]
source = os.path.join(config.PHOTO_ROOT_PRIVATE, large) source = os.path.join(Config.photo.root_private, large)
for size in ["thumb", "avatar"]: for size in ["thumb", "avatar"]:
pic = resize_photo(source, size, crop=dict( pic = resize_photo(source, size, crop=dict(
x=x, x=x,
@ -305,7 +304,7 @@ def rotate_photo(key, rotate):
new_names = dict() new_names = dict()
for size in ["large", "thumb", "avatar"]: for size in ["large", "thumb", "avatar"]:
fname = os.path.join(config.PHOTO_ROOT_PRIVATE, photo[size]) fname = os.path.join(Config.photo.root_private, photo[size])
logger.info("Rotating image {} by {} degrees.".format(fname, degrees)) logger.info("Rotating image {} by {} degrees.".format(fname, degrees))
# Give it a new name. # Give it a new name.
@ -315,7 +314,7 @@ def rotate_photo(key, rotate):
img = Image.open(fname) img = Image.open(fname)
img = img.rotate(degrees) img = img.rotate(degrees)
img.save(os.path.join(config.PHOTO_ROOT_PRIVATE, outfile)) img.save(os.path.join(Config.photo.root_private, outfile))
# Delete the old name. # Delete the old name.
os.unlink(fname) os.unlink(fname)
@ -339,7 +338,7 @@ def delete_photo(key):
# Delete all the images. # Delete all the images.
for size in ["large", "thumb", "avatar"]: for size in ["large", "thumb", "avatar"]:
logger.info("Delete: {}".format(photo[size])) logger.info("Delete: {}".format(photo[size]))
fname = os.path.join(config.PHOTO_ROOT_PRIVATE, photo[size]) fname = os.path.join(Config.photo.root_private, photo[size])
if os.path.isfile(fname): if os.path.isfile(fname):
os.unlink(fname) os.unlink(fname)
@ -428,7 +427,7 @@ def upload_from_pc(request):
if not allowed_filetype(upload.filename): if not allowed_filetype(upload.filename):
return dict(success=False, error="Unsupported file extension.") return dict(success=False, error="Unsupported file extension.")
tempfile = "{}/rophako-photo-{}.{}".format(config.TEMPDIR, int(time.time()), filetype) tempfile = "{}/rophako-photo-{}.{}".format(Config.site.tempdir, int(time.time()), filetype)
logger.debug("Save incoming photo to: {}".format(tempfile)) logger.debug("Save incoming photo to: {}".format(tempfile))
upload.save(tempfile) upload.save(tempfile)
@ -461,7 +460,7 @@ def upload_from_www(form):
# Make a temp filename for it. # Make a temp filename for it.
filetype = url.rsplit(".", 1)[1] filetype = url.rsplit(".", 1)[1]
tempfile = "{}/rophako-photo-{}.{}".format(config.TEMPDIR, int(time.time()), filetype) tempfile = "{}/rophako-photo-{}.{}".format(Config.site.tempdir, int(time.time()), filetype)
logger.debug("Save incoming photo to: {}".format(tempfile)) logger.debug("Save incoming photo to: {}".format(tempfile))
# Grab the file. # Grab the file.
@ -500,7 +499,7 @@ def process_photo(form, filename):
album = sanitize_name(album) album = sanitize_name(album)
if album == "": if album == "":
logger.warning("Album name didn't pass sanitization! Fall back to default album name.") logger.warning("Album name didn't pass sanitization! Fall back to default album name.")
album = config.PHOTO_DEFAULT_ALBUM album = Config.photo.default_album
# Make up a unique public key for this set of photos. # Make up a unique public key for this set of photos.
key = random_hash() key = random_hash()
@ -574,7 +573,7 @@ def resize_photo(filename, size, crop=None):
# Make up a unique filename. # Make up a unique filename.
outfile = random_name(filetype) outfile = random_name(filetype)
target = os.path.join(config.PHOTO_ROOT_PRIVATE, outfile) target = os.path.join(Config.photo.root_private, outfile)
logger.debug("Output file for {} scale: {}".format(size, target)) logger.debug("Output file for {} scale: {}".format(size, target))
# Get the image's dimensions. # Get the image's dimensions.
@ -679,7 +678,7 @@ def random_name(filetype):
"""Get a random available file name to save a new photo.""" """Get a random available file name to save a new photo."""
filetype = filetype.lower() filetype = filetype.lower()
outfile = random_hash() + "." + filetype outfile = random_hash() + "." + filetype
while os.path.isfile(os.path.join(config.PHOTO_ROOT_PRIVATE, outfile)): while os.path.isfile(os.path.join(Config.photo.root_private, outfile)):
outfile = random_hash() + "." + filetype outfile = random_hash() + "." + filetype
return outfile return outfile

View File

@ -5,7 +5,7 @@
import bcrypt import bcrypt
import time import time
import config from rophako.settings import Config
import rophako.jsondb as JsonDB import rophako.jsondb as JsonDB
import rophako.model.photo as Photo import rophako.model.photo as Photo
from rophako.log import logger from rophako.log import logger
@ -147,7 +147,7 @@ def exists(uid=None, username=None):
def hash_password(password): def hash_password(password):
return bcrypt.hashpw(str(password).encode("utf-8"), bcrypt.gensalt(config.BCRYPT_ITERATIONS)).decode("utf-8") return bcrypt.hashpw(str(password).encode("utf-8"), bcrypt.gensalt(int(Config.security.bcrypt_iterations))).decode("utf-8")
def check_auth(username, password): def check_auth(username, password):
@ -172,4 +172,4 @@ def get_next_uid():
uid = 1 uid = 1
while exists(uid=uid): while exists(uid=uid):
uid += 1 uid += 1
return uid return uid

View File

@ -14,8 +14,8 @@ import rophako.model.emoticons as Emoticons
from rophako.utils import (template, render_markdown, pretty_time, from rophako.utils import (template, render_markdown, pretty_time,
login_required, remote_addr) login_required, remote_addr)
from rophako.plugin import load_plugin from rophako.plugin import load_plugin
from rophako.settings import Config
from rophako.log import logger from rophako.log import logger
from config import *
mod = Blueprint("blog", __name__, url_prefix="/blog") mod = Blueprint("blog", __name__, url_prefix="/blog")
load_plugin("rophako.modules.comment") load_plugin("rophako.modules.comment")
@ -34,16 +34,16 @@ def archive():
groups = dict() groups = dict()
friendly_months = dict() friendly_months = dict()
for post_id, data in index.items(): for post_id, data in index.items():
time = datetime.datetime.fromtimestamp(data["time"]) ts = datetime.datetime.fromtimestamp(data["time"])
date = time.strftime("%Y-%m") date = ts.strftime("%Y-%m")
if not date in groups: if not date in groups:
groups[date] = dict() groups[date] = dict()
friendly = time.strftime("%B %Y") friendly = ts.strftime("%B %Y")
friendly_months[date] = friendly friendly_months[date] = friendly
# Get author's profile && Pretty-print the time. # Get author's profile && Pretty-print the time.
data["profile"] = User.get_user(uid=data["author"]) data["profile"] = User.get_user(uid=data["author"])
data["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, data["time"]) data["pretty_time"] = pretty_time(Config.blog.time_format, data["time"])
groups[date][post_id] = data groups[date][post_id] = data
# Sort by calendar month. # Sort by calendar month.
@ -101,10 +101,10 @@ def entry(fid):
# Get the author's information. # Get the author's information.
post["profile"] = User.get_user(uid=post["author"]) post["profile"] = User.get_user(uid=post["author"])
post["photo"] = User.get_picture(uid=post["author"]) post["photo"] = User.get_picture(uid=post["author"])
post["photo_url"] = PHOTO_ROOT_PUBLIC post["photo_url"] = Config.photo.root_public
# Pretty-print the time. # Pretty-print the time.
post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"]) post["pretty_time"] = pretty_time(Config.blog.time_format, post["time"])
# Count the comments for this post # Count the comments for this post
post["comment_count"] = Comment.count_comments("blog-{}".format(post_id)) post["comment_count"] = Comment.count_comments("blog-{}".format(post_id))
@ -153,9 +153,9 @@ def update():
format="markdown", format="markdown",
avatar="", avatar="",
categories="", categories="",
privacy=BLOG_DEFAULT_PRIVACY, privacy=Config.blog.default_privacy,
emoticons=True, emoticons=True,
comments=BLOG_ALLOW_COMMENTS, comments=Config.blog.allow_comments,
month="", month="",
day="", day="",
year="", year="",
@ -328,14 +328,14 @@ def rss():
today = time.strftime(rss_time, time.gmtime()) today = time.strftime(rss_time, time.gmtime())
xml_add_text_tags(doc, channel, [ xml_add_text_tags(doc, channel, [
["title", RSS_TITLE], ["title", Config.blog.title],
["link", RSS_LINK], ["link", Config.blog.link],
["description", RSS_DESCRIPTION], ["description", Config.blog.description],
["language", RSS_LANGUAGE], ["language", Config.blog.language],
["copyright", RSS_COPYRIGHT], ["copyright", Config.blog.copyright],
["pubDate", today], ["pubDate", today],
["lastBuildDate", today], ["lastBuildDate", today],
["webmaster", RSS_WEBMASTER], ["webmaster", Config.blog.webmaster],
]) ])
###### ######
@ -345,12 +345,12 @@ def rss():
image = doc.createElement("image") image = doc.createElement("image")
channel.appendChild(image) channel.appendChild(image)
xml_add_text_tags(doc, image, [ xml_add_text_tags(doc, image, [
["title", RSS_IMAGE_TITLE], ["title", Config.blog.image_title],
["url", RSS_IMAGE_URL], ["url", Config.blog.image_url],
["link", RSS_LINK], ["link", Config.blog.link],
["width", RSS_IMAGE_WIDTH], ["width", Config.blog.image_width],
["height", RSS_IMAGE_HEIGHT], ["height", Config.blog.image_height],
["description", RSS_IMAGE_DESCRIPTION], ["description", Config.blog.image_description],
]) ])
###### ######
@ -359,7 +359,7 @@ def rss():
index = Blog.get_index() index = Blog.get_index()
posts = get_index_posts(index) posts = get_index_posts(index)
for post_id in posts[:BLOG_ENTRIES_PER_RSS]: for post_id in posts[:int(Config.blog.entries_per_feed)]:
post = Blog.get_entry(post_id) post = Blog.get_entry(post_id)
item = doc.createElement("item") item = doc.createElement("item")
channel.appendChild(item) channel.appendChild(item)
@ -432,8 +432,8 @@ def partial_index():
# Handle the offsets, and get those for the "older" and "earlier" posts. # Handle the offsets, and get those for the "older" and "earlier" posts.
# "earlier" posts count down (towards index 0), "older" counts up. # "earlier" posts count down (towards index 0), "older" counts up.
g.info["offset"] = offset g.info["offset"] = offset
g.info["earlier"] = offset - BLOG_ENTRIES_PER_PAGE if offset > 0 else 0 g.info["earlier"] = offset - int(Config.blog.entries_per_page) if offset > 0 else 0
g.info["older"] = offset + BLOG_ENTRIES_PER_PAGE g.info["older"] = offset + int(Config.blog.entries_per_page)
if g.info["earlier"] < 0: if g.info["earlier"] < 0:
g.info["earlier"] = 0 g.info["earlier"] = 0
if g.info["older"] < 0 or g.info["older"] > len(posts): if g.info["older"] < 0 or g.info["older"] > len(posts):
@ -446,7 +446,7 @@ def partial_index():
# Load the selected posts. # Load the selected posts.
selected = [] selected = []
stop = offset + BLOG_ENTRIES_PER_PAGE stop = offset + int(Config.blog.entries_per_page)
if stop > len(posts): stop = len(posts) if stop > len(posts): stop = len(posts)
index = 1 # Let each post know its position on-page. index = 1 # Let each post know its position on-page.
for i in range(offset, stop): for i in range(offset, stop):
@ -468,9 +468,9 @@ def partial_index():
# Get the author's information. # Get the author's information.
post["profile"] = User.get_user(uid=post["author"]) post["profile"] = User.get_user(uid=post["author"])
post["photo"] = User.get_picture(uid=post["author"]) post["photo"] = User.get_picture(uid=post["author"])
post["photo_url"] = PHOTO_ROOT_PUBLIC post["photo_url"] = Config.photo.root_public
post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"]) post["pretty_time"] = pretty_time(Config.blog.time_format, post["time"])
# Count the comments for this post # Count the comments for this post
post["comment_count"] = Comment.count_comments("blog-{}".format(post_id)) post["comment_count"] = Comment.count_comments("blog-{}".format(post_id))

View File

@ -2,8 +2,7 @@
"""Endpoints for the commenting subsystem.""" """Endpoints for the commenting subsystem."""
from flask import Blueprint, g, request, redirect, url_for, session, flash from flask import Blueprint, g, request, redirect, url_for, flash
import re
import time import time
import rophako.model.user as User import rophako.model.user as User
@ -11,8 +10,7 @@ import rophako.model.comment as Comment
from rophako.utils import (template, pretty_time, login_required, sanitize_name, from rophako.utils import (template, pretty_time, login_required, sanitize_name,
remote_addr) remote_addr)
from rophako.plugin import load_plugin from rophako.plugin import load_plugin
from rophako.log import logger from rophako.settings import Config
from config import *
mod = Blueprint("comment", __name__, url_prefix="/comments") mod = Blueprint("comment", __name__, url_prefix="/comments")
load_plugin("rophako.modules.emoticons") load_plugin("rophako.modules.emoticons")
@ -71,7 +69,7 @@ def preview():
# Gravatar. # Gravatar.
g.info["gravatar"] = gravatar g.info["gravatar"] = gravatar
g.info["preview"] = Comment.format_message(form["message"]) g.info["preview"] = Comment.format_message(form["message"])
g.info["pretty_time"] = pretty_time(COMMENT_TIME_FORMAT, time.time()) g.info["pretty_time"] = pretty_time(Config.comment.time_format, time.time())
g.info.update(form) g.info.update(form)
return template("comment/preview.html") return template("comment/preview.html")
@ -191,7 +189,7 @@ def partial_index(thread, subject, header=True):
comment["image"] = avatar comment["image"] = avatar
# Add the pretty time. # Add the pretty time.
comment["pretty_time"] = pretty_time(COMMENT_TIME_FORMAT, comment["time"]) comment["pretty_time"] = pretty_time(Config.comment.time_format, comment["time"])
# Format the message for display. # Format the message for display.
comment["formatted_message"] = Comment.format_message(comment["message"]) comment["formatted_message"] = Comment.format_message(comment["message"])
@ -203,7 +201,7 @@ def partial_index(thread, subject, header=True):
g.info["subject"] = subject g.info["subject"] = subject
g.info["url"] = request.url g.info["url"] = request.url
g.info["comments"] = sorted_comments g.info["comments"] = sorted_comments
g.info["photo_url"] = PHOTO_ROOT_PUBLIC g.info["photo_url"] = Config.photo.root_public
return template("comment/index.inc.html") return template("comment/index.inc.html")

View File

@ -5,7 +5,7 @@
from flask import Blueprint, request, redirect, url_for, flash from flask import Blueprint, request, redirect, url_for, flash
from rophako.utils import template, send_email, remote_addr from rophako.utils import template, send_email, remote_addr
from config import * from rophako.settings import Config
mod = Blueprint("contact", __name__, url_prefix="/contact") mod = Blueprint("contact", __name__, url_prefix="/contact")
@ -42,9 +42,9 @@ def send():
# Send the e-mail. # Send the e-mail.
send_email( send_email(
to=NOTIFY_ADDRESS, to=Config.site.notify_address,
reply_to=reply_to, reply_to=reply_to,
subject="Contact Form on {}: {}".format(SITE_NAME, subject), subject="Contact Form on {}: {}".format(Config.site.site_name, subject),
message="""A visitor to {site_name} has sent you a message! message="""A visitor to {site_name} has sent you a message!
IP Address: {ip} IP Address: {ip}
@ -55,7 +55,7 @@ E-mail: {email}
Subject: {subject} Subject: {subject}
{message}""".format( {message}""".format(
site_name=SITE_NAME, site_name=Config.site.site_name,
ip=remote_addr(), ip=remote_addr(),
ua=request.user_agent.string, ua=request.user_agent.string,
referer=request.headers.get("Referer", ""), referer=request.headers.get("Referer", ""),

View File

@ -6,8 +6,7 @@ from flask import Blueprint, g
import rophako.model.emoticons as Emoticons import rophako.model.emoticons as Emoticons
from rophako.utils import template from rophako.utils import template
from rophako.log import logger from rophako.settings import Config
from config import *
mod = Blueprint("emoticons", __name__, url_prefix="/emoticons") mod = Blueprint("emoticons", __name__, url_prefix="/emoticons")
@ -24,7 +23,7 @@ def index():
"triggers": theme["map"][img], "triggers": theme["map"][img],
}) })
g.info["theme"] = EMOTICON_THEME g.info["theme"] = Config.emoticons.theme
g.info["theme_name"] = theme["name"] g.info["theme_name"] = theme["name"]
g.info["smileys"] = smileys g.info["smileys"] = smileys
return template("emoticons/index.html") return template("emoticons/index.html")

View File

@ -2,15 +2,14 @@
"""Endpoints for the photo albums.""" """Endpoints for the photo albums."""
from flask import Blueprint, g, request, redirect, url_for, session, flash from flask import Blueprint, g, request, redirect, url_for, flash
import rophako.model.user as User import rophako.model.user as User
import rophako.model.photo as Photo import rophako.model.photo as Photo
from rophako.utils import (template, pretty_time, render_markdown, from rophako.utils import (template, pretty_time, render_markdown,
login_required, ajax_response) login_required, ajax_response)
from rophako.plugin import load_plugin from rophako.plugin import load_plugin
from rophako.log import logger from rophako.settings import Config
from config import *
mod = Blueprint("photo", __name__, url_prefix="/photos") mod = Blueprint("photo", __name__, url_prefix="/photos")
load_plugin("rophako.modules.comment") load_plugin("rophako.modules.comment")
@ -68,7 +67,7 @@ def view_photo(key):
g.info["photo"] = photo g.info["photo"] = photo
g.info["photo"]["key"] = key g.info["photo"]["key"] = key
g.info["photo"]["pretty_time"] = pretty_time(PHOTO_TIME_FORMAT, photo["uploaded"]) g.info["photo"]["pretty_time"] = pretty_time(Config.photo.time_format, photo["uploaded"])
g.info["photo"]["markdown"] = render_markdown(photo.get("description", "")) g.info["photo"]["markdown"] = render_markdown(photo.get("description", ""))
return template("photos/view.html") return template("photos/view.html")
@ -125,10 +124,10 @@ def upload():
g.info["album_list"] = [ g.info["album_list"] = [
"My Photos", # the default "My Photos", # the default
] ]
g.info["selected"] = PHOTO_DEFAULT_ALBUM g.info["selected"] = Config.photo.default_album
albums = Photo.list_albums() albums = Photo.list_albums()
if len(albums): if len(albums):
g.info["album_list"] = [ album["name"] for album in albums ] g.info["album_list"] = [ x["name"] for x in albums ]
g.info["selected"] = albums[0] g.info["selected"] = albums[0]
return template("photos/upload.html") return template("photos/upload.html")

View File

@ -26,4 +26,4 @@ def load_plugin(name, as_blueprint=True, template_path=None):
module_path = name.replace(".", "/") module_path = name.replace(".", "/")
template_path = os.path.join(module_path, "templates") template_path = os.path.join(module_path, "templates")
BLUEPRINT_PATHS.append(template_path) BLUEPRINT_PATHS.append(template_path)

60
rophako/settings.py Normal file
View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
import os
import datetime
from attrdict import AttrDict
from ConfigParser import ConfigParser
from rophako.plugin import load_plugin
# Get the base directory of the git root.
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))
# https://github.com/bcj/AttrDict/issues/20
if not hasattr(AttrDict, "copy"):
setattr(AttrDict, "copy", lambda self: self._mapping.copy())
class ConfigHandler(object):
settings = None
def load_settings(self):
"""Load the settings files and make them available in the global config."""
self.settings = ConfigParser(dict_type=AttrDict)
# Set dynamic default variables.
self.settings.set("DEFAULT", "_basedir", basedir)
self.settings.set("DEFAULT", "_year", str(datetime.datetime.now().strftime("%Y")))
# Read the defaults and then apply the custom settings on top.
self.settings.read(["defaults.ini", "settings.ini"])
def print_settings(self):
"""Pretty-print the contents of the configuration as JSON."""
for section in self.settings.sections():
print "[{}]".format(section)
for opt in self.settings.options(section):
print "{} = {}".format(opt, repr(self.settings.get(section, opt)))
print ""
def load_plugins(self):
"""Load all the plugins specified by the config file."""
for plugin in self.plugins.blueprints.split("\n"):
plugin = plugin.strip()
if not plugin:
continue
load_plugin(plugin)
for custom in self.plugins.custom.split("\n"):
custom = custom.strip()
if not custom:
continue
load_plugin(custom, as_blueprint=False)
def __getattr__(self, section):
"""Attribute access for the config object.
You can access config settings via Config.<section>.<name>, for example
Config.site.notify_email and Config.blog.posts_per_page. All results are
returned as strings per ConfigParser, so cast them if you need to."""
return AttrDict(dict(self.settings.items(section)))
Config = ConfigHandler()

View File

@ -14,7 +14,7 @@ import json
import urlparse import urlparse
from rophako.log import logger from rophako.log import logger
from config import * from rophako.settings import Config
def login_required(f): def login_required(f):
@ -167,13 +167,13 @@ def render_markdown(body, html_escape=True, extensions=None, blacklist=None):
def send_email(to, subject, message, sender=None, reply_to=None): def send_email(to, subject, message, sender=None, reply_to=None):
"""Send an e-mail out.""" """Send an e-mail out."""
if sender is None: if sender is None:
sender = MAIL_SENDER sender = Config.mail.sender
if type(to) != list: if type(to) != list:
to = [to] to = [to]
logger.info("Send email to {}".format(to)) logger.info("Send email to {}".format(to))
if MAIL_METHOD == "smtp": if Config.mail.method == "smtp":
# Send mail with SMTP. # Send mail with SMTP.
for email in to: for email in to:
# Construct the mail headers. # Construct the mail headers.
@ -186,7 +186,7 @@ def send_email(to, subject, message, sender=None, reply_to=None):
headers.append("Subject: {}".format(subject)) headers.append("Subject: {}".format(subject))
# Prepare the mail for transport. # Prepare the mail for transport.
server = smtplib.SMTP(MAIL_SERVER, MAIL_PORT) server = smtplib.SMTP(Config.mail.server, Config.mail.port)
msg = "\n".join(headers) + "\n\n" + message msg = "\n".join(headers) + "\n\n" + message
server.sendmail(sender, email, msg) server.sendmail(sender, email, msg)
server.quit() server.quit()