"BlogFace - simple webblog product"

from AccessControl import ClassSecurityInfo
from Globals import InitializeClass
import Globals
from OFS.Folder import Folder
from Products.ZCatalog.ZCatalog import ZCatalog
from DateTime import DateTime
#from OFS.Cache import Cacheable
import re
import string
from datetools import seqToDate, dateToSeq

import os
dtmlprefix = os.environ.get('INSTANCE_HOME', None)
if dtmlprefix:
    dtmlprefix = dtmlprefix + '/Products/BlogFace/dtml/'
else:
    dtmlprefix = 'lib/python/Products/BlogFace/dtml/'
            
def addBlogFace(self, id, title, newCatalog = None, REQUEST = None):
    "add BlogFace instance"
    ob = BlogFace(id, newCatalog)
    ob.title = str(title)
    id = self._setObject(id, ob)
    if REQUEST: 
        return self.manage_main(self, REQUEST)

manage_addBlogFace = Globals.DTMLFile('dtml/BlogFaceAdd', globals())

class BlogFace(Folder):
    "Container and manager class for weblog items"

    meta_type='BlogFace'

    security = ClassSecurityInfo()

    manage_blogForm = Globals.DTMLFile('dtml/main', globals())

    manage_options = ({'label' : 'Main', 'action' : 'manage_blogForm',
                      'help' : ('BlogFace', 'BlogFace_main.stx')},) + \
                      Folder.manage_options

    # entry attributes to catalog, if we build a default catalog
    catAttrs = (('title', 'FieldIndex'), ('Date', 'FieldIndex'),
                ('document_src', 'TextIndex'))
    # catalog index to get the date from
    dateIndex = 'Date'

    # entries displayed per page
    entriesDisplayed = 10

    # dict for turing traversal params into search refine arguments
    # keys are params in the URL, values are 2-tuples for the
    # catalog search dict - criterion, value.
    # keys cannot be intable, since those are turned to date params
    # keys that match subobject ids will shadow traversing to subobjects
    # this is set in __init__
    #traverseParamOpts = {}

    # list of 2-tuples for default search refine arguments
    # like traverseParamOpts, but automatic, no URL params
    # this is set in __init__    
    #defaultSearchOpts = []

    def __init__(self, id, newCatalog = 1):
        self.id = id
        # why does this need to be set here, and not in the class?
        # perhaps because dicts aren't autopersistent?
        self.traverseParamOpts = {}
        self.defaultSearchOpts = []
        if newCatalog:
            self.initCatalog()

        # views

        tmpfile = open(dtmlprefix + 'index.dtml')
        self.manage_addDTMLMethod('index_htmlPage', '', tmpfile.read())
        tmpfile.close()
        
        tmpfile = open(dtmlprefix + 'entries.dtml')
        self.manage_addDTMLMethod('entries', '', tmpfile.read())
        tmpfile.close()
        
        tmpfile = open(dtmlprefix + 'blogCalendar.dtml')
        self.manage_addDTMLMethod('blogCalendar', '', tmpfile.read())
        tmpfile.close()

        tmpfile = open(dtmlprefix + 'URLFmtError.dtml')
        self.manage_addDTMLMethod('URLFmtError', '', tmpfile.read())
        tmpfile.close()
        
        tmpfile = open(dtmlprefix + 'rss.dtml')
        self.manage_addDTMLMethod('rss', '', tmpfile.read())
        tmpfile.close()

    def initCatalog(self):
        "init the default blog catalog"
        if not hasattr(self, 'Catalog'):
            self._setObject('Catalog',
                            ZCatalog('Catalog', title='', container=self))
        self.catalogPath = "Catalog"
        # add indices, tables if we don't have them yet
        schema = self.getCatalog().schema()
        indexes = self.getCatalog().indexes()
        for at in self.catAttrs:
            if not at[0] in schema:
                self.getCatalog().manage_addColumn(at[0])
            if not at[0] in indexes:
                self.getCatalog().manage_addIndex(at[0], at[1])

    def getCatalog(self):
        "Return the catalog used."
        return self.unrestrictedTraverse(self.catalogPath)

    security.declareProtected('View management screens',
                              'getTraverseParamOpts') 
    def getTraverseParamOpts(self):
        """
        Return traverse paramater options as items.
        This is to make it easier to handle in DTML.
        """
        return self.traverseParamOpts.items()

    security.declarePublic('searchForward')
    def searchForward(self, date, params = ()):
        "Return the results of a forward search of the catalog."
        dict = {}
        dict['sort_on'] = self.dateIndex
        dict[self.dateIndex] = date
        dict[self.dateIndex + '_usage'] = 'range:max'
        dict['sort_order'] = 'reverse'
        for param in self.defaultSearchOpts:
            dict[param[0]] = param[1]
        for param in params:
            dict[param[0]] = param[1]
        return self.getCatalog().searchResults(dict)

    security.declarePublic('searchReverse')
    def searchReverse(self, date, params = ()):
        "Return the results of a backward search of the catalog."
        dict = {}
        dict['sort_on'] = self.dateIndex
        dict[self.dateIndex] = date
        dict[self.dateIndex + '_usage'] = 'range:min'
        dict['sort_order'] = 'ascending'
        for param in self.defaultSearchOpts:
            dict[param[0]] = param[1]
        for param in params:
            dict[param[0]] = param[1]
        return self.getCatalog().searchResults(dict)

    def __bobo_traverse__(self, REQUEST, name):
        """
        Consume trailing URL path and turn into arguments so long as
        they are convertable to ints.  Otherwise, traverse normally with
        collected arguments.  Note that this shadows subobjects with IDs
        that are convertable to ints.
        """
        if re.match('^[0-9]+$', name):
            if not REQUEST.has_key('traverse_date'):
                REQUEST['traverse_date'] = []
            (REQUEST['traverse_date']).append(name)
        elif name in self.traverseParamOpts.keys():
            if not REQUEST.has_key('traverse_params'):
                REQUEST['traverse_params'] = []
            (REQUEST['traverse_params']).append(name)
        else:
            # normal traverse.  This can raise an AttributeError.  What can
            # we raise to get the publisher to do what it does in other
            # cases, besides making and raising a notFoundError ourselves?
            return getattr(self, name)
        return self

    security.declareProtected('View management screens', 'manage_blog') 
    def manage_blog(self, title, entries, dateIndex, catalogPath,
                    delSearch = None, search = None,
                    delDefaultSearch = None, defaultSearch = None,
                    REQUEST = None):
        "main management method"
        self.title = str(title)
        self.entriesDisplayed = int(entries)
        if dateIndex:
            self.dateIndex = dateIndex
        if catalogPath:
            try:
                newCat = self.unrestrictedTraverse(catalogPath)
            except:
                if REQUEST is not None:                
                    return Globals.MessageDialog(
                        title= 'Error',
                        message = "No catalog found at %s." % catalogPath,
                        action = 'manage_blogForm')
                raise
            if not self.dateIndex in newCat.indexes():
                if REQUEST is not None:                
                    return Globals.MessageDialog(
                        title= 'Error',
                        message = ("Catalog %s has no date index (%s)."
                                   % (catalogPath, self.dateIndex)),
                        action = 'manage_blogForm')
            self.catalogPath = catalogPath
        if delSearch:
            for i in delSearch:
                del self.traverseParamOpts[i]
                self._p_changed = 1
        if search:
            if (search.get('param', None) and search.get('term', None) and
                search.get('crit', None)):
                self.traverseParamOpts[search.param] = (
                    search.term, search.crit)
                self._p_changed = 1
        if delDefaultSearch:
            for i in delDefaultSearch:
                self.defaultSearchOpts.remove(tuple(string.split(i, ' ', 1)))
                self._p_changed = 1
        if defaultSearch:
            if (defaultSearch.get('term', None)
                and defaultSearch.get('crit', None)):
                self.defaultSearchOpts.append((defaultSearch.get('term'),
                                               defaultSearch.get('crit')))
                self._p_changed = 1
        if REQUEST:
            message = "Saved changes."
            return self.manage_blogForm(self, REQUEST,
                                        management_view="Edit",
                                        manage_tabs_message=message)

    security.declarePublic('hasEntryDay')
    def hasEntryDay(self, date, params = ()):
        """
        Return true if there is an entry between the time of the date
        and the end of that day.  date is an ISO formatted string.
        """
        # note we don't use the URL params here
        dict = {}
        dict['sort_on'] = self.dateIndex
        dict[self.dateIndex] = [date, DateTime(date).latestTime().ISO()]
        dict[self.dateIndex + '_usage'] = 'range:min:max'
        #dict['sort_order'] = 'ascending'
        for param in self.defaultSearchOpts:
            dict[param[0]] = param[1]
        for param in params:
            dict[param[0]] = param[1]
        return len(self.getCatalog().searchResults(dict)) > 0
        
    security.declarePublic('dateURL')
    def dateURL(self, dateStr, REQUEST = None, withAnchor = None):
        """
        Return a URL for the page for dateStr, which is an ISO formatted
        string.  If withAnchor is true, include an anchor of the DateTime
        TimeMinutes() format.  This assumes that the page includes that
        target!
        """
        date = DateTime(dateStr)
        out = self.absolute_url() + '/'
        if REQUEST and REQUEST.get('traverse_params', None):
            out = out + string.join(REQUEST.traverse_params, '/') + '/'
        out = out + string.join(dateToSeq(date, 3), '/')
        if withAnchor:
            out = out + '#' + date.TimeMinutes()
        return out
    
    # Is this efficient?  Search results are LazyMaps,
    # so we shouldn't be grumbling through the entire catalog
    # if we only use the front few entries
    def getEntrySeq(self, date, REQUEST):
        """
        Return a tuple containing the sequence of latest entries earlier
        than date, the URL of the previous
        sequence or None, and the URL of the next sequence or None.
        """
        params = self.requestParams(REQUEST)
        searchOut = self.searchForward(date, params)
        lastOut = self.entriesDisplayed
        # add to return list so long as they're on the same day
        while (len(searchOut) > lastOut and
               string.split(getattr(searchOut[lastOut-1],
                                    self.dateIndex))[0] ==
               string.split(getattr(searchOut[lastOut],
                                    self.dateIndex))[0]):
            lastOut += 1
        # now index lastOut is a later day than lastOut-1, if it exists.
        if len(searchOut) > lastOut:
            # earlyURL is the URL for that prev day
            earlyURL = self.dateURL(
                getattr(searchOut[lastOut], self.dateIndex), REQUEST)
        else:
            earlyURL = None
        revSearchOut = self.searchReverse(date, params)
        if revSearchOut:
            # lateURL is the URL for the next day
            # return the day URL, might point to earlier entries, but
            # the whole day will be displayed
            lateIndex = min(self.entriesDisplayed - 1, len(revSearchOut) - 1)
            lateURL = self.dateURL(
                getattr(revSearchOut[lateIndex], self.dateIndex), REQUEST)
        else:
            lateURL = None
        return (searchOut[0:lastOut], earlyURL, lateURL)

    security.declarePublic('requestDate')
    def requestDate(self, REQUEST):
        "Return a DateTime for the date passed by the REQUEST date parameters."
        if not REQUEST.has_key('traverse_date'):
            REQUEST['traverse_date'] = []
        return seqToDate(REQUEST.traverse_date)    

    security.declarePublic('requestParams')
    def requestParams(self, REQUEST):
        "Return the search params corresponding to the REQUEST parameters."
        searchParams = []
        if REQUEST.has_key('traverse_params'):
            for param in REQUEST['traverse_params']:
                searchParams.append(self.traverseParamOpts[param])
        return searchParams
    
    security.declarePublic('index_html')
    def index_html(self, REQUEST, RESPONSE):
        """
        Return the index page with blog entries searched according to
        args collected by __bobo_traverse__, or return error page on raise.
        """
        try:        
            date = self.requestDate(REQUEST)
        except:
            return self.URLFmtError(self, REQUEST)            
        searchParams = self.requestParams(REQUEST)
        # put these in REQUEST, otherwise DTML pages called from the
        # index page don't get them.  I don't think this is necessary,
        # I might be missing something...
        REQUEST.set('blogDate', date)
        REQUEST.set('searchParams', searchParams)
        (entryList, earlyURL, lateURL) = self.getEntrySeq(date, REQUEST)
        REQUEST.set('entryList', entryList)
        REQUEST.set('lateURL', lateURL)
        REQUEST.set('earlyURL', earlyURL)
        return self.index_htmlPage(self, REQUEST, RESPONSE)

    security.declarePublic('index_html')
    def rssItems(self, REQUEST):
        "Return list of items for a RSS page."
        date = DateTime().latestTime().ISO()
        params = self.requestParams(REQUEST)
        return self.searchForward(date, params)[0:10] #XXX hardcoded#


InitializeClass(BlogFace)
