radiant-forum-extension 1.2.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (144) hide show
  1. data/README.md +37 -56
  2. data/VERSION +1 -1
  3. data/app/controllers/forum_base_controller.rb +78 -0
  4. data/app/controllers/forums_controller.rb +5 -34
  5. data/app/controllers/posts_controller.rb +61 -153
  6. data/app/controllers/topics_controller.rb +6 -63
  7. data/app/helpers/forum_helper.rb +24 -48
  8. data/app/models/forum.rb +5 -18
  9. data/app/models/post.rb +53 -55
  10. data/app/models/post_attachment.rb +9 -6
  11. data/app/models/topic.rb +20 -113
  12. data/app/views/admin/reader_configuration/_edit_forum.html.haml +4 -2
  13. data/app/views/admin/reader_configuration/_forum.html.haml +6 -2
  14. data/app/views/forums/_forum.html.haml +14 -20
  15. data/app/views/forums/_latest.html.haml +12 -0
  16. data/app/views/forums/_standard_parts.html.haml +49 -0
  17. data/app/views/forums/index.html.haml +16 -24
  18. data/app/views/forums/show.html.haml +18 -28
  19. data/app/views/pages/_comment.html.haml +1 -1
  20. data/app/views/posts/_attachments.html.haml +13 -0
  21. data/app/views/posts/_confirm_delete.html.haml +10 -0
  22. data/app/views/posts/_context.html.haml +16 -0
  23. data/app/views/posts/_edit_links.html.haml +11 -0
  24. data/app/views/posts/_form.html.haml +40 -2
  25. data/app/views/posts/_latest.html.haml +10 -17
  26. data/app/views/posts/_new_attachment.html.haml +2 -0
  27. data/app/views/posts/_post.html.haml +25 -60
  28. data/app/views/posts/{_search.html.haml → _search_form.html.haml} +1 -1
  29. data/app/views/posts/_uploader.html.haml +2 -2
  30. data/app/views/posts/edit.html.haml +26 -13
  31. data/app/views/posts/index.html.haml +19 -27
  32. data/app/views/posts/new.html.haml +27 -7
  33. data/app/views/posts/remove.html.haml +27 -0
  34. data/app/views/posts/show.html.haml +20 -7
  35. data/app/views/readers/_forum_messages.html.haml +20 -0
  36. data/app/views/readers/_messages_summary.html.haml +3 -0
  37. data/app/views/topics/_context.html.haml +12 -0
  38. data/app/views/topics/_latest.html.haml +14 -16
  39. data/app/views/topics/_locked.html.haml +1 -1
  40. data/app/views/topics/_replies.html.haml +6 -0
  41. data/app/views/topics/_reply.html.haml +23 -0
  42. data/app/views/topics/_topic.html.haml +14 -43
  43. data/app/views/topics/_voices.html.haml +5 -0
  44. data/app/views/topics/index.html.haml +11 -24
  45. data/app/views/topics/show.html.haml +25 -38
  46. data/config/initializers/radiant_config.rb +5 -1
  47. data/config/locales/en.yml +73 -27
  48. data/config/routes.rb +3 -4
  49. data/db/migrate/20101222160900_page_posts.rb +21 -0
  50. data/db/migrate/20101222163605_no_comment_forum.rb +10 -0
  51. data/db/migrate/20110105103827_topic_merely_associative.rb +23 -0
  52. data/db/migrate/20110111080550_detach_observer.rb +11 -0
  53. data/db/migrate/20110127113852_import_attachments.rb +9 -0
  54. data/forum_extension.rb +12 -13
  55. data/lib/commentable_model.rb +98 -0
  56. data/lib/forum_page.rb +2 -22
  57. data/lib/forum_reader_sessions_controller.rb +14 -0
  58. data/lib/forum_readers_controller.rb +2 -9
  59. data/lib/forum_red_cloth3.rb +23 -4
  60. data/lib/forum_red_cloth4.rb +23 -4
  61. data/lib/forum_tags.rb +298 -194
  62. data/lib/sanitize/config/forum.rb +47 -0
  63. data/public/images/furniture/blank.png +0 -0
  64. data/public/images/furniture/emoticons.png +0 -0
  65. data/public/javascripts/forum.js +349 -93
  66. data/public/javascripts/jquery.tools.min.js +195 -0
  67. data/public/punymce/blank.htm +1 -0
  68. data/public/punymce/css/content.css +4 -0
  69. data/public/punymce/css/editor.css +58 -0
  70. data/public/punymce/i18n/sv.js +28 -0
  71. data/public/punymce/img/icons.gif +0 -0
  72. data/public/punymce/img/icons_uncompressed.png +0 -0
  73. data/public/punymce/plugins/bbcode.js +1 -0
  74. data/public/punymce/plugins/bbcode_src.js +50 -0
  75. data/public/punymce/plugins/editsource/css/editor.css +3 -0
  76. data/public/punymce/plugins/editsource/editsource.js +1 -0
  77. data/public/punymce/plugins/editsource/editsource_src.js +81 -0
  78. data/public/punymce/plugins/editsource/img/icons.gif +0 -0
  79. data/public/punymce/plugins/emoticons/css/content.css +13 -0
  80. data/public/punymce/plugins/emoticons/css/editor.css +17 -0
  81. data/public/punymce/plugins/emoticons/emoticons.js +1 -0
  82. data/public/punymce/plugins/emoticons/emoticons_src.js +303 -0
  83. data/public/punymce/plugins/emoticons/img/emoticons.gif +0 -0
  84. data/public/punymce/plugins/emoticons/img/emoticons.png +0 -0
  85. data/public/punymce/plugins/emoticons/img/trans.gif +0 -0
  86. data/public/punymce/plugins/entities.js +1 -0
  87. data/public/punymce/plugins/entities_src.js +37 -0
  88. data/public/punymce/plugins/forceblocks.js +1 -0
  89. data/public/punymce/plugins/forceblocks_src.js +465 -0
  90. data/public/punymce/plugins/forcenl.js +1 -0
  91. data/public/punymce/plugins/forcenl_src.js +26 -0
  92. data/public/punymce/plugins/image/css/editor.css +1 -0
  93. data/public/punymce/plugins/image/image.js +1 -0
  94. data/public/punymce/plugins/image/image_src.js +30 -0
  95. data/public/punymce/plugins/image/img/icons.gif +0 -0
  96. data/public/punymce/plugins/link/css/editor.css +2 -0
  97. data/public/punymce/plugins/link/img/icons.gif +0 -0
  98. data/public/punymce/plugins/link/link.js +1 -0
  99. data/public/punymce/plugins/link/link_src.js +36 -0
  100. data/public/punymce/plugins/paste.js +1 -0
  101. data/public/punymce/plugins/paste_src.js +169 -0
  102. data/public/punymce/plugins/protect.js +1 -0
  103. data/public/punymce/plugins/protect_src.js +30 -0
  104. data/public/punymce/plugins/safari2x.js +1 -0
  105. data/public/punymce/plugins/safari2x_src.js +284 -0
  106. data/public/punymce/plugins/tabfocus.js +1 -0
  107. data/public/punymce/plugins/tabfocus_src.js +45 -0
  108. data/public/punymce/plugins/textcolor/css/editor.css +7 -0
  109. data/public/punymce/plugins/textcolor/img/icons.gif +0 -0
  110. data/public/punymce/plugins/textcolor/textcolor.js +1 -0
  111. data/public/punymce/plugins/textcolor/textcolor_src.js +73 -0
  112. data/public/punymce/puny_mce.js +1 -0
  113. data/public/punymce/puny_mce_full.js +1 -0
  114. data/public/punymce/puny_mce_src.js +1460 -0
  115. data/public/stylesheets/sass/forum.sass +175 -169
  116. data/radiant-forum-extension.gemspec +81 -19
  117. data/spec/controllers/admin/forums_controller_spec.rb +2 -2
  118. data/spec/controllers/forums_controller_spec.rb +3 -6
  119. data/spec/controllers/posts_controller_spec.rb +76 -59
  120. data/spec/controllers/topics_controller_spec.rb +4 -95
  121. data/spec/datasets/forum_readers_dataset.rb +1 -0
  122. data/spec/datasets/forums_dataset.rb +91 -10
  123. data/spec/lib/commentable_model_spec.rb +88 -0
  124. data/spec/lib/forum_page_spec.rb +2 -34
  125. data/spec/lib/forum_site_spec.rb +2 -3
  126. data/spec/lib/forum_tags_spec.rb +35 -0
  127. data/spec/models/forum_spec.rb +31 -20
  128. data/spec/models/post_spec.rb +40 -39
  129. data/spec/models/topic_spec.rb +29 -71
  130. data/spec/spec_helper.rb +10 -1
  131. metadata +84 -22
  132. data/app/views/posts/_reply.html.haml +0 -35
  133. data/app/views/posts/_upload.html.haml +0 -2
  134. data/app/views/posts/preview.html.haml +0 -32
  135. data/app/views/posts/search.html.haml +0 -63
  136. data/app/views/posts/search.rss.builder +0 -14
  137. data/app/views/topics/_form.html.haml +0 -30
  138. data/app/views/topics/_help.html.haml +0 -8
  139. data/app/views/topics/comments.html.haml +0 -6
  140. data/app/views/topics/edit.html.haml +0 -26
  141. data/app/views/topics/new.html.haml +0 -56
  142. data/spec/datasets/forum_pages_dataset.rb +0 -11
  143. data/spec/datasets/posts_dataset.rb +0 -31
  144. data/spec/datasets/topics_dataset.rb +0 -37
