diff --git a/.gitignore b/.gitignore
index 6417abc..636d872 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
# Don't check in site specific settings.
-config.py
+settings.ini
# Compiled Python
*.pyc
diff --git a/defaults.ini b/defaults.ini
new file mode 100644
index 0000000..0282c0a
--- /dev/null
+++ b/defaults.ini
@@ -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
+
+#------------------------------------------------------------------------------#
+# 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 =
diff --git a/kirsle_legacy.py b/kirsle_legacy.py
index a5164b8..3a72710 100644
--- a/kirsle_legacy.py
+++ b/kirsle_legacy.py
@@ -7,7 +7,7 @@ import re
import os
import json
-import config
+from rophako.settings import Config
from rophako.app import app
from rophako.utils import template, login_required
import rophako.model.blog as Blog
@@ -109,6 +109,6 @@ def ssl_test():
},
"App Configuration": {
"Session cookies secure": app.config["SESSION_COOKIE_SECURE"],
- "config.FORCE_SSL": config.FORCE_SSL,
+ "config.FORCE_SSL": Config.security.force_ssl,
},
}))
diff --git a/requirements.txt b/requirements.txt
index e95c9bb..1b84f03 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,4 +5,4 @@ bcrypt
pillow
requests
Markdown
-Pygments
\ No newline at end of file
+Pygments
diff --git a/rophako/app.py b/rophako/app.py
index bbd4de3..6bef520 100644
--- a/rophako/app.py
+++ b/rophako/app.py
@@ -23,17 +23,20 @@ app = Flask(__name__,
# jinja2.ChoiceLoader.
BLUEPRINT_PATHS = []
-import config
+from rophako.settings import Config
+Config.load_settings()
+Config.load_plugins()
+
from rophako import __version__
from rophako.plugin import load_plugin
import rophako.model.tracking as Tracking
import rophako.utils
-app.DEBUG = config.DEBUG
-app.secret_key = config.SECRET_KEY
+app.DEBUG = Config.site.debug == "true"
+app.secret_key = Config.security.secret_key.decode("string_escape")
# Security?
-if config.FORCE_SSL:
+if Config.security.force_ssl == "true":
app.config['SESSION_COOKIE_SECURE'] = True
sslify = SSLify(app)
@@ -44,8 +47,8 @@ load_plugin("rophako.modules.account")
# Custom Jinja handler to support custom- and default-template folders for
# rendering templates.
template_paths = [
- config.SITE_ROOT, # Site specific.
- "rophako/www", # Default/fall-back
+ Config.site.site_root, # Site specific.
+ "rophako/www", # Default/fall-back
]
template_paths.extend(BLUEPRINT_PATHS)
app.jinja_loader = jinja2.ChoiceLoader([ jinja2.FileSystemLoader(x) for x in template_paths])
@@ -70,7 +73,7 @@ def before_request():
"version": __version__,
"python_version": "{}.{}".format(sys.version_info.major, sys.version_info.minor),
"author": "Noah Petherbridge",
- "photo_url": config.PHOTO_ROOT_PUBLIC,
+ "photo_url": Config.photo.root_public,
},
"uri": request.path,
"session": {
@@ -125,7 +128,7 @@ def catchall(path):
otherwise we give the 404 error page."""
# 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))
if os.path.isfile(abspath):
return send_file(abspath)
diff --git a/rophako/jsondb.py b/rophako/jsondb.py
index 7bcc3d1..3650f43 100644
--- a/rophako/jsondb.py
+++ b/rophako/jsondb.py
@@ -12,7 +12,7 @@ import redis
import json
import time
-import config
+from rophako.settings import Config
from rophako.log import logger
redis_client = None
@@ -111,7 +111,7 @@ def mkpath(document):
if document.endswith(".json"):
# Let's not do that.
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):
@@ -137,6 +137,7 @@ def read_json(path):
data = json.loads(text)
except:
logger.error("Couldn't decode JSON data from {}".format(path))
+ logger.error(text)
data = None
return data
@@ -179,16 +180,16 @@ def get_redis():
global redis_client
if not redis_client:
redis_client = redis.StrictRedis(
- host = config.REDIS_HOST,
- port = config.REDIS_PORT,
- db = config.REDIS_DB,
+ host = Config.db.redis_host,
+ port = Config.db.redis_port,
+ db = Config.db.redis_db,
)
return redis_client
def set_cache(key, value, expires=None):
"""Set a key in the Redis cache."""
- key = config.REDIS_PREFIX + key
+ key = Config.db.redis_prefix + key
try:
client = get_redis()
client.set(key, json.dumps(value))
@@ -202,7 +203,7 @@ def set_cache(key, value, expires=None):
def get_cache(key):
"""Get a cached item."""
- key = config.REDIS_PREFIX + key
+ key = Config.db.redis_prefix + key
value = None
try:
client = get_redis()
@@ -217,6 +218,6 @@ def get_cache(key):
def del_cache(key):
"""Delete a cached item."""
- key = config.REDIS_PREFIX + key
+ key = Config.db.redis_prefix + key
client = get_redis()
client.delete(key)
diff --git a/rophako/log.py b/rophako/log.py
index e542e08..c68083f 100644
--- a/rophako/log.py
+++ b/rophako/log.py
@@ -7,7 +7,7 @@ from __future__ import print_function
from flask import g, request
import logging
-import config
+from rophako.settings import Config
class LogHandler(logging.Handler):
"""A custom logging handler."""
@@ -29,7 +29,7 @@ handler.setFormatter(logging.Formatter("[%(asctime)s] [%(levelname)s] $prefix$%(
logger.addHandler(handler)
# Log level.
-if config.DEBUG:
+if Config.site.debug == "true":
logger.setLevel(logging.DEBUG)
else:
- logger.setLevel(logging.INFO)
\ No newline at end of file
+ logger.setLevel(logging.INFO)
diff --git a/rophako/model/blog.py b/rophako/model/blog.py
index bda8cdc..de5be2e 100644
--- a/rophako/model/blog.py
+++ b/rophako/model/blog.py
@@ -8,7 +8,7 @@ import re
import glob
import os
-import config
+from rophako.settings import Config
import rophako.jsondb as JsonDB
from rophako.log import logger
@@ -206,7 +206,7 @@ def list_avatars():
# 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.
"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 filename in glob.glob(path):
@@ -228,4 +228,4 @@ def get_next_id(index):
# Sanity check!
if next_id in index:
raise Exception("Failed to get_next_id for the blog. Chosen ID is still in the index!")
- return next_id
\ No newline at end of file
+ return next_id
diff --git a/rophako/model/comment.py b/rophako/model/comment.py
index fef3a39..c1e2791 100644
--- a/rophako/model/comment.py
+++ b/rophako/model/comment.py
@@ -2,7 +2,7 @@
"""Commenting models."""
-from flask import g, url_for
+from flask import url_for
import time
import hashlib
import urllib
@@ -10,7 +10,7 @@ import random
import re
import sys
-import config
+from rophako.settings import Config
import rophako.jsondb as JsonDB
import rophako.model.user as User
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_email(
- to=config.NOTIFY_ADDRESS,
+ to=Config.site.notify_address,
subject="New comment: {}".format(subject),
message="""{name} has left a comment on: {subject}
@@ -230,7 +230,7 @@ def gravatar(email):
"""Generate a Gravatar link for an email address."""
if "@" in email:
# Default avatar?
- default = config.COMMENT_DEFAULT_AVATAR
+ default = Config.comment.default_avatar
# Construct the URL.
params = {
diff --git a/rophako/model/emoticons.py b/rophako/model/emoticons.py
index fcf7582..6f06675 100644
--- a/rophako/model/emoticons.py
+++ b/rophako/model/emoticons.py
@@ -2,14 +2,12 @@
"""Emoticon models."""
-from flask import g, url_for
import os
import codecs
import json
import re
-import config
-import rophako.jsondb as JsonDB
+from rophako.settings import Config
from rophako.log import logger
@@ -18,7 +16,7 @@ _cache = {}
def load_theme():
"""Pre-load and cache the emoticon theme. This happens on startup."""
- theme = config.EMOTICON_THEME
+ theme = Config.emoticons.theme
global _cache
# Cached?
@@ -26,13 +24,13 @@ def load_theme():
return _cache
# 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):
logger.error("Failed to load smiley theme {}: not found!")
# Try the default (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):
logger.info("Falling back to default theme: tango")
else:
@@ -71,11 +69,11 @@ def render(message):
if trigger in message:
# Substitute it.
sub = """""".format(
- url="/static/smileys/{}/{}".format(config.EMOTICON_THEME, img),
+ url="/static/smileys/{}/{}".format(Config.emoticons.theme, img),
trigger=trigger,
)
pattern = r'([^A-Za-z0-9:\-]|^){}([^A-Za-z0-9:\-]|$)'.format(re.escape(trigger))
result = r'\1{}\2'.format(sub)
message = re.sub(pattern, result, message)
- return message
\ No newline at end of file
+ return message
diff --git a/rophako/model/photo.py b/rophako/model/photo.py
index 33b53a5..271d45d 100644
--- a/rophako/model/photo.py
+++ b/rophako/model/photo.py
@@ -10,16 +10,16 @@ from PIL import Image
import hashlib
import random
-import config
+from rophako.settings import Config
import rophako.jsondb as JsonDB
from rophako.utils import sanitize_name, remote_addr
from rophako.log import logger
# Maps the friendly names of photo sizes with their pixel values from config.
PHOTO_SCALES = dict(
- large=config.PHOTO_WIDTH_LARGE,
- thumb=config.PHOTO_WIDTH_THUMB,
- avatar=config.PHOTO_WIDTH_AVATAR,
+ large=int(Config.photo.width_large),
+ thumb=int(Config.photo.width_thumb),
+ avatar=int(Config.photo.width_avatar),
)
@@ -57,7 +57,6 @@ def list_albums():
def get_album(name):
"""Get details about an album."""
index = get_index()
- result = []
if not name in index["albums"]:
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
album's name."""
old_name = sanitize_name(old_name)
- newname = sanitize_name(new_name)
+ new_name = sanitize_name(new_name)
index = get_index()
# New name is unique?
@@ -196,7 +195,7 @@ def get_photo(key):
def get_image_dimensions(pic):
"""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)
return img.size
@@ -233,11 +232,11 @@ def crop_photo(key, x, y, length):
for size in ["thumb", "avatar"]:
pic = index["albums"][album][key][size]
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.
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"]:
pic = resize_photo(source, size, crop=dict(
x=x,
@@ -305,7 +304,7 @@ def rotate_photo(key, rotate):
new_names = dict()
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))
# Give it a new name.
@@ -315,7 +314,7 @@ def rotate_photo(key, rotate):
img = Image.open(fname)
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.
os.unlink(fname)
@@ -339,7 +338,7 @@ def delete_photo(key):
# Delete all the images.
for size in ["large", "thumb", "avatar"]:
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):
os.unlink(fname)
@@ -428,7 +427,7 @@ def upload_from_pc(request):
if not allowed_filetype(upload.filename):
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))
upload.save(tempfile)
@@ -461,7 +460,7 @@ def upload_from_www(form):
# Make a temp filename for it.
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))
# Grab the file.
@@ -500,7 +499,7 @@ def process_photo(form, filename):
album = sanitize_name(album)
if album == "":
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.
key = random_hash()
@@ -574,7 +573,7 @@ def resize_photo(filename, size, crop=None):
# Make up a unique filename.
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))
# Get the image's dimensions.
@@ -679,7 +678,7 @@ def random_name(filetype):
"""Get a random available file name to save a new photo."""
filetype = filetype.lower()
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
return outfile
diff --git a/rophako/model/user.py b/rophako/model/user.py
index b90c481..c1ca67d 100644
--- a/rophako/model/user.py
+++ b/rophako/model/user.py
@@ -5,7 +5,7 @@
import bcrypt
import time
-import config
+from rophako.settings import Config
import rophako.jsondb as JsonDB
import rophako.model.photo as Photo
from rophako.log import logger
@@ -147,7 +147,7 @@ def exists(uid=None, username=None):
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):
@@ -172,4 +172,4 @@ def get_next_uid():
uid = 1
while exists(uid=uid):
uid += 1
- return uid
\ No newline at end of file
+ return uid
diff --git a/rophako/modules/blog/__init__.py b/rophako/modules/blog/__init__.py
index 71f19c9..06333da 100644
--- a/rophako/modules/blog/__init__.py
+++ b/rophako/modules/blog/__init__.py
@@ -14,8 +14,8 @@ import rophako.model.emoticons as Emoticons
from rophako.utils import (template, render_markdown, pretty_time,
login_required, remote_addr)
from rophako.plugin import load_plugin
+from rophako.settings import Config
from rophako.log import logger
-from config import *
mod = Blueprint("blog", __name__, url_prefix="/blog")
load_plugin("rophako.modules.comment")
@@ -34,16 +34,16 @@ def archive():
groups = dict()
friendly_months = dict()
for post_id, data in index.items():
- time = datetime.datetime.fromtimestamp(data["time"])
- date = time.strftime("%Y-%m")
+ ts = datetime.datetime.fromtimestamp(data["time"])
+ date = ts.strftime("%Y-%m")
if not date in groups:
groups[date] = dict()
- friendly = time.strftime("%B %Y")
+ friendly = ts.strftime("%B %Y")
friendly_months[date] = friendly
# Get author's profile && Pretty-print the time.
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
# Sort by calendar month.
@@ -101,10 +101,10 @@ def entry(fid):
# Get the author's information.
post["profile"] = User.get_user(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.
- 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
post["comment_count"] = Comment.count_comments("blog-{}".format(post_id))
@@ -153,9 +153,9 @@ def update():
format="markdown",
avatar="",
categories="",
- privacy=BLOG_DEFAULT_PRIVACY,
+ privacy=Config.blog.default_privacy,
emoticons=True,
- comments=BLOG_ALLOW_COMMENTS,
+ comments=Config.blog.allow_comments,
month="",
day="",
year="",
@@ -328,14 +328,14 @@ def rss():
today = time.strftime(rss_time, time.gmtime())
xml_add_text_tags(doc, channel, [
- ["title", RSS_TITLE],
- ["link", RSS_LINK],
- ["description", RSS_DESCRIPTION],
- ["language", RSS_LANGUAGE],
- ["copyright", RSS_COPYRIGHT],
+ ["title", Config.blog.title],
+ ["link", Config.blog.link],
+ ["description", Config.blog.description],
+ ["language", Config.blog.language],
+ ["copyright", Config.blog.copyright],
["pubDate", today],
["lastBuildDate", today],
- ["webmaster", RSS_WEBMASTER],
+ ["webmaster", Config.blog.webmaster],
])
######
@@ -345,12 +345,12 @@ def rss():
image = doc.createElement("image")
channel.appendChild(image)
xml_add_text_tags(doc, image, [
- ["title", RSS_IMAGE_TITLE],
- ["url", RSS_IMAGE_URL],
- ["link", RSS_LINK],
- ["width", RSS_IMAGE_WIDTH],
- ["height", RSS_IMAGE_HEIGHT],
- ["description", RSS_IMAGE_DESCRIPTION],
+ ["title", Config.blog.image_title],
+ ["url", Config.blog.image_url],
+ ["link", Config.blog.link],
+ ["width", Config.blog.image_width],
+ ["height", Config.blog.image_height],
+ ["description", Config.blog.image_description],
])
######
@@ -359,7 +359,7 @@ def rss():
index = Blog.get_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)
item = doc.createElement("item")
channel.appendChild(item)
@@ -432,8 +432,8 @@ def partial_index():
# Handle the offsets, and get those for the "older" and "earlier" posts.
# "earlier" posts count down (towards index 0), "older" counts up.
g.info["offset"] = offset
- g.info["earlier"] = offset - BLOG_ENTRIES_PER_PAGE if offset > 0 else 0
- g.info["older"] = offset + BLOG_ENTRIES_PER_PAGE
+ g.info["earlier"] = offset - int(Config.blog.entries_per_page) if offset > 0 else 0
+ g.info["older"] = offset + int(Config.blog.entries_per_page)
if g.info["earlier"] < 0:
g.info["earlier"] = 0
if g.info["older"] < 0 or g.info["older"] > len(posts):
@@ -446,7 +446,7 @@ def partial_index():
# Load the selected posts.
selected = []
- stop = offset + BLOG_ENTRIES_PER_PAGE
+ stop = offset + int(Config.blog.entries_per_page)
if stop > len(posts): stop = len(posts)
index = 1 # Let each post know its position on-page.
for i in range(offset, stop):
@@ -468,9 +468,9 @@ def partial_index():
# Get the author's information.
post["profile"] = User.get_user(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
post["comment_count"] = Comment.count_comments("blog-{}".format(post_id))
diff --git a/rophako/modules/comment/__init__.py b/rophako/modules/comment/__init__.py
index 1fd9079..99808d2 100644
--- a/rophako/modules/comment/__init__.py
+++ b/rophako/modules/comment/__init__.py
@@ -2,8 +2,7 @@
"""Endpoints for the commenting subsystem."""
-from flask import Blueprint, g, request, redirect, url_for, session, flash
-import re
+from flask import Blueprint, g, request, redirect, url_for, flash
import time
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,
remote_addr)
from rophako.plugin import load_plugin
-from rophako.log import logger
-from config import *
+from rophako.settings import Config
mod = Blueprint("comment", __name__, url_prefix="/comments")
load_plugin("rophako.modules.emoticons")
@@ -71,7 +69,7 @@ def preview():
# Gravatar.
g.info["gravatar"] = gravatar
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)
return template("comment/preview.html")
@@ -191,7 +189,7 @@ def partial_index(thread, subject, header=True):
comment["image"] = avatar
# 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.
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["url"] = request.url
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")
diff --git a/rophako/modules/contact/__init__.py b/rophako/modules/contact/__init__.py
index 6c37d56..12692d2 100644
--- a/rophako/modules/contact/__init__.py
+++ b/rophako/modules/contact/__init__.py
@@ -5,7 +5,7 @@
from flask import Blueprint, request, redirect, url_for, flash
from rophako.utils import template, send_email, remote_addr
-from config import *
+from rophako.settings import Config
mod = Blueprint("contact", __name__, url_prefix="/contact")
@@ -42,9 +42,9 @@ def send():
# Send the e-mail.
send_email(
- to=NOTIFY_ADDRESS,
+ to=Config.site.notify_address,
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!
IP Address: {ip}
@@ -55,7 +55,7 @@ E-mail: {email}
Subject: {subject}
{message}""".format(
- site_name=SITE_NAME,
+ site_name=Config.site.site_name,
ip=remote_addr(),
ua=request.user_agent.string,
referer=request.headers.get("Referer", ""),
diff --git a/rophako/modules/emoticons/__init__.py b/rophako/modules/emoticons/__init__.py
index 1ecccd4..6fa99ba 100644
--- a/rophako/modules/emoticons/__init__.py
+++ b/rophako/modules/emoticons/__init__.py
@@ -6,8 +6,7 @@ from flask import Blueprint, g
import rophako.model.emoticons as Emoticons
from rophako.utils import template
-from rophako.log import logger
-from config import *
+from rophako.settings import Config
mod = Blueprint("emoticons", __name__, url_prefix="/emoticons")
@@ -24,7 +23,7 @@ def index():
"triggers": theme["map"][img],
})
- g.info["theme"] = EMOTICON_THEME
+ g.info["theme"] = Config.emoticons.theme
g.info["theme_name"] = theme["name"]
g.info["smileys"] = smileys
- return template("emoticons/index.html")
\ No newline at end of file
+ return template("emoticons/index.html")
diff --git a/rophako/modules/photo/__init__.py b/rophako/modules/photo/__init__.py
index 08bd84b..6d05654 100644
--- a/rophako/modules/photo/__init__.py
+++ b/rophako/modules/photo/__init__.py
@@ -2,15 +2,14 @@
"""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.photo as Photo
from rophako.utils import (template, pretty_time, render_markdown,
login_required, ajax_response)
from rophako.plugin import load_plugin
-from rophako.log import logger
-from config import *
+from rophako.settings import Config
mod = Blueprint("photo", __name__, url_prefix="/photos")
load_plugin("rophako.modules.comment")
@@ -68,7 +67,7 @@ def view_photo(key):
g.info["photo"] = photo
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", ""))
return template("photos/view.html")
@@ -125,10 +124,10 @@ def upload():
g.info["album_list"] = [
"My Photos", # the default
]
- g.info["selected"] = PHOTO_DEFAULT_ALBUM
+ g.info["selected"] = Config.photo.default_album
albums = Photo.list_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]
return template("photos/upload.html")
diff --git a/rophako/plugin.py b/rophako/plugin.py
index a287606..71fa21d 100644
--- a/rophako/plugin.py
+++ b/rophako/plugin.py
@@ -26,4 +26,4 @@ def load_plugin(name, as_blueprint=True, template_path=None):
module_path = name.replace(".", "/")
template_path = os.path.join(module_path, "templates")
- BLUEPRINT_PATHS.append(template_path)
\ No newline at end of file
+ BLUEPRINT_PATHS.append(template_path)
diff --git a/rophako/settings.py b/rophako/settings.py
new file mode 100644
index 0000000..9d2d87f
--- /dev/null
+++ b/rophako/settings.py
@@ -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.., 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()
diff --git a/rophako/utils.py b/rophako/utils.py
index ab9b6fe..e74a808 100644
--- a/rophako/utils.py
+++ b/rophako/utils.py
@@ -14,7 +14,7 @@ import json
import urlparse
from rophako.log import logger
-from config import *
+from rophako.settings import Config
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):
"""Send an e-mail out."""
if sender is None:
- sender = MAIL_SENDER
+ sender = Config.mail.sender
if type(to) != list:
to = [to]
logger.info("Send email to {}".format(to))
- if MAIL_METHOD == "smtp":
+ if Config.mail.method == "smtp":
# Send mail with SMTP.
for email in to:
# Construct the mail headers.
@@ -186,7 +186,7 @@ def send_email(to, subject, message, sender=None, reply_to=None):
headers.append("Subject: {}".format(subject))
# 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
server.sendmail(sender, email, msg)
server.quit()