commit d524359250f3301e18b1552fd8c1a41de82eb773 Author: Антон Касимов Date: Mon Oct 15 01:43:11 2018 +0300 Начальная редакция diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..7abd490 --- /dev/null +++ b/.hgignore @@ -0,0 +1,49 @@ +syntax: glob + +.gitignore +node_modules +.c9revisions +.DS_Store +Thumbs.db + +*.pyc +*.pyo +*.pyd + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +include +man +__pycache__ +public + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +# Translations +*.mo + +cms/webpack + +# Ignore IDEA files +.idea diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..14b902f --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2861c2d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include cms *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..d8f86aa --- /dev/null +++ b/README.txt @@ -0,0 +1,12 @@ +Radium Flat-file CMS +========== + +Getting Started +--------------- + +- Install flatfilecms + + pip install flatfilecms + +- Use flatfilecms:main as a WSGI application + diff --git a/flatfilecms/__init__.py b/flatfilecms/__init__.py new file mode 100644 index 0000000..5bba17f --- /dev/null +++ b/flatfilecms/__init__.py @@ -0,0 +1,80 @@ +from pyramid.config import Configurator +from pyramid.path import AssetResolver +from .resources import Root + +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler + +from pathlib import Path + + +class PagesEventHandler(FileSystemEventHandler): + def __init__(self, root, root_path): + super(PagesEventHandler, self).__init__() + self.root = root + self.root_path = root_path + + def find_dir(self, path): + folder = self.root + path = path.relative_to(self.root_path) + for name in path.parts[:-1]: + folder = folder[name] + return folder + + def on_created(self, event): + super(PagesEventHandler, self).on_created(event) + path = Path(event.dest_path if hasattr(event, 'dest_path') + else event.src_path) + folder = self.find_dir(path) + if event.is_directory: + folder.create_dir(path) + else: + folder.create_file(path) + + def on_deleted(self, event): + super(PagesEventHandler, self).on_deleted(event) + path = Path(event.src_path) + folder = self.find_dir(path) + name = path.stem + if path.suffix not in ['.md', '.yaml', '.j2', '.jinja2'] \ + and path.name != 'index.html': + name = path.name + del folder[name] + + def on_moved(self, event): + super(PagesEventHandler, self).on_moved(event) + self.on_deleted(event) + self.on_created(event) + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + from pyramid.settings import asbool + watchdog = asbool(settings.get( + 'watchdog', 'false')) + settings['watchdog'] = watchdog + settings.setdefault('pages_dir', 'pages') + settings.setdefault('data_dir', 'data') + + pages = Root(settings['pages_dir']) + + def factory(request): + return pages + + config = Configurator(settings=settings, + root_factory=factory) + config.include('pyramid_jinja2') + config.include('.routes') + config.scan() + if watchdog: + path = AssetResolver().resolve( + settings['pages_dir']).abspath() + observer = Observer() + event_handler = PagesEventHandler(pages, path) + observer.schedule( + event_handler, + path, + recursive=True) + observer.start() + return config.make_wsgi_app() diff --git a/flatfilecms/filters.py b/flatfilecms/filters.py new file mode 100644 index 0000000..5fa5685 --- /dev/null +++ b/flatfilecms/filters.py @@ -0,0 +1,57 @@ +from jinja2 import contextfilter, Markup +import markdown +from pathlib import Path +from markdown.extensions import Extension +from markdown.blockprocessors import BlockProcessor + + +class Jinja2Processor(BlockProcessor): + def test(self, parent, block): + return block.startswith('{%') or block.startswith('{{') + + def run(self, parent, blocks): + block = blocks.pop(0) + sibling = self.lastChild(parent) + if sibling is not None: + if sibling.tail: + sibling.tail = sibling.tail + block + else: + sibling.tail = block + else: + if parent.text: + parent.text = parent.text + block + else: + parent.text = block + return True + + +class IgnoreExtension(Extension): + def extendMarkdown(self, md, md_globals): + md.parser.blockprocessors.add( + 'jinja2', + Jinja2Processor(md.parser), + ">hashheader") + + +@contextfilter +def markdown2html(context, text, render=True): + result = markdown.markdown( + text, + extensions=[IgnoreExtension(), 'markdown.extensions.extra'], + output_format='html5', + tab_length=2) + if render: + result = context.environment.from_string(result).render(context) + if context.eval_ctx.autoescape: + result = Markup(result) + return result + + +def merge_dict(a, b): + if isinstance(a, list): + return [merge_dict(i, b) for i in a] + return {**a, **b} + + +def fileglob(path, root): + return [p.relative_to(root) for p in Path(root).glob(path)] diff --git a/flatfilecms/resources.py b/flatfilecms/resources.py new file mode 100644 index 0000000..061bb49 --- /dev/null +++ b/flatfilecms/resources.py @@ -0,0 +1,75 @@ +from pyramid.path import AssetResolver +from pathlib import PurePath + + +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 Folder(dict): + def __init__(self, name, parent, path): + self.__path__ = path + self.__name__ = name + self.__parent__ = parent + for entry in AssetResolver().resolve(path).listdir(): + asset = f"{path}/{entry}" + if AssetResolver().resolve(asset).isdir(): + self.create_dir(asset) + else: + self.create_file(asset) + + def create_file(self, asset): + path = PurePath(asset) + if path.suffix == '.md': + self[path.stem] = Markdown(path.stem, self, asset) + elif path.suffix == '.yaml': + self[path.stem] = YAML(path.stem, self, asset) + elif path.suffix == '.j2' or path.suffix == '.jinja2': + self[path.stem] = Jinja2(path.stem, self, asset) + else: + name = path.name + # Если имя файла не index.html, + # то отдавать по имени файла + if name == 'index.html': + name = 'index' + self[name] = Document(name, self, asset) + + def create_dir(self, asset): + path = PurePath(asset) + self[path.name] = Folder(path.name, self, asset) + + def structure(self, base_dir=''): + return flat(self, base_dir) + + +class Root(Folder): + def __init__(self, path): + super(Root, self).__init__('', None, path) + + +class Document(object): + def __init__(self, name, parent, path): + self.__name__ = name + self.__parent__ = parent + self.__path__ = path + + +class Markdown(Document): + pass + + +class YAML(Document): + pass + + +class Jinja2(Document): + pass diff --git a/flatfilecms/routes.py b/flatfilecms/routes.py new file mode 100644 index 0000000..411eaa8 --- /dev/null +++ b/flatfilecms/routes.py @@ -0,0 +1,7 @@ +def includeme(config): + settings = config.get_settings() + if 'static' in settings: + config.add_static_view( + 'static', + settings['static'], + cache_max_age=3600) diff --git a/flatfilecms/templates/404.jinja2 b/flatfilecms/templates/404.jinja2 new file mode 100644 index 0000000..54f0d22 --- /dev/null +++ b/flatfilecms/templates/404.jinja2 @@ -0,0 +1,5 @@ +{% extends "layout.jinja2" %} + +{% set title="404: страница не найдена!" %} +{% set description="Попробуйте найти нужную вам страницу в меню сайта." %} +{% set image="https://source.unsplash.com/Q1p7bh3SHj8" %} diff --git a/flatfilecms/templates/amp-counter.jinja2 b/flatfilecms/templates/amp-counter.jinja2 new file mode 100644 index 0000000..e69de29 diff --git a/flatfilecms/templates/amp.jinja2 b/flatfilecms/templates/amp.jinja2 new file mode 100644 index 0000000..b9949b5 --- /dev/null +++ b/flatfilecms/templates/amp.jinja2 @@ -0,0 +1,18 @@ + + + + + {{title}} + + + + + + + + {% block body %} + {{content}} + {% endblock %} + {% include 'amp-counter.jinja2' %} + + diff --git a/flatfilecms/templates/base.jinja2 b/flatfilecms/templates/base.jinja2 new file mode 100644 index 0000000..5b2d0f0 --- /dev/null +++ b/flatfilecms/templates/base.jinja2 @@ -0,0 +1,29 @@ +{%- from 'macros.jinja2' import b -%} + + + + {% block head -%} + + + + + + + {{title}} + {% block links %} + {%- endblock %} + + + {%- if json_ld %} + + {%- endif -%} + {% endblock %} + + + {% include 'counter.jinja2' %} + {% block body %} + {% endblock %} + + diff --git a/flatfilecms/templates/bootstrap/fullscreen-jumbotron.jinja2 b/flatfilecms/templates/bootstrap/fullscreen-jumbotron.jinja2 new file mode 100644 index 0000000..88358a9 --- /dev/null +++ b/flatfilecms/templates/bootstrap/fullscreen-jumbotron.jinja2 @@ -0,0 +1,6 @@ +
+
+

