Managing loaner chromebooks for students and teachers in the HUUSD school district.
1# == Schema Information
2#
3# Table name: loaners
4#
5# id :integer not null, primary key
6# active :boolean
7# asset_tag :string
8# serial_number :string
9# status :string
10# created_at :datetime not null
11# updated_at :datetime not null
12# current_loan_id :integer
13# freindly_id :integer
14# loaner_id :integer
15#
16# Indexes
17#
18# index_loaners_on_asset_tag (asset_tag)
19# index_loaners_on_current_loan_id (current_loan_id)
20# index_loaners_on_loaner_id (loaner_id)
21# index_loaners_on_status (status)
22#
23# Foreign Keys
24#
25# current_loan_id (current_loan_id => loans.id)
26#
27class AssetTagValidator < ActiveModel::Validator
28 def validate(record)
29 # Increment a counter for validation attempts
30 StatsD.increment("asset_tag_validation_attempts")
31
32 if record.asset_tag.blank?
33 # Log and increment counter for blank asset tag errors
34 StatsD.increment("asset_tag_validation_blank_errors")
35 record.errors.add(:asset_tag, "can't be blank")
36 elsif record.asset_tag =~ /^[0-9]{6}$/
37 # Log successful validation
38 StatsD.increment("asset_tag_validation_success")
39 else
40 # Log and increment counter for format errors
41 StatsD.increment("asset_tag_validation_format_errors")
42 record.errors.add(:asset_tag, 'must be a 6-digit number')
43 end
44 end
45end
46
47class Loaner < ApplicationRecord
48 include AASM
49
50 before_create :set_loaner_id
51
52 def chrome_device
53 # TODO: Get Google Auth working
54 GoogleService.instance.get_chrome_device(self.serial_number)
55 end
56
57 def chrome_status
58 chrome_device.status
59 end
60
61 def chrome_status_update(status)
62 chrome_device.status = status
63 # TODO: Get Google Auth working
64 GoogleService.instance.update_chrome_device(self.serial_number, chrome_device)
65 end
66
67 def chrome_disable
68 chrome_status_update("DISABLED")
69 end
70
71 def chrome_enable
72 chrome_status_update("ACTIVE")
73 end
74
75 aasm column: 'status' do
76 state :available, initial: true, display: "Available"
77 state :loaned, display: "Loaned"
78 state :disabled, display: "Disabled"
79 state :maintenance, display: "Maintenance"
80 state :decommissioned, display: "Decommissioned"
81
82 after_all_transitions :log_status_change
83
84 event :loan do
85 transitions from: :available, to: :loaned
86
87 after do
88 StatsD.increment("loaner.loaned")
89 assign_current_loan
90 end
91 end
92
93 event :return do
94 transitions from: :loaned, to: :available
95
96 after do
97 StatsD.increment("loaner.returned")
98
99 if self.current_loan
100 self.current_loan.return!
101 else
102 Rails.logger.warn "Attempted to return a loaner without a current loan"
103 end
104 end
105 end
106
107
108 event :disable do
109 transitions from: [:available, :loaned], to: :disabled
110
111 after do
112 chrome_disable()
113 StatsD.increment("loaner.disabled")
114 end
115 end
116
117 event :enable do
118 transitions from: :disabled, to: :available
119
120 after do
121 chrome_enable()
122 StatsD.increment("loaner.enabled")
123 end
124 end
125
126 event :broken do
127 transitions from: [:available, :loaned], to: :maintenance
128
129 after do
130 StatsD.increment("loaner.broken")
131 end
132 end
133
134 event :repair do
135 transitions from: :maintenance, to: :available
136
137 after do
138 StatsD.increment("loaner.repaired")
139 end
140 end
141 end
142
143 def log_status_change
144 StatsD.increment("loaner.status_change", tags: ["from:#{aasm.from_state}", "to:#{aasm.to_state}", "event:#{aasm.current_event}"])
145 Rails.logger.info "Changing from #{aasm.from_state} to #{aasm.to_state} (event: #{aasm.current_event})"
146 end
147
148 validates :asset_tag, presence: true, uniqueness: true
149 validates_with AssetTagValidator
150 validates :serial_number, uniqueness: true
151 validates :loaner_id, presence: true, uniqueness: true
152
153 has_many :loans, foreign_key: 'loaner_id'
154 has_one :current_loan, -> { where(status: 'out').order(loaned_at: :desc) }, class_name: 'Loan'
155 has_many :borrowers, through: :loans
156
157 private
158
159 def set_loaner_id
160 StatsD.measure("loaner.set_loaner_id_time") do
161 max_id = Loaner.maximum(:loaner_id) || 0
162 self.loaner_id = max_id + 1
163 StatsD.increment("loaner.id_set")
164 end
165 end
166
167 def assign_current_loan
168 StatsD.measure("loaner.assign_current_loan_time") do
169 current_loan = loans.pending.first
170 if current_loan
171 self.current_loan_id = current_loan.id
172 save!
173 StatsD.increment("loaner.current_loan_assigned")
174 current_loan.loan!
175 end
176 end
177 end
178end