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