# An Activity represents a single continuous event that the members of a group may attend. # An Activity belongs to a group, and has many participants. class Activity < ApplicationRecord # @!attribute name # @return [String] # a short name for the activity. # # @!attribute description # @return [String] # a short text describing the activity. This text is always visible to # all users. # # @!attribute location # @return [String] # a short text describing where the activity will take place. Always # visible to all participants. # # @!attribute start # @return [TimeWithZone] # when the activity starts. # # @!attribute end # @return [TimeWithZone] # when the activity ends. # # @!attribute deadline # @return [TimeWithZone] # when the normal participants (everyone who isn't an organizer or group # leader) may not change their own attendance anymore. Disabled if set to # nil. # # @!attribute reminder_at # @return [TimeWithZone] # when all participants which haven't responded yet (attending is nil) # will be automatically set to 'present' and emailed. Must be before the # deadline, disabled if nil. # # @!attribute reminder_done # @return [Boolean] # whether or not sending the reminder has finished. # # @!attribute subgroup_division_enabled # @return [Boolean] # whether automatic subgroup division on the deadline is enabled. # # @!attribute subgroup_division_done # @return [Boolean] # whether subgroup division has been performed. # # @!attribute no_response_action # @return [Boolean] # what action to take when a participant has not responded and the # reminder is being sent. True to set the participant to attending, false # to set to absent. belongs_to :group has_many :participants, dependent: :destroy has_many :people, through: :participants has_many :subgroups, dependent: :destroy validates :name, presence: true validates :start, presence: true validate :deadline_before_start, unless: "self.deadline.blank?" validate :end_after_start, unless: "self.end.blank?" validate :reminder_before_deadline, unless: "self.reminder_at.blank?" validate :subgroups_for_division_present, on: :update after_create :create_missing_participants! after_create :copy_default_subgroups! after_create :schedule_reminder after_create :schedule_subgroup_division after_commit :schedule_reminder, if: Proc.new { |a| a.previous_changes["reminder_at"] } after_commit :schedule_subgroup_division, if: Proc.new { |a| (a.previous_changes['deadline'] || a.previous_changes['subgroup_division_enabled']) && !a.subgroup_division_done && a.subgroup_division_enabled } # Get all people (not participants) that are organizers. Does not include # group leaders, although they may modify the activity as well. def organizers self.participants.includes(:person).where(is_organizer: true) end def organizer_names self.organizers.map { |o| o.person.full_name } end # Determine whether the passed Person participates in the activity. def is_participant?(person) Participant.exists?( activity_id: self.id, person_id: person.id ) end # Determine whether the passed Person is an organizer for the activity. def is_organizer?(person) Participant.exists?( person_id: person.id, activity_id: self.id, is_organizer: true ) end # Query the database to determine the amount of participants that are present/absent/unknown def state_counts self.participants.group(:attending).count end # Return participants attending, absent, unknown def human_state_counts c = self.state_counts p = c[true] a = c[false] u = c[nil] return "#{p or 0}, #{a or 0}, #{u or 0}" end # Determine whether the passed Person may change this activity. def may_change?(person) person.is_admin || self.is_organizer?(person) || self.group.is_leader?(person) end # Create Participants for all People that # 1. are members of the group # 2. do not have Participants (and thus, no way to confirm) yet def create_missing_participants! people = self.group.people if not self.participants.empty? people = people.where('people.id NOT IN (?)', self.people.ids) end people.each do |p| Participant.create( activity: self, person: p, ) end end # Create Subgroups from the defaults set using DefaultSubgroups def copy_default_subgroups! defaults = self.group.default_subgroups # If there are no subgroups, there cannot be subgroup division. self.update_attribute(:subgroup_division_enabled, false) if defaults.none? defaults.each do |dsg| sg = Subgroup.new(activity: self) sg.name = dsg.name sg.is_assignable = dsg.is_assignable sg.save! # Should never fail, as DSG and SG have identical validation, and names cannot clash. end end # Create multiple Activities from data in a CSV file, assign to a group, return. def self.from_csv(content, group) reader = CSV.parse(content, { headers: true, skip_blanks: true }) result = [] reader.each do |row| a = Activity.new a.group = group a.name = row['name'] a.description = row['description'] a.location = row['location'] sd = Date.strptime(row['start_date']) st = Time.strptime(row['start_time'], '%H:%M') a.start = Time.zone.local(sd.year, sd.month, sd.day, st.hour, st.min) unless row['end_date'].blank? ed = Date.strptime(row['end_date']) et = Time.strptime(row['end_time'], '%H:%M') a.end = Time.zone.local(ed.year, ed.month, ed.day, et.hour, et.min) end unless row['deadline_date'].blank? dd = Date.strptime(row['deadline_date']) dt = Time.strptime(row['deadline_time'], '%H:%M') a.deadline = Time.zone.local(dd.year, dd.month, dd.day, dt.hour, dt.min) end unless row['reminder_at_date'].blank? rd = Date.strptime(row['reminder_at_date']) rt = Time.strptime(row['reminder_at_time'], '%H:%M') a.reminder_at = Time.zone.local(rd.year, rd.month, rd.day, rt.hour, rt.min) end unless row['subgroup_division_enabled'].blank? a.subgroup_division_enabled = row['subgroup_division_enabled'].downcase == 'y' end unless row['no_response_action'].blank? a.no_response_action = row['no_response_action'].downcase == 'p' end result << a end result end # Send a reminder to all participants who haven't responded, and set their # response to 'attending'. def send_reminder # Sanity check that the reminder date didn't change while queued. return unless !self.reminder_done && self.reminder_at return if self.reminder_at > Time.zone.now participants = self.participants.where(attending: nil) participants.each { |p| p.send_reminder } self.reminder_done = true self.save end def schedule_reminder return if self.reminder_at.nil? || self.reminder_done self.delay(run_at: self.reminder_at).send_reminder end def schedule_subgroup_division return if self.deadline.nil? || self.subgroup_division_done self.delay(run_at: self.deadline).assign_subgroups!(mail: true) end # Assign a subgroup to all attending participants without one. def assign_subgroups!(mail = false) # Sanity check: we need subgroups to divide into. return unless self.subgroups.any? # Get participants in random order ps = self .participants .where(attending: true) .where(subgroup: nil) .to_a ps.shuffle! # Get groups, link to participant count groups = self .subgroups .where(is_assignable: true) .to_a .map { |sg| [sg.participants.count, sg] } ps.each do |p| # Sort groups so the group with the least participants gets the following participant groups.sort! # Assign participant to group with least members p.subgroup = groups.first.second p.save # Update the group's position in the list, will sort when next participant is processed. groups.first[0] += 1 end if mail self.notify_subgroups! end end def clear_subgroups!(only_assignable = true) sgs = self .subgroups if only_assignable sgs = sgs .where(is_assignable: true) end ps = self .participants .where(subgroup: sgs) ps.each do |p| p.subgroup = nil p.save end end # Notify participants of the current subgroups, if any. def notify_subgroups! ps = self .participants .joins(:person) .where.not(subgroup: nil) ps.each do |pp| pp.send_subgroup_notification end end private # Assert that the deadline for participants to change the deadline, if any, # is set before the event starts. def deadline_before_start if self.deadline > self.start errors.add(:deadline, I18n.t('activities.errors.must_be_before_start')) end end # Assert that the activity's end, if any, occurs after the event's start. def end_after_start if self.end < self.start errors.add(:end, I18n.t('activities.errors.must_be_after_start')) end end # Assert that the reminder for non-response is sent while participants still # can change their response. def reminder_before_deadline if self.reminder_at > self.deadline errors.add(:reminder_at, I18n.t('activities.errors.must_be_before_deadline')) end end # Assert that there is at least one divisible subgroup. def subgroups_for_division_present if self.subgroups.where(is_assignable: true).none? && subgroup_division_enabled? errors.add(:subgroup_division_enabled, I18n.t('activities.errors.cannot_divide_without_subgroups')) end end end