Skip to content

Commit 8dcd13d

Browse files
disk quota in my account and near quota report - squashed rebased #1292
1 parent 0e4b3bf commit 8dcd13d

File tree

11 files changed

+312
-11
lines changed

11 files changed

+312
-11
lines changed

BrainPortal/app/assets/stylesheets/cbrain.css.erb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2796,6 +2796,14 @@ img {
27962796
background-color: #fdd; /* light pink */
27972797
}
27982798

2799+
.quota_almost_exceeded {
2800+
background-color: #ffdfbf;
2801+
}
2802+
2803+
.disk_quota_user_select {
2804+
float: right;
2805+
}
2806+
27992807
/* % ######################################################### */
28002808
/* % Report Generator Styles */
28012809
/* % ######################################################### */

BrainPortal/app/controllers/quotas_controller.rb

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,16 @@ def index #:nodoc:
3737
@mode = :cpu if params[:mode].to_s == 'cpu'
3838
@mode = :disk if params[:mode].to_s == 'disk' || @mode != :cpu
3939
cbrain_session[:quota_mode] = @mode.to_s
40-
4140
@scope = scope_from_session("#{@mode}_quotas#index")
4241

42+
# Make sure the target user is set if viewing quotas for another user.
43+
@as_user = see_as_user params['as_user_id']
44+
@scope.custom['as_user_id'] = @as_user.id
45+
4346
@base_scope = base_scope.includes([:user, :data_provider ]) if @mode == :disk
4447
@base_scope = base_scope.includes([:user, :remote_resource]) if @mode == :cpu
45-
@view_scope = @scope.apply(@base_scope)
48+
49+
@view_scope = @scope.apply(@base_scope)
4650

4751
@scope.pagination ||= Scope::Pagination.from_hash({ :per_page => 15 })
4852
@quotas = @scope.pagination.apply(@view_scope, api_request?)
@@ -343,6 +347,73 @@ def report_disk_quotas #:nodoc:
343347

344348
end
345349

350+
def report_almost
351+
@mode = params[:mode].to_s == 'cpu' ? :cpu : :disk
352+
cb_exception("not supported") if @mode == :cpu
353+
report_disk_almost if @mode == :disk
354+
end
355+
356+
def report_disk_almost
357+
almost = 0.95 # share of resource use qualifying for 'almost exceeding'
358+
quota_to_user_ids = {} # quota_obj => [uid, uid...]
359+
360+
# Scan DP-wide quota objects
361+
DiskQuota.where(:user_id => 0).all.each do |quota|
362+
exceed_size_user_ids = Userfile
363+
.where(:data_provider_id => quota.data_provider_id)
364+
.group(:user_id)
365+
.sum(:size)
366+
.select { |user_id,size| size >= quota.max_bytes * almost }
367+
.keys
368+
exceed_numfiles_user_ids = Userfile
369+
.where(:data_provider_id => quota.data_provider_id)
370+
.group(:user_id)
371+
.sum(:num_files)
372+
.select { |user_id,num_files| num_files >= quota.max_files * almost }
373+
.keys
374+
375+
union_ids = exceed_size_user_ids | exceed_numfiles_user_ids
376+
union_ids -= DiskQuota
377+
.where(:data_provider_id => quota.data_provider_id, :user_id => union_ids)
378+
.pluck(:user_id) # remove user IDs that have their own quota records
379+
quota_to_user_ids[quota] = union_ids if union_ids.size > 0
380+
end
381+
382+
# Scan user-specific quota objects
383+
DiskQuota.where('user_id > 0').all.each do |quota|
384+
quota_to_user_ids[quota] = [ quota.user_id ] if quota.almost_exceeded?
385+
end
386+
387+
# Inverse relation: user_id => [ quota, quota ]
388+
user_id_to_quotas = {}
389+
quota_to_user_ids.each do |quota,user_ids|
390+
user_ids.each do |user_id|
391+
user_id_to_quotas[user_id] ||= []
392+
user_id_to_quotas[user_id] << quota
393+
end
394+
end
395+
396+
# Table content: [ [ user_id, quota ], [user_id, quota] ... ]
397+
# Note: the rows are grouped by user_id, but not sorted in any way...
398+
@user_id_and_quota = []
399+
user_id_to_quotas.each do |user_id, quotas|
400+
quotas.each do |quota|
401+
@user_id_and_quota << [ user_id, quota ]
402+
end
403+
end
404+
405+
end
406+
407+
# a clone of browse_as
408+
def see_as_user(as_user_id) #:nodoc:
409+
scope = scope_from_session("#{@mode}_quotas#index")
410+
users = current_user.available_users
411+
as_user = users.where(:id => as_user_id).first
412+
as_user ||= users.where(:id => scope.custom['as_user_id']).first
413+
as_user ||= current_user
414+
as_user
415+
end
416+
346417
private
347418