{{jumbotron_header}}

+ {{jumbotron_content}} +
+
diff --git a/flatfilecms/templates/counter.jinja2 b/flatfilecms/templates/counter.jinja2 new file mode 100644 index 0000000..e69de29 diff --git a/flatfilecms/templates/default.jinja2 b/flatfilecms/templates/default.jinja2 new file mode 100644 index 0000000..e69de29 diff --git a/flatfilecms/templates/footer.jinja2 b/flatfilecms/templates/footer.jinja2 new file mode 100644 index 0000000..d06179e --- /dev/null +++ b/flatfilecms/templates/footer.jinja2 @@ -0,0 +1,8 @@ + diff --git a/flatfilecms/templates/header.jinja2 b/flatfilecms/templates/header.jinja2 new file mode 100644 index 0000000..79ba08e --- /dev/null +++ b/flatfilecms/templates/header.jinja2 @@ -0,0 +1,60 @@ + + +
+

{{title}}

+ {%- if description %} +

{{description}}

+ {% endif %} +
+ diff --git a/flatfilecms/templates/home.jinja2 b/flatfilecms/templates/home.jinja2 new file mode 100644 index 0000000..47bef0d --- /dev/null +++ b/flatfilecms/templates/home.jinja2 @@ -0,0 +1,5 @@ +{% extends "layout.jinja2" %} + +{% block content %} + {{ contents|safe }} +{% endblock content %} diff --git a/flatfilecms/templates/layout.jinja2 b/flatfilecms/templates/layout.jinja2 new file mode 100644 index 0000000..245b3a8 --- /dev/null +++ b/flatfilecms/templates/layout.jinja2 @@ -0,0 +1,26 @@ +{% extends "base.jinja2" %} +{% block links %} + {% webpack 'layout', '.js' -%} + + {% endwebpack %} + {% webpack 'layout', '.css' -%} + + {% endwebpack %} +{% endblock %} +{% block body %} +
+ {% include "header.jinja2" %} +
+
+
+ {% block content %} + {% if content %} + {{content|markdown}} + {% endif %} + {% endblock %} +
+
+
+ {% include "footer.jinja2" %} +
+{% endblock %} diff --git a/flatfilecms/templates/logo.jinja2 b/flatfilecms/templates/logo.jinja2 new file mode 100644 index 0000000..e69de29 diff --git a/flatfilecms/templates/macros.jinja2 b/flatfilecms/templates/macros.jinja2 new file mode 100644 index 0000000..7b1adcd --- /dev/null +++ b/flatfilecms/templates/macros.jinja2 @@ -0,0 +1,10 @@ +{% macro b(class=None, tag="div", id=None) -%} +<{{ tag }} +{%- if class is not none %} class="{{class}}" {%- endif -%} +{%- if id is not none %} id="{{id}}" {%- endif -%} +> +{%- if caller %} + {{ caller() }} +{% endif -%} + +{%- endmacro %} diff --git a/flatfilecms/templates/onepage.jinja2 b/flatfilecms/templates/onepage.jinja2 new file mode 100644 index 0000000..d14bce4 --- /dev/null +++ b/flatfilecms/templates/onepage.jinja2 @@ -0,0 +1,42 @@ +{% extends "base.jinja2" %} +{% block links %} + {% webpack 'onepage', '.js' -%} + + {% endwebpack %} + {% webpack 'onepage', '.css' -%} + + {% endwebpack %} +{% endblock %} +{% block body %} +
+ +
+
+
+ {% block content %} + {% if content %} + {{content|markdown}} + {% endif %} + {% endblock %} +
+
+
+ {% include "footer.jinja2" %} +
+{% endblock %} diff --git a/flatfilecms/templates/service.jinja2 b/flatfilecms/templates/service.jinja2 new file mode 100644 index 0000000..de74cab --- /dev/null +++ b/flatfilecms/templates/service.jinja2 @@ -0,0 +1 @@ +{% extends "layout.jinja2" %} diff --git a/flatfilecms/templates/sitemap.jinja2 b/flatfilecms/templates/sitemap.jinja2 new file mode 100644 index 0000000..295bc38 --- /dev/null +++ b/flatfilecms/templates/sitemap.jinja2 @@ -0,0 +1,8 @@ + + + {%- for item in request.root.structure() %}{% if item != '/sitemap.xml' and item not in data.ignore %} + + {{data.base_url}}{{item}} + + {%- endif %}{%- endfor %} + diff --git a/flatfilecms/templates/solution.jinja2 b/flatfilecms/templates/solution.jinja2 new file mode 100644 index 0000000..9b1ace1 --- /dev/null +++ b/flatfilecms/templates/solution.jinja2 @@ -0,0 +1,20 @@ +{% extends "layout.jinja2" %} +{% block content %} + {% filter markdown %} + {% if content %} +{{content|safe}} + {% endif %} + {% for name in ['why', 'what', 'who', 'call'] %} + {% if name in data %} +{{("{% call b(tag='section', id='"+name+"') %}")|safe}} + +{{data[name]|safe}} + +{{"{% endcall %}"|safe}} + {% endif %} + {% endfor %} + {% if 'abbr' in data %} +{{data['abbr']|safe}} + {% endif %} + {% endfilter %} +{% endblock %} diff --git a/flatfilecms/views/__init__.py b/flatfilecms/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flatfilecms/views/notfound.py b/flatfilecms/views/notfound.py new file mode 100644 index 0000000..69d6e28 --- /dev/null +++ b/flatfilecms/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/flatfilecms/views/pages.py b/flatfilecms/views/pages.py new file mode 100644 index 0000000..1b31280 --- /dev/null +++ b/flatfilecms/views/pages.py @@ -0,0 +1,120 @@ +from pyramid.view import ( + view_config, + render_view_to_response, + ) +from pyramid.renderers import render_to_response +from pyramid.httpexceptions import (HTTPFound, HTTPNotFound) +from pyramid.response import FileResponse +from pyramid.path import AssetResolver +from pathlib import PurePath +from ..resources import (Folder, Document, Markdown, YAML, Jinja2) +import yaml +import frontmatter + + +class Loader(yaml.Loader): + def __init__(self, stream): + super(Loader, self).__init__(stream) + Loader.add_constructor('!include', Loader.include) + + def include(self, node): + if isinstance(node, yaml.ScalarNode): + return self.extractFile(self.construct_scalar(node)) + + elif isinstance(node, yaml.SequenceNode): + return [self.extractFile(filename) + for filename in self.construct_sequence(node)] + + elif isinstance(node, yaml.MappingNode): + result = {} + for k, v in self.construct_mapping(node).items(): + result[k] = self.extractFile(v) + return result + + else: + raise yaml.constructor.ConstructorError( + "Error:: unrecognised node type in !include statement") + + def extractFile(self, filename): + path = PurePath(self.data_dir) / filename + f = AssetResolver().resolve(str(path)).stream() + if f: + if path.suffix in ['.yaml', '.yml', '.json']: + return yaml.load(f, Loader) + return f.read().decode() + + +def LoaderFactory(data_dir): + cl = Loader + cl.data_dir = data_dir + return cl + + +class CustomYAMLHandler(frontmatter.YAMLHandler): + def __init__(self, data_dir): + self.loader = LoaderFactory(data_dir) + + def load(self, fm, **kwargs): + return yaml.load(fm, self.loader) + + +class PagesView: + def __init__(self, context, request): + self.context = context + self.request = request + + @view_config(context=Folder) + def folder(self): + if 'index' not in self.context: + raise HTTPNotFound + return render_view_to_response(self.context['index'], self.request) + + @view_config(context=Document) + def document(self): + return FileResponse( + AssetResolver().resolve(self.context.__path__).abspath(), + request=self.request) + + def process_yaml(self): + if 'redirect' in self.post: + return HTTPFound(location=self.post.redirect) + if 'menu' not in self.post: + self.post['menu'] = yaml.load( + AssetResolver().resolve( + str(PurePath(self.request.registry.settings['data_dir']) / + 'menu/default.yaml')).stream(), + LoaderFactory(self.request.registry.settings['data_dir'])) + response = render_to_response( + '{0}.jinja2'.format( + self.post.get('template', 'default')), + self.post, + request=self.request + ) + if 'content_type' in self.post: + response.content_type = self.post['content_type'] + return response + + @view_config(context=YAML) + def yaml(self): + self.post = yaml.load( + AssetResolver().resolve( + self.context.__path__).stream(), + LoaderFactory(self.request.registry.settings['data_dir'])) + return self.process_yaml() + + @view_config(context=Markdown) + def markdown(self): + self.post = frontmatter.load( + AssetResolver().resolve( + self.context.__path__).stream(), + handler=CustomYAMLHandler(self.request.registry.settings['data_dir']), + ).to_dict() + return self.process_yaml() + + @view_config(context=Jinja2) + def jinja2(self): + return render_to_response( + self.context.path, + {}, + request=self.request + ) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b5f6496 --- /dev/null +++ b/setup.py @@ -0,0 +1,64 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'plaster_pastedeploy', + 'pyramid >= 1.9a', + 'pyramid_debugtoolbar', + 'pyramid_jinja2', + 'pyramid_retry', + 'pyramid_tm', + 'transaction', + 'waitress', + 'Markdown', + 'PyYAML', + 'python-frontmatter', + 'watchdog < 0.9.0', + 'pyramid-htmlmin', + 'pyramid-webpack', +] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', + 'pytest-cov', +] + +setup( + name='flatfilecms', + version='0.0', + description='Radium CMS', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + 'Programming Language :: Python', + 'Framework :: Pyramid', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + ], + author='Anton Kasimov', + author_email='kasimov@radium-it.ru', + url='http://www.radium.group', + keywords='web pyramid pylons flat-file CMS', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points={ + 'paste.app_factory': [ + 'main = flatfilecms:main', + ], + 'console_scripts': [ + 'generate_static_site = flatfilecms.scripts.generate:main', + ], + }, +)