@@ -0,0 +1,98 @@
1
+ module CommentableModel # for inclusion into ActiveRecord::Base
2
+ def self.included(base)
3
+ base.extend ClassMethods
4
+ end
5
+
6
+ module ClassMethods
7
+ def has_comments?
8
+ false
9
+ end
10
+
11
+ def has_comments
12
+ return if has_comments?
13
+ has_many :posts, :order => 'posts.created_at ASC', :dependent => :destroy
14
+ class_eval {
15
+ extend CommentableModel::CommentableClassMethods
16
+ include CommentableModel::CommentableInstanceMethods
17
+ }
18
+ end
19
+ end
20
+
21
+ module CommentableClassMethods
22
+ def has_comments?
23
+ true
24
+ end
25
+ end
26
+
27
+ module CommentableInstanceMethods
28
+ def has_posts?
29
+ posts.any?
30
+ end
31
+
32
+ def has_replies?
33
+ posts.count > 1
34
+ end
35
+
36
+ def replies
37
+ posts.except(posts.first) # double query but therefore still chainable and paginatable
38
+ end
39
+
40
+ def reply_count
41
+ posts.count - 1
42
+ end
43
+
44
+ def voices
45
+ posts.distinct_readers.map(&:reader)
46
+ end
47
+
48
+ def voice_count
49
+ posts.distinct_readers.count #actually counting posts, but the total is the same
50
+ end
51
+
52
+ def other_voices
53
+ voices - [posts.first.reader]
54
+ end
55
+
56
+ def other_voice_count
57
+ voice_count - 1
58
+ end
59
+
60
+ def paged?
61
+ Radiant::Config['forum.paginate_posts?'] && posts.count > posts_per_page
62
+ end
63
+
64
+ def posts_per_page
65
+ (Radiant::Config['forum.posts_per_page'] || 25).to_i
66
+ end
67
+
68
+ def last_page
69
+ (posts.count.to_f/posts_per_page.to_f).ceil.to_i
70
+ end
71
+
72
+ def page_for(post)
73
+ return nil unless post.holder == self
74
+ return 1 unless paged?
75
+ position = posts.index(post) || posts.count + 1 # new post not always present in cached posts collection
76
+ (position.to_f/posts_per_page.to_f).ceil.to_i
77
+ end
78
+
79
+ def notice_destruction_of(post)
80
+ if self.respond_to?(:replied_at) && self.replied_at == post.created_at
81
+ set_last_post(self.posts.except(post).last)
82
+ end
83
+ end
84
+
85
+ def notice_creation_of(post)
86
+ set_last_post(post)
87
+ end
88
+
89
+ def set_last_post(post=nil)
90
+ if post
91
+ self.replied_by = post.reader if self.respond_to? :replied_by
92
+ self.replied_at = post.created_at if self.respond_to? :replied_at
93
+ self.save if self.changed?
94
+ end
95
+ end
96
+
97
+ end
98
+ end
data/lib/forum_page.rb CHANGED
@@ -2,33 +2,13 @@ module ForumPage
2
2
 
