pyramid_pages

pyramid_pages provides a collections of pages to your Pyramid application. This is very similar to django.contrib.flatpages but with a tree structure and traversal algorithm in URL dispath.

pyramid_pages - example of website pages tree

Installing

github

pip install git+http://github.com/uralbash/pyramid_pages.git

PyPi

pip install pyramid_pages

source

git clone git+http://github.com/uralbash/pyramid_pages.git
cd pyramid_pages
pip install -e .

Configuration

Custom model for tree pages

To build a tree, using the model from sqlalchemy_mptt.

Note

Otherwise, it will look like flat pages.
Plans to add a recursive model with only parent_id field.

Create model of tree pages. For more detail see example pyramid_pages_example.

from pyramid_pages.models import FlatPageMixin, MpttPageMixin, RedirectMixin

...

class WebPage(Base, MpttPageMixin, RedirectMixin):
    __tablename__ = 'mptt_pages'

    id = Column('id', Integer, primary_key=True)


class NewsPage(Base, FlatPageMixin):
    __tablename__ = 'flat_news'

    id = Column('id', Integer, primary_key=True)
    date = Column(Date, default=func.now())

If you use pyramid_sacrud, you can inherited from BaseSacrudMpttPage or BaseSacrudFlatPage or just use SacrudOptions.

It’s look likes this:

Integration pyramid_pages with pyramid_sacrud

Configure pyramid_pages

Then add settings of pyramid_pages.

from youproject.models import WebPage, NewsPage

...

settings['pyramid_pages.models'] = {
   '': WebPage,
   'pages': WebPage,  # available with prefix '/pages/'
   'news': NewsPage
}

# pyramid_pages - put it after all routes
# and after pyramid_pages configuration.
config.include("pyramid_pages")

If you use version of pyramid >= 1.6a1, there is a possibility put config.include("pyramid_pages") before pyramid_pages Configuration.

from youproject.models import WebPage, NewsPage

...

config.include("pyramid_pages")
settings['pyramid_pages.models'] = {
   '': WebPage,
   'pages': WebPage,  # available with prefix '/pages/'
   'news': NewsPage
}

Custom resource

Base resource for pages can be found in the module pyramid_pages.routes.

Just inherit your resource from PageResource.

Custom resource for gallery.
1
2
3
class GalleryResource(BasePageResource):
    model = Gallery
    template = 'gallery/index.jinja2'
Model for GalleryResource.
1
2
3
4
5
6
7
8
class Gallery(BasePage, MpttPageMixin):
    __tablename__ = 'mptt_gallery'

    id = Column(Integer, ForeignKey('base_pages.id'), primary_key=True)

    __mapper_args__ = {
        'polymorphic_identity': 'gallery_page',
    }
Model photo for Gallery.
1
2
3
4
5
6
7
class Photo(Base):
    __tablename__ = 'photos'

    id = Column('id', Integer, primary_key=True)
    path = Column('path', Text)
    gallery_id = Column(Integer, ForeignKey('mptt_gallery.id'))
    gallery = relationship('Gallery', backref='photos')

And add it to config.

settings['pyramid_pages.models'] = {
   '': WebPage,
   'pages': WebPage,  # available with prefix '/pages/'
   'news': NewsPage,
   'gallery': GalleryResource
}

Generate menu

Make menu object and pass it in template context.

from pyramid_pages.common import Menu


class Gallery(Base, MpttPageMixin):
    __tablename__ = 'mptt_gallery'

    menu_template = 'myproject/templates/my_custom_menu.mako'

    id = Column('id', Integer, primary_key=True)

page_menu = Menu(DBSession, WebPage).mptt
news_menu = Menu(DBSession, NewsPage).flat
gallery_menu = Menu(DBSession, Gallery).mptt

Just include menu template.

{% with menu=news_menu() %}
  {% include menu.template with context %}
{% endwith %}

{% with menu=page_menu(from_lvl=1, to_lvl=6, trees=(1, 2, 3)) %}
  {% include menu.template with context %}
{% endwith %}

