thredded 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.mkdn +46 -0
  4. data/Rakefile +31 -0
  5. data/app/assets/javascripts/thredded/application.js +15 -0
  6. data/app/assets/stylesheets/thredded/application.css +13 -0
  7. data/app/controllers/thredded/application_controller.rb +50 -0
  8. data/app/controllers/thredded/messageboards_controller.rb +9 -0
  9. data/app/controllers/thredded/posts_controller.rb +86 -0
  10. data/app/controllers/thredded/preferences_controller.rb +20 -0
  11. data/app/controllers/thredded/private_topics_controller.rb +25 -0
  12. data/app/controllers/thredded/setups_controller.rb +39 -0
  13. data/app/controllers/thredded/topics_controller.rb +121 -0
  14. data/app/decorators/thredded/post_decorator.rb +52 -0
  15. data/app/decorators/thredded/topic_decorator.rb +96 -0
  16. data/app/helpers/thredded/application_helper.rb +9 -0
  17. data/app/helpers/thredded/messageboard_helper.rb +52 -0
  18. data/app/helpers/thredded/posts_helper.rb +7 -0
  19. data/app/helpers/thredded/topics_helper.rb +21 -0
  20. data/app/mailers/thredded/post_mailer.rb +28 -0
  21. data/app/mailers/thredded/topic_mailer.rb +29 -0
  22. data/app/models/thredded/ability.rb +50 -0
  23. data/app/models/thredded/attachment.rb +24 -0
  24. data/app/models/thredded/category.rb +10 -0
  25. data/app/models/thredded/image.rb +27 -0
  26. data/app/models/thredded/messageboard.rb +92 -0
  27. data/app/models/thredded/messageboard_decorator.rb +18 -0
  28. data/app/models/thredded/messageboard_preference.rb +15 -0
  29. data/app/models/thredded/null_preference.rb +9 -0
  30. data/app/models/thredded/null_topic_read.rb +13 -0
  31. data/app/models/thredded/null_user.rb +27 -0
  32. data/app/models/thredded/post.rb +75 -0
  33. data/app/models/thredded/post_notification.rb +6 -0
  34. data/app/models/thredded/private_topic.rb +33 -0
  35. data/app/models/thredded/private_user.rb +7 -0
  36. data/app/models/thredded/role.rb +18 -0
  37. data/app/models/thredded/topic.rb +145 -0
  38. data/app/models/thredded/topic_category.rb +7 -0
  39. data/app/models/thredded/user_detail.rb +6 -0
  40. data/app/models/thredded/user_extender.rb +27 -0
  41. data/app/models/thredded/user_preference.rb +6 -0
  42. data/app/models/thredded/user_topic_read.rb +67 -0
  43. data/app/uploaders/thredded/attachment_uploader.rb +30 -0
  44. data/app/uploaders/thredded/image_uploader.rb +17 -0
  45. data/app/views/thredded/messageboards/_messageboard.html.erb +17 -0
  46. data/app/views/thredded/messageboards/index.html.erb +15 -0
  47. data/app/views/thredded/post_mailer/at_notification.html.erb +17 -0
  48. data/app/views/thredded/post_mailer/at_notification.text.erb +13 -0
  49. data/app/views/thredded/posts/_form.html.erb +25 -0
  50. data/app/views/thredded/posts/_null_user.html.erb +0 -0
  51. data/app/views/thredded/posts/_post.html.erb +25 -0
  52. data/app/views/thredded/posts/_user.html.erb +7 -0
  53. data/app/views/thredded/posts/edit.html.erb +28 -0
  54. data/app/views/thredded/posts/index.html.erb +74 -0
  55. data/app/views/thredded/preferences/edit.html.erb +23 -0
  56. data/app/views/thredded/search/_form.html.erb +9 -0
  57. data/app/views/thredded/setups/_form.html.erb +35 -0
  58. data/app/views/thredded/setups/new.html.erb +5 -0
  59. data/app/views/thredded/shared/_currently_online.html.erb +10 -0
  60. data/app/views/thredded/shared/_error_msgs.html.erb +7 -0
  61. data/app/views/thredded/shared/_google_analytics.html.erb +10 -0
  62. data/app/views/thredded/shared/_topic_nav.html.erb +18 -0
  63. data/app/views/thredded/topic_mailer/message_notification.html.erb +17 -0
  64. data/app/views/thredded/topic_mailer/message_notification.text.erb +13 -0
  65. data/app/views/thredded/topics/_form.html.erb +6 -0
  66. data/app/views/thredded/topics/_topic_condensed.html.erb +20 -0
  67. data/app/views/thredded/topics/_topic_form.html.erb +27 -0
  68. data/app/views/thredded/topics/by_category.html.erb +51 -0
  69. data/app/views/thredded/topics/edit.html.erb +25 -0
  70. data/app/views/thredded/topics/index.html.erb +52 -0
  71. data/app/views/thredded/topics/menu/_new_topic.html.erb +3 -0
  72. data/app/views/thredded/topics/new.html.erb +18 -0
  73. data/app/views/thredded/topics/search.html.erb +45 -0
  74. data/config/routes.rb +26 -0
  75. data/db/migrate/20130425230852_create_thredded_tables.rb +143 -0
  76. data/lib/tasks/thredded_tasks.rake +4 -0
  77. data/lib/thredded.rb +38 -0
  78. data/lib/thredded/at_notification_extractor.rb +15 -0
  79. data/lib/thredded/at_notifier.rb +65 -0
  80. data/lib/thredded/at_users.rb +17 -0
  81. data/lib/thredded/email_processor.rb +74 -0
  82. data/lib/thredded/engine.rb +17 -0
  83. data/lib/thredded/filter.rb +4 -0
  84. data/lib/thredded/filter/at_notification.rb +11 -0
  85. data/lib/thredded/filter/attachment.rb +51 -0
  86. data/lib/thredded/filter/base.rb +13 -0
  87. data/lib/thredded/filter/bbcode.rb +75 -0
  88. data/lib/thredded/filter/emoji.rb +11 -0
  89. data/lib/thredded/filter/markdown.rb +25 -0
  90. data/lib/thredded/filter/syntax.rb +28 -0
  91. data/lib/thredded/filter/textile.rb +21 -0
  92. data/lib/thredded/messageboard_user_permissions.rb +27 -0
  93. data/lib/thredded/post_sql_builder.rb +34 -0
  94. data/lib/thredded/post_user_permissions.rb +37 -0
  95. data/lib/thredded/private_topic_notifier.rb +56 -0
  96. data/lib/thredded/private_topic_user_permissions.rb +32 -0
  97. data/lib/thredded/search_parser.rb +45 -0
  98. data/lib/thredded/search_sql_builder.rb +25 -0
  99. data/lib/thredded/setup_thredded.rb +7 -0
  100. data/lib/thredded/table_sql_builder.rb +105 -0
  101. data/lib/thredded/topic_sql_builder.rb +32 -0
  102. data/lib/thredded/topic_user_permissions.rb +51 -0
  103. data/lib/thredded/version.rb +3 -0
  104. metadata +537 -0
