from __future__ import absolute_import
from collections import Counter
import datetime
import os.path
import shutil
from .config import Config
from .exceptions import PostyError
from .page import Page
from .post import Post
from posty.renderer import (
HtmlRenderer,
JsonRenderer,
RssRenderer,
AtomRenderer,
Posty1RedirectRenderer
)
from .util import slugify
[docs]class Site(object):
"""
Representation of an entire site with posts and pages. This is the main
class that conrols everything.
:param site_path:
Path to the directory containing site content (pages, posts, templates)
:param config_path:
Path to the config file, defaults to ``$SITE_PATH/config.yml``
"""
def __init__(self, site_path='.', config_path=None):
self.site_path = site_path
if config_path:
self.config_path = config_path
else:
self.config_path = os.path.join(site_path, 'config.yml')
self._config = None
self.payload = {
'pages': [],
'posts': [],
'tags': [],
}
self.loaded = False
@property
def config(self):
"""
Returns this site's config as read from the config file
"""
if not self._config:
config_path = os.path.join(self.config_path)
self._config = Config(config_path)
self._config.load()
return self._config
[docs] def init(self):
"""
Initialize a new Posty site at the given path
"""
skel_path = os.path.join(os.path.dirname(__file__), 'skel')
for thing in os.listdir(skel_path):
src = os.path.join(skel_path, thing)
dst = os.path.join(self.site_path, thing)
if os.path.exists(dst):
print("{} already exists, not overwriting".format(thing))
else:
if os.path.isdir(src):
shutil.copytree(src, dst)
elif os.path.isfile(src):
shutil.copy(src, dst)
[docs] def load(self):
"""
Load the site from files on disk into our internal representation
"""
# Include the whole config
# TODO: deprecate the previous items
self.payload['config'] = dict(self.config)
self._load_pages()
self._load_posts()
self.payload['copyright'] = self.copyright
self.loaded = True
[docs] def render(self, output_path='build'):
"""
Render the site with the various renderers
* HTML
* JSON
* RSS (if ``feeds.rss`` is True in the config)
* Atom (if ``feeds.atom`` is True in the config)
"""
HtmlRenderer(self, output_path=output_path).render_site()
JsonRenderer(self, output_path=output_path).render_site()
if self.config['feeds']['rss']:
RssRenderer(self, output_path=output_path).render_site()
if self.config['feeds']['atom']:
AtomRenderer(self, output_path=output_path).render_site()
if self.config['compat']['redirect_posty1_urls']:
Posty1RedirectRenderer(self, output_path=output_path).render_site()
def _load_pages(self):
pages = []
page_dir = os.path.join(self.site_path, 'pages')
for filename in os.listdir(page_dir):
contents = open(os.path.join(page_dir, filename)).read()
pages.append(Page.from_yaml(contents, config=self._config))
self.payload['pages'] = sorted(pages, key=lambda x: x['title'].lower())
def _load_posts(self):
posts = []
tags = []
# Load each post
post_dir = os.path.join(self.site_path, 'posts')
for filename in os.listdir(post_dir):
contents = open(os.path.join(post_dir, filename)).read()
post = Post.from_yaml(contents, config=self._config)
posts.append(post)
tags.extend(post['tags'])
self.payload['posts'] = sorted(posts, key=lambda x: x['date'],
reverse=True)
# uniquify tags and sort by frequency (descending)
self.payload['tags'] = [t for t, c in Counter(tags).most_common()]
[docs] def post(self, slug):
"""
Returns a Post object by its slug
:param slug:
slug of the post to find
:returns:
A post dict
:raises PostyError:
if no post could be found
"""
for post in self.payload['posts']:
post_slug = post['slug'] or slugify(post['title'])
if slug == post_slug:
return post
else:
raise PostyError(
'Unable to find post {}. Available posts: {}'.format(
slug,
[slugify(p['title']) for p in self.payload['pages']]
)
)
[docs] def page(self, slug):
"""
Returns a Page object by its slug
:param slug:
slug of the page to find
:returns:
A page dict
:raises PostyError:
if no page could be found
"""
for page in self.payload['pages']:
page_slug = page.get('slug') or slug == slugify(page['title'])
if slug == page_slug:
return page
else:
raise PostyError(
'Unable to find post {}. Available posts: {}'.format(
slug,
[p.get('slug') or slugify(p['title'])
for p in self.payload['pages']]
)
)
@property
def copyright(self):
"""
Returns a string of the copyright info, based on the configured author
and the years of the first and last post
"""
first_post = self.payload['posts'][-1]
last_post = self.payload['posts'][0]
copyright = 'Copyright {start} - {end}, {author}'.format(
author=self.config['author'],
start=first_post['date'].year,
end=last_post['date'].year
)
return copyright
[docs] def new_post(self, name="New Post"):
"""
Create a new post in the site directory from the skeleton post
"""
post_dir = os.path.join(self.site_path, 'posts')
if not os.path.exists(post_dir):
raise PostyError('You must initialize the site first')
date = datetime.date.today()
filename = '{}_{}.yaml'.format(date, slugify(name))
post_path = os.path.join(post_dir, filename)
skel_path = os.path.join(os.path.dirname(__file__),
'skel/posts/1970-01-01_new-post.yaml')
post = Post.from_yaml(open(skel_path).read(), config=self.config)
post['title'] = name
post['date'] = date
with open(post_path, 'w') as output_file:
output_file.write(post.to_yaml())
[docs] def new_page(self, name="New Page"):
"""
Create a new page in the site directory from the skeleton page
"""
page_dir = os.path.join(self.site_path, 'pages')
if not os.path.exists(page_dir):
raise PostyError('You must initialize the site first')
filename = '{}.yaml'.format(slugify(name))
page_path = os.path.join(page_dir, filename)
skel_path = os.path.join(os.path.dirname(__file__),
'skel/pages/new-page.yaml')
page = Page.from_yaml(open(skel_path).read(), config=self.config)
page['title'] = name
with open(page_path, 'w') as output_file:
output_file.write(page.to_yaml())