0

I have a series of view callables that all need to execute a series of functions that perform validations and if one of these validations fails, it will return the output for view callable. If all of these validations pass, then the rest of the logic in the view callable should execute and ultimately produce the appropriate output. This looks like this in pseudo code:

@view_config(...)
def my_view(request):
   # perform a validation and return an error dictionary if there was a problem
   o = validate_thingy_a(request)
   if o: return o

   # perform a validation and return an error dictionary if there was a problem
   o = validate_thingy_b(request)
   if o: return o

   # validations were all good, go on to produce sunny day output
   return { "result" : "normal view results"}

So although that's not as elegant as a like, it works. But here's my real question: If you have a series of related view callables that all need these same validations done up front, is there a good way to code them so that each of them doesn't have to list those same few validation blocks?

I thought about decorators but the problem is I think if I created multiple decorators (one for each validation) then what I need to happen is if a given validator decorator fails, it should emit an error dictionary on behalf of the view callable and the other decorators shouldn't then run. But I don't think you can wire up decorators to easily skip the "remaining decorators" applied to a function.

Then, I went on consider doing this in a class somehow, like this, maybe:

class ApiStandard(object):
   def __init__(self, request)
     self.request = request

     # would like to do validations here that precede all view callables below
     # but can't figure out how to "return" output for the callable if a
     # validation fails - then we don't want the view callable to be called.

   @view_config(route=...)
   def view_callable1(self):
      ...
   @view_config(route=...)
   def view_callable2(self):
      ...

But I don't think that can work because I don't think init can emit a result on behalf of a view and cause the view not to be called.

The last arrangement with a class is adding a validate method to the class and having each view callable call it. This is slightly better than putting all the individual checks in each callable, but not much, you still have to remember to call this method when adding another view callable.

class ApiStandard(object):
   def __init__(self, request)
     self.request = request

   def common_validations():
      # perform common validations and return an error dict if there was a problem

   @view_config(route=...)
   def view_callable1(self):
      o = common_validations()
      if o: return o
      ...
enter code here
   @view_config(route=...)
   def view_callable2(self):
      o = common_validations()
      if o: return o
      ...

I don't find any of the above solutions to be very elegant. Is there a good way to handle common bit of code for related view callables??

Bart
  • 496
  • 10
  • 23
Michael Ray Lovett
  • 6,668
  • 7
  • 27
  • 36

2 Answers2

0

You're right to say that it would be great to extract that code outside of the view function.

Personally I really like the way Cornice did this for REST APIs: https://cornice.readthedocs.io/en/latest/validation.html

For example, from the link above:

from cornice import Service

foo = Service(name='foo', path='/foo')


def has_paid(request, **kwargs):
    if not 'X-Verified' in request.headers:
        request.errors.add('header', 'X-Verified', 'You need to provide a token')

@foo.get(validators=has_paid)
def get_value(request):
    """Returns the value.
    """
    return 'Hello'

I work on an app where there are both Cornice endpoints and regular views declared with @view_config, so I wrote a decorator that can be used to wrap Colander schema classes (https://docs.pylonsproject.org/projects/colander/en/latest/).

The decorator looks like this:

from collections import defaultdict
from pyramid.renderers import render_to_response
import colander
from translationstring import TranslationString


class PreValidator:
    def __init__(self, schema_class, renderer, on_invalid=None, extra_vars=None):
        self.schema_class = schema_class
        self.renderer = renderer
        self.on_invalid = on_invalid
        self.extra_vars = extra_vars

    def __call__(self, wrapped):
        def wrapper(context, request):
            schema = self.schema_class().bind(request=request)
            try:
                values = schema.deserialize(request.POST.mixed())
                request.validated = values
                return wrapped(context, request)
            except colander.Invalid as e:
                if hasattr(self.on_invalid, '__call__'):
                    self.on_invalid(request)
                errors = dict([(c.node.name, c.messages()) for c in e.children])
                general_errors = e.messages()
                if len(general_errors) > 0:
                    errors['_general'] = general_errors
                for _, msgs in errors.items():
                    for i, msg in enumerate(msgs):
                        if type(msg) == TranslationString:
                            msgs[i] = request.localizer.translate(msg)
                for_renderer = dict(
                    values=defaultdict(lambda: '', request.POST.items()),
                    errors=errors,
                )
                if hasattr(self.extra_vars, '__call__'):
                    self.extra_vars(request, for_renderer)
                return render_to_response(
                    self.renderer, for_renderer, request, response=request.response)

        return wrapper

And it's used similar to this:

from colander import (
    Schema,
    SchemaNode,
    String,
    Invalid,
    deferred,
)


class LoginSchema(Schema):
    # here you would do more complex validation
    email = SchemaNode(String())
    password = SchemaNode(String())


def extra_vars(request, templ_vars):
    # in case you need to set again some value
    pass


@view_config(
    route_name='login',
    request_method='POST',
    renderer='/website/login.jinja2',
    decorator=PreValidator(
        LoginSchema,
        '/website/login.jinja2',
        on_invalid=lambda r: r.POST.pop('password', None),
        extra_vars=extra_vars),
)
def login_post(request):
    # here you know the request is valid
    do_something(request.validated['email'], request.validated['password'])
    return HTTPFound('/')

I'm not saying you should use this as-is, but I thought that seeing how someone else did this could help.

Also, make sure to use Factories/context (even if you don't use traversal routing) and Pyramid ACL to make sure you extract as much as possible from your view functions.

Antoine Leclair
  • 17,540
  • 3
  • 26
  • 18
0

Maybe I'm missing something, but this seems like stuff that is all pretty straightforward if you make your views as classes and use inheritance and mixins with them. You can also make callable ViewClassFactories that return view classes from params. In addition to that, sometimes it's good to take some of your view code out of the view and push it into a RootFactory for that view. And you can make factories that make RootFactories. If you haven't experimented with the combination of cooperating view-as-classes and root factory classes, I'd recommend it. Pyramid has a lot of options for code re-use.

Iain Duncan
  • 1,738
  • 16
  • 17