Загрузка глобальных переменных [closes #29]

Загрузка меню по-умолчанию через события [closes #27]
master
Антон Касимов 2019-05-21 20:31:11 +03:00
parent 95c18e3e3f
commit 9e5af50a4b
19 changed files with 132 additions and 112 deletions

View File

@ -5,26 +5,26 @@ from pyramid.path import AssetResolver
from watchdog.observers import Observer from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler from watchdog.events import FileSystemEventHandler
from flatfilecms.resources import Root from flatfilecms.resources import Root, root_factory, roots
class PagesEventHandler(FileSystemEventHandler): class PagesEventHandler(FileSystemEventHandler):
def __init__(self, root, root_path): def __init__(self, root_path):
super(PagesEventHandler, self).__init__() super(PagesEventHandler, self).__init__()
self.root = root
self.root_path = root_path self.root_path = root_path
self.abspath = AssetResolver().resolve(root_path).abspath()
def find_dir(self, path): def find_dir(self, path):
folder = self.root folder = roots.setdefault(self.root_path, Root(self.root_path))
path = path.relative_to(self.root_path) path = path.relative_to(self.abspath)
for name in path.parts[:-1]: for name in path.parts[:-1]:
folder = folder[name] folder = folder[name]
return folder return folder
def on_created(self, event): def on_created(self, event):
super(PagesEventHandler, self).on_created(event) super(PagesEventHandler, self).on_created(event)
path = Path(event.dest_path if hasattr(event, 'dest_path') path = Path(
else event.src_path) event.dest_path if hasattr(event, 'dest_path') else event.src_path)
folder = self.find_dir(path) folder = self.find_dir(path)
if event.is_directory: if event.is_directory:
folder.create_dir(path) folder.create_dir(path)
@ -51,30 +51,24 @@ def main(global_config, **settings):
""" This function returns a Pyramid WSGI application. """ This function returns a Pyramid WSGI application.
""" """
from pyramid.settings import asbool from pyramid.settings import asbool
watchdog = asbool(settings.get( watchdog = asbool(settings.get('watchdog', 'false'))
'watchdog', 'false'))
settings['watchdog'] = watchdog settings['watchdog'] = watchdog
settings.setdefault('pages_path', 'pages') settings.setdefault('pages_path', 'pages')
settings.setdefault('data_path', 'data') settings.setdefault('data_path', 'data')
default_jinja2_filters = """markdown = flatfilecms.filters:markdown2html
fileglob = flatfilecms.filters:fileglob
merge_dict = flatfilecms.filters.merge_dict
join_url = flatfilecms.filters.join_url"""
settings['jinja2.filters'] = default_jinja2_filters + settings.get(
'jinja2.filters', '')
pages = Root(settings['pages_path'], settings['data_path']) config = Configurator(settings=settings, root_factory=root_factory)
def factory(request):
return pages
config = Configurator(settings=settings,
root_factory=factory)
config.include('pyramid_jinja2') config.include('pyramid_jinja2')
config.include('.routes') config.include('.routes')
config.scan() config.scan()
if watchdog: if watchdog:
path = AssetResolver().resolve(
settings['pages_path']).abspath()
observer = Observer() observer = Observer()
event_handler = PagesEventHandler(pages, path) event_handler = PagesEventHandler(settings['pages_path'])
observer.schedule( observer.schedule(event_handler, event_handler.abspath, recursive=True)
event_handler,
path,
recursive=True)
observer.start() observer.start()
return config.make_wsgi_app() return config.make_wsgi_app()

View File

@ -4,6 +4,8 @@ from datetime import date
from pyramid.events import subscriber from pyramid.events import subscriber
from pyramid.events import BeforeRender from pyramid.events import BeforeRender
from .models import load_yaml
@subscriber(BeforeRender) @subscriber(BeforeRender)
def add_global(event): def add_global(event):
@ -13,3 +15,19 @@ def add_global(event):
'format_datetime': dates.format_datetime, 'format_datetime': dates.format_datetime,
'today': date.today(), 'today': date.today(),
} }
@subscriber(BeforeRender)
def add_data(event):
try:
event['data'] = load_yaml('globals.yaml', True)
except FileNotFoundError:
event['data'] = {}
@subscriber(BeforeRender)
def add_menu(event):
try:
event['menu'] = load_yaml('menu/default.yaml', True)
except FileNotFoundError:
event['menu'] = []

View File

