aboutsummaryrefslogtreecommitdiff
blob: 078c37c9edea459c63201ddf728d8e47917b9397 (plain)
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