thredded 0.0.1

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.
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