348419
def disk_quota_params #:nodoc:
@@ -366,12 +437,12 @@ def base_scope #:nodoc:
366437
scope = DiskQuota.where(nil) if @mode == :disk
367438
scope = CpuQuota.where(nil) if @mode == :cpu
368439

369-
return scope if current_user.has_role?(:admin_user)
440+
return scope if current_user.has_role?(:admin_user) && @as_user.id == current_user.id
370441

371442
if @mode == :disk
372-
dp_ids = DataProvider.all.select { |dp| dp.can_be_accessed_by?(current_user) }.map(&:id)
443+
dp_ids = DataProvider.all.select { |dp| dp.can_be_accessed_by?(@as_user) }.map(&:id)
373444
scope = scope.where(
374-
:user_id => [ 0, current_user.id ],
445+
:user_id => [ 0, @as_user.id ],
375446
:data_provider_id => dp_ids,
376447
)
377448
end

BrainPortal/app/controllers/users_controller.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ def show #:nodoc:
9696
# Hash of OIDC uris with the OIDC name as key
9797
@oidc_uris = generate_oidc_login_uri(@oidc_configs)
9898

99+
# few attributes for quotes table
100+
@scope = scope_from_session("mydiskquotes")
101+
dp_ids = DataProvider.all.select { |dp| dp.can_be_accessed_by?(current_user) }.map(&:id)
102+
@base_scope = DiskQuota.where(
103+
:data_provider_id => dp_ids,
104+
:user_id => [ 0, @user.id ],
105+
).includes([:user, :data_provider])
106+
107+
@view_scope = @scope.apply(@base_scope)
108+
109+
@scope.pagination ||= Scope::Pagination.from_hash({ :per_page => 10 })
110+
@quotas = @scope.pagination.apply(@view_scope)
111+
112+
99113
respond_to do |format|
100114
format.html # show.html.erb
101115
format.xml do

BrainPortal/app/models/disk_quota.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,29 @@ def self.exceeded!(user_id, data_provider_id)
101101
raise CbrainDiskQuotaExceeded.new(user_id, data_provider_id)
102102
end
103103

104+
105+
# Returns true if currently, the user specified by +user_id+
106+
# uses uses almost all disk space or more total files on +data_provider_id+ than
107+
# the quota limit configured by the admin. A share is considered almost all
108+
# if it exceeds fraction. Fraction should be a number greater than 0 and smaller
109+
# than 1
110+
#
111+
# The quota record for the limits is first looked up specifically for the pair
112+
# (user, data_provider); if no quota record is found, the pair (0, data_provider)
113+
# will be fetched instead (meaning a default quota for all users on that DP)
114+
#
115+
# Possible returned values:
116+
# nil : all is OK
117+
# :bytes : disk space is exceeded
118+
# :files : number of files is exceeded
119+
# :bytes_and_files : both are exceeded
120+
def self.almost_exceeded?(user_id, data_provider_id)
121+
quota = self.where(:user_id => user_id, :data_provider_id => data_provider_id).first
122+
quota ||= self.where(:user_id => 0 , :data_provider_id => data_provider_id).first
123+
return nil if quota.nil?
124+
quota.almost_exceeded?(user_id)
125+
end
126+
104127
# Returns true if currently, the user specified by +user+ (specified by id)
105128
# uses more disk space or more total files on than configured in the limits
106129
# of this quota object. Since a quota object can contain '0' for the user attribute
@@ -142,6 +165,38 @@ def exceeded!(user_id = self.user_id)
142165
raise CbrainDiskQuotaExceeded.new(user_id, self.data_provider_id)
143166
end
144167

