Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit.

  • Loading branch information...
commit 6491737d58177e7af0d299316047a5639e25ad52 0 parents
wbharding authored
Showing with 9,562 additions and 0 deletions.
  1. +8 −0 README
  2. +10 −0 Rakefile
  3. +5 −0 app/controllers/application.rb
  4. +64 −0 app/controllers/forums_controller.rb
  5. +10 −0 app/controllers/moderators_controller.rb
  6. +22 −0 app/controllers/monitorships_controller.rb
  7. +134 −0 app/controllers/posts_controller.rb
  8. +110 −0 app/controllers/topics_controller.rb
  9. +96 −0 app/helpers/application_helper.rb
  10. +15 −0 app/helpers/forums_helper.rb
  11. +2 −0  app/helpers/moderators_helper.rb
  12. +2 −0  app/helpers/monitorships_helper.rb
  13. +2 −0  app/helpers/posts_helper.rb
  14. +2 −0  app/helpers/topics_helper.rb
  15. +26 −0 app/models/forum.rb
  16. +6 −0 app/models/moderatorship.rb
  17. +5 −0 app/models/monitorship.rb
  18. +9 −0 app/models/monitorships_sweeper.rb
  19. +33 −0 app/models/post.rb
  20. +12 −0 app/models/posts_sweeper.rb
  21. +94 −0 app/models/topic.rb
  22. +21 −0 app/views/forums/_form.html.erb
  23. +12 −0 app/views/forums/edit.html.erb
  24. +75 −0 app/views/forums/index.html.erb
  25. +10 −0 app/views/forums/new.html.erb
  26. +95 −0 app/views/forums/show.html.erb
  27. +35 −0 app/views/layouts/_head.html.erb
  28. +9 −0 app/views/layouts/_post.rss.builder
  29. +29 −0 app/views/layouts/application.html.erb
  30. +4 −0 app/views/monitorships/create.js.rjs
  31. +4 −0 app/views/monitorships/destroy.js.rjs
  32. +38 −0 app/views/posts/_edit.html.erb
  33. +14 −0 app/views/posts/edit.html.erb
  34. +6 −0 app/views/posts/edit.js.rjs
  35. +54 −0 app/views/posts/index.html.erb
  36. +20 −0 app/views/posts/index.rss.builder
  37. +56 −0 app/views/posts/monitored.html.erb
  38. +15 −0 app/views/posts/monitored.rss.builder
  39. +3 −0  app/views/posts/update.js.rjs
  40. +28 −0 app/views/topics/_form.html.erb
  41. +10 −0 app/views/topics/edit.html.erb
  42. +2 −0  app/views/topics/index.html.erb
  43. +18 −0 app/views/topics/new.html.erb
  44. +185 −0 app/views/topics/show.html.erb
  45. +16 −0 app/views/topics/show.rss.builder
  46. +73 −0 db/migrate/001_create_savage_tables.rb
  47. +79 −0 init.rb
  48. +201 −0 lang/en.yml
  49. +52 −0 lib/savage_beast/authentication_system.rb
  50. +79 −0 lib/savage_beast/user_init.rb
  51. +90 −0 lib/tasks/capistrano.rake
  52. +44 −0 lib/tasks/deploy_edge.rake
  53. +712 −0 po/beast.pot
  54. +700 −0 po/nl/beast.po
  55. +40 −0 public/.htaccess
  56. +52 −0 public/404.html
  57. +52 −0 public/500.html
  58. +10 −0 public/dispatch.cgi
  59. +24 −0 public/dispatch.fcgi
  60. +10 −0 public/dispatch.rb
  61. 0  public/favicon.ico
  62. +12 −0 public/images/clearbits/_readme.txt
  63. BIN  public/images/clearbits/add.gif
  64. BIN  public/images/clearbits/addressbook.gif
  65. BIN  public/images/clearbits/alert.gif
  66. BIN  public/images/clearbits/apple.gif
  67. BIN  public/images/clearbits/arrow1_e.gif
  68. BIN  public/images/clearbits/arrow1_n.gif
  69. BIN  public/images/clearbits/arrow1_ne.gif
  70. BIN  public/images/clearbits/arrow1_nw.gif
  71. BIN  public/images/clearbits/arrow1_s.gif
  72. BIN  public/images/clearbits/arrow1_se.gif
  73. BIN  public/images/clearbits/arrow1_sw.gif
  74. BIN  public/images/clearbits/arrow1_w.gif
  75. BIN  public/images/clearbits/arrow2_e.gif
  76. BIN  public/images/clearbits/arrow2_n.gif
  77. BIN  public/images/clearbits/arrow2_ne.gif
  78. BIN  public/images/clearbits/arrow2_nw.gif
  79. BIN  public/images/clearbits/arrow2_s.gif
  80. BIN  public/images/clearbits/arrow2_se.gif
  81. BIN  public/images/clearbits/arrow2_sw.gif
  82. BIN  public/images/clearbits/arrow2_w.gif
  83. BIN  public/images/clearbits/arrow3_e.gif
  84. BIN  public/images/clearbits/arrow3_n.gif
  85. BIN  public/images/clearbits/arrow3_ne.gif
  86. BIN  public/images/clearbits/arrow3_nw.gif
  87. BIN  public/images/clearbits/arrow3_s.gif
  88. BIN  public/images/clearbits/arrow3_se.gif
  89. BIN  public/images/clearbits/arrow3_sw.gif
  90. BIN  public/images/clearbits/arrow3_w.gif
  91. BIN  public/images/clearbits/ascii.gif
  92. BIN  public/images/clearbits/back.gif
  93. BIN  public/images/clearbits/bg_blank.gif
  94. BIN  public/images/clearbits/bg_circle.gif
  95. BIN  public/images/clearbits/bg_rounded.gif
  96. BIN  public/images/clearbits/bg_rounded_ne.gif
  97. BIN  public/images/clearbits/bg_rounded_nw.gif
  98. BIN  public/images/clearbits/bg_rounded_se.gif
  99. BIN  public/images/clearbits/bg_rounded_sw.gif
  100. BIN  public/images/clearbits/bigsmile.gif
  101. BIN  public/images/clearbits/binary.gif
  102. BIN  public/images/clearbits/blah.gif
  103. BIN  public/images/clearbits/bstop.gif
  104. BIN  public/images/clearbits/buy.gif
  105. BIN  public/images/clearbits/calday.gif
  106. BIN  public/images/clearbits/calendar.gif
  107. BIN  public/images/clearbits/camera.gif
  108. BIN  public/images/clearbits/cart.gif
  109. BIN  public/images/clearbits/cd.gif
  110. BIN  public/images/clearbits/cellphone.gif
  111. BIN  public/images/clearbits/chat.gif
  112. BIN  public/images/clearbits/check.gif
  113. BIN  public/images/clearbits/close.gif
  114. BIN  public/images/clearbits/comment.gif
  115. BIN  public/images/clearbits/cube.gif
  116. BIN  public/images/clearbits/day.gif
  117. BIN  public/images/clearbits/denied.gif
  118. BIN  public/images/clearbits/document.gif
  119. BIN  public/images/clearbits/download.gif
  120. BIN  public/images/clearbits/edit.gif
  121. BIN  public/images/clearbits/eject.gif
  122. BIN  public/images/clearbits/equalizer.gif
  123. BIN  public/images/clearbits/first.gif
  124. BIN  public/images/clearbits/flag.gif
  125. BIN  public/images/clearbits/flash.gif
  126. BIN  public/images/clearbits/folder.gif
  127. BIN  public/images/clearbits/forward.gif
  128. BIN  public/images/clearbits/frown.gif
  129. BIN  public/images/clearbits/ftp.gif
  130. BIN  public/images/clearbits/graph.gif
  131. BIN  public/images/clearbits/heart.gif
  132. BIN  public/images/clearbits/home.gif
  133. BIN  public/images/clearbits/html.gif
  134. BIN  public/images/clearbits/ipod.gif
  135. BIN  public/images/clearbits/last.gif
  136. BIN  public/images/clearbits/lock.gif
  137. BIN  public/images/clearbits/loop.gif
  138. BIN  public/images/clearbits/mail.gif
  139. BIN  public/images/clearbits/man.gif
  140. BIN  public/images/clearbits/manman.gif
  141. BIN  public/images/clearbits/music.gif
  142. BIN  public/images/clearbits/mute.gif
  143. BIN  public/images/clearbits/mute_centered.gif
  144. BIN  public/images/clearbits/newwindow.gif
  145. BIN  public/images/clearbits/next.gif
  146. BIN  public/images/clearbits/night.gif
  147. BIN  public/images/clearbits/open.gif
  148. BIN  public/images/clearbits/pause.gif
  149. BIN  public/images/clearbits/phone.gif
  150. BIN  public/images/clearbits/play.gif
  151. BIN  public/images/clearbits/previous.gif
  152. BIN  public/images/clearbits/quicktime.gif
  153. BIN  public/images/clearbits/redo.gif
  154. BIN  public/images/clearbits/reload.gif
  155. BIN  public/images/clearbits/sad.gif
  156. BIN  public/images/clearbits/save.gif
  157. BIN  public/images/clearbits/scream.gif
  158. BIN  public/images/clearbits/search.gif
  159. BIN  public/images/clearbits/seconds.gif
  160. BIN  public/images/clearbits/smile.gif
  161. BIN  public/images/clearbits/smirk.gif
  162. BIN  public/images/clearbits/star.gif
  163. BIN  public/images/clearbits/stop.gif
  164. BIN  public/images/clearbits/subtract.gif
  165. BIN  public/images/clearbits/switch.gif
  166. BIN  public/images/clearbits/target.gif
  167. BIN  public/images/clearbits/tcp.gif
  168. BIN  public/images/clearbits/time.gif
  169. BIN  public/images/clearbits/toggle.gif
  170. BIN  public/images/clearbits/tongue.gif
  171. BIN  public/images/clearbits/tools.gif
  172. BIN  public/images/clearbits/trackback.gif
  173. BIN  public/images/clearbits/trash.gif
  174. BIN  public/images/clearbits/tv.gif
  175. BIN  public/images/clearbits/type.gif
  176. BIN  public/images/clearbits/undo.gif
  177. BIN  public/images/clearbits/unlock.gif
  178. BIN  public/images/clearbits/upload.gif
  179. BIN  public/images/clearbits/user.gif
  180. BIN  public/images/clearbits/video.gif
  181. BIN  public/images/clearbits/volume_high.gif
  182. BIN  public/images/clearbits/volume_low.gif
  183. BIN  public/images/clearbits/wifi.gif
  184. BIN  public/images/clearbits/window.gif
  185. BIN  public/images/clearbits/woman.gif
  186. BIN  public/images/clearbits/womanman.gif
  187. BIN  public/images/clearbits/work.gif
  188. BIN  public/images/clearbits/zoomin.gif
  189. BIN  public/images/clearbits/zoomout.gif
  190. BIN  public/images/feed-icon.png
  191. BIN  public/images/rails.png
  192. BIN  public/images/reply_background.png
  193. BIN  public/images/small_circle.gif
  194. BIN  public/images/spinner.gif
  195. BIN  public/images/spinner_black.gif
  196. BIN  public/images/spinner_bounce.gif
  197. +82 −0 public/javascripts/application.js
  198. +15 −0 public/open_search.xml
  199. +1 −0  public/robots.txt
  200. +941 −0 public/stylesheets/display.css
  201. +18 −0 routes.rb
  202. +23 −0 tested_plugins/acts_as_list/README
  203. +3 −0  tested_plugins/acts_as_list/init.rb
  204. +256 −0 tested_plugins/acts_as_list/lib/active_record/acts/list.rb
  205. +332 −0 tested_plugins/acts_as_list/test/list_test.rb
  206. +1 −0  tested_plugins/engines
  207. +18 −0 tested_plugins/gibberish/LICENSE
  208. +118 −0 tested_plugins/gibberish/README
  209. +14 −0 tested_plugins/gibberish/Rakefile
  210. +3 −0  tested_plugins/gibberish/init.rb
  211. +3 −0  tested_plugins/gibberish/lang/es.yml
  212. +3 −0  tested_plugins/gibberish/lang/fr.yml
  213. +8 −0 tested_plugins/gibberish/lib/gibberish.rb
  214. +88 −0 tested_plugins/gibberish/lib/gibberish/localize.rb
  215. +17 −0 tested_plugins/gibberish/lib/gibberish/string_ext.rb
  216. +203 −0 tested_plugins/gibberish/test/gibberish_test.rb
  217. +1 −0  tested_plugins/gibberish/test/lang/es.yml
  218. +1 −0  tested_plugins/gibberish/test/lang/fr.yml
  219. +110 −0 tested_plugins/mislav-will_paginate/CHANGELOG.rdoc
  220. +18 −0 tested_plugins/mislav-will_paginate/LICENSE
  221. +107 −0 tested_plugins/mislav-will_paginate/README.rdoc
  222. +53 −0 tested_plugins/mislav-will_paginate/Rakefile
  223. BIN  tested_plugins/mislav-will_paginate/examples/apple-circle.gif
  224. +69 −0 tested_plugins/mislav-will_paginate/examples/index.haml
  225. +92 −0 tested_plugins/mislav-will_paginate/examples/index.html
  226. +90 −0 tested_plugins/mislav-will_paginate/examples/pagination.css
  227. +91 −0 tested_plugins/mislav-will_paginate/examples/pagination.sass
  228. +1 −0  tested_plugins/mislav-will_paginate/init.rb
  229. +78 −0 tested_plugins/mislav-will_paginate/lib/will_paginate.rb
  230. +16 −0 tested_plugins/mislav-will_paginate/lib/will_paginate/array.rb
  231. +146 −0 tested_plugins/mislav-will_paginate/lib/will_paginate/collection.rb
  232. +32 −0 tested_plugins/mislav-will_paginate/lib/will_paginate/core_ext.rb
  233. +264 −0 tested_plugins/mislav-will_paginate/lib/will_paginate/finder.rb
  234. +170 −0 tested_plugins/mislav-will_paginate/lib/will_paginate/named_scope.rb
  235. +37 −0 tested_plugins/mislav-will_paginate/lib/will_paginate/named_scope_patch.rb
  236. +9 −0 tested_plugins/mislav-will_paginate/lib/will_paginate/version.rb
  237. +402 −0 tested_plugins/mislav-will_paginate/lib/will_paginate/view_helpers.rb
  238. +21 −0 tested_plugins/mislav-will_paginate/test/boot.rb
  239. +143 −0 tested_plugins/mislav-will_paginate/test/collection_test.rb
  240. +8 −0 tested_plugins/mislav-will_paginate/test/console
  241. +22 −0 tested_plugins/mislav-will_paginate/test/database.yml
  242. +476 −0 tested_plugins/mislav-will_paginate/test/finder_test.rb
  243. +3 −0  tested_plugins/mislav-will_paginate/test/fixtures/admin.rb
  244. +14 −0 tested_plugins/mislav-will_paginate/test/fixtures/developer.rb
  245. +13 −0 tested_plugins/mislav-will_paginate/test/fixtures/developers_projects.yml
  246. +15 −0 tested_plugins/mislav-will_paginate/test/fixtures/project.rb
  247. +6 −0 tested_plugins/mislav-will_paginate/test/fixtures/projects.yml
  248. +29 −0 tested_plugins/mislav-will_paginate/test/fixtures/replies.yml
  249. +7 −0 tested_plugins/mislav-will_paginate/test/fixtures/reply.rb
  250. +38 −0 tested_plugins/mislav-will_paginate/test/fixtures/schema.rb
  251. +10 −0 tested_plugins/mislav-will_paginate/test/fixtures/topic.rb
  252. +30 −0 tested_plugins/mislav-will_paginate/test/fixtures/topics.yml
  253. +2 −0  tested_plugins/mislav-will_paginate/test/fixtures/user.rb
  254. +35 −0 tested_plugins/mislav-will_paginate/test/fixtures/users.yml
  255. +40 −0 tested_plugins/mislav-will_paginate/test/helper.rb
  256. +43 −0 tested_plugins/mislav-will_paginate/test/lib/activerecord_test_case.rb
  257. +75 −0 tested_plugins/mislav-will_paginate/test/lib/activerecord_test_connector.rb
  258. +11 −0 tested_plugins/mislav-will_paginate/test/lib/load_fixtures.rb
  259. +178 −0 tested_plugins/mislav-will_paginate/test/lib/view_test_process.rb
  260. +59 −0 tested_plugins/mislav-will_paginate/test/tasks.rake
  261. +365 −0 tested_plugins/mislav-will_paginate/test/view_test.rb
  262. +20 −0 tested_plugins/mislav-will_paginate/will_paginate.gemspec
  263. +29 −0 tested_plugins/white_list/README
  264. +22 −0 tested_plugins/white_list/Rakefile
  265. +2 −0  tested_plugins/white_list/init.rb
  266. +97 −0 tested_plugins/white_list/lib/white_list_helper.rb
  267. +132 −0 tested_plugins/white_list/test/white_list_test.rb
  268. +27 −0 tested_plugins/white_list_formatted_content/init.rb
