diff --git a/apps/dashboard/app/controllers/launchers_controller.rb b/apps/dashboard/app/controllers/launchers_controller.rb index 65d8b25312..0c3f2d726c 100644 --- a/apps/dashboard/app/controllers/launchers_controller.rb +++ b/apps/dashboard/app/controllers/launchers_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# The controller for apps pages /dashboard/projects/:project_id/scripts +# The controller for apps pages /dashboard/projects/:project_id/launchers class LaunchersController < ApplicationController before_action :find_project @@ -21,7 +21,7 @@ def new @script = Launcher.new(project_dir: @project.directory) end - # POST /dashboard/projects/:project_id/scripts + # POST /dashboard/projects/:project_id/launchers def create opts = { project_dir: @project.directory }.merge(create_script_params[:launcher]) @script = Launcher.new(opts) @@ -36,12 +36,12 @@ def create end end - # GET /projects/:project_id/scripts/:id/edit + # GET /projects/:project_id/launchers/:id/edit # edit def edit end - # DELETE /projects/:project_id/scripts/:id + # DELETE /projects/:project_id/launchers/:id def destroy if @script.destroy redirect_to project_path(params[:project_id]), notice: I18n.t('dashboard.jobs_scripts_deleted') @@ -50,8 +50,8 @@ def destroy end end - # POST /projects/:project_id/scripts/:id/save - # save the script after editing + # POST /projects/:project_id/launchers/:id/save + # save the launcher after editing def save @script.update(save_script_params[:launcher]) @@ -62,7 +62,7 @@ def save end end - # POST /projects/:project_id/scripts/:id/submit + # POST /projects/:project_id/launchers/:id/submit # submit the job def submit opts = submit_script_params[:launcher].to_h.symbolize_keys diff --git a/apps/dashboard/app/models/launcher.rb b/apps/dashboard/app/models/launcher.rb index 9bab82af73..c433199238 100644 --- a/apps/dashboard/app/models/launcher.rb +++ b/apps/dashboard/app/models/launcher.rb @@ -9,8 +9,8 @@ class ClusterNotFound < StandardError; end attr_reader :title, :id, :created_at, :project_dir, :smart_attributes class << self - def scripts_dir(project_dir) - Pathname.new("#{project_dir}/.ondemand/scripts") + def launchers_dir(project_dir) + Pathname.new("#{project_dir}/.ondemand/launchers") end def find(id, project_dir) @@ -20,7 +20,7 @@ def find(id, project_dir) end def all(project_dir) - Dir.glob("#{scripts_dir(project_dir).to_s}/*/form.yml").map do |file| + Dir.glob("#{launchers_dir(project_dir).to_s}/*/form.yml").map do |file| Launcher.from_yaml(file, project_dir) end.compact.sort_by do |s| s.created_at @@ -226,7 +226,7 @@ def self.script_path(root_dir, script_id) raise(StandardError, "#{script_id} is invalid. Does not match #{ID_REX.inspect}") end - Pathname.new(File.join(Launcher.scripts_dir(root_dir), script_id.to_s)) + Pathname.new(File.join(Launcher.launchers_dir(root_dir), script_id.to_s)) end def default_script_path @@ -293,7 +293,7 @@ def cache_file_exists? def cached_values @cached_values ||= begin - cache_file_path = OodAppkit.dataroot.join(Launcher.scripts_dir("#{project_dir}"), "#{id}_opts.json") + cache_file_path = OodAppkit.dataroot.join(Launcher.launchers_dir("#{project_dir}"), "#{id}_opts.json") cache_file_content = File.read(cache_file_path) if cache_file_path.exist? File.exist?(cache_file_path) ? JSON.parse(cache_file_content) : {} diff --git a/apps/dashboard/app/models/project.rb b/apps/dashboard/app/models/project.rb index c876e98879..e29b7303cd 100644 --- a/apps/dashboard/app/models/project.rb +++ b/apps/dashboard/app/models/project.rb @@ -263,7 +263,7 @@ def sync_template # that point to the _new_ project directory, not the template's directory. # This creates them _and_ serializes them to yml in the new directory. def save_new_launchers - dir = Launcher.scripts_dir(template) + dir = Launcher.launchers_dir(template) Dir.glob("#{dir}/*/form.yml").map do |script_yml| Launcher.from_yaml(script_yml, project_dataroot) end.map do |script| @@ -279,7 +279,7 @@ def save_new_launchers def rsync_args [ 'rsync', '-rltp', - '--exclude', 'scripts/*', + '--exclude', 'launchers/*', '--exclude', '.ondemand/job_log.yml', "#{template}/", project_dataroot.to_s ] diff --git a/apps/dashboard/test/fixtures/projects/chemistry-5533/.ondemand/scripts/8woi7ghd/form.yml b/apps/dashboard/test/fixtures/projects/chemistry-5533/.ondemand/launchers/8woi7ghd/form.yml similarity index 100% rename from apps/dashboard/test/fixtures/projects/chemistry-5533/.ondemand/scripts/8woi7ghd/form.yml rename to apps/dashboard/test/fixtures/projects/chemistry-5533/.ondemand/launchers/8woi7ghd/form.yml diff --git a/apps/dashboard/test/fixtures/projects/chemistry-5533/.ondemand/scripts/gtqxzsek/form.yml b/apps/dashboard/test/fixtures/projects/chemistry-5533/.ondemand/launchers/gtqxzsek/form.yml similarity index 100% rename from apps/dashboard/test/fixtures/projects/chemistry-5533/.ondemand/scripts/gtqxzsek/form.yml rename to apps/dashboard/test/fixtures/projects/chemistry-5533/.ondemand/launchers/gtqxzsek/form.yml diff --git a/apps/dashboard/test/fixtures/projects/chemistry-5533/.ondemand/scripts/jxbrz2vm/form.yml b/apps/dashboard/test/fixtures/projects/chemistry-5533/.ondemand/launchers/jxbrz2vm/form.yml similarity index 100% rename from apps/dashboard/test/fixtures/projects/chemistry-5533/.ondemand/scripts/jxbrz2vm/form.yml rename to apps/dashboard/test/fixtures/projects/chemistry-5533/.ondemand/launchers/jxbrz2vm/form.yml diff --git a/apps/dashboard/test/models/launcher_test.rb b/apps/dashboard/test/models/launcher_test.rb index afeeacaa9b..59af8fb4c9 100644 --- a/apps/dashboard/test/models/launcher_test.rb +++ b/apps/dashboard/test/models/launcher_test.rb @@ -25,7 +25,7 @@ def setup target = Launcher.new({ project_dir: projects_path.to_s, id: '12345678', title: 'Test Script' }) assert target.save - assert Dir.entries("#{projects_path}/.ondemand/scripts").include?('12345678') + assert Dir.entries("#{projects_path}/.ondemand/launchers").include?('12345678') end end @@ -36,10 +36,10 @@ def setup target = Launcher.new({ project_dir: projects_path.to_s, id: '12345678', title: 'Test Script' }) assert target.save - assert Dir.entries("#{projects_path}/.ondemand/scripts").include?('12345678') + assert Dir.entries("#{projects_path}/.ondemand/launchers").include?('12345678') assert target.destroy - assert_not Dir.entries("#{projects_path}/.ondemand/scripts").include?('12345678') + assert_not Dir.entries("#{projects_path}/.ondemand/launchers").include?('12345678') end end @@ -61,14 +61,14 @@ def setup OodAppkit.stubs(:dataroot).returns(projects_path) script = Launcher.new({ project_dir: projects_path.to_s, id: '12345678', title: 'Test Script' }) assert script.save - assert Dir.entries("#{projects_path}/.ondemand/scripts").include?('12345678') + assert Dir.entries("#{projects_path}/.ondemand/launchers").include?('12345678') target = Launcher.new({ project_dir: projects_path.to_s, id: '33333333', title: 'Not saved' }) - assert_not Dir.entries("#{projects_path}/.ondemand/scripts").include?('33333333') + assert_not Dir.entries("#{projects_path}/.ondemand/launchers").include?('33333333') assert target.destroy - assert Dir.entries("#{projects_path}/.ondemand/scripts").include?('12345678') - assert_not Dir.entries("#{projects_path}/.ondemand/scripts").include?('33333333') + assert Dir.entries("#{projects_path}/.ondemand/launchers").include?('12345678') + assert_not Dir.entries("#{projects_path}/.ondemand/launchers").include?('33333333') end end @@ -154,7 +154,7 @@ def setup refute(launcher.save) assert(launcher.errors.size, 1) assert_equal(launcher.errors.full_messages[0], "Id ID does not match #{Launcher::ID_REX.inspect}") - refute(Dir.exist?(Launcher.scripts_dir(tmp).to_s)) + refute(Dir.exist?(Launcher.launchers_dir(tmp).to_s)) end end end diff --git a/apps/dashboard/test/models/projects_test.rb b/apps/dashboard/test/models/projects_test.rb index 1ca31ebace..1e0162053b 100644 --- a/apps/dashboard/test/models/projects_test.rb +++ b/apps/dashboard/test/models/projects_test.rb @@ -127,14 +127,14 @@ class ProjectsTest < ActiveSupport::TestCase template_dir = File.join(tmp,'template') job_log_path = "#{template_dir}/.ondemand/job_log.yml" launcher_id = '50r4nd0m' - cache_json_path = "#{template_dir}/.ondemand/scripts/#{launcher_id}/cache.json" + cache_json_path = "#{template_dir}/.ondemand/launchers/#{launcher_id}/cache.json" file_content = <<~HEREDOC some multiline content echo 'multiline content' description: multiline content HEREDOC - Pathname.new("#{template_dir}/.ondemand/scripts/#{launcher_id}").mkpath + Pathname.new("#{template_dir}/.ondemand/launchers/#{launcher_id}").mkpath File.open(job_log_path, 'w') { |file| file.write(file_content) } File.open(cache_json_path, 'w') { |file| file.write(file_content) } @@ -143,7 +143,7 @@ class ProjectsTest < ActiveSupport::TestCase project = create_project(projects_path, template: template_dir) assert Dir.glob(cache_json_path).present? && - Dir.glob("#{project.directory}/.ondemand/scripts/*/cache.json").empty? + Dir.glob("#{project.directory}/.ondemand/launchers/*/cache.json").empty? assert Dir.glob(job_log_path).present? && Dir.glob("#{project.directory}/.ondemand/*").exclude?("#{project.directory}/.ondemand/job_log.yml") end diff --git a/apps/dashboard/test/system/project_manager_test.rb b/apps/dashboard/test/system/project_manager_test.rb index 655c5631df..e4d151f93f 100644 --- a/apps/dashboard/test/system/project_manager_test.rb +++ b/apps/dashboard/test/system/project_manager_test.rb @@ -51,9 +51,9 @@ def setup_script(project_id) script_element[:id].gsub('launcher_', '') end - def add_account(project_id, script_id, save: true) + def add_account(project_id, launcher_id, save: true) visit project_path(project_id) - edit_launcher_path = edit_project_launcher_path(project_id, script_id) + edit_launcher_path = edit_project_launcher_path(project_id, launcher_id) find("[href='#{edit_launcher_path}']").click # now add 'auto_accounts' @@ -63,9 +63,9 @@ def add_account(project_id, script_id, save: true) click_on(I18n.t('dashboard.save')) if save end - def add_bc_num_hours(project_id, script_id) + def add_bc_num_hours(project_id, launcher_id) visit project_path(project_id) - edit_launcher_path = edit_project_launcher_path(project_id, script_id) + edit_launcher_path = edit_project_launcher_path(project_id, launcher_id) find("[href='#{edit_launcher_path}']").click # now add 'bc_num_hours' @@ -76,7 +76,7 @@ def add_bc_num_hours(project_id, script_id) click_on(I18n.t('dashboard.save')) end - def add_auto_environment_variable(project_id, script_id, save: true) + def add_auto_environment_variable(project_id, launcher_id, save: true) # now add 'auto_environment_variable' click_on('Add new option') select('Environment Variable', from: 'add_new_field_select') @@ -224,7 +224,7 @@ def add_auto_environment_variable(project_id, script_id, save: true) test 'creating and showing scripts' do Dir.mktmpdir do |dir| project_id = setup_project(dir) - script_id = setup_script(project_id) + launcher_id = setup_script(project_id) expected_yml = <<~HEREDOC --- @@ -259,9 +259,9 @@ def add_auto_environment_variable(project_id, script_id, save: true) success_message = I18n.t('dashboard.jobs_scripts_created') assert_selector('.alert-success', text: "Close\n#{success_message}") - assert_equal(expected_yml, File.read("#{dir}/projects/#{project_id}/.ondemand/scripts/#{script_id}/form.yml")) + assert_equal(expected_yml, File.read("#{dir}/projects/#{project_id}/.ondemand/launchers/#{launcher_id}/form.yml")) - launcher_path = project_launcher_path(project_id, script_id) + launcher_path = project_launcher_path(project_id, launcher_id) find("[href='#{launcher_path}'].btn-success").click assert_selector('h1', text: 'the script title', count: 1) end @@ -271,7 +271,7 @@ def add_auto_environment_variable(project_id, script_id, save: true) Dir.mktmpdir do |dir| Configuration.stubs(:launcher_default_items).returns(['bc_num_hours']) project_id = setup_project(dir) - script_id = setup_script(project_id) + launcher_id = setup_script(project_id) # note that bc_num_hours is in this YAML. expected_yml = <<~HEREDOC @@ -314,18 +314,18 @@ def add_auto_environment_variable(project_id, script_id, save: true) success_message = I18n.t('dashboard.jobs_scripts_created') assert_selector('.alert-success', text: "Close\n#{success_message}") - assert_equal(expected_yml, File.read("#{dir}/projects/#{project_id}/.ondemand/scripts/#{script_id}/form.yml")) + assert_equal(expected_yml, File.read("#{dir}/projects/#{project_id}/.ondemand/launchers/#{launcher_id}/form.yml")) end end test 'showing scripts with auto attributes' do Dir.mktmpdir do |dir| project_id = setup_project(dir) - script_id = setup_script(project_id) + launcher_id = setup_script(project_id) project_dir = File.join(dir, 'projects', project_id) - add_account(project_id, script_id) + add_account(project_id, launcher_id) - launcher_path = project_launcher_path(project_id, script_id) + launcher_path = project_launcher_path(project_id, launcher_id) find("[href='#{launcher_path}'].btn-success").click assert_selector('h1', text: 'the script title', count: 1) @@ -344,39 +344,39 @@ def add_auto_environment_variable(project_id, script_id, save: true) test 'deleting a script that succeeds' do Dir.mktmpdir do |dir| project_id = setup_project(dir) - script_id = setup_script(project_id) + launcher_id = setup_script(project_id) project_dir = File.join(dir, 'projects', project_id) ondemand_dir = File.join(project_dir, '.ondemand') - script_dir = File.join(ondemand_dir, 'scripts', script_id) + launcher_dir = File.join(ondemand_dir, 'launchers', launcher_id) # ASSERT SCRIPT DIRECTORY IS CREATED - assert_equal true, File.directory?(script_dir) + assert_equal true, File.directory?(launcher_dir) - expected_script_files = ["#{script_dir}/form.yml", "#{ondemand_dir}/job_log.yml"] + expected_script_files = ["#{launcher_dir}/form.yml", "#{ondemand_dir}/job_log.yml"] # ASSERT EXPECTED SCRIPT FILES expected_script_files.each do |file_path| assert_equal true, File.exist?(file_path), "#{file_path} does not exist" end accept_confirm do - find("#delete_#{script_id}").click + find("#delete_#{launcher_id}").click end assert_selector '.alert-success', text: 'Script successfully deleted!' # ASSERT SCRIPT DIRECTORY IS DELETED - assert_not File.directory? script_dir + assert_not File.directory? launcher_dir end end test 'submitting a script with auto attributes that succeeds' do Dir.mktmpdir do |dir| project_id = setup_project(dir) - script_id = setup_script(project_id) + launcher_id = setup_script(project_id) project_dir = File.join(dir, 'projects', project_id) ondemand_dir = File.join(project_dir, '.ondemand') - add_account(project_id, script_id) + add_account(project_id, launcher_id) - launcher_path = project_launcher_path(project_id, script_id) + launcher_path = project_launcher_path(project_id, launcher_id) find("[href='#{launcher_path}'].btn-success").click assert_selector('h1', text: 'the script title', count: 1) @@ -412,10 +412,10 @@ def add_auto_environment_variable(project_id, script_id, save: true) test 'submitting a script with job name' do Dir.mktmpdir do |dir| project_id = setup_project(dir) - script_id = setup_script(project_id) + launcher_id = setup_script(project_id) project_dir = File.join(dir, 'projects', project_id) ondemand_dir = File.join(project_dir, '.ondemand') - add_account(project_id, script_id, save: false) + add_account(project_id, launcher_id, save: false) click_on('Add new option') select('Job Name', from: 'add_new_field_select') @@ -423,7 +423,7 @@ def add_auto_environment_variable(project_id, script_id, save: true) fill_in('launcher_auto_job_name', with: 'my cool job name') click_on(I18n.t('dashboard.save')) - launcher_path = project_launcher_path(project_id, script_id) + launcher_path = project_launcher_path(project_id, launcher_id) find("[href='#{launcher_path}'].btn-success").click assert_selector('h1', text: 'the script title', count: 1) @@ -460,12 +460,12 @@ def add_auto_environment_variable(project_id, script_id, save: true) test 'submitting a script with auto attributes that fails' do Dir.mktmpdir do |dir| project_id = setup_project(dir) - script_id = setup_script(project_id) + launcher_id = setup_script(project_id) project_dir = File.join(dir, 'projects', project_id) ondemand_dir = File.join(project_dir, '.ondemand') - add_account(project_id, script_id) + add_account(project_id, launcher_id) - launcher_path = project_launcher_path(project_id, script_id) + launcher_path = project_launcher_path(project_id, launcher_id) find("[href='#{launcher_path}'].btn-success").click assert_selector('h1', text: 'the script title', count: 1) @@ -494,11 +494,11 @@ def add_auto_environment_variable(project_id, script_id, save: true) test 'editing scripts initializes correctly' do Dir.mktmpdir do |dir| project_id = setup_project(dir) - script_id = setup_script(project_id) + launcher_id = setup_script(project_id) visit project_path(project_id) - edit_launcher_path = edit_project_launcher_path(project_id, script_id) + edit_launcher_path = edit_project_launcher_path(project_id, launcher_id) find("[href='#{edit_launcher_path}']").click click_on('Add new option') @@ -516,11 +516,11 @@ def add_auto_environment_variable(project_id, script_id, save: true) test 'adding new fields to scripts' do Dir.mktmpdir do |dir| project_id = setup_project(dir) - script_id = setup_script(project_id) + launcher_id = setup_script(project_id) visit project_path(project_id) - edit_launcher_path = edit_project_launcher_path(project_id, script_id) + edit_launcher_path = edit_project_launcher_path(project_id, launcher_id) find("[href='#{edit_launcher_path}']").click # only shows 'cluster' & 'auto_scripts' @@ -533,8 +533,8 @@ def add_auto_environment_variable(project_id, script_id, save: true) end # add bc_num_hours - add_bc_num_hours(project_id, script_id) - script_edit_path = edit_project_launcher_path(project_id, script_id) + add_bc_num_hours(project_id, launcher_id) + script_edit_path = edit_project_launcher_path(project_id, launcher_id) find("[href='#{script_edit_path}']").click # now shows 'cluster', 'auto_scripts' & the newly added'bc_num_hours' @@ -552,7 +552,7 @@ def add_auto_environment_variable(project_id, script_id, save: true) find('#save_launcher_bc_num_hours').click # add auto_environment_variable - add_auto_environment_variable(project_id, script_id) + add_auto_environment_variable(project_id, launcher_id) find('#edit_launcher_auto_environment_variable').click find("[data-auto-environment-variable='name']").fill_in(with: 'SOME_VARIABLE') @@ -616,22 +616,22 @@ def add_auto_environment_variable(project_id, script_id, save: true) required: false HEREDOC - assert_equal(expected_yml, File.read("#{dir}/projects/#{project_id}/.ondemand/scripts/#{script_id}/form.yml")) + assert_equal(expected_yml, File.read("#{dir}/projects/#{project_id}/.ondemand/launchers/#{launcher_id}/form.yml")) end end test 'removing script fields' do Dir.mktmpdir do |dir| project_id = setup_project(dir) - script_id = setup_script(project_id) + launcher_id = setup_script(project_id) # add bc_num_hours - add_bc_num_hours(project_id, script_id) - add_account(project_id, script_id) + add_bc_num_hours(project_id, launcher_id) + add_account(project_id, launcher_id) # go to edit it and see that there is cluster and bc_num_hours visit project_path(project_id) - edit_launcher_path = edit_project_launcher_path(project_id, script_id) + edit_launcher_path = edit_project_launcher_path(project_id, launcher_id) find("[href='#{edit_launcher_path}']").click # puts page.body assert_equal 4, page.all('.editable-form-field').size @@ -708,7 +708,7 @@ def add_auto_environment_variable(project_id, script_id, save: true) required: false HEREDOC - assert_equal(expected_yml, File.read("#{dir}/projects/#{project_id}/.ondemand/scripts/#{script_id}/form.yml")) + assert_equal(expected_yml, File.read("#{dir}/projects/#{project_id}/.ondemand/launchers/#{launcher_id}/form.yml")) end end @@ -769,10 +769,10 @@ def add_auto_environment_variable(project_id, script_id, save: true) test 'excluding and including select options' do Dir.mktmpdir do |dir| project_id = setup_project(dir) - script_id = setup_script(project_id) - add_account(project_id, script_id) + launcher_id = setup_script(project_id) + add_account(project_id, launcher_id) - visit edit_project_launcher_path(project_id, script_id) + visit edit_project_launcher_path(project_id, launcher_id) find('#edit_launcher_auto_accounts').click exclude_accounts = ['pas2051', 'pas1871', 'pas1754', 'pas1604'] @@ -792,7 +792,7 @@ def add_auto_environment_variable(project_id, script_id, save: true) find('#save_script_edit').click assert_current_path(project_path(project_id)) - launcher_path = project_launcher_path(project_id, script_id) + launcher_path = project_launcher_path(project_id, launcher_id) find("[href='#{launcher_path}'].btn-success").click # now let's check scripts#show to see if they've actually been excluded. @@ -801,7 +801,7 @@ def add_auto_environment_variable(project_id, script_id, save: true) assert(!show_account_options.include?(acct)) end - visit edit_project_launcher_path(project_id, script_id) + visit edit_project_launcher_path(project_id, launcher_id) find('#edit_launcher_auto_accounts').click exclude_accounts.each do |acct| @@ -820,7 +820,7 @@ def add_auto_environment_variable(project_id, script_id, save: true) find('#save_script_edit').click assert_current_path(project_path(project_id)) - launcher_path = project_launcher_path(project_id, script_id) + launcher_path = project_launcher_path(project_id, launcher_id) find("[href='#{launcher_path}'].btn-success").click # now let's check scripts#show and they should be back. @@ -834,10 +834,10 @@ def add_auto_environment_variable(project_id, script_id, save: true) test 'fixing select options' do Dir.mktmpdir do |dir| project_id = setup_project(dir) - script_id = setup_script(project_id) - add_account(project_id, script_id) + launcher_id = setup_script(project_id) + add_account(project_id, launcher_id) - visit edit_project_launcher_path(project_id, script_id) + visit edit_project_launcher_path(project_id, launcher_id) find('#edit_launcher_auto_accounts').click accounts_select = find('#launcher_auto_accounts') @@ -865,8 +865,8 @@ def add_auto_environment_variable(project_id, script_id, save: true) test 'excluding newly created options' do Dir.mktmpdir do |dir| project_id = setup_project(dir) - script_id = setup_script(project_id) - visit(edit_project_launcher_path(project_id, script_id)) + launcher_id = setup_script(project_id) + visit(edit_project_launcher_path(project_id, launcher_id)) # now add 'auto_accounts' click_on('Add new option') @@ -915,11 +915,11 @@ def add_auto_environment_variable(project_id, script_id, save: true) forms = Dir.glob("#{abs_project_dir}/.ondemand/**/*/form.yml") assert_equal(3, forms.size) - script_id = '8woi7ghd' - orig_form = "#{Rails.root}/test/fixtures/projects/chemistry-5533/.ondemand/scripts/#{script_id}/form.yml" + launcher_id = '8woi7ghd' + orig_form = "#{Rails.root}/test/fixtures/projects/chemistry-5533/.ondemand/launchers/#{launcher_id}/form.yml" orig_form = YAML.safe_load(File.read(orig_form)) - new_form = "#{abs_project_dir}/.ondemand/scripts/#{script_id}/form.yml" + new_form = "#{abs_project_dir}/.ondemand/launchers/#{launcher_id}/form.yml" new_form = YAML.safe_load(File.read(new_form)) # 'form' & 'title' are the same