From 464dd4266bdc433805010b5dca428f4cb75c2a81 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 31 Jul 2026 15:55:43 +0200 Subject: Group user accounts by role Replaces the two-way admin/user split with four groups ordered by capability: administration, Redaktion, editors, alumni. alumni takes precedence over capability in role_group, so a retired admin appears at the bottom rather than the top. Forms now offer the three roles as checkboxes rather than a single admin checkbox, with a trailing hidden blank so an empty set can be posted, and user_params permits roles only for admins. Three create buttons prefill the common combinations. --- app/controllers/users_controller.rb | 27 ++++++++++++------ app/helpers/users_helper.rb | 9 ------ app/models/user.rb | 51 +++++++++++++++++++++++++++++++++- app/views/users/_user.html.erb | 13 ++++++--- app/views/users/edit.html.erb | 14 ++++++++-- app/views/users/index.html.erb | 55 ++++++++++++++++++++----------------- app/views/users/new.html.erb | 14 ++++++++-- app/views/users/show.html.erb | 6 ++-- 8 files changed, 135 insertions(+), 54 deletions(-) delete mode 100644 app/helpers/users_helper.rb (limited to 'app') diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index cb71db23..95dff220 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -9,14 +9,20 @@ class UsersController < ApplicationController layout 'admin' + ROLE_PRESETS = { + "editor" => [], + "redaktion" => ["redaktion"], + "admin" => ["admin", "redaktion"] + }.freeze + + GROUP_ORDER = [:admin, :redaktion, :editor, :alumni].freeze + def index - @users = User.order("login ASC").all.group_by do |user| - user.admin? ? :admin : :user - end + @users = User.order("login ASC").all.group_by(&:role_group) end def new - @user = User.new(admin: params[:admin].present?) + @user = User.new(:roles => ROLE_PRESETS.fetch(params[:preset], [])) end def create @@ -35,8 +41,7 @@ class UsersController < ApplicationController def update permitted = user_params - permitted.delete(:admin) unless current_user.is_admin? - + if @user.update(permitted) flash[:notice] = t("flash.users.updated", :login => @user.login) redirect_to user_path(@user) @@ -63,9 +68,13 @@ class UsersController < ApplicationController private def user_params - allowed = [:login, :email, :password, :password_confirmation] - allowed << :admin if current_user.admin? - params.fetch(:user, {}).permit(allowed) + permitted = params.fetch(:user, {}) + .permit(:login, :email, :password, :password_confirmation, + :roles => []) + # Checkbox arrays post a leading blank from the hidden field. + permitted[:roles] = Array(permitted[:roles]).reject(&:blank?) if permitted.key?(:roles) + permitted.delete(:roles) unless current_user.is_admin? + permitted end def find_user diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb deleted file mode 100644 index ff031387..00000000 --- a/app/helpers/users_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -module UsersHelper - def user_list_by_admin_status - if current_user && current_user.admin - render :partial => 'admin_user_item', :collection => @users - else - render :partial => 'user_item', :collection => @users - end - end -end diff --git a/app/models/user.rb b/app/models/user.rb index e1eff059..2e9da86c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,8 @@ class User < ApplicationRecord include Authentication include Authentication::ByPassword + ROLES = %w[redaktion admin alumni].freeze + # Validations validates_presence_of :login validates_length_of :login, :within => 1..40 @@ -22,6 +24,8 @@ class User < ApplicationRecord validates_format_of :email, :with => Authentication.email_regex, :message => Authentication.bad_email_message + validate :roles_are_known + # Authenticates a user by their login name and unencrypted password. Returns the user or nil. def self.authenticate(login, password) return if login.blank? || password.blank? @@ -60,7 +64,45 @@ class User < ApplicationRecord end def is_admin? - !!admin + roles.include?("admin") + end + + # Compatibility shims for the users form, which posts user[admin] as a + # checkbox. Goes away when that form learns about roles. + def admin + is_admin? + end + + def admin? + is_admin? + end + + def admin=(value) + if ActiveModel::Type::Boolean.new.cast(value) + self.roles = (roles | ["admin"]) + else + self.roles = (roles - ["admin"]) + end + end + + def redaktion? + roles.include?("redaktion") + end + + def alumni? + roles.include?("alumni") + end + + def role_group + return :alumni if alumni? + return :admin if is_admin? + return :redaktion if redaktion? + :editor + end + + # Human-readable role names, for the list and the forms. + def role_labels + roles.map { |r| I18n.t("users.roles.#{r}", :default => r) } end # otp_secret present == enrolled. otp_pending_secret holds the secret @@ -133,4 +175,11 @@ class User < ApplicationRecord end true end + + private + + def roles_are_known + unknown = roles.to_a - ROLES + errors.add(:roles, :unknown, :list => unknown.join(", ")) if unknown.any? + end end diff --git a/app/views/users/_user.html.erb b/app/views/users/_user.html.erb index 9c6466a2..04884be8 100644 --- a/app/views/users/_user.html.erb +++ b/app/views/users/_user.html.erb @@ -1,11 +1,16 @@ <% users.each do |user| %> <%= user.login %> - <%= link_to "show", user_path(user) %> - <% if current_user.admin? || current_user == user %> - - <%= link_to "edit", edit_user_path(user) %> + + <% if user.roles.any? %> + <%= user.role_labels.join(", ") %> + <% else %> + <%= t(".no_roles") %> + <% end %> + <%= link_to t("admin.common.show"), user_path(user) %> + <% if current_user.admin? || current_user == user %> + <%= link_to t("admin.common.edit"), edit_user_path(user) %> <%= button_to user_path(user), method: :delete, form: { data: { confirm: t(".confirm_destroy", :login => user.login) }, class: 'button_to destructive' } do %> diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index 9df93815..ad2e0891 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -21,8 +21,18 @@
<%= f.password_field :password_confirmation, :autocomplete => "new-password" %>
<% if current_user.admin? %> -
<%= t("users.labels.admin") %>
-
<%= f.check_box :admin %>
+
<%= t("users.labels.roles") %>
+
+ <% User::ROLES.each do |role| %> + + <% end %> + <%= hidden_field_tag "user[roles][]", "" %> +
<% end %>
diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 0a003cb1..854811a2 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -1,27 +1,32 @@ -

