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,52 @@
1
+ module Thredded
2
+ class PostDecorator < SimpleDelegator
3
+ attr_reader :post
4
+
5
+ def initialize(post)
6
+ super
7
+ @post = post
8
+ end
9
+
10
+ def user_name
11
+ if user
12
+ user.name
13
+ else
14
+ 'Anonymous'
15
+ end
16
+ end
17
+
18
+ def original
19
+ post
20
+ end
21
+
22
+ def created_at_timeago
23
+ if created_at.nil?
24
+ <<-eohtml.strip_heredoc.html_safe
25
+ <abbr>
26
+ a little while ago
27
+ </abbr>
28
+ eohtml
29
+ else
30
+ <<-eohtml.strip_heredoc.html_safe
31
+ <abbr class="timeago" title="#{created_at_utc}">
32
+ #{created_at_str}
33
+ </abbr>
34
+ eohtml
35
+ end
36
+ end
37
+
38
+ def gravatar_url
39
+ super.gsub /http:/, ''
40
+ end
41
+
42
+ private
43
+
44
+ def created_at_str
45
+ created_at.getutc.to_s
46
+ end
47
+
48
+ def created_at_utc
49
+ created_at.getutc.iso8601
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,96 @@
1
+ module Thredded
2
+ class TopicDecorator < SimpleDelegator
3
+ include Rails.application.routes.url_helpers
4
+ include ActionView::Helpers::UrlHelper
5
+
6
+ attr_reader :topic
7
+
8
+ def initialize(topic)
9
+ super
10
+ @topic = topic
11
+ end
12
+
13
+ def css_class
14
+ classes = []
15
+ classes << 'locked' if locked?
16
+ classes << 'sticky' if sticky?
17
+ classes << 'private' if private?
18
+ classes += ['category'] + categories.map(&:name) if categories.present?
19
+ classes.join(' ')
20
+ end
21
+
22
+ def user_link
23
+ if user.nil?
24
+ '<a href="#">?</a>'.html_safe
25
+ else
26
+ "<a href='/users/#{user_name}'>#{user_name}</a>".html_safe
27
+ end
28
+ end
29
+
30
+ def last_user_link
31
+ if last_user.nil?
32
+ 'Anonymous'
33
+ else
34
+ "<a href='/users/#{last_user.name}'>#{last_user.name}</a>".html_safe
35
+ end
36
+ end
37
+
38
+ def original
39
+ topic
40
+ end
41
+
42
+ def updated_at_timeago
43
+ if updated_at.nil?
44
+ <<-eohtml.html_safe
45
+ <abbr>
46
+ a little while ago
47
+ </abbr>
48
+ eohtml
49
+ else
50
+ <<-eohtml.html_safe
51
+ <abbr class="timeago" title="#{updated_at_utc}">
52
+ #{updated_at_str}
53
+ </abbr>
54
+ eohtml
55
+ end
56
+ end
57
+
58
+ def created_at_timeago
59
+ if created_at.nil?
60
+ <<-eohtml.html_safe
61
+ <abbr class="started_at">
62
+ a little while ago
63
+ </abbr>
64
+ eohtml
65
+ else
66
+ <<-eohtml.html_safe
67
+ <abbr class="started_at timeago" title="#{created_at_utc}">
68
+ #{created_at_str}
69
+ </abbr>
70
+ eohtml
71
+ end
72
+ end
73
+
74
+ def gravatar_url
75
+ super.gsub /http:/, ''
76
+ end
77
+
78
+ private
79
+
80
+ def updated_at_str
81
+ updated_at.to_s
82
+ end
83
+
84
+ def updated_at_utc
85
+ updated_at.getutc.iso8601
86
+ end
87
+
88
+ def created_at_str
89
+ created_at.to_s
90
+ end
91
+
92
+ def created_at_utc
93
+ created_at.getutc.iso8601
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,9 @@
1
+ module Thredded
2
+ module ApplicationHelper
3
+ def method_missing(method, *args, &block)
4
+ main_app.send(method, *args, &block)
5
+ rescue NoMethodError
6
+ super
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,52 @@
1
+ module Thredded
2
+ module MessageboardHelper
3
+ def link_or_text_to(messageboard)
4
+ if can? :read, messageboard
5
+ link_to messageboard.name, messageboard_topics_path(messageboard)
6
+ else
7
+ messageboard.name
8
+ end
9
+ end
10
+
11
+ def meta_for(messageboard)
12
+ topics = messageboard.topics_count
13
+ posts = messageboard.posts_count
14
+ "#{number_to_human topics} topics,
15
+ #{number_to_human posts} posts".downcase
16
+ end
17
+
18
+ def admin_link_for(messageboard)
19
+ if can? :manage, messageboard
20
+ '<p class="admin"><a href="#edit">Edit</a></p>'
21
+ else
22
+ ''
23
+ end
24
+ end
25
+
26
+ def latest_thread_for(messageboard)
27
+ topic = messageboard.topics.first
28
+
29
+ if topic.present?
30
+ abbr = content_tag :abbr, class: 'updated_at timeago', title: topic.updated_at.strftime('%Y-%m-%dT%H:%M:%S') do
31
+ topic.updated_at.strftime('%b %d, %Y %I:%M:%S %Z')
32
+ end
33
+
34
+ if can? :read, messageboard
35
+ link_to abbr , messageboard_topic_posts_path(messageboard, topic)
36
+ else
37
+ abbr
38
+ end
39
+ else
40
+ ''
41
+ end
42
+ end
43
+
44
+ def latest_user_for(messageboard)
45
+ if messageboard.topics.first.present? && messageboard.topics.first.user.present?
46
+ messageboard.topics.first.last_user.name
47
+ else
48
+ ''
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,7 @@
1
+ module Thredded
2
+ module PostsHelper
3
+ def link_to_edit_post(site, messageboard, topic, post)
4
+ path = edit_messageboard_topic_post_path(messageboard.name, topic, post)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ module Thredded
2
+ module TopicsHelper
3
+ require 'digest/md5'
4
+
5
+ def md5(s)
6
+ Digest::MD5.hexdigest(s)
7
+ end
8
+
9
+ def already_read(topic, tracked_user_reads)
10
+ if tracked_user_reads
11
+ topic_status = tracked_user_reads.select{ |t| t.topic_id == topic.id }.first
12
+
13
+ if topic_status && topic_status.posts_count == topic.posts_count
14
+ 'read'
15
+ else
16
+ 'unread'
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ module Thredded
2
+ class PostMailer < ActionMailer::Base
3
+ def at_notification(post_id, user_emails)
4
+ @post = Post.find(post_id)
5
+
6
+ headers['X-SMTPAPI'] = %Q{{"category": ["thredded_#{@post.messageboard.name}","at_notification"]}}
7
+ mail from: no_reply,
8
+ to: no_reply,
9
+ bcc: user_emails,
10
+ reply_to: reply_to,
11
+ subject: subject
12
+ end
13
+
14
+ private
15
+
16
+ def subject
17
+ "#{Thredded.email_outgoing_prefix} #{@post.topic.title}"
18
+ end
19
+
20
+ def reply_to
21
+ "#{@post.topic.hash_id}@#{Thredded.email_incoming_host}"
22
+ end
23
+
24
+ def no_reply
25
+ Thredded.email_from
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ module Thredded
2
+ class TopicMailer < ActionMailer::Base
3
+ def message_notification(topic_id, emails)
4
+ @topic = Topic.find(topic_id)
5
+ headers['X-SMTPAPI'] =
6
+ %Q{{"category": ["thredded_#{@topic.messageboard.name}","at_notification"]}}
7
+
8
+ mail from: no_reply,
9
+ to: no_reply,
10
+ bcc: emails,
11
+ reply_to: reply_to,
12
+ subject: subject
13
+ end
14
+
15
+ private
16
+
17
+ def subject
18
+ "#{Thredded.email_outgoing_prefix} #{@topic.title}"
19
+ end
20
+
21
+ def reply_to
22
+ "#{@topic.hash_id}@#{Thredded.email_incoming_host}"
23
+ end
24
+
25
+ def no_reply
26
+ Thredded.email_from
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,50 @@
1
+ module Thredded
2
+ class Ability
3
+ include ::CanCan::Ability
4
+
5
+ def initialize(user)
6
+ user ||= Thredded::NullUser.new
7
+ user_details = Thredded::UserDetail.where(user_id: user.id).first
8
+
9
+ can :manage, :all if user_details.try(:superadmin?)
10
+
11
+ can :read, Thredded::Messageboard do |messageboard|
12
+ Thredded::MessageboardUserPermissions.new(messageboard, user).readable?
13
+ end
14
+
15
+ can :manage, Thredded::Topic do |topic|
16
+ Thredded::TopicUserPermissions.new(topic, user, user_details).manageable?
17
+ end
18
+
19
+ can :read, Thredded::Topic do |topic|
20
+ Thredded::TopicUserPermissions.new(topic, user, user_details).readable?
21
+ end
22
+
23
+ can :create, Thredded::Topic do |topic|
24
+ Thredded::TopicUserPermissions.new(topic, user, user_details).creatable?
25
+ end
26
+
27
+ cannot :manage, Thredded::PrivateTopic
28
+
29
+ can :manage, Thredded::PrivateTopic do |private_topic|
30
+ Thredded::PrivateTopicUserPermissions.new(private_topic, user, user_details).manageable?
31
+ end
32
+
33
+ can :create, Thredded::PrivateTopic do |private_topic|
34
+ Thredded::PrivateTopicUserPermissions.new(private_topic, user, user_details).creatable?
35
+ end
36
+
37
+ can :read, Thredded::PrivateTopic do |private_topic|
38
+ Thredded::PrivateTopicUserPermissions.new(private_topic, user, user_details).readable?
39
+ end
40
+
41
+ can :manage, Thredded::Post do |post|
42
+ Thredded::PostUserPermissions.new(post, user, user_details).manageable?
43
+ end
44
+
45
+ can :create, Thredded::Post do |post|
46
+ Thredded::PostUserPermissions.new(post, user, user_details).creatable?
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,24 @@
1
+ module Thredded
2
+ class Attachment < ActiveRecord::Base
3
+ belongs_to :post
4
+ validates_presence_of :attachment
5
+ attr_accessible :attachment
6
+ mount_uploader :attachment, Thredded::AttachmentUploader
7
+ before_save :update_attachment_attributes
8
+
9
+ def cache_dir
10
+ "#{Rails.root}/tmp/uploads"
11
+ end
12
+
13
+ def filename
14
+ File.basename(attachment.path)
15
+ end
16
+
17
+ def update_attachment_attributes
18
+ if attachment.present? && attachment_changed?
19
+ self.content_type = attachment.file.content_type
20
+ self.file_size = attachment.file.size
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ module Thredded
2
+ class Category < ActiveRecord::Base
3
+ attr_accessible :description, :messageboard_id, :name
4
+ validates :name, presence: true
5
+
6
+ belongs_to :messageboard
7
+ has_many :topic_categories
8
+ has_many :topics, through: :topic_categories
9
+ end
10
+ end
@@ -0,0 +1,27 @@
1
+ module Thredded
2
+ class Image < ActiveRecord::Base
3
+ mount_uploader :image, ImageUploader
4
+ attr_accessible :height, :orientation, :width
5
+ validates :image, presence: true
6
+ before_validation :save_dimensions, :save_orientation, :save_position
7
+
8
+ private
9
+
10
+ def save_dimensions
11
+ if image.path
12
+ self.width = MiniMagick::Image.open(image.path)[:width]
13
+ self.height = MiniMagick::Image.open(image.path)[:height]
14
+ end
15
+ end
16
+
17
+ def save_orientation
18
+ if image.path
19
+ self.orientation = (height.to_i > width.to_i) ? 'portrait' : 'landscape'
20
+ end
21
+ end
22
+
23
+ def save_position
24
+ self.position = (self._index + 1) if self.new_record? and self._index
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,92 @@
1
+ module Thredded
2
+ class Messageboard < ActiveRecord::Base
3
+ SECURITY = %w{private logged_in public}
4
+ PERMISSIONS = %w{members logged_in anonymous}
5
+
6
+ extend FriendlyId
7
+ friendly_id :name
8
+
9
+ attr_accessible :description,
10
+ :name,
11
+ :posting_permission,
12
+ :security
13
+
14
+ default_scope where(closed: false).order('topics_count DESC')
15
+
16
+ validates_numericality_of :topics_count
17
+ validates_inclusion_of :security, in: SECURITY
18
+ validates_inclusion_of :posting_permission, in: PERMISSIONS
19
+ validates_presence_of :name
20
+ validates_format_of :name, with: /\A[\w\-]+\z/, on: :create,
21
+ message: 'should be letters, nums, dash, underscore only.'
22
+ validates_uniqueness_of :name,
23
+ message: 'must be a unique board name. Try again.'
24
+ validates_length_of :name, within: 1..16,
25
+ message: 'should be between 1 and 16 characters'
26
+
27
+ has_many :categories
28
+ has_many :messageboard_preferences
29
+ has_many :posts
30
+ has_many :roles
31
+ has_many :topics
32
+ has_many :private_topics
33
+ has_many :users, through: :roles, class_name: Thredded.user_class
34
+
35
+ def active_users
36
+ Role
37
+ .joins(:user)
38
+ .where(messageboard_id: self.id)
39
+ .where('last_seen > ?', 5.minutes.ago)
40
+ .order(:last_seen)
41
+ .map(&:user)
42
+ end
43
+
44
+ def decorate
45
+ MessageboardDecorator.new(self)
46
+ end
47
+
48
+ def add_member(user, as='member')
49
+ roles.create(user_id: user.id, level: as)
50
+ end
51
+
52
+ def has_member?(user)
53
+ roles.where(user_id: user.id).exists?
54
+ end
55
+
56
+ def member_is_a?(user, as)
57
+ roles.where(user_id: user.id, level: as).exists?
58
+ end
59
+
60
+ def members_from_list(user_list)
61
+ users.where('lower(name) in (?)', user_list.map(&:downcase))
62
+ end
63
+
64
+ def posting_for_anonymous?
65
+ 'anonymous' == posting_permission
66
+ end
67
+
68
+ def posting_for_logged_in?
69
+ 'logged_in' == posting_permission
70
+ end
71
+
72
+ def posting_for_members?
73
+ 'members' == posting_permission
74
+ end
75
+
76
+ def public?
77
+ 'public' == security
78
+ end
79
+
80
+ def restricted_to_logged_in?
81
+ 'logged_in' == security
82
+ end
83
+
84
+ def restricted_to_private?
85
+ 'private' == security
86
+ end
87
+
88
+ def to_param
89
+ name.downcase
90
+ end
91
+ end
92
+ end