diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index bd751b2d4e..0e9c6e55b1 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -84,6 +84,17 @@ def welcome_my_journey_into_teaching render_page("welcome/my-journey-into-teaching") end + def authenticate? + # restrict the /values page to any user who has a login + %w[values].include?(action_name) || super + end + + def values + # restrict the /values page to any user who has a login + render_forbidden if session[:user].blank? + render template: "pages/values", layout: "content" + end + private def adviser_sign_up_url diff --git a/app/helpers/content_helper.rb b/app/helpers/content_helper.rb index 3e44c811c1..dea14d134c 100644 --- a/app/helpers/content_helper.rb +++ b/app/helpers/content_helper.rb @@ -8,4 +8,9 @@ def article_classes(front_matter) def display_content_errors? Rails.application.config.x.display_content_errors end + + def value(key) + Value.data[key.to_s] + end + alias_method :v, :value end diff --git a/app/models/value.rb b/app/models/value.rb new file mode 100644 index 0000000000..6ecf81399b --- /dev/null +++ b/app/models/value.rb @@ -0,0 +1,36 @@ +class Value + PATH = "config/values/**/*.yml".freeze + attr_reader :data + + def self.data + # the value data will rarely change, so OK to cache in a class variable + @@data ||= new.data + end + + def initialize(path = nil) + @data = load_values(path || Rails.root.join(PATH)) + end + +private + + def load_values(dir_spec) + {}.tap do |data| + Dir[dir_spec].each do |filename| + data.merge!(flatten_hash(YAML.load_file(filename))) + end + end + end + + def flatten_hash(hash) + # based on https://stackoverflow.com/questions/23521230/flattening-nested-hash-to-a-single-hash-with-ruby-rails + hash.each_with_object({}) do |(k, v), h| + if v.is_a? Hash + flatten_hash(v).map do |h_k, h_v| + h["#{k}_#{h_k}"] = h_v + end + else + h[k] = v + end + end + end +end diff --git a/app/views/pages/values.md b/app/views/pages/values.md new file mode 100644 index 0000000000..c2e39bb987 --- /dev/null +++ b/app/views/pages/values.md @@ -0,0 +1,6 @@ +--- +title: Values +noindex: true +content: + - pages/values/table +--- diff --git a/app/views/pages/values/_table.html.erb b/app/views/pages/values/_table.html.erb new file mode 100644 index 0000000000..ecb3595cb2 --- /dev/null +++ b/app/views/pages/values/_table.html.erb @@ -0,0 +1,26 @@ + + + + + + + + + <% Value.data.each do |key, value| %> + + + + + <% end %> + +
+ Key + + Value +
+ + $<%= key %>$ + + + <%= value %> +
diff --git a/config/routes.rb b/config/routes.rb index 92896a2e8b..4db24e5e50 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,6 +74,7 @@ get "/privacy-policy", to: "pages#privacy_policy", as: :privacy_policy get "/cookies", to: "pages#cookies", as: :cookies get "/session-expired", to: "pages#session_expired", as: :session_expired + get "/values", to: "pages#values", as: :values get "/welcome", to: "pages#welcome", as: :welcome_guide get "/welcome/my-journey-into-teaching", to: "pages#welcome_my_journey_into_teaching", as: :welcome_my_journey_into_teaching diff --git a/config/values/dates.yml b/config/values/dates.yml new file mode 100644 index 0000000000..0bd99f71ef --- /dev/null +++ b/config/values/dates.yml @@ -0,0 +1,5 @@ +dates: + example: + opening: 1st September 2024 + +dates_example_closing: 31/12/2024 diff --git a/config/values/salaries.yml b/config/values/salaries.yml new file mode 100644 index 0000000000..95b21789e5 --- /dev/null +++ b/config/values/salaries.yml @@ -0,0 +1,5 @@ +salaries: + example: + min: £30,000 + +salaries_example_max: £41k diff --git a/docs/content.md b/docs/content.md index cab009eeae..ee5867d3bc 100644 --- a/docs/content.md +++ b/docs/content.md @@ -21,6 +21,7 @@ This documentation aims to be a reference for content editors that want to make * [Inset text](#inset-text) * [YouTube Video](#youtube-video) * [Hero](#hero) + * [Values](#values) 4. [Creating a Blog Post](#creating-a-blog-post) * [Images](#images) * [Footers](#footers) @@ -287,6 +288,41 @@ hero_bg_color: white hero_blend_content: true ``` +### Values + +You can use the Values system to maintain key values (e.g. salaries, dates, fees etc) in a single file, and then use these values throughout the site's content. Set up a list of values in one or more YML files stored in the `config/values/` folder (or sub-folder), for example `config/values/dates.yml`: + +```yaml +dates: + example: + opening: 1st September 2024 + +dates_example_closing: 31/12/2024 +``` + +These values can then be used in markdown files by referencing the value as `$value_name$`, e.g. `$dates_example_opening$` or `$dates_example_closing$`. Note that structured composite keys will be flattened to a single key. + +An example markdown implementation might be: + +```markdown +# Useful dates + +The closing date for applications is $dates_example_closing$. It is important to submit your application in good time. +``` + +Values can also be used in ERB templates using `<%= value :value_name %>` (or as a shorthand, `<%= v :value_name %>`). + +An example ERB implementation might be: + +```html +

Useful dates

+

+ The opening date for applications is <%= v :dates_example_opening %>. +

+``` + +A list of the current values available on the site can be viewed at the `/values` endpoint. + ## Creating a Blog Post Blog posts should be written in Markdown format using the following template as a guide: @@ -574,4 +610,4 @@ You can find everywhere a page is linked to by: ## Resolving comments People can add comments to pull requests in Github which makes it easier to see feedback and keep track of changes. You can also tag people and reply to their comments. -Once you've addressed a comment click on resolve. This hides it from the conversation making it easier to keep track of what's been updated. \ No newline at end of file +Once you've addressed a comment click on resolve. This hides it from the conversation making it easier to keep track of what's been updated. diff --git a/lib/template_handlers/markdown.rb b/lib/template_handlers/markdown.rb index 887c57397d..cfbbcb4297 100644 --- a/lib/template_handlers/markdown.rb +++ b/lib/template_handlers/markdown.rb @@ -57,7 +57,7 @@ def markdown # entire placeholder to the arg (including dollar symbols) but we only # want what's inside the capture group parsed.content.gsub(COMPONENT_PLACEHOLDER_REGEX) do - safe_join([cta_component($1), content_component($1), image($1)].compact).strip + safe_join([cta_component($1), content_component($1), image($1), value($1)].compact).strip end end # rubocop:enable Style/PerlBackrefs @@ -95,6 +95,10 @@ def image(placeholder) ApplicationController.render(component, layout: false) end + def value(placeholder) + Value.data[placeholder] + end + def front_matter @front_matter ||= self.class.global_front_matter.deep_merge(parsed.front_matter) end diff --git a/spec/fixtures/files/example_values/data1.yml b/spec/fixtures/files/example_values/data1.yml new file mode 100644 index 0000000000..a1cfb4f44e --- /dev/null +++ b/spec/fixtures/files/example_values/data1.yml @@ -0,0 +1,5 @@ +data1: + example: + date: 01/02/2003 + +data1_example_amount: £1,234.56 diff --git a/spec/fixtures/files/example_values/data2.yml b/spec/fixtures/files/example_values/data2.yml new file mode 100644 index 0000000000..c94094ffdc --- /dev/null +++ b/spec/fixtures/files/example_values/data2.yml @@ -0,0 +1,5 @@ +data2: + example: + string: Hello World! + +data2_example_number: 0.01 diff --git a/spec/lib/template_handlers/markdown_spec.rb b/spec/lib/template_handlers/markdown_spec.rb index 5a08f25bbc..b2a76ae123 100644 --- a/spec/lib/template_handlers/markdown_spec.rb +++ b/spec/lib/template_handlers/markdown_spec.rb @@ -262,4 +262,38 @@ is_expected.to have_css("ol.steps") end end + + describe "injecting values" do + let(:front_matter) { { "title": "Page with view components" } } + + let :markdown do + <<~MARKDOWN + # Some page + + data1_example_amount: $data1_example_amount$ + + Some text + + data2_example_string: $data2_example_string$ + MARKDOWN + end + + let(:value_data) { Value.new("spec/fixtures/files/example_values/**/*.yml").data } + + before do + allow(described_class).to receive(:global_front_matter).and_return(front_matter) + allow(Value).to receive(:data).and_return(value_data) + stub_template "page_with_rich_content.md" => markdown + render template: "page_with_rich_content" + end + + subject { rendered } + + it "contains the specified view components" do + is_expected.to have_text("Some page") + is_expected.to have_text("data1_example_amount: £1,234.56") + is_expected.to have_text("Some text") + is_expected.to have_text("data2_example_string: Hello World!") + end + end end diff --git a/spec/models/value_spec.rb b/spec/models/value_spec.rb new file mode 100644 index 0000000000..3a98ce30bf --- /dev/null +++ b/spec/models/value_spec.rb @@ -0,0 +1,36 @@ +require "rails_helper" + +describe Value do + describe "##data (class method)" do + subject { described_class.data } + + it { is_expected.to be_a Hash } + end + + describe "#data" do + subject { instance.data } + + let(:instance) { described_class.new(path) } + + context "with default path" do + let(:path) { nil } + + it { is_expected.to be_a Hash } + end + + context "with specific file path" do + let(:path) { "spec/fixtures/files/example_values/**/*.yml" } + + it { + is_expected.to eql( + { + "data1_example_amount" => "£1,234.56", + "data1_example_date" => "01/02/2003", + "data2_example_number" => 0.01, + "data2_example_string" => "Hello World!", + }, + ) + } + end + end +end