Sprankelprachtig aan/afmeldsysteem

activity.rb 8.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. # An Activity represents a single continuous event that the members of a group may attend.
  2. # An Activity belongs to a group, and has many participants.
  3. class Activity < ApplicationRecord
  4. # @!attribute name
  5. # @return [String]
  6. # a short name for the activity.
  7. #
  8. # @!attribute description
  9. # @return [String]
  10. # a short text describing the activity. This text is always visible to
  11. # all users.
  12. #
  13. # @!attribute location
  14. # @return [String]
  15. # a short text describing where the activity will take place. Always
  16. # visible to all participants.
  17. #
  18. # @!attribute start
  19. # @return [TimeWithZone]
  20. # when the activity starts.
  21. #
  22. # @!attribute end
  23. # @return [TimeWithZone]
  24. # when the activity ends.
  25. #
  26. # @!attribute deadline
  27. # @return [TimeWithZone]
  28. # when the normal participants (everyone who isn't an organizer or group
  29. # leader) may not change their own attendance anymore. Disabled if set to
  30. # nil.
  31. #
  32. # @!attribute reminder_at
  33. # @return [TimeWithZone]
  34. # when all participants which haven't responded yet (attending is nil)
  35. # will be automatically set to 'present' and emailed. Must be before the
  36. # deadline, disabled if nil.
  37. #
  38. # @!attribute reminder_done
  39. # @return [Boolean]
  40. # whether or not sending the reminder has finished.
  41. #
  42. # @!attribute subgroup_division_enabled
  43. # @return [Boolean]
  44. # whether automatic subgroup division on the deadline is enabled.
  45. #
  46. # @!attribute subgroup_division_done
  47. # @return [Boolean]
  48. # whether subgroup division has been performed.
  49. belongs_to :group
  50. has_many :participants,
  51. dependent: :destroy
  52. has_many :people, through: :participants
  53. has_many :subgroups,
  54. dependent: :destroy
  55. validates :name, presence: true
  56. validates :start, presence: true
  57. validate :deadline_before_start, unless: "self.deadline.blank?"
  58. validate :end_after_start, unless: "self.end.blank?"
  59. validate :reminder_before_deadline, unless: "self.reminder_at.blank?"
  60. validate :subgroups_for_division_present, on: :update
  61. after_create :create_missing_participants!
  62. after_create :copy_default_subgroups!
  63. after_commit :schedule_reminder,
  64. if: Proc.new { |a| a.previous_changes["reminder_at"] }
  65. after_commit :schedule_subgroup_division,
  66. if: Proc.new { |a| (a.previous_changes['deadline'] ||
  67. a.previous_changes['subgroup_division_enabled']) &&
  68. !a.subgroup_division_done &&
  69. a.subgroup_division_enabled }
  70. # Get all people (not participants) that are organizers. Does not include
  71. # group leaders, although they may modify the activity as well.
  72. def organizers
  73. self.participants.includes(:person).where(is_organizer: true)
  74. end
  75. def organizer_names
  76. self.organizers.map { |o| o.person.full_name }
  77. end
  78. # Determine whether the passed Person participates in the activity.
  79. def is_participant?(person)
  80. Participant.exists?(
  81. activity_id: self.id,
  82. person_id: person.id
  83. )
  84. end
  85. # Determine whether the passed Person is an organizer for the activity.
  86. def is_organizer?(person)
  87. Participant.exists?(
  88. person_id: person.id,
  89. activity_id: self.id,
  90. is_organizer: true
  91. )
  92. end
  93. # Query the database to determine the amount of participants that are present/absent/unknown
  94. def state_counts
  95. self.participants.group(:attending).count
  96. end
  97. # Return participants attending, absent, unknown
  98. def human_state_counts
  99. c = self.state_counts
  100. p = c[true]
  101. a = c[false]
  102. u = c[nil]
  103. return "#{p or 0}, #{a or 0}, #{u or 0}"
  104. end
  105. # Determine whether the passed Person may change this activity.
  106. def may_change?(person)
  107. person.is_admin ||
  108. self.is_organizer?(person) ||
  109. self.group.is_leader?(person)
  110. end
  111. # Create Participants for all People that
  112. # 1. are members of the group
  113. # 2. do not have Participants (and thus, no way to confirm) yet
  114. def create_missing_participants!
  115. people = self.group.people
  116. if not self.participants.empty?
  117. people = people.where('people.id NOT IN (?)', self.people.ids)
  118. end
  119. people.each do |p|
  120. Participant.create(
  121. activity: self,
  122. person: p,
  123. )
  124. end
  125. end
  126. # Create Subgroups from the defaults set using DefaultSubgroups
  127. def copy_default_subgroups!
  128. defaults = self.group.default_subgroups
  129. # If there are no subgroups, there cannot be subgroup division.
  130. self.update_attribute(:subgroup_division_enabled, false) if defaults.none?
  131. defaults.each do |dsg|
  132. sg = Subgroup.new(activity: self)
  133. sg.name = dsg.name
  134. sg.is_assignable = dsg.is_assignable
  135. sg.save! # Should never fail, as DSG and SG have identical validation, and names cannot clash.
  136. end
  137. end
  138. # Create multiple Activities from data in a CSV file, assign to a group, return.
  139. def self.from_csv(content, group)
  140. reader = CSV.parse(content, {headers: true, skip_blanks: true})
  141. result = []
  142. reader.each do |row|
  143. a = Activity.new
  144. a.group = group
  145. a.name = row['name']
  146. a.description = row['description']
  147. a.location = row['location']
  148. sd = Date.strptime(row['start_date'])
  149. st = Time.strptime(row['start_time'], '%H:%M')
  150. a.start = Time.zone.local(sd.year, sd.month, sd.day, st.hour, st.min)
  151. if not row['end_date'].blank?
  152. ed = Date.strptime(row['end_date'])
  153. et = Time.strptime(row['end_time'], '%H:%M')
  154. a.end = Time.zone.local(ed.year, ed.month, ed.day, et.hour, et.min)
  155. end
  156. dd = Date.strptime(row['deadline_date'])
  157. dt = Time.strptime(row['deadline_time'], '%H:%M')
  158. a.deadline = Time.zone.local(dd.year, dd.month, dd.day, dt.hour, dt.min)
  159. result << a
  160. end
  161. result
  162. end
  163. # Send a reminder to all participants who haven't responded, and set their
  164. # response to 'attending'.
  165. def send_reminder
  166. # Sanity check that the reminder date didn't change while queued.
  167. return unless !self.reminder_done && self.reminder_at
  168. return if self.reminder_at > Time.zone.now
  169. participants = self.participants.where(attending: nil)
  170. participants.each { |p| p.send_reminder }
  171. self.reminder_done = true
  172. self.save
  173. end
  174. def schedule_reminder
  175. return if self.reminder_at.nil? || self.reminder_done
  176. self.delay(run_at: self.reminder_at).send_reminder
  177. end
  178. def schedule_subgroup_division
  179. return if self.deadline.nil? || self.subgroup_division_done
  180. self.delay(run_at: self.deadline).assign_subgroups!(mail: true)
  181. end
  182. # Assign a subgroup to all attending participants without one.
  183. def assign_subgroups!(mail= false)
  184. # Sanity check: we need subgroups to divide into.
  185. return unless self.subgroups.any?
  186. # Get participants in random order
  187. ps = self
  188. .participants
  189. .where(attending: true)
  190. .where(subgroup: nil)
  191. .to_a
  192. ps.shuffle!
  193. # Get groups, link to participant count
  194. groups = self
  195. .subgroups
  196. .where(is_assignable: true)
  197. .to_a
  198. .map { |sg| [sg.participants.count, sg] }
  199. ps.each do |p|
  200. # Sort groups so the group with the least participants gets the following participant
  201. groups.sort!
  202. # Assign participant to group with least members
  203. p.subgroup = groups.first.second
  204. p.save
  205. # Update the group's position in the list, will sort when next participant is processed.
  206. groups.first[0] += 1
  207. end
  208. if mail
  209. self.notify_subgroups!
  210. end
  211. end
  212. # Notify participants of the current subgroups, if any.
  213. def notify_subgroups!
  214. ps = self
  215. .participants
  216. .joins(:person)
  217. .where.not(subgroup: nil)
  218. ps.each do |pp|
  219. pp.send_subgroup_notification
  220. end
  221. end
  222. private
  223. # Assert that the deadline for participants to change the deadline, if any,
  224. # is set before the event starts.
  225. def deadline_before_start
  226. if self.deadline > self.start
  227. errors.add(:deadline, I18n.t('activities.errors.must_be_before_start'))
  228. end
  229. end
  230. # Assert that the activity's end, if any, occurs after the event's start.
  231. def end_after_start
  232. if self.end < self.start
  233. errors.add(:end, I18n.t('activities.errors.must_be_after_start'))
  234. end
  235. end
  236. # Assert that the reminder for non-response is sent while participants still
  237. # can change their response.
  238. def reminder_before_deadline
  239. if self.reminder_at > self.deadline
  240. errors.add(:reminder_at, I18n.t('activities.errors.must_be_before_deadline'))
  241. end
  242. end
  243. # Assert that there is at least one divisible subgroup.
  244. def subgroups_for_division_present
  245. if self.subgroups.where(is_assignable: true).none?
  246. errors.add(:subgroup_division_enabled, I18n.t('activities.errors.cannot_divide_without_subgroups'))
  247. end
  248. end
  249. end