diff --git a/app/models/plan.rb b/app/models/plan.rb index aae4795..eba48ae 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -6,6 +6,7 @@ has_many :sections, through: :phases has_many :questions, through: :sections has_many :answers + has_many :roles has_many :notes, through: :answers has_many :users, through: :roles has_many :exported_plans @@ -13,29 +14,32 @@ ## # Possibly needed for active_admin # -relies on protected_attributes gem as syntax depricated in rails 4.2 - attr_accessible :locked, :project_id, :version_id, :version, :plan_sections, + attr_accessible :locked, :project_id, :version_id, :version, :plan_sections, :exported_plans, :project, :title, :template, :grant_number, :identifier, :principal_investigator, :principal_investigator_identifier, :description, :data_contact, :funder_name, :visibility, :exported_plans, - :as => [:default, :admin] + :roles, :users, :as => [:default, :admin] + accepts_nested_attributes_for :roles # public is a Ruby keyword so using publicly enum visibility: [:organisationally_visible, :publicly_visible, :is_test, :privately_visible] + validates :template, :title, :users, presence: true + ## # Constants - A4_PAGE_HEIGHT = 297 #(in mm) - A4_PAGE_WIDTH = 210 #(in mm) - ROUNDING = 5 #round estimate up to nearest 5% - FONT_HEIGHT_CONVERSION_FACTOR = 0.35278 #convert font point size to mm - FONT_WIDTH_HEIGHT_RATIO = 0.4 #Assume glyph width averages 2/5 the height + A4_PAGE_HEIGHT = 297 #(in mm) + A4_PAGE_WIDTH = 210 #(in mm) + ROUNDING = 5 #round estimate up to nearest 5% + FONT_HEIGHT_CONVERSION_FACTOR = 0.35278 #convert font point size to mm + FONT_WIDTH_HEIGHT_RATIO = 0.4 #Assume glyph width averages 2/5 the height ## # Settings for the template - has_settings :export, class_name: 'Settings::Template' do |s| - s.key :export, defaults: Settings::Template::DEFAULT_SETTINGS - end - alias_method :super_settings, :settings + has_settings :export, class_name: 'Settings::Template' do |s| + s.key :export, defaults: Settings::Template::DEFAULT_SETTINGS + end + alias_method :super_settings, :settings @@ -57,46 +61,51 @@ ## - # Proxy through to the template settings (or defaults if this plan doesn't have - # an associated template) if there are no settings stored for this plan. - # `key` is required by rails-settings, so it's required here, too. + # Proxy through to the template settings (or defaults if this plan doesn't have + # an associated template) if there are no settings stored for this plan. + # `key` is required by rails-settings, so it's required here, too. # # @param key [Key] a key required by rails # @return [Settings] settings for this plan's template - def settings(key) - self_settings = self.super_settings(key) - return self_settings if self_settings.value? -# self.dmptemplate.settings(key) - self.template.settings(key) - end + def settings(key) + self_settings = self.super_settings(key) + return self_settings if self_settings.value? +# self.dmptemplate.settings(key) + self.template.settings(key) unless self.template.nil? + end ## # returns the template for this plan, or generates an empty template and returns that # # @return [Dmptemplate] the template associated with this plan - def dmptemplate -# self.project.try(:dmptemplate) || Dmptemplate.new + def dmptemplate +# self.project.try(:dmptemplate) || Dmptemplate.new self.try(:template) || Template.new - end + end ## # returns the title for this project as defined by the settings # # @return [String] the title for this project - def title - logger.debug "Title in settings: #{self.settings(:export).title}" - if self.settings(:export).title == "" + def title + if self.settings(:export).nil? + return I18n.t('tool_title2') + + else +# logger.debug "Title in settings: #{self.settings(:export).title}" + if self.settings(:export).title == "" # if !self.version.nil? && !self.version.phase.nil? && !self.version.phase.title? then - if !self.template.nil? && !self.template.phases.empty? -# return self.version.phase.title - return self.template.phases.first.title + if !self.template.nil? && !self.template.phases.empty? +# return self.version.phase.title + return self.template.phases.first.title + else + return I18n.t('tool_title2') + end else - return I18n.t('tool_title2') - end - else - return self.settings(:export).title - end - end + return self.settings(:export).title + end + end + end ## # returns the most recent answer to the given question id @@ -105,78 +114,78 @@ # @param qid [Integer] the id for the question to find the answer for # @param create_if_missing [Boolean] if true, will genereate a default answer to the question # @return [Answer,nil] the most recent answer to the question, or a new question with default value, or nil - def answer(qid, create_if_missing = true) - answer = answers.where(:question_id => qid).order("created_at DESC").first - question = Question.find(qid) - if answer.nil? && create_if_missing then - answer = Answer.new - answer.plan_id = id - answer.question_id = qid - answer.text = question.default_value - default_options = Array.new - question.options.each do |option| - if option.is_default - default_options << option - end - end - answer.options = default_options - end - return answer - end + def answer(qid, create_if_missing = true) + answer = answers.where(:question_id => qid).order("created_at DESC").first + question = Question.find(qid) + if answer.nil? && create_if_missing then + answer = Answer.new + answer.plan_id = id + answer.question_id = qid + answer.text = question.default_value + default_options = Array.new + question.options.each do |option| + if option.is_default + default_options << option + end + end + answer.options = default_options + end + return answer + end ## # returns all of the sections for this version of the plan, and for the project's organisation # # @return [Array
,nil] either a list of sections, or nil if none were found - def sections - unless project.organisation.nil? then - sections = version.global_sections + project.organisation.all_sections(version_id) - else - sections = version.global_sections - end - return sections.uniq.sort_by &:number - end + def sections + unless project.organisation.nil? then + sections = version.global_sections + project.organisation.all_sections(version_id) + else + sections = version.global_sections + end + return sections.uniq.sort_by &:number + end ## # returns the guidances associated with the project's organisation, for a specified question # # @param question [Question] the question to find guidance for # @return [Array] the list of guidances which pretain to the specified question - def guidance_for_question(question) - guidances = {} - # If project org isn't nil, get guidance by theme from any "non-subset" groups belonging to project org - unless project.organisation.nil? then - project.organisation.guidance_groups.each do |group| - if !group.optional_subset && (group.dmptemplates.pluck(:id).include?(project.dmptemplate_id) || group.dmptemplates.count == 0) then - group.guidances.each do |guidance| - guidance.themes.where("id IN (?)", question.theme_ids).each do |theme| - guidances = self.add_guidance_to_array(guidances, group, theme, guidance) - end - end - end - end - end - # Get guidance by theme from any guidance groups selected on creation - project.guidance_groups.each do |group| - if group.dmptemplates.pluck(:id).include?(project.dmptemplate_id) || group.dmptemplates.count == 0 then - group.guidances.each do |guidance| - guidance.themes.where("id IN (?)", question.theme_ids).each do |theme| - guidances = self.add_guidance_to_array(guidances, group, theme, guidance) - end - end - end - end - # Get guidance by question where guidance group was selected on creation or if group is organisation default - question.guidances.each do |guidance| - guidance.guidance_groups.each do |group| - if (group.organisation == project.organisation && !group.optional_subset) || project.guidance_groups.include?(group) then - guidances = self.add_guidance_to_array(guidances, group, nil, guidance) - end + def guidance_for_question(question) + guidances = {} + # If project org isn't nil, get guidance by theme from any "non-subset" groups belonging to project org + unless project.organisation.nil? then + project.organisation.guidance_groups.each do |group| + if !group.optional_subset && (group.dmptemplates.pluck(:id).include?(project.dmptemplate_id) || group.dmptemplates.count == 0) then + group.guidances.each do |guidance| + guidance.themes.where("id IN (?)", question.theme_ids).each do |theme| + guidances = self.add_guidance_to_array(guidances, group, theme, guidance) + end + end + end end - end + end + # Get guidance by theme from any guidance groups selected on creation + project.guidance_groups.each do |group| + if group.dmptemplates.pluck(:id).include?(project.dmptemplate_id) || group.dmptemplates.count == 0 then + group.guidances.each do |guidance| + guidance.themes.where("id IN (?)", question.theme_ids).each do |theme| + guidances = self.add_guidance_to_array(guidances, group, theme, guidance) + end + end + end + end + # Get guidance by question where guidance group was selected on creation or if group is organisation default + question.guidances.each do |guidance| + guidance.guidance_groups.each do |group| + if (group.organisation == project.organisation && !group.optional_subset) || project.guidance_groups.include?(group) then + guidances = self.add_guidance_to_array(guidances, group, nil, guidance) + end + end + end - return guidances - end + return guidances + end ## # adds the given guidance to a hash indexed by a passed guidance group and theme @@ -186,27 +195,27 @@ # @param theme [Theme] the theme object for the GuidanceGroup # @param guidance [Guidance] the guidance object to be appended to the correct section of the array # @return [{GuidanceGroup => {Theme => Array}}] the updated object which was passed in - def add_guidance_to_array(guidance_array, guidance_group, theme, guidance) - if guidance_array[guidance_group].nil? then - guidance_array[guidance_group] = {} - end - if theme.nil? then - if guidance_array[guidance_group]["no_theme"].nil? then - guidance_array[guidance_group]["no_theme"] = [] - end - if !guidance_array[guidance_group]["no_theme"].include?(guidance) then - guidance_array[guidance_group]["no_theme"].push(guidance) - end - else - if guidance_array[guidance_group][theme].nil? then - guidance_array[guidance_group][theme] = [] - end - if !guidance_array[guidance_group][theme].include?(guidance) then - guidance_array[guidance_group][theme].push(guidance) - end - end + def add_guidance_to_array(guidance_array, guidance_group, theme, guidance) + if guidance_array[guidance_group].nil? then + guidance_array[guidance_group] = {} + end + if theme.nil? then + if guidance_array[guidance_group]["no_theme"].nil? then + guidance_array[guidance_group]["no_theme"] = [] + end + if !guidance_array[guidance_group]["no_theme"].include?(guidance) then + guidance_array[guidance_group]["no_theme"].push(guidance) + end + else + if guidance_array[guidance_group][theme].nil? then + guidance_array[guidance_group][theme] = [] + end + if !guidance_array[guidance_group][theme].include?(guidance) then + guidance_array[guidance_group][theme].push(guidance) + end + end return guidance_array - end + end ## # determines if the plan is editable by the specified user @@ -214,9 +223,9 @@ # # @param user_id [Integer] the id for a user # @return [Boolean] true if user can edit the plan - def editable_by(user_id) - return project.editable_by(user_id) - end + def editable_by(user_id) + return project.editable_by(user_id) + end ## # determines if the plan is readable by the specified user @@ -224,13 +233,13 @@ # # @param user_id [Integer] the id for a user # @return [Boolean] true if the user can read the plan - def readable_by(user_id) - if project.nil? - return false - else - return project.readable_by(user_id) - end - end + def readable_by(user_id) + if project.nil? + return false + else + return project.readable_by(user_id) + end + end ## # determines if the plan is administerable by the specified user @@ -238,9 +247,9 @@ # # @param user_id [Integer] the id for the user # @return [Boolean] true if the user can administer the plan - def administerable_by(user_id) - return project.readable_by(user_id) - end + def administerable_by(user_id) + return project.readable_by(user_id) + end ## @@ -250,63 +259,63 @@ # for each question, it contains the answer_id, answer_created_by, answer_text, answer_options_id, aand answered_by # # @return [Status] - def status - status = { - "num_questions" => 0, - "num_answers" => 0, - "sections" => {}, - "questions" => {}, - "space_used" => 0 # percentage of available space in pdf used - } + def status + status = { + "num_questions" => 0, + "num_answers" => 0, + "sections" => {}, + "questions" => {}, + "space_used" => 0 # percentage of available space in pdf used + } - space_used = height_of_text(self.project.title, 2, 2) + space_used = height_of_text(self.project.title, 2, 2) - sections.each do |s| - space_used += height_of_text(s.title, 1, 1) - section_questions = 0 - section_answers = 0 - status["sections"][s.id] = {} - status["sections"][s.id]["questions"] = Array.new - s.questions.each do |q| - status["num_questions"] += 1 - section_questions += 1 - status["sections"][s.id]["questions"] << q.id - status["questions"][q.id] = {} - answer = answer(q.id, false) + sections.each do |s| + space_used += height_of_text(s.title, 1, 1) + section_questions = 0 + section_answers = 0 + status["sections"][s.id] = {} + status["sections"][s.id]["questions"] = Array.new + s.questions.each do |q| + status["num_questions"] += 1 + section_questions += 1 + status["sections"][s.id]["questions"] << q.id + status["questions"][q.id] = {} + answer = answer(q.id, false) - space_used += height_of_text(q.text) unless q.text == s.title - space_used += height_of_text(answer.try(:text) || I18n.t('helpers.plan.export.pdf.question_not_answered')) + space_used += height_of_text(q.text) unless q.text == s.title + space_used += height_of_text(answer.try(:text) || I18n.t('helpers.plan.export.pdf.question_not_answered')) - if ! answer.nil? then - status["questions"][q.id] = { - "answer_id" => answer.id, - "answer_created_at" => answer.created_at.to_i, - "answer_text" => answer.text, - "answer_option_ids" => answer.option_ids, - "answered_by" => answer.user.name - } + if ! answer.nil? then + status["questions"][q.id] = { + "answer_id" => answer.id, + "answer_created_at" => answer.created_at.to_i, + "answer_text" => answer.text, + "answer_option_ids" => answer.option_ids, + "answered_by" => answer.user.name + } q_format = q.question_format - status["num_answers"] += 1 if (q_format.title == I18n.t("helpers.checkbox") || q_format.title == I18n.t("helpers.multi_select_box") || + status["num_answers"] += 1 if (q_format.title == I18n.t("helpers.checkbox") || q_format.title == I18n.t("helpers.multi_select_box") || q_format.title == I18n.t("helpers.radio_buttons") || q_format.title == I18n.t("helpers.dropdown")) || answer.text.present? - section_answers += 1 - #TODO: include selected options in space estimate - else - status["questions"][q.id] = { - "answer_id" => nil, - "answer_created_at" => nil, - "answer_text" => nil, - "answer_option_ids" => nil, - "answered_by" => nil - } - end - status["sections"][s.id]["num_questions"] = section_questions - status["sections"][s.id]["num_answers"] = section_answers - end - end + section_answers += 1 + #TODO: include selected options in space estimate + else + status["questions"][q.id] = { + "answer_id" => nil, + "answer_created_at" => nil, + "answer_text" => nil, + "answer_option_ids" => nil, + "answered_by" => nil + } + end + status["sections"][s.id]["num_questions"] = section_questions + status["sections"][s.id]["num_answers"] = section_answers + end + end - status['space_used'] = estimate_space_used(space_used) - return status - end + status['space_used'] = estimate_space_used(space_used) + return status + end ## @@ -315,35 +324,35 @@ # section: title, question text for each question, answer type and answer value # # @return [Details] - def details - details = { - "project_title" => project.title, - "phase_title" => version.phase.title, - "sections" => {} - } - sections.sort_by(&:"number").each do |s| - details["sections"][s.number] = {} - details["sections"][s.number]["title"] = s.title - details["sections"][s.number]["questions"] = {} - s.questions.order("number").each do |q| - details["sections"][s.number]["questions"][q.number] = {} - details["sections"][s.number]["questions"][q.number]["question_text"] = q.text - answer = answer(q.id, false) - if ! answer.nil? then + def details + details = { + "project_title" => project.title, + "phase_title" => version.phase.title, + "sections" => {} + } + sections.sort_by(&:"number").each do |s| + details["sections"][s.number] = {} + details["sections"][s.number]["title"] = s.title + details["sections"][s.number]["questions"] = {} + s.questions.order("number").each do |q| + details["sections"][s.number]["questions"][q.number] = {} + details["sections"][s.number]["questions"][q.number]["question_text"] = q.text + answer = answer(q.id, false) + if ! answer.nil? then q_format = q.question_format - if (q_format.title == t("helpers.checkbox") || q_format.title == t("helpers.multi_select_box") || + if (q_format.title == t("helpers.checkbox") || q_format.title == t("helpers.multi_select_box") || q_format.title == t("helpers.radio_buttons") || q_format.title == t("helpers.dropdown")) then - details["sections"][s.number]["questions"][q.number]["selections"] = {} - answer.options.each do |o| - details["sections"][s.number]["questions"][q.number]["selections"][o.number] = o.text - end - end - details["sections"][s.number]["questions"][q.number]["answer_text"] = answer.text - end - end - end - return details - end + details["sections"][s.number]["questions"][q.number]["selections"] = {} + answer.options.each do |o| + details["sections"][s.number]["questions"][q.number]["selections"][o.number] = o.text + end + end + details["sections"][s.number]["questions"][q.number]["answer_text"] = answer.text + end + end + end + return details + end ## # determines wether or not a specified section of a plan is locked to a specified user and returns a status hash @@ -351,55 +360,55 @@ # @param section_id [Integer] the setion to determine if locked # @param user_id [Integer] the user to determine if locked for # @return [Hash{String => Hash{String => Boolean, nil, String, Integer}}] - def locked(section_id, user_id) - plan_section = plan_sections.where("section_id = ? AND user_id != ? AND release_time > ?", section_id, user_id, Time.now).last - if plan_section.nil? then - status = { - "locked" => false, - "locked_by" => nil, - "timestamp" => nil, - "id" => nil - } - else - status = { - "locked" => true, - "locked_by" => plan_section.user.name, - "timestamp" => plan_section.updated_at, - "id" => plan_section.id - } - end - end + def locked(section_id, user_id) + plan_section = plan_sections.where("section_id = ? AND user_id != ? AND release_time > ?", section_id, user_id, Time.now).last + if plan_section.nil? then + status = { + "locked" => false, + "locked_by" => nil, + "timestamp" => nil, + "id" => nil + } + else + status = { + "locked" => true, + "locked_by" => plan_section.user.name, + "timestamp" => plan_section.updated_at, + "id" => plan_section.id + } + end + end ## # for each section, lock the section with the given user_id # # @param user_id [Integer] the id for the user who can use the sections - def lock_all_sections(user_id) - sections.each do |s| - lock_section(s.id, user_id, 1800) - end - end + def lock_all_sections(user_id) + sections.each do |s| + lock_section(s.id, user_id, 1800) + end + end ## # for each section, unlock the section # # @param user_id [Integer] the id for the user to unlock the sections for - def unlock_all_sections(user_id) - plan_sections.where(:user_id => user_id).order("created_at DESC").each do |lock| - lock.delete - end - end + def unlock_all_sections(user_id) + plan_sections.where(:user_id => user_id).order("created_at DESC").each do |lock| + lock.delete + end + end ## # for each section, unlock the section # Not sure how this is different from unlock_all_sections # # @param user_id [Integer] - def delete_recent_locks(user_id) - plan_sections.where(:user_id => user_id).each do |lock| - lock.delete - end - end + def delete_recent_locks(user_id) + plan_sections.where(:user_id => user_id).each do |lock| + lock.delete + end + end ## # Locks the specified section to only be used by the specified user, for the number of secconds specified @@ -408,23 +417,23 @@ # @param user_id [Integer] the id of the user who can use the section # @param release_time [Integer] the number of secconds the section will be locked for, defaults to 60 # @return [Boolean] wether or not the section was locked - def lock_section(section_id, user_id, release_time = 60) - status = locked(section_id, user_id) - if ! status["locked"] then - plan_section = PlanSection.new - plan_section.plan_id = id - plan_section.section_id = section_id - plan_section.release_time = Time.now + release_time.seconds - plan_section.user_id = user_id - plan_section.save - elsif status["current_user"] then - plan_section = PlanSection.find(status["id"]) - plan_section.release_time = Time.now + release_time.seconds - plan_section.save - else - return false - end - end + def lock_section(section_id, user_id, release_time = 60) + status = locked(section_id, user_id) + if ! status["locked"] then + plan_section = PlanSection.new + plan_section.plan_id = id + plan_section.section_id = section_id + plan_section.release_time = Time.now + release_time.seconds + plan_section.user_id = user_id + plan_section.save + elsif status["current_user"] then + plan_section = PlanSection.find(status["id"]) + plan_section.release_time = Time.now + release_time.seconds + plan_section.save + else + return false + end + end ## # unlocks the specified section for the specified user @@ -432,28 +441,28 @@ # @param section_id [Integer] the id for the section to be unlocked # @param user_id [Integer] the id for the user for whom the section was previously locked # @return [Boolean] wether or not the lock was removed - def unlock_section(section_id, user_id) - plan_sections.where(:section_id => section_id, :user_id => user_id).order("created_at DESC").each do |lock| - lock.delete - end - end + def unlock_section(section_id, user_id) + plan_sections.where(:section_id => section_id, :user_id => user_id).order("created_at DESC").each do |lock| + lock.delete + end + end ## # returns the time of either the latest answer to any question, or the latest update to the model # # @return [DateTime] the time at which the plan was last changed - def latest_update - if answers.any? then - last_answered = answers.order("updated_at DESC").first.updated_at - if last_answered > updated_at then - return last_answered - else - return updated_at - end - else - return updated_at - end - end + def latest_update + if answers.any? then + last_answered = answers.order("updated_at DESC").first.updated_at + if last_answered > updated_at then + return last_answered + else + return updated_at + end + else + return updated_at + end + end ## # returns an array of hashes. Each hash contains the question's id, the answer_id, @@ -461,89 +470,89 @@ # # @param section_id [Integer] the section to find answers of # @return [Array nil,String,Integer,DateTime}] - def section_answers(section_id) - section = Section.find(section_id) - section_questions = Array.new - counter = 0 - section.questions.each do |q| - section_questions[counter] = {} - section_questions[counter]["id"] = q.id - #section_questions[counter]["multiple_choice"] = q.multiple_choice - q_answer = answer(q.id, false) - if q_answer.nil? then - section_questions[counter]["answer_id"] = nil - if q.suggested_answers.find_by_organisation_id(project.organisation_id).nil? then - section_questions[counter]["answer_text"] = "" - else - section_questions[counter]["answer_text"] = q.default_value - end - section_questions[counter]["answer_timestamp"] = nil - section_questions[counter]["answer_options"] = Array.new - else - section_questions[counter]["answer_id"] = q_answer.id - section_questions[counter]["answer_text"] = q_answer.text - section_questions[counter]["answer_timestamp"] = q_answer.created_at - section_questions[counter]["answer_options"] = q_answer.options.pluck(:id) - end - counter = counter + 1 - end - return section_questions - end + def section_answers(section_id) + section = Section.find(section_id) + section_questions = Array.new + counter = 0 + section.questions.each do |q| + section_questions[counter] = {} + section_questions[counter]["id"] = q.id + #section_questions[counter]["multiple_choice"] = q.multiple_choice + q_answer = answer(q.id, false) + if q_answer.nil? then + section_questions[counter]["answer_id"] = nil + if q.suggested_answers.find_by_organisation_id(project.organisation_id).nil? then + section_questions[counter]["answer_text"] = "" + else + section_questions[counter]["answer_text"] = q.default_value + end + section_questions[counter]["answer_timestamp"] = nil + section_questions[counter]["answer_options"] = Array.new + else + section_questions[counter]["answer_id"] = q_answer.id + section_questions[counter]["answer_text"] = q_answer.text + section_questions[counter]["answer_timestamp"] = q_answer.created_at + section_questions[counter]["answer_options"] = q_answer.options.pluck(:id) + end + counter = counter + 1 + end + return section_questions + end private ## - # Based on the height of the text gathered so far and the available vertical - # space of the pdf, estimate a percentage of how much space has been used. - # This is highly dependent on the layout in the pdf. A more accurate approach - # would be to render the pdf and check how much space had been used, but that - # could be very slow. - # NOTE: This is only an estimate, rounded up to the nearest 5%; it is intended - # for guidance when editing plan data, not to be 100% accurate. + # Based on the height of the text gathered so far and the available vertical + # space of the pdf, estimate a percentage of how much space has been used. + # This is highly dependent on the layout in the pdf. A more accurate approach + # would be to render the pdf and check how much space had been used, but that + # could be very slow. + # NOTE: This is only an estimate, rounded up to the nearest 5%; it is intended + # for guidance when editing plan data, not to be 100% accurate. # # @param used_height [Integer] an estimate of the height used so far # @return [Integer] the estimate of space used of an A4 portrain - def estimate_space_used(used_height) - @formatting ||= self.settings(:export).formatting + def estimate_space_used(used_height) + @formatting ||= self.settings(:export).formatting - return 0 unless @formatting[:font_size] > 0 + return 0 unless @formatting[:font_size] > 0 - margin_height = @formatting[:margin][:top].to_i + @formatting[:margin][:bottom].to_i - page_height = A4_PAGE_HEIGHT - margin_height # 297mm for A4 portrait - available_height = page_height * self.dmptemplate.settings(:export).max_pages + margin_height = @formatting[:margin][:top].to_i + @formatting[:margin][:bottom].to_i + page_height = A4_PAGE_HEIGHT - margin_height # 297mm for A4 portrait + available_height = page_height * self.dmptemplate.settings(:export).max_pages - percentage = (used_height / available_height) * 100 - (percentage / ROUNDING).ceil * ROUNDING # round up to nearest five - end + percentage = (used_height / available_height) * 100 + (percentage / ROUNDING).ceil * ROUNDING # round up to nearest five + end ## - # Take a guess at the vertical height (in mm) of the given text based on the - # font-size and left/right margins stored in the plan's settings. - # This assumes a fixed-width for each glyph, which is obviously - # incorrect for the font-face choices available; the idea is that - # they'll hopefully average out to that in the long-run. - # Allows for hinting different font sizes (offset from base via font_size_inc) - # and vertical margins (i.e. for heading text) + # Take a guess at the vertical height (in mm) of the given text based on the + # font-size and left/right margins stored in the plan's settings. + # This assumes a fixed-width for each glyph, which is obviously + # incorrect for the font-face choices available; the idea is that + # they'll hopefully average out to that in the long-run. + # Allows for hinting different font sizes (offset from base via font_size_inc) + # and vertical margins (i.e. for heading text) # # @param text [String] the text to estimate size of # @param font_size_inc [Integer] the size of the font of the text, defaults to 0 # @param vertical_margin [Integer] the top margin above the text, defaults to 0 - def height_of_text(text, font_size_inc = 0, vertical_margin = 0) - @formatting ||= self.settings(:export).formatting - @margin_width ||= @formatting[:margin][:left].to_i + @formatting[:margin][:right].to_i - @base_font_size ||= @formatting[:font_size] + def height_of_text(text, font_size_inc = 0, vertical_margin = 0) + @formatting ||= self.settings(:export).formatting + @margin_width ||= @formatting[:margin][:left].to_i + @formatting[:margin][:right].to_i + @base_font_size ||= @formatting[:font_size] - return 0 unless @base_font_size > 0 + return 0 unless @base_font_size > 0 - font_height = FONT_HEIGHT_CONVERSION_FACTOR * (@base_font_size + font_size_inc) - font_width = font_height * FONT_WIDTH_HEIGHT_RATIO # Assume glyph width averages at 2/5s the height - leading = font_height / 2 + font_height = FONT_HEIGHT_CONVERSION_FACTOR * (@base_font_size + font_size_inc) + font_width = font_height * FONT_WIDTH_HEIGHT_RATIO # Assume glyph width averages at 2/5s the height + leading = font_height / 2 - chars_in_line = (A4_PAGE_WIDTH - @margin_width) / font_width # 210mm for A4 portrait - num_lines = (text.length / chars_in_line).ceil + chars_in_line = (A4_PAGE_WIDTH - @margin_width) / font_width # 210mm for A4 portrait + num_lines = (text.length / chars_in_line).ceil - (num_lines * font_height) + vertical_margin + leading - end + (num_lines * font_height) + vertical_margin + leading + end diff --git a/test/test_helper.rb b/test/test_helper.rb index 8d0057c..9b8050d 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -36,31 +36,37 @@ # each of the possible Question Formats. # ---------------------------------------------------------------------- def scaffold_template - template = Template.create(title: 'Test template', description: 'My test template', - published: true, org: Org.first, locale: nil, is_default: false, - version: 1, visibility: 0) + template = Template.new(title: 'Test template', description: 'My test template', + published: true, org: Org.first, locale: nil, is_default: false, + version: 1, visibility: 0) - phase = Phase.create(title: 'Test phase', description: 'My test phase', number: 1, - template: template, modifiable: false) + template.phases << Phase.new(title: 'Test phase', description: 'My test phase', + number: 1, modifiable: false) - section = Section.create(title: 'Test section', description: 'My test section', - number: 99, published: true, phase: phase, modifiable: false) + section = Section.new(title: 'Test section', description: 'My test section', + number: 99, published: true, modifiable: false) i = 1 # Add each type of Question to the new section QuestionFormat.all.each do |frmt| - q = Question.create(text: "Test question - #{frmt.title}", number: i, - question_format: frmt, section: section) + question = Question.new(text: "Test question - #{frmt.title}", number: i, + question_format: frmt) if frmt.option_based? 3.times do |j| - QuestionOption.create(text: "Option #{j}", number: j, question: q) + question.question_options << QuestionOption.new(text: "Option #{j}", number: j) end end + section.questions << question i += 1 end + template.phases.first.sections << section + + assert template.valid?, "unable to create new Template: #{template.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" + template.save! + @template = template.reload end @@ -69,10 +75,14 @@ def scaffold_plan scaffold_template if @template.nil? - @plan = Plan.create(template: @template, title: 'Test Plan', grant_number: 'Grant-123', + @plan = Plan.new(template: @template, title: 'Test Plan', grant_number: 'Grant-123', principal_investigator: 'me', principal_investigator_identifier: 'me-1234', description: "this is my plan's informative description", - identifier: '1234567890', data_contact: 'me@example.com', visibility: 0) + identifier: '1234567890', data_contact: 'me@example.com', visibility: 0, + users: [User.last]) + + assert @plan.valid?, "unable to create new Plan: #{@plan.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" + @plan.save! end diff --git a/test/unit/answer_test.rb b/test/unit/answer_test.rb index 5ac6e5b..991ab88 100644 --- a/test/unit/answer_test.rb +++ b/test/unit/answer_test.rb @@ -57,6 +57,15 @@ end # --------------------------------------------------- + test "answer's template must match the plan's template" do + plan = Plan.new(title: 'Wrong plan test', template: Template.where.not(id: @plan.template.id).first) + q = @plan.template.questions.select{|q| !q.question_format.option_based }.first + + assert_not Answer.new(user: @user, plan: plan, question: @plan.questions.first, + text: 'Testing').valid?, "expected to only be able to add an answer if it belongs to the template associated with the plan" + end + + # --------------------------------------------------- test "can CRUD answers for text based questions" do QuestionFormat.where(option_based: false).each do |qf| q = @plan.template.questions.select{|q| q.question_format == qf }.first @@ -102,7 +111,7 @@ # --------------------------------------------------- test "can manage belongs_to relationship with Plan" do - verify_belongs_to_relationship(@answer, Plan.last) + verify_belongs_to_relationship(@answer, @plan) end # --------------------------------------------------- diff --git a/test/unit/plan_test.rb b/test/unit/plan_test.rb new file mode 100644 index 0000000..1275ab6 --- /dev/null +++ b/test/unit/plan_test.rb @@ -0,0 +1,76 @@ +require 'test_helper' + +class PlanTest < ActiveSupport::TestCase + + setup do + @org = Org.first + @template = Template.first + @plan = Plan.create(title: 'Test Plan', template: @template, grant_number: 'Plan12345', + identifier: '000912', description: 'This is a test plan', + principal_investigator: 'John Doe', principal_investigator_identifier: 'ABC', + data_contact: 'john.doe@example.com', visibility: 1, users: [User.last]) + end + + # --------------------------------------------------- + test "required fields are required" do + assert_not Plan.new.valid? + assert_not Plan.new(users: [User.last], title: 'Testing').valid?, "expected the template field to be required" + assert_not Plan.new(template: @template, title: 'Testing').valid?, "expected at least one user to be required" + + # Make sure that the Settings gem is defaulting the title for us + assert Plan.new(users: [User.last], template: @template).valid?, "expected the title field to have been set by default by the Settings gem" + + # Ensure the bare minimum and complete versions are valid + a = Plan.new(title: 'Testing', template: @template, users: [User.last]) + assert a.valid?, "expected the 'title', 'template' and at least one 'user' fields to be enough to create an Plan! - #{a.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" + end + + # --------------------------------------------------- + test "a slug is properly generated when creating a record" do + #p = Plan.create(title: 'Testing 123', template: @template, users: [User.last]) + #assert_equal "testing-123", p.slug + end + + # --------------------------------------------------- + test "has_sections returns false if there are NO published versions with sections" do + # TODO: build out this test if the has_sections method is actually necessary + end + + # --------------------------------------------------- + test "can CRUD Plan" do + obj = Plan.create(title: 'Testing CRUD', template: Template.where.not(id: @template.id).first, + users: [User.last], description: "should change") + assert_not obj.id.nil?, "was expecting to be able to create a new Plan! - #{obj.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}" + + obj.description = 'changed' + obj.save! + obj.reload + assert_equal 'changed', obj.description, "Was expecting to be able to update the title of the Plan!" + + assert obj.destroy!, "Was unable to delete the Plan!" + end + + # --------------------------------------------------- + test "can manage has_many relationship with Answers" do + a = Answer.new(user: User.last, plan: @plan, question: @plan.questions.first, text: 'Test!') + verify_has_many_relationship(@plan, a, @plan.answers.count) + end + + # --------------------------------------------------- + test "can manage has_many relationship with Users" do + verify_has_many_relationship(@plan, User.first, @plan.users.count) + end + + # --------------------------------------------------- + test "can manage has_many relationship with ExportedPlan" do + ep = ExportedPlan.create(format: ExportedPlan::VALID_FORMATS.last) + verify_has_many_relationship(@plan, ep, @plan.exported_plans.count) + end + + # --------------------------------------------------- + test "can manage belongs_to relationship with Template" do + tmplt = Template.create(org: @org, title: 'Testing relationship') + verify_belongs_to_relationship(@plan, tmplt) + end + +end