guide 0.0.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/LICENSE +22 -0
- data/Rakefile +18 -0
- data/app/assets/javascripts/guide/application.js +21723 -0
- data/app/assets/javascripts/guide/scenario.js +12642 -0
- data/app/assets/stylesheets/guide/application.css +141 -0
- data/app/controllers/guide/base_controller.rb +83 -0
- data/app/controllers/guide/nodes_controller.rb +40 -0
- data/app/controllers/guide/scenarios_controller.rb +44 -0
- data/app/helpers/guide/application_helper.rb +4 -0
- data/app/helpers/guide/document_helper.rb +20 -0
- data/app/models/guide/arborist.rb +44 -0
- data/app/models/guide/bouncer.rb +34 -0
- data/app/models/guide/cartographer.rb +49 -0
- data/app/models/guide/content.rb +28 -0
- data/app/models/guide/default_authentication_system.rb +13 -0
- data/app/models/guide/default_authorisation_system.rb +18 -0
- data/app/models/guide/diplomat.rb +60 -0
- data/app/models/guide/document.rb +20 -0
- data/app/models/guide/endpoint_stocktaker.rb +94 -0
- data/app/models/guide/errors.rb +2 -0
- data/app/models/guide/errors/base.rb +6 -0
- data/app/models/guide/errors/interface_violation.rb +2 -0
- data/app/models/guide/errors/invalid_node.rb +2 -0
- data/app/models/guide/errors/invalid_scenario.rb +2 -0
- data/app/models/guide/errors/invalid_visibility_option.rb +2 -0
- data/app/models/guide/errors/permission_denied.rb +2 -0
- data/app/models/guide/fixture.rb +15 -0
- data/app/models/guide/fixtures.rb +3 -0
- data/app/models/guide/form_object.rb +3 -0
- data/app/models/guide/monkey.rb +47 -0
- data/app/models/guide/nobilizer.rb +9 -0
- data/app/models/guide/node.rb +67 -0
- data/app/models/guide/photographer.rb +20 -0
- data/app/models/guide/scout.rb +59 -0
- data/app/models/guide/simulator.rb +26 -0
- data/app/models/guide/structure.rb +75 -0
- data/app/models/guide/view_model.rb +37 -0
- data/app/view_models/guide/layout_view.rb +81 -0
- data/app/view_models/guide/navigation_view.rb +25 -0
- data/app/view_models/guide/node_view.rb +59 -0
- data/app/view_models/guide/scenario_layout_view.rb +49 -0
- data/app/view_models/guide/scenario_view.rb +39 -0
- data/app/views/guide/_content.html.erb +17 -0
- data/app/views/guide/common/_category.html.erb +9 -0
- data/app/views/guide/common/_footer.html.erb +21 -0
- data/app/views/guide/common/_locale_switcher.html.erb +6 -0
- data/app/views/guide/common/_navigation.html.erb +12 -0
- data/app/views/guide/common/_navigation_node.html.erb +39 -0
- data/app/views/guide/common/_page_title.html.erb +8 -0
- data/app/views/guide/common/_search.html.erb +23 -0
- data/app/views/guide/common/_unsupported_browser_message.html.erb +6 -0
- data/app/views/guide/common/_visibility_banner.html.erb +25 -0
- data/app/views/guide/nodes/_document.html.erb +3 -0
- data/app/views/guide/nodes/_scenario.html.erb +14 -0
- data/app/views/guide/nodes/_scenario_list.html.erb +10 -0
- data/app/views/guide/nodes/_structure.html.erb +26 -0
- data/app/views/guide/nodes/_template_location.html.erb +7 -0
- data/app/views/guide/nodes/show.html.erb +7 -0
- data/app/views/guide/scenarios/_scenario.html.erb +25 -0
- data/app/views/guide/scenarios/scenario/_locale_switcher.html.erb +5 -0
- data/app/views/guide/scenarios/scenario/_toolbar.html.erb +39 -0
- data/app/views/guide/scenarios/scenario/_visibility.html.erb +10 -0
- data/app/views/guide/scenarios/show.html.erb +1 -0
- data/app/views/layouts/guide/application.html.erb +76 -0
- data/app/views/layouts/guide/scenario.html.erb +38 -0
- data/app/views/layouts/guide/scenario/default.html.erb +1 -0
- data/app/views/layouts/guide/scenario/default.text.erb +1 -0
- data/config/initializers/assets.rb +1 -0
- data/config/locales/models/guide/monkey.en.yml +6 -0
- data/config/locales/views/guide/nodes/_structure.en.yml +7 -0
- data/config/routes.rb +6 -0
- data/lib/guide.rb +12 -0
- data/lib/guide/authorisation_spec_helper.rb +83 -0
- data/lib/guide/configuration.rb +29 -0
- data/lib/guide/consistency_spec_helper.rb +33 -0
- data/lib/guide/engine.rb +9 -0
- data/lib/guide/version.rb +3 -0
- data/lib/tasks/guide_tasks.rake +4 -0
- metadata +207 -16
- data/.gitignore +0 -26
- data/Gemfile +0 -4
- data/guide.gemspec +0 -12
@@ -0,0 +1,28 @@
|
|
1
|
+
class Guide::Content < Guide::Document
|
2
|
+
# The content in the Living Guide for your application is organised
|
3
|
+
# into a tree structure. This class is the root node of that tree.
|
4
|
+
# To add child nodes, use the following DSL:
|
5
|
+
#
|
6
|
+
# contains :child_node_name
|
7
|
+
#
|
8
|
+
# This example declares that the tree contains a child node named:
|
9
|
+
#
|
10
|
+
# Guide::Content::ChildNodeName
|
11
|
+
#
|
12
|
+
# You will still need to create a class for it.
|
13
|
+
#
|
14
|
+
# It is highly recommended that all of your content lives in the
|
15
|
+
# Guide::Content namespace or Guide will have trouble finding it!
|
16
|
+
#
|
17
|
+
# To specify options such as visibility, append them to the declaration:
|
18
|
+
#
|
19
|
+
# contains :child_node_name, :visibility => :unpublished
|
20
|
+
#
|
21
|
+
# Feel free to redeclare this class in your system at the following path:
|
22
|
+
#
|
23
|
+
# app/<whatever_you_want>/guide/content.rb
|
24
|
+
#
|
25
|
+
# The convention for the subdirectory in app/ is "documentation",
|
26
|
+
# but if you don't like that, you can use something else (even "models"!).
|
27
|
+
# You probably shouldn't use any other standard rails directories though.
|
28
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Guide::DefaultAuthorisationSystem
|
2
|
+
def allow?(action)
|
3
|
+
true
|
4
|
+
end
|
5
|
+
|
6
|
+
def user_is_privileged?
|
7
|
+
allow?(:view_guide_unpublished) ||
|
8
|
+
allow?(:view_guide_restricted)
|
9
|
+
end
|
10
|
+
|
11
|
+
def valid_visibility_options
|
12
|
+
[
|
13
|
+
nil,
|
14
|
+
:unpublished,
|
15
|
+
:restricted,
|
16
|
+
]
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
class Guide::Diplomat
|
2
|
+
def initialize(session, params, default_locale)
|
3
|
+
@session = session
|
4
|
+
@params = params
|
5
|
+
@default_locale = default_locale
|
6
|
+
end
|
7
|
+
|
8
|
+
def negotiate_locale
|
9
|
+
store_new_locale_in_session if supported_locales.has_value? new_locale
|
10
|
+
|
11
|
+
best_locale
|
12
|
+
end
|
13
|
+
|
14
|
+
def supported_locales
|
15
|
+
Guide.configuration.supported_locales
|
16
|
+
end
|
17
|
+
|
18
|
+
def multiple_supported_locales?
|
19
|
+
supported_locales.keys.size > 1
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_locale
|
23
|
+
if supported_locales.has_value? locale_from_session
|
24
|
+
locale_from_session
|
25
|
+
else
|
26
|
+
clear_locale_from_session
|
27
|
+
@default_locale
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def best_locale
|
34
|
+
if supported_locales.has_value? temporary_locale
|
35
|
+
temporary_locale
|
36
|
+
else
|
37
|
+
current_locale
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def new_locale
|
42
|
+
@params[:locale]
|
43
|
+
end
|
44
|
+
|
45
|
+
def temporary_locale
|
46
|
+
@params[:temp_locale]
|
47
|
+
end
|
48
|
+
|
49
|
+
def locale_from_session
|
50
|
+
@session[:guide_locale]
|
51
|
+
end
|
52
|
+
|
53
|
+
def store_new_locale_in_session
|
54
|
+
@session[:guide_locale] = new_locale
|
55
|
+
end
|
56
|
+
|
57
|
+
def clear_locale_from_session
|
58
|
+
@session.delete(:guide_locale)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Guide::Document < Guide::Node
|
2
|
+
def template
|
3
|
+
partial || self.class.name.underscore
|
4
|
+
end
|
5
|
+
|
6
|
+
def partial
|
7
|
+
end
|
8
|
+
|
9
|
+
def stylesheets
|
10
|
+
Guide.configuration.default_stylesheets_for_documents
|
11
|
+
end
|
12
|
+
|
13
|
+
def javascripts
|
14
|
+
Guide.configuration.default_javascripts_for_documents
|
15
|
+
end
|
16
|
+
|
17
|
+
def can_be_rendered?
|
18
|
+
true
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
class Guide::EndpointStocktaker
|
2
|
+
def initialize(starting_node:)
|
3
|
+
@starting_node = starting_node
|
4
|
+
@result = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_hash
|
8
|
+
@result.tap do
|
9
|
+
add_guide_content if @result.empty?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def add_guide_content
|
16
|
+
renderable_nodes.keys.each do |node_path|
|
17
|
+
node = monkey.fetch_node(node_path)
|
18
|
+
if node.node_type == :structure
|
19
|
+
add_structure(node_path: node_path, structure: node)
|
20
|
+
else
|
21
|
+
add_document(node_path: node_path)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_structure(node_path:, structure:)
|
27
|
+
structure.formats.each do |format|
|
28
|
+
structure.scenarios.keys.each do |scenario_id|
|
29
|
+
@result[endpoint_key(node_path, scenario_id: scenario_id, scenario_format: format)] = {
|
30
|
+
"path" => url_helpers.scenario_path(
|
31
|
+
:scenario_id => scenario_id,
|
32
|
+
:scenario_format => format,
|
33
|
+
:node_path => node_path,
|
34
|
+
)
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_document(node_path:)
|
41
|
+
@result[endpoint_key(node_path)] = { "path" => url_helpers.node_path(node_path) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def endpoint_key(node_path, scenario_id: nil, scenario_format: nil)
|
45
|
+
[
|
46
|
+
formatted_node_path(node_path),
|
47
|
+
formatted_scenario_id(scenario_id),
|
48
|
+
scenario_format,
|
49
|
+
].compact.join('-')
|
50
|
+
end
|
51
|
+
|
52
|
+
def formatted_node_path(node_path)
|
53
|
+
node_path.gsub('/', '.')
|
54
|
+
end
|
55
|
+
|
56
|
+
def formatted_scenario_id(scenario_id)
|
57
|
+
scenario_id = scenario_id.to_s
|
58
|
+
|
59
|
+
case scenario_id.length
|
60
|
+
when 0
|
61
|
+
nil
|
62
|
+
when 1..50
|
63
|
+
scenario_id.to_s
|
64
|
+
else
|
65
|
+
"#{scenario_id.first(24)}..#{scenario_id.last(24)}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def bouncer
|
70
|
+
@bouncer ||= BribedBouncer.new
|
71
|
+
end
|
72
|
+
|
73
|
+
def cartographer
|
74
|
+
Guide::Cartographer.new(bouncer)
|
75
|
+
end
|
76
|
+
|
77
|
+
def monkey
|
78
|
+
@monkey ||= Guide::Monkey.new(@starting_node, bouncer)
|
79
|
+
end
|
80
|
+
|
81
|
+
def renderable_nodes
|
82
|
+
cartographer.draw_paths_to_visible_renderable_nodes(starting_node: @starting_node)
|
83
|
+
end
|
84
|
+
|
85
|
+
def url_helpers
|
86
|
+
Guide::Engine.routes.url_helpers
|
87
|
+
end
|
88
|
+
|
89
|
+
class BribedBouncer
|
90
|
+
def user_can_access?(_)
|
91
|
+
true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
class Guide::Errors::Base < StandardError
|
2
|
+
# This class exists so that we can tell the difference between an error
|
3
|
+
# that is native to Guide and an error that has occurred
|
4
|
+
# outside of our domain. All explictly thrown errors in Guide
|
5
|
+
# should inherit from this.
|
6
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Guide::Fixture
|
2
|
+
# Fixtures are used to generate content for your Living Guide
|
3
|
+
# that would otherwise be repetitive, such as view models that are used
|
4
|
+
# from multiple components. Declaring these in a single place is useful
|
5
|
+
# because it helps keep a consistent interface between the fake view models
|
6
|
+
# in the Living Guide and the real view models in your application.
|
7
|
+
#
|
8
|
+
# Feel free to override or redeclare this class if you want.
|
9
|
+
# Nothing in the Guide gem depends on it.
|
10
|
+
private
|
11
|
+
|
12
|
+
def self.image_path(image_name, extension)
|
13
|
+
Guide::Photographer.new(image_name, extension).image_path
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class Guide::Monkey
|
2
|
+
def initialize(starting_node, bouncer)
|
3
|
+
@starting_node = starting_node
|
4
|
+
@bouncer = bouncer
|
5
|
+
|
6
|
+
ensure_starting_node_exists
|
7
|
+
ensure_user_can_access(@starting_node)
|
8
|
+
end
|
9
|
+
|
10
|
+
def fetch_node(node_path)
|
11
|
+
current_node = @starting_node
|
12
|
+
|
13
|
+
node_ids_along_path(node_path).each do |node_id|
|
14
|
+
current_node = current_node.child_nodes[node_id]
|
15
|
+
ensure_node_exists(current_node, node_id)
|
16
|
+
ensure_user_can_access(current_node)
|
17
|
+
end
|
18
|
+
|
19
|
+
current_node
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def node_ids_along_path(node_path)
|
25
|
+
node_path.split("/").map {|node_id| node_id.to_sym }
|
26
|
+
end
|
27
|
+
|
28
|
+
def ensure_node_exists(node, node_id)
|
29
|
+
unless node.present?
|
30
|
+
raise Guide::Errors::InvalidNode,
|
31
|
+
I18n.t('.guide.monkey.invalid_node', :node_id => node_id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def ensure_starting_node_exists
|
36
|
+
unless @starting_node.present?
|
37
|
+
raise Guide::Errors::InvalidNode,
|
38
|
+
I18n.t('.guide.monkey.invalid_starting_node')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def ensure_user_can_access(node)
|
43
|
+
unless @bouncer.user_can_access?(node)
|
44
|
+
raise Guide::Errors::PermissionDenied
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class Guide::Node
|
2
|
+
class_attribute :child_nodes
|
3
|
+
|
4
|
+
def self.inherited(sub_class)
|
5
|
+
sub_class.child_nodes = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.contains(id, options = {})
|
9
|
+
child_nodes[id] = child_node_class(id).new(options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.child_node_class(id)
|
13
|
+
child_node_class_name = "#{self}::#{id.to_s.camelize}"
|
14
|
+
child_node_class_name.constantize
|
15
|
+
rescue NameError => name_error
|
16
|
+
raise Guide::Errors::InvalidNode,
|
17
|
+
"I can't build the tree that backs Guide because I could not load the class #{child_node_class_name}.",
|
18
|
+
name_error.backtrace
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :id, :options
|
22
|
+
|
23
|
+
def initialize(options = {})
|
24
|
+
@id = infer_id_from_class_name
|
25
|
+
@options = options
|
26
|
+
end
|
27
|
+
|
28
|
+
def name
|
29
|
+
@id.to_s.titleize
|
30
|
+
end
|
31
|
+
|
32
|
+
def leaf_node?
|
33
|
+
self.child_nodes.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def child_nodes
|
37
|
+
self.class.child_nodes
|
38
|
+
end
|
39
|
+
|
40
|
+
def ==(other)
|
41
|
+
self.class == other.class
|
42
|
+
end
|
43
|
+
|
44
|
+
def node_type
|
45
|
+
# for direct children, this will be :node
|
46
|
+
self.class.superclass.name.demodulize.underscore.to_sym
|
47
|
+
end
|
48
|
+
|
49
|
+
def can_be_rendered?
|
50
|
+
false
|
51
|
+
end
|
52
|
+
|
53
|
+
def view_model
|
54
|
+
Guide::ViewModel.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def image_path(image_name, extension = "png")
|
58
|
+
Guide::Photographer.new(image_name, extension).image_path
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def infer_id_from_class_name
|
64
|
+
# for example, Structures::Checkout::Page becomes :page
|
65
|
+
self.class.name.demodulize.underscore.to_sym
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Guide::Photographer
|
2
|
+
def initialize(image_name, extension = 'png')
|
3
|
+
@image_name = image_name
|
4
|
+
@extension = extension
|
5
|
+
end
|
6
|
+
|
7
|
+
def image_path
|
8
|
+
"#{asset_host}#{image_with_path_and_digest}"
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def image_with_path_and_digest
|
14
|
+
ActionController::Base.helpers.image_path "guide/#{@image_name}.#{@extension}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def asset_host
|
18
|
+
Rails.application.config.action_controller.asset_host || ""
|
19
|
+
end
|
20
|
+
end
|