add missing check
[shapado:shapado.git] / app / models / question.rb
1 class Question
2   include Mongoid::Document
3   include Mongoid::Timestamps
4
5   include MongoidExt::Filter
6   include MongoidExt::Slugizer
7   include MongoidExt::Tags
8   include MongoidExt::Random
9   include MongoidExt::Storage
10
11   include MongoidExt::Versioning
12   include MongoidExt::Voteable
13
14   include Shapado::Models::GeoCommon
15   include Shapado::Models::Trackable
16
17   paginates_per 25
18
19   track_activities :user, :title, :language, :scope => [:group_id]
20
21   index :tags
22   index [
23     [:group_id, Mongo::ASCENDING],
24     [:banned, Mongo::ASCENDING],
25     [:language, Mongo::ASCENDING]
26   ]
27
28   identity :type => String
29
30   field :title, :type => String, :default => ""
31   field :body, :type => String
32   slug_key :title, :unique => true, :min_length => 8
33   field :slugs, :type => Array, :default  => []
34   index :slugs
35
36   field :answers_count, :type => Integer, :default => 0
37   field :views_count, :type => Integer, :default => 0
38   field :hotness, :type => Integer, :default => 0
39   field :flags_count, :type => Integer, :default => 0
40   field :close_requests_count, :type => Integer, :default => 0
41   field :open_requests_count, :type => Integer, :default => 0
42
43   field :adult_content, :type => Boolean, :default => false
44   field :banned, :type => Boolean, :default => false
45   index :banned
46   field :accepted, :type => Boolean, :default => false
47   field :closed, :type => Boolean, :default => false
48   field :closed_at, :type => Time
49
50   field :anonymous, :type => Boolean, :default => false
51   index :anonymous
52
53   field :answered_with_id, :type => String
54   referenced_in :answered_with, :class_name => "Answer"
55
56   field :wiki, :type => Boolean, :default => false
57   field :subjetive, :type => Boolean, :default => false
58   field :language, :type => String, :default => "en"
59   index :language
60
61   field :activity_at, :type => Time
62
63   field :short_url, :type => String
64
65   referenced_in :user
66   index :user_id
67
68   field :answer_id, :type => String
69   referenced_in :answer
70
71   referenced_in :group
72   index :group_id
73
74   field :followers_count, :type => Integer, :default => 0
75   references_and_referenced_in_many :followers, :class_name => "User"
76
77   field :contributors_count, :type => Integer, :default => 0
78   references_and_referenced_in_many :contributors, :class_name => "User"
79
80   field :updated_by_id, :type => String
81   referenced_in :updated_by, :class_name => "User"
82
83   field :close_reason_id, :type => String
84
85   field :last_target_type, :type => String
86   field :last_target_id, :type => String
87   field :last_target_date, :type => Time
88   field :last_target_parent, :type => Hash
89
90   file_list :attachments
91
92   attr_accessor :removed_tags
93
94 #   referenced_in :last_target, :polymorphic => true
95
96   field :last_target_user_id, :type => String
97   referenced_in :last_target_user, :class_name => "User"
98
99   references_many :answers, :dependent => :destroy
100   references_many :badges, :as => "source", :validate => false
101
102   embeds_many :comments, :as => "commentable"#, :order => "created_at asc"
103   embeds_many :flags, :as => "flaggable", :validate => false
104   embeds_many :close_requests, :as => "closeable", :validate => false
105   embeds_many :open_requests, :as => "openable", :validate => false
106
107   embeds_one :follow_up
108   embeds_one :reward
109
110   validates_presence_of :title
111   validates_presence_of :user
112   validates_uniqueness_of :slug, :scope => "group_id", :allow_blank => true
113
114   validates_length_of       :title,    :in => 5..100, :wrong_length => lambda { I18n.t("questions.model.messages.title_too_long") }
115   validates_length_of       :body,     :minimum => 5, :allow_blank => true #, :if => lambda { |q| !q.disable_limits? }
116
117 #  FIXME mongoid (create a validator for tags size)
118 #   validates_true_for :tags, :logic => lambda { |q| q.tags.size <= 9},
119 #                      :message => lambda { |q| I18n.t("questions.model.messages.too_many_tags") if q.tags.size > 9 }
120
121   versionable_keys :title, :body, :tags, :owner_field => "updated_by_id"
122   filterable_keys :title, :body
123   language :language
124
125   before_save :remove_empty_tags
126   before_save :update_activity_at
127   before_save :save_slug
128   validate :update_language, :on => :create
129
130   validate :group_language
131   validate :disallow_spam
132   validate :check_useful
133
134   xapit do
135     language :language
136     text :title
137     text :body do |body|
138       body.gsub(/<\/?[^>]*>/, " ").gsub(/[\S]{245,}/, "") unless body.nil?
139     end
140     field :group_id, :banned, :id, :language, :tags
141   end
142
143   def self.minimal
144     without(:_keywords, :close_requests, :open_requests, :versions)
145   end
146
147   def followed_up_by
148     Question.minimal.without(:comments).where(:"follow_up.original_question_id" => self.id)
149   end
150
151   def email
152     "#{AppConfig.mailing["user"]}+#{self.group.subdomain}-#{self.id}@#{self.group.domain}"
153   end
154
155   def first_tags
156     tags[0..5]
157   end
158
159   def tags=(t)
160     if t.kind_of?(String)
161       t = t.downcase.split(/[,\+\s]+/).uniq
162     end
163
164     if self.user && !self.user.can_create_new_tags_on?(self.group)
165       tmp_tags = self.group.tags.where(:name.in => t).only(:name).map(&:name)
166       self.removed_tags = t-tmp_tags
167       t = tmp_tags
168     end
169
170     self[:tags] = t
171   end
172
173   def self.related_questions(question, opts = {})
174     opts[:group_id] = question.group_id
175     opts[:banned] = false
176
177     if question.new?
178       question.language = nil
179     elsif question.language
180       opts[:language] = question.language
181     end
182     opts[:tags] = question.tags if !question.tags.blank?
183
184     Question.search.where(opts).similar_to(question)
185   end
186
187   def viewed!(ip)
188     view_count_id = "#{self.id}-#{ip}"
189     if ViewsCount.where({:_id => view_count_id}).first.nil?
190       ViewsCount.create(:_id => view_count_id)
191       self.inc(:views_count, 1)
192     end
193   end
194
195   def answer_added!
196     self.inc(:answers_count, 1)
197     on_activity
198   end
199
200   def answer_removed!
201     self.decrement(:answers_count => 1)
202   end
203
204   def flagged!
205     self.inc(:flags_count, 1)
206   end
207
208   def on_add_vote(v, voter)
209     if v > 0
210       self.user.update_reputation(:question_receives_up_vote, self.group)
211       voter.on_activity(:vote_up_question, self.group)
212     else
213       self.user.update_reputation(:question_receives_down_vote, self.group)
214       voter.on_activity(:vote_down_question, self.group)
215     end
216     on_activity(false)
217   end
218
219   def on_remove_vote(v, voter)
220     if v > 0
221       self.user.update_reputation(:question_undo_up_vote, self.group)
222       voter.on_activity(:undo_vote_up_question, self.group)
223     else
224       self.user.update_reputation(:question_undo_down_vote, self.group)
225       voter.on_activity(:undo_vote_down_question, self.group)
226     end
227     on_activity(false)
228   end
229
230   def on_activity(bring_to_front = true)
231     update_activity_at if bring_to_front
232     self.inc(:hotness, 1)
233   end
234
235   def update_activity_at
236     self[:subjetive] = self.tags.include?(I18n.t("global.subjetive", :default =>
237 "subjetive"))
238
239     now = Time.now
240     if new_record?
241       self.activity_at = now
242     else
243       self.override(:activity_at => now)
244     end
245   end
246
247   def ban
248     self.override(:banned => true)
249     self.user.update_reputation("post_banned", self.group)
250   end
251
252   def self.ban(ids, options = {})
253     self.override({:_id => {"$in" => ids}}.merge(options), {:banned => true})
254     Question.where({:_id => {"$in" => ids}}).only(:user_id, :group_id).each do |question|
255       question.user.update_reputation("post_banned", question.group)
256     end
257   end
258
259   def unban
260     self.override(:banned => false)
261   end
262
263   def self.unban(ids, options = {})
264     self.override({:_id => {"$in" => ids}}.merge(options), {:banned => false})
265   end
266
267   def add_follower(user)
268     if !follower?(user)
269       self.push_uniq(:follower_ids => user.id)
270       self.increment(:followers_count => 1)
271       Jobs::Questions.async.on_question_followed(self.id, user.id).commit!
272     end
273   end
274
275   def remove_follower(user)
276     if follower?(user)
277       self.pull(:follower_ids => user.id)
278       self.decrement(:followers_count => 1)
279       self.user.update_reputation(:question_undo_follow, self.group)
280     end
281   end
282
283   def follower?(user)
284     self.follower_ids && self.follower_ids.include?(user.id)
285   end
286
287   def add_contributor(user)
288     if !contributor?(user)
289       self.push_uniq(:contributor_ids => user.id)
290       self.increment(:contributors_count => 1)
291     end
292   end
293
294   def remove_contributor(user)
295     if contributor?(user)
296       self.pull(:contributor_ids => user.id)
297       self.decrement(:contributors_count => 1)
298     end
299   end
300
301   def contributor?(user)
302     self.contributor_ids && self.contributor_ids.include?(user.id)
303   end
304
305   def disable_limits?
306     self.user.present? && self.user.can_post_whithout_limits_on?(self.group)
307   end
308
309   def answered
310     self.answered_with_id.present?
311   end
312
313   def update_last_target(target)
314     self.class.update_last_target(self._id, target)
315   end
316
317   def self.update_last_target(question_id, target)
318     return if target.nil?
319     data = {:last_target_id => target.id,
320             :last_target_user_id => target.user_id,
321             :last_target_type => target.class.to_s
322     }
323     if target.class == Comment && target.commentable
324       data.merge({ :last_target_parent => { :type => target.commentable.class,
325                      :id => target.commentable.id}})
326     end
327     if target.respond_to?(:updated_at) && target.updated_at.present?
328       data[:last_target_date] = target.updated_at.utc
329     end
330
331     self.override({:_id => question_id}, data)
332   end
333
334   def can_be_requested_to_close_by?(user)
335     return false if self.closed
336     ((self.user_id == user.id) && user.can_vote_to_close_own_question_on?(self.group)) ||
337     user.can_vote_to_close_any_question_on?(self.group)
338   end
339
340   def can_be_requested_to_open_by?(user)
341     return false if !self.closed
342     ((self.user_id == user.id) && user.can_vote_to_open_own_question_on?(self.group)) ||
343     user.can_vote_to_open_any_question_on?(self.group)
344   end
345
346   def can_be_deleted_by?(user)
347     (self.user_id == user.id && self.answers.count < 1) ||
348     (self.closed && user.can_delete_closed_questions_on?(self.group))
349   end
350
351   def close_reason
352     self.close_requests.detect{ |rq| rq.id == close_reason_id }
353   end
354
355   def last_target=(target)
356     self.last_target_id = target.id
357     self.last_target_type = target.class.to_s
358     self.last_target_date = target.updated_at
359     self.last_target_user_id = target.user_id
360   end
361
362   def attachments=(files)
363     files.each do |k,v|
364       if(v.size > 0)
365         self.attachments.put(BSON::ObjectId.new.to_s, v)
366
367         Group.increment({:_id => self.group_id}, {:used_quota => v.size})
368       end
369     end
370   end
371
372   def self.find_file_from_params(params, request)
373     if request.path =~ /\/(attachment)\/([^\/\.?]+)\/([^\/\.?]+)\/([^\/\.?]+)/
374       @group = Group.by_slug($2)
375       @question = @group.questions.find($3)
376       case $1
377       when "attachment"
378         @question.attachments.get($4)
379       end
380     end
381   end
382
383   def self.humanize_action(action)
384     case action
385     when "create"
386       "asked"
387     when "update"
388       "changed"
389     when "destroy"
390       "deleted"
391     end
392   end
393
394   def update_last_target
395     q = self
396     last = q
397     q.answers.each do |a|
398       if last.updated_at < a.updated_at
399         last = a
400       end
401
402       a.comments.each do |c|
403         if last.updated_at < c.updated_at
404           last = c
405         end
406       end
407     end
408
409     q.comments.each do |c|
410       if last.updated_at < c.updated_at
411         last = c
412       end
413     end
414     Question.update_last_target(q.id, last)
415     last
416   end
417
418   protected
419   def self.map_filter_operators(quotes, ops)
420     mongoquery = {}
421     if !quotes.empty?
422       q = {:$in => quotes.map { |quote| /#{Regexp.escape(quote)}/i }}
423       mongoquery[:$or] = [{:title => q}, {:body => q}]
424     end
425
426     if ops["is"]
427       ops["is"].each do |d|
428         case d
429         when "answered"
430           mongoquery[:answered] = true
431         when "accepted"
432           mongoquery[:accepted] = true
433         end
434       end
435     end
436
437     if ops["not"]
438       ops["not"].each do |d|
439         case d
440         when "answered"
441           mongoquery[:answered] = false
442         when "accepted"
443           mongoquery[:accepted] = false
444         end
445       end
446     end
447
448     if ops["lang"]
449       mongoquery["language"] = {:$in => ops["lang"].map{|l| /#{l}/ }}
450     end
451
452     mongoquery
453   end
454
455   def update_answer_count
456     self.answers_count = self.answers.count
457     votes_average = 0
458     self.votes.each {|e| votes_average+=e.value }
459     self.votes_average = votes_average
460
461     self.votes_count = self.votes.count
462   end
463
464   def update_language
465     self.language = self.language.split("-").first
466   end
467
468   def group_language
469     if AppConfig.enable_i18n && self.group.present?
470       unless self.group.languages.include? self.language || self.language
471         self.errors.add :language, I18n.t("questions.model.messages.not_group_languages")
472       end
473     end
474   end
475
476   def check_useful
477     unless disable_limits?
478       if !self.title.blank? && self.title.gsub(/[^\x00-\x7F]/, "").size < 5
479         return
480       end
481
482       if !self.title.blank? && (self.title.split.count < 2)
483         self.errors.add(:title, I18n.t("questions.model.messages.too_short", :count => 2))
484       end
485
486       if !self.body.blank? && (self.body.split.count < 2)
487         self.errors.add(:body, I18n.t("questions.model.messages.too_short", :count => 3))
488       end
489     end
490   end
491
492   def disallow_spam
493     if self.new_record? && !disable_limits?
494       last_question = Question.where(:user_id => self.user_id,
495                                      :group_id => self.group_id).
496                                           order_by(:created_at.desc).first
497
498       valid = (last_question.nil? || (Time.now - last_question.created_at) > 20)
499       if !valid
500         self.errors.add(:body, I18n.t('questions.disallow_spam.error'))
501       end
502     end
503   end
504
505   def save_slug
506     if self.slug_changed?
507       self.push_uniq(:slugs => self.slug_was)
508     end
509   end
510
511   def remove_empty_tags
512     self.tags.delete_if {|tag| tag.blank? }
513   end
514 end
515