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

问题描述 投票:0回答:2

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

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

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

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

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

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的常见代码?

python pyramid software-design
2个回答
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,以确保从视图函数中尽可能多地提取。


0
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.