Finish port to fabrication
[shapado:piglops-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] do |activity, question|
20     follower_ids = question.follower_ids+question.contributor_ids
21     follower_ids.delete(activity.user_id)
22     activity.add_followers(*follower_ids)
23   end
24
25   index :tags
26   index [
27     [:group_id, Mongo::ASCENDING],
28     [:banned, Mongo::ASCENDING],
29     [:language, Mongo::ASCENDING]
30   ]
31
32   identity :type => String
33
34   field :title, :type => String, :default => ""
35   field :body, :type => String
36   slug_key :title, :unique => true, :min_length => 8
37   field :slugs, :type => Array, :default  => []
38   index :slugs
39
40   field :answers_count, :type => Integer, :default => 0
41   field :views_count, :type => Integer, :default => 0
42   field :hotness, :type => Integer, :default => 0
43   field :flags_count, :type => Integer, :default => 0
44   field :close_requests_count, :type => Integer, :default => 0
45   field :open_requests_count, :type => Integer, :default => 0
46
47   field :adult_content, :type => Boolean, :default => false
48   field :banned, :type => Boolean, :default => false
49   index :banned
50   field :accepted, :type => Boolean, :default => false
51   field :closed, :type => Boolean, :default => false
52   field :closed_at, :type => Time
53
54   field :anonymous, :type => Boolean, :default => false
55   index :anonymous
56
57   field :answered_with_id, :type => String
58   referenced_in :answered_with, :class_name => "Answer"
59
60   field :wiki, :type => Boolean, :default => false
61   field :subjetive, :type => Boolean, :default => false
62   field :language, :type => String, :default => "en"
63   index :language
64
65   field :activity_at, :type => Time
66
67   field :short_url, :type => String
68
69   referenced_in :user
70   index :user_id
71
72   field :answer_id, :type => String
73   referenced_in :answer
74
75   referenced_in :group
76   index :group_id
77
78   field :followers_count, :type => Integer, :default => 0
79   references_and_referenced_in_many :followers, :class_name => "User"
80
81   field :contributors_count, :type => Integer, :default => 0
82   references_and_referenced_in_many :contributors, :class_name => "User"
83
84   field :updated_by_id, :type => String
85   referenced_in :updated_by, :class_name => "User"
86
87   field :close_reason_id, :type => String
88
89   field :last_target_type, :type => String
90   field :last_target_id, :type => String
91   field :last_target_date, :type => Time
92   field :last_target_parent, :type => Hash
93
94   file_list :attachments
95
96   attr_accessor :removed_tags
97
98 #   referenced_in :last_target, :polymorphic => true
99
100   field :last_target_user_id, :type => String
101   referenced_in :last_target_user, :class_name => "User"
102
103   references_many :answers, :dependent => :destroy
104   references_many :badges, :as => "source", :validate => false
105
106   embeds_many :comments, :as => :commentable#, :order => "created_at asc"
107   embeds_many :flags, :as => "flaggable", :validate => false
108   embeds_many :close_requests, :as => :closeable, :validate => false
109   embeds_many :open_requests, :as => "openable", :validate => false
110
111   embeds_one :follow_up
112   embeds_one :reward
113
114   validates_presence_of :title
115   validates_presence_of :user
116   validates_uniqueness_of :slug, :scope => "group_id", :allow_blank => true
117
118   validates_length_of       :title,    :in => 5..100, :wrong_length => lambda { I18n.t("questions.model.messages.title_too_long") }
119   validates_length_of       :body,     :minimum => 5, :allow_blank => true #, :if => lambda { |q| !q.disable_limits? }
120
121 #  FIXME mongoid (create a validator for tags size)
122 #   validates_true_for :tags, :logic => lambda { |q| q.tags.size <= 9},
123 #                      :message => lambda { |q| I18n.t("questions.model.messages.too_many_tags") if q.tags.size > 9 }
124
125   versionable_keys :title, :body, :tags, :owner_field => "updated_by_id"
126   filterable_keys :title, :body
127   language :language
128
129   before_save :remove_empty_tags
130   before_save :update_activity_at
131   before_save :save_slug
132   validate :update_language, :on => :create
133
134   validate :group_language
135   validate :disallow_spam
136   validate :check_useful
137
138   xapit do
139     language :language
140     text :title
141     text :body do |body|
142       body.gsub(/<\/?[^>]*>/, " ").gsub(/[\S]{245,}/, "") unless body.nil?
143     end
144     field :group_id, :banned, :id, :language, :tags
145   end
146
147   def self.minimal
148     without(:_keywords, :close_requests, :open_requests, :versions)
149   end
150
151   def followed_up_by
152     Question.minimal.without(:comments).where(:"follow_up.original_question_id" => self.id)
153   end
154
155   def email
156     "#{AppConfig.mailing["user"]}+#{self.group.subdomain}-#{self.id}@#{self.group.domain}"
157   end
158
159   def first_tags
160     tags[0..5]
161   end
162
163   def tags=(t)
164     if t.kind_of?(String)
165       t = t.downcase.split(/[,\+\s]+/).uniq
166     end
167
168     if self.user && !self.user.can_create_new_tags_on?(self.group)
169       tmp_tags = self.group.tags.where(:name.in => t).only(:name).map(&:name)
170       self.removed_tags = t-tmp_tags
171       t = tmp_tags
172     end
173
174     self[:tags] = t
175   end
176
177   def self.related_questions(question, opts = {})
178     opts[:group_id] = question.group_id
179     opts[:banned] = false
180
181     if question.new?
182       question.language = nil
183     elsif question.language
184       opts[:language] = question.language
185     end
186     opts[:tags] = question.tags if !question.tags.blank?
187     Question.search(question.title).where(opts).similar_to(question)
188   end
189
190   def viewed!(ip)
191     view_count_id = "#{self.id}-#{ip}"
192     if ViewsCount.where({:_id => view_count_id}).first.nil?
193       ViewsCount.create(:_id => view_count_id)
194       self.inc(:views_count, 1)
195     end
196   end
197
198   def answer_added!
199     self.inc(:answers_count, 1)
200     on_activity
201   end
202
203   def answer_removed!
204     self.decrement(:answers_count => 1)
205   end
206
207   def flagged!
208     self.inc(:flags_count, 1)
209   end
210
211   def on_add_vote(v, voter_id)
212     if voter_id.is_a? User
213       voter = voter_id
214     else
215       voter = User.find(voter_id)
216     end
217
218     if v > 0
219       self.user.update_reputation(:question_receives_up_vote, self.group)
220       voter.on_activity(:vote_up_question, self.group)
221     else
222       self.user.update_reputation(:question_receives_down_vote, self.group)
223       voter.on_activity(:vote_down_question, self.group)
224     end
225     on_activity(false)
226   end
227
228   def on_remove_vote(v, voter)
229     if v > 0
230       self.user.update_reputation(:question_undo_up_vote, self.group)
231       voter.on_activity(:undo_vote_up_question, self.group)
232     else
233       self.user.update_reputation(:question_undo_down_vote, self.group)
234       voter.on_activity(:undo_vote_down_question, self.group)
235     end
236     on_activity(false)
237   end
238
239   def on_activity(bring_to_front = true)
240     update_activity_at if bring_to_front
241     self.inc(:hotness, 1)
242   end
243
244   def update_activity_at
245     self[:subjetive] = self.tags.include?(I18n.t("global.subjetive", :default =>
246 "subjetive"))
247
248     now = Time.now
249     if new_record?
250       self.activity_at = now
251     else
252       self.override(:activity_at => now)
253     end
254   end
255
256   def ban
257     self.override(:banned => true)
258     self.user.update_reputation("post_banned", self.group)
259   end
260
261   def self.ban(ids, options = {})
262     self.override({:_id => {"$in" => ids}}.merge(options), {:banned => true})
263     Question.where({:_id => {"$in" => ids}}).only(:user_id, :group_id).each do |question|
264       question.user.update_reputation("post_banned", question.group)
265     end
266   end
267
268   def unban
269     self.override(:banned => false)
270   end
271
272   def self.unban(ids, options = {})
273     self.override({:_id => {"$in" => ids}}.merge(options), {:banned => false})
274   end
275
276   def add_follower(user)
277     if !follower?(user)
278       self.push_uniq(:follower_ids => user.id)
279       self.increment(:followers_count => 1)
280       Jobs::Questions.async.on_question_followed(self.id, user.id).commit!
281     end
282   end
283
284   def remove_follower(user)
285     if follower?(user)
286       self.pull(:follower_ids => user.id)
287       self.decrement(:followers_count => 1)
288       self.user.update_reputation(:question_undo_follow, self.group)
289     end
290   end
291
292   def follower?(user)
293     self.follower_ids && self.follower_ids.include?(user.id)
294   end
295
296   def add_contributor(user)
297     if !contributor?(user)
298       self.push_uniq(:contributor_ids => user.id)
299       self.increment(:contributors_count => 1)
300     end
301   end
302
303   def remove_contributor(user)
304     if contributor?(user)
305       self.pull(:contributor_ids => user.id)
306       self.decrement(:contributors_count => 1)
307     end
308   end
309
310   def contributor?(user)
311     self.contributor_ids && self.contributor_ids.include?(user.id)
312   end
313
314   def disable_limits?
315     self.user.present? && self.user.can_post_whithout_limits_on?(self.group)
316   end
317
318   def answered
319     self.answered_with_id.present?
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   def find_last_target
424     @last_target_data ||= begin
425       target_id = self.last_target_id || self.id
426       date = self.last_target_date || self.updated_at
427       owner = self.last_target_user || self.user
428
429       [target_id, date, owner]
430     end
431   end
432
433   protected
434   def self.map_filter_operators(quotes, ops)
435     mongoquery = {}
436     if !quotes.empty?
437       q = {:$in => quotes.map { |quote| /#{Regexp.escape(quote)}/i }}
438       mongoquery[:$or] = [{:title => q}, {:body => q}]
439     end
440
441     if ops["is"]
442       ops["is"].each do |d|
443         case d
444         when "answered"
445           mongoquery[:answered] = true
446         when "accepted"
447           mongoquery[:accepted] = true
448         end
449       end
450     end
451
452     if ops["not"]
453       ops["not"].each do |d|
454         case d
455         when "answered"
456           mongoquery[:answered] = false
457         when "accepted"
458           mongoquery[:accepted] = false
459         end
460       end
461     end
462
463     if ops["lang"]
464       mongoquery["language"] = {:$in => ops["lang"].map{|l| /#{l}/ }}
465     end
466
467     mongoquery
468   end
469
470   def update_answer_count
471     self.answers_count = self.answers.count
472     votes_average = 0
473     self.votes.each {|e| votes_average+=e.value }
474     self.votes_average = votes_average
475
476     self.votes_count = self.votes.count
477   end
478
479   def update_language
480     self.language = self.language.split("-").first
481   end
482
483   def group_language
484     if AppConfig.enable_i18n && self.group.present?
485       unless (self.group.languages.include? self.language) || (self.language == self.group.language)
486         self.errors.add :language, I18n.t("questions.model.messages.not_group_languages")
487       end
488     end
489   end
490
491   def check_useful
492     unless disable_limits?
493       if !self.title.blank? && self.title.gsub(/[^\x00-\x7F]/, "").size < 5
494         return
495       end
496
497       if !self.title.blank? && (self.title.split.count < 2)
498         self.errors.add(:title, I18n.t("questions.model.messages.too_short", :count => 2))
499       end
500
501       if !self.body.blank? && (self.body.split.count < 2)
502         self.errors.add(:body, I18n.t("questions.model.messages.too_short", :count => 3))
503       end
504     end
505   end
506
507   def disallow_spam
508     if self.new_record? && !disable_limits?
509       last_question = Question.where(:user_id => self.user_id,
510                                      :group_id => self.group_id).
511                                           order_by(:created_at.desc).first
512
513       valid = (last_question.nil? || (Time.now - last_question.created_at) > 20)
514       if !valid
515         self.errors.add(:body, I18n.t('questions.disallow_spam.error'))
516       end
517     end
518   end
519
520   def save_slug
521     if self.slug_changed?
522       self.push_uniq(:slugs => self.slug_was)
523     end
524   end
525
526   def remove_empty_tags
527     self.tags.delete_if {|tag| tag.blank? }
528   end
529 end
530