3
3
  def self.included(base)
4
4
  base.class_eval {
5
- has_one :topic
5
+ has_comments
6
6
  include InstanceMethods
7
7
  }
8
8
  end
9
9
 
10
10
  module InstanceMethods
11
11
 
12
- def find_or_build_topic
13
- if self.topic
14
- self.topic
15
- elsif self.still_commentable?
16
- self.build_topic(:name => title, :forum => Forum.find_or_create_comments_forum, :reader => Reader.find_or_create_for_user(created_by)) # posts_controller will do the right thing with a new topic
17
- end
18
- end
19
-
20
- def posts
21
- self.topic ? self.topic.posts : []
22
- end
23
-
24
- def has_posts?
25
- self.topic && self.topic.has_posts?
26
- end
27
-
28
- # def cache?
29
- # !has_posts?
30
- # end
31
-
32
12
  def still_commentable?
33
13
  commentable? && !comments_closed? && (!commentable_period || Time.now - self.created_at < commentable_period)
34
14
  end
@@ -40,6 +20,6 @@ module ForumPage
40
20
  def locked?
41
21
  !still_commentable?
42
22
  end
43
-
23
+
44
24
  end
45
25
  end
@@ -0,0 +1,14 @@
1
+ module ForumReaderSessionsController
2
+
3
+ def self.included(base)
4
+ base.class_eval do
5
+ def default_loggedin_url_with_forum
6
+ Rails.logger.warn ">>> default_loggedin_url is going to be #{topics_url}"
7
+ topics_url
8
+ end
9
+ alias_method_chain :default_loggedin_url, :forum
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -1,16 +1,9 @@
1
1
  module ForumReadersController
2
-
3
2
  def self.included(base)
4
3
  base.class_eval do
5
4
  helper :forum
6
- alias_method_chain :show, :forum
5
+ add_show_partial 'readers/forum_messages'
6
+ add_index_partial 'readers/messages_summary'
7
7
  end
8
8
  end
9
-
10
- def show_with_forum
11
- @reader = Reader.find(params[:id])
12
- @posts = Post.paginate_by_reader_id(@reader.id, :page => params[:page], :include => :topic, :order => 'posts.created_at desc') if @reader
13
- render :template => 'readers/show_with_posts'
14
- end
15
-
16
9
  end
@@ -1,10 +1,29 @@
1
1
  module ForumRedCloth3
2
2
 
3
3
  def smilies(text)
4
- %w{angry smile bigsmile confused cool cry devil neutral sad shamed shocked surprised tongue wink }.each do |icon|
5
- imgtag =
6
- text.gsub!(/:#{icon}:/, %{<img src="/images/emoticons/#{icon}.gif" alt="(#{icon})" class="smiley" />})
4
+ emoticons = {
5
+ ':)' => 'happy',
6
+ ':|' => 'neutral',
7
+ ':(' => 'sad',
8
+ ':D' => 'grin',
9
+ ':O' => 'surprised',
10
+ ';)' => 'wink',
11
+ '}:)' => 'devil',
12
+ ':P' => 'tongue',
13
+ ':[' => 'mad',
14
+ '8|' => 'shocked',
15
+ ':@' => 'lol',
16
+ 'B]' => 'cool'
17
+ }
18
+
19
+ # old syntax carried over from vanilla
20
+ text.gsub!(/\:(angry|smile|bigsmile|confused|cool|cry|devil|neutral|sad|shamed|shocked|surprised|tongue|wink)\:/) do |w|
21
+ %{<img src="/images/emoticons/#{$1}.gif" alt="(#{$1})" title="#{$1}" class="smiley" />}
7
22
  end
8
- end
9
23
 
24
+ # these are generally put in by the punymce toolbar, so we use their nasty but effective combination of blank image with sprite background
25
+ text.gsub!(/(\}\:\)|\:\)|\:\||\:\(|\:D|\:O|\;\)|\:P|\:\@|8\||\:\[|B\])/) do |w|
26
+ %{<img src="/images/furniture/blank.png" class="emoticon #{emoticons[w]}" />};
27
+ end
28
+ end
10
29
  end
