require 'set'
namespace :upgrade do
desc "Upgrade to v2.1.2:"
task v2_1_2: :environment do
Rake::Task["upgrade:add_date_question_format"].execute
end
desc "Upgrade to v2.1.0:"
task v2_1_0: :environment do
Rake::Task["data_cleanup:deactivate_orphaned_plans"].execute
end
desc "Upgrade to v2.0.0: Part 1"
task v2_0_0_part_1: :environment do
Rake::Task['upgrade:add_default_values_v2_0_0'].execute
Rake::Task['db:migrate'].execute
Rake::Task['data_cleanup:find_known_invalidations'].execute
puts "If any invalid records were reported above you will need to correct them before running part 2."
end
desc "Upgrade to v2.0.0: Part 2"
task v2_0_0_part_2: :environment do
Rake::Task['data_cleanup:clean_invalid_records'].execute
Rake::Task['upgrade:add_versioning_id_to_templates'].execute
Rake::Task['upgrade:normalize_language_formats'].execute
Rake::Task['stat:build'].execute
end
desc "Upgrade to v1.1.2"
task v1_1_2: :environment do
Rake::Task['upgrade:check_org_contact_emails'].execute
Rake::Task['upgrade:check_for_guidance_multiple_themes'].execute
Rake::Task['upgrade:remove_admin_preferences'].execute
Rake::Task['upgrade:add_other_org'].execute
end
desc "Upgrade to 1.0"
task v1_0_0: :environment do
Rake::Task['upgrade:set_template_visibility'].execute
Rake::Task['upgrade:set_org_links_defaults'].execute
Rake::Task['upgrade:set_template_links_defaults'].execute
Rake::Task['upgrade:set_plan_complete'].execute
Rake::Task['upgrade:stats_api_org_admin'].execute
end
desc "Bug fixes for version v0.3.3"
task v0_3_3: :environment do
Rake::Task['upgrade:fix_question_formats'].execute
Rake::Task['upgrade:add_missing_token_permission_types'].execute
end
desc "Add the missing formattype to the question_formats table"
task fix_question_formats: :environment do
QuestionFormat.all.each do |qf|
case qf.title.downcase
when 'text area'
qf.formattype = :textarea
when 'text field'
qf.formattype = :textfield
when 'radio buttons'
qf.formattype = :radiobuttons
when 'check box'
qf.formattype = :checkbox
when 'dropdown'
qf.formattype = :dropdown
when 'multi select box'
qf.formattype = :multiselectbox
when 'date'
qf.formattype = :date
end
qf.save!
end
if QuestionFormat.find_by(formattype: QuestionFormat.formattypes[:date]).nil?
QuestionFormat.create!({ title: "Date", option_based: false, formattype: QuestionFormat.formattypes[:date] })
end
end
desc "Add the missing token_permission_types"
task add_missing_token_permission_types: :environment do
if TokenPermissionType.find_by(token_type: 'templates').nil?
TokenPermissionType.create!({token_type: 'templates',
text_description: 'allows a user access to the templates api endpoint'})
end
if TokenPermissionType.find_by(token_type: 'statistics').nil?
TokenPermissionType.create!({token_type: 'statistics',
text_description: 'allows a user access to the statistics api endpoint'})
end
end
desc "Set all funder templates (and the default template) to 'public' visibility and all others to 'organisational'"
task set_template_visibility: :environment do
funders = Org.funder.pluck(:id)
Template.update_all(visibility: Template.visibilities[:organisationally_visible])
Template.where(org_id: funders).update_all(visibility: Template.visibilities[:publicly_visible])
Template.default.update(visibility: Template.visibilities[:publicly_visible])
end
desc "Set all orgs.links defaults"
task set_org_links_defaults: :environment do
Org.update_all(links: { 'org': [] })
end
desc "Set all template.links defaults"
task set_template_links_defaults: :environment do
Template.update_all(links: {'funder':[],'sample_plan':[]})
end
desc "Sets completed for plans whose no. questions matches no. valid answers"
task set_plan_complete: :environment do
Plan.all.each do |p|
if p.no_questions_matches_no_answers?
p.update_column(:complete, true) # Avoids updating the column updated_at
end
end
end
desc "Allow Statistics API Usage for Org Admin Users"
task stats_api_org_admin: :environment do
Rake::Task['upgrade:add_missing_token_permission_types'].execute
orgs = Org.where(is_other: false).select(:id)
orgs.each do |org|
org.grant_api!(TokenPermissionType.where(token_type: 'statistics'))
end
users = User.joins(:perms).where(org_id: orgs).where(api_token: [nil, ''])
users.each do |user|
if user.can_org_admin?
# Generate the tokens directly instead of via the User.keep_or_generate_token! method so that we do not spam users!!
user.api_token = loop do
random_token = SecureRandom.urlsafe_base64(nil, false)
break random_token unless User.exists?(api_token: random_token)
end
user.save!
end
end
end
desc "Remove Duplicate Answers"
task remove_duplicate_answers: :environment do
## Concat Duplicate Answers
ActiveRecord::Base.transaction do
plan_ids = ActiveRecord::Base.connection.select_all("SELECT a1.plan_id as plan_id FROM Answers a1 INNER JOIN Answers a2 ON a1.plan_id = a2.plan_id AND a1.question_id = a2.question_id WHERE a1.id > a2.id" ).to_a.map{|h| h["plan_id"]}.uniq
plans = Plan.where(id: plan_ids)
plans.each do |plan|
plan.answers.pluck(:question_id).uniq.each do |question_id|
answers = Answer.where(plan_id: plan.id, question_id: question_id).order(:updated_at)
if answers.length > 1 # Duplicates found
puts "found duplicate for plan:#{plan.id}\tquestion:#{question_id} \n\tanswers:[#{answers.map{|answer| answer.id}}]"
new_answer = Answer.new
new_answer.user_id = answers.last.user_id
new_answer.plan_id = plan.id
new_answer.question_id = question_id
new_answer.created_at = answers.last.created_at
num_text = 0
qf = answers.last.question.question_format
puts "\tquestion format #{qf.title}"
if qf.dropdown?
new_answer.question_options << answers.last.question_options.first
puts "\t adding option answers.last.question_options.first.text" unless answers.last.question_options.first.blank?
end
answers.reverse.each do |answer|
if num_text == 0 && answer.text.present? # case first present text
new_answer.text = answer.text
num_text += 1
end
if num_text == 1 && answer.text.present?
text = "<p><strong>ANSWER SAVED TWICE - REQUIRES MERGING</strong></p>"
text += new_answer.text
new_answer.text = text + "<p><strong>-------------</strong></p>" + answer.text
end
new_answer.save
new_answer.reload
answer.notes.each do |note|
note.answer_id = new_answer.id
note.save
end
answer.question_options.each do |op|
unless qf.dropdown?
new_answer.question_options << op unless new_answer.question_options.any? {|aop| aop.id == op.id}
puts "\t adding option #{op.text}"
end
end
answer.destroy
end
new_answer.save
puts "\tsaved new answer with text:\n#{new_answer.text}"
end
end
end
end
end
desc "Remove deprecated themes"
task theme_delete_deprecated: :environment do
if t = Theme.find_by(title:'Project Description') then t.destroy end
if t = Theme.find_by(title:'Project Name') then t.destroy end
if t = Theme.find_by(title:'ID') then t.destroy end
if t = Theme.find_by(title:'PI / Researcher') then t.destroy end
end
desc "Create new Theme list"
task theme_new_themes: :environment do
["Data description", "Data collection", "Metadata & documentation", "Storage & security",
"Preservation", "Data sharing", "Related policies", "Data format", "Data volume",
"Ethics & privacy", "Intellectual Property Rights", "Data repository", "Roles & responsibilities",
"Budget"].each do |t|
Theme.create(title: t)
end
end
desc "Transform existing themes and their associations into new theme list"
task theme_transform: :environment do
ActiveRecord::Base.transaction do
[
{'Budget':'Resourcing'},
{'Data collection':'Data Capture Methods'},
{'Data collection':'Data Quality'},
{'Data description':'Data Description'},
{'Data description':'Data Type'},
{'Data description':'Existing Data'},
{'Data description':'Relationship to Existing Data'},
{'Data format':'Data Format'},
{'Data repository':'Data Repository'},
{'Data sharing':'Expected Reuse'},
{'Data sharing':'Managed Access Procedures'},
{'Data sharing':'Method For Data Sharing'},
{'Data sharing':'Restrictions on Sharing'},
{'Data sharing':'Timeframe For Data Sharing'},
{'Data volume':'Data Volumes'},
{'Ethics & privacy':'Ethical Issues'},
{'Intellectual Property Rights':'IPR Ownership and Licencing'},
{'Metadata & documentation':'Discovery by Users'},
{'Metadata & documentation':'Documentation'},
{'Metadata & documentation':'Metadata '}, # there may be a whitespace here!
{'Preservation':'Data Selection'},
{'Preservation':'Period of Preservation'},
{'Preservation':'Preservation Plan'},
{'Related policies':'Related Policies'},
{'Roles & responsibilities':'Responsibilities'},
{'Storage & security':'Data Security'},
{'Storage & security':'Storage and Backup'},
].each do |pair|
themeto = Theme.find_by(title: pair.keys[0].to_s)
themefrom = Theme.find_by(title: pair.values[0])
Guidance.joins(:themes).where('themes.title' => themefrom.title).each do |gui|
gui.themes.delete(themefrom)
gui.themes << themeto
end
Question.joins(:themes).where('themes.title' => themefrom.title).each do |q|
q.themes.delete(themefrom)
q.themes << themeto
end
end
end
end
desc "Delete migrated themes and their associations"
task theme_remove_migrated: :environment do
ActiveRecord::Base.transaction do
["Data Type", "Existing Data", "Relationship to Existing Data", "Data Quality", "Documentation",
"Discovery by Users", "Data Security", "Data Selection", "Period of Preservation",
"Expected Reuse", "Timeframe For Data Sharing", "Restrictions on Sharing",
"Managed Access Procedures", "Related Policies", "Data Description", "Data Volumes",
"Data Format", "Data Capture Methods", "Metadata ", "Ethical Issues",
"IPR Ownership and Licencing", "Storage and Backup", "Preservation Plan", "Data Repository",
"Method For Data Sharing", "Responsibilities", "Resourcing"].each do |t|
if deltheme = Theme.find_by(title: t) then deltheme.destroy end
end
end
end
desc "Deduplicate multiple associations resulting from Theme merges"
task theme_deduplicate_questions: :environment do
ActiveRecord::Base.transaction do
Question.all.each do |q|
themelist = []
if q.themes.present?
q.themes.each do |qt|
q.themes.delete(qt)
q.themes << qt
end
end
end
end
end
############# Make sure there are no guidances with multiple themes before this step!! #############
desc "Concatenate Guidance which refers to the same Theme as a result of merges"
task single_guidance_for_theme: :environment do
ActiveRecord::Base.transaction do
allthemes = Theme.all
GuidanceGroup.all.each do |group|
if group.guidances.present?
allthemes.each do |theme|
themeguidances = group.guidances.joins(:themes).where('themes.id = ?', theme.id)
if themeguidances.present? && themeguidances.length >= 2
themeguidances.drop(1).each do |guidance|
themeguidances.first.text += '<p>——</p>' + guidance.text
guidance.destroy
end #themeguidances loop
themeguidances.first.save
end
end #allthemes loop
end
end #GuidanceGroup loop
end
end
desc "Remove duplicated non customised template versions"
task remove_duplicated_non_customised_template_versions: :environment do
templates = Template
.select(:id, :family_id, :version, :updated_at)
.group(:family_id, :version, :id)
.order(family_id: :asc, version: :asc, updated_at: :desc)
current_family_id = nil
unique_versions = Set.new
duplicates = []
templates.each do |template|
if current_family_id != template.family_id
current_family_id = template.family_id
unique_versions = Set.new
end
if unique_versions.add?(template.version).nil?
duplicates << template
end
end
current_family_id = nil
version_counter = nil
duplicates.each do |template|
if current_family_id != template.family_id
current_family_id = template.family_id
version_counter = nil
end
num_plans = Plan.where(template_id: template.id).count
if num_plans > 0
version_counter = version_counter.nil? ? -1 : version_counter - 1
unsaved_template = Template.find(template.id)
unsaved_template.version = version_counter
if Template.exists?(customization_of: template.family_id)
puts "template with id: #{template.id} has NOT been ARCHIVED since it had customised templates"
else
puts "template with id: #{template.id} has been ARCHIVED since it had plans associated but no customised templates"
unsaved_template.archived = true
end
unsaved_template.save!
else
Template.destroy(template.id)
puts "template with id: #{template.id} has been REMOVED since it had no plans associated"
end
end
puts "remove_duplicated_non_customised_template_versions DONE"
end
desc "Remove duplicated customised template versions"
task remove_duplicated_customised_template_versions: :environment do
templates = Template
.select(:id, :customization_of, :version, :org_id, :updated_at)
.where('customization_of IS NOT NULL')
.group(:customization_of, :org_id, :version, :id)
.order(customization_of: :asc, org_id: :asc, version: :asc, updated_at: :desc)
generate_compound_key = lambda{ |customization_of, org_id| return "#{customization_of}_#{org_id}" }
current = nil
unique_versions = Set.new
duplicates = []
templates.each do |template|
key = generate_compound_key.call(template.customization_of, template.org_id)
if current != key
current = key
unique_versions = Set.new
end
if unique_versions.add?(template.version).nil?
duplicates << template
end
end
current = nil
version_counter = nil
duplicates.each do |template|
key = generate_compound_key.call(template.customization_of, template.org_id)
if current != key
current = key
version_counter = nil
end
num_plans = Plan.where(template_id: template.id).count
if num_plans > 0
version_counter = version_counter.nil? ? -1 : version_counter - 1
unsaved_template = Template.find(template.id)
unsaved_template.version = version_counter
unsaved_template.archived = true
unsaved_template.save!
puts "template with id: #{template.id} has been ARCHIVED since it had plans associated"
else
Template.destroy(template.id)
puts "template with id: #{template.id} has been REMOVED since it has no plans associated"
end
end
puts "remove_duplicated_customised_template_versions DONE"
end
desc "Remove duplicated template versions"
task remove_duplicated_template_versions: :environment do
Rake::Task['upgrade:remove_duplicated_non_customised_template_versions'].execute
Rake::Task['upgrade:remove_duplicated_customised_template_versions'].execute
end
desc "Org.contact_email is now required, sets any nil values to the helpdesk email defined in branding.yml"
task check_org_contact_emails: :environment do
branding = YAML.load(File.open('./config/branding.yml'))
if branding.is_a?(Hash) &&
branding['defaults'].present? &&
branding['defaults']['organisation'].present? &&
branding['defaults']['organisation']['name'].present?
branding['defaults']['organisation']['helpdesk_email'].present?
email = branding['defaults']['organisation']['helpdesk_email']
name = "#{branding['defaults']['organisation']['name']} helpdesk"
puts "Searching for Orgs with an undefined contact_email ..."
Org.where("contact_email IS NULL OR contact_email = ''").each do |org|
puts " Setting contact_email to #{email} for #{org.name}"
org.update_attributes(contact_email: email, contact_name: name)
end
else
puts "No helpdesk_email and/or name found in your config/branding.yml. Please add them under the defaults -> organisation section"
puts "For example:"
puts " defaults: &defaults"
puts " organisation:"
puts " name: 'Curation Center'"
puts " helpdesk_email: 'helpdesk@example.org'"
end
puts "Search complete"
puts ""
end
desc "The system now only allows for one theme selection per guidance, so check for violations"
task check_for_guidance_multiple_themes: :environment do
puts "Searching for guidance with multiple theme selections (you will need to manually reconcile these records) ..."
ids = Guidance.select('guidances.id, count(themes.id) theme_count').
joins(:themes).group('guidances.id').
having('count(themes.id) > 1').pluck('guidances.id')
GuidanceGroup.joins(:guidances).includes(:org).where('guidances.id IN (?)', ids).
distinct.order('orgs.name, guidance_groups.name').each do |grp|
puts " #{grp.org.name} - Guidance group, '#{grp.name}', has guidance with multiple themes"
end
puts "Search complete"
puts ""
end
desc "Remove admin preferences"
task remove_admin_preferences: :environment do
Pref.all.each do |p|
if p.settings.present?
if p.settings['email'].present?
if p.settings['email']['admin'].present?
p.settings['email'].delete('admin')
p.save!
end
end
end
end
end
desc "Add the 'other' org if it is not present."
task add_other_org: :environment do
puts "Checking for existence of an 'Other' org. Unaffiliated users should be affiliated with this org"
# Get the helpdesk email from the branding YAML
branding = YAML.load(File.open('./config/branding.yml'))
if branding.present? && branding['defaults'].present? && branding['defaults']['organisation'].present? && branding['defaults']['organisation']['helpdesk_email'].present?
email = branding['defaults']['organisation']['helpdesk_email']
name = branding['defaults']['organisation']['name'].present? ? "#{branding['defaults']['organisation']['name']} helpdesk" : 'Helpdesk'
else
email = 'other.organisation@example.org'
name = 'Helpdesk'
end
other_org = Org.find_by(is_other: true)
if other_org.present?
puts "Found the 'Other' org (is_other == true)"
else
puts "Could not find the 'Other' org (is_other == true), adding 'Other' org"
other_org = Org.create!({
name: 'Other Organisation',
abbreviation: 'OTHER',
org_type: Org.org_type_values_for(:organisation).min,
contact_email: email,
contact_name: name,
links: {"org": []},
is_other: true,
})
end
unaffiliated = User.where(org_id: nil)
unless unaffiliated.empty?
puts "The following users are not associated with an org. Assigning them to the 'Other' org."
puts unaffiliated.collect(&:email).join(', ')
unaffiliated.update_all(org_id: other_org.id)
end
end
desc "Apply default column values for v2.0.0"
task :add_default_values_v2_0_0 => :environment do
results = GuidanceGroup.where(optional_subset: nil)
puts "Found #{results.length} GuidanceGroups with a null optional_subset ... set values to false"
results.update_all(optional_subset: false)
results = GuidanceGroup.where(published: nil)
puts "Found #{results.length} GuidanceGroups with a null published ... set values to false"
results.update_all(published: false)
results = Note.where(archived: nil)
puts "Found #{results.length} Notes with a null archived ... set values to false"
results.update_all(archived: false)
results = Org.where(is_other: nil)
puts "Found #{results.length} Orgs with a null is_other ... set values to false"
results.update_all(is_other: false)
end
desc "Add verisoning_id to published Templates"
task :add_versioning_id_to_templates => :environment do
safe_require 'text'
safe_require 'progress_bar'
template_count = Template.latest_version.where(customization_of: nil)
.includes(phases: { sections: { questions: :annotations }})
.count
bar = ProgressBar.new(template_count)
# Remove attr_readonly restrictions form these models
Phase.attr_readonly.delete('versionable_id')
Section.attr_readonly.delete('versionable_id')
Question.attr_readonly.delete('versionable_id')
Annotation.attr_readonly.delete('versionable_id')
# Get each of the funder templates...
Template.latest_version.where(customization_of: nil)
.includes(phases: { sections: { questions: :annotations }})
.each do |funder_template|
bar.increment!(1)
Rails.logger.info "Updating versionable_id for Template: #{funder_template.id}"
funder_template.phases.each do |funder_phase|
Rails.logger.info "Updating versionable_id for Phase: #{funder_phase.id}"
funder_phase.update! versionable_id: SecureRandom.uuid
Phase.joins(:template)
.where(templates: { customization_of: funder_template.family_id })
.where(number: funder_phase.number).each do |phase|
if fuzzy_match?(phase.title, funder_phase.title)
phase.update! versionable_id: funder_phase.versionable_id
end
end
funder_phase.sections.each do |funder_section|
Rails.logger.info "Updating versionable_id for Section: #{funder_section.id}"
funder_section.update! versionable_id: SecureRandom.uuid
Section.joins(:template).where(templates: {
customization_of: funder_template.family_id
}).each do |section|
# Prefix the match text with the number. This will make it easier to match
# Sections where the number hasn't changed
text_a = "#{section.number} - #{section.description}"
text_b = "#{funder_section.number} - #{funder_section.description}"
if fuzzy_match?(text_a, text_b)
section.update! versionable_id: funder_section.versionable_id
end
end
funder_section.questions.each do |funder_question|
Rails.logger.info "Updating versionable_id for Question: #{funder_question.id}"
funder_question.update! versionable_id: SecureRandom.uuid
Question.joins(:template).where(templates: {
customization_of: funder_template.family_id
}).each do |question|
# Prefix the match text with the number. This will make it easier to match
# Questions where the number hasn't changed
text_a = "#{question.number} - #{question.text}"
text_b = "#{funder_question.number} - #{funder_question.text}"
if fuzzy_match?(text_a, text_b)
question.update! versionable_id: funder_question.versionable_id
end
end
funder_question.annotations.each do |funder_annotation|
Rails.logger.info "Updating versionable_id for Annotation: #{funder_annotation.id}"
funder_annotation.update! versionable_id: SecureRandom.uuid
Annotation.joins(:template).where(templates: {
customization_of: funder_template.family_id,
}).where(type: funder_annotation.type).each do |ann|
if fuzzy_match?(ann.text, funder_annotation.text)
ann.update! versionable_id: funder_annotation.versionable_id
end
end
end
end
end
end
end
# Add versionable_id to any customized Sections...
Section.joins(:template)
.includes(questions: :annotations)
.where(templates: { id: Template.latest_version.ids })
.where(versionable_id: nil, modifiable: true).each do |section|
section.update! versionable_id: SecureRandom.uuid
section.questions.each do |question|
question.update! versionable_id: SecureRandom.uuid
question.annotations.each do |annotation|
annotation.update! versionable_id: SecureRandom.uuid
end
end
end
end
desc "Update Language abbreviations to use ISO format"
task :normalize_language_formats => :environment do
Language.all.each do |language|
language.update(abbreviation: LocaleFormatter.new(language.abbreviation))
end
Template.all.each do |template|
next if template.locale.blank?
template.update(locale: LocaleFormatter.new(template.locale))
end
Theme.all.each do |theme|
next if theme.locale.blank?
theme.update(locale: LocaleFormatter.new(theme.locale))
end
end
desc "Adds the Date question format"
task :add_date_question_format => :environment do
unless QuestionFormat.id_for(QuestionFormat.formattypes[:date]).present?
QuestionFormat.create(
title: "Date field",
description: "Date field format",
option_based: false,
formattype: QuestionFormat.formattypes[:date]
)
end
end
private
def fuzzy_match?(text_a, text_b, min = 3)
Text::Levenshtein.distance(text_a, text_b) <= min
end
def safe_require(libname)
begin
require libname
rescue LoadError
puts "Please install the #{libname} gem locally and try again:
gem install #{libname}"
exit 1
end
end
end