Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions BrainPortal/app/assets/stylesheets/cbrain.css.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2796,6 +2796,14 @@ img {
background-color: #fdd; /* light pink */
}

.quota_almost_exceeded {
background-color: #ffdfbf;
}

.disk_quota_user_select {
float: right;
}

/* % ######################################################### */
/* % Report Generator Styles */
/* % ######################################################### */
Expand Down
81 changes: 76 additions & 5 deletions BrainPortal/app/controllers/quotas_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,16 @@ def index #:nodoc:
@mode = :cpu if params[:mode].to_s == 'cpu'
@mode = :disk if params[:mode].to_s == 'disk' || @mode != :cpu
cbrain_session[:quota_mode] = @mode.to_s

@scope = scope_from_session("#{@mode}_quotas#index")

# Make sure the target user is set if viewing quotas for another user.
@as_user = see_as_user params['as_user_id']
@scope.custom['as_user_id'] = @as_user.id

@base_scope = base_scope.includes([:user, :data_provider ]) if @mode == :disk
@base_scope = base_scope.includes([:user, :remote_resource]) if @mode == :cpu
@view_scope = @scope.apply(@base_scope)

@view_scope = @scope.apply(@base_scope)

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

end

def report_almost
@mode = params[:mode].to_s == 'cpu' ? :cpu : :disk
cb_exception("not supported") if @mode == :cpu
report_disk_almost if @mode == :disk
end

def report_disk_almost
almost = 0.95 # share of resource use qualifying for 'almost exceeding'
quota_to_user_ids = {} # quota_obj => [uid, uid...]

# Scan DP-wide quota objects
DiskQuota.where(:user_id => 0).all.each do |quota|
exceed_size_user_ids = Userfile
.where(:data_provider_id => quota.data_provider_id)
.group(:user_id)
.sum(:size)
.select { |user_id,size| size >= quota.max_bytes * almost }
.keys
exceed_numfiles_user_ids = Userfile
.where(:data_provider_id => quota.data_provider_id)
.group(:user_id)
.sum(:num_files)
.select { |user_id,num_files| num_files >= quota.max_files * almost }
.keys

union_ids = exceed_size_user_ids | exceed_numfiles_user_ids
union_ids -= DiskQuota
.where(:data_provider_id => quota.data_provider_id, :user_id => union_ids)
.pluck(:user_id) # remove user IDs that have their own quota records
quota_to_user_ids[quota] = union_ids if union_ids.size > 0
end

# Scan user-specific quota objects
DiskQuota.where('user_id > 0').all.each do |quota|
quota_to_user_ids[quota] = [ quota.user_id ] if quota.almost_exceeded?
end

# Inverse relation: user_id => [ quota, quota ]
user_id_to_quotas = {}
quota_to_user_ids.each do |quota,user_ids|
user_ids.each do |user_id|
user_id_to_quotas[user_id] ||= []
user_id_to_quotas[user_id] << quota
end
end

# Table content: [ [ user_id, quota ], [user_id, quota] ... ]
# Note: the rows are grouped by user_id, but not sorted in any way...
@user_id_and_quota = []
user_id_to_quotas.each do |user_id, quotas|
quotas.each do |quota|
@user_id_and_quota << [ user_id, quota ]
end
end

end

# a clone of browse_as
def see_as_user(as_user_id) #:nodoc:
scope = scope_from_session("#{@mode}_quotas#index")
users = current_user.available_users
as_user = users.where(:id => as_user_id).first
as_user ||= users.where(:id => scope.custom['as_user_id']).first
as_user ||= current_user
as_user
end

private

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

return scope if current_user.has_role?(:admin_user)
return scope if current_user.has_role?(:admin_user) && @as_user.id == current_user.id

if @mode == :disk
dp_ids = DataProvider.all.select { |dp| dp.can_be_accessed_by?(current_user) }.map(&:id)
dp_ids = DataProvider.all.select { |dp| dp.can_be_accessed_by?(@as_user) }.map(&:id)
scope = scope.where(
:user_id => [ 0, current_user.id ],
:user_id => [ 0, @as_user.id ],
:data_provider_id => dp_ids,
)
end
Expand Down
14 changes: 14 additions & 0 deletions BrainPortal/app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ def show #:nodoc:
# Hash of OIDC uris with the OIDC name as key
@oidc_uris = generate_oidc_login_uri(@oidc_configs)

# few attributes for quotes table
@scope = scope_from_session("mydiskquotes")
dp_ids = DataProvider.all.select { |dp| dp.can_be_accessed_by?(current_user) }.map(&:id)
@base_scope = DiskQuota.where(
:data_provider_id => dp_ids,
:user_id => [ 0, @user.id ],
).includes([:user, :data_provider])