@@ -12,10 +12,29 @@ module ForumRedCloth4
12
12
  end
13
13
 
14
14
  def smilies(text)
15
- %w{angry smile bigsmile confused cool cry devil neutral sad shamed shocked surprised tongue wink }.each do |icon|
16
- imgtag =
17
- text.gsub!(/:#{icon}:/, %{<img src="/images/emoticons/#{icon}.gif" alt="(#{icon})" title="#{icon}" class="smiley" />})
15
+ emoticons = {
16
+ ':)' => 'happy',
17
+ ':|' => 'neutral',
18
+ ':(' => 'sad',
19
+ ':D' => 'grin',
20
+ ':O' => 'surprised',
21
+ ';)' => 'wink',
22
+ '}:)' => 'devil',
23
+ ':P' => 'tongue',
24
+ ':[' => 'mad',
25
+ '8|' => 'shocked',
26
+ ':@' => 'lol',
27
+ 'B]' => 'cool'
28
+ }
29
+
30
+ # old syntax carried over from vanilla
31
+ text.gsub!(/\:(angry|smile|bigsmile|confused|cool|cry|devil|neutral|sad|shamed|shocked|surprised|tongue|wink)\:/) do |w|
32
+ %{<img src="/images/emoticons/#{$1}.gif" alt="(#{$1})" title="#{$1}" class="smiley" />}
18
33
  end
19
- end
20
34
 
35
+ # these are generally put in by the punymce toolbar, so we use their nasty but effective combination of blank image with sprite background
36
+ text.gsub!(/(\}\:\)|\:\)|\:\||\:\(|\:D|\:O|\;\)|\:P|\:\@|8\||\:\[|B\])/) do |w|
37
+ %{<img src="/images/furniture/blank.png" class="emoticon #{emoticons[w]}" />};
38
+ end
39
+ end
21
40
  end
data/lib/forum_tags.rb CHANGED
@@ -1,284 +1,388 @@
1
1
  module ForumTags
2
2
  include Radiant::Taggable
3
- include ActionView::Helpers::TagHelper
4
- include ActionView::Helpers::FormTagHelper
5
- include ActionView::Helpers::FormOptionsHelper
3
+ include ActionView::Helpers::UrlHelper
4
+ include ActionController::UrlWriter
5
+ include I18n
6
6
 
7
7
  class TagError < StandardError; end
8
8
 
9
- tag 'comments' do |tag|
10
- raise TagError, "can't have comments without a page" unless page = tag.locals.page
11
- if page.commentable?
12
- tag.locals.comments = page.posts
13
- tag.expand
9
+ tag 'forum' do |tag|
10
+ tag.expand
11
+ end
12
+
13
+ tag 'forum:topics' do |tag|
14
+ tag.expand
15
+ end
16
+
17
+ desc %{
18
+ Renders a standard list of recent topics.
19
+ Pass a 'limit' parameter to set the length of the list: default is 10.
20
+
21
+ <pre><code>
22
+ <r:forum:topics:latest limit="5" />
23
+ # is the same as:
24
+ <ul>
25
+ <r:forum:topics:each limit="5">
26
+ <li><r:forum:topic:link /><br /><r:forum:topic:context /></li>
27
+ </r:forum:topics:each>
28
+ </ul>
29
+ </code></pre>
30
+ }
31
+ tag 'forum:topics:latest' do |tag|
32
+ limit = (tag.attr['limit'] || 10).to_i
33
+ output = "<ul>"
34
+ Topic.latest(limit).each do |topic|
35
+ tag.locals.topic = topic
36
+ tag.locals.post = topic.posts.last
37
+ output << tag.render('forum:topic:summary', tag.attr.dup)
14
38
  end
39
+ output << "</ul>"
40
+ output
15
41
  end
16
42
 
17
43
  desc %{
18
- Returns a string in the form "x comments" or "no comments yet".
19
-
20
- *Usage:*
21
- <pre><code><r:comments:summary /></code></pre>
44
+ Loops over the most recently-updated forum topics.
45
+ Supply a `limit` attribute to set the number of topics shown. The default is 10.
46
+ Within the loop you can use all the usual r:forum:topic and r:forum:post tags.
47
+ The post tags will refer to the latest reply (or to the first post if there are no replies).
22
48
  }
23
-
24
- tag 'comments:summary' do |tag|
25
- if tag.locals.comments.empty?
26
- "no comments yet"
27
- elsif tag.locals.comments.size == 1
28
- "one comment"
29
- else
30
- "#{tag.locals.comments.size} comments"
49
+ tag 'forum:topics:each' do |tag|
50
+ output = []
51
+ limit = (tag.attr['limit'] || 10).to_i
52
+ Topic.latest(limit).each do |topic|
53
+ tag.locals.topic = topic
54
+ tag.locals.post = topic.posts.last
55
+ output << tag.expand
31
56
  end
