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