pax_global_header00006660000000000000000000000064121760600460014514gustar00rootroot0000000000000052 comment=c3f638b13b42d2dbd174a4a229cd4ff54fbaab44 pyblosxom-1.5.3/000077500000000000000000000000001217606004600135565ustar00rootroot00000000000000pyblosxom-1.5.3/.gitignore000066400000000000000000000000761217606004600155510ustar00rootroot00000000000000build dist *.pyc pyblosxom.egg-info docs/_build *~ .gitignore pyblosxom-1.5.3/AUTHORS000066400000000000000000000020511217606004600146240ustar00rootroot00000000000000========= Authors ========= Pyblosxom was originally written by Wari Wahab and is now maintained by Akai Kitsune. Over the years, Pyblosxom has had many contributors who have helped make the project what it is today (in no particular order): * Will Kahn-Greene * Martin Kraft * Joerg Wendland * Enrico Zini * Gpal. V * Nathan Gray * Alexandre Patry * Brian Warner * Ryan Thiessen * Dewayne Christensen * Joe Gregorio * James Henstridge * Myers Carpenter * Scott C. * Axel Kollmorgen * Thenault Sylvain * Russell Nelson * IWS * Joseph Reagle * Tollef Fog Heen * Colin Walters * Norbert Tretkowski * David Stanek * FX * Zoom Quiet * Matej Cepl * Andrew Kuchling * Dieter Plaetinck * Jordi Mallach * Marius Gedminas * Mikko Värri * Robert Wall * Ryan Barrett * Sebastian Spaeth * Steven Armstrong * Ted Leung * Wari Wahab * Weakish Jakukyo * Weakish Jiang * Doug Ransom * Abe Fettig * Benjamin Mako Hill * David Pashley * David Geller * Roberto De Almeida * Antonio "Willy" Malara * Sean Whitton * Nicholas Tollervey Many thanks for all their work and efforts! pyblosxom-1.5.3/INSTALL000066400000000000000000000264531217606004600146210ustar00rootroot00000000000000.. _install: ====================== Installing Pyblosxom ====================== Installing Pyblosxom is pretty easy for most situations. There are three basic steps to get Pyblosxom running your blog: 1. Installing Pyblosxom Installs the Pyblosxom software onto the computer you intend to use it on. This is covered here. 2. Deploying Pyblosxom Sets up your blog on the web server you're going to use. Since there are many ways to deploy Pyblosxom, this is a separate step. This is covered in the deployment chapters. 3. Configure your blog to make it your own Adjust your ``config.py`` file, install and modify flavours, install and configure plugins, ... This is covered by the rest of the documentation. If the instructions here don't meet your needs, ask us on the pyblosxom-users mailing list or on IRC. Information for both is on the `website`_. .. _website: http://pyblosxom.github.com/ Upgrading ========= .. only:: text If you're upgrading from a previous version, read through this and then read the upgrade instructions in UPGRADE. .. only:: html If you're upgrading from a previous version, read through this and then read the upgrade instructions in :ref:`upgrading`. .. _requirements: Requirements ============ Hosting your blog ----------------- Pyblosxom works with any web server as a CGI application, a WSGI application, and might work in other contexts. If you run your blog in this way, then you'll install Pyblosxom on the web server you plan to host your blog. If you can't run CGI/WSGI processes on your web server, then you'll want to use Pyblosxom to compile your blog into HTML files which you can host on any web server. If you run your blog this way, then you'll install Pyblosxom on a desktop or laptop computer that has your blog files, you'll compile your blog on that computer, and then push the resulting HTML files to your web server which will host them. .. only:: text For more information on static rendering, see docs/deploy_staticrendering.rst after installing Pyblosxom. .. only:: html For more information on static rendering, see :ref:`static-rendering`. Operating System ---------------- Pyblosxom is well supported on GNU/Linux and Mac OSX. Pyblosxom is not supported on Windows and may not work well. If you decide to run your blog on a Windows server you will be pretty much on your own as nearly all the documentation and testing is GNU/Linux and Mac OSX centric. Having said that, if you run your blog using static rendering, you can serve the resulting HTML pages on any server you like, Windows included. Python ------ Pyblosxom requires at least Python 2.4. Pyblosxom does not work with Python 3 or higher---Python 3 is a significant rework of the language. Installation ============ First step is to install Pyblosxom. Like all Python programs, there are a few different ways to install Pyblosxom. We'll cover two we think are particularly useful here. .. Note:: .. only:: text If you're planning to install Pyblosxom to hack on it, then check out the install instructions in docs/hacking.rst. .. only:: html If you're planning to install Pyblosxom to hack on it, then check out the install instructions in :ref:`hacking-chapter`. 1. You can install Pyblosxom site-wide which allows all users on the computer to use Pyblosxom. In order to do this, you must have an administrative account on the computer you're installing it on. See :ref:`Installing site-wide` if this is what you want to do. 2. You can install Pyblosxom in a virtual environment using `virtualenv`_. If you do this, then only you will be able to use Pyblosxom. Use this method if any of the following are true: 1. you don't have administrative access to the computer you're going to run Pyblosxom on 2. you plan to use static rendering to compile your blog into HTML files and push those files to the web server 3. you're just trying out Pyblosxom and you want to install Pyblosxom in a way that doesn't affect your computer and is easily removed See :ref:`Installing in a virtual environment` if this is what you want to do. .. Note:: If your circumstances change, you can always reinstall Pyblosxom in a different way. .. _Installing site-wide: Installing site-wide -------------------- This walks through installing Pyblosxom site-wide which means it will be available to all users on the computer you're installing it on. In order to do this, you must have an administrative account on the computer you're installing it on. This requires: * Python 2.4 or higher * the Python package management tool `setuptools`_ and either one of the Python package installers `pip`_ (newer, better) or easy_install (ships with setuptools) * administrative access to the machine you're installing on To install: 1. If you have `pip`_ then do:: sudo pip install pyblosxom Or if you have `easy_install`_ then do:: sudo easy_install pyblosxom If you have neither `pip`_ nor `easy_install`_, then you should probably install one or the other to make your life with Python easier. That's it! You can move on to :ref:`Creating a blog` now. .. _easy_install: http://pypi.python.org/pypi/setuptools .. _setuptools: http://pypi.python.org/pypi/setuptools .. _pip: http://pypi.python.org/pypi/pip .. _Installing in a virtual environment: Installing in a virtual environment ----------------------------------- This walks through installing Pyblosxom into a `virtualenv`_ virtual environment. If you install Pyblosxom this way, then only you will be able to use Pyblosxom. Use this method if any of the following are true: 1. you don't have administrative access to the computer you're going to run Pyblosxom on 2. you plan to use static rendering to compile your blog into HTML files and push those files to the web server 3. you're just trying out Pyblosxom and you want to install Pyblosxom in a way that doesn't affect your computer and is easily removed This requires: * Python 2.4 or higher * a Python package installer: `pip`_ or `easy_install`_ * `virtualenv`_, the virtual Python environment builder .. _pip: http://pypi.python.org/pypi/pip .. _easy_install: http://pypi.python.org/pypi/setuptools .. _virtualenv: http://pypi.python.org/pypi/virtualenv To install: 1. Create a virtual environment for Pyblosxom in a directory of your choosing as denoted by ````:: virtualenv This command creates a new directory ```` that contains its own Python package installation directories and a Python interpreter that uses those directories. This is the virtual environment that Pyblosxom will be installed into and will run in. If you want to delete Pyblosxom at some point, just delete this virtual environment directory and all its contents. The virtual environment directory can be anywhere---it doesn't have to be in your blog directory and your blog directory doesn't have to be inside your virtual environment directory. For example, I have a lot of virtual environments set up because I fiddle with a lot of Python software. I have all my virtual environment directories under ``/home/willg/venvs``. I have my blog files in ``/home/willkg/blog``. 2. Activate the virtual environment in your current shell session:: source /bin/activate This command changes your current shell's ``$PATH`` environment variable to point to the ``/bin`` directory, so that commands such as ``python``, ``pip`` and ``easy_install`` will use the virtual environment instead of your default Python environment. It will also change your shell prompt to indicate that the virtual environment is active. If you exit your current shell session and start a new one, the virtual environment will no longer be active. You have to activate the virtual environment for each new shell session before doing anything with Pyblosxom. Additionally, if you're running Pyblosxom from CGI or a cron job, you want to use the ``python`` interpreter located in the ``bin`` directory of your virtual environment---not the system one. 3. Finally, install Pyblosxom into the activated virtual environment. If you have `pip`_ installed, then do:: pip install pyblosxom If you don't have `pip`_ installed, but have `easy_install`_, then do:: easy_install pyblosxom That's it! You can move on to :ref:`Creating a blog` now. .. _Creating a blog: Creating a blog =============== .. Note:: If you're using a virtual environment, make sure you've activated the virtual environment and are using the ``pyblosxom-cmd`` in the ``bin`` directory of your virtual environment! To create a blog, do:: pyblosxom-cmd create ```` can be any directory. For example:: pyblosxom-cmd create ./blog/ will generate a directory called ``blog`` in the directory I'm currently in and put a general blog structure in that directory including some required files and a first post. Deploying ========= We use the word "deploy" to cover the steps and setup required to turn your blog into a website. This could be any of the following: 1. static rendering where your blog is compiled into a set of HTML pages 2. running your blog as a CGI program 3. running your blog as a WSGI application 4. some fourth thing! The Pyblosxom documentation covers some of these deployment options. .. only:: text See docs/deploy_cgi.rst for deploying your blog as a CGI process. See docs/deploy_paste.rst for deploying your blog using Paste---this is useful for testing out Pyblosxom on a desktop or a laptop to see whether it'd be useful to you). See docs/deploy_staticrendering.rst for compiling your blog to HTML files that you would then copy to the web server. See docs/deploy_apache_mod_wsgi.rst for deploying your blog using Apache and mod_wsgi. .. only:: html See :ref:`deploy-cgi-chapter` for deploying your blog as a CGI process. See :ref:`deploy-paste-chapter` for deploying your blog using Paste---this is useful for testing out Pyblosxom on a desktop or a laptop to see whether it'd be useful to you). See :ref:`static-rendering` for compiling your blog to HTML files that you would then copy to the web server. See :ref:`deploy-apache-mod-wsgi` for deploying your blog using Apache and mod_wsgi. If you want to deploy your blog in a way that's not covered in the documentation, ask on the pyblosxom-users mailing list or on the ``#pyblosxom`` IRC channel on ``irc.freenode.net``. You can find details on the `website `_. After installing ================ After you finish installing and deploying Pyblosxom, you should look at plugins and flavours to make your blog your own. .. only:: text See the rest of the files in docs/ for more details. .. only:: html See the rest of :ref:`part-one` for more details. You should also sign up on the pyblosxom-users mailing list. Additionally, please hop on the ``#pyblosxom`` IRC channel on ``irc.freenode.net`` and say hi. It'll almost certainly help you get acquainted with Pyblosxom and it'll reduce the amount of time it takes to get your blog up and going. Details are on the `website `_. pyblosxom-1.5.3/LICENSE000066400000000000000000000024561217606004600145720ustar00rootroot00000000000000========= License ========= Pyblosxom codebase ================== The MIT License (http://www.opensource.org/licenses/mit-license.php) Copyright (c) 2003-2011 by the Pyblosxom team (see AUTHORS file). Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Core plugins ============ See headers in plugin code files for license/copyright information. pyblosxom-1.5.3/MANIFEST.in000066400000000000000000000004641217606004600153200ustar00rootroot00000000000000include WHATSNEW include README.rst include INSTALL include LICENSE include AUTHORS include UPGRADE include setup.py recursive-include bin * recursive-include Pyblosxom * recursive-include Pyblosxom/data * recursive-include Pyblosxom/flavours * recursive-include Pyblosxom/tests * recursive-include docs * pyblosxom-1.5.3/Pyblosxom/000077500000000000000000000000001217606004600155525ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/__init__.py000066400000000000000000000006521217606004600176660ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ This package holds Pyblosxom modules. """ from _version import __version__ pyblosxom-1.5.3/Pyblosxom/_version.py000066400000000000000000000010731217606004600177510ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### # valid version formats: # * x.y - final release # * x.ya1 - alpha 1 # * x.yb1 - beta 1 # * x.yrc1 - release candidate 1 # * x.y.dev - dev # see http://www.python.org/dev/peps/pep-0386/ __version__ = "1.5.3" pyblosxom-1.5.3/Pyblosxom/cache/000077500000000000000000000000001217606004600166155ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/cache/__init__.py000066400000000000000000000017521217606004600207330ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Pyblosxom works by pulling data from the file system, passing it through a series of filters and transformations, and then rendering it. Some of these steps are somewhat intensive and to alleviate this, we cache things. For example, the entries.base.BaseEntry class allows entries to be cached after they've been pulled from the file system and passed through a series of filters/formatters. There are several cache mechanisms. Read the documentation for each to understand how they work and how to set them up. Additional caching mechanisms can be dropped in this directory and used by setting the "cacheDriver" item in the config dict. """ pass pyblosxom-1.5.3/Pyblosxom/cache/base.py000066400000000000000000000075461217606004600201150ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ The cache base class. Subclasses of this class provide caching for blog entry data in Pyblosxom. """ class BlosxomCacheBase: """ Base Class for Caching stories in pyblosxom. A cache is a disposable piece of data that gets updated when an entry is in a fresh state. Drivers are to subclass this object, overriding methods defined in this class. If there is an error in creating cache data, be as quite as possible, document how a user could check whether his cache works. Driver should expect empty caches and should attempt to create them from scratch. @ivar _config: String containing config on where to store the cache. The value of config is derived from C{py['cacheConfig']} in config.py. @type _config: string """ def __init__(self, req, config): """ Constructor - setup and load up the cache @param req: the request object @type req: Request @param config: String containing config on where to store the cache @type config: string """ self._request = req self._config = config self._entryid = "" self._entrydata = {} def load(self, entryid): """ Try to load up the cache with entryid (a unique key for the entry) @param entryid: The key identifier for your cache @type entryid: string """ self._entryid = entryid # The filename of the entry self._entrydata = {} # The data of the entry def getEntry(self): """ Gets the data from the cache, returns a dict or an empty dict. """ return self._entrydata def isCached(self): """ Returns 0 or 1 based on whether there is cached data, returns 0 is cache data is stale @returns: 0 or 1 based on cache @rtype: boolean """ return 0 def saveEntry(self, entrydata): """ Store entrydata in cache @param entrydata: The payload, usually a dict @type entrydata: dict """ pass def rmEntry(self): """ Remove cache entry: This is not used by pyblosxom, but used by utilities. """ pass def close(self): """ Override this to close your cache if necessary. """ pass def __getitem__(self, key): """ Convenience function to make this class look like a dict. """ self.load(key) if not self.has_key(key): raise KeyError return self.getEntry() def __setitem__(self, key, value): """ Synonymous to L{saveEntry} """ self.load(key) self.saveEntry(value) def __delitem__(self, key): """ Convenience function to make this look more like a dict. """ self.load(key) self.rmEntry() def has_key(self, key): """ Convenience function to make this look more like a dict. """ self.load(key) return self.isCached() def keys(self): """ List out a list of keys for the cache, to be overridden by a subclass if a full dict interface is required. """ return [] def get(self, key, default=None): """ Convenience function to make this look more like a dict. """ try: return self.__getitem__(key) except KeyError: return default class BlosxomCache(BlosxomCacheBase): """ A null cache. """ pass pyblosxom-1.5.3/Pyblosxom/cache/entrypickle.py000066400000000000000000000103121217606004600215150ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ This cache driver creates pickled data as cache in a directory. To use this driver, add the following configuration options in your config.py py['cacheDriver'] = 'entrypickle' py['cacheConfig'] = '/path/to/a/cache/directory' If successful, you will see the cache directory filled up with files that ends with .entryplugin extention in the drectory. """ from Pyblosxom import tools from Pyblosxom.cache.base import BlosxomCacheBase import cPickle as pickle import os from os import makedirs from os.path import normpath, dirname, exists, abspath class BlosxomCache(BlosxomCacheBase): """ This cache stores each entry as a separate pickle file of the entry's contents. """ def __init__(self, req, config): """ Takes in a Pyblosxom request object and a configuration string which determines where to store the pickle files. """ BlosxomCacheBase.__init__(self, req, config) self._cachefile = "" def load(self, entryid): """ Takes an entryid and keeps track of the filename. We only open the file when it's requested with getEntry. """ BlosxomCacheBase.load(self, entryid) filename = os.path.join(self._config, entryid.replace('/', '_')) self._cachefile = filename + '.entrypickle' def getEntry(self): """ Open the pickle file and return the data therein. If this fails, then we return None. """ filep = None try: filep = open(self._cachefile, 'rb') data = pickle.load(filep) filep.close() return data except IOError: return None if filep: filep.close() def isCached(self): """ Check to see if the file is updated. """ return os.path.isfile(self._cachefile) and \ os.stat(self._cachefile)[8] >= os.stat(self._entryid)[8] def saveEntry(self, entrydata): """ Save the data in the entry object to a pickle file. """ filep = None try: self.__makepath(self._cachefile) filep = open(self._cachefile, "w+b") entrydata.update({'realfilename': self._entryid}) pickle.dump(entrydata, filep, 1) except IOError: pass if filep: filep.close() def rmEntry(self): """ Removes the pickle file for this entry if it exists. """ if os.path.isfile(self._cachefile): os.remove(self._cachefile) def keys(self): """ Returns a list of the keys found in this entrypickle instance. This corresponds to the list of entries that are cached. @returns: list of full paths to entries that are cached @rtype: list of strings """ import re keys = [] cached = [] if os.path.isdir(self._config): cached = tools.walk(self._request, self._config, 1, re.compile(r'.*\.entrypickle$')) for cache in cached: cache_data = pickle.load(open(cache)) key = cache_data.get('realfilename', '') if not key and os.path.isfile(cache): os.remove(cache) self.load(key) if not self.isCached(): self.rmEntry() else: keys.append(key) return keys def __makepath(self, path): """ Creates the directory and all parent directories for a specified path. @param path: the path to create @type path: string @returns: the normalized absolute path @rtype: string """ dpath = normpath(dirname(path)) if not exists(dpath): makedirs(dpath) return normpath(abspath(path)) pyblosxom-1.5.3/Pyblosxom/cache/entryshelve.py000066400000000000000000000053651217606004600215500ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ This cache driver creates shelved data as cache in a dbm file. To use this driver, add the following configuration options in your config.py py['cacheDriver'] = 'entryshelve' py['cacheConfig'] = '/path/to/a/cache/dbm/file' If successful, you will see the cache file. Be sure that you have write access to the cache file. """ from Pyblosxom.cache.base import BlosxomCacheBase import shelve import os class BlosxomCache(BlosxomCacheBase): """ This stores entries in shelves in a .dbm file. """ def __init__(self, req, config): """ Initializes BlosxomCacheBase.__init__ and also opens the shelf file. """ BlosxomCacheBase.__init__(self, req, config) self._db = shelve.open(self._config) def load(self, entryid): """ Loads a specific entryid. """ BlosxomCacheBase.load(self, entryid) def getEntry(self): """ Get an entry from the shelf. """ data = self._db.get(self._entryid, {}) return data.get('entrydata', {}) def isCached(self): """ Returns true if the entry is cached and the cached version is not stale. Returns false otherwise. """ data = self._db.get(self._entryid, {'mtime':0}) if os.path.isfile(self._entryid): return data['mtime'] == os.stat(self._entryid)[8] else: return None def saveEntry(self, entrydata): """ Save data in the pickled file. """ payload = {} payload['mtime'] = os.stat(self._entryid)[8] payload['entrydata'] = entrydata self._db[self._entryid] = payload def rmEntry(self): """ Removes an entry from the shelf. """ if self._db.has_key(self._entryid): del self._db[self._entryid] def keys(self): """ Returns a list of entries that are cached in the shelf. @returns: list of entry paths @rtype: list of strings """ ret = [] for key in self._db.keys(): self.load(key) if self.isCached(): ret.append(key) else: # Remove this key, why is it there in the first place? del self._db[self._entryid] return ret def close(self): """ Closes the db file. """ self._db.close() self._db = None pyblosxom-1.5.3/Pyblosxom/commandline.py000066400000000000000000000430661217606004600204230ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2008-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ This module holds commandline related stuff. Installation verification, blog creation, commandline argument parsing, ... """ import os import os.path import sys import random import time from optparse import OptionParser from Pyblosxom import __version__ from Pyblosxom.pyblosxom import Pyblosxom from Pyblosxom.tools import run_callback, pwrap, pwrap_error from Pyblosxom import plugin_utils USAGE = "%prog [options] [command] [command-options]" VERSION = "%prog " + __version__ def build_pyblosxom(): """Imports config.py and builds an empty Pyblosxom object. """ pwrap("Trying to import the config module....") try: from config import py as cfg except StandardError: h, t = os.path.split(sys.argv[0]) scriptname = t or h pwrap_error("ERROR: Cannot find your config.py file. Please execute " "%s in the directory with the config.py file in it or use " "the --config flag.\n\n" "See \"%s --help\" for more details." % (scriptname, scriptname)) return None return Pyblosxom(cfg, {}) def build_parser(usage): parser = OptionParser(usage=usage, version=VERSION) parser.add_option("-q", "--quiet", action="store_false", dest="verbose", default=True, help="If the quiet flag is specified, then Pyblosxom " "will run quietly.") parser.add_option("--config", help="This specifies the directory that the config.py " "for the blog you want to work with is in. If the " "config.py file is in the current directory, then " "you don't need to specify this. All commands except " "the 'create' command need a config.py file.") return parser def generate_entries(command, argv): """ This function is primarily for testing purposes. It creates a bunch of blog entries with random text in them. """ parser = build_parser("%prog entries [options] ") (options, args) = parser.parse_args() if args: try: num_entries = int(args[0]) assert num_entries > 0 except ValueError: pwrap_error("ERROR: num_entries must be a positive integer.") return 0 else: num_entries = 5 verbose = options.verbose p = build_pyblosxom() if not p: return 0 datadir = p.get_request().config["datadir"] sm_para = "

Lorem ipsum dolor sit amet.

" med_para = """

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus in mi lacus, sed interdum nisi. Vestibulum commodo urna et libero vestibulum gravida. Vivamus hendrerit justo quis lorem auctor consectetur. Aenean ornare, tortor in sollicitudin imperdiet, neque diam pellentesque risus, vitae.

""" lg_para = """

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris dictum tortor orci. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam quis lectus vel odio convallis tincidunt sed et magna. Suspendisse at dolor suscipit eros ullamcorper iaculis. In aliquet ornare libero eget rhoncus. Sed ac ipsum eget eros fringilla aliquet ut eget velit. Curabitur dui nibh, eleifend non suscipit at, laoreet ac purus. Morbi id sem diam. Cras sit amet ante lacus, nec euismod urna. Curabitur iaculis, lorem at fringilla malesuada, nunc ligula eleifend nisi, at bibendum libero est quis tellus. Pellentesque habitant morbi tristique senectus et netus et malesuada.

""" paras = [sm_para, med_para, lg_para] if verbose: print "Creating %d entries" % num_entries now = time.time() for i in range(num_entries): title = "post number %d\n" % (i + 1) body = [] for _ in range(random.randrange(1, 6)): body.append(random.choice(paras)) fn = os.path.join(datadir, "post%d.txt" % (i + 1)) f = open(fn, "w") f.write(title) f.write("\n".join(body)) f.close() mtime = now - ((num_entries - i) * 3600) os.utime(fn, (mtime, mtime)) if verbose: print "Creating '%s'..." % fn if verbose: print "Done!" return 0 def test_installation(command, argv): """ This function gets called when someone starts up pyblosxom.cgi from the command line with no REQUEST_METHOD environment variable. It: 1. verifies config.py file properties 2. initializes all the plugins they have installed 3. runs ``cb_verify_installation``--plugins can print out whether they are installed correctly (i.e. have valid config property settings and can read/write to data files) The goal is to be as useful and informative to the user as we can be without being overly verbose and confusing. This is designed to make it easier for a user to verify their Pyblosxom installation is working and also to install new plugins and verify that their configuration is correct. """ parser = build_parser("%prog test [options]") parser.parse_args() p = build_pyblosxom() if not p: return 0 request = p.get_request() config = request.config pwrap("System Information") pwrap("==================") pwrap("") pwrap("- pyblosxom: %s" % __version__) pwrap("- sys.version: %s" % sys.version.replace("\n", " ")) pwrap("- os.name: %s" % os.name) codebase = os.path.dirname(os.path.dirname(__file__)) pwrap("- codebase: %s" % config.get("codebase", codebase)) pwrap("") pwrap("Checking config.py file") pwrap("=======================") pwrap("- properties set: %s" % len(config)) config_keys = config.keys() if "datadir" not in config_keys: pwrap_error("- ERROR: 'datadir' must be set. Refer to installation " "documentation.") elif not os.path.isdir(config["datadir"]): pwrap_error("- ERROR: datadir '%s' does not exist." " You need to create your datadir and give it " " appropriate permissions." % config["datadir"]) else: pwrap("- datadir '%s' exists." % config["datadir"]) if "flavourdir" not in config_keys: pwrap("- WARNING: You should consider setting flavourdir and putting " "your flavour templates there. See the documentation for " "more details.") elif not os.path.isdir(config["flavourdir"]): pwrap_error("- ERROR: flavourdir '%s' does not exist." " You need to create your flavourdir and give it " " appropriate permissions." % config["flavourdir"]) else: pwrap("- flavourdir '%s' exists." % config["flavourdir"]) if (("blog_encoding" in config_keys and config["blog_encoding"].lower() != "utf-8")): pwrap_error("- WARNING: 'blog_encoding' is set to something other " "than 'utf-8'. As of Pyblosxom 1.5, " "this isn't a good idea unless you're absolutely certain " "it's going to work for your blog.") pwrap("") pwrap("Checking plugin configuration") pwrap("=============================") import traceback no_verification_support = [] if len(plugin_utils.plugins) + len(plugin_utils.bad_plugins) == 0: pwrap(" - There are no plugins installed.") else: if len(plugin_utils.bad_plugins) > 0: pwrap("- Some plugins failed to load.") pwrap("") pwrap("----") for mem in plugin_utils.bad_plugins: pwrap("plugin: %s" % mem[0]) print "%s" % mem[1] pwrap("----") pwrap_error("FAIL") return(1) if len(plugin_utils.plugins) > 0: pwrap("- This goes through your plugins and asks each of them " "to verify configuration and installation.") pwrap("") pwrap("----") for mem in plugin_utils.plugins: if hasattr(mem, "verify_installation"): pwrap("plugin: %s" % mem.__name__) print "file: %s" % mem.__file__ print "version: %s" % (str(getattr(mem, "__version__"))) try: if mem.verify_installation(request) == 1: pwrap("PASS") else: pwrap_error("FAIL") except StandardError: pwrap_error("FAIL: Exception thrown:") traceback.print_exc(file=sys.stdout) pwrap("----") else: mn = mem.__name__ mf = mem.__file__ no_verification_support.append( "'%s' (%s)" % (mn, mf)) if len(no_verification_support) > 0: pwrap("") pwrap("The following plugins do not support installation " "verification:") no_verification_support.sort() for mem in no_verification_support: print "- %s" % mem pwrap("") pwrap("Verification complete. Correct any errors and warnings above.") def create_blog(command, argv): """ Creates a blog in the specified directory. Mostly this involves copying things over, but there are a few cases where we expand template variables. """ parser = build_parser("%prog create [options] ") (options, args) = parser.parse_args() if args: d = args[0] else: d = "." if d == ".": d = "." + os.sep + "blog" d = os.path.abspath(d) verbose = options.verbose if os.path.isfile(d) or os.path.isdir(d): pwrap_error("ERROR: Cannot create '%s'--something is in the way." % d) return 0 def _mkdir(d): if verbose: print "Creating '%s'..." % d os.makedirs(d) _mkdir(d) _mkdir(os.path.join(d, "entries")) _mkdir(os.path.join(d, "plugins")) source = os.path.join(os.path.dirname(__file__), "flavours") for root, dirs, files in os.walk(source): if ".svn" in root: continue dest = os.path.join(d, "flavours", root[len(source)+1:]) if not os.path.isdir(dest): if verbose: print "Creating '%s'..." % dest os.mkdir(dest) for mem in files: if verbose: print "Creating file '%s'..." % os.path.join(dest, mem) fpin = open(os.path.join(root, mem), "r") fpout = open(os.path.join(dest, mem), "w") fpout.write(fpin.read()) fpout.close() fpin.close() def _copyfile(frompath, topath, fn, fix=False): if verbose: print "Creating file '%s'..." % os.path.join(topath, fn) fp = open(os.path.join(frompath, fn), "r") filedata = fp.readlines() fp.close() if fix: basedir = topath if not basedir.endswith(os.sep): basedir = basedir + os.sep if os.sep == "\\": basedir = basedir.replace(os.sep, os.sep + os.sep) datamap = { "basedir": basedir, "codedir": os.path.dirname(os.path.dirname(__file__)) } filedata = [line % datamap for line in filedata] fp = open(os.path.join(topath, fn), "w") fp.write("".join(filedata)) fp.close() source = os.path.join(os.path.dirname(__file__), "data") _copyfile(source, d, "config.py", fix=True) _copyfile(source, d, "blog.ini", fix=True) _copyfile(source, d, "pyblosxom.cgi", fix=True) datadir = os.path.join(d, "entries") firstpost = os.path.join(datadir, "firstpost.txt") if verbose: print "Creating file '%s'..." % firstpost fp = open(firstpost, "w") fp.write("""First post!

This is your first post! If you can see this with a web-browser, then it's likely that everything's working nicely!

""") fp.close() if verbose: print "Done!" return 0 def render_url(command, argv): """Renders a single url. """ parser = build_parser("%prog renderurl [options] [...]") parser.add_option("--headers", action="store_true", dest="headers", default=False, help="Option that causes headers to be displayed " "when rendering a single url.") (options, args) = parser.parse_args() if not args: parser.print_help() return 0 for url in args: p = build_pyblosxom() base_url = p.get_request().config.get("base_url", "") if url.startswith(base_url): url = url[len(base_url):] p.run_render_one(url, options.headers) return 0 def run_static_renderer(command, argv): parser = build_parser("%prog staticrender [options]") parser.add_option("--incremental", action="store_true", dest="incremental", default=False, help="Option that causes static rendering to be " "incremental.") (options, args) = parser.parse_args() # Turn on memcache. from Pyblosxom import memcache memcache.usecache = True p = build_pyblosxom() if not p: return 0 return p.run_static_renderer(options.incremental) DEFAULT_HANDLERS = ( ("create", create_blog, "Creates directory structure for a new blog."), ("test", test_installation, "Tests installation and configuration for a blog."), ("staticrender", run_static_renderer, "Statically renders your blog into an HTML site."), ("renderurl", render_url, "Renders a single url of your blog."), ("generate", generate_entries, "Generates random entries--helps " "with blog setup.") ) def get_handlers(): try: from config import py as cfg plugin_utils.initialize_plugins(cfg.get("plugin_dirs", []), cfg.get("load_plugins", None)) except ImportError: pass handlers_dict = dict([(v[0], (v[1], v[2])) for v in DEFAULT_HANDLERS]) handlers_dict = run_callback("commandline", handlers_dict, mappingfunc=lambda x, y: y, defaultfunc=lambda x: x) # test the handlers, drop any that aren't the right return type, # and print a warning. handlers = [] for k, v in handlers_dict.items(): if not len(v) == 2 or not callable(v[0]) or not isinstance(v[1], str): print "Plugin returned '%s' for commandline." % ((k, v),) continue handlers.append((k, v[0], v[1])) return handlers def command_line_handler(scriptname, argv): if "--silent" in argv: sys.stdout = open(os.devnull, "w") argv.remove("--silent") print "%s version %s" % (scriptname, __version__) # slurp off the config file setting and add it to sys.path. # this needs to be first to pick up plugin-based command handlers. configdir = None for i, mem in enumerate(argv): if mem.startswith("--config"): if "=" in mem: _, configdir = mem.split("=") break else: try: configdir = argv[i+1] break except IndexError: pwrap_error("Error: no config file argument specified.") pwrap_error("Exiting.") return 1 if configdir is not None: if configdir.endswith("config.py"): configdir = configdir[0:-9] if not os.path.exists(configdir): pwrap_error("ERROR: '%s' does not exist--cannot find config.py " "file." % configdir) pwrap_error("Exiting.") return 1 if not "config.py" in os.listdir(configdir): pwrap_error("Error: config.py not in '%s'. " "Cannot find config.py file." % configdir) pwrap_error("Exiting.") return 1 sys.path.insert(0, configdir) print "Inserting %s to beginning of sys.path...." % configdir handlers = get_handlers() if len(argv) == 1 or (len(argv) == 2 and argv[1] in ("-h", "--help")): parser = build_parser("%prog [command]") parser.print_help() print "" print "Commands:" for command_str, _, command_help in handlers: print " %-14s %s" % (command_str, command_help) return 0 if argv[1] == "--version": return 0 # then we execute the named command with options, or print help if argv[1].startswith("-"): pwrap_error("Command '%s' does not exist." % argv[1]) pwrap_error('') pwrap_error("Commands:") for command_str, _, command_help in handlers: pwrap_error ( " %-14s %s" % (command_str, command_help)) return 1 command = argv.pop(1) for (c, f, h) in handlers: if c == command: return f(command, argv) pwrap_error("Command '%s' does not exist." % command) for command_str, command_func, command_help in handlers: pwrap_error(" %-14s %s" % (command_str, command_help)) return 1 pyblosxom-1.5.3/Pyblosxom/crashhandling.py000066400000000000000000000110011217606004600207220ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ This module has the code for handling crashes. .. Note:: This is a leaf node module! It should never import other Pyblosxom modules or packages. """ import sys import StringIO import cgi import traceback _e = cgi.escape class Response: """This is a minimal response that is returned by the crash handler. """ def __init__(self, status, headers, body): self.status = status self.headers = headers self.body = body self.seek = body.seek self.read = body.read class CrashHandler: def __init__(self, httpresponse=False, environ=None): """ :param httpresponse: boolean representing whether when handling a crash, we do http response headers """ self.httpresponse = httpresponse if environ: self.environ = environ else: self.environ = {} def __call__(self, exc_type, exc_value, exc_tb): response = self.handle(exc_type, exc_value, exc_tb) if self.httpresponse: response.headers.append( "Content-Length: %d" % httpresponse.body.len) sys.output.write("HTTP/1.0 %s\n" % response.status) for key, val in response.headers.items(): sys.output.write("%s: %s\n" % (key, val)) sys.output.write("\n") sys.output.write(response.body.read()) sys.output.flush() def handle_by_response(self, exc_type, exc_value, exc_tb): """Returns a basic response object holding crash information for display. """ headers = {} output = StringIO.StringIO() headers["Content-Type"] = "text/html" # FIXME - are there other userful headers? output.write("") output.write("HTTP 500: Oops!") output.write("") output.write("

HTTP 500: Oops!

") output.write( "

A problem has occurred while Pyblosxom was rendering " "this page.

") output.write( "

If this is your blog and you've just upgraded Pyblosxom, " "check the manual for changes you need to make to your " "config.py, pyblosxom.cgi, blog.ini, plugins, and flavour " "files. This is usually covered in the Upgrade and What's New " "chapters.

\n" "

If you need help, contact us on IRC or the pyblosxom-users " "mailing list.

\n" "

The manual and details on IRC and the pyblosxom-users " "mailing list are all on the " "website.

") output.write("

Here is some useful information to track down " "the root cause of the problem:

") output.write("
") try: import Pyblosxom version = Pyblosxom.__version__ except: version = "unknown" output.write("

Pyblosxom version: %s

" % _e(version)) output.write("

Python version: %s" % _e(sys.version)) output.write("

Error traceback:

") output.write("
")
        tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
        output.write(_e(tb))
        output.write("
") output.write("

HTTP environment:

") output.write("
")
        for key, val in self.environ.items():
            output.write("%s: %s\n" % (_e(repr(key)), _e(repr(val))))
        output.write("
") output.write("
") output.write("") output.write("") headers["Content-Length"] = str(output.len) return Response("500 Server Error", headers, output) def enable_excepthook(httpresponse=False): """This attaches the crashhandler to the sys.excepthook. This will handle any exceptions thrown that don't get handled anywhere else. If you're running Pyblosxom as a WSGI application or as a CGI script, you should create a ``CrashHandler`` instance and call ``handle_by_response`` directly. See :ref:`pyblosxom.PyblosxomWSGIApp`. """ sys.excepthook = CrashHandler(httpresponse=httpresponse) pyblosxom-1.5.3/Pyblosxom/data/000077500000000000000000000000001217606004600164635ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/data/blog.ini000066400000000000000000000014771217606004600201200ustar00rootroot00000000000000# # Pyblosxom blog.ini file for Paste. # [DEFAULT] # Set this to false for production environments debug = True [server:main] # These next three lines define how Paste will serve this blog. # Refer to the Paste documentation for more details: # http://pythonpaste.org/deploy/ use = egg:Paste#http host = 0.0.0.0 port = 5000 [app:main] paste.app_factory = Pyblosxom.pyblosxom:pyblosxom_app_factory # This is the directory your config.py file is in. configpydir = %(basedir)s # Pyblosxom config properties can be set after this point if you # like. Or you can set them in the config.py file. Setting things # here overrides what's in your config.py file. This makes it easier # to have a development and a production blog--you just have a .ini # file for each one. # Example of setting a property: # blog_title = Joe's Blog pyblosxom-1.5.3/Pyblosxom/data/config.py000066400000000000000000000150511217606004600203040ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ================================================================= # This is the config file for Pyblosxom. You should go through # this file and fill in values for the various properties. This # affects the behavior of your blog. # # This is a Python code file and as such must be written in # Python. # # There are configuration properties that are not detailed in # this file. These are the properties that are most often used. # To see a full list of configuration properties as well as # additional documentation, see the Pyblosxom documentation on # the web-site for your version of Pyblosxom. # ================================================================= # Don't touch this next line. py = {} # Codebase configuration # ====================== # If you did not install Pyblosxom as a library (i.e. python setup.py install) # then uncomment this next line and point it to your Pyblosxom installation # directory. # # Note, this should be the parent directory of the "Pyblosxom" directory # (note the case--uppercase P lowercase b!). #py["codebase"] = "%(codedir)s" import os blogdir = "%(basedir)s" # Blog configuration # ================== # What is the title of this blog? py["blog_title"] = "Another pyblosxom blog" # What is the description of this blog? py["blog_description"] = "blosxom with a touch of python" # Who are the author(s) of this blog? py["blog_author"] = "name" # What is the email address through which readers of the blog may contact # the authors? py["blog_email"] = "email@example.com" # These are the rights you give to others in regards to the content # on your blog. Generally, this is the copyright information. # This is used in the Atom feeds. Leaving this blank or not filling # it in correctly could result in a feed that doesn't validate. py["blog_rights"] = "Copyright 2005 Joe Bobb" # What is this blog's primary language (for outgoing RSS feed)? py["blog_language"] = "en" # Encoding for output. This defaults to utf-8. py["blog_encoding"] = "utf-8" # What is the locale for this blog? This is used when formatting dates # and other locale-sensitive things. Make sure the locale is valid for # your system. See the configuration chapter in the Pyblosxom documentation # for details. #py["locale"] = "en_US.iso-8859-1" # Where are this blog's entries kept? py["datadir"] = os.path.join(blogdir, "entries") # Where are this blog's flavours kept? py["flavourdir"] = os.path.join(blogdir, "flavours") # List of strings with directories that should be ignored (e.g. "CVS") # ex: py['ignore_directories'] = ["CVS", "temp"] py["ignore_directories"] = [] # Should I stick only to the datadir for items or travel down the directory # hierarchy looking for items? If so, to what depth? # 0 = infinite depth (aka grab everything) # 1 = datadir only # n = n levels down py["depth"] = 0 # How many entries should I show on the home page and category pages? # If you put 0 here, then I will show all pages. # Note: this doesn't affect date-based archive pages. py["num_entries"] = 5 # What is the default flavour you want to use when the user doesn't # specify a flavour in the request? py["default_flavour"] = "html" # Logging configuration # ===================== # Where should Pyblosxom write logged messages to? # If set to "NONE" log messages are silently ignored. # Falls back to sys.stderr if the file can't be opened for writing. #py["log_file"] = os.path.join(blogdir, "logs", "pyblosxom.log") # At what level should we log to log_file? # One of: "critical", "error", "warning", "info", "debug" # For production, "warning" or "error' is recommended. #py["log_level"] = "warning" # This lets you specify which channels should be logged. # If specified, only messages from the listed channels are logged. # Each plugin logs to it's own channel, therefor channelname == pluginname. # Application level messages are logged to a channel named "root". # If you use log_filter and ommit the "root" channel here, app level messages # are not logged! log_filter is mainly interesting to debug a specific plugin. #py["log_filter"] = ["root", "plugin1", "plugin2"] # Plugin configuration # ==================== # Plugin directories: # This allows you to specify which directories have plugins that you # want to load. You can list as many plugin directories as you # want. # Example: py['plugin_dirs'] = ["/home/joe/blog/plugins", # "/var/lib/pyblosxom/plugins"] py["plugin_dirs"] = [os.path.join(blogdir, "plugins")] # There are two ways for Pyblosxom to load plugins: # # The first is the default way where Pyblosxom loads all plugins it # finds in the directories specified by "plugins_dir" in alphanumeric # order by filename. # # The second is by specifying a "load_plugins" key here. Specifying # "load_plugins" will cause Pyblosxom to load only the plugins you name # and in in the order you name them. # # The "load_plugins" key is a list of strings where each string is # the name of a plugin module (i.e. the filename without the .py at # the end). # # If you specify an empty list, then this will load no plugins. # ex: py["load_plugins"] = ["pycalendar", "pyfortune", "pyarchives"] py["load_plugins"] = [] # ====================== # Optional Configuration # ====================== # What should this blog use as its base url? #py["base_url"] = "http://www.example.com/weblog" # Default parser/preformatter. Defaults to plain (does nothing) #py["parser"] = "plain" # Static rendering # ================ # Doing static rendering? Static rendering essentially "compiles" your # blog into a series of static html pages. For more details, see the # documentation. # # What directory do you want your static html pages to go into? #py["static_dir"] = "/path/to/static/dir" # What flavours should get generated? #py["static_flavours"] = ["html"] # What other paths should we statically render? # This is for additional urls handled by other plugins like the booklist # and plugin_info plugins. If there are multiple flavours you want # to capture, specify each: # ex: py["static_urls"] = ["/booklist.rss", "/booklist.html"] #py["static_urls"] = ["/path/to/url1", "/path/to/url2"] # Whether (True) or not (False) you want to generate date indexes with month # names? (ex. /2004/Apr/01) Defaults to True. #py["static_monthnames"] = True # Whether (True) or not (False) you want to generate date indexes # using month numbers? (ex. /2004/04/01) Defaults to False. #py["static_monthnumbers"] = False # Whether (True) or not (False) you want to generate year indexes? # (ex. /2004) Defaults to True. #py["static_yearindexes"] = True pyblosxom-1.5.3/Pyblosxom/data/pyblosxom.cgi000077500000000000000000000024451217606004600212130ustar00rootroot00000000000000#!/usr/bin/env python # -u turns off character translation to allow transmission # of gzip compressed content on Windows and OS/2 #!/path/to/python -u import os, sys # Uncomment this line to add the directory your config.py file is in to the # python path: #sys.path.append("%(basedir)s") # ------------------------------------------------------- # You shouldn't have to adjust anything below this point. # ------------------------------------------------------- # this allows for a config.py override script = os.environ.get('SCRIPT_FILENAME', None) if script is not None: script = script[0:script.rfind("/")] sys.path.insert(0, script) # this allows for grabbing the config based on the DocumentRoot # setting if you're using apache root = os.environ.get('DOCUMENT_ROOT', None) if root is not None: sys.path.insert(0, root) # Settings are now in config.py, you should disable access to it by htaccess # (make it executable or deny access) from config import py as cfg # If the user defined a "codebase" property in their config file, # then we insert that into our sys.path because that's where the # Pyblosxom installation is. if cfg.has_key("codebase"): sys.path.insert(0, cfg["codebase"]) from Pyblosxom.pyblosxom import run_pyblosxom if __name__ == '__main__': run_pyblosxom() pyblosxom-1.5.3/Pyblosxom/entries/000077500000000000000000000000001217606004600172235ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/entries/__init__.py000066400000000000000000000014611217606004600213360ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Pyblosxom does most of its work on "entries". Each entry is a single unit of content which has a series of metadata properties (mtime, filename, id, ...) and also a block of data content. Entries can come from the filesystem, SQL, or anywhere else. They can be generated dynamically or statically. The purpose of an Entry object is to encapsulate the metadata and data of the entry for later filtering and rendering by the other components of Pyblosxom. """ pass pyblosxom-1.5.3/Pyblosxom/entries/base.py000066400000000000000000000275451217606004600205240ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ This module contains the base class for all the Entry classes. The EntryBase class is essentially the API for entries in Pyblosxom. Reading through the comments for this class will walk you through building your own EntryBase derivatives. This module also holds a generic generate_entry function which will generate a BaseEntry with data that you provide for it. """ import time, locale from Pyblosxom import tools BIGNUM = 2000000000 CONTENT_KEY = "body" DOESNOTEXIST = "THISKEYDOESNOTEXIST" DOESNOTEXIST2 = "THISKEYDOESNOTEXIST2" class EntryBase: """ EntryBase is the base class for all the Entry classes. Each instance of an Entry class represents a single entry in the weblog, whether it came from a file, or a database, or even somewhere off the InterWeeb. EntryBase derivatives are dict-like except for one key difference: when doing ``__getitem__`` on a nonexistent key, it returns an empty string. For example: >>> entry["some_nonexistent_key"] "" """ def __init__(self, request): self._data = "" self._metadata = dict(tools.STANDARD_FILTERS) self._id = "" self._mtime = BIGNUM self._request = request def __repr__(self): """ Returns a friendly debuggable representation of self. Useful to know on what entry pyblosxom fails on you (though unlikely) :returns: Identifiable representation of object """ return "\n" % self.getId() def get_id(self): """ This should return an id that's unique enough for caching purposes. Override this. :returns: string id """ return self._id getId = tools.deprecated_function(get_id) def get_data(self): """ Returns the data string. This method should be overridden to provide from pulling the data from other places. Override this. :returns: the data as a string """ return str(self._data) getData = tools.deprecated_function(get_data) def set_data(self, data): """ Sets the data content for this entry. If you are not creating the entry, then you have no right to set the data of the entry. Doing so could be hazardous depending on what EntryBase subclass you're dealing with. Override this. :param data: the data """ self._data = data setData = tools.deprecated_function(set_data) def get_metadata(self, key, default=None): """ Returns a given piece of metadata. Override this. :param key: the key being sought :param default: the default to return if the key does not exist :return: either the default (if the key did not exist) or the value of the key in the metadata dict """ return self._metadata.get(key, default) getMetadata = tools.deprecated_function(get_metadata) def set_metadata(self, key, value): """ Sets a key/value pair in the metadata dict. Override this. :param key: the key string :param value: the value string """ self._metadata[key] = value setMetadata = tools.deprecated_function(set_metadata) def get_metadata_keys(self): """ Returns the list of keys for which we have values in our stored metadata. .. Note:: This list gets modified later downstream. If you cache your list of metadata keys, then this method should return a copy of that list and not the list itself lest it get adjusted. Override this. :returns: list of metadata keys """ return self._metadata.keys() getMetadataKeys = tools.deprecated_function(get_metadata_keys) def get_from_cache(self, entryid): """ Retrieves information from the cache that pertains to this specific entryid. This is a helper method--call this to get data from the cache. Do not override it. :param entryid: a unique key for the information you're retrieving :returns: dict with the values or None if there's nothing for that entryid """ cache = tools.get_cache(self._request) # cache.__getitem__ returns None if the id isn't there if cache.has_key(entryid): return cache[entryid] return None getFromCache = tools.deprecated_function(get_from_cache) def add_to_cache(self, entryid, data): """ Over-writes the cached dict for key entryid with the data dict. This is a helper method--call this to add data to the cache. Do not override it. :param entryid: a unique key for the information you're storing :param data: the data to store--this should probably be a dict """ mycache = tools.get_cache(self._request) if mycache: # This could be extended to cover all keys used by # set_time(), but this is the key most likely to turn # up in metadata. If #date is not blocked from caching # here, the templates will use the raw string value # from the user metadata, rather than the value # derived from mtime. if data.has_key('date'): data.pop('date') mycache[entryid] = data addToCache = tools.deprecated_function(add_to_cache) def set_time(self, timetuple): """ This takes in a given time tuple and sets all the magic metadata variables we have according to the items in the time tuple. :param timetuple: the timetuple to use to set the data with--this is the same thing as the mtime/atime portions of an os.stat. This time is expected to be local time, not UTC. """ self['timetuple'] = timetuple self._mtime = time.mktime(timetuple) gmtimetuple = time.gmtime(self._mtime) self['mtime'] = self._mtime self['ti'] = time.strftime('%H:%M', timetuple) self['mo'] = time.strftime('%b', timetuple) self['mo_num'] = time.strftime('%m', timetuple) self['da'] = time.strftime('%d', timetuple) self['dw'] = time.strftime('%A', timetuple) self['yr'] = time.strftime('%Y', timetuple) self['fulltime'] = time.strftime('%Y%m%d%H%M%S', timetuple) self['date'] = time.strftime('%a, %d %b %Y', timetuple) # YYYY-MM-DDThh:mm:ssZ self['w3cdate'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', gmtimetuple) # Temporarily disable the set locale, so RFC-compliant date is # really RFC-compliant: directives %a and %b are locale # dependent. Technically, we're after english locale, but # only 'C' locale is guaranteed to exist. loc = locale.getlocale(locale.LC_ALL) locale.setlocale(locale.LC_ALL, 'C') self['rfc822date'] = time.strftime('%a, %d %b %Y %H:%M GMT', \ gmtimetuple) # set the locale back locale.setlocale(locale.LC_ALL, loc) setTime = tools.deprecated_function(set_time) # everything below this point involves convenience functions # that work with the above functions. def __getitem__(self, key, default=None): """ Retrieves an item from this dict based on the key given. If the item does not exist, then we return the default. If the item is ``CONTENT_KEY``, it calls ``get_data``, otherwise it calls ``get_metadata``. Don't override this. .. Warning:: There's no reason to override this--override ``get_data`` and ``get_metadata`` instead. :param key: the key being sought :param default: the default to return if the key does not exist :returns: the value of ``get_metadata`` or ``get_data`` """ if key == CONTENT_KEY: return self.get_data() return self.get_metadata(key, default) def get(self, key, default=None): """ Retrieves an item from the internal dict based on the key given. All this does is turn aroun and call ``__getitem__``. .. Warning:: There's no reason to override this--override ``get_data`` and ``get_metadata`` instead. :param key: the key being sought :param default: the default to return if the key does not exist :returns: the value of ``get_metadata`` or ``get_data`` (through ``__getitem__``) """ return self.__getitem__(key, default) def __setitem__(self, key, value): """ Sets the metadata[key] to the given value. This uses ``set_data`` and ``set_metadata``. Don't override this. :param key: the given key name :param value: the given value """ if key == CONTENT_KEY: self.set_data(value) else: self.set_metadata(key, value) def update(self, newdict): """ Updates the contents in this entry with the contents in the dict. It does so by calling ``set_data`` and ``set_metadata``. .. Warning:: There's no reason to override this--override ``set_data`` and ``set_metadata`` instead. :param newdict: the dict we're updating this one with """ for mem in newdict.keys(): if mem == CONTENT_KEY: self.set_data(newdict[mem]) else: self.set_metadata(mem, newdict[mem]) def has_key(self, key): """ Returns whether a given key is in the metadata dict. If the key is the ``CONTENT_KEY``, then we automatically return true. .. Warning:: There's no reason to override this--override ``get_metadata`` instead. :param key: the key to check in the metadata dict for :returns: whether (True) or not (False) the key exists """ if key == CONTENT_KEY or key == CONTENT_KEY + "_escaped": return True value = self.get_metadata(key, DOESNOTEXIST) if value == DOESNOTEXIST: value = self.get_metadata(key, DOESNOTEXIST2) if value == DOESNOTEXIST2: return False return True def keys(self): """ Returns a list of the keys that can be accessed through ``__getitem__``. .. Warning:: There's no reason to override this--override ``get_metadata_keys`` instead. :returns: list of key names """ keys = self.get_metadata_keys() if CONTENT_KEY not in keys: keys.append(CONTENT_KEY) return keys def generate_entry(request, properties, data, mtime=None): """ Takes a properties dict and a data string and generates a generic entry using the data you provided. :param request: the Request object :param properties: the dict of properties for the entry :param data: the data content for the entry :param mtime: the mtime tuple (as given by ``time.localtime()``). if you pass in None, then we'll use localtime. """ entry = EntryBase(request) entry.update(properties) entry.set_data(data) if mtime: entry.set_time(mtime) else: entry.set_time(time.localtime()) return entry pyblosxom-1.5.3/Pyblosxom/entries/fileentry.py000066400000000000000000000120251217606004600215760ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ This module contains FileEntry class which is used to retrieve entries from a file system. Since pulling data from the file system and parsing it is expensive (especially when you have 100s of entries) we delay fetching data until it's demanded. The FileEntry calls EntryBase methods addToCache and getFromCache to handle caching. """ import time import os import re from Pyblosxom import tools from Pyblosxom.entries import base class FileEntry(base.EntryBase): """ This class gets it's data and metadata from the file specified by the filename argument. """ def __init__(self, request, filename, root, datadir=""): """ :param request: the Request object :param filename: the complete filename for the file in question including path :param root: i have no clue what this is :param datadir: the datadir """ base.EntryBase.__init__(self, request) self._config = request.get_configuration() self._filename = filename.replace(os.sep, '/') self._root = root.replace(os.sep, '/') self._datadir = datadir or self._config["datadir"] if self._datadir.endswith(os.sep): self._datadir = self._datadir[:-1] self._timetuple = tools.filestat(self._request, self._filename) self._mtime = time.mktime(self._timetuple) self._fulltime = time.strftime("%Y%m%d%H%M%S", self._timetuple) self._populated_data = 0 def __repr__(self): return "" % (self._filename, self._root) def get_id(self): """ Returns the id for this content item--in this case, it's the filename. :returns: the id of the fileentry (the filename) """ return self._filename getId = tools.deprecated_function(get_id) def get_data(self): """ Returns the data for this file entry. The data is the parsed (via the entryparser) content of the entry. We do this on-demand by checking to see if we've gotten it and if we haven't then we get it at that point. :returns: the content for this entry """ if self._populated_data == 0: self._populatedata() return self._data getData = tools.deprecated_function(get_data) def get_metadata(self, key, default=None): """ This overrides the ``base.EntryBase`` ``get_metadata`` method. .. Note:: We populate our metadata lazily--only when it's requested. This delays parsing of the file as long as we can. """ if self._populated_data == 0: self._populatedata() return self._metadata.get(key, default) getMetadata = tools.deprecated_function(get_metadata) def _populatedata(self): """ Fills the metadata dict with metadata about the given file. This metadata consists of things we pick up from an os.stat call as well as knowledge of the filename and the root directory. We then parse the file and fill in the rest of the information that we know. """ file_basename = os.path.basename(self._filename) path = self._filename.replace(self._root, '') path = path.replace(os.path.basename(self._filename), '') path = path[:-1] absolute_path = self._filename.replace(self._datadir, '') absolute_path = self._filename.replace(self._datadir, '', 1) absolute_path = absolute_path.replace(file_basename, '') absolute_path = absolute_path[1:][:-1] if absolute_path and absolute_path[-1] == "/": absolute_path = absolute_path[0:-1] filenamenoext = os.path.splitext(file_basename)[0] if absolute_path == '': file_path = filenamenoext else: file_path = '/'.join((absolute_path, filenamenoext)) tb_id = '%s/%s' % (absolute_path, filenamenoext) tb_id = re.sub(r'[^A-Za-z0-9]', '_', tb_id) self['path'] = path self['tb_id'] = tb_id self['absolute_path'] = absolute_path self['file_path'] = file_path self['fn'] = filenamenoext self['filename'] = self._filename self.set_time(self._timetuple) data = self._request.get_data() entrydict = self.get_from_cache(self._filename) if not entrydict: fileext = os.path.splitext(self._filename) if fileext: fileext = fileext[1][1:] eparser = data['extensions'][fileext] entrydict = eparser(self._filename, self._request) self.add_to_cache(self._filename, entrydict) self.update(entrydict) self._populated_data = 1 pyblosxom-1.5.3/Pyblosxom/flavours/000077500000000000000000000000001217606004600174135ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/flavours/atom.flav/000077500000000000000000000000001217606004600213025ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/flavours/atom.flav/content_type000066400000000000000000000000251217606004600237350ustar00rootroot00000000000000application/atom+xml pyblosxom-1.5.3/Pyblosxom/flavours/atom.flav/foot000066400000000000000000000000101217606004600221630ustar00rootroot00000000000000 pyblosxom-1.5.3/Pyblosxom/flavours/atom.flav/head000066400000000000000000000013411217606004600221250ustar00rootroot00000000000000 $(escape(blog_title)) $(escape(blog_description)) $(url) $(escape(blog_author)) $(escape(url)) $(escape(blog_email)) $(escape(blog_rights)) Pyblosxom hhttp://pyblosxom.github.com/ $(escape(pyblosxom_version)) $(latest_w3cdate) pyblosxom-1.5.3/Pyblosxom/flavours/atom.flav/story000066400000000000000000000005571217606004600224140ustar00rootroot00000000000000 $(escape(title)) $(base_url)/$(yr)/$(mo_num)/$(da)/$(fn) $(w3cdate) $(w3cdate) $(escape(body)) pyblosxom-1.5.3/Pyblosxom/flavours/error.flav/000077500000000000000000000000001217606004600214735ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/flavours/error.flav/content_type000066400000000000000000000000121217606004600241220ustar00rootroot00000000000000text/html pyblosxom-1.5.3/Pyblosxom/flavours/error.flav/foot000066400000000000000000000000201217606004600223550ustar00rootroot00000000000000 pyblosxom-1.5.3/Pyblosxom/flavours/error.flav/head000066400000000000000000000001151217606004600223140ustar00rootroot00000000000000 Pyblosxom encountered an error pyblosxom-1.5.3/Pyblosxom/flavours/error.flav/story000066400000000000000000000001021217606004600225670ustar00rootroot00000000000000

$(title)

$(body)

pyblosxom-1.5.3/Pyblosxom/flavours/html.flav/000077500000000000000000000000001217606004600213065ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/flavours/html.flav/content_type000066400000000000000000000000311217606004600237360ustar00rootroot00000000000000text/html; charset=utf-8 pyblosxom-1.5.3/Pyblosxom/flavours/html.flav/date_foot000066400000000000000000000000001217606004600231630ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/flavours/html.flav/date_head000066400000000000000000000000211217606004600231200ustar00rootroot00000000000000

$(date)

pyblosxom-1.5.3/Pyblosxom/flavours/html.flav/foot000066400000000000000000000002551217606004600222020ustar00rootroot00000000000000

Made with Pyblosxom

pyblosxom-1.5.3/Pyblosxom/flavours/html.flav/head000066400000000000000000000003551217606004600221350ustar00rootroot00000000000000 $(blog_title) : $(pi_bl)

$(blog_title)

[ Home | RSS 2.0 | ATOM 1.0 ]

pyblosxom-1.5.3/Pyblosxom/flavours/html.flav/story000066400000000000000000000004521217606004600224120ustar00rootroot00000000000000

$(title)

$(body)

posted at: $(ti) | path: /$(absolute_path) | permanent link to this entry

pyblosxom-1.5.3/Pyblosxom/flavours/rss.flav/000077500000000000000000000000001217606004600211515ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/flavours/rss.flav/content_type000066400000000000000000000000111217606004600235770ustar00rootroot00000000000000text/xml pyblosxom-1.5.3/Pyblosxom/flavours/rss.flav/foot000066400000000000000000000000221217606004600220350ustar00rootroot00000000000000 pyblosxom-1.5.3/Pyblosxom/flavours/rss.flav/head000066400000000000000000000006431217606004600220000ustar00rootroot00000000000000 $(blog_title) $(base_url) $(blog_description) $(blog_language) pyblosxom-1.5.3/Pyblosxom/flavours/rss.flav/story000066400000000000000000000002311217606004600222500ustar00rootroot00000000000000 $(escape(title)) $(base_url)/$(urlencode(file_path)).html $(escape(body)) pyblosxom-1.5.3/Pyblosxom/flavours/rss20.flav/000077500000000000000000000000001217606004600213135ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/flavours/rss20.flav/content_type000066400000000000000000000000241217606004600237450ustar00rootroot00000000000000application/rss+xml pyblosxom-1.5.3/Pyblosxom/flavours/rss20.flav/foot000066400000000000000000000000221217606004600221770ustar00rootroot00000000000000 pyblosxom-1.5.3/Pyblosxom/flavours/rss20.flav/head000066400000000000000000000013441217606004600221410ustar00rootroot00000000000000 $(escape(blog_title)) $(base_url) $(escape(blog_description)) $(escape(blog_language)) $(escape(blog_rights)) 60 $(latest_rfc822date) $(escape(blog_email)) ($(escape(blog_author))) Pyblosxom http://pyblosxom.github.com/ $(escape(pyblosxom_version)) pyblosxom-1.5.3/Pyblosxom/flavours/rss20.flav/story000066400000000000000000000005031217606004600224140ustar00rootroot00000000000000 $(escape(title)) $(escape(file_path)) $(base_url)/$(urlencode(file_path)).$(default_flavour) $(escape(body)) $(escape(path)) $(rfc822date) pyblosxom-1.5.3/Pyblosxom/memcache.py000066400000000000000000000042361217606004600176730ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """Holds memcache functions. """ # Whether or not to use memcache. usecache = False _memcache_cache = {} def memcache_decorator(scope, instance=False): """Caches function results in memory This is a pretty classic memoization system for plugins. There's no expiration of cached data---it just hangs out in memory forever. This is great for static rendering, but probably not for running as a CGI/WSGI application. This is disabled by default. It must be explicitly enabled to have effect. Some notes: 1. the function arguments MUST be hashable--no dicts, lists, etc. 2. this probably does not play well with non-static-rendering--that should get checked. 3. TODO: the two arguments are poorly named--that should get fixed. :arg scope: string defining the scope. e.g. 'pycategories'. :arg instance: whether or not the function being decorated is bound to an instance (i.e. is the first argument "self" or "cls"?) """ def _memcache(fun): def _memcache_decorated(*args, **kwargs): if not usecache: return fun(*args, **kwargs) try: if instance: hash_key = hash((args[1:], frozenset(sorted(kwargs.items())))) else: hash_key = hash((args, frozenset(sorted(kwargs.items())))) except TypeError: print repr((args, kwargs)) hash_key = None if not hash_key: return fun(*args, **kwargs) try: ret = _memcache_cache.setdefault(scope, {})[hash_key] except KeyError: ret = fun(*args, **kwargs) _memcache_cache[scope][hash_key] = ret return ret return _memcache_decorated return _memcache pyblosxom-1.5.3/Pyblosxom/plugin_utils.py000066400000000000000000000126761217606004600206560ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Holds a series of utility functions for cataloguing, retrieving, and manipulating callback functions and chains. Refer to the documentation for which callbacks are available and their behavior. """ import os import glob import sys import os.path import traceback # this holds the list of plugins that have been loaded. if you're running # Pyblosxom as a long-running process, this only gets cleared when the # process is restarted. plugins = [] # this holds a list of callbacks (any function that begins with cp_) and the # list of function instances that support that callback. # if you're running Pyblosxom as a long-running process, this only # gets cleared when the process is restarted. callbacks = {} # this holds a list of (plugin name, exception) tuples for plugins that # didn't import. bad_plugins = [] def catalogue_plugin(plugin_module): """ Goes through the plugin's contents and catalogues all the functions that start with cb_. Functions that start with cb_ are callbacks. :param plugin_module: the module to catalogue """ listing = dir(plugin_module) listing = [item for item in listing if item.startswith("cb_")] for mem in listing: func = getattr(plugin_module, mem) memadj = mem[3:] if callable(func): callbacks.setdefault(memadj, []).append(func) def get_callback_chain(chain): """ Returns a list of functions registered with the callback. @returns: list of functions registered with the callback (or an empty list) @rtype: list of functions """ return callbacks.get(chain, []) def initialize_plugins(plugin_dirs, plugin_list): """ Imports and initializes plugins from the directories in the list specified by "plugins_dir". If no such list exists, then we don't load any plugins. If the user specifies a "load_plugins" list of plugins to load, then we explicitly load those plugins in the order they're listed. If the load_plugins key does not exist, then we load all the plugins in the plugins directory using an alphanumeric sorting order. .. Note:: If Pyblosxom is part of a long-running process, you must restart Pyblosxom in order to pick up any changes to your plugins. :param plugin_dirs: the list of directories to add to the sys.path because that's where our plugins are located. :param plugin_list: the list of plugins to load, or if None, we'll load all the plugins we find in those dirs. """ if plugins or bad_plugins: return # we clear out the callbacks dict so we can rebuild them callbacks.clear() # handle plugin_dirs here for mem in plugin_dirs: if os.path.isdir(mem): sys.path.append(mem) else: raise Exception("Plugin directory '%s' does not exist. " \ "Please check your config file." % mem) plugin_list = get_plugin_list(plugin_list, plugin_dirs) for mem in plugin_list: try: _module = __import__(mem) except (SystemExit, KeyboardInterrupt): raise except: # this needs to be a catch-all bad_plugins.append((mem, "".join(traceback.format_exc()))) continue for comp in mem.split(".")[1:]: _module = getattr(_module, comp) catalogue_plugin(_module) plugins.append(_module) def get_plugin_by_name(name): """ This retrieves a plugin instance (it's a Python module instance) by name. :param name: the name of the plugin to retrieve (ex: "xmlrpc") :returns: the Python module instance for the plugin or None """ if plugins: for mem in plugins: if mem.__name__ == name: return mem return None def get_module_name(filename): """ Takes a filename and returns the module name from the filename. Example: passing in "/blah/blah/blah/module.ext" returns "module" :param filename: the filename in question (with a full path) :returns: the filename without path or extension """ return os.path.splitext(os.path.split(filename)[1])[0] def get_plugin_list(plugin_list, plugin_dirs): """ This handles the situation where the user has provided a series of plugin dirs, but has not specified which plugins they want to load from those dirs. In this case, we load all possible plugins except the ones whose names being with _ . :param plugin_list: List of plugins to load :param plugin_dirs: A list of directories where plugins can be loaded from :return: list of python module names of the plugins to load """ if plugin_list == None: plugin_list = [] for mem in plugin_dirs: file_list = glob.glob(os.path.join(mem, "*.py")) file_list = [get_module_name(filename) for filename in file_list] # remove plugins that start with a _ file_list = [plugin for plugin in file_list \ if not plugin.startswith('_')] plugin_list += file_list plugin_list.sort() return plugin_list pyblosxom-1.5.3/Pyblosxom/plugins/000077500000000000000000000000001217606004600172335ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/plugins/__init__.py000066400000000000000000000005311217606004600213430ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2010 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### pass pyblosxom-1.5.3/Pyblosxom/plugins/acronyms.py000066400000000000000000000156361217606004600214530ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2010, 2011 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= This plugin marks abbreviations and acronyms based on an acronyms/abbreviations files. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. In your ``config.py`` file, add ``Pyblosxom.plugins.acronyms`` to the ``load_plugins`` variable. 2. Create an acronyms file with acronyms and abbreviations in it. See below for the syntax. 3. (optional) In your ``config.py`` file, add a line like this:: py["acronyms_file"] = "/path/to/file" Make sure to use the complete path to your acronyms file in place of ``"/path/to/file"``. This defaults to ``acronyms.txt`` in the parent directory of your datadir. Building the acronyms file ========================== The file should be a text file with one acronym or abbreviation followed by an = followed by the explanation. The acronym/abbreviation is a regular expression and can contain regular expression bits. An acronym is upper or lower case letters that is NOT followed by a period. If it's followed by a period, then you need to explicitly state it's an acronym. = explanation = acronym|explanation Examples:: ASCII = American Standard Code for Information Interchange CGI = Common Gateway Interface; Computer Generated Imagery CSS = Cascading Stylesheets HTML = Hypertext Markup Language HTTP = Hypertext Transport Protocol RDF = Resource Description Framework RSS = Really Simple Sindication URL = Uniform Resource Locator URI = Uniform Resource Indicator WSGI = Web Server Gateway Interface XHTML = Extensible Hypertext Markup Language XML = Extensible Markup Language This one is explicitly labeled an acronym:: X.M.L. = acronym|Extensible Markup Language This one uses regular expression to match both ``UTF-8`` and ``UTF8``:: UTF\-?8 = 8-bit UCS/Unicode Transformation Format An abbreviation is a series of characters followed by a period. If it's not followed by a period, then you need to explicitly state that it's an abbreviation. = explanation = abbr|explanation Examples:: dr. = doctor This one is explicitly labeled an abbreviation:: dr = abbr|doctor .. Note:: If the first part is an improperly formed regular expression, then it will be skipped. You can verify that your file is properly formed by running ``pyblosxom-cmd test``. Using acronyms in your blog entries =================================== When writing a blog entry, write the acronyms and abbreviations and they'll be marked up by the plugin in the story callback. If you're writing an entry that you don't want to have marked up, add this to the metadata for the entry:: #noacronyms 1 Styling ======= You might want to add something like this to your CSS:: acronym { bordor-bottom: 1px dashed #aaa; cursor: help; } abbr { bordor-bottom: 1px dashed #aaa; cursor: help; } Origins ======= Based on the Blosxom acronyms plugin by Axel Beckert at http://noone.org/blosxom/acronyms . """ __author__ = "Will Kahn-Greene" __email__ = "willg at bluesock dot org" __version__ = "2011-10-21" __url__ = "http://pyblosxom.github.com/" __description__ = "Marks acronyms and abbreviations in blog entries." __category__ = "text" __license__ = "MIT" __registrytags__ = "1.5, core" import os import re from Pyblosxom import tools from Pyblosxom.tools import pwrap_error def get_acronym_file(cfg): datadir = cfg["datadir"] filename = cfg.get("acronym_file", os.path.join(datadir, os.pardir, "acronyms.txt")) return filename def verify_installation(request): config = request.get_configuration() filename = get_acronym_file(config) if not os.path.exists(filename): pwrap_error("There is no acronym file at %s." % filename) pwrap_error( "You should create one. Refer to documentation for examples.") return False try: fp = open(filename, "r") except IOError: pwrap_error( "Your acronyms file %s cannot be opened for reading. Please " "adjust the permissions." % filename) return False malformed = False # FIXME - this is a repeat of build_acronyms for line in fp.readlines(): line = line.strip() firstpart = line.split("=", 1)[0] firstpart = "(\\b" + firstpart.strip() + "\\b)" try: re.compile(firstpart) except re.error, s: pwrap_error("- '%s' is not a properly formed regexp. (%s)" % (line, s)) malformed = True fp.close() if malformed: return False return True def build_acronyms(lines): acronyms = [] for line in lines: line = line.split("=", 1) firstpart = line[0].strip() try: firstpartre = re.compile("(\\b" + firstpart + "\\b)") except re.error: logger = tools.get_logger() logger.error("acronyms: '%s' is not a regular expression", firstpart) continue secondpart = line[1].strip() secondpart = secondpart.replace("\"", """) if (secondpart.startswith("abbr|") or firstpart.endswith(".")): if secondpart.startswith("abbr|"): secondpart = secondpart[5:] repl = "\\1" % secondpart else: if secondpart.startswith("acronym|"): secondpart = secondpart[8:] repl = "\\1" % secondpart acronyms.append((firstpartre, repl)) return acronyms def cb_start(args): request = args["request"] config = request.get_configuration() filename = get_acronym_file(config) try: fp = open(filename, "r") except IOError: return lines = fp.readlines() fp.close() request.get_data()["acronyms"] = build_acronyms(lines) TAG_RE = re.compile("<\D.*?>") TAG_DIGIT_RE = re.compile("<\d+?>") def cb_story(args): request = args["request"] acrolist = request.get_data()["acronyms"] entry = args["entry"] if entry.get("noacronyms"): return args body = entry.get("body", "") tags = {} def matchrepl(matchobj): ret = "<%d>" % len(tags) tags[ret] = matchobj.group(0) return ret body = TAG_RE.sub(matchrepl, body) for reob, repl in acrolist: body = reob.sub(repl, body) def matchrepl(matchobj): return tags[matchobj.group(0)] body = TAG_DIGIT_RE.sub(matchrepl, body) entry["body"] = body return args pyblosxom-1.5.3/Pyblosxom/plugins/akismetcomments.py000066400000000000000000000120151217606004600230070ustar00rootroot00000000000000####################################################################### # Copyright (C) 2006 Benjamin Mako Hill # Blake Winton # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 # USA ####################################################################### """ Summary ======= Run comments and trackbacks through `Akismet `_ to see whether to reject them or not. Install ======= Requires the ``comments`` plugin. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.akismetcomments`` to the ``load_plugins`` list in your ``config.py`` file. 2. Install the ``akismet`` library. You can get it at http://www.voidspace.org.uk/python/modules.shtml#akismet 3. Set up a Wordpress.com API key. You can find more information from http://faq.wordpress.com/2005/10/19/api-key/ . 4. Use this key to put put the following line into your config.py file:: py['akismet_api_key'] = 'MYKEYID' 5. Add ``$(comment_message)`` to the comment-form template if it isn't there already. When akismetcomments rejects a comment, it'll populate that variable with a message explaining what happened. History ======= This plugin merges the work done on the ``akismetComments.py`` plugin by Blake Winton with the the ``akismet.py`` plugin by Benjamin Mako Hill. """ __author__ = "Benjamin Mako Hill" __version__ = "0.2" __email__ = "" __url__ = "http://pyblosxom.github.com/" __description__ = "Rejects comments using akismet" __category__ = "comments" __license__ = "GPLv2" __registrytags__ = "1.4, 1.5, core" from Pyblosxom.tools import pwrap_error def verify_installation(request): try: from akismet import Akismet except ImportError: pwrap_error( "Missing module 'akismet'. See documentation for getting it.") return False config = request.get_configuration() # try to check to se make sure that the config file has a key if not "akismet_api_key" in config: pwrap_error("Missing required configuration value 'akismet_key'") return False a = Akismet(config['akismet_api_key'], config['base_url'], agent='Pyblosxom/1.3') if not a.verify_key(): pwrap_error("Could not verify akismet API key.") return False return True def cb_comment_reject(args): from akismet import Akismet, AkismetError request = args['request'] comment = args['comment'] config = request.get_configuration() http = request.get_http() fields = {'comment': 'description', 'comment_author_email': 'email', 'comment_author': 'author', 'comment_author_url': 'link', 'comment_type': 'type', } data = {} for field in fields: if fields[field] in comment: data[field] = "" for char in list(comment[fields[field]]): try: char.encode('ascii') # FIXME - bare except--bad! except: data[field] = data[field] + "&#" + str(ord(char)) + ";" else: data[field] = data[field] + char if not data.get('comment'): pwrap_error("Comment info not enough.") return False body = data['comment'] if 'ipaddress' in comment: data['user_ip'] = comment['ipaddress'] data['user_agent'] = http.get('HTTP_USER_AGENT', '') data['referrer'] = http.get('HTTP_REFERER', '') api_key = config.get('akismet_api_key') base_url = config.get('base_url') # initialize the api api = Akismet(api_key, base_url, agent='Pyblosxom/1.5') if not api.verify_key(): pwrap_error("Could not verify akismet API key. Comments accepted.") return False # false is ham, true is spam try: if api.comment_check(body, data): pwrap_error("Rejecting comment") return (True, 'I\'m sorry, but your comment was rejected by ' 'the Akismet ' 'spam filtering system.') else: return False except AkismetError: pwrap_error("Rejecting comment (AkismetError)") return (True, "Missing essential data (e.g., a UserAgent string).") # akismet can handle trackback spam too cb_trackback_reject = cb_comment_reject pyblosxom-1.5.3/Pyblosxom/plugins/check_blacklist.py000066400000000000000000000102311217606004600227070ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2002-2011 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= This works in conjunction with the comments plugin and allows you to xreduce comment spam by a words blacklist. Any comment that contains one of the blacklisted words will be rejected immediately. This shouldn't be the only way you reduce comment spam. It's probably not useful to everyone, but would be useful to some people as a quick way of catching some of the comment spam they're getting. Install ======= This requires the ``comments`` plugin. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.check_blacklist`` to the ``load_plugins`` list in your ``config.py`` file. 2. Configure as documented below. Usage ===== For setup, all you need to do is set the comment_rejected_words property in your config.py file. For example, the following will reject any incoming comments with the words ``gambling`` or ``casino`` in them:: py["comment_rejected_words"] = ["gambling", "casino"] The comment_rejected_words property takes a list of strings as a value. .. Note:: There's a deficiency in the algorithm. Currently, it will match substrings, too. So if you blacklist the word "word", that'll nix comments with "word" in it as well as comments with "crossword" because "word" is a substring of "crossword". Pick your blacklisted words carefully or fix the algorithm! .. Note:: This checks all parts of the comment including the ip address of the poster. Blacklisting ip addresses is as easy as adding the ip address to the list:: py["comment_rejected_words"] = ["192.168.1.1", ...] Additionally, the wbgcomments_blacklist plugin can log when it blacklisted a comment and what word was used to blacklist it. Sometimes this information is interesting. True, "yes, I want to log" and False (default) if "no, i don't want to log". Example:: py["comment_rejected_words_log"] = False """ __author__ = "Will Kahn-Greene" __email__ = "willg at bluesock dot org" __version__ = "2011-10-25" __url__ = "http://pyblosxom.github.com/" __description__ = "Rejects comments using a word blacklist." __category__ = "comments" __license__ = "MIT" __registrytags__ = "1.4, 1.5, core" import time import os.path from Pyblosxom.tools import pwrap_error def verify_installation(request): config = request.get_configuration() if not "comment_rejected_words" in config: pwrap_error( "The \"comment_rejected_words\" property must be set in your " "config.py file. It takes a list of strings as a value. " "Refer to the documentation for more details.") return False crw = config["comment_rejected_words"] if not isinstance(crw, (list, tuple)): pwrap_error( "The \"comment_rejected_words\" property is incorrectly set in " "your config.py file. It takes a list of strings as a value. " "Refer to the documentation at the top of the comment_blacklist " "plugin for more details.") return False return True def cb_comment_reject(args): r = args["request"] c = args["comment"] config = r.get_configuration() badwords = config.get("comment_rejected_words", []) for mem in c.values(): mem = mem.lower() for word in badwords: # FIXME - this matches on substrings, too. should use # word-boundaries. if mem.find(word) != -1: if ((config.get("comment_rejected_words_log", False) and "logdir" in config)): fn = os.path.join(config["logdir"], "blacklist.log") f = open(fn, "a") f.write("%s: %s %s\n" % ( time.ctime(), c.get("ipaddress", None), word)) f.close() return (True, "Comment rejected: contains blacklisted words.") return False pyblosxom-1.5.3/Pyblosxom/plugins/check_javascript.py000066400000000000000000000061741217606004600231200ustar00rootroot00000000000000####################################################################### # Copyright (c) 2006 Ryan Barrett # Copyright (c) 2011 Will Kahn-Greene # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. ####################################################################### """ Summary ======= This plugin filters spam with a dash of JavaScript on the client side. The JavaScript sets a hidden input field ``secretToken`` in the comment form to the blog's title. This plugin checks the ``secretToken`` URL parameter and rejects the comment if it's not set correctly. The benefit of JavaScript as an anti-spam technique is that it's very successful. It has extremely low false positive and false negative rates, as compared to conventional techniques like CAPTCHAs, bayesian filtering, and keyword detection. Of course, JavaScript has its own drawbacks, primarily that it's not supported in extremely old browsers, and that users can turn it off. That's a very small minority of cases, though. Its effectiveness as an anti-spam technique usually make that tradeoff worthwhile. Install ======= Requires the ``comments`` plugin. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.check_javascript`` to the ``load_plugins`` list in your ``config.py`` file. 2. Configure as documented below. Configure ========= 1. Make sure you have ``blog_title`` set in your ``config.py``. 2. Add the following bits to your ``comment-form`` template inside the ``
`` tags:: """ __author__ = "Ryan Barrett" __email__ = "pyblosxom at ryanb dot org" __version__ = "2011-10-25" __url__ = "http://pyblosxom.github.com/" __description__ = "Rejects comments using JavaScript" __category__ = "comments" __license__ = "GPLv2" __registrytags__ = "1.4, 1.5, core" from Pyblosxom import tools def verify_installation(request): return True def cb_comment_reject(args): request = args["request"] config = request.get_configuration() http = request.get_http() form = http['form'] if (('secretToken' in form and form['secretToken'].value == config['blog_title'])): return False dump = '\n'.join(['%s: %s' % (arg.name, arg.value) for arg in dict(form).values()]) logger = tools.get_logger() logger.info('Comment rejected from %s:\n%s' % ( http['REMOTE_ADDR'], dump)) return True pyblosxom-1.5.3/Pyblosxom/plugins/check_nonhuman.py000066400000000000000000000062431217606004600225720ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2006-2011 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= This works in conjunction with the comments plugin and allows you to significantly reduce comment spam by adding a "I am human" checkbox to your form. Any comments that aren't "from a human" get rejected immediately. This shouldn't be the only way you reduce comment spam. It's probably not useful to everyone, but would be useful to some people as a quick way of catching some of the comment spam they're getting. Usually this works for a while, then spam starts coming in again. At that point, I change the ``nonhuman_name`` config.py variable value and I stop getting comment spam. Install ======= Requires the ``comments`` plugin. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.check_nonhuman`` to the ``load_plugins`` list in your ``config.py`` file. 2. Configure as documented below. Usage ===== For setup, copy the plugin to your plugins directory and add it to your load_plugins list in your config.py file. Then add the following item to your config.py (this defaults to "iamhuman"):: py["nonhuman_name"] = "iamhuman" Then add the following to your comment-form template just above the submit button (make sure to match the input name to your configured input name):: Yes, I am human! Alternatively, if you set the ``nonhuman_name`` property, then you should do this:: Yes, I am human! Additionally, the nonhuman plugin can log when it rejected a comment. This is good for statistical purposes. 1 if "yes, I want to log" and 0 (default) if "no, i don't want to log". Example:: py["nonhuman_log"] = 1 And that's it! The idea came from:: http://www.davidpashley.com/cgi/pyblosxom.cgi/2006/04/28#blog-spam """ __author__ = "Will Kahn-Greene" __email__ = "willg at bluesock dot org" __version__ = "2011-10-25" __url__ = "http://pyblosxom.github.com/" __description__ = "Rejects non-human comments." __category__ = "comments" __license__ = "MIT" __registrytags__ = "1.4, 1.5, core" import os import time from Pyblosxom.tools import pwrap def verify_installation(request): config = request.get_configuration() if not "nonhuman_name" in config: pwrap("missing optional property: 'nonhuman_name'") return True def cb_comment_reject(args): r = args["request"] c = args["comment"] config = r.get_configuration() if not config.get("nonhuman_name", "iamhuman") in c: if config.get("nonhuman_log", 0) and "logdir" in config: fn = os.path.join(config["logdir"], "nothuman.log") f = open(fn, "a") f.write("%s: %s\n" % ( time.ctime(), c.get("ipaddress", None))) f.close() return (True, "Comment rejected: I don't think you're human.") return False pyblosxom-1.5.3/Pyblosxom/plugins/comments.py000066400000000000000000001317341217606004600214430ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= Adds comments to your blog. Supports preview, AJAX posting, SMTP notifications, plugins for rejecting comments (and thus reducing spam), ... Comments are stored in a directory that parallels the data directory. The comments themselves are stored as XML files named entryname-datetime.suffix. The comment system allows you to specify the directory where the comment directory tree will stored, and the suffix used for comment files. You need to make sure that this directory is writable by whatever is running Pyblosxom. Comments are stored one or more per file in a parallel hierarchy to the datadir hierarchy. The filename of the comment is the filename of the blog entry, plus the creation time of the comment as a float, plus the comment extension. Comments now follow the ``blog_encoding`` variable specified in ``config.py``. If you don't include a ``blog_encoding`` variable, this will default to utf-8. Comments will be shown for a given page if one of the following is true: 1. the page has only one blog entry on it and the request is for a specific blog entry as opposed to a category with only one entry in it 2. if "showcomments=yes" is in the querystring then comments will be shown .. Note:: This comments plugin does not work with static rendering. If you are using static rendering to build your blog, you won't be able to use this plugin. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.comments`` to the ``load_plugins`` list of your ``config.py`` file. Example:: py["load_plugins"] = ["Pyblosxom.plugins.comments"] 2. Configure as documented below in the Configuration section. 3. Add templates to your html flavour as documented in the Flavour templates section. Configuration ============= 1. Set ``py['comment_dir']`` to the directory (in your data directory) where you want the comments to be stored. The default value is a directory named "comments" in your datadir. 2. (optional) The comment system can notify you via e-mail when new comments/trackbacks/pingbacks are posted. If you want to enable this feature, create the following config.py entries: py['comment_smtp_from'] - the email address sending the notification py['comment_smtp_to'] - the email address receiving the notification If you want to use an SMTP server, then set:: py['comment_smtp_server'] - your SMTP server hostname/ip address **OR** if you want to use a mail command, set:: py['comment_mta_cmd'] - the path to your MTA, e.g. /usr/bin/mail Example 1:: py['comment_smtp_from'] = "joe@joe.com" py['comment_smtp_to'] = "joe@joe.com" py['comment_smtp_server'] = "localhost" Example 2:: py['comment_smtp_from'] = "joe@joe.com" py['comment_smtp_to'] = "joe@joe.com" py['comment_mta_cmd'] = "/usr/bin/mail" 3. (optional) Set ``py['comment_ext']`` to the change comment file extension. The default file extension is "cmt". This module supports the following config parameters (they are not required): ``comment_dir`` The directory we're going to store all our comments in. This defaults to datadir + "comments". Example:: py["comment_dir"] = "/home/joe/blog/comments/" ``comment_ext`` The file extension used to denote a comment file. This defaults to "cmt". ``comment_draft_ext`` The file extension used for new comments that have not been manually approved by you. This defaults to the value in ``comment_ext``---i.e. there is no draft stage. ``comment_smtp_server`` The smtp server to send comments notifications through. ``comment_mta_cmd`` Alternatively, a command line to invoke your MTA (e.g. sendmail) to send comment notifications through. ``comment_smtp_from`` The email address comment notifications will be from. If you're using SMTP, this should be an email address accepted by your SMTP server. If you omit this, the from address will be the e-mail address as input in the comment form. ``comment_smtp_to`` The email address to send comment notifications to. ``comment_nofollow`` Set this to 1 to add ``rel="nofollow"`` attributes to links in the description---these attributes are embedded in the stored representation. ``comment_disable_after_x_days`` Set this to a positive integer and users won't be able to leave comments on entries older than x days. Related files ============= .. only:: text You can find the comment-story file in the docs at http://pyblosxom.github.com/ or in the tarball under docs/_static/plugins/comments/. This plugin has related files like flavour templates, javascript file, shell scripts and such. All of these files can be gotten from `here <../_static/plugins/comments/>`_ Flavour templates ================= The comments plugin requires at least the ``comment-story``, ``comment``, and ``comment-form`` templates. The way the comments plugin assembles flavour files is like this:: comment-story comment (zero or more) comment-form Thus if you want to have your entire comment section in a div container, you'd start the div container at the top of ``comment-story`` and end it at the bottom of ``comment-form``. comment-story ------------- The ``comment-story`` template comes at the beginning of the comment section before the comments and the comment form. Variables available: $num_comments - Contains an integer count of the number of comments associated with this entry .. only:: text You can find the comment-story file in the docs at http://pyblosxom.github.com/ or in the tarball under docs/_static/plugins/comments/. Link to file: `comment-story <../_static/plugins/comments/comment-story>`_ .. literalinclude:: ../_static/plugins/comments/comment-story :language: html comment ------- The ``comment`` template is used to format a single entry that has comments. Variables available:: $cmt_title - the title of the comment $cmt_description - the content of the comment or excerpt of the trackback/pingback $cmt_link - the pingback link referring to this entry $cmt_author - the author of the comment or trackback $cmt_optionally_linked_author - the author, wrapped in an tag to their link if one was provided $cmt_pubDate - the date and time of the comment/trackback/pingback $cmt_source - the source of the trackback .. only:: text You can find the comment-story file in the docs at http://pyblosxom.github.com/ or in the tarball under docs/_static/plugins/comments/. Link to file: `comment <../_static/plugins/comments/comment>`_ .. literalinclude:: ../_static/plugins/comments/comment :language: html comment-form ------------ The ``comment-form`` comes at the end of all the comments. It has the comment form used to enter new comments. .. only:: text You can find the comment-story file in the docs at http://pyblosxom.github.com/ or in the tarball under docs/_static/plugins/comments/. Link to file: `comment-form <../_static/plugins/comments/comment-form>`_ .. literalinclude:: ../_static/plugins/comments/comment-form :language: html Dealing with comment spam ========================= You'll probably have comment spam. There are a bunch of core plugins that will help you reduce the comment spam that come with Pyblosxom as well as ones that don't. Best to check the core plugins first. Compacting comments =================== This plugin always writes each comment to its own file, but as an optimization, it supports files that contain multiple comments. You can use ``compact_comments.sh`` to compact comments into a single file per entry. .. only:: text compact_comments.sh is located in docs/_static/plugins/comments/ You can find ``compact_comments.sh`` `here <../_static/plugins/comments/>`_. Implementing comment preview ============================ .. Note:: Comment preview is implemented by default---all the bits listed below are in the comment-form and comment-preview templates. This documentation is here in case you had an older version of the comments plugin or you want to know what to remove to remove comment preview. If you would like comment previews, you need to do 2 things. 1. Add a preview button to the ``comment-form`` template like this:: You may change the contents of the value attribute, but the name of the input must be "preview". I put it next to the "Submit" button. 2. Still in your ``comment-form.html`` template, you need to use the comment values to fill in the values of your input fields like so:: If there is no preview available, these variables will be stripped from the text and cause no problem. 3. Create a ``comment-preview`` template. This can be a copy of your ``comment`` template if you like with some additional text along the lines of **"This is a preview!"** All of the available variables from the ``comment`` template are available in the ``comment-preview`` template. .. only:: text You can find the comment-story file in the docs at http://pyblosxom.github.com/ or in the tarball under docs/_static/plugins/comments/. comment-preview --------------- Link to file: `comment-preview <../_static/plugins/comments/comment-preview>`_ .. literalinclude:: ../_static/plugins/comments/comment-preview :language: html AJAX support ============ Comment previewing and posting can optionally use AJAX, as opposed to full HTTP POST requests. This avoids a full-size roundtrip and re-render, so commenting feels faster and more lightweight. AJAX commenting degrades gracefully in older browsers. If JavaScript is disabled or not supported in the user's browser, or if it doesn't support XmlHttpRequest, comment posting and preview will use normal HTTP POST. This will also happen if comment plugins that use alternative protocols are detected, like ``comments_openid.py``. To add AJAX support, you need to make the following modifications to your ``comment-form`` template: 1. The comment-anchor tag must be the first thing in the ``comment-form`` template::

2. Change the ```` tag to something like this:: .. Note:: If you run pyblosxom inside cgiwrap, you'll probably need to remove ``#comment-anchor`` from the URL in the action attribute. They're incompatible. Your host may even be using cgiwrap without your knowledge. If AJAX comment previewing and posting don't work, try removing ``#comment-anchor``. 3. Add ``onclick`` handlers to the button input tags:: 4. Copy ``comments.js`` file to a location on your server that's servable by your web server. You can find this file `here <../_static/plugins/comments/>`_. .. only:: text You can find comments.js in docs/_static/plugins/comments/. 5. Include this script tag somewhere after the ```` closing tag:: Set the url for ``comments.js`` to the url for where ``comments.js`` is located on your server from step 4. .. Note:: Note the separate closing ```` tag! It's for IE; without it, IE won't actually run the code in ``comments.js``. nofollow support ================ This implements Google's nofollow support for links in the body of the comment. If you display the link of the comment poster in your HTML template then you must add the ``rel="nofollow"`` attribute to your template as well Note to developers who are writing plugins that create comments =============================================================== Each entry has to have the following properties in order to work with comments: 1. ``absolute_path`` - the category of the entry. Example: "dev/pyblosxom" or "" 2. ``fn`` - the filename of the entry without the file extension and without the directory. Example: "staticrendering" 3. ``file_path`` - the absolute_path plus the fn. Example: "dev/pyblosxom/staticrendering" Also, if you don't want comments for an entry, add:: #nocomments 1 to the entry or set ``nocomments`` to ``1`` in the properties of the entry. """ __author__ = "Ted Leung, et al" __email__ = "pyblosxom-devel at sourceforge dot net" __version__ = "2011-12-17" __url__ = "http://pyblosxom.github.com/" __description__ = "Adds comments to a blog entry." __category__ = "comments" __license__ = "MIT" __registrytags__ = "1.4, 1.5, core" import cgi import glob import re import time import cPickle import os import codecs import sys import subprocess import traceback from email.MIMEText import MIMEText from xml.sax.saxutils import escape from Pyblosxom import tools from Pyblosxom.renderers import blosxom from Pyblosxom.tools import pwrap, pwrap_error LATEST_PICKLE_FILE = 'LATEST.cmt' def cb_start(args): request = args["request"] config = request.get_configuration() if not 'comment_dir' in config: config['comment_dir'] = os.path.join(config['datadir'],'comments') if not 'comment_ext' in config: config['comment_ext'] = 'cmt' if not 'comment_draft_ext' in config: config['comment_draft_ext'] = config['comment_ext'] if not 'comment_nofollow' in config: config['comment_nofollow'] = 0 def verify_installation(request): config = request.get_configuration() retval = True if 'comment_dir' in config and not os.path.isdir(config['comment_dir']): pwrap_error( 'The "comment_dir" property in the config file must refer ' 'to a directory') retval = False smtp_keys_defined = [] smtp_keys=[ 'comment_smtp_server', 'comment_smtp_from', 'comment_smtp_to'] for k in smtp_keys: if k in config: smtp_keys_defined.append(k) if smtp_keys_defined: for i in smtp_keys: if i not in smtp_keys_defined: pwrap_error("Missing comment SMTP property: '%s'" % i) retval = False optional_keys = [ 'comment_dir', 'comment_ext', 'comment_draft_ext', 'comment_nofollow', 'comment_disable_after_x_days'] for i in optional_keys: if not i in config: pwrap("missing optional property: '%s'" % i) if 'comment_disable_after_x_days' in config: if ((not isinstance(config['comment_disable_after_x_days'], int) or config['comment_disable_after_x_days'] <= 0)): pwrap("comment_disable_after_x_days has a non-positive " "integer value.") return retval def createhtmlmail(html, headers): """ Create a mime-message that will render HTML in popular MUAs, text in better ones Based on: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/67083 """ import MimeWriter import mimetools import cStringIO out = cStringIO.StringIO() # output buffer for our message htmlin = cStringIO.StringIO(html) text = re.sub('<.*?>', '', html) txtin = cStringIO.StringIO(text) # FIXME MimeWriter is deprecated as of Python 2.6 writer = MimeWriter.MimeWriter(out) for header,value in headers: writer.addheader(header, value) writer.addheader("MIME-Version", "1.0") writer.startmultipartbody("alternative") writer.flushheaders() subpart = writer.nextpart() subpart.addheader("Content-Transfer-Encoding", "quoted-printable") pout = subpart.startbody("text/plain", [("charset", 'us-ascii')]) mimetools.encode(txtin, pout, 'quoted-printable') txtin.close() subpart = writer.nextpart() subpart.addheader("Content-Transfer-Encoding", "quoted-printable") pout = subpart.startbody("text/html", [("charset", 'us-ascii')]) mimetools.encode(htmlin, pout, 'quoted-printable') htmlin.close() writer.lastpart() msg = out.getvalue() out.close() return msg def read_comments(entry, config): """ @param: a file entry @type: dict @returns: a list of comment dicts """ filelist = glob.glob(cmt_expr(entry, config)) comments = [] for f in filelist: comments += read_file(f, config) comments = [(cmt['cmt_time'], cmt) for cmt in comments] comments.sort() return [c[1] for c in comments] def cmt_expr(entry, config): """ Return a string containing the regular expression for comment entries @param: a file entry @type: dict @returns: a string with the directory path for the comment @param: configuratioin dictionary @type: dict @returns: a string containing the regular expression for comment entries """ cmt_dir = os.path.join(config['comment_dir'], entry['absolute_path']) cmt_expr = os.path.join(cmt_dir, entry['fn'] + '-*.' + config['comment_ext']) return cmt_expr def read_file(filename, config): """ Read comment(s) from filename @param filename: filename containing comment(s) @type filename: string @param config: the pyblosxom configuration settings @type config: dictionary @returns: a list of comment dicts """ from xml.sax import make_parser from xml.sax.handler import feature_namespaces, ContentHandler class cmt_handler(ContentHandler): def __init__(self, cmts): self.cmts = cmts def startElement(self, name, atts): if name == 'item': self.cur_cmt = {} self._data = "" def endElement(self, name): self.cur_cmt['cmt_' + name] = self._data if name == 'item': self.cmts.append(self.cur_cmt) def characters(self, content): self._data += content cmts = [] try: parser = make_parser() parser.setFeature(feature_namespaces, 0) handler = cmt_handler(cmts) parser.setContentHandler(handler) parser.parse(filename) # FIXME - bare except here--bad! except: logger = tools.get_logger() logger.error("bad comment file: %s\nerror was: %s" % (filename, traceback.format_exception(*sys.exc_info()))) return [] for cmt in cmts: # time.time() cmt['cmt_time'] = float(cmt['cmt_pubDate']) # pretty time cmt['cmt_pubDate'] = time.ctime(float(cmt['cmt_pubDate'])) cmt['cmt_w3cdate'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(cmt['cmt_time'])) cmt['cmt_date'] = time.strftime('%a %d %b %Y', time.gmtime(cmt['cmt_time'])) if cmt['cmt_link']: link = add_dont_follow('%s' % (cmt['cmt_link'], cmt['cmt_author']), config) cmt['cmt_optionally_linked_author'] = link else: cmt['cmt_optionally_linked_author'] = cmt['cmt_author'] return cmts def write_comment(request, config, data, comment, encoding): """ Write a comment @param config: dict containing pyblosxom config info @type config: dict @param data: dict containing entry info @type data: dict @param comment: dict containing comment info @type comment: dict @return: The success or failure of creating the comment. @rtype: string """ entry_list = data.get("entry_list", []) if not entry_list: return "No such entry exists." entry = data['entry_list'][0] cdir = os.path.join(config['comment_dir'], entry['absolute_path']) cdir = os.path.normpath(cdir) if not os.path.isdir(cdir): os.makedirs(cdir) cfn = os.path.join(cdir, entry['fn'] + "-" + comment['pubDate'] + "." + config['comment_draft_ext']) def make_xml_field(name, field): return "<" + name + ">" + cgi.escape(field.get(name, "")) + "\n"; filedata = '\n' % encoding filedata += "\n" for key in comment: filedata += make_xml_field(key, comment) filedata += "\n" try : cfile = codecs.open(cfn, "w", encoding) except IOError: logger = tools.get_logger() logger.error("couldn't open comment file '%s' for writing" % cfn) return "Internal error: Your comment could not be saved." cfile.write(filedata) cfile.close() # write latest pickle latest = None latest_filename = os.path.join(config['comment_dir'], LATEST_PICKLE_FILE) try: latest = open(latest_filename, "w") except IOError: logger = tools.get_logger() logger.error("couldn't open latest comment pickle for writing") return "Couldn't open latest comment pickle for writing." else: mod_time = float(comment['pubDate']) try: cPickle.dump(mod_time, latest) latest.close() except IOError: if latest: latest.close() logger = tools.get_logger() logger.error("comment may not have been saved to pickle file.") return "Internal error: Your comment may not have been saved." if ((('comment_mta_cmd' in config or 'comment_smtp_server' in config) and 'comment_smtp_to' in config)): # FIXME - removed grabbing send_email's return error message # so there's no way to know if email is getting sent or not. send_email(config, entry, comment, cdir, cfn) # figure out if the comment was submitted as a draft if config["comment_ext"] != config["comment_draft_ext"]: return "Comment was submitted for approval. Thanks!" return "Comment submitted. Thanks!" def send_email(config, entry, comment, comment_dir, comment_filename): """Send an email to the blog owner on a new comment @param config: configuration as parsed by Pyblosxom @type config: dictionary @param entry: a file entry @type config: dictionary @param comment: comment as generated by read_comments @type comment: dictionary @param comment_dir: the comment directory @type comment_dir: string @param comment_filename: file name of current comment @type comment_filename: string """ import smtplib # import the formatdate function which is in a different # place in Python 2.3 and up. try: from email.Utils import formatdate except ImportError: from rfc822 import formatdate from socket import gethostbyaddr author = escape_smtp_commands(clean_author(comment['author'])) description = escape_smtp_commands(comment['description']) ipaddress = escape_smtp_commands(comment.get('ipaddress', '?')) if 'comment_smtp_from' in config: email = config['comment_smtp_from'] else: email = escape_smtp_commands(clean_author(comment['email'])) try: curl = "%s/%s" % (config['base_url'], tools.urlencode_text(entry['file_path'])) comment_dir = os.path.join(config['comment_dir'], entry['absolute_path']) # create the message message = [] message.append("Name: %s" % author) if 'email' in comment: message.append("Email: %s" % comment['email']) if 'link' in comment: message.append("URL: %s" % comment['link']) try: host_name = gethostbyaddr(ipaddress)[0] message.append("Hostname: %s (%s)" % (host_name, ipaddress)) # FIXME - bare except here--bad! except: message.append("IP: %s" % ipaddress) message.append("Entry URL: %s" % curl) message.append("Comment location: %s" % comment_filename) message.append("\n\n%s" % description) if 'comment_mta_cmd' in config: # set the message headers message.insert(0, "") message.insert(0, "Subject: comment on %s" % curl) message.insert(0, "Date: %s" % formatdate(float(comment['pubDate']))) message.insert(0, "To: %s" % config["comment_smtp_to"]) message.insert(0, "From: %s" % email) body = '\n'.join(message).encode('utf-8') argv = [config['comment_mta_cmd'], '-s', '"comment on %s"' % curl, config['comment_smtp_to']] process = subprocess.Popen( argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) process.stdin.write(body) process.stdin.close() process.wait() stdout = process.stdout.read() stderr = process.stderr.read() tools.get_logger().debug('Ran MTA command: ' + ' '.join(argv)) tools.get_logger().debug('Received stdout: ' + stdout) tools.get_logger().debug('Received stderr: ' + stderr) # the except clause below will catch this assert stderr == '', stderr else: assert 'comment_smtp_server' in config server = smtplib.SMTP(config['comment_smtp_server']) mimemsg = MIMEText("\n".join(message).encode("utf-8"), 'plain', 'utf-8') # set the message headers mimemsg["From"] = email mimemsg["To"] = config["comment_smtp_to"] mimemsg["Date"] = formatdate(float(comment["pubDate"])) mimemsg["Subject"] = ("comment on %s" % curl) # send the message via smtp server.sendmail(from_addr=email, to_addrs=config['comment_smtp_to'], msg=mimemsg.as_string()) server.quit() except Exception, e: tools.get_logger().error("error sending email: %s" % traceback.format_exception(*sys.exc_info())) def check_comments_disabled(config, entry): disabled_after_x_days = config.get("comment_disable_after_x_days", 0) if not isinstance(disabled_after_x_days, int): # FIXME - log an error? return False if disabled_after_x_days <= 0: # FIXME - log an error? return False if not entry.has_key('mtime'): return False entry_age = (time.time() - entry['mtime']) / (60 * 60 * 24) if entry_age > disabled_after_x_days: return True return False def clean_author(s): """ Guard against blasterattacko style attacks that embedd SMTP commands in author field. If author field is more than one line, reduce to one line @param the string to be checked @type string @returns the sanitized string """ return s.splitlines()[0] def escape_smtp_commands(s): """ Guard against blasterattacko style attacks that embed SMTP commands by using an HTML span to make the command syntactically invalid to SMTP but renderable by HTML @param the string to be checked @type string @returns the sanitized string """ def repl_fn(mo): return ''+mo.group(0)+'' s = re.sub('([Tt]o:.*)',repl_fn,s) s = re.sub('([Ff]rom:.*)',repl_fn,s) s = re.sub('([Ss]ubject:.*)',repl_fn,s) return s def sanitize(body): """ This code shamelessly lifted from Sam Ruby's mombo/post.py """ body=re.sub(r'\s+$','',body) body=re.sub('\r\n?','\n', body) # naked urls become hypertext links body=re.sub('(^|[\\s.:;?\\-\\]<])' + '(http://[-\\w;/?:@&=+$.!~*\'()%,#]+[\\w/])' + '(?=$|[\\s.:;?\\-\\[\\]>])', '\\1\\2',body) # html characters used in text become escaped body = escape(body) # passthru , , , ,

,
,

, # , , , , , , ,

, 
    # , , , , , 
    ,
      ,
    1. body = re.sub('<a href="([^"]*)">([^&]*)</a>', '\\2', body) body = re.sub('<a href=\'([^\']*)\'>([^&]*)</a>', '\\2', body) body = re.sub('<em>([^&]*)</em>', '\\1', body) body = re.sub('<i>([^&]*)</i>', '\\1', body) body = re.sub('<b>([^&]*)</b>', '\\1', body) body = re.sub('<blockquote>([^&]*)</blockquote>', '
      \\1
      ', body) body = re.sub('<br\s*/?>\n?', '\n', body) body = re.sub('<abbr>([^&]*)</abbr>', '\\1', body) body = re.sub('<acronym>([^&]*)</acronym>', '\\1', body) body = re.sub('<big>([^&]*)</big>', '\\1', body) body = re.sub('<cite>([^&]*)</cite>', '\\1', body) body = re.sub('<code>([^&]*)</code>', '\\1', body) body = re.sub('<dfn>([^&]*)</dfn>', '\\1', body) body = re.sub('<kbd>([^&]*)</kbd>', '\\1', body) body = re.sub('<pre>([^&]*)</pre>', '
      \\1
      ', body) body = re.sub('<small>([^&]*)</small>', '\\1', body) body = re.sub('<strong>([^&]*)</strong>', '\\1', body) body = re.sub('<sub>([^&]*)</sub>', '\\1', body) body = re.sub('<sup>([^&]*)</sup>', '\\1', body) body = re.sub('<tt>([^&]*)</tt>', '\\1', body) body = re.sub('<var>([^&]*)</var>', '\\1', body) # handle lists body = re.sub('<ul>\s*', '
        ', body) body = re.sub('</ul>\s*', '
      ', body) body = re.sub('<ol>\s*', '
        ', body) body = re.sub('</ol>\s*', '
      ', body) body = re.sub('<li>([^&]*)</li>\s*', '
    2. \\1
    3. ', body) body = re.sub('<li>', '
    4. ', body) body = re.sub('</?p>', '\n\n', body).strip() # wiki like support: _em_, *b*, [url title] body = re.sub(r'\b_(\w.*?)_\b', r'\1', body) body = re.sub(r'\*(\w.*?)\*', r'\1', body) body = re.sub(r'\[(\w+:\S+\.gif) (.*?)\]', r'\2', body) body = re.sub(r'\[(\w+:\S+\.jpg) (.*?)\]', r'\2', body) body = re.sub(r'\[(\w+:\S+\.png) (.*?)\]', r'\2', body) body = re.sub(r'\[(\w+:\S+) (.*?)\]', r'\2', body).strip() # unordered lists: consecutive lines starting with spaces and an asterisk chunk = re.compile(r'^( *\*.*(?:\n *\*.*)+)',re.M).split(body) for i in range(1, len(chunk), 2): html, stack = '', [''] for indent, line in re.findall(r'( +)\* +(.*)', chunk[i]) + [('','')]: if indent > stack[-1]: stack, html = stack + [indent], html + '
        \r' while indent\r' if line: html += '
      • ' + line + '
      • \r' chunk[i] = html # white space chunk = re.split('\n\n+', ''.join(chunk)) body = re.sub('\n', '
        \n', body) body = re.compile('

        (

          .*?
        )\r

        ?', re.M).sub(r'\1', body) body = re.compile('

        (

        .*?
        )

        ?', re.M).sub(r'\1', body) body = re.sub('\r', '\n', body) body = re.sub(' +', '  ', body) return body def dont_follow(mo): return '' def add_dont_follow(s, config): url_pat_str = ']+)>' url_pat = re.compile(url_pat_str) if config['comment_nofollow'] == 1: return url_pat.sub(dont_follow, s) else: return s def cb_prepare(args): """ Handle comment related HTTP POST's. @param request: pyblosxom request object @type request: a Pyblosxom request object """ request = args["request"] form = request.get_http()['form'] config = request.get_configuration() data = request.get_data() pyhttp = request.get_http() # first we check to see if we're going to print out comments # the default is not to show comments data['display_comment_default'] = False # check to see if they have "showcomments=yes" in the querystring qstr = pyhttp.get('QUERY_STRING', None) if qstr != None: parsed_qs = cgi.parse_qs(qstr) if 'showcomments' in parsed_qs: if parsed_qs['showcomments'][0] == 'yes': data['display_comment_default'] = True # check to see if the bl_type is "file" if "bl_type" in data and data["bl_type"] == "file": data["bl_type_file"] = "yes" data['display_comment_default'] = True # second, we check to see if they're posting a comment and we # need to write the comment to disk. posting = (('ajax' in form and form['ajax'].value == 'post') or not "preview" in form) if (("title" in form and "author" in form and "body" in form and posting)): entry = data.get("entry_list", []) if len(entry) == 0: data["rejected"] = True data["comment_message"] = "No such entry exists." return entry = entry[0] if check_comments_disabled(config, entry): data["rejected"] = True data["comment_message"] = "Comments for that entry are disabled." return encoding = config.get('blog_encoding', 'utf-8') decode_form(form, encoding) body = form['body'].value author = form['author'].value title = form['title'].value url = ('url' in form and [form['url'].value] or [''])[0] # sanitize incoming data body = sanitize(body) author = sanitize(author) title = sanitize(title) # it doesn't make sense to add nofollow to link here, but we should # escape it. If you don't like the link escaping, I'm not attached # to it. cmt_time = time.time() w3cdate = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(cmt_time)) date = time.strftime('%a %d %b %Y', time.gmtime(cmt_time)) cdict = {'title': title, 'author': author, 'pubDate': str(cmt_time), 'w3cdate': w3cdate, 'date': date, 'link': massage_link(url), 'source': '', 'description': add_dont_follow(body, config)} keys = form.keys() keys = [k for k in keys if k not in ["title", "url", "author", "body", "description"]] for k in keys: cdict[k] = form[k].value if 'email' in form: cdict['email'] = form['email'].value cdict['ipaddress'] = pyhttp.get('REMOTE_ADDR', '') # record the comment's timestamp, so we can extract it and send it # back alone, without the rest of the page, if the request was ajax. data['cmt_time'] = float(cdict['pubDate']) argdict = {"request": request, "comment": cdict} reject = tools.run_callback("comment_reject", argdict, donefunc=lambda x:x != 0) if (((isinstance(reject, tuple) or isinstance(reject, list)) and len(reject) == 2)): reject_code, reject_message = reject else: reject_code, reject_message = reject, "Comment rejected." if reject_code == 1: data["comment_message"] = reject_message data["rejected"] = True else: data["comment_message"] = write_comment(request, config, data, \ cdict, encoding) class AjaxRenderer(blosxom.Renderer): """ The renderer used when responding to AJAX requests to preview and post comments. Renders *only* the comment and comment-preview divs. """ def __init__(self, request, data): out = request.get_configuration().get('stdoutput', sys.stdout) blosxom.Renderer.__init__(self, request, out) self._ajax_type = request.get_http()['form']['ajax'].value self._config = request.get_configuration() self._data = data def _should_output(self, entry, template_name): """ Return whether we should output this template, depending on the type of ajax request we're responding to. """ if (self._ajax_type == 'post' and template_name == 'story'): entry['comments'] = read_comments(entry, self._config) return False if self._ajax_type == 'preview' and template_name == 'comment-preview': return True elif (self._ajax_type == 'post' and template_name == 'comment' and round(self._data.get('cmt_time', 0)) == round(entry['cmt_time'])): return True else: return False def render_template(self, entry, template_name, override=0): if self._should_output(entry, template_name): return blosxom.Renderer.render_template( self, entry, template_name, override) else: return "" def _output_flavour(self, entry, template_name): if self._should_output(entry, template_name): blosxom.Renderer._output_flavour(self, entry, template_name) def cb_renderer(args): request = args['request'] config = request.get_configuration() http = request.get_http() form = http['form'] # intercept ajax requests with our renderer if 'ajax' in form and http.get('REQUEST_METHOD', '') == 'POST': data = '&'.join(['%s=%s' % (arg.name, arg.value) for arg in form.list]) tools.get_logger().info('AJAX request: %s' % data) return AjaxRenderer(request, request.get_data()) def cb_handle(args): request = args['request'] config = request.get_configuration() # serve /comments.js for ajax comments if request.get_http()['PATH_INFO'] == '/comments.js': response = request.get_response() response.add_header('Content-Type', 'text/javascript') # look for it in each of the plugin_dirs for dir in config['plugin_dirs']: comments_js = os.path.join(dir, 'comments.js') if os.path.isfile(comments_js): f = open(comments_js, 'r') response.write(f.read()) f.close() return True def massage_link(linkstring): """Don't allow html in the link string. Prepend http:// if there isn't already a protocol.""" for c in "<>'\"": linkstring = linkstring.replace(c, '') if linkstring and linkstring.find(':') == -1: linkstring = 'http://' + linkstring return linkstring def decode_form(d, blog_encoding): """Attempt to decode the POST data with a few likely character encodings. If the Content-type header in the HTTP request includes a charset, try that first. Then, try the encoding specified in the pybloscom config file. if Those fail, try iso-8859-1, utf-8, and ascii. """ encodings = [blog_encoding, 'iso-8859-1', 'utf-8', 'ascii'] charset = get_content_type_charset() if charset: encodings = [charset] + encodings for key in d.keys(): for e in encodings: try: d[key].value = d[key].value.decode(e) break # FIXME - bare except--bad! except: continue def get_content_type_charset(): """Extract and return the charset part of the HTTP Content-Type header. Returns None if the Content-Type header doesn't specify a charset. """ content_type = os.environ.get('CONTENT_TYPE', '') match = re.match('.+; charset=([^;]+)', content_type) if match: return match.group(1) else: return None def cb_head(args): renderer = args['renderer'] template = args['template'] if 'comment-head' in renderer.flavour and len(renderer.getContent()) == 1: args['template'] = renderer.flavour['comment-head'] # expand all of entry vars for expansion entry = args['entry'] single_entry = entry['entry_list'][0] single_entry['title'] # force lazy evaluation entry.update(single_entry) args['entry'] = entry return template def cb_story(args): """For single entry requests, when commenting is enabled and the flavour has a comment-story template, append its contents to the story template's. """ renderer = args['renderer'] entry = args['entry'] template = args['template'] request = args["request"] data = request.get_data() config = request.get_configuration() # FIXME - entry is currently broken and doesn't support "in" if entry.has_key('absolute_path') and not entry.has_key('nocomments'): entry['comments'] = read_comments(entry, config) entry['num_comments'] = len(entry['comments']) if ((len(renderer.get_content()) == 1 and 'comment-story' in renderer.flavour and data['display_comment_default'])): template = renderer.flavour.get('comment-story', '') args['template'] = args['template'] + template return template def build_preview_comment(form, entry, config): """Build a prevew comment by brute force @param form: cgi form object (or compatible) @type form: Dictionary of objects with a .value propery @param entry: pyblosxom entry object @type entry: pyblosxom entry object @param config: the pyblosxom configuration settings @type config: dictionary @return: the comment HTML, a string """ c = {} # required fields try: c['cmt_time'] = float(time.time()) c['cmt_author'] = form['author'].value c['cmt_title'] = form['title'].value c['cmt_item'] = sanitize(form['body'].value) cmt_time = time.time() c['cmt_pubDate'] = time.ctime(cmt_time) c['cmt_w3cdate'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', (time.gmtime(cmt_time))) c['cmt_date'] = time.strftime('%a %d %b %Y', time.gmtime(cmt_time)) c['cmt_description'] = sanitize(form['body'].value) # optional fields c['cmt_optionally_linked_author'] = c['cmt_author'] if 'url' in form: c['cmt_link'] = massage_link(form['url'].value) if c['cmt_link']: link = add_dont_follow('%s' % (c['cmt_link'], c['cmt_author']), config) c['cmt_optionally_linked_author'] = link if 'openid_url' in form: c['cmt_openid_url'] = massage_link(form['openid_url'].value) if 'email' in form: c['cmt_email'] = form['email'].value except KeyError, e: c['cmt_error'] = 'Missing value: %s' % e entry.update(c) return c def cb_story_end(args): renderer = args['renderer'] entry = args['entry'] template = args['template'] request = args["request"] data = request.get_data() form = request.get_http()['form'] config = request.get_configuration() # FIXME - entry is currently broken and doesn't support "in" if ((entry.has_key('absolute_path') and len(renderer.get_content()) == 1 and 'comment-story' in renderer.flavour and not entry.has_key('nocomments') and data['display_comment_default'])): output = [] if entry.get('comments', []): comment_entry_base = dict(entry) del comment_entry_base['comments'] for comment in entry['comments']: comment_entry = dict(comment_entry_base) comment_entry.update(comment) output.append(renderer.render_template(comment_entry, 'comment')) if (('preview' in form and 'comment-preview' in renderer.flavour)): com = build_preview_comment(form, entry, config) output.append(renderer.render_template(com, 'comment-preview')) elif 'rejected' in data: rejected = build_preview_comment(form, entry, config) msg = '%s' % data["comment_message"] rejected['cmt_description'] = msg rejected['cmt_description_escaped'] = escape(msg) output.append(renderer.render_template(rejected, 'comment')) if not check_comments_disabled(config, entry): output.append(renderer.render_template(entry, 'comment-form')) args['template'] = template + "".join(output) return template pyblosxom-1.5.3/Pyblosxom/plugins/conditionalhttp.py000066400000000000000000000063471217606004600230220ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2004, 2005 Wari Wahab # Copyright (c) 2010, 2011 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= This plugin can help save bandwidth for low bandwidth quota sites. This is done by outputing cache friendly HTTP header tags like Last-Modified and ETag. These values are calculated from the first entry returned by ``entry_list``. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. In your ``config.py`` file, add ``Pyblosxom.plugins.conditionalhttp`` to the ``load_plugins`` variable. """ __author__ = "Wari Wahab" __email__ = "pyblosxom at wari dot per dot sg" __version__ = "2011-10-22" __url__ = "http://pyblosxom.github.com/" __description__ = "Allows browser-side caching with if-not-modified-since." __category__ = "headers" __license__ = "MIT" __registrytags__ = "1.4, 1.5, core" import time import os import cPickle from Pyblosxom import tools def verify_installation(request): # This should just work--no configuration needed. return True def cb_prepare(args): request = args["request"] data = request.get_data() config = request.get_configuration() http = request.get_http() entry_list = data["entry_list"] renderer = data["renderer"] if entry_list and entry_list[0].has_key('mtime'): # FIXME - this should be generalized to a callback for updated # things. mtime = entry_list[0]['mtime'] latest_cmtime = - 1 if 'comment_dir' in config: latest_filename = os.path.join(config['comment_dir'], 'LATEST.cmt') if os.path.exists(latest_filename): latest = open(latest_filename) latest_cmtime = cPickle.load(latest) latest.close() if latest_cmtime > mtime: mtime = latest_cmtime # Get our first file timestamp for ETag and Last Modified # Last-Modified: Wed, 20 Nov 2002 10:08:12 GMT # ETag: "2bdc4-7b5-3ddb5f0c" last_modified = time.strftime( '%a, %d %b %Y %H:%M:%S GMT', time.gmtime(mtime)) if (((http.get('HTTP_IF_NONE_MATCH', '') == '"%s"' % mtime) or (http.get('HTTP_IF_NONE_MATCH', '') == '%s' % mtime) or (http.get('HTTP_IF_MODIFIED_SINCE', '') == last_modified))): renderer.add_header('Status', '304 Not Modified') renderer.add_header('ETag', '"%s"' % mtime) renderer.add_header('Last-Modified', '%s' % last_modified) # whack the content here so that we don't then go render it renderer.set_content(None) renderer.render() # Log request as "We have it!" tools.run_callback("logrequest", {'filename': config.get('logfile', ''), 'return_code': '304', 'request': request}) return renderer.add_header('ETag', '"%s"' % mtime) renderer.add_header('Last-Modified', '%s' % last_modified) pyblosxom-1.5.3/Pyblosxom/plugins/disqus.py000066400000000000000000000066041217606004600211230ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2011 Blake Winton # Copyright (c) 2011 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= Plugin for adding Disqus comments. It's not hard to do this by hand, but this plugin makes it so that comments only show up when you're looking at a single blog entry. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. In your ``config.py`` file, add ``Pyblosxom.plugins.disqus`` to the ``load_plugins`` variable. 2. Set ``disqus_shortname`` in your ``config.py`` file. This comes from Disqus when you set up your account. For help, see http://docs.disqus.com/help/2/ . 3. Save the ``comment_form`` template into your html flavour. comment_form template::
        blog comments powered by Disqus """ __author__ = "Blake Winton" __email__ = "willg at bluesock dot org" __version__ = "2011-12-12" __url__ = "http://pyblosxom.github.com/" __description__ = "Lets me use Disqus for comments." __category__ = "comments" __license__ = "MIT" __registrytags__ = "1.5, core" import os from Pyblosxom.tools import pwrap_error def verify_installation(request): config = request.get_configuration() if not config.has_key('disqus_shortname'): pwrap_error( "missing required config property 'disqus_shortname' which" "is necessary to determine which disqus site to link to.") return False return True def cb_story(args): renderer = args['renderer'] entry = args['entry'] template = args['template'] request = args["request"] config = request.get_configuration() did = os.path.realpath(entry['filename']) did = did.replace(entry['datadir'], '') did = os.path.splitext(did)[0] entry['disqus_id'] = did entry['disqus_shortname'] = config.get( 'disqus_shortname', 'missing disqus_shortname') # This uses the same logic as comments.py for determining when # to show the comments. if ((entry.has_key('absolute_path') and len(renderer.getContent()) == 1 and 'comment_form' in renderer.flavour and not entry.has_key('nocomments'))): # entry.getId() contains the path. output = [] renderer.output_template(output, entry, 'comment_form') args['template'] = template + "".join(output) return args pyblosxom-1.5.3/Pyblosxom/plugins/entrytitle.py000066400000000000000000000040521217606004600220110ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2010, 2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= If Pyblosxom is rendering a single entry (i.e. entry_list has 1 item in it), then this populates the ``entry_title`` variable for the header template. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.entrytitle`` to the ``load_plugins`` list of your ``config.py`` file. 2. Configure as documented below. Configuration ============= To use, add the ``entry_title`` variable to your header template in the ```` area. Example:: <title>$(blog_title)$(entry_title) The default ``$(entry_title)`` starts with a ``::`` and ends with the title of the entry. For example:: :: Guess what happened today You can set the entry title template in the configuration properties with the ``entry_title_template`` variable:: config["entry_title_template"] = ":: %(title)s" .. Note:: ``%(title)s`` is a Python string formatter that gets filled in with the entry title. """ __author__ = "Will Kahn-Greene" __email__ = "willg at bluesock dot org" __version__ = "2011-10-22" __url__ = "http://pyblosxom.github.com/" __description__ = "Puts entry title in page title." __category__ = "date" __license__ = "MIT" __registrytags__ = "1.4, 1.5, core" def verify_installation(request): # This needs no verification. return True def cb_head(args): req = args["request"] entry = args["entry"] data = req.get_data() entry_list = data.get("entry_list", []) if len(entry_list) == 1: config = req.get_configuration() tmpl = config.get("entry_title_template", ":: %(title)s") entry["entry_title"] = (tmpl % {"title": entry_list[0].get("title", "No title")}) return args pyblosxom-1.5.3/Pyblosxom/plugins/firstdaydiv.py000066400000000000000000000047301217606004600221410ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2004, 2005 Blake Winton # Copyright (c) 2010, 2011 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= Adds a token which allows you to differentiate between the first day of entries in a series of entries to be displayed from the other days. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. In your ``config.py`` file, add ``Pyblosxom.plugins.firstdaydiv`` to the ``load_plugins`` list. 2. (optional) Set the ``firstDayDiv`` config variable. This defaults to ``blosxomFirstDayDiv``. Example:: py['firstDayDiv'] = 'blosxomFirstDayDiv' Usage ===== This denotes the first day with the css class set in the ``firstDayDiv`` config variable. This is available in the ``$(dayDivClass)`` template variable. You probably want to put this in your ``date_head`` template in a ```` tag. For example, in your ``date_head``, you could have::
        $date and in your ``date_foot``, you'd want to close that ``
        `` off::
        Feel free to use this in other ways. """ __author__ = "Blake Winton" __email__ = "bwinton@latte.ca" __version__ = "2011-10-22" __url__ = "http://pyblosxom.github.com/" __description__ = ("Adds a token which tells us whether " "we're the first day being displayed or not.") __category__ = "date" __license__ = "MIT" __registrytags__ = "1.4, 1.5, core" class PyFirstDate: """ This class stores the state needed to determine whether we're supposed to return the first-day-div class or the not-the-first-day-div class. """ def __init__(self, request): config = request.get_configuration() self._day_div = config.get("firstDayDiv", "blosxomFirstDayDiv") self._count = 0 def __str__(self): if self._count == 0: self._count = 1 else: self._day_div = "blosxomDayDiv" return self._day_div def cb_prepare(args): """ Populate the ``Pyblosxom.pyblosxom.Request`` with an instance of the ``PyFirstDate`` class in the ``dayDivClass`` key. """ request = args["request"] data = request.get_data() data["dayDivClass"] = PyFirstDate(request) pyblosxom-1.5.3/Pyblosxom/plugins/flavourfiles.py000066400000000000000000000114311217606004600223060ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2010, 2011 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= This plugin allows flavour templates to use file urls that will resolve to files in the flavour directory. Those files will then get served by Pyblosxom. This solves the problem that flavour packs are currently difficult to package, install, and maintain because static files (images, css, js, ...) have to get put somewhere else and served by the web server and this is difficult to walk a user through. It handles urls that start with ``flavourfiles/``, then the flavour name, then the path to the file. For example:: http://example.com/blog/flavourfiles/html/style.css .. Note:: This plugin is very beta! It's missing important functionality, probably has bugs, and hasn't been well tested! Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.flavourfiles`` to the ``load_plugins`` list of your ``config.py`` file. 2. In templates you want to use flavourfiles, use urls like this:: $(base_url)/flavourfiles/$(flavour)/path-to-file For example:: The ``$(base_url)`` will get filled in with the correct url root. The ``$(flavour)`` will get filled in with the name of the url. This allows users to change the flavour name without having to update all the templates. """ __author__ = "Will Kahn-Greene" __email__ = "willg at bluesock dot org" __version__ = "2011-10-22" __url__ = "http://pyblosxom.github.com/" __description__ = "Serves static files related to flavours (css, js, ...)" __license__ = "MIT License" __registrytags__ = "1.5, core, experimental" import os import mimetypes import sys from Pyblosxom.renderers import base TRIGGER = "/flavourfiles/" class FileRenderer(base.RendererBase): def set_filepath(self, filepath): self.filepath = filepath def render(self, header=True): if not os.path.exists(self.filepath): self.render_404() self.rendered = 1 return # FIXME - handle e-tag/etc conditional stuff here try: fp = open(self.filepath, "r") except OSError: # FIXME - this could be a variety of issues, but is # probably a permission denied error. should catch the # error message and send it to the 403 page. self.render_403() self.rendered = 1 return # mimetype mimetype = mimetypes.guess_type(self.filepath) if mimetype: mimetype = mimetype[0] if mimetype is None: mimetype = "application/octet-stream" self.add_header('Content-type', mimetype) # content length length = os.stat(self.filepath)[6] self.add_header('Content-Length', str(length)) if header: self.show_headers() self.write(fp.read()) fp.close() self.rendered = 1 def render_403(self): resp = self._request.getResponse() resp.set_status("403 Forbidden") def render_404(self): resp = self._request.getResponse() resp.set_status("404 Not Found") def cb_handle(args): """This is the flavour file handler. This handles serving static files related to flavours. It handles paths like /flavourfiles//. It calls the prepare callback to do any additional preparation before rendering the entries. Then it tells the renderer to render the entries. :param request: the request object. """ request = args["request"] path_info = request.get_http()["PATH_INFO"] if not path_info.startswith(TRIGGER): return config = request.get_configuration() data = request.get_data() # get the renderer object rend = FileRenderer(request, config.get("stdoutput", sys.stdout)) data['renderer'] = rend filepath = path_info.replace(TRIGGER, "") while filepath.startswith(("/", os.sep)): filepath = filepath[1:] if not filepath: rend.render_404() return filepath = filepath.split("/") flavour = filepath[0] filepath = "/".join(filepath[1:]) root = config.get("flavourdir", config["datadir"]) root = os.path.join(root, flavour + ".flav") filepath = os.path.join(root, filepath) filepath = os.path.normpath(filepath) if not filepath.startswith(root) or not os.path.exists(filepath): rend.render_404() return rend.set_filepath(filepath) rend.render() return 1 pyblosxom-1.5.3/Pyblosxom/plugins/magicword.py000066400000000000000000000067561217606004600215770ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2005 Nathaniel Gray # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= This is about the simplest anti-comment-spam measure you can imagine, but it's probably effective enough for all but the most popular blogs. Here's how it works. You pick a question and put a field on your comment for for the answer to the question. If the user answers it correctly, his comment is accepted. Otherwise it's rejected. Here's how it works: Install ======= Requires the ``comments`` plugin. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.magicword`` to the ``load_plugins`` list in your ``config.py`` file. 2. Configure as documented below. Configure ========= Here's an example of what to put in config.py:: py['mw_question'] = "What is the first word in this sentence?" py['mw_answer'] = "what" Note that ``mw_answer`` must be lowercase and without leading or trailing whitespace, even if you expect the user to enter capital letters. Their input will be lowercased and stripped before it is compared to ``mw_answer``. Here's what you put in your ``comment-form`` file:: The Magic Word:
        $(mw_question)

        It's important that the name of the input field is exactly "magicword". Security note ============= In order for this to be secure(ish) you need to protect your ``config.py`` file. This is a good idea anyway! If your ``config.py`` file is in your web directory, protect it from being seen by creating or modifying a ``.htaccess`` file in the directory where ``config.py`` lives with the following contents:: Order allow,deny deny from all This will prevent people from being able to view ``config.py`` by browsing to it. """ __author__ = "Nathaniel Gray" __email__ = "n8gray /at/ caltech /dot/ edu" __version__ = "2011-10-28" __url__ = "http://pyblosxom.github.com/" __description__ = "Magic word method for reducing comment spam" __category__ = "comments" __license__ = "MIT" __registrytags__ = "1.4, 1.5, core" from Pyblosxom.tools import pwrap_error def verify_installation(request): config = request.get_configuration() status = True if not 'mw_question' in config: pwrap_error("Missing required property: mw_question") status = False if not 'mw_answer' in config: pwrap_error("Missing required property: mw_answer") return False a = config["mw_answer"] if a != a.strip().lower(): pwrap_error("mw_answer must be lowercase, without leading " "or trailing whitespace") return False return status def cb_comment_reject(args): """ Verifies that the commenter answered with the correct magic word. @param args: a dict containing: pyblosxom request, comment dict @type config: C{dict} @return: True if the comment should be rejected, False otherwise @rtype: C{bool} """ request = args['request'] form = request.get_form() config = request.get_configuration() try: mw = form["magicword"].value.strip().lower() if mw == config["mw_answer"]: return False except KeyError: pass return True pyblosxom-1.5.3/Pyblosxom/plugins/markdown_parser.py000066400000000000000000000063371217606004600230140ustar00rootroot00000000000000####################################################################### # Copyright (C) 2005, 2011 Benjamin Mako Hill # Copyright (c) 2009, 2010, seanh # Copyright (c) 2011 Blake Winton # Copyright (c) 2011 Will Kahn-Greene # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at # your option) any later version. ####################################################################### """ Summary ======= A Markdown entry formatter for Pyblosxom. Install ======= Requires python-markdown to be installed. See http://www.freewisdom.org/projects/python-markdown/ for details. 1. Add ``Pyblosxom.plugins.markdown_parser`` to the ``load_plugins`` list in your ``config.py`` file Usage ===== Write entries using Markdown markup. Entry filenames can end in ``.markdown``, ``.md``, and ``.mkd``. You can also configure this as your default preformatter for ``.txt`` files by configuring it in your config file as follows:: py['parser'] = 'markdown' Additionally, you can do this on an entry-by-entry basis by adding a ``#parser markdown`` line in the metadata section. For example:: My Little Blog Entry #parser markdown My main story... """ __author__ = ( "Benjamin Mako Hill , seanh , " "Blake Winton ") __email__ = "" __version__ = "2011-11-02" __url__ = "http://pyblosxom.github.com/" __description__ = "Markdown entry parser" __category__ = "text" __license__ = "GPLv3 or later" __registrytags__ = "1.5, core" PREFORMATTER_ID = "markdown" FILENAME_EXTENSIONS = ("markdown", "md", "mkd") import markdown from Pyblosxom import tools md = markdown.Markdown(output_format="html4", extensions=["footnotes", "codehilite"]) def verify_installation(args): # no configuration needed return 1 def cb_entryparser(args): for ext in FILENAME_EXTENSIONS: args[ext] = readfile return args def cb_preformat(args): if args.get("parser", None) == PREFORMATTER_ID: return parse("".join(args["story"]), args["request"]) def parse(story, request): body = md.convert(story.decode("utf-8")).encode("utf-8") md.reset() return body def readfile(filename, request): logger = tools.get_logger() logger.info("Calling readfile for %s", filename) entry_data = {} lines = open(filename).readlines() if len(lines) == 0: return {"title": "", "body": ""} title = lines.pop(0).strip() # absorb meta data while lines and lines[0].startswith("#"): meta = lines.pop(0) # remove the hash meta = meta[1:].strip() meta = meta.split(" ", 1) # if there's no value, we append a 1 if len(meta) == 1: meta.append("1") entry_data[meta[0].strip()] = meta[1].strip() body = parse("".join(lines), request) entry_data["title"] = title entry_data["body"] = body # Call the postformat callbacks tools.run_callback("postformat", {"request": request, "entry_data": entry_data}) logger.info("Returning %r", entry_data) return entry_data pyblosxom-1.5.3/Pyblosxom/plugins/no_old_comments.py000066400000000000000000000031231217606004600227630ustar00rootroot00000000000000####################################################################### # Copyright (c) 2006 Blake Winton # # Released into the Public Domain. ####################################################################### """ Summary ======= This plugin implements the ``comment_reject`` callback of the comments plugin. If someone tries to comment on an entry that's older than 28 days, the comment is rejected. Install ======= Requires the ``comments`` plugin. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.no_old_comments`` to the ``load_plugins`` list in your ``config.py`` file. Revisions ========= 1.0 - August 5th 2006: First released. """ __author__ = "Blake Winton" __email__ = "bwinton+blog@latte.ca" __version__ = "2011-10-28" __url__ = "http://pyblosxom.github.com/" __description__ = "Prevent comments on entries older than a month." __category__ = "comments" __license__ = "Public Domain" __registrytags__ = "1.4, 1.5, core" import time from Pyblosxom import tools def verify_installation(request): return True def cb_comment_reject(args): req = args["request"] comment = args["comment"] blog_config = req.get_configuration() max_age = blog_config.get('no_old_comments_max_age', 2419200) data = req.get_data() entry = data['entry_list'][0] logger = tools.get_logger() logger.debug('%s -> %s', entry['mtime'], comment) if ((time.time() - entry['mtime']) >= max_age): logger.info('Entry too old, comment not posted!') return 1 logger.info('Entry ok, comment posted!') return 0 pyblosxom-1.5.3/Pyblosxom/plugins/pages.py000066400000000000000000000206641217606004600207140ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2002-2011 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= Blogs don't always consist solely of blog entries. Sometimes you want to add other content to your blog that's not a blog entry. For example, an "about this blog" page or a page covering a list of your development projects. This plugin allows you to have pages served by Pyblosxom that aren't blog entries. Additionally, this plugin allows you to have a non-blog-entry front page. This makes it easier to use Pyblosxom to run your entire website. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. add ``Pyblosxom.plugins.pages`` to the ``load_plugins`` list in your ``config.py`` file. 2. configure the plugin using the configuration variables below ``pagesdir`` This is the directory that holds the pages files. For example, if you wanted your pages in ``/home/foo/blog/pages/``, then you would set it to:: py["pagesdir"] = "/home/foo/blog/pages/" If you have ``blogdir`` defined in your ``config.py`` file which holds your ``datadir`` and ``flavourdir`` directories, then you could set it to:: py["pagesdir"] = os.path.join(blogdir, "pages") ``pages_trigger`` (optional) Defaults to ``pages``. This is the url trigger that causes the pages plugin to look for pages. py["pages_trigger"] = "pages" ``pages_frontpage`` (optional) Defaults to False. If set to True, then pages will show the ``frontpage`` page for the front page. This requires you to have a ``frontpage`` file in your pages directory. The extension for this file works the same way as blog entries. So if your blog entries end in ``.txt``, then you would need a ``frontpage.txt`` file. Example:: py["pages_frontpage"] = True Usage ===== Pages looks for urls that start with the trigger ``pages_trigger`` value as set in your ``config.py`` file. For example, if your ``pages_trigger`` was ``pages``, then it would look for urls like this:: /pages/blah /pages/blah.html and pulls up the file ``blah.txt`` [1]_ which is located in the path specified in the config file as ``pagesdir``. If the file is not there, it kicks up a 404. .. [1] The file ending (the ``.txt`` part) can be any file ending that's valid for entries on your blog. For example, if you have the textile entryparser installed, then ``.txtl`` is also a valid file ending. Template ======== pages formats the page using the ``pages`` template. So you need a ``pages`` template in the flavours that you want these pages to be rendered in. I copy my ``story`` template and remove some bits. For example, if you're using the html flavour and that is stored in ``/home/foo/blog/flavours/html.flav/``, then you could copy the ``story`` file in that directory to ``pages`` and that would become your ``pages`` template. Python code blocks ================== pages handles evaluating python code blocks. Enclose python code in ``<%`` and ``%>``. The assumption is that only you can edit your pages files, so there are no restrictions (security or otherwise). For example:: <% print "testing" %> <% x = { "apple": 5, "banana": 6, "pear": 4 } for mem in x.keys(): print "
      • %s - %s
      • " % (mem, x[mem]) %> The request object is available in python code blocks. Reference it by ``request``. Example:: <% config = request.get_configuration() print "your datadir is: %s" % config["datadir"] %> """ __author__ = "Will Kahn-Greene" __email__ = "willg at bluesock dot org" __version__ = "2011-10-22" __url__ = "http://pyblosxom.github.com/" __description__ = ( "Allows you to include non-blog-entry files in your site and have a " "non-blog-entry front page.") __category__ = "content" __license__ = "MIT" __registrytags__ = "1.4, 1.5, core" import os import StringIO import sys import os.path from Pyblosxom.entries.fileentry import FileEntry from Pyblosxom import tools from Pyblosxom.tools import pwrap_error TRIGGER = "pages" INIT_KEY = "pages_pages_file_initiated" def verify_installation(req): config = req.get_configuration() retval = True if not 'pagesdir' in config: pwrap_error("'pagesdir' property is not set in the config file.") retval = False elif not os.path.isdir(config["pagesdir"]): pwrap_error( "'pagesdir' directory does not exist. %s" % config["pagesdir"]) retval = False return retval def cb_date_head(args): req = args["request"] data = req.get_data() if INIT_KEY in data: args["template"] = "" return args def cb_date_foot(args): return cb_date_head(args) def eval_python_blocks(req, body): localsdict = {"request": req} globalsdict = {} old_stdout = sys.stdout old_stderr = sys.stderr try: start = 0 while body.find("<%", start) != -1: start = body.find("<%") end = body.find("%>", start) if start != -1 and end != -1: codeblock = body[start + 2:end].lstrip() sys.stdout = StringIO.StringIO() sys.stderr = StringIO.StringIO() try: exec codeblock in localsdict, globalsdict except Exception, e: print "ERROR in processing: %s" % e output = sys.stdout.getvalue() + sys.stderr.getvalue() body = body[:start] + output + body[end + 2:] finally: sys.stdout = old_stdout sys.stderr = old_stderr return body def is_frontpage(pyhttp, config): if not config.get("pages_frontpage"): return False pathinfo = pyhttp.get("PATH_INFO", "") if pathinfo == "/": return True path, ext = os.path.splitext(pathinfo) if path == "/index" and not ext in [".rss20", ".atom", ".rss"]: return True return False def is_trigger(pyhttp, config): trigger = config.get("pages_trigger", TRIGGER) if not trigger.startswith("/"): trigger = "/" + trigger return pyhttp["PATH_INFO"].startswith(trigger) def cb_filelist(args): req = args["request"] pyhttp = req.get_http() data = req.get_data() config = req.get_configuration() page_name = None if not (is_trigger(pyhttp, config) or is_frontpage(pyhttp, config)): return data[INIT_KEY] = 1 datadir = config["datadir"] data['root_datadir'] = config['datadir'] pagesdir = config["pagesdir"] pagesdir = pagesdir.replace("/", os.sep) if not pagesdir[-1] == os.sep: pagesdir = pagesdir + os.sep pathinfo = pyhttp.get("PATH_INFO", "") path, ext = os.path.splitext(pathinfo) if pathinfo == "/" or path == "/index": page_name = "frontpage" else: page_name = pyhttp["PATH_INFO"][len("/" + TRIGGER) + 1:] if not page_name: return # FIXME - need to do a better job of sanitizing page_name = page_name.replace(os.sep, "/") if not page_name: return if page_name[-1] == os.sep: page_name = page_name[:-1] if page_name.find("/") > 0: page_name = page_name[page_name.rfind("/"):] # if the page has a flavour, we use that. otherwise # we default to the default flavour. page_name, flavour = os.path.splitext(page_name) if flavour: data["flavour"] = flavour[1:] ext = tools.what_ext(data["extensions"].keys(), pagesdir + page_name) if not ext: return [] data['root_datadir'] = page_name + '.' + ext data['bl_type'] = 'file' filename = pagesdir + page_name + "." + ext if not os.path.isfile(filename): return [] fe = FileEntry(req, filename, pagesdir) # now we evaluate python code blocks body = fe.get_data() body = eval_python_blocks(req, body) body = ("\n\n" + body + "\n") fe.set_data(body) fe["absolute_path"] = TRIGGER fe["fn"] = page_name fe["file_path"] = TRIGGER + "/" + page_name fe["template_name"] = "pages" data['blog_title_with_path'] = ( config.get("blog_title", "") + " : " + fe.get("title", "")) # set the datadir back config["datadir"] = datadir return [fe] pyblosxom-1.5.3/Pyblosxom/plugins/paginate.py000066400000000000000000000237111217606004600214010ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2004-2012 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= Plugin for paging long index pages. Pyblosxom uses the ``num_entries`` configuration variable to prevent more than ``num_entries`` being rendered by cutting the list down to ``num_entries`` entries. So if your ``num_entries`` is set to 20, you will only see the first 20 entries rendered. The paginate plugin overrides this functionality and allows for paging. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.paginate`` to your ``load_plugins`` list variable in your ``config.py`` file. Make sure it's the first plugin in the ``load_plugins`` list so that it has a chance to operate on the entry list before other plugins. 2. add the ``$(page_navigation)`` variable to your head or foot (or both) templates. this is where the page navigation HTML will appear. Here are some additional configuration variables to adjust the behavior:: ``paginate_count_from`` Defaults to 0. This is the number to start counting from. Some folks like their pages to start at 0 and others like it to start at 1. This enables you to set it as you like. Example:: py["paginate_count_from"] = 1 ``paginate_previous_text`` Defaults to "<<". This is the text for the "previous page" link. ``paginate_next_text`` Defaults to ">>". This is the text for the "next page" link. ``paginate_linkstyle`` Defaults to 1. This allows you to change the link style of the paging. Style 0:: [1] 2 3 4 5 6 7 8 9 ... >> Style 1:: Page 1 of 4 >> If you want a style different than that, you'll have to copy the plugin and implement your own style. Note about static rendering =========================== This plugin works fine with static rendering, but the urls look different. Instead of adding a ``page=4`` kind of thing to the querystring, this adds it to the url. For example, say your front page was ``/index.html`` and you had 5 pages of entries. Then the urls would look like this:: /index.html first page /index_page2.html second page /index_page3.html third page ... """ __author__ = "Will Kahn-Greene" __email__ = "willg at bluesock dot org" __version__ = "2011-10-22" __url__ = "http://pyblosxom.github.com/" __description__ = ( "Allows navigation by page for indexes that have too many entries.") __category__ = "display" __license__ = "MIT" __registrytags__ = "1.5, core" import os from Pyblosxom.tools import pwrap_error, render_url_statically def verify_installation(request): config = request.get_configuration() if config.get("num_entries", 0) == 0: pwrap_error( "Missing config property 'num_entries'. paginate won't do " "anything without num_entries set. Either set num_entries " "to a positive integer, or disable the paginate plugin." "See the documentation at the top of the paginate plugin " "code file for more details.") return False return True class PageDisplay: def __init__(self, url_template, current_page, max_pages, count_from, previous_text, next_text, linkstyle): self._url_template = url_template self._current_page = current_page self._max_pages = max_pages self._count_from = count_from self._previous = previous_text self._next = next_text self._linkstyle = linkstyle def __str__(self): output = [] # prev if self._current_page != self._count_from: prev_url = self._url_template % (self._current_page - 1) output.append('%s ' % (prev_url, self._previous)) # pages if self._linkstyle == 0: for i in range(self._count_from, self._max_pages): if i == self._current_page: output.append('[%d]' % i) else: page_url = self._url_template % i output.append('%d' % (page_url, i)) elif self._linkstyle == 1: output.append(' Page %s of %s ' % (self._current_page, self._max_pages - 1)) # next if self._current_page < self._max_pages - 1: next_url = self._url_template % (self._current_page + 1) output.append(' %s' % (next_url, self._next)) return " ".join(output) def page(request, num_entries, entry_list): http = request.get_http() config = request.get_configuration() data = request.get_data() previous_text = config.get("paginate_previous_text", "<<") next_text = config.get("paginate_next_text", ">>") linkstyle = config.get("paginate_linkstyle", 1) if linkstyle > 1: linkstyle = 1 entries_per_page = num_entries count_from = config.get("paginate_count_from", 0) if ((entries_per_page > 0 and isinstance(entry_list, list) and len(entry_list) > entries_per_page)): page = count_from url = http.get("REQUEST_URI", http.get("HTTP_REQUEST_URI", "")) url_template = url if not data.get("STATIC"): form = request.get_form() if form: try: page = int(form.getvalue("page")) except (TypeError, ValueError): page = count_from # Restructure the querystring so that page= is at the end # where we can fill in the next/previous pages. if url_template.find("?") != -1: query = url_template[url_template.find("?") + 1:] url_template = url_template[:url_template.find("?")] query = query.split("&") query = [m for m in query if not m.startswith("page=")] if len(query) == 0: url_template = url_template + "?" + "page=%d" else: # Note: We're using & here because it needs to # be url_templateencoded. url_template = (url_template + "?" + "&".join(query) + "&page=%d") else: url_template = url_template + "?page=%d" else: try: page = data["paginate_page"] except KeyError: page = count_from # The REQUEST_URI isn't the full url here--it's only the # path and so we need to add the base_url. base_url = config["base_url"].rstrip("/") url_template = base_url + url_template url_template = url_template.split("/") ret = url_template[-1].rsplit("_", 1) if len(ret) == 1: fn, ext = os.path.splitext(ret[0]) pageno = "_page%d" else: fn, pageno = ret pageno, ext = os.path.splitext(pageno) pageno = "_page%d" url_template[-1] = fn + pageno + ext url_template = "/".join(url_template) begin = (page - count_from) * entries_per_page end = (page + 1 - count_from) * entries_per_page if end > len(entry_list): end = len(entry_list) max_pages = ((len(entry_list) - 1) / entries_per_page) + 1 + count_from data["entry_list"] = entry_list[begin:end] data["page_navigation"] = PageDisplay( url_template, page, max_pages, count_from, previous_text, next_text, linkstyle) # If we're static rendering and there wasn't a page specified # and this is one of the flavours to statically render, then # this is the first page and we need to render all the rest of # the pages, so we do that here. static_flavours = config.get("static_flavours", ["html"]) if ((data.get("STATIC") and page == count_from and data.get("flavour") in static_flavours)): # Turn http://example.com/index.html into # http://example.com/index_page5.html for each page. url = url.split('/') fn = url[-1] fn, ext = os.path.splitext(fn) template = '/'.join(url[:-1]) + '/' + fn + '_page%d' if ext: template = template + ext for i in range(count_from + 1, max_pages): print " rendering page %s ..." % (template % i) render_url_statically(dict(config), template % i, '') def cb_truncatelist(args): request = args["request"] entry_list = args["entry_list"] page(request, request.config.get("num_entries", 10), entry_list) return request.data.get("entry_list", entry_list) def cb_pathinfo(args): request = args["request"] data = request.get_data() # This only kicks in during static rendering. if not data.get("STATIC"): return http = request.get_http() pathinfo = http.get("PATH_INFO", "").split("/") # Handle the http://example.com/index_page5.html case. If we see # that, put the page information in the data dict under # "paginate_page" and "fix" the pathinfo. if pathinfo and "_page" in pathinfo[-1]: fn, pageno = pathinfo[-1].rsplit("_") pageno, ext = os.path.splitext(pageno) try: pageno = int(pageno[4:]) except (ValueError, TypeError): # If it's not a valid page number, then we shouldn't be # doing anything here. return pathinfo[-1] = fn pathinfo = "/".join(pathinfo) if ext: pathinfo += ext http["PATH_INFO"] = pathinfo data["paginate_page"] = pageno pyblosxom-1.5.3/Pyblosxom/plugins/pyarchives.py000066400000000000000000000076231217606004600217720ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2004-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= Walks through your blog root figuring out all the available monthly archives in your blogs. It generates html with this information and stores it in the ``$(archivelinks)`` variable which you can use in your head and foot templates. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.pyarchives`` to the ``load_plugins`` list in your ``config.py`` file. 2. Configure using the following configuration variables. ``archive_template`` Let's you change the format of the output for an archive link. For example:: py['archive_template'] = ('
      • ' '%(m)s/%(y)s
      • ') This displays the archives as list items, with a month number, then a slash, then the year number. The formatting variables available in the ``archive_template`` are:: b 'Jun' m '6' Y '1978' y '78' These work the same as ``time.strftime`` in python. Additionally, you can use variables from config and data. .. Note:: The syntax used here is the Python string formatting syntax---not the Pyblosxom template rendering syntax! Usage ===== Add ``$(archivelinks)`` to your head and/or foot templates. """ __author__ = "Wari Wahab" __email__ = "wari at wari dot per dot sg" __version__ = "2011-10-22" __url__ = "http://pyblosxom.github.com/" __description__ = "Builds month/year-based archives listing." __category__ = "archives" __license__ = "MIT" __registrytags__ = "1.4, 1.5, core" from Pyblosxom import tools from Pyblosxom.memcache import memcache_decorator from Pyblosxom.tools import pwrap import time def verify_installation(request): config = request.get_configuration() if not "archive_template" in config: pwrap( "missing optional config property 'archive_template' which " "allows you to specify how the archive links are created. " "refer to pyarchive plugin documentation for more details.") return True class PyblArchives: def __init__(self, request): self._request = request self._archives = None @memcache_decorator('pyarchives', True) def __str__(self): if self._archives == None: self.gen_linear_archive() return self._archives def gen_linear_archive(self): config = self._request.get_configuration() data = self._request.get_data() root = config["datadir"] archives = {} archive_list = tools.walk(self._request, root) fulldict = {} fulldict.update(config) fulldict.update(data) template = config.get('archive_template', '%(Y)s-%(b)s
        ') for mem in archive_list: timetuple = tools.filestat(self._request, mem) timedict = {} for x in ["B", "b", "m", "Y", "y"]: timedict[x] = time.strftime("%" + x, timetuple) fulldict.update(timedict) if not (timedict['Y'] + timedict['m']) in archives: archives[timedict['Y'] + timedict['m']] = (template % fulldict) arc_keys = archives.keys() arc_keys.sort() arc_keys.reverse() result = [] for key in arc_keys: result.append(archives[key]) self._archives = '\n'.join(result) def cb_prepare(args): request = args["request"] data = request.get_data() data["archivelinks"] = PyblArchives(request) pyblosxom-1.5.3/Pyblosxom/plugins/pycalendar.py000066400000000000000000000236121217606004600217330ustar00rootroot00000000000000####################################################################### # This is placed in the Public Domain. ####################################################################### """ Summary ======= Generates a calendar along the lines of this one (with month and day names in the configured locale):: < January 2003 > Mo Tu We Th Fr Sa Su 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 It walks through all your entries and marks the dates that have entries so you can click on the date and see entries for that date. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.pycalendar`` to your ``load_plugins`` list in your ``config.py`` file. 2. Configure it as documented below. 3. Add the ``$(calendar)`` variable to your head and/or foot template. Configuration ============= You can set the start of the week using the ``calendar_firstweekday`` configuration setting, for example:: py['calendar_firstweekday'] = 0 will make the week start on Monday (day '0'), instead of Sunday (day '6'). Pycalendar is locale-aware. If you set the ``locale`` config property, then month and day names will be displayed according to your locale. It uses the following CSS classes: * blosxomCalendar: for the calendar table * blosxomCalendarHead: for the month year header (January 2003) * blosxomCalendarWeekHeader: for the week header (Su, Mo, Tu, ...) * blosxomCalendarEmpty: for filler days * blosxomCalendarCell: for calendar days that aren't today * blosxomCalendarBlogged: for calendar days that aren't today that have entries * blosxomCalendarSpecificDay: for the specific day we're looking at (if we're looking at a specific day) * blosxomCalendarToday: for today's calendar day """ __author__ = "Will Kahn-Greene" __email__ = "willg at bluesock dot org" __version__ = "2011-10-23" __url__ = "http://pyblosxom.github.com/" __description__ = "Displays a calendar on your blog." __category__ = "date" __license__ = "Public domain" __registrytags__ = "1.4, 1.5, core" import time import calendar import string from Pyblosxom import tools from Pyblosxom.memcache import memcache_decorator def verify_installation(request): # there's no configuration needed for this plugin. return True class PyblCalendar: def __init__(self, request): self._request = request self._cal = None self._today = None self._view = None self._specificday = None self._entries = {} @memcache_decorator('pycalendar', True) def __str__(self): """ Returns the on-demand generated string. """ if self._cal == None: self.generate_calendar() return self._cal def generate_calendar(self): """ Generates the calendar. We'd like to walk the archives for things that happen in this month and mark the dates accordingly. After doing that we pass it to a formatting method which turns the thing into HTML. """ config = self._request.get_configuration() data = self._request.get_data() entry_list = data["entry_list"] root = config["datadir"] baseurl = config.get("base_url", "") self._today = time.localtime() if len(entry_list) == 0: # if there are no entries, we shouldn't even try to # do something fancy. self._cal = "" return view = list(entry_list[0]["timetuple"]) # this comes in as '', 2001, 2002, 2003, ... so we can convert it # without an issue temp = data.get("pi_yr") if not temp: view[0] = int(self._today[0]) else: view[0] = int(temp) # the month is a bit harder since it can come in as "08", "", or # "Aug" (in the example of August). temp = data.get("pi_mo") if temp and temp.isdigit(): view[1] = int(temp) elif temp and temp in tools.month2num: view[1] = int(tools.month2num[temp]) else: view[1] = int(self._today[1]) self._view = view = tuple(view) # if we're looking at a specific day, we figure out what it is if data.get("pi_yr") and data.get("pi_mo") and data.get("pi_da"): if data["pi_mo"].isdigit(): mon = data["pi_mo"] else: mon = tools.month2num[data["pi_mo"]] self._specificday = (int(data.get("pi_yr", self._today[0])), int(mon), int(data.get("pi_da", self._today[2]))) archive_list = tools.walk(self._request, root) yearmonth = {} for mem in archive_list: timetuple = tools.filestat(self._request, mem) # if we already have an entry for this date, we skip to the # next one because we've already done this processing day = str(timetuple[2]).rjust(2) if day in self._entries: continue # add an entry for yyyymm so we can figure out next/previous year = str(timetuple[0]) dayzfill = string.zfill(timetuple[1], 2) yearmonth[year + dayzfill] = time.strftime("%b", timetuple) # if the entry isn't in the year/month we're looking at with # the calendar, then we skip to the next one if timetuple[0:2] != view[0:2]: continue # mark the entry because it's one we want to show if config.get("static_monthnumbers"): datepiece = time.strftime("%Y/%m/%d", timetuple) else: datepiece = time.strftime("%Y/%b/%d", timetuple) self._entries[day] = (baseurl + "/" + datepiece, day) # Set the first day of the week (Sunday by default) first = config.get('calendar_firstweekday', 6) calendar.setfirstweekday(first) # create the calendar cal = calendar.monthcalendar(view[0], view[1]) # insert the days of the week cal.insert(0, calendar.weekheader(2).split()) # figure out next and previous links by taking the dict of # yyyymm strings we created, turning it into a list, sorting # them, and then finding "today"'s entry. then the one before # it (index-1) is prev, and the one after (index+1) is next. keys = yearmonth.keys() keys.sort() thismonth = time.strftime("%Y%m", view) # do some quick adjustment to make sure we didn't pick a # yearmonth that's outside the yearmonths of the entries we # know about. if thismonth in keys: index = keys.index(thismonth) elif len(keys) == 0 or keys[0] > thismonth: index = 0 else: index = len(keys) - 1 # build the prev link if index == 0 or len(keys) == 0: prev = None else: prev = ("%s/%s/%s" % (baseurl, keys[index - 1][:4], yearmonth[keys[index - 1]]), "<") # build the next link if index == len(yearmonth) - 1 or len(keys) == 0: next = None else: next = ("%s/%s/%s" % (baseurl, keys[index + 1][:4], yearmonth[keys[index + 1]]), ">") # insert the month name and next/previous links cal.insert(0, [prev, time.strftime("%B %Y", view), next]) self._cal = self.format_with_css(cal) def _fixlink(self, link): if link: return "%s" % (link[0], link[1]) else: return " " def _fixday(self, day): if day == 0: return " " strday = str(day).rjust(2) if strday in self._entries: entry = self._entries[strday] link = "%s" % (entry[0], entry[1]) else: link = strday td_class_str = "" # if it's today if (self._view[0], self._view[1], day) == self._today[0:3]: td_class_str += "blosxomCalendarToday " if self._specificday: # if it's the day we're viewing if (self._view[0], self._view[1], day) == self._specificday: td_class_str += "blosxomCalendarSpecificDay " # if it's a day that's been blogged if strday in self._entries: td_class_str += "blosxomCalendarBlogged" if td_class_str != "": td_class_str = "%s" % link else: td_class_str = "%s" % strday return td_class_str def _fixweek(self, item): return "%s" % item def format_with_css(self, cal): """ This formats the calendar using HTML table and CSS. The output can be made to look prettier. """ cal2 = [""] cal2.append("") cal2.append("") cal2.append( '') cal2.append("") cal2.append("") cal2.append("%s" % "".join([self._fixweek(m) for m in cal[1]])) for mem in cal[2:]: mem = [self._fixday(m) for m in mem] cal2.append("" + "".join(mem) + "") cal2.append("
        " + self._fixlink(cal[0][0]) + "' + cal[0][1] + '" + self._fixlink(cal[0][2]) + "
        ") return "\n".join(cal2) def cb_prepare(args): request = args["request"] data = request.get_data() if data.get('entry_list', None): data["calendar"] = PyblCalendar(request) pyblosxom-1.5.3/Pyblosxom/plugins/pycategories.py000066400000000000000000000204151217606004600223050ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2004-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= Walks through your blog root figuring out all the categories you have and how many entries are in each category. It generates html with this information and stores it in the ``$(categorylinks)`` variable which you can use in your head or foot templates. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.pycategories`` to the ``load_plugins`` list in your ``config.py`` file. 2. Add ``$(categorylinks)`` to your head and/or foot templates. Configuration ============= You can format the output by setting ``category_begin``, ``category_item``, and ``category_end`` properties. Categories exist in a hierarchy. ``category_start`` starts the category listing and is only used at the very beginning. The ``category_begin`` property begins a new category group and the ``category_end`` property ends that category group. The ``category_item`` property is the template for each category item. Then after all the categories are printed, ``category_finish`` ends the category listing. For example, the following properties will use ``
          `` to open a category, ``
        `` to close a category and ``
      • `` for each item:: py["category_start"] = "
          " py["category_begin"] = "" py["category_finish"] = "
        " Another example, the following properties don't have a begin or an end but instead use indentation for links and displays the number of entries in that category:: py["category_start"] = "" py["category_begin"] = "" py["category_item"] = ( r'%(indent)s' r'%(category)s (%(count)d)
        ') py["category_end"] = "" py["category_finish"] = "" There are no variables available in the ``category_begin`` or ``category_end`` templates. Available variables in the category_item template: ======================= ========================== ==================== variable example datatype ======================= ========================== ==================== base_url http://joe.com/blog/ string fullcategory_urlencoded 'dev/pyblosxom/status/' string fullcategory 'dev/pyblosxom/status/' string (urlencoded) category 'status/' string category_urlencoded 'status/' string (urlencoed) flavour 'html' string count 70 int indent '    ' string ======================= ========================== ==================== """ __author__ = "Will Kahn-Greene" __email__ = "willg at bluesock dot org" __version__ = "$Id$" __url__ = "http://pyblosxom.github.com/" __description__ = "Builds a list of categories." __category__ = "category" __license__ = "MIT" __registrytags__ = "1.4, 1.5, core" from Pyblosxom import tools from Pyblosxom.memcache import memcache_decorator from Pyblosxom.tools import pwrap import os DEFAULT_START = r'
          ' DEFAULT_BEGIN = r'
        • " DEFAULT_FINISH = "
        " def verify_installation(request): config = request.get_configuration() if not "category_item" in config: pwrap( "missing optional config property 'category_item' which allows " "you to specify how the category hierarchy is rendered. see" "the documentation at the top of the pycategories plugin code " "file for more details.") return True class PyblCategories: def __init__(self, request): self._request = request self._categories = None @memcache_decorator('pycategories', True) def __str__(self): if self._categories == None: self.gen_categories() return self._categories def gen_categories(self): config = self._request.get_configuration() root = config["datadir"] start_t = config.get("category_start", DEFAULT_START) begin_t = config.get("category_begin", DEFAULT_BEGIN) item_t = config.get("category_item", DEFAULT_ITEM) end_t = config.get("category_end", DEFAULT_END) finish_t = config.get("category_finish", DEFAULT_FINISH) self._baseurl = config.get("base_url", "") form = self._request.get_form() if form.has_key('flav'): flavour = form['flav'].value else: flavour = config.get('default_flavour', 'html') # build the list of all entries in the datadir elist = tools.walk(self._request, root) # peel off the root dir from the list of entries elist = [mem[len(root) + 1:] for mem in elist] # go through the list of entries and build a map that # maintains a count of how many entries are in each category elistmap = {} for mem in elist: mem = os.path.dirname(mem) elistmap[mem] = 1 + elistmap.get(mem, 0) self._elistmap = elistmap # go through the elistmap keys (which is the list of # categories) and for each piece in the key (i.e. the key # could be "dev/pyblosxom/releases" and the pieces would be # "dev", "pyblosxom", and "releases") we build keys for the # category list map (i.e. "dev", "dev/pyblosxom", # "dev/pyblosxom/releases") clistmap = {} for mem in elistmap.keys(): mem = mem.split(os.sep) for index in range(len(mem) + 1): p = os.sep.join(mem[0:index]) clistmap[p] = 0 # then we take the category list from the clistmap and sort it # alphabetically clist = clistmap.keys() clist.sort() output = [] indent = 0 output.append(start_t) # then we generate each item in the list for item in clist: itemlist = item.split(os.sep) num = 0 for key in self._elistmap.keys(): if item == '' or key == item or key.startswith(item + os.sep): num = num + self._elistmap[key] if not item: tab = "" else: tab = len(itemlist) * "  " if itemlist != ['']: if indent > len(itemlist): for i in range(indent - len(itemlist)): output.append(end_t) elif indent < len(itemlist): for i in range(len(itemlist) - indent): output.append(begin_t) # now we build the dict with the values for substitution d = {"base_url": self._baseurl, "fullcategory": item + "/", "category": itemlist[-1] + "/", "flavour": flavour, "count": num, "indent": tab} # this prevents a double / in the root category url if item == "": d["fullcategory"] = item # this adds urlencoded versions d["fullcategory_urlencoded"] = ( tools.urlencode_text(d["fullcategory"])) d["category_urlencoded"] = tools.urlencode_text(d["category"]) # and we toss it in the thing output.append(item_t % d) if itemlist != ['']: indent = len(itemlist) output.append(end_t * indent) output.append(finish_t) # then we join the list and that's the final string self._categories = "\n".join(output) def cb_prepare(args): request = args["request"] data = request.get_data() data["categorylinks"] = PyblCategories(request) pyblosxom-1.5.3/Pyblosxom/plugins/pyfilenamemtime.py000066400000000000000000000046571217606004600230060ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2004, 2005 Tim Roberts # Copyright (c) 2011 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= Allows you to specify the mtime for a file in the file name. If a filename contains a timestamp in the form of ``YYYY-MM-DD-hh-mm``, change the mtime to be the timestamp instead of the one kept by the filesystem. For example, a valid filename would be ``foo-2002-04-01-00-00.txt`` for April fools day on the year 2002. It is also possible to use timestamps in the form of ``YYYY-MM-DD``. http://www.probo.com/timr/blog/ Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.pyfilenamemtime`` to the ``load_plugins`` list of your ``config.py`` file. 2. Use date stamps in your entry filenames. """ __author__ = "Tim Roberts" __email__ = "" __version__ = "2011-10-23" __url__ = "http://pyblosxom.github.com/" __description__ = "Allows you to codify the mtime in the filename." __category__ = "date" __license__ = "MIT" __registrytags__ = "1.4, 1.5, core" import os import re import time from Pyblosxom import tools from Pyblosxom.memcache import memcache_decorator DAYMATCH = re.compile( '([0-9]{4})-' '([0-1][0-9])-' '([0-3][0-9])' '(-([0-2][0-9])-([0-5][0-9]))?.[\w]+$') @memcache_decorator('pyfilenamemtime') def get_mtime(filename): mtime = 0 mtch = DAYMATCH.search(os.path.basename(filename)) if mtch: try: year = int(mtch.group(1)) mo = int(mtch.group(2)) day = int(mtch.group(3)) if mtch.group(4) is None: hr = 0 minute = 0 else: hr = int(mtch.group(5)) minute = int(mtch.group(6)) mtime = time.mktime((year, mo, day, hr, minute, 0, 0, 0, -1)) except StandardError: # TODO: Some sort of debugging code here? pass return mtime return None def cb_filestat(args): filename = args["filename"] stattuple = args["mtime"] mtime = get_mtime(filename) if mtime is not None: args["mtime"] = ( tuple(list(stattuple[:8]) + [mtime] + list(stattuple[9:]))) return args pyblosxom-1.5.3/Pyblosxom/plugins/readmore.py000066400000000000000000000134511217606004600214070ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2010 Menno Smits # Copyright (c) 2011 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= Allows you to break a long entry into a summary and the rest making it easier to show just the summary in indexes. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.readmore`` to the ``load_plugins`` list in your ``config.py`` file. .. Note:: If you're using the rst_parser plugin, make sure this plugin shows up in load_plugins list before the rst_parser plugin. See the rst_parser section below. 2. Configure as documented below. Configuration ============= ``readmore_breakpoint`` (optional) string; defaults to "BREAK" This is the text that you'll use in your blog entry that breaks the body into the summary part above and the rest of the blog entry below. For example:: py["readmore_breakpoint"] = "BREAK" ``readmore_template`` (optional) string; defaults to:: '

        read more after the break...

        ' When the entry is being shown in an index with other entries, then the ``readmore_breakpoint`` text is replaced with this text. This text is done with HTML markup. Variables available: * ``%(url)s`` - the full path to the story * ``%(base_url)s`` - base_url * ``%(flavour)s`` - the flavour selected now * ``%(file_path)s`` - path to the story (without extension) .. Note:: This template is formatted using Python string formatting---not Pyblosxom template formatting! Usage ===== For example, if the value of ``readmore_breakpoint`` is ``"BREAK"``, then you could have a blog entry like this:: First post

        This is my first post. In this post, I set out to explain why it is that I'm blogging and what I hope to accomplish with this blog. See more below the break.

        BREAK

        Ha ha! Made you look below the break!

        Usage with rst_parser ===================== Since the rst_parser parses the restructured text and turns it into HTML and this plugin operates on HTML in the story callback, we have to do a two-step replacement. Thus, instead of using BREAK or whatever you have set in ``readmore_breakpoint`` in your blog entry, you use the break directive:: First post This is my first post. In this post, I set out to explain why it is that I'm blogging and what I hope to accomplish with this blog. .. break:: Ha ha! Made you look below the break! History ======= This is based on the original readmore plugin written by IWS years ago. It's since been reworked. Additionally, I folded in the rst_break plugin break directive from Menno Smits at http://freshfoo.com/wiki/CodeIndex . """ __author__ = "Will Kahn-Greene" __email__ = "willg at bluesock dot org" __version__ = "2011-11-05" __url__ = "http://pyblosxom.github.com/" __description__ = "Breaks blog entries into summary and details" __category__ = "display" __license__ = "MIT" __registrytags__ = "1.5, core" import re from Pyblosxom.tools import pwrap READMORE_BREAKPOINT = "BREAK" READMORE_TEMPLATE = ( '

        ' 'read more after the break...' '

        ') def verify_installation(request): config = request.get_configuration() for mem in ("readmore_template", "readmore_breakpoint"): if mem not in config: pwrap("missing optional config property '%s'" % mem) return True def cb_start(args): """Register a break directive if docutils is installed.""" try: from docutils import nodes from docutils.parsers.rst import directives, Directive except ImportError: return request = args['request'] config = request.get_configuration() breakpoint = config.get("readmore_breakpoint", READMORE_BREAKPOINT) class Break(Directive): """ Transform a break directive (".. break::") into the text that the Pyblosxom readmore plugin looks for. This allows blog entries written in reST to use this plugin. """ required_arguments = 0 optional_arguments = 0 final_argument_whitespace = True has_content = False def run(self): return [nodes.raw("", breakpoint + "\n", format="html")] directives.register_directive("break", Break) def cb_story(args): entry = args["entry"] if not entry.has_key("body"): return request = args["request"] data = request.get_data() config = request.get_configuration() breakpoint = config.get("readmore_breakpoint", READMORE_BREAKPOINT) template = config.get("readmore_template", READMORE_TEMPLATE) # check to see if the breakpoint is in the body. match = re.search(breakpoint, entry["body"]) # if not, return because we don't have to do anything if not match: return # if we're showing just one entry, then we show the whole thing if data["bl_type"] == 'file': entry["body"] = re.sub(breakpoint, "", entry["body"]) return # otherwise we replace the breakpoint with the template base_url = config["base_url"] file_path = entry["file_path"] flavour = config.get("default_flavour", "html") url = '%s/%s.%s' % (base_url, file_path, flavour) link = (template % {"url": url, "base_url": base_url, "file_path": file_path, "flavour": flavour}) entry["just_summary"] = 1 entry["body"] = entry["body"][:match.start()] + link pyblosxom-1.5.3/Pyblosxom/plugins/rst_parser.py000066400000000000000000000101631217606004600217720ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2003, 2004, 2005 Sean Bowman # Copyright (c) 2011 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= A reStructuredText entry formatter for pyblosxom. reStructuredText is part of the docutils project (http://docutils.sourceforge.net/). To use, you need a *recent* version of docutils. A development snapshot (http://docutils.sourceforge.net/#development-snapshots) will work fine. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.rst_parser`` to the ``load_plugins`` list in your ``config.py`` file. 2. Install docutils. Instructions are at http://docutils.sourceforge.net/ Usage ===== Blog entries with a ``.rst`` extension will be parsed as restructuredText. You can also configure this as your default preformatter for ``.txt`` files by configuring it in your config file as follows:: py['parser'] = 'reST' Additionally, you can do this on an entry-by-entry basis by adding a ``#parser reST`` line in the metadata section. For example:: My Little Blog Entry #parser reST My main story... Configuration ============= There's two optional configuration parameter you can for additional control over the rendered HTML:: # To set the starting level for the rendered heading elements. # 1 is the default. py['reST_initial_header_level'] = 1 # Enable or disable the promotion of a lone top-level section title to # document title (and subsequent section title to document subtitle # promotion); enabled by default. py['reST_transform_doctitle'] = 1 .. Note:: If you're not seeing headings that you think should be there, try changing the ``reST_initial_header_level`` property to 0. """ __author__ = "Sean Bowman" __email__ = "sean dot bowman at acm dot org" __version__ = "2011-10-23" __url__ = "http://pyblosxom.github.com/" __description__ = "restructured text support for blog entries" __category__ = "text" __license__ = "MIT" __registrytags__ = "1.5, core" from docutils.core import publish_parts from Pyblosxom import tools from Pyblosxom.memcache import memcache_decorator PREFORMATTER_ID = 'reST' FILE_EXT = 'rst' def verify_installation(args): # no configuration needed return 1 def cb_entryparser(args): args[FILE_EXT] = readfile return args def cb_preformat(args): if args.get("parser", None) == PREFORMATTER_ID: return parse(''.join(args['story']), args['request']) @memcache_decorator('rst_parser') def _parse(initial_header_level, transform_doctitle, story): parts = publish_parts( story, writer_name='html', settings_overrides={ 'initial_header_level': initial_header_level, 'doctitle_xform': transform_doctitle, 'syntax_highlight': 'short' }) return parts['body'] def parse(story, request): config = request.getConfiguration() initial_header_level = config.get('reST_initial_header_level', 1) transform_doctitle = config.get('reST_transform_doctitle', 1) return _parse(initial_header_level, transform_doctitle, story) def readfile(filename, request): entry_data = {} lines = open(filename).readlines() if len(lines) == 0: return {"title": "", "body": ""} title = lines.pop(0).strip() # absorb meta data while lines and lines[0].startswith("#"): meta = lines.pop(0) # remove the hash meta = meta[1:].strip() meta = meta.split(" ", 1) # if there's no value, we append a 1 if len(meta) == 1: meta.append("1") entry_data[meta[0].strip()] = meta[1].strip() body = parse(''.join(lines), request) entry_data["title"] = title entry_data["body"] = body # Call the postformat callbacks tools.run_callback('postformat', {'request': request, 'entry_data': entry_data}) return entry_data pyblosxom-1.5.3/Pyblosxom/plugins/tags.py000066400000000000000000000404171217606004600205510ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2009, 2010, 2011 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= This is a tags plugin. It uses Pyblosxom's command line abilities to split generation of tags index data from display of tags index data. It creates a ``$(tagslist)`` variable for head and foot templates which lists all the tags. It creates a ``$(tags)`` variable for story templates which lists tags for the story. It creates a ``$(tagcloud)`` variable for the tag cloud. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.tags`` to the ``load_plugins`` list in your ``config.py`` file. 2. Configure as documented below. Configuration ============= The following config properties define where the tags file is located, how tag metadata is formatted, and how tag lists triggered. ``tags_separator`` This defines the separator between tags in the metadata line. Defaults to ",". After splitting on the separator, each individual tag is stripped of whitespace before and after the text. For example:: Weather in Boston #tags weather, boston

        The weather in Boston today is pretty nice.

        returns tags ``weather`` and ``boston``. If the ``tags_separator`` is:: py["tags_separator"] = "::" then tags could be declared in the entries like this:: Weather in Boston #tags weather::boston

        The weather in Boston today is pretty nice.

        ``tags_filename`` This is the file that holds indexed tags data. Defaults to datadir + os.pardir + ``tags.index``. This file needs to be readable by the process that runs your blog. This file needs to be writable by the process that creates the index. ``tags_trigger`` This is the url trigger to indicate that the tags plugin should handle the file list based on the tag. Defaults to ``tag``. ``truncate_tags`` If this is True, then tags index listings will get passed through the truncate callback. If this is False, then the tags index listing will not be truncated. If you're using a paging plugin, then setting this to True will allow your tags index to be paged. Example:: py["truncate_tags"] = True Defaults to True. In the head and foot templates, you can list all the tags with the ``$(tagslist)`` variable. The templates for this listing use the following three config properties: ``tags_list_start`` Printed before the list. Defaults to ``

        ``. ``tags_list_item`` Used for each tag in the list. There are a bunch of variables you can use: * ``base_url`` - the baseurl for your blog * ``flavour`` - the default flavour or flavour currently showing * ``tag`` - the tag name * ``count`` - the number of items that are tagged with this tag * ``tagurl`` - url composed of baseurl, trigger, and tag Defaults to ``%(tag)s``. ``tags_list_finish`` Printed after the list. Defaults to ``

        ``. In the head and foot templates, you can also add a tag cloud with the ``$(tagcloud)`` variable. The templates for the cloud use the following three config properties: ``tags_cloud_start`` Printed before the cloud. Defaults to ``

        ``. ``tags_cloud_item`` Used for each tag in the cloud list. There are a bunch of variables you can use: * ``base_url`` - the baseurl for your blog * ``flavour`` - the default flavour or flavour currently showing * ``tag`` - the tag name * ``count`` - the number of items that are tagged with this tag * ``class`` - biggestTag, bigTag, mediumTag, smallTag or smallestTag--the css class for this tag representing the frequency the tag is used * ``tagurl`` - url composed of baseurl, trigger, and tag Defaults to ``%(tag)s``. ``tags_cloud_finish`` Printed after the cloud. Defaults to ``

        ``. You'll also want to add CSS classes for the size classes to your CSS. For example, you could add this:: .biggestTag { font-size: 16pt; } .bigTag { font-size: 14pt } .mediumTag { font-size: 12pt } .smallTag { font-size: 10pt ] .smallestTag { font-size: 8pt ] You can list the tags for a given entry in the story template with the ``$(tags)`` variable. The tag items in the story are formatted with one configuration property: ``tags_item`` This is the template for a single tag for an entry. It can use the following bits: * ``base_url`` - the baseurl for this blog * ``flavour`` - the default flavour or flavour currently being viewed * ``tag`` - the tag * ``tagurl`` - url composed of baseurl, trigger and tag Defaults to ``%(tag)s``. Tags are joined together with ``,``. Creating the tags index file ============================ Run:: pyblosxom-cmd buildtags from the directory your ``config.py`` is in or:: pyblosxom-cmd buildtags --config=/path/to/config/file from anywhere. This builds the tags index file that the tags plugin requires to generate tags-based bits for the request. Until you rebuild the tags index file, the entry will not have its tags indexed. Thus you should either rebuild the tags file after writing or updating an entry or you should rebuild the tags file as a cron job. .. Note:: If you're using static rendering, you need to build the tags index before you statically render your blog. Converting from categories to tags ================================== This plugin has a command that goes through your entries and adds tag metadata based on the category. There are some caveats: 1. it assumes entries are in the blosxom format of title, then metadata, then the body. 2. it only operates on entries in the datadir. It maintains the atime and mtime of the file. My suggestion is to back up your files (use tar or something that maintains file stats), then try it out and see how well it works, and figure out if that works or not. To run the command do:: pyblosxom-cmd categorytotags from the directory your ``config.py`` is in or:: pyblosxom-cmd categorytotags --config=/path/to/config/file from anywhere. """ __author__ = "Will Kahn-Greene" __email__ = "willg at bluesock dot org" __version__ = "2011-10-23" __url__ = "http://pyblosxom.github.com/" __description__ = "Tags plugin" __category__ = "tags" __license__ = "MIT" __registrytags__ = "1.5, core" import os import cPickle as pickle import shutil from Pyblosxom.memcache import memcache_decorator def savefile(path, tagdata): """Saves tagdata to file at path.""" fp = open(path + ".new", "w") pickle.dump(tagdata, fp) fp.close() shutil.move(path + ".new", path) @memcache_decorator('tags') def loadfile(path): """Loads tagdata from file at path.""" fp = open(path, "r") tagdata = pickle.load(fp) fp.close() return tagdata def get_tagsfile(cfg): """Generates tagdata filename.""" datadir = cfg["datadir"] tagsfile = cfg.get("tags_filename", os.path.join(datadir, os.pardir, "tags.index")) return tagsfile def buildtags(command, argv): """Command for building the tags index.""" import config datadir = config.py.get("datadir") if not datadir: raise ValueError("config.py has no datadir property.") sep = config.py.get("tags_separator", ",") tagsfile = get_tagsfile(config.py) from Pyblosxom.pyblosxom import blosxom_entry_parser, Pyblosxom from Pyblosxom import tools from Pyblosxom.entries import fileentry # build a Pyblosxom object, initialize it, and run the start # callback. this gives entry parsing related plugins a chance to # get their stuff together so that they work correctly. p = Pyblosxom(config.py, {}) p.initialize() req = p.get_request() tools.run_callback("start", {"request": req}) # grab all the entries in the datadir filelist = tools.walk(req, datadir) entrylist = [fileentry.FileEntry(req, e, datadir) for e in filelist] tags_to_files = {} for mem in entrylist: tagsline = mem["tags"] if not tagsline: continue tagsline = [t.strip() for t in tagsline.split(sep)] for t in tagsline: tags_to_files.setdefault(t, []).append(mem["filename"]) savefile(tagsfile, tags_to_files) return 0 def category_to_tags(command, argv): """Goes through all entries and converts the category to tags metadata. It adds the tags line as the second line. It maintains the mtime for the file. """ import config datadir = config.py.get("datadir") if not datadir: raise ValueError("config.py has no datadir property.") sep = config.py.get("tags_separator", ",") from Pyblosxom.pyblosxom import blosxom_entry_parser, Request from Pyblosxom import tools data = {} # register entryparsers so that we parse all possible file types. data["extensions"] = tools.run_callback("entryparser", {"txt": blosxom_entry_parser}, mappingfunc=lambda x, y: y, defaultfunc=lambda x: x) req = Request(config.py, {}, data) # grab all the entries in the datadir filelist = tools.walk(req, datadir) if not datadir.endswith(os.sep): datadir = datadir + os.sep for mem in filelist: print "working on %s..." % mem category = os.path.dirname(mem)[len(datadir):] tags = category.split(os.sep) print " adding tags %s" % tags tags = "#tags %s\n" % (sep.join(tags)) atime, mtime = os.stat(mem)[7:9] fp = open(mem, "r") data = fp.readlines() fp.close() data.insert(1, tags) fp = open(mem, "w") fp.write("".join(data)) fp.close() os.utime(mem, (atime, mtime)) return 0 def cb_commandline(args): args["buildtags"] = (buildtags, "builds the tags index") args["categorytotags"] = ( category_to_tags, "builds tag metadata from categories for entries") return args def cb_start(args): request = args["request"] data = request.get_data() tagsfile = get_tagsfile(request.get_configuration()) if os.path.exists(tagsfile): try: tagsdata = loadfile(tagsfile) except IOError: tagsdata = {} else: tagsdata = {} data["tagsdata"] = tagsdata def cb_filelist(args): from Pyblosxom.pyblosxom import blosxom_truncate_list_handler from Pyblosxom import tools # handles /trigger/tag to show all the entries tagged that # way req = args["request"] pyhttp = req.get_http() data = req.get_data() config = req.get_configuration() trigger = "/" + config.get("tags_trigger", "tag") if not pyhttp["PATH_INFO"].startswith(trigger): return datadir = config["datadir"] tagsfile = get_tagsfile(config) tagsdata = loadfile(tagsfile) tag = pyhttp["PATH_INFO"][len(trigger) + 1:] filelist = tagsdata.get(tag, []) if not filelist: tag, ext = os.path.splitext(tag) filelist = tagsdata.get(tag, []) if filelist: data["flavour"] = ext[1:] from Pyblosxom.entries import fileentry entrylist = [fileentry.FileEntry(req, e, datadir) for e in filelist] # sort the list by mtime entrylist = [(e._mtime, e) for e in entrylist] entrylist.sort() entrylist.reverse() entrylist = [e[1] for e in entrylist] data["truncate"] = config.get("truncate_tags", True) args = {"request": req, "entry_list": entrylist} entrylist = tools.run_callback("truncatelist", args, donefunc=lambda x: x != None, defaultfunc=blosxom_truncate_list_handler) return entrylist def cb_story(args): # adds tags to the entry properties request = args["request"] entry = args["entry"] config = request.get_configuration() sep = config.get("tags_separator", ",") tags = [t.strip() for t in entry.get("tags", "").split(sep)] tags.sort() entry["tags_raw"] = tags form = request.get_form() try: flavour = form["flav"].value except KeyError: flavour = config.get("default_flavour", "html") baseurl = config.get("base_url", "") trigger = config.get("tags_trigger", "tag") template = config.get("tags_item", '%(tag)s') tags = [template % {"base_url": baseurl, "flavour": flavour, "tag": tag, "tagurl": "/".join([baseurl, trigger, tag])} for tag in tags] entry["tags"] = ", ".join(tags) return args def cb_head(args): # adds a taglist to header/footer request = args["request"] entry = args["entry"] data = request.get_data() config = request.get_configuration() tagsdata = data.get("tagsdata", {}) # first, build the tags list tags = tagsdata.keys() tags.sort() start_t = config.get("tags_list_start", '

        ') item_t = config.get("tags_list_item", ' %(tag)s ') finish_t = config.get("tags_list_finish", '

        ') output = [] form = request.get_form() try: flavour = form["flav"].value except KeyError: flavour = config.get("default_flavour", "html") baseurl = config.get("base_url", "") trigger = config.get("tags_trigger", "tag") output.append(start_t) for item in tags: d = {"base_url": baseurl, "flavour": flavour, "tag": item, "count": len(tagsdata[item]), "tagurl": "/".join([baseurl, trigger, item])} output.append(item_t % d) output.append(finish_t) entry["tagslist"] = "\n".join(output) # second, build the tags cloud tags_by_file = tagsdata.items() start_t = config.get("tags_cloud_start", "

        ") item_t = config.get("tags_cloud_item", '%(tag)s') finish_t = config.get("tags_cloud_finish", "

        ") tagcloud = [start_t] if len(tags_by_file) > 0: tags_by_file.sort(key=lambda x: len(x[1])) # the most popular tag is at the end--grab the number of files # that have that tag max_count = len(tags_by_file[-1][1]) min_count = len(tags_by_file[0]) # figure out the bin size for the tag size classes b = (max_count - min_count) / 5 range_and_class = ( (min_count + (b * 4), "biggestTag"), (min_count + (b * 3), "bigTag"), (min_count + (b * 2), "mediumTag"), (min_count + b, "smallTag"), (0, "smallestTag") ) # sorts it alphabetically tags_by_file.sort() for tag, files in tags_by_file: len_files = len(files) for tag_range, tag_size_class in range_and_class: if len_files > tag_range: tag_class = tag_size_class break d = {"base_url": baseurl, "flavour": flavour, "class": tag_class, "tag": tag, "count": len(tagsdata[tag]), "tagurl": "/".join([baseurl, trigger, tag])} tagcloud.append(item_t % d) tagcloud.append(finish_t) entry["tagcloud"] = "\n".join(tagcloud) return args cb_foot = cb_head def cb_staticrender_filelist(args): req = args["request"] # We call our own cb_start() here because we need to initialize # the tagsdata. cb_start({"request": req}) config = req.get_configuration() filelist = args["filelist"] tagsdata = req.get_data()["tagsdata"] index_flavours = config.get("static_index_flavours", ["html"]) trigger = "/" + config.get("tags_trigger", "tag") # Go through and add an index.flav for each index_flavour # for each tag. for tag in tagsdata.keys(): for flavour in index_flavours: filelist.append((trigger + "/" + tag + "." + flavour, "")) pyblosxom-1.5.3/Pyblosxom/plugins/trackback.py000066400000000000000000000130731217606004600215360ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2003-2005 Ted Leung # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= This plugin allows pyblosxom to process trackback http://www.sixapart.com/pronet/docs/trackback_spec pings. Install ======= Requires the ``comments`` plugin. Though you don't need to have comments enabled on your blog in order for trackbacks to work. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.trackback`` to the ``load_plugins`` list in your ``config.py`` file. 2. Add this to your ``config.py`` file:: py['trackback_urltrigger'] = "/trackback" These web forms are useful for testing. You can use them to send trackback pings with arbitrary content to the URL of your choice: * http://kalsey.com/tools/trackback/ * http://www.reedmaniac.com/scripts/trackback_form.php 3. Now you need to advertise the trackback ping link. Add this to your ``story`` template:: TB 4. You can supply an embedded RDF description of the trackback ping, too. Add this to your ``story`` or ``comment-story`` template:: """ __author__ = "Ted Leung" __email__ = "" __version__ = "" __url__ = "http://pyblosxom.github.com/" __description__ = "Trackback support." __category__ = "comments" __license__ = "MIT" __registrytags__ = "1.4, core" from Pyblosxom import tools from Pyblosxom.tools import pwrap tb_good_response = """ 0 """ tb_bad_response = """ 1 %s """ def verify_installation(request): config = request.get_configuration() # all config properties are optional if not 'trackback_urltrigger' in config: pwrap("missing optional property: 'trackback_urltrigger'") return True def cb_handle(args): request = args['request'] pyhttp = request.get_http() config = request.get_configuration() urltrigger = config.get('trackback_urltrigger', '/trackback') logger = tools.get_logger() path_info = pyhttp['PATH_INFO'] if path_info.startswith(urltrigger): response = request.get_response() response.add_header("Content-type", "text/xml") form = request.get_form() message = ("A trackback must have at least a URL field (see " "http://www.sixapart.com/pronet/docs/trackback_spec)") if "url" in form: from comments import decode_form encoding = config.get('blog_encoding', 'iso-8859-1') decode_form(form, encoding) import time cdict = {'title': form.getvalue('title', ''), 'author': form.getvalue('blog_name', ''), 'pubDate': str(time.time()), 'link': form['url'].value, 'source': form.getvalue('blog_name', ''), 'description': form.getvalue('excerpt', ''), 'ipaddress': pyhttp.get('REMOTE_ADDR', ''), 'type': 'trackback' } argdict = {"request": request, "comment": cdict} reject = tools.run_callback("trackback_reject", argdict, donefunc=lambda x: x != 0) if isinstance(reject, (tuple, list)) and len(reject) == 2: reject_code, reject_message = reject else: reject_code, reject_message = reject, "Trackback rejected." if reject_code == 1: print >> response, tb_bad_response % reject_message return 1 from Pyblosxom.entries.fileentry import FileEntry datadir = config['datadir'] from comments import writeComment try: import os pi = path_info.replace(urltrigger, '') path = os.path.join(datadir, pi[1:]) data = request.get_data() ext = tools.what_ext(data['extensions'].keys(), path) entry = FileEntry(request, '%s.%s' % (path, ext), datadir) data = {} data['entry_list'] = [entry] # Format Author cdict['author'] = ( 'Trackback from %s' % form.getvalue('blog_name', '')) writeComment(request, config, data, cdict, encoding) print >> response, tb_good_response except OSError: message = 'URI ' + path_info + " doesn't exist" logger.error(message) print >> response, tb_bad_response % message else: logger.error(message) print >> response, tb_bad_response % message # no further handling is needed return 1 return 0 pyblosxom-1.5.3/Pyblosxom/plugins/w3cdate.py000066400000000000000000000062521217606004600211440ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2003-2005 Ted Leung # Copyright (c) 2010, 2011 Will Kahn-Greene # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= Adds a ``$(w3cdate)`` variable to the head and foot templates which has the mtime of the first entry in the entrylist being displayed (this is often the youngest/most-recent entry). Install ======= .. Note:: If you have pyxml installed, then this will work better than if you don't. If you don't have it installed, it uses home-brew code to compute the w3cdate. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.w3cdate`` to the beginning of the ``load_plugins`` list of your ``config.py`` file. 2. Add the ``$(w3cdate)`` variable to the place you need it in your head and/or foot templates. Thanks ====== Thanks to Matej Cepl for the hacked iso8601 code that doesn't require PyXML. """ __author__ = "Ted Leung" __email__ = "twl at sauria dot com" __version__ = "2011-10-23" __url__ = "http://pyblosxom.github.com/" __description__ = ( "Adds a 'w3cdate' variable which is the mtime in ISO8601 format.") __category__ = "date" __license__ = "MIT" __registrytags__ = "1.4, 1.5, core" import time def iso8601_hack_tostring(t, timezone): timezone = int(timezone) if timezone: sign = (timezone < 0) and "+" or "-" timezone = abs(timezone) hours = timezone / (60 * 60) minutes = (timezone % (60 * 60)) / 60 tzspecifier = "%c%02d:%02d" % (sign, hours, minutes) else: tzspecifier = "Z" psecs = t - int(t) t = time.gmtime(int(t) - timezone) year, month, day, hours, minutes, seconds = t[:6] if seconds or psecs: if psecs: psecs = int(round(psecs * 100)) f = "%4d-%02d-%02dT%02d:%02d:%02d.%02d%s" v = (year, month, day, hours, minutes, seconds, psecs, tzspecifier) else: f = "%4d-%02d-%02dT%02d:%02d:%02d%s" v = (year, month, day, hours, minutes, seconds, tzspecifier) else: f = "%4d-%02d-%02dT%02d:%02d%s" v = (year, month, day, hours, minutes, tzspecifier) return f % v try: from xml.utils import iso8601 format_date = iso8601.tostring except (ImportError, AttributeError): format_date = iso8601_hack_tostring def get_formatted_date(entry): if not entry: return "" time_tuple = entry['timetuple'] tzoffset = time.timezone # if is_dst flag set, adjust for daylight savings time if time_tuple[8] == 1: tzoffset = time.altzone return format_date(time.mktime(time_tuple), tzoffset) def cb_story(args): entry = args['entry'] entry["w3cdate"] = get_formatted_date(entry) def cb_head(args): entry = args["entry"] req = args["request"] data = req.get_data() entrylist = data.get("entry_list", None) if not entrylist: return args entry["w3cdate"] = get_formatted_date(entrylist[0]) return args cb_foot = cb_head pyblosxom-1.5.3/Pyblosxom/plugins/xmlrpc_pingback.py000066400000000000000000000136041217606004600227540ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (c) 2003-2006 Ted Leung, Ryan Barrett # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= This module contains an XML-RPC extension to support pingback http://www.hixie.ch/specs/pingback/pingback pings. Install ======= Requires the ``comments`` plugin, but you don't have to enable comments on your blog for pingbacks to work. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.xmlrpc_pingback`` to the ``load_plugins`` list of your ``config.py`` file 2. Set the ``xmlrpc_trigger`` variable in your ``config.py`` file to a trigger for this plugin. For example:: py["xmlrpc_trigger"] = "RPC" 3. Add to the ```` section of your ``head`` template:: This test blog, maintained by Ian Hickson, is useful for testing. You can post to it, linking to a post on your site, and it will send a pingback. * http://www.dummy-blog.org/ """ __author__ = "Ted Leung, Ryan Barrett" __email__ = "" __version__ = "2011-10-28" __url__ = "http://pyblosxom.github.com/" __description__ = "XMLRPC pingback support." __category__ = "comments" __license__ = "MIT" __registrytags__ = "1.4, core" from Pyblosxom import tools from xmlrpclib import Fault import re import sgmllib import time import urllib import urlparse def verify_installation(request): # no config parameters return True class parser(sgmllib.SGMLParser): """ Shamelessly grabbed from Sam Ruby from http://www.intertwingly.net/code/mombo/pingback.py """ intitle = 0 title = "" hrefs = [] def do_a(self, attrs): attrs = dict(attrs) if 'href' in attrs: self.hrefs.append(attrs['href']) def do_title(self, attrs): if self.title == "": self.intitle = 1 def unknown_starttag(self, tag, attrs): self.intitle = 0 def unknown_endtag(self, tag): self.intitle = 0 def handle_charref(self, ref): if self.intitle: self.title = self.title + ("&#%s;" % ref) def handle_data(self, text): if self.intitle: self.title = self.title + text def fileFor(req, uri): config = req.get_configuration() urldata = urlparse.urlsplit(uri) # Reconstruct uri to something sane uri = "%s://%s%s" % (urldata[0], urldata[1], urldata[2]) fragment = urldata[4] # We get our path here path = uri.replace(config['base_url'], '') req.add_http({'PATH_INFO': path, "form": {}}) from Pyblosxom.pyblosxom import blosxom_process_path_info blosxom_process_path_info({'request': req}) args = {'request': req} from Pyblosxom.pyblosxom import blosxom_file_list_handler es = blosxom_file_list_handler(args) # We're almost there if len(es) == 1 and path.find(es[0]['file_path']) >= 0: return es[0] # Could be a fragment link for i in es: if i['fn'] == fragment: return i # Point of no return if len(es) >= 1: raise Fault(0x0021, "%s cannot be used as a target" % uri) else: raise Fault(0x0020, "%s does not exist") def pingback(request, source, target): logger = tools.get_logger() logger.info("pingback started") source_file = urllib.urlopen(source.split('#')[0]) if source_file.headers.get('error', '') == '404': raise Fault(0x0010, "Target %s not exists" % target) source_page = parser() source_page.feed(source_file.read()) source_file.close() if source_page.title == "": source_page.title = source if not target in source_page.hrefs: raise Fault(0x0011, "%s does not point to %s" % (source, target)) target_entry = fileFor(request, target) body = '' try: from rssfinder import getFeeds from rssparser import parse baseurl = source.split("#")[0] for feed in getFeeds(baseurl): for item in parse(feed)['items']: if item['link'] == source: if 'title' in item: source_page.title = item['title'] if 'content_encoded' in item: body = item['content_encoded'].strip() if 'description' in item: body = item['description'].strip() or body body = re.compile('<.*?>', re.S).sub('', body) body = re.sub('\s+', ' ', body) body = body[:body.rfind(' ', 0, 250)][:250] + " ...
        " except: pass cmt = {'title': source_page.title, 'author': 'Pingback from %s' % source_page.title, 'pubDate': str(time.time()), 'link': source, 'source': '', 'description': body} # run anti-spam plugins argdict = {"request": request, "comment": cmt} reject = tools.run_callback("trackback_reject", argdict, donefunc=lambda x: x != 0) if isinstance(reject, (tuple, list)) and len(reject) == 2: reject_code, reject_message = reject else: reject_code, reject_message = reject, "Pingback rejected." if reject_code == 1: raise Fault(0x0031, reject_message) from comments import writeComment config = request.get_configuration() data = request.get_data() data['entry_list'] = [target_entry] # TODO: Check if comment from the URL exists writeComment(request, config, data, cmt, config['blog_encoding']) return "success pinging %s from %s\n" % (target, source) def cb_xmlrpc_register(args): """ Register as a pyblosxom XML-RPC plugin """ args['methods'].update({'pingback.ping': pingback}) return args pyblosxom-1.5.3/Pyblosxom/plugins/yeararchives.py000066400000000000000000000174031217606004600222770ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2004-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Summary ======= Walks through your blog root figuring out all the available years for the archives list. It stores the years with links to year summaries in the variable ``$(archivelinks)``. You should put this variable in either your head or foot template. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.yeararchives`` to the ``load_plugins`` list in your ``config.py`` file. 2. Add ``$(archivelinks)`` to your head and/or foot templates. 3. Configure as documented below. Usage ===== When the user clicks on one of the year links (e.g. ``http://base_url/2004/``), then yeararchives will display a summary page for that year. The summary is generated using the ``yearsummarystory`` template for each month in the year. My ``yearsummarystory`` template looks like this::
        $title
        $body
        The ``$(archivelinks)`` link can be configured with the ``archive_template`` config variable. It uses the Python string formatting syntax. Example:: py['archive_template'] = ( '' '%(Y)s
        ') The vars available with typical example values are:: Y 4-digit year ex: '1978' y 2-digit year ex: '78' f the flavour ex: 'html' .. Note:: The ``archive_template`` variable value is formatted using Python string formatting rules--not Pyblosxom template rules! """ __author__ = "Will Kahn-Greene" __email__ = "willg at bluesock dot org" __version__ = "2010-05-08" __url__ = "http://pyblosxom.github.com/" __description__ = "Builds year-based archives listing." __category__ = "archives" __license__ = "MIT" __registrytags__ = "1.4, 1.5, core" from Pyblosxom import tools, entries from Pyblosxom.memcache import memcache_decorator from Pyblosxom.tools import pwrap import time def verify_installation(request): config = request.get_configuration() if not 'archive_template' in config: pwrap( "missing optional config property 'archive_template' which " "allows you to specify how the archive links are created. " "refer to yeararchives plugin documentation for more details.") return True class YearArchives: def __init__(self, request): self._request = request self._archives = None self._items = None @memcache_decorator('yeararchives', True) def __str__(self): if self._archives == None: self.gen_linear_archive() return self._archives def gen_linear_archive(self): config = self._request.get_configuration() data = self._request.get_data() root = config["datadir"] baseurl = config.get("base_url", "") archives = {} archive_list = tools.walk(self._request, root) items = [] fulldict = {} fulldict.update(config) fulldict.update(data) flavour = data.get( "flavour", config.get("default_flavour", "html")) template = config.get( 'archive_template', '%(Y)s
        ') for mem in archive_list: timetuple = tools.filestat(self._request, mem) timedict = {} for x in ["m", "Y", "y", "d"]: timedict[x] = time.strftime("%" + x, timetuple) fulldict.update(timedict) fulldict["f"] = flavour year = fulldict["Y"] if not year in archives: archives[year] = template % fulldict items.append( ["%(Y)s-%(m)s" % fulldict, "%(Y)s-%(m)s-%(d)s" % fulldict, time.mktime(timetuple), mem]) arc_keys = archives.keys() arc_keys.sort() arc_keys.reverse() result = [] for key in arc_keys: result.append(archives[key]) self._archives = '\n'.join(result) self._items = items def new_entry(request, yearmonth, body): """ Takes a bunch of variables and generates an entry out of it. It creates a timestamp so that conditionalhttp can handle it without getting all fussy. """ entry = entries.base.EntryBase(request) entry['title'] = yearmonth entry['filename'] = yearmonth + "/summary" entry['file_path'] = yearmonth entry._id = yearmonth + "::summary" entry["template_name"] = "yearsummarystory" entry["nocomments"] = "yes" entry["absolute_path"] = "" entry["fn"] = "" entry.set_time(time.strptime(yearmonth, "%Y-%m")) entry.set_data(body) return entry INIT_KEY = "yeararchives_initiated" def cb_prepare(args): request = args["request"] data = request.get_data() data["archivelinks"] = YearArchives(request) def cb_date_head(args): request = args["request"] data = request.get_data() if INIT_KEY in data: args["template"] = "" return args def parse_path_info(path): """Returns None or (year, flav) tuple. Handles urls of this type: - /2003 - /2003/ - /2003/index - /2003/index.flav """ path = path.split("/") path = [m for m in path if m] if not path: return year = path[0] if not year.isdigit() or not len(year) == 4: return if len(path) == 1: return (year, None) if len(path) == 2 and path[1].startswith("index"): flav = None if "." in path[1]: flav = path[1].split(".", 1)[1] return (year, flav) return def cb_filelist(args): request = args["request"] pyhttp = request.get_http() data = request.get_data() config = request.get_configuration() baseurl = config.get("base_url", "") path = pyhttp["PATH_INFO"] ret = parse_path_info(path) if ret == None: return # note: returned flavour is None if there is no .flav appendix year, flavour = ret data[INIT_KEY] = 1 # get all the entries wa = YearArchives(request) wa.gen_linear_archive() items = wa._items # peel off the items for this year items = [m for m in items if m[0].startswith(year)] items.sort() items.reverse() # Set and use current (or default) flavour for permalinks if not flavour: flavour = data.get( "flavour", config.get("default_flavour", "html")) data["flavour"] = flavour l = ("(%(path)s) %(title)s
        ") e = "\n%s\n%s\n" d = "" m = "" day = [] month = [] entrylist = [] for mem in items: if not m: m = mem[0] if not d: d = mem[1] if m != mem[0]: month.append(e % (d, "\n".join(day))) entrylist.append(new_entry(request, m, "\n".join(month))) m = mem[0] d = mem[1] day = [] month = [] elif d != mem[1]: month.append(e % (d, "\n".join(day))) d = mem[1] day = [] entry = entries.fileentry.FileEntry( request, mem[3], config['datadir']) day.append(l % entry) if day: month.append(e % (d, "\n".join(day))) if month: entrylist.append(new_entry(request, m, "\n".join(month))) return entrylist pyblosxom-1.5.3/Pyblosxom/pyblosxom.py000066400000000000000000001305421217606004600201650ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """This is the main module for Pyblosxom functionality. Pyblosxom's setup and default handlers are defined here. """ from __future__ import nested_scopes, generators # Python imports import os import time import locale import sys import os.path import cgi try: from cStringIO import StringIO except ImportError: from StringIO import StringIO # Pyblosxom imports from Pyblosxom import __version__ from Pyblosxom import crashhandling from Pyblosxom import tools from Pyblosxom import plugin_utils from Pyblosxom.entries.fileentry import FileEntry VERSION = __version__ class Pyblosxom: """Main class for Pyblosxom functionality. It handles initialization, defines default behavior, and also pushes the request through all the steps until the output is rendered and we're complete. """ def __init__(self, config, environ, data=None): """Sets configuration and environment and creates the Request object. :param config: dict containing the configuration variables. :param environ: dict containing the environment variables. :param data: dict containing data variables. """ # FIXME: These shouldn't be here. config['pyblosxom_name'] = "pyblosxom" config['pyblosxom_version'] = __version__ self._config = config self._request = Request(config, environ, data) def initialize(self): """The initialize step further initializes the Request by setting additional information in the ``data`` dict, registering plugins, and entryparsers. """ data = self._request.get_data() pyhttp = self._request.get_http() config = self._request.get_configuration() # initialize the locale, if wanted (will silently fail if locale # is not available) if config.get('locale', None): try: locale.setlocale(locale.LC_ALL, config['locale']) except locale.Error: # invalid locale pass # initialize the tools module tools.initialize(config) data["pyblosxom_version"] = __version__ data['pi_bl'] = '' # if the user specifies base_url in config, we use that. # otherwise we compose it from SCRIPT_NAME in the environment # or we leave it blank. if not "base_url" in config: if pyhttp.has_key('SCRIPT_NAME'): # allow http and https config['base_url'] = '%s://%s%s' % \ (pyhttp['wsgi.url_scheme'], pyhttp['HTTP_HOST'], pyhttp['SCRIPT_NAME']) else: config["base_url"] = "" # take off the trailing slash for base_url if config['base_url'].endswith("/"): config['base_url'] = config['base_url'][:-1] datadir = config["datadir"] if datadir.endswith("/") or datadir.endswith("\\"): datadir = datadir[:-1] config['datadir'] = datadir # import and initialize plugins plugin_utils.initialize_plugins(config.get("plugin_dirs", []), config.get("load_plugins", None)) # entryparser callback is run here first to allow other # plugins register what file extensions can be used data['extensions'] = tools.run_callback("entryparser", {'txt': blosxom_entry_parser}, mappingfunc=lambda x,y:y, defaultfunc=lambda x:x) def cleanup(self): """This cleans up Pyblosxom after a run. This should be called when Pyblosxom has done everything it needs to do before exiting. """ # log some useful stuff for debugging # this will only be logged if the log_level is "debug" log = tools.getLogger() response = self.get_response() log.debug("status = %s" % response.status) log.debug("headers = %s" % response.headers) def get_request(self): """Returns the Request object for this Pyblosxom instance. """ return self._request getRequest = tools.deprecated_function(get_request) def get_response(self): """Returns the Response object associated with this Request. """ return self._request.getResponse() getResponse = tools.deprecated_function(get_response) def run(self, static=False): """This is the main loop for Pyblosxom. This method will run the handle callback to allow registered handlers to handle the request. If nothing handles the request, then we use the ``default_blosxom_handler``. :param static: True if Pyblosxom should execute in "static rendering mode" and False otherwise. """ self.initialize() # buffer the input stream in a StringIO instance if dynamic # rendering is used. This is done to have a known/consistent # way of accessing incomming data. if static == False: self.get_request().buffer_input_stream() # run the start callback tools.run_callback("start", {'request': self._request}) # allow anyone else to handle the request at this point handled = tools.run_callback("handle", {'request': self._request}, mappingfunc=lambda x,y:x, donefunc=lambda x:x) if not handled == 1: blosxom_handler(self._request) # do end callback tools.run_callback("end", {'request': self._request}) # we're done, clean up. # only call this if we're not in static rendering mode. if static == False: self.cleanup() def run_callback(self, callback="help"): """This method executes the start callback (initializing plugins), executes the requested callback, and then executes the end callback. This is useful for scripts outside of Pyblosxom that need to do things inside of the Pyblosxom framework. If you want to run a callback from a plugin, use ``tools.run_callback`` instead. :param callback: the name of the callback to execute. :returns: the results of the callback. """ self.initialize() # run the start callback tools.run_callback("start", {'request': self._request}) # invoke all callbacks for the 'callback' handled = tools.run_callback(callback, {'request': self._request}, mappingfunc=lambda x,y:x, donefunc=lambda x:x) # do end callback tools.run_callback("end", {'request': self._request}) return handled runCallback = tools.deprecated_function(run_callback) def run_render_one(self, url, headers): """Renders a single page from the blog. :param url: the url to render--this has to be relative to the base url for this blog. :param headers: True if you want headers to be rendered and False if not. """ self.initialize() config = self._request.get_configuration() if url.find("?") != -1: url = url[:url.find("?")] query = url[url.find("?")+1:] else: query = "" url = url.replace(os.sep, "/") response = tools.render_url(config, url, query) if headers: response.send_headers(sys.stdout) response.send_body(sys.stdout) print response.read() # we're done, clean up self.cleanup() def run_static_renderer(self, incremental=False): """This will go through all possible things in the blog and statically render everything to the ``static_dir`` specified in the config file. This figures out all the possible ``path_info`` settings and calls ``self.run()`` a bazillion times saving each file. :param incremental: Whether (True) or not (False) to incrementally render the pages. If we're incrementally rendering pages, then we render only the ones that have changed. """ self.initialize() config = self._request.get_configuration() data = self._request.get_data() print "Performing static rendering." if incremental: print "Incremental is set." staticdir = config.get("static_dir", "") datadir = config["datadir"] if not staticdir: print "Error: You must set static_dir in your config file." return 0 flavours = config.get("static_flavours", ["html"]) index_flavours = config.get("static_index_flavours", ["html"]) renderme = [] monthnames = config.get("static_monthnames", True) monthnumbers = config.get("static_monthnumbers", False) yearindexes = config.get("static_yearindexes", True) dates = {} categories = {} # first we handle entries and categories listing = tools.walk(self._request, datadir) for mem in listing: # skip the ones that have bad extensions ext = mem[mem.rfind(".")+1:] if not ext in data["extensions"].keys(): continue # grab the mtime of the entry file mtime = time.mktime(tools.filestat(self._request, mem)) # remove the datadir from the front and the bit at the end mem = mem[len(datadir):mem.rfind(".")] # this is the static filename fn = os.path.normpath(staticdir + mem) # grab the mtime of one of the statically rendered file try: smtime = os.stat(fn + "." + flavours[0])[8] except: smtime = 0 # if the entry is more recent than the static, we want to # re-render if smtime < mtime or not incremental: # grab the categories temp = os.path.dirname(mem).split(os.sep) for i in range(len(temp)+1): p = os.sep.join(temp[0:i]) categories[p] = 0 # grab the date mtime = time.localtime(mtime) year = time.strftime("%Y", mtime) month = time.strftime("%m", mtime) day = time.strftime("%d", mtime) if yearindexes: dates[year] = 1 if monthnumbers: dates[year + "/" + month] = 1 dates[year + "/" + month + "/" + day] = 1 if monthnames: monthname = tools.num2month[month] dates[year + "/" + monthname] = 1 dates[year + "/" + monthname + "/" + day] = 1 # toss in the render queue for f in flavours: renderme.append((mem + "." + f, "")) print "rendering %d entries." % len(renderme) # handle categories categories = categories.keys() categories.sort() # if they have stuff in their root category, it'll add a "/" # to the category list and we want to remove that because it's # a duplicate of "". if "/" in categories: categories.remove("/") print "rendering %d category indexes." % len(categories) for mem in categories: mem = os.path.normpath(mem + "/index.") for f in index_flavours: renderme.append((mem + f, "")) # now we handle dates dates = dates.keys() dates.sort() dates = ["/" + d for d in dates] print "rendering %d date indexes." % len(dates) for mem in dates: mem = os.path.normpath(mem + "/index.") for f in index_flavours: renderme.append((mem + f, "")) # now we handle arbitrary urls additional_stuff = config.get("static_urls", []) print "rendering %d arbitrary urls." % len(additional_stuff) for mem in additional_stuff: if mem.find("?") != -1: url = mem[:mem.find("?")] query = mem[mem.find("?")+1:] else: url = mem query = "" renderme.append((url, query)) # now we pass the complete render list to all the plugins via # cb_staticrender_filelist and they can add to the filelist # any (url, query) tuples they want rendered. print "(before) building %s files." % len(renderme) tools.run_callback("staticrender_filelist", {'request': self._request, 'filelist': renderme, 'flavours': flavours, 'incremental': incremental}) renderme = sorted(set(renderme)) print "building %s files." % len(renderme) for url, q in renderme: url = url.replace(os.sep, "/") print "rendering '%s' ..." % url tools.render_url_statically(dict(config), url, q) # we're done, clean up self.cleanup() Pyblosxom = Pyblosxom class PyblosxomWSGIApp: """This class is the WSGI application for Pyblosxom. """ def __init__(self, environ=None, start_response=None, configini=None): """ Make WSGI app for Pyblosxom. :param environ: FIXME :param start_response: FIXME :param configini: Dict encapsulating information from a ``config.ini`` file or any other property file that will override the ``config.py`` file. """ self.environ = environ self.start_response = start_response if configini == None: configini = {} _config = tools.convert_configini_values(configini) import config self.config = dict(config.py) self.config.update(_config) if "codebase" in _config: sys.path.insert(0, _config["codebase"]) def run_pyblosxom(self, env, start_response): """ Executes a single run of Pyblosxom wrapped in the crash handler. """ response = None try: # ensure that PATH_INFO exists. a few plugins break if this is # missing. if "PATH_INFO" not in env: env["PATH_INFO"] = "" p = Pyblosxom(dict(self.config), env) p.run() response = p.get_response() except Exception: ch = crashhandling.CrashHandler(True, env) response = ch.handle_by_response(*sys.exc_info()) start_response(response.status, list(response.headers.items())) response.seek(0) return response.read() def __call__(self, env, start_response): return [self.run_pyblosxom(env, start_response)] def __iter__(self): yield self.run_pyblosxom(self.environ, self.start_response) # Do this for historical reasons PyblosxomWSGIApp = PyblosxomWSGIApp def pyblosxom_app_factory(global_config, **local_config): """App factory for paste. :returns: WSGI application """ conf = global_config.copy() conf.update(local_config) conf.update(dict(local_config=local_config, global_config=global_config)) if "configpydir" in conf: sys.path.insert(0, conf["configpydir"]) return PyblosxomWSGIApp(configini=conf) class EnvDict(dict): """Wrapper arround a dict to provide a backwards compatible way to get the ``form`` with syntax as:: request.get_http()['form'] instead of:: request.get_form() """ def __init__(self, request, env): """Wraps an environment (which is a dict) and a request. :param request: the Request object for this request. :param env: the environment dict for this request. """ dict.__init__(self) self._request = request self.update(env) def __getitem__(self, key): """If the key argument is ``form``, we return ``_request.get_form()``. Otherwise this returns the item for that key in the wrapped dict. """ if key == "form": return self._request.get_form() return dict.__getitem__(self, key) class Request(object): """ This class holds the Pyblosxom request. It holds configuration information, HTTP/CGI information, and data that we calculate and transform over the course of execution. There should be only one instance of this class floating around and it should get created by ``pyblosxom.cgi`` and passed into the Pyblosxom instance which will do further manipulation on the Request instance. """ def __init__(self, config, environ, data): """Sets configuration and environment. Creates the Response object which handles all output related functionality. :param config: dict containing configuration variables. :param environ: dict containing environment variables. :param data: dict containing data variables. """ # this holds configuration data that the user changes in # config.py self._configuration = config # this holds HTTP/CGI oriented data specific to the request # and the environment in which the request was created self._http = EnvDict(self, environ) # this holds run-time data which gets created and transformed # by pyblosxom during execution if data == None: self._data = dict() else: self._data = data # this holds the input stream. initialized for dynamic # rendering in Pyblosxom.run. for static rendering there is # no input stream. self._in = StringIO() # copy methods to the Request object. self.read = self._in.read self.readline = self._in.readline self.readlines = self._in.readlines self.seek = self._in.seek self.tell = self._in.tell # this holds the FieldStorage instance. # initialized when request.get_form is called the first time self._form = None self._response = None # create and set the Response self.setResponse(Response(self)) def __iter__(self): """ Can't copy the __iter__ method over from the StringIO instance cause iter looks for the method in the class instead of the instance. See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/252151 """ return self._in def buffer_input_stream(self): """ Buffer the input stream in a StringIO instance. This is done to have a known/consistent way of accessing incomming data. For example the input stream passed by mod_python does not offer the same functionallity as ``sys.stdin``. """ # TODO: tests on memory consumption when uploading huge files pyhttp = self.get_http() winput = pyhttp['wsgi.input'] method = pyhttp["REQUEST_METHOD"] # there's no data on stdin for a GET request. pyblosxom # will block indefinitely on the read for a GET request with # thttpd. if method != "GET": try: length = int(pyhttp.get("CONTENT_LENGTH", 0)) except ValueError: length = 0 if length > 0: self._in.write(winput.read(length)) # rewind to start self._in.seek(0) def set_response(self, response): """Sets the Response object. """ self._response = response # for backwards compatibility self.get_configuration()['stdoutput'] = response setResponse = tools.deprecated_function(set_response) def get_response(self): """Returns the Response for this request. """ return self._response getResponse = tools.deprecated_function(get_response) def _getform(self): form = cgi.FieldStorage(fp=self._in, environ=self._http, keep_blank_values=0) # rewind the input buffer self._in.seek(0) return form def get_form(self): """Returns the form data submitted by the client. The ``form`` instance is created only when requested to prevent overhead and unnecessary consumption of the input stream. :returns: a ``cgi.FieldStorage`` instance. """ if self._form == None: self._form = self._getform() return self._form getForm = tools.deprecated_function(get_form) def get_configuration(self): """Returns the *actual* configuration dict. The configuration dict holds values that the user sets in their ``config.py`` file. Modifying the contents of the dict will affect all downstream processing. """ return self._configuration getConfiguration = tools.deprecated_function(get_configuration) def get_http(self): """Returns the *actual* http dict. Holds HTTP/CGI data derived from the environment of execution. Modifying the contents of the dict will affect all downstream processing. """ return self._http getHttp = tools.deprecated_function(get_http) def get_data(self): """Returns the *actual* data dict. Holds run-time data which is created and transformed by pyblosxom during execution. Modifying the contents of the dict will affect all downstream processing. """ return self._data getData = tools.deprecated_function(get_data) def add_http(self, d): """Takes in a dict and adds/overrides values in the existing http dict with the new values. """ self._http.update(d) addHttp = tools.deprecated_function(add_http) def add_data(self, d): """Takes in a dict and adds/overrides values in the existing data dict with the new values. """ self._data.update(d) addData = tools.deprecated_function(add_data) def add_configuration(self, newdict): """Takes in a dict and adds/overrides values in the existing configuration dict with the new values. """ self._configuration.update(newdict) addConfiguration = tools.deprecated_function(add_configuration) def __getattr__(self, name): if name in ["config", "configuration", "conf"]: return self._configuration if name == "data": return self._data if name == "http": return self._http raise AttributeError, name def __repr__(self): return "Request" class Response(object): """Response class to handle all output related tasks in one place. This class is basically a wrapper arround a ``StringIO`` instance. It also provides methods for managing http headers. """ def __init__(self, request): """Sets the ``Request`` object that leaded to this response. Creates a ``StringIO`` that is used as a output buffer. """ self._request = request self._out = StringIO() self._headers_sent = False self.headers = {} self.status = "200 OK" self.close = self._out.close self.flush = self._out.flush self.read = self._out.read self.readline = self._out.readline self.readlines = self._out.readlines self.seek = self._out.seek self.tell = self._out.tell self.write = self._out.write self.writelines = self._out.writelines def __iter__(self): """Can't copy the ``__iter__`` method over from the ``StringIO`` instance because iter looks for the method in the class instead of the instance. See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/252151 """ return self._out def set_status(self, status): """Sets the status code for this response. The status should be a valid HTTP response status. Examples: >>> resp.set_status("200 OK") >>> resp.set_status("404 Not Found") :param status: the status string. """ self.status = status setStatus = tools.deprecated_function(set_status) def get_status(self): """Returns the status code and message of this response. """ return self.status def add_header(self, key, value): """Populates the HTTP header with lines of text. Sets the status code on this response object if the given argument list containes a 'Status' header. Example: >>> resp.add_header("Content-type", "text/plain") >>> resp.add_header("Content-Length", "10500") :raises ValueError: This happens when the parameters are not correct. """ key = key.strip() if key.find(' ') != -1 or key.find(':') != -1: raise ValueError, 'There should be no spaces in header keys' value = value.strip() if key.lower() == "status": self.setStatus(str(value)) else: self.headers.update({key: str(value)}) addHeader = tools.deprecated_function(add_header) def get_headers(self): """Returns the headers. """ return self.headers getHeaders = tools.deprecated_function(get_headers) def send_headers(self, out): """Send HTTP Headers to the given output stream. .. Note:: This prints the headers and then the ``\\n\\n`` that separates headers from the body. :param out: The file-like object to print headers to. """ out.write("Status: %s\n" % self.status) out.write('\n'.join(['%s: %s' % (hkey, self.headers[hkey]) for hkey in self.headers.keys()])) out.write('\n\n') self._headers_sent = True sendHeaders = tools.deprecated_function(send_headers) def send_body(self, out): """Send the response body to the given output stream. :param out: the file-like object to print the body to. """ self.seek(0) try: out.write(self.read()) except IOError: # this is usually a Broken Pipe because the client dropped the # connection. so we skip it. pass sendBody = tools.deprecated_function(send_body) # # blosxom behavior stuff # def blosxom_handler(request): """This is the default blosxom handler. It calls the renderer callback to get a renderer. If there is no renderer, it uses the blosxom renderer. It calls the pathinfo callback to process the path_info http variable. It calls the filelist callback to build a list of entries to display. It calls the prepare callback to do any additional preparation before rendering the entries. Then it tells the renderer to render the entries. :param request: the request object. """ config = request.get_configuration() data = request.get_data() # go through the renderer callback to see if anyone else wants to # render. this renderer gets stored in the data dict for # downstream processing. rend = tools.run_callback('renderer', {'request': request}, donefunc = lambda x: x != None, defaultfunc = lambda x: None) if not rend: # get the renderer we want to use rend = config.get("renderer", "blosxom") # import the renderer rend = tools.importname("Pyblosxom.renderers", rend) # get the renderer object rend = rend.Renderer(request, config.get("stdoutput", sys.stdout)) data['renderer'] = rend # generate the timezone variable data["timezone"] = time.tzname[time.localtime()[8]] # process the path info to determine what kind of blog entry(ies) # this is tools.run_callback("pathinfo", {"request": request}, donefunc=lambda x:x != None, defaultfunc=blosxom_process_path_info) # call the filelist callback to generate a list of entries data["entry_list"] = tools.run_callback( "filelist", {"request": request}, donefunc=lambda x:x != None, defaultfunc=blosxom_file_list_handler) # figure out the blog-level mtime which is the mtime of the head # of the entry_list entry_list = data["entry_list"] if isinstance(entry_list, list) and len(entry_list) > 0: mtime = entry_list[0].get("mtime", time.time()) else: mtime = time.time() mtime_tuple = time.localtime(mtime) mtime_gmtuple = time.gmtime(mtime) data["latest_date"] = time.strftime('%a, %d %b %Y', mtime_tuple) # Make sure we get proper 'English' dates when using standards loc = locale.getlocale(locale.LC_ALL) locale.setlocale(locale.LC_ALL, 'C') data["latest_w3cdate"] = time.strftime('%Y-%m-%dT%H:%M:%SZ', mtime_gmtuple) data['latest_rfc822date'] = time.strftime('%a, %d %b %Y %H:%M GMT', mtime_gmtuple) # set the locale back locale.setlocale(locale.LC_ALL, loc) # we pass the request with the entry_list through the prepare # callback giving everyone a chance to transform the data. the # request is modified in place. tools.run_callback("prepare", {"request": request}) # now we pass the entry_list through the renderer entry_list = data["entry_list"] renderer = data['renderer'] if renderer and not renderer.rendered: if entry_list: renderer.set_content(entry_list) # Log it as success tools.run_callback("logrequest", {'filename':config.get('logfile',''), 'return_code': '200', 'request': request}) else: renderer.add_header('Status', '404 Not Found') renderer.set_content( {'title': 'The page you are looking for is not available', 'body': 'Somehow I cannot find the page you want. ' + 'Go Back to %s?' % (config["base_url"], config["blog_title"])}) # Log it as failure tools.run_callback("logrequest", {'filename':config.get('logfile',''), 'return_code': '404', 'request': request}) renderer.render() elif not renderer: output = config.get('stdoutput', sys.stdout) output.write("Content-Type: text/plain\n\n" + "There is something wrong with your setup.\n" + "Check your config files and verify that your " + "configuration is correct.\n") cache = tools.get_cache(request) if cache: cache.close() def blosxom_entry_parser(filename, request): """Open up a ``.txt`` file and read its contents. The first line becomes the title of the entry. The other lines are the body of the entry. :param filename: a filename to extract data and metadata from :param request: a standard request object :returns: dict containing parsed data and meta data with the particular file (and plugin) """ config = request.get_configuration() entry_data = {} f = open(filename, "r") lines = f.readlines() f.close() # the file has nothing in it... so we're going to return a blank # entry data object. if len(lines) == 0: return {"title": "", "body": ""} # the first line is the title entry_data["title"] = lines.pop(0).strip() # absorb meta data lines which begin with a # while lines and lines[0].startswith("#"): meta = lines.pop(0) # remove the hash meta = meta[1:].strip() meta = meta.split(" ", 1) # if there's no value, we append a 1 if len(meta) == 1: meta.append("1") entry_data[meta[0].strip()] = meta[1].strip() # call the preformat function args = {'parser': entry_data.get('parser', config.get('parser', 'plain')), 'story': lines, 'request': request} entry_data["body"] = tools.run_callback( 'preformat', args, donefunc=lambda x: x != None, defaultfunc=lambda x: ''.join(x['story'])) # call the postformat callbacks tools.run_callback('postformat', {'request': request, 'entry_data': entry_data}) return entry_data def blosxom_file_list_handler(args): """This is the default handler for getting entries. It takes the request object in and figures out which entries based on the default behavior that we want to show and generates a list of EntryBase subclass objects which it returns. :param args: dict containing the incoming Request object :returns: the content we want to render """ request = args["request"] data = request.get_data() config = request.get_configuration() if data['bl_type'] == 'dir': filelist = tools.walk(request, data['root_datadir'], int(config.get("depth", "0"))) elif data['bl_type'] == 'file': filelist = [data['root_datadir']] else: filelist = [] entrylist = [FileEntry(request, e, data["root_datadir"]) for e in filelist] # if we're looking at a set of archives, remove all the entries # that aren't in the archive if data.get("pi_yr", ""): tmp_pi_mo = data.get("pi_mo", "") datestr = "%s%s%s" % (data.get("pi_yr", ""), tools.month2num.get(tmp_pi_mo, tmp_pi_mo), data.get("pi_da", "")) entrylist = [x for x in entrylist if time.strftime("%Y%m%d%H%M%S", x["timetuple"]).startswith(datestr)] args = {"request": request, "entry_list": entrylist} entrylist = tools.run_callback("sortlist", args, donefunc=lambda x: x != None, defaultfunc=blosxom_sort_list_handler) args = {"request": request, "entry_list": entrylist} entrylist = tools.run_callback("truncatelist", args, donefunc=lambda x: x != None, defaultfunc=blosxom_truncate_list_handler) return entrylist def blosxom_sort_list_handler(args): """Sorts the list based on ``_mtime`` attribute such that most recently written entries are at the beginning of the list and oldest entries are at the end. :param args: args dict with ``request`` object and ``entry_list`` list of entries :returns: the sorted ``entry_list`` """ entrylist = args["entry_list"] entrylist = [(e._mtime, e) for e in entrylist] entrylist.sort() entrylist.reverse() entrylist = [e[1] for e in entrylist] return entrylist def blosxom_truncate_list_handler(args): """If ``config["num_entries"]`` is not 0 and ``data["truncate"]`` is not 0, then this truncates ``args["entry_list"]`` by ``config["num_entries"]``. :param args: args dict with ``request`` object and ``entry_list`` list of entries :returns: the truncated ``entry_list``. """ request = args["request"] entrylist = args["entry_list"] data = request.data config = request.config num_entries = config.get("num_entries", 5) truncate = data.get("truncate", 0) if num_entries and truncate: entrylist = entrylist[:num_entries] return entrylist def blosxom_process_path_info(args): """Process HTTP ``PATH_INFO`` for URI according to path specifications, fill in data dict accordingly. The paths specification looks like this: - ``/foo.html`` and ``/cat/foo.html`` - file foo.* in / and /cat - ``/cat`` - category - ``/2002`` - category - ``/2002`` - year - ``/2002/Feb`` and ``/2002/02`` - Year and Month - ``/cat/2002/Feb/31`` and ``/cat/2002/02/31``- year and month day in category. :param args: dict containing the incoming Request object """ request = args['request'] config = request.get_configuration() data = request.get_data() pyhttp = request.get_http() form = request.get_form() # figure out which flavour to use. the flavour is determined by # looking at the "flav" post-data variable, the "flav" query # string variable, the "default_flavour" setting in the config.py # file, or "html" flav = config.get("default_flavour", "html") if form.has_key("flav"): flav = form["flav"].value data['flavour'] = flav data['pi_yr'] = '' data['pi_mo'] = '' data['pi_da'] = '' path_info = pyhttp.get("PATH_INFO", "") data['root_datadir'] = config['datadir'] data["pi_bl"] = path_info # first we check to see if this is a request for an index and we # can pluck the extension (which is certainly a flavour) right # off. newpath, ext = os.path.splitext(path_info) if newpath.endswith("/index") and ext: # there is a flavour-like thing, so that's our new flavour and # we adjust the path_info to the new filename data["flavour"] = ext[1:] path_info = newpath while path_info and path_info.startswith("/"): path_info = path_info[1:] absolute_path = os.path.join(config["datadir"], path_info) path_info = path_info.split("/") if os.path.isdir(absolute_path): # this is an absolute path data['root_datadir'] = absolute_path data['bl_type'] = 'dir' elif absolute_path.endswith("/index") and \ os.path.isdir(absolute_path[:-6]): # this is an absolute path with /index at the end of it data['root_datadir'] = absolute_path[:-6] data['bl_type'] = 'dir' else: # this is either a file or a date ext = tools.what_ext(data["extensions"].keys(), absolute_path) if not ext: # it's possible we didn't find the file because it's got a # flavour thing at the end--so try removing it and # checking again. newpath, flav = os.path.splitext(absolute_path) if flav: ext = tools.what_ext(data["extensions"].keys(), newpath) if ext: # there is a flavour-like thing, so that's our new # flavour and we adjust the absolute_path and # path_info to the new filename data["flavour"] = flav[1:] absolute_path = newpath path_info, flav = os.path.splitext("/".join(path_info)) path_info = path_info.split("/") if ext: # this is a file data["bl_type"] = "file" data["root_datadir"] = absolute_path + "." + ext else: data["bl_type"] = "dir" # it's possible to have category/category/year/month/day # (or something like that) so we pluck off the categories # here. pi_bl = "" while len(path_info) > 0 and \ not (len(path_info[0]) == 4 and path_info[0].isdigit()): pi_bl = os.path.join(pi_bl, path_info.pop(0)) # handle the case where we do in fact have a category # preceeding the date. if pi_bl: pi_bl = pi_bl.replace("\\", "/") data["pi_bl"] = pi_bl data["root_datadir"] = os.path.join(config["datadir"], pi_bl) if len(path_info) > 0: item = path_info.pop(0) # handle a year token if len(item) == 4 and item.isdigit(): data['pi_yr'] = item item = "" if (len(path_info) > 0): item = path_info.pop(0) # handle a month token if item in tools.MONTHS: data['pi_mo'] = item item = "" if (len(path_info) > 0): item = path_info.pop(0) # handle a day token if len(item) == 2 and item.isdigit(): data["pi_da"] = item item = "" if len(path_info) > 0: item = path_info.pop(0) # if the last item we picked up was "index", then we # just ditch it because we don't need it. if item == "index": item = "" # if we picked off an item we don't recognize and/or # there is still stuff in path_info to pluck out, then # it's likely this wasn't a date. if item or len(path_info) > 0: data["bl_type"] = "dir" data["root_datadir"] = absolute_path # construct our final URL url = config['base_url'] if data['pi_bl'].startswith("/") and url.endswith("/"): url = url[:-1] + data['pi_bl'] elif data['pi_bl'].startswith("/") or url.endswith("/"): url = url + data["pi_bl"] else: url = url + "/" + data['pi_bl'] data['url'] = url # set path_info to our latest path_info data['path_info'] = path_info if data.get("pi_yr"): data["truncate"] = config.get("truncate_date", False) elif data.get("bl_type") == "dir": if data["path_info"] == [''] or data["path_info"] == ['index']: data["truncate"] = config.get("truncate_frontpage", True) else: data["truncate"] = config.get("truncate_category", True) else: data["truncate"] = False def run_pyblosxom(): """Executes Pyblosxom either as a commandline script or CGI script. """ from config import py as cfg env = {} # if there's no REQUEST_METHOD, then this is being run on the # command line and we should execute the command_line_handler. if not "REQUEST_METHOD" in os.environ: from Pyblosxom.commandline import command_line_handler if len(sys.argv) <= 1: sys.argv.append("test") sys.exit(command_line_handler("pyblosxom.cgi", sys.argv)) # names taken from wsgi instead of inventing something new env['wsgi.input'] = sys.stdin env['wsgi.errors'] = sys.stderr # figure out what the protocol is for the wsgi.url_scheme # property. we look at the base_url first and if there's nothing # set there, we look at environ. if 'base_url' in cfg: env['wsgi.url_scheme'] = cfg['base_url'][:cfg['base_url'].find("://")] else: if os.environ.get("HTTPS", "off") in ("on", "1"): env["wsgi.url_scheme"] = "https" else: env['wsgi.url_scheme'] = "http" try: # try running as a WSGI-CGI from wsgiref.handlers import CGIHandler CGIHandler().run(PyblosxomWSGIApp()) except ImportError: # run as a regular CGI if os.environ.get("HTTPS") in ("yes", "on", "1"): env['wsgi.url_scheme'] = "https" for mem in ["HTTP_HOST", "HTTP_USER_AGENT", "HTTP_REFERER", "PATH_INFO", "QUERY_STRING", "REMOTE_ADDR", "REQUEST_METHOD", "REQUEST_URI", "SCRIPT_NAME", "HTTP_IF_NONE_MATCH", "HTTP_IF_MODIFIED_SINCE", "HTTP_COOKIE", "CONTENT_LENGTH", "CONTENT_TYPE", "HTTP_ACCEPT", "HTTP_ACCEPT_ENCODING"]: env[mem] = os.environ.get(mem, "") p = Pyblosxom(dict(cfg), env) p.run() response = p.get_response() response.send_headers(sys.stdout) response.send_body(sys.stdout) pyblosxom-1.5.3/Pyblosxom/renderers/000077500000000000000000000000001217606004600175435ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/renderers/__init__.py000066400000000000000000000014201217606004600216510ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ The end of the Pyblosxom request lifecycle involves rendering the entries we've decided we want to render. This is the job of the renderers. They handle pulling in templates, expanding variables, formatting entries into stories, and then outputting it to stdout (or wherever) for final output. Creating a new renderer involves dropping a new module in this directory and adjusting your config file to use your new module. """ pass pyblosxom-1.5.3/Pyblosxom/renderers/base.py000066400000000000000000000136541217606004600210400ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ The is the base renderer module. If you were to dislike the blosxom renderer and wanted to build a renderer that used a different templating system, you would extend the RendererBase class and implement the functionality required by the other rendering system. For examples, look at the BlosxomRenderer and the Renderer in the debug module. """ import sys import time from Pyblosxom import tools class RendererBase: """ Pyblosxom core handles the Input and Process of the system and passes the result of the process to the Renderers for output. All renderers are child classes of RendererBase. RenderBase will contain the public interfaces for all Renderer onject. """ def __init__(self, request, stdoutput=sys.stdout): """ Constructor: Initializes the Renderer :param request: The ``Pyblosxom.pyblosxom.Request`` object :param stdoutput: File like object to print to. """ self._request = request # this is a list of tuples of the form (key, value) self._header = [] self._out = stdoutput self._content = None self._content_mtime = None self._needs_content_type = 1 self.rendered = None def write(self, data): """ Convenience method for programs to use instead of accessing self._out.write() Other classes can override this if there is a unique way to write out data, for example, a two stream output, e.g. one output stream and one output log stream. Another use for this could be a plugin that writes out binary files, but because renderers and other frameworks may probably not want you to write to ``stdout`` directly, this method assists you nicely. For example:: def cb_start(args): req = args['request'] renderer = req['renderer'] if reqIsGif and gifFileExists(theGifFile): # Read the file data = open(theGifFile).read() # Modify header renderer.addHeader('Content-type', 'image/gif') renderer.addHeader('Content-Length', len(data)) renderer.showHeaders() # Write to output renderer.write(data) # Tell pyblosxom not to render anymore as data is # processed already renderer.rendered = 1 This simple piece of pseudocode explains what you could do with this method, though I highly don't recommend this, unless pyblosxom is running continuously. :param data: Piece of string you want printed """ self._out.write(data) def add_header(self, *args): """ Populates the HTTP header with lines of text :param args: Paired list of headers :raises ValueError: This happens when the parameters are not correct """ args = list(args) if len(args) % 2 != 0: raise ValueError('Headers recieved are not in the correct form') while args: key = args.pop(0).strip() if key.find(' ') != -1 or key.find(':') != -1: raise ValueError('There should be no spaces in header keys') value = args.pop(0).strip() self._header.append( (key, value) ) addHeader = tools.deprecated_function(add_header) def set_content(self, content): """ Sets the content. The content can be any of the following: * dict * list of entries :param content: the content to be displayed """ self._content = content if isinstance(self._content, dict): mtime = self._content.get("mtime", time.time()) elif isinstance(self._content, list): mtime = self._content[0].get("mtime", time.time()) else: mtime = time.time() self._content_mtime = mtime setContent = tools.deprecated_function(set_content) def get_content(self): """ Return the content field This is exposed for blosxom callbacks. :returns: content """ return self._content getContent = tools.deprecated_function(get_content) def needs_content_type(self, flag): """ Use the renderer to determine 'Content-Type: x/x' default is to use the renderer for Content-Type, set flag to None to indicate no Content-Type generation. :param flag: True of false value """ self._needs_content_type = flag needsContentType = tools.deprecated_function(needs_content_type) def show_headers(self): """ Updated the headers of the ``Response`` instance. This is here for backwards compatibility. """ response = self._request.getResponse() for k, v in self._header: response.addHeader(k, v) showHeaders = tools.deprecated_function(show_headers) def render(self, header=True): """ Do final rendering. :param header: whether (True) or not (False) to show the headers """ if header: if self._header: self.show_headers() else: self.add_header('Content-Type', 'text/plain') self.show_headers() if self._content: self.write(self._content) self.rendered = 1 class Renderer(RendererBase): """ This is a null renderer. """ pass pyblosxom-1.5.3/Pyblosxom/renderers/blosxom.py000066400000000000000000000367521217606004600216150ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ This is the default blosxom renderer. It tries to match the behavior of the blosxom renderer. """ import os import sys from Pyblosxom import tools from Pyblosxom.renderers.base import RendererBase class NoSuchFlavourException(Exception): """ This exception gets thrown when the flavour requested is not available in this blog. """ pass def get_included_flavour(taste): """ Pyblosxom comes with flavours in taste.flav directories in the flavours subdirectory of the Pyblosxom package. This method pulls the template files for the associated taste (assuming it exists) or None if it doesn't. :param taste: The name of the taste. e.g. "html", "rss", ... :returns: A dict of template type to template file or None """ path = __file__[:__file__.rfind(os.sep)] path = path[:path.rfind(os.sep)+1] + "flavours" + os.sep path = path + taste + ".flav" if os.path.isdir(path): template_files = os.listdir(path) template_d = {} for mem in template_files: name, ext = os.path.splitext(mem) if ext not in ["." + taste, ""] or name.startswith("."): continue template_d[name] = os.path.join(path, mem) return template_d return None def get_flavour_from_dir(path, taste): """ Tries to get the template files for the flavour of a certain taste (html, rss, atom10, ...) in a directory. The files could be in the directory or in a taste.flav subdirectory. :param path: the path of the directory to look for the flavour templates in :param taste: the flavour files to look for (e.g. html, rss, atom10, ...) :returns: the map of template name to template file path """ template_d = {} # if we have a taste.flav directory, we check there if os.path.isdir(path + os.sep + taste + ".flav"): newpath = path + os.sep + taste + ".flav" template_files = os.listdir(newpath) for mem in template_files: name, ext = os.path.splitext(mem) if ext not in ["." + taste, ""]: continue template_d[name] = os.path.join(path + os.sep + taste + ".flav", mem) return template_d # now we check the directory itself for flavour templates template_files = os.listdir(path) for mem in template_files: if not mem.endswith("." + taste): continue template_d[os.path.splitext(mem)[0]] = path + os.sep + mem if template_d: return template_d return None class BlosxomRenderer(RendererBase): """ This is the default blosxom renderer. It tries to match the behavior of the blosxom renderer. """ def __init__(self, request, stdoutput=sys.stdout): RendererBase.__init__(self, request, stdoutput) config = request.get_configuration() self._request = request self.flavour = None def get_parse_vars(self): """Returns a dict starting with standard filters, config information, then data information. This allows vars to override each other correctly. For example, plugins should be adding to the data dict which will override stuff in the config dict. """ parsevars = dict(tools.STANDARD_FILTERS) parsevars.update(self._request.config) parsevars.update(self._request.data) return parsevars def get_flavour(self, taste='html'): """ This retrieves all the template files for a given flavour taste. This will first pull the templates for the default flavour of this taste if there are any. Then it looks at EITHER the configured datadir OR the flavourdir (if configured). It'll go through directories overriding the template files it has already picked up descending the category path of the Pyblosxom request. For example, if the user requested the ``html`` flavour and is looking at an entry in the category ``dev/pyblosxom``, then ``get_flavour`` will: 1. pick up the flavour files in the default html flavour 2. start in EITHER datadir OR flavourdir (if configured) 3. override the default html flavour files with html flavour files in this directory or in ``html.flav/`` subdirectory 4. override the html flavour files it's picked up so far with html files in ``dev/`` or ``dev/html.flav/`` 5. override the html flavour files it's picked up so far with html files in ``dev/pyblosxom/`` or ``dev/pyblosxom/html.flav/`` If it doesn't find any flavour files at all, then it returns None which indicates the flavour doesn't exist in this blog. :param taste: the taste to retrieve flavour files for. :returns: mapping of template name to template file data """ data = self._request.get_data() config = self._request.get_configuration() datadir = config["datadir"] # if they have flavourdir set, then we look there. otherwise # we look in the datadir. flavourdir = config.get("flavourdir", datadir) # first we grab the flavour files for the included flavour (if # we have one). template_d = get_included_flavour(taste) if not template_d: template_d = {} pathinfo = list(data["path_info"]) # check the root of flavourdir for templates new_files = get_flavour_from_dir(flavourdir, taste) if new_files: template_d.update(new_files) # go through all the directories from the flavourdir all # the way up to the root_datadir. this way template files # can override template files in parent directories. while len(pathinfo) > 0: flavourdir = os.path.join(flavourdir, pathinfo.pop(0)) if os.path.isfile(flavourdir): break if not os.path.isdir(flavourdir): break new_files = get_flavour_from_dir(flavourdir, taste) if new_files: template_d.update(new_files) # if we still haven't found our flavour files, we raise an exception if not template_d: raise NoSuchFlavourException("Flavour '%s' does not exist." % taste) for k in template_d.keys(): try: flav_template = open(template_d[k]).read() template_d[k] = flav_template except (OSError, IOError): pass return template_d def render_content(self, content): """ Processes the content for the story portion of a page. :param content: the content to be rendered :returns: the content string """ data = self._request.get_data() outputbuffer = [] if callable(content): # if the content is a callable function, then we just spit out # whatever it returns as a string outputbuffer.append(content()) elif isinstance(content, dict): # if the content is a dict, then we parse it as if it were an # entry--except it's distinctly not an EntryBase derivative var_dict = self.get_parse_vars() var_dict.update(content) output = tools.parse(self._request, var_dict, self.flavour['story']) outputbuffer.append(output) elif isinstance(content, list): if len(content) > 0: current_date = content[0]["date"] if current_date and "date_head" in self.flavour: parse_vars = self.get_parse_vars() parse_vars.update({"date": current_date, "yr": content[0]["yr"], "mo": content[0]["mo"], "da": content[0]["da"]}) outputbuffer.append( self.render_template(parse_vars, "date_head")) for entry in content: if entry["date"] and entry["date"] != current_date: if "date_foot" in self.flavour: parse_vars = self.get_parse_vars() parse_vars.update({"date": current_date, "yr": content[0]["yr"], "mo": content[0]["mo"], "da": content[0]["da"]}) outputbuffer.append( self.render_template(parse_vars, "date_foot")) if "date_head" in self.flavour: current_date = entry["date"] parse_vars = self.get_parse_vars() parse_vars.update({"date": current_date, "yr": content[0]["yr"], "mo": content[0]["mo"], "da": content[0]["da"]}) outputbuffer.append( self.render_template(parse_vars, "date_head")) if data['content-type'] == 'text/plain': s = tools.Stripper() s.feed(entry.get_data()) s.close() p = [' ' + line for line in s.gettext().split('\n')] entry.set_data('\n'.join(p)) parse_vars = self.get_parse_vars() parse_vars.update(entry) outputbuffer.append( self.render_template(parse_vars, "story", override=1)) args = {"entry": parse_vars, "template": ""} args = self._run_callback("story_end", args) outputbuffer.append(args["template"]) if current_date and "date_foot" in self.flavour: parse_vars = self.get_parse_vars() parse_vars.update({"date": current_date}) outputbuffer.append( self.render_template(parse_vars, "date_foot")) return outputbuffer renderContent = tools.deprecated_function(render_content) def render(self, header=True): """ Figures out flavours and such and then renders the content according to which flavour we're using. :param header: whether (True) or not (False) to render the HTTP headers """ # if we've already rendered, then we don't want to do so again if self.rendered: return data = self._request.get_data() config = self._request.get_configuration() try: self.flavour = self.get_flavour(data.get("flavour", "html")) except NoSuchFlavourException, nsfe: error_msg = str(nsfe) try: self.flavour = self.get_flavour("error") except NoSuchFlavourException: self.flavour = get_included_flavour("error") error_msg += " And your error flavour doesn't exist, either." resp = self._request.getResponse() resp.set_status("404 Not Found") self._content = {"title": "HTTP 404: Flavour error", "body": error_msg} data['content-type'] = self.flavour['content_type'].strip() if header: if self._needs_content_type and data['content-type'] != "": self.add_header('Content-type', '%(content-type)s' % data) self.show_headers() if self._content: if "head" in self.flavour: self.write(self.render_template(self.get_parse_vars(), "head")) if "story" in self.flavour: content = self.render_content(self._content) for i, mem in enumerate(content): if isinstance(mem, unicode): content[i] = mem.encode("utf-8") content = "".join(content) self.write(content) if "foot" in self.flavour: self.write(self.render_template(self.get_parse_vars(), "foot")) self.rendered = 1 def render_template(self, entry, template_name, override=0): """ Find the flavour template for template_name, run any blosxom callbacks, substitute entry into it and render the template. If the entry has a ``template_name`` property and override is True (this happens in the story template), then we'll use that template instead. :param entry: the entry/variable-dict to use for expanding variables :param template_name: template name (gets looked up in self.flavour) :param override: whether (True) or not (False) this template can be overriden with the ``template_name`` value in the entry """ template = "" if override: # here we do a quick override... if the entry has a # template field we use that instead of the template_name # argument passed in. actual_template_name = entry.get("template_name", template_name) template = self.flavour.get(actual_template_name, '') if not template: template = self.flavour.get(template_name, '') # we run this through the regular callbacks args = self._run_callback(template_name, {"entry": entry, "template": template}) template = args["template"] # FIXME - the finaltext.replace(...) below causes \$ to get # unescaped in title and body text which is wrong. this # fix alleviates that somewhat, but there are still edge # cases regarding function data. need a real template # engine with a real parser here. entry = dict(args["entry"]) for k, v in entry.items(): if isinstance(v, basestring): entry[k] = v.replace(r"\$", r"\\$") finaltext = tools.parse(self._request, entry, template) return finaltext.replace(r'\$', '$') renderTemplate = tools.deprecated_function(render_template) def _run_callback(self, chain, input): """ Makes calling blosxom callbacks a bit easier since they all have the same mechanics. This function merely calls run_callback with the arguments given and a mappingfunc. The mappingfunc copies the ``template`` value from the output to the input for the next function. Refer to run_callback for more details. """ input.update({"renderer": self}) input.update({"request": self._request}) return tools.run_callback(chain, input, mappingfunc=lambda x,y: x, defaultfunc=lambda x:x) def output_template(self, output, entry, template_name): """ Deprecated. Here for backwards compatibility. """ output.append(self.render_template(entry, template_name)) outputTemplate = tools.deprecated_function(output_template) class Renderer(BlosxomRenderer): pass pyblosxom-1.5.3/Pyblosxom/renderers/debug.py000066400000000000000000000133251217606004600212070ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ This is the debug renderer. This is very useful for debugging plugins and templates. """ from Pyblosxom.renderers.base import RendererBase from Pyblosxom import tools, plugin_utils def escv(s): """ Takes in a value. If it's not a string, we repr it and turn it into a string. Then we escape it so it can be printed in HTML safely. :param s: any value :returns: a safe-to-print-in-html string representation of the value """ if not s: return "" if not isinstance(s, str): s = repr(s) return tools.escape_text(s) def print_map(printfunc, keymap): """ Takes a map of keys to values and applies the function f to a pretty printed version of each key/value pair. :param printfunc: function for printing :param keymap: a mapping of key/value pairs """ keys = keymap.keys() keys.sort() for key in keys: printfunc("%s -> %s\n" % \ (escv(key), escv(keymap[key]))) class Renderer(RendererBase): """ This is the debug renderer. This is very useful for debugging plugins and templates. """ def render(self, header=True): """ Renders a Pyblosxom request after we've gone through all the motions of converting data and getting entries to render. :param header: either prints (True) or does not print (True) the http headers. """ pyhttp = self._request.get_http() config = self._request.get_configuration() data = self._request.get_data() printout = self.write hbar = "------------------------------------------------------\n" if header: self.add_header('Content-type', 'text/html') self.show_headers() printout("") printout("") printout("
        ")
                printout("Welcome to debug mode!\n")
                printout("You requested the %(flavour)s flavour.\n" % data)
        
                printout(hbar)
                printout("HTTP return headers:\n")
                printout(hbar)
                for k, v in self._header:
                    printout("%s -> %s\n" % \
                             (escv(k), escv(v)))
        
                printout(hbar)
                printout("The OS environment contains:\n")
                printout(hbar)
                import os
                print_map(printout, os.environ)
        
                printout(hbar)
                printout("Plugins:\n")
                printout(hbar)
                printout("Plugins that loaded:\n")
                if plugin_utils.plugins:
                    for plugin in plugin_utils.plugins:
                        printout(" * " + escv(plugin) + "\n")
                else:
                    printout("None\n")
        
                printout("\n")
        
                printout("Plugins that didn't load:\n")
                if plugin_utils.bad_plugins:
                    for plugin, exc in plugin_utils.bad_plugins:
                        exc = "    " + "\n    ".join(exc.splitlines()) + "\n"
                        printout(" * " + escv(plugin) + "\n")
                        printout(escv(exc))
                else:
                    printout("None\n")
        
                printout(hbar)
                printout("Request.get_http() dict contains:\n")
                printout(hbar)
                print_map(printout, pyhttp)
        
                printout(hbar)
                printout("Request.get_configuration() dict contains:\n")
                printout(hbar)
                print_map(printout, config)
        
                printout(hbar)
                printout("Request.get_data() dict contains:\n")
                printout(hbar)
                print_map(printout, data)
        
                printout(hbar)
                printout("Entries to process:\n")
                printout(hbar)
                for content in self._content:
                    if not isinstance(content, str):
                        printout("%s\n" %
                                 escv(content.get('filename', 'No such file\n')))
        
                printout(hbar)
                printout("Entries processed:\n")
                printout(hbar)
                for content in self._content:
                    if not isinstance(content, str):
                        printout(hbar)
                        emsg = escv(content.get('filename', 'No such file\n'))
                        printout("Items for %s:\n" % emsg)
                        printout(hbar)
                        print_map(printout, content)
        
                printout(hbar)
                if not config.has_key("cacheDriver"):
                    printout("No cache driver configured.")
                else:
                    printout("Cached Titles:\n")
                    printout(hbar)
                    cache = tools.get_cache(self._request)
                    for content in self._content:
                        if not isinstance(content, str):
                            filename = content['filename']
        
                            if cache.has_key(filename):
                                printout("%s\n" % escv(cache[filename]['title']))
                            cache.close()
        
                    printout(hbar)
                    printout("Cached Entry Bodies:\n")
                    printout(hbar)
                    for content in self._content:
                        if not isinstance(content, str):
                            filename = content['filename']
                            if cache.has_key(filename):
                                printout("%s\n" % escv(cache[filename]['title']))
                                printout(hbar.replace("-", "="))
                                printout("%s\n" % escv(cache[filename]['body']))
                            else:
                                printout("Contents of %s is not cached\n" % \
                                         escv(filename))
                            cache.close()
                            printout(hbar)
        
                printout("")
                printout("")
        pyblosxom-1.5.3/Pyblosxom/tests/000077500000000000000000000000001217606004600167145ustar00rootroot00000000000000pyblosxom-1.5.3/Pyblosxom/tests/__init__.py000066400000000000000000000247751217606004600210440ustar00rootroot00000000000000#######################################################################
        # This file is part of Pyblosxom.
        #
        # Copyright (C) 2010 by the Pyblosxom team.  See AUTHORS.
        #
        # Pyblosxom is distributed under the MIT license.  See the file
        # LICENSE for distribution details.
        #######################################################################
        
        """
        Testing utilities.
        
        Includes up a number of mocks, environment variables, and Pyblosxom
        data structures for useful testing plugins.
        """
        
        from Pyblosxom import pyblosxom, tools, entries
        from Pyblosxom.renderers.blosxom import Renderer
        import cgi
        import StringIO
        import os
        import os.path
        import tempfile
        import time
        import urllib
        import shutil
        import unittest
        
        
        def req_():
            return pyblosxom.Request({}, {}, {})
        
        
        class UnitTestBase(unittest.TestCase):
            def setUp(self):
                self._tempdir = None
        
            def tearDown(self):
                if self._tempdir:
                    try:
                        shutil.rmtree(self._tempdir)
                    except OSError:
                        pass
        
            def eq_(self, a, b, text=None):
                self.assertEquals(a, b, text)
        
            def get_temp_dir(self):
                if self._tempdir == None:
                    self._tempdir = tempfile.mkdtemp()
                return self._tempdir
        
            def setup_files(self, files):
                # sort so that we're building the directories in order
                files.sort()
        
                tempdir = self.get_temp_dir()
                os.makedirs(os.path.join(tempdir, "entries"))
        
                for fn in files:
                    d, f = os.path.split(fn)
        
                    try:
                        os.makedirs(d)
                    except OSError, e:
                        pass
        
                    if f:
                        f = open(fn, "w")
                        f.write("test file: %s\n" % fn)
                        f.close()
                    
            def build_file_set(self, filelist):
                return [os.path.join(self.get_temp_dir(), "entries/%s" % fn)
                        for fn in filelist]
        
            def build_request(self, cfg=None, http=None, data=None, inputstream=""):
                """
                process_path_info uses:
                - req.pyhttp["PATH_INFO"]         - string
        
                - req.config["default_flavour"]   - string
                - req.config["datadir"]           - string
                - req.config["blog_title"]        - string
                - req.config["base_url"]          - string
        
                - req.data["extensions"]          - dict of string -> func
        
                if using req.get_form():
                - req.pyhttp["wsgi.input"]        - StringIO instance
                - req.pyhttp["REQUEST_METHOD"]    - GET or POST
                - req.pyhttp["CONTENT_LENGTH"]    - integer
                """
                _config = {"default_flavour": "html",
                           "datadir": os.path.join(self.get_temp_dir(), "entries"),
                           "blog_title": "Joe's blog",
                           "base_url": "http://www.example.com/"}
                if cfg:
                    _config.update(cfg)
        
                _data = {"extensions": {"txt": 0}}
                if data:
                    _data.update(data)
        
                _http = {"wsgi.input": StringIO.StringIO(inputstream),
                         "REQUEST_METHOD": len(inputstream) and "GET" or "POST",
                         "CONTENT_LENGTH": len(inputstream)}
                if http: _http.update(http)
        
                return pyblosxom.Request(_config, _http, _data)
                
            def test_setup_teardown(self):
                fileset1 = self.build_file_set(["file.txt",
                                                "cata/file.txt",
                                                "cata/subcatb/file.txt"])
        
                self.setup_files(fileset1)
                try:
                    for mem in fileset1:
                        assert os.path.isfile(mem)
        
                finally:
                    self.tearDown()
        
                for mem in fileset1:
                    assert not os.path.isfile(mem)
        
            def cmpdict(self, expected, actual):
                """expected <= actual
                """
                for mem in expected.keys():
                    if mem in actual:
                        self.assertEquals(expected[mem], actual[mem])
                    else:
                        assert False, "%s not in actual" % mem
        
        
        TIMESTAMP = time.mktime(time.strptime('Wed Dec 26 11:00:00 2007'))
        
        
        class FrozenTime:
            """Wraps the time module to provide a single, frozen timestamp.
        
            Allows for dependency injection."""
            def __init__(self, timestamp):
                """Sets the time to timestamp, as seconds since the epoch."""
                self.timestamp = timestamp
        
            def __getattr__(self, attr):
                if attr == 'time':
                    return lambda: self.timestamp
                else:
                    return getattr(time, attr)
        
        
        class PluginTest(unittest.TestCase):
            """Base class for plugin unit tests. Subclass this to test
            plugins.
        
            Many common Pyblosxom data structures are populated as attributes
            of this class, including self.environ, self.config, self.data,
            self.request, and self.args.
        
            By default, self.request is configured as a request for a single
            entry; its name is stored in self.entry_name. This can be
            overridden by modifying the attributes above in your test's
            setUp() method. The entry's timestamp, as seconds since the epoch,
            is stored in self.timestamp. String representations in
            self.timestamp_str and self.timestamp_w3c.
        
            You can change any of the data structures by modifying them
            directly in your tests or your subclass's setUp() method.
        
            The datadir is set to a unique temporary directory in /tmp. This
            directory is created fresh for each test, and deleted when the
            test is done.
        
            NOTE(ryanbarrett): Creating and deleting multiple files and
            directories for each test is inefficient. If this becomes a
            bottleneck, it might need to be reconsidered.
            """
        
            def setUp(self, plugin_module):
                """Subclasses should call this in their setUp() methods.
        
                The plugin_module arg is the plugin module being tested. This
                is used to set the plugin_dir config variable.
                """
                # freeze time
                self.timestamp = TIMESTAMP
                self.frozen_time = self.freeze_pyblosxom_time(self.timestamp)
                self.timestamp_asc = time.ctime(self.timestamp)
                gmtime = time.gmtime(self.timestamp)
                self.timestamp_date = time.strftime('%a %d %b %Y', gmtime)
                self.timestamp_w3c = time.strftime('%Y-%m-%dT%H:%M:%SZ', gmtime)
        
                # set up config, including datadir and plugin_dirs
                self.datadir = tempfile.mkdtemp(prefix='pyblosxom_test_datadir')
        
                plugin_file = os.path.dirname(plugin_module.__file__)
                self.config_base = {'datadir': self.datadir,
                                    'plugin_dirs': [plugin_file],
                                    'base_url': 'http://bl.og/',
                                    }
                self.config = self.config_base
                tools.initialize(self.config)
        
                # set up environment vars and http request
                self.environ = {'PATH_INFO': '/', 'REMOTE_ADDR': ''}
                self.form_data = ''
                self.request = pyblosxom.Request(self.config, self.environ, {})
                self.http = self.request.get_http()
        
                # set up entries and data dict
                self.entry_name = 'test_entry'
                entry_properties = {'absolute_path': '.',
                                    'fn': self.entry_name}
                self.entry = entries.base.generate_entry(
                    self.request, entry_properties, {}, gmtime)
                self.entry_list = [self.entry]
                self.data = {'entry_list': self.entry_list,
                             'bl_type': 'file',
                             }
                self.request._data = self.data
        
                # set up renderer and templates
                self.renderer = Renderer(self.request)
                self.renderer.set_content(self.entry_list)
                templates = ('content_type', 'head', 'story', 'foot', 'date_head',
                             'date_foot')
                self.renderer.flavour = dict([(t, t) for t in templates])
        
                # populate args dict
                self.args = {'request': self.request,
                             'renderer': self.renderer,
                             'entry': self.entry,
                             'template': 'template starts:',
                             }
        
                # this stores the callbacks that have been injected. it maps
                # callback names to the injected methods to call. any
                # callbacks that haven't been injected are passed through to
                # pyblosxom's callback chain.
                #
                # use inject_callback() to inject a callback.
                self.injected_callbacks = {}
                orig_run_callback = tools.run_callback
        
                def intercept_callback(name, args, **kwargs):
                    if name in self.injected_callbacks:
                        return self.injected_callbacks[name]()
                    else:
                        return orig_run_callback(name, args, **kwargs)
        
                tools.run_callback = intercept_callback
        
            def tearDown(self):
                """Subclasses should call this in their tearDown() methods."""
                self.delete_datadir()
        
            def delete_datadir(self):
                """Deletes the datadir and its contents."""
                self.remove_dir(self.datadir)
        
            def freeze_pyblosxom_time(self, timestamp):
                """Injects a frozen time module into Pyblosxom.
        
                The timestamp argument should be seconds since the epoch. Returns the
                FrozenTime instance.
                """
                assert isinstance(timestamp, (int, float))
                frozen_time = FrozenTime(timestamp)
                pyblosxom.time = frozen_time
                tools.time = frozen_time
                return frozen_time
        
            def add_form_data(self, args):
                """Adds the given argument names and values to the request's form data.
        
                The argument names and values are URL-encoded and escaped before
                populating them in the request. This method also sets the request
                method to POST.
                """
                self.environ['REQUEST_METHOD'] = 'POST'
                self.request.add_http({'REQUEST_METHOD': 'POST'})
        
                encoded = ['%s=%s' % (arg, urllib.quote(val))
                           for arg, val in args.items()]
                self.form_data += ('&' + '&'.join(encoded))
                input_ = StringIO.StringIO(self.form_data)
                self.request._form = cgi.FieldStorage(fp=input_, environ=self.environ)
        
            def set_form_data(self, args):
                """Clears the request's form data, then adds the given
                arguments.
                """
                self.form_data = ''
                self.add_form_data(args)
        
            def inject_callback(self, name, callback):
                """Injects a callback to be run by tools.run_callback().
        
                The callback is run *instead* of Pyblosxom's standard callback
                chain.
                """
                self.injected_callbacks[name] = callback
        
            def remove_dir(self, dir):
                """Recursively removes a directory and all files and
                subdirectories.
        
                If dir doesn't exist or is not a directory, does nothing.
                """
                shutil.rmtree(self.datadir, ignore_errors=True)
        
            # allows us to use shorthand
            eq_ = unittest.TestCase.assertEquals
        pyblosxom-1.5.3/Pyblosxom/tests/test_acronyms.py000066400000000000000000000061531217606004600221650ustar00rootroot00000000000000#######################################################################
        # This file is part of Pyblosxom.
        #
        # Copyright (C) 2010-2011 by the Pyblosxom team.  See AUTHORS.
        #
        # Pyblosxom is distributed under the MIT license.  See the file
        # LICENSE for distribution details.
        #######################################################################
        
        import time
        import os
        import re
        
        from Pyblosxom import pyblosxom
        from Pyblosxom.tests import PluginTest, TIMESTAMP
        from Pyblosxom.plugins import acronyms
        
        class Test_acronyms(PluginTest):
            def setUp(self):
                PluginTest.setUp(self, acronyms)
        
            def tearDown(self):
                PluginTest.tearDown(self)
        
            def test_get_acronym_file(self):
                config = dict(self.config_base)
                self.assert_(acronyms.get_acronym_file(config),
                             os.path.join(self.datadir, os.pardir, "acronyms.txt"))
        
                config["acronym_file"] = os.path.join(self.datadir, "foo.txt")
                self.assert_(acronyms.get_acronym_file(config),
                             os.path.join(self.datadir, "foo.txt"))
        
            def test_verify_installation(self):
                config = dict(self.config_base)
                req = pyblosxom.Request(config, self.environ, {})
                self.assert_(acronyms.verify_installation(req) == 0)
        
                config["acronym_file"] = os.path.join(self.datadir, "foo.txt")
                req = pyblosxom.Request(config, self.environ, {})
                filename = acronyms.get_acronym_file(config)
                fp = open(filename, "w")
                fp.write("...")
                fp.close()
                
                self.assert_(acronyms.verify_installation(req) == 1)
        
            def test_build_acronyms(self):
                def check_this(lines, output):
                    for inmem, outmem in zip(acronyms.build_acronyms(lines), output):
                        self.assertEquals(inmem[0].pattern, outmem[0])
                        self.assertEquals(inmem[1], outmem[1])
        
                check_this(["FOO = bar"],
                           [("(\\bFOO\\b)", "\\1")])
                check_this(["FOO. = bar"],
                           [("(\\bFOO.\\b)", "\\1")])
                check_this(["FOO = abbr|bar"],
                           [("(\\bFOO\\b)", "\\1")])
                check_this(["FOO = acronym|bar"],
                           [("(\\bFOO\\b)", "\\1")])
                # this re doesn't compile, so it gets skipped
                check_this(["FOO[ = bar"], [])
        
            def test_cb_story(self):
                req = pyblosxom.Request(
                    self.config, self.environ,
                    {"acronyms":acronyms.build_acronyms(["FOO = bar"])})
        
                # basic test
                args = {"request": req,
                        "entry": {"body": "

        This is FOO!

        "}} ret = acronyms.cb_story(args) self.assertEquals( args["entry"]["body"], "

        This is FOO!

        ") # test to make sure substitutions don't happen in tags args = {"request": req, "entry": {"body": "This is FOO!"}} ret = acronyms.cb_story(args) self.assertEquals( args["entry"]["body"], "This is FOO!") pyblosxom-1.5.3/Pyblosxom/tests/test_akismetcomments.py000066400000000000000000000113331217606004600235310ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2010-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Tests for the akismetcomments plugin. """ __author__ = 'Ryan Barrett ' __url__ = 'http://pyblosxom.github.com/wiki/index.php/Framework_for_testing_plugins' from Pyblosxom.tests import PluginTest from Pyblosxom.plugins import akismetcomments import sys # FIXME: we do some icky things here to mock Akismet. It'd be better # to have a real mocking module like Mock or Fudge. class MockAkismet: """A mock Akismet class.""" GOOD_KEY = 'my_test_key' IPADDRESS = '12.34.56.78' BLOG_URL = 'http://blog.url/' comment_check_return = None comment_check_error = False def __init__(self, key=None, blog_url=None, agent=None): self.key = key assert MockAkismet.BLOG_URL == blog_url def verify_key(self): return self.key == MockAkismet.GOOD_KEY def comment_check(self, comment, data=None, build_data=True, DEBUG=False): if MockAkismet.comment_check_error: MockAkismet.comment_check_error = False raise akismet.AkismetError() else: assert 'foo' == comment ret = MockAkismet.comment_check_return MockAkismet.comment_check_return = None return ret @classmethod def inject_comment_check(cls, ret): cls.comment_check_return = ret @classmethod def inject_comment_check_error(cls): cls.comment_check_error = True class Mockakismet: class AkismetError(Exception): pass Akismet = MockAkismet sys.modules['akismet'] = Mockakismet import akismet class TestAkismetComments(PluginTest): """Test class for the akismetcomments plugin. """ def setUp(self): PluginTest.setUp(self, akismetcomments) akismet.Akismet = MockAkismet self.config['base_url'] = MockAkismet.BLOG_URL self.config['akismet_api_key'] = MockAkismet.GOOD_KEY self.args['comment'] = {'description': "foo", 'ipaddress': MockAkismet.IPADDRESS} def test_verify_installation(self): """verify_installation should check for an api key and verify it.""" self.assertEquals( True, akismetcomments.verify_installation(self.request)) # try without an akismet_api_key config var del self.config['akismet_api_key'] self.assertEquals( False, akismetcomments.verify_installation(self.request)) # try with an import error akismet = sys.modules['akismet'] del sys.modules['akismet'] self.assertEquals( False, akismetcomments.verify_installation(self.request)) sys.modules['akismet'] = akismet # try with a key that doesn't verify self.config['akismet_api_key'] = 'bad_key' orig_verify_key = akismet.Akismet.verify_key self.assertEquals(False, akismetcomments.verify_installation(self.request)) def test_comment_reject(self): """comment_reject() should pass the comment through to akismet.""" # no comment to reject assert 'comment' not in self.data self.assertEquals( False, akismetcomments.cb_comment_reject(self.args)) self.set_form_data({}) self.assertEquals( False, akismetcomments.cb_comment_reject(self.args)) self.set_form_data({'body': 'body'}) def test_bad_api_key_reject(self): # bad api key self.config['akismet_api_key'] = 'bad_key' self.assertEquals( False, akismetcomments.cb_comment_reject(self.args)) self.config['akismet_api_key'] = MockAkismet.GOOD_KEY def test_akismet_error(self): # akismet error MockAkismet.inject_comment_check_error() print akismet.Akismet.comment_check_error self.assertEquals( (True, 'Missing essential data (e.g., a UserAgent string).'), akismetcomments.cb_comment_reject(self.args)) def test_akismet_ham(self): # akismet says ham MockAkismet.inject_comment_check(False) self.assertEquals( False, akismetcomments.cb_comment_reject(self.args)) def test_akismet_spam(self): # akismet says spam MockAkismet.inject_comment_check(True) self.assertEquals( (True, 'I\'m sorry, but your comment was rejected by the Akismet spam filtering system.'), akismetcomments.cb_comment_reject(self.args)) pyblosxom-1.5.3/Pyblosxom/tests/test_backwards_compatibility.py000066400000000000000000000020001217606004600252070ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2010-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### from Pyblosxom.tests import UnitTestBase from Pyblosxom.pyblosxom import Request class TestRequest(UnitTestBase): """Need to be backwards compatible with pre-existing methods of getting config, data and http dicts from the Request object. """ def test_conf(self): r = Request({"foo": "bar"}, {}, {}) conf = r.get_configuration() for mem in (r.conf, r.config, r.configuration): yield self.eq_, mem, conf def test_http(self): r = Request({}, {"foo": "bar"}, {}) self.eq_(r.http, r.get_http()) def test_data(self): r = Request({}, {}, {"foo": "bar"}) self.eq_(r.data, r.get_data()) pyblosxom-1.5.3/Pyblosxom/tests/test_blog.py000066400000000000000000000040171217606004600212520ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2010 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### import os import time import shutil from Pyblosxom.tests import UnitTestBase from Pyblosxom import tools def gen_time(s): """ Takes a string in YYYY/MM/DD hh:mm format and converts it to a float of seconds since the epoch. For example: >>> gen_time("2007/02/14 14:14") 1171480440.0 """ return time.mktime(time.strptime(s, "%Y/%m/%d %H:%M")) class BlogTest(UnitTestBase): def get_datadir(self): tempdir = self.get_temp_dir() return os.path.join(tempdir, "datadir") def setup_blog(self, blist): datadir = self.get_datadir() for mem in blist: tools.create_entry(datadir, mem["category"], mem["filename"], mem["mtime"], mem["title"], mem["metadata"], mem["body"]) def cleanup_blog(self): shutil.rmtree(self.get_datadir(), ignore_errors=True) class TestBlogTest(BlogTest): blog = [{"category": "cat1", "filename": "entry1.txt", "mtime": gen_time("2007/02/14 14:14"), "title": "Happy Valentine's Day!", "metadata": {}, "body": "

        Today is Valentine's Day! w00t!

        "}] def test_harness(self): tempdir = self.get_temp_dir() # this is kind of a bogus assert, but if we get this far # without raising an exception, then our test harness is # probably working. try: self.setup_blog(TestBlogTest.blog) self.eq_(1, 1) finally: self.cleanup_blog() self.eq_(1, 1) pyblosxom-1.5.3/Pyblosxom/tests/test_blosxom_renderer.py000066400000000000000000000032411217606004600236760ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### from StringIO import StringIO from Pyblosxom.tests import UnitTestBase from Pyblosxom.pyblosxom import Request from Pyblosxom.renderers import blosxom req = Request({}, {}, {}) class TestBlosxomRenderer(UnitTestBase): def test_dollar_parse_problem(self): output = StringIO() renderer = blosxom.BlosxomRenderer(req, output) renderer.flavour = {"story": "$(body)"} # mocking out _run_callback to just return the args dict renderer._run_callback = lambda c, args: args entry = {"body": r'PS1="\u@\h \[\$foo \]\W\[$RST\] \$"'} # the rendered template should be exactly the same as the body # in the entry--no \$ -> $ silliness. self.eq_(renderer.render_template(entry, "story"), entry["body"]) def test_date_head(self): output = StringIO() renderer = blosxom.BlosxomRenderer(req, output) renderer.flavour = {"date_head": "$(yr) $(mo) $(da) $(date)"} # mocking out _run_callback to just return the args dict renderer._run_callback = lambda c, args: args vardict = { "yr": "2011", "mo": "01", "da": "25", "date": "Tue, 25 Jan 2011" } self.eq_(renderer.render_template(vardict, "date_head"), "2011 01 25 Tue, 25 Jan 2011") pyblosxom-1.5.3/Pyblosxom/tests/test_check_blacklist.py000066400000000000000000000024241217606004600234340ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2010-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### import unittest from Pyblosxom.tests import PluginTest from Pyblosxom.plugins import check_blacklist class TestCheckBlacklist(PluginTest): def setUp(self): PluginTest.setUp(self, check_blacklist) def test_comment_reject(self): comment = {} self.args['comment'] = comment cfg = self.args["request"].get_configuration() # no comment_rejected_words--so it passes ret = check_blacklist.cb_comment_reject(self.args) self.assertEquals(False, ret) # rejected words, but none in the comment cfg["comment_rejected_words"] = ["foo"] comment["body"] = "this is a happy comment" ret = check_blacklist.cb_comment_reject(self.args) self.assertEquals(False, ret) # rejected words, one is in the comment cfg["comment_rejected_words"] = ["this"] ret = check_blacklist.cb_comment_reject(self.args) self.assertEquals(True, ret[0]) pyblosxom-1.5.3/Pyblosxom/tests/test_check_javascript.py000066400000000000000000000026251217606004600236350ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2010-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Tests for the check_javascript plugin. """ __author__ = 'Ryan Barrett ' __url__ = 'http://pyblosxom.github.com/wiki/index.php/Framework_for_testing_plugins' from Pyblosxom.tests import PluginTest from Pyblosxom.plugins import check_javascript class TestCheckJavascript(PluginTest): """Test class for the check_javascript plugin. """ def setUp(self): PluginTest.setUp(self, check_javascript) self.config['blog_title'] = 'test title' def test_comment_reject(self): """check_javascript should check the secretToken query argument.""" # no secretToken assert 'secretToken' not in self.http self.assertEquals(True, check_javascript.cb_comment_reject(self.args)) # bad secretToken self.set_form_data({'secretToken': 'not the title'}) self.assertEquals(True, check_javascript.cb_comment_reject(self.args)) # good secretToken self.set_form_data({'secretToken': 'test title'}) self.assertEquals(False, check_javascript.cb_comment_reject(self.args)) pyblosxom-1.5.3/Pyblosxom/tests/test_check_nonhuman.py000066400000000000000000000023051217606004600233050ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2010-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### import unittest from Pyblosxom.tests import PluginTest, TIMESTAMP from Pyblosxom.plugins import check_nonhuman class TestCheckNonhuman(PluginTest): def setUp(self): PluginTest.setUp(self, check_nonhuman) def test_comment_reject(self): comment = {} self.args['comment'] = comment # no iamhuman, rejection! ret = check_nonhuman.cb_comment_reject(self.args) self.assertEquals(True, ret[0]) # iamhuman, so it passes comment['iamhuman'] = 'yes' ret = check_nonhuman.cb_comment_reject(self.args) self.assertEquals(False, ret) # foo, so it passes del comment['iamhuman'] self.args["request"].get_configuration()["nonhuman_name"] = "foo" comment['foo'] = 'yes' ret = check_nonhuman.cb_comment_reject(self.args) self.assertEquals(False, ret) pyblosxom-1.5.3/Pyblosxom/tests/test_comments.py000066400000000000000000000543071217606004600221630ustar00rootroot00000000000000####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2010-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### """ Tests for the comments plugin. """ from Pyblosxom.tests import PluginTest, FrozenTime, TIMESTAMP from Pyblosxom.plugins import comments import cgi import cPickle import os class TestComments(PluginTest): """Test class for the comments plugin. """ def setUp(self): PluginTest.setUp(self, comments) # add comment templates self.renderer.flavour.update({'comment-story': 'comment-story', 'comment': 'comment'}) # inject the frozen time module comments.time = self.frozen_time # populate with default config vars comments.cb_start(self.args) def tearDown(self): PluginTest.tearDown(self) def comment_path(self): """Returns the comment path that would currently be created.""" filename = '%s-%0.1f.%s' % (self.entry_name, self.timestamp, self.config['comment_draft_ext']) return os.path.join(self.config['comment_dir'], filename) def comment(self, title='title', author='author', body='body', url=None, email=None, ipaddress=None, encoding=None, preview=None, **kwargs): """Posts a comment with the given contents.""" # set the encoding in the config. it should default to utf-8 if encoding: self.config['blog_encoding'] = encoding # build up the form data and post the comment args = [(arg, vars()[arg]) for arg in ('title', 'author', 'body', 'url', 'email', 'preview') if vars()[arg] is not None] self.set_form_data(dict(args)) comments.cb_prepare(self.args) def check_comment_file(self, title='title', author='author', body='body', url='', email='', encoding=None, ipaddress='', expected_title=None, expected_author=None, expected_body=None, expected_url=None, preview=None, delete_datadir=True): """Posts a comment and checks its contents on disk.""" self.comment(title, author, body, url, email, ipaddress, encoding, preview) if encoding is None: encoding = 'utf-8' if expected_title is None: expected_title = title if expected_author is None: expected_author = author if expected_body is None: expected_body = body if expected_url is None: expected_url = url # check the files in the comments directory files = os.listdir(self.config['comment_dir']) self.assert_(os.path.basename(self.comment_path()) in files) self.assert_(comments.LATEST_PICKLE_FILE in files) # check the coment file's contents expected_lines = [ '\n' % encoding, '\n', '%s\n' % cgi.escape(expected_body), '%0.1f\n' % self.timestamp, '%s\n' % cgi.escape(expected_author), '%s\n' % cgi.escape(expected_title), '\n', '%s\n' % cgi.escape(expected_url), '%s\n' % self.timestamp_w3c, '%s\n' % self.timestamp_date, '%s\n' % ipaddress, '\n', ] if email: expected_lines.insert(-1, '%s\n' % email) file = open(self.comment_path()) actual_lines = file.readlines() file.close() expected_lines.sort() actual_lines.sort() for expected, actual in zip(expected_lines, actual_lines): self.assertEquals(expected, actual) if delete_datadir: self.delete_datadir() def check_comment_output(self, expected, delete_datadir=True, **kwargs): """Posts a comment and checks its rendered output. Note that this deletes the datadir before it posts the comment! """ self.data['display_comment_default'] = True self.comment(**kwargs) comments.cb_story(self.args) self.args['template'] = '' comments.cb_story_end(self.args) self.assertEquals(expected, self.args['template']) if delete_datadir: self.delete_datadir() def test_sanitize(self): # test
          ...
        ulbody = ( "
          \n" "
        • entry within a ul list
        • \n" "
        • entry within a ul list, with\n" "newlines in between\n" "
        • \n" "
        ") self.assertEquals( comments.sanitize(ulbody), "
          " "
        • entry within a ul list
        • " "
        • entry within a ul list, with
          \n" "newlines in between
          \n" "
        • " "
        ") # test
          ...
        ulbody = ( "
          \n" "
        1. entry within a ol list
        2. \n" "
        3. entry within a ol list, with\n" "newlines in between\n" "
        4. \n" "
        ") self.assertEquals( comments.sanitize(ulbody), "
          " "
        1. entry within a ol list
        2. " "
        3. entry within a ol list, with
          \n" "newlines in between
          \n" "
        4. " "
        ") def test_cb_start(self): """cb_start() should set defaults for some config variables.""" self.config = self.config_base comments.cb_start(self.args) self.assertEquals(os.path.join(self.datadir, 'comments'), self.config['comment_dir']) self.assertEquals('cmt', self.config['comment_ext']) self.assertEquals('cmt', self.config['comment_draft_ext']) self.assertEquals(0, self.config['comment_nofollow']) def test_verify_installation(self): """verify_installation should check the comment dir and smtp config.""" # comment_dir must exist assert not os.path.exists('/not/a/directory') self.config['comment_dir'] = '/not/a/directory' self.assertEquals(0, comments.verify_installation(self.request)) del self.config['comment_dir'] # either all smtp config variables must be defined, or none smtp_vars = ['comment_smtp_server', 'comment_smtp_from', 'comment_smtp_to'] for smtp_var in smtp_vars: [self.config.pop(var, '') for var in smtp_vars] self.config[smtp_var] = 'abc' self.assertEquals(0, comments.verify_installation(self.request)) del self.config[smtp_vars[-1]] self.assertEquals(1, comments.verify_installation(self.request)) def test_check_comments_disabled(self): time = FrozenTime(TIMESTAMP) entry = self.entry config = self.config key = "comment_disable_after_x_days" day = 60 * 60 * 24 # not set -> False self.eq_(comments.check_comments_disabled(config, entry), False) # set to non-int -> False config[key] = "abc" self.eq_(comments.check_comments_disabled(config, entry), False) # set to negative int -> False config[key] = -10 self.eq_(comments.check_comments_disabled(config, entry), False) # entry has no mtime -> False config[key] = 10 self.eq_(comments.check_comments_disabled(config, entry), False) # inside range -> False config[key] = 10 # 10 days entry['mtime'] = time.time() - (5 * day) self.eq_(comments.check_comments_disabled(config, entry), False) # outside range -> True config[key] = 10 # 10 days entry['mtime'] = time.time() - (15 * day) self.eq_(comments.check_comments_disabled(config, entry), True) # def test_cb_handle(self): # """cb_handle() should intercept requests for /comments.js.""" # self.assertEquals(None, comments.cb_handle(self.args)) # self.request.add_http({'PATH_INFO': '/not_comments.js'}) # self.assertEquals(None, comments.cb_handle(self.args)) # self.request.add_http({'PATH_INFO': '/comments.js'}) # self.assertEquals(True, comments.cb_handle(self.args)) # response = self.request.get_response() # self.assertEquals('text/javascript', # response.get_headers()['Content-Type']) # out = cStringIO.StringIO() # response.send_body(out) # self.assert_(out.getvalue().startswith( # '/* AJAX comment support for pyblosxom')) def test_cb_prepare_showcomments(self): """cb_prepare() should set display_comment_default to show comments.""" # default is to not show comments del self.data['bl_type'] comments.cb_prepare(self.args) self.assertEquals(False, self.data['display_comment_default']) # show them if the bl_type config var is 'file' self.data['bl_type'] = 'db' comments.cb_prepare(self.args) self.assertEquals(False, self.data['display_comment_default']) self.data['bl_type'] = 'file' comments.cb_prepare(self.args) self.assertEquals(True, self.data['display_comment_default']) # or if the query string has showcomments=yes del self.data['bl_type'] self.request.add_http({'QUERY_STRING': 'x=yes&showcomments=no7&y=no'}) comments.cb_prepare(self.args) self.assertEquals(False, self.data['display_comment_default']) self.request.add_http({'QUERY_STRING': 'x=yes&showcomments=yes&y=no'}) comments.cb_prepare(self.args) self.assertEquals(True, self.data['display_comment_default']) def test_cb_prepare_new_comment(self): """A new comment should be packaged in XML and stored in a new file.""" self.check_comment_file(title='title', author='author', body='body') # url is optional. try setting it. self.check_comment_file(url='http://home/') # previewed comments shouldn't be stored self.comment(preview='yes') self.assert_(not os.path.exists(self.comment_path())) def test_cb_prepare_encoding(self): """If the blog_encoding config var is set, it should be used.""" self.check_comment_file(encoding='us-ascii') def test_cb_prepare_massage_link(self): """User-provided URLs should be scrubbed and linkified if necessary.""" # html control characters should be stripped self.check_comment_file(url=' License ======= Plugin is distributed under license: GPLv2 pyblosxom-1.5.3/docs/plugins/check_nonhuman.rst000066400000000000000000000043531217606004600217060ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ================================================= check_nonhuman - Rejects non-human comments.... ================================================= Summary ======= This works in conjunction with the comments plugin and allows you to significantly reduce comment spam by adding a "I am human" checkbox to your form. Any comments that aren't "from a human" get rejected immediately. This shouldn't be the only way you reduce comment spam. It's probably not useful to everyone, but would be useful to some people as a quick way of catching some of the comment spam they're getting. Usually this works for a while, then spam starts coming in again. At that point, I change the ``nonhuman_name`` config.py variable value and I stop getting comment spam. Install ======= Requires the ``comments`` plugin. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.check_nonhuman`` to the ``load_plugins`` list in your ``config.py`` file. 2. Configure as documented below. Usage ===== For setup, copy the plugin to your plugins directory and add it to your load_plugins list in your config.py file. Then add the following item to your config.py (this defaults to "iamhuman"):: py["nonhuman_name"] = "iamhuman" Then add the following to your comment-form template just above the submit button (make sure to match the input name to your configured input name):: Yes, I am human! Alternatively, if you set the ``nonhuman_name`` property, then you should do this:: Yes, I am human! Additionally, the nonhuman plugin can log when it rejected a comment. This is good for statistical purposes. 1 if "yes, I want to log" and 0 (default) if "no, i don't want to log". Example:: py["nonhuman_log"] = 1 And that's it! The idea came from:: http://www.davidpashley.com/cgi/pyblosxom.cgi/2006/04/28#blog-spam License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/comments.rst000066400000000000000000000316651217606004600205610ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ============================================== comments - Adds comments to a blog entry.... ============================================== Summary ======= Adds comments to your blog. Supports preview, AJAX posting, SMTP notifications, plugins for rejecting comments (and thus reducing spam), ... Comments are stored in a directory that parallels the data directory. The comments themselves are stored as XML files named entryname-datetime.suffix. The comment system allows you to specify the directory where the comment directory tree will stored, and the suffix used for comment files. You need to make sure that this directory is writable by whatever is running Pyblosxom. Comments are stored one or more per file in a parallel hierarchy to the datadir hierarchy. The filename of the comment is the filename of the blog entry, plus the creation time of the comment as a float, plus the comment extension. Comments now follow the ``blog_encoding`` variable specified in ``config.py``. If you don't include a ``blog_encoding`` variable, this will default to utf-8. Comments will be shown for a given page if one of the following is true: 1. the page has only one blog entry on it and the request is for a specific blog entry as opposed to a category with only one entry in it 2. if "showcomments=yes" is in the querystring then comments will be shown .. Note:: This comments plugin does not work with static rendering. If you are using static rendering to build your blog, you won't be able to use this plugin. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.comments`` to the ``load_plugins`` list of your ``config.py`` file. Example:: py["load_plugins"] = ["Pyblosxom.plugins.comments"] 2. Configure as documented below in the Configuration section. 3. Add templates to your html flavour as documented in the Flavour templates section. Configuration ============= 1. Set ``py['comment_dir']`` to the directory (in your data directory) where you want the comments to be stored. The default value is a directory named "comments" in your datadir. 2. (optional) The comment system can notify you via e-mail when new comments/trackbacks/pingbacks are posted. If you want to enable this feature, create the following config.py entries: py['comment_smtp_from'] - the email address sending the notification py['comment_smtp_to'] - the email address receiving the notification If you want to use an SMTP server, then set:: py['comment_smtp_server'] - your SMTP server hostname/ip address **OR** if you want to use a mail command, set:: py['comment_mta_cmd'] - the path to your MTA, e.g. /usr/bin/mail Example 1:: py['comment_smtp_from'] = "joe@joe.com" py['comment_smtp_to'] = "joe@joe.com" py['comment_smtp_server'] = "localhost" Example 2:: py['comment_smtp_from'] = "joe@joe.com" py['comment_smtp_to'] = "joe@joe.com" py['comment_mta_cmd'] = "/usr/bin/mail" 3. (optional) Set ``py['comment_ext']`` to the change comment file extension. The default file extension is "cmt". This module supports the following config parameters (they are not required): ``comment_dir`` The directory we're going to store all our comments in. This defaults to datadir + "comments". Example:: py["comment_dir"] = "/home/joe/blog/comments/" ``comment_ext`` The file extension used to denote a comment file. This defaults to "cmt". ``comment_draft_ext`` The file extension used for new comments that have not been manually approved by you. This defaults to the value in ``comment_ext``---i.e. there is no draft stage. ``comment_smtp_server`` The smtp server to send comments notifications through. ``comment_mta_cmd`` Alternatively, a command line to invoke your MTA (e.g. sendmail) to send comment notifications through. ``comment_smtp_from`` The email address comment notifications will be from. If you're using SMTP, this should be an email address accepted by your SMTP server. If you omit this, the from address will be the e-mail address as input in the comment form. ``comment_smtp_to`` The email address to send comment notifications to. ``comment_nofollow`` Set this to 1 to add ``rel="nofollow"`` attributes to links in the description---these attributes are embedded in the stored representation. ``comment_disable_after_x_days`` Set this to a positive integer and users won't be able to leave comments on entries older than x days. Related files ============= .. only:: text You can find the comment-story file in the docs at http://pyblosxom.github.com/ or in the tarball under docs/_static/plugins/comments/. This plugin has related files like flavour templates, javascript file, shell scripts and such. All of these files can be gotten from `here <../_static/plugins/comments/>`_ Flavour templates ================= The comments plugin requires at least the ``comment-story``, ``comment``, and ``comment-form`` templates. The ``comment-preview`` template is optional. The way the comments plugin assembles flavour files is like this:: comment-story comment (zero or more) comment-preview (optional) comment-form Thus if you want to have your entire comment section in a div container, you'd start the div container at the top of comment-story and end it at the bottom of comment-form. comment-story ------------- The ``comment-story`` template comes at the beginning of the comment section before the comments and the comment form. Variables available: $num_comments - Contains an integer count of the number of comments associated with this entry .. only:: text You can find the comment-story file in the docs at http://pyblosxom.github.com/ or in the tarball under docs/_static/plugins/comments/. Link to file: `comment-story <../_static/plugins/comments/comment-story>`_ .. literalinclude:: ../_static/plugins/comments/comment-story :language: html comment ------- The ``comment`` template is used to format a single entry that has comments. Variables available:: $cmt_title - the title of the comment $cmt_description - the content of the comment or excerpt of the trackback/pingback $cmt_link - the pingback link referring to this entry $cmt_author - the author of the comment or trackback $cmt_optionally_linked_author - the author, wrapped in an tag to their link if one was provided $cmt_pubDate - the date and time of the comment/trackback/pingback $cmt_source - the source of the trackback .. only:: text You can find the comment-story file in the docs at http://pyblosxom.github.com/ or in the tarball under docs/_static/plugins/comments/. Link to file: `comment <../_static/plugins/comments/comment>`_ .. literalinclude:: ../_static/plugins/comments/comment :language: html comment-preview --------------- The ``comment-preview`` template shows a comment that is being previewed, but hasn't been posted to the blog, yet. .. only:: text You can find the comment-story file in the docs at http://pyblosxom.github.com/ or in the tarball under docs/_static/plugins/comments/. Link to file: `comment-preview <../_static/plugins/comments/comment-preview>`_ .. literalinclude:: ../_static/plugins/comments/comment-preview :language: html comment-form ------------ The ``comment-form`` comes at the end of all the comments. It has the comment form used to enter new comments. .. only:: text You can find the comment-story file in the docs at http://pyblosxom.github.com/ or in the tarball under docs/_static/plugins/comments/. Link to file: `comment-form <../_static/plugins/comments/comment-form>`_ .. literalinclude:: ../_static/plugins/comments/comment-form :language: html Dealing with comment spam ========================= You'll probably have comment spam. There are a bunch of core plugins that will help you reduce the comment spam that come with Pyblosxom as well as ones that don't. Best to check the core plugins first. Compacting comments =================== This plugin always writes each comment to its own file, but as an optimization, it supports files that contain multiple comments. You can use ``compact_comments.sh`` to compact comments into a single file per entry. .. only:: text compact_comments.sh is located in docs/_static/plugins/comments/ You can find ``compact_comments.sh`` `here <../_static/plugins/comments/>`_. Implementing comment preview ============================ If you would like comment previews, you need to do 2 things. 1. Add a preview button to comment-form.html like this:: You may change the contents of the value attribute, but the name of the input must be "preview". 2. Still in your ``comment-form.html`` template, you need to use the comment values to fill in the values of your input fields like so:: If there is no preview available, these variables will be stripped from the text and cause no problem. 3. Copy ``comment.html`` to a template called ``comment-preview.html``. All of the available variables from the comment template are available for this template. AJAX support ============ Comment previewing and posting can optionally use AJAX, as opposed to full HTTP POST requests. This avoids a full-size roundtrip and re-render, so commenting feels faster and more lightweight. AJAX commenting degrades gracefully in older browsers. If JavaScript is disabled or not supported in the user's browser, or if it doesn't support XmlHttpRequest, comment posting and preview will use normal HTTP POST. This will also happen if comment plugins that use alternative protocols are detected, like ``comments_openid.py``. To add AJAX support, you need to make the following modifications to your ``comment-form`` template: 1. The comment-anchor tag must be the first thing in the ``comment-form`` template::

        2. Change the ```` tag to something like this::

        .. Note:: If you run pyblosxom inside cgiwrap, you'll probably need to remove ``#comment-anchor`` from the URL in the action attribute. They're incompatible. Your host may even be using cgiwrap without your knowledge. If AJAX comment previewing and posting don't work, try removing ``#comment-anchor``. 3. Add ``onclick`` handlers to the button input tags:: 4. Copy ``comments.js`` file to a location on your server that's servable by your web server. You can find this file `here <../_static/plugins/comments/>`_. .. only:: text You can find comments.js in docs/_static/plugins/comments/. 5. Include this script tag somewhere after the ``
        `` closing tag:: Set the url for ``comments.js`` to the url for where ``comments.js`` is located on your server from step 4. .. Note:: Note the separate closing ```` tag! It's for IE; without it, IE won't actually run the code in ``comments.js``. nofollow support ================ This implements Google's nofollow support for links in the body of the comment. If you display the link of the comment poster in your HTML template then you must add the ``rel="nofollow"`` attribute to your template as well Note to developers who are writing plugins that create comments =============================================================== Each entry has to have the following properties in order to work with comments: 1. ``absolute_path`` - the category of the entry. Example: "dev/pyblosxom" or "" 2. ``fn`` - the filename of the entry without the file extension and without the directory. Example: "staticrendering" 3. ``file_path`` - the absolute_path plus the fn. Example: "dev/pyblosxom/staticrendering" Also, if you don't want comments for an entry, add:: #nocomments 1 to the entry or set ``nocomments`` to ``1`` in the properties of the entry. License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/conditionalhttp.rst000066400000000000000000000016211217606004600221240ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ========================================================== conditionalhttp - Allows browser-side caching with if... ========================================================== Summary ======= This plugin can help save bandwidth for low bandwidth quota sites. This is done by outputing cache friendly HTTP header tags like Last-Modified and ETag. These values are calculated from the first entry returned by ``entry_list``. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. In your ``config.py`` file, add ``Pyblosxom.plugins.conditionalhttp`` to the ``load_plugins`` variable. License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/disqus.rst000066400000000000000000000036631217606004600202410ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ============================================== disqus - Lets me use Disqus for comments.... ============================================== Summary ======= Plugin for adding Disqus comments. It's not hard to do this by hand, but this plugin makes it so that comments only show up when you're looking at a single blog entry. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. In your ``config.py`` file, add ``Pyblosxom.plugins.disqus`` to the ``load_plugins`` variable. 2. Set ``disqus_shortname`` in your ``config.py`` file. This comes from Disqus when you set up your account. For help, see http://docs.disqus.com/help/2/ . 3. Save the ``comment_form`` template into your html flavour. comment_form template::
        blog comments powered by Disqus License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/entrytitle.rst000066400000000000000000000026151217606004600211300ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ================================================= entrytitle - Puts entry title in page title.... ================================================= Summary ======= If Pyblosxom is rendering a single entry (i.e. entry_list has 1 item in it), then this populates the ``entry_title`` variable for the header template. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.entrytitle`` to the ``load_plugins`` list of your ``config.py`` file. 2. Configure as documented below. Configuration ============= To use, add the ``entry_title`` variable to your header template in the ```` area. Example:: <title>$(blog_title)$(entry_title) The default ``$(entry_title)`` starts with a ``::`` and ends with the title of the entry. For example:: :: Guess what happened today You can set the entry title template in the configuration properties with the ``entry_title_template`` variable:: config["entry_title_template"] = ":: %(title)s" .. Note:: ``%(title)s`` is a Python string formatter that gets filled in with the entry title. License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/firstdaydiv.rst000066400000000000000000000026671217606004600212640ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ====================================================== firstdaydiv - Adds a token which tells us whether... ====================================================== Summary ======= Adds a token which allows you to differentiate between the first day of entries in a series of entries to be displayed from the other days. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. In your ``config.py`` file, add ``Pyblosxom.plugins.firstdaydiv`` to the ``load_plugins`` list. 2. (optional) Set the ``firstDayDiv`` config variable. This defaults to ``blosxomFirstDayDiv``. Example:: py['firstDayDiv'] = 'blosxomFirstDayDiv' Usage ===== This denotes the first day with the css class set in the ``firstDayDiv`` config variable. This is available in the ``$(dayDivClass)`` template variable. You probably want to put this in your ``date_head`` template in a ```` tag. For example, in your ``date_head``, you could have::
        $date and in your ``date_foot``, you'd want to close that ``
        `` off::
        Feel free to use this in other ways. License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/flavourfiles.rst000066400000000000000000000034371217606004600214310ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ======================================================= flavourfiles - Serves static files related to flav... ======================================================= Summary ======= This plugin allows flavour templates to use file urls that will resolve to files in the flavour directory. Those files will then get served by Pyblosxom. This solves the problem that flavour packs are currently difficult to package, install, and maintain because static files (images, css, js, ...) have to get put somewhere else and served by the web server and this is difficult to walk a user through. It handles urls that start with ``flavourfiles/``, then the flavour name, then the path to the file. For example:: http://example.com/blog/flavourfiles/html/style.css .. Note:: This plugin is very beta! It's missing important functionality, probably has bugs, and hasn't been well tested! Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.flavourfiles`` to the ``load_plugins`` list of your ``config.py`` file. 2. In templates you want to use flavourfiles, use urls like this:: $(base_url)/flavourfiles/$(flavour)/path-to-file For example:: The ``$(base_url)`` will get filled in with the correct url root. The ``$(flavour)`` will get filled in with the name of the url. This allows users to change the flavour name without having to update all the templates. License ======= Plugin is distributed under license: MIT License pyblosxom-1.5.3/docs/plugins/magicword.rst000066400000000000000000000042701217606004600207000ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ==================================================== magicword - Magic word method for reducing comm... ==================================================== Summary ======= This is about the simplest anti-comment-spam measure you can imagine, but it's probably effective enough for all but the most popular blogs. Here's how it works. You pick a question and put a field on your comment for for the answer to the question. If the user answers it correctly, his comment is accepted. Otherwise it's rejected. Here's how it works: Install ======= Requires the ``comments`` plugin. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.magicword`` to the ``load_plugins`` list in your ``config.py`` file. 2. Configure as documented below. Configure ========= Here's an example of what to put in config.py:: py['mw_question'] = "What is the first word in this sentence?" py['mw_answer'] = "what" Note that ``mw_answer`` must be lowercase and without leading or trailing whitespace, even if you expect the user to enter capital letters. Their input will be lowercased and stripped before it is compared to ``mw_answer``. Here's what you put in your ``comment-form`` file:: The Magic Word:
        $(mw_question)

        It's important that the name of the input field is exactly "magicword". Security note ============= In order for this to be secure(ish) you need to protect your ``config.py`` file. This is a good idea anyway! If your ``config.py`` file is in your web directory, protect it from being seen by creating or modifying a ``.htaccess`` file in the directory where ``config.py`` lives with the following contents:: Order allow,deny deny from all This will prevent people from being able to view ``config.py`` by browsing to it. License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/markdown_parser.rst000066400000000000000000000022721217606004600221220ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ============================================ markdown_parser - Markdown entry parser... ============================================ Summary ======= A Markdown entry formatter for Pyblosxom. Install ======= Requires python-markdown to be installed. See http://www.freewisdom.org/projects/python-markdown/ for details. 1. Add ``Pyblosxom.plugins.markdown_parser`` to the ``load_plugins`` list in your ``config.py`` file Usage ===== Write entries using Markdown markup. Entry filenames can end in ``.markdown``, ``.md``, and ``.mkd``. You can also configure this as your default preformatter for ``.txt`` files by configuring it in your config file as follows:: py['parser'] = 'markdown' Additionally, you can do this on an entry-by-entry basis by adding a ``#parser markdown`` line in the metadata section. For example:: My Little Blog Entry #parser markdown My main story... License ======= Plugin is distributed under license: GPLv3 or later pyblosxom-1.5.3/docs/plugins/no_old_comments.rst000066400000000000000000000016731217606004600221070ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ========================================================== no_old_comments - Prevent comments on entries older t... ========================================================== Summary ======= This plugin implements the ``comment_reject`` callback of the comments plugin. If someone tries to comment on an entry that's older than 28 days, the comment is rejected. Install ======= Requires the ``comments`` plugin. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.no_old_comments`` to the ``load_plugins`` list in your ``config.py`` file. Revisions ========= 1.0 - August 5th 2006: First released. License ======= Plugin is distributed under license: Public Domain pyblosxom-1.5.3/docs/plugins/pages.rst000066400000000000000000000075201217606004600200240ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ================================================ pages - Allows you to include non-blog-entr... ================================================ Summary ======= Blogs don't always consist solely of blog entries. Sometimes you want to add other content to your blog that's not a blog entry. For example, an "about this blog" page or a page covering a list of your development projects. This plugin allows you to have pages served by Pyblosxom that aren't blog entries. Additionally, this plugin allows you to have a non-blog-entry front page. This makes it easier to use Pyblosxom to run your entire website. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. add ``Pyblosxom.plugins.pages`` to the ``load_plugins`` list in your ``config.py`` file. 2. configure the plugin using the configuration variables below ``pagesdir`` This is the directory that holds the pages files. For example, if you wanted your pages in ``/home/foo/blog/pages/``, then you would set it to:: py["pagesdir"] = "/home/foo/blog/pages/" If you have ``blogdir`` defined in your ``config.py`` file which holds your ``datadir`` and ``flavourdir`` directories, then you could set it to:: py["pagesdir"] = os.path.join(blogdir, "pages") ``pages_trigger`` (optional) Defaults to ``pages``. This is the url trigger that causes the pages plugin to look for pages. py["pages_trigger"] = "pages" ``pages_frontpage`` (optional) Defaults to False. If set to True, then pages will show the ``frontpage`` page for the front page. This requires you to have a ``frontpage`` file in your pages directory. The extension for this file works the same way as blog entries. So if your blog entries end in ``.txt``, then you would need a ``frontpage.txt`` file. Example:: py["pages_frontpage"] = True Usage ===== Pages looks for urls that start with the trigger ``pages_trigger`` value as set in your ``config.py`` file. For example, if your ``pages_trigger`` was ``pages``, then it would look for urls like this:: /pages/blah /pages/blah.html and pulls up the file ``blah.txt`` [1]_ which is located in the path specified in the config file as ``pagesdir``. If the file is not there, it kicks up a 404. .. [1] The file ending (the ``.txt`` part) can be any file ending that's valid for entries on your blog. For example, if you have the textile entryparser installed, then ``.txtl`` is also a valid file ending. Template ======== pages formats the page using the ``pages`` template. So you need a ``pages`` template in the flavours that you want these pages to be rendered in. I copy my ``story`` template and remove some bits. For example, if you're using the html flavour and that is stored in ``/home/foo/blog/flavours/html.flav/``, then you could copy the ``story`` file in that directory to ``pages`` and that would become your ``pages`` template. Python code blocks ================== pages handles evaluating python code blocks. Enclose python code in ``<%`` and ``%>``. The assumption is that only you can edit your pages files, so there are no restrictions (security or otherwise). For example:: <% print "testing" %> <% x = { "apple": 5, "banana": 6, "pear": 4 } for mem in x.keys(): print "
      • %s - %s
      • " % (mem, x[mem]) %> The request object is available in python code blocks. Reference it by ``request``. Example:: <% config = request.get_configuration() print "your datadir is: %s" % config["datadir"] %> License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/paginate.rst000066400000000000000000000046161217606004600205200ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. =================================================== paginate - Allows navigation by page for index... =================================================== Summary ======= Plugin for paging long index pages. Pyblosxom uses the num_entries configuration variable to prevent more than ``num_entries`` being rendered by cutting the list down to ``num_entries`` entries. So if your ``num_entries`` is set to 20, you will only see the first 20 entries rendered. The paginate overrides this functionality and allows for paging. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.paginate`` to your ``load_plugins`` list variable in your ``config.py`` file. Make sure it's the first plugin in the ``load_plugins`` list so that it has a chance to operate on the entry list before other plugins. 2. add the ``$(page_navigation)`` variable to your head or foot (or both) templates. this is where the page navigation HTML will appear. Here are some additional configuration variables to adjust the behavior:: ``paginate_count_from`` Defaults to 0. This is the number to start counting from. Some folks like their pages to start at 0 and others like it to start at 1. This enables you to set it as you like. Example:: py["paginate_count_from"] = 1 ``paginate_previous_text`` Defaults to "<<". This is the text for the "previous page" link. ``paginate_next_text`` Defaults to ">>". This is the text for the "next page" link. ``paginate_linkstyle`` Defaults to 1. This allows you to change the link style of the paging. Style 0:: [1] 2 3 4 5 6 7 8 9 ... >> Style 1:: Page 1 of 4 >> If you want a style different than that, you'll have to copy the plugin and implement your own style. Note about static rendering =========================== This plugin doesn't work with static rendering. The problem is that it relies on the querystring to figure out which page to show and when you're static rendering, only the first page is rendered. At some point, someone will fix this. License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/pyarchives.rst000066400000000000000000000034161217606004600211020ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ===================================================== pyarchives - Builds month/year-based archives li... ===================================================== Summary ======= Walks through your blog root figuring out all the available monthly archives in your blogs. It generates html with this information and stores it in the ``$(archivelinks)`` variable which you can use in your head and foot templates. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.pyarchives`` to the ``load_plugins`` list in your ``config.py`` file. 2. Configure using the following configuration variables. ``archive_template`` Let's you change the format of the output for an archive link. For example:: py['archive_template'] = ('
      • ' '%(m)s/%(y)s
      • ') This displays the archives as list items, with a month number, then a slash, then the year number. The formatting variables available in the ``archive_template`` are:: b 'Jun' m '6' Y '1978' y '78' These work the same as ``time.strftime`` in python. Additionally, you can use variables from config and data. .. Note:: The syntax used here is the Python string formatting syntax---not the Pyblosxom template rendering syntax! Usage ===== Add ``$(archivelinks)`` to your head and/or foot templates. License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/pycalendar.rst000066400000000000000000000041041217606004600210420ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. =================================================== pycalendar - Displays a calendar on your blog.... =================================================== Summary ======= Generates a calendar along the lines of this one (with month and day names in the configured locale):: < January 2003 > Mo Tu We Th Fr Sa Su 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 It walks through all your entries and marks the dates that have entries so you can click on the date and see entries for that date. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.pycalendar`` to your ``load_plugins`` list in your ``config.py`` file. 2. Configure it as documented below. 3. Add the ``$(calendar)`` variable to your head and/or foot template. Configuration ============= You can set the start of the week using the ``calendar_firstweekday`` configuration setting, for example:: py['calendar_firstweekday'] = 0 will make the week start on Monday (day '0'), instead of Sunday (day '6'). Pycalendar is locale-aware. If you set the ``locale`` config property, then month and day names will be displayed according to your locale. It uses the following CSS classes: * blosxomCalendar: for the calendar table * blosxomCalendarHead: for the month year header (January 2003) * blosxomCalendarWeekHeader: for the week header (Su, Mo, Tu, ...) * blosxomCalendarEmpty: for filler days * blosxomCalendarCell: for calendar days that aren't today * blosxomCalendarBlogged: for calendar days that aren't today that have entries * blosxomCalendarSpecificDay: for the specific day we're looking at (if we're looking at a specific day) * blosxomCalendarToday: for today's calendar day License ======= Plugin is distributed under license: Public domain pyblosxom-1.5.3/docs/plugins/pycategories.rst000066400000000000000000000063251217606004600214250ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ================================================ pycategories - Builds a list of categories.... ================================================ Summary ======= Walks through your blog root figuring out all the categories you have and how many entries are in each category. It generates html with this information and stores it in the ``$(categorylinks)`` variable which you can use in your head or foot templates. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.pycategories`` to the ``load_plugins`` list in your ``config.py`` file. 2. Add ``$(categorylinks)`` to your head and/or foot templates. Configuration ============= You can format the output by setting ``category_begin``, ``category_item``, and ``category_end`` properties. Categories exist in a hierarchy. ``category_start`` starts the category listing and is only used at the very beginning. The ``category_begin`` property begins a new category group and the ``category_end`` property ends that category group. The ``category_item`` property is the template for each category item. Then after all the categories are printed, ``category_finish`` ends the category listing. For example, the following properties will use ``
          `` to open a category, ``
        `` to close a category and ``
      • `` for each item:: py["category_start"] = "
          " py["category_begin"] = "
        • " py["category_finish"] = "
        " Another example, the following properties don't have a begin or an end but instead use indentation for links and displays the number of entries in that category:: py["category_start"] = "" py["category_begin"] = "" py["category_item"] = ( r'%(indent)s' r'%(category)s (%(count)d)
        ') py["category_end"] = "" py["category_finish"] = "" There are no variables available in the ``category_begin`` or ``category_end`` templates. Available variables in the category_item template: ======================= ========================== ==================== variable example datatype ======================= ========================== ==================== base_url http://joe.com/blog/ string fullcategory_urlencoded 'dev/pyblosxom/status/' string fullcategory 'dev/pyblosxom/status/' string (urlencoded) category 'status/' string category_urlencoded 'status/' string (urlencoed) flavour 'html' string count 70 int indent '    ' string ======================= ========================== ==================== License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/pyfilenamemtime.rst000066400000000000000000000021711217606004600221070ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ========================================================== pyfilenamemtime - Allows you to codify the mtime in t... ========================================================== Summary ======= Allows you to specify the mtime for a file in the file name. If a filename contains a timestamp in the form of ``YYYY-MM-DD-hh-mm``, change the mtime to be the timestamp instead of the one kept by the filesystem. For example, a valid filename would be ``foo-2002-04-01-00-00.txt`` for April fools day on the year 2002. It is also possible to use timestamps in the form of ``YYYY-MM-DD``. http://www.probo.com/timr/blog/ Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.pyfilenamemtime`` to the ``load_plugins`` list of your ``config.py`` file. 2. Use date stamps in your entry filenames. License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/readmore.rst000066400000000000000000000061551217606004600205260ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. =================================================== readmore - Breaks blog entries into summary an... =================================================== Summary ======= Allows you to break a long entry into a summary and the rest making it easier to show just the summary in indexes. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.readmore`` to the ``load_plugins`` list in your ``config.py`` file. .. Note:: If you're using the rst_parser plugin, make sure this plugin shows up in load_plugins list before the rst_parser plugin. See the rst_parser section below. 2. Configure as documented below. Configuration ============= ``readmore_breakpoint`` (optional) string; defaults to "BREAK" This is the text that you'll use in your blog entry that breaks the body into the summary part above and the rest of the blog entry below. For example:: py["readmore_breakpoint"] = "BREAK" ``readmore_template`` (optional) string; defaults to:: '

        read more after the break...

        ' When the entry is being shown in an index with other entries, then the ``readmore_breakpoint`` text is replaced with this text. This text is done with HTML markup. Variables available: * ``%(url)s`` - the full path to the story * ``%(base_url)s`` - base_url * ``%(flavour)s`` - the flavour selected now * ``%(file_path)s`` - path to the story (without extension) .. Note:: This template is formatted using Python string formatting---not Pyblosxom template formatting! Usage ===== For example, if the value of ``readmore_breakpoint`` is ``"BREAK"``, then you could have a blog entry like this:: First post

        This is my first post. In this post, I set out to explain why it is that I'm blogging and what I hope to accomplish with this blog. See more below the break.

        BREAK

        Ha ha! Made you look below the break!

        Usage with rst_parser ===================== Since the rst_parser parses the restructured text and turns it into HTML and this plugin operates on HTML in the story callback, we have to do a two-step replacement. Thus, instead of using BREAK or whatever you have set in ``readmore_breakpoint`` in your blog entry, you use the break directive:: First post This is my first post. In this post, I set out to explain why it is that I'm blogging and what I hope to accomplish with this blog. .. break:: Ha ha! Made you look below the break! History ======= This is based on the original readmore plugin written by IWS years ago. It's since been reworked. Additionally, I folded in the rst_break plugin break directive from Menno Smits at http://freshfoo.com/wiki/CodeIndex . License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/rst_parser.rst000066400000000000000000000040371217606004600211110ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ===================================================== rst_parser - restructured text support for blog ... ===================================================== Summary ======= A reStructuredText entry formatter for pyblosxom. reStructuredText is part of the docutils project (http://docutils.sourceforge.net/). To use, you need a *recent* version of docutils. A development snapshot (http://docutils.sourceforge.net/#development-snapshots) will work fine. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.rst_parser`` to the ``load_plugins`` list in your ``config.py`` file. 2. Install docutils. Instructions are at http://docutils.sourceforge.net/ Usage ===== Blog entries with a ``.rst`` extension will be parsed as restructuredText. You can also configure this as your default preformatter for ``.txt`` files by configuring it in your config file as follows:: py['parser'] = 'reST' Additionally, you can do this on an entry-by-entry basis by adding a ``#parser reST`` line in the metadata section. For example:: My Little Blog Entry #parser reST My main story... Configuration ============= There's two optional configuration parameter you can for additional control over the rendered HTML:: # To set the starting level for the rendered heading elements. # 1 is the default. py['reST_initial_header_level'] = 1 # Enable or disable the promotion of a lone top-level section title to # document title (and subsequent section title to document subtitle # promotion); enabled by default. py['reST_transform_doctitle'] = 1 .. Note:: If you're not seeing headings that you think should be there, try changing the ``reST_initial_header_level`` property to 0. License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/tags.rst000066400000000000000000000146311217606004600176640ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ======================= tags - Tags plugin... ======================= Summary ======= This is a tags plugin. It uses Pyblosxom's command line abilities to split generation of tags index data from display of tags index data. It creates a ``$(tagslist)`` variable for head and foot templates which lists all the tags. It creates a ``$(tags)`` variable for story templates which lists tags for the story. It creates a ``$(tagcloud)`` variable for the tag cloud. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.tags`` to the ``load_plugins`` list in your ``config.py`` file. 2. Configure as documented below. Configuration ============= The following config properties define where the tags file is located, how tag metadata is formatted, and how tag lists triggered. ``tags_separator`` This defines the separator between tags in the metadata line. Defaults to ",". After splitting on the separator, each individual tag is stripped of whitespace before and after the text. For example:: Weather in Boston #tags weather, boston

        The weather in Boston today is pretty nice.

        returns tags ``weather`` and ``boston``. If the ``tags_separator`` is:: py["tags_separator"] = "::" then tags could be declared in the entries like this:: Weather in Boston #tags weather::boston

        The weather in Boston today is pretty nice.

        ``tags_filename`` This is the file that holds indexed tags data. Defaults to datadir + os.pardir + ``tags.index``. This file needs to be readable by the process that runs your blog. This file needs to be writable by the process that creates the index. ``tags_trigger`` This is the url trigger to indicate that the tags plugin should handle the file list based on the tag. Defaults to ``tag``. ``truncate_tags`` If this is True, then tags index listings will get passed through the truncate callback. If this is False, then the tags index listing will not be truncated. If you're using a paging plugin, then setting this to True will allow your tags index to be paged. Example:: py["truncate_tags"] = True Defaults to True. In the head and foot templates, you can list all the tags with the ``$(tagslist)`` variable. The templates for this listing use the following three config properties: ``tags_list_start`` Printed before the list. Defaults to ``

        ``. ``tags_list_item`` Used for each tag in the list. There are a bunch of variables you can use: * ``base_url`` - the baseurl for your blog * ``flavour`` - the default flavour or flavour currently showing * ``tag`` - the tag name * ``count`` - the number of items that are tagged with this tag * ``tagurl`` - url composed of baseurl, trigger, and tag Defaults to ``%(tag)s``. ``tags_list_finish`` Printed after the list. Defaults to ``

        ``. In the head and foot templates, you can also add a tag cloud with the ``$(tagcloud)`` variable. The templates for the cloud use the following three config properties: ``tags_cloud_start`` Printed before the cloud. Defaults to ``

        ``. ``tags_cloud_item`` Used for each tag in the cloud list. There are a bunch of variables you can use: * ``base_url`` - the baseurl for your blog * ``flavour`` - the default flavour or flavour currently showing * ``tag`` - the tag name * ``count`` - the number of items that are tagged with this tag * ``class`` - biggestTag, bigTag, mediumTag, smallTag or smallestTag--the css class for this tag representing the frequency the tag is used * ``tagurl`` - url composed of baseurl, trigger, and tag Defaults to ``%(tag)s``. ``tags_cloud_finish`` Printed after the cloud. Defaults to ``

        ``. You'll also want to add CSS classes for the size classes to your CSS. For example, you could add this:: .biggestTag { font-size: 16pt; } .bigTag { font-size: 14pt } .mediumTag { font-size: 12pt } .smallTag { font-size: 10pt ] .smallestTag { font-size: 8pt ] You can list the tags for a given entry in the story template with the ``$(tags)`` variable. The tag items in the story are formatted with one configuration property: ``tags_item`` This is the template for a single tag for an entry. It can use the following bits: * ``base_url`` - the baseurl for this blog * ``flavour`` - the default flavour or flavour currently being viewed * ``tag`` - the tag * ``tagurl`` - url composed of baseurl, trigger and tag Defaults to ``%(tag)s``. Tags are joined together with ``,``. Creating the tags index file ============================ Run:: pyblosxom-cmd buildtags from the directory your ``config.py`` is in or:: pyblosxom-cmd buildtags --config=/path/to/config/file from anywhere. This builds the tags index file that the tags plugin requires to generate tags-based bits for the request. Until you rebuild the tags index file, the entry will not have its tags indexed. Thus you should either rebuild the tags file after writing or updating an entry or you should rebuild the tags file as a cron job. .. Note:: If you're using static rendering, you need to build the tags index before you statically render your blog. Converting from categories to tags ================================== This plugin has a command that goes through your entries and adds tag metadata based on the category. There are some caveats: 1. it assumes entries are in the blosxom format of title, then metadata, then the body. 2. it only operates on entries in the datadir. It maintains the atime and mtime of the file. My suggestion is to back up your files (use tar or something that maintains file stats), then try it out and see how well it works, and figure out if that works or not. To run the command do:: pyblosxom-cmd categorytotags from the directory your ``config.py`` is in or:: pyblosxom-cmd categorytotags --config=/path/to/config/file from anywhere. License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/trackback.rst000066400000000000000000000036501217606004600206520ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. =================================== trackback - Trackback support.... =================================== Summary ======= This plugin allows pyblosxom to process trackback http://www.sixapart.com/pronet/docs/trackback_spec pings. Install ======= Requires the ``comments`` plugin. Though you don't need to have comments enabled on your blog in order for trackbacks to work. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.trackback`` to the ``load_plugins`` list in your ``config.py`` file. 2. Add this to your ``config.py`` file:: py['trackback_urltrigger'] = "/trackback" These web forms are useful for testing. You can use them to send trackback pings with arbitrary content to the URL of your choice: * http://kalsey.com/tools/trackback/ * http://www.reedmaniac.com/scripts/trackback_form.php 3. Now you need to advertise the trackback ping link. Add this to your ``story`` template:: TB 4. You can supply an embedded RDF description of the trackback ping, too. Add this to your ``story`` or ``comment-story`` template:: License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/w3cdate.rst000066400000000000000000000022741217606004600202600ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ================================================== w3cdate - Adds a 'w3cdate' variable which is ... ================================================== Summary ======= Adds a ``$(w3cdate)`` variable to the head and foot templates which has the mtime of the first entry in the entrylist being displayed (this is often the youngest/most-recent entry). Install ======= .. Note:: If you have pyxml installed, then this will work better than if you don't. If you don't have it installed, it uses home-brew code to compute the w3cdate. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.w3cdate`` to the beginning of the ``load_plugins`` list of your ``config.py`` file. 2. Add the ``$(w3cdate)`` variable to the place you need it in your head and/or foot templates. Thanks ====== Thanks to Matej Cepl for the hacked iso8601 code that doesn't require PyXML. License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/xmlrpc_pingback.rst000066400000000000000000000024361217606004600220710ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. =============================================== xmlrpc_pingback - XMLRPC pingback support.... =============================================== Summary ======= This module contains an XML-RPC extension to support pingback http://www.hixie.ch/specs/pingback/pingback pings. Install ======= Requires the ``comments`` plugin, but you don't have to enable comments on your blog for pingbacks to work. This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.xmlrpc_pingback`` to the ``load_plugins`` list of your ``config.py`` file 2. Set the ``xmlrpc_trigger`` variable in your ``config.py`` file to a trigger for this plugin. For example:: py["xmlrpc_trigger"] = "RPC" 3. Add to the ```` section of your ``head`` template:: This test blog, maintained by Ian Hickson, is useful for testing. You can post to it, linking to a post on your site, and it will send a pingback. * http://www.dummy-blog.org/ License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/plugins/yeararchives.rst000066400000000000000000000037601217606004600214140ustar00rootroot00000000000000 .. only:: text This document file was automatically generated. If you want to edit the documentation, DON'T do it here--do it in the docstring of the appropriate plugin. Plugins are located in ``Pyblosxom/plugins/``. ======================================================= yeararchives - Builds year-based archives listing.... ======================================================= Summary ======= Walks through your blog root figuring out all the available years for the archives list. It stores the years with links to year summaries in the variable ``$(archivelinks)``. You should put this variable in either your head or foot template. Install ======= This plugin comes with Pyblosxom. To install, do the following: 1. Add ``Pyblosxom.plugins.yeararchives`` to the ``load_plugins`` list in your ``config.py`` file. 2. Add ``$(archivelinks)`` to your head and/or foot templates. 3. Configure as documented below. Usage ===== When the user clicks on one of the year links (e.g. ``http://base_url/2004/``), then yeararchives will display a summary page for that year. The summary is generated using the ``yearsummarystory`` template for each month in the year. My ``yearsummarystory`` template looks like this::
        $title
        $body
        The ``$(archivelinks)`` link can be configured with the ``archive_template`` config variable. It uses the Python string formatting syntax. Example:: py['archive_template'] = ( '' '%(Y)s
        ') The vars available with typical example values are:: Y 4-digit year ex: '1978' y 2-digit year ex: '78' f the flavour ex: 'html' .. Note:: The ``archive_template`` variable value is formatted using Python string formatting rules--not Pyblosxom template rules! License ======= Plugin is distributed under license: MIT pyblosxom-1.5.3/docs/pyblosxom_cmd.rst000066400000000000000000000012741217606004600201230ustar00rootroot00000000000000============================= Pyblosxom on the command line ============================= Pyblosxom comes with a command line tool called ``pyblosxom-cmd``. It allows you to create new blogs, verify your configuration, run static rendering, render single urls, and run command line functions implemented in plugins. For help, do:: pyblosxom-cmd --help It'll list the commands and options available. If you tell it where your config file is, then it'll list commands and options available as well as those implemented in plugins you have installed. For example:: pyblosxom-cmd --config=/path/to/config.py --help For more information on static rendering, see :ref:`static-rendering`. pyblosxom-1.5.3/docs/renderers.rst000066400000000000000000000022441217606004600172330ustar00rootroot00000000000000.. _renderers: ========= Renderers ========= Pyblosxom supports multiple renderers and comes with two by default: debug and blosxom. blosxom renderer ================ You can set which renderer to use in your ``config.py`` file like this:: py["renderer"] = "blosxom" .. Note:: If you don't specify the ``renderer`` configuration variable, Pyblosxom uses the blosxom renderer. The blosxom renderer is named as such because it operates similarly to blosxom. Read the chapter on :ref:`flavours and templates `. debug renderer ============== The debug renderer outputs your blog in a form that makes it easy to see the data generated when handling a Pyblosxom request. This is useful for debugging plugins, working on blosxom flavours and templates, and probably other things as well. To set Pyblosxom to use the debug renderer, do this in your ``config.py`` file:: py["renderer"] = "debug" Other renderers =============== If you want your blog rendered by a different renderer, say one that uses a different template system like Jinja or Cheetah, then you will need to install a plugin that implements the ``renderer`` callback. pyblosxom-1.5.3/docs/syndication.rst000066400000000000000000000071061217606004600175700ustar00rootroot00000000000000=========== Syndication =========== Summary ======= Syndicating your blog is very important as it provides a mechanism for readers of your blog to keep up to date. Typically this is done with newsreader software. Additionally, there are websites that post blog entries from a variety of blogs that have similar content. Both newsreaders and planet-type websites need a semantically marked up version of your blog. Most newsreaders read most of the syndication formats. So you shouldn't feel that you have to implement each one of them in your blog---you can most assuredly get away with implementing RSS 2.0 or Atom and be just fine. The syndication flavours that come with Pyblosxom should be fine for most blogs. When pointing people to your syndication feed, just use one of the syndication flavours. Examples: * ``http://example.com/blog/index.rss`` * ``http://example.com/~willg/blog/index.rss20`` * ``http://example.com/index.atom`` Feed formats that come with Pyblosxom ===================================== Pyblosxom comes with a few default flavours, three of which are feed formats. RSS 0.9.1 --------- Pyblosxom comes with an rss flavour that produces RSS 0.9.1 output. Here's a sample of what it produces:: My Blog http://www.joe.com/blog/index.rss This is my blog about trivial things en Example of an entry post http://www.joe.com/blog/entries/example1.html <p> Here's an example of an entry in my blog. This is the body of the entry. </p> This example only has one entry in it. The number of entries the rss flavour will display is determined by the ``num_entries` property in your ``config.py`` file. .. Note:: Probably better not to use RSS 0.9.1: RSS 0.9.1 format lacks dates in the data for the items. Unless you include the date for the entry somewhere in the description block, people looking at your RSS 0.9.1 feed won't know what the date the entry was created on was. Unless you have some reason to use RSS 0.9.1 as your syndication format, you should look at using RSS 2.0 or Atom both of which also come with Pyblosxom. For more information, look at the `RSS 0.9.1 spec`_. .. _RSS 0.9.1 spec: http://my.netscape.com/publish/formats/rss-spec-0.91.html RSS 2.0 ------- Pyblosxom 1.3 comes with an RSS 2.0 flavour called "rss20". If it's missing features that you want (for example, some folks are doing podcasting with their blog), then override the individual templates you need to adjust. For more information on RSS 2.0, see the `RSS 2.0 spec`_. .. _RSS 2.0 spec: http://blogs.law.harvard.edu/tech/rss Atom ---- Starting with 1.3, Pyblosxom comes with an Atom flavour called "atom". If it's missing features that you want, then override the individual templates you need to adjust. For more information on Atom, see the `Atom spec`_. .. _Atom spec: http://atomenabled.org/ Debugging your feeds ==================== `FeedValidator`_ is a useful tool for figuring out whether your feed is valid and fixing bugs in your feed content. .. _FeedValidator: http://feedvalidator.org/ Additionally, feel free to ask on the pyblosxom-users mailing list. pyblosxom-1.5.3/docs/upgrade.rst000066400000000000000000000000301217606004600166600ustar00rootroot00000000000000.. include:: ../UPGRADE pyblosxom-1.5.3/docs/whatsnew.rst000066400000000000000000000000311217606004600170720ustar00rootroot00000000000000.. include:: ../WHATSNEW pyblosxom-1.5.3/docs/whatsnew_1_0.rst000066400000000000000000000044331217606004600175430ustar00rootroot00000000000000What's new in 1.0 (May 2004) ============================ Pertinent to users ------------------ 1. We ditched ``blosxom_custom_flavours``---you can remove it from your ``config.py`` file. 2. We added static rendering---see the howto in the PyBlosxom manual. 3. Rewrote comments to use the new handler system. You should replace the comments, pingbacks, trackbacks, and other comments-oriented plugins with the new versions from ``contrib/plugins/comments/``. 4. pingbacks plugin is now ``xmlrpc_pingbacks.py`` . 5. Adjusted the default templates for HTML and RSS. Removed all other default templates. Look at the flavour_examples directory for flavour examples. 6. Added an ``ignore_properties`` property to ``config.py`` which allows you to specify which directories in your datadir should be ignored. For example:: py["ignore_directories"] = ["CVS", ".svn"] 7. Added a template variable ``pyblosxom_version`` which points to ``pyblosxom/Pyblosxom/pyblosxom.VERSION_DATE`` . 8. Fixed some code in pyarchives so it worked with Python 2.1. Thanks to Wilhelm. 9. Fixed template retrieving code so that you can specify templates to use for a given category or parent categories. Thanks to Nathan for fixing this. 10. We added a ``logdir`` property to config. PyBlosxom (and plugins) will create logs in this directory so the directory has to have write access for whatever user the webserver uses to run the script. Pertinent to developers and plugin developers --------------------------------------------- 1. Rewrote the startup for PyBlosxom request handling---we ditched the common_start function and picked up a common initialize function. 2. Unhardcoded where contrib and web files go when doing a multi-user installation using ``python setup.py install``. 3. Adjusted the comments plugin so that if a given entry has a ``nocomments`` property, then it won't get comments. 4. Moved the Request object into ``pyblosxom/Pyblosxom/pyblosxom.py``. 5. Fixed variable parsing so that if the variable value is a function that takes arguments, we pass the request in as the first argument. 6. Added VERSION, VERSION_DATE, and VERSION_SPLIT. This allows you to verify that your plugin works with the version of PyBlosxom the user is using. pyblosxom-1.5.3/docs/whatsnew_1_1.rst000066400000000000000000000016451217606004600175460ustar00rootroot00000000000000What's new in 1.1 (January 2005) ================================ Pertinent to users ------------------ 1. We no longer include contributed plugins and flavours. To find plugins and flavours, go to the PyBlosxom registry located at http://pyblosxom.sourceforge.net/ . 2. We changed how ``num_entries`` is handled internally. If ``num_entries`` is set to 0, the blosxom default file handler will display all the entries. If ``num_entries`` is set to a positive number, then the blosxom default file handler will display at most that many entries. Pertinent to developers ----------------------- 1. Plugins that implement cb_filelist are now in charge of adjusting the number of entries to be displayed based on the ``num_entries`` configuration variable. This is no longer done in the renderer. 2. We added HTTP_COOKIE to the list of things that get added to the http dict in the Request object. pyblosxom-1.5.3/docs/whatsnew_1_2.rst000066400000000000000000000057671217606004600175600ustar00rootroot00000000000000What's new in 1.2 (March 2005) ============================== Pertinent to users ------------------ 1. We added a ``blog_email`` item to config.py and changed ``blog_author`` to just the author's name. Examples: py["blog_email"] = "joe@blah.com" py["blog_author"] = "Joe Man" 2. We no longer adjust blog_title from what you set in ``config.py``. Now we have a ``blog_title_with_path`` variable which is in the data dict which is the ``blog_title`` with the path information. People who want the title of their blog to be the title and not include the path should use ``$blog_title``. Folks who want the old behavior where the path was appended to the title should use ``$blog_title_with_path`` . 3. We now support WSGI, mod_python, and Twisted in addition to CGI. 4. Upped our Python requirement to Python 2.2. If you have an earlier version than that, you won't be able to use PyBlosxom 1.2. 5. Changed ``defaultFlavour`` to ``default_flavour``. This property allows you to specify the flavour to use by default if the URI doesn't specify one. It default to ``html``. 6. We moved the main PyBlosxom site to http://pyblosxom.sourceforge.net/ . There's a "powered by pyblosxom" image at http://pyblosxom.sourceforge.net/images/pb_pyblosxom.gif You should adjust your templates accordingly. Pertinent to developers ----------------------- 1. We now have a Request and a Response object. See API documentation for more details. 2. Don't use ``os.environ`` directly---use the http dict. For example, this is bad:: path_info = os.environ["HTTP_PATHINFO"] This is what you should be doing:: http = request.getHttp() path_info = http["HTTP_PATHINFO"] If you use os.environ directly, it's likely your plugin won't work with non-CGI installations of PyBlosxom. 3. We added __iter__, read, readline, readlines, seek, and tell to the Request object. All of them access the input stream. You should not use sys.stdin directly. Usage:: data = request.read() data_part = request.read(1024) one_line = request.readline() lines = request.readlines() 4. The output stream should be accessed through the PyBlosxom Response object. The following methods are implemented in the Response object: __iter__, close, flush, read, readline, readlines, seek, tell, write, writelines, setStatus, and addHeader. You should not use sys.stdout directly. See the API for more details. Usage:: response = request.getResponse() response.addHeader('Status', '200 Ok') response.addHeader('Content-type', 'text/html') response.write("Hello World") response.writelines(["list", "of", "data"]) 5. Instead of doing:: form = request.getHttp()["form"] you can now do:: form = request.getForm() 6. Plugins should not be importing the config module and looking at the ``py`` dict directly. You should instead use the Request getConfiguration() method to get the config py dict. pyblosxom-1.5.3/docs/whatsnew_1_3.rst000066400000000000000000000072051217606004600175460ustar00rootroot00000000000000What's new in 1.3.2 (February 2006) =================================== Pertinent to users ------------------ 1. Fixes a security issue where the path_info can come in with multiple / at the beginning. Whether this happens or not depends on the web server you're using and possibly other things. Some people have the issue and some don't. If you're in doubt, upgrade. Thanks FX! What's new in 1.3.1 (February 2006) =================================== Pertinent to users ------------------ 1. The ``num_entries`` property now affects the home page and category index pages. It no longer affects archive index pages. 2. Fixed the RSS 0.9.1 feed templates. It has the correct link url and shows the entry bodies. Thanks Norbert! 3. The version string is correct. 4. Added support for ``$body_escaped`` . 5. Fixed the blog encoding on the RSS 2.0 feed so that it uses the value provided in the config.py ``$blog_encoding`` variable. 6. Fixed the Atom 1.0 story flavour to use ``$body_escaped`` instead of ```` 7. Fixed a problem with static rendering where we'd render ``/index.html`` and ``//index.html`` if the user had entries in their root category. Pertinent to developers ----------------------- 1. If you have plugins that use the logger functions in PyBlosxom 1.2, you need to update those plugins to use the new logger functions in PyBlosxom 1.3. Read through the API for details. 2. Moved documentation in ReadMeForPlugins.py over to the manual. What's new in 1.3 (January 2006) ================================ Pertinent to users ------------------ 1. We added a ``blog_rights`` property. This holds a textual description of the rights you give to others who read your blog. Leaving this blank or not filling it in will affect the RSS 2.0 and Atom 1.0 feeds. 2. If you set ``flavourdir``in your config.py file, you have to put your flavour files in that directory tree. If you don't set ``flavourdir``, then PyBlosxom expects to find your flavour files in your ``datadir``. The flavour overhaul is backwards compatible with previous PyBlosxom versions. So if you want to upgrade your blog, but don't want to move your flavour files to a new directory, DON'T set your ``flavourdir`` property. 3. Moved the content that was in README to CHANGELOG. 4. You can now organize the directory hierarchy of your blog by date. For example, you could create a category for each year and put posts for that year in that year (2003, 2004, 2005, ...). Previously URLs requesting "2003", "2004", ... would get parsed as dates and would pull blog entries by mtime and not by category. 5. Logging works now. The following configuration properties are useful for determining whether events in PyBlosxom are logged and what will get logged: * "log_file" - the file that PyBlosxom events will be logged to---the web server MUST be able to write to this file. * "log_level" - the level of events to write to the log. options are "critical", "error", "warning", "info", and "debug" * "log_filter" - the list of channels that should have messages logged. if you set the log_filter and omit "root", then app-level messages are not logged. It's likely you'll want to set log_file and log_level and that's it. Omit log_file and logging will fall back to stderr which usually gets logged to your web server's error log. Pertinent to developers and plugin developers --------------------------------------------- 1. Plugins that used logging in 1.2 need to be changed to use the new logging utilities in 1.3. Until that happens, they won't work. pyblosxom-1.5.3/docs/whatsnew_1_4.rst000066400000000000000000000107061217606004600175470ustar00rootroot00000000000000What's new in 1.4.3 (January 2008) ================================== Pertinent to users ------------------ 1. Adjusted the code that parses blog.ini values so that it can take values like:: foo = 'a' # string foo = "a" # string foo = 23 # integer foo = [ "a", 23, "b" ] # list of strings and integers as well as:: foo = a # string Note: if you want the string "23", then you MUST enclose it in quotes, otherwise it will be parsed as an integer. blog.ini is used when you set up PyBlosxom using Paste. 2. Fixed PyBlosxomWSGIApp so that it's WSGI compliant as an application. Thanks Michael! 3. Template variables can be parenthesized. Examples:: $foo - variable is "foo" $(foo) - variable is "foo" $(url)index.atom - variable is "url" This reduces ambiguity which was causing problems with recognition of variables. Pertinent to developers ----------------------- 1. Fixed tools.importname---it now logs errors to the logger. 2. Fixed PyBlosxomWSGIApp so that it's WSGI compliant as an application. Thanks Michael! 3. Added more unit tests and corrected more behavior. Details on running unit tests are in the REDAME. What's new in 1.4.2 (August 2007) ================================= Pertinent to users ------------------ 1. Fixed another bug with the WSGI application creation code. (Thanks Christine!) 2. Added instructions for installing PyBlosxom with mod_wsgi to ``install_wsgi.txt``. This includes a basic wsgi script for PyBlosxom. (Thanks Christine!) 3. Fixed up the Python Paste installation document. (Thanks Liz!) 4. Fixed the ``month2num`` code in tools so that PyBlosxom runs on Windows (Windows doesn't have ``nl_langinfo`` in the ``locale`` module). (Thanks Liz!) What's new in 1.4.1 (July 2007) =============================== Pertinent to users ------------------ 1. Fixed a problem where running PyBlosxom under Paste won't pick up the ``config.py`` file. Be sure to add a ``configpydir`` property to your ``blog.ini`` file which points to the directory your ``config.py`` file is in. 2. Fixed a problem where running PyBlosxom in Python 2.5 won't pick up the ``config.py`` file. 3. Merged Ryan's optimization to Walk (removes an os.listdir call). 4. Updated documentation. What's new in 1.4 (July 2007) ============================= Pertinent to users ------------------ 1. Added a pyblcmd command line program for PyBlosxom command line things. This now handles static rendering, rendering a single url to stdout, testing your blog setup, ... 2. The Atom story template now has a $default_flavour bit in the link. Bug 1667937. (Thanks Michael!) 3. PyBlosxom is now locale aware in respects to dates, months, days of the week and such. Users should set the locale config property to a valid locale if they don't want English. 4. Added a ``blog_icbm`` config variable for use in the ICBM meta tag. See config_variables.txt for more information. 5. Changed the ``num_entries`` property in config.py from 40 to a much more conservative 5. Also changed the default value from 0 to 5 if you happened not to set ``num_entries`` at all. http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=373658 (Thanks Jon!) 6. Changed the self link in the atom feed to be of type application/atom+xml. http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=403008 (Thanks Brian!) 7. Added DOCUMENT_ROOT to the python path per Martin's suggestion. http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=367127 (Thanks Martin!) 8. Translated all documentation from Docbook to reST. reST documentation is easier to read in "source-form" and a lot easier to convert to HTML and other formats using the Python docutils tools. (Thanks John!) 9. Added support for Paste and brought the WSGI support into the codebase. (Thanks Steven and Yury!) Pertinent to developers ----------------------- 1. Lots of code clean-up, documentation, test-code, and some refactoring. 2. cb_filestat will only do an os.stat if no plugin handles the filestat. Previously, cb_filestat did an os.stat and ran through all the plugins allowing them to over-ride it. 3. Added some testing framework pieces. This requires nose. To run the tests, do:: nosetests --verbose --include unit nosetests --verbose --include functional pyblosxom-1.5.3/docs/whatsnewolder.rst000066400000000000000000000003671217606004600201340ustar00rootroot00000000000000============================== What's new in older versions ============================== .. include:: whatsnew_1_4.rst .. include:: whatsnew_1_3.rst .. include:: whatsnew_1_2.rst .. include:: whatsnew_1_1.rst .. include:: whatsnew_1_0.rst pyblosxom-1.5.3/docs/writing_entries.rst000066400000000000000000000160441217606004600204610ustar00rootroot00000000000000=============== Writing Entries =============== .. _writing-entries: Categories ========== Writing entries in Pyblosxom is fairly straightforward. Each entry is a single text file located somewhere in the directory tree of your datadir. The directory that the entry is in is the category the entry is "filed under". For example, if my datadir was ``/home/joe/myblog/entries`` and I stored an entry named ``firstpost.txt`` in ``/home/joe/myblog/entries/status`` then the category for my entry would be ``/status``. .. Warning:: A warning about category names: Be careful when you create your categories---be sure to use characters that are appropriate in directory names for the file system you're using. .. Note:: Categories are NOT the same thing as tags. An entry can only belong to ONE category. If that's not what you want, you should write or install a tags plugin. Don't worry about making sure you have all the categories you need up front---you can add them as you need them. The format of an entry ====================== Pyblosxom entries consist of three parts: the title, the metadata, and then the body of the entry. The first line is the title of the entry. Then comes zero or more lines of metadata. After the metadata comes the body of the entry. Title ----- The title consists of a single line of plain text. You can have whatever characters you like in the title of your entry. The title doesn't have to be the same as the entry file name. Metadata -------- The metadata section is between the title and the body of the entry. It consists of a series of lines that start with the hash mark ``#``, then a metadata variable name, then a space, then the value of the metadata item. Example of metadata lines:: #mood bored #music The Doors - Greatest Hits Vol 1 The metadata variables set in the metadata section of the entry are available in your story template. So for the above example, the template variable ``$(mood)`` would be filled in with ``bored`` and ``$*music)`` would be filled in with ``The Doors - Greatest Hits Vol 1``. .. Note:: Metadata is not collected in a multi-dict. If you include two pieces of metadata with the same key, the second one will overwrite the first one. Example:: #mood bored #mood happy will result in ``'mood'`` --> ``'happy'`` in the metadata. .. Note:: You can provide metadata keys with no value. If you do this, then the default value is ``'1'``. This seems a bit weird, but it makes it easier for plugin developers to use these as flags. Body ---- The body of the entry is written in HTML and comprises the rest of the entry file. Examples -------- Here's an example first post entry with a title and a body:: This is my first post!

        This is the body of the first post to my blog.

        Here's a more complex example with a title and a body:: The rain in Spain....

        The rain

        in Spain

        is mainly on the plain.

        Here's an example of a post with title, metadata, and a body:: The rain in Spain.... #mood bored #music The Doors - Greatest Hits Vol 1

        The rain

        in Spain

        is mainly on the plain.

        Posting date ============ The posting date of the entry file is the modification time (also known as mtime) of the file itself as stored by your file system. Every time you go to edit an entry, it changes the modification time. You can see this in the following example of output:: willg ~/blog/entries/blosxom/site: vi testpost.txt [1] willg ~/blog/entries/blosxom/site: ls -l total 16 -rw-r--r-- 1 willg willg 764 Jul 20 2003 minoradjustments.txt -rw-r--r-- 1 willg willg 524 Jul 24 2003 moreminoradjustments.txt -rw-r--r-- 1 willg willg 284 Aug 15 2004 nomorecalendar.txt -rw-r--r-- 1 willg willg 59 Mar 21 16:30 testpost.txt [2] willg ~/blog/entries/blosxom/site: vi testpost.txt [3] willg ~/blog/entries/blosxom/site: ls -l total 16 -rw-r--r-- 1 willg willg 764 Jul 20 2003 minoradjustments.txt -rw-r--r-- 1 willg willg 524 Jul 24 2003 moreminoradjustments.txt -rw-r--r-- 1 willg willg 284 Aug 15 2004 nomorecalendar.txt -rw-r--r-- 1 willg willg 59 Mar 21 16:34 testpost.txt [4] 1. I create the blog entry ``testpost.txt`` using ``vi`` (vi is a text editor). The mtime of the file will be the time I last save the file and exit out of vi. 2. Note that the mtime on the file is ``Mar 21 16:30``. That's when I last saved the blog entry and exited out of vi. 3. I discover that I made a spelling mistake in my entry... So I edit it again in vi and fix the mistake. The mtime of the entry has now changed! 4. Now the mtime of the file is ``Mar 21 16:34``. This is the time that will show up in my blog as the posting date. .. Warning:: A warning about mtimes: There are some issues with this method for storing the posting date. First, if you ever change the blog entry, the mtime will change as well. That makes updating blog entries very difficult down the line. Second, if you move files around (backup/restore, changing the category structure, ...), you need to make sure you do so in a way that maintains the file's mtime. .. _Entry parsers: Entry parsers ============= Pyblosxom supports one format for entry files by default. This format is the same format that blosxom uses and is described in previous sections. A sample blog entry could look like this:: First post

        Here's the body of my first post.

        Some people don't like writing in HTML. Other people use their entries in other places, so they need a different markup format. Some folks write a lot of material in a non-HTML markup format and would like to use that same format for blog entries. These are all very valid reasons to want to use other markup formats. Pyblosxom allows you to install entry parser plugins which are Pyblosxom plugins that implement an entry parser. These entry parser plugins allow you to use other markup formats. Check the Plugin Registry on the `website`_ for other available entry parsers. Pyblosxom comes with a restructured text entry parser. If you don't see your favorite markup format represented, try looking at the code for other entry parsers and implement it yourself. If you need help, please ask on the pyblosxom-devel mailing list or on IRC. Details for both of these are on the `website`_. .. _website: http://pyblosxom.github.com/ Additionally, you're not locked into using a single markup across your blog. You can use any markup for an entry that you have an entry parser for. Beyond editors ============== There's no reason that all your entries have to come from editing blog entry text files in your datadir. Check the Pyblosxom Registry for scripts and other utilities that generate entries from other input sources. pyblosxom-1.5.3/maketarball.sh000077500000000000000000000040771217606004600164040ustar00rootroot00000000000000#!/bin/bash ####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### USAGE="Usage: $0 -h | [-dr] REVISH Creates a tarball of the repository at rev REVISH. This can be a branch name, tag, or commit sha. Options: -d Adds date to the directory name. -h Shows help and exits. Examples: ./maketarball v1.5 ./maketarball -d master " REVISH="none" PREFIX="none" NOWDATE="" while getopts ":dhr" opt; do case "$opt" in h) echo "$USAGE" exit 0 ;; d) NOWDATE=`date "+%Y-%m-%d-"` shift $((OPTIND-1)) ;; \?) echo "Invalid option: -$OPTARG" >&2 echo "$USAGE" >&2 ;; esac done if [[ -z "$1" ]]; then echo "$USAGE"; exit 1; fi REVISH=$1 PREFIX="$NOWDATE$REVISH" # convert PREFIX to all lowercase and nix the v from tag names. PREFIX=`echo "$PREFIX" | tr '[A-Z]' '[a-z]' | sed s/v//` # build the filename base minus the .tar.gz stuff--this is also # the directory in the tarball. FNBASE="pyblosxom-$PREFIX" STARTDIR=`pwd` function cleanup { pushd $STARTDIR if [[ -e tmp ]] then echo "+ cleaning up tmp/" rm -rf tmp fi popd } echo "+ Building tarball from: $REVISH" echo "+ Using prefix: $PREFIX" echo "+ Release?: $RELEASE" echo "" if [[ -e tmp ]] then echo "+ there's an existing tmp/. please remove it." exit 1 fi mkdir $STARTDIR/tmp echo "+ generating archive...." git archive \ --format=tar \ --prefix=$FNBASE/ \ $REVISH > tmp/$FNBASE.tar if [[ $? -ne 0 ]] then echo "+ git archive command failed. See above text for reason." cleanup exit 1 fi echo "+ compressing...." gzip tmp/$FNBASE.tar echo "+ archive at tmp/$FNBASE.tar.gz" echo "+ done." pyblosxom-1.5.3/setup.py000066400000000000000000000035311217606004600152720ustar00rootroot00000000000000#!/usr/bin/env python ####################################################################### # This file is part of Pyblosxom. # # Copyright (C) 2003-2011 by the Pyblosxom team. See AUTHORS. # # Pyblosxom is distributed under the MIT license. See the file # LICENSE for distribution details. ####################################################################### import os import re from setuptools import setup, find_packages READMEFILE = "README.rst" VERSIONFILE = os.path.join("Pyblosxom", "_version.py") VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" def get_version(): verstrline = open(VERSIONFILE, "rt").read() mo = re.search(VSRE, verstrline, re.M) if mo: return mo.group(1) else: raise RuntimeError( "Unable to find version string in %s." % VERSIONFILE) setup( name="pyblosxom", version=get_version(), description="Pyblosxom is a file-based weblog engine.", long_description=open(READMEFILE).read(), license='MIT', author="Will Kahn-Greene, et al", author_email="willg@bluesock.org", keywords="blog pyblosxom cgi weblog wsgi", url="http://pyblosxom.github.com/", packages=find_packages(exclude=["ez_setup"]), scripts=["bin/pyblosxom-cmd"], zip_safe=False, test_suite="Pyblosxom.tests.testrunner.test_suite", include_package_data=True, install_requires=[], classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content" ] )