| @@ -4,11 +4,12 @@ Contains classes and utilities related to sortin | |||||
| resources and nodes in hyde. | resources and nodes in hyde. | ||||
| """ | """ | ||||
| import re | import re | ||||
| from hyde.model import Expando | |||||
| from hyde.plugin import Plugin | from hyde.plugin import Plugin | ||||
| from hyde.site import Node, Resource | from hyde.site import Node, Resource | ||||
| from functools import partial | from functools import partial | ||||
| from itertools import ifilter, izip, tee | |||||
| from itertools import ifilter, izip, tee, product | |||||
| from operator import attrgetter | from operator import attrgetter | ||||
| def pairwalk(iterable): | def pairwalk(iterable): | ||||
| @@ -68,6 +69,29 @@ def add_method(obj, method_name, method_, settings): | |||||
| setattr(obj, method_name, m) | setattr(obj, method_name, m) | ||||
| # class SortGroup(Expando): | |||||
| # """ | |||||
| # A wrapper around sort groups. Understand hierarchical groups | |||||
| # and group metadata. | |||||
| # """ | |||||
| # | |||||
| # def update(self, d): | |||||
| # """ | |||||
| # Updates the Sortgroup with a new grouping | |||||
| # """ | |||||
| # | |||||
| # d = d or {} | |||||
| # if isinstance(d, dict): | |||||
| # for key, value in d.items(): | |||||
| # if key == "groups": | |||||
| # for group in value: | |||||
| # | |||||
| # setattr(self, key, Expando.transform(value)) | |||||
| # elif isinstance(d, Expando): | |||||
| # self.update(d.__dict__) | |||||
| class SorterPlugin(Plugin): | class SorterPlugin(Plugin): | ||||
| """ | """ | ||||
| Sorter plugin for hyde. Adds the ability to do | Sorter plugin for hyde. Adds the ability to do | ||||
| @@ -109,6 +133,7 @@ class SorterPlugin(Plugin): | |||||
| return | return | ||||
| for name, settings in config.sorter.__dict__.items(): | for name, settings in config.sorter.__dict__.items(): | ||||
| self._add_groups(name, settings) | |||||
| self.logger.info("Adding sort methods for [%s]" % name) | self.logger.info("Adding sort methods for [%s]" % name) | ||||
| sort_method_name = 'walk_resources_sorted_by_%s' % name | sort_method_name = 'walk_resources_sorted_by_%s' % name | ||||
| add_method(Node, sort_method_name, sort_method, settings) | add_method(Node, sort_method_name, sort_method, settings) | ||||
| @@ -127,3 +152,30 @@ class SorterPlugin(Plugin): | |||||
| for prev, next in pairwalk(walker()): | for prev, next in pairwalk(walker()): | ||||
| setattr(prev, next_att, next) | setattr(prev, next_att, next) | ||||
| setattr(next, prev_att, prev) | setattr(next, prev_att, prev) | ||||
| def _add_groups(self, sorter_name, settings): | |||||
| """ | |||||
| Checks if the given `settings` contains a `groups` attribute. | |||||
| If it does, it loads the specified groups into the `site` object. | |||||
| """ | |||||
| if not hasattr(settings, 'grouping') or \ | |||||
| not hasattr(settings.grouping, 'groups') or \ | |||||
| not hasattr(settings.grouping, 'name'): | |||||
| return | |||||
| grouping = Expando(settings.grouping) | |||||
| setattr(self.site, sorter_name, grouping) | |||||
| group_dict = dict([(g.name, g) for g in grouping.groups]) | |||||
| for resource in self.site.content.walk_resources(): | |||||
| try: | |||||
| group_value = getattr(resource.meta, grouping.name) | |||||
| except AttributeError: | |||||
| continue | |||||
| if group_value in group_dict: | |||||
| group = group_dict[group_value] | |||||
| if not hasattr(group, 'resources'): | |||||
| group.resources = [] | |||||
| print "Adding resource[%s] to group[%s]" % (resource, group.name) | |||||
| group.resources.append(resource) | |||||
| @@ -31,41 +31,7 @@ class Generator(object): | |||||
| self.template = None | self.template = None | ||||
| Plugin.load_all(site) | Plugin.load_all(site) | ||||
| class PluginProxy(object): | |||||
| """ | |||||
| A proxy class to raise events in registered plugins | |||||
| """ | |||||
| def __init__(self, site): | |||||
| super(PluginProxy, self).__init__() | |||||
| self.site = site | |||||
| def __getattr__(self, method_name): | |||||
| if hasattr(Plugin, method_name): | |||||
| def __call_plugins__(*args): | |||||
| #logger.debug("Calling plugin method [%s]", method_name) | |||||
| res = None | |||||
| if self.site.plugins: | |||||
| for plugin in self.site.plugins: | |||||
| if hasattr(plugin, method_name): | |||||
| #logger.debug( | |||||
| # "\tCalling plugin [%s]", | |||||
| # plugin.__class__.__name__) | |||||
| function = getattr(plugin, method_name) | |||||
| res = function(*args) | |||||
| if res: | |||||
| targs = list(args) | |||||
| last = None | |||||
| if len(targs): | |||||
| last = targs.pop() | |||||
| targs.append(res if res else last) | |||||
| args = tuple(targs) | |||||
| return res | |||||
| return __call_plugins__ | |||||
| raise HydeException( | |||||
| "Unknown plugin method [%s] called." % method_name) | |||||
| self.events = PluginProxy(self.site) | |||||
| self.events = Plugin.get_proxy(self.site) | |||||
| @contextmanager | @contextmanager | ||||
| def context_for_resource(self, resource): | def context_for_resource(self, resource): | ||||
| @@ -1,7 +1,7 @@ | |||||
| {% extends "root.j2" %} | {% extends "root.j2" %} | ||||
| {% block main -%} | {% block main -%} | ||||
| <article> | <article> | ||||
| <hgroup class="article_head"> | |||||
| <hgroup> | |||||
| <h1 class="title">{{ resource.meta.title }}</h1> | <h1 class="title">{{ resource.meta.title }}</h1> | ||||
| <h3 class="subtitle">{{ resource.meta.subtitle }}</h3> | <h3 class="subtitle">{{ resource.meta.subtitle }}</h3> | ||||
| </hgroup> | </hgroup> | ||||
| @@ -23,7 +23,7 @@ | |||||
| compatibility mode is within the first 1K bytes | compatibility mode is within the first 1K bytes | ||||
| code.google.com/p/chromium/issues/detail?id=23003 --> | code.google.com/p/chromium/issues/detail?id=23003 --> | ||||
| <title>{% block title %}{{ resource.meta.title }} / Hyde Docs{% endblock %}</title> | |||||
| <title>{% block title %}{{ resource.meta.title }}{% endblock %}</title> | |||||
| <meta name="description" content="{{ resource.meta.description }}"> | <meta name="description" content="{{ resource.meta.description }}"> | ||||
| <meta name="author" content="{{ resource.meta.author }}"> | <meta name="author" content="{{ resource.meta.author }}"> | ||||
| @@ -8,6 +8,44 @@ from hyde import loader | |||||
| from hyde.util import getLoggerWithNullHandler | from hyde.util import getLoggerWithNullHandler | ||||
| from functools import partial | from functools import partial | ||||
| logger = getLoggerWithNullHandler('hyde.engine') | |||||
| class PluginProxy(object): | |||||
| """ | |||||
| A proxy class to raise events in registered plugins | |||||
| """ | |||||
| def __init__(self, site): | |||||
| super(PluginProxy, self).__init__() | |||||
| self.site = site | |||||
| def __getattr__(self, method_name): | |||||
| if hasattr(Plugin, method_name): | |||||
| def __call_plugins__(*args): | |||||
| # logger.debug("Calling plugin method [%s]", method_name) | |||||
| res = None | |||||
| if self.site.plugins: | |||||
| for plugin in self.site.plugins: | |||||
| if hasattr(plugin, method_name): | |||||
| # logger.debug( | |||||
| # "\tCalling plugin [%s]", | |||||
| # plugin.__class__.__name__) | |||||
| function = getattr(plugin, method_name) | |||||
| res = function(*args) | |||||
| if res: | |||||
| targs = list(args) | |||||
| last = None | |||||
| if len(targs): | |||||
| last = targs.pop() | |||||
| targs.append(res if res else last) | |||||
| args = tuple(targs) | |||||
| return res | |||||
| return __call_plugins__ | |||||
| raise HydeException( | |||||
| "Unknown plugin method [%s] called." % method_name) | |||||
| class Plugin(object): | class Plugin(object): | ||||
| """ | """ | ||||
| The plugin protocol | The plugin protocol | ||||
| @@ -132,6 +170,9 @@ class Plugin(object): | |||||
| """ | """ | ||||
| pass | pass | ||||
| def raise_event(self, event_name): | |||||
| return getattr(Plugin.proxy, event_name)() | |||||
| @staticmethod | @staticmethod | ||||
| def load_all(site): | def load_all(site): | ||||
| """ | """ | ||||
| @@ -140,3 +181,7 @@ class Plugin(object): | |||||
| """ | """ | ||||
| site.plugins = [loader.load_python_object(name)(site) | site.plugins = [loader.load_python_object(name)(site) | ||||
| for name in site.config.plugins] | for name in site.config.plugins] | ||||
| @staticmethod | |||||
| def get_proxy(site): | |||||
| return PluginProxy(site) | |||||
| @@ -29,7 +29,7 @@ class TestLess(object): | |||||
| def test_can_execute_less(self): | def test_can_execute_less(self): | ||||
| s = Site(TEST_SITE) | s = Site(TEST_SITE) | ||||
| s.config.plugins = ['hyde.ext.plugins.less.LessCSSPlugin'] | s.config.plugins = ['hyde.ext.plugins.less.LessCSSPlugin'] | ||||
| s.config.less = Expando(dict(app='/Users/lvfp/local/bin/lessc')) | |||||
| s.config.less = Expando(dict(app='~/local/bin/lessc')) | |||||
| source = TEST_SITE.child('content/media/css/site.less') | source = TEST_SITE.child('content/media/css/site.less') | ||||
| target = File(Folder(s.config.deploy_root_path).child('media/css/site.css')) | target = File(Folder(s.config.deploy_root_path).child('media/css/site.css')) | ||||
| gen = Generator(s) | gen = Generator(s) | ||||
| @@ -4,6 +4,7 @@ Use nose | |||||
| `$ pip install nose` | `$ pip install nose` | ||||
| `$ nosetests` | `$ nosetests` | ||||
| """ | """ | ||||
| from hyde.ext.plugins.meta import MetaPlugin | |||||
| from hyde.ext.plugins.sorter import SorterPlugin | from hyde.ext.plugins.sorter import SorterPlugin | ||||
| from hyde.fs import File, Folder | from hyde.fs import File, Folder | ||||
| from hyde.generator import Generator | from hyde.generator import Generator | ||||
| @@ -136,55 +137,6 @@ class TestSorter(object): | |||||
| File(p).parent.name]))] | File(p).parent.name]))] | ||||
| assert pages == expected_sorted | assert pages == expected_sorted | ||||
| # def test_walk_resources_sorted_with_grouping_one_level(self): | |||||
| # s = Site(TEST_SITE) | |||||
| # cfg = """ | |||||
| # plugins: | |||||
| # - hyde.ext.sorter.SorterPlugin | |||||
| # sorter: | |||||
| # multi: | |||||
| # groups: sections.yaml | |||||
| # attr: | |||||
| # - source_file.kind | |||||
| # - node.name | |||||
| # | |||||
| # """ | |||||
| # sections = """ | |||||
| # group_name: section | |||||
| # groups: | |||||
| # - | |||||
| # name: support | |||||
| # description: All site support pages | |||||
| # - | |||||
| # name: media | |||||
| # description: Media files | |||||
| # """ | |||||
| # s.config = Config(TEST_SITE, config_dict=yaml.load(cfg)) | |||||
| # s.load() | |||||
| # SorterPlugin(s).begin_site() | |||||
| # | |||||
| # assert hasattr(s.content, 'walk_resources_sorted_by_multi') | |||||
| # expected = ["content/404.html", | |||||
| # "content/about.html", | |||||
| # "content/apple-touch-icon.png", | |||||
| # "content/blog/2010/december/merry-christmas.html", | |||||
| # "content/crossdomain.xml", | |||||
| # "content/favicon.ico", | |||||
| # "content/robots.txt", | |||||
| # "content/site.css" | |||||
| # ] | |||||
| # | |||||
| # pages = [page.name for page in s.content.walk_resources_sorted_by_multi()] | |||||
| # | |||||
| # expected_sorted = [File(page).name | |||||
| # for page in | |||||
| # sorted(expected, | |||||
| # key=lambda p: tuple( | |||||
| # [File(p).kind, | |||||
| # File(p).parent.name]))] | |||||
| # assert pages == expected_sorted | |||||
| def test_walk_resources_sorted_no_default_is_processable(self): | def test_walk_resources_sorted_no_default_is_processable(self): | ||||
| s = Site(TEST_SITE) | s = Site(TEST_SITE) | ||||
| cfg = """ | cfg = """ | ||||
| @@ -318,3 +270,67 @@ class TestSorter(object): | |||||
| q = PyQuery(text) | q = PyQuery(text) | ||||
| assert q('span.latest').text() == 'YayYayYay' | assert q('span.latest').text() == 'YayYayYay' | ||||
| class TestGrouper(object): | |||||
| def setUp(self): | |||||
| TEST_SITE.make() | |||||
| TEST_SITE.parent.child_folder( | |||||
| 'sites/test_grouper').copy_contents_to(TEST_SITE) | |||||
| def tearDown(self): | |||||
| TEST_SITE.delete() | |||||
| def test_walk_resources_sorted_with_grouping_one_level(self): | |||||
| s = Site(TEST_SITE) | |||||
| cfg = """ | |||||
| plugins: | |||||
| - hyde.ext.meta.MetaPlugin | |||||
| - hyde.ext.sorter.SorterPlugin | |||||
| sorter: | |||||
| sectional: | |||||
| grouping: | |||||
| name: section | |||||
| description: Sections in the site | |||||
| groups: | |||||
| - | |||||
| name: start | |||||
| description: Getting Started | |||||
| - | |||||
| name: plugins | |||||
| description: Plugins | |||||
| attr: | |||||
| - source_file.kind | |||||
| - node.name | |||||
| """ | |||||
| s.config = Config(TEST_SITE, config_dict=yaml.load(cfg)) | |||||
| s.load() | |||||
| MetaPlugin(s).begin_site() | |||||
| SorterPlugin(s).begin_site() | |||||
| assert hasattr(s, 'sectional') | |||||
| assert hasattr(s.sectional, 'groups') | |||||
| assert len(s.sectional.groups) == 2 | |||||
| groups = dict([(g.name, g) for g in s.sectional.groups]) | |||||
| assert 'start' in groups | |||||
| assert 'plugins' in groups | |||||
| start = groups['start'] | |||||
| assert hasattr(start, 'resources') | |||||
| start_resources = [resource.name for resource in | |||||
| start.resources if resource.is_processable] | |||||
| assert len(start_resources) == 3 | |||||
| assert 'installation.html' in start_resources | |||||
| assert 'overview.html' in start_resources | |||||
| assert 'templating.html' in start_resources | |||||
| plugin = groups['plugins'] | |||||
| assert hasattr(plugin, 'resources') | |||||
| plugin_resources = [resource.name for resource in | |||||
| plugin.resources if resource.is_processable] | |||||
| assert len(plugin_resources) == 2 | |||||
| assert 'plugins.html' in plugin_resources | |||||
| assert 'tags.html' in plugin_resources | |||||
| @@ -1,8 +1 @@ | |||||
| {% extends "base.html" %} | |||||
| {% block main %} | |||||
| Hi! | |||||
| I am a test template to make sure jinja2 generation works well with hyde. | |||||
| {{resource.name}} | |||||
| {% endblock %} | |||||
| # Installation Guide | |||||
| @@ -0,0 +1,3 @@ | |||||
| extends: base.html | |||||
| default_block: main | |||||
| section: start | |||||
| @@ -1,8 +1 @@ | |||||
| {% extends "base.html" %} | |||||
| {% block main %} | |||||
| Hi! | |||||
| I am a test template to make sure jinja2 generation works well with hyde. | |||||
| {{resource.name}} | |||||
| {% endblock %} | |||||
| # Overview | |||||
| @@ -1,8 +1,4 @@ | |||||
| {% extends "base.html" %} | |||||
| {% block main %} | |||||
| Hi! | |||||
| I am a test template to make sure jinja2 generation works well with hyde. | |||||
| {{resource.name}} | |||||
| {% endblock %} | |||||
| === | |||||
| section: plugins | |||||
| === | |||||
| # Plugins | |||||
| @@ -1,8 +1,4 @@ | |||||
| {% extends "base.html" %} | |||||
| {% block main %} | |||||
| Hi! | |||||
| I am a test template to make sure jinja2 generation works well with hyde. | |||||
| {{resource.name}} | |||||
| {% endblock %} | |||||
| === | |||||
| section: plugins | |||||
| === | |||||
| # Tags | |||||
| @@ -1,8 +1 @@ | |||||
| {% extends "base.html" %} | |||||
| {% block main %} | |||||
| Hi! | |||||
| I am a test template to make sure jinja2 generation works well with hyde. | |||||
| {{resource.name}} | |||||
| {% endblock %} | |||||
| # Templating | |||||
| @@ -2,6 +2,5 @@ mode: development | |||||
| media_root:: media # Relative path from site root (the directory where this file exists) | media_root:: media # Relative path from site root (the directory where this file exists) | ||||
| media_url: /media | media_url: /media | ||||
| template: hyde.ext.jinja2 | template: hyde.ext.jinja2 | ||||
| widgets: | |||||
| plugins: | |||||
| aggregators: | |||||
| meta: | |||||
| nodemeta: meta.yaml | |||||