57
+ output
58
+ end
59
+
60
+ tag 'forum:topic' do |tag|
61
+ tag.locals.topic = Topic.find( tag.attr['id'] ) unless tag.attr['id'].blank?
62
+ raise TagError, "can't have forum:topic without a topic" unless tag.locals.topic
63
+ tag.expand
32
64
  end
33
65
 
34
66
  desc %{
35
- Anything between if_comments tags is displayed only - dramatic pause - if there are comments.
36
-
37
- *Usage:*
38
- <pre><code><r:if_comments>...</r:if_comments></code></pre>
67
+ Renders a standard, minimal topic list item consisting of link and explanation.
68
+ This is the shorthand used by forum:topics:latest but it can also be used in other settings.
69
+
70
+ <pre><code>
71
+ <r:forum:topic:summary />
72
+ # is the same as:
73
+ <li><r:forum:topic:link /><br /><r:forum:topic:context /></li>
74
+ </code></pre>
39
75
  }
40
- tag 'if_comments' do |tag|
41
- raise TagError, "can't have if_comments without a page" unless page = tag.locals.page
42
- tag.expand if page.posts.any?
76
+ tag 'forum:topic:summary' do |tag|
77
+ "<li>#{tag.render('forum:topic:link')}<br />#{tag.render('forum:topic:context')}</li>"
43
78
  end
44
79
 
45
80
  desc %{
46
- Anything between unless_comments tags is displayed only if there are no comments.
47
-
48
- *Usage:*
49
- <pre><code><r:unless_comments>...</r:unless_comments></code></pre>
81
+ Renders the url of the current topic.
50
82
  }
51
- tag 'unless_comments' do |tag|
52
- raise TagError, "can't have unless_comments without a page" unless page = tag.locals.page
53
- tag.expand if page.posts.any?
83
+ tag 'forum:topic:url' do |tag|
84
+ forum_topic_path(tag.locals.topic.forum, tag.locals.topic)
54
85
  end
55
-
86
+
56
87
  desc %{
57
- If you want more control over the display of page comments, you can spell them out:
58
-
59
- *Usage:*
60
- <pre><code><r:comments:each>
61
- <h2><r:comment:reader:name /></h2>
62
- <p class="date"><r:comment:date /></p>
63
- <r:comment:body_html />
64
- </r:comments:each>
65
- <r:comment_link />
66
- </code></pre>
88
+ Renders a link to the current topic using its name as the text.
67
89
  }
