send email when admin logs in
[shapado:shapado.git] / app / models / user.rb
1 require 'digest/sha1'
2
3 class User
4   include Mongoid::Document
5   include Mongoid::Timestamps
6   include MultiauthSupport
7   include MongoidExt::Storage
8   include Shapado::Models::GeoCommon
9
10   devise :database_authenticatable, :recoverable, :registerable, :rememberable,
11          :lockable, :token_authenticatable, :encryptable, :trackable, :omniauthable, :encryptor => :restful_authentication_sha1
12
13   ROLES = %w[user moderator admin]
14   LANGUAGE_FILTERS = %w[any user] + AVAILABLE_LANGUAGES
15   LOGGED_OUT_LANGUAGE_FILTERS = %w[any] + AVAILABLE_LANGUAGES
16
17   identity :type => String
18   field :login,                     :type => String, :limit => 40, :index => true
19   field :name,                      :type => String, :limit => 100, :default => '', :null => true
20
21   field :bio,                       :type => String, :limit => 200
22   field :website,                   :type => String, :limit => 200
23   field :location,                  :type => String, :limit => 200
24   field :birthday,                  :type => Time
25
26   field :identity_url,              :type => String
27   index :identity_url
28
29   field :role,                      :type => String, :default => "user"
30   field :last_logged_at,            :type => Time
31
32   field :preferred_languages,       :type => Array, :default => []
33
34   field :language,                  :type => String, :default => "en"
35   index :language
36   field :timezone,                  :type => String
37   field :language_filter,           :type => String, :default => "user", :in => LANGUAGE_FILTERS
38
39   field :ip,                        :type => String
40   field :country_code,              :type => String
41   field :country_name,              :type => String, :default => "unknown"
42   field :hide_country,              :type => Boolean, :default => false
43
44   field :default_subtab,            :type => Hash, :default => {}
45
46   field :followers_count,           :type => Integer, :default => 0
47   field :following_count,           :type => Integer, :default => 0
48
49   field :membership_list,           :type => MembershipList
50
51   field :feed_token,                :type => String, :default => lambda { BSON::ObjectId.new.to_s }
52   field :socket_key,                :type => String, :default => lambda { BSON::ObjectId.new.to_s }
53
54   field :anonymous,                 :type => Boolean, :default => false
55   index :anonymous
56
57   field :friend_list_id, :type => String
58   field :notification_opts, :type => NotificationConfig
59
60   file_key :avatar, :max_length => 1.megabytes
61   field :use_gravatar, :type => Boolean, :default => true
62
63   referenced_in :friend_list
64
65   references_many :questions, :dependent => :destroy
66   references_many :answers, :dependent => :destroy
67   references_many :comments, :dependent => :destroy
68   references_many :badges, :dependent => :destroy
69   references_many :searches, :dependent => :destroy
70
71   before_create :create_friend_list
72   before_create :generate_uuid
73   after_create :update_anonymous_user
74
75   validates_inclusion_of :language, :in => AVAILABLE_LOCALES
76   validates_inclusion_of :role,  :in => ROLES
77
78   with_options :if => lambda { |e| !e.anonymous } do |v|
79     v.validates_presence_of     :login
80     v.validates_length_of       :login,    :in => 3..40
81     v.validates_uniqueness_of   :login
82     v.validates_format_of       :login,    :with => /\w+/
83   end
84
85   validates_length_of       :name,     :maximum => 100
86
87   validates_presence_of     :email,    :if => lambda { |e| !e.openid_login? && !e.twitter_login? }
88   validates_uniqueness_of   :email,    :if => lambda { |e| e.anonymous || (!e.openid_login? && !e.twitter_login?) }
89   validates_length_of       :email,    :in => 6..100, :allow_nil => true, :if => lambda { |e| !e.email.blank? }
90
91   with_options :if => :password_required? do |v|
92     v.validates_presence_of     :password
93     v.validates_confirmation_of :password
94     v.validates_length_of       :password, :in => 6..20, :allow_blank => true
95   end
96
97   before_save :update_languages
98   before_create :logged!
99
100   def self.find_for_authentication(conditions={})
101     where(conditions).first || where(:login => conditions[:email]).first
102   end
103
104   def membership_list
105     m = self[:membership_list]
106
107     if m.nil?
108       m = self[:membership_list] = MembershipList.new
109     elsif !m.kind_of?(MembershipList)
110       m = self[:membership_list] = MembershipList[m]
111     end
112     m
113   end
114
115   def login=(value)
116     write_attribute :login, (value ? value.downcase : nil)
117   end
118
119   def email=(value)
120     write_attribute :email, (value ? value.downcase : nil)
121   end
122
123   def self.find_by_login_or_id(login, conds = {})
124     where(conds.merge(:login => login)).first || where(conds.merge(:_id => login)).first
125   end
126
127   def self.find_experts(tags, langs = AVAILABLE_LANGUAGES, options = {})
128     opts = {}
129
130     if except = options[:except]
131       except = [except] unless except.is_a?(Array)
132       opts[:user_id] = {:$nin => except}
133     end
134
135     user_ids = UserStat.only(:user_id).where(opts.merge({:answer_tags => {:$in => tags}})).all.map(&:user_id)
136
137     conditions = {:"notification_opts.give_advice" => {:$in => ["1", true]},
138                   :preferred_languages.in => langs,
139                   :_id.in => user_ids}
140
141     if group_id = options[:group_id]
142       conditions[:"membership_list.#{group_id}"] = {:$exists => true}
143     end
144
145     User.only([:email, :login, :name, :language]).where(conditions)
146   end
147
148   def to_param
149     if self.login.blank? || !self.login.match(/^\w[\w\s]*$/)
150       self.id
151     else
152       self.login
153     end
154   end
155
156   def add_preferred_tags(t, group)
157     if t.kind_of?(String)
158       t = t.split(",").map{|e| e.strip}
159     end
160
161     self.collection.update({:_id => self._id},
162                            {:$addToSet => {"membership_list.#{group.id}.preferred_tags" => {:$each => t.uniq}}})
163   end
164
165   def remove_preferred_tags(t, group)
166     if t.kind_of?(String)
167       t = t.split(",").join(" ").split(" ")
168     end
169     self.class.pull_all({:_id => self._id}, {"membership_list.#{group.id}.preferred_tags" => t})
170   end
171
172   def preferred_tags_on(group)
173     @group_preferred_tags ||= (config_for(group, false).preferred_tags || []).to_a
174   end
175
176   def update_language_filter(filter)
177     if LANGUAGE_FILTERS.include? filter
178       User.set({:_id => self.id}, {:language_filter => filter})
179       true
180     else
181       false
182     end
183   end
184
185   def languages_to_filter
186     @languages_to_filter ||= begin
187       languages = nil
188       case self.language_filter
189       when "any"
190         languages = AVAILABLE_LANGUAGES
191       when "user"
192         languages = (self.preferred_languages.empty?) ? AVAILABLE_LANGUAGES : self.preferred_languages
193       else
194         languages = [self.language_filter]
195       end
196       languages
197     end
198   end
199
200   def is_preferred_tag?(group, *tags)
201     ptags = config_for(group, false).preferred_tags
202     tags.detect { |t| ptags.include?(t) } if ptags
203   end
204
205   def admin?
206     self.role == "admin"
207   end
208
209   def age
210     return if self.birthday.blank?
211
212     Time.zone.now.year - self.birthday.year - (self.birthday.to_time.change(:year => Time.zone.now.year) >
213 Time.zone.now ? 1 : 0)
214   end
215
216   def can_modify?(model)
217     return false unless model.respond_to?(:user)
218     self.admin? || self == model.user
219   end
220
221   def can_create_reward?(question)
222     (Time.now - question.created_at) >= 2.days && config_for(question.group_id).reputation >= 75 && (question.reward.nil? || !question.reward.active)
223   end
224
225   def groups(options = {})
226     self.membership_list.groups(options).order_by([:activity_rate, :desc])
227   end
228
229   def member_of?(group)
230     if group.kind_of?(Group)
231       group = group.id
232     end
233
234     self.membership_list.has_key?(group)
235   end
236
237   def role_on(group)
238     config_for(group, false).role
239   end
240
241   def owner_of?(group)
242     admin? || group.owner_id == self.id || role_on(group) == "owner"
243   end
244
245   def admin_of?(group)
246     role_on(group) == "admin" || owner_of?(group)
247   end
248
249   def mod_of?(group)
250     owner_of?(group) || role_on(group) == "moderator" || self.reputation_on(group) >= group.reputation_constrains["moderate"].to_i
251   end
252
253   def editor_of?(group)
254     if c = config_for(group, false)
255       c.is_editor
256     else
257       false
258     end
259   end
260
261   def user_of?(group)
262     mod_of?(group) || self.membership_list.has_key?(group.id)
263   end
264
265   def main_language
266     @main_language ||= self.language.split("-").first
267   end
268
269   def openid_login?
270     !self.auth_keys.blank? || (AppConfig.enable_facebook_auth && !facebook_id.blank?)
271   end
272
273   def twitter_login?
274     user_info && !user_info["twitter"].blank?
275   end
276
277   def has_voted?(voteable)
278     !vote_on(voteable).nil?
279   end
280
281   def vote_on(voteable)
282     voteable.votes[self.id]
283   end
284
285   def favorites(opts = {})
286     Answer.where(opts.merge(:favoriter_ids => id))
287   end
288
289   def logged!(group = nil)
290     now = Time.zone.now
291
292     if new_record?
293       self.last_logged_at = now
294     elsif group && (member_of?(group) || !group.private)
295       on_activity(:login, group)
296     end
297   end
298
299   def on_activity(activity, group)
300     if activity == :login
301       self.last_logged_at ||= Time.now
302       if !self.last_logged_at.today?
303         self.override( {:last_logged_at => Time.zone.now.utc} )
304       end
305     else
306       self.update_reputation(activity, group) if activity != :login
307     end
308     activity_on(group, Time.zone.now)
309   end
310
311   def activity_on(group, date)
312     day = date.utc.at_beginning_of_day
313     last_day = config_for(group, false).last_activity_at
314
315     if last_day != day
316       self.override({"membership_list.#{group.id}.last_activity_at" => day})
317       if last_day
318         if last_day.utc.between?(day.yesterday - 12.hours, day.tomorrow)
319           self.increment({"membership_list.#{group.id}.activity_days" => 1})
320
321           Jobs::Activities.async.on_activity(group.id, self.id).commit!
322         elsif !last_day.utc.today? && (last_day.utc != Time.now.utc.yesterday)
323           Rails.logger.info ">> Resetting act days!! last known day: #{last_day}"
324           reset_activity_days!(group)
325         end
326       end
327     end
328   end
329
330   def reset_activity_days!(group)
331     self.override({"membership_list.#{group.id}.activity_days" => 0})
332   end
333
334   def upvote!(group, v = 1.0)
335     self.increment({"membership_list.#{group.id}.votes_up" => v.to_f})
336   end
337
338   def downvote!(group, v = 1.0)
339     self.increment({"membership_list.#{group.id}.votes_down" => v.to_f})
340   end
341
342   def update_reputation(key, group, v = nil)
343     if v.nil?
344       value = group.reputation_rewards[key.to_s].to_i
345       value = key if key.kind_of?(Integer)
346     else
347       value = v
348     end
349
350     Rails.logger.info "#{self.login} received #{value} points of karma by #{key} on #{group.name}"
351     current_reputation = config_for(group, false).reputation
352
353     if value
354       self.increment(:"membership_list.#{group.id}.reputation" =>  value)
355     end
356
357     stats = self.reputation_stats(group)
358     stats.save if stats.new?
359
360     event = ReputationEvent.new(:time => Time.now, :event => key,
361                                 :reputation => current_reputation,
362                                 :delta => value )
363     ReputationStat.collection.update({:_id => stats.id}, {:$addToSet => {:events => event.attributes}})
364   end
365
366   def reputation_on(group)
367     config_for(group, false).reputation.to_i
368   end
369
370   def stats(*extra_fields)
371     fields = [:_id]
372
373     UserStat.only(fields+extra_fields).where(:user_id => self.id).first || UserStat.create(:user_id => self.id)
374   end
375
376   def badges_count_on(group)
377     config = config_for(group, false)
378     [config.bronze_badges_count, config.silver_badges_count, config.gold_badges_count]
379   end
380
381   def badges_on(group, opts = {})
382     self.badges.where(opts.merge(:group_id => group.id)).order_by(:created_at.desc)
383   end
384
385   def find_badge_on(group, token, opts = {})
386     self.badges.where(opts.merge(:token => token, :group_id => group.id)).first
387   end
388
389   # self follows user
390   def add_friend(user)
391     return false if user == self
392     FriendList.push_uniq(self.friend_list_id, :following_ids => user.id)
393     FriendList.push_uniq(user.friend_list_id, :follower_ids => self.id)
394
395     User.increment(self.id, :following_count => 1)
396     User.increment(user.id, :followers_count => 1)
397     true
398   end
399
400   def remove_friend(user)
401     return false if user == self
402     FriendList.pull(self.friend_list_id, :following_ids => user.id)
403     FriendList.pull(user.friend_list_id, :follower_ids => self.id)
404
405     User.decrement(self.id, :following_count => 1)
406     User.decrement(user.id, :followers_count => 1)
407
408     true
409   end
410
411   def followers(scope = {})
412     conditions = {}
413     conditions[:preferred_languages] = {:$in => scope[:languages]}  if scope[:languages]
414     conditions[:"membership_list.#{scope[:group_id]}"] = {:$exists => true} if scope[:group_id]
415     self.friend_list.followers.where(conditions)
416   end
417
418   def following
419     self.friend_list.following
420   end
421
422   def following?(user)
423     FriendList.only(:following_ids).where(:_id => self.friend_list_id).first.following_ids.include?(user.id)
424   end
425
426   def viewed_on!(group)
427     self.increment("membership_list.#{group.id}.views_count" => 1.0)
428   end
429
430   def method_missing(method, *args, &block)
431     if !args.empty? && method.to_s =~ /can_(\w*)\_on?/
432       key = $1
433       group = args.first
434       if group.reputation_constrains.include?(key.to_s)
435         if group.has_reputation_constrains
436           return self.owner_of?(group) || self.mod_of?(group) || (self.reputation_on(group) >= group.reputation_constrains[key].to_i)
437         else
438           return true
439         end
440       end
441     end
442     super(method, *args, &block)
443   end
444
445   def config_for(group, init = false)
446     if group.kind_of?(Group)
447       group = group.id
448     end
449
450     config = self.membership_list.get(group)
451     if config.nil?
452       if init
453         config = self.membership_list[group] = Membership.new(:group_id => group)
454       else
455         config = Membership.new(:group_id => group)
456       end
457     end
458     config
459   end
460
461   def reputation_stats(group, options = {})
462     if group.kind_of?(Group)
463       group = group.id
464     end
465     default_options = { :user_id => self.id,
466                         :group_id => group}
467     stats = ReputationStat.where(default_options.merge(options)).first ||
468             ReputationStat.new(default_options)
469   end
470
471   def has_flagged?(flaggeable)
472     flaggeable.flags.detect do |flag|
473       flag.user_id == self.id
474     end
475   end
476
477   def has_requested_to_close?(question)
478     question.close_requests.detect do |close_request|
479       close_request.user_id == self.id
480     end
481   end
482
483   def has_requested_to_open?(question)
484     question.open_requests.detect do |open_request|
485       open_request.user_id == self.id
486     end
487   end
488
489   def generate_uuid
490     self.feed_token = UUIDTools::UUID.random_create.hexdigest
491   end
492
493   def self.find_file_from_params(params, request)
494     if request.path =~ %r{/(avatar)/([^/\.\?]+)}
495       @user = User.find($2)
496       case $1
497       when "avatar"
498         @user.avatar
499       end
500     end
501   end
502
503   protected
504   def update_languages
505     self.preferred_languages = self.preferred_languages.map { |e| e.split("-").first }
506   end
507
508   def password_required?
509     return false if openid_login? || twitter_login? || self.anonymous
510
511     (encrypted_password.blank? || !password.blank?)
512   end
513
514   def create_friend_list
515     if !self.friend_list.present?
516       f = FriendList.new
517       f.save
518       self.friend_list_id = f.id
519     end
520     self.notification_opts = NotificationConfig.new if self.notification_opts.nil?
521   end
522
523   def update_anonymous_user
524     return if self.anonymous
525
526     user = User.where({:email => self.email, :anonymous => true}).first
527     if user.present?
528       Rails.logger.info "Merging #{self.email}(#{self.id}) into #{user.email}(#{user.id})"
529       merge_user(user)
530       self.membership_list = user.membership_list
531
532       user.destroy
533     end
534   end
535 end
536