@ -27,19 +27,17 @@ class Jinja2Processor(BlockProcessor):
class IgnoreExtension(Extension): class IgnoreExtension(Extension):
def extendMarkdown(self, md, md_globals): def extendMarkdown(self, md, md_globals):
md.parser.blockprocessors.add( md.parser.blockprocessors.add('jinja2', Jinja2Processor(md.parser),
'jinja2', ">hashheader")
Jinja2Processor(md.parser),
">hashheader")
@contextfilter @contextfilter
def markdown2html(context, text, render=True): def markdown2html(context, text, render=True):
result = markdown.markdown( result = markdown.markdown(
text, text,
extensions=[IgnoreExtension(), 'markdown.extensions.extra'], extensions=[IgnoreExtension(), 'markdown.extensions.extra'],
output_format='html5', output_format='html5',
tab_length=2) tab_length=2)
if render: if render:
result = context.environment.from_string(result).render(context) result = context.environment.from_string(result).render(context)
if context.eval_ctx.autoescape: if context.eval_ctx.autoescape:
@ -55,3 +53,7 @@ def merge_dict(a, b):
def fileglob(path, root): def fileglob(path, root):
return [p.relative_to(root) for p in Path(root).glob(path)] return [p.relative_to(root) for p in Path(root).glob(path)]
def join_url(a, b):
return a.rstrip('/') + '/' + b.lstrip('/')

View File

@ -5,6 +5,7 @@ import markdown
from datetime import datetime from datetime import datetime
from pyramid.path import AssetResolver from pyramid.path import AssetResolver
from zope.interface import (Interface, implementer) from zope.interface import (Interface, implementer)
from pyramid.threadlocal import get_current_registry
from flatfilecms.filters import IgnoreExtension from flatfilecms.filters import IgnoreExtension
@ -67,6 +68,27 @@ class CustomYAMLHandler(frontmatter.YAMLHandler):
return yaml.load(fm, self.loader) return yaml.load(fm, self.loader)
def load_yaml(path, data=False):
registry = get_current_registry()
settings = registry.settings
data_path = settings['data_path']
if data:
path = str(PurePath(data_path) / path)
return yaml.load(
AssetResolver().resolve(path).stream(),
LoaderFactory(data_path))
def load_frontmatter(path):
registry = get_current_registry()
settings = registry.settings
data_path = settings['data_path']
return frontmatter.load(
AssetResolver().resolve(path).stream(),
handler=CustomYAMLHandler(data_path),
).to_dict()
class HiddenFile(Exception): class HiddenFile(Exception):
pass pass
@ -76,8 +98,6 @@ class Folder(dict):
self.path = path self.path = path
self.name = name self.name = name
self.__parent__ = parent self.__parent__ = parent
if parent is not None:
self.data_path = parent.data_path
for entry in AssetResolver().resolve(path).listdir(): for entry in AssetResolver().resolve(path).listdir():
asset = f"{path}/{entry}" asset = f"{path}/{entry}"
if AssetResolver().resolve(asset).isdir(): if AssetResolver().resolve(asset).isdir():
@ -124,7 +144,6 @@ class Document(object):
self.name = name self.name = name
self.__parent__ = parent self.__parent__ = parent
self.path = path self.path = path
self.data_path = parent.data_path
class IMarkdown(Interface): class IMarkdown(Interface):
@ -135,25 +154,19 @@ class IMarkdown(Interface):
class Markdown(Document): class Markdown(Document):
def __init__(self, name, parent, path): def __init__(self, name, parent, path):
super(Markdown, self).__init__('', parent, path) super(Markdown, self).__init__('', parent, path)
self.page = frontmatter.load( self.page = load_frontmatter(path)
AssetResolver().resolve(self.path).stream(),
handler=CustomYAMLHandler(self.data_path),
).to_dict()
if 'published' in self.page and \ if 'published' in self.page and \
str(datetime.now()) < str(self.page['published']): self.page['published'] > datetime.now():
raise HiddenFile raise HiddenFile
@implementer(IMarkdown) @implementer(IMarkdown)
class YAML(Document): class YAML(Document):
def __init__(self, name, parent, path): def __init__(self, name, parent, path):
super(YAML, self).__init__('', parent, path) super(YAML, self).__init__('', parent, path)
self.page = yaml.load( self.page = load_yaml(path)
AssetResolver().resolve(self.path).stream(),
LoaderFactory(self.data_path))
if 'published' in self.page and \ if 'published' in self.page and \
str(datetime.now()) < str(self.page['published']): self.page['published'] > datetime.now():
raise HiddenFile raise HiddenFile

View File

