1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
|
#TODO: merge before_creation hooks
# Model representing signed up users.
# Hooks:
# * before creation application gives user token
# * before creation application gives user mentor role and Gentoo nick if user
# used https://dev.gentoo.org/~nick openid
class User < ActiveRecord::Base
hobo_user_model # Don't put anything above this
fields do
name :string, :required, :unique
email_address :email_address, :login => true
administrator :boolean, :default => false
role Role, :default => 'recruit'
nick :string
openid :string
contributions HoboFields::MarkdownString
project_lead :boolean, :default => false
token :string
timestamps
end
has_many :user_categories
has_many :user_question_groups
has_many :question_categories, :through => :user_categories, :accessible => true, :uniq => true
has_many :grouped_questions, :through => :user_question_groups
has_many :answers, :foreign_key => :owner_id
has_many :answered_questions, :through => :answers, :class_name => "Question", :source => :question
has_many :project_acceptances, :accessible => true, :uniq => true
belongs_to :mentor, :class_name => "User"
has_many :recruits, :class_name => "User", :foreign_key => :mentor_id
named_scope :mentorless_recruits, :conditions => { :role => 'recruit', :mentor_id => nil}
named_scope :recruits_answered_all, :conditions => "role = 'recruit' AND NOT EXISTS
(SELECT questions.id FROM questions
INNER JOIN question_categories cat ON questions.question_category_id = cat.id INNER JOIN
user_categories ON user_categories.question_category_id = cat.id WHERE
user_categories.user_id = users.id AND questions.question_group_id IS NULL AND NOT EXISTS (
SELECT answers.id FROM answers WHERE answers.question_id = questions.id AND answers.owner_id = users.id))
AND NOT EXISTS
(SELECT questions.id FROM questions INNER JOIN user_question_groups ON questions.id = user_question_groups.question_id
WHERE user_question_groups.user_id = users.id AND NOT EXISTS (
SELECT answers.id FROM answers WHERE answers.question_id = questions.id AND answers.owner_id = users.id))"
# --- Signup lifecycle --- #
lifecycle do
state :active, :default => true
create :signup, :available_to => "Guest",
:params => [:name, :email_address, :password, :password_confirmation],
:become => :active
transition :request_password_reset, { :active => :active }, :new_key => true do
UserMailer.deliver_forgot_password(self, lifecycle.key)
end
transition :reset_password, { :active => :active }, :available_to => :key_holder,
:params => [ :password, :password_confirmation ]
end
validate :only_recruiter_can_be_administrator
validate :recruit_cant_mentor
validate :mentors_and_recruiters_must_have_nick
validate :mentor_is_gentoo_dev_long_enough
validates_uniqueness_of :nick, :if => :nick
validates_uniqueness_of :openid, :if => :openid
never_show :project_lead
# Token
never_show :token
# Generate new token
def token=(more_salt)
# Time.now.to_f.to_s gives enough precision to be considered random
token = Digest::SHA1.hexdigest("#{Time.now.to_f.to_s}#{@salt}#{more_salt}")
write_attribute("token", token)
token
end
# Give user token on creation
before_create do |u|
u.token = ''
end
# --- Permissions --- #
def create_permitted?
false
end
def update_permitted?
# Allow edit in one of four cases:
# Acting user is administrator
# Acting user is editing his/her self and changes only what [s]he is allowed to
# Acting user is recruiter and changes only what [s]he is allowed to
# Acting user was mentor of edited user and resigned
# Acting user became mentor of edited recruit and edited user had no mentor
return true if acting_user.administrator?
return true if acting_user == self && changes_allowed_to_self?
return true if acting_user.role.is_recruiter? && changes_allowed_for_recruiter?
return true if role.is_recruit? && acting_user.role.is_mentor? && mentor_picked_up_or_resigned?
end
def role_edit_permitted?
acting_user.role.is_recruiter?
end
def mentor_edit_permitted?
return true if mentor_is?(acting_user)
return true if mentor.nil? && acting_user.role.is_mentor?
return true if mentor.nil? && acting_user.role.is_recruiter?
return true if acting_user.administrator?
false
end
def destroy_permitted?
acting_user.administrator?
end
def view_permitted?(field)
true
end
# Returns all questions user should answer
# TODO: rewrite as SQL (?)
def all_questions
Question.ungrouped_questions_of_user(id) + Question.grouped_questions_of_user(id)
end
# Returns questions user should answer but didn't answer yet.
def unanswered_questions
Question.unanswered_grouped(id) + Question.unanswered_ungrouped(id)
end
def answered_all_questions?
Question.unanswered_grouped(id).count.zero? && Question.unanswered_ungrouped(id).count.zero?
end
# Returns true if there is at least one unapproved project acceptance for this user
def any_pending_project_acceptances?
(ProjectAcceptance.count :conditions => { :accepting_nick => nick, :accepted => false}) > 0
end
# This returns named scope, so it's efficient to use
# some_user.questions_to_approve.count
def questions_to_approve
if administrator?
Question.questions_to_approve
else
[]
end
end
def answered_all_multi_choice_questions?
Question.multiple_choice.ungrouped_questions_of_user(id).unanswered(id).count == 0 &&
Question.multiple_choice.grouped_questions_of_user(id).unanswered_grouped(id).count == 0
end
def required_questions_count
Question.ungrouped_questions_of_user(id).count + Question.grouped_questions_of_user(id).count
end
def required_unanswered_count
Question.unanswered_grouped(id).count + Question.unanswered_ungrouped(id).count
end
def required_answered_count
self.required_questions_count - self.required_unanswered_count
end
# Returns string describing recruit progress. Includes information only about
# question recruit is required to answer.
def progress
"Answered #{self.required_answered_count} of #{self.required_questions_count} questions."
end
before_create do |u|
# Users using OpenID from dev.gentoo.org are mentors
# Note that this doesn't make sure user can authenticate with
# specified OpenID.
match = /^https:\/\/dev.gentoo.org\/~(\w+)$/i.match(u.openid)
if match
u.role = :mentor
u.nick = match.captures.first
end
end
protected
def only_recruiter_can_be_administrator
errors.add(:administrator, 'only recruiters can be administrators') if administrator and !role.is_recruiter?
end
def recruit_cant_mentor
errors.add(:mentor, "recruit can't mentor") if mentor && mentor.role.is_recruit?
end
def mentors_and_recruiters_must_have_nick
if (role.is_mentor? || role.is_recruiter?) && (nick.nil? || nick.empty?)
errors.add(:nick, "Mentors and administrators must have nicks set")
end
end
def changes_allowed_for_recruiter?
return mentor_picked_up_or_resigned? if mentor_changed?
# make sure recruiters change only what they are allowed to
return false unless only_changed?(:question_categories, :role, :nick)
# and make sure change to role wasn't changed or was promotion of recruit
# to mentor or demotion of mentor to recruit
return true unless role_changed?
return true if role.is_mentor? && Role.new(role_was).is_recruit?
return true if role.is_recruit? && Role.new(role_was).is_mentor?
false
end
def changes_allowed_to_self?
only_changed?(:email_address, :crypted_password, :current_password,
:password, :password_confirmation, :nick, :contributions, :name)
# Note: crypted_password has attr_protected so although it is permitted to change, it cannot be changed
# directly from a form submission.
end
def mentor_picked_up_or_resigned?
# Mentor picked up or resigned if
# only mentor attribute was changed
# and mentor changed from nil to acting user
# or mentor changed from nil to current user
return false unless only_changed?(:mentor)
return false unless (mentor_id_was.nil? || (mentor_id_was == acting_user.id))
return false unless (mentor_id.nil? || (mentor_id == acting_user.id))
true
end
def mentor_is_gentoo_dev_long_enough
return unless role.is_mentor? # User isn't mentor
return unless APP_CONFIG['developer_data']['check'] # Configured not to check
dev_data_str = APP_CONFIG['developer_data']['data'] # Data about developers from configuration
if dev_data_str.nil? # If there isn't any fetch it from configured location
uri = URI.parse(APP_CONFIG['developer_data']['uri'])
dev_data_str = Net::HTTP.get_response(uri).body
end
dev_data = YAML::load(dev_data_str)['developers']
max_join_date = APP_CONFIG['developer_data']['min_months_mentor_is_dev'].to_i.months.ago.to_date
for dev in dev_data
if dev['nick'] == nick # Found dev
joined = dev['joined'].to_date
return if joined < max_join_date # Joined early enough: finish check, no errors
# Otherwise: finish check, report error
errors.add(:role, "developer with provided nick can't be mentor yet (joined Gentoo less then
#{ APP_CONFIG['min_months_mentor_is_dev']} months ago)")
return
end
end
errors.add(:nick, "not found this nick amoung Gentoo Developers.")
end
end
|