degal/folder.py
author Tero Marttila <terom@fixme.fi>
Fri, 05 Jun 2009 21:50:49 +0300
changeset 64 4ebd563214d2
parent 44 533b7e8b5d3b
child 70 67dd32adf159
permissions -rw-r--r--
begin implementation of folder, gallery
"""
    Per-directory gallery state
"""

import filesystem, image, html

from utils import lazy_load

class Folder (filesyste.Directory) :
    """
        A Folder is a filesystem Directory that contains any number of other Folders and Images.
    """

    def __init__ (self, node) :
        super(Folder, self).__init__(node)

        # info
        self.title = None
        self.description = None

    @lazy_load
    def preview_dir (self) :
        """
            Load and return the Directory for previews
        """
        
        return self.subdir(self.config.preview_dir, create=True)
    
    @lazy_load
    def thumb_dir (self) :
        """
            Load and return the Directory for thumbs
        """
        
        return self.subdir(self.config.thumb_dir, create=True)
   
    @lazy_load_iter
    def subnodes (self) :
        """
            Load and return an ordered list of child-Nodes
        """

        return super(Folder, self).subnodes(skip_dotfiles=True, sorted=True)
    
    @lazy_load_iter
    def subfolders (self) :
        """
            Load and return an ordered list of sub-Folders
        """

        return (Folder(node) for node in self.subnodes if isinstance(node, filesystem.Directory))

    @lazy_load_iter
    def images (self) :
        """
            Load and return an ordered/linked list of sub-Images
        """

        prev = None

        for node in self.subnodes :
            # skip non-relevant ones
            # XXX: node should need to be a File
            if not isinstance(node, filesystem.File) or not self.config.is_image(node) :
                continue
            
            # create new
            img = image.Image(node, prev)

            # link up
            if prev :
                prev.next = img

                # yield the linked-up prev
                yield prev
            
            # continue
            prev = img
        
        # and the last one
        if prev :
            yield prev

    @property
    def page_count (self) :
        """
            Returns the number of pages needed to show this folder's images
        """

        return math.ceil(len(self.images) / float(self.config.images_per_page))

    def images_for_page (self, page) :
        """
            Returns the list of Images to be displayed for the given page, if any
        """
        
        # offset to first image
        offset = page * self.config.images_per_page
        
        # slice
        return self.images[offset : offset + self.config.images_per_page]

    def html_file (self, page=0) :
        """
            Returns the File representing the .html for the given page
        """
        
        if page :
            return self.subfile("index_%d.html" % page)
        
        else :
            return self.subfile("index.html")
    
    def index (self) :
        """
            Recursively index this Folder, yielding a series of all Folder objects inside.
        """
        
        # and subfolders
        for subfolder in self.subfolders :
            yield subfolder

            for item in subfolder.index_subfolders() :
                yield item
   
