--- a/.hgignore Sun Sep 14 12:55:49 2014 +0300
+++ b/.hgignore Sun Sep 14 13:05:35 2014 +0300
@@ -1,5 +1,7 @@
-syntax: regexp
-\.[^/]+.sw[op]$
-\.pyc$
-^cache/templates/.
-^stuff/
+syntax: glob
+
+.*.swo
+.*.swp
+*.pyc
+
+opt/
--- a/__init__.py Sun Sep 14 12:55:49 2014 +0300
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-"""
- The www.qmsk.net site is just a simple site with a filesystem-based URL mapping
-"""
-
--- a/index.cgi Sun Sep 14 12:55:49 2014 +0300
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-#!/usr/bin/python
-# :set filetype=py encoding=utf8
-
-"""
- CGI implementation
-"""
-
-# CGI handler for WSGI
-import wsgiref.handlers
-
-def cgi_error () :
- """
- Dumps out a raw traceback of the current exception to stdout, intended for use from except
- """
-
- import traceback, sys
-
- print 'Status: 500 Internal Server Error\r'
- print 'Content-type: text/plain\r'
- print '\r'
-
- traceback.print_exc(100, sys.stdout)
-
-def cgi_main () :
- """
- Run in CGI mode
- """
-
- try :
- from qmsk.web import wsgi, template
- import lookup
-
- # create handler
- cgi_handler = wsgiref.handlers.CGIHandler()
-
- # create app handler
- handler = lookup.PageMapper("/var/www/qmsk.net/pages", templates=template.TemplateLoader("/var/www/qmsk.net/templates", cache_dir="/var/www/qmsk.net/cache/templates"))
-
- # create app
- app = wsgi.Application(handler)
-
- # run once
- cgi_handler.run(app)
-
- except :
- cgi_error()
-
-if __name__ == '__main__' :
- cgi_main()
--- a/lookup.py Sun Sep 14 12:55:49 2014 +0300
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +0,0 @@
-
-import os, os.path
-
-from qmsk.web import http, template, handler
-
-import page, page_tree
-
-class PageMapper (handler.RequestHandler) :
- """
- Translates requests to handlers based on a filesystem directory containing various kinds of files
- """
-
- # list of page handlers, by type
- PAGE_TYPES = [
- ('html', page.HTMLPage ),
- (template.TEMPLATE_EXT, page.TemplatePage ),
- ]
-
- def __init__ (self, path, templates) :
- """
- Create, path is where the pages are stored. The list of pages is loaded from $path/list
- """
-
- # store
- self.path = path
- self.templates = templates
-
- # load the page tree
- self.tree = page_tree.PageTree(path + '/list')
-
- def _lookup_page_type (self, url, path, filename, basename, extension, tail) :
- """
- We found the file that we looked for, now get the correct type
- """
-
- # find appropriate handler
- for handler_ext, type in self.PAGE_TYPES :
- # match against file extension?
- if handler_ext == extension :
- # found handler, return instance
- return type(self, url, path, basename, tail)
-
- # no handler found
- raise PageError("No handler found for page %r of type %r" % (url, extension))
-
- def _lookup_page (self, name) :
- """
- Look up and return a Page object for the given page, or raise an error
- """
-
- # inital path
- path = self.path
- url_segments = []
-
- # name segments
- segments = name.split('/')
-
- # iterate through the parts of the page segments
- while True :
- segment = None
-
- # pop segment
- if segments :
- segment = segments.pop(0)
-
- url_segments.append(segment)
-
- # translate empty -> index
- if not segment :
- segment = 'index'
-
- # look for it in the dir
- for filename in os.listdir(path) :
- # build full file path
- file_path = os.path.join(path, filename)
-
- # stat, recurse into subdirectory?
- if os.path.isdir(file_path) and filename == segment :
- # use new dir
- path = file_path
-
- # break for-loop to look at next segment
- break
-
- # split into basename + extension
- basename, extension = os.path.splitext(filename)
-
- # ...remove that dot
- extension = extension.lstrip('.')
-
- # match against requested page name?
- if basename == segment :
- # found the file we wanted
- return self._lookup_page_type('/'.join(url_segments), file_path, filename, basename, extension, '/'.join(segments))
-
- else :
- # inspect next file in dir
- continue
-
- else :
- # did not find any dir or file, break out of while loop
- break
-
- # did not find the filename we were looking for in os.listdir
- raise NameError(name)
-
- def handle_request (self, request) :
- """
- Looks up the appropriate Page, and then renders it
- """
-
- # determine the page name
- page_name = request.get_page_name()
-
- # get the page handler
- page = self._lookup_page(page_name)
-
- # pass on
- return page.handle_request(request)
-
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/manage.py Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "qmsk_www.settings")
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
--- a/menu.py Sun Sep 14 12:55:49 2014 +0300
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-"""
- Handling the list of available pages
-"""
-
-class Menu (object) :
- """
- Contains info needed to render the menu
- """
-
- def __init__ (self, fs, page) :
- """
- Gather the menu information for the given page, as part of the given FilesystemMapper
- """
-
- # the selected page
- self.page = fs.tree.get_page(page.url)
-
- # the selected pagen's inheritance
- self.ancestry = self.page.get_ancestry() if self.page else []
-
- # list of menu items == root children, since we always show the full menu...
- self.items = fs.tree.root.children
-
--- a/page.py Sun Sep 14 12:55:49 2014 +0300
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,135 +0,0 @@
-
-"""
- Handling page requests
-"""
-
-# for filesystem ops
-import os, os.path
-import time
-
-from qmsk.web import http, handler, config
-
-import menu
-
-class PageError (http.ResponseError) :
- """
- Error looking up/handling a page
- """
-
- pass
-
-# XXX: should inherit from PageInfo
-class Page (handler.RequestHandler) :
- """
- This object represents the information about our attempt to render some specific page
- """
-
- def __init__ (self, fs, url, path, basename, url_tail, charset='utf8') :
- """
- Initialize the page at the given location
-
- @param fs the FilesysteMapper
- @param url the URL leading to this page
- @param path the filesystem path to this page's file
- @param basename the filesystem name of this page's file, without the file extension
- @param url_trail trailing URL for this page
- @param charset file charset
- """
-
- # store
- self.fs = fs
- self.url = url
- self.path = path
- self.basename = basename
- self.url_tail = url_tail
- self.charset = charset
-
- # sub-init
- self._init()
-
- def _init (self) :
- """
- Do initial data loading, etc
- """
-
- pass
-
- @property
- def title (self) :
- """
- Return the page's title
-
- Defaults to the retreiving the page title from page_list, or basename in Titlecase.
- """
-
- # lookup in PageTree
- page_info = self.fs.tree.get_page(self.url)
-
- # fallback to titlecase
- if page_info :
- title = page_info.title
-
- else :
- title = self.basename.title()
-
- return title
-
- @property
- def content (self) :
- """
- Return the page content as a string
- """
-
- abstract
-
- @property
- def modified (self) :
- """
- Returns the page modification timestamp
- """
-
- # stat
- timestamp = os.stat(self.path).st_mtime
-
- return time.strftime(config.DATETIME_FMT, time.gmtime(timestamp))
-
- def handle_request (self, request) :
- """
- Renders the fs's layout template with this page + menu
- """
-
- # render the template
- return self.fs.templates.render_to_response("layout",
- req = request,
- page = self,
- menu = menu.Menu(self.fs, self),
- )
-
-class HTMLPage (Page) :
- """
- A simple .html page that's just passed through directly
- """
-
- @property
- def content (self) :
- """
- Opens the .html file, reads and returns contents
- """
-
- return open(self.path, 'rb').read().decode(self.charset)
-
-class TemplatePage (Page) :
- """
- A template that's rendered using our template library
- """
-
- @property
- def content (self) :
- """
- Loads the .tmpl file, and renders it
- """
-
- return self.fs.templates.render_file(self.path,
- page_tree = self.fs.tree,
- )
-
--- a/page_tree.py Sun Sep 14 12:55:49 2014 +0300
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,218 +0,0 @@
-"""
- Implements the tree containing pages and their metadata
-"""
-
-from qmsk.web import tree_parse
-
-class PageTreeError (Exception) :
- """
- Error parsing/loading the page tree
- """
-
- pass
-
-class PageInfo (object) :
- """
- Contains metainformation about a page
- """
-
- def __init__ (self, parent, name, title, children=None) :
- """
- Initialize, children defaults to empty list
- """
-
- # store
- self.parent = parent
- self.name = name
- self.title = title
- self.children = children if children else []
-
- # no url get
- self._url = None
-
- def set_parent (self, parent) :
- """
- Set a parent where non was set before
- """
-
- assert self.parent is None
-
- self.parent = parent
-
- def add_child (self, child) :
- """
- Add a PageInfo child
- """
-
- self.children.append(child)
-
- def get_child (self, name) :
- """
- Look up a child by name, returning None if not found
- """
-
- return dict((c.name, c) for c in self.children).get(name)
-
- def get_ancestry (self) :
- """
- Returns a list of this page's parents and the page itself, but not root
- """
-
- # collect in reverse order
- ancestry = []
-
- # starting from self
- item = self
-
- # add all items, but not root
- while item and item.parent :
- ancestry.append(item)
-
- item = item.parent
-
- # reverse
- ancestry.reverse()
-
- # done
- return ancestry
-
- @property
- def url (self) :
- """
- Build this page's URL
- """
-
- # cached?
- if self._url :
- return self._url
-
- segments = [item.name for item in self.get_ancestry()]
-
- # add empty segment if dir
- if self.children :
- segments.append('')
-
- # join
- url = '/'.join(segments)
-
- # cache
- self._url = url
-
- # done
- return url
-
-class PageTree (object) :
- """
- The tree of pages, rooted at .root.
-
- Use load_page_tree to initialize the global page_tree instance, and then use that
- """
-
- def __init__ (self, path) :
- """
- Loads the PageTree root from the given file
- """
-
- # store
- self.path = path
-
- # load
- self._load(path)
-
- def _load (self, path) :
- """
- Processes the lines in the given file
- """
-
- # parse tree
- tree = tree_parse.parse(path, ':')
-
- if not tree :
- raise PageTreeError("No root node found")
-
- def _create_node (parent, item) :
- """
- Creates and returns a PageInfo from the given parent node and (line_number, line, children) tuple item
- """
-
- # unpack
- line_number, line, children = item
-
- # parse line
- url = title = None
-
- try :
- url, title = line.split(':')
-
- except :
- raise PageTreeError("Invalid line: %s:%d: %r" % (path, line_number, line))
-
- # remove whitespace
- url = url.strip()
- title = title.strip()
-
- # create PageInfo
- node = PageInfo(parent, url, title)
-
- # set node children
- node.children = [
- _create_node(node, child_item) for child_item in children
- ]
-
- # return
- return node
-
- # translate
- self.root = _create_node(None, tree)
-
- # *evil cackle*
- self.root.children.insert(0, self.root)
-
- def get_page (self, url) :
- """
- Lookup the given page URL, and return the matching PageInfo object, or None, if not found
- """
-
- # start from root
- node = self.root
-
- # traverse the object tree
- for segment in url.split('/') :
- if segment :
- node = node.get_child(segment)
-
- if not node :
- return None
-
- # return
- return node
-
- def get_siblings (self, url) :
- """
- Get the list of siblings for the given url, including the given page itself
- """
-
- # look up the page itself
- page = self.get_page(url)
-
- # specialcase root/unknown node
- if page and page.parent :
- return page.parent.children
-
- else :
- return self.root.children
-
- def dump (self) :
- """
- Returns a string representation of the tree
- """
-
- def _print_node (indent, node) :
- return '\n'.join('%s%s' % (' '*indent, line) for line in [
- "%-15s : %s" % (node.name, node.title)
- ] + [
- _print_node(indent + 4, child) for child in node.children if child != node
- ])
-
- return _print_node(0, self.root)
-
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www/settings/__init__.py Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,19 @@
+"""
+ Django settings for qmsk_www project.
+
+ For more information on this file, see
+ https://docs.djangoproject.com/en/1.7/topics/settings/
+
+ For the full list of settings and their values, see
+ https://docs.djangoproject.com/en/1.7/ref/settings/
+"""
+
+from qmsk_www.settings.database import *
+from qmsk_www.settings.localization import *
+from qmsk_www.settings.pages import *
+from qmsk_www.settings.site import *
+
+try:
+ from qmsk_www.settings.production import *
+except ImportError:
+ from qmsk_www.settings.development import *
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www/settings/database.py Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,6 @@
+## Database
+# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
+DATABASES = {
+
+}
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www/settings/development.py Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,37 @@
+# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '&e#@fir!k-*w(=0$@j&0guyv8qq*mh7_55j6w@2=hm0x9^ya-2'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+TEMPLATE_DEBUG = True
+
+ALLOWED_HOSTS = []
+
+## Logging
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'formatters': {
+ 'qmsk': {
+ 'format': '[%(levelname)5s] %(module)20s:%(funcName)-20s : %(message)s',
+ },
+ },
+ 'handlers': {
+ 'console': {
+ 'level': 'DEBUG',
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'qmsk',
+ },
+ },
+ 'loggers': {
+ 'qmsk_www_pages': {
+ 'handlers': [ 'console' ],
+ 'level': 'INFO',
+ }
+ },
+}
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www/settings/localization.py Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,13 @@
+## Internationalization
+# https://docs.djangoproject.com/en/1.7/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www/settings/pages.py Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,2 @@
+QMSK_WWW_PAGES_DIR = './pages'
+QMSK_WWW_PAGES_SITE = "qmsk.net"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www/settings/site.py Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,27 @@
+## Application definition
+INSTALLED_APPS = (
+ 'django.contrib.staticfiles',
+
+ 'qmsk_www_pages',
+)
+
+MIDDLEWARE_CLASSES = (
+
+)
+
+ROOT_URLCONF = 'qmsk_www.urls'
+
+WSGI_APPLICATION = 'qmsk_www.wsgi.application'
+
+## Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.7/howto/static-files/
+STATIC_URL = '/static/'
+STATICFILES_DIRS = (
+ './qmsk_www/static',
+)
+
+## Site templates
+TEMPLATE_DIRS = (
+ './qmsk_www/templates',
+)
+
Binary file qmsk_www/static/link.png has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www/static/style.css Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,172 @@
+
+/*
+ * Three main areas:
+ * header
+ * nav
+ * content
+ *
+ * Border layout:
+ * Header (bottom)
+ * Content (left)
+ */
+
+/*
+ * General
+ */
+a:hover {
+ color: inherit;
+}
+
+code {
+ color: inherit;
+ background-color: inherit;
+}
+
+/*
+ * Top header
+ */
+div#header {
+ padding: 30px;
+
+ border-bottom: 1px dashed #a5a5a5;
+}
+
+div#header h1 {
+ font-size: 48pt;
+ font-weight: bold;
+}
+
+div#header a:hover {
+ text-decoration: none;
+}
+
+/*
+ * Main navigation menu
+ */
+#nav {
+ margin: 20px 0px;
+}
+
+#nav ul {
+
+}
+
+#nav ul ul {
+
+}
+
+#nav li a {
+
+}
+
+#nav li a:hover {
+ background-color: #d0d0d0;
+ text-decoration: none;
+}
+
+#nav li.page-tree-active {
+ background-color: #808080;
+}
+
+/*
+ * Content
+ */
+div#content {
+
+}
+
+div#breadcrumb {
+ display: none;
+ font-size: x-small;
+}
+
+/*
+ * Footer
+ */
+div#footer {
+ padding: 20px 0px;
+
+ border-top: 1px dashed #a5a5a5;
+
+ font-size: x-small;
+ font-style: italic;
+}
+
+div#footer-left {
+ float: left;
+}
+
+.page-footer-modified {
+ float: right;
+ text-align: right;
+}
+
+div#footer-center {
+ text-align: center;
+}
+
+/*
+ * General styles
+ */
+a {
+ color: black;
+ text-decoration: none;
+ font-weight: bold;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+h1 {
+ font-size: xx-large;
+
+ text-align: center;
+}
+
+h2 {
+ font-size: large;
+
+ margin-left: 0px;
+ padding: 5px;
+ width: 100%;
+
+ background-color: #e5e5e5;
+
+ border: 1px dashed #c5c5c5;
+}
+
+h3 {
+ font-size: medium;
+ font-style: italic;
+}
+
+#content p {
+ margin-left: 0.5em;
+}
+
+#content li {
+ padding: 2px;
+}
+
+#content a {
+ padding-right: 13px;
+
+ background: transparent url(/static/link.png) no-repeat center right;
+}
+
+dt {
+ font-size: large;
+}
+
+dd {
+ margin: 1em auto 1em 5em;
+}
+
+code {
+ display: block;
+ margin: 8px;
+ padding: 8px;
+
+ border: 1px dotted #b5b5b5;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www/templates/site.html Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,25 @@
+{% load staticfiles %}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>{% block title %}{% endblock %}</title>
+
+ <!-- Bootstrap -->
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
+
+ <!-- qmsk.www.pages -->
+ <link rel="stylesheet" href="{% static "style.css" %}">
+ <link rel="stylesheet" href="{% static "pages/pages.css" %}">
+ </head>
+ <body>
+ {% block content %}{% endblock %}
+
+ <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
+
+ <!-- Include all compiled plugins (below), or include individual files as needed -->
+ <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
+ </body>
+</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www/urls.py Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,15 @@
+from django.conf.urls import patterns, include, url
+
+urlpatterns = patterns('',
+ # Examples:
+ # url(r'^$', 'qmsk_www.views.home', name='home'),
+ # url(r'^blog/', include('blog.urls')),
+ url(r'^', include('qmsk_www_pages.urls')),
+)
+
+if False:
+ from django.contrib import admin
+
+ urlpatterns += patterns('',
+ url(r'^admin/', include(admin.site.urls)),
+ )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www/wsgi.py Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,14 @@
+"""
+WSGI config for qmsk_www project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
+"""
+
+import os
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "qmsk_www.settings")
+
+from django.core.wsgi import get_wsgi_application
+application = get_wsgi_application()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www_pages/pages.py Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,351 @@
+from django.conf import settings
+
+import codecs
+import datetime
+import logging; log = logging.getLogger('qmsk_www_pages.pages')
+import os, os.path
+
+import markdown
+
+class NotFound (Exception):
+ pass
+
+class Site (object):
+ @classmethod
+ def lookup (cls):
+ return cls(
+ root = settings.QMSK_WWW_PAGES_DIR,
+ name = settings.QMSK_WWW_PAGES_SITE,
+ )
+
+ def __init__ (self, root, name):
+ self.root = root
+ self.name = name
+
+class Tree (object):
+ INDEX = 'index'
+
+ @classmethod
+ def lookup (cls, site, parts):
+ """
+ Returns Tree
+
+ Raises NotFound
+ """
+
+ parents = ( )
+ tree = cls(site.root, None, parents, site,
+ title = site.name,
+ )
+
+ for name in parts:
+ if name.startswith('.'):
+ # evil
+ raise NotFound()
+
+ if not name:
+ continue
+
+ path = os.path.join(tree.path, name)
+
+ if not os.path.exists(path):
+ raise NotFound()
+
+ if not os.path.isdir(path):
+ raise NotFound()
+
+ # title
+ title = tree.item_title(name)
+
+ parents += (tree, )
+ tree = cls(path, name, parents, site,
+ title = title,
+ )
+
+ return tree
+
+ def __init__ (self, path, name, parents, site,
+ title = None,
+ ):
+ """
+ path: filesystem path
+ name: subtree name, or None for root
+ parents: (Tree)
+ site: Site
+ """
+
+ self.path = path
+ self.name = name
+ self.parents = parents
+ self.site = site
+
+ self.title = title or name
+
+ def hierarchy (self):
+ """
+ Yield Tree.
+ """
+
+ for tree in self.parents:
+ yield tree
+
+ yield self
+
+ def url (self, tree=None, page=None):
+ path = '/'.join(tree.name for tree in self.hierarchy() if tree.name is not None)
+
+ if path:
+ path += '/'
+
+ if tree:
+ path = tree + '/'
+
+ if page:
+ path += page
+
+ return path
+
+ def scan (self):
+ """
+ Scan for files in tree.
+ """
+
+ for filename in os.listdir(self.path):
+ if filename.startswith('.'):
+ continue
+
+ if '.' in filename:
+ file_name, file_type = filename.rsplit('.', 1)
+ else:
+ file_name = filename
+ file_type = None
+
+ if not file_name:
+ continue
+
+ path = os.path.join(self.path, filename)
+
+ yield path, file_name, file_type
+
+ def item_title (self, name):
+ """
+ Lookup item title if exists.
+ """
+
+ title_path = os.path.join(self.path, name + '.title')
+
+ log.info("%s: %s title_path=%s", self, name, title_path)
+
+ if os.path.exists(title_path):
+ return open(title_path).read().strip()
+ else:
+ return None
+
+ def list (self):
+ """
+ Lists all Trees and Pages for this Tree.
+
+ Yields (name, url, page_type or None, title)
+ """
+
+ for path, name, file_type in self.scan():
+ title = self.item_title(name) or name
+
+ # trees
+ if os.path.isdir(path):
+ yield name, self.url(tree=name), None, title
+
+ if name == self.INDEX:
+ continue
+
+ # pages
+ if not file_type:
+ continue
+
+ if file_type not in TYPES:
+ continue
+
+ yield name, self.url(page=name), file_type, title
+
+ def list_sorted (self):
+ return sorted(list(self.list()))
+
+ def page (self, name):
+ """
+ Scans through tree looking for a matching page.
+
+ Returns Page or None.
+ """
+
+ if not name:
+ name = self.INDEX
+ title_default = self.title
+ else:
+ title_default = None
+
+ parents = self.parents + (self, )
+
+ for path, file_name, file_type in self.scan():
+ # match on name
+ if file_name != name and (file_name + '.' + file_type != name):
+ continue
+
+ # redirects?
+ if os.path.islink(path):
+ target = os.readlink(path)
+
+ # XXX: this should be some kind of common code
+ if '.' in target:
+ target, target_type = target.rsplit('.', 1)
+
+ log.info("%s: %s -> %s", self, name, target)
+
+ return RedirectPage(path, name, self, parents,
+ target = target,
+ )
+
+ # match on type
+ if not file_type:
+ continue
+
+ page_type = TYPES.get(file_type)
+
+ if not page_type:
+ continue
+
+ # out
+ title = self.item_title(file_name) or title_default
+
+ return page_type(path, name, self, parents,
+ title = title,
+ )
+
+class Page (object):
+ ENCODING = 'utf-8'
+
+ @classmethod
+ def lookup (cls, site, page):
+ """
+ Returns Page.
+
+ Raises NotFound
+ """
+
+ log.info("page=%r", page)
+
+ if page:
+ parts = page.split('/')
+ else:
+ parts = [ ]
+
+ if parts:
+ page_name = parts.pop(-1)
+ tree_parts = parts
+ else:
+ page_name = ''
+ tree_parts = []
+
+ # scan dir
+ tree = Tree.lookup(site, tree_parts)
+
+ # scan page
+ page = tree.page(page_name)
+
+ if not page:
+ raise NotFound()
+
+ return page
+
+ def __init__ (self, path, name, tree, parents=(), encoding=ENCODING, title=None):
+ self.path = path
+ self.name = name
+ self.tree = tree
+ self.parents = parents
+
+ self.encoding = encoding
+ self.title = title or name
+
+ def hierarchy (self):
+ """
+ Yield (Tree, name) pairs
+ """
+
+ parent = None
+
+ for tree in self.parents:
+ if parent:
+ yield parent, tree.name
+
+ parent = tree
+
+ yield parent, self.name
+
+ def url (self):
+ return self.tree.url(page=self.name)
+
+ def open (self):
+ return codecs.open(self.path, encoding=self.encoding)
+
+ def stat (self):
+ return os.stat(self.path)
+
+ def breadcrumb (self):
+ for tree in self.tree.hierarchy():
+ yield tree.url(), tree.title
+
+ if self.name != self.tree.INDEX:
+ yield self.url(), self.title
+
+ def modified (self):
+ return datetime.datetime.utcfromtimestamp(self.stat().st_mtime)
+
+ def redirect_page (self, request):
+ return None
+
+ def render_html (self, request):
+ raise NotImplementedError()
+
+# TODO: tree redirects
+class RedirectPage (Page):
+ def __init__ (self, path, name, tree, parents,
+ target,
+ **opts
+ ) :
+ super(RedirectPage, self).__init__(path, name, tree, parents, **opts)
+
+ self.target = target
+
+ def redirect_page (self, request):
+ return os.path.normpath(self.tree.url() + '/' + self.target)
+
+class HTML_Page (Page):
+ def render_html (self, request):
+ return self.open().read()
+
+class MarkdownPage (Page):
+ FORMAT = 'html5'
+
+ def __init__ (self, path, name, tree, parents,
+ format=FORMAT,
+ **opts
+ ) :
+ super(MarkdownPage, self).__init__(path, name, tree, parents, **opts)
+
+ self.format = format
+
+ def render_html (self, request):
+ return markdown.markdown(self.open().read(),
+ output_format = self.format,
+ )
+
+SITE = Site.lookup()
+
+TYPES = {
+ 'html': HTML_Page,
+ 'md': MarkdownPage,
+ 'markdown': MarkdownPage,
+}
+
+def page (page):
+ return Page.lookup(SITE, page)
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www_pages/static/pages/pages.css Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,3 @@
+li.page-tree-item i.glyphicon {
+ float: right;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www_pages/templates/pages/page.html Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,51 @@
+{% extends "site.html" %}
+
+{% block title %}{{ site_name }} :: {{ page_title }}{% endblock %}
+
+{% block content %}
+ <div class="container">
+ <div id="header">
+ <h1 class="page-header-title">
+ <a href="{% url 'page' '' %}">{{ site_name }}</a>
+ </h1>
+ </div>
+ <div id="breadcrumb">
+ <ol class="breadcrumb">
+ {% for page, title in page_breadcrumb %}
+ <li><a href="{% url 'page' page %}">{{ title }}</a></li>
+ {% endfor %}
+ </ol>
+ </div>
+ <div class="row">
+ <div class="col-sm-2" id="nav">
+ {% for tree, tree_name in page_hierarchy %}
+ {% if not forloop.first %}
+ <hr />
+ {% endif %}
+ <ul class="nav">
+ {% for name, page, type, title in tree.list_sorted %}
+ <li class="page-tree-item{% if type %} page-tree-{{type}}{% else %} page-tree-tree{% endif %}{% if name == tree_name %} page-tree-active{% endif %}">
+ <a href="{% url 'page' page %}">
+ {% if not type %}
+ <i class="glyphicon glyphicon-chevron-right"></i>
+ {% endif %}
+ {{ title }}
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+ {% endfor %}
+ </div>
+ <div class="col-sm-8" id="content">
+ <h1>{{ page_title }}</h1>
+
+ {{ page_html|safe }}
+ </div>
+ </div>
+ <div id="footer">
+ <p class="page-footer-modified">
+ Page modified <span title="{{ page_modified|date:'DATETIME_FORMAT' }}">{{ page_modified|date }}</span>
+ </p>
+ </div>
+ </div>
+{% endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www_pages/urls.py Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,7 @@
+from django.conf.urls import patterns, include, url
+
+from qmsk_www_pages import views
+
+urlpatterns = patterns('',
+ url(r'^(?P<page>.*)$', views.page, name='page'),
+)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk_www_pages/views.py Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,26 @@
+from django.http import Http404
+from django.shortcuts import render, redirect
+
+from qmsk_www_pages import pages
+
+# Create your views here.
+def page (request, page):
+ try:
+ page = pages.page(page)
+ except pages.NotFound:
+ raise Http404
+
+ redirect_page = page.redirect_page(request)
+
+ if redirect_page:
+ return redirect('page', redirect_page)
+
+ return render(request, 'pages/page.html', dict(
+ site_name = page.tree.site.name,
+ page_name = page.name,
+ page_title = page.title,
+ page_breadcrumb = page.breadcrumb(),
+ page_hierarchy = list(page.hierarchy()),
+ page_html = page.render_html(request),
+ page_modified = page.modified(),
+ ))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/requirements.txt Sun Sep 14 13:05:35 2014 +0300
@@ -0,0 +1,2 @@
+Django==1.7
+Markdown==2.5
Binary file static/link.png has changed
--- a/static/style.css Sun Sep 14 12:55:49 2014 +0300
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,195 +0,0 @@
-
-/*
- * Three main areas:
- * header
- * nav
- * content
- *
- * Border layout:
- * Header (bottom)
- * Content (left)
- */
-
-/*
- * Global styles
- */
-body {
- padding: 0px;
- margin: 0px;
-
- background-color: #ffffff;
- color: #000000;
-}
-
-/*
- * Top header
- */
-div#header {
- padding: 30px;
-
- font-size: 48pt;
- font-weight: bold;
-
- border-bottom: 1px dashed #a5a5a5;
-}
-
-div#header a:hover {
- text-decoration: none;
-}
-
-/*
- * Container for layout items
- */
- #container {
- overflow: hidden;
-}
-
-/*
- * Main navigation menu
- */
-#nav {
- float: left;
-
- padding-bottom: 2000px;
- margin-bottom: -2000px;
-
- border-right: 1px dashed #a5a5a5;
-}
-
-#nav ul {
- margin: 0px;
- padding: 0px;
-
- list-style: none;
-
- width: 180px;
-
- padding-top: 25px;
-}
-
-#nav ul ul {
- padding-top: 0px;
- border-left: 15px solid #d0d0d0;
-}
-
-#nav li a {
- display: block;
-
- padding: 10px;
- padding-left: 20px;
-}
-
-#nav li a:hover {
- background-color: #d0d0d0;
- text-decoration: none;
-}
-
-#nav li a.selected-page {
- border-left: 5px solid black;
- padding-left: 15px;
-}
-
-/*
- * Content
- */
-div#content {
- margin-left: 180px;
- padding: 25px;
-
- padding-right: 50px;
-}
-
-div#breadcrumb {
- font-size: x-small;
-}
-
-/*
- * Footer
- */
-div#footer {
- padding: 10px;
-
- border-top: 1px dashed #a5a5a5;
-
- font-size: x-small;
- font-style: italic;
-}
-
-div#footer-left {
- float: left;
-}
-
-div#footer-right {
- float: right;
- text-align: right;
-}
-
-div#footer-center {
- text-align: center;
-}
-
-/*
- * General styles
- */
-a {
- color: black;
- text-decoration: none;
- font-weight: bold;
-}
-
-a:hover {
- text-decoration: underline;
-}
-
-h1 {
- font-size: xx-large;
-
- text-align: center;
-}
-
-h2 {
- font-size: large;
-
- margin-left: 0px;
- padding: 5px;
- width: 100%;
-
- background-color: #e5e5e5;
-
- border: 1px dashed #c5c5c5;
-}
-
-h3 {
- font-size: medium;
- font-style: italic;
-}
-
-div#content p {
- margin-left: 0.5em;
-}
-
-#content li {
- padding: 2px;
-}
-
-#content a {
- padding-right: 13px;
-
- background: transparent url(/static/link.png) no-repeat center right;
-}
-
-dt {
- font-size: large;
-}
-
-dd {
- margin: 1em auto 1em 5em;
-}
-
-code {
- display: block;
- margin: 8px;
- padding: 8px;
-
- border: 1px dotted #b5b5b5;
-}
--- a/templates/layout.tmpl Sun Sep 14 12:55:49 2014 +0300
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-
-<%def name="render_menu(open_page, page, items, ancestry)">
-<ul>
-% for pi in items :
- <li>
- <a href="${req.page_prefix}/${pi.url}"${' class="selected-page"' if pi == open_page else ''}>${pi.title} ${'»' if pi.children and pi.parent else ''}</a>
- % if pi in ancestry and pi.children and pi.parent :
- ${render_menu(page, pi, pi.children, ancestry)}
- % endif
- </li>
-% endfor
-</ul>
-</%def>
-
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
- <head>
- <title>qmsk.net ${' :: ' + h.breadcrumb(menu.ancestry, links=False) if menu.ancestry else ''}</title>
- <link rel="Stylesheet" type="text/css" href="${req.site_root}/static/style.css" />
- </head>
- <body>
- <div id="header">
- <a href="${req.page_prefix}/">QMSK.NET</a>
- </div>
-
- <div id="container">
- <div id="nav">
- ${render_menu(menu.page, menu.page, menu.items, menu.ancestry)}
- </div>
-
- <div id="content">
- <div id="breadcrumb">
- <!-- ${h.breadcrumb(menu.ancestry)} -->
- </div>
- ${page.content}
- </div>
-
- </div>
-
- <div id="footer">
- <div id="footer-right">
- Page Modified ${page.modified} <br/>
- Current time ${h.now()}
- </div>
-
- <div id="footer-left">
- © ${h.copyright_year()} Tero Marttila
- </div>
-
- <div id="footer-center">
- ${h.validation_notice(req.site_host)}
- </div>
- </div>
- </body>
-</html>
-