Sprankelprachtig aan/afmeldsysteem

activity.rb 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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. #
  50. # @!attribute no_response_action
  51. # @return [Boolean]
  52. # what action to take when a participant has not responded and the
  53. # reminder is being sent. True to set the participant to attending, false
  54. # to set to absent.
  55. belongs_to :group
  56. has_many :participants,
  57. dependent: :destroy
  58. has_many :people, through: :participants
  59. has_many :subgroups,
  60. dependent: :destroy
  61. validates :name, presence: true
  62. validates :start, presence: true
  63. validate :deadline_before_start, unless: "self.deadline.blank?"
  64. validate :end_after_start, unless: "self.end.blank?"
  65. validate :reminder_before_deadline, unless: "self.reminder_at.blank?"
  66. validate :subgroups_for_division_present, on: :update
  67. after_create :create_missing_participants!
  68. after_create :copy_default_subgroups!
  69. after_create :schedule_reminder
  70. after_create :schedule_subgroup_division
  71. after_commit :schedule_reminder,
  72. if: proc { |a| a.previous_changes["reminder_at"] }
  73. after_commit :schedule_subgroup_division,
  74. if: proc { |a|
  75. (a.previous_changes['deadline'] ||
  76. a.previous_changes['subgroup_division_enabled']) &&
  77. !a.subgroup_division_done &&
  78. a.subgroup_division_enabled
  79. }
  80. # Get all people (not participants) that are organizers. Does not include
  81. # group leaders, although they may modify the activity as well.
  82. def organizers
  83. participants.includes(:person).where(is_organizer: true)
  84. end
  85. def organizer_names
  86. organizers.map { |o| o.person.full_name }
  87. end
  88. # Determine whether the passed Person participates in the activity.
  89. def participant?(person)
  90. Participant.exists?(
  91. activity_id: id,
  92. person_id: person.id
  93. )
  94. end
  95. # Determine whether the passed Person is an organizer for the activity.
  96. def organizer?(person)
  97. Participant.exists?(
  98. person_id: person.id,
  99. activity_id: id,
  100. is_organizer: true
  101. )
  102. end
  103. # Query the database to determine the amount of participants that are present/absent/unknown
  104. def state_counts
  105. participants.group(:attending).count
  106. end
  107. # Return participants attending, absent, unknown
  108. def human_state_counts
  109. c = state_counts
  110. p = c[true]
  111. a = c[false]
  112. u = c[nil]
  113. "#{p || 0}, #{a || 0}, #{u || 0}"
  114. end
  115. # Determine whether the passed Person may change this activity.
  116. def may_change?(person)
  117. person.is_admin ||
  118. organizer?(person) ||
  119. group.leader?(person)
  120. end
  121. # Create Participants for all People that
  122. # 1. are members of the group
  123. # 2. do not have Participants (and thus, no way to confirm) yet
  124. def create_missing_participants!
  125. people = group.people
  126. people = people.where('people.id NOT IN (?)', self.people.ids) unless participants.empty?
  127. people.each do |p|
  128. Participant.create(
  129. activity: self,
  130. person: p
  131. )
  132. end
  133. end
  134. # Create Subgroups from the defaults set using DefaultSubgroups
  135. def copy_default_subgroups!
  136. defaults = group.default_subgroups
  137. # If there are no subgroups, there cannot be subgroup division.
  138. update!(:subgroup_division_enabled, false) if defaults.none?
  139. defaults.each do |dsg|
  140. sg = Subgroup.new(activity: self)
  141. sg.name = dsg.name
  142. sg.is_assignable = dsg.is_assignable
  143. sg.save! # Should never fail, as DSG and SG have identical validation, and names cannot clash.
  144. end
  145. end
  146. # Create multiple Activities from data in a CSV file, assign to a group, return.
  147. def self.from_csv(content, group)
  148. reader = CSV.parse(content, headers: true, skip_blanks: true)
  149. result = []
  150. reader.each do |row|
  151. a = Activity.new
  152. a.group = group
  153. a.name = row['name']
  154. a.description = row['description']
  155. a.location = row['location']
  156. sd = Date.parse row['start_date']
  157. st = Time.strptime(row['start_time'], '%H:%M')
  158. a.start = Time.zone.local(sd.year, sd.month, sd.day, st.hour, st.min)
  159. if row['end_date'].present?
  160. ed = Date.parse row['end_date']
  161. et = Time.strptime(row['end_time'], '%H:%M')
  162. a.end = Time.zone.local(ed.year, ed.month, ed.day, et.hour, et.min)
  163. end
  164. if row['deadline_date'].present?
  165. dd = Date.parse row['deadline_date']
  166. dt = Time.strptime(row['deadline_time'], '%H:%M')
  167. a.deadline = Time.zone.local(dd.year, dd.month, dd.day, dt.hour, dt.min)
  168. end
  169. if row['reminder_at_date'].present?
  170. rd = Date.parse row['reminder_at_date']
  171. rt = Time.strptime(row['reminder_at_time'], '%H:%M')
  172. a.reminder_at = Time.zone.local(rd.year, rd.month, rd.day, rt.hour, rt.min)
  173. end
  174. a.subgroup_division_enabled = row['subgroup_division_enabled'].casecmp('y').zero? if row['subgroup_division_enabled'].present?
  175. a.no_response_action = row['no_response_action'].casecmp('p').zero? if row['no_response_action'].present?
  176. result << a
  177. end
  178. result
  179. end
  180. # Send a reminder to all participants who haven't responded, and set their
  181. # response to 'attending'.
  182. def send_reminder
  183. # Sanity check that the reminder date didn't change while queued.
  184. return unless !reminder_done && reminder_at
  185. return if reminder_at > Time.zone.now
  186. participants = self.participants.where(attending: nil)
  187. participants.each(&:send_reminder)
  188. self.reminder_done = true
  189. save
  190. end
  191. def schedule_reminder
  192. return if reminder_at.nil? || reminder_done
  193. delay(run_at: reminder_at).send_reminder
  194. end
  195. def schedule_subgroup_division
  196. return if deadline.nil? || subgroup_division_done
  197. delay(run_at: deadline).assign_subgroups!(mail: true)
  198. end
  199. # Assign a subgroup to all attending participants without one.
  200. def assign_subgroups!(mail = false)
  201. # Sanity check: we need subgroups to divide into.
  202. return unless subgroups.any?
  203. # Get participants in random order
  204. ps =
  205. participants
  206. .where(attending: true)
  207. .where(subgroup: nil)
  208. .to_a
  209. ps.shuffle!
  210. # Get groups, link to participant count
  211. groups =
  212. subgroups
  213. .where(is_assignable: true)
  214. .to_a
  215. .map { |sg| [sg.participants.count, sg] }
  216. ps.each do |p|
  217. # Sort groups so the group with the least participants gets the following participant
  218. groups.sort!
  219. # Assign participant to group with least members
  220. p.subgroup = groups.first.second
  221. p.save
  222. # Update the group's position in the list, will sort when next participant is processed.
  223. groups.first[0] += 1
  224. end
  225. notify_subgroups! if mail
  226. end
  227. def clear_subgroups!(only_assignable = true)
  228. sgs =
  229. subgroups
  230. if only_assignable
  231. sgs = sgs
  232. .where(is_assignable: true)
  233. end
  234. ps =
  235. participants
  236. .where(subgroup: sgs)
  237. ps.each do |p|
  238. p.subgroup = nil
  239. p.save
  240. end
  241. end
  242. # Notify participants of the current subgroups, if any.
  243. def notify_subgroups!
  244. ps =
  245. participants
  246. .joins(:person)
  247. .where.not(subgroup: nil)
  248. ps.each(&:send_subgroup_notification)
  249. end
  250. # @return [Activity] the Activity that will start after this Activity. `nil` if no such Activity exists.
  251. def next_in_group
  252. group.activities
  253. .where('start > ?', start)
  254. .order(start: :asc)
  255. .first
  256. end
  257. # @return [Activity] the Activity that started before this Activity. `nil` if no such Activity exists.
  258. def previous_in_group
  259. group.activities
  260. .where('start < ?', start)
  261. .order(start: :desc)
  262. .first
  263. end
  264. private
  265. # Assert that the deadline for participants to change the deadline, if any,
  266. # is set before the event starts.
  267. def deadline_before_start
  268. errors.add(:deadline, I18n.t('activities.errors.must_be_before_start')) if deadline > start
  269. end
  270. # Assert that the activity's end, if any, occurs after the event's start.
  271. def end_after_start
  272. errors.add(:end, I18n.t('activities.errors.must_be_after_start')) if self.end < start
  273. end
  274. # Assert that the reminder for non-response is sent while participants still
  275. # can change their response.
  276. def reminder_before_deadline
  277. errors.add(:reminder_at, I18n.t('activities.errors.must_be_before_deadline')) if reminder_at > deadline
  278. end
  279. # Assert that there is at least one divisible subgroup.
  280. def subgroups_for_division_present
  281. errors.add(:subgroup_division_enabled, I18n.t('activities.errors.cannot_divide_without_subgroups')) if subgroups.where(is_assignable: true).none? && subgroup_division_enabled?
  282. end
  283. end