app/models/event.rb
# == Schema Information## Table name: events## id :integer not null, primary key# acceptances_have_been_sent :boolean default(FALSE)# application_deadline :date# custom_application_fields :text# custom_image :string# description :text# hidden :boolean default(FALSE)# image :string# knowledge_level :string# max_participants :integer# name :string# organizer :string# published :boolean default(FALSE)# rejections_have_been_sent :boolean default(FALSE)# created_at :datetime not null# updated_at :datetime not null# Class `Event` has 40 methods (exceeds 20 allowed). Consider refactoring.class Event < ActiveRecord::Base UNREASONABLY_LONG_DATE_SPAN = 300 TRUNCATE_DESCRIPTION_TEXT_LENGTH = 250 serialize :custom_application_fields, Array mount_uploader :custom_image, EventImageUploader has_many :application_letters has_many :agreement_letters has_many :participant_groups has_many :date_ranges accepts_nested_attributes_for :date_ranges validates_presence_of :name, :description, :application_deadline validates :max_participants, numericality: { only_integer: true, greater_than: 0 } validate :has_date_ranges validate :application_deadline_before_start_of_event validates :hidden, inclusion: { in: [true, false] } validates :hidden, exclusion: { in: [nil] } validates :published, inclusion: { in: [true, false] } validates :published, exclusion: { in: [nil] } validate :check_image_dimensions after_validation :update_image # if we uploaded a custom image, we want it to be synced to the "official" # image slot. only do this if we actually uploaded one and that image is valid def update_imageFavor modifier `if` usage when having a single-line body. Another good alternative is the usage of control flow `&&`/`||`. if custom_image.filename.present? && errors[:custom_image].empty? self.image = '/' + custom_image.list_view.store_path end end # Use the image dimensions as returned from our uploader # to verify that the image has sufficient size def check_image_dimensions if custom_image.upload_width.present? && custom_image.upload_height.present? && (custom_image.upload_width < 200 || custom_image.upload_height < 155) errors.add(:custom_image, I18n.t('events.errors.image_too_small')) custom_image.remove! end end # Returns all participants for this event in following order: # 1. All participants that have to submit an letter of agreement but did not yet do so, ordered by name. # 2. All participants that have to submit an letter of agreement and did do so, ordered by name. # 3. All participants that do not have to submit an letter of agreement, ordered by name. # # @param none # @return [Array<User>] the event's participants in that order. def participants_by_agreement_letter @participants = participants @participants.sort { |x, y| compare_participants_by_agreement(x, y) } end # Checks if the participant selection is locked # # @param none # @return true if participant selection is locked def participant_selection_locked acceptances_have_been_sent || rejections_have_been_sent end # @return the minimum start_date over all date ranges def start_date date_ranges.min_by(&:start_date).start_date end # @return the minimum end_date over all date ranges def end_date date_ranges.max_by(&:end_date).end_date end # @return whether this event appears unreasonably long as defined by # the corresponding constant def unreasonably_long end_date - start_date > Rails.configuration.unreasonably_long_event_time_span end # validation function on whether we have at least one date range def has_date_ranges errors.add(:date_ranges, I18n.t('date_range.errors.no_timespan')) if date_ranges.blank? end # validate that application deadline is before the start of the event def application_deadline_before_start_of_event errors.add(:application_deadline, I18n.t('events.errors.application_deadline_before_start_of_event')) if application_deadline.present? && !date_ranges.blank? && application_deadline > start_date end # Checks if the application deadline is over # # @param none # @return [Boolean] true if deadline is over def after_deadline? Date.current > application_deadline end # Returns the participants whose application for this Event has been accepted # # @param none # @return [Array<User>] the event's participants def participants accepted_applications = application_letters.where(status: ApplicationLetter.statuses[:accepted]) accepted_applications.collect(&:user) end # Returns the participant group for this event for a given participant (user). If it doesn't exist, it is created # # @param user [User] the user whose participant group we want # @return [ParticipantGroup] the user's participant group def participant_group_for(user) participant_group = participant_groups.find_by(user: user)Favor modifier `if` usage when having a single-line body. Another good alternative is the usage of control flow `&&`/`||`. if participant_group.nil? participant_group = ParticipantGroup.create(event: self, user: user, group: ParticipantGroup::GROUPS.default) end participant_group end # Returns the agreement letter a user submitted for this event # # @param user [User] the user whose agreement letter we want # @return [AgreementLetter, nil] the user's agreement letter or nil def agreement_letter_for(user) agreement_letters.where(user: user).take end # Returns whether all application_letters are classified or not # # @param none # @return [Boolean] if status of all application_letters is not pending def applications_classified? application_letters.all? { |application_letter| application_letter.status != 'pending' } end # Returns the tooltip used to help explain to the user why he can't send mails yet # # @return [String] the translated tooltip text or nil if mails can be sent def send_mails_tooltip if !applications_classified? I18n.t 'events.applicants_overview.unclassified_applications_left' elsif compute_free_places < 0 I18n.t 'events.applicants_overview.maximum_number_of_participants_exeeded' end end # Sets the status of all the event's application letters to accepted # # @param none # @return none def accept_all_application_letters application_letters.map { |application| application.update(status: :accepted) } end # Sets the status_notification_sent flag for all application letters of the given type # # @param status [Type] the desired application status the flag should be set for # @return none def set_status_notification_flag_for_applications_with_status(status) applications = application_letters.select { |application| application.status == status.to_s } applications.each do |application_letter| application_letter.update(status_notification_sent: true) end end # Returns an array of strings of all email addresses of applications with a given status type # # @param type [Type] the status type of the email addresses that will be returned # @return [Array<String>] Array of all email addresses of applications with given type, that don't have status_notification_sent set def email_addresses_of_type_without_notification_sent(type) applications = application_letters.where(status: ApplicationLetter.statuses[type], status_notification_sent: false) applications.collect { |a| a.user.email } end # Returns a new email to a set of participants # # @param all [Boolean] If set to true, addresses all participants # @param groups [Array<Integer>] The group-ids whoose members should be addressed # @param users [Array<Integer>] The user-ids which should be addressed # @return [Email] new email def generate_participants_email(all, groups, users) Email.new( hide_recipients: false, recipients: email_addresses_of_participants(all, groups, users), reply_to: '', subject: '', content: '' ) end # Returns a list of tuples containing all participant names and their id # # @return [Array<Array<String, Int>>] def participants_with_id participants.map { |participant| [participant.profile.name, participant.id] } end # Returns a list of group names and their id # # @return Array<Array<String, Int>> def groups_with_id existing = ParticipantGroup::GROUPS.select do |group_id, _| group_id != 0 && participant_groups.where(group: group_id).any? end existing.map do |group_id, color_code| [I18n.t("participant_groups.options.#{color_code}"), group_id] end end # Returns the number of free places of the event, this value may be negative # # @param none # @return [Int] for number of free places available def compute_free_places max_participants - compute_occupied_places end # Returns the number of already occupied places of the event # # @param none # @return [Int] for number of occupied places def compute_occupied_places application_letters.where(status: ApplicationLetter.statuses[:accepted]).count end # Returns whether there are applications of given state with no status notification sent of the event # # @param [Symbol] status # @return [Boolean] true if there are participants of given state without status notification sent def has_participants_without_status_notification?(status) application_letters.exists?(status: ApplicationLetter.statuses[status], status_notification_sent: false) end # Returns the current state of the event (draft-, application-, selection- and execution-phase) # # @param none # @return [Symbol] stateMethod `phase` has a Cognitive Complexity of 6 (exceeds 5 allowed). Consider refactoring. def phase return :draft unless published return :application if published && !after_deadline? return :selection if published && after_deadline? && !(acceptances_have_been_sent && rejections_have_been_sent) return :execution if published && after_deadline? && acceptances_have_been_sent && rejections_have_been_sent end # Returns whether the event has application letters with status alternative # # @param none # @return [Boolean] true, if any exists, false otherwise def has_alternative_application_letters? application_letters.any? { |application| application.status == 'alternative' } end # Returns a label listing the number of days to the deadline if # it's <= 7 days to go. Otherwise returns nil. # # @return string containing the label or nil def application_deadline_label days = (application_deadline - Date.current).to_i return I18n.t('events.notices.deadline_approaching', count: days) if days <= 7 && days >= 0 end # Uses the start date to determine whether or not this event is in the past (or more # precisely, in the past or currently running) # # @return boolean if it's in the past def is_past start_date < Date.current end # Returns a label that describes the duration of the event in days, # also mentioning whether or not the event happens on consecutive # days. # # @return the duration label or nil def duration_label # gotta add 1 since from Sunday to Monday is on two days, but only # a difference of a single day days = (end_date - start_date).to_i + 1 if date_ranges.size > 1 I18n.t('events.notices.time_span_non_consecutive', count: days) else I18n.t('events.notices.time_span_consecutive', count: days) end end # Returns the application letters ordered by either "email", "first_name", "last_name", "birth_date" # either "asc" (ascending) or "desc" (descending). # # @param field [String] the field that should be used to order # @param order_by [String] the order that should be used # @return [ApplicationLetter] the application letters found def application_letters_ordered(field, order_by) field = case field when 'email' 'users.email' when 'birth_date', 'first_name', 'last_name' 'profiles.' + field else 'users.email' endAvoid comparing a variable with multiple items in a conditional, use `Array#include?` instead. order_by = 'asc' unless order_by == 'asc' || order_by == 'desc' application_letters.joins(user: :profile).order(field + ' ' + order_by) end # Make sure any assignment coming from the controller # replaces all date ranges instead of adding new ones def date_ranges_attributes=(*args) date_ranges.clear super(*args) end # Gets the path of the event in the material storage # # @return [String] path in the material storage def material_path File.join('storage/materials/', id.to_s + '_event') end # Make sure we add errors from our date_range children # to the base event object for displaying validate do |event| event.date_ranges.each do |date_range| next if date_range.valid? date_range.errors.full_messages.each do |msg| errors.add :date_ranges, msg unless errors[:date_ranges].include? msg end end end scope :draft_is, ->(status) { where('not published = ?', status) } scope :hidden_is, ->(status) { where('hidden = ?', status) } scope :with_date_ranges, -> { joins(:date_ranges).group('events.id').order('MIN(start_date)') } scope :future, -> { with_date_ranges.having('date(MAX(end_date)) > ?', Time.zone.yesterday.end_of_day) } scope :past, -> { with_date_ranges.having('date(MAX(end_date)) < ?', Time.zone.now.end_of_day) } # Returns events sorted by start date, returning only public ones # if requested # # @param limit Maximum number of events to return # @param only_public Set to true to not include drafts and hidden events # @return List of events def self.sorted_by_start_date(only_public) (only_public ? Event.draft_is(false).where(hidden: false) : Event.all) .sort_by(&:start_date) end # Returns the date_ranges of the event in ical format # # @param none # @return ical attachment of date_ranges def get_ical_attachment cal = Icalendar::Calendar.new for date_range in date_ranges do cal.event do |e| e.dtstart = date_range.start_date e.dtend = date_range.end_date e.summary = name e.description = description e.url = Rails.application.routes.url_helpers.event_path(self) end end { name: (I18n.t 'emails.ical_attachment'), content: cal.to_ical } end protected # Returns a string of all email addresses of accepted applications # # @param none # @return [String] Concatenation of all email addresses of accepted applications, seperated by ',' def email_addresses_of_accepted_applicants participants.collect(&:email).join(',') end # Returns a list of email addresses of all participants that are in one of the given groups # # @return [String] def email_addresses_of_groups(groups) groups = [] if groups.nil? groups.reduce([]) { |addresses, group| addresses + email_addresses_of_group(group) }.uniq end # Returns a list of email addresses of all participants that are in this exact group # # @return [String] def email_addresses_of_group(group) participant_groups.where(group: group).map { |participant_group| participant_group.user.email } end # Returns a list of email addresses def email_addresses_of_users(users) user = User.where(id: users) user.map(&:email) end # Returns all email addresses of user that fit the given criteria # # @param all [Boolean] If set to true, return addresses of all participants # @param groups [Array<Integer>] The group-ids whoose members-addresses should be looked up # @param users [Array<Integer>] The user-ids whoose addresses should be returned # @return [String] list of email addresses def email_addresses_of_participants(all, groups, users) if all email_addresses_of_accepted_applicants else (email_addresses_of_groups(groups) + email_addresses_of_users(users)).uniq.join(',') end end # Compares two participants to achieve following order: # 1. All participants that have to submit an letter of agreement but did not yet do so, ordered by email. # 2. All participants that have to submit an letter of agreement and did do so, ordered by email. # 3. All participants that do not have to submit an letter of agreement, ordered by email.Method `compare_participants_by_agreement` has a Cognitive Complexity of 11 (exceeds 5 allowed). Consider refactoring. def compare_participants_by_agreement(participant1, participant2) unless participant1.requires_agreement_letter_for_event?(self)Favor modifier `unless` usage when having a single-line body. Another good alternative is the usage of control flow `&&`/`||`. unless participant2.requires_agreement_letter_for_event?(self) return participant1.email <=> participant2.email end return 1 end return -1 unless participant2.requires_agreement_letter_for_event?(self) if participant1.agreement_letter_for_event?(self)Favor modifier `if` usage when having a single-line body. Another good alternative is the usage of control flow `&&`/`||`. if participant2.agreement_letter_for_event?(self) return participant1.email <=> participant2.email endAvoid too many `return` statements within this method. return 1 endAvoid too many `return` statements within this method. return -1 if participant2.agreement_letter_for_event?(self) participant1.email <=> participant2.email endend