<%= t(".admins") %>

-<%= link_to new_user_path(admin: true), class: 'action_button' do %> - <%= icon("plus", library: "tabler", "aria-hidden": true) %> <%= t(".create_admin") %> -<% end %> - - - - - - - - <%= render :partial => "user", :locals => {:users => @users[:admin] ||= []} %> -
<%= t("users.labels.login") %>
+

<%= t(".title") %>

+ +

+ <% UsersController::ROLE_PRESETS.each_key do |preset| %> + <%= link_to new_user_path(:preset => preset), class: 'action_button' do %> + <%= icon("plus", library: "tabler", "aria-hidden": true) %> + <%= t(".create_#{preset}") %> + <% end %> + <% end %> +

-

<%= t(".users") %>

-<%= link_to new_user_path, class: 'action_button' do %> - <%= icon("plus", library: "tabler", "aria-hidden": true) %> <%= t(".create_user") %> +<% UsersController::GROUP_ORDER.each do |group| %> + <% members = @users[group] || [] %> +

"> + <%= t(".group_#{group}") %> + <%= members.size %> +

+ <% if members.any? %> + "> + + + + + + + + <%= render :partial => "user", :locals => { :users => members } %> +
<%= t("users.labels.login") %><%= t("users.labels.roles") %>
+ <% else %> +

<%= t(".group_empty") %>

+ <% end %> <% end %> - - - - - - - - <%= render :partial => "user", :locals => {:users => @users[:user] ||= []} %> -
<%= t("users.labels.login") %>
diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb index 776e6e96..8d99fd92 100644 --- a/app/views/users/new.html.erb +++ b/app/views/users/new.html.erb @@ -21,8 +21,18 @@
<%= t("users.labels.confirm") %>
<%= f.password_field :password_confirmation %>
-
<%= t("users.labels.admin") %>
-
<%= f.check_box :admin %>
+
<%= t("users.labels.roles") %>
+
+ <% User::ROLES.each do |role| %> + + <% end %> + <%= hidden_field_tag "user[roles][]", "" %> +
<%= f.submit t("admin.common.create") %>
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 44976800..a320e53e 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -19,7 +19,9 @@
<%= t("users.labels.email") %>
<%= @user.email %>
-
<%= t("users.labels.admin") %>
-
<%= @user.admin ? t("admin.common.yes") : t("admin.common.no") %>
+
<%= t("users.labels.roles") %>
+
+ <%= @user.roles.any? ? @user.role_labels.join(", ") : t("users.no_roles") %> +
-- cgit v1.3