Merge v2.7-alpha (#1951)

* Fix wrong conditional (reported by LGTM) (#1477)

Signed-off-by: Stefan Weil <sw@weilnetz.de>

Co-authored-by: Ahmad Farhat <ahmad.af.farhat@gmail.com>

* Bump rack from 2.2.2 to 2.2.3 (#1839)

Bumps [rack](https://github.com/rack/rack) from 2.2.2 to 2.2.3.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/compare/v2.2.2...2.2.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* [FIX]  Unable to edit long recording names #1776  (#1780)

* Allow to set a filter for LDAP authentication

* [FIX] Unable to edit long recording names #1776

Co-authored-by: François Ménabé <francois.menabe@unistra.fr>
Co-authored-by: farhatahmad <ahmad.af.farhat@gmail.com>

* Desgin for Manage Users Tabs (#1777)

* Update _subtitle.html.erb

* Update _manage_users_tags.html.erb

* Update admins.scss

* Update _primary_themes.scss

* Update _manage_users_tags.html.erb

* Minor style changes to manage users (#1845)

* Maintenance banner moved to admin site (#1775)

* initial

* finish

* travis fixes

* travis again

* not required

* Co-authored-by: Tobias Fiebig <t.fiebig@tudelft.nl> (#1296)

Co-authored-by: Ahmad Farhat <ahmad.af.farhat@gmail.com>

* Enhance Room OpenGraph Metadata (#1601)

* Revert "Enhance Room OpenGraph Metadata (#1601)" (#1852)

This reverts commit 3b007c233ae12e0407f216ae269c63d6179f73b8.

* GRN2-xx: Tab title now displays the current page name (#1853)

* Tab title now displays the current page name

* Added page title for the rest of the pages

* Split Site Settings into 3 different tabs (#1858)

* Split Site Settings into 3 different tabs

* Fix copyright

* Added redirect to correct tab

* Make sure settings are displaying when they should

* Update en.yml (#1857)

* Build images for alpha branches (#1867)

* Upgraded jquery to latest version (#1896)

* Added favicon tag (#1898)

* Fixed XSS issue with role name (#1899)

* Update path for coloring redirect (#1908)

* Added a fourth section to the room uid (#1910)

* Fixed issue with insecure room sharing removal (#1914)

* Fixes typo (#1917)

Fixes typo: successfully was written incorrect.

* Fixed order of rooms in server rooms (#1915)

* Change default room sort to latest activity (#1919)

* GRN2-xx: Small changes/improvements to the recording settings (#1851)

* Small changes/improvements to the recording settings

* Replaced room warning with info flash

* Added global setting to enable/disable the recording consent feature

* Replace Legal with Terms (#1931)

* Added a more friendly OpenGraph description when invited to join a room (#1932)

* Fixed issue causing maintenance banner not to hide correctly (#1933)

* Hide recording menu and recording list when it is disabled (#1935)

* Hide recording menu and recording list when it is disabled

* Hide recording list when disabled

* GRN2-xx: Added an auto-refresh after 2 mins while waiting for room to start (#1947)

* Added an auto-refresh after 2 mins while waiting for room to start

* Fixed random issue with test case

* GRN2-xx: Added ability to preupload presentations to rooms (#1895)

* Added ability to preupload presentations to rooms (#1868)

* Added setting to site settings and allowed admins to change the presentation

* Added AWS S3 and GCS Storage ENV variables

* Added check to ensure file extension is correct

* Added icon to remove presentation

* Added testcases for preupload

* Add nginx redirect to solve issue with relative root

* Record title, instead of room name, in the popup (#1924)

* Update _public_recording_row.html.erb

* Update _recording_row.html.erb

Co-authored-by: Stefan Weil <sw@weilnetz.de>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: beckerr-rzht <beckerr@hochschule-trier.de>
Co-authored-by: François Ménabé <francois.menabe@unistra.fr>
Co-authored-by: MrKeksi <mrkeksi@users.noreply.github.com>
Co-authored-by: yanosz <yanosz@users.noreply.github.com>
Co-authored-by: Moritz Schlarb <moschlar@metalabs.de>
Co-authored-by: chronikum <34622984+chronikum@users.noreply.github.com>
Co-authored-by: Mitsutaka Sato <miztaka@honestyworks.jp>
Co-authored-by: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com>
v2
Ahmad Farhat 3 years ago committed by GitHub
parent 50c2070188
commit 60cf5f7440
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 8
      Gemfile
  3. 91
      Gemfile.lock
  4. BIN
      app/assets/images/favicon.ico
  5. 27
      app/assets/javascripts/admins.js
  6. 2
      app/assets/javascripts/application.js
  7. 9
      app/assets/javascripts/cookies.js
  8. 70
      app/assets/javascripts/room.js
  9. 10
      app/assets/javascripts/wait.js
  10. 12
      app/assets/stylesheets/admins.scss
  11. 4
      app/assets/stylesheets/application.scss
  12. 9
      app/assets/stylesheets/rooms.scss
  13. 16
      app/controllers/admins_controller.rb
  14. 18
      app/controllers/application_controller.rb
  15. 16
      app/controllers/concerns/bbb_server.rb
  16. 2
      app/controllers/concerns/joiner.rb
  17. 64
      app/controllers/rooms_controller.rb
  18. 20
      app/helpers/admins_helper.rb
  19. 7
      app/helpers/application_helper.rb
  20. 4
      app/helpers/rooms_helper.rb
  21. 4
      app/helpers/theming_helper.rb
  22. 2
      app/models/ability.rb
  23. 29
      app/models/room.rb
  24. 6
      app/models/setting.rb
  25. 2
      app/views/admins/components/_admins_role.html.erb
  26. 31
      app/views/admins/components/_manage_users_tags.html.erb
  27. 29
      app/views/admins/components/_room_settings.html.erb
  28. 5
      app/views/admins/components/_server_room_row.html.erb
  29. 269
      app/views/admins/components/_settings.html.erb
  30. 96
      app/views/admins/components/site_settings/_administration.html.erb
  31. 56
      app/views/admins/components/site_settings/_appearance.html.erb
  32. 201
      app/views/admins/components/site_settings/_settings.html.erb
  33. 3
      app/views/admins/server_rooms.html.erb
  34. 9
      app/views/layouts/application.html.erb
  35. 5
      app/views/rooms/components/_room_block.html.erb
  36. 9
      app/views/rooms/components/_room_event.html.erb
  37. 10
      app/views/rooms/join.html.erb
  38. 12
      app/views/rooms/show.html.erb
  39. 2
      app/views/shared/_flash_messages.html.erb
  40. 4
      app/views/shared/_header.html.erb
  41. 11
      app/views/shared/components/_public_recording_row.html.erb
  42. 11
      app/views/shared/components/_recording_row.html.erb
  43. 12
      app/views/shared/components/_subtitle.html.erb
  44. 10
      app/views/shared/modals/_create_room_modal.html.erb
  45. 52
      app/views/shared/modals/_preupload_presentation_modal.html.erb
  46. 1
      app/views/shared/modals/_remove_access_modal.html.erb
  47. 11
      config/application.rb
  48. 8
      config/environments/production.rb
  49. 50
      config/locales/en.yml
  50. 3
      config/routes.rb
  51. 32
      config/storage.yml
  52. 29
      db/migrate/20200615190507_create_active_storage_tables.active_storage.rb
  53. 23
      db/schema.rb
  54. 1
      docker-compose.yml
  55. 7
      greenlight.nginx
  56. 14
      lib/assets/_primary_themes.scss
  57. 14
      lib/tasks/room.rake
  58. 2
      lib/tasks/user.rake
  59. 24
      sample.env
  60. 2
      scripts/image_build.sh
  61. 54
      spec/controllers/admins_controller_spec.rb
  62. 119
      spec/controllers/rooms_controller_spec.rb
  63. BIN
      spec/fixtures/files/invalid.jpg
  64. 198
      spec/fixtures/files/sample.pdf

3
.gitignore vendored

@ -17,6 +17,9 @@ vendor/bundle
/public/assets/**
/public/b/**
# Ignore uploaded files
/storage
# Ignore production paths.
/db/production
/db/production-postgres

@ -26,7 +26,7 @@ gem 'coffee-rails', '~> 4.2'
# gem 'mini_racer', platforms: :ruby
# Use jquery as the JavaScript library
gem 'jquery-rails', '~> 4.3.3'
gem 'jquery-rails', '~> 4.4'
gem 'jquery-ui-rails'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
@ -56,7 +56,7 @@ gem 'bn-ldap-authentication', '~> 0.1.4'
gem 'omniauth-bn-office365', '~> 0.1.1'
# BigBlueButton API wrapper.
gem 'bigbluebutton-api-ruby'
gem 'bigbluebutton-api-ruby', git: 'https://github.com/mconf/bigbluebutton-api-ruby.git', branch: 'master'
# Front-end.
gem 'bootstrap', '~> 4.3.1'
@ -76,6 +76,10 @@ gem 'redcarpet'
# For limiting access based on user roles
gem 'cancancan', '~> 2.0'
# Active Storage gems
gem 'aws-sdk-s3', '~> 1.75'
gem 'google-cloud-storage', '~> 1.26'
group :production do
# Use a postgres database in production.
gem 'pg', '~> 0.18'

@ -6,6 +6,20 @@ GIT
tabler-rubygem (0.1.4.1)
autoprefixer-rails (>= 6.0.3)
GIT
remote: https://github.com/mconf/bigbluebutton-api-ruby.git
revision: 91dc495324a6b7e162773227ec3650f8a5b39c50
branch: master
specs:
bigbluebutton-api-ruby (1.7.0)
childprocess (>= 1.0.1)
ffi (>= 1.9.24)
json (>= 1.8.6)
nokogiri (>= 1.10.4)
rack (>= 1.6.11)
rubyzip (>= 1.3.0)
xml-simple (~> 1.1)
GEM
remote: https://rubygems.org/
specs:
@ -58,9 +72,23 @@ GEM
ast (2.4.0)
autoprefixer-rails (9.7.6)
execjs
aws-eventstream (1.1.0)
aws-partitions (1.343.0)
aws-sdk-core (3.104.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.36.0)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.75.0)
aws-sdk-core (~> 3, >= 3.104.1)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.1)
aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.13)
bigbluebutton-api-ruby (1.7.0)
xml-simple (~> 1.1)
bindex (0.8.1)
bn-ldap-authentication (0.1.4)
net-ldap (~> 0)
@ -73,6 +101,7 @@ GEM
builder (3.2.4)
byebug (11.1.3)
cancancan (2.3.0)
childprocess (4.0.0)
coffee-rails (4.2.2)
coffee-script (>= 2.2.0)
railties (>= 4.0.0)
@ -90,7 +119,11 @@ GEM
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.6)
declarative (0.0.20)
declarative-option (0.1.0)
diff-lcs (1.3)
digest-crc (0.6.1)
rake (~> 13.0)
docile (1.3.2)
dotenv (2.7.5)
dotenv-rails (2.7.5)
@ -112,16 +145,46 @@ GEM
sassc (>= 1.11)
globalid (0.4.2)
activesupport (>= 4.2.0)
google-api-client (0.42.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.12)
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.3.3)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.1)
google-cloud-storage (1.26.2)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.13.0)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.14)
hashdiff (1.0.1)
hashie (4.1.0)
hiredis (0.6.3)
http_accept_language (2.1.1)
httpclient (2.8.3)
i18n (1.8.2)
concurrent-ruby (~> 1.0)
i18n-language-mapping (0.1.2)
jbuilder (2.10.0)
activesupport (>= 5.0.0)
jquery-rails (4.3.5)
jmespath (1.4.0)
jquery-rails (4.4.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
@ -144,6 +207,7 @@ GEM
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
memoist (0.16.2)
method_source (1.0.0)
mimemagic (0.3.5)
mini_mime (1.0.2)
@ -186,6 +250,7 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
os (1.1.0)
pagy (3.8.1)
parallel (1.19.1)
parser (2.7.1.3)
@ -194,7 +259,7 @@ GEM
popper_js (1.16.0)
public_suffix (4.0.5)
puma (3.12.6)
rack (2.2.2)
rack (2.2.3)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.4.3)
@ -237,8 +302,13 @@ GEM
redis (4.1.4)
remote_syslog_logger (1.0.4)
syslog_protocol
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
request_store (1.5.0)
rack (>= 1.4)
retriable (3.1.2)
rexml (3.2.4)
rspec-core (3.9.2)
rspec-support (~> 3.9.3)
@ -268,6 +338,7 @@ GEM
rubocop-ast (0.0.3)
parser (>= 2.7.0.1)
ruby-progressbar (1.10.1)
rubyzip (2.3.0)
safe_yaml (1.0.5)
sassc (2.3.0)
ffi (~> 1.9)
@ -280,6 +351,11 @@ GEM
sequel (5.32.0)
shoulda-matchers (3.1.3)
activesupport (>= 4.0.0)
signet (0.14.0)
addressable (~> 2.3)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simplecov (0.16.1)
docile (~> 1.1)
json (>= 1.8, < 3)
@ -313,6 +389,7 @@ GEM
thread_safe (~> 0.1)
tzinfo-data (1.2020.1)
tzinfo (>= 1.0.0)
uber (0.1.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unicode-display_width (1.7.0)
@ -335,8 +412,9 @@ PLATFORMS
DEPENDENCIES
action-cable-testing
aws-sdk-s3 (~> 1.75)
bcrypt (~> 3.1.7)
bigbluebutton-api-ruby
bigbluebutton-api-ruby!
bn-ldap-authentication (~> 0.1.4)
bootsnap (>= 1.1.0)
bootstrap (~> 4.3.1)
@ -348,11 +426,12 @@ DEPENDENCIES
factory_bot_rails
faker
font-awesome-sass (~> 5.9.0)
google-cloud-storage (~> 1.26)
hiredis
http_accept_language
i18n-language-mapping (~> 0.1.1)
jbuilder (~> 2.5)
jquery-rails (~> 4.3.3)
jquery-rails (~> 4.4)
jquery-ui-rails
listen (~> 3.0.5)
lograge

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -86,7 +86,11 @@ $(document).on('turbolinks:load', function(){
})
}
else if(action == "site_settings"){
loadColourSelectors()
var urlParams = new URLSearchParams(window.location.search);
// Only load the colour selectors if on the appearance tab
if (urlParams.get("tab") == null || urlParams.get("tab") == "appearance") {
loadColourSelectors()
}
}
else if (action == "roles"){
// Refreshes the new role modal
@ -119,19 +123,30 @@ $(document).on('turbolinks:load', function(){
// Change the branding image to the image provided
function changeBrandingImage(path) {
var url = $("#branding-url").val()
$.post(path, {value: url})
$.post(path, {value: url, tab: "appearance"})
}
// Change the Legal URL to the one provided
function changeLegalURL(path) {
var url = $("#legal-url").val()
$.post(path, {value: url})
$.post(path, {value: url, tab: "administration"})
}
// Change the Privacy Policy URL to the one provided
function changePrivacyPolicyURL(path) {
var url = $("#privpolicy-url").val()
$.post(path, {value: url})
$.post(path, {value: url, tab: "administration"})
}
// Display the maintenance Banner
function displayMaintenanceBanner(path) {
var message = $("#maintenance-banner").val()
$.post(path, {value: message, tab: "administration"})
}
// Clear the maintenance Banner
function clearMaintenanceBanner(path) {
$.post(path, {value: "", tab: "administration"})
}
function mergeUsers() {
@ -234,13 +249,13 @@ function loadColourSelectors() {
})
pickrLighten.on("save", (color, instance) => {
$.post($("#coloring-path-lighten").val(), {value: color.toHEXA().toString()}).done(function() {
$.post($("#coloring-path-lighten").val(), {value: color.toHEXA().toString(), tab: "appearance"}).done(function() {
location.reload()
});
})
pickrDarken.on("save", (color, instance) => {
$.post($("#coloring-path-darken").val(), {value: color.toHEXA().toString()}).done(function() {
$.post($("#coloring-path-darken").val(), {value: color.toHEXA().toString(), tab: "appearance"}).done(function() {
location.reload()
});
})

@ -27,7 +27,7 @@
// about supported directives.
//
//= require turbolinks
//= require jquery
//= require jquery3
//= require tabler
//= require tabler.plugins
//= require jquery_ujs

@ -26,9 +26,12 @@ $(document).on('turbolinks:load', function(){
})
$("#maintenance-close").click(function(event) {
//create a cookie that lasts 1 year
var cookieDate = new Date();
cookieDate.setFullYear(cookieDate.getFullYear() + 1); //1 year from now
//create a cookie that lasts 1 day
var cookieDate = new Date()
cookieDate.setDate(cookieDate.getDate() + 1) //1 day from now
console.log("maintenance_window=" + $(event.target).data("date") + "; path=/; expires=" + cookieDate.toUTCString() + ";")
document.cookie = "maintenance_window=" + $(event.target).data("date") + "; path=/; expires=" + cookieDate.toUTCString() + ";"
})
})

@ -48,6 +48,8 @@ $(document).on('turbolinks:load', function(){
$("#create-room-block").click(function(){
showCreateRoom(this)
})
checkIfAutoJoin()
}
// Autofocus on the Room Name label when creating a room only
@ -129,6 +131,27 @@ $(document).on('turbolinks:load', function(){
$("#user-list").append(listItem)
}
})
$("#presentation-upload").change(function(data) {
var file = data.target.files[0]
// Check file type and size to make sure they aren't over the limit
if (validFileUpload(file)) {
$("#presentation-upload-label").text(file.name)
} else {
$("#invalid-file-type").show()
$("#presentation-upload").val("")
$("#presentation-upload-label").text($("#presentation-upload-label").data("placeholder"))
}
})
$(".preupload-room").click(function() {
updatePreuploadPresentationModal(this)
})
$("#remove-presentation").click(function(data) {
removePreuploadPresentation($(this).data("remove"))
})
}
});
@ -138,11 +161,11 @@ function showCreateRoom(target) {
$("#room_access_code").val(null)
$("#createRoomModal form").attr("action", $("body").data('relative-root'))
$("#room_mute_on_join").prop("checked", $("#room_mute_on_join").data("default"))
$("#room_require_moderator_approval").prop("checked", $("#room_require_moderator_approval").data("default"))
$("#room_anyone_can_start").prop("checked", $("#room_anyone_can_start").data("default"))
$("#room_all_join_moderator").prop("checked", $("#room_all_join_moderator").data("default"))
$("#room_recording").prop("checked", $("#room_recording").data("default"))
//show all elements & their children with a create-only class
$(".create-only").each(function() {
@ -197,12 +220,12 @@ function showDeleteRoom(target) {
//Update the createRoomModal to show the correct current settings
function updateCurrentSettings(settings_path){
// Get current room settings and set checkbox
$.get(settings_path, function(room_settings) {
var settings = JSON.parse(room_settings)
$.get(settings_path, function(settings) {
$("#room_mute_on_join").prop("checked", $("#room_mute_on_join").data("default") || settings.muteOnStart)
$("#room_require_moderator_approval").prop("checked", $("#room_require_moderator_approval").data("default") || settings.requireModeratorApproval)
$("#room_anyone_can_start").prop("checked", $("#room_anyone_can_start").data("default") || settings.anyoneCanStart)
$("#room_all_join_moderator").prop("checked", $("#room_all_join_moderator").data("default") || settings.joinModerator)
$("#room_recording").prop("checked", $("#room_recording").data("default") || Boolean(settings.recording))
})
}
@ -264,3 +287,44 @@ function removeSharedUser(target) {
parentLI.classList.add("remove-shared")
}
}
function updatePreuploadPresentationModal(target) {
$.get($(target).data("settings-path"), function(presentation) {
if(presentation.attached) {
$("#current-presentation").show()
$("#presentation-name").text(presentation.name)
$("#change-pres").show()
$("#use-pres").hide()
} else {
$("#current-presentation").hide()
$("#change-pres").hide()
$("#use-pres").show()
}
});
$("#preuploadPresentationModal form").attr("action", $(target).data("path"))
$("#remove-presentation").data("remove", $(target).data("remove"))
// Reset values to original to prevent confusion
$("#presentation-upload").val("")
$("#presentation-upload-label").text($("#presentation-upload-label").data("placeholder"))
$("#invalid-file-type").hide()
}
function removePreuploadPresentation(path) {
$.post(path, {})
}
function validFileUpload(file) {
return file.size/1024/1024 <= 30
}
// Automatically click the join button if this is an action cable reload
function checkIfAutoJoin() {
var url = new URL(window.location.href)
if (url.searchParams.get("reload") == "true") {
$("#joiner-consent").click()
$("#room-join").click()
}
}

@ -27,6 +27,7 @@ $(document).on("turbolinks:load", function(){
}, {
connected: function() {
console.log("connected");
setTimeout(startRefreshTimeout, 120000);
},
disconnected: function(data) {
@ -40,7 +41,7 @@ $(document).on("turbolinks:load", function(){
received: function(data){
console.log(data);
if(data.action = "started"){
if(data.action == "started"){
request_to_join_meeting();
}
}
@ -68,3 +69,10 @@ var request_to_join_meeting = function(){
}
});
}
// Refresh the page after 2 mins and attempt to reconnect to ActionCable
function startRefreshTimeout() {
var url = new URL(window.location.href)
url.searchParams.set("reload","true")
window.location.href = url.href
}

@ -84,10 +84,8 @@
color: white !important;
}
.manage-users-tab {
&:hover {
cursor: pointer;
}
#manage-users-nav.nav-tabs .nav-item {
margin-bottom: -1px;
}
#merge-account-arrow {
@ -96,4 +94,8 @@
right: 47%;
z-index: 999;
background: white;
}
}
.admin-tabs {
justify-content: space-around;
}

@ -145,6 +145,10 @@ input:focus {
border-color: $primary !important;
}
.input-group button:focus {
box-shadow: none !important;
}
.list-group-item-action.active {
color: $primary;
}

@ -113,3 +113,12 @@
background: lightgray;
pointer-events: none;
}
#recording-table .edit_hover_class {
word-break: break-all;
white-space: normal;
}
#room-owner-name {
line-height: 12px;
}

@ -47,6 +47,7 @@ class AdminsController < ApplicationController
# GET /admins/site_settings
def site_settings
@tab = params[:tab] || "appearance"
end
# GET /admins/server_recordings
@ -191,6 +192,7 @@ class AdminsController < ApplicationController
# POST /admins/update_settings
def update_settings
tab = params[:tab] || "settings"
@settings.update_value(params[:setting], params[:value])
flash_message = I18n.t("administrator.flash.settings")
@ -199,7 +201,7 @@ class AdminsController < ApplicationController
flash_message += ". " + I18n.t("administrator.site_settings.recording_visibility.warning")
end
redirect_to admin_site_settings_path, flash: { success: flash_message }
redirect_to admin_site_settings_path(tab: tab), flash: { success: flash_message }
end
# POST /admins/color
@ -207,7 +209,7 @@ class AdminsController < ApplicationController
@settings.update_value("Primary Color", params[:value])
@settings.update_value("Primary Color Lighten", color_lighten(params[:value]))
@settings.update_value("Primary Color Darken", color_darken(params[:value]))
redirect_to admin_site_settings_path, flash: { success: I18n.t("administrator.flash.settings") }
redirect_to admin_site_settings_path(tab: "appearance"), flash: { success: I18n.t("administrator.flash.settings") }
end
# POST /admins/registration_method/:method
@ -216,11 +218,11 @@ class AdminsController < ApplicationController
# Only allow change to Join by Invitation if user has emails enabled
if !Rails.configuration.enable_email_verification && new_method == Rails.configuration.registration_methods[:invite]
redirect_to admin_site_settings_path,
redirect_to admin_site_settings_path(tab: "settings"),
flash: { alert: I18n.t("administrator.flash.invite_email_verification") }
else
@settings.update_value("Registration Method", new_method)
redirect_to admin_site_settings_path,
redirect_to admin_site_settings_path(tab: "settings"),
flash: { success: I18n.t("administrator.flash.registration_method_updated") }
end
end
@ -229,7 +231,7 @@ class AdminsController < ApplicationController
def clear_auth
User.include_deleted.where(provider: @user_domain).update_all(social_uid: nil)
redirect_to admin_site_settings_path, flash: { success: I18n.t("administrator.flash.settings") }
redirect_to admin_site_settings_path(tab: "settings"), flash: { success: I18n.t("administrator.flash.settings") }
end
# POST /admins/clear_cache
@ -237,14 +239,14 @@ class AdminsController < ApplicationController
Rails.cache.delete("#{@user_domain}/getUser")
Rails.cache.delete("#{@user_domain}/getUserGreenlightCredentials")
redirect_to admin_site_settings_path, flash: { success: I18n.t("administrator.flash.settings") }
redirect_to admin_site_settings_path(tab: "settings"), flash: { success: I18n.t("administrator.flash.settings") }
end
# POST /admins/log_level
def log_level
Rails.logger.level = params[:value].to_i
redirect_to admin_site_settings_path, flash: { success: I18n.t("administrator.flash.settings") }
redirect_to admin_site_settings_path(tab: "administration"), flash: { success: I18n.t("administrator.flash.settings") }
end
# ROOM CONFIGURATION

@ -84,9 +84,9 @@ class ApplicationController < ActionController::Base
help: I18n.t("errors.maintenance.help"),
}
end
if Rails.configuration.maintenance_window.present?
unless cookies[:maintenance_window] == Rails.configuration.maintenance_window
flash.now[:maintenance] = Rails.configuration.maintenance_window
if @settings.get_value("Maintenance Banner").present?
unless cookies[:maintenance_window] == @settings.get_value("Maintenance Banner")
flash.now[:maintenance] = @settings.get_value("Maintenance Banner")
end
end
end
@ -182,6 +182,18 @@ class ApplicationController < ActionController::Base
end
helper_method :shared_access_allowed
# Indicates whether users are allowed to share rooms
def recording_consent_required?
@settings.get_value("Require Recording Consent") == "true"
end
helper_method :recording_consent_required?
# Returns a list of allowed file types
def allowed_file_types
Rails.configuration.allowed_file_types
end
helper_method :allowed_file_types
# Returns the page that the logo redirects to when clicked on
def home_page
return admins_path if current_user.has_role? :super_admin

@ -61,7 +61,7 @@ module BbbServer
# Creates a meeting on the BigBlueButton server.
def start_session(room, options = {})
create_options = {
record: options[:meeting_recorded].to_s,
record: options[:record].to_s,
logoutURL: options[:meeting_logout_url] || '',
moderatorPW: room.moderator_pw,
attendeePW: room.attendee_pw,
@ -77,11 +77,17 @@ module BbbServer
# Send the create request.
begin
meeting = bbb_server.create_meeting(room.name, room.bbb_id, create_options)
# Update session info.
meeting = if room.presentation.attached?
modules = BigBlueButton::BigBlueButtonModules.new
logger.info("Support: Room #{room.uid} starting using presentation: #{rails_blob_url(room.presentation)}")
modules.add_presentation(:url, rails_blob_url(room.presentation))
bbb_server.create_meeting(room.name, room.bbb_id, create_options, modules)
else
bbb_server.create_meeting(room.name, room.bbb_id, create_options)
end
unless meeting[:messageKey] == 'duplicateWarning'
room.update_attributes(sessions: room.sessions + 1,
last_session: DateTime.now)
room.update_attributes(sessions: room.sessions + 1, last_session: DateTime.now)
end
rescue BigBlueButton::BigBlueButtonException => e
puts "BigBlueButton failed on create: #{e.key}: #{e.message}"

@ -105,6 +105,8 @@ module Joiner
"Room Configuration All Join Moderator"
when "anyoneCanStart"
"Room Configuration Allow Any Start"
when "recording"
"Room Configuration Recording"
end
case @settings.get_value(config)

@ -27,7 +27,7 @@ class RoomsController < ApplicationController
unless: -> { !Rails.configuration.enable_email_verification }
before_action :find_room, except: [:create, :join_specific_room, :cant_create_rooms]
before_action :verify_room_ownership_or_admin_or_shared, only: [:start, :shared_access]
before_action :verify_room_ownership_or_admin, only: [:update_settings, :destroy]
before_action :verify_room_ownership_or_admin, only: [:update_settings, :destroy, :preupload_presentation, :remove_presentation]
before_action :verify_room_ownership_or_shared, only: [:remove_shared_access]
before_action :verify_room_owner_verified, only: [:show, :join],
unless: -> { !Rails.configuration.enable_email_verification }
@ -171,6 +171,7 @@ class RoomsController < ApplicationController
@room_settings = JSON.parse(@room[:room_settings])
opts[:mute_on_start] = room_setting_with_config("muteOnStart")
opts[:require_moderator_approval] = room_setting_with_config("requireModeratorApproval")
opts[:record] = record_meeting
begin
redirect_to join_path(@room, current_user.name, opts, current_user.uid)
@ -209,6 +210,45 @@ class RoomsController < ApplicationController
redirect_back fallback_location: room_path(@room)
end
# GET /:room_uid/current_presentation
def current_presentation
attached = @room.presentation.attached?
# Respond with JSON object of presentation name
respond_to do |format|
format.json { render body: { attached: attached, name: attached ? @room.presentation.filename.to_s : "" }.to_json }
end
end
# POST /:room_uid/preupload_presenstation
def preupload_presentation
begin
raise "Invalid file type" unless valid_file_type
@room.presentation.attach(room_params[:presentation])
flash[:success] = I18n.t("room.preupload_success")
rescue => e
logger.error "Support: Error in updating room presentation: #{e}"
flash[:alert] = I18n.t("room.preupload_error")
end
redirect_back fallback_location: room_path(@room)
end
# POST /:room_uid/remove_presenstation
def remove_presentation
begin
@room.presentation.purge
flash[:success] = I18n.t("room.preupload_remove_success")
rescue => e
logger.error "Support: Error in removing room presentation: #{e}"
flash[:alert] = I18n.t("room.preupload_remove_error")
end
redirect_back fallback_location: room_path(@room)
end
# POST /:room_uid/update_shared_access
def shared_access
begin
@ -240,7 +280,7 @@ class RoomsController < ApplicationController
# POST /:room_uid/remove_shared_access
def remove_shared_access
begin
SharedAccess.find_by!(room_id: @room.id, user_id: params[:user_id]).destroy
SharedAccess.find_by!(room_id: @room.id, user_id: current_user).destroy
flash[:success] = I18n.t("room.remove_shared_access_success")
rescue => e
logger.error "Support: Error in removing room shared access: #{e}"
@ -262,7 +302,7 @@ class RoomsController < ApplicationController
def room_settings
# Respond with JSON object of the room_settings
respond_to do |format|
format.json { render body: @room.room_settings.to_json }
format.json { render body: @room.room_settings }
end
end
@ -291,6 +331,7 @@ class RoomsController < ApplicationController
"requireModeratorApproval": options[:require_moderator_approval] == "1",
"anyoneCanStart": options[:anyone_can_start] == "1",
"joinModerator": options[:all_join_moderator] == "1",
"recording": options[:recording] == "1",
}
room_settings.to_json
@ -298,7 +339,8 @@ class RoomsController < ApplicationController
def room_params
params.require(:room).permit(:name, :auto_join, :mute_on_join, :access_code,
:require_moderator_approval, :anyone_can_start, :all_join_moderator)
:require_moderator_approval, :anyone_can_start, :all_join_moderator,
:recording, :presentation)
end
# Find the room from the uid.
@ -364,4 +406,18 @@ class RoomsController < ApplicationController
current_user.rooms.length >= limit
end
helper_method :room_limit_exceeded
def record_meeting
# If the require consent setting is checked, then check the room setting, else, set to true
if recording_consent_required?
room_setting_with_config("recording")
else
true
end
end
# Checks if the file extension is allowed
def valid_file_type
Rails.configuration.allowed_file_types.split(",").include?(File.extname(room_params[:presentation].original_filename))
end
end

@ -61,6 +61,14 @@ module AdminsHelper
end
end
def preupload_string
if @settings.get_value("Preupload Presentation") == "true"
I18n.t("administrator.site_settings.authentication.enabled")
else
I18n.t("administrator.site_settings.authentication.disabled")
end
end
def recording_default_visibility_string
if @settings.get_value("Default Recording Visibility") == "public"
I18n.t("recording.visibility.public")
@ -80,6 +88,14 @@ module AdminsHelper
end
end
def require_consent_string
if @settings.get_value("Require Recording Consent") == "true"
I18n.t("administrator.site_settings.authentication.enabled")
else
I18n.t("administrator.site_settings.authentication.disabled")
end
end
def log_level_string
case Rails.logger.level
when 0
@ -97,6 +113,10 @@ module AdminsHelper
end
end
def show_log_dropdown
current_user.has_role?(:super_admin) || !Rails.configuration.loadbalanced_configuration
end
def room_limit_number
@settings.get_value("Room Limit").to_i
end

@ -117,4 +117,11 @@ module ApplicationHelper
rescue
false
end
# Specifies which title should be the tab title and returns original string
def title(page_title)
# Only set the content_for if not already set on the page so that only the first title appears as the tab title
content_for(:page_title) { page_title } if content_for(:page_title).blank?
page_title
end
end

@ -41,4 +41,8 @@ module RoomsHelper
def room_configuration(name)
@settings.get_value(name)
end
def preupload_allowed?
@settings.get_value("Preupload Presentation") == "true"
end
end

@ -36,4 +36,8 @@ module ThemingHelper
def user_color
@settings.get_value("Primary Color") || Rails.configuration.primary_color_default
end
def maintenance_banner
@settings.get_value("Maintenance Banner")
end
end

@ -28,7 +28,7 @@ class Ability
highest_role = user.role
if highest_role.get_permission("can_edit_site_settings")
can [:site_settings, :room_configuration, :update_settings,
:update_room_configuration, :coloring, :registration_method], :admin
:update_room_configuration, :coloring, :registration_method, :log_level], :admin
end
if highest_role.get_permission("can_edit_roles")

@ -23,11 +23,15 @@ class Room < ApplicationRecord
before_create :setup
before_destroy :destroy_presentation
validates :name, presence: true
belongs_to :owner, class_name: 'User', foreign_key: :user_id
has_many :shared_access
has_one_attached :presentation
def self.admins_search(string)
active_database = Rails.configuration.database_configuration[Rails.env]["adapter"]
# Postgres requires created_at to be cast to a string
@ -51,6 +55,8 @@ class Room < ApplicationRecord
# Rely on manual ordering if trying to sort by status
return order_by_status(table, running_ids) if column == "status"
return table.order("COALESCE(rooms.last_session,rooms.created_at) DESC") if column == "created_at"
return table.order(Arel.sql("rooms.#{column} #{direction}")) if table.column_names.include?(column)
return table.order(Arel.sql("#{column} #{direction}")) if column == "users.name"
@ -86,15 +92,17 @@ class Room < ApplicationRecord
def self.order_by_status(table, ids)
return table if ids.blank?
order_string = "CASE bbb_id "
# Get active rooms first
active_rooms = table.where(bbb_id: ids)
ids.each_with_index do |id, index|
order_string += "WHEN '#{id}' THEN #{index} "
end
# Get other rooms sorted by last session date || created at date (whichever is higher)
inactive_rooms = table.where.not(bbb_id: ids).order("COALESCE(rooms.last_session,rooms.created_at) DESC")
order_string += "ELSE #{ids.length} END"
active_rooms + inactive_rooms
end
table.order(Arel.sql(order_string))
def recording_enabled?
JSON.parse(room_settings)["recording"]
end
private
@ -110,9 +118,9 @@ class Room < ApplicationRecord
# Generates a fully random room uid.
def random_room_uid
# 6 character long random string of chars from a..z and 0..9
full_chunk = SecureRandom.alphanumeric(6).downcase
full_chunk = SecureRandom.alphanumeric(9).downcase
[owner.name_chunk, full_chunk[0..2], full_chunk[3..5]].join("-")
[owner.name_chunk, full_chunk[0..2], full_chunk[3..5], full_chunk[6..8]].join("-")
end
# Generates a unique bbb_id based on uuid.
@ -122,4 +130,9 @@ class Room < ApplicationRecord
break bbb_id unless Room.exists?(bbb_id: bbb_id)
end
end
# Before destroying the room, make sure you also destroy the presentation attached
def destroy_presentation
presentation.purge if presentation.attached?
end
end

@ -58,10 +58,14 @@ class Setting < ApplicationRecord
Rails.configuration.registration_method_default
when "Room Authentication"
false
when "Require Recording Consent"
Rails.configuration.require_consent_default
when "Room Limit"
Rails.configuration.number_of_rooms_default
when "Shared Access"
Rails.configuration.shared_access_default
when "Preupload Presentation"
Rails.configuration.preupload_presentation_default
when "Room Configuration Mute On Join"
room_config_setting("mute-on-join")
when "Room Configuration Require Moderator"
@ -70,6 +74,8 @@ class Setting < ApplicationRecord
room_config_setting("anyone-can-start")
when "Room Configuration All Join Moderator"
room_config_setting("all-join-moderator")
when "Room Configuration Recording"
room_config_setting("recording")
end
end

@ -13,6 +13,6 @@
# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
%>
<button style="<%= "background-color: #{role_colour(role)};border-color: #{role_colour(role)}" %>" class="user-role btn btn-sm" onclick="filterRole('<%= role.name %>')">
<button style="<%= "background-color: #{role_colour(role)};border-color: #{role_colour(role)}" %>" class="user-role btn btn-sm" onclick="filterRole('<%= escape_javascript(role.name) %>')">
<%= translated_role_name(role) %>
</button>

@ -1,5 +1,5 @@
<%
# BigBlueButton open source conferencing system - http://www.bigbluespan.org/.
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
# This program is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free Software
@ -14,20 +14,25 @@
%>
<div class="form-group mt-n3">
<div class="row">
<div class="col-12 tags">
<span id="active" class="btn btn-sm tag manage-users-tab <%= 'selected' if @tab == 'active' %>">
<nav class="row m-0">
<div class="nav col nav-tabs admin-tabs m-0 p-0" id="manage-users-nav" role="tablist">
<a class="nav-item p-3 nav-link <%= 'active' if @tab == 'active' %>" id="Active" href="?tab=active" role="tab" aria-selected="true"><i class="fas mr-3 fa-users"></i>
<%= t("roles.active") %>
</span>
<span id="pending" class="btn btn-sm tag manage-users-tab <%= 'selected' if @tab == 'pending' %>">
</a>
<a class="nav-item p-3 nav-link <%= 'active' if @tab == 'pending' %>" id="Pending" href="?tab=pending" role="tab" aria-selected="false"><i class="fas mr-3 fa-user-clock"></i>
<%= t("roles.pending") %>
</span>
<span id="denied" class="btn btn-sm tag manage-users-tab <%= 'selected' if @tab == 'denied' %>">
</a>
<a class="nav-item p-3 nav-link <%= 'active' if @tab == 'denied' %>" id="Denied" href="?tab=denied" role="tab" aria-selected="false"><i class="fas mr-3 fa-user-times"></i>
<%= t("roles.banned") %>
</span>
<span id="deleted" class="btn btn-sm tag manage-users-tab <%= 'selected' if @tab == 'deleted' %>">
</a>
<a class="nav-item p-3 nav-link <%= 'active' if @tab == 'deleted' %>" id="Deleted" href="?tab=deleted" role="tab" aria-selected="false"><i class="far mr-2 fa-trash-alt"></i>
<%= t("roles.deleted") %>
</span>
</a>
</div>
</div>
</div>
<% if admin_invite_registration %>
<%= link_to "#inviteModal", class: "btn btn-primary pt-3", id: "invite-user", "data-toggle": "modal" do %>
<%= t("administrator.users.invite") %><i class="fas fa-paper-plane ml-3"></i>
<% end %>
<% end %>
</nav>
</div>

@ -98,4 +98,31 @@
</div>
</div>
</div>
</div>
<% if recording_consent_required? %>
<div class="mb-6 row">
<div class="col-12">
<div class="form-group">
<label class="form-label"><%= t("modal.room_settings.recording") %></label>
<label class="form-label text-muted"><%= t("administrator.room_configuration.recordings.info") %></label>
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<%= room_configuration_string("Room Configuration Recording") %>
</button>
<div class="dropdown-menu">
<%= button_to admin_update_room_configuration_path(setting: "Room Configuration Recording", value: "enabled"), class: "dropdown-item", "data-disable": "" do %>
<%= t("administrator.room_configuration.options.enabled") %>
<% end %>
<%= button_to admin_update_room_configuration_path(setting: "Room Configuration Recording", value: "optional"), class: "dropdown-item", "data-disable": "" do %>
<%= t("administrator.room_configuration.options.optional") %>
<% end %>
<%= button_to admin_update_room_configuration_path(setting: "Room Configuration Recording", value: "disabled"), class: "dropdown-item", "data-disable": "" do %>
<%= t("administrator.room_configuration.options.disabled") %>
<% end %>
</div>
</div>
</div>
</div>
</div>
<% end %>
</div>

@ -65,6 +65,11 @@
<a href="" data-toggle="modal" data-target="#createRoomModal" class="update-room dropdown-item" data-settings-path="<%= room_settings_path(room) %>">
<i class="dropdown-icon fas fa-cog"></i> <%= t("room.settings") %>
</a>
<% if preupload_allowed? %>
<a href="" data-toggle="modal" data-target="#preuploadPresentationModal" class="preupload-room dropdown-item" data-path="<%= preupload_presentation_path(room) %>" data-settings-path="<%= current_presentation_path(room) %>" data-remove="<%= remove_presentation_path(room) %>">
<i class="dropdown-icon fas fa-file-upload"></i> <%= t("room.add_presentation") %>
</a>
<% end %>
<% if shared_access_allowed %>
<a href="" data-toggle="modal" data-target="#shareRoomModal" class="share-room dropdown-item" data-path="<%= room_shared_access_path(room) %>" data-users-path="<%= room_shared_users_path(room) %>">
<i class="dropdown-icon fas fa-users"></i> <%= t("room.share") %>

@ -13,247 +13,30 @@
# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
%>
<div class="form-group">
<div class="row">
<div class="col-12">
<div class="mb-6 form-group">
<label class="form-label"><%= t("administrator.site_settings.branding.title") %></label>
<label class="form-label text-muted"><%= t("administrator.site_settings.branding.info") %></label>
<div class="input-group">
<input id="branding-url" type="text" class="form-control" value="<%= logo_image %>">
<span class="input-group-append">
<button id="branding-image" onclick="changeBrandingImage('<%= admin_update_settings_path(setting: 'Branding Image') %>')" class="btn btn-primary" type="button"><%= t("administrator.site_settings.branding.change") %></button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="mb-6 form-group">
<label class="form-label"><%= t("administrator.site_settings.legal.title") %></label>
<label class="form-label text-muted"><%= t("administrator.site_settings.legal.info") %></label>
<div class="input-group">
<input id="legal-url" type="text" class="form-control" value="<%= legal_url %>">
<span class="input-group-append">
<button id="legal-url" onclick="changeLegalURL('<%= admin_update_settings_path(setting: 'Legal URL') %>')" class="btn btn-primary" type="button"><%= t("administrator.site_settings.legal.change") %></button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="mb-6 form-group">
<label class="form-label"><%= t("administrator.site_settings.privpolicy.title") %></label>
<label class="form-label text-muted"><%= t("administrator.site_settings.privpolicy.info") %></label>
<div class="input-group">
<input id="privpolicy-url" type="text" class="form-control" value="<%= privpolicy_url %>">
<span class="input-group-append">
<button id="privpolicy-url" onclick="changePrivacyPolicyURL('<%= admin_update_settings_path(setting: 'Privacy Policy URL') %>')" class="btn btn-primary" type="button"><%= t("administrator.site_settings.privpolicy.change") %></button>
</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="mb-6 form-group">
<label class="form-label"><%= t("administrator.site_settings.color.title") %></label>
<label class="form-label text-muted"><%= t("administrator.site_settings.color.info") %></label>
<div class="color-inputs">
<input id="coloring-path-regular" type="hidden" value="<%= admin_coloring_path %>">
<input id="coloring-path-lighten" type="hidden" value="<%= admin_update_settings_path(setting: "Primary Color Lighten") %>">
<input id="coloring-path-darken" type="hidden" value="<%= admin_update_settings_path(setting: "Primary Color Darken") %>">
<div id="colorinput-regular" class="btn primary-regular mr-3">
<%= t("administrator.site_settings.color.regular") %>
</div>
<div class="form-group mt-n3">
<nav class="row m-0">
<div class="nav col nav-tabs admin-tabs m-0 p-0" >
<a class="nav-item p-3 nav-link <%= 'active' if @tab == 'appearance' %>" href="?tab=appearance" role="tab" aria-selected="true">
<i class="fas mr-3 fa-palette"></i>
<%= t("administrator.site_settings.tabs.appearance") %>
</a>
<a class="nav-item p-3 nav-link <%= 'active' if @tab == 'administration' %>" href="?tab=administration" role="tab" aria-selected="false">
<i class="fas mr-3 fa-toolbox"></i>
<%= t("administrator.site_settings.tabs.administration") %>
</a>
<a class="nav-item p-3 nav-link <%= 'active' if @tab == 'settings' %>" href="?tab=settings" role="tab" aria-selected="false">
<i class="fas mr-3 fa-tools"></i>
<%= t("administrator.site_settings.tabs.settings") %>
</a>
</div>
</nav>
</div>
<div id="colorinput-lighten" class="btn primary-lighten mr-3">
<%= t("administrator.site_settings.color.lighten") %>
</div>
<div id="colorinput-darken" class="btn primary-darken">
<%= t("administrator.site_settings.color.darken") %>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="mb-6 col-12">
<div class="form-group">
<label class="form-label"><%= t("administrator.site_settings.registration.title") %></label>
<label class="form-label text-muted"><%= t("administrator.site_settings.registration.info") %></label>
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="registrationMethods" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<%= registration_method_string %>
</button>
<div class="dropdown-menu" aria-labelledby="registrationMethods">
<%= button_to admin_change_registration_path(value: "open"), class: "dropdown-item", "data-disable": "" do %>
<%= t("administrator.site_settings.registration.methods.open") %>
<% end %>
<%= button_to admin_change_registration_path(value: "invite"), class: "dropdown-item", "data-disable": "" do %>
<%= t("administrator.site_settings.registration.methods.invite") %>
<% end %>