diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index bbd3201..03c3bc2 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -5,7 +5,7 @@ class TemplatesController < ApplicationController #respond_to :html after_action :verify_authorized - + helper TemplateHelper # GET /org/admin/templates/:id/admin_index # ----------------------------------------------------- def admin_index @@ -273,6 +273,8 @@ end @template.description = params["template-desc"] + @template.links = JSON.parse(params["template-links"]) if params["template-links"].present? + if @template.update_attributes(params[:template]) flash[:notice] = success_message(_('template'), _('saved')) diff --git a/app/helpers/template_helper.rb b/app/helpers/template_helper.rb new file mode 100644 index 0000000..85be0d4 --- /dev/null +++ b/app/helpers/template_helper.rb @@ -0,0 +1,8 @@ +module TemplateHelper + def links_to_a_elements(links) + a = links.map do |l| + "#{l['text']}" + end + a.join(", ") + end +end \ No newline at end of file diff --git a/app/models/concerns/json_link_validator.rb b/app/models/concerns/json_link_validator.rb new file mode 100644 index 0000000..8315a7d --- /dev/null +++ b/app/models/concerns/json_link_validator.rb @@ -0,0 +1,32 @@ +module JSONLinkValidator + extend ActiveSupport::Concern + + included do + # Parses a stringified JSON according to validate_links (e.g. [{ link: String, text: String}, ...]) + # param {String} the stringified JSON value + # Returns an Array of hashes after decoding/validating the stringified JSON passed, otherwise nil + def parse_links(value) + return nil unless value.is_a?(String) + begin + parsed_value = JSON.parse(value) + return valid_links?(parsed_value) ? parsed_value : nil + rescue JSON::ParserError + nil + end + end + # Validates whether or not the value passed is conforming to [{ link: String, text: String}, ...] + def valid_links?(value) + if value.is_a?(Array) + r = value.all? do |o| + o.is_a?(Hash) && + o.has_key?('link') && + o.has_key?('text') && + o['link'].is_a?(String) && + o['text'].is_a?(String) + end + return r + end + false + end + end +end \ No newline at end of file diff --git a/app/models/template.rb b/app/models/template.rb index bb69d48..c191858 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -1,7 +1,13 @@ class Template < ActiveRecord::Base include GlobalHelpers + include ActiveModel::Validations + validates_with TemplateLinksValidator before_validation :set_creation_defaults + + # Stores links as an JSON object: { funder: [{"link":"www.example.com","text":"foo"}, ...], sample_plan: [{"link":"www.example.com","text":"foo"}, ...]} + # The links is validated against custom validator allocated at validators/template_links_validator.rb + serialize :links, JSON ## # Associations diff --git a/app/validators/template_links_validator.rb b/app/validators/template_links_validator.rb new file mode 100644 index 0000000..61ef486 --- /dev/null +++ b/app/validators/template_links_validator.rb @@ -0,0 +1,18 @@ +class TemplateLinksValidator < ActiveModel::Validator + include JSONLinkValidator + def validate(record) + links = record.links + expected_keys = ['funder', 'sample_plan'] + if links.is_a?(Hash) + expected_keys.each do |k| + if !links.has_key?(k) + record.errors[:links] << _('A key %{key} is expected for links hash') %{ :key => k } + else + record.errors[:links] << _('The key %{key} does not have a valid set of object links') %{ :key => k } unless valid_links?(links[k]) + end + end + else + record.errors[:links] << _('A hash is expected for links') + end + end +end \ No newline at end of file diff --git a/app/views/shared/_links.html.erb b/app/views/shared/_links.html.erb new file mode 100644 index 0000000..c7d137b --- /dev/null +++ b/app/views/shared/_links.html.erb @@ -0,0 +1,46 @@ +<%# locals: { context, title, links, max_number_links, tooltip } %> + \ No newline at end of file diff --git a/app/views/templates/_edit_template.html.erb b/app/views/templates/_edit_template.html.erb index 7b18016..dca8915 100644 --- a/app/views/templates/_edit_template.html.erb +++ b/app/views/templates/_edit_template.html.erb @@ -39,6 +39,28 @@

