Completed
Push — master ( c714c7...739021 )
by
unknown
05:26 queued 03:15
created

Session.attendance_count()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
1
class Session < ActiveRecord::Base
2
3
  has_many :categorizations, :dependent => :destroy
4
  has_many :categories, :through => :categorizations
5
  belongs_to :participant  # TODO: rename to 'owner'
6
7
  has_many :presentations, :dependent => :destroy
8
  has_many :presenters, :through => :presentations, :source => :participant
9
  belongs_to :event
10
  belongs_to :timeslot
11
  belongs_to :room
12
  belongs_to :level
13
  has_many :attendances, :dependent => :destroy
14
  has_many :participants, :through => :attendances
15
16
  delegate :name, to: :room, prefix: true, allow_nil: true
17
  delegate :starts_at, to: :timeslot, allow_nil: true
18
  delegate :name, to: :level, prefix: true, allow_nil: true
19
20
  scope :with_attendence_count, -> { select('*').joins("LEFT OUTER JOIN (SELECT session_id, count(id) AS attendence_count FROM attendances GROUP BY session_id) AS attendence_aggregation ON attendence_aggregation.session_id = sessions.id") }
21
22
  scope :for_current_event, -> { where(event_id: Event.current_event.id) }
23
24
  scope :recent, -> { order('created_at desc') }
25
  
26
  scope :random_order, -> { order('random()') }
27
28
  validates_presence_of :description
29
  validates_presence_of :event_id
30
  validates_presence_of :participant_id
31
  validates_presence_of :title
32
  validates_length_of :summary, :maximum => 100, :allow_blank => true
33
  #validates_uniqueness_of :timeslot_id, :scope => :room_id, :allow_blank => true, :message => 'and room combination already in use'
34
35
36
  attr_accessor :name, :email
37
38
  after_create :create_presenter
39
40
  def self.swap_timeslot_and_rooms(session_1, session_2)
41
    Session.transaction do
42
      session_1.room, session_2.room = session_2.room, session_1.room
43
      session_1.timeslot, session_2.timeslot = session_2.timeslot, session_1.timeslot
44
      session_1.save
45
      session_2.save
46
    end
47
  end
48
49
  def self.swap_rooms(session_1, session_2)
50
    if session_1.timeslot != session_2.timeslot
51
      raise "Sessions must be in the same timeslot to swap"
52
    end
53
54
    Session.transaction do
55
      session_1.room, session_2.room = session_2.room, session_1.room
56
      session_1.save
57
      session_2.save
58
    end
59
  end
60
61
  def self.attendee_preferences
62
    result = {}
63
    sessions = Event.current_event.sessions.includes(:participants)
64
65
    sessions.each do |session|
66
      prefs = {}
67
68
      session.participant_ids.each do |p_id|
69
        prefs[p_id] = 1
70
      end
71
72
      result[session.id] = prefs
73
    end
74
75
    result
76
  end
77
78
79
  def self.session_similarity()
80
    Rails.cache.fetch('session_similarity', :expires_in => 30.minutes) do
81
      preferences = Session.attendee_preferences
82
      ::Recommender.calculate_similar_items(preferences, 5)
83
    end
84
  end
85
86
  def presenter_names
87
    presenters.map(&:name)
88
  end
89
90
  def other_presenters
91
    presenters.reject{ |p| p == self.participant }
92
  end
93
  def other_presenter_names
94
    other_presenters.map(&:name)
95
  end
96
97
98
  def attending?(user)
99
    return false if user.nil?
100
101
    participants.include?(user)
102
  end
103
104
  def recommended_sessions
105
    similarity = Session.session_similarity()
106
    recommended = similarity[self.id]
107
108
    if recommended
109
      # find will not order by recommendation strength; use conditions instead of find to ignore missing sessions in the cache
110
      sessions = Session.where(["id in (?)", recommended.map { |r| r[1] }])
111
      sessions.sort_by do |session|
112
        recommended.find_index { |r| r[1] == session.id }
113
      end
114
    else
115
      []
116
    end
117
  end
118
119
  # The raw number of votes for this session.
120
  #
121
  # To avoid O(n) queries, use preload_attendance_counts if you’ll be calling this on a
122
  # collection of sessions.
123
  #
124
  def attendance_count
125
    @attendance_count ||= attendances.count
126
  end
127
  attr_writer :attendance_count  # for preload
128
129
  def self.preload_attendance_counts(sessions)
130
    sessions_by_id = {}
131
    sessions.each do |session|
132
      sessions_by_id[session.id] = session
133
    end
134
    
135
    # Surely there’s a Rails helper for this?
136
    # But I can’t find it — only some abandoned gems.
137
    Attendance
138
      .select("session_id, count(*) as attendance_count")
139
      .where('session_id in (?)', sessions_by_id.keys)
140
      .group('session_id')
141
      .each do |row|
142
        sessions_by_id[row.session_id].attendance_count = row.attendance_count
143
      end
144
145
    nil
146
  end
147
148
  # Estimates actual event-day interest for this session relative to other sessions,
149
  # expressed as a corrected number of votes.
150
  #
151
  # Sessions that were created earlier tend to accumulate more votes, so the naive method
152
  # of using raw vote count will underestimate interest in sessions created later.
153
  # To fix that, we take the number of votes this session received as a proportion of all
154
  # votes cast since it was created. We also include a normalizing factor for last-minute
155
  # sessions created after most of the voting was already done.
156
  #
157
  def estimated_interest
158
    @estimated_interest ||= begin
159
      session_votes     = attendance_count.to_f
160
      possible_votes    = event.attendances.where('attendances.created_at >= ?', created_at).count.to_f
161
      session_count     = event.sessions.count.to_f
162
      participant_count = event.participants.count.to_f
163
164
      # For sessions created at the last minute, we don't have enough information to make
165
      # a good estimate; both session_votes and possible_votes are too low. If we just divide
166
      # session_votes / possible_votes, we'll get wildly inaccurate answers when the denominator
167
      # is small.
168
      #
169
      # We therefore add some ghost "ballast votes" across the board to all sessions, so as to
170
      # make estimated_interest tend toward the mean in cases when there are few real votes.
171
172
      ballast_votes = 3.0
173
174
      (session_votes + ballast_votes) / (possible_votes + ballast_votes * session_count) * participant_count
175
    end
176
  end
177
178
  def to_h
179
    SessionsJsonBuilder.new.to_hash(self)
180
  end
181
182
  private
183
184
  # assign the creator as the first presenter
185
  def create_presenter
186
    presentations.create(participant: participant)
187
  end
188
189
end
190