@view_scope = @scope.apply(@base_scope)

@scope.pagination ||= Scope::Pagination.from_hash({ :per_page => 10 })
@quotas = @scope.pagination.apply(@view_scope)


respond_to do |format|
format.html # show.html.erb
format.xml do
Expand Down
55 changes: 55 additions & 0 deletions BrainPortal/app/models/disk_quota.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,29 @@ def self.exceeded!(user_id, data_provider_id)
raise CbrainDiskQuotaExceeded.new(user_id, data_provider_id)
end


# Returns true if currently, the user specified by +user_id+
# uses uses almost all disk space or more total files on +data_provider_id+ than
# the quota limit configured by the admin. A share is considered almost all
# if it exceeds fraction. Fraction should be a number greater than 0 and smaller
# than 1
#
# The quota record for the limits is first looked up specifically for the pair
# (user, data_provider); if no quota record is found, the pair (0, data_provider)
# will be fetched instead (meaning a default quota for all users on that DP)
#
# Possible returned values:
# nil : all is OK
# :bytes : disk space is exceeded
# :files : number of files is exceeded
# :bytes_and_files : both are exceeded
def self.almost_exceeded?(user_id, data_provider_id)
quota = self.where(:user_id => user_id, :data_provider_id => data_provider_id).first
quota ||= self.where(:user_id => 0 , :data_provider_id => data_provider_id).first
return nil if quota.nil?
quota.almost_exceeded?(user_id)
end

# Returns true if currently, the user specified by +user+ (specified by id)
# uses more disk space or more total files on than configured in the limits
# of this quota object. Since a quota object can contain '0' for the user attribute
Expand Down Expand Up @@ -142,6 +165,38 @@ def exceeded!(user_id = self.user_id)
raise CbrainDiskQuotaExceeded.new(user_id, self.data_provider_id)
end

# same as exceeded but evaluates true also when almost all allowed disk space or file
# quota are used
def almost_exceeded?(user_id = self.user_id, fraction = 0.95)

return nil if user_id == 0 # just in case

@cursize, @curfiles = Rails.cache.fetch(
"disk_usage-u=#{user_id}-dp=#{data_provider_id}",
:expires_in => CACHED_USAGE_EXPIRATION
) do
req = Userfile
.where(:user_id => user_id)
.where(:data_provider_id => data_provider_id)
[ req.sum(:size), req.sum(:num_files) ]
end

what_is_exceeded = nil

# exceeded? method, as a side effect sets @cursize and @

if @cursize > self.max_bytes * fraction
what_is_exceeded = :bytes
end

if @curfiles > self.max_files * fraction
what_is_exceeded &&= :bytes_and_files
what_is_exceeded ||= :files
end

return what_is_exceeded # one of nil, :bytes, :files, or :bytes_and_files
end

#####################################################
# Validations callbacks
#####################################################
Expand Down
9 changes: 5 additions & 4 deletions BrainPortal/app/views/quotas/_disk_quotas_table.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,12 @@
:sortable => true,
) { |dq| pretty_quota_max_files(dq) }

# This column is a bit misleading: it shows the CURRENT USER's resources for all
# This column is a bit misleading: it shows the CURRENT OR BROWSE_AS USER's resources for all
# quota records that are DP-wide, and the AFFECTED USER'S resources for the user-specific quotas.
t.column("My Usage") do |dq|
what = dq.exceeded?(dq.user_id == 0 ? current_user.id : dq.user_id)
what = nil if dq.cursize.zero? && dq.curfiles.zero?
# To see a specific user quotas goto the user profile
t.column("Usage") do |dq|
what = dq.exceeded?(dq.user_id == 0 ? @as_user.id : dq.user_id)
what = nil if dq.cursize.zero? && dq.curfiles.zero? # happens for dq with -1,-1
if what.nil?
html_colorize("OK","green") +
" (#{colored_pretty_size(dq.cursize)} and #{number_with_commas(dq.curfiles)} files)".html_safe
Expand Down
2 changes: 2 additions & 0 deletions BrainPortal/app/views/quotas/_disk_report.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

<div class="menu_bar">
<%= link_to "Back To Disk Quotas", quotas_path(:mode => :disk), :class => :button %>
<%= link_to "Close to Quotas", report_almost_quotas_path(:mode => :disk), :class => :button %>
<p>
</div>

<p>
Expand Down
94 changes: 94 additions & 0 deletions BrainPortal/app/views/quotas/_disk_report_almost.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@