@ -1,23 +1,14 @@
from flatfilecms.models import Folder from flatfilecms.models import Folder
def flat(d, path):
structure = []
for k, v in d.items():
if isinstance(v, dict):
structure.extend(flat(v, f"{path}/{k}"))
else:
if k == 'index':
structure.append(f"{path}/")
else:
structure.append(f"{path}/{k}")
return structure
class Root(Folder): class Root(Folder):
def __init__(self, path, data_path): def __init__(self, path):
self.data_path = data_path
super(Root, self).__init__('', None, path) super(Root, self).__init__('', None, path)
def structure(self, base_dir=''):
return flat(self, base_dir) roots = {}
def root_factory(request):
path = request.registry.settings['pages_path']
return roots.setdefault(path, Root(path))

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="{{data.base_url}}"> <feed xmlns="http://www.w3.org/2005/Atom">
<title type="text">{{title}}</title> <title type="text">{{title}}</title>
{%- if subtitle %} {%- if subtitle %}
<subtitle type="html">{{subtitle}}</subtitle> <subtitle type="html">{{subtitle}}</subtitle>
{%- endif %} {%- endif %}
<link rel="self" href="{{request.path_qs}}"/> <link rel="self" href="{{data.base_url|join_url(request.path_qs)}}"/>
{%- if 'links' in feed and feed['index'] %} {%- if 'links' in feed and feed['index'] %}
<link rel="alternate" type="text/html" hreflang="{{request.locale_name}}" href="{{feed['links']['index']}}"/> <link rel="alternate" type="text/html" hreflang="{{request.locale_name}}" href="{{data.base_url|join_url(feed['links']['index'])}}"/>
{%- endif %} {%- endif %}
<updated>{{(pages[0][1].page['updated'] or pages[0][1].page['published']).isoformat()}}+00:00</updated> <updated>{{(pages[0][1].page['updated'] or pages[0][1].page['published']).isoformat()}}+00:00</updated>
<id>urn:uuid:{{feed['id']}}</id> <id>urn:uuid:{{feed['id']}}</id>
@ -20,7 +20,7 @@
{%- for item in pages %} {%- for item in pages %}
<entry> <entry>
<title>{{item[1].page['title']}}</title> <title>{{item[1].page['title']}}</title>
<link href="{{item[0]}}"/> <link href="{{data.base_url|join_url(item[0])}}"/>
<id>urn:uuid:{{item[1].page['id']}}</id> <id>urn:uuid:{{item[1].page['id']}}</id>
<updated>{{(item[1].page['updated'] or item[1].page['published']).isoformat()}}+00:00</updated> <updated>{{(item[1].page['updated'] or item[1].page['published']).isoformat()}}+00:00</updated>
<published>{{item[1].page['published'].isoformat()}}+00:00</published> <published>{{item[1].page['published'].isoformat()}}+00:00</published>

View File

@ -2,7 +2,7 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{%- for item, lastmod in pages %} {%- for item, lastmod in pages %}
<url> <url>
<loc>{{data.base_url}}/{{item}}</loc> <loc>{{data['base_url']|join_url(item)}}</loc>
{%- if lastmod %} {%- if lastmod %}
<lastmod>{{lastmod}}</lastmod> <lastmod>{{lastmod}}</lastmod>
{%- endif %} {%- endif %}

View File

