formol 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|