23、授权与多态:实现不同用户上下文的操作与视图

授权与多态:实现不同用户上下文的操作与视图

1. 多态测试

在开发过程中,为了确保代码的正确性和稳定性,测试是必不可少的环节。对于不同用户上下文下的操作,我们需要进行多态测试。

首先,在 test/concepts/thing/create_test.rb 文件中添加新的测试块。在测试文件顶部添加所需的夹具:

class ThingCreateTest < MiniTest::Spec
  let (:current_user) { User::Create.(user: {email: "fred@trb.org"}).model }
  let (:admin) { User::Create.(user: {email: "admin@trb.org"}).model }
end

为了测试 :is_author 特性,添加新的 describe 块:

class ThingCreateTest < MiniTest::Spec
  # ..
  describe "I'm the author!" do
    let (:user) { User::Create.(user: {email: "nick@trb.org"}).model }

    # 匿名用户测试
    it do
      thing = Thing::Create.(thing: {name: "Rails", users: [{"email"=>user.email}], is_author: "1"}, current_user: nil).model
      thing.users.must_equal [user]
    end

    # 已登录用户测试
    it do
      thing = Thing::Create.(thing: {name: "Rails", users: [{"email"=>user.email}], is_author: "1"}, current_user: current_user).model
      thing.users.must_equal [user, current_user]
    end

    # 管理员用户测试
    it do
      op = Thing::Create.(thing: {name: "Rails", users: [{"email"=>user.email}], is_author: "1"}, current_user: admin)
      thing = op.model
      thing.users.must_equal [user, admin]
      op.must_be_instance_of Thing::Create::Admin
    end
  end
end

通过这些测试,我们可以确保不同用户上下文下的操作能够正确处理 is_author 属性,并且操作的子类能够正确实例化并表现出不同的行为。

2. 多态视图

在完成操作和测试代码后,我们需要将这些更改应用到实际的视图中。对于不同用户类型,我们希望看到不同的表单。

当处理上下文敏感的视图时,有三种解决方案:
- 使用Rails控制器视图 :使用 if 语句和部分视图,传递局部变量和实例变量。但这种方法在处理视图依赖时会变得复杂。
- 使用多态单元 :将视图封装在多态单元中,并使用视图继承来覆盖模板的部分内容。这种方法有清晰的接口,但会产生过多的类和视图文件。
- 结合两者 :使用带有 if 语句的单元,以实用且简洁的方式解决问题。

我们选择第三种方法,将 app/views/things/new.html.haml 模板重命名并转换为 app/concepts/thing/views/form.haml 单元视图:

%h3 What do you want to talk about?
= simple_form_for contract, html: { class: css_class} do |f|
  = f.input :name, readonly: contract.readonly?(:name)
  = f.input :description
  = f.input :file, as: :file

  %legend Do you know any authors?
  - if signed_in?
    = f.input :is_author, as: :boolean, label: "I'm the author!"

在视图中,我们将实例变量 @form 改为 contract ,并添加了 is_author 复选框,该复选框仅在 SignedIn? true 时显示。

接下来,更新控制器。在 ThingsController 中添加新方法 render_form

class ThingsController < ApplicationController
  # ..
  private
  def render_form
    render text: concept("thing/cell/form", @operation), layout: true
  end
end

使用 concept 方法渲染 Thing::Cell::Form 视图模型,将操作对象作为单元的模型。

更新 ThingsController#new 方法:

class ThingsController < ApplicationController
  def new
    form Thing::Create
    render_form
  end
end

最后,编写 Thing::Cell::Form 单元类:

class Thing::Cell::Form < ::Cell::Concept
  inherit_views Thing::Cell
  include ActionView::RecordIdentifier
  include SimpleForm::ActionViewExtensions::FormHelper

  def show
    render :form
  end

  property :contract

  def css_class
    return "admin" if admin?
    ""
  end

  def signed_in?
    model.policy.signed_in?
  end

  def admin?
    model.policy.admin?
  end
end