Or write your own.

Flat menu template.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<div class="main-menu-list">
    {% for node in menu %}
        <div class="main-menu-list__item
                {% if node == context.node or node == page %}
                  main-menu-list__item_state_current
                {% endif %}">
            <a href="{{ request.resource_url(context.resource_of_node(node)) }}"
              class="main-menu-list__item-link">
              {{ node.name }}
            </a>
        </div>
    {% endfor %}
</div>
MPTT menu template.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<div class="main-menu-list">
  {% for resource in menu if resource.node.in_menu and resource.node.visible recursive %}
    <div class="main-menu-list__item
            {% for item in lineage(context) if item and item.node==resource.node %}
                main-menu-list__item_in_current_branch
            {% endfor %}
            {% if resource.node == context.node or resource.node == page %}
              main-menu-list__item_state_current
            {% endif %}
            {% if resource.children_qty %}
              main-menu-list__item_state_has-child
            {% endif %}">
      <a href="{{ request.resource_url(resource) }}"
        class="main-menu-list__item-link">
        {{ resource.node.name }}
      </a>
      <div class="main-menu-list">{{ loop(resource.children) }}</div>
    </div>
  {% endfor %}
</div>

If you want show mptt menu with to_lvl==max-2 or similar, just use to_lvl=-2.

{% with menu=page_menu(from_lvl=1, to_lvl=-2, trees=(1, 2, 3)) %}
  {% include menu.template with context %}
{% endwith %}

API

common

resources

Base pages resources.

class pyramid_pages.resources.BasePageResource(node, prefix=None, request=None, parent=None, *args, **kwargs)[source]

Bases: object

Base resource tree class for pages.

View:view of resource.
Attr:The view machinery defaults to using the __call__ method of the view callable (or the function itself, if the view callable is a function) to obtain a response. The attr value allows you to vary the method attribute used to obtain the response. For example, if your view was a class, and the class has a method named index and you wanted to use this method instead of the class’s __call__ method to return the response, you’d say attr=”index” in the view configuration for the view. This is most useful when the view definition is a class.
Template:template for view of resource.
attr = 'page_with_redirect'
children
children_qty
get_prefix()[source]

Each resource defined in config for pages as dict. This method returns key from config where located current resource.

name
resource_of_node(node, parent=None)[source]
slug
template = 'pyramid_pages/index.jinja2'
view

alias of pyramid_pages.views.PageView

pyramid_pages.resources.models_of_config(config)[source]

Return list of models from all resources in config.

pyramid_pages.resources.resource_of_node(resources, node)[source]

Returns resource of node.

pyramid_pages.resources.resources_of_config(config)[source]

Returns all resources and models from config.

models

Models for page.

class pyramid_pages.models.BaseSacrudFlatPage[source]

Bases: pyramid_pages.models.SacrudOptions, pyramid_pages.models.SeoMixin, pyramid_pages.models.RedirectMixin, pyramid_pages.models.FlatPageMixin

Base flat page class for pyramid_sacrud.

class pyramid_pages.models.BaseSacrudMpttPage[source]

Bases: pyramid_pages.models.SacrudOptions, pyramid_pages.models.SeoMixin, pyramid_pages.models.RedirectMixin, pyramid_pages.models.MpttPageMixin

Base mptt page class for pyramid_sacrud.

class pyramid_pages.models.FlatPageMixin[source]

Bases: pyramid_pages.models.PageMixin

class pyramid_pages.models.MpttPageMixin[source]

Bases: sqlalchemy_mptt.mixins.BaseNestedSets, pyramid_pages.models.PageMixin

class pyramid_pages.models.PageMixin[source]

Bases: object

description = Column(None, UnicodeText(), table=None)
in_menu = Column(None, Boolean(), table=None)
name = Column(None, String(), table=None, nullable=False)
slug = Column(None, SlugType(), table=None, nullable=False)
visible = Column(None, Boolean(), table=None)
class pyramid_pages.models.RecursionPageMixin[source]

