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