8 README
@@ -0,0 +1,8 @@
+See http://www.williambharding.com/blog/?p=100 for install instructions
+See http://code.google.com/p/savage-beast-2/ to grab the latest version
+
+Please post any comments or suggestions to the blog post. If you would like
+to make a contribution to improve this plugin, send me an email via the site.
+
+And be sure you carefully follow the Engines installation instructions! That
+line to add to your environment file is easily missed.
10 Rakefile
@@ -0,0 +1,10 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
5 app/controllers/application.rb
@@ -0,0 +1,5 @@
+class ApplicationController < ActionController::Base
+ # Commented by WBH for Savage Beast... unnecessary? This looks like what was needed prior to Gibberish
+ # init_gettext "beast" if Object.const_defined?(:GetText)
+
+end
64 app/controllers/forums_controller.rb
@@ -0,0 +1,64 @@
+class ForumsController < ApplicationController
+ before_filter :login_required, :except => [:index, :show]
+ before_filter :find_or_initialize_forum, :except => :index
+ before_filter :admin?, :except => [:show, :index]
+
+ cache_sweeper :posts_sweeper, :only => [:create, :update, :destroy]
+
+ def index
+ @forums = Forum.find_ordered
+ # reset the page of each forum we have visited when we go back to index
+ session[:forum_page] = nil
+ respond_to do |format|
+ format.html
+ format.xml { render :xml => @forums }
+ end
+ end
+
+ def show
+ respond_to do |format|
+ format.html do
+ # keep track of when we last viewed this forum for activity indicators
+ (session[:forums] ||= {})[@forum.id] = Time.now.utc if logged_in?
+ (session[:forum_page] ||= Hash.new(1))[@forum.id] = params[:page].to_i if params[:page]
+
+ @topics = @forum.topics.paginate :page => params[:page]
+ User.find(:all, :conditions => ['id IN (?)', @topics.collect { |t| t.replied_by }.uniq]) unless @topics.blank?
+ end
+ format.xml { render :xml => @forum }
+ end
+ end
+
+ # new renders new.html.erb
+ def create
+ @forum.attributes = params[:forum]
+ @forum.save!
+ respond_to do |format|
+ format.html { redirect_to @forum }
+ format.xml { head :created, :location => formatted_forum_url(@forum, :xml) }
+ end
+ end
+
+ def update
+ @forum.update_attributes!(params[:forum])
+ respond_to do |format|
+ format.html { redirect_to @forum }
+ format.xml { head 200 }
+ end
+ end
+
+ def destroy
+ @forum.destroy
+ respond_to do |format|
+ format.html { redirect_to forums_path }
+ format.xml { head 200 }
+ end
+ end
+
+ protected
+ def find_or_initialize_forum
+ @forum = params[:id] ? Forum.find(params[:id]) : Forum.new
+ end
+
+ alias authorized? admin?
+end
10 app/controllers/moderators_controller.rb
@@ -0,0 +1,10 @@
+class ModeratorsController < ApplicationController
+ before_filter :login_required
+
+ def destroy
+ Moderatorship.delete_all ['id = ?', params[:id]]
+ redirect_to user_path(params[:user_id])
+ end
+
+ alias authorized? admin?
+end
22 app/controllers/monitorships_controller.rb
@@ -0,0 +1,22 @@
+class MonitorshipsController < ApplicationController
+ before_filter :login_required
+
+ cache_sweeper :monitorships_sweeper, :only => [:create, :destroy]
+
+ def create
+ @monitorship = Monitorship.find_or_initialize_by_user_id_and_topic_id(current_user.id, params[:topic_id])
+ @monitorship.update_attribute :active, true
+ respond_to do |format|
+ format.html { redirect_to forum_topic_path(params[:forum_id], params[:topic_id]) }
+ format.js
+ end
+ end
+
+ def destroy
+ Monitorship.update_all ['active = ?', false], ['user_id = ? and topic_id = ?', current_user.id, params[:topic_id]]
+ respond_to do |format|
+ format.html { redirect_to forum_topic_path(params[:forum_id], params[:topic_id]) }
+ format.js
+ end
+ end
+end
134 app/controllers/posts_controller.rb
@@ -0,0 +1,134 @@
+class PostsController < ApplicationController
+ before_filter :find_post, :except => [:index, :create, :monitored, :search]
+ before_filter :login_required, :except => [:index, :monitored, :search, :show]
+ @@query_options = { :select => "#{Post.table_name}.*, #{Topic.table_name}.title as topic_title, #{Forum.table_name}.name as forum_name", :joins => "inner join #{Topic.table_name} on #{Post.table_name}.topic_id = #{Topic.table_name}.id inner join #{Forum.table_name} on #{Topic.table_name}.forum_id = #{Forum.table_name}.id" }
+
+ # @WBH@ TODO: This uses the caches_formatted_page method. In the main Beast project, this is implemented via a Config/Initializer file. Not
+ # sure what analogous place to put it in this plugin. It don't work in the init.rb
+ #caches_formatted_page :rss, :index, :monitored
+ cache_sweeper :posts_sweeper, :only => [:create, :update, :destroy]
+
+ def index
+ conditions = []
+ [:user_id, :forum_id, :topic_id].each { |attr| conditions << Post.send(:sanitize_sql, ["#{Post.table_name}.#{attr} = ?", params[attr]]) if params[attr] }
+ conditions = conditions.empty? ? nil : conditions.collect { |c| "(#{c})" }.join(' AND ')
+ @posts = Post.paginate @@query_options.merge(:conditions => conditions, :page => params[:page], :count => {:select => "#{Post.table_name}.id"}, :order => post_order)
+ @users = User.find(:all, :select => 'distinct *', :conditions => ['id in (?)', @posts.collect(&:user_id).uniq]).index_by(&:id)
+ render_posts_or_xml
+ end
+
+ def search
+ conditions = params[:q].blank? ? nil : Post.send(:sanitize_sql, ["LOWER(#{Post.table_name}.body) LIKE ?", "%#{params[:q]}%"])
+ @posts = Post.paginate @@query_options.merge(:conditions => conditions, :page => params[:page], :count => {:select => "#{Post.table_name}.id"}, :order => post_order)
+ @users = User.find(:all, :select => 'distinct *', :conditions => ['id in (?)', @posts.collect(&:user_id).uniq]).index_by(&:id)
+ render_posts_or_xml :index
+ end
+
+ def monitored
+ @user = User.find params[:user_id]
+ options = @@query_options.merge(:conditions => ["#{Monitorship.table_name}.user_id = ? and #{Post.table_name}.user_id != ? and #{Monitorship.table_name}.active = ?", params[:user_id], @user.id, true])
+ options[:order] = post_order
+ options[:joins] += " inner join #{Monitorship.table_name} on #{Monitorship.table_name}.topic_id = #{Topic.table_name}.id"
+ options[:page] = params[:page]
+ options[:count] = {:select => "#{Post.table_name}.id"}
+ @posts = Post.paginate options
+ render_posts_or_xml
+ end
+
+ def show
+ respond_to do |format|
+ format.html { redirect_to forum_topic_path(@post.forum_id, @post.topic_id) }
+ format.xml { render :xml => @post.to_xml }
+ end
+ end
+
+ def create
+ @topic = Topic.find_by_id_and_forum_id(params[:topic_id],params[:forum_id])
+ if @topic.locked?
+ respond_to do |format|
+ format.html do
+ flash[:notice] = 'This topic is locked.'[:locked_topic]
+ redirect_to(forum_topic_path(:forum_id => params[:forum_id], :id => params[:topic_id]))
+ end
+ format.xml do
+ render :text => 'This topic is locked.'[:locked_topic], :status => 400
+ end
+ end
+ return
+ end
+ @forum = @topic.forum
+ @post = @topic.posts.build(params[:post])
+ @post.user = current_user
+ @post.save!
+ respond_to do |format|
+ format.html do
+ redirect_to forum_topic_path(:forum_id => params[:forum_id], :id => params[:topic_id], :anchor => @post.dom_id, :page => params[:page] || '1')
+ end
+ format.xml { head :created, :location => formatted_post_url(:forum_id => params[:forum_id], :topic_id => params[:topic_id], :id => @post, :format => :xml) }
+ end
+ rescue ActiveRecord::RecordInvalid
+ flash[:bad_reply] = 'Please post something at least...'[:post_something_message]
+ respond_to do |format|
+ format.html do
+ redirect_to forum_topic_path(:forum_id => params[:forum_id], :id => params[:topic_id], :anchor => 'reply-form', :page => params[:page] || '1')
+ end
+ format.xml { render :xml => @post.errors.to_xml, :status => 400 }
+ end
+ end
+
+ def edit
+ respond_to do |format|
+ format.html
+ format.js
+ end
+ end
+
+ def update
+ @post.attributes = params[:post]
+ @post.save!
+ rescue ActiveRecord::RecordInvalid
+ flash[:bad_reply] = 'An error occurred'[:error_occured_message]
+ ensure
+ respond_to do |format|
+ format.html do
+ redirect_to forum_topic_path(:forum_id => params[:forum_id], :id => params[:topic_id], :anchor => @post.dom_id, :page => params[:page] || '1')
+ end
+ format.js
+ format.xml { head 200 }
+ end
+ end
+
+ def destroy
+ @post.destroy
+ flash[:notice] = "Post of '{title}' was deleted."[:post_deleted_message, @post.topic.title]
+ respond_to do |format|
+ format.html do
+ redirect_to(@post.topic.frozen? ?
+ forum_path(params[:forum_id]) :
+ forum_topic_path(:forum_id => params[:forum_id], :id => params[:topic_id], :page => params[:page]))
+ end
+ format.xml { head 200 }
+ end
+ end
+
+ protected
+ def authorized?
+ action_name == 'create' || @post.editable_by?(current_user)
+ end
+
+ def post_order
+ "#{Post.table_name}.created_at#{params[:forum_id] && params[:topic_id] ? nil : " desc"}"
+ end
+
+ def find_post
+ @post = Post.find_by_id_and_topic_id_and_forum_id(params[:id], params[:topic_id], params[:forum_id]) || raise(ActiveRecord::RecordNotFound)
+ end
+
+ def render_posts_or_xml(template_name = action_name)
+ respond_to do |format|
+ format.html { render :action => template_name }
+ format.rss { render :action => template_name, :layout => false }
+ format.xml { render :xml => @posts.to_xml }
+ end
+ end
+end
110 app/controllers/topics_controller.rb
@@ -0,0 +1,110 @@
+class TopicsController < ApplicationController
+ before_filter :find_forum_and_topic, :except => :index
+ before_filter :login_required, :only => [:new, :create, :edit, :update, :destroy]
+
+ # @WBH@ TODO: This uses the caches_formatted_page method. In the main Beast project, this is implemented via a Config/Initializer file. Not
+ # sure what analogous place to put it in this plugin. It don't work in the init.rb
+ #caches_formatted_page :rss, :show
+ cache_sweeper :posts_sweeper, :only => [:create, :update, :destroy]
+
+ def index
+ respond_to do |format|
+ format.html { redirect_to forum_path(params[:forum_id]) }
+ format.xml do
+ @topics = Topic.paginate_by_forum_id(params[:forum_id], :order => 'sticky desc, replied_at desc', :page => params[:page])
+ render :xml => @topics.to_xml
+ end
+ end
+ end
+
+ def new
+ @topic = Topic.new
+ end
+
+ def show
+ respond_to do |format|
+ format.html do
+ # see notes in application.rb on how this works
+ update_last_seen_at
+ # keep track of when we last viewed this topic for activity indicators
+ (session[:topics] ||= {})[@topic.id] = Time.now.utc if logged_in?
+ # authors of topics don't get counted towards total hits
+ @topic.hit! unless logged_in? and @topic.user == current_user
+ @posts = @topic.posts.paginate :page => params[:page]
+ User.find(:all, :conditions => ['id IN (?)', @posts.collect { |p| p.user_id }.uniq]) unless @posts.blank?
+ @post = Post.new
+ end
+ format.xml do
+ render :xml => @topic.to_xml
+ end
+ format.rss do
+ @posts = @topic.posts.find(:all, :order => 'created_at desc', :limit => 25)
+ render :action => 'show', :layout => false
+ end
+ end
+ end
+
+ def create
+ topic_saved, post_saved = false, false
+ # this is icky - move the topic/first post workings into the topic model?
+ Topic.transaction do
+ @topic = @forum.topics.build(params[:topic])
+ assign_protected
+ @post = @topic.posts.build(params[:topic])
+ @post.topic = @topic
+ @post.user = current_user
+ # only save topic if post is valid so in the view topic will be a new record if there was an error
+ @topic.body = @post.body # incase save fails and we go back to the form
+ topic_saved = @topic.save if @post.valid?
+ post_saved = @post.save
+ end
+
+ if topic_saved && post_saved
+ respond_to do |format|
+ format.html { redirect_to forum_topic_path(@forum, @topic) }
+ format.xml { head :created, :location => formatted_topic_url(:forum_id => @forum, :id => @topic, :format => :xml) }
+ end
+ else
+ render :action => "new"
+ end
+ end
+
+ def update
+ @topic.attributes = params[:topic]
+ assign_protected
+ @topic.save!
+ respond_to do |format|
+ format.html { redirect_to forum_topic_path(@forum, @topic) }
+ format.xml { head 200 }
+ end
+ end
+
+ def destroy
+ @topic.destroy
+ flash[:notice] = "Topic '{title}' was deleted."[:topic_deleted_message, @topic.title]
+ respond_to do |format|
+ format.html { redirect_to forum_path(@forum) }
+ format.xml { head 200 }
+ end
+ end
+
+ protected
+ def assign_protected
+ @topic.user = current_user if @topic.new_record?
+ # admins and moderators can sticky and lock topics
+ return unless admin? or current_user.moderator_of?(@topic.forum)
+ @topic.sticky, @topic.locked = params[:topic][:sticky], params[:topic][:locked]
+ # only admins can move
+ return unless admin?
+ @topic.forum_id = params[:topic][:forum_id] if params[:topic][:forum_id]
+ end
+
+ def find_forum_and_topic
+ @forum = Forum.find(params[:forum_id])
+ @topic = @forum.topics.find(params[:id]) if params[:id]
+ end
+
+ def authorized?
+ %w(new create).include?(action_name) || @topic.editable_by?(current_user)
+ end
+end
96 app/helpers/application_helper.rb
@@ -0,0 +1,96 @@
+require 'md5'
+
+module ApplicationHelper
+ # convenient plugin point
+ def head_extras
+ end
+
+ def submit_tag(value = "Save Changes"[], options={} )
+ or_option = options.delete(:or)
+ return super + "<span class='button_or'>"+"or"[]+" " + or_option + "</span>" if or_option
+ super
+ end
+
+ def ajax_spinner_for(id, spinner="spinner.gif")
+ "<img src='/plugin_assets/savage_beast/images/#{spinner}' style='display:none; vertical-align:middle;' id='#{id.to_s}_spinner'> "
+ end
+
+ def avatar_for(user, size=32)
+ image_tag "http://www.gravatar.com/avatar.php?gravatar_id=#{MD5.md5(user.email)}&rating=PG&size=#{size}", :size => "#{size}x#{size}", :class => 'photo'
+ end
+
+ def beast_user_name
+ (current_user ? current_user.display_name : "Guest" )
+ end
+
+ def beast_user_link
+ user_link = (current_user ? user_path(current_user) : "#")
+ link_to beast_user_name, user_link
+ end
+
+ def feed_icon_tag(title, url)
+ (@feed_icons ||= []) << { :url => url, :title => title }
+ link_to image_tag('feed-icon.png', :size => '14x14', :style => 'margin-right:5px', :alt => "Subscribe to #{title}", :plugin => "savage_beast"), url
+ end
+
+ def search_posts_title
+ returning(params[:q].blank? ? 'Recent Posts'[] : "Searching for"[] + " '#{h params[:q]}'") do |title|
+ title << " "+'by {user}'[:by_user,h(User.find(params[:user_id]).display_name)] if params[:user_id]
+ title << " "+'in {forum}'[:in_forum,h(Forum.find(params[:forum_id]).name)] if params[:forum_id]
+ end
+ end
+
+ def topic_title_link(topic, options)
+ if topic.title =~ /^\[([^\]]{1,15})\]((\s+)\w+.*)/
+ "<span class='flag'>#{$1}</span>" +
+ link_to(h($2.strip), forum_topic_path(@forum, topic), options)
+ else
+ link_to(h(topic.title), forum_topic_path(@forum, topic), options)
+ end
+ end
+
+ def search_posts_path(rss = false)
+ options = params[:q].blank? ? {} : {:q => params[:q]}
+ prefix = rss ? 'formatted_' : ''
+ options[:format] = 'rss' if rss
+ [[:user, :user_id], [:forum, :forum_id]].each do |(route_key, param_key)|
+ return send("#{prefix}#{route_key}_posts_path", options.update(param_key => params[param_key])) if params[param_key]
+ end
+ options[:q] ? search_all_posts_path(options) : send("#{prefix}all_posts_path", options)
+ end
+
+ # on windows and this isn't working like you expect?
+ # check: http://beast.caboo.se/forums/1/topics/657
+ # strftime on windows doesn't seem to support %e and you'll need to
+ # use the less cool %d in the strftime line below
+ def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
+ from_time = from_time.to_time if from_time.respond_to?(:to_time)
+ to_time = to_time.to_time if to_time.respond_to?(:to_time)
+ distance_in_minutes = (((to_time - from_time).abs)/60).round
+
+ case distance_in_minutes
+ when 0..1 then (distance_in_minutes==0) ? 'a few seconds ago'[] : '1 minute ago'[]
+ when 2..59 then "{minutes} minutes ago"[:minutes_ago, distance_in_minutes]
+ when 60..90 then "1 hour ago"[]
+ when 90..1440 then "{hours} hours ago"[:hours_ago, (distance_in_minutes.to_f / 60.0).round]
+ when 1440..2160 then '1 day ago'[] # 1 day to 1.5 days
+ when 2160..2880 then "{days} days ago"[:days_ago, (distance_in_minutes.to_f / 1440.0).round] # 1.5 days to 2 days
+ else from_time.strftime("%b %e, %Y %l:%M%p"[:datetime_format]).gsub(/([AP]M)/) { |x| x.downcase }
+ end
+ end
+
+ def pagination collection
+ if collection.page_count > 1
+ "<p class='pages'>" + 'Pages'[:pages_title] + ": <strong>" +
+ will_paginate(collection, :inner_window => 10, :next_label => "next"[], :prev_label => "previous"[]) +
+ "</strong></p>"
+ end
+ end
+
+ def next_page collection
+ unless collection.current_page == collection.page_count or collection.page_count == 0
+ "<p style='float:right;'>" + link_to("Next page"[], { :page => collection.current_page.next }.merge(params.reject{|k,v| k=="page"})) + "</p>"
+ end
+ end
+
+end
15 app/helpers/forums_helper.rb
@@ -0,0 +1,15 @@
+module ForumsHelper
+
+ # used to know if a topic has changed since we read it last
+ def recent_topic_activity(topic)
+ return false if not logged_in?
+ return topic.replied_at > ((session[:topics] && session[:topics][topic.id]) || 3.days.ago) # was: last_active. TODO: Could implement something to look at the user
+ end
+
+ # used to know if a forum has changed since we read it last
+ def recent_forum_activity(forum)
+ return false unless logged_in? && forum.recent_topic
+ return forum.recent_topic.replied_at > ((session[:forums] && session[:forums][forum.id]) || 3.days.ago) # was: last_active. TODO: Could implement something to look at the user
+ end
+
+end
2  app/helpers/moderators_helper.rb
@@ -0,0 +1,2 @@
+module ModeratorsHelper
+end
2  app/helpers/monitorships_helper.rb
@@ -0,0 +1,2 @@
+module MonitorshipsHelper
+end
2  app/helpers/posts_helper.rb
@@ -0,0 +1,2 @@
+module PostsHelper
+end
2  app/helpers/topics_helper.rb
@@ -0,0 +1,2 @@
+module TopicsHelper
+end
26 app/models/forum.rb
@@ -0,0 +1,26 @@
+class Forum < ActiveRecord::Base
+ acts_as_list
+
+ validates_presence_of :name
+
+ has_many :moderatorships, :dependent => :delete_all
+ has_many :moderators, :through => :moderatorships, :source => :user
+
+ has_many :topics, :order => 'sticky desc, replied_at desc', :dependent => :delete_all
+ has_one :recent_topic, :class_name => 'Topic', :order => 'sticky desc, replied_at desc'
+
+ # this is used to see if a forum is "fresh"... we can't use topics because it puts
+ # stickies first even if they are not the most recently modified
+ has_many :recent_topics, :class_name => 'Topic', :order => 'replied_at DESC'
+ has_one :recent_topic, :class_name => 'Topic', :order => 'replied_at DESC'
+
+ has_many :posts, :order => "#{Post.table_name}.created_at DESC", :dependent => :delete_all
+ has_one :recent_post, :order => "#{Post.table_name}.created_at DESC", :class_name => 'Post'
+
+ format_attribute :description
+
+ # retrieves forums ordered by position
+ def self.find_ordered(options = {})
+ find :all, options.update(:order => 'position')
+ end
+end
6 app/models/moderatorship.rb
@@ -0,0 +1,6 @@
+class Moderatorship < ActiveRecord::Base
+ belongs_to :forum
+ belongs_to :user
+ before_create { |r| count(:id, :conditions => ['forum_id = ? and user_id = ?', r.forum_id, r.user_id]).zero? }
+
+end
5 app/models/monitorship.rb
@@ -0,0 +1,5 @@
+class Monitorship < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :topic
+
+end
9 app/models/monitorships_sweeper.rb
@@ -0,0 +1,9 @@
+class MonitorshipsSweeper < ActionController::Caching::Sweeper
+ observe Monitorship
+
+ def after_save(monitorship)
+ FileUtils.rm_rf File.join(RAILS_ROOT, 'public', 'users', monitorship.user_id.to_s)
+ end
+
+ alias_method :after_destroy, :after_save
+end
33 app/models/post.rb
@@ -0,0 +1,33 @@
+class Post < ActiveRecord::Base
+ def self.per_page() 25 end
+
+ belongs_to :forum
+ belongs_to :user
+ belongs_to :topic
+
+ format_attribute :body
+ before_create { |r| r.forum_id = r.topic.forum_id }
+ after_create :update_cached_fields
+ after_destroy :update_cached_fields
+
+ validates_presence_of :user_id, :body, :topic
+ attr_accessible :body
+
+ def editable_by?(user)
+ user && (user.id == user_id || user.admin? || user.moderator_of?(forum_id))
+ end
+
+ def to_xml(options = {})
+ options[:except] ||= []
+ options[:except] << :topic_title << :forum_name
+ super
+ end
+
+ protected
+ # using count isn't ideal but it gives us correct caches each time
+ def update_cached_fields
+ Forum.update_all ['posts_count = ?', Post.count(:id, :conditions => {:forum_id => forum_id})], ['id = ?', forum_id]
+ User.update_posts_count(user_id)
+ topic.update_cached_post_fields(self)
+ end
+end
12 app/models/posts_sweeper.rb
@@ -0,0 +1,12 @@
+class PostsSweeper < ActionController::Caching::Sweeper
+ observe Post
+
+ def after_save(post)
+ FileUtils.rm_rf File.join(RAILS_ROOT, 'public', 'forums', post.forum_id.to_s, 'posts.rss')
+ FileUtils.rm_rf File.join(RAILS_ROOT, 'public', 'forums', post.forum_id.to_s, 'topics', "#{post.topic_id}.rss")
+ FileUtils.rm_rf File.join(RAILS_ROOT, 'public', 'users')
+ FileUtils.rm_rf File.join(RAILS_ROOT, 'public', 'posts.rss')
+ end
+
+ alias_method :after_destroy, :after_save
+end
94 app/models/topic.rb
@@ -0,0 +1,94 @@
+class Topic < ActiveRecord::Base
+ validates_presence_of :forum, :user, :title
+ before_create :set_default_replied_at_and_sticky
+ before_update :check_for_changing_forums
+ after_save :update_forum_counter_cache
+ before_destroy :update_post_user_counts
+ after_destroy :update_forum_counter_cache
+
+ belongs_to :forum
+ belongs_to :user
+ belongs_to :last_post, :class_name => "Post", :foreign_key => 'last_post_id'
+ has_many :monitorships
+ has_many :monitors, :through => :monitorships, :conditions => ["#{Monitorship.table_name}.active = ?", true], :source => :user
+
+ has_many :posts, :order => "#{Post.table_name}.created_at", :dependent => :delete_all
+ has_one :recent_post, :order => "#{Post.table_name}.created_at DESC", :class_name => 'Post'
+
+ has_many :voices, :through => :posts, :source => :user, :uniq => true
+ belongs_to :replied_by_user, :foreign_key => "replied_by", :class_name => "User"
+
+ attr_accessible :title
+ # to help with the create form
+ attr_accessor :body
+
+ def hit!
+ self.class.increment_counter :hits, id
+ end
+
+ def sticky?() sticky == 1 end
+
+ def views() hits end
+
+ def paged?() posts_count > Post.per_page end
+
+ def last_page
+ [(posts_count.to_f / Post.per_page).ceil.to_i, 1].max
+ end
+
+ def editable_by?(user)
+ user && (user.id == user_id || user.admin? || user.moderator_of?(forum_id))
+ end
+
+ def update_cached_post_fields(post)
+ # these fields are not accessible to mass assignment
+ remaining_post = post.frozen? ? recent_post : post
+ if remaining_post
+ self.class.update_all(['replied_at = ?, replied_by = ?, last_post_id = ?, posts_count = ?',
+ remaining_post.created_at, remaining_post.user_id, remaining_post.id, posts.count], ['id = ?', id])
+ else
+ self.destroy
+ end
+ end
+
+ protected
+ def set_default_replied_at_and_sticky
+ self.replied_at = Time.now.utc
+ self.sticky ||= 0
+ end
+
+ def set_post_forum_id
+ Post.update_all ['forum_id = ?', forum_id], ['topic_id = ?', id]
+ end
+
+ def check_for_changing_forums
+ old = Topic.find(id)
+ @old_forum_id = old.forum_id if old.forum_id != forum_id
+ true
+ end
+
+ # using count isn't ideal but it gives us correct caches each time
+ def update_forum_counter_cache
+ forum_conditions = ['topics_count = ?', Topic.count(:id, :conditions => {:forum_id => forum_id})]
+ # if the topic moved forums
+ if !frozen? && @old_forum_id && @old_forum_id != forum_id
+ set_post_forum_id
+ Forum.update_all ['topics_count = ?, posts_count = ?',
+ Topic.count(:id, :conditions => {:forum_id => @old_forum_id}),
+ Post.count(:id, :conditions => {:forum_id => @old_forum_id})], ['id = ?', @old_forum_id]
+ end
+ # if the topic moved forums or was deleted
+ if frozen? || (@old_forum_id && @old_forum_id != forum_id)
+ forum_conditions.first << ", posts_count = ?"
+ forum_conditions << Post.count(:id, :conditions => {:forum_id => forum_id})
+ end
+ # User doesn't have update_posts_count method in SB2, as reported by Ryan
+ #@voices.each &:update_posts_count if @voices
+ Forum.update_all forum_conditions, ['id = ?', forum_id]
+ @old_forum_id = @voices = nil
+ end
+
+ def update_post_user_counts
+ @voices = voices.to_a
+ end
+end
21 app/views/forums/_form.html.erb
@@ -0,0 +1,21 @@
+<p style="float:right; margin-top:0">
+</p>
+
+<p id="forum_name">
+
+<table border="0" cellspacing="0" cellpadding="0" class="noborder nopad wide">
+ <td>
+ <label><%= 'Title'[:title_title] %></label><br />
+ <%= form.text_field :name, :class => "primary" %>
+ </td>
+ <td style="text-align:right">
+ <label><%= 'Position'[:position_title] %></label><br />
+ <%= form.text_field :position, :size => 5 %>
+
+ </td>
+</table>
+
+</p>
+<p id="forum_descripion">
+<label><%= 'Description'[:description_title] %></label><br />
+<%= form.text_area :description, :rows => 7 %></p>
12 app/views/forums/edit.html.erb
@@ -0,0 +1,12 @@
+<div class="crumbs">
+ <%= link_to 'Forums'[:forums_title], forums_path %> <span class="arrow">&rarr;</span>
+</div>
+
+<h1><%= 'Edit Forum'[:edit_forum] %></h1>
+
+<% form_for :forum,
+ :url => forum_path(@forum),
+ :html => { :method => :put } do |f| -%>
+<%= render :partial => "form", :object => f %>
+<%= submit_tag 'Save Forum'[:save_forum] -%> or <%= link_to('Cancel'[:cancel], forums_path) %>
+<% end -%>
75 app/views/forums/index.html.erb
@@ -0,0 +1,75 @@
+<% content_for :right do %>
+
+<% if admin? %>
+<h6><%= 'Admin'[:admin_title] %></h6>
+<p><%= link_to 'Create New Forum'[:create_new_forum], new_forum_path, :class => "utility" %></p>
+<% end %>
+
+<% end %>
+
+<h1 style="margin-top:0;"><%= 'Forums'[:forums_title] %></h1>
+<p class="subtitle">
+<%= feed_icon_tag "Recent Posts"[:recent_posts], formatted_posts_path(:format => 'rss') %>
+<%= '{count} topic(s)'[(count=Topic.count)==1 ? :topic_count : :topics_count, number_with_delimiter(count)] %>,
+<%= '{count} post(s)'[(count=Post.count)==1 ? :post_count : :posts_count, number_with_delimiter(count)] %>, <%= '{count} voice(s)'[(count=User.count(:conditions => "posts_count > 0"))==1 ? :voice_count : :voices_count, number_with_delimiter(count)] %>
+
+</p>
+
+<table border="0" cellspacing="0" cellpadding="0" class="wide forums">
+ <tr>
+ <th class="la" width="70%" colspan="3"><%= 'Forum'[:forum_title] %></th>
+<!--
+ <th width="5%">Topics</th>
+ <th width="5%">Posts</th>
+-->
+ <th class="la" width="30%" colspan="1"><%= 'Last Post'[:last_post] %></th>
+ </tr>
+<% for forum in @forums do %>
+ <tr>
+ <td class="vat c1">
+
+ <% if recent_forum_activity(forum) %>
+ <%= image_tag "clearbits/comment.gif", :class => "icon green", :title => 'Recent activity'[:recent_activity], :plugin => "savage_beast" %>
+ <% else %>
+ <%= image_tag "clearbits/comment.gif", :class => "icon grey", :title => 'No recent activity'[:no_recent_activity], :plugin => "savage_beast" %>
+ <% end %>
+ </td>
+ <td class="c2 vat">
+ <%= link_to h(forum.name), forum_path(forum), :class => "title" %>
+ <div class="posts">
+ <%= '{count} topics'[(count=forum.topics.size)==1 ? :topic_count : :topics_count, number_with_delimiter(count)] %>,
+ <%= '{count} posts'[(count=forum.posts.size)==1 ? :post_count : :posts_count, number_with_delimiter(count)] %>
+ </div>
+ <p class="desc"><%= forum.description_html %>
+ </p>
+ </td>
+ <td class="c3">
+ <%= link_to 'Edit'[:edit_title], edit_forum_path(forum), :class => "tiny", :rel => "directory", :style => "float:right" if admin? %>
+ </td>
+
+ <td class="inv lp">
+ <% if forum.recent_post %>
+ <%= time_ago_in_words(forum.recent_post.created_at) %><br />
+ <%= 'by {user}'[:by_user,"<strong>#{h(forum.recent_post.user.display_name)}</strong>"] %>
+ <span>(<%= link_to 'view'[], forum_topic_path(:forum_id => forum, :id => forum.recent_post.topic_id, :page => forum.recent_post.topic.last_page, :anchor => forum.recent_post.dom_id) %>)</span>
+ <% end %>
+ </td>
+ </tr>
+<% end %>
+</table>
+
+<p>
+ <%= link_to 'Recent posts'[:recent_posts], all_posts_path %>
+</p>
+
+<% online_users = User.currently_online -%>
+<% unless !online_users || online_users.empty? %>
+<div class="stats">
+<div class="users">
+<% unless !online_users || online_users.empty? %>
+<%= 'Users online:'[:users_online] %> <%= online_users.map { |u| link_to "<strong>#{h u.display_name}</strong>", user_path(u) } * ", " %><br />
+<% end %>
+</div>
+</div>
+<% end %>
+
10 app/views/forums/new.html.erb
@@ -0,0 +1,10 @@
+<div class="crumbs">
+ <%= link_to "Forums"[:forums_title], forums_path %> <span class="arrow">&rarr;</span>
+</div>
+
+<h1><%= 'New Forum'[:new_forum] %></h1>
+
+<% form_for :forum, :url => forums_path do |f| -%>
+<%= render :partial => "form", :object => f %>
+<%= submit_tag 'Create'[:Create] -%> or <%= link_to('Cancel'[:cancel], forums_path) -%>
+<% end -%>
95 app/views/forums/show.html.erb
@@ -0,0 +1,95 @@
+<% content_for :right do %>
+
+<% unless @forum.description.blank? %>
+<%= @forum.description_html %>
+<hr />
+<% end %>
+
+<h5 style="margin-bottom:1.0em;"><%= 'Moderators'[:moderators] %></h5>
+
+<% unless @forum.moderators.empty? -%>
+<ul class="flat" style="margin-top:1em;">
+<% @forum.moderators.each do |user| -%>
+ <li><%= link_to user.display_name, user_path(user) %></li>
+<% end -%>
+</ul>
+<% else -%>
+<p><%= 'This forum is currently unmoderated.'[:forum_is_unmoderated] %></p>
+<p><%= 'Please always be courteous.'[:please_be_courteous] %></p>
+<% end -%>
+
+<% end %>
+
+<% @page_title = @forum.name %>
+
+<div class="crumbs">
+<%= link_to 'Forums'[:forums_title], forums_path %> <span class="arrow">&rarr;</span>
+</div>
+<h1 style="margin-top:0.5em">
+ <%= h @forum.name %>
+</h1>
+
+<p class="subtitle">
+ <%= feed_icon_tag @forum.name, formatted_forum_posts_path(@forum, :rss) %>
+ <%= '{count} topic(s)'[(count=@forum.topics.size)==1 ? :topic_count : :topics_count, number_with_delimiter(count)] %>,
+ <%= '{count} post(s)'[(count=@forum.posts.size)==1 ? :post_count : :posts_count, number_with_delimiter(count)] %>
+</p>
+
+<% if @topics.page_count > 1 -%>
+<% if logged_in? %>
+<p style="float:right; margin-top:0;"><%= link_to 'New topic'[], new_forum_topic_path(@forum), :class => "utility" %></p>
+<% end %>
+<%= pagination @topics %>
+<% end -%>
+
+<table border="0" cellspacing="0" cellpadding="0" class="wide topics">
+<tr>
+ <th class="la" colspan="2"><%= 'Topic'[:topic_title] %></th>
+ <th width="1%"><%= 'Posts'[:posts_title] %></th>
+ <th width="1%"><%= 'Views'[:views_title] %></th>
+ <th class="la"><%= 'Last post'[:last_post] %></th>
+</tr>
+<% for topic in @topics %>
+<tr class="hentry">
+ <td style="padding:5px; width:16px;" class="c1">
+ <%
+ icon = "comment"
+ color = ""
+ if topic.locked?
+ icon = "lock"
+ post = ", this topic is locked."[:comma_locked_topic]
+ color = "darkgrey"
+ end
+ %>
+ <% if recent_topic_activity(topic) %>
+ <%= image_tag "clearbits/#{icon}.gif", :class => "icon green", :title => "Recent activity"[]+"#{post}", :plugin => "savage_beast" %>
+ <% else %>
+ <%= image_tag "clearbits/#{icon}.gif", :class => "icon grey #{color}", :title => "No recent activity"[]+"#{post}", :plugin => "savage_beast" %>
+ <% end %>
+ </td>
+ <td class="c2">
+ <%= "Sticky"[:sticky_title]+": <strong>" if topic.sticky? %>
+ <%= topic_title_link (topic), :class => "entry-title", :rel => "bookmark" %>
+ <%#= link_to h(topic.title), topic_path(@forum, topic), :class => "entry-title", :rel => "bookmark" %>
+ <%= "</strong>" if topic.sticky? %>
+ <% if topic.paged? -%>
+ <small><%= link_to 'last'[], forum_topic_path(:forum_id => @forum, :id => topic, :page => topic.last_page) %></small>
+ <% end -%>
+ </td>
+ <td class="ca inv stat"><%= topic.posts.size %></td>
+ <td class="ca inv stat"><%= number_with_delimiter(topic.views) %></td>
+ <td class="lp">
+ <abbr class="updated" title="<%= topic.replied_at.xmlschema %>"><%= time_ago_in_words(topic.replied_at) %></abbr>
+ <%= 'by {user}'[:by_user, "<span class=\"author\"><strong class=\"fn\">#{h(topic.replied_by_user.display_name)}</strong></span>"] %>
+ <span><%= link_to 'view'[], forum_topic_path(:forum_id => @forum, :id => topic, :page => topic.last_page, :anchor => "posts-#{topic.last_post_id}") %></span>
+ </td>
+</tr>
+<% end %>
+</table>
+
+<%= next_page @topics %>
+<%= pagination @topics %>
+
+<% if logged_in? %>
+<p><%= link_to 'New topic'[:new_topic], new_forum_topic_path(@forum), :class => "utility" %></p>
+<% end%>
35 app/views/layouts/_head.html.erb
@@ -0,0 +1,35 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <title><%= "#{h @page_title} - " if @page_title %><%= 'Beast'[:beast_title] %></title>
+ <%= stylesheet_link_tag 'display', :plugin => 'savage_beast' %>
+ <%= javascript_include_tag "lowpro", "application", :cache => 'beast', :plugin => 'savage_beast' %>
+ <%# Assumption: If you're a rails project, you already have your own version of these, and don't need the SB version %>
+ <%= javascript_include_tag "prototype", "effects" %>
+<% unless @feed_icons.blank? -%>
+ <% @feed_icons.each do |feed| -%>
+ <%= auto_discovery_link_tag :rss, feed[:url], :title => "Subscribe to '#{feed[:title]}'" %>
+ <% end -%>
+<% end -%>
+ <%= head_extras %>
+ <link rel="search" type="application/opensearchdescription+xml" href="http://<%= request.host_with_port %>/open_search.xml" />
+</head>
+<body>
+
+<div id="header">
+ <ul id="nav">
+ <li><%= link_to 'Forums'[:forums_title], forums_path, :rel => 'home' %></li>
+ <li><%= link_to 'Users'[:users_title], users_path %></li>
+ <li id="search">
+ <% form_tag search_all_posts_url, :method => :get do -%>
+ <%= text_field_tag :q, params[:q], :size => 15, :id => :search_box %>
+ <li><%= submit_tag 'Search'[:search_title] %></li>
+ <% end -%>
+ </li>
+ <li class="login"><%= beast_user_link %></li>
+ </ul>
+ <h1><%= link_to 'Beast'[:beast_title], forums_path %></h1>
+</div>
+
9 app/views/layouts/_post.rss.builder
@@ -0,0 +1,9 @@
+xm.item do
+ key = post.topic.posts.size == 1 ? :topic_posted_by : :topic_replied_by
+ xm.title "{title} posted by {user} @ {date}"[key, h(post.respond_to?(:topic_title) ? post.topic_title : post.topic.title), h(post.user.display_name), post.created_at.rfc822]
+ xm.description post.body_html
+ xm.pubDate post.created_at.rfc822
+ xm.guid [request.host_with_port+request.relative_url_root, post.forum_id.to_s, post.topic_id.to_s, post.id.to_s].join(":"), "isPermaLink" => "false"
+ xm.author "#{post.user.display_name}"
+ xm.link forum_topic_url(post.forum_id, post.topic_id)
+end
29 app/views/layouts/application.html.erb
@@ -0,0 +1,29 @@
+<%= render :partial => "layouts/head" %>
+
+<div id="container">
+
+<div id="content">
+
+<%= content_tag 'p', h(flash[:notice]), :class => 'notice' if flash[:notice] %>
+<%= content_tag 'p', h(flash[:error]), :class => 'notice error' if flash[:error] %>
+
+<%= yield %>
+</div>
+
+<div id="right">
+ <%= yield :right %>
+</div>
+
+<br style="clear:both;" />
+
+</div>
+
+<div id="footer">
+<p class="disclaim">
+<strong>
+ </strong>
+</p>
+<br style="clear:both;" />
+</div>
+</body>
+</html>
4 app/views/monitorships/create.js.rjs
@@ -0,0 +1,4 @@
+#page["monitorship-icon-topics-#{params[:topic_id]}"].remove_class_name(:grey)
+#page["monitorship-icon-topics-#{params[:topic_id]}"].add_class_name(:green)
+
+page[:monitor_label].innerHTML = "Monitoring topic"[]
4 app/views/monitorships/destroy.js.rjs
@@ -0,0 +1,4 @@
+#page["monitorship-icon-topics-#{params[:topic_id]}"].remove_class_name(:green)
+#page["monitorship-icon-topics-#{params[:topic_id]}"].add_class_name(:darkgrey)
+
+page[:monitor_label].innerHTML = "Monitor topic"[]
38 app/views/posts/_edit.html.erb
@@ -0,0 +1,38 @@
+<div id="edit" class="editbox">
+<div class="container">
+ <% remote_form_for :post, :url => post_path(:forum_id => @post.forum_id, :topic_id => @post.topic_id, :id => @post),
+ :html => { :method => :put }, :before => "$('editbox_spinner').show();" do |f| -%>
+
+ <table width="100%" border="0" cellpadding="0" cellspacing="0">
+ <tr>
+ <td rowspan="2" width="70%">
+ <%= f.text_area :body, :rows => 10, :id => "edit_post_body", :tabindex => 1 %>
+ </td>
+ <td valign="top">
+
+ <%= link_to('delete post'[], post_path(:forum_id => @post.topic.forum, :topic_id => @post.topic, :id => @post, :page => params[:page]),
+ :class => "utility", :method => :delete, :confirm => "Delete this post? Are you sure?"[:delete_post_conf]) %>
+
+
+ <h5><%= 'Formatting Help'[] %></h5>
+
+ <ul class="help">
+ <li><%= '*bold*'[:formatting_bold] %></li>
+ <li><%= '_italics_'[:formatting_italics] %></li>
+ <li><%= 'bq. <span>(quotes)</span>'[:formatting_blockquote] %></li>
+ <li>"IBM":http://www.ibm.com</li>
+ <li><%= '* or # <span>(lists)</span>'[:formatting_list] %></li>
+ </ul>
+
+ </td>
+ </tr>
+ <tr>
+ <td valign="bottom" style="padding-bottom:15px;">
+ <%= ajax_spinner_for "editbox", "spinner_black.gif" %>
+ <%= submit_tag 'Save Changes'[], :or => link_to_function('cancel'[], "EditForm.cancel()"), :tabindex => 2 %>
+ </td>
+ </tr>
+ </table>
+ <% end -%>
+</div>
+</div>
14 app/views/posts/edit.html.erb
@@ -0,0 +1,14 @@
+<h1><%= 'Edit Post'[] %></h1>
+
+<h2><%= link_to h(@post.topic.title), forum_topic_path(@post.forum_id, @post.topic) %></h2>
+
+<%= link_to('Delete post'[], post_path(:forum_id => @post.forum_id, :topic_id => @post.topic, :id => @post, :page => params[:page]),
+ :class => "utility", :method => :delete, :confirm => 'Delete this post forever?'[:delete_post_conf]) %>
+
+<%= error_messages_for :topic %>
+<% form_for :post, :html => { :method => :put },
+ :url => post_path(:forum_id => params[:forum_id], :topic_id => params[:topic_id], :id => @post, :page => params[:page]) do |f| -%>
+<p id="post_body"><%= f.text_area :body %></p>
+
+<%= submit_tag 'Save'[:save_title] %> or <%= link_to 'cancel'[], forum_topic_path(:forum_id => params[:forum_id], :topic_id => params[:topic_id], :page => params[:page]) %>
+<% end -%>
6 app/views/posts/edit.js.rjs
@@ -0,0 +1,6 @@
+page.replace :edit, :partial => "edit"
+page.edit_form.set_reply_id @post.id
+page["edit-post-#{@post.id}_spinner"].hide
+page.delay(0.25) do
+ page[:edit_post_body].focus
+end
54 app/views/posts/index.html.erb
@@ -0,0 +1,54 @@
+<% @page_title = search_posts_title -%>
+
+<h1>
+<% if params[:q].blank? -%>
+ <%= @page_title %>
+<% else -%>
+ <%= 'Searching for'[] %> '<%= h params[:q] %>'
+<% end -%>
+</h1>
+<p class="subtitle">
+ <%= feed_icon_tag @page_title, search_posts_path(true) %>
+ <%= '{count} post(s) found'[(count=@posts.total_entries)==1 ? :post_count_found : :posts_count_found, number_with_delimiter(count)] %>
+</p>
+
+<%= pagination @posts %>
+
+<table border="0" cellspacing="0" cellpadding="0" class="posts wide">
+<% for post in @posts do %>
+<% unless post == @posts.first %>
+<tr class="spacer">
+ <td colspan="2">&nbsp;</td>
+</tr>
+<% end %>
+<tr class="post hentry" id="<%= dom_id post %>">
+ <td class="author vcard">
+ <div class="date">
+ <abbr class="updated" title="<%= post.created_at.xmlschema %>">
+ <% if post.created_at > Time.now.utc-24.hours%>
+ <%= time_ago_in_words(post.created_at).sub(/about /, '') %>
+ <% else %>
+ <%= post.created_at.strftime("%b %e, %Y"[:date_format])%>
+ <% end %>
+ </abbr>
+ </div>
+
+ <%= avatar_for @users[post.user_id] %>
+ <span class="fn"><%= link_to truncate(h(@users[post.user_id].display_name), 15), user_path(post.user_id) %></span>
+ <span class="posts"><%= '{count} posts'[(count=@users[post.user_id].posts.size)==1 ? :post_count : :posts_count, number_with_delimiter(count)] %></span>
+ </td>
+ <td class="body entry-content">
+ <p class="topic">
+ <%= "Topic"[:topic_title] %>: <%= link_to h(post.forum_name), forum_path(post.forum_id) %> /
+ <%= link_to h(post.topic_title), forum_topic_path(post.forum_id, post.topic_id) %>
+ </p>
+
+ <%= post.body_html %>
+ </td>
+</tr>
+
+<% end %>
+</table>
+
+<%= next_page @posts %>
+<%= pagination @posts %>
20 app/views/posts/index.rss.builder
@@ -0,0 +1,20 @@
+xml.instruct! :xml, :version => "1.0", :encoding => "UTF-8"
+
+xml.rss "version" => "2.0",
+ 'xmlns:opensearch' => "http://a9.com/-/spec/opensearch/1.1/",
+ 'xmlns:atom' => "http://www.w3.org/2005/Atom" do
+ xml.channel do
+ xml.title "{search_posts_title} | Beast"[:posts_feed_title, search_posts_title]
+ xml.link "http://#{request.host_with_port}#{search_posts_path}"
+ xml.language "en-us"[:feed_language]
+ xml.ttl "60"
+ xml.tag! "atom:link", :rel => 'search', :type => 'application/opensearchdescription+xml', :href => "http://#{request.host_with_port+request.relative_url_root}/open_search.xml"
+ unless params[:q].blank?
+ xml.tag! "opensearch:totalResults", @posts.total_entries
+ xml.tag! "opensearch:startIndex", (((params[:page] || 1).to_i - 1) * @posts.per_page)
+ xml.tag! "opensearch:itemsPerPage", @posts.per_page
+ xml.tag! "opensearch:Query", :role => 'request', :searchTerms => params[:q], :startPage => (params[:page] || 1)
+ end
+ render :partial => "layouts/post", :collection => @posts, :locals => {:xm => xml}
+ end
+end
56 app/views/posts/monitored.html.erb
@@ -0,0 +1,56 @@
+<% @page_title = "Posts that {user} is monitoring"[:posts_user_is_monitoring, h(@user.display_name)] -%>
+
+<% content_for :right do %>
+
+<h4><%= "{name}'s Monitored Topics"[:users_monitored_topics, link_to(h(@user.display_name), user_path(@user))] %></h4>
+<ul class="flat">
+<% @user.monitored_topics.find(:all, :limit => 25).each do |topic| %>
+ <li><%= link_to topic.title, forum_topic_path(topic.forum_id, topic) %></li>
+<% end %>
+</ul>
+
+<% end -%>
+
+<h1><%= @page_title %></h1>
+<p class="subtitle">
+ <%= feed_icon_tag @page_title, formatted_monitored_posts_path(:user_id => @user, :format => 'rss') %>
+ <%= '{count} post(s) found'[(count=@posts.total_entries)==1 ? :post_count_found : :posts_count_found, number_with_delimiter(count)] %>
+</p>
+
+<%= pagination @posts %>
+
+<table border="0" cellspacing="0" cellpadding="0" class="posts wide">
+<% for post in @posts do %>
+<% unless post == @posts.first %>
+<tr class="spacer">
+ <td colspan="2">&nbsp;</td>
+</tr>
+<% end %>
+<tr class="post hentry" id="<%= dom_id post %>">
+ <td class="author vcard">
+ <div class="date">
+ <abbr class="updated" title="<%= post.created_at.xmlschema %>">
+ <% if post.created_at > Time.now.utc-24.hours%>
+ <%= time_ago_in_words(post.created_at).sub(/about /, '') %>
+ <% else %>
+ <%= post.created_at.strftime("%b %e, %Y"[:date_format])%>
+ <% end %>
+ </abbr>
+ </div>
+
+ <%= avatar_for post.user %>
+ <span class="fn"><%= link_to truncate(h(post.user.display_name), 15), user_path(post.user), :class => (post.user == @posts.first.user ? "admin" : nil) %></span>
+ <span class="posts"><%= '{count} post(s)'[(count=post.user.posts.size)==1 ? :post_count : :posts_count, number_with_delimiter(count)] %></span>
+ </td>
+ <td class="body entry-content">
+ <p class="topic"><%= 'Topic'[:topic_title] %>: <%= link_to h(post.topic_title), forum_topic_path(post.forum_id, post.topic_id) %></p>
+
+ <%= post.body_html %>
+ </td>
+</tr>
+
+<% end %>
+</table>
+
+<%= next_page @posts %>
+<%= pagination @posts %>
15 app/views/posts/monitored.rss.builder
@@ -0,0 +1,15 @@
+xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
+
+xml.rss "version" => "2.0",
+ 'xmlns:opensearch' => "http://a9.com/-/spec/opensearch/1.1/",
+ 'xmlns:atom' => "http://www.w3.org/2005/Atom" do
+ xml.channel do
+ xml.title "Posts that {user} is monitoring | Beast"[:posts_user_is_monitoring, @user.display_name]
+ xml.link monitored_posts_url(@user)
+ xml.language "en-us"[:feed_language]
+ xml.ttl "60"
+ xml.tag! "atom:link", :rel => 'search', :type => 'application/opensearchdescription+xml', :href => "http://#{request.host_with_port+request.relative_url_root}/open_search.xml"
+
+ render :partial => "layouts/post", :collection => @posts, :locals => {:xm => xml}
+ end
+end
3  app/views/posts/update.js.rjs
@@ -0,0 +1,3 @@
+page.edit_form.cancel
+page.replace_html "post-body-#{@post.id}", @post.body_html
+page.visual_effect :highlight, "post-body-#{@post.id}", :duration => 1.5
28 app/views/topics/_form.html.erb
@@ -0,0 +1,28 @@
+<p>
+<label for="topic_title"><%= 'Title'[:title_title] %></label><br />
+<%= form.text_field :title, :onchange => "/*TopicForm.editNewTitle(this);*/", :class => "primary", :tabindex => 10 %>
+
+<% if admin? or current_user.moderator_of?(@topic.forum) %>
+<label style="margin-left:1em;">
+<%= form.check_box :sticky %> <%= 'Sticky'[:sticky_title] %>
+</label>
+
+<label style="margin-left:1em;">
+<%= form.check_box :locked %> <%= 'Locked'[:locked_title] %>
+</label>
+
+<% end %>
+
+</p>
+<% if @topic.new_record? %>
+<p>
+<label for="topic_body"><%= 'Body'[:body_title] %></label><br />
+<%= form.text_area :body, :rows => 12, :tabindex => 20 %></p>
+<% end %>
+
+<% if admin? and not @topic.new_record? %>
+<p id="topic_forum_id">
+ <label for="topic_forum_id"><%= 'Forum'[:forum_title] %></label><br />
+ <%= form.select :forum_id, Forum.find(:all, :order => "position").map {|x| [x.name, x.id] } %></p>
+</p>
+<% end %>
10 app/views/topics/edit.html.erb
@@ -0,0 +1,10 @@
+<h1><%= 'Edit Topic'[] %></h1>
+
+<%= error_messages_for :topic %>
+<% form_for :topic,
+ :url => forum_topic_path(@forum, @topic),
+ :html => { :method => :put } do |f| -%>
+<%= render :partial => "form", :object => f %>
+<br />
+<%= submit_tag 'Save Changes'[], :or => link_to('Cancel'[], forum_topic_path(@forum, @topic)) %>
+<% end -%>
2  app/views/topics/index.html.erb
@@ -0,0 +1,2 @@
+<h1>Post#index</h1>
+<p>Find me in app/views/posts/index.rhtml</p>
18 app/views/topics/new.html.erb
@@ -0,0 +1,18 @@
+<div class="crumbs" xstyle="margin-top:1.1em;">
+ <%= link_to 'Forums'[:forums_title], forums_path %> <span class="arrow">&rarr;</span>
+ <%= link_to h(@forum.name), forum_path(@forum) %> <span class="arrow">&rarr;</span>
+</div>
+
+
+<h1 id="new_topic"><%= 'New Topic'[] %></h1>
+<p class="subtitle"><%= 'by {user}'[:by_user, current_user.display_name] %></p>
+
+<%= error_messages_for :post %>
+<%= error_messages_for :topic %>
+<% form_for :topic, :url => forum_topics_path(@forum) do |f| -%>
+<%= render :partial => "form", :object => f %>
+<%= submit_tag 'Post Topic'[], :or => link_to('Cancel'[], forum_path(@forum)) %>
+<% end -%>
+
+<%= javascript_tag "$('topic_title').focus();" %>
+
185 app/views/topics/show.html.erb
@@ -0,0 +1,185 @@
+<% @page_title = @topic.title %>
+<% @monitoring = logged_in? && !Monitorship.count(:id, :conditions => ['user_id = ? and topic_id = ? and active = ?', current_user.id, @topic.id, true]).zero? %>
+
+<% content_for :right do -%>
+
+<h5><%= 'Voices'[:voices_title] %></h5>
+<ul class="flat talking">
+<% @topic.voices.each do | user | %>
+ <li><%= link_to h(user.display_name), user_path(user) %></li>
+<% end %>
+</ul>
+
+
+<% end # right content -%>
+
+<% if logged_in? %>
+<% form_tag forum_topic_monitorship_path(@forum, @topic), :style => 'margin-top:0em; float:right;' do -%>
+<div>
+ <input id="monitor_checkbox" type="checkbox" <%= "checked='checked'" if @monitoring %>
+ onclick="if (this.checked) {<%= remote_function :url => forum_topic_monitorship_path(@forum, @topic) %>} else {<%= remote_function :url => forum_topic_monitorship_path(@forum, @topic), :method => :delete %>}" />
+ <label id="monitor_label" for="monitor_checkbox"><%= @monitoring ? 'Monitoring topic'[] : 'Monitor topic'[] %></label>
+ <%= hidden_field_tag '_method', 'delete' if @monitoring %>
+ <%= submit_tag :Set, :id => 'monitor_submit' %>
+</div>
+<% end -%>
+
+<% end -%>
+
+
+<div class="crumbs">
+ <%= link_to "Forums"[:forums_title], forums_path %> <span class="arrow">&rarr;</span>
+ <%= link_to h(@topic.forum.name), forum_path(@topic.forum) %>
+ <%
+ page=session[:forum_page] ? session[:forum_page][@topic.forum.id] : nil
+ if page and page!=1 %>
+ <small style="color:#ccc">
+ (<%= link_to 'page {page}'[:page, page], forum_path(:id => @topic.forum, :page => page) %>)
+ </small>
+ <% end %>
+ <span class="arrow">&rarr;</span>
+</div>
+
+<h1 id="topic-title" style="margin-top:0.5em;"<%= %( onmouseover="$('topic_mod').show();" onmouseout="$('topic_mod').hide();") if logged_in? %>>
+
+
+ <%= h @topic.title %>
+ <% if @topic.locked? %>
+ <span>(<%= 'locked'[] %>)</span>
+ <% end %>
+ <% if logged_in? %>
+ <span style="display:none;" id="topic_mod">
+ <% if @topic.editable_by?(current_user) -%>
+ <%= link_to('edit'[], edit_forum_topic_path(@forum, @topic), :class => "utility") %> |
+ <%= link_to('delete'[], forum_topic_path(@forum, @topic), :class => "utility", :method => :delete, :confirm => 'Delete this topic forever?'[:delete_topic_conf]) %>
+ <% end -%>
+ </span>
+ <% end %>
+</h1>
+
+<p class="subtitle">
+ <%= feed_icon_tag @topic.title, formatted_forum_topic_path(@forum, @topic, :rss) %>
+ <%= '{count} post(s)'[(count=@topic.posts.size)==1 ? :post_count : :posts_count, number_with_delimiter(count)] %>,
+ <%= '{count} voice(s)'[(count=@topic.voices.size)==1 ? :voice_count : :voices_count, number_with_delimiter(count)] %>
+</p>
+
+<%= pagination @posts %>
+
+<a name="<%= dom_id @posts.first %>" id="<%= dom_id @posts.first %>">&nbsp;</a>
+
+<table border="0" cellspacing="0" cellpadding="0" class="posts wide">
+<% for post in @posts do %>
+<% unless post == @posts.first %>
+<tr class="spacer">
+ <td colspan="2">
+ <a name="<%= dom_id post %>" id="<%= dom_id post %>">&nbsp;</a>
+ </td>
+</tr>
+<% end %>
+<tr class="post hentry" id="<%= dom_id post %>-row">
+ <td class="author vcard">
+ <div class="date">
+ <a href="#<%= dom_id post %>" rel="bookmark">
+ <abbr class="updated" title="<%= post.created_at.xmlschema %>">
+ <%= time_ago_in_words(post.created_at) %>
+ </abbr>
+ </a>
+ </div>
+
+ <%= avatar_for post.user %>
+ <span class="fn"><%= link_to truncate(h(post.user.display_name), 15), user_path(post.user), :class => (post.user == @posts.first.user ? "threadauthor" : nil) %></span>
+ <% if post.user.admin? or post.forum.moderators.include?(post.user) %>
+ <span class="admin">
+ <% if post.user.admin? %>
+ <%= 'Administator'[:administrator_title] %>
+ <% elsif post.forum.moderators.include?(post.user) %>
+ <%= 'Moderator'[:moderator_title] %>
+ <% end %>
+ </span>
+ <% end %>
+ <span class="posts"><%= '{count} post(s)'[(count=post.user.posts.size)==1 ? :post_count : :posts_count, number_with_delimiter(count)] %></span>
+
+
+ <% if logged_in? && post.editable_by?(current_user) -%>
+ <p>
+ <span class="edit">
+ <%= ajax_spinner_for "edit-post-#{post.id}", "spinner_bounce.gif" %>
+ <%= link_to_remote('Edit post'[],
+ {:url => edit_post_path(:forum_id => @forum, :topic_id => @topic, :id => post), :method => :get,
+ :before => "EditForm.init(#{post.id});", :condition => "!EditForm.isEditing(#{post.id})" },
+ {:href => edit_post_path(:forum_id => @forum, :topic_id => @topic, :id => post, :page => params[:page]), :class => "utility"}) %>
+ </span>
+ </p>
+ <% end -%>
+
+
+ </td>
+ <td class="body entry-content" id="post-body-<%= post.id %>">
+<!--
+ <%= link_to_function image_tag('clearbits/comment.gif', :class => 'icon reply'), "$('reply').toggle()" if logged_in? %>
+-->
+ <%= post.body_html %>
+ </td>
+</tr>
+
+<% end %>
+</table>
+
+<%= next_page @posts %>
+<%= pagination @posts %>
+
+<% if logged_in? %>
+<div id="edit"></div>
+<% if @topic.locked? %>
+<p>
+ <%= image_tag "clearbits/lock.gif", :class => "icon grey", :title => "Topic locked"[:topic_locked_title], :plugin => "savage_beast" %>
+ <label>
+ <%= 'This topic is locked'[:locked_topic] %>.</label>
+</p>
+<% else %>
+
+<p><%= link_to_function 'Reply to topic'[], "ReplyForm.init()", :class => "utility" %></p>
+
+<div id="reply" class="editbox">
+<div class="container">
+ <%= content_tag 'p', h(flash[:bad_reply]), :class => 'notice' if flash[:bad_reply] %>
+ <% form_for :post, :url => posts_path(:forum_id => @forum, :topic_id => @topic, :page => @topic.last_page) do |f| -%>
+ <table width="100%" border="0" cellpadding="0" cellspacing="0">
+ <tr>
+ <td rowspan="2" width="70%">
+ <%= f.text_area :body, :rows => 8 %>
+ </td>
+ <td valign="top">
+
+
+ <h5><%= 'Formatting Help'[] %></h5>
+
+ <ul class="help">
+ <li><%= '*bold*'[:formatting_bold] %>
+ &nbsp; &nbsp; &nbsp;
+ <%= '_italics_'[:formatting_italics] %>
+ &nbsp; &nbsp; &nbsp;<br />
+ <%= 'bq. <span>(quotes)</span>'[:formatting_blockquote] %></li>
+ <li>"IBM":http://www.ibm.com</li>
+ <li><%= '* or # <span>(lists)</span>'[:formatting_list] %></li>
+ </ul>
+
+ </td>
+ </tr>
+ <tr>
+ <td valign="bottom" style="padding-bottom:15px;">
+ <%= submit_tag "Save Reply"[] %><span class="button_or">or <%= link_to_function 'cancel'[], "$('reply').hide()" %></span>
+ </td>
+ </tr>
+ </table>
+ <% end -%>
+</div>
+</div>
+<%= javascript_tag "$('reply').hide();"%>
+<% end %>
+<% end %>
+
+<div class="crumbs" style="margin-top:1.1em;">
+ <%= link_to "Forums"[:forums_title], forums_path %> <span class="arrow">&rarr;</span>
+ <%= link_to h(@topic.forum.name), forum_path(@topic.forum) %> <span class="arrow">&rarr;</span>
+</div>
16 app/views/topics/show.rss.builder
@@ -0,0 +1,16 @@
+xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
+
+xml.rss "version" => "2.0",
+ 'xmlns:opensearch' => "http://a9.com/-/spec/opensearch/1.1/",
+ 'xmlns:atom' => "http://www.w3.org/2005/Atom" do
+ xml.channel do
+ xml.title "Recent Posts in '{topic}' | Beast"[:recent_posts_in_topic,@topic.title]
+ xml.link forum_topic_url # implicitly uses (@forum, @topic)
+ xml.language "en-us"[:feed_language]
+ xml.ttl "60"
+ xml.tag! "atom:link", :rel => 'search', :type => 'application/opensearchdescription+xml', :href => "http://#{request.host_with_port+request.relative_url_root}/open_search.xml"
+ xml.description @topic.body
+
+ render :partial => "layouts/post", :collection => @posts, :locals => {:xm => xml}
+ end
+end
73 db/migrate/001_create_savage_tables.rb
@@ -0,0 +1,73 @@
+class CreateSavageTables < ActiveRecord::Migration
+ def self.up
+ create_table "forums", :force => true do |t|
+ t.string "name"
+ t.string "description"
+ t.integer "topics_count", :default => 0
+ t.integer "posts_count", :default => 0
+ t.integer "position"
+ t.text "description_html"
+ end
+
+ create_table "moderatorships", :force => true do |t|
+ t.integer "forum_id"
+ t.integer "user_id"
+ end
+
+ add_index "moderatorships", ["forum_id"], :name => "index_moderatorships_on_forum_id"
+
+ create_table "monitorships", :force => true do |t|
+ t.integer "topic_id"
+ t.integer "user_id"
+ t.boolean "active", :default => true
+ end
+
+ create_table "posts", :force => true do |t|
+ t.integer "user_id"
+ t.integer "topic_id"
+ t.text "body"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "forum_id"
+ t.text "body_html"
+ end
+
+ add_index "posts", ["forum_id", "created_at"], :name => "index_posts_on_forum_id"
+ add_index "posts", ["user_id", "created_at"], :name => "index_posts_on_user_id"
+ add_index "posts", ["topic_id", "created_at"], :name => "index_posts_on_topic_id"
+
+ create_table "topics", :force => true do |t|
+ t.integer "forum_id"
+ t.integer "user_id"
+ t.string "title"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "hits", :default => 0
+ t.integer "sticky", :default => 0
+ t.integer "posts_count", :default => 0
+ t.datetime "replied_at"
+ t.boolean "locked", :default => false
+ t.integer "replied_by"
+ t.integer "last_post_id"
+ end
+
+ add_index "topics", ["forum_id"], :name => "index_topics_on_forum_id"
+ add_index "topics", ["forum_id", "sticky", "replied_at"], :name => "index_topics_on_sticky_and_replied_at"
+ add_index "topics", ["forum_id", "replied_at"], :name => "index_topics_on_forum_id_and_replied_at"
+
+ add_column :users, :posts_count, :integer, :default => 0
+ add_column :users, :last_seen_at, :datetime
+ end
+
+ def self.down
+ remove_column :users, :posts_count
+ remove_column :users, :last_seen_at
+
+ drop_table :topics
+ drop_table :posts
+ drop_table :monitorships
+ drop_table :moderatorships
+ drop_table :forums
+ end
+
+end
79 init.rb
@@ -0,0 +1,79 @@
+ActionView::Base.send :include, SavageBeast::AuthenticationSystem
+ActionController::Base.send :include, SavageBeast::AuthenticationSystem
+
+# FIX for engines model reloading issue in development mode
+if ENV['RAILS_ENV'] != 'production'
+ load_paths.each do |path|
+ ActiveSupport::Dependencies.load_once_paths.delete(path)
+ end
+end
+
+# Include your application configuration below
+# @WBH@ would be nice for this to not be necessary somehow...
+# PASSWORD_SALT = '48e45be7d489cbb0ab582d26e2168621' unless Object.const_defined?(:PASSWORD_SALT)
+Module.class_eval do
+ def expiring_attr_reader(method_name, value)
+ class_eval(<<-EOS, __FILE__, __LINE__)
+ def #{method_name}
+ class << self; attr_reader :#{method_name}; end
+ @#{method_name} = eval(%(#{value}))
+ end
+ EOS
+ end
+end
+
+
+# All this is given in engines plugin
+# Define the means by which to add our own routing to Rails' routing
+class ActionController::Routing::RouteSet::Mapper
+ def from_plugin(name)
+ eval File.read(File.join(RAILS_ROOT, "vendor/plugins/#{name}/routes.rb"))
+ end
+end
+
+#--------------------------------------------------------------------------------
+# Uncommenting this section of code allows the plugin to work without the engines plugin
+# installed. Just need to copy the helpers into the lib directory.
+# So why use Engines?
+# It allows controller methods to be overridden
+# It gives an easy way to access images
+# It allows controller views to be overridden
+#--------------------------------------------------------------------------------
+# Add our models and controllers to the application
+# Stolen from http://weblog.techno-weenie.net/2007/1/24/understanding-the-rails-initialization-process
+# You can't use config.load_paths because #set_autoload_paths has already been called in the Rails Initialization process
+#models_path = File.join(directory, 'app', 'models')
+#$LOAD_PATH << models_path
+#Dependencies.load_paths << models_path
+
+#controller_path = File.join(directory, 'app', 'controllers')
+#$LOAD_PATH << controller_path
+#Dependencies.load_paths << controller_path
+#config.controller_paths << controller_path
+
+#view_path = File.join(directory, 'app', 'views')
+#if File.exist?(view_path)
+# ActionController::Base.view_paths.insert(1, view_path) # push it just underneath the app
+#end
+
+# Include helpers
+#ActionView::Base.send :include, ForumsHelper
+#ActionView::Base.send :include, ApplicationHelper
+#ActionView::Base.send :include, ModeratorsHelper
+#ActionView::Base.send :include, PostsHelper
+#ActionView::Base.send :include, TopicsHelper
+#--------------------------------------------------------------------------------
+
+begin
+ require 'gettext/rails'
+ GetText.locale = "nl" # Change this to your preference language
+ #puts "GetText found!"
+rescue MissingSourceFile, LoadError
+ #puts "GetText not found. Using English."
+ class ActionView::Base
+ def _(s)
+ s
+ end
+ end
+end
+
201 lang/en.yml
@@ -0,0 +1,201 @@
+beast_title: "Beast"
+beast_sidebar_title: "What is Beast?"
+beast_sidebar_body: A small, light-weight forum in Rails with a scary name and a goal of around 500 lines of code when we're done.
+beast_welcome: Welcome to Beast
+
+forum_title: Forum
+forums_title: Forums
+new_forum: New forum
+edit_forum: Edit forum
+save_forum: Save forum
+create_new_forum: Create new forum
+
+forum_is_unmoderated: This forum is currently unmoderated.
+please_be_courteous: Please always be courteous.
+
+topic_title: Topic
+Topics_title: Topics
+edit_topic: Edit topic
+new_topic: New topic
+topic_posted_by: "{title} posted by {user} @ {date}"
+topic_replied_by: "{title} replied by {user} @ {date}"
+locked_topic: This topic is locked
+monitoring_topic: Monitoring topic
+monitor_topic: Monitor topic
+post_topic: Post topic
+reply_to_topic: Reply to topic
+save_reply: Save Reply
+
+post_title: Post
+posts_title: Posts
+edit_post: Edit post
+delete_post: Delete post
+last_post: Last Post
+recent_posts: Recent Posts
+
+voices_title: Voices
+monitor_title: Monitor
+users_title: Users
+recent_activity: Recent Activity
+no_recent_activity: No recent activity
+users_online: Users Online
+change_login: Please change your login
+gravatar_notice: To have your very own avatar displayed on this forum visit {gravatar} and sign up for a free gravatar.
+change_email_or_password: Change e-mail or password
+name_or_login: Name / Login
+reset_password: Reset Password
+user_profile: User Profile
+update_profile: Update Profile
+find_a_user: Find a User
+display_name_or_login: Display name or login
+users: users
+active: active
+lurking: lurking
+add_as_moderator: Add as moderator
+user_is_an_administrator: User is an administrator
+monitored: monitored
+user_since: User since
+
+pages_title: Pages
+next_page: Next Page
+next: "Next &raquo;"
+previous: "&laquo; Previous"
+topics_count: "{count} topic(s)"
+posts_count: "{count} post(s)"
+voices_count: "{count} voice(s)"
+posts_count_found: "{count} post(s) found"
+cancel: cancel
+page: "page {page}"
+
+# field titles
+admin_title: Admin
+title_title: Title
+position_title: Position
+description_title: Description
+edit_title: Edit
+moderators_title: Moderators
+views_title: Views
+settings_title: Settings
+logout_title: Logout
+signup_title: Signup
+login_title: Login
+save_title: Save
+password_title: Password
+sticky_title: Sticky
+locked_title: Locked
+body_title: Body
+display_name_title: Display Name
+identity_url_title: Identity Url
+website_title: Website
+bio_title: Bio
+email_title: Email
+basics_title: Basics
+avatars_title: Avatars
+search_title: Search
+
+by: by
+save_changes: Save Changes
+remember_me: Remember me on this computer
+delete_conf: "Delete this {thing} forever?"
+email_directions: Enter your email, and a brand new login key will be sent to you. Click the link in the email to log in, and then change your password.
+email_submit: E-mail me the link
+powered_by: Powered by
+and: and
+users_monitored_topics: "{name}'s Monitored Topics"
+delete: delete
+activation: Please visit the link below to activate your account.
+without_http: without http://
+admin_in_parens: '(admin)'
+admin_and_moderation: Admin &amp; Moderation
+remove_moderated_forum: This user can moderate the following forums. Click one to remove.
+remove_user_as_moderator: Remove user as moderator for
+searching_for: Searching for
+locked: locked
+edit: edit
+delete: delete
+login: login
+once: once
+and_again: and again
+
+open_id_field: Enter your OpenID Identity Url if you know it
+open_id_field_extended: Enter your OpenID Identity Url if you know it. If your Identity Url supports the {sreg} extension, you can {login_link} and create your account automatically
+login_field: Logins should start with least 2 characters and may consist o