formol 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +147 -0
- data/Rakefile +29 -0
- data/app/assets/images/formol/read.png +0 -0
- data/app/assets/images/formol/unread.png +0 -0
- data/app/assets/javascripts/formol/admin/categories.js.coffee +55 -0
- data/app/assets/javascripts/formol/admin/forums.js +2 -0
- data/app/assets/javascripts/formol/application.js +9 -0
- data/app/assets/javascripts/formol/categories.js +2 -0
- data/app/assets/javascripts/formol/forums.js +2 -0
- data/app/assets/javascripts/formol/poll_votes.js +2 -0
- data/app/assets/javascripts/formol/posts.js.coffee +9 -0
- data/app/assets/javascripts/formol/preferences.js +2 -0
- data/app/assets/javascripts/formol/subscriptions.js.coffee +13 -0
- data/app/assets/javascripts/formol/topics.js.coffee +54 -0
- data/app/assets/javascripts/lib/jquery.prevent_double_submit.js.coffee +11 -0
- data/app/assets/javascripts/markitup/jquery.markitup.js +593 -0
- data/app/assets/javascripts/markitup/sets/markdown/set.js +53 -0
- data/app/assets/stylesheets/form_struct.css.scss +39 -0
- data/app/assets/stylesheets/formol/admin/categories.css.scss +30 -0
- data/app/assets/stylesheets/formol/admin/forums.css +4 -0
- data/app/assets/stylesheets/formol/categories.css.scss +3 -0
- data/app/assets/stylesheets/formol/development.css +4 -0
- data/app/assets/stylesheets/formol/forums.css.scss +3 -0
- data/app/assets/stylesheets/formol/mixins/after_clear.css.scss +7 -0
- data/app/assets/stylesheets/formol/partials/breadcrumb.css.scss +9 -0
- data/app/assets/stylesheets/formol/partials/categories/list.css.scss +101 -0
- data/app/assets/stylesheets/formol/partials/formol_lists.css.scss +6 -0
- data/app/assets/stylesheets/formol/partials/forums/list.css.scss +97 -0
- data/app/assets/stylesheets/formol/partials/polls/poll.css.scss +9 -0
- data/app/assets/stylesheets/formol/partials/posts/post.css.scss +116 -0
- data/app/assets/stylesheets/formol/posts.css.scss +8 -0
- data/app/assets/stylesheets/formol/topics.css.scss +42 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/block_code.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/bold.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/code.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/h1.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/h2.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/h3.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/h4.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/h5.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/h6.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/italic.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/link.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/list-bullet.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/list-numeric.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/picture.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/preview.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/images/quotes.png +0 -0
- data/app/assets/stylesheets/markitup/sets/markdown/style.css +57 -0
- data/app/assets/stylesheets/markitup/skins/markitup/images/bg-container.png +0 -0
- data/app/assets/stylesheets/markitup/skins/markitup/images/bg-editor-bbcode.png +0 -0
- data/app/assets/stylesheets/markitup/skins/markitup/images/bg-editor-dotclear.png +0 -0
- data/app/assets/stylesheets/markitup/skins/markitup/images/bg-editor-html.png +0 -0
- data/app/assets/stylesheets/markitup/skins/markitup/images/bg-editor-json.png +0 -0
- data/app/assets/stylesheets/markitup/skins/markitup/images/bg-editor-markdown.png +0 -0
- data/app/assets/stylesheets/markitup/skins/markitup/images/bg-editor-textile.png +0 -0
- data/app/assets/stylesheets/markitup/skins/markitup/images/bg-editor-wiki.png +0 -0
- data/app/assets/stylesheets/markitup/skins/markitup/images/bg-editor-xml.png +0 -0
- data/app/assets/stylesheets/markitup/skins/markitup/images/bg-editor.png +0 -0
- data/app/assets/stylesheets/markitup/skins/markitup/images/handle.png +0 -0
- data/app/assets/stylesheets/markitup/skins/markitup/images/menu.png +0 -0
- data/app/assets/stylesheets/markitup/skins/markitup/images/submenu.png +0 -0
- data/app/assets/stylesheets/markitup/skins/markitup/style.css +147 -0
- data/app/assets/stylesheets/markitup/skins/simple/images/handle.png +0 -0
- data/app/assets/stylesheets/markitup/skins/simple/images/menu.png +0 -0
- data/app/assets/stylesheets/markitup/skins/simple/images/submenu.png +0 -0
- data/app/assets/stylesheets/markitup/skins/simple/style.css +118 -0
- data/app/assets/stylesheets/pygments/autumn.css +58 -0
- data/app/assets/stylesheets/pygments/borland.css +46 -0
- data/app/assets/stylesheets/pygments/bw.css +34 -0
- data/app/assets/stylesheets/pygments/colorful.css +61 -0
- data/app/assets/stylesheets/pygments/default.css +62 -0
- data/app/assets/stylesheets/pygments/emacs.css +61 -0
- data/app/assets/stylesheets/pygments/friendly.css +61 -0
- data/app/assets/stylesheets/pygments/fruity.css +70 -0
- data/app/assets/stylesheets/pygments/manni.css +61 -0
- data/app/assets/stylesheets/pygments/murphy.css +61 -0
- data/app/assets/stylesheets/pygments/native.css +70 -0
- data/app/assets/stylesheets/pygments/pastie.css +60 -0
- data/app/assets/stylesheets/pygments/perldoc.css +58 -0
- data/app/assets/stylesheets/pygments/tango.css +69 -0
- data/app/assets/stylesheets/pygments/trac.css +59 -0
- data/app/assets/stylesheets/pygments/vim.css +70 -0
- data/app/assets/stylesheets/pygments/vs.css +33 -0
- data/app/controllers/formol/admin/admin_controller.rb +11 -0
- data/app/controllers/formol/admin/categories_controller.rb +60 -0
- data/app/controllers/formol/admin/forums_controller.rb +56 -0
- data/app/controllers/formol/application_controller.rb +47 -0
- data/app/controllers/formol/categories_controller.rb +21 -0
- data/app/controllers/formol/forums_controller.rb +19 -0
- data/app/controllers/formol/poll_votes_controller.rb +23 -0
- data/app/controllers/formol/posts_controller.rb +71 -0
- data/app/controllers/formol/preferences_controller.rb +32 -0
- data/app/controllers/formol/subscriptions_controller.rb +29 -0
- data/app/controllers/formol/topics_controller.rb +91 -0
- data/app/helpers/formol/admin/categories_helper.rb +4 -0
- data/app/helpers/formol/admin/forums_helper.rb +4 -0
- data/app/helpers/formol/application_helper.rb +35 -0
- data/app/helpers/formol/categories_helper.rb +15 -0
- data/app/helpers/formol/forums_helper.rb +19 -0
- data/app/helpers/formol/poll_votes_helper.rb +4 -0
- data/app/helpers/formol/posts_helper.rb +27 -0
- data/app/helpers/formol/preferences_helper.rb +4 -0
- data/app/helpers/formol/subscriptions_helper.rb +4 -0
- data/app/helpers/formol/topics_helper.rb +27 -0
- data/app/mailers/formol/subscription_mailer.rb +13 -0
- data/app/models/formol/category.rb +42 -0
- data/app/models/formol/forum.rb +77 -0
- data/app/models/formol/poll/option.rb +20 -0
- data/app/models/formol/poll/vote.rb +32 -0
- data/app/models/formol/poll.rb +107 -0
- data/app/models/formol/post.rb +162 -0
- data/app/models/formol/post_observer.rb +9 -0
- data/app/models/formol/topic/subscription.rb +16 -0
- data/app/models/formol/topic/track.rb +57 -0
- data/app/models/formol/topic.rb +185 -0
- data/app/models/formol/tracking.rb +45 -0
- data/app/models/formol/user_preference.rb +18 -0
- data/app/views/formol/admin/categories/_category.html.erb +9 -0
- data/app/views/formol/admin/categories/_form.html.erb +8 -0
- data/app/views/formol/admin/categories/edit.html.erb +3 -0
- data/app/views/formol/admin/categories/index.html.erb +3 -0
- data/app/views/formol/admin/categories/new.html.erb +3 -0
- data/app/views/formol/admin/forums/_form.html.erb +9 -0
- data/app/views/formol/admin/forums/_forum.html.erb +12 -0
- data/app/views/formol/admin/forums/edit.html.erb +3 -0
- data/app/views/formol/admin/forums/new.html.erb +3 -0
- data/app/views/formol/categories/_category.html.erb +5 -0
- data/app/views/formol/categories/index.html.erb +5 -0
- data/app/views/formol/categories/show.html.erb +3 -0
- data/app/views/formol/forums/_forum.html.erb +13 -0
- data/app/views/formol/forums/_list.html.erb +16 -0
- data/app/views/formol/forums/show.html.erb +18 -0
- data/app/views/formol/poll/options/_form.html.erb +1 -0
- data/app/views/formol/poll_votes/_form.html.erb +8 -0
- data/app/views/formol/polls/_form.html.erb +14 -0
- data/app/views/formol/polls/_poll.html.erb +24 -0
- data/app/views/formol/posts/_form.html.erb +7 -0
- data/app/views/formol/posts/_last_post.html.erb +9 -0
- data/app/views/formol/posts/_list.html.erb +3 -0
- data/app/views/formol/posts/_post.html.erb +47 -0
- data/app/views/formol/posts/edit.html.erb +10 -0
- data/app/views/formol/posts/new.html.erb +14 -0
- data/app/views/formol/preferences/show.html.erb +11 -0
- data/app/views/formol/subscription_mailer/topic_reply.en.html.erb +8 -0
- data/app/views/formol/subscription_mailer/topic_reply.fr.html.erb +8 -0
- data/app/views/formol/topics/_form.html.erb +20 -0
- data/app/views/formol/topics/_list.html.erb +16 -0
- data/app/views/formol/topics/_topic.html.erb +13 -0
- data/app/views/formol/topics/edit.html.erb +5 -0
- data/app/views/formol/topics/new.html.erb +5 -0
- data/app/views/formol/topics/show.html.erb +25 -0
- data/app/views/layouts/formol/mail.html.erb +9 -0
- data/config/locales/en.yml +3 -0
- data/config/locales/formol/views/forums/en.yml +13 -0
- data/config/locales/formol/views/forums/fr.yml +13 -0
- data/config/locales/formol/views/poll_votes/en.yml +5 -0
- data/config/locales/formol/views/polls/en.yml.yml +5 -0
- data/config/locales/formol/views/posts/en.yml +23 -0
- data/config/locales/formol/views/posts/fr.yml +20 -0
- data/config/locales/formol/views/topics/en.yml +23 -0
- data/config/locales/formol/views/topics/fr.yml +20 -0
- data/config/routes.rb +31 -0
- data/db/migrate/20111221101158_create_formol_categories.rb +7 -0
- data/db/migrate/20111221111417_create_formol_forums.rb +10 -0
- data/db/migrate/20111221121010_create_formol_topics.rb +9 -0
- data/db/migrate/20111221123515_create_formol_posts.rb +9 -0
- data/db/migrate/20111221151653_add_index_on_tables.rb +15 -0
- data/db/migrate/20120103163438_add_user_to_models.rb +13 -0
- data/db/migrate/20120104132631_add_counter_cache_to_models.rb +12 -0
- data/db/migrate/20120104163741_add_pinned_to_topics.rb +7 -0
- data/db/migrate/20120105133522_add_position_to_categories.rb +7 -0
- data/db/migrate/20120105190708_add_position_to_forums.rb +7 -0
- data/db/migrate/20120105212559_add_lock_to_topics.rb +7 -0
- data/db/migrate/20120106130944_add_first_post_and_last_post_to_topics.rb +8 -0
- data/db/migrate/20120106192951_change_forum_description_to_string.rb +9 -0
- data/db/migrate/20120107012517_add_last_post_to_forum.rb +7 -0
- data/db/migrate/20120107122921_add_missing_indexes.rb +12 -0
- data/db/migrate/20120107123654_add_default_on_forum_counters.rb +10 -0
- data/db/migrate/20120108114030_add_views_to_topics.rb +7 -0
- data/db/migrate/20120108131533_create_formol_topic_tracks.rb +13 -0
- data/db/migrate/20120109115141_create_formol_topic_subscriptions.rb +10 -0
- data/db/migrate/20120109220633_create_formol_user_preferences.rb +11 -0
- data/db/migrate/20120109223624_add_display_signature_to_posts.rb +7 -0
- data/db/migrate/20120109233054_add_display_signatures_and_avatars_to_user_preferences.rb +8 -0
- data/db/migrate/20120110152548_create_formol_polls.rb +14 -0
- data/db/migrate/20120110152618_create_formol_poll_options.rb +11 -0
- data/db/migrate/20120110152632_create_formol_poll_votes.rb +12 -0
- data/lib/formol/config.rb +12 -0
- data/lib/formol/controllers/auth.rb +9 -0
- data/lib/formol/controllers/nested/has_parent_category.rb +11 -0
- data/lib/formol/controllers/nested/has_parent_forum.rb +11 -0
- data/lib/formol/controllers/nested/has_parent_topic.rb +11 -0
- data/lib/formol/controllers.rb +5 -0
- data/lib/formol/engine.rb +18 -0
- data/lib/formol/formatters/redcarpet/formatter.rb +36 -0
- data/lib/formol/formatters/redcarpet/syntax_highlighter.rb +23 -0
- data/lib/formol/formatters.rb +2 -0
- data/lib/formol/generators/install/install_generator.rb +94 -0
- data/lib/formol/generators/install/templates/config/formol.yml +29 -0
- data/lib/formol/generators/install/templates/initializer/formol.rb.erb +21 -0
- data/lib/formol/generators/install/templates/lib/permissions/special_rights.rb +24 -0
- data/lib/formol/generators/install/templates/migrations/add_formol_profile_to_users.rb.erb +7 -0
- data/lib/formol/generators/install/templates/migrations/add_posts_count_to_user.rb.erb +7 -0
- data/lib/formol/generators.rb +1 -0
- data/lib/formol/integration/user.rb +76 -0
- data/lib/formol/integration.rb +1 -0
- data/lib/formol/permissions/controllers.rb +30 -0
- data/lib/formol/permissions/manager.rb +29 -0
- data/lib/formol/permissions/profile.rb +24 -0
- data/lib/formol/permissions/profiles.rb +44 -0
- data/lib/formol/permissions/user.rb +29 -0
- data/lib/formol/permissions.rb +5 -0
- data/lib/formol/rails/engine.rb +81 -0
- data/lib/formol/rails.rb +1 -0
- data/lib/formol/version.rb +10 -0
- data/lib/formol/view_helpers/assets_helper.rb +11 -0
- data/lib/formol/view_helpers/format_helper.rb +12 -0
- data/lib/formol/view_helpers.rb +2 -0
- data/lib/formol/will_paginate/list_link_renderer.rb +29 -0
- data/lib/formol/will_paginate.rb +1 -0
- data/lib/formol.rb +61 -0
- data/lib/formol_.rb +13 -0
- data/lib/tasks/formol_tasks.rake +0 -0
- metadata +348 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
module Formol
|
2
|
+
class Forum < ActiveRecord::Base
|
3
|
+
#Modules
|
4
|
+
include RankedModel
|
5
|
+
include Tracking
|
6
|
+
|
7
|
+
#Security
|
8
|
+
attr_protected :posts_count, :topics_count
|
9
|
+
|
10
|
+
#Associations
|
11
|
+
belongs_to :category
|
12
|
+
belongs_to :last_post, :class_name => 'Formol::Post'
|
13
|
+
|
14
|
+
has_many :topics, :dependent => :destroy
|
15
|
+
has_many :posts, :through => :topics
|
16
|
+
|
17
|
+
#Validations
|
18
|
+
validates :category, :presence => true
|
19
|
+
validates :label, :presence => true,
|
20
|
+
:length => { :in => 3..32,
|
21
|
+
:allow_blank => true },
|
22
|
+
:uniqueness => { :scope => :category_id,
|
23
|
+
:case_sensitive => false,
|
24
|
+
:allow_blank => true }
|
25
|
+
validates :description, :length => { :maximum => 128,
|
26
|
+
:allow_blank => true }
|
27
|
+
|
28
|
+
#Normalization
|
29
|
+
normalize_attribute :label, :with => [:squish, :blank]
|
30
|
+
normalize_attribute :description
|
31
|
+
|
32
|
+
#Sorting
|
33
|
+
ranks :position, :with_same => :category_id
|
34
|
+
|
35
|
+
#Scopes
|
36
|
+
class << self
|
37
|
+
# this scope eager loads conveniently topic for using it in breadcrumb
|
38
|
+
def ready_for_breadcrumb
|
39
|
+
includes(:category)
|
40
|
+
end
|
41
|
+
|
42
|
+
# return all unread forums for a given user
|
43
|
+
def unread(user)
|
44
|
+
unread_scope_base(user)
|
45
|
+
.select('DISTINCT formol_forums.id')
|
46
|
+
.joins(:topics => :last_post)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
#Business
|
51
|
+
class << self
|
52
|
+
# Sorts topics from an array of ids and sets the new category
|
53
|
+
def reorganize(forum_ids, category_id = nil)
|
54
|
+
category = Formol::Category.find(category_id) if category_id
|
55
|
+
|
56
|
+
#TODO: should maybe optimize this?
|
57
|
+
(forum_ids || []).each_with_index do |forum_id, index|
|
58
|
+
attr = { :position => index }
|
59
|
+
attr[:category] = category if category_id
|
60
|
+
|
61
|
+
find(forum_id).update_attributes(attr)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Register a post being the last one of the current forum
|
67
|
+
def register_last_post(post)
|
68
|
+
update_attribute(:last_post, post)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Unregister a post by re-assigning last post.
|
72
|
+
# Needed when a post is destroyed
|
73
|
+
def unregister_last_post
|
74
|
+
update_attributes!(:last_post => posts.order('formol_posts.id').last)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Formol
|
2
|
+
class Poll::Option < ActiveRecord::Base
|
3
|
+
#Security
|
4
|
+
attr_protected :poll_id
|
5
|
+
|
6
|
+
#Associations
|
7
|
+
belongs_to :poll
|
8
|
+
|
9
|
+
has_many :votes
|
10
|
+
|
11
|
+
#Validations
|
12
|
+
validates :poll, :presence => true
|
13
|
+
validates :answer, :presence => true,
|
14
|
+
:length => { :maximum => 32,
|
15
|
+
:allow_blank => true }
|
16
|
+
|
17
|
+
#Normalization
|
18
|
+
normalize_attribute :answer, :with => [:squish, :blank]
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Formol
|
2
|
+
class Poll::Vote < ActiveRecord::Base
|
3
|
+
#Security
|
4
|
+
attr_protected :voter_id, :option_id
|
5
|
+
|
6
|
+
#Associations
|
7
|
+
belongs_to :option, :counter_cache => true
|
8
|
+
belongs_to :voter, :class_name => Formol.config.user_class
|
9
|
+
|
10
|
+
has_one :poll, :through => :option
|
11
|
+
|
12
|
+
#Validations
|
13
|
+
validates :option, :presence => true
|
14
|
+
validates :voter, :presence => true
|
15
|
+
|
16
|
+
#Callbacks
|
17
|
+
after_create :increment_poll_votes_count
|
18
|
+
after_destroy :decrement_poll_votes_count
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
#Callbacks methods
|
23
|
+
|
24
|
+
def increment_poll_votes_count
|
25
|
+
Formol::Poll.increment_counter(:votes_count, poll.id)
|
26
|
+
end
|
27
|
+
|
28
|
+
def decrement_poll_votes_count
|
29
|
+
Formol::Poll.decrement_counter(:votes_count, poll.id)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Formol
|
2
|
+
class Poll < ActiveRecord::Base
|
3
|
+
#Exceptions
|
4
|
+
class PollException < Exception; end
|
5
|
+
class TooManyVotesError < PollException; end
|
6
|
+
class UserAlreadyVoter < PollException; end
|
7
|
+
class NoOptionProvided < PollException; end
|
8
|
+
class TopicLockedError < PollException; end
|
9
|
+
class PollExpiredError < PollException; end
|
10
|
+
|
11
|
+
#Security
|
12
|
+
attr_protected :topic_id
|
13
|
+
attr_readonly :topic_id
|
14
|
+
|
15
|
+
#Virtual attributes
|
16
|
+
attr_accessor :poll_vote_ids #to not conflict with has_many :votes
|
17
|
+
|
18
|
+
#Default values
|
19
|
+
default_value_for :duration, 1.week.to_i
|
20
|
+
default_value_for :max_options, 1 #default to single mode
|
21
|
+
|
22
|
+
#Associations
|
23
|
+
belongs_to :topic
|
24
|
+
|
25
|
+
has_many :options
|
26
|
+
has_many :votes, :through => :options
|
27
|
+
has_many :voters, :through => :votes,
|
28
|
+
:class_name => Formol.config.user_class,
|
29
|
+
:uniq => true
|
30
|
+
|
31
|
+
#Nested attributes
|
32
|
+
accepts_nested_attributes_for :options, :limit => 20
|
33
|
+
|
34
|
+
#Validations
|
35
|
+
validates :topic, :presence => true
|
36
|
+
validates :question, :presence => true,
|
37
|
+
:length => { :in => 5..64,
|
38
|
+
:allow_blank => true }
|
39
|
+
validates :duration, :presence => true,
|
40
|
+
:numericality => { :only_integer => true,
|
41
|
+
:greater_than => 0,
|
42
|
+
:less_than => 3.months.to_i,
|
43
|
+
:allow_blank => true }
|
44
|
+
validates :max_options, :presence => true,
|
45
|
+
:numericality => { :only_integer => true,
|
46
|
+
:greater_than => 0,
|
47
|
+
:less_than_or_equal_to => Proc.new{ |p| p.options.length },
|
48
|
+
:allow_blank => true }
|
49
|
+
validates :options, :length => { :in => 1..20 }
|
50
|
+
|
51
|
+
#Normalization
|
52
|
+
normalize_attribute :question, :with => [:squish, :blank]
|
53
|
+
|
54
|
+
#Callbacks
|
55
|
+
before_validation :setup_options_assoc, :on => :create
|
56
|
+
|
57
|
+
#Business methods
|
58
|
+
|
59
|
+
def multiple?
|
60
|
+
max_options > 1
|
61
|
+
end
|
62
|
+
|
63
|
+
def single?
|
64
|
+
!multiple?
|
65
|
+
end
|
66
|
+
|
67
|
+
def expired?
|
68
|
+
expires_at < Time.now
|
69
|
+
end
|
70
|
+
|
71
|
+
def expires_at
|
72
|
+
created_at + duration.to_i
|
73
|
+
end
|
74
|
+
|
75
|
+
def voted?(user)
|
76
|
+
voters.include?(user)
|
77
|
+
end
|
78
|
+
|
79
|
+
def vote(user, option_ids)
|
80
|
+
option_ids = [option_ids] unless option_ids.respond_to?(:each)
|
81
|
+
option_ids.reject!(&:blank?)
|
82
|
+
option_ids.compact!
|
83
|
+
|
84
|
+
raise NoOptionProvided.new if option_ids.empty?
|
85
|
+
raise TooManyVotesError.new if option_ids.length > max_options
|
86
|
+
raise UserAlreadyVoter.new if voted?(user)
|
87
|
+
raise TopicLockedError.new if topic.locked?
|
88
|
+
raise PollExpiredError.new if expired?
|
89
|
+
|
90
|
+
options.find(option_ids).to_a.each do |option| #?
|
91
|
+
option.votes.create!(:voter => user)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
#Callback methods
|
98
|
+
|
99
|
+
# set poll for each options
|
100
|
+
# AR doesn't do it by itself and it ruins validations
|
101
|
+
def setup_options_assoc
|
102
|
+
options.each do |o|
|
103
|
+
o.poll = self
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module Formol
|
2
|
+
class Post < ActiveRecord::Base
|
3
|
+
#Security
|
4
|
+
attr_protected :topic_id, :user_id, :created_by_topic
|
5
|
+
|
6
|
+
#Virtual attributes
|
7
|
+
attr_accessor :created_by_topic, :quote_id, :register_user_as_subscriber
|
8
|
+
|
9
|
+
#Default values
|
10
|
+
default_value_for :created_by_topic, false
|
11
|
+
default_value_for :display_signature do |p|
|
12
|
+
p.user ? p.user.preference.use_signature : true
|
13
|
+
end
|
14
|
+
|
15
|
+
#Associations
|
16
|
+
belongs_to :topic, :touch => true,
|
17
|
+
:counter_cache => true
|
18
|
+
belongs_to :user, :class_name => Formol.config.user_class,
|
19
|
+
:counter_cache => :formol_posts_count
|
20
|
+
|
21
|
+
has_one :forum, :through => :topic
|
22
|
+
|
23
|
+
#Validations
|
24
|
+
validates :topic, :presence => true
|
25
|
+
validates :user, :presence => true
|
26
|
+
#Length is set with a minimum of 2 to allow posts with simple "ok" answer
|
27
|
+
#Length is set with a maximum of 50 000 because 50Kb is enough for a non
|
28
|
+
#binary forum
|
29
|
+
validates :content, :presence => true,
|
30
|
+
:length => { :in => 2..50_000,
|
31
|
+
:allow_blank => true }
|
32
|
+
|
33
|
+
validate :validates_topic_not_locked
|
34
|
+
|
35
|
+
#Normalization
|
36
|
+
normalize_attribute :content, :with => [:strip, :blank]
|
37
|
+
|
38
|
+
#Callbacks
|
39
|
+
after_initialize :feed_content_with_quote
|
40
|
+
|
41
|
+
after_save :register_author_as_subscriber
|
42
|
+
after_create :increment_forum_counter_cache,
|
43
|
+
:register_as_last_post_on_topic,
|
44
|
+
:register_as_last_post_on_forum
|
45
|
+
after_destroy :decrement_forum_counter_cache,
|
46
|
+
:unregister_as_last_post_on_topic,
|
47
|
+
:unregister_as_last_post_on_forum
|
48
|
+
|
49
|
+
#Scopes
|
50
|
+
class << self
|
51
|
+
# scope for ordering posts conveniently for listing views
|
52
|
+
# order by id (no order is random with mysql)
|
53
|
+
def ordered_for_listing
|
54
|
+
order('formol_posts.id ASC')
|
55
|
+
end
|
56
|
+
|
57
|
+
# scope to get posts conveniently prepared for listing
|
58
|
+
def ready_for_listing
|
59
|
+
ordered_for_listing.includes(:topic, { :user => :preference })
|
60
|
+
end
|
61
|
+
|
62
|
+
# scope to get last five posts with reverse ordering
|
63
|
+
# usefull to have a little history when replying to a topic
|
64
|
+
def last_fives
|
65
|
+
order('formol_posts.id DESC').limit(5).includes(:user)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
#Business methods
|
70
|
+
|
71
|
+
# Read accessor for 'register_user_as_subscriber'
|
72
|
+
def register_user_as_subscriber(force_recompute = false)
|
73
|
+
if @register_user_as_subscriber.nil? || force_recompute
|
74
|
+
@register_user_as_subscriber = compute_register_subscribing
|
75
|
+
end
|
76
|
+
|
77
|
+
@register_user_as_subscriber
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# Convert 'register_user_as_subscriber' to boolean
|
83
|
+
def register_user_as_subscriber?
|
84
|
+
[true, '1', 'true'].include?(register_user_as_subscriber)
|
85
|
+
end
|
86
|
+
|
87
|
+
#Callback methods
|
88
|
+
|
89
|
+
# Registers author as subscriber if he's not already
|
90
|
+
def register_author_as_subscriber
|
91
|
+
if register_user_as_subscriber?
|
92
|
+
topic.register_subscriber(user) unless topic.subscriber?(user)
|
93
|
+
else
|
94
|
+
topic.unregister_subscriber(user) if topic.subscriber?(user)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Validates topic is not locked
|
99
|
+
# If topic is locked, it will add an error on base (not on topic)
|
100
|
+
# This validation does not trigger on first post
|
101
|
+
def validates_topic_not_locked
|
102
|
+
errors.add(:base, :topic_locked) if topic.locked? && !created_by_topic
|
103
|
+
end
|
104
|
+
|
105
|
+
# Holds counter cache on Forum till AR does not support it on has_one
|
106
|
+
def increment_forum_counter_cache
|
107
|
+
Formol::Forum.increment_counter(:posts_count, forum.id) if forum.present?
|
108
|
+
end
|
109
|
+
|
110
|
+
# Holds counter cache on Forum till AR does not support it on has_one
|
111
|
+
def decrement_forum_counter_cache
|
112
|
+
Formol::Forum.decrement_counter(:posts_count, forum.id) if forum.present?
|
113
|
+
end
|
114
|
+
|
115
|
+
# Feed content with quote's content
|
116
|
+
# If you're trying to pass quote_id in a POST, your content will be wiped
|
117
|
+
# Too bad!
|
118
|
+
def feed_content_with_quote
|
119
|
+
return true if quote_id.blank?
|
120
|
+
|
121
|
+
q = topic.posts.find(quote_id)
|
122
|
+
formatter = Formol.config.formatter.new
|
123
|
+
|
124
|
+
# Extract and agnostize it
|
125
|
+
content_with_quote = "> **#{q.user.display_name} #{I18n.t('.wrote')}:**\n>\n"
|
126
|
+
content_with_quote << formatter.quote(q.content)
|
127
|
+
|
128
|
+
self.content = content_with_quote
|
129
|
+
self.content << "\n\n" #use '<<' to skip normalize_attribute
|
130
|
+
self.content
|
131
|
+
end
|
132
|
+
|
133
|
+
# Register current post being the last of its topic
|
134
|
+
def register_as_last_post_on_topic
|
135
|
+
topic.register_last_post(self)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Unregister current post being the last of its topic
|
139
|
+
def unregister_as_last_post_on_topic
|
140
|
+
topic.unregister_last_post if topic.last_post == self
|
141
|
+
end
|
142
|
+
|
143
|
+
# Register current post being the last post of its forum
|
144
|
+
def register_as_last_post_on_forum
|
145
|
+
forum.register_last_post(self)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Unregister current post being the last of its forum
|
149
|
+
def unregister_as_last_post_on_forum
|
150
|
+
forum.unregister_last_post if forum.last_post == self
|
151
|
+
end
|
152
|
+
|
153
|
+
# Computes 'register_user_as_subscriber
|
154
|
+
def compute_register_subscribing
|
155
|
+
if topic && user
|
156
|
+
topic.subscriber?(user)
|
157
|
+
else
|
158
|
+
false
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Formol
|
2
|
+
class Topic::Subscription < ActiveRecord::Base
|
3
|
+
#Security
|
4
|
+
attr_protected :subscriber_id, :topic_id
|
5
|
+
|
6
|
+
#Associations
|
7
|
+
belongs_to :subscriber, :class_name => Formol.config.user_class
|
8
|
+
belongs_to :topic
|
9
|
+
|
10
|
+
#Validations
|
11
|
+
validates :subscriber, :presence => true
|
12
|
+
validates :topic, :presence => true
|
13
|
+
validates :subscriber_id, :uniqueness => { :scope => :topic_id,
|
14
|
+
:allow_blank => true }
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Formol
|
2
|
+
class Topic::Track < ActiveRecord::Base
|
3
|
+
#Security
|
4
|
+
attr_protected :user_id, :topic_id, :forum_id, :marked_at
|
5
|
+
attr_readonly :user_id, :topic_id, :forum_id
|
6
|
+
|
7
|
+
#Association
|
8
|
+
belongs_to :user, :class_name => Formol.config.user_class
|
9
|
+
belongs_to :topic
|
10
|
+
belongs_to :forum
|
11
|
+
|
12
|
+
#Validations
|
13
|
+
validates :user_id, :presence => true
|
14
|
+
validates :topic_id, :presence => true
|
15
|
+
validates :forum_id, :presence => true
|
16
|
+
validates :marked_at, :presence => true
|
17
|
+
|
18
|
+
#Callbacks
|
19
|
+
before_validation :assign_forum, :on => :create
|
20
|
+
before_validation :touch_marked_at
|
21
|
+
|
22
|
+
#Business methods
|
23
|
+
|
24
|
+
class << self
|
25
|
+
# mark a given topic for a given user
|
26
|
+
# if track already exists, it will be updated otherwhise it will be created
|
27
|
+
# handle nil user if forum is browsed without being connected
|
28
|
+
def mark_topic_for_user(topic, user)
|
29
|
+
return nil if user.nil?
|
30
|
+
|
31
|
+
track = find_or_initialize_by_topic_id_and_user_id(topic.id, user.id)
|
32
|
+
track.mark
|
33
|
+
|
34
|
+
track
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def mark
|
39
|
+
save
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
#Callback methods
|
45
|
+
|
46
|
+
# assigns forum from topic
|
47
|
+
# convenient to know if a forum is read or not
|
48
|
+
def assign_forum
|
49
|
+
self.forum = topic.forum if topic
|
50
|
+
end
|
51
|
+
|
52
|
+
# assigns current time to marked_at
|
53
|
+
def touch_marked_at
|
54
|
+
self.marked_at = Time.now
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module Formol
|
2
|
+
class Topic < ActiveRecord::Base
|
3
|
+
#Modules
|
4
|
+
include Tracking
|
5
|
+
|
6
|
+
#Security
|
7
|
+
attr_protected :forum_id, :user_id, :first_post_id, :last_post_id,
|
8
|
+
:posts_count, :views_count
|
9
|
+
|
10
|
+
#Virtual attributes
|
11
|
+
attr_accessor :attach_poll
|
12
|
+
|
13
|
+
#Default values
|
14
|
+
default_value_for :attach_poll, false
|
15
|
+
|
16
|
+
#Associations
|
17
|
+
belongs_to :forum, :counter_cache => true
|
18
|
+
belongs_to :user, :class_name => Formol.config.user_class
|
19
|
+
belongs_to :first_post, :class_name => 'Formol::Post'
|
20
|
+
belongs_to :last_post, :class_name => 'Formol::Post'
|
21
|
+
|
22
|
+
has_many :posts, :dependent => :destroy
|
23
|
+
has_many :subscriptions, :dependent => :destroy
|
24
|
+
has_many :subscribers, :through => :subscriptions
|
25
|
+
|
26
|
+
has_one :poll
|
27
|
+
|
28
|
+
#Nested attributes
|
29
|
+
accepts_nested_attributes_for :posts, :limit => 1
|
30
|
+
accepts_nested_attributes_for :poll
|
31
|
+
|
32
|
+
#Validations
|
33
|
+
validates :title, :presence => true,
|
34
|
+
:length => { :in => 3..48,
|
35
|
+
:allow_blank => true }
|
36
|
+
validates :forum, :presence => true
|
37
|
+
validates :user, :presence => true
|
38
|
+
validates :posts, :presence => true
|
39
|
+
validates :first_post, :presence => true
|
40
|
+
|
41
|
+
#Normalization
|
42
|
+
normalize_attribute :title, :with => [:squish, :blank]
|
43
|
+
|
44
|
+
#Callbacks
|
45
|
+
before_validation :setup_first_post_assoc, :on => :create
|
46
|
+
before_validation :setup_poll_assoc, :on => :create
|
47
|
+
before_validation :reject_poll_if_needed
|
48
|
+
|
49
|
+
#Scopes
|
50
|
+
class << self
|
51
|
+
# scope for ordering topics conveniently for listing views
|
52
|
+
# order by updated_at and id
|
53
|
+
# id is used as discard if two topics have been updated at the same second
|
54
|
+
def ordered_for_listing
|
55
|
+
order('formol_topics.pinned DESC').
|
56
|
+
order('formol_topics.updated_at DESC').
|
57
|
+
order('formol_topics.id DESC')
|
58
|
+
end
|
59
|
+
|
60
|
+
# scope to get topics conveniently prepared for listing
|
61
|
+
def ready_for_listing
|
62
|
+
ordered_for_listing.includes(:user, { :last_post => :user }, :posts)
|
63
|
+
end
|
64
|
+
|
65
|
+
# this scope eager loads conveniently topic for using it in breadcrumb
|
66
|
+
def ready_for_breadcrumb
|
67
|
+
includes(:forum => :category)
|
68
|
+
end
|
69
|
+
|
70
|
+
# return all unread topics for a given forum and a given user
|
71
|
+
# A topic is unread if it's
|
72
|
+
# * tracked for given user and marked_at is after last post's created_at
|
73
|
+
# * not tracked and created after user registered
|
74
|
+
def unread(user, opts = {})
|
75
|
+
unread_scope_base(user, opts)
|
76
|
+
.select('formol_topics.id')
|
77
|
+
.joins(:last_post)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
#Business methods
|
82
|
+
|
83
|
+
# Increments views counter
|
84
|
+
def view!
|
85
|
+
# Use class method instead of instance to avoid concurrence problems
|
86
|
+
self.class.increment_counter(:views_count, id)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Track current topic for a given user
|
90
|
+
def track_for_user!(user)
|
91
|
+
Track.mark_topic_for_user(self, user)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Return if one post is the first post or not
|
95
|
+
# Should be in Formol::Post class but while :inverse_of does not work
|
96
|
+
# on has_many, optimization is easier in this sense
|
97
|
+
def first_post?(post)
|
98
|
+
post == first_post
|
99
|
+
end
|
100
|
+
|
101
|
+
# Register a post being the last one of the current topic
|
102
|
+
def register_last_post(post)
|
103
|
+
update_attribute(:last_post, post)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Unregister a post by re-assigning last post.
|
107
|
+
# Needed when a post is destroyed
|
108
|
+
def unregister_last_post
|
109
|
+
update_attribute(:last_post, posts(true).last)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Register a subscriber to the current topic
|
113
|
+
def register_subscriber(subscriber)
|
114
|
+
subscriptions.create!(:subscriber => subscriber, :topic => self)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Remove a user from subscribers
|
118
|
+
def unregister_subscriber(subscriber)
|
119
|
+
#subscriptions.where(:subscriber_id => subscriber.id).try(:destroy)
|
120
|
+
subscribers.destroy(subscriber)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Tells if a user has subscribed (or not) to the current topic
|
124
|
+
def subscriber?(user)
|
125
|
+
subscriptions.where(:subscriber_id => user.id).exists?
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
#Callback methods
|
131
|
+
|
132
|
+
# Setup poll association
|
133
|
+
def setup_poll_assoc
|
134
|
+
poll.topic = self if poll
|
135
|
+
end
|
136
|
+
|
137
|
+
# Setup first post's associations
|
138
|
+
def setup_first_post_assoc
|
139
|
+
#register first post
|
140
|
+
self.first_post = posts.first
|
141
|
+
|
142
|
+
set_first_post_topic
|
143
|
+
set_first_post_user
|
144
|
+
set_first_post_created_by_topic
|
145
|
+
end
|
146
|
+
|
147
|
+
# Set the first post's user
|
148
|
+
# It's just the same as topics
|
149
|
+
def set_first_post_user
|
150
|
+
# done with 'each' and not 'first' to avoid empty association
|
151
|
+
posts.each do |p|
|
152
|
+
p.user = self.user
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Set the first post's topic
|
157
|
+
# If not, nested attributes triggers presence validation of topic in
|
158
|
+
# post model and this last fails
|
159
|
+
def set_first_post_topic
|
160
|
+
# done with 'each' and not 'first' to avoid empty association
|
161
|
+
# if association is empty or contains more than 1 post,
|
162
|
+
# validations handle it, not this method
|
163
|
+
posts.each do |p|
|
164
|
+
p.topic = self
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Inform first post's topic it's the first one
|
169
|
+
# Needed to not perform 'locked' validation
|
170
|
+
def set_first_post_created_by_topic
|
171
|
+
# done with 'each' and not 'first' to avoid empty association
|
172
|
+
# if association is empty or contains more than 1 post,
|
173
|
+
# validations handle it, not this method
|
174
|
+
posts.each do |p|
|
175
|
+
p.created_by_topic = true
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Reject poll if attach_poll is false
|
180
|
+
# Needed because poll's default values perform validations
|
181
|
+
def reject_poll_if_needed
|
182
|
+
self.poll = nil if poll && !attach_poll
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|