Merge pull request #4030 from consul/multitenancy
Add support for multitenancy
This commit is contained in:
@@ -20,8 +20,10 @@ storage/
|
||||
|
||||
# Files generated by scripts or compiled
|
||||
public/sitemap.xml
|
||||
public/tenants/*/sitemap.xml
|
||||
public/assets/
|
||||
public/machine_learning/data/
|
||||
public/tenants/*/machine_learning/data/
|
||||
|
||||
# Bundler config, cache and gemsets
|
||||
**/.bundle/
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,8 +22,10 @@ tmp/
|
||||
|
||||
# Files generated by scripts or compiled
|
||||
/public/sitemap.xml
|
||||
/public/tenants/*/sitemap.xml
|
||||
/public/assets/
|
||||
/public/machine_learning/data/
|
||||
/public/tenants/*/machine_learning/data/
|
||||
|
||||
# Bundler config, cache and gemsets
|
||||
.bundle/
|
||||
|
||||
1
Gemfile
1
Gemfile
@@ -49,6 +49,7 @@ gem "recipient_interceptor", "~> 0.3.1"
|
||||
gem "redcarpet", "~> 3.5.1"
|
||||
gem "responders", "~> 3.0.1"
|
||||
gem "rinku", "~> 2.0.6", require: "rails_rinku"
|
||||
gem "ros-apartment", "~> 2.11.0", require: "apartment"
|
||||
gem "sassc-rails", "~> 2.1.2"
|
||||
gem "savon", "~> 2.13.0"
|
||||
gem "sitemap_generator", "~> 6.3.0"
|
||||
|
||||
@@ -472,7 +472,7 @@ GEM
|
||||
pronto-scss (0.11.0)
|
||||
pronto (~> 0.11.0)
|
||||
scss_lint (~> 0.43, >= 0.43.0)
|
||||
public_suffix (5.0.0)
|
||||
public_suffix (4.0.7)
|
||||
puma (4.3.12)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.6.0)
|
||||
@@ -534,6 +534,11 @@ GEM
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rinku (2.0.6)
|
||||
ros-apartment (2.11.0)
|
||||
activerecord (>= 5.0.0, < 7.1)
|
||||
parallel (< 2.0)
|
||||
public_suffix (>= 2.0.5, < 5.0)
|
||||
rack (>= 1.3.6, < 3.0)
|
||||
rspec-core (3.11.0)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-expectations (3.11.0)
|
||||
@@ -784,6 +789,7 @@ DEPENDENCIES
|
||||
redcarpet (~> 3.5.1)
|
||||
responders (~> 3.0.1)
|
||||
rinku (~> 2.0.6)
|
||||
ros-apartment (~> 2.11.0)
|
||||
rspec-rails (~> 5.1.2)
|
||||
rubocop (~> 1.35.1)
|
||||
rubocop-performance (~> 1.11.4)
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
<a href="#" class="settings-link"><%= t("admin.menu.title_settings") %></a>
|
||||
<%= link_list(
|
||||
settings_link,
|
||||
tenants_link,
|
||||
tags_link,
|
||||
geozones_link,
|
||||
images_link,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Admin::MenuComponent < ApplicationComponent
|
||||
include LinkListHelper
|
||||
delegate :can?, to: :helpers
|
||||
|
||||
private
|
||||
|
||||
@@ -32,8 +33,8 @@ class Admin::MenuComponent < ApplicationComponent
|
||||
end
|
||||
|
||||
def settings?
|
||||
controllers_names = ["settings", "tags", "geozones", "images", "content_blocks",
|
||||
"local_census_records", "imports"]
|
||||
controllers_names = ["settings", "tenants", "tags", "geozones", "images",
|
||||
"content_blocks", "local_census_records", "imports"]
|
||||
controllers_names.include?(controller_name) &&
|
||||
controller.class.module_parent != Admin::Poll::Questions::Answers
|
||||
end
|
||||
@@ -300,6 +301,16 @@ class Admin::MenuComponent < ApplicationComponent
|
||||
]
|
||||
end
|
||||
|
||||
def tenants_link
|
||||
if can?(:read, Tenant)
|
||||
[
|
||||
t("admin.menu.multitenancy"),
|
||||
admin_tenants_path,
|
||||
controller_name == "tenants"
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
def tags_link
|
||||
[
|
||||
t("admin.menu.proposals_topics"),
|
||||
|
||||
3
app/components/admin/tenants/edit_component.html.erb
Normal file
3
app/components/admin/tenants/edit_component.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<%= back_link_to admin_tenants_path %>
|
||||
<%= header %>
|
||||
<%= render Admin::Tenants::FormComponent.new(tenant) %>
|
||||
12
app/components/admin/tenants/edit_component.rb
Normal file
12
app/components/admin/tenants/edit_component.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
class Admin::Tenants::EditComponent < ApplicationComponent
|
||||
include Header
|
||||
attr_reader :tenant
|
||||
|
||||
def initialize(tenant)
|
||||
@tenant = tenant
|
||||
end
|
||||
|
||||
def title
|
||||
tenant.name
|
||||
end
|
||||
end
|
||||
7
app/components/admin/tenants/form_component.html.erb
Normal file
7
app/components/admin/tenants/form_component.html.erb
Normal file
@@ -0,0 +1,7 @@
|
||||
<%= form_for [:admin, tenant] do |f| %>
|
||||
<%= render "shared/errors", resource: tenant %>
|
||||
|
||||
<%= f.text_field :name %>
|
||||
<%= f.text_field :schema %>
|
||||
<%= f.submit %>
|
||||
<% end %>
|
||||
7
app/components/admin/tenants/form_component.rb
Normal file
7
app/components/admin/tenants/form_component.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class Admin::Tenants::FormComponent < ApplicationComponent
|
||||
attr_reader :tenant
|
||||
|
||||
def initialize(tenant)
|
||||
@tenant = tenant
|
||||
end
|
||||
end
|
||||
27
app/components/admin/tenants/index_component.html.erb
Normal file
27
app/components/admin/tenants/index_component.html.erb
Normal file
@@ -0,0 +1,27 @@
|
||||
<%= header do %>
|
||||
<%= link_to t("admin.tenants.index.create"), new_admin_tenant_path %>
|
||||
<% end %>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= attribute_name(:name) %></th>
|
||||
<th><%= attribute_name(:schema) %></th>
|
||||
<th><%= t("admin.shared.actions") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% @tenants.each do |tenant| %>
|
||||
<tr id="<%= dom_id(tenant) %>">
|
||||
<td><%= tenant.name %></td>
|
||||
<td><%= tenant.schema %></td>
|
||||
<td>
|
||||
<%= render Admin::TableActionsComponent.new(tenant, actions: [:edit]) do |actions| %>
|
||||
<%= actions.action(:show, text: t("admin.shared.view"), path: root_url(host: tenant.host)) %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
18
app/components/admin/tenants/index_component.rb
Normal file
18
app/components/admin/tenants/index_component.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
class Admin::Tenants::IndexComponent < ApplicationComponent
|
||||
include Header
|
||||
attr_reader :tenants
|
||||
|
||||
def initialize(tenants)
|
||||
@tenants = tenants
|
||||
end
|
||||
|
||||
def title
|
||||
t("admin.menu.multitenancy")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attribute_name(attribute)
|
||||
Tenant.human_attribute_name(attribute)
|
||||
end
|
||||
end
|
||||
3
app/components/admin/tenants/new_component.html.erb
Normal file
3
app/components/admin/tenants/new_component.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<%= back_link_to admin_tenants_path %>
|
||||
<%= header %>
|
||||
<%= render Admin::Tenants::FormComponent.new(tenant) %>
|
||||
12
app/components/admin/tenants/new_component.rb
Normal file
12
app/components/admin/tenants/new_component.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
class Admin::Tenants::NewComponent < ApplicationComponent
|
||||
include Header
|
||||
attr_reader :tenant
|
||||
|
||||
def initialize(tenant)
|
||||
@tenant = tenant
|
||||
end
|
||||
|
||||
def title
|
||||
t("admin.tenants.new.title")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
<%= attributes -%>
|
||||
21
app/components/layout/common_html_attributes_component.rb
Normal file
21
app/components/layout/common_html_attributes_component.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
class Layout::CommonHTMLAttributesComponent < ApplicationComponent
|
||||
delegate :rtl?, to: :helpers
|
||||
|
||||
private
|
||||
|
||||
def attributes
|
||||
sanitize([dir, lang, html_class].compact.join(" "))
|
||||
end
|
||||
|
||||
def dir
|
||||
'dir="rtl"' if rtl?
|
||||
end
|
||||
|
||||
def lang
|
||||
"lang=\"#{I18n.locale}\""
|
||||
end
|
||||
|
||||
def html_class
|
||||
"class=\"tenant-#{Tenant.current_schema}\"" if Rails.application.config.multitenancy
|
||||
end
|
||||
end
|
||||
35
app/controllers/admin/tenants_controller.rb
Normal file
35
app/controllers/admin/tenants_controller.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
class Admin::TenantsController < Admin::BaseController
|
||||
load_and_authorize_resource
|
||||
|
||||
def index
|
||||
@tenants = @tenants.order(:name)
|
||||
end
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def create
|
||||
if @tenant.save
|
||||
redirect_to admin_tenants_path, notice: t("admin.tenants.create.notice")
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @tenant.update(tenant_params)
|
||||
redirect_to admin_tenants_path, notice: t("admin.tenants.update.notice")
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tenant_params
|
||||
params.require(:tenant).permit(:name, :schema)
|
||||
end
|
||||
end
|
||||
@@ -26,12 +26,13 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
def authenticate_http_basic
|
||||
authenticate_or_request_with_http_basic do |username, password|
|
||||
username == Rails.application.secrets.http_basic_username && password == Rails.application.secrets.http_basic_password
|
||||
username == Tenant.current_secrets.http_basic_username &&
|
||||
password == Tenant.current_secrets.http_basic_password
|
||||
end
|
||||
end
|
||||
|
||||
def http_basic_auth_site?
|
||||
Rails.application.secrets.http_basic_auth
|
||||
Tenant.current_secrets.http_basic_auth
|
||||
end
|
||||
|
||||
def verify_lock
|
||||
|
||||
@@ -26,6 +26,6 @@ module RemotelyTranslatable
|
||||
end
|
||||
|
||||
def api_key_has_been_set_in_secrets?
|
||||
Rails.application.secrets.microsoft_api_key.present?
|
||||
Tenant.current_secrets.microsoft_api_key.present?
|
||||
end
|
||||
end
|
||||
|
||||
7
app/controllers/robots_controller.rb
Normal file
7
app/controllers/robots_controller.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class RobotsController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
def index
|
||||
respond_to :text
|
||||
end
|
||||
end
|
||||
@@ -53,6 +53,8 @@ module ApplicationHelper
|
||||
|
||||
if image
|
||||
polymorphic_path(image)
|
||||
elsif AssetFinder.find_asset(File.join(Tenant.subfolder_path, filename))
|
||||
File.join(Tenant.subfolder_path, filename)
|
||||
else
|
||||
filename
|
||||
end
|
||||
|
||||
@@ -9,4 +9,8 @@ module LayoutsHelper
|
||||
link_to(text, path, options.merge(title: title))
|
||||
end
|
||||
end
|
||||
|
||||
def common_html_attributes
|
||||
render Layout::CommonHTMLAttributesComponent.new
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
class ApplicationMailer < ActionMailer::Base
|
||||
helper :settings
|
||||
helper :application
|
||||
helper :mailer
|
||||
helper :application, :layouts, :mailer, :settings
|
||||
default from: proc { "#{Setting["mailer_from_name"]} <#{Setting["mailer_from_address"]}>" }
|
||||
layout "mailer"
|
||||
before_action :set_asset_host
|
||||
after_action :set_smtp_settings
|
||||
|
||||
def default_url_options
|
||||
Tenant.current_url_options
|
||||
end
|
||||
|
||||
def set_asset_host
|
||||
self.asset_host ||= root_url.delete_suffix("/")
|
||||
end
|
||||
|
||||
def set_smtp_settings
|
||||
unless Tenant.default?
|
||||
mail.delivery_method.settings.merge!(Tenant.current_secrets.smtp_settings.to_h)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -134,6 +134,10 @@ module Abilities
|
||||
|
||||
can :manage, LocalCensusRecord
|
||||
can [:create, :read], LocalCensusRecords::Import
|
||||
|
||||
if Rails.application.config.multitenancy && Tenant.default?
|
||||
can [:create, :read, :update], Tenant
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,6 @@ class MachineLearning
|
||||
attr_accessor :job
|
||||
|
||||
SCRIPTS_FOLDER = Rails.root.join("public", "machine_learning", "scripts").freeze
|
||||
DATA_FOLDER = Rails.root.join("public", "machine_learning", "data").freeze
|
||||
|
||||
def initialize(job)
|
||||
@job = job
|
||||
@@ -11,6 +10,10 @@ class MachineLearning
|
||||
@previous_modified_date = set_previous_modified_date
|
||||
end
|
||||
|
||||
def data_folder
|
||||
self.class.data_folder
|
||||
end
|
||||
|
||||
def run
|
||||
begin
|
||||
export_proposals_to_json
|
||||
@@ -81,17 +84,25 @@ class MachineLearning
|
||||
"comments.json"
|
||||
end
|
||||
|
||||
def data_folder
|
||||
Rails.root.join("public", tenant_data_folder)
|
||||
end
|
||||
|
||||
def tenant_data_folder
|
||||
Tenant.path_with_subfolder("machine_learning/data")
|
||||
end
|
||||
|
||||
def data_output_files
|
||||
files = { tags: [], related_content: [], comments_summary: [] }
|
||||
|
||||
files[:tags] << proposals_tags_filename if File.exists?(DATA_FOLDER.join(proposals_tags_filename))
|
||||
files[:tags] << proposals_taggings_filename if File.exists?(DATA_FOLDER.join(proposals_taggings_filename))
|
||||
files[:tags] << investments_tags_filename if File.exists?(DATA_FOLDER.join(investments_tags_filename))
|
||||
files[:tags] << investments_taggings_filename if File.exists?(DATA_FOLDER.join(investments_taggings_filename))
|
||||
files[:related_content] << proposals_related_filename if File.exists?(DATA_FOLDER.join(proposals_related_filename))
|
||||
files[:related_content] << investments_related_filename if File.exists?(DATA_FOLDER.join(investments_related_filename))
|
||||
files[:comments_summary] << proposals_comments_summary_filename if File.exists?(DATA_FOLDER.join(proposals_comments_summary_filename))
|
||||
files[:comments_summary] << investments_comments_summary_filename if File.exists?(DATA_FOLDER.join(investments_comments_summary_filename))
|
||||
files[:tags] << proposals_tags_filename if File.exists?(data_folder.join(proposals_tags_filename))
|
||||
files[:tags] << proposals_taggings_filename if File.exists?(data_folder.join(proposals_taggings_filename))
|
||||
files[:tags] << investments_tags_filename if File.exists?(data_folder.join(investments_tags_filename))
|
||||
files[:tags] << investments_taggings_filename if File.exists?(data_folder.join(investments_taggings_filename))
|
||||
files[:related_content] << proposals_related_filename if File.exists?(data_folder.join(proposals_related_filename))
|
||||
files[:related_content] << investments_related_filename if File.exists?(data_folder.join(investments_related_filename))
|
||||
files[:comments_summary] << proposals_comments_summary_filename if File.exists?(data_folder.join(proposals_comments_summary_filename))
|
||||
files[:comments_summary] << investments_comments_summary_filename if File.exists?(data_folder.join(investments_comments_summary_filename))
|
||||
|
||||
files
|
||||
end
|
||||
@@ -110,10 +121,10 @@ class MachineLearning
|
||||
proposals_comments_summary_filename,
|
||||
investments_comments_summary_filename
|
||||
]
|
||||
json = Dir[DATA_FOLDER.join("*.json")].map do |full_path_filename|
|
||||
json = Dir[data_folder.join("*.json")].map do |full_path_filename|
|
||||
full_path_filename.split("/").last
|
||||
end
|
||||
csv = Dir[DATA_FOLDER.join("*.csv")].map do |full_path_filename|
|
||||
csv = Dir[data_folder.join("*.csv")].map do |full_path_filename|
|
||||
full_path_filename.split("/").last
|
||||
end
|
||||
(json + csv - excluded).sort
|
||||
@@ -152,7 +163,7 @@ class MachineLearning
|
||||
end
|
||||
|
||||
def data_path(filename)
|
||||
"/machine_learning/data/" + filename
|
||||
"/#{tenant_data_folder}/#{filename}"
|
||||
end
|
||||
|
||||
def script_kinds
|
||||
@@ -196,29 +207,35 @@ class MachineLearning
|
||||
private
|
||||
|
||||
def create_data_folder
|
||||
FileUtils.mkdir_p DATA_FOLDER
|
||||
FileUtils.mkdir_p data_folder
|
||||
end
|
||||
|
||||
def export_proposals_to_json
|
||||
create_data_folder
|
||||
filename = DATA_FOLDER.join(MachineLearning.proposals_filename)
|
||||
filename = data_folder.join(MachineLearning.proposals_filename)
|
||||
Proposal::Exporter.new.to_json_file(filename)
|
||||
end
|
||||
|
||||
def export_budget_investments_to_json
|
||||
create_data_folder
|
||||
filename = DATA_FOLDER.join(MachineLearning.investments_filename)
|
||||
filename = data_folder.join(MachineLearning.investments_filename)
|
||||
Budget::Investment::Exporter.new(Array.new).to_json_file(filename)
|
||||
end
|
||||
|
||||
def export_comments_to_json
|
||||
create_data_folder
|
||||
filename = DATA_FOLDER.join(MachineLearning.comments_filename)
|
||||
filename = data_folder.join(MachineLearning.comments_filename)
|
||||
Comment::Exporter.new.to_json_file(filename)
|
||||
end
|
||||
|
||||
def run_machine_learning_scripts
|
||||
output = `cd #{SCRIPTS_FOLDER} && python #{job.script} 2>&1`
|
||||
command = if Tenant.default?
|
||||
"python #{job.script}"
|
||||
else
|
||||
"CONSUL_TENANT=#{Tenant.current_schema} python #{job.script}"
|
||||
end
|
||||
|
||||
output = `cd #{SCRIPTS_FOLDER} && #{command} 2>&1`
|
||||
result = $?.success?
|
||||
if result == false
|
||||
job.update!(finished_at: Time.current, error: output)
|
||||
@@ -254,7 +271,7 @@ class MachineLearning
|
||||
end
|
||||
|
||||
def import_ml_proposals_comments_summary
|
||||
json_file = DATA_FOLDER.join(MachineLearning.proposals_comments_summary_filename)
|
||||
json_file = data_folder.join(MachineLearning.proposals_comments_summary_filename)
|
||||
json_data = JSON.parse(File.read(json_file)).each(&:deep_symbolize_keys!)
|
||||
json_data.each do |attributes|
|
||||
attributes.delete(:id)
|
||||
@@ -266,7 +283,7 @@ class MachineLearning
|
||||
end
|
||||
|
||||
def import_ml_investments_comments_summary
|
||||
json_file = DATA_FOLDER.join(MachineLearning.investments_comments_summary_filename)
|
||||
json_file = data_folder.join(MachineLearning.investments_comments_summary_filename)
|
||||
json_data = JSON.parse(File.read(json_file)).each(&:deep_symbolize_keys!)
|
||||
json_data.each do |attributes|
|
||||
attributes.delete(:id)
|
||||
@@ -278,7 +295,7 @@ class MachineLearning
|
||||
end
|
||||
|
||||
def import_proposals_related_content
|
||||
json_file = DATA_FOLDER.join(MachineLearning.proposals_related_filename)
|
||||
json_file = data_folder.join(MachineLearning.proposals_related_filename)
|
||||
json_data = JSON.parse(File.read(json_file)).each(&:deep_symbolize_keys!)
|
||||
json_data.each do |related|
|
||||
id = related.delete(:id)
|
||||
@@ -306,7 +323,7 @@ class MachineLearning
|
||||
end
|
||||
|
||||
def import_budget_investments_related_content
|
||||
json_file = DATA_FOLDER.join(MachineLearning.investments_related_filename)
|
||||
json_file = data_folder.join(MachineLearning.investments_related_filename)
|
||||
json_data = JSON.parse(File.read(json_file)).each(&:deep_symbolize_keys!)
|
||||
json_data.each do |related|
|
||||
id = related.delete(:id)
|
||||
@@ -335,7 +352,7 @@ class MachineLearning
|
||||
|
||||
def import_ml_proposals_tags
|
||||
ids = {}
|
||||
json_file = DATA_FOLDER.join(MachineLearning.proposals_tags_filename)
|
||||
json_file = data_folder.join(MachineLearning.proposals_tags_filename)
|
||||
json_data = JSON.parse(File.read(json_file)).each(&:deep_symbolize_keys!)
|
||||
json_data.each do |attributes|
|
||||
if attributes[:name].present?
|
||||
@@ -348,7 +365,7 @@ class MachineLearning
|
||||
end
|
||||
end
|
||||
|
||||
json_file = DATA_FOLDER.join(MachineLearning.proposals_taggings_filename)
|
||||
json_file = data_folder.join(MachineLearning.proposals_taggings_filename)
|
||||
json_data = JSON.parse(File.read(json_file)).each(&:deep_symbolize_keys!)
|
||||
json_data.each do |attributes|
|
||||
if attributes[:tag_id].present?
|
||||
@@ -365,7 +382,7 @@ class MachineLearning
|
||||
|
||||
def import_ml_investments_tags
|
||||
ids = {}
|
||||
json_file = DATA_FOLDER.join(MachineLearning.investments_tags_filename)
|
||||
json_file = data_folder.join(MachineLearning.investments_tags_filename)
|
||||
json_data = JSON.parse(File.read(json_file)).each(&:deep_symbolize_keys!)
|
||||
json_data.each do |attributes|
|
||||
if attributes[:name].present?
|
||||
@@ -378,7 +395,7 @@ class MachineLearning
|
||||
end
|
||||
end
|
||||
|
||||
json_file = DATA_FOLDER.join(MachineLearning.investments_taggings_filename)
|
||||
json_file = data_folder.join(MachineLearning.investments_taggings_filename)
|
||||
json_data = JSON.parse(File.read(json_file)).each(&:deep_symbolize_keys!)
|
||||
json_data.each do |attributes|
|
||||
if attributes[:tag_id].present?
|
||||
@@ -421,13 +438,13 @@ class MachineLearning
|
||||
end
|
||||
|
||||
def last_modified_date_for(filename)
|
||||
return nil unless File.exists? DATA_FOLDER.join(filename)
|
||||
return nil unless File.exists? data_folder.join(filename)
|
||||
|
||||
File.mtime DATA_FOLDER.join(filename)
|
||||
File.mtime data_folder.join(filename)
|
||||
end
|
||||
|
||||
def updated_file?(filename)
|
||||
return false unless File.exists? DATA_FOLDER.join(filename)
|
||||
return false unless File.exists? data_folder.join(filename)
|
||||
return true unless previous_modified_date[filename].present?
|
||||
|
||||
last_modified_date_for(filename) > previous_modified_date[filename]
|
||||
|
||||
125
app/models/tenant.rb
Normal file
125
app/models/tenant.rb
Normal file
@@ -0,0 +1,125 @@
|
||||
class Tenant < ApplicationRecord
|
||||
validates :schema,
|
||||
presence: true,
|
||||
uniqueness: true,
|
||||
exclusion: { in: ->(*) { excluded_subdomains }},
|
||||
format: { with: URI::DEFAULT_PARSER.regexp[:HOST] }
|
||||
validates :name, presence: true, uniqueness: true
|
||||
|
||||
after_create :create_schema
|
||||
after_update :rename_schema
|
||||
after_destroy :destroy_schema
|
||||
|
||||
def self.resolve_host(host)
|
||||
return nil unless Rails.application.config.multitenancy.present?
|
||||
return nil if host.blank? || host.match?(Resolv::AddressRegex)
|
||||
|
||||
host_domain = allowed_domains.find { |domain| host == domain || host.ends_with?(".#{domain}") }
|
||||
host.delete_prefix("www.").sub(/\.?#{host_domain}\Z/, "").presence
|
||||
end
|
||||
|
||||
def self.allowed_domains
|
||||
dev_domains = %w[localhost lvh.me example.com]
|
||||
dev_domains + [default_host]
|
||||
end
|
||||
|
||||
def self.excluded_subdomains
|
||||
%w[mail public shared_extensions www]
|
||||
end
|
||||
|
||||
def self.default_url_options
|
||||
ActionMailer::Base.default_url_options
|
||||
end
|
||||
|
||||
def self.default_host
|
||||
default_url_options[:host]
|
||||
end
|
||||
|
||||
def self.current_url_options
|
||||
default_url_options.merge(host: current_host)
|
||||
end
|
||||
|
||||
def self.current_host
|
||||
host_for(current_schema)
|
||||
end
|
||||
|
||||
def self.host_for(schema)
|
||||
if schema == "public"
|
||||
default_host
|
||||
elsif default_host == "localhost"
|
||||
"#{schema}.lvh.me"
|
||||
else
|
||||
"#{schema}.#{default_host}"
|
||||
end
|
||||
end
|
||||
|
||||
def self.current_secrets
|
||||
if default?
|
||||
Rails.application.secrets
|
||||
else
|
||||
@secrets ||= {}
|
||||
@cached_rails_secrets ||= Rails.application.secrets
|
||||
|
||||
if @cached_rails_secrets != Rails.application.secrets
|
||||
@secrets = {}
|
||||
@cached_rails_secrets = nil
|
||||
end
|
||||
|
||||
@secrets[current_schema] ||= Rails.application.secrets.merge(
|
||||
Rails.application.secrets.dig(:tenants, current_schema.to_sym).to_h
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def self.subfolder_path
|
||||
if default?
|
||||
""
|
||||
else
|
||||
File.join("tenants", current_schema)
|
||||
end
|
||||
end
|
||||
|
||||
def self.path_with_subfolder(filename_or_folder)
|
||||
File.join(subfolder_path, filename_or_folder).delete_prefix("/")
|
||||
end
|
||||
|
||||
def self.default?
|
||||
current_schema == "public"
|
||||
end
|
||||
|
||||
def self.current_schema
|
||||
Apartment::Tenant.current
|
||||
end
|
||||
|
||||
def self.switch(...)
|
||||
Apartment::Tenant.switch(...)
|
||||
end
|
||||
|
||||
def self.run_on_each(&block)
|
||||
["public"].union(Apartment.tenant_names).each do |schema|
|
||||
switch(schema, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def host
|
||||
self.class.host_for(schema)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_schema
|
||||
Apartment::Tenant.create(schema)
|
||||
end
|
||||
|
||||
def rename_schema
|
||||
if saved_change_to_schema?
|
||||
ActiveRecord::Base.connection.execute(
|
||||
"ALTER SCHEMA \"#{schema_before_last_save}\" RENAME TO \"#{schema}\";"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_schema
|
||||
Apartment::Tenant.drop(schema)
|
||||
end
|
||||
end
|
||||
1
app/views/admin/tenants/edit.html.erb
Normal file
1
app/views/admin/tenants/edit.html.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= render Admin::Tenants::EditComponent.new(@tenant) %>
|
||||
1
app/views/admin/tenants/index.html.erb
Normal file
1
app/views/admin/tenants/index.html.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= render Admin::Tenants::IndexComponent.new(@tenants) %>
|
||||
1
app/views/admin/tenants/new.html.erb
Normal file
1
app/views/admin/tenants/new.html.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= render Admin::Tenants::NewComponent.new(@tenant) %>
|
||||
@@ -1,6 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html <%= "dir=rtl" if rtl? %> lang="<%= I18n.locale %>">
|
||||
|
||||
<html <%= common_html_attributes %>>
|
||||
<head>
|
||||
<%= render "layouts/common_head", default_title: "Admin" %>
|
||||
<%= content_for :head %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html <%= "dir=rtl" if rtl? %> lang="<%= I18n.locale %>" data-current-user-id="<%= current_user&.id %>">
|
||||
<html <%= common_html_attributes %> data-current-user-id="<%= current_user&.id %>">
|
||||
<head>
|
||||
<%= render "layouts/common_head", default_title: setting["org_name"] %>
|
||||
<%= render "layouts/meta_tags" %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="<%= I18n.locale %>" data-current-user-id="<%= current_user&.id %>">
|
||||
<html <%= common_html_attributes %> data-current-user-id="<%= current_user&.id %>">
|
||||
<head>
|
||||
<%= render "layouts/common_head", default_title: setting["org_name"] %>
|
||||
<%= render "layouts/meta_tags" %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html <%= "dir=rtl" if rtl? %> lang="<%= I18n.locale %>">
|
||||
<html <%= common_html_attributes %>>
|
||||
<head>
|
||||
<%= render "layouts/common_head", default_title: setting["org_name"] %>
|
||||
<%= render "layouts/meta_tags" %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html lang="<%= I18n.locale %>">
|
||||
<html <%= common_html_attributes %>>
|
||||
<head>
|
||||
<title><%= t("mailers.title") %></title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html <%= "dir=rtl" if rtl? %> lang="<%= I18n.locale %>">
|
||||
|
||||
<html <%= common_html_attributes %>>
|
||||
<head>
|
||||
<%= render "layouts/common_head", default_title: "Management" %>
|
||||
<%= stylesheet_link_tag "print", media: "print" %>
|
||||
|
||||
@@ -13,3 +13,5 @@ Disallow: /*?*search
|
||||
Disallow: /*?*locale-switcher
|
||||
Disallow: /*?*filter
|
||||
Disallow: user_id
|
||||
|
||||
Sitemap: <%= "#{root_url}#{Tenant.path_with_subfolder("sitemap.xml")}" %>
|
||||
@@ -17,4 +17,4 @@
|
||||
<meta id="ogimage" property="og:image" content="<%= root_url + (local_assigns[:og_image_url] || "social_media_icon.png") %>" />
|
||||
<meta property="og:site_name" content="<%= setting["org_name"] %>" />
|
||||
<meta id="ogdescription" property="og:description" content="<%= description %>" />
|
||||
<meta property="fb:app_id" content="<%= Rails.application.secrets.facebook_key %>" />
|
||||
<meta property="fb:app_id" content="<%= Tenant.current_secrets.facebook_key %>" />
|
||||
|
||||
@@ -134,6 +134,9 @@ module Consul
|
||||
config.autoload_paths << "#{Rails.root}/app/graphql/custom"
|
||||
config.autoload_paths << "#{Rails.root}/app/models/custom"
|
||||
config.paths["app/views"].unshift(Rails.root.join("app", "views", "custom"))
|
||||
|
||||
# Set to true to enable managing different tenants using the same application
|
||||
config.multitenancy = Rails.application.secrets.multitenancy
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ default: &default
|
||||
host: database #<--the name of the db in the docker-compose
|
||||
pool: 5
|
||||
port: 5432
|
||||
schema_search_path: "public,shared_extensions"
|
||||
username: postgres
|
||||
password: <%= ENV["POSTGRES_PASSWORD"] %>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ default: &default
|
||||
encoding: unicode
|
||||
host: localhost
|
||||
pool: 5
|
||||
schema_search_path: "public,shared_extensions"
|
||||
username:
|
||||
password:
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ default: &default
|
||||
encoding: unicode
|
||||
host: postgres
|
||||
pool: 5
|
||||
schema_search_path: "public,shared_extensions"
|
||||
username: consul
|
||||
password:
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Rails.application.configure do
|
||||
config.action_controller.perform_caching = true
|
||||
config.action_controller.enable_fragment_cache_logging = true
|
||||
|
||||
config.cache_store = :memory_store
|
||||
config.cache_store = :memory_store, { namespace: proc { Tenant.current_schema }}
|
||||
config.public_file_server.headers = {
|
||||
"Cache-Control" => "public, max-age=#{2.days.to_i}"
|
||||
}
|
||||
@@ -28,6 +28,10 @@ Rails.application.configure do
|
||||
config.cache_store = :null_store
|
||||
end
|
||||
|
||||
# Allow accessing the application through a domain so subdomains can be used
|
||||
config.hosts << "lvh.me"
|
||||
config.hosts << /.*\.lvh\.me/
|
||||
|
||||
# Don't care if the mailer can't send.
|
||||
config.action_mailer.raise_delivery_errors = false
|
||||
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
|
||||
|
||||
@@ -58,7 +58,7 @@ Rails.application.configure do
|
||||
config.log_tags = [:request_id]
|
||||
|
||||
# Use a different cache store in production.
|
||||
config.cache_store = :mem_cache_store
|
||||
config.cache_store = :mem_cache_store, { namespace: proc { Tenant.current_schema }}
|
||||
|
||||
# Use a real queuing backend for Active Job (and separate queues per environment).
|
||||
# config.active_job.queue_adapter = :resque
|
||||
|
||||
@@ -58,7 +58,7 @@ Rails.application.configure do
|
||||
config.log_tags = [:request_id]
|
||||
|
||||
# Use a different cache store in production.
|
||||
config.cache_store = :mem_cache_store
|
||||
config.cache_store = :mem_cache_store, { namespace: proc { Tenant.current_schema }}
|
||||
|
||||
# Use a real queuing backend for Active Job (and separate queues per environment).
|
||||
# config.active_job.queue_adapter = :resque
|
||||
|
||||
@@ -59,6 +59,9 @@ Rails.application.configure do
|
||||
Bullet.raise = true # raise an error if n+1 query occurs
|
||||
end
|
||||
end
|
||||
|
||||
# Allow managing different tenants using the same application
|
||||
config.multitenancy = true
|
||||
end
|
||||
|
||||
require Rails.root.join("config", "environments", "custom", "test")
|
||||
|
||||
117
config/initializers/apartment.rb
Normal file
117
config/initializers/apartment.rb
Normal file
@@ -0,0 +1,117 @@
|
||||
# You can have Apartment route to the appropriate Tenant by adding some Rack middleware.
|
||||
# Apartment can support many different "Elevators" that can take care of this routing to your data.
|
||||
# Require whichever Elevator you're using below or none if you have a custom one.
|
||||
#
|
||||
require "apartment/elevators/generic"
|
||||
# require "apartment/elevators/domain"
|
||||
# require "apartment/elevators/subdomain"
|
||||
# require "apartment/elevators/first_subdomain"
|
||||
# require "apartment/elevators/host"
|
||||
|
||||
#
|
||||
# Apartment Configuration
|
||||
#
|
||||
Apartment.configure do |config|
|
||||
ENV["IGNORE_EMPTY_TENANTS"] = "true" if Rails.env.test? || Rails.application.config.multitenancy.blank?
|
||||
config.seed_after_create = !Rails.env.test?
|
||||
|
||||
# Add any models that you do not want to be multi-tenanted, but remain in the global (public) namespace.
|
||||
# A typical example would be a Customer or Tenant model that stores each Tenant's information.
|
||||
#
|
||||
config.excluded_models = %w[Delayed::Job Tenant]
|
||||
|
||||
# In order to migrate all of your Tenants you need to provide a list of Tenant names to Apartment.
|
||||
# You can make this dynamic by providing a Proc object to be called on migrations.
|
||||
# This object should yield either:
|
||||
# - an array of strings representing each Tenant name.
|
||||
# - a hash which keys are tenant names, and values custom db config
|
||||
# (must contain all key/values required in database.yml)
|
||||
#
|
||||
# config.tenant_names = lambda{ Customer.pluck(:tenant_name) }
|
||||
# config.tenant_names = ["tenant1", "tenant2"]
|
||||
# config.tenant_names = {
|
||||
# "tenant1" => {
|
||||
# adapter: "postgresql",
|
||||
# host: "some_server",
|
||||
# port: 5555,
|
||||
# database: "postgres" # this is not the name of the tenant's db
|
||||
# # but the name of the database to connect to before creating the tenant's db
|
||||
# # mandatory in postgresql
|
||||
# },
|
||||
# "tenant2" => {
|
||||
# adapter: "postgresql",
|
||||
# database: "postgres" # this is not the name of the tenant's db
|
||||
# # but the name of the database to connect to before creating the tenant's db
|
||||
# # mandatory in postgresql
|
||||
# }
|
||||
# }
|
||||
# config.tenant_names = lambda do
|
||||
# Tenant.all.each_with_object({}) do |tenant, hash|
|
||||
# hash[tenant.name] = tenant.db_configuration
|
||||
# end
|
||||
# end
|
||||
#
|
||||
config.tenant_names = -> { Tenant.pluck :schema }
|
||||
|
||||
# PostgreSQL:
|
||||
# Specifies whether to use PostgreSQL schemas or create a new database per Tenant.
|
||||
#
|
||||
# MySQL:
|
||||
# Specifies whether to switch databases by using `use` statement or re-establish connection.
|
||||
#
|
||||
# The default behaviour is true.
|
||||
#
|
||||
# config.use_schemas = true
|
||||
|
||||
#
|
||||
# ==> PostgreSQL only options
|
||||
|
||||
# Apartment can be forced to use raw SQL dumps instead of schema.rb for creating new schemas.
|
||||
# Use this when you are using some extra features in PostgreSQL that can't be represented in
|
||||
# schema.rb, like materialized views etc. (only applies with use_schemas set to true).
|
||||
# (Note: this option doesn't use db/structure.sql, it creates SQL dump by executing pg_dump)
|
||||
#
|
||||
# config.use_sql = false
|
||||
|
||||
# There are cases where you might want some schemas to always be in your search_path
|
||||
# e.g when using a PostgreSQL extension like hstore.
|
||||
# Any schemas added here will be available along with your selected Tenant.
|
||||
#
|
||||
config.persistent_schemas = ["shared_extensions"]
|
||||
|
||||
# <== PostgreSQL only options
|
||||
#
|
||||
|
||||
# By default, and only when not using PostgreSQL schemas, Apartment will prepend the environment
|
||||
# to the tenant name to ensure there is no conflict between your environments.
|
||||
# This is mainly for the benefit of your development and test environments.
|
||||
# Uncomment the line below if you want to disable this behaviour in production.
|
||||
#
|
||||
# config.prepend_environment = !Rails.env.production?
|
||||
|
||||
# When using PostgreSQL schemas, the database dump will be namespaced, and
|
||||
# apartment will substitute the default namespace (usually public) with the
|
||||
# name of the new tenant when creating a new tenant. Some items must maintain
|
||||
# a reference to the default namespace (ie public) - for instance, a default
|
||||
# uuid generation. Uncomment the line below to create a list of namespaced
|
||||
# items in the schema dump that should *not* have their namespace replaced by
|
||||
# the new tenant
|
||||
#
|
||||
# config.pg_excluded_names = ["uuid_generate_v4"]
|
||||
|
||||
# Specifies whether the database and schema (when using PostgreSQL schemas) will prepend in ActiveRecord log.
|
||||
# Uncomment the line below if you want to enable this behavior.
|
||||
#
|
||||
# config.active_record_log = true
|
||||
end
|
||||
|
||||
# Setup a custom Tenant switching middleware. The Proc should return the name of the Tenant that
|
||||
# you want to switch to.
|
||||
Rails.application.config.middleware.insert_before Warden::Manager, Apartment::Elevators::Generic, ->(request) do
|
||||
Tenant.resolve_host(request.host)
|
||||
end
|
||||
|
||||
# Rails.application.config.middleware.use Apartment::Elevators::Domain
|
||||
# Rails.application.config.middleware.use Apartment::Elevators::Subdomain
|
||||
# Rails.application.config.middleware.use Apartment::Elevators::FirstSubdomain
|
||||
# Rails.application.config.middleware.use Apartment::Elevators::Host
|
||||
@@ -14,3 +14,15 @@ Delayed::Worker.read_ahead = 10
|
||||
Delayed::Worker.default_queue_name = "default"
|
||||
Delayed::Worker.raise_signal_exceptions = :term
|
||||
Delayed::Worker.logger = Logger.new(File.join(Rails.root, "log", "delayed_job.log"))
|
||||
|
||||
class ApartmentDelayedJobPlugin < Delayed::Plugin
|
||||
callbacks do |lifecycle|
|
||||
lifecycle.before(:enqueue) { |job| job.tenant = Tenant.current_schema }
|
||||
|
||||
lifecycle.around(:perform) do |worker, job, *args, &block|
|
||||
Tenant.switch(job.tenant) { block.call(worker, job, *args) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Delayed::Worker.plugins << ApartmentDelayedJobPlugin
|
||||
|
||||
@@ -245,14 +245,26 @@ Devise.setup do |config|
|
||||
# Add a new OmniAuth provider. Check the wiki for more information on setting
|
||||
# up on your models and hooks.
|
||||
# config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
|
||||
config.omniauth :twitter, Rails.application.secrets.twitter_key, Rails.application.secrets.twitter_secret
|
||||
config.omniauth :facebook, Rails.application.secrets.facebook_key, Rails.application.secrets.facebook_secret, scope: "email", info_fields: "email,name,verified"
|
||||
config.omniauth :google_oauth2, Rails.application.secrets.google_oauth2_key, Rails.application.secrets.google_oauth2_secret
|
||||
config.omniauth :twitter,
|
||||
Rails.application.secrets.twitter_key,
|
||||
Rails.application.secrets.twitter_secret,
|
||||
setup: OmniauthTenantSetup.twitter
|
||||
config.omniauth :facebook,
|
||||
Rails.application.secrets.facebook_key,
|
||||
Rails.application.secrets.facebook_secret,
|
||||
scope: "email",
|
||||
info_fields: "email,name,verified",
|
||||
setup: OmniauthTenantSetup.facebook
|
||||
config.omniauth :google_oauth2,
|
||||
Rails.application.secrets.google_oauth2_key,
|
||||
Rails.application.secrets.google_oauth2_secret,
|
||||
setup: OmniauthTenantSetup.google_oauth2
|
||||
config.omniauth :wordpress_oauth2,
|
||||
Rails.application.secrets.wordpress_oauth2_key,
|
||||
Rails.application.secrets.wordpress_oauth2_secret,
|
||||
strategy_class: OmniAuth::Strategies::Wordpress,
|
||||
client_options: { site: Rails.application.secrets.wordpress_oauth2_site }
|
||||
client_options: { site: Rails.application.secrets.wordpress_oauth2_site },
|
||||
setup: OmniauthTenantSetup.wordpress_oauth2
|
||||
|
||||
# ==> Warden configuration
|
||||
# If you want to use other strategies, that are not supported by Devise, or
|
||||
|
||||
@@ -124,6 +124,9 @@ en:
|
||||
images:
|
||||
one: "Image"
|
||||
other: "Images"
|
||||
tenant:
|
||||
one: "tenant"
|
||||
other: "tenants"
|
||||
topic:
|
||||
one: "Topic"
|
||||
other: "Topics"
|
||||
@@ -378,6 +381,8 @@ en:
|
||||
body: Body
|
||||
tag:
|
||||
name: "Type the name of the topic"
|
||||
tenant:
|
||||
schema: "Subdomain"
|
||||
topic:
|
||||
title: "Title"
|
||||
description: "Initial text"
|
||||
|
||||
@@ -785,6 +785,7 @@ en:
|
||||
comments: "Comments"
|
||||
local_census_records: Manage local census
|
||||
machine_learning: "AI / Machine learning"
|
||||
multitenancy: Multitenancy
|
||||
administrators:
|
||||
index:
|
||||
title: Administrators
|
||||
@@ -1628,6 +1629,15 @@ en:
|
||||
notice: "Card updated successfully"
|
||||
destroy:
|
||||
notice: "Card removed successfully"
|
||||
tenants:
|
||||
create:
|
||||
notice: Tenant created successfully
|
||||
index:
|
||||
create: Create tenant
|
||||
new:
|
||||
title: New tenant
|
||||
update:
|
||||
notice: Tenant updated successfully
|
||||
homepage:
|
||||
title: Homepage
|
||||
description: The active modules appear in the homepage in the same order as here.
|
||||
|
||||
@@ -124,6 +124,9 @@ es:
|
||||
images:
|
||||
one: "Imagen"
|
||||
other: "Imágenes"
|
||||
tenant:
|
||||
one: "entidad"
|
||||
other: "entidades"
|
||||
topic:
|
||||
one: "Tema"
|
||||
other: "Temas"
|
||||
@@ -378,6 +381,8 @@ es:
|
||||
body: Contenido
|
||||
tag:
|
||||
name: "Escribe el nombre del tema"
|
||||
tenant:
|
||||
schema: "Subdominio"
|
||||
topic:
|
||||
title: "Título"
|
||||
description: "Texto inicial"
|
||||
|
||||
@@ -784,6 +784,7 @@ es:
|
||||
comments: "Comentarios"
|
||||
local_census_records: Gestionar censo local
|
||||
machine_learning: "IA / Machine learning"
|
||||
multitenancy: Multientidad
|
||||
administrators:
|
||||
index:
|
||||
title: Administradores
|
||||
@@ -1627,6 +1628,15 @@ es:
|
||||
notice: "Tarjeta actualizada con éxito"
|
||||
destroy:
|
||||
notice: "Tarjeta eliminada con éxito"
|
||||
tenants:
|
||||
create:
|
||||
notice: Entidad creada correctamente
|
||||
index:
|
||||
create: Crear entidad
|
||||
new:
|
||||
title: Nueva entidad
|
||||
update:
|
||||
notice: Entidad actualizada correctamente
|
||||
homepage:
|
||||
title: Homepage
|
||||
description: Los módulos activos aparecerán en la homepage en el mismo orden que aquí.
|
||||
|
||||
@@ -30,6 +30,7 @@ Rails.application.routes.draw do
|
||||
root "welcome#index"
|
||||
get "/welcome", to: "welcome#welcome"
|
||||
get "/consul.json", to: "installation#details"
|
||||
get "robots.txt", to: "robots#index"
|
||||
|
||||
resources :stats, only: [:index]
|
||||
resources :images, only: [:destroy]
|
||||
|
||||
@@ -277,6 +277,8 @@ namespace :admin do
|
||||
post :execute, on: :collection
|
||||
delete :cancel, on: :collection
|
||||
end
|
||||
|
||||
resources :tenants, except: [:show, :destroy]
|
||||
end
|
||||
|
||||
resolve "Milestone" do |milestone|
|
||||
|
||||
@@ -24,7 +24,7 @@ every 1.minute do
|
||||
end
|
||||
|
||||
every 1.day, at: "5:00 am" do
|
||||
rake "-s sitemap:refresh"
|
||||
rake "-s sitemap:refresh:no_ping"
|
||||
end
|
||||
|
||||
every 2.hours do
|
||||
|
||||
@@ -18,6 +18,7 @@ http_basic_auth: &http_basic_auth
|
||||
development:
|
||||
http_basic_username: "dev"
|
||||
http_basic_password: "pass"
|
||||
multitenancy: false
|
||||
secret_key_base: 56792feef405a59b18ea7db57b4777e855103882b926413d4afdfb8c0ea8aa86ea6649da4e729c5f5ae324c0ab9338f789174cf48c544173bc18fdc3b14262e4
|
||||
<<: *maps
|
||||
|
||||
@@ -48,6 +49,16 @@ staging:
|
||||
http_basic_password: ""
|
||||
managers_url: ""
|
||||
managers_application_key: ""
|
||||
multitenancy: false
|
||||
tenants:
|
||||
# If you've enabled multitenancy, you can overwrite secrets for a
|
||||
# specific tenant with:
|
||||
#
|
||||
# my_tenant_subdomain:
|
||||
# secret_key: my_secret_value
|
||||
#
|
||||
# Currently you can overwrite SMTP, SMS, manager, microsoft API,
|
||||
# HTTP basic, twitter, facebook, google and wordpress settings.
|
||||
<<: *maps
|
||||
<<: *apis
|
||||
|
||||
@@ -74,6 +85,16 @@ preproduction:
|
||||
http_basic_password: ""
|
||||
managers_url: ""
|
||||
managers_application_key: ""
|
||||
multitenancy: false
|
||||
tenants:
|
||||
# If you've enabled multitenancy, you can overwrite secrets for a
|
||||
# specific tenant with:
|
||||
#
|
||||
# my_tenant_subdomain:
|
||||
# secret_key: my_secret_value
|
||||
#
|
||||
# Currently you can overwrite SMTP, SMS, manager, microsoft API,
|
||||
# HTTP basic, twitter, facebook, google and wordpress settings.
|
||||
twitter_key: ""
|
||||
twitter_secret: ""
|
||||
facebook_key: ""
|
||||
@@ -105,6 +126,16 @@ production:
|
||||
http_basic_password: ""
|
||||
managers_url: ""
|
||||
managers_application_key: ""
|
||||
multitenancy: false
|
||||
tenants:
|
||||
# If you've enabled multitenancy, you can overwrite secrets for a
|
||||
# specific tenant with:
|
||||
#
|
||||
# my_tenant_subdomain:
|
||||
# secret_key: my_secret_value
|
||||
#
|
||||
# Currently you can overwrite SMTP, SMS, manager, microsoft API,
|
||||
# HTTP basic, twitter, facebook, google and wordpress settings.
|
||||
twitter_key: ""
|
||||
twitter_secret: ""
|
||||
facebook_key: ""
|
||||
|
||||
@@ -6,49 +6,54 @@ class SitemapGenerator::FileAdapter
|
||||
end
|
||||
end
|
||||
SitemapGenerator::Sitemap.namer = SitemapGenerator::SimpleNamer.new(:sitemap, extension: ".xml")
|
||||
|
||||
# default host
|
||||
SitemapGenerator::Sitemap.verbose = false if Rails.env.test?
|
||||
SitemapGenerator::Sitemap.default_host = root_url(ActionMailer::Base.default_url_options)
|
||||
|
||||
# sitemap generator
|
||||
SitemapGenerator::Sitemap.create do
|
||||
add help_path
|
||||
add how_to_use_path
|
||||
add faq_path
|
||||
Tenant.run_on_each do
|
||||
SitemapGenerator::Sitemap.default_host = root_url(Tenant.current_url_options)
|
||||
SitemapGenerator::Sitemap.sitemaps_path = Tenant.subfolder_path
|
||||
|
||||
if Setting["process.debates"]
|
||||
add debates_path, priority: 0.7, changefreq: "daily"
|
||||
Debate.find_each do |debate|
|
||||
add debate_path(debate), lastmod: debate.updated_at
|
||||
SitemapGenerator::Sitemap.create do
|
||||
add help_path
|
||||
add how_to_use_path
|
||||
add faq_path
|
||||
|
||||
if Setting["process.debates"]
|
||||
add debates_path, priority: 0.7, changefreq: "daily"
|
||||
Debate.find_each do |debate|
|
||||
add debate_path(debate), lastmod: debate.updated_at
|
||||
end
|
||||
end
|
||||
|
||||
if Setting["process.proposals"]
|
||||
add proposals_path, priority: 0.7, changefreq: "daily"
|
||||
Proposal.find_each do |proposal|
|
||||
add proposal_path(proposal), lastmod: proposal.updated_at
|
||||
end
|
||||
end
|
||||
|
||||
if Setting["process.budgets"]
|
||||
add budgets_path, priority: 0.7, changefreq: "daily"
|
||||
Budget.find_each do |budget|
|
||||
add budget_path(budget), lastmod: budget.updated_at
|
||||
end
|
||||
end
|
||||
|
||||
if Setting["process.polls"]
|
||||
add polls_path, priority: 0.7, changefreq: "daily"
|
||||
Poll.find_each do |poll|
|
||||
add poll_path(poll), lastmod: poll.starts_at
|
||||
end
|
||||
end
|
||||
|
||||
if Setting["process.legislation"]
|
||||
add legislation_processes_path, priority: 0.7, changefreq: "daily"
|
||||
Legislation::Process.find_each do |process|
|
||||
add legislation_process_path(process), lastmod: process.start_date
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if Setting["process.proposals"]
|
||||
add proposals_path, priority: 0.7, changefreq: "daily"
|
||||
Proposal.find_each do |proposal|
|
||||
add proposal_path(proposal), lastmod: proposal.updated_at
|
||||
end
|
||||
end
|
||||
|
||||
if Setting["process.budgets"]
|
||||
add budgets_path, priority: 0.7, changefreq: "daily"
|
||||
Budget.find_each do |budget|
|
||||
add budget_path(budget), lastmod: budget.updated_at
|
||||
end
|
||||
end
|
||||
|
||||
if Setting["process.polls"]
|
||||
add polls_path, priority: 0.7, changefreq: "daily"
|
||||
Poll.find_each do |poll|
|
||||
add poll_path(poll), lastmod: poll.starts_at
|
||||
end
|
||||
end
|
||||
|
||||
if Setting["process.legislation"]
|
||||
add legislation_processes_path, priority: 0.7, changefreq: "daily"
|
||||
Legislation::Process.find_each do |process|
|
||||
add legislation_process_path(process), lastmod: process.start_date
|
||||
end
|
||||
unless Rails.env.development? || Rails.env.test?
|
||||
SitemapGenerator::Sitemap.ping_search_engines
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
local:
|
||||
service: Disk
|
||||
service: TenantDisk
|
||||
root: <%= Rails.root.join("storage") %>
|
||||
|
||||
test:
|
||||
service: Disk
|
||||
service: TenantDisk
|
||||
root: <%= Rails.root.join("tmp/storage") %>
|
||||
|
||||
# s3:
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
ActiveRecord::Tasks::DatabaseTasks.truncate_all unless Rails.env.test?
|
||||
unless Rails.env.test?
|
||||
Tenant.destroy_all if Tenant.default?
|
||||
ActiveRecord::Tasks::DatabaseTasks.truncate_all
|
||||
end
|
||||
|
||||
@logger = Logger.new(STDOUT)
|
||||
@logger.formatter = proc do |_severity, _datetime, _progname, msg|
|
||||
msg unless @avoid_log
|
||||
msg unless Rails.env.test?
|
||||
end
|
||||
|
||||
def load_dev_seeds(dev_seeds_file)
|
||||
load Rails.root.join("db", "dev_seeds", "#{dev_seeds_file}.rb")
|
||||
end
|
||||
|
||||
def section(section_title)
|
||||
@logger.info section_title
|
||||
yield
|
||||
@@ -28,28 +36,30 @@ def random_locales_attributes(**attribute_names_with_values)
|
||||
end
|
||||
end
|
||||
|
||||
require_relative "dev_seeds/settings"
|
||||
require_relative "dev_seeds/geozones"
|
||||
require_relative "dev_seeds/users"
|
||||
require_relative "dev_seeds/tags_categories"
|
||||
require_relative "dev_seeds/debates"
|
||||
require_relative "dev_seeds/proposals"
|
||||
require_relative "dev_seeds/budgets"
|
||||
require_relative "dev_seeds/comments"
|
||||
require_relative "dev_seeds/votes"
|
||||
require_relative "dev_seeds/flags"
|
||||
require_relative "dev_seeds/hiddings"
|
||||
require_relative "dev_seeds/banners"
|
||||
require_relative "dev_seeds/polls"
|
||||
require_relative "dev_seeds/communities"
|
||||
require_relative "dev_seeds/legislation_processes"
|
||||
require_relative "dev_seeds/newsletters"
|
||||
require_relative "dev_seeds/notifications"
|
||||
require_relative "dev_seeds/widgets"
|
||||
require_relative "dev_seeds/admin_notifications"
|
||||
require_relative "dev_seeds/legislation_proposals"
|
||||
require_relative "dev_seeds/milestones"
|
||||
require_relative "dev_seeds/pages"
|
||||
require_relative "dev_seeds/sdg"
|
||||
log "Creating dev seeds for tenant #{Tenant.current_schema}" unless Tenant.default?
|
||||
|
||||
load_dev_seeds "settings"
|
||||
load_dev_seeds "geozones"
|
||||
load_dev_seeds "users"
|
||||
load_dev_seeds "tags_categories"
|
||||
load_dev_seeds "debates"
|
||||
load_dev_seeds "proposals"
|
||||
load_dev_seeds "budgets"
|
||||
load_dev_seeds "comments"
|
||||
load_dev_seeds "votes"
|
||||
load_dev_seeds "flags"
|
||||
load_dev_seeds "hiddings"
|
||||
load_dev_seeds "banners"
|
||||
load_dev_seeds "polls"
|
||||
load_dev_seeds "communities"
|
||||
load_dev_seeds "legislation_processes"
|
||||
load_dev_seeds "newsletters"
|
||||
load_dev_seeds "notifications"
|
||||
load_dev_seeds "widgets"
|
||||
load_dev_seeds "admin_notifications"
|
||||
load_dev_seeds "legislation_proposals"
|
||||
load_dev_seeds "milestones"
|
||||
load_dev_seeds "pages"
|
||||
load_dev_seeds "sdg"
|
||||
|
||||
log "All dev seeds created successfuly 👍"
|
||||
|
||||
@@ -5,7 +5,7 @@ section "Creating Admin Notifications & Templates" do
|
||||
-> { I18n.t("seeds.admin_notifications.proposal.#{attribute}") }
|
||||
end
|
||||
).merge(
|
||||
link: Rails.application.routes.url_helpers.proposals_url(ActionMailer::Base.default_url_options),
|
||||
link: Rails.application.routes.url_helpers.proposals_url(Tenant.current_url_options),
|
||||
segment_recipient: "administrators"
|
||||
)
|
||||
).deliver
|
||||
|
||||
12
db/migrate/20180502075740_create_tenants.rb
Normal file
12
db/migrate/20180502075740_create_tenants.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
class CreateTenants < ActiveRecord::Migration[4.2]
|
||||
def change
|
||||
create_table :tenants do |t|
|
||||
t.string :name
|
||||
t.string :schema
|
||||
t.timestamps null: false
|
||||
|
||||
t.index :name, unique: true
|
||||
t.index :schema, unique: true
|
||||
end
|
||||
end
|
||||
end
|
||||
5
db/migrate/20190214103106_add_tenant_to_delayed_job.rb
Normal file
5
db/migrate/20190214103106_add_tenant_to_delayed_job.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddTenantToDelayedJob < ActiveRecord::Migration[4.2]
|
||||
def change
|
||||
add_column :delayed_jobs, :tenant, :string
|
||||
end
|
||||
end
|
||||
137
db/migrate/20200602233844_create_shared_extensions_schema.rb
Normal file
137
db/migrate/20200602233844_create_shared_extensions_schema.rb
Normal file
@@ -0,0 +1,137 @@
|
||||
class CreateSharedExtensionsSchema < ActiveRecord::Migration[6.0]
|
||||
def up
|
||||
unless schema_exists?(extensions_schema)
|
||||
execute_or_log_create_schema_warning("CREATE SCHEMA #{extensions_schema}")
|
||||
end
|
||||
|
||||
%w[unaccent pg_trgm].each do |extension|
|
||||
if extension_enabled?(extension)
|
||||
unless extension_already_in_extensions_schema?(extension)
|
||||
execute_or_log_extension_warning("ALTER EXTENSION #{extension} SET SCHEMA #{extensions_schema}")
|
||||
end
|
||||
else
|
||||
execute_or_log_extension_warning("CREATE EXTENSION #{extension} SCHEMA #{extensions_schema}")
|
||||
end
|
||||
end
|
||||
|
||||
unless schema_exists?(extensions_schema) && public_has_usage_privilege_on_extensions_schema?
|
||||
execute_or_log_grant_usage_warning("GRANT usage ON SCHEMA #{extensions_schema} TO public")
|
||||
end
|
||||
|
||||
show_full_warning_message if warning_messages.any?
|
||||
end
|
||||
|
||||
def down
|
||||
%w[unaccent pg_trgm].each do |extension|
|
||||
unless extension_already_in_public_schema?(extension)
|
||||
execute "ALTER EXTENSION #{extension} SET SCHEMA public;"
|
||||
end
|
||||
end
|
||||
|
||||
execute "DROP SCHEMA #{extensions_schema};" if schema_exists?(extensions_schema)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extensions_schema
|
||||
"shared_extensions"
|
||||
end
|
||||
|
||||
def extension_already_in_extensions_schema?(extension)
|
||||
associated_schema_id_for(extension) == extensions_schema_id
|
||||
end
|
||||
|
||||
def extension_already_in_public_schema?(extension)
|
||||
associated_schema_id_for(extension) == public_schema_id
|
||||
end
|
||||
|
||||
def associated_schema_id_for(extension)
|
||||
query_value("SELECT extnamespace FROM pg_extension WHERE extname=#{quote(extension)}")
|
||||
end
|
||||
|
||||
def extensions_schema_id
|
||||
schema_id_for(extensions_schema)
|
||||
end
|
||||
|
||||
def public_schema_id
|
||||
schema_id_for("public")
|
||||
end
|
||||
|
||||
def schema_id_for(schema)
|
||||
query_value("SELECT oid FROM pg_namespace WHERE nspname=#{quote(schema)}")
|
||||
end
|
||||
|
||||
def execute_or_log_create_schema_warning(statement)
|
||||
if create_permission?
|
||||
execute statement
|
||||
else
|
||||
log_warning(
|
||||
"GRANT CREATE ON DATABASE #{query_value("SELECT CURRENT_DATABASE()")} "\
|
||||
"TO #{query_value("SELECT CURRENT_USER")}"
|
||||
)
|
||||
log_warning(statement)
|
||||
end
|
||||
end
|
||||
|
||||
def execute_or_log_extension_warning(statement)
|
||||
if superuser?
|
||||
execute statement
|
||||
else
|
||||
log_warning(statement)
|
||||
end
|
||||
end
|
||||
|
||||
def execute_or_log_grant_usage_warning(statement)
|
||||
if schema_exists?(extensions_schema) && grant_usage_permission?
|
||||
execute statement
|
||||
else
|
||||
log_warning(statement)
|
||||
end
|
||||
end
|
||||
|
||||
def create_permission?
|
||||
query_value("SELECT has_database_privilege(CURRENT_USER, CURRENT_DATABASE(), 'CREATE');")
|
||||
end
|
||||
|
||||
def superuser?
|
||||
query_value("SELECT usesuper FROM pg_user WHERE usename = CURRENT_USER")
|
||||
end
|
||||
|
||||
def grant_usage_permission?
|
||||
query_value("SELECT has_schema_privilege(CURRENT_USER, '#{extensions_schema}', 'CREATE');")
|
||||
end
|
||||
|
||||
def public_has_usage_privilege_on_extensions_schema?
|
||||
query_value("SELECT has_schema_privilege('public', '#{extensions_schema}', 'USAGE');")
|
||||
end
|
||||
|
||||
def log_warning(statement)
|
||||
warning_messages.push(statement)
|
||||
end
|
||||
|
||||
def warning_messages
|
||||
@warning_messages ||= []
|
||||
end
|
||||
|
||||
def show_full_warning_message
|
||||
message = <<~WARNING
|
||||
---------------------- Multitenancy Warning ----------------------
|
||||
Multitenancy is a feature that allows managing multiple
|
||||
institutions in a completely independent way using just one
|
||||
CONSUL installation.
|
||||
|
||||
NOTE: If you aren't going to use multitenancy, you can safely
|
||||
ignore this warning.
|
||||
|
||||
If you'd like to enable this feature, first run:
|
||||
#{warning_messages.join(";\n ")};
|
||||
using a user with enough database privileges.
|
||||
|
||||
Check the CONSUL release notes for more information.
|
||||
------------------------------------------------------------------
|
||||
WARNING
|
||||
|
||||
puts message
|
||||
Rails.logger.warn(message)
|
||||
end
|
||||
end
|
||||
10
db/schema.rb
10
db/schema.rb
@@ -561,6 +561,7 @@ ActiveRecord::Schema.define(version: 2022_09_15_154808) do
|
||||
t.string "queue"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.string "tenant"
|
||||
t.index ["priority", "run_at"], name: "delayed_jobs_priority"
|
||||
end
|
||||
|
||||
@@ -1555,6 +1556,15 @@ ActiveRecord::Schema.define(version: 2022_09_15_154808) do
|
||||
t.index ["proposals_count"], name: "index_tags_on_proposals_count"
|
||||
end
|
||||
|
||||
create_table "tenants", id: :serial, force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.string "schema"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["name"], name: "index_tenants_on_name", unique: true
|
||||
t.index ["schema"], name: "index_tenants_on_schema", unique: true
|
||||
end
|
||||
|
||||
create_table "topics", id: :serial, force: :cascade do |t|
|
||||
t.string "title", null: false
|
||||
t.text "description"
|
||||
|
||||
13
lib/active_storage/service/tenant_disk_service.rb
Normal file
13
lib/active_storage/service/tenant_disk_service.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
require "active_storage/service/disk_service"
|
||||
|
||||
module ActiveStorage
|
||||
class Service::TenantDiskService < Service::DiskService
|
||||
def path_for(key)
|
||||
if Tenant.default?
|
||||
super
|
||||
else
|
||||
super.sub(root, File.join(root, Tenant.subfolder_path))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,10 @@ class ApplicationLogger
|
||||
logger.info(message) unless Rails.env.test?
|
||||
end
|
||||
|
||||
def warn(message)
|
||||
logger.warn(message) unless Rails.env.test?
|
||||
end
|
||||
|
||||
def logger
|
||||
@logger ||= Logger.new(STDOUT).tap do |logger|
|
||||
logger.formatter = proc { |severity, _datetime, _progname, msg| "#{severity} #{msg}\n" }
|
||||
|
||||
@@ -31,7 +31,7 @@ class ManagerAuthenticator
|
||||
end
|
||||
|
||||
def client
|
||||
@client ||= Savon.client(wsdl: Rails.application.secrets.managers_url)
|
||||
@client ||= Savon.client(wsdl: Tenant.current_secrets.managers_url)
|
||||
end
|
||||
|
||||
def parser
|
||||
@@ -39,6 +39,6 @@ class ManagerAuthenticator
|
||||
end
|
||||
|
||||
def application_key
|
||||
Rails.application.secrets.managers_application_key.to_s
|
||||
Tenant.current_secrets.managers_application_key.to_s
|
||||
end
|
||||
end
|
||||
|
||||
47
lib/omniauth_tenant_setup.rb
Normal file
47
lib/omniauth_tenant_setup.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
module OmniauthTenantSetup
|
||||
class << self
|
||||
def twitter
|
||||
->(env) do
|
||||
oauth(env, secrets.twitter_key, secrets.twitter_secret)
|
||||
end
|
||||
end
|
||||
|
||||
def facebook
|
||||
->(env) do
|
||||
oauth2(env, secrets.facebook_key, secrets.facebook_secret)
|
||||
end
|
||||
end
|
||||
|
||||
def google_oauth2
|
||||
->(env) do
|
||||
oauth2(env, secrets.google_oauth2_key, secrets.google_oauth2_secret)
|
||||
end
|
||||
end
|
||||
|
||||
def wordpress_oauth2
|
||||
->(env) do
|
||||
oauth2(env, secrets.wordpress_oauth2_key, secrets.wordpress_oauth2_secret)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def oauth(env, key, secret)
|
||||
unless Tenant.default?
|
||||
env["omniauth.strategy"].options[:consumer_key] = key
|
||||
env["omniauth.strategy"].options[:consumer_secret] = secret
|
||||
end
|
||||
end
|
||||
|
||||
def oauth2(env, key, secret)
|
||||
unless Tenant.default?
|
||||
env["omniauth.strategy"].options[:client_id] = key
|
||||
env["omniauth.strategy"].options[:client_secret] = secret
|
||||
end
|
||||
end
|
||||
|
||||
def secrets
|
||||
Tenant.current_secrets
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -36,7 +36,7 @@ class RemoteTranslations::Microsoft::AvailableLocales
|
||||
uri = URI(host + path)
|
||||
|
||||
request = Net::HTTP::Get.new(uri)
|
||||
request["Ocp-Apim-Subscription-Key"] = Rails.application.secrets.microsoft_api_key
|
||||
request["Ocp-Apim-Subscription-Key"] = Tenant.current_secrets.microsoft_api_key
|
||||
|
||||
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
||||
http.request(request)
|
||||
|
||||
@@ -6,7 +6,7 @@ class RemoteTranslations::Microsoft::Client
|
||||
PREVENTING_TRANSLATION_KEY = "notranslate".freeze
|
||||
|
||||
def initialize
|
||||
api_key = Rails.application.secrets.microsoft_api_key
|
||||
api_key = Tenant.current_secrets.microsoft_api_key
|
||||
@client = TranslatorText::Client.new(api_key)
|
||||
end
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ class SMSApi
|
||||
def url
|
||||
return "" unless end_point_available?
|
||||
|
||||
URI.parse(Rails.application.secrets.sms_end_point).to_s
|
||||
URI.parse(Tenant.current_secrets.sms_end_point).to_s
|
||||
end
|
||||
|
||||
def authorization
|
||||
Base64.encode64("#{Rails.application.secrets.sms_username}:#{Rails.application.secrets.sms_password}")
|
||||
Base64.encode64("#{Tenant.current_secrets.sms_username}:#{Tenant.current_secrets.sms_password}")
|
||||
end
|
||||
|
||||
def sms_deliver(phone, code)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
namespace :budgets do
|
||||
namespace :email do
|
||||
desc "Sends emails to authors of selected investments"
|
||||
task selected: :environment do
|
||||
Budget.last.email_selected
|
||||
task :selected, [:tenant] => :environment do |_, args|
|
||||
Tenant.switch(args[:tenant]) { Budget.current.email_selected }
|
||||
end
|
||||
|
||||
desc "Sends emails to authors of unselected investments"
|
||||
task unselected: :environment do
|
||||
Budget.last.email_unselected
|
||||
task :unselected, [:tenant] => :environment do |_, args|
|
||||
Tenant.switch(args[:tenant]) { Budget.current.email_unselected }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace :consul do
|
||||
desc "Runs tasks needed to upgrade from 1.5.0 to 1.6.0"
|
||||
task "execute_release_1.6.0_tasks": [
|
||||
"db:calculate_tsv",
|
||||
"polls:set_ends_at_to_end_of_day"
|
||||
"polls:set_ends_at_to_end_of_day",
|
||||
"db:add_schema_search_path"
|
||||
]
|
||||
end
|
||||
|
||||
9
lib/tasks/create_shared_extensions_schema.rake
Normal file
9
lib/tasks/create_shared_extensions_schema.rake
Normal file
@@ -0,0 +1,9 @@
|
||||
require Rails.root.join("db/migrate/20200602233844_create_shared_extensions_schema.rb")
|
||||
|
||||
Rake::Task["db:schema:load"].enhance do
|
||||
CreateSharedExtensionsSchema.new.up
|
||||
end
|
||||
|
||||
Rake::Task["db:test:purge"].enhance do
|
||||
CreateSharedExtensionsSchema.new.up
|
||||
end
|
||||
@@ -1,9 +1,8 @@
|
||||
namespace :db do
|
||||
desc "Resets the database and loads it from db/dev_seeds.rb"
|
||||
task :dev_seed, [:print_log] => [:environment] do |t, args|
|
||||
@avoid_log = args[:print_log] == "avoid_log"
|
||||
task :dev_seed, [:tenant] => [:environment] do |_, args|
|
||||
I18n.enforce_available_locales = false
|
||||
load(Rails.root.join("db", "dev_seeds.rb"))
|
||||
Tenant.switch(args[:tenant]) { load(Rails.root.join("db", "dev_seeds.rb")) }
|
||||
end
|
||||
|
||||
desc "Calculates the TSV column for all comments and proposal notifications"
|
||||
@@ -16,4 +15,30 @@ namespace :db do
|
||||
logger.info "Calculating tsvector for proposal notifications"
|
||||
ProposalNotification.with_hidden.find_each(&:calculate_tsvector)
|
||||
end
|
||||
|
||||
desc "Adds shared extensions to the schema search path in the database.yml file"
|
||||
task add_schema_search_path: :environment do
|
||||
logger = ApplicationLogger.new
|
||||
logger.info "Adding search path to config/database.yml"
|
||||
|
||||
config = Rails.application.config.paths["config/database"].first
|
||||
lines = File.readlines(config)
|
||||
changes_done = false
|
||||
|
||||
adapter_indices = lines.map.with_index do |line, index|
|
||||
index if line.start_with?(" adapter: postgresql")
|
||||
end.compact
|
||||
|
||||
adapter_indices.reverse_each do |index|
|
||||
unless lines[index + 1]&.match?("schema_search_path")
|
||||
lines.insert(index + 1, " schema_search_path: \"public,shared_extensions\"\n")
|
||||
changes_done = true
|
||||
end
|
||||
end
|
||||
|
||||
if changes_done
|
||||
File.write(config, lines.join)
|
||||
logger.warn "The database search path has been updated. Restart the application to apply the changes."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
namespace :files do
|
||||
desc "Removes cached attachments which weren't deleted for some reason"
|
||||
task remove_old_cached_attachments: :environment do
|
||||
ActiveStorage::Blob.unattached
|
||||
.where("active_storage_blobs.created_at <= ?", 1.day.ago)
|
||||
.find_each(&:purge_later)
|
||||
Tenant.run_on_each do
|
||||
ActiveStorage::Blob.unattached
|
||||
.where("active_storage_blobs.created_at <= ?", 1.day.ago)
|
||||
.find_each(&:purge_later)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace :settings do
|
||||
desc "Add new settings"
|
||||
task add_new_settings: :environment do
|
||||
ApplicationLogger.new.info "Adding new settings"
|
||||
Setting.add_new_settings
|
||||
Tenant.run_on_each { Setting.add_new_settings }
|
||||
end
|
||||
|
||||
desc "Rename existing settings"
|
||||
|
||||
@@ -2,23 +2,28 @@ namespace :stats do
|
||||
desc "Generates stats which are not cached yet"
|
||||
task generate: :environment do
|
||||
ApplicationLogger.new.info "Updating budget and poll stats"
|
||||
admin_ability = Ability.new(Administrator.first.user)
|
||||
|
||||
Budget.accessible_by(admin_ability, :read_stats).find_each do |budget|
|
||||
Budget::Stats.new(budget).generate
|
||||
print "."
|
||||
end
|
||||
Tenant.run_on_each do
|
||||
admin_ability = Ability.new(Administrator.first.user)
|
||||
|
||||
Poll.accessible_by(admin_ability, :stats).find_each do |poll|
|
||||
Poll::Stats.new(poll).generate
|
||||
print "."
|
||||
Budget.accessible_by(admin_ability, :read_stats).find_each do |budget|
|
||||
Budget::Stats.new(budget).generate
|
||||
print "."
|
||||
end
|
||||
|
||||
Poll.accessible_by(admin_ability, :stats).find_each do |poll|
|
||||
Poll::Stats.new(poll).generate
|
||||
print "."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Expires stats cache"
|
||||
task expire_cache: :environment do
|
||||
[Budget, Poll].each do |model_class|
|
||||
model_class.find_each { |record| record.find_or_create_stats_version.touch }
|
||||
Tenant.run_on_each do
|
||||
[Budget, Poll].each do |model_class|
|
||||
model_class.find_each { |record| record.find_or_create_stats_version.touch }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -5,9 +5,12 @@ namespace :votes do
|
||||
|
||||
models.each do |model|
|
||||
print "Updating votes hot_score for #{model}s"
|
||||
model.find_each do |resource|
|
||||
new_hot_score = resource.calculate_hot_score
|
||||
resource.update_columns(hot_score: new_hot_score, updated_at: Time.current)
|
||||
|
||||
Tenant.run_on_each do
|
||||
model.find_each do |resource|
|
||||
new_hot_score = resource.calculate_hot_score
|
||||
resource.update_columns(hot_score: new_hot_score, updated_at: Time.current)
|
||||
end
|
||||
end
|
||||
puts " ✅ "
|
||||
end
|
||||
|
||||
@@ -63,14 +63,17 @@ tqdm_notebook = True
|
||||
|
||||
|
||||
# In[2]:
|
||||
import os
|
||||
|
||||
if os.environ.get("CONSUL_TENANT"):
|
||||
data_path = '../../tenants/' + os.environ["CONSUL_TENANT"] + '/machine_learning/data'
|
||||
else:
|
||||
data_path = '../data'
|
||||
|
||||
data_path = '../data'
|
||||
config_file = 'budgets_related_content_and_tags_nmf.ini'
|
||||
logging_file ='budgets_related_content_and_tags_nmf.log'
|
||||
|
||||
# Read the configuration file
|
||||
import os
|
||||
import configparser
|
||||
config = configparser.ConfigParser()
|
||||
check_file(os.path.join(data_path,config_file))
|
||||
|
||||
@@ -60,14 +60,17 @@ tqdm_notebook = True
|
||||
|
||||
|
||||
# In[ ]:
|
||||
import os
|
||||
|
||||
if os.environ.get("CONSUL_TENANT"):
|
||||
data_path = '../../tenants/' + os.environ["CONSUL_TENANT"] + '/machine_learning/data'
|
||||
else:
|
||||
data_path = '../data'
|
||||
|
||||
data_path = '../data'
|
||||
config_file = 'budgets_summary_comments_textrank.ini'
|
||||
logging_file ='budgets_summary_comments_textrank.log'
|
||||
|
||||
# Read the configuration file
|
||||
import os
|
||||
import configparser
|
||||
config = configparser.ConfigParser()
|
||||
check_file(os.path.join(data_path,config_file))
|
||||
|
||||
@@ -63,14 +63,17 @@ tqdm_notebook = True
|
||||
|
||||
|
||||
# In[2]:
|
||||
import os
|
||||
|
||||
if os.environ.get("CONSUL_TENANT"):
|
||||
data_path = '../../tenants/' + os.environ["CONSUL_TENANT"] + '/machine_learning/data'
|
||||
else:
|
||||
data_path = '../data'
|
||||
|
||||
data_path = '../data'
|
||||
config_file = 'proposals_related_content_and_tags_nmf.ini'
|
||||
logging_file ='proposals_related_content_and_tags_nmf.log'
|
||||
|
||||
# Read the configuration file
|
||||
import os
|
||||
import configparser
|
||||
config = configparser.ConfigParser()
|
||||
check_file(os.path.join(data_path,config_file))
|
||||
|
||||
@@ -60,14 +60,17 @@ tqdm_notebook = True
|
||||
|
||||
|
||||
# In[3]:
|
||||
import os
|
||||
|
||||
if os.environ.get("CONSUL_TENANT"):
|
||||
data_path = '../../tenants/' + os.environ["CONSUL_TENANT"] + '/machine_learning/data'
|
||||
else:
|
||||
data_path = '../data'
|
||||
|
||||
data_path = '../data'
|
||||
config_file = 'proposals_summary_comments_textrank.ini'
|
||||
logging_file ='proposals_summary_comments_textrank.log'
|
||||
|
||||
# Read the configuration file
|
||||
import os
|
||||
import configparser
|
||||
config = configparser.ConfigParser()
|
||||
check_file(os.path.join(data_path,config_file))
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe Layout::CommonHTMLAttributesComponent do
|
||||
let(:component) { Layout::CommonHTMLAttributesComponent.new }
|
||||
|
||||
context "with multitenancy disabled" do
|
||||
before { allow(Rails.application.config).to receive(:multitenancy).and_return(false) }
|
||||
|
||||
it "includes the default language by default" do
|
||||
render_inline component
|
||||
|
||||
expect(page.text).to eq 'lang="en"'
|
||||
end
|
||||
|
||||
it "includes the current language" do
|
||||
I18n.with_locale(:es) { render_inline component }
|
||||
|
||||
expect(page.text).to eq 'lang="es"'
|
||||
end
|
||||
end
|
||||
|
||||
context "with multitenancy enabled" do
|
||||
it "includes a class with the 'public' suffix for the default tenant" do
|
||||
render_inline component
|
||||
|
||||
expect(page.text).to eq 'lang="en" class="tenant-public"'
|
||||
end
|
||||
|
||||
it "includes a class with the schema name as suffix for other tenants" do
|
||||
allow(Tenant).to receive(:current_schema).and_return "private"
|
||||
|
||||
render_inline component
|
||||
|
||||
expect(page.text).to eq 'lang="en" class="tenant-private"'
|
||||
end
|
||||
end
|
||||
|
||||
context "RTL languages" do
|
||||
let!(:default_enforce) { I18n.enforce_available_locales }
|
||||
|
||||
before do
|
||||
I18n.enforce_available_locales = false
|
||||
allow(I18n).to receive(:available_locales).and_return(%i[ar en es])
|
||||
end
|
||||
|
||||
after { I18n.enforce_available_locales = default_enforce }
|
||||
|
||||
context "with multitenancy disabled" do
|
||||
before { allow(Rails.application.config).to receive(:multitenancy).and_return(false) }
|
||||
|
||||
it "includes the dir attribute" do
|
||||
I18n.with_locale(:ar) { render_inline component }
|
||||
|
||||
expect(page.text).to eq 'dir="rtl" lang="ar"'
|
||||
end
|
||||
end
|
||||
|
||||
context "with multitenancy enabled" do
|
||||
it "includes the dir and the class attributes" do
|
||||
I18n.with_locale(:ar) { render_inline component }
|
||||
|
||||
expect(page.text).to eq 'dir="rtl" lang="ar" class="tenant-public"'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -98,4 +98,9 @@ FactoryBot.define do
|
||||
value_es { "Texto en español" }
|
||||
value_en { "Text in english" }
|
||||
end
|
||||
|
||||
factory :tenant do
|
||||
sequence(:name) { |n| "Tenant #{n}" }
|
||||
sequence(:schema) { |n| "subdomain#{n}" }
|
||||
end
|
||||
end
|
||||
|
||||
59
spec/lib/tasks/budgets_spec.rb
Normal file
59
spec/lib/tasks/budgets_spec.rb
Normal file
@@ -0,0 +1,59 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe "budget tasks" do
|
||||
describe "rake budgets:email:selected" do
|
||||
before { Rake::Task["budgets:email:selected"].reenable }
|
||||
|
||||
it "sends emails to users from the current budget and not the last budget created" do
|
||||
create(:budget_investment, :selected, author: create(:user, email: "selectme@consul.dev"))
|
||||
create(:budget, :drafting)
|
||||
|
||||
Rake.application.invoke_task("budgets:email:selected")
|
||||
|
||||
expect(ActionMailer::Base.deliveries.count).to eq 1
|
||||
expect(ActionMailer::Base.deliveries.last.to).to eq ["selectme@consul.dev"]
|
||||
end
|
||||
|
||||
it "accepts specifying the tenant" do
|
||||
create(:budget_investment, :selected, author: create(:user, email: "default@consul.dev"))
|
||||
create(:tenant, schema: "different")
|
||||
|
||||
Tenant.switch("different") do
|
||||
create(:budget_investment, :selected, author: create(:user, email: "different@consul.dev"))
|
||||
end
|
||||
|
||||
Rake.application.invoke_task("budgets:email:selected[different]")
|
||||
|
||||
expect(ActionMailer::Base.deliveries.count).to eq 1
|
||||
expect(ActionMailer::Base.deliveries.last.to).to eq ["different@consul.dev"]
|
||||
end
|
||||
end
|
||||
|
||||
describe "rake budgets:email:unselected" do
|
||||
before { Rake::Task["budgets:email:unselected"].reenable }
|
||||
|
||||
it "sends emails to users from the current budget and not the last budget created" do
|
||||
create(:budget_investment, author: create(:user, email: "ignorme@consul.dev"))
|
||||
create(:budget, :drafting)
|
||||
|
||||
Rake.application.invoke_task("budgets:email:unselected")
|
||||
|
||||
expect(ActionMailer::Base.deliveries.count).to eq 1
|
||||
expect(ActionMailer::Base.deliveries.last.to).to eq ["ignorme@consul.dev"]
|
||||
end
|
||||
|
||||
it "accepts specifying the tenant" do
|
||||
create(:budget_investment, author: create(:user, email: "default@consul.dev"))
|
||||
create(:tenant, schema: "different")
|
||||
|
||||
Tenant.switch("different") do
|
||||
create(:budget_investment, author: create(:user, email: "different@consul.dev"))
|
||||
end
|
||||
|
||||
Rake.application.invoke_task("budgets:email:unselected[different]")
|
||||
|
||||
expect(ActionMailer::Base.deliveries.count).to eq 1
|
||||
expect(ActionMailer::Base.deliveries.last.to).to eq ["different@consul.dev"]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,11 +1,18 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe "rake db:dev_seed" do
|
||||
let :run_rake_task do
|
||||
Rake.application.invoke_task("db:dev_seed[avoid_log]")
|
||||
end
|
||||
before { Rake::Task["db:dev_seed"].reenable }
|
||||
|
||||
it "seeds the database without errors" do
|
||||
expect { run_rake_task }.not_to raise_error
|
||||
expect { Rake.application.invoke_task("db:dev_seed") }.not_to raise_error
|
||||
end
|
||||
|
||||
it "can seed a tenant" do
|
||||
create(:tenant, schema: "democracy")
|
||||
|
||||
Rake.application.invoke_task("db:dev_seed[democracy]")
|
||||
|
||||
expect(Debate.count).to eq 0
|
||||
Tenant.switch("democracy") { expect(Debate.count).not_to eq 0 }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,6 +2,7 @@ require "rails_helper"
|
||||
|
||||
describe "rake sitemap:create", type: :system do
|
||||
let(:file) { Rails.root.join("public", "sitemap.xml") }
|
||||
let(:run_rake_task) { Rake.application.invoke_task("sitemap:create") }
|
||||
|
||||
before do
|
||||
FileUtils.rm_f(file)
|
||||
@@ -9,11 +10,7 @@ describe "rake sitemap:create", type: :system do
|
||||
end
|
||||
|
||||
describe "when processes are enabled" do
|
||||
before { Rake.application.invoke_task("sitemap:create") }
|
||||
|
||||
it "generates a sitemap" do
|
||||
expect(file).to exist
|
||||
end
|
||||
before { run_rake_task }
|
||||
|
||||
it "generates a valid sitemap" do
|
||||
sitemap = Nokogiri::XML(File.open(file))
|
||||
@@ -24,16 +21,16 @@ describe "rake sitemap:create", type: :system do
|
||||
sitemap = File.read(file)
|
||||
|
||||
# Static pages
|
||||
expect(sitemap).to include(faq_path)
|
||||
expect(sitemap).to include(help_path)
|
||||
expect(sitemap).to include(how_to_use_path)
|
||||
expect(sitemap).to have_content(faq_path)
|
||||
expect(sitemap).to have_content(help_path)
|
||||
expect(sitemap).to have_content(how_to_use_path)
|
||||
|
||||
# Dynamic URLs
|
||||
expect(sitemap).to include(polls_path)
|
||||
expect(sitemap).to include(budgets_path)
|
||||
expect(sitemap).to include(debates_path)
|
||||
expect(sitemap).to include(proposals_path)
|
||||
expect(sitemap).to include(legislation_processes_path)
|
||||
expect(sitemap).to have_content(polls_path)
|
||||
expect(sitemap).to have_content(budgets_path)
|
||||
expect(sitemap).to have_content(debates_path)
|
||||
expect(sitemap).to have_content(proposals_path)
|
||||
expect(sitemap).to have_content(legislation_processes_path)
|
||||
|
||||
expect(sitemap).to have_content("0.7", count: 5)
|
||||
expect(sitemap).to have_content("daily", count: 5)
|
||||
@@ -48,11 +45,7 @@ describe "rake sitemap:create", type: :system do
|
||||
Setting["process.polls"] = nil
|
||||
Setting["process.legislation"] = nil
|
||||
|
||||
Rake.application.invoke_task("sitemap:create")
|
||||
end
|
||||
|
||||
it "generates a sitemap" do
|
||||
expect(file).to exist
|
||||
run_rake_task
|
||||
end
|
||||
|
||||
it "generates a valid sitemap" do
|
||||
@@ -64,19 +57,61 @@ describe "rake sitemap:create", type: :system do
|
||||
sitemap = File.read(file)
|
||||
|
||||
# Static pages
|
||||
expect(sitemap).to include(faq_path)
|
||||
expect(sitemap).to include(help_path)
|
||||
expect(sitemap).to include(how_to_use_path)
|
||||
expect(sitemap).to have_content(faq_path)
|
||||
expect(sitemap).to have_content(help_path)
|
||||
expect(sitemap).to have_content(how_to_use_path)
|
||||
|
||||
# Dynamic URLs
|
||||
expect(sitemap).not_to include(polls_path)
|
||||
expect(sitemap).not_to include(budgets_path)
|
||||
expect(sitemap).not_to include(debates_path)
|
||||
expect(sitemap).not_to include(proposals_path)
|
||||
expect(sitemap).not_to include(legislation_processes_path)
|
||||
expect(sitemap).not_to have_content(polls_path)
|
||||
expect(sitemap).not_to have_content(budgets_path)
|
||||
expect(sitemap).not_to have_content(debates_path)
|
||||
expect(sitemap).not_to have_content(proposals_path)
|
||||
expect(sitemap).not_to have_content(legislation_processes_path)
|
||||
|
||||
expect(sitemap).not_to have_content("0.7")
|
||||
expect(sitemap).not_to have_content("daily")
|
||||
end
|
||||
end
|
||||
|
||||
it "generates a sitemap for every tenant" do
|
||||
allow(Tenant).to receive(:default_url_options).and_return({ host: "consul.dev" })
|
||||
FileUtils.rm_f(Dir.glob(Rails.root.join("public", "tenants", "*", "sitemap.xml")))
|
||||
|
||||
create(:tenant, schema: "debates")
|
||||
create(:tenant, schema: "proposals")
|
||||
|
||||
Setting["process.budgets"] = true
|
||||
Setting["process.debates"] = false
|
||||
Setting["process.proposals"] = false
|
||||
|
||||
Tenant.switch("debates") do
|
||||
Setting["process.debates"] = true
|
||||
Setting["process.budgets"] = false
|
||||
Setting["process.proposals"] = false
|
||||
end
|
||||
|
||||
Tenant.switch("proposals") do
|
||||
Setting["process.proposals"] = true
|
||||
Setting["process.budgets"] = false
|
||||
Setting["process.debates"] = false
|
||||
end
|
||||
|
||||
run_rake_task
|
||||
|
||||
public_sitemap = File.read(file)
|
||||
debates_sitemap = File.read(Rails.root.join("public", "tenants", "debates", "sitemap.xml"))
|
||||
proposals_sitemap = File.read(Rails.root.join("public", "tenants", "proposals", "sitemap.xml"))
|
||||
|
||||
expect(public_sitemap).to have_content budgets_url(host: "consul.dev")
|
||||
expect(public_sitemap).not_to have_content debates_path
|
||||
expect(public_sitemap).not_to have_content proposals_path
|
||||
|
||||
expect(debates_sitemap).to have_content debates_url(host: "debates.consul.dev")
|
||||
expect(debates_sitemap).not_to have_content budgets_path
|
||||
expect(debates_sitemap).not_to have_content proposals_path
|
||||
|
||||
expect(proposals_sitemap).to have_content proposals_url(host: "proposals.consul.dev")
|
||||
expect(proposals_sitemap).not_to have_content budgets_path
|
||||
expect(proposals_sitemap).not_to have_content debates_path
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe ApplicationMailer do
|
||||
describe "#default_url_options" do
|
||||
it "returns the same options on the default tenant" do
|
||||
allow(ActionMailer::Base).to receive(:default_url_options).and_return({ host: "consul.dev" })
|
||||
|
||||
expect(ApplicationMailer.new.default_url_options).to eq({ host: "consul.dev" })
|
||||
end
|
||||
|
||||
it "returns the host with a subdomain on other tenants" do
|
||||
allow(ActionMailer::Base).to receive(:default_url_options).and_return({ host: "consul.dev" })
|
||||
allow(Tenant).to receive(:current_schema).and_return("my")
|
||||
|
||||
expect(ApplicationMailer.new.default_url_options).to eq({ host: "my.consul.dev" })
|
||||
end
|
||||
|
||||
it "uses lvh.me for subdomains when the host is localhost" do
|
||||
allow(ActionMailer::Base).to receive(:default_url_options).and_return({ host: "localhost", port: 3000 })
|
||||
allow(Tenant).to receive(:current_schema).and_return("dev")
|
||||
|
||||
expect(ApplicationMailer.new.default_url_options).to eq({ host: "dev.lvh.me", port: 3000 })
|
||||
end
|
||||
end
|
||||
|
||||
describe "#set_asset_host" do
|
||||
let(:mailer) { ApplicationMailer.new }
|
||||
|
||||
@@ -24,6 +46,24 @@ describe ApplicationMailer do
|
||||
expect(mailer.asset_host).to eq "https://localhost:3000"
|
||||
end
|
||||
|
||||
it "returns the host with a subdomain on other tenants" do
|
||||
allow(ActionMailer::Base).to receive(:default_url_options).and_return(host: "consul.dev")
|
||||
allow(Tenant).to receive(:current_schema).and_return("my")
|
||||
|
||||
mailer.set_asset_host
|
||||
|
||||
expect(mailer.asset_host).to eq "http://my.consul.dev"
|
||||
end
|
||||
|
||||
it "uses lvh.me for subdomains when the host is localhost" do
|
||||
allow(ActionMailer::Base).to receive(:default_url_options).and_return(host: "localhost", port: 3000)
|
||||
allow(Tenant).to receive(:current_schema).and_return("dev")
|
||||
|
||||
mailer.set_asset_host
|
||||
|
||||
expect(mailer.asset_host).to eq "http://dev.lvh.me:3000"
|
||||
end
|
||||
|
||||
it "returns the asset host when set manually" do
|
||||
default_asset_host = ActionMailer::Base.asset_host
|
||||
|
||||
|
||||
@@ -51,4 +51,59 @@ describe Mailer do
|
||||
expect(user.subscriptions_token).to eq "subscriptions_token_value"
|
||||
end
|
||||
end
|
||||
|
||||
describe "multitenancy" do
|
||||
it "uses the current tenant when using delayed jobs", :delay_jobs do
|
||||
allow(ActionMailer::Base).to receive(:default_url_options).and_return({ host: "consul.dev" })
|
||||
create(:tenant, schema: "delay")
|
||||
|
||||
Tenant.switch("delay") do
|
||||
Setting["org_name"] = "Delayed tenant"
|
||||
|
||||
Mailer.delay.user_invite("test@consul.dev")
|
||||
end
|
||||
|
||||
Delayed::Worker.new.work_off
|
||||
body = ActionMailer::Base.deliveries.last.body.to_s
|
||||
expect(body).to match "Delayed tenant"
|
||||
expect(body).to match "href=\"http://delay.consul.dev/"
|
||||
expect(body).to match "src=\"http://delay.consul.dev/"
|
||||
end
|
||||
|
||||
describe "SMTP settings" do
|
||||
let(:default_settings) { { address: "mail.consul.dev", username: "main" } }
|
||||
let(:super_settings) { { address: "super.consul.dev", username: "super" } }
|
||||
|
||||
before do
|
||||
allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge(
|
||||
smtp_settings: default_settings,
|
||||
tenants: {
|
||||
supermailer: { smtp_settings: super_settings }
|
||||
}
|
||||
))
|
||||
end
|
||||
|
||||
it "does not overwrite the settings for the default tenant" do
|
||||
Mailer.user_invite("test@consul.dev").deliver_now
|
||||
|
||||
expect(ActionMailer::Base.deliveries.last.delivery_method.settings).to eq({})
|
||||
end
|
||||
|
||||
it "uses specific secret settings for tenants overwriting them" do
|
||||
allow(Tenant).to receive(:current_schema).and_return("supermailer")
|
||||
|
||||
Mailer.user_invite("test@consul.dev").deliver_now
|
||||
|
||||
expect(ActionMailer::Base.deliveries.last.delivery_method.settings).to eq super_settings
|
||||
end
|
||||
|
||||
it "uses the default secret settings for other tenants" do
|
||||
allow(Tenant).to receive(:current_schema).and_return("ultramailer")
|
||||
|
||||
Mailer.user_invite("test@consul.dev").deliver_now
|
||||
|
||||
expect(ActionMailer::Base.deliveries.last.delivery_method.settings).to eq default_settings
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -164,4 +164,37 @@ describe Abilities::Administrator do
|
||||
it { should be_able_to(:destroy, SDG::Manager) }
|
||||
|
||||
it { should be_able_to(:manage, Widget::Card) }
|
||||
|
||||
describe "tenants" do
|
||||
context "with multitenancy disabled" do
|
||||
before { allow(Rails.application.config).to receive(:multitenancy).and_return(false) }
|
||||
|
||||
it { should_not be_able_to :create, Tenant }
|
||||
it { should_not be_able_to :read, Tenant }
|
||||
it { should_not be_able_to :update, Tenant }
|
||||
it { should_not be_able_to :destroy, Tenant }
|
||||
end
|
||||
|
||||
context "with multitenancy enabled" do
|
||||
before { allow(Rails.application.config).to receive(:multitenancy).and_return(true) }
|
||||
|
||||
it { should be_able_to :create, Tenant }
|
||||
it { should be_able_to :read, Tenant }
|
||||
it { should be_able_to :update, Tenant }
|
||||
it { should_not be_able_to :destroy, Tenant }
|
||||
|
||||
it "does not allow administrators from other tenants to manage tenants " do
|
||||
create(:tenant, schema: "subsidiary")
|
||||
|
||||
Tenant.switch("subsidiary") do
|
||||
admin = create(:administrator).user
|
||||
|
||||
expect(admin).not_to be_able_to :create, Tenant
|
||||
expect(admin).not_to be_able_to :read, Tenant
|
||||
expect(admin).not_to be_able_to :update, Tenant
|
||||
expect(admin).not_to be_able_to :destroy, Tenant
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
17
spec/models/attachable_spec.rb
Normal file
17
spec/models/attachable_spec.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe Attachable do
|
||||
it "stores attachments for the default tenant in the default folder" do
|
||||
file_path = build(:image).file_path
|
||||
|
||||
expect(file_path).to include "storage/"
|
||||
expect(file_path).not_to include "storage//"
|
||||
expect(file_path).not_to include "tenants"
|
||||
end
|
||||
|
||||
it "stores tenant attachments in a folder for the tenant" do
|
||||
allow(Tenant).to receive(:current_schema).and_return("image-master")
|
||||
|
||||
expect(build(:image).file_path).to include "storage/tenants/image-master/"
|
||||
end
|
||||
end
|
||||
@@ -309,7 +309,7 @@ describe MachineLearning do
|
||||
machine_learning = MachineLearning.new(job)
|
||||
machine_learning.send(:export_proposals_to_json)
|
||||
|
||||
json_file = MachineLearning::DATA_FOLDER.join("proposals.json")
|
||||
json_file = MachineLearning.data_folder.join("proposals.json")
|
||||
json = JSON.parse(File.read(json_file))
|
||||
|
||||
expect(json).to be_an Array
|
||||
@@ -335,7 +335,7 @@ describe MachineLearning do
|
||||
machine_learning = MachineLearning.new(job)
|
||||
machine_learning.send(:export_budget_investments_to_json)
|
||||
|
||||
json_file = MachineLearning::DATA_FOLDER.join("budget_investments.json")
|
||||
json_file = MachineLearning.data_folder.join("budget_investments.json")
|
||||
json = JSON.parse(File.read(json_file))
|
||||
|
||||
expect(json).to be_an Array
|
||||
@@ -359,7 +359,7 @@ describe MachineLearning do
|
||||
machine_learning = MachineLearning.new(job)
|
||||
machine_learning.send(:export_comments_to_json)
|
||||
|
||||
json_file = MachineLearning::DATA_FOLDER.join("comments.json")
|
||||
json_file = MachineLearning.data_folder.join("comments.json")
|
||||
json = JSON.parse(File.read(json_file))
|
||||
|
||||
expect(json).to be_an Array
|
||||
@@ -428,7 +428,7 @@ describe MachineLearning do
|
||||
]
|
||||
|
||||
filename = "ml_comments_summaries_proposals.json"
|
||||
json_file = MachineLearning::DATA_FOLDER.join(filename)
|
||||
json_file = MachineLearning.data_folder.join(filename)
|
||||
expect(File).to receive(:read).with(json_file).and_return data.to_json
|
||||
|
||||
machine_learning.send(:import_ml_proposals_comments_summary)
|
||||
@@ -450,7 +450,7 @@ describe MachineLearning do
|
||||
]
|
||||
|
||||
filename = "ml_comments_summaries_budgets.json"
|
||||
json_file = MachineLearning::DATA_FOLDER.join(filename)
|
||||
json_file = MachineLearning.data_folder.join(filename)
|
||||
expect(File).to receive(:read).with(json_file).and_return data.to_json
|
||||
|
||||
machine_learning.send(:import_ml_investments_comments_summary)
|
||||
@@ -476,7 +476,7 @@ describe MachineLearning do
|
||||
]
|
||||
|
||||
filename = "ml_related_content_proposals.json"
|
||||
json_file = MachineLearning::DATA_FOLDER.join(filename)
|
||||
json_file = MachineLearning.data_folder.join(filename)
|
||||
expect(File).to receive(:read).with(json_file).and_return data.to_json
|
||||
|
||||
machine_learning.send(:import_proposals_related_content)
|
||||
@@ -504,7 +504,7 @@ describe MachineLearning do
|
||||
]
|
||||
|
||||
filename = "ml_related_content_budgets.json"
|
||||
json_file = MachineLearning::DATA_FOLDER.join(filename)
|
||||
json_file = MachineLearning.data_folder.join(filename)
|
||||
expect(File).to receive(:read).with(json_file).and_return data.to_json
|
||||
|
||||
machine_learning.send(:import_budget_investments_related_content)
|
||||
@@ -538,11 +538,11 @@ describe MachineLearning do
|
||||
]
|
||||
|
||||
tags_filename = "ml_tags_proposals.json"
|
||||
tags_json_file = MachineLearning::DATA_FOLDER.join(tags_filename)
|
||||
tags_json_file = MachineLearning.data_folder.join(tags_filename)
|
||||
expect(File).to receive(:read).with(tags_json_file).and_return tags_data.to_json
|
||||
|
||||
taggings_filename = "ml_taggings_proposals.json"
|
||||
taggings_json_file = MachineLearning::DATA_FOLDER.join(taggings_filename)
|
||||
taggings_json_file = MachineLearning.data_folder.join(taggings_filename)
|
||||
expect(File).to receive(:read).with(taggings_json_file).and_return taggings_data.to_json
|
||||
|
||||
machine_learning.send(:import_ml_proposals_tags)
|
||||
@@ -580,11 +580,11 @@ describe MachineLearning do
|
||||
]
|
||||
|
||||
tags_filename = "ml_tags_budgets.json"
|
||||
tags_json_file = MachineLearning::DATA_FOLDER.join(tags_filename)
|
||||
tags_json_file = MachineLearning.data_folder.join(tags_filename)
|
||||
expect(File).to receive(:read).with(tags_json_file).and_return tags_data.to_json
|
||||
|
||||
taggings_filename = "ml_taggings_budgets.json"
|
||||
taggings_json_file = MachineLearning::DATA_FOLDER.join(taggings_filename)
|
||||
taggings_json_file = MachineLearning.data_folder.join(taggings_filename)
|
||||
expect(File).to receive(:read).with(taggings_json_file).and_return taggings_data.to_json
|
||||
|
||||
machine_learning.send(:import_ml_investments_tags)
|
||||
|
||||
303
spec/models/tenant_spec.rb
Normal file
303
spec/models/tenant_spec.rb
Normal file
@@ -0,0 +1,303 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe Tenant do
|
||||
describe ".resolve_host" do
|
||||
before do
|
||||
allow(Tenant).to receive(:default_url_options).and_return({ host: "consul.dev" })
|
||||
end
|
||||
|
||||
it "returns nil for empty hosts" do
|
||||
expect(Tenant.resolve_host("")).to be nil
|
||||
expect(Tenant.resolve_host(nil)).to be nil
|
||||
end
|
||||
|
||||
it "returns nil for IP addresses" do
|
||||
expect(Tenant.resolve_host("127.0.0.1")).to be nil
|
||||
end
|
||||
|
||||
it "returns nil using development and test domains" do
|
||||
expect(Tenant.resolve_host("localhost")).to be nil
|
||||
expect(Tenant.resolve_host("lvh.me")).to be nil
|
||||
expect(Tenant.resolve_host("example.com")).to be nil
|
||||
expect(Tenant.resolve_host("www.example.com")).to be nil
|
||||
end
|
||||
|
||||
it "treats lvh.me as localhost" do
|
||||
expect(Tenant.resolve_host("jupiter.lvh.me")).to eq "jupiter"
|
||||
expect(Tenant.resolve_host("www.lvh.me")).to be nil
|
||||
end
|
||||
|
||||
it "returns nil for the default host" do
|
||||
expect(Tenant.resolve_host("consul.dev")).to be nil
|
||||
end
|
||||
|
||||
it "ignores the www prefix" do
|
||||
expect(Tenant.resolve_host("www.consul.dev")).to be nil
|
||||
end
|
||||
|
||||
it "returns subdomains when present" do
|
||||
expect(Tenant.resolve_host("saturn.consul.dev")).to eq "saturn"
|
||||
end
|
||||
|
||||
it "ignores the www prefix when subdomains are present" do
|
||||
expect(Tenant.resolve_host("www.saturn.consul.dev")).to eq "saturn"
|
||||
end
|
||||
|
||||
it "returns nested additional subdomains" do
|
||||
expect(Tenant.resolve_host("europa.jupiter.consul.dev")).to eq "europa.jupiter"
|
||||
end
|
||||
|
||||
it "ignores the www prefix in additional nested subdomains" do
|
||||
expect(Tenant.resolve_host("www.europa.jupiter.consul.dev")).to eq "europa.jupiter"
|
||||
end
|
||||
|
||||
it "does not ignore www if it isn't the prefix" do
|
||||
expect(Tenant.resolve_host("wwwsaturn.consul.dev")).to eq "wwwsaturn"
|
||||
expect(Tenant.resolve_host("saturn.www.consul.dev")).to eq "saturn.www"
|
||||
end
|
||||
|
||||
it "returns the host as a subdomain" do
|
||||
expect(Tenant.resolve_host("consul.dev.consul.dev")).to eq "consul.dev"
|
||||
end
|
||||
|
||||
it "returns nested subdomains containing the host" do
|
||||
expect(Tenant.resolve_host("saturn.consul.dev.consul.dev")).to eq "saturn.consul.dev"
|
||||
end
|
||||
|
||||
it "returns full domains when they don't contain the host" do
|
||||
expect(Tenant.resolve_host("unrelated.dev")).to eq "unrelated.dev"
|
||||
expect(Tenant.resolve_host("mercury.anotherconsul.dev")).to eq "mercury.anotherconsul.dev"
|
||||
end
|
||||
|
||||
it "ignores the www prefix in full domains" do
|
||||
expect(Tenant.resolve_host("www.unrelated.dev")).to eq "unrelated.dev"
|
||||
expect(Tenant.resolve_host("www.mercury.anotherconsul.dev")).to eq "mercury.anotherconsul.dev"
|
||||
end
|
||||
|
||||
context "multitenancy disabled" do
|
||||
before { allow(Rails.application.config).to receive(:multitenancy).and_return(false) }
|
||||
|
||||
it "always returns nil" do
|
||||
expect(Tenant.resolve_host("saturn.consul.dev")).to be nil
|
||||
expect(Tenant.resolve_host("jupiter.lvh.me")).to be nil
|
||||
end
|
||||
end
|
||||
|
||||
context "default host contains subdomains" do
|
||||
before do
|
||||
allow(Tenant).to receive(:default_url_options).and_return({ host: "demo.consul.dev" })
|
||||
end
|
||||
|
||||
it "ignores subdomains already present in the default host" do
|
||||
expect(Tenant.resolve_host("demo.consul.dev")).to be nil
|
||||
end
|
||||
|
||||
it "ignores the www prefix" do
|
||||
expect(Tenant.resolve_host("www.demo.consul.dev")).to be nil
|
||||
end
|
||||
|
||||
it "returns additional subdomains" do
|
||||
expect(Tenant.resolve_host("saturn.demo.consul.dev")).to eq "saturn"
|
||||
end
|
||||
|
||||
it "ignores the www prefix in additional subdomains" do
|
||||
expect(Tenant.resolve_host("www.saturn.demo.consul.dev")).to eq "saturn"
|
||||
end
|
||||
|
||||
it "returns nested additional subdomains" do
|
||||
expect(Tenant.resolve_host("europa.jupiter.demo.consul.dev")).to eq "europa.jupiter"
|
||||
end
|
||||
|
||||
it "ignores the www prefix in additional nested subdomains" do
|
||||
expect(Tenant.resolve_host("www.europa.jupiter.demo.consul.dev")).to eq "europa.jupiter"
|
||||
end
|
||||
|
||||
it "does not ignore www if it isn't the prefix" do
|
||||
expect(Tenant.resolve_host("wwwsaturn.demo.consul.dev")).to eq "wwwsaturn"
|
||||
expect(Tenant.resolve_host("saturn.www.demo.consul.dev")).to eq "saturn.www"
|
||||
end
|
||||
end
|
||||
|
||||
context "default host is similar to development and test domains" do
|
||||
before do
|
||||
allow(Tenant).to receive(:default_url_options).and_return({ host: "mylvh.me" })
|
||||
end
|
||||
|
||||
it "returns nil for the default host" do
|
||||
expect(Tenant.resolve_host("mylvh.me")).to be nil
|
||||
end
|
||||
|
||||
it "returns subdomains when present" do
|
||||
expect(Tenant.resolve_host("neptune.mylvh.me")).to eq "neptune"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ".host_for" do
|
||||
before do
|
||||
allow(Tenant).to receive(:default_url_options).and_return({ host: "consul.dev" })
|
||||
end
|
||||
|
||||
it "returns the default host for the default schema" do
|
||||
expect(Tenant.host_for("public")).to eq "consul.dev"
|
||||
end
|
||||
|
||||
it "returns the host with a subdomain on other schemas" do
|
||||
expect(Tenant.host_for("uranus")).to eq "uranus.consul.dev"
|
||||
end
|
||||
|
||||
it "uses lvh.me for subdomains when the host is localhost" do
|
||||
allow(Tenant).to receive(:default_url_options).and_return({ host: "localhost" })
|
||||
|
||||
expect(Tenant.host_for("uranus")).to eq "uranus.lvh.me"
|
||||
end
|
||||
end
|
||||
|
||||
describe ".current_secrets" do
|
||||
context "same secrets for all tenants" do
|
||||
before do
|
||||
allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge(
|
||||
star: "Sun",
|
||||
volume: "Medium"
|
||||
))
|
||||
end
|
||||
|
||||
it "returns the default secrets for the default tenant" do
|
||||
allow(Tenant).to receive(:current_schema).and_return("public")
|
||||
|
||||
expect(Tenant.current_secrets.star).to eq "Sun"
|
||||
expect(Tenant.current_secrets.volume).to eq "Medium"
|
||||
end
|
||||
|
||||
it "returns the default secrets for other tenants" do
|
||||
allow(Tenant).to receive(:current_schema).and_return("earth")
|
||||
|
||||
expect(Tenant.current_secrets.star).to eq "Sun"
|
||||
expect(Tenant.current_secrets.volume).to eq "Medium"
|
||||
end
|
||||
end
|
||||
|
||||
context "tenant overwriting secrets" do
|
||||
before do
|
||||
allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge(
|
||||
star: "Sun",
|
||||
volume: "Medium",
|
||||
tenants: { proxima: { star: "Alpha Centauri" }}
|
||||
))
|
||||
end
|
||||
|
||||
it "returns the default secrets for the default tenant" do
|
||||
allow(Tenant).to receive(:current_schema).and_return("public")
|
||||
|
||||
expect(Tenant.current_secrets.star).to eq "Sun"
|
||||
expect(Tenant.current_secrets.volume).to eq "Medium"
|
||||
end
|
||||
|
||||
it "returns the overwritten secrets for tenants overwriting them" do
|
||||
allow(Tenant).to receive(:current_schema).and_return("proxima")
|
||||
|
||||
expect(Tenant.current_secrets.star).to eq "Alpha Centauri"
|
||||
expect(Tenant.current_secrets.volume).to eq "Medium"
|
||||
end
|
||||
|
||||
it "returns the default secrets for other tenants" do
|
||||
allow(Tenant).to receive(:current_schema).and_return("earth")
|
||||
|
||||
expect(Tenant.current_secrets.star).to eq "Sun"
|
||||
expect(Tenant.current_secrets.volume).to eq "Medium"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ".run_on_each" do
|
||||
it "runs the code on all tenants, including the default one" do
|
||||
create(:tenant, schema: "andromeda")
|
||||
create(:tenant, schema: "milky-way")
|
||||
|
||||
Tenant.run_on_each do
|
||||
Setting["org_name"] = "oh-my-#{Tenant.current_schema}"
|
||||
end
|
||||
|
||||
expect(Setting["org_name"]).to eq "oh-my-public"
|
||||
|
||||
Tenant.switch("andromeda") do
|
||||
expect(Setting["org_name"]).to eq "oh-my-andromeda"
|
||||
end
|
||||
|
||||
Tenant.switch("milky-way") do
|
||||
expect(Setting["org_name"]).to eq "oh-my-milky-way"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
let(:tenant) { build(:tenant) }
|
||||
|
||||
it "is valid" do
|
||||
expect(tenant).to be_valid
|
||||
end
|
||||
|
||||
it "is not valid without a schema" do
|
||||
tenant.schema = nil
|
||||
expect(tenant).not_to be_valid
|
||||
end
|
||||
|
||||
it "is not valid with an already existing schema" do
|
||||
expect(create(:tenant, schema: "subdomainx")).to be_valid
|
||||
expect(build(:tenant, schema: "subdomainx")).not_to be_valid
|
||||
end
|
||||
|
||||
it "is not valid with an excluded subdomain" do
|
||||
%w[mail public shared_extensions www].each do |subdomain|
|
||||
tenant.schema = subdomain
|
||||
expect(tenant).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
it "is valid with nested subdomains" do
|
||||
tenant.schema = "multiple.sub.domains"
|
||||
expect(tenant).to be_valid
|
||||
end
|
||||
|
||||
it "is not valid with an invalid subdomain" do
|
||||
tenant.schema = "my sub domain"
|
||||
expect(tenant).not_to be_valid
|
||||
end
|
||||
|
||||
it "is not valid without a name" do
|
||||
tenant.name = ""
|
||||
expect(tenant).not_to be_valid
|
||||
end
|
||||
|
||||
it "is not valid with an already existing name" do
|
||||
expect(create(:tenant, name: "Name X")).to be_valid
|
||||
expect(build(:tenant, name: "Name X")).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe "#create_schema" do
|
||||
it "creates a schema creating a record" do
|
||||
create(:tenant, schema: "new")
|
||||
expect { Tenant.switch("new") { nil } }.not_to raise_exception
|
||||
end
|
||||
end
|
||||
|
||||
describe "#rename_schema" do
|
||||
it "renames the schema when updating the schema" do
|
||||
tenant = create(:tenant, schema: "typo")
|
||||
tenant.update!(schema: "notypo")
|
||||
|
||||
expect { Tenant.switch("typo") { nil } }.to raise_exception(Apartment::TenantNotFound)
|
||||
expect { Tenant.switch("notypo") { nil } }.not_to raise_exception
|
||||
end
|
||||
end
|
||||
|
||||
describe "#destroy_schema" do
|
||||
it "drops the schema when destroying a record" do
|
||||
tenant = create(:tenant, schema: "wrong")
|
||||
tenant.destroy!
|
||||
|
||||
expect { Tenant.switch("wrong") { nil } }.to raise_exception(Apartment::TenantNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -65,3 +65,14 @@ Capybara.enable_aria_label = true
|
||||
Capybara.disable_animation = true
|
||||
|
||||
OmniAuth.config.test_mode = true
|
||||
|
||||
def with_subdomain(subdomain, &block)
|
||||
app_host = Capybara.app_host
|
||||
|
||||
begin
|
||||
Capybara.app_host = "http://#{subdomain}.lvh.me"
|
||||
block.call
|
||||
ensure
|
||||
Capybara.app_host = app_host
|
||||
end
|
||||
end
|
||||
|
||||
@@ -117,6 +117,14 @@ RSpec.configure do |config|
|
||||
Delayed::Worker.delay_jobs = false
|
||||
end
|
||||
|
||||
config.before(:each, :seed_tenants) do
|
||||
Apartment.seed_after_create = true
|
||||
end
|
||||
|
||||
config.after(:each, :seed_tenants) do
|
||||
Apartment.seed_after_create = false
|
||||
end
|
||||
|
||||
config.before(:each, :small_window) do
|
||||
@window_size = Capybara.current_window.size
|
||||
Capybara.current_window.resize_to(639, 479)
|
||||
|
||||
@@ -207,7 +207,7 @@ describe "Machine learning" do
|
||||
end
|
||||
|
||||
scenario "Show output files info on settins page" do
|
||||
FileUtils.mkdir_p MachineLearning::DATA_FOLDER
|
||||
FileUtils.mkdir_p MachineLearning.data_folder
|
||||
|
||||
allow_any_instance_of(MachineLearning).to receive(:run) do
|
||||
MachineLearningJob.first.update!(finished_at: 2.minutes.from_now)
|
||||
@@ -215,9 +215,9 @@ describe "Machine learning" do
|
||||
script: "proposals_summary_comments_textrank.py",
|
||||
kind: "comments_summary",
|
||||
updated_at: 2.minutes.from_now)
|
||||
comments_file = MachineLearning::DATA_FOLDER.join(MachineLearning.comments_filename)
|
||||
comments_file = MachineLearning.data_folder.join(MachineLearning.comments_filename)
|
||||
File.write(comments_file, [].to_json)
|
||||
proposals_comments_summary_file = MachineLearning::DATA_FOLDER.join(MachineLearning.proposals_comments_summary_filename)
|
||||
proposals_comments_summary_file = MachineLearning.data_folder.join(MachineLearning.proposals_comments_summary_filename)
|
||||
File.write(proposals_comments_summary_file, [].to_json)
|
||||
end
|
||||
|
||||
|
||||
45
spec/system/admin/tenants_spec.rb
Normal file
45
spec/system/admin/tenants_spec.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe "Tenants", :admin, :seed_tenants do
|
||||
before { allow(Tenant).to receive(:default_host).and_return("localhost") }
|
||||
|
||||
scenario "Create" do
|
||||
visit admin_root_path
|
||||
|
||||
within("#side_menu") do
|
||||
click_link "Settings"
|
||||
click_link "Multitenancy"
|
||||
end
|
||||
|
||||
click_link "Create tenant"
|
||||
fill_in "Subdomain", with: "earth"
|
||||
fill_in "Name", with: "Earthlings"
|
||||
click_button "Create tenant"
|
||||
|
||||
expect(page).to have_content "Tenant created successfully"
|
||||
|
||||
within("tr", text: "earth") { click_link "View" }
|
||||
|
||||
expect(current_host).to eq "http://earth.lvh.me"
|
||||
expect(page).to have_current_path root_path
|
||||
expect(page).to have_link "Sign in"
|
||||
end
|
||||
|
||||
scenario "Update" do
|
||||
create(:tenant, schema: "moon")
|
||||
|
||||
visit admin_tenants_path
|
||||
within("tr", text: "moon") { click_link "Edit" }
|
||||
|
||||
fill_in "Subdomain", with: "the-moon"
|
||||
click_button "Update tenant"
|
||||
|
||||
expect(page).to have_content "Tenant updated successfully"
|
||||
|
||||
within("tr", text: "the-moon") { click_link "View" }
|
||||
|
||||
expect(current_host).to eq "http://the-moon.lvh.me"
|
||||
expect(page).to have_current_path root_path
|
||||
expect(page).to have_link "Sign in"
|
||||
end
|
||||
end
|
||||
172
spec/system/multitenancy_spec.rb
Normal file
172
spec/system/multitenancy_spec.rb
Normal file
@@ -0,0 +1,172 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe "Multitenancy", :seed_tenants do
|
||||
before do
|
||||
create(:tenant, schema: "mars")
|
||||
create(:tenant, schema: "venus")
|
||||
end
|
||||
|
||||
scenario "Disabled features", :no_js do
|
||||
Tenant.switch("mars") { Setting["process.debates"] = true }
|
||||
Tenant.switch("venus") { Setting["process.debates"] = nil }
|
||||
|
||||
with_subdomain("mars") do
|
||||
visit debates_path
|
||||
|
||||
expect(page).to have_css "#debates"
|
||||
end
|
||||
|
||||
with_subdomain("venus") do
|
||||
expect { visit debates_path }.to raise_exception(FeatureFlags::FeatureDisabled)
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Content is different for differents tenants" do
|
||||
Tenant.switch("mars") { create(:poll, name: "Human rights for Martians?") }
|
||||
|
||||
with_subdomain("mars") do
|
||||
visit polls_path
|
||||
|
||||
expect(page).to have_content "Human rights for Martians?"
|
||||
expect(page).to have_css "html.tenant-mars"
|
||||
expect(page).not_to have_css "html.tenant-venus"
|
||||
end
|
||||
|
||||
with_subdomain("venus") do
|
||||
visit polls_path
|
||||
|
||||
expect(page).to have_content "There are no open votings"
|
||||
expect(page).to have_css "html.tenant-venus"
|
||||
expect(page).not_to have_css "html.tenant-mars"
|
||||
end
|
||||
end
|
||||
|
||||
scenario "PostgreSQL extensions work for tenants" do
|
||||
Tenant.switch("mars") { login_as(create(:user)) }
|
||||
|
||||
with_subdomain("mars") do
|
||||
visit new_proposal_path
|
||||
fill_in "Proposal title", with: "Use the unaccent extension in Mars"
|
||||
fill_in "Proposal summary", with: "tsvector for María the Martian"
|
||||
check "I agree to the Privacy Policy and the Terms and conditions of use"
|
||||
|
||||
click_button "Create proposal"
|
||||
|
||||
expect(page).to have_content "Proposal created successfully."
|
||||
|
||||
click_link "No, I want to publish the proposal"
|
||||
|
||||
expect(page).to have_content "You've created a proposal!"
|
||||
|
||||
visit proposals_path
|
||||
click_button "Advanced search"
|
||||
fill_in "With the text", with: "Maria the Martian"
|
||||
click_button "Filter"
|
||||
|
||||
expect(page).to have_content "Search results"
|
||||
expect(page).to have_content "María the Martian"
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Creating content in one tenant doesn't affect other tenants" do
|
||||
Tenant.switch("mars") { login_as(create(:user)) }
|
||||
|
||||
with_subdomain("mars") do
|
||||
visit new_debate_path
|
||||
fill_in "Debate title", with: "Found any water here?"
|
||||
fill_in_ckeditor "Initial debate text", with: "Found any water here?"
|
||||
check "I agree to the Privacy Policy and the Terms and conditions of use"
|
||||
|
||||
click_button "Start a debate"
|
||||
|
||||
expect(page).to have_content "Debate created successfully."
|
||||
expect(page).to have_content "Found any water here?"
|
||||
end
|
||||
|
||||
with_subdomain("venus") do
|
||||
visit debates_path
|
||||
|
||||
expect(page).to have_content "Sign in"
|
||||
expect(page).not_to have_css ".debate"
|
||||
|
||||
visit new_debate_path
|
||||
|
||||
expect(page).to have_content "You must sign in or register to continue."
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Users from another tenant cannot vote" do
|
||||
Tenant.switch("mars") { create(:proposal, title: "Earth invasion") }
|
||||
Tenant.switch("venus") { login_as(create(:user)) }
|
||||
|
||||
with_subdomain("venus") do
|
||||
visit proposals_path
|
||||
|
||||
expect(page).to have_content "Sign out"
|
||||
expect(page).not_to have_content "Earth invasion"
|
||||
end
|
||||
|
||||
with_subdomain("mars") do
|
||||
visit proposals_path
|
||||
|
||||
within(".proposal", text: "Earth invasion") do
|
||||
click_button "Support"
|
||||
|
||||
expect(page).to have_content "You must sign in or sign up to continue"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Sign up into subdomain" do
|
||||
with_subdomain("mars") do
|
||||
visit "/"
|
||||
click_link "Register"
|
||||
|
||||
fill_in "Username", with: "Marty McMartian"
|
||||
fill_in "Email", with: "marty@consul.dev"
|
||||
fill_in "Password", with: "20151021"
|
||||
fill_in "Confirm password", with: "20151021"
|
||||
check "By registering you accept the terms and conditions of use"
|
||||
click_button "Register"
|
||||
|
||||
confirm_email
|
||||
|
||||
expect(page).to have_content "Your account has been confirmed."
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Users from another tenant can't sign in" do
|
||||
Tenant.switch("mars") { create(:user, email: "marty@consul.dev", password: "20151021") }
|
||||
|
||||
with_subdomain("mars") do
|
||||
visit new_user_session_path
|
||||
fill_in "Email or username", with: "marty@consul.dev"
|
||||
fill_in "Password", with: "20151021"
|
||||
click_button "Enter"
|
||||
|
||||
expect(page).to have_content "You have been signed in successfully."
|
||||
end
|
||||
|
||||
with_subdomain("venus") do
|
||||
visit new_user_session_path
|
||||
fill_in "Email or username", with: "marty@consul.dev"
|
||||
fill_in "Password", with: "20151021"
|
||||
click_button "Enter"
|
||||
|
||||
expect(page).to have_content "Invalid Email or username or password."
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Uses the right tenant after failing to sign in" do
|
||||
with_subdomain("mars") do
|
||||
visit new_user_session_path
|
||||
fill_in "Email or username", with: "wrong@consul.dev"
|
||||
fill_in "Password", with: "wrong"
|
||||
click_button "Enter"
|
||||
|
||||
expect(page).to have_content "Invalid Email or username or password"
|
||||
expect(page).to have_css "html.tenant-mars"
|
||||
expect(page).not_to have_css "html.tenant-public"
|
||||
end
|
||||
end
|
||||
end
|
||||
19
spec/system/robots_spec.rb
Normal file
19
spec/system/robots_spec.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe "robots.txt" do
|
||||
scenario "uses the default sitemap for the default tenant" do
|
||||
visit "/robots.txt"
|
||||
|
||||
expect(page).to have_content "Sitemap: #{app_host}/sitemap.xml"
|
||||
end
|
||||
|
||||
scenario "uses a different sitemap for other tenants" do
|
||||
create(:tenant, schema: "cyborgs")
|
||||
|
||||
with_subdomain("cyborgs") do
|
||||
visit "/robots.txt"
|
||||
|
||||
expect(page).to have_content "Sitemap: http://cyborgs.lvh.me:#{app_port}/tenants/cyborgs/sitemap.xml"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user