Bases: pyramid_pages.models.PageMixin

Model with single parent_id field

class pyramid_pages.models.RedirectMixin[source]

Bases: object

redirect
redirect_page
redirect_type = Column(None, ChoiceType(), table=None)
redirect_url = Column(None, String(), table=None)
class pyramid_pages.models.SacrudOptions[source]

Bases: object

sacrud_detail_col
class pyramid_pages.models.SeoMixin[source]

Bases: object

seo_description = Column(None, String(), table=None)
seo_keywords = Column(None, String(), table=None)
seo_metatags = Column(None, UnicodeText(), table=None)
seo_title = Column(None, String(), table=None)

routes

Routes for pyramid_pages

pyramid_pages.routes.add_globals(event)[source]
pyramid_pages.routes.home_page_factory(request)[source]
pyramid_pages.routes.includeme(config)[source]
pyramid_pages.routes.page_factory(request)[source]

Page factory.

Config models example:

models = {
    '': [WebPage, CatalogResource],
    'catalogue': CatalogResource,
    'news': NewsResource,
}
pyramid_pages.routes.register_views(*args)[source]

Registration view for each resource from config.

views

Views for pages

class pyramid_pages.views.PageView(context, request)[source]

Bases: object

page_with_redirect()[source]

security

Module contents

pyramid_pages.includeme(config)[source]

Tutorials and Cookbook Recipes

Simple Web-site with tree pages

This is an example of using pyramid_pages in just one file (pyramid_pages_example.py). Full code you can see there.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2014 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.

"""
Main for example
"""
import os
import json

import transaction
from sqlalchemy import (
    Date,
    Text,
    Column,
    String,
    Integer,
    ForeignKey,
    engine_from_config
)
from pyramid.config import Configurator
from pyramid.events import BeforeRender
from sqlalchemy.orm import relationship, sessionmaker, scoped_session
from sqlalchemy.sql import func
from pyramid.session import SignedCookieSessionFactory
from sqlalchemy_mptt import mptt_sessionmaker
from zope.sqlalchemy import ZopeTransactionExtension
from sqlalchemy.ext.declarative import declarative_base

from pyramid_pages.models import FlatPageMixin, MpttPageMixin, RedirectMixin
from pyramid_pages.resources import (
    BasePageResource,
    resource_of_node,
    resources_of_config
)

Base = declarative_base()
DBSession = scoped_session(
    mptt_sessionmaker(
        sessionmaker(extension=ZopeTransactionExtension())
    )
)

CONFIG_SQLALCHEMY_URL = 'sqlalchemy.url'
CONFIG_PYRAMID_PAGES_MODELS = 'pyramid_pages.models'
CONFIG_PYRAMID_PAGES_DBSESSION = 'pyramid_pages.dbsession'


class BasePage(Base, RedirectMixin):
    __tablename__ = 'base_pages'
    id = Column(Integer, primary_key=True)
    page_type = Column(String(50))

    __mapper_args__ = {
        'polymorphic_identity': 'base_page',
        'polymorphic_on': page_type,
        'with_polymorphic': '*'
    }

    @classmethod
    def get_pk_name(cls):
        return 'id'

    @classmethod
    def get_pk_with_class_name(cls):
        return 'BasePage.id'


class WebPage(BasePage, MpttPageMixin):
    __tablename__ = 'mptt_pages'

    id = Column(Integer, ForeignKey('base_pages.id'), primary_key=True)

    __mapper_args__ = {
        'polymorphic_identity': 'web_page',
    }


class NewsPage(BasePage, FlatPageMixin):
    __tablename__ = 'flat_news'

    id = Column(Integer, ForeignKey('base_pages.id'), primary_key=True)
    date = Column(Date, default=func.now())

    __mapper_args__ = {
        'polymorphic_identity': 'news_page',
    }


