summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-31 15:55:43 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-31 15:55:43 +0200
commit464dd4266bdc433805010b5dca428f4cb75c2a81 (patch)
tree47fdf4f065960d49da015eafdf56cb817dcf9ea4
parent5f17f421b176d48ef556fb379f59bbb7d284b48e (diff)
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.
-rw-r--r--app/controllers/users_controller.rb27
-rw-r--r--app/helpers/users_helper.rb9
-rw-r--r--app/models/user.rb51
-rw-r--r--app/views/users/_user.html.erb13
-rw-r--r--app/views/users/edit.html.erb14
-rw-r--r--app/views/users/index.html.erb55
-rw-r--r--app/views/users/new.html.erb14
-rw-r--r--app/views/users/show.html.erb6
-rw-r--r--config/locales/de.yml29
-rw-r--r--config/locales/en.yml29
-rw-r--r--db/migrate/20260731124136_add_roles_to_users.rb13
-rw-r--r--lib/authenticated_system.rb7
-rw-r--r--lib/tasks/development_init.rake2
-rw-r--r--public/stylesheets/admin.css19
-rw-r--r--test/controllers/users_controller_test.rb27
-rw-r--r--test/fixtures/users.yml3
16 files changed, 243 insertions, 75 deletions
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
9 9
10 layout 'admin' 10 layout 'admin'
11 11
12 ROLE_PRESETS = {
13 "editor" => [],
14 "redaktion" => ["redaktion"],
15 "admin" => ["admin", "redaktion"]
16 }.freeze
17
18 GROUP_ORDER = [:admin, :redaktion, :editor, :alumni].freeze
19
12 def index 20 def index
13 @users = User.order("login ASC").all.group_by do |user| 21 @users = User.order("login ASC").all.group_by(&:role_group)
14 user.admin? ? :admin : :user
15 end
16 end 22 end
17 23
18 def new 24 def new
19 @user = User.new(admin: params[:admin].present?) 25 @user = User.new(:roles => ROLE_PRESETS.fetch(params[:preset], []))
20 end 26 end
21 27
22 def create 28 def create
@@ -35,8 +41,7 @@ class UsersController < ApplicationController
35 41
36 def update 42 def update
37 permitted = user_params 43 permitted = user_params
38 permitted.delete(:admin) unless current_user.is_admin? 44
39
40 if @user.update(permitted) 45 if @user.update(permitted)
41 flash[:notice] = t("flash.users.updated", :login => @user.login) 46 flash[:notice] = t("flash.users.updated", :login => @user.login)
42 redirect_to user_path(@user) 47 redirect_to user_path(@user)
@@ -63,9 +68,13 @@ class UsersController < ApplicationController
63 private 68 private
64 69
65 def user_params 70 def user_params
66 allowed = [:login, :email, :password, :password_confirmation] 71 permitted = params.fetch(:user, {})
67 allowed << :admin if current_user.admin? 72 .permit(:login, :email, :password, :password_confirmation,
68 params.fetch(:user, {}).permit(allowed) 73 :roles => [])
74 # Checkbox arrays post a leading blank from the hidden field.
75 permitted[:roles] = Array(permitted[:roles]).reject(&:blank?) if permitted.key?(:roles)
76 permitted.delete(:roles) unless current_user.is_admin?
77 permitted
69 end 78 end
70 79
71 def find_user 80 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 @@
1module UsersHelper
2 def user_list_by_admin_status
3 if current_user && current_user.admin
4 render :partial => 'admin_user_item', :collection => @users
5 else
6 render :partial => 'user_item', :collection => @users
7 end
8 end
9end
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
9 include Authentication 9 include Authentication
10 include Authentication::ByPassword 10 include Authentication::ByPassword
11 11
12 ROLES = %w[redaktion admin alumni].freeze
13
12 # Validations 14 # Validations
13 validates_presence_of :login 15 validates_presence_of :login
14 validates_length_of :login, :within => 1..40 16 validates_length_of :login, :within => 1..40
@@ -22,6 +24,8 @@ class User < ApplicationRecord
22 validates_format_of :email, :with => Authentication.email_regex, 24 validates_format_of :email, :with => Authentication.email_regex,
23 :message => Authentication.bad_email_message 25 :message => Authentication.bad_email_message
24 26
27 validate :roles_are_known
28
25 # Authenticates a user by their login name and unencrypted password. Returns the user or nil. 29 # Authenticates a user by their login name and unencrypted password. Returns the user or nil.
26 def self.authenticate(login, password) 30 def self.authenticate(login, password)
27 return if login.blank? || password.blank? 31 return if login.blank? || password.blank?
@@ -60,7 +64,45 @@ class User < ApplicationRecord
60 end 64 end
61 65
62 def is_admin? 66 def is_admin?
63 !!admin 67 roles.include?("admin")
68 end
69
70 # Compatibility shims for the users form, which posts user[admin] as a
71 # checkbox. Goes away when that form learns about roles.
72 def admin
73 is_admin?
74 end
75
76 def admin?
77 is_admin?
78 end
79
80 def admin=(value)
81 if ActiveModel::Type::Boolean.new.cast(value)
82 self.roles = (roles | ["admin"])
83 else
84 self.roles = (roles - ["admin"])
85 end
86 end
87
88 def redaktion?
89 roles.include?("redaktion")
90 end
91
92 def alumni?
93 roles.include?("alumni")
94 end
95
96 def role_group
97 return :alumni if alumni?
98 return :admin if is_admin?
99 return :redaktion if redaktion?
100 :editor
101 end
102
103 # Human-readable role names, for the list and the forms.
104 def role_labels
105 roles.map { |r| I18n.t("users.roles.#{r}", :default => r) }
64 end 106 end
65 107
66 # otp_secret present == enrolled. otp_pending_secret holds the secret 108 # otp_secret present == enrolled. otp_pending_secret holds the secret
@@ -133,4 +175,11 @@ class User < ApplicationRecord
133 end 175 end
134 true 176 true
135 end 177 end
178
179 private
180
181 def roles_are_known
182 unknown = roles.to_a - ROLES
183 errors.add(:roles, :unknown, :list => unknown.join(", ")) if unknown.any?
184 end
136end 185end
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 @@
1<% users.each do |user| %> 1<% users.each do |user| %>
2<tr> 2<tr>
3 <td class="user_login"><%= user.login %></td> 3 <td class="user_login"><%= user.login %></td>
4 <td><%= link_to "show", user_path(user) %></td> 4 <td class="user_roles">
5 <% if current_user.admin? || current_user == user %> 5 <% if user.roles.any? %>
6 <td> 6 <%= user.role_labels.join(", ") %>
7 <%= link_to "edit", edit_user_path(user) %> 7 <% else %>
8 <span class="field_hint"><%= t(".no_roles") %></span>
9 <% end %>
8 </td> 10 </td>
11 <td><%= link_to t("admin.common.show"), user_path(user) %></td>
12 <% if current_user.admin? || current_user == user %>
13 <td><%= link_to t("admin.common.edit"), edit_user_path(user) %></td>
9 <td> 14 <td>
10 <%= button_to user_path(user), method: :delete, 15 <%= button_to user_path(user), method: :delete,
11 form: { data: { confirm: t(".confirm_destroy", :login => user.login) }, class: 'button_to destructive' } do %> 16 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 @@
21 <div class="layout_row_content"><%= f.password_field :password_confirmation, :autocomplete => "new-password" %></div> 21 <div class="layout_row_content"><%= f.password_field :password_confirmation, :autocomplete => "new-password" %></div>
22 22
23 <% if current_user.admin? %> 23 <% if current_user.admin? %>
24 <div class="layout_row_label"><%= t("users.labels.admin") %></div> 24 <div class="layout_row_label"><%= t("users.labels.roles") %></div>
25 <div class="layout_row_content"><%= f.check_box :admin %></div> 25 <div class="layout_row_content">
26 <% User::ROLES.each do |role| %>
27 <label class="role_choice">
28 <%= check_box_tag "user[roles][]", role, @user.roles.include?(role),
29 :id => "user_roles_#{role}" %>
30 <%= t("users.roles.#{role}") %>
31 <span class="field_hint"><%= t("users.role_hints.#{role}") %></span>
32 </label>
33 <% end %>
34 <%= hidden_field_tag "user[roles][]", "" %>
35 </div>
26 <% end %> 36 <% end %>
27 37
28 <div class="layout_row_label"></div> 38 <div class="layout_row_label"></div>
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 @@
1<h1><%= t(".admins") %></h1> 1<h1><%= t(".title") %></h1>
2<%= link_to new_user_path(admin: true), class: 'action_button' do %> 2
3 <%= icon("plus", library: "tabler", "aria-hidden": true) %> <%= t(".create_admin") %> 3<p class="button_row">
4<% end %> 4 <% UsersController::ROLE_PRESETS.each_key do |preset| %>
5<table class="user_table"> 5 <%= link_to new_user_path(:preset => preset), class: 'action_button' do %>
6 <tr class="header"> 6 <%= icon("plus", library: "tabler", "aria-hidden": true) %>
7 <th><%= t("users.labels.login") %></th> 7 <%= t(".create_#{preset}") %>
8 <th></th> 8 <% end %>
9 <th></th> 9 <% end %>
10 <th></th> 10</p>
11 </tr>
12 <%= render :partial => "user", :locals => {:users => @users[:admin] ||= []} %>
13</table>
14 11
15<h1><%= t(".users") %></h1> 12<% UsersController::GROUP_ORDER.each do |group| %>
16<%= link_to new_user_path, class: 'action_button' do %> 13 <% members = @users[group] || [] %>
17 <%= icon("plus", library: "tabler", "aria-hidden": true) %> <%= t(".create_user") %> 14 <h2 class="user_group_heading <%= "user_group_alumni" if group == :alumni %>">
15 <%= t(".group_#{group}") %>
16 <span class="dashboard_widget_meta"><%= members.size %></span>
17 </h2>
18 <% if members.any? %>
19 <table class="user_table <%= "user_table_alumni" if group == :alumni %>">
20 <tr class="header">
21 <th><%= t("users.labels.login") %></th>
22 <th><%= t("users.labels.roles") %></th>
23 <th></th>
24 <th></th>
25 <th></th>
26 </tr>
27 <%= render :partial => "user", :locals => { :users => members } %>
28 </table>
29 <% else %>
30 <p class="field_hint"><%= t(".group_empty") %></p>
31 <% end %>
18<% end %> 32<% end %>
19<table class="user_table">
20 <tr class="header">
21 <th><%= t("users.labels.login") %></th>
22 <th></th>
23 <th></th>
24 <th></th>
25 </tr>
26 <%= render :partial => "user", :locals => {:users => @users[:user] ||= []} %>
27</table>
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 @@
21 <div class="layout_row_label"><%= t("users.labels.confirm") %></div> 21 <div class="layout_row_label"><%= t("users.labels.confirm") %></div>
22 <div class="layout_row_content"><%= f.password_field :password_confirmation %></div> 22 <div class="layout_row_content"><%= f.password_field :password_confirmation %></div>
23 23
24 <div class="layout_row_label"><%= t("users.labels.admin") %></div> 24 <div class="layout_row_label"><%= t("users.labels.roles") %></div>
25 <div class="layout_row_content"><%= f.check_box :admin %></div> 25 <div class="layout_row_content">
26 <% User::ROLES.each do |role| %>
27 <label class="role_choice">
28 <%= check_box_tag "user[roles][]", role, @user.roles.include?(role),
29 :id => "user_roles_#{role}" %>
30 <%= t("users.roles.#{role}") %>
31 <span class="field_hint"><%= t("users.role_hints.#{role}") %></span>
32 </label>
33 <% end %>
34 <%= hidden_field_tag "user[roles][]", "" %>
35 </div>
26 36
27 <div class="layout_row_label"></div> 37 <div class="layout_row_label"></div>
28 <div class="layout_row_content"><%= f.submit t("admin.common.create") %></div> 38 <div class="layout_row_content"><%= f.submit t("admin.common.create") %></div>
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 @@
19 <div class="layout_row_label"><%= t("users.labels.email") %></div> 19 <div class="layout_row_label"><%= t("users.labels.email") %></div>
20 <div class="layout_row_content"><%= @user.email %></div> 20 <div class="layout_row_content"><%= @user.email %></div>
21 21
22 <div class="layout_row_label"><%= t("users.labels.admin") %></div> 22 <div class="layout_row_label"><%= t("users.labels.roles") %></div>
23 <div class="layout_row_content"><%= @user.admin ? t("admin.common.yes") : t("admin.common.no") %></div> 23 <div class="layout_row_content">
24 <%= @user.roles.any? ? @user.role_labels.join(", ") : t("users.no_roles") %>
25 </div>
24 </div> 26 </div>
25</div> 27</div>
diff --git a/config/locales/de.yml b/config/locales/de.yml
index cf93e05b..e15a08d2 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -145,6 +145,10 @@ de:
145 restore_target_invalid: "Das Wiederherstellungsziel muss ein lebender Node sein" 145 restore_target_invalid: "Das Wiederherstellungsziel muss ein lebender Node sein"
146 destroy_outside_trash: "Nodes können nur aus dem Papierkorb gelöscht werden" 146 destroy_outside_trash: "Nodes können nur aus dem Papierkorb gelöscht werden"
147 attach_in_trash: "An einen Node im Papierkorb können keine Assets angehängt werden" 147 attach_in_trash: "An einen Node im Papierkorb können keine Assets angehängt werden"
148 user:
149 attributes:
150 roles:
151 unknown: "enthält unbekannte Rollen: %{list}"
148 related_asset: 152 related_asset:
149 attributes: 153 attributes:
150 headline: 154 headline:
@@ -276,11 +280,28 @@ de:
276 admin: "Admin?" 280 admin: "Admin?"
277 user: 281 user:
278 confirm_destroy: "Benutzer %{login} wirklich löschen?" 282 confirm_destroy: "Benutzer %{login} wirklich löschen?"
283 no_roles: "—"
279 index: 284 index:
280 admins: "Admins" 285 title: "Benutzerkonten"
281 create_admin: "Admin-Benutzer anlegen" 286 create_editor: "Editor-Konto anlegen"
282 users: "Benutzer" 287 create_redaktion: "Redaktions-Konto anlegen"
283 create_user: "Benutzer anlegen" 288 create_admin: "Admin-Konto anlegen"
289 group_admin: "Administration"
290 group_redaktion: "Redaktion"
291 group_editor: "Editors"
292 group_alumni: "Ehemalige"
293 group_empty: "— keine —"
294 labels:
295 roles: "Rollen"
296 roles:
297 admin: "Administration"
298 redaktion: "Redaktion"
299 alumni: "Ehemalig"
300 role_hints:
301 admin: "Benutzerverwaltung und Navigation, jeweils nach erneuter Bestätigung."
302 redaktion: "Darf in den geschützten Bereichen veröffentlichen."
303 alumni: "Kein Login mehr möglich; Zuschreibungen bleiben erhalten."
304 no_roles: "keine besonderen Rollen"
284 new: 305 new:
285 title: "Neuen Benutzer anlegen" 306 title: "Neuen Benutzer anlegen"
286 307
diff --git a/config/locales/en.yml b/config/locales/en.yml
index c3f515b5..f8b94a00 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -92,6 +92,10 @@ en:
92 restore_target_invalid: "Restore target must be a living node" 92 restore_target_invalid: "Restore target must be a living node"
93 destroy_outside_trash: "Nodes are only destroyed from the Trash" 93 destroy_outside_trash: "Nodes are only destroyed from the Trash"
94 attach_in_trash: "Cannot attach assets to a node in the Trash" 94 attach_in_trash: "Cannot attach assets to a node in the Trash"
95 user:
96 attributes:
97 roles:
98 unknown: "contains unknown roles: %{list}"
95 related_asset: 99 related_asset:
96 attributes: 100 attributes:
97 headline: 101 headline:
@@ -224,11 +228,28 @@ en:
224 admin: "admin?" 228 admin: "admin?"
225 user: 229 user:
226 confirm_destroy: "Do you really want to destroy user %{login}?" 230 confirm_destroy: "Do you really want to destroy user %{login}?"
231 no_roles: "—"
227 index: 232 index:
228 admins: "Admins" 233 title: "User accounts"
229 create_admin: "Create admin user" 234 create_editor: "Create editor account"
230 users: "Users" 235 create_redaktion: "Create Redaktion account"
231 create_user: "Create user" 236 create_admin: "Create admin account"
237 group_admin: "Administration"
238 group_redaktion: "Redaktion"
239 group_editor: "Editors"
240 group_alumni: "Alumni"
241 group_empty: "— none —"
242 labels:
243 roles: "Roles"
244 roles:
245 admin: "Administration"
246 redaktion: "Redaktion"
247 alumni: "Alumnus"
248 role_hints:
249 admin: "User management and navigation, each behind a fresh confirmation."
250 redaktion: "May publish in the protected sections."
251 alumni: "Can no longer log in; attributions are preserved."
252 no_roles: "no special roles"
232 new: 253 new:
233 title: "Create new user" 254 title: "Create new user"
234 255
diff --git a/db/migrate/20260731124136_add_roles_to_users.rb b/db/migrate/20260731124136_add_roles_to_users.rb
new file mode 100644
index 00000000..ea003e60
--- /dev/null
+++ b/db/migrate/20260731124136_add_roles_to_users.rb
@@ -0,0 +1,13 @@
1class AddRolesToUsers < ActiveRecord::Migration[8.1]
2 def up
3 add_column :users, :roles, :string, :array => true, :default => [], :null => false
4 execute "UPDATE users SET roles = ARRAY['admin','redaktion'] WHERE admin = true"
5 remove_column :users, :admin
6 end
7
8 def down
9 add_column :users, :admin, :boolean
10 execute "UPDATE users SET admin = true WHERE 'admin' = ANY(roles)"
11 remove_column :users, :roles
12 end
13end
diff --git a/lib/authenticated_system.rb b/lib/authenticated_system.rb
index 2ec15a77..4e70c28d 100644
--- a/lib/authenticated_system.rb
+++ b/lib/authenticated_system.rb
@@ -102,7 +102,12 @@ module AuthenticatedSystem
102 def login_from_session 102 def login_from_session
103 return unless session[:user_id] 103 return unless session[:user_id]
104 if session[:logged_in_at].to_i > SESSION_MAX_AGE.ago.to_i 104 if session[:logged_in_at].to_i > SESSION_MAX_AGE.ago.to_i
105 self.current_user = User.find_by(:id => session[:user_id]) 105 user = User.find_by(:id => session[:user_id])
106 if user.nil? || user.alumni?
107 session[:user_id] = nil
108 else
109 self.current_user = user
110 end
106 else 111 else
107 session[:user_id] = nil 112 session[:user_id] = nil
108 end 113 end
diff --git a/lib/tasks/development_init.rake b/lib/tasks/development_init.rake
index 94f323c3..91797d9b 100644
--- a/lib/tasks/development_init.rake
+++ b/lib/tasks/development_init.rake
@@ -19,7 +19,7 @@ namespace :cccms do
19 :email => 'admin@cccms.de', 19 :email => 'admin@cccms.de',
20 :password => 'foobar', 20 :password => 'foobar',
21 :password_confirmation => 'foobar', 21 :password_confirmation => 'foobar',
22 :admin => true 22 :roles => ["admin", "redaktion"]
23 ) 23 )
24 end 24 end
25 25
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index c0496c6e..9991fad0 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -796,6 +796,25 @@ table.revisions_table tr:hover {
796 margin: 0; 796 margin: 0;
797} 797}
798 798
799.user_group_heading {
800 margin-top: 1.5rem;
801}
802
803/* Retired accounts: present for attribution, not for action. */
804.user_group_alumni,
805.user_table_alumni {
806 color: var(--text-muted);
807}
808
809.role_choice {
810 display: block;
811 margin-bottom: 0.35rem;
812}
813
814.role_choice .field_hint {
815 margin-left: 0.5rem;
816}
817
799/* ============================================================ 818/* ============================================================
800 Translation compare view (page_translations#show) 819 Translation compare view (page_translations#show)
801 ============================================================ */ 820 ============================================================ */
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb
index 67f7c053..1c5d16fc 100644
--- a/test/controllers/users_controller_test.rb
+++ b/test/controllers/users_controller_test.rb
@@ -9,12 +9,12 @@ class UsersControllerTest < ActionController::TestCase
9 assert_select "a", { :count => 0, :text => "Destroy" } 9 assert_select "a", { :count => 0, :text => "Destroy" }
10 end 10 end
11 11
12 test "get index as admin user renders admin partial" do 12 test "get index as admin shows every group with per-row actions" do
13 login_as :aaron 13 login_as :aaron
14 get :index 14 get :index
15 assert_response :success 15 assert_response :success
16 assert_select "button[type=submit]", I18n.t("admin.common.destroy") 16 assert_select "button[type=submit]", I18n.t("admin.common.destroy")
17 assert_select "a", "show" 17 assert_select "a", I18n.t("admin.common.show")
18 end 18 end
19 19
20 test "get new when logged in as admin" do 20 test "get new when logged in as admin" do
@@ -60,7 +60,7 @@ class UsersControllerTest < ActionController::TestCase
60 :email => "foo@bar.com", 60 :email => "foo@bar.com",
61 :password => "xxxzzz", 61 :password => "xxxzzz",
62 :password_confirmation => "xxxzzz", 62 :password_confirmation => "xxxzzz",
63 :admin => true 63 :roles => ["admin", "redaktion"]
64 } 64 }
65 } 65 }
66 end 66 end
@@ -174,19 +174,17 @@ class UsersControllerTest < ActionController::TestCase
174 test "admin user can promote regular users to admins" do 174 test "admin user can promote regular users to admins" do
175 login_as :aaron 175 login_as :aaron
176 user = users(:quentin) 176 user = users(:quentin)
177 put :update, params: { :id => user.id, :user => {:admin => true} } 177 put :update, params: { :id => user.id, :user => {:roles => ["admin", "redaktion"]} }
178 178
179 user.reload 179 assert_equal true, user.reload.is_admin?
180 assert_equal true, user.is_admin?
181 end 180 end
182 181
183 test "regular users cannot promote themselves to admins" do 182 test "regular users cannot promote themselves to admins" do
184 login_as :quentin 183 login_as :quentin
185 user = users(:quentin) 184 user = users(:quentin)
186 put :update, params: { :id => user.id, :user => {:admin => true} } 185 put :update, params: { :id => user.id, :user => {:roles => ["admin", "redaktion"]} }
187 186
188 user.reload 187 assert_equal false, user.reload.is_admin?
189 assert_equal false, user.is_admin?
190 end 188 end
191 189
192 test "reset_otp is admin-only and witnessed" do 190 test "reset_otp is admin-only and witnessed" do
@@ -202,4 +200,15 @@ class UsersControllerTest < ActionController::TestCase
202 assert_not user.reload.otp_enrolled? 200 assert_not user.reload.otp_enrolled?
203 assert_equal "otp_reset", NodeAction.last.action 201 assert_equal "otp_reset", NodeAction.last.action
204 end 202 end
203
204 test "index groups a retired admin under alumni, not administration" do
205 login_as :aaron
206 user = users(:quentin)
207 user.update_column(:roles, ["admin", "alumni"])
208
209 get :index
210
211 assert_response :success
212 assert_select "h2", :text => /#{I18n.t("users.index.group_alumni")}/
213 end
205end 214end
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml
index 7276bcb4..f8d32d3c 100644
--- a/test/fixtures/users.yml
+++ b/test/fixtures/users.yml
@@ -13,5 +13,4 @@ aaron:
13 salt: 5be6f9cdd04fd7ab3c91cd32a5334ba2339b8005 13 salt: 5be6f9cdd04fd7ab3c91cd32a5334ba2339b8005
14 crypted_password: 740a48caf7dd5ff11318d812d57c0a0928cfbc12 # 'monkey' 14 crypted_password: 740a48caf7dd5ff11318d812d57c0a0928cfbc12 # 'monkey'
15 created_at: 2024-01-02 00:00:00 15 created_at: 2024-01-02 00:00:00
16 admin: true 16 roles: ["admin", "redaktion"]
17