-
Notifications
You must be signed in to change notification settings - Fork 211
Modifying thredded for a multi tenant app
Thredded is a great Ruby on Rails engine to support discussion forums within a Rails application. It's got loads of features, from basic messageboard functionality to moderation, to LaTeX mark-up. But it's designed to host one set of messageboards within an application, presenting a few problems for applications that want to present the thredded interface to several different tenants, independent of each other. Here's what we did to use thredded to power messageboards for each project within FromThePage
In our application, we we wanted the messageboard experience to be scoped to a single project -- a model we call a Collection
. The easiest way to accomplish this was to connect the Collection
model to a Thredded::MessageboardGroup
record, since thredded already has controllers and views for showing multiple message boards within the context of a single MessageboardGroup
.
First, add the foreign key to your application's model:
class AddMessageboardGroupToCollection < ActiveRecord::Migration[6.0]
def change
add_reference :collections, :thredded_messageboard_group, null: true, foreign_key: true
end
end
This also requires an association within your model
belongs_to :messageboard_group, class_name: 'Thredded::MessageboardGroup', foreign_key: 'thredded_messageboard_group_id', optional: true
Since we wanted message boards to be an optional feature of our application (to be toggled on or off by administrative users on the Collection
level), we also needed to add a flag for enabling/disabling message boards:
class AddMesssageboardsEnabledToCollection < ActiveRecord::Migration[6.0]
def change
add_column :collections, :messageboards_enabled, :boolean
end
end
Now we're ready to create message board groups when users enable the message board functionality. We wanted to create a couple of default message boards when this happens, as well.
def enable_messageboards
if self.messageboard_group.nil?
self.messageboard_group = Thredded::MessageboardGroup.create!(name: self.title)
# now create the default messageboards
Thredded::Messageboard.create!(name: 'General', description: 'General discussion', messageboard_group_id: self.messageboard_group.id)
Thredded::Messageboard.create!(name: 'Help', messageboard_group_id: self.messageboard_group.id)
end
self.messageboards_enabled = true
self.save!
end
def disable_messageboards
self.messageboards_enabled=false
self.save!
end
Now we run into the first real problem using Thredded in a multi-tenant environment -- we'd like to create a "General" messageboard for each tenant, but there is a unique validation rule on messagboard names. This requires an override.
First, modify your application initializer to load overrides if you have not already.
In config/application.rb
add this code:
# load overrides for Thredded and other engines
# config/application.rb
overrides = "#{Rails.root}/app/overrides"
Rails.autoloaders.main.ignore(overrides)
config.to_prepare do
Dir.glob("#{overrides}/**/*_override.rb").each do |override|
load override
end
end
Next, add an override to the Thredded model by creating app/overrides/models/thredded/messageboard_override.rb
with these contents:
Thredded::Messageboard.class_eval do
clear_validators!
validates :name,
length: { within: Thredded.messageboard_name_length_range },
presence: true
validates :topics_count, numericality: true
validates :position, presence: true, on: :update
end
Now you should be able to hook into the UI by updating the controller for the tenant model (app/controllers/collection_controller.rb
)
def enable_messageboards
@collection.enable_messageboards
redirect_to edit_collection_path(@collection.owner, @collection)
end
def disable_messageboards
@collection.disable_messageboards
redirect_to edit_collection_path(@collection.owner, @collection)
end
and modifying your view (app/views/collection/edit.html.slim
)
h3 =t('.message_boards')
p =t('.message_boards_description')
-if @collection.messageboards_enabled?
=link_to(collection_disable_messageboards_path(:collection_id => @collection.id), class: 'button')
span =t('.disable_message_boards')
-else
=link_to(collection_enable_messageboards_path(:collection_id => @collection.id), class: 'button')
span =t('.enable_message_boards')
For the rest of our application, resources are mounted under a path containing the user slug and the collection slug. We'd like thredded to appear under that, so that /john-smith/first-project/forum
loads Thredded within the navigation framework used by other resources under /john-smith/first-project
.
First, mount Thredded under the resource in config/routes.rb
scope ':user_slug' do
scope ':collection_id' do
mount Thredded::Engine => '/forum'
end
end
Next, add a reference to Thredded within your application navigation. (/app/views/shared/_collection_tabs.html.slim
)
-if @collection.messageboards_enabled? && signed_in?
-tabs +=[{\
:name => t('.forum'),
:selected => 14,
:path => "#{Thredded::UrlsHelper::show_messageboard_group_path(@collection.messageboard_group, user_slug: @collection.owner.slug, collection_id: @collection.slug)}",
}]
(NB: If this is a partial rendered by your application and also by a Thredded layout, you'll need to prefix all your path helpers with main_app.
)
Because our application only uses one layout to render all pages (navigation links like tabs are rendered by the views themselves, which is not recommended), we needed to render the tenant-specific navigation within a thredded-specific part of our application layout (/app/layouts/application.html/slim
):
-if content_for?(:thredded)
=render({ :partial => '/shared/collection_tabs', :locals => { :selected => 14, :collection_id => @collection.id }})
But wait! If Thredded will be rendering URLs using the user slug and collection slug, it needs to know what those values are! We load a @collection
object in /app/controllers/application_controller.rb
, so after loading that we need to add this code to make the path helpers work:
if self.class.module_parent == Thredded && @collection
Thredded::Engine.routes.default_url_options = { user_slug: @collection.owner.slug, collection_id: @collection.slug }
else
Thredded::Engine.routes.default_url_options = { user_slug: 'nil', collection_id: 'nil' }
end
Now we have a tab under our tenant object that renders thredded, with links back to the rest of our application. All we need to do is suppress cross-talk, to keep users from seeing messageboards outside the context of their tenant object. For this, we'll need to override a handful of Thredded views and controllers.
Suppress display of "Create Messageboard Group" and navigation to "All Messageboards" within these files:
app/views/thredded/messageboard_groups/show.html.erb
app/views/thredded/messageboards/_form.html.erb
app/views/thredded/shared/_breadcrumbs.html.erb
app/views/thredded/topics/search.html.erb
You'll also need to override a handful of actions on the Thredded controllers.
In app/overrides/controllers/thredded/messageboards_controller_override.rb
Thredded::MessageboardsController.class_eval do
def create
@new_messageboard = Thredded::Messageboard.new(messageboard_params)
authorize_creating @new_messageboard
if Thredded::CreateMessageboard.new(@new_messageboard, thredded_current_user).run
redirect_to Thredded::UrlsHelper::show_messageboard_group_path(@collection.messageboard_group)
else
render :new
end
end
end
And suppress the search across messageboard groups within the topic controller
In app/overrides/controllers/thredded/topics_controller_override.rb
Thredded::TopicsController.class_eval do
def search
@query = params[:q].to_s
# add messageboard_group_id
messageboard_ids = @collection.messageboard_group.messageboards.map{|mb| mb.id}
page_scope = topics_scope
.where(messageboard_id: messageboard_ids)
.search_query(@query)
.order_recently_posted_first
.includes(:categories, :last_user, :user)
.send(Kaminari.config.page_method_name, current_page)
return redirect_to(last_page_params(page_scope)) if page_beyond_last?(page_scope)
@topics = Thredded::TopicsPageView.new(thredded_current_user, page_scope)
end
end
The full list of changes we made to our application to integrate Thredded and make it multi-tenant are at this pull request
Many thanks to the folks running Thredded.