<%-
#
# CBRAIN Project
#
# Copyright (C) 2008-2023
# The Royal Institution for the Advancement of Learning
# McGill University
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-%>

<% title 'Almost Exceeded Quotas' %>

<div class="menu_bar">
<%= link_to "Back to Exceeded Quotas Report", report_quotas_path(:mode => :disk), :class => :button %>
<%= link_to "Back To Disk Quotas", quotas_path(:mode => :disk), :class => :button %>
<p>
</div>

<table>
<tr>
<th>User</th>
<th>DataProvider</th>
<th>Size</th>
<th>Size quota</th>
<th>Number of files</th>
<th>Number of files quota</th>
<th>Close to Quota</th>
<th>Exceeded</th>
<th>Details</th>
<th>Quota record</th>
<tr>

<% @user_id_and_quota.each do |user_id,quota| %>
<%
# the following next statement should never get triggered, unless
# at some point I enhance the controller code to show 'nearly exceeded' quotas
# and then we should remove it.
%>
<% exceeded = quota.exceeded?(user_id) %>
<% almost = quota.almost_exceeded?(user_id) unless exceeded %>

<% next unless almost || exceeded %>
<% user_class = quota.is_for_user? ? 'class="quota_user_quota_highlight"'.html_safe : "" %>
<% dp_class = quota.is_for_resource? ? 'class="quota_dp_quota_highlight"'.html_safe : "" %>
<% bytes_class = almost.to_s =~ /bytes/ ? 'class="quota_almost_exceeded"'.html_safe : "" %>
<% bytes_class = 'class="quota_exceeded"'.html_safe if exceeded.to_s =~ /bytes/ %>
<% files_class = almost.to_s =~ /files/ ? 'class="quota_almost_exceeded"'.html_safe : "" %>
<% files_class = 'class="quota_exceeded"'.html_safe if exceeded.to_s =~ /files/ %>


<tr>
<td <%= user_class %>><%= link_to_user_if_accessible(user_id) %></td>
<td <%= dp_class %>><%= link_to_data_provider_if_accessible(quota.data_provider_id) %></td>
<td <%= bytes_class %>><%= colored_pretty_size(quota.cursize) %></td>
<td><%= pretty_quota_max_bytes(quota) %></td>
<td <%= files_class %>><%= number_with_commas(quota.curfiles) %></td>
<td><%= pretty_quota_max_files(quota) %></td>
<td><%= almost.to_s.humanize %></td>
<td><%= exceeded.to_s.humanize %></td>
<td><%=
link_to 'Table',
report_path(
:table_name => 'userfiles.combined_file_rep',
:user_id => user_id,
:data_provider_id => quota.data_provider_id,
:row_type => :user_id ,
:col_type => :type,
:generate => "ok"
), :class => "action_link"
%>
</td>
<td>
<% label = quota.is_for_resource? ? "(DP Quota)" : "(User Quota)" %>
<%= link_to("Show/Edit #{label}", quota_path(quota), :class => "action_link") %>
</td>
</tr>
<% end %>

</table>

23 changes: 21 additions & 2 deletions BrainPortal/app/views/quotas/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,26 @@
<% if @mode == :disk %>
<%= link_to "Switch to CPU Quotas", quotas_path(:mode => :cpu), :class => :button %>
<% end %>
</div>

<% user_list = current_user.available_users.sort_by(&:login) %>
<% if @mode == :disk && user_list.size > 1 && current_user.has_role?(:admin_user) %>


<div class="disk_quota_user_select">
View as
<%=
ajax_onchange_select(:as_user_id,
quotas_path,
options_for_select(
user_list.collect { |u| [ u.login, u.id.to_s ] },
@as_user.id.to_s
),
:datatype => 'script'
)
%>
</div>
</div>
<% end %>

<% if @mode == :disk %>
<fieldset class="disk_quota_explanations" style="display: none">
Expand All @@ -63,7 +82,7 @@
<p class="long_paragraphs">
This page shows the limits for the amount of CPU processing time
that a user can historically accumulate. There are three rolling
windows: for the CPU time accummulated over the past week, over
windows: for the CPU time accumulated over the past week, over
the past month, and over the entire lifetime of the user's account.
<p class="long_paragraphs">
Each row contains a quota entry with all three limits. Quotas
Expand Down
27 changes: 27 additions & 0 deletions BrainPortal/app/views/quotas/report_almost.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

<%-
#
# CBRAIN Project
#
# Copyright (C) 2008-2025
# The Royal Institution for the Advancement of Learning
# McGill University
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-%>

<% if @mode == :disk %>
<%= render :partial => 'disk_report_almost' %>
<% end %>
Loading