qmsk/pages/pages.py
changeset 233 f5227f26231b
parent 230 4439815ab108
equal deleted inserted replaced
232:5a16a53e9800 233:f5227f26231b
       
     1 from django.conf import settings
       
     2 
       
     3 import codecs
       
     4 import datetime
       
     5 import logging; log = logging.getLogger('qmsk.pages.pages')
       
     6 import os, os.path
       
     7 
       
     8 import markdown
       
     9 import mako.template
       
    10 
       
    11 class NotFound (Exception):
       
    12     pass
       
    13 
       
    14 class RenderError (Exception):
       
    15     pass
       
    16 
       
    17 class Site (object):
       
    18     @classmethod
       
    19     def lookup (cls):
       
    20         return cls(
       
    21             root        = settings.QMSK_PAGES_DIR,
       
    22             name        = settings.QMSK_PAGES_SITE,
       
    23         )
       
    24 
       
    25     def __init__ (self, root, name):
       
    26         self.root = root
       
    27         self.name = name
       
    28 
       
    29     def tree (self):
       
    30         return Tree(self.root, None, (), self,
       
    31                 title       = self.name,
       
    32         )
       
    33 
       
    34 class Tree (object):
       
    35     INDEX = 'index'
       
    36 
       
    37     @classmethod
       
    38     def lookup (cls, site, parts):
       
    39         """
       
    40             Returns Tree
       
    41 
       
    42             Raises NotFound
       
    43         """
       
    44 
       
    45         parents = ( )
       
    46         tree = site.tree()
       
    47 
       
    48         for name in parts:
       
    49             if name.startswith('.'):
       
    50                 # evil
       
    51                 raise NotFound()
       
    52             
       
    53             if not name:
       
    54                 continue
       
    55         
       
    56             path = os.path.join(tree.path, name)
       
    57 
       
    58             if not os.path.exists(path):
       
    59                 raise NotFound()
       
    60             
       
    61             if not os.path.isdir(path):
       
    62                 raise NotFound()
       
    63 
       
    64             # title
       
    65             title = tree.item_title(name)
       
    66 
       
    67             parents += (tree, )
       
    68             tree = cls(path, name, parents, site,
       
    69                     title   = title,
       
    70             )
       
    71 
       
    72         return tree
       
    73 
       
    74     def __init__ (self, path, name, parents, site,
       
    75             title   = None,
       
    76     ):
       
    77         """
       
    78             path:       filesystem path
       
    79             name:       subtree name, or None for root
       
    80             parents:    (Tree)
       
    81             site:       Site
       
    82         """
       
    83 
       
    84         self.path = path
       
    85         self.name = name
       
    86         self.parents = parents
       
    87         self.site = site
       
    88 
       
    89         self.title = title or name
       
    90 
       
    91     def hierarchy (self):
       
    92         """
       
    93             Yield Tree.
       
    94         """
       
    95 
       
    96         for tree in self.parents:
       
    97             yield tree
       
    98 
       
    99         yield self
       
   100 
       
   101     def url (self, tree=None, page=None):
       
   102         path = '/'.join(tree.name for tree in self.hierarchy() if tree.name is not None)
       
   103 
       
   104         if path:
       
   105             path += '/'
       
   106 
       
   107         if tree:
       
   108             path = tree + '/'
       
   109 
       
   110         if page:
       
   111             path += page
       
   112 
       
   113         return path
       
   114 
       
   115     def scan (self):
       
   116         """
       
   117             Scan for files in tree.
       
   118         """
       
   119 
       
   120         for filename in os.listdir(self.path):
       
   121             if filename.startswith('.'):
       
   122                 continue
       
   123             
       
   124             if '.' in filename:
       
   125                 file_name, file_type = filename.rsplit('.', 1)
       
   126             else:
       
   127                 file_name = filename
       
   128                 file_type = None
       
   129 
       
   130             if not file_name:
       
   131                 continue
       
   132 
       
   133             path = os.path.join(self.path, filename)
       
   134             
       
   135             yield path, file_name, file_type
       
   136 
       
   137     def item_title (self, name):
       
   138         """
       
   139             Lookup item title if exists.
       
   140         """
       
   141 
       
   142         title_path = os.path.join(self.path, name + '.title')
       
   143 
       
   144         log.info("%s: %s title_path=%s", self, name, title_path)
       
   145 
       
   146         if os.path.exists(title_path):
       
   147             return open(title_path).read().strip()
       
   148         else:
       
   149             return None
       
   150 
       
   151     def list (self):
       
   152         """
       
   153             Lists all Trees and Pages for this Tree.
       
   154 
       
   155             Yields (name, url, page_type or None, title)
       
   156         """
       
   157         
       
   158         for path, name, file_type in self.scan():
       
   159             title = self.item_title(name) or name
       
   160 
       
   161             # trees
       
   162             if os.path.isdir(path):
       
   163                 yield name, self.url(tree=name), None, title
       
   164 
       
   165             if name == self.INDEX:
       
   166                 continue
       
   167             
       
   168             # pages
       
   169             if not file_type:
       
   170                 continue
       
   171 
       
   172             if file_type not in TYPES:
       
   173                 continue
       
   174 
       
   175             yield name, self.url(page=name), file_type, title
       
   176 
       
   177     def list_sorted (self):
       
   178         return sorted(list(self.list()))
       
   179 
       
   180     def page (self, name):
       
   181         """
       
   182             Scans through tree looking for a matching page.
       
   183             
       
   184             Returns Page or None.
       
   185         """
       
   186         
       
   187         if not name:
       
   188             name = self.INDEX
       
   189             title_default = self.title
       
   190         else:
       
   191             title_default = None
       
   192 
       
   193         parents = self.parents + (self, )
       
   194 
       
   195         for path, file_name, file_type in self.scan():
       
   196             # match on name
       
   197             if file_name == name:
       
   198                 pass
       
   199             elif file_type and (file_name + '.' + file_type == name):
       
   200                 pass
       
   201             else:
       
   202                 continue
       
   203 
       
   204             # redirects?
       
   205             if os.path.islink(path):
       
   206                 target = os.readlink(path)
       
   207             
       
   208                 # XXX: this should be some kind of common code
       
   209                 if '.' in target:
       
   210                     target, target_type = target.rsplit('.', 1)
       
   211 
       
   212                 log.info("%s: %s -> %s", self, name, target)
       
   213 
       
   214                 return RedirectPage(path, name, self, parents,
       
   215                     target  = target,
       
   216                 )
       
   217             
       
   218             # match on type
       
   219             if not file_type:
       
   220                 continue
       
   221 
       
   222             page_type = TYPES.get(file_type)
       
   223 
       
   224             if not page_type:
       
   225                 continue
       
   226             
       
   227             # out
       
   228             title = self.item_title(file_name) or title_default
       
   229 
       
   230             return page_type(path, name, self, parents,
       
   231                 title   = title,
       
   232             )
       
   233 
       
   234 class Page (object):
       
   235     ENCODING = 'utf-8'
       
   236 
       
   237     @classmethod
       
   238     def lookup (cls, site, page):
       
   239         """
       
   240             Returns Page.
       
   241             
       
   242             Raises NotFound
       
   243         """
       
   244         
       
   245         log.info("page=%r", page)
       
   246 
       
   247         if page:
       
   248             parts = page.split('/')
       
   249         else:
       
   250             parts = [ ]
       
   251             
       
   252         if parts:
       
   253             page_name = parts.pop(-1)
       
   254             tree_parts = parts
       
   255         else:
       
   256             page_name = ''
       
   257             tree_parts = []
       
   258 
       
   259         # scan dir
       
   260         tree = Tree.lookup(site, tree_parts)
       
   261 
       
   262         # scan page
       
   263         page = tree.page(page_name)
       
   264 
       
   265         if not page:
       
   266             raise NotFound()
       
   267 
       
   268         return page
       
   269 
       
   270     def __init__ (self, path, name, tree, parents=(), encoding=ENCODING, title=None):
       
   271         self.path = path
       
   272         self.name = name
       
   273         self.tree = tree
       
   274         self.parents = parents
       
   275 
       
   276         self.encoding = encoding
       
   277         self.title = title or name
       
   278 
       
   279     def hierarchy (self):
       
   280         """
       
   281             Yield (Tree, name) pairs
       
   282         """
       
   283 
       
   284         parent = None
       
   285 
       
   286         for tree in self.parents:
       
   287             if parent:
       
   288                 yield parent, tree.name
       
   289 
       
   290             parent = tree
       
   291 
       
   292         yield parent, self.name
       
   293 
       
   294     def url (self):
       
   295         return self.tree.url(page=self.name)
       
   296 
       
   297     def open (self):
       
   298         return codecs.open(self.path, encoding=self.encoding)
       
   299 
       
   300     def stat (self):
       
   301         return os.stat(self.path)
       
   302 
       
   303     def breadcrumb (self):
       
   304         for tree in self.tree.hierarchy():
       
   305             yield tree.url(), tree.title
       
   306         
       
   307         if self.name != self.tree.INDEX:
       
   308             yield self.url(), self.title
       
   309 
       
   310     def modified (self):
       
   311         return datetime.datetime.utcfromtimestamp(self.stat().st_mtime)
       
   312 
       
   313     def redirect_page (self, request):
       
   314         return None
       
   315     
       
   316     def render_html (self, request):
       
   317         raise NotImplementedError()
       
   318 
       
   319 # TODO: tree redirects
       
   320 class RedirectPage (Page):
       
   321     def __init__ (self, path, name, tree, parents,
       
   322             target,
       
   323             **opts
       
   324     ) :
       
   325         super(RedirectPage, self).__init__(path, name, tree, parents, **opts)
       
   326 
       
   327         self.target = target
       
   328 
       
   329     def redirect_page (self, request):
       
   330         return os.path.normpath(self.tree.url() + '/' + self.target)
       
   331 
       
   332 class HTML_Page (Page):
       
   333     def render_html (self, request):
       
   334         return self.open().read()
       
   335 
       
   336 class MarkdownPage (Page):
       
   337     FORMAT = 'html5'
       
   338 
       
   339     def __init__ (self, path, name, tree, parents,
       
   340             format=FORMAT,
       
   341             **opts
       
   342     ) :
       
   343         super(MarkdownPage, self).__init__(path, name, tree, parents, **opts)
       
   344 
       
   345         self.format = format
       
   346 
       
   347     def render_html (self, request):
       
   348         return markdown.markdown(self.open().read(),
       
   349             output_format   = self.format,
       
   350         )
       
   351 
       
   352 class TemplatePage (Page):
       
   353     def render_html (self, request):
       
   354         """
       
   355             Raises RenderError if !DEBUG, arbitrary error with stack trace otherwise.
       
   356         """
       
   357 
       
   358         try:
       
   359             return mako.template.Template(filename=self.path).render(
       
   360                     request = request,
       
   361             )
       
   362         except Exception as error:
       
   363             if settings.DEBUG:
       
   364                 raise
       
   365             else:
       
   366                 raise RenderError(error)
       
   367 
       
   368 SITE = Site.lookup()
       
   369 
       
   370 TYPES = {
       
   371     'html':         HTML_Page,
       
   372     'md':           MarkdownPage,
       
   373     'markdown':     MarkdownPage,
       
   374     'tmpl':         TemplatePage,
       
   375 }
       
   376 
       
   377 def page (page):
       
   378     return Page.lookup(SITE, page)
       
   379