68
- tag 'comments:each' do |tag|
69
- results = []
70
- tag.locals.comments.each do |post|
71
- tag.locals.comment = post
72
- results << tag.expand
73
- end
74
- results
90
+ tag 'forum:topic:link' do |tag|
91
+ options = tag.attr.dup
92
+ anchor = options['anchor'] ? "##{options.delete('anchor')}" : ''
93
+ attributes = options.inject('') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
94
+ attributes = " #{attributes}" unless attributes.empty?
95
+ text = tag.double? ? tag.expand : tag.render('forum:topic:name')
96
+ %{<a href="#{tag.render('forum:topic:url')}#{anchor}"#{attributes}>#{text}</a>}
97
+ end
98
+ tag 'link' do |tag|
75
99
  end
76
100
 
77
101
  desc %{
78
- To enable page commenting, all you have to do is put this in your layout:
79
-
80
- *Usage:*
81
- <pre><code><r:comments:all /></code></pre>
102
+ Renders a standard gravatar block (as found in the forum pages) for the reader who
103
+ started the current topic.
82
104
  }
83
- tag 'comments:all' do |tag|
84
- posts = tag.locals.comments
85
- results = ""
86
- results << %{<div class="page_comments">}
87
- results << "<h2>Comments</h2>"
88
- results << %{<div id="forum">
89
- }
90
- if posts.empty?
91
- results << "<p>None yet.</p>"
92
- else
93
- posts.each do |post|
94
- tag.locals.comment = post
95
- results << tag.render('comment')
96
- end
97
- end
98
- results << %{#{tag.render('comment_link', 'class' => 'inline inviting')}}
99
- results << "</div></div>"
100
- results
105
+ tag 'forum:topic:gravatar' do |tag|
106
+ %{<div class="speaker">#{standard_gravatar_for(tag.locals.topic.reader)}</div>}
101
107
  end
102
108
 
103
- tag 'comment' do |tag|
104
- raise TagError, "can't have r:comment without a post" unless post = tag.locals.comment
105
- if tag.double?
106
- tag.locals.reader = post.reader
107
- tag.expand
108
- else
109
- %{<div class="post" id="#{post.dom_id}">
110
- <div class="post_header">
111
- <h2><img src="#{post.reader.gravatar_url(:size => 40)}" width="40" height ="40" class="gravatar" /> #{post.reader.name}</h2>
112
- <p class="context">#{friendly_date(post.created_at)}</p>
113
- </div>
114
- <div class="post_body">#{post.body_html}</div>
115
- </div>}
116
- end
109
+ desc %{
110
+ Renders the name of the reader who started the current topic.
111
+ }
112
+ tag 'forum:topic:author' do |tag|
113
+ tag.locals.topic.reader.name
114
+ end
115
+
116
+ desc %{
117
+ Renders the name of the current topic.
118
+ }
119
+ tag 'forum:topic:name' do |tag|
120
+ tag.locals.topic.name
121
+ end
122
+
123
+ desc %{
124
+ Renders the (sanitized and textilized) body of the first post in the current topic.
125
+ }
126
+ tag 'forum:topic:body' do |tag|
127
+ tag.locals.topic.posts.first.body_html
128
+ end
129
+
130
+ desc %{
131
+ Renders the author and date context line for the current topic.
132
+ }
133
+ tag 'forum:topic:context' do |tag|
134
+ output = []
135
+ output << I18n.t('started_by')
136
+ output << %{<a href="#{reader_path(tag.locals.topic.reader)}">#{tag.render('forum:topic:author')}</a>}
137
+ output << tag.render('forum:topic:date')
138
+ output.join(' ')
117
139
  end
118
140
 
119
- tag 'comment:reader' do |tag|
120
- raise TagError, "can't have comment:reader without a comment" unless reader = tag.locals.reader
141
+ desc %{
142
+ Renders the creation date of the current topic in a friendly, colloquial form.
143
+ }
144
+ tag 'forum:topic:date' do |tag|
145
+ I18n.l tag.locals.topic.created_at, :format => :standard
146
+ end
147
+
148
+ tag 'forum:posts' do |tag|
121
149
  tag.expand
122
150
  end
123
151
 
124
152
  desc %{
125
- The name of the commenter
153
+ Loops over the posts most recently added. In effect this is very similar to calling
154
+ r:topics:each, but there are some differences:
155
+
156
+ * page comments and any other non-topic posts are included
157
+ * here r:post tags always refer to the current post.
158
+ Within the topics:each loop they always refer to the last reply to that topic.
159
+ * tag.locals.page is set if the foreground post is a page comment, so you can use
160
+ all the usual radius tags for that page.
161
+
162
+ Supply a `limit` attribute to set the number of posts shown. The default is 10.
126
163
  }
127
- tag 'comment:reader:name' do |tag|
128
- tag.locals.reader.name
164
+ tag 'forum:posts:each' do |tag|
165
+ results = []
166
+ limit = (tag.attr['limit'] || 10).to_i
167
+ Post.latest(limit).each do |post|
168
+ tag.locals.post = post
169
+ tag.locals.topic = post.topic
170
+ tag.locals.page = post.page
171
+ results << tag.expand
172
+ end
173
+ results
174
+ end
175
+
176
+ desc %{
177
+ This tag is generally used in double form or as a silent prefix, where it will just expand:
178
+
179
+ <pre><code>
180
+ <r:forum:post><r:gravatar /> <r:link /></r:forum:post>
181
+ # or just
182
+ <r:forum:post:link />
183
+ </code></pre>
184
+
185
+ But if used in single form it will return a standard, minimal post list item
186
+ consisting of link and explanation:
187
+
188
+ <pre><code>
189
+ <r:forum:post />
190
+ # is the same as:
191
+ <li><r:forum:post:link /><br /><r:forum:post:context /></li>
192
+ </code></pre>
193
+
194
+ Note that the text of the link will be the name of the topic or page to which this post is attached,
195
+ and that within a topics:each loop any r:forum:post tags will show the last post to the topic.
196
+ }
197
+ tag 'forum:post' do |tag|
198
+ tag.locals.post = Post.find(tag.attr['id']) unless tag.attr['id'].blank?
199
+ raise TagError, "can't have forum:post without a post" unless tag.locals.post
200
+ tag.expand if tag.locals.post
129
201
  end
130
202
 
131
203
  desc %{
132
- A gravatar for the commenter
204
+ Renders a url (with pagination and anchor) for the current post. Within a topics:each loop this
205
+ is a way to link to the last post.
133
206
  }
134
- tag 'comment:reader:gravatar' do |tag|
135
- tag.locals.reader.gravatar
207
+ tag 'forum:post:url' do |tag|
208
+ paginated_post_path(tag.locals.post)
136
209
  end
137
210
 
138
211
  desc %{
139
- The date of the comment
212
+ Renders a title that can be used over the post: this will be the name of its page or topic.
140
213
  }
141
- tag 'comment:date' do |tag|
142
- tag.locals.comment.created_at.to_s(:html_date)
214
+ tag 'forum:post:name' do |tag|
215
+ tag.locals.post.holder.title
143
216
  end
144
217
 
145
218
  desc %{
146
- The time_ago of the comment
219
+ Renders a link to the current post. The link text will be the page or topic title and within that
220
+ the destination of the link will be the page and anchor for this post.
147
221
  }
148
- tag 'comment:ago' do |tag|
149
- time_ago_in_words(tag.locals.comment.created_at)
222
+ tag 'forum:post:link' do |tag|
223
+ link_to tag.render('forum:post:name'), tag.render('forum:post:url')
150
224
  end
151
225
 
152
226
  desc %{
153
- The body of the comment as it was entered (but html-escaped)
227
+ Renders a standard gravatar block (as in the forum pages) for the author of this post.
154
228
  }
155
- tag 'comment:body' do |tag|
156
- h(tag.locals.comment.body)
229
+ tag 'forum:post:gravatar' do |tag|
230
+ %{<div class="speaker">#{standard_gravatar_for(tag.locals.post.reader)}</div>}
157
231
  end
158
232
 
159
233
  desc %{
160
- The body of the comment rendered into html (and whitelisted, so this ought to be safe)
234
+ Renders the name of the author of this post.
161
235
  }
162
- tag 'comment:body_html' do |tag|
163
- tag.locals.comment.body_html
236
+ tag 'forum:post:author' do |tag|
237
+ tag.locals.post.reader.name
164
238
  end
165
239
 
166
240
  desc %{
167
- A link to the post-a-comment form. Typically you'll use a bit of remote scripting to replace this with
168
- a comment or login form depending on whether a reader is detected, but you can just leave the link too.
169
-
170
- If text is given, the link will be wrapped around it. The default is just "Add a comment". Any supplied
171
- attributes are passed through, so you can specify class, id and anything else you like.
241
+ Renders the (sanitized and textilized) body of the current post.
242
+ }
243
+ tag 'forum:post:body' do |tag|
244
+ tag.locals.post.body_html
245
+ end
172
246
 
173
- *Usage:*
174
- <pre><code>
175
- <r:if_comments>
176
- <r:comment_link />
177
- </r:if_comments>
178
- <r:unless_comments>
179
- <r:comment_link class="how_exciting">Be the first to add a comment!</r:comment_link>
180
- </r:unless_comments>
181
- </code></pre>
247
+ desc %{
248
+ Renders a description line for the current post, which is usually something like
249
+ 'comment added by', 'new reply from' or 'new topic begun by' followed by the author's
250
+ name and the colloquial form of the creation date.
182
251
  }
252
+ tag 'forum:post:context' do |tag|
253
+ output = []
254
+ post = tag.locals.post
255
+ if post.page
256
+ output << I18n.t('new_comment_from')
257
+ elsif post.first?
258
+ output << I18n.t('new_reply_from')
259
+ else
260
+ output << I18n.t('new_topic_from')
261
+ end
262
+ output << %{<a href="#{reader_path(tag.locals.post.reader)}">#{tag.render('forum:post:author')}</a>}
263
+ output << tag.render('forum:post:date')
264
+ output.join(' ')
265
+ end
266
+
267
+ desc %{
268
+ Renders the creation date of the current post
269
+ }
270
+ tag 'forum:post:date' do |tag|
271
+ I18n.l tag.locals.post.created_at, :format => :standard
272
+ end
273
+
274
+ # page comments are just a special case of posts that have a page but not topic
275
+ # there is the difference that we generally want to display the whole set
276
+ # and the added complication that they should be paginated.
277
+ # but we only need to define some more collection and summary tags and set the post collection appropriately
278
+
279
+ desc %{
280
+ The address for add-a-comment links
281
+ }
282
+ tag 'comment_url' do |tag|
283
+ new_page_post_path(tag.locals.page)
284
+ end
285
+
183
286
  tag 'comment_link' do |tag|
184
- raise TagError, "can't have `r:comment_link' without a page." unless tag.locals.page
185
287
  options = tag.attr.dup
186
- options['class'] ||= 'newmessage'
187
288
  attributes = options.inject('') { |s, (k, v)| s << %{#{k.to_s.downcase}="#{v}" } }.strip
188
289
  attributes = " #{attributes}" unless attributes.empty?
189
- text = tag.double? ? tag.expand : "Add a comment"
290
+ text = tag.double? ? tag.expand : I18n.t("Add a comment")
190
291
  %{<a href="#{tag.render('comment_url')}"#{attributes}>#{text}</a>}
191
292
  end
192
293
 
193
294
  desc %{
194
- The address for add-a-comment links
295
+ Anything between if_comments tags is displayed only - dramatic pause - if there are comments.
195
296
  }
196
- tag 'comment_url' do |tag|
197
- raise TagError, "can't have `r:comment_url' without a page." unless tag.locals.page
198
- new_page_post_url(tag.locals.page)
297
+ tag 'if_comments' do |tag|
298
+ raise TagError, "can't have if_comments without a page" unless page = tag.locals.page
299
+ tag.expand if page.posts.any?
199
300
  end
200
301
 
201
302
  desc %{
202
- Shows the standard block of recent discussion activity.
303
+ Anything between unless_comments tags is displayed only if there are no comments.
304
+
305
+ *Usage:*
306
+ <pre><code><r:unless_comments>...</r:unless_comments></code></pre>
203
307
  }
204
- tag 'forum_latest' do |tag|
205
- results = []
206
- results << %{<ul class="clean">}
207
- Topic.visible.latest(6).each do |topic|
208
- tag.locals.topic = topic
209
- results << %{<li>#{tag.render('topic:summary')}</li>}
308
+ tag 'unless_comments' do |tag|
309
+ raise TagError, "can't have unless_comments without a page" unless page = tag.locals.page
310
+ tag.expand unless page.posts.any?
311
+ end
312
+
313
+ tag 'comments' do |tag|
314
+ raise TagError, "can't have comments without a page" unless page = tag.locals.page
315
+ if page.commentable?
316
+ output = []
317
+ tag.locals.posts = tag.locals.paginated_list = page.posts.paginate
318
+ tag.expand
210
319
  end
211
- results << %{</ul>}
212
- results
213
320
  end
214
321
 
215
322
  desc %{
216
- Shows the standard forum search form in a reasonably compact and stylable way.
217
-
218
- Takes options with_title (set to false to omit the usual heading), by_forum (set to true to show a discussion category dropdown) and by_reader (set to true to show a message-from dropdown) and label (set to the title you would like to display over the main search field).
323
+ Renders string (internationalised) like "1 comment", "27 comments" or "no comments yet".
219
324
  }
220
- tag 'forum_search' do |tag|
221
- results = []
222
- compact = true unless tag.attr['by_forum'] == 'true' || tag.attr['by_reader'] == 'true'
223
- q_label = tag.attr['label']
224
- q_label = "Look for this text" if q_label.blank? && !compact
225
- results << %{<form class="friendly" action="#{search_posts_url}">}
226
- results << %{<h2>Forum Search</h2>} unless tag.attr['with_title'] == 'false'
227
- results << %{<p>}
228
- results << %{<label for="q">#{q_label}</label><br />} unless q_label.blank?
229
- results << %{#{text_field_tag("q", params[:q], :class => 'standard')}}
230
- results << %{#{submit_tag "search", :class => 'button'}} if compact
231
- results << %{</p>}
232
- unless compact
233
- results << %{<p><label for="reader_id">From this person</label><br /><select name="reader_id"><option value="">anyone</option>#{options_from_collection_for_select(Reader.all, "id", "name")}</select></p>} if tag.attr['by_reader'] == 'true'
234
- results << %{<p><label for="forum_id">In this discussion category</label><br /><select name="forum_id"><option value="">anywhere</option>#{options_from_collection_for_select(Forum.visible, "id", "name")}</select></p>} if tag.attr['by_forum'] == 'true'
235
- results << %{<p class="buttons">#{submit_tag "search", :class => 'button'}</p>}
236
- end
237
- results << %{</form>}
238
- results
325
+ tag 'comments:summary' do |tag|
326
+ I18n.t("comment_count", :count => tag.locals.posts.total_entries)
239
327
  end
240
328
 
241
- tag 'topic' do |tag|
242
- tag.expand if tag.locals.topic
243
- end
244
- tag 'topic:summary' do |tag|
329
+ desc %{
330
+ Loops over the (paginated) comment set in ascending order of date. Within the loop you can
331
+ use the r:comment shorthand or any r:forum:post:* tags. Note that r:forum:topic tags won't
332
+ work: there is no topic to show.
333
+ }
334
+ tag 'comments:each' do |tag|
245
335
  results = []
246
- topic = tag.locals.topic
247
- post = topic.last_post
248
- results << %{<img src="#{post.reader.gravatar_url(:size => 42)}" width="42" height="42" class="gravatar"> } if tag.attr['gravatar'] == 'true'
249
- results << %{<a href="#{forum_topic_path(topic.forum, topic)}">#{topic.name}</a> }
250
- results << %{<span class="credit">}
251
- if topic.page
252
- results << " commented upon by "
253
- elsif post.first?
254
- results << " started by "
255
- else
256
- results << " replied to by "
336
+ tag.locals.posts.each do |post|
337
+ tag.locals.post = post
338
+ results << tag.expand
257
339
  end
258
- results << "#{post.reader.name} #{friendly_date(post.created_at)}"
259
- results << %{</span>}
340
+ results << tag.render('pagination', tag.attr.dup)
260
341
  results
261
342
  end
262
343
 
344
+ desc %{
345
+ A useful shortcut: To enable page commenting, all you have to do is put this in your layout:
263
346
 
347
+ <pre><code><r:comments:all /></code></pre>
348
+
349
+ It will display a (paginated) list of page comments followed by an 'add comment' link that you
350
+ can hook into using the supplied forum javascript or your own equivalent.
351
+ }
352
+ tag 'comments:all' do |tag|
353
+ posts = tag.locals.posts
354
+ results = ""
355
+ results << %{<div class="page_comments">}
356
+ results << %{<p class="context">#{tag.render('comments:summary')}</p>}
357
+ posts.each do |post|
358
+ tag.locals.post = post
359
+ results << tag.render('comment')
360
+ end
361
+ results << %{<p class="add_comment">#{tag.render('comment_link', 'class' => 'remote')}</p>}
362
+ results << tag.render('pagination', tag.attr.dup)
363
+ results << "</div>"
364
+ results
365
+ end
264
366
 
265
- private
266
-
267
- # copied from forum_helper
268
-
269
- def friendly_date(datetime)
270
- if datetime
271
- date = datetime.to_date
272
- if (date == Date.today)
273
- format = "today at %l:%M%p"
274
- elsif (date == Date.yesterday)
275
- format = "yesterday at %l:%M%p"
276
- elsif (date.year == Date.today.year)
277
- format = "on %B %e"
278
- else
279
- format = "on %B %e, %Y"
280
- end
281
- datetime.strftime(format)
367
+ desc %{
368
+ A useful shortcut that renders an entire post - in much the same way as a post would appear
369
+ in the forum - so that it can be displayed as a comment on the page.
370
+ }
371
+ tag 'comment' do |tag|
372
+ raise TagError, "can't have r:comment without a post" unless post = tag.locals.post
373
+ if tag.double?
374
+ tag.locals.reader = post.reader
375
+ tag.expand
376
+ else
377
+ output = %{<div class="post"><div class="post_wrapper">}
378
+ output << tag.render("forum:post:gravatar")
379
+ output << %{<div class="post_header">}
380
+ output << %{<h2>#{tag.render("forum:post:reader")}</h2>}
381
+ output << %{<p class="context">#{tag.render("forum:post:context")}</p>}
382
+ output << %{</div>}
383
+ output << %{<div class="post_body">}
384
+ output << tag.render("forum:post:body")
385
+ output
282
386
  end
283
387
  end
284
388