class Folder (object) :
    def __init__ (self, name='.', parent=None) :
        # the directory name, no trailing /
        self.name = unicode(name.rstrip(os.sep))

        # our parent Folder, or None
        self.parent = parent

        # the path to this dir, as a relative path to the root of the image gallery, always starts with .
        if parent and name :
            self.path = parent.pathFor(self.name)
        else :
            self.path = self.name

        # the url-path to the index.html file
        self.html_path = self.path
        
        # dict of fname -> Folder
        self.subdirs = {}

        # dict of fname -> Image
        self.images = {}
        
        # our human-friendly title
        self.title = None

        # our long-winded description
        self.descr = ''

        # is this folder non-empty?
        self.alive = None
        
        # self.images.values(), but sorted by filename
        self.sorted_images = []
        
        # the ShortURL key to this dir
        self.shorturl_code = None

        # were we filtered out?
        self.filtered = False
   
    def pathFor (self, *fnames) :
        """
            Return a root-relative path to the given path inside this dir
        """
        return os.path.join(self.path, *fnames)

    def index (self, filters=None) :
        """
            Look for other dirs and images inside this dir. Filters must be either None,
            whereupon all files will be included, or a dict of {filename -> next_filter}.
            If given, only filenames that are present in the dict will be indexed, and in
            the case of dirs, the next_filter will be passed on to that Folder's index
            method.
        """

        if filters :
            self.filtered = True
        
        # iterate through listdir
        for fname in os.listdir(self.path) :
            # the full filesystem path to it
            fpath = self.pathFor(fname)
            
            # ignore dotfiles
            if fname.startswith('.') :
                log.debug("Skipping dotfile %s", fname)
                continue
            
            # apply filters
            if filters :
                if fname in filters :
                    next_filter = filters[fname]
                else :
                    log.debug("Skip `%s' as we have a filter", fname)
                    continue
            else :
                next_filter = None
                
            # recurse into subdirs, but not thumbs/previews
            if (os.path.isdir(fpath) 
                and (fname not in (settings.THUMB_DIR, settings.PREVIEW_DIR))
                and (self.parent or fname not in settings.ROOT_IGNORE)
            ) :
                log.down(fname)

                f = Folder(fname, self)
                
                try :
                    if f.index(next_filter) :   # recursion
                        # if a subdir is alive, we are alive as well
                        self.subdirs[fname] = f
                        self.alive = True
                except Exception, e :
                    log.warning("skip - %s: %s" % (type(e), e))

                log.up()

            # handle images
            elif os.path.isfile(fpath) and utils.isImage(fname) :
                log.next(fname)
                self.images[fname] = image.Image(self, fname)

            # ignore everything else
            else :
                log.debug("Ignoring file %s", fname)
        
        # sort and link the images
        if self.images :
            self.alive = True

            # sort the images
            fnames = self.images.keys()
            fnames.sort()

            prev = None

            # link
            for fname in fnames :
                img = self.images[fname]

                img.prev = prev

                if prev :
                    prev.next = img

                prev = img
                
                # add to the sorted images list
                self.sorted_images.append(img)
                
        # figure out our title/ descr. Must be done before our parent dir is rendered (self.title)
        title_path = self.pathFor(settings.TITLE_FILE)
        
        self.title, self.descr = utils.readTitleDescr(title_path)
        
        # default title for the root dir
        if self.title or self.descr :
            self.alive = True
            pass # use what was in the title file
            
        elif not self.parent :
            self.title = 'Index'

        else :
            self.title = self.name
        
        if not self.alive :
            log.debug("Dir %s isn't alive" % self.path)

        return self.alive

    def getObjInfo (self) :
        """
            Metadata for shorturls2.db
        """
        return 'dir', self.path, ''

    def breadcrumb (self, forImg=None) :
        """
            Returns a [(fname, title)] list of this dir's parent dirs
        """

        f = self
        b = []
        d = 0
        
        while f :
            # functionality of the slightly-hacked-in variety
            if f is self and forImg is not None :
                url = helpers.url_for_page(self.getPageNumber(forImg))
            else :
                url = dirUp(d)
                
            b.insert(0, (url, f.title))

            d += 1
            f = f.parent
        
        return b
        
    def getPageNumber (self, img) :
        """
            Get the page number that the given image is on
        """
        
        return self.sorted_images.index(img) // settings.IMAGE_COUNT

    def countParents (self, acc=0) :
        if self.parent :
            return self.parent.countParents(acc+1)
        else :
            return acc
    
    def inRoot (self, *fnames) :
        """
            Return a relative URL from this dir to the given path in the root dir
        """

        c = self.countParents()

        return utils.url_join(*((['..']*c) + list(fnames)))

    def render (self) :
        """
            Render the index.html, Images, and recurse into subdirs
        """
        
        # ded folders are skipped
        if not self.alive :
            # dead, skip, no output
            return
        
        index_mtime = utils.mtime(self.pathFor("index.html"))
        dir_mtime = utils.mtime(self.path)

        # if this dir's contents were filtered out, then we can't render the index.html, as we aren't aware of all the images in here
        if self.filtered :
            log.warning("Dir `%s' contents were filtered, so we won't render the gallery index again", self.path)

        elif index_mtime > dir_mtime :
            # no changes, pass, ignored
            pass

        else :  
            # create the thumb/preview dirs if needed
            for dir in (settings.THUMB_DIR, settings.PREVIEW_DIR) :
                path = self.pathFor(dir)

                if not os.path.isdir(path) :
                    log.info("mkdir %s", dir)
                    os.mkdir(path)

            # sort the subdirs
            subdirs = self.subdirs.values()
            subdirs.sort(key=lambda d: d.name)
            
            # paginate!
            images = self.sorted_images
            image_count = len(images)
            pages = []
            
            while images :
                pages.append(images[:settings.IMAGE_COUNT])
                images = images[settings.IMAGE_COUNT:]

            pagination_required = len(pages) > 1

            if pagination_required :
                log.info("%d pages @ %d images", len(pages), settings.IMAGE_COUNT)
            elif not pages :
                log.info("no images, render for subdirs")
                pages = [[]]

            for cur_page, images in enumerate(pages) :
                if pagination_required and cur_page > 0 :
                    shorturl = "%s/%s" % (self.shorturl_code, cur_page+1)
                else :
                    shorturl = self.shorturl_code
                
                # render to index.html
                gallery_tpl.render_to(self.pathFor(url_for_page(cur_page)), 
                    stylesheet_url               = self.inRoot('style.css'),
                    title                        = self.title,
                    breadcrumb                   = self.breadcrumb(),
                    
                    dirs                         = subdirs,
                    images                       = images,
                    
                    num_pages                    = len(pages),
                    cur_page                     = cur_page,
                    
                    description                  = self.descr,
                    
                    shorturl                     = self.inRoot('s', shorturl),
                    shorturl_code                = shorturl,
                )

        # render images
        image_count = len(self.sorted_images)
        for i, img in enumerate(self.images.itervalues()) :
            log.next("[%-4d/%4d] %s", i + 1, image_count, img.name)

            img.render()
        
        # recurse into subdirs
        for dir in self.subdirs.itervalues() :
            log.down(dir.name)

            dir.render()

            log.up()