哪里最好放置通用视图代码以避免重复?

时间:2018-01-11 14:09:38

标签: python pyramid software-design

我有一系列视图可调用,它们都需要执行一系列执行验证的函数,如果其中一个验证失败,它将返回视图可调用的输出。如果所有这些验证都通过,那么视图中可调用的其余逻辑应该执行并最终生成适当的输出。在伪代码中看起来像这样:

@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"}

所以尽管它并不像它那样优雅,但它确实有效。但这是我的真正的问题:如果你有一系列相关的视图可调用,所有这些都需要预先完成相同的验证,那么有一种很好的方法可以对它们进行编码,这样它们每个都不会#39; t必须列出那些相同的几个验证块?

我想过装饰器,但问题是我想如果我创建了多个装饰器(每个验证一个)那么我需要发生的是如果给定的验证器装饰器失败,它应该代表可调用的视图发出一个错误字典然后其他装饰者就不应该跑了。但我不认为你可以通过连接装饰器来轻松跳过剩下的装饰者"应用于函数。

然后,我继续考虑以某种方式在课堂上这样做,也许:

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):
      ...

但我认为这不起作用,因为我不认为 init 可以代表视图发出结果并导致视图不被调用。

与类的最后一种安排是向类中添加一个validate方法,并让每个视图都可调用它。这比在每个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
      ...

我没有发现任何上述解决方案非常优雅。有没有一种很好的方法来处理相关视图callables的常见代码?

2 个答案:

答案 0 :(得分:0)

你说对于在视图函数之外提取代码会很好。

我个人非常喜欢Cornice为REST API做的这种方式:https://cornice.readthedocs.io/en/latest/validation.html

例如,从上面的链接:

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'

我在一个应用程序上工作,其中有Cornice端点和使用@view_config声明的常规视图,所以我写了一个装饰器,可以用来包装Colander模式类(https://docs.pylonsproject.org/projects/colander/en/latest/)。

装饰者看起来像这样:

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

它的用法类似于:

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('/')

我不是说你应该按原样使用它,但我认为看到其他人这样做会有所帮助。

此外,请确保使用工厂/上下文(即使您不使用遍历路由)和金字塔ACL,以确保从视图函数中尽可能多地提取。

答案 1 :(得分:0)

也许我错过了一些东西,但如果你把你的观点作为类并使用继承和mixins,这看起来很简单。您还可以生成可调用的ViewClassFactories,它从params返回视图类。除此之外,有时最好从视图中取出一些视图代码并将其推送到该视图的RootFactory中。你可以建立制造RootFactories的工厂。如果您还没有尝试过合作视图类和根工厂类的组合,我会推荐它。 Pyramid有一个很多选项可供代码重用。