在单元类中,我们定义了 contract 属性,以及 css_class signed_in? admin? 方法,这些方法用于在视图中区分不同的用户上下文。

3. 功能测试:创建

虽然我们用单元替换了整个控制器视图,但不需要编写孤立的单元测试,而是在集成测试中进行功能级别的测试。

test/controller/things/create.rb 文件中,对不同用户类型的表单渲染进行测试:

class ThingsControllerCreateTest < IntegrationTest
  describe "#new" do
    # 匿名用户测试
    it do
      visit "/things/new"
      assert_new_form
      page.wont_have_css("#thing_is_author")
      page.wont_have_css("form.admin") # 无橙色背景
    end

    # 已登录用户测试
    it do
      sign_in!
      visit "/things/new"
      assert_new_form
      page.must_have_css("#thing_is_author")
      page.wont_have_css("form.admin") # 无橙色背景
    end
  end
end

通过这些测试,我们可以确保不同用户类型下的表单渲染正确。

4. 生命周期冒烟测试

仅测试表单渲染是不够的,还需要测试事物的创建生命周期。我们可以使用生命周期冒烟测试来模拟手动点击测试。

class ThingsControllerCreateTest < IntegrationTest
  describe "click path" do
    # 匿名用户测试
    it do
      visit "/things/new"
      # 创建事物
      page.current_path.must_equal thing_path(Thing.last)
      page.wont_have_css "a", text: "Edit"
    end

    # 已登录用户测试
    it do
      sign_in!
      visit "/things/new"

      # 无效提交
      click_button "Create Thing"
      page.must_have_css ".error"

      # 正确提交
      fill_in 'Name', with: "Bad Religion"
      check "I'm the author!"
      click_button "Create Thing"

      page.current_path.must_equal thing_path(Thing.last)
      page.must_have_content "By fred@trb.org"

      # 编辑
      click_link "Edit"
    end
  end
end

通过这些测试,我们可以确保不同用户类型下的事物创建和编辑流程正常。

5. 更新操作

对于 Thing::Update 操作,我们引入 Update::SignedIn Update::Admin 来处理不同用户上下文。

基本更新操作的类头如下:

class Thing < ActiveRecord::Base
  class Update < Create
    self.builder_class = Create.builder_class
    action :update
    policy Thing::Policy, :update?
  end
end

app/concepts/thing/policy.rb 中定义新的规则:

class Thing::Policy
  def update?
    edit?
  end

  def edit?
    signed_in? and (admin? or model.users.include?(user))
  end
end

Update 操作继承自 Create ,并手动引用创建的构建器块。 update? 规则映射到 edit? 规则,该规则要求当前用户必须是管理员或事物的作者才能进行更新操作。

对于 Update::SignedIn 操作:

class Update < Create
  # ..
  class SignedIn < self
    contract do
      property :name, writeable: false
      # 这里是Create中的其余原始代码
    end
  end
end

Update::SignedIn 操作不包含 Thing::SignedIn 模块,因为已登录用户只能编辑自己创建的事物,不需要 is_author 复选框。

对于 Update::Admin 操作:

class Admin < SignedIn
  include Thing::SignedIn
  contract do
    property :name
  end
end

Update::Admin 操作包含 Thing::SignedIn 模块,因为管理员可以编辑任何记录,需要 is_author 复选框。同时,管理员可以更改事物的名称。

6. 特定合约视图

在创建和更新表单中,我们不能再通过询问 policy.signed_in? 来决定是否显示 is_author 复选框。因此,修改 form.haml 视图:

- ..
%legend Do you know any authors?
- if has_author_field?
  = f.input :is_author, as: :boolean, label: "I'm the author!"

将查询方法改为 has_author_field? ,并在 Thing::Cell::Form 中实现该方法。

总结

通过以上步骤,我们实现了不同用户上下文下的操作和视图的多态性。通过多态测试确保操作的正确性,通过多态视图为不同用户提供不同的表单,通过功能测试和生命周期冒烟测试验证整个流程的正确性。同时,我们引入了不同的更新操作来处理不同用户上下文,并修改了视图以适应新的需求。