168+
# same as exceeded but evaluates true also when almost all allowed disk space or file
169+
# quota are used
170+
def almost_exceeded?(user_id = self.user_id, fraction = 0.95)
171+
172+
return nil if user_id == 0 # just in case
173+
174+
@cursize, @curfiles = Rails.cache.fetch(
175+
"disk_usage-u=#{user_id}-dp=#{data_provider_id}",
176+
:expires_in => CACHED_USAGE_EXPIRATION
177+
) do
178+
req = Userfile
179+
.where(:user_id => user_id)
180+
.where(:data_provider_id => data_provider_id)
181+
[ req.sum(:size), req.sum(:num_files) ]
182+
end
183+
184+
what_is_exceeded = nil
185+
186+
# exceeded? method, as a side effect sets @cursize and @
187+
188+
if @cursize > self.max_bytes * fraction
189+
what_is_exceeded = :bytes
190+
end
191+
192+
if @curfiles > self.max_files * fraction
193+
what_is_exceeded &&= :bytes_and_files
194+
what_is_exceeded ||= :files
195+
end
196+
197+
return what_is_exceeded # one of nil, :bytes, :files, or :bytes_and_files
198+
end
199+
145200
#####################################################
146201
# Validations callbacks
147202
#####################################################

BrainPortal/app/views/quotas/_disk_quotas_table.html.erb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,12 @@
7777
:sortable => true,
7878
) { |dq| pretty_quota_max_files(dq) }
7979

80-
# This column is a bit misleading: it shows the CURRENT USER's resources for all
80+
# This column is a bit misleading: it shows the CURRENT OR BROWSE_AS USER's resources for all
8181
# quota records that are DP-wide, and the AFFECTED USER'S resources for the user-specific quotas.
82-
t.column("My Usage") do |dq|
83-
what = dq.exceeded?(dq.user_id == 0 ? current_user.id : dq.user_id)
84-
what = nil if dq.cursize.zero? && dq.curfiles.zero?
82+
# To see a specific user quotas goto the user profile
83+
t.column("Usage") do |dq|
84+
what = dq.exceeded?(dq.user_id == 0 ? @as_user.id : dq.user_id)
85+
what = nil if dq.cursize.zero? && dq.curfiles.zero? # happens for dq with -1,-1
8586
if what.nil?
8687
html_colorize("OK","green") +
8788
" (#{colored_pretty_size(dq.cursize)} and #{number_with_commas(dq.curfiles)} files)".html_safe

BrainPortal/app/views/quotas/_disk_report.html.erb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
<div class="menu_bar">
2828
<%= link_to "Back To Disk Quotas", quotas_path(:mode => :disk), :class => :button %>
29+
<%= link_to "Close to Quotas", report_almost_quotas_path(:mode => :disk), :class => :button %>
30+
<p>
2931
</div>
3032

