Configuration

The “configuration” of [master] view classes ties into the Pyramid web framework. The most basic task is to connect a URL pattern to a view class method, optionally with permission requirement. Repeat as needed for all URL routes.

Pyramid Basics

Ultimately we are just using Pyramid’s normal Configurator machinery, with some convenience wrappers.

Note

As such, the config in examples below refers to that Configurator instance, not the app’s native config object.

The MasterView class has a defaults() method which handles most or all of the configuration based on class attributes. This method should be called from within a module’s includeme() function, for example:

from wuttaweb.views import MasterView
from poser.db.model import Sprocket

class SprocketView(MasterView):
    model_class = Sprocket

def includeme(config):
    SprocketView.defaults(config)

If we assume the above module lives at poser.web.views.sprockets then we need only “include” that module from the central config:

def includeme(config):

    # ..<other config elided>..

    config.include("poser.web.views.sprockets")

And now that master view class is part of the app. In this example (since it doesn’t specify otherwise) all CRUD features would be enabled, and each would use appropriate permission checks etc.

If a master view needs to expose additional routes not provided by the framework, it should override the defaults() method accordingly (example below).

Route Ordering

Note that the order in which routes are configured matters. Consider the following:

  • /sprockets/ - main index/grid view

  • /sprockets/{uuid} - view single record

  • /sprockets/{uuid}/edit - edit single record

  • /sprockets/group-action - custom page

Pyramid will pattern-match the incoming request URL against the configured routes, and invoke the relevant view callable when a match is found. In the list above, the custom page will never be matched as it is “eclipsed” by the “view single record” URL (i.e. it would see group-action and think it’s a value for {uuid}).

So when adding a custom page like that you may need to make sure your custom route is defined before the base class defines the CRUD routes.

And, when you do that, you will also need to explicitly set the “permission group” title. That’s an unfortunate quirk, not sure yet how to avoid it.)

Custom Example

You may also want to define a custom permission for the custom route, depending on your needs.

And since we’re here: Sometimes you may need access to the Wutta config object to determine how the Pyramid stuff should be configured. So this example will show all of that:

class SprocketView(MasterView):
    model_class = Sprocket

    def group_action(self):
        sprockets = self.get_grid_data()
        return {"msg": "hello from my group action",
                "sprockets": sprockets}

    def single_action(self):
        sprocket = self.get_instance()
        return {"msg": "hello from my single action",
                "sprocket": sprocket}

    @classmethod
    def defaults(cls, config):
        wutta_config = config.registry.settings.get("wutta_config")
        # nb. if you need it, can get at the app handler etc.
        #app = wutta_config.get_app()

        # nb. fix permission group
        config.add_wutta_permission_group("sprockets", "Sprockets")

        # custom permission
        config.add_wutta_permission("sprockets", "sprockets.special_action",
                                    "Take special action on Sprocket(s)")

        # group route/view
        config.add_route("sprockets.group_action",
                         "/sprockets/group-action")
        config.add_view(cls, attr="group_action",
                        route_name="sprockets.group_action",
                        template="/sprockets/group_action.mako",
                        permission="sprockets.special_action")

        # single route/view (but only if app config says so)
        if wutta_config.get_bool("poser.should_include_single_action"):
            config.add_route("sprockets.single_action",
                             "/sprockets/{uuid}/single-action")
            config.add_view(cls, attr="single_action",
                            route_name="sprockets.single_action",
                            template="/sprockets/single_action.mako",
                            permission="sprockets.special_action")

        # nb. do normal config, per base class
        cls._defaults(config)

Note that last line uses an underscore prefix for the method name. We can’t use super() here since we’re dealing with class methods, so subclass must call _defaults() to get the base config.

Config Prefixes

The master view has a set of “prefixes” which are used to generate the route and permission names, as well as URL and template paths:

For instance a master view for sprockets will typically use sprockets for the route prefix, and therefore default CRUD routes would be:

  • sprockets (index/grid view)

  • sprockets.view (view single)

  • sprockets.edit (edit single)

  • sprockets.delete (delete single)

And similarly with /sprockets for the URL prefix:

  • /sprockets/ (index/grid view)

  • /sprockets/{uuid} (view single)

  • /sprockets/{uuid}/edit (edit single)

  • /sprockets/{uuid}/delete (delete single)

A subclass can set these explicitly; otherwise they will be automatically determined based on the model class/name:

class SprocketView(MasterView):
    model_class = Sprocket

    route_prefix = "funky_sprockets"
    permission_prefix = "funky_sprockets"

    url_prefix = "/funky/sprockets"
    template_prefix = "/funky/sprockets"

It’s typical, and arguably preferable, for the route and permission prefixes to have the same value. And similarly for the URL and template prefixes. However they are tracked separately precisely so they can deviate if necessary.

A subclass is free to configure the routes etc. with or without using these prefixes. (Unless it is part of a library, as opposed to the final app. Libraries should honor the prefixes.)

The prefixes are of course how the base class logic is able to configure views for each subclass automatically.

The custom example in previous section did not use prefixes, so here is the same logic but using prefixes. It uses a similar trick for the model title:

class SprocketView(MasterView):
    model_class = Sprocket

    def group_action(self):
        sprockets = self.get_grid_data()
        return {"msg": "hello from my group action",
                "sprockets": sprockets}

    def single_action(self):
        sprocket = self.get_instance()
        return {"msg": "hello from my single action",
                "sprocket": sprocket}

    @classmethod
    def defaults(cls, config):
        wutta_config = config.registry.settings.get("wutta_config")
        # nb. if you need it, can get at the app handler etc.
        #app = wutta_config.get_app()

        # fetch attributes
        route_prefix = cls.get_route_prefix()
        permission_prefix = cls.get_permission_prefix()
        url_prefix = cls.get_url_prefix()
        instance_url_prefix = cls.get_instance_url_prefix()
        template_prefix = cls.get_template_prefix()
        model_title_plural = cls.get_model_title_plural()

        # nb. fix permission group
        config.add_wutta_permission_group(permission_prefix, model_title_plural)

        # custom permission
        config.add_wutta_permission(permission_prefix,
                                    f"{permission_prefix}.special_action",
                                    f"Take special action on {model_title_plural}")

        # group route/view
        config.add_route(f"{route_prefix}.group_action",
                         f"{url_prefix}/group-action")
        config.add_view(cls, attr="group_action",
                        route_name=f"{route_prefix}.group_action",
                        template=f"{template_prefix}/group_action.mako",
                        permission=f"{permission_prefix}.special_action")

        # single route/view (but only if app config says so)
        if wutta_config.get_bool("poser.should_include_single_action"):
            config.add_route(f"{route_prefix}.single_action",
                             f"{instance_url_prefix}/single-action")
            config.add_view(cls, attr="single_action",
                            route_name=f"{route_prefix}.single_action",
                            template=f"{template_prefix}/single_action.mako",
                            permission=f"{permission_prefix}.special_action")

        # nb. do normal config, per base class
        cls._defaults(config)