以下是不同操作的对比表格:
| 操作 | 适用用户 | 特点 |
| ---- | ---- | ---- |
| Thing::Create | 所有用户 | 根据用户类型实例化不同子类,处理 is_author 属性 |
| Thing::Update | 未登录用户 | 继承自 Create ,有策略配置,调用会抛出策略异常 |
| Thing::Update::SignedIn | 已登录用户 | 继承自 Update ,名称字段只读,不处理 is_author 字段 |
| Thing::Update::Admin | 管理员用户 | 继承自 Update::SignedIn ,包含 is_author 复选框,名称字段可写 |

以下是操作流程的mermaid流程图:

graph LR
    A[开始] --> B{用户类型}
    B -->|匿名用户| C[Thing::Create 正常操作]
    B -->|已登录用户| D[Thing::Create::SignedIn 操作]
    B -->|管理员用户| E[Thing::Create::Admin 操作]
    C --> F[创建事物]
    D --> F
    E --> F
    F --> G{是否更新事物}
    G -->|是| H{用户类型}
    H -->|未登录用户| I[Thing::Update 抛出异常]
    H -->|已登录用户| J[Thing::Update::SignedIn 操作]
    H -->|管理员用户| K[Thing::Update::Admin 操作]
    J --> L[更新事物]
    K --> L
    G -->|否| M[结束]
    I --> M
    L --> M

通过这些操作和视图的实现,我们可以为不同用户提供不同的功能和体验,同时确保代码的可维护性和可扩展性。

授权与多态:实现不同用户上下文的操作与视图

7. 策略的作用与优势

在Trailblazer中,策略(Policy)扮演着至关重要的角色。策略对象将所有与认证相关的代码封装在一个小范围的对象中,并且可以在操作、渲染单元或表示器之间传递。

以我们之前的代码为例,在操作和单元中都通过 model.policy 方法访问策略对象。例如在 Thing::Cell::Form 单元类中:

def signed_in?
  model.policy.signed_in?
end

def admin?
  model.policy.admin?
end

这里的 model.policy 返回的策略对象,与我们在构建器中使用的是同一个。策略对象包含了如 signed_in? admin? edit? update? 等方法,用于评估用户是否具有相应的权限。

这种设计的优势在于,它允许在操作对象之外进行基于策略的决策,同时遵循整个过程中使用的相同规则集。例如,在视图渲染时,我们可以根据策略对象的方法决定是否显示某些元素,而不需要在视图中编写复杂的认证逻辑。

8. 不同用户上下文下的视图表现

不同用户上下文下,视图呈现出不同的表现形式:
- 匿名用户 :访问 /things/new 时,看到的是旧的、繁琐的表单,只有标题、描述和三个作者字段,没有“我是作者!”复选框,表单也没有橙色背景。
- 已登录用户 :除了基本的表单字段外,还会看到“我是作者!”复选框。
- 管理员用户 :不仅能看到与已登录用户相同的字段,整个表单还会有明亮的橙色背景。

这种不同的表现形式通过视图模板和单元类中的逻辑实现。例如在 form.haml 视图中:

- if signed_in?
  = f.input :is_author, as: :boolean, label: "I'm the author!"

以及在 Thing::Cell::Form 单元类中定义的 css_class 方法:

def css_class
  return "admin" if admin?
  ""
end

通过这些代码,我们可以根据用户的上下文动态地改变视图的显示。

9. 测试的重要性与布局

测试在整个开发过程中起着关键作用。我们进行了多种类型的测试,包括多态测试、功能测试和生命周期冒烟测试,以确保不同用户上下文下的操作和视图的正确性。

测试布局清晰,将不同的功能测试放在 describe 块中,每个 it 块包含针对一种用户类型的断言。例如在 ThingsControllerCreateTest 中:

class ThingsControllerCreateTest < IntegrationTest
  describe "#new" do
    # 匿名用户测试
    it do
      visit "/things/new"
      assert_new_form
      page.wont_have_css("#thing_is_author")
      page.wont_have_css("form.admin") 
    end

    # 已登录用户测试
    it do
      sign_in!
      visit "/things/new"
      assert_new_form
      page.must_have_css("#thing_is_author")
      page.wont_have_css("form.admin") 
    end
  end

  describe "click path" do
    # 匿名用户测试
    it do
      visit "/things/new"
      page.current_path.must_equal thing_path(Thing.last)
      page.wont_have_css "a", text: "Edit"
    end

    # 已登录用户测试
    it do
      sign_in!
      visit "/things/new"
      click_button "Create Thing"
      page.must_have_css ".error"
      fill_in 'Name', with: "Bad Religion"
      check "I'm the author!"
      click_button "Create Thing"
      page.current_path.must_equal thing_path(Thing.last)
      page.must_have_content "By fred@trb.org"
      click_link "Edit"
    end
  end
end

这种测试布局使得代码易于理解和维护,同时能够全面覆盖不同用户类型的各种情况。

10. 操作继承与组合的优势

Thing::Update 操作中,我们通过继承和组合的方式创建了不同的子类,如 Update::SignedIn Update::Admin

继承的优势在于可以复制父类的合约、策略、表示器和回调对象,减少代码重复。例如 Thing::Update 继承自 Thing::Create

class Thing < ActiveRecord::Base
  class Update < Create
    self.builder_class = Create.builder_class
    action :update
    policy Thing::Policy, :update?
  end
end

组合的优势在于可以根据不同用户上下文灵活地添加或移除功能。例如 Update::SignedIn 不包含 Thing::SignedIn 模块,因为已登录用户只能编辑自己创建的事物,不需要“我是作者!”复选框;而 Update::Admin 包含该模块,因为管理员可以编辑任何记录。

class Update < Create
  class SignedIn < self
    contract do
      property :name, writeable: false
    end
  end

  class Admin < SignedIn
    include Thing::SignedIn
    contract do
      property :name
    end
  end
end

通过继承和组合,我们可以为不同用户上下文创建定制化的操作,提高代码的可维护性和可扩展性。

11. 未来展望

虽然我们已经实现了不同用户上下文下的操作和视图的多态性,但仍有一些可以改进的地方。例如,可以进一步优化视图的渲染逻辑,减少视图模板中的 if 语句,提高代码的可读性。另外,可以增加更多的测试用例,覆盖更多的边界情况,确保系统的稳定性。

同时,随着业务的发展,可能会有更多的用户类型和功能需求,我们可以继续使用继承和组合的方式扩展操作和视图,以适应新的变化。

总结

本文详细介绍了如何实现不同用户上下文下的操作和视图的多态性。通过多态测试、多态视图、功能测试和生命周期冒烟测试,我们确保了操作的正确性和视图的动态性。引入不同的更新操作处理不同用户上下文,通过继承和组合的方式提高了代码的可维护性和可扩展性。

以下是不同用户类型的视图表现对比表格:
| 用户类型 | 视图表现 |
| ---- | ---- |
| 匿名用户 | 旧表单,无“我是作者!”复选框,无橙色背景 |
| 已登录用户 | 有“我是作者!”复选框,无橙色背景 |
| 管理员用户 | 有“我是作者!”复选框,橙色背景 |

以下是测试流程的mermaid流程图:

graph LR
    A[开始测试] --> B{测试类型}
    B -->|多态测试| C[测试操作的多态性]
    B -->|功能测试| D[测试视图渲染]
    B -->|生命周期冒烟测试| E[测试事物创建和更新流程]
    C --> F[验证操作结果]
    D --> F
    E --> F
    F --> G{是否通过测试}
    G -->|是| H[测试结束]
    G -->|否| I[修复问题]
    I --> A

通过这些技术和方法,我们可以为不同用户提供个性化的功能和体验,同时保证系统的稳定性和可维护性。在未来的开发中,我们可以继续优化和扩展这些功能,以满足不断变化的业务需求。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值