3133
<p>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
2+
<%-
3+
#
4+
# CBRAIN Project
5+
#
6+
# Copyright (C) 2008-2023
7+
# The Royal Institution for the Advancement of Learning
8+
# McGill University
9+
#
10+
# This program is free software: you can redistribute it and/or modify
11+
# it under the terms of the GNU General Public License as published by
12+
# the Free Software Foundation, either version 3 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# This program is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU General Public License
21+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
#
23+
-%>
24+
25+
<% title 'Almost Exceeded Quotas' %>
26+
27+
<div class="menu_bar">
28+
<%= link_to "Back to Exceeded Quotas Report", report_quotas_path(:mode => :disk), :class => :button %>
29+
<%= link_to "Back To Disk Quotas", quotas_path(:mode => :disk), :class => :button %>
30+
<p>
31+
</div>
32+
33+
<table>
34+
<tr>
35+
<th>User</th>
36+
<th>DataProvider</th>
37+
<th>Size</th>
38+
<th>Size quota</th>
39+
<th>Number of files</th>
40+
<th>Number of files quota</th>
41+
<th>Close to Quota</th>
42+
<th>Exceeded</th>
43+
<th>Details</th>
44+
<th>Quota record</th>
45+
<tr>
46+
47+
<% @user_id_and_quota.each do |user_id,quota| %>
48+
<%
49+
# the following next statement should never get triggered, unless
50+
# at some point I enhance the controller code to show 'nearly exceeded' quotas
51+
# and then we should remove it.
52+
%>
53+
<% exceeded = quota.exceeded?(user_id) %>
54+
<% almost = quota.almost_exceeded?(user_id) unless exceeded %>
55+
56+
<% next unless almost || exceeded %>
57+
<% user_class = quota.is_for_user? ? 'class="quota_user_quota_highlight"'.html_safe : "" %>
58+
<% dp_class = quota.is_for_resource? ? 'class="quota_dp_quota_highlight"'.html_safe : "" %>
59+
<% bytes_class = almost.to_s =~ /bytes/ ? 'class="quota_almost_exceeded"'.html_safe : "" %>
60+
<% bytes_class = 'class="quota_exceeded"'.html_safe if exceeded.to_s =~ /bytes/ %>
61+
<% files_class = almost.to_s =~ /files/ ? 'class="quota_almost_exceeded"'.html_safe : "" %>
62+
<% files_class = 'class="quota_exceeded"'.html_safe if exceeded.to_s =~ /files/ %>
63+
64+
65+
<tr>
66+
<td <%= user_class %>><%= link_to_user_if_accessible(user_id) %></td>
67+
<td <%= dp_class %>><%= link_to_data_provider_if_accessible(quota.data_provider_id) %></td>
68+
<td <%= bytes_class %>><%= colored_pretty_size(quota.cursize) %></td>
69+
<td><%= pretty_quota_max_bytes(quota) %></td>
70+
<td <%= files_class %>><%= number_with_commas(quota.curfiles) %></td>
71+
<td><%= pretty_quota_max_files(quota) %></td>
72+
<td><%= almost.to_s.humanize %></td>
73+
<td><%= exceeded.to_s.humanize %></td>
74+
<td><%=
75+
link_to 'Table',
76+
report_path(
77+
:table_name => 'userfiles.combined_file_rep',
78+
:user_id => user_id,
79+
:data_provider_id => quota.data_provider_id,
80+
:row_type => :user_id ,
81+
:col_type => :type,
82+
:generate => "ok"
83+
), :class => "action_link"
84+
%>
85+
</td>
86+
<td>
87+
<% label = quota.is_for_resource? ? "(DP Quota)" : "(User Quota)" %>
88+
<%= link_to("Show/Edit #{label}", quota_path(quota), :class => "action_link") %>
89+
</td>
90+
</tr>
91+
<% end %>
92+
93+
</table>
94+

BrainPortal/app/views/quotas/index.html.erb

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,26 @@
4242
<% if @mode == :disk %>
4343
<%= link_to "Switch to CPU Quotas", quotas_path(:mode => :cpu), :class => :button %>
4444
<% end %>
45-
</div>
45+
46+
<% user_list = current_user.available_users.sort_by(&:login) %>
47+
<% if @mode == :disk && user_list.size > 1 && current_user.has_role?(:admin_user) %>
48+
49+
50+
<div class="disk_quota_user_select">
51+
View as
52+
<%=
53+
ajax_onchange_select(:as_user_id,
54+
quotas_path,
55+
options_for_select(
56+
user_list.collect { |u| [ u.login, u.id.to_s ] },
57+
@as_user.id.to_s
58+
),
59+
:datatype => 'script'
60+
)
61+
%>
62+
</div>
63+
</div>
64+
<% end %>
4665

4766
<% if @mode == :disk %>
4867
<fieldset class="disk_quota_explanations" style="display: none">
@@ -63,7 +82,7 @@
6382
<p class="long_paragraphs">
6483
This page shows the limits for the amount of CPU processing time
6584
that a user can historically accumulate. There are three rolling
66-
windows: for the CPU time accummulated over the past week, over
85+
windows: for the CPU time accumulated over the past week, over
6786
the past month, and over the entire lifetime of the user's account.
6887
<p class="long_paragraphs">
6988
Each row contains a quota entry with all three limits. Quotas
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
<%-
3+
#
4+
# CBRAIN Project
5+
#
6+
# Copyright (C) 2008-2025
7+
# The Royal Institution for the Advancement of Learning
8+
# McGill University
9+
#
10+
# This program is free software: you can redistribute it and/or modify
11+
# it under the terms of the GNU General Public License as published by
12+
# the Free Software Foundation, either version 3 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# This program is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU General Public License
21+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
#
23+
-%>
24+
25+
<% if @mode == :disk %>
26+
<%= render :partial => 'disk_report_almost' %>
27+
<% end %>

0 commit comments

Comments
 (0)