class Gallery(BasePage, MpttPageMixin):
    __tablename__ = 'mptt_gallery'

    id = Column(Integer, ForeignKey('base_pages.id'), primary_key=True)

    __mapper_args__ = {
        'polymorphic_identity': 'gallery_page',
    }


class Photo(Base):
    __tablename__ = 'photos'

    id = Column('id', Integer, primary_key=True)
    path = Column('path', Text)
    gallery_id = Column(Integer, ForeignKey('mptt_gallery.id'))
    gallery = relationship('Gallery', backref='photos')


class GalleryResource(BasePageResource):
    model = Gallery
    template = 'gallery/index.jinja2'


class NewsResource(BasePageResource):
    model = NewsPage
    template = 'news/index.jinja2'


class Fixtures(object):

    def __init__(self, session):
        self.session = session

    def add(self, model, fixtures):
        here = os.path.dirname(os.path.realpath(__file__))
        file = open(os.path.join(here, fixtures))
        fixtures = json.loads(file.read(), encoding='utf-8')
        for fixture in fixtures:
            self.session.add(model(**fixture))
            self.session.flush()
        transaction.commit()

models = {
    '': WebPage,
    'pages': WebPage,
    'news': NewsResource,
    'gallery': GalleryResource,
}


def add_globals(event):

    class Menu(object):
        resources = resources_of_config(models)

        def __init__(self, model):
            self.nodes = DBSession.query(model)
            self.template = 'pyramid_pages/menu/flat.jinja2'
            if hasattr(model, 'parent'):
                self.nodes = self.nodes.filter_by(parent=None)\
                    .order_by(model.tree_id)
                self.template = 'pyramid_pages/menu/mptt.jinja2'

        def __iter__(self):
            for node in self.nodes:
                yield resource_of_node(self.resources, node)(node)

    event['pages_menu'] = Menu(WebPage)
    event['news_menu'] = Menu(NewsPage)
    event['gallery_menu'] = Menu(Gallery)


def main(global_settings, **settings):
    config = Configurator(
        settings=settings,
        session_factory=SignedCookieSessionFactory('itsaseekreet')
    )
    config.include('pyramid_jinja2')
    config.add_jinja2_search_path('pyramid_pages_example:templates')
    config.add_static_view('pyramid_pages_example_static',
                           'static')

    # Database
    settings = config.get_settings()
    settings[CONFIG_SQLALCHEMY_URL] =\
        settings.get(CONFIG_SQLALCHEMY_URL,
                     'sqlite:///example.sqlite')
    engine = engine_from_config(settings)
    DBSession.configure(bind=engine)
    Base.metadata.drop_all(engine)
    Base.metadata.create_all(engine)
    fixture = Fixtures(DBSession)
    fixture.add(WebPage, 'fixtures/pages.json')
    fixture.add(WebPage, 'fixtures/country.json')
    fixture.add(NewsPage, 'fixtures/news.json')
    fixture.add(Gallery, 'fixtures/gallery.json')
    fixture.add(Photo, 'fixtures/photos.json')

    # pyramid_pages
    settings[CONFIG_PYRAMID_PAGES_DBSESSION] =\
        settings.get(CONFIG_PYRAMID_PAGES_DBSESSION,
                     DBSession)
    settings[CONFIG_PYRAMID_PAGES_MODELS] =\
        settings.get(CONFIG_PYRAMID_PAGES_MODELS, models)
    config.include("pyramid_pages")
    config.add_subscriber(add_globals, BeforeRender)
    return config.make_wsgi_app()

if __name__ == '__main__':
    settings = {}
    app = main({}, **settings)

    from wsgiref.simple_server import make_server
    httpd = make_server('0.0.0.0', 6543, app)
    httpd.serve_forever()

Support and Development

To report bugs, use the issue tracker.

We welcome any contribution: suggestions, ideas, commits with new futures, bug fixes, refactoring, docs, tests, translations etc

If you have question, contact me sacrud@uralbash.ru or IRC channel #sacrud

Indices and tables