@@ -0,0 +1,18 @@
1
+ module Thredded
2
+ class MessageboardDecorator < SimpleDelegator
3
+ attr_reader :messageboard
4
+
5
+ def initialize(messageboard)
6
+ super
7
+ @messageboard = messageboard
8
+ end
9
+
10
+ def category_options
11
+ messageboard.categories.collect { |cat| [cat.name, cat.id] }
12
+ end
13
+
14
+ def users_options
15
+ messageboard.users.collect{ |user| [user.name, user.id] }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ module Thredded
2
+ class MessageboardPreference < ActiveRecord::Base
3
+ attr_accessible :notify_on_mention, :notify_on_message, :filter
4
+ belongs_to :user
5
+ belongs_to :messageboard
6
+
7
+ def self.for(user)
8
+ where(user_id: user.id)
9
+ end
10
+
11
+ def self.in(messageboard)
12
+ where(messageboard_id: messageboard.id)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ class NullPreference
2
+ def notify_on_mention
3
+ true
4
+ end
5
+
6
+ def notify_on_message
7
+ true
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ class NullTopicRead
2
+ def update_status(topic)
3
+ # ¯\_(ツ)_/¯
4
+ end
5
+
6
+ def page
7
+ 0
8
+ end
9
+
10
+ def post_id
11
+ 0
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ module Thredded
2
+ class NullUser
3
+ def admins?(messageboard)
4
+ false
5
+ end
6
+
7
+ def can_read_messageboard?(messageboard)
8
+ messageboard.public?
9
+ end
10
+
11
+ def id
12
+ 0
13
+ end
14
+
15
+ def member_of?(messageboard)
16
+ false
17
+ end
18
+
19
+ def name
20
+ 'Anonymous User'
21
+ end
22
+
23
+ def valid?
24
+ false
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,75 @@
1
+ module Thredded
2
+ class Post < ActiveRecord::Base
3
+ require 'gravtastic'
4
+ include Gravtastic
5
+ include Thredded::Filter::Base
6
+ include Thredded::Filter::Textile
7
+ include Thredded::Filter::Bbcode
8
+ include Thredded::Filter::Markdown
9
+ include Thredded::Filter::Attachment
10
+ include Thredded::Filter::Emoji
11
+ include Thredded::Filter::AtNotification
12
+
13
+ gravtastic :user_email
14
+ paginates_per 50
15
+
16
+ attr_accessible :attachments_attributes,
17
+ :content,
18
+ :filter,
19
+ :ip,
20
+ :messageboard,
21
+ :source,
22
+ :topic,
23
+ :user
24
+
25
+ default_scope order: 'id ASC'
26
+
27
+ belongs_to :messageboard, counter_cache: true
28
+ belongs_to :topic, counter_cache: true
29
+ belongs_to :user, class_name: Thredded.user_class
30
+ has_many :attachments
31
+ has_many :post_notifications
32
+
33
+ accepts_nested_attributes_for :attachments
34
+
35
+ validates_presence_of :content, :messageboard_id
36
+
37
+ before_validation :set_user_email
38
+ after_create :modify_parent_topic
39
+
40
+ def create
41
+ UserDetail.increment_counter(:posts_count, user_id)
42
+ super
43
+ end
44
+
45
+ def created_date
46
+ created_at.strftime("%b %d, %Y %I:%M:%S %Z") if created_at
47
+ end
48
+
49
+ def created_timestamp
50
+ created_at.strftime("%Y-%m-%dT%H:%M:%S") if created_at
51
+ end
52
+
53
+ def gravatar_url
54
+ super.gsub /http:/, ''
55
+ end
56
+
57
+ def self.filters
58
+ ['bbcode', 'markdown']
59
+ end
60
+
61
+ private
62
+
63
+ def modify_parent_topic
64
+ topic.last_user = user
65
+ topic.touch
66
+ topic.save
67
+ end
68
+
69
+ def set_user_email
70
+ if user
71
+ self.user_email = user.email
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,6 @@
1
+ module Thredded
2
+ class PostNotification < ActiveRecord::Base
3
+ attr_accessible :email, :post_id
4
+ belongs_to :post
5
+ end
6
+ end
@@ -0,0 +1,33 @@
1
+ module Thredded
2
+ class PrivateTopic < Thredded::Topic
3
+ has_many :private_users
4
+ has_many :users, through: :private_users
5
+ attr_accessible :user_id
6
+
7
+ def add_user(user)
8
+ if String == user.class
9
+ user = User.find_by_name(user)
10
+ end
11
+
12
+ users << user
13
+ end
14
+
15
+ def public?
16
+ false
17
+ end
18
+
19
+ def private?
20
+ true
21
+ end
22
+
23
+ def user_id=(ids)
24
+ if ids.size > 0
25
+ self.users = User.where(id: ids.uniq)
26
+ end
27
+ end
28
+
29
+ def users_to_sentence
30
+ users.map{ |user| user.name.capitalize }.to_sentence
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ module Thredded
2
+ class PrivateUser < ActiveRecord::Base
3
+ attr_accessible :private_topic_id, :user_id
4
+ belongs_to :private_topic
5
+ belongs_to :user
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ module Thredded
2
+ class Role < ActiveRecord::Base
3
+ ROLES = ['superadmin', 'admin', 'moderator', 'member']
4
+
5
+ belongs_to :messageboard
6
+ belongs_to :user, class_name: Thredded.user_class
7
+
8
+ validates_presence_of :level
9
+ validates_inclusion_of :level, in: ROLES
10
+ validates_presence_of :messageboard_id
11
+ validates_presence_of :user_id
12
+
13
+ attr_accessible :level, :messageboard_id, :user_id
14
+
15
+ scope :for, lambda { |messageboard| where(messageboard_id: messageboard.id) }
16
+ scope :as, lambda { |role| where(level: role) }
17
+ end
18
+ end
@@ -0,0 +1,145 @@
1
+ require 'thredded/search_sql_builder'
2
+
3
+ module Thredded
4
+ class Topic < ActiveRecord::Base
5
+ STATES = %w{pending approved}
6
+
7
+ extend FriendlyId
8
+ friendly_id :title, use: :scoped, scope: :messageboard
9
+ paginates_per 50 if self.respond_to?(:paginates_per)
10
+
11
+ has_many :posts, include: :attachments
12
+ has_many :topic_categories
13
+ has_many :categories, through: :topic_categories
14
+
15
+ belongs_to :last_user, class_name: Thredded.user_class,
16
+ foreign_key: 'last_user_id'
17
+
18
+ belongs_to :user, class_name: Thredded.user_class
19
+
20
+ belongs_to :messageboard, counter_cache: true, touch: true
21
+
22
+ validates_inclusion_of :state, in: STATES
23
+ validates_presence_of :hash_id
24
+ validates_presence_of :last_user_id
25
+ validates_presence_of :messageboard_id
26
+ validates_numericality_of :posts_count
27
+ validates_uniqueness_of :hash_id
28
+
29
+ attr_accessible :category_ids,
30
+ :last_user,
31
+ :locked,
32
+ :messageboard,
33
+ :posts_attributes,
34
+ :sticky,
35
+ :type,
36
+ :title,
37
+ :user,
38
+ :usernames
39
+
40
+ accepts_nested_attributes_for :posts, reject_if: :updating?
41
+ accepts_nested_attributes_for :categories
42
+
43
+ default_scope order('updated_at DESC')
44
+
45
+ delegate :name, :name=, :email, :email=, to: :user, prefix: true
46
+
47
+ before_validation do
48
+ self.hash_id = SecureRandom.hex(10) if self.hash_id.nil?
49
+ end
50
+
51
+ def self.stuck
52
+ where(sticky: true)
53
+ end
54
+
55
+ def self.unstuck
56
+ where(sticky: false)
57
+ end
58
+
59
+ def self.on_page(page_num)
60
+ page(page_num).per(30)
61
+ end
62
+
63
+ def self.for_messageboard(messageboard)
64
+ where(messageboard_id: messageboard.id)
65
+ end
66
+
67
+ def self.order_by_updated
68
+ order('updated_at DESC')
69
+ end
70
+
71
+ def self.full_text_search(query, messageboard)
72
+ if query.empty?
73
+ []
74
+ else
75
+ sql_builder = SearchSqlBuilder.new(query, messageboard)
76
+ sql = sql_builder.build
77
+ sql_params = [sql].concat(sql_builder.binds)
78
+ find_by_sql sql_params
79
+ end
80
+ end
81
+
82
+ def self.decorate
83
+ all.map do |topic|
84
+ TopicDecorator.new(topic)
85
+ end
86
+ end
87
+
88
+ def decorate
89
+ TopicDecorator.new(self)
90
+ end
91
+
92
+ def create
93
+ UserDetail.increment_counter(:topics_count, user_id)
94
+ super
95
+ end
96
+
97
+ def last_user
98
+ super || NullUser.new
99
+ end
100
+
101
+ def public?
102
+ true
103
+ end
104
+
105
+ def private?
106
+ false
107
+ end
108
+
109
+ def pending?
110
+ state == 'pending'
111
+ end
112
+
113
+ def users
114
+ []
115
+ end
116
+
117
+ def users_to_sentence
118
+ ''
119
+ end
120
+
121
+ def self.inherited(child)
122
+ child.instance_eval do
123
+ def model_name
124
+ Topic.model_name
125
+ end
126
+ end
127
+
128
+ super
129
+ end
130
+
131
+ def self.select_options
132
+ subclasses.map(&:to_s).sort
133
+ end
134
+
135
+ def updating?
136
+ id.present?
137
+ end
138
+
139
+ def categories_to_sentence
140
+ if categories
141
+ categories.map(&:name).to_sentence
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,7 @@
1
+ module Thredded
2
+ class TopicCategory < ActiveRecord::Base
3
+ attr_accessible :category_id, :topic_id
4
+ belongs_to :category
5
+ belongs_to :topic
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ module Thredded
2
+ class UserDetail < ActiveRecord::Base
3
+ belongs_to :user, class_name: Thredded.user_class
4
+ validates :user_id, presence: true
5
+ end
6
+ end
@@ -0,0 +1,27 @@
1
+ require 'active_support/concern'
2
+
3
+ module Thredded
4
+ module UserExtender
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ has_many :thredded_messageboard_preferences, class_name: 'Thredded::MessageboardPreference', foreign_key: 'user_id'
9
+ has_many :thredded_posts, class_name: 'Thredded::Post', foreign_key: 'user_id'
10
+ has_many :thredded_private_topics, through: :thredded_private_users, class_name: 'Thredded::PrivateTopic', source: :private_topic
11
+ has_many :thredded_private_users, class_name: 'Thredded::PrivateUser', foreign_key: 'user_id'
12
+ has_many :thredded_roles, class_name: 'Thredded::Role', foreign_key: 'user_id'
13
+ has_many :thredded_topics, class_name: 'Thredded::Topic', foreign_key: 'user_id'
14
+
15
+ has_one :thredded_user_detail, class_name: 'Thredded::UserDetail', foreign_key: 'user_id'
16
+ has_one :thredded_user_preference, class_name: 'Thredded::UserPreference', foreign_key: 'user_id'
17
+
18
+
19
+ def self.recently_active_in(messageboard)
20
+ joins(:thredded_roles)
21
+ .where(thredded_roles: { messageboard_id: messageboard.id })
22
+ .where('thredded_roles.last_seen > ?', 5.minutes.ago)
23
+ .order('thredded_roles.last_seen')
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ module Thredded
2
+ class UserPreference < ActiveRecord::Base
3
+ belongs_to :user, class_name: Thredded.user_class
4
+ validates :user_id, presence: true
5
+ end
6
+ end
@@ -0,0 +1,67 @@
1
+ module Thredded
2
+ class UserTopicRead < ActiveRecord::Base
3
+ attr_accessible :page,
4
+ :post_id,
5
+ :posts_count,
6
+ :topic_id,
7
+ :user_id
8
+
9
+ attr_accessor :viewing_page, :viewing_topic
10
+
11
+ def self.find_or_create_by_user_and_topic(user, topic, page)
12
+ page = page.nil? ? 1 : page.to_i
13
+
14
+ if user
15
+ user_topic_read = find_by_user_id_and_topic_id(user.id, topic.id)
16
+
17
+ if user_topic_read.blank?
18
+ posts_count = [post_limit * (page - 1), topic.posts.size].min
19
+ posts_count = [posts_count, 1].max
20
+ last_post = topic.posts.each_slice(post_limit).to_a[page - 1].first
21
+
22
+ user_topic_read = create(
23
+ user_id: user.id,
24
+ topic_id: topic.id,
25
+ post_id: last_post.id,
26
+ posts_count: posts_count,
27
+ page: page
28
+ )
29
+ end
30
+
31
+ user_topic_read
32
+ else
33
+ NullTopicRead.new
34
+ end
35
+ end
36
+
37
+ def self.statuses_for(user, topics)
38
+ if user && topics
39
+ topic_ids = topics.map { |topic| topic.id }
40
+ where('user_id = ?', user.id).find_all_by_topic_id(topic_ids)
41
+ end
42
+ end
43
+
44
+ def self.update_read_status!(user, topic, page)
45
+ user_id = user.nil? ? '0' : user.id
46
+ page = page.nil? ? 1 : page.to_i
47
+ post_id = topic.posts.each_slice(post_limit).to_a[page-1].last.id
48
+ posts_count = [post_limit * page, topic.posts.size].min
49
+
50
+ user_topic_read = find_by_user_id_and_topic_id(user_id, topic.id)
51
+
52
+ if user_topic_read && page >= user_topic_read.page && user_topic_read.post_id != post_id
53
+ user_topic_read.update_attributes(
54
+ post_id: post_id,
55
+ posts_count: posts_count,
56
+ page: page
57
+ )
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def self.post_limit
64
+ Post.default_per_page
65
+ end
66
+ end
67
+ end