@ -1,9 +1,5 @@
import re import re
import yaml
from pyramid.renderers import render_to_response from pyramid.renderers import render_to_response
from pyramid.path import AssetResolver
from pathlib import PurePath
from flatfilecms.models import LoaderFactory
from pyramid.httpexceptions import HTTPFound from pyramid.httpexceptions import HTTPFound
from pyramid.traversal import (find_root, find_resource) from pyramid.traversal import (find_root, find_resource)
@ -30,12 +26,6 @@ def blog(self, options={}):
post = self.context.page.copy() post = self.context.page.copy()
post['pages'] = sorted( post['pages'] = sorted(
postlist, key=lambda t: t[1].page['published'], reverse=True) postlist, key=lambda t: t[1].page['published'], reverse=True)
if 'menu' not in post:
post['menu'] = yaml.load(
AssetResolver().resolve(
str(PurePath(self.context.data_path) /
'menu/default.yaml')).stream(),
LoaderFactory(self.context.data_path))
if format == 'atom': if format == 'atom':
set_content_type(self, 'application/atom+xml') set_content_type(self, 'application/atom+xml')
return render_to_response( return render_to_response(

View File

@ -6,11 +6,8 @@ from pyramid.renderers import render_to_response
from pyramid.httpexceptions import HTTPNotFound from pyramid.httpexceptions import HTTPNotFound
from pyramid.response import FileResponse from pyramid.response import FileResponse
from pyramid.path import (DottedNameResolver, AssetResolver) from pyramid.path import (DottedNameResolver, AssetResolver)
from pathlib import PurePath
import yaml
from flatfilecms.models import (Folder, Document, IMarkdown, Jinja2, from flatfilecms.models import (Folder, Document, IMarkdown, Jinja2)
LoaderFactory)
class PagesView: class PagesView:
@ -55,13 +52,6 @@ class PagesView:
if response: if response:
return response return response
post = self.context.page post = self.context.page
if 'menu' not in post:
post['menu'] = yaml.load(
AssetResolver().resolve(
str(
PurePath(self.context.data_path) /
'menu/default.yaml')).stream(),
LoaderFactory(self.context.data_path))
return render_to_response( return render_to_response(
'{0}.jinja2'.format(post.get('template', 'default')), '{0}.jinja2'.format(post.get('template', 'default')),
post, post,

View File

@ -1,9 +1,27 @@
import pytest import pytest
@pytest.fixture
def current_directory():
from pathlib import Path
return Path(__file__).parent
@pytest.fixture
def config(current_directory):
from pyramid.testing import testConfig
settings = {
'data_path': current_directory / 'data',
'jinja2.filters': 'join_url = flatfilecms.filters.join_url'
}
with testConfig(settings=settings) as config:
config.include('pyramid_jinja2')
config.add_jinja2_search_path('tests:templates')
config.add_jinja2_search_path('flatfilecms:templates')
yield config
@pytest.fixture @pytest.fixture
def fake_request(): def fake_request():
from pyramid.testing import DummyRequest from pyramid.testing import DummyRequest
return DummyRequest() return DummyRequest()

1
tests/data/globals.yaml Normal file
View File

@ -0,0 +1 @@
base_url: https://flatfilecms.test/

View File

@ -2,7 +2,7 @@
title: Блоговая запись 1 в формате markdown title: Блоговая запись 1 в формате markdown
description: Тест записей блогов description: Тест записей блогов
template: blogpost template: blogpost
published: 2019-02-01 published: 2019-02-01T12:00:00+00:00
--- ---
Hello World! Hello World!

View File

@ -1,4 +1,4 @@
title: Блоговая запись 2 в формате YAML title: Блоговая запись 2 в формате YAML
template: blogpost template: blogpost
published: 2019-02-02 published: 2019-02-02T11:00:00+3:00
content: Hello World! 2 content: Hello World! 2

View File

@ -2,7 +2,7 @@
title: Блоговая запись 1 в формате markdown title: Блоговая запись 1 в формате markdown
description: Тест записей блогов description: Тест записей блогов
template: blogpost template: blogpost
published: 3019-02-01 published: 3019-02-01T23:59:59+0:00
--- ---
Hello World! Hello World!

2
tests/pages/menu.yaml Normal file
View File

@ -0,0 +1,2 @@
template: tests:templates/test_menu
menu: 'Menu - OK'

View File

@ -3,4 +3,4 @@ view:
ignore: ignore:
- blog/.*$ - blog/.*$
data: data:
base_url: https://flatfilecms.test base_url: https://flatfilecms.test/

View File

@ -0,0 +1 @@
{{menu}}

View File

@ -5,3 +5,11 @@ def test_globals(fake_request):
assert 'globals' in event assert 'globals' in event
assert 'format_date' in event['globals'] assert 'format_date' in event['globals']
assert 'format_datetime' in event['globals'] assert 'format_datetime' in event['globals']
def test_data(config):
from flatfilecms.events import add_data
event = {}
add_data(event)
assert 'data' in event
assert event['data']['base_url'] == 'https://flatfilecms.test/'

View File

@ -2,27 +2,10 @@ import pytest
@pytest.fixture @pytest.fixture
def current_directory(): def pages(current_directory, config):
from pathlib import Path
return Path(__file__).parent
@pytest.fixture
def pages(current_directory):
from flatfilecms.resources import Root from flatfilecms.resources import Root
return Root( return Root(
str(current_directory / 'pages'), str(current_directory / 'pages'))
str(current_directory / 'data'))
@pytest.fixture
def config(current_directory):
from pyramid.testing import testConfig
with testConfig() as config:
config.include('pyramid_jinja2')
config.add_jinja2_search_path('tests:templates')
config.add_jinja2_search_path('flatfilecms:templates')
yield config
def test_loading(pages): def test_loading(pages):
@ -83,5 +66,14 @@ def test_sitemap(pages, fake_request, config):
<url> <url>
<loc>https://flatfilecms.test/view-with-options</loc> <loc>https://flatfilecms.test/view-with-options</loc>
</url> </url>
<url>
<loc>https://flatfilecms.test/menu</loc>
</url>
</urlset>""" </urlset>"""
assert response.content_type == 'text/xml' assert response.content_type == 'text/xml'
def test_menu(pages, fake_request, config):
from flatfilecms.views.pages import PagesView
response = PagesView(pages['menu'], fake_request).process_yaml()
assert response.text == 'Menu - OK'