From ff75921129ecb815f97d56f4530592e464fbe4e8 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Thu, 4 Dec 2014 15:06:44 -0800 Subject: [PATCH] Move to new settings system --- .gitignore | 2 +- defaults.ini | 180 ++++++++++++++++++++++++++ kirsle_legacy.py | 4 +- requirements.txt | 2 +- rophako/app.py | 19 +-- rophako/jsondb.py | 17 +-- rophako/log.py | 6 +- rophako/model/blog.py | 6 +- rophako/model/comment.py | 8 +- rophako/model/emoticons.py | 14 +- rophako/model/photo.py | 33 +++-- rophako/model/user.py | 6 +- rophako/modules/blog/__init__.py | 54 ++++---- rophako/modules/comment/__init__.py | 12 +- rophako/modules/contact/__init__.py | 8 +- rophako/modules/emoticons/__init__.py | 7 +- rophako/modules/photo/__init__.py | 11 +- rophako/plugin.py | 2 +- rophako/settings.py | 60 +++++++++ rophako/utils.py | 8 +- 20 files changed, 348 insertions(+), 111 deletions(-) create mode 100644 defaults.ini create mode 100644 rophako/settings.py 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 = """{trigger}""".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()