授权与多态:实现不同用户上下文的操作与视图
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
通过这些技术和方法,我们可以为不同用户提供个性化的功能和体验,同时保证系统的稳定性和可维护性。在未来的开发中,我们可以继续优化和扩展这些功能,以满足不断变化的业务需求。
超级会员免费看
925

被折叠的 条评论
为什么被折叠?