+ <% if template.org.funder? %> +
+ <%= render(partial: '/shared/links', + locals: { + context: 'funder', + title: _('Funder Links'), + links: template.links['funder'], + max_number_links: MAX_NUMBER_LINKS_FUNDER, + tooltip: _('Add links to funder websites that provide additional information about the requirements for this template') }) %> +
+
+ <%= render(partial: '/shared/links', + locals: { + context: 'sample_plan', + title: _('Sample Plan Links'), + links: template.links['sample_plan'], + max_number_links: MAX_NUMBER_LINKS_SAMPLE_PLAN, + tooltip: _('Add links to sample plans if provided by the funder.') }) %> +
+ <%= hidden_field_tag('template-links', value: template.links) %> + <% end %> +
<%= f.button(_('Save'), class: 'btn btn-default', type: "submit") %> <%= link_to(_('Cancel'), '#', { class: 'btn btn-default template_show_link', role: "button" }) %> diff --git a/app/views/templates/admin_index.html.erb b/app/views/templates/admin_index.html.erb index 92b1fde..41c72a5 100644 --- a/app/views/templates/admin_index.html.erb +++ b/app/views/templates/admin_index.html.erb @@ -84,7 +84,7 @@ <%= _('Create a template') %> <%= render partial: '/templates/list_template', locals: { title: n_('Default template', 'Default templates', @organisation_templates.length), templates: @organisation_templates } %> - + <% if !current_user.org.funder? %> <%= render partial: '/templates/list_template', locals: { title: _('Funders templates'), templates: @funder_templates } %> <% end %> diff --git a/config/initializers/constants.rb b/config/initializers/constants.rb index c3f0124..51d0993 100644 --- a/config/initializers/constants.rb +++ b/config/initializers/constants.rb @@ -1,3 +1,5 @@ LANGUAGES = (ActiveRecord::Base.connection.table_exists? 'languages') ? Language.sorted_by_abbreviation : [] MANY_LANGUAGES = LANGUAGES.length > 1 TABLE_FILTER_MIN_ROWS = 10 +MAX_NUMBER_LINKS_FUNDER = 5 +MAX_NUMBER_LINKS_SAMPLE_PLAN = 5 diff --git a/db/migrate/20171124133802_add_links_to_templates.rb b/db/migrate/20171124133802_add_links_to_templates.rb new file mode 100644 index 0000000..1ff9799 --- /dev/null +++ b/db/migrate/20171124133802_add_links_to_templates.rb @@ -0,0 +1,5 @@ +class AddLinksToTemplates < ActiveRecord::Migration + def change + add_column :templates, :links, :string, default: '{"funder":[], "sample_plan":[]}' + end +end diff --git a/db/schema.rb b/db/schema.rb index fa4df11..d7b031d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,75 +11,72 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171122195828) do +ActiveRecord::Schema.define(version: 20171124133802) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" create_table "annotations", force: :cascade do |t| - t.integer "question_id", limit: 4 - t.integer "org_id", limit: 4 - t.text "text", limit: 65535 - t.integer "type", limit: 4, default: 0, null: false + t.integer "question_id" + t.integer "org_id" + t.text "text" + t.integer "type", default: 0, null: false t.datetime "created_at" t.datetime "updated_at" end - add_index "annotations", ["org_id"], name: "fk_rails_aca7521f72", using: :btree add_index "annotations", ["question_id"], name: "index_annotations_on_question_id", using: :btree create_table "answers", force: :cascade do |t| - t.text "text", limit: 65535 - t.integer "plan_id", limit: 4 - t.integer "user_id", limit: 4 - t.integer "question_id", limit: 4 + t.text "text" + t.integer "plan_id" + t.integer "user_id" + t.integer "question_id" t.datetime "created_at" t.datetime "updated_at" - t.integer "lock_version", limit: 4, default: 0 + t.integer "lock_version", default: 0 end - add_index "answers", ["plan_id"], name: "fk_rails_84a6005a3e", using: :btree - add_index "answers", ["question_id"], name: "fk_rails_3d5ed4418f", using: :btree - add_index "answers", ["user_id"], name: "fk_rails_584be190c2", using: :btree - create_table "answers_question_options", id: false, force: :cascade do |t| - t.integer "answer_id", limit: 4, null: false - t.integer "question_option_id", limit: 4, null: false + t.integer "answer_id", null: false + t.integer "question_option_id", null: false end add_index "answers_question_options", ["answer_id"], name: "index_answers_question_options_on_answer_id", using: :btree - add_index "answers_question_options", ["question_option_id"], name: "fk_rails_01ba00b569", using: :btree create_table "exported_plans", force: :cascade do |t| - t.integer "plan_id", limit: 4 - t.integer "user_id", limit: 4 - t.string "format", limit: 255 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "phase_id", limit: 4 + t.integer "plan_id" + t.integer "user_id" + t.string "format" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "phase_id" end create_table "file_types", force: :cascade do |t| - t.string "name", limit: 255 - t.string "icon_name", limit: 255 - t.integer "icon_size", limit: 4 - t.string "icon_location", limit: 255 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "name" + t.string "icon_name" + t.integer "icon_size" + t.string "icon_location" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "file_uploads", force: :cascade do |t| - t.string "name", limit: 255 - t.string "title", limit: 255 - t.text "description", limit: 65535 - t.integer "size", limit: 4 + t.string "name" + t.string "title" + t.text "description" + t.integer "size" t.boolean "published" - t.string "location", limit: 255 - t.integer "file_type_id", limit: 4 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "location" + t.integer "file_type_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "friendly_id_slugs", force: :cascade do |t| - t.string "slug", limit: 255, null: false - t.integer "sluggable_id", limit: 4, null: false + t.string "slug", null: false + t.integer "sluggable_id", null: false t.string "sluggable_type", limit: 40 t.datetime "created_at" end @@ -89,10 +86,10 @@ add_index "friendly_id_slugs", ["sluggable_type"], name: "index_friendly_id_slugs_on_sluggable_type", using: :btree create_table "guidance_groups", force: :cascade do |t| - t.string "name", limit: 255 - t.integer "org_id", limit: 4 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "name" + t.integer "org_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.boolean "optional_subset" t.boolean "published" end @@ -100,167 +97,156 @@ add_index "guidance_groups", ["org_id"], name: "index_guidance_groups_on_org_id", using: :btree create_table "guidances", force: :cascade do |t| - t.text "text", limit: 65535 - t.integer "guidance_group_id", limit: 4 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "question_id", limit: 4 + t.text "text" + t.integer "guidance_group_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "question_id" t.boolean "published" end add_index "guidances", ["guidance_group_id"], name: "index_guidances_on_guidance_group_id", using: :btree create_table "identifier_schemes", force: :cascade do |t| - t.string "name", limit: 255 - t.string "description", limit: 255 + t.string "name" + t.string "description" t.boolean "active" t.datetime "created_at" t.datetime "updated_at" - t.string "logo_url", limit: 255 - t.string "user_landing_url", limit: 255 + t.string "logo_url" + t.string "user_landing_url" end create_table "languages", force: :cascade do |t| - t.string "abbreviation", limit: 255 - t.string "description", limit: 255 - t.string "name", limit: 255 + t.string "abbreviation" + t.string "description" + t.string "name" t.boolean "default_language" end create_table "notes", force: :cascade do |t| - t.integer "user_id", limit: 4 - t.text "text", limit: 65535 + t.integer "user_id" + t.text "text" t.boolean "archived" - t.integer "answer_id", limit: 4 - t.integer "archived_by", limit: 4 + t.integer "answer_id" + t.integer "archived_by" t.datetime "created_at" t.datetime "updated_at" end add_index "notes", ["answer_id"], name: "index_notes_on_answer_id", using: :btree - add_index "notes", ["user_id"], name: "fk_rails_7f2323ad43", using: :btree create_table "org_identifiers", force: :cascade do |t| - t.string "identifier", limit: 255 - t.string "attrs", limit: 255 + t.string "identifier" + t.string "attrs" t.datetime "created_at" t.datetime "updated_at" - t.integer "org_id", limit: 4 - t.integer "identifier_scheme_id", limit: 4 + t.integer "org_id" + t.integer "identifier_scheme_id" end - add_index "org_identifiers", ["identifier_scheme_id"], name: "fk_rails_189ad2e573", using: :btree - add_index "org_identifiers", ["org_id"], name: "fk_rails_36323c0674", using: :btree - create_table "org_token_permissions", force: :cascade do |t| - t.integer "org_id", limit: 4 - t.integer "token_permission_type_id", limit: 4 + t.integer "org_id" + t.integer "token_permission_type_id" t.datetime "created_at" t.datetime "updated_at" end add_index "org_token_permissions", ["org_id"], name: "index_org_token_permissions_on_org_id", using: :btree - add_index "org_token_permissions", ["token_permission_type_id"], name: "fk_rails_2aa265f538", using: :btree create_table "orgs", force: :cascade do |t| - t.string "name", limit: 255 - t.string "abbreviation", limit: 255 - t.string "target_url", limit: 255 - t.string "wayfless_entity", limit: 255 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "parent_id", limit: 4 + t.string "name" + t.string "abbreviation" + t.string "target_url" + t.string "wayfless_entity" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "parent_id" t.boolean "is_other" - t.string "sort_name", limit: 255 - t.text "banner_text", limit: 65535 - t.string "logo_file_name", limit: 255 - t.integer "region_id", limit: 4 - t.integer "language_id", limit: 4 - t.string "logo_uid", limit: 255 - t.string "logo_name", limit: 255 - t.string "contact_email", limit: 255 - t.integer "org_type", limit: 4, default: 0, null: false - t.string "links", limit: 255, default: "[]" - t.string "contact_name", limit: 255 - t.boolean "feedback_enabled", default: false - t.string "feedback_email_subject", limit: 255 - t.text "feedback_email_msg", limit: 65535 + t.string "sort_name" + t.text "banner_text" + t.string "logo_file_name" + t.integer "region_id" + t.integer "language_id" + t.string "logo_uid" + t.string "logo_name" + t.string "contact_email" + t.integer "org_type", default: 0, null: false + t.string "links", default: "[]" + t.string "contact_name" + t.boolean "feedback_enabled", default: false + t.string "feedback_email_subject" + t.text "feedback_email_msg" end - add_index "orgs", ["language_id"], name: "fk_rails_5640112cab", using: :btree - add_index "orgs", ["region_id"], name: "fk_rails_5a6adf6bab", using: :btree - create_table "perms", force: :cascade do |t| - t.string "name", limit: 255 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end add_index "perms", ["name"], name: "index_perms_on_name", using: :btree add_index "perms", ["name"], name: "index_roles_on_name_and_resource_type_and_resource_id", using: :btree create_table "phases", force: :cascade do |t| - t.string "title", limit: 255 - t.text "description", limit: 65535 - t.integer "number", limit: 4 - t.integer "template_id", limit: 4 + t.string "title" + t.text "description" + t.integer "number" + t.integer "template_id" t.datetime "created_at" t.datetime "updated_at" - t.string "slug", limit: 255 + t.string "slug" t.boolean "modifiable" end add_index "phases", ["template_id"], name: "index_phases_on_template_id", using: :btree create_table "plans", force: :cascade do |t| - t.string "title", limit: 255 - t.integer "template_id", limit: 4 + t.string "title" + t.integer "template_id" t.datetime "created_at" t.datetime "updated_at" - t.string "slug", limit: 255 - t.string "grant_number", limit: 255 - t.string "identifier", limit: 255 - t.text "description", limit: 65535 - t.string "principal_investigator", limit: 255 - t.string "principal_investigator_identifier", limit: 255 - t.string "data_contact", limit: 255 - t.string "funder_name", limit: 255 - t.integer "visibility", limit: 4, null: false - t.string "data_contact_email", limit: 255 - t.string "data_contact_phone", limit: 255 - t.string "principal_investigator_email", limit: 255 - t.string "principal_investigator_phone", limit: 255 - t.boolean "feedback_requested", default: false + t.string "slug" + t.string "grant_number" + t.string "identifier" + t.text "description" + t.string "principal_investigator" + t.string "principal_investigator_identifier" + t.string "data_contact" + t.string "funder_name" + t.integer "visibility", null: false + t.string "data_contact_email" + t.string "data_contact_phone" + t.string "principal_investigator_email" + t.string "principal_investigator_phone" + t.boolean "feedback_requested", default: false end add_index "plans", ["template_id"], name: "index_plans_on_template_id", using: :btree create_table "plans_guidance_groups", force: :cascade do |t| - t.integer "guidance_group_id", limit: 4 - t.integer "plan_id", limit: 4 + t.integer "guidance_group_id" + t.integer "plan_id" end - add_index "plans_guidance_groups", ["guidance_group_id"], name: "fk_rails_ec1c5524d7", using: :btree - add_index "plans_guidance_groups", ["plan_id"], name: "fk_rails_13d0671430", using: :btree - create_table "prefs", force: :cascade do |t| - t.string "settings", limit: 255 - t.integer "user_id", limit: 4 + t.text "settings" + t.integer "user_id" end create_table "question_formats", force: :cascade do |t| - t.string "title", limit: 255 - t.text "description", limit: 65535 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "option_based", default: false - t.integer "formattype", limit: 4, default: 0 + t.string "title" + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "option_based", default: false + t.integer "formattype", default: 0 end create_table "question_options", force: :cascade do |t| - t.integer "question_id", limit: 4 - t.string "text", limit: 255 - t.integer "number", limit: 4 + t.integer "question_id" + t.string "text" + t.integer "number" t.boolean "is_default" t.datetime "created_at" t.datetime "updated_at" @@ -269,213 +255,209 @@ add_index "question_options", ["question_id"], name: "index_question_options_on_question_id", using: :btree create_table "questions", force: :cascade do |t| - t.text "text", limit: 65535 - t.text "default_value", limit: 65535 - t.integer "number", limit: 4 - t.integer "section_id", limit: 4 + t.text "text" + t.text "default_value" + t.integer "number" + t.integer "section_id" t.datetime "created_at" t.datetime "updated_at" - t.integer "question_format_id", limit: 4 - t.boolean "option_comment_display", default: true + t.integer "question_format_id" + t.boolean "option_comment_display", default: true t.boolean "modifiable" end - add_index "questions", ["question_format_id"], name: "fk_rails_4fbc38c8c7", using: :btree add_index "questions", ["section_id"], name: "index_questions_on_section_id", using: :btree create_table "questions_themes", id: false, force: :cascade do |t| - t.integer "question_id", limit: 4, null: false - t.integer "theme_id", limit: 4, null: false + t.integer "question_id", null: false + t.integer "theme_id", null: false end add_index "questions_themes", ["question_id"], name: "index_questions_themes_on_question_id", using: :btree - add_index "questions_themes", ["theme_id"], name: "fk_rails_0489d5eeba", using: :btree create_table "regions", force: :cascade do |t| - t.string "abbreviation", limit: 255 - t.string "description", limit: 255 - t.string "name", limit: 255 - t.integer "super_region_id", limit: 4 + t.string "abbreviation" + t.string "description" + t.string "name" + t.integer "super_region_id" end create_table "roles", force: :cascade do |t| - t.integer "user_id", limit: 4 - t.integer "plan_id", limit: 4 + t.integer "user_id" + t.integer "plan_id" t.datetime "created_at" t.datetime "updated_at" - t.integer "access", limit: 4, default: 0, null: false - t.boolean "active", default: true + t.integer "access", default: 0, null: false + t.boolean "active", default: true end add_index "roles", ["plan_id"], name: "index_roles_on_plan_id", using: :btree add_index "roles", ["user_id"], name: "index_roles_on_user_id", using: :btree create_table "sections", force: :cascade do |t| - t.string "title", limit: 255 - t.text "description", limit: 65535 - t.integer "number", limit: 4 + t.string "title" + t.text "description" + t.integer "number" t.datetime "created_at" t.datetime "updated_at" t.boolean "published" - t.integer "phase_id", limit: 4 + t.integer "phase_id" t.boolean "modifiable" end add_index "sections", ["phase_id"], name: "index_sections_on_phase_id", using: :btree create_table "settings", force: :cascade do |t| - t.string "var", limit: 255, null: false - t.text "value", limit: 65535 - t.integer "target_id", limit: 4, null: false - t.string "target_type", limit: 255, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "var", null: false + t.text "value" + t.integer "target_id", null: false + t.string "target_type", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end add_index "settings", ["target_type", "target_id", "var"], name: "index_settings_on_target_type_and_target_id_and_var", unique: true, using: :btree create_table "splash_logs", force: :cascade do |t| - t.string "destination", limit: 255 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "destination" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "templates", force: :cascade do |t| - t.string "title", limit: 255 - t.text "description", limit: 65535 + t.string "title" + t.text "description" t.boolean "published" - t.integer "org_id", limit: 4 - t.string "locale", limit: 255 + t.integer "org_id" + t.string "locale" t.boolean "is_default" t.datetime "created_at" t.datetime "updated_at" - t.integer "version", limit: 4 - t.integer "visibility", limit: 4 - t.integer "customization_of", limit: 4 - t.integer "dmptemplate_id", limit: 4 + t.integer "version" + t.integer "visibility" + t.integer "customization_of" + t.integer "dmptemplate_id" t.boolean "migrated" - t.boolean "dirty", default: false + t.boolean "dirty", default: false + t.string "links", default: "{\"funder\":[], \"sample_plan\":[]}" end add_index "templates", ["org_id", "dmptemplate_id"], name: "template_organisation_dmptemplate_index", using: :btree add_index "templates", ["org_id"], name: "index_templates_on_org_id", using: :btree create_table "themes", force: :cascade do |t| - t.string "title", limit: 255 - t.text "description", limit: 65535 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "locale", limit: 255 + t.string "title" + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "locale" end create_table "themes_in_guidance", id: false, force: :cascade do |t| - t.integer "theme_id", limit: 4 - t.integer "guidance_id", limit: 4 + t.integer "theme_id" + t.integer "guidance_id" end add_index "themes_in_guidance", ["guidance_id"], name: "index_themes_in_guidance_on_guidance_id", using: :btree add_index "themes_in_guidance", ["theme_id"], name: "index_themes_in_guidance_on_theme_id", using: :btree create_table "token_permission_types", force: :cascade do |t| - t.string "token_type", limit: 255 - t.text "text_description", limit: 65535 + t.string "token_type" + t.text "text_description" t.datetime "created_at" t.datetime "updated_at" end create_table "user_identifiers", force: :cascade do |t| - t.string "identifier", limit: 255 + t.string "identifier" t.datetime "created_at" t.datetime "updated_at" - t.integer "user_id", limit: 4 - t.integer "identifier_scheme_id", limit: 4 + t.integer "user_id" + t.integer "identifier_scheme_id" end - add_index "user_identifiers", ["identifier_scheme_id"], name: "fk_rails_fe95df7db0", using: :btree add_index "user_identifiers", ["user_id"], name: "index_user_identifiers_on_user_id", using: :btree create_table "users", force: :cascade do |t| - t.string "firstname", limit: 255 - t.string "surname", limit: 255 - t.string "email", limit: 255, default: "", null: false - t.string "orcid_id", limit: 255 - t.string "shibboleth_id", limit: 255 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "encrypted_password", limit: 255, default: "" - t.string "reset_password_token", limit: 255 + t.string "firstname" + t.string "surname" + t.string "email", default: "", null: false + t.string "orcid_id" + t.string "shibboleth_id" + t.datetime "created_at" + t.datetime "updated_at" + t.string "encrypted_password", default: "" + t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", limit: 4, default: 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" - t.string "current_sign_in_ip", limit: 255 - t.string "last_sign_in_ip", limit: 255 - t.string "confirmation_token", limit: 255 + t.string "current_sign_in_ip" + t.string "last_sign_in_ip" + t.string "confirmation_token" t.datetime "confirmed_at" t.datetime "confirmation_sent_at" - t.string "invitation_token", limit: 255 + t.string "invitation_token" t.datetime "invitation_created_at" t.datetime "invitation_sent_at" t.datetime "invitation_accepted_at" - t.string "other_organisation", limit: 255 + t.string "other_organisation" t.boolean "accept_terms" - t.integer "org_id", limit: 4 - t.string "api_token", limit: 255 - t.integer "invited_by_id", limit: 4 - t.string "invited_by_type", limit: 255 - t.integer "language_id", limit: 4 - t.string "recovery_email", limit: 255 + t.integer "org_id" + t.string "api_token" + t.integer "invited_by_id" + t.string "invited_by_type" + t.integer "language_id" + t.string "recovery_email" end add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree - add_index "users", ["language_id"], name: "fk_rails_45f4f12508", using: :btree add_index "users", ["org_id"], name: "index_users_on_org_id", using: :btree create_table "users_perms", id: false, force: :cascade do |t| - t.integer "user_id", limit: 4 - t.integer "perm_id", limit: 4 + t.integer "user_id" + t.integer "perm_id" end - add_index "users_perms", ["perm_id"], name: "fk_rails_457217c31c", using: :btree add_index "users_perms", ["user_id"], name: "index_users_perms_on_user_id", using: :btree - add_foreign_key "annotations", "orgs" - add_foreign_key "annotations", "questions" - add_foreign_key "answers", "plans" - add_foreign_key "answers", "questions" - add_foreign_key "answers", "users" - add_foreign_key "answers_question_options", "answers" - add_foreign_key "answers_question_options", "question_options" - add_foreign_key "guidance_groups", "orgs" - add_foreign_key "guidances", "guidance_groups" - add_foreign_key "notes", "answers" - add_foreign_key "notes", "users" - add_foreign_key "org_identifiers", "identifier_schemes" - add_foreign_key "org_identifiers", "orgs" - add_foreign_key "org_token_permissions", "orgs" - add_foreign_key "org_token_permissions", "token_permission_types" - add_foreign_key "orgs", "languages" - add_foreign_key "orgs", "regions" - add_foreign_key "phases", "templates" - add_foreign_key "plans", "templates" - add_foreign_key "plans_guidance_groups", "guidance_groups" - add_foreign_key "plans_guidance_groups", "plans" - add_foreign_key "question_options", "questions" - add_foreign_key "questions", "question_formats" - add_foreign_key "questions", "sections" - add_foreign_key "questions_themes", "questions" - add_foreign_key "questions_themes", "themes" - add_foreign_key "roles", "plans" - add_foreign_key "roles", "users" - add_foreign_key "sections", "phases" - add_foreign_key "templates", "orgs" - add_foreign_key "themes_in_guidance", "guidances" - add_foreign_key "themes_in_guidance", "themes" - add_foreign_key "user_identifiers", "identifier_schemes" - add_foreign_key "user_identifiers", "users" - add_foreign_key "users", "languages" - add_foreign_key "users", "orgs" - add_foreign_key "users_perms", "perms" + add_foreign_key "annotations", "orgs" + add_foreign_key "annotations", "questions" + add_foreign_key "answers", "plans" + add_foreign_key "answers", "questions" + add_foreign_key "answers", "users" + add_foreign_key "answers_question_options", "answers" + add_foreign_key "answers_question_options", "question_options" + add_foreign_key "guidance_groups", "orgs" + add_foreign_key "guidances", "guidance_groups" + add_foreign_key "notes", "answers" + add_foreign_key "notes", "users" + add_foreign_key "org_identifiers", "identifier_schemes" + add_foreign_key "org_identifiers", "orgs" + add_foreign_key "org_token_permissions", "orgs" + add_foreign_key "org_token_permissions", "token_permission_types" + add_foreign_key "orgs", "languages" + add_foreign_key "orgs", "regions" + add_foreign_key "phases", "templates" + add_foreign_key "plans", "templates" + add_foreign_key "plans_guidance_groups", "guidance_groups" + add_foreign_key "plans_guidance_groups", "plans" + add_foreign_key "question_options", "questions" + add_foreign_key "questions", "question_formats" + add_foreign_key "questions", "sections" + add_foreign_key "questions_themes", "questions" + add_foreign_key "questions_themes", "themes" + add_foreign_key "roles", "plans" + add_foreign_key "roles", "users" + add_foreign_key "sections", "phases" + add_foreign_key "templates", "orgs" + add_foreign_key "themes_in_guidance", "guidances" + add_foreign_key "themes_in_guidance", "themes" + add_foreign_key "user_identifiers", "identifier_schemes" + add_foreign_key "user_identifiers", "users" + add_foreign_key "users", "languages" + add_foreign_key "users", "orgs" + add_foreign_key "users_perms", "perms" add_foreign_key "users_perms", "users" -end \ No newline at end of file +end diff --git a/lib/assets/javascripts/application.js b/lib/assets/javascripts/application.js index d5868f6..8be3644 100644 --- a/lib/assets/javascripts/application.js +++ b/lib/assets/javascripts/application.js @@ -1,6 +1,7 @@ // Generic JS that is applicable across multiple pages import './utils/paginable'; import './utils/linkHelper'; +import './utils/links'; import './utils/tabHelper'; import './utils/tableHelper'; import './utils/tooltipHelper'; diff --git a/lib/assets/javascripts/utils/links.js b/lib/assets/javascripts/utils/links.js new file mode 100644 index 0000000..41999a0 --- /dev/null +++ b/lib/assets/javascripts/utils/links.js @@ -0,0 +1,80 @@ +import 'number-to-text/converters/en-us'; +import { convertToText } from 'number-to-text/index'; +import { isFunction } from './isType'; + +const getLinks = elem => + $(elem).find('.link').map((i, el) => { + const linkVal = $(el).find('input[name="link_link"]').val(); + const textVal = $(el).find('input[name="link_text"]').val(); + if (linkVal.length > 0 && textVal.length > 0) { + return { link: linkVal, text: textVal }; + } + return undefined; + }).get(); + +/* + * Iterates through any links class found and for each one found, + * executes the function cb by passing two parameters: the data-context value + * and value param being an Array of link objects (e.g. [{ link: String, text: String }, ...]) + * that represent the values for the input texts introduced. +*/ +export const eachLinks = (cb) => { + if (isFunction(cb)) { + $('.links').each((i, el) => { + cb($(el).attr('data-context'), getLinks(el)); + }); + eachLinks.done(); + } + return eachLinks; +}; +eachLinks.done = (cb) => { + if (isFunction(cb)) { + cb(); + } +}; + +$(() => { + const regExp = /([^\d]*)(\d)+/; + const replacer = (match, p1, p2) => `${p1}${(p2 * 1) + 1}`; + const replacerFor = (i, el) => $(el).attr('for', $(el).attr('for').replace(regExp, replacer)); + const replacerId = (i, el) => $(el).attr('id', $(el).attr('id').replace(regExp, replacer)); + const changeIds = (jqueryElem) => { + jqueryElem.find('label').each(replacerFor); + jqueryElem.find('input[type="text"]').each(replacerId); + }; + const clearVals = (jqueryElem) => { + jqueryElem.find('input[type="text"]').each((i, el) => { + $(el).val(''); + }); + }; + const linksLength = jqueryElem => jqueryElem.closest('.links').find('.link').length; + const maxNumberLinks = jqueryElem => jqueryElem.closest('.links').attr('data-max-number-links') * 1; + + $('.links').on('click', '.new', (e) => { + e.preventDefault(); + const target = $(e.target); + const max = maxNumberLinks(target); + if (linksLength(target) < max) { + const lastLink = target.closest('.links').find('.link').last(); + const clonedLink = lastLink.clone(); + changeIds(clonedLink); + clearVals(clonedLink); + lastLink.after(clonedLink); + } + }); + $('.links').on('click', '.delete', (e) => { + e.preventDefault(); + const target = $(e.target); + if (linksLength(target) > 1) { + target.closest('.link').remove(); + } + }); + + $('.links').find('.max-number-links').each((i, el) => { + const target = $(el); + const max = target.closest('.links').attr('data-max-number-links'); + target.text(convertToText(max).toLowerCase()); + }); +}); + +export { eachLinks as default }; diff --git a/lib/assets/javascripts/views/templates/edit.js b/lib/assets/javascripts/views/templates/edit.js index eb4de0d..448a8c7 100644 --- a/lib/assets/javascripts/views/templates/edit.js +++ b/lib/assets/javascripts/views/templates/edit.js @@ -1,4 +1,5 @@ import { Tinymce } from '../../utils/tinymce'; +import { eachLinks } from '../../utils/links'; $(() => { Tinymce.init({ selector: '.template' }); @@ -7,4 +8,12 @@ $(e.target).closest('.template_edit').hide(); $(e.target).closest('.tab-pane').find('.template_show').show(); }); + $('.edit_template').on('submit', () => { + const links = {}; + eachLinks((ctx, value) => { + links[ctx] = value; + }).done(() => { + $('#template-links').val(JSON.stringify(links)); + }); + }); }); diff --git a/test/unit/concerns/json_link_validator_test.rb b/test/unit/concerns/json_link_validator_test.rb new file mode 100644 index 0000000..5148133 --- /dev/null +++ b/test/unit/concerns/json_link_validator_test.rb @@ -0,0 +1,50 @@ +require 'test_helper' + +class JSONLinkValidatorTest < ActiveSupport::TestCase + include JSONLinkValidator + + test 'returns nil for a non-string value passed' do + assert_nil(parse_links(nil)) + assert_nil(parse_links(true)) + assert_nil(parse_links(false)) + assert_nil(parse_links([])) + assert_nil(parse_links({})) + assert_nil(parse_links(1)) + assert_nil(parse_links(1.1)) + end + + test 'returns nil for a non-array object parsed' do + assert_nil(parse_links("string")) + assert_nil(parse_links("1")) + assert_nil(parse_links("{}")) + assert_nil(parse_links("true")) + assert_nil(parse_links("false")) + assert_nil(parse_links("null")) + assert(parse_links("[]")) + end + + test 'returns nil for a non-hash item within the array parsed' do + assert_nil(parse_links("[{}, \"\"]")) + end + + test 'returns nil for a non-valid link key at array item' do + assert_nil(parse_links("[{\"link\": \"\", \"text\": \"\"}, {}]")) + end + + test 'returns nil for a non-valid text key at array item' do + assert_nil(parse_links("[{\"link\": \"\", \"text\": \"\"}, {\"link\": \"\"}]")) + end + + test 'returns nil for a non-string value for link key at array item' do + assert_nil(parse_links("[{\"link\": \"\", \"text\": \"\"}, {\"link\": [], \"text\": \"\"}]")) + end + + test 'returns nil for a non-string value for a text key at array item' do + assert_nil(parse_links("[{\"link\": \"\", \"text\": \"\"}, {\"link\": \"\", \"text\": []}]")) + end + + test 'returns Array for a valid array of link objects' do + assert_equal([{ "link" => "", "text" => ""}, { "link" => "", "text" => ""}], + parse_links("[{\"link\": \"\", \"text\": \"\"}, {\"link\": \"\", \"text\": \"\"}]")) + end +end \ No newline at end of file diff --git a/test/unit/template_test.rb b/test/unit/template_test.rb index b9ccb42..562b319 100644 --- a/test/unit/template_test.rb +++ b/test/unit/template_test.rb @@ -120,4 +120,31 @@ verify_belongs_to_relationship(tmplt, @org) end + test 'should be invalid when links is not a hash' do + t = Template.new(title: 'My test', version: 1, org: @org) + t.links = [] + refute(t.valid?) + assert_equal(['A hash is expected for links'], t.errors.messages[:links]) + end + + test 'should be invalid when links hash does not have the expected keys' do + t = Template.new(title: 'My test', version: 1, org: @org) + t.links = { "foo" => [], "bar" => [] } + refute(t.valid?) + assert_equal(['A key funder is expected for links hash', 'A key sample_plan is expected for links hash'], t.errors.messages[:links]) + end + + test 'should be invalid when links hash keys are not compliant to object links format' do + t = Template.new(title: 'My test', version: 1, org: @org) + t.links = { "funder" => [{}], "sample_plan" => [{}] } + refute(t.valid?) + assert_equal(['The key funder does not have a valid set of object links', 'The key sample_plan does not have a valid set of object links'], t.errors.messages[:links]) + end + + test 'should be valid when links hash keys are compliant to object links format' do + t = Template.new(title: 'My test', version: 1, org: @org) + t.links = { "funder" => [{ "link" => "foo", "text" => "bar" }], "sample_plan" => [] } + assert(t.valid?) + assert_equal(nil, t.errors.messages[:links]) + end end