diff --git a/BrainPortal/app/assets/stylesheets/cbrain.css.erb b/BrainPortal/app/assets/stylesheets/cbrain.css.erb index bccab7597..2215637fd 100644 --- a/BrainPortal/app/assets/stylesheets/cbrain.css.erb +++ b/BrainPortal/app/assets/stylesheets/cbrain.css.erb @@ -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 */ /* % ######################################################### */ diff --git a/BrainPortal/app/controllers/quotas_controller.rb b/BrainPortal/app/controllers/quotas_controller.rb index 71e776ed5..987cde9ab 100644 --- a/BrainPortal/app/controllers/quotas_controller.rb +++ b/BrainPortal/app/controllers/quotas_controller.rb @@ -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?) @@ -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: @@ -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 diff --git a/BrainPortal/app/controllers/users_controller.rb b/BrainPortal/app/controllers/users_controller.rb index 0fa0d2f07..eb4c1bd9f 100644 --- a/BrainPortal/app/controllers/users_controller.rb +++ b/BrainPortal/app/controllers/users_controller.rb @@ -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 diff --git a/BrainPortal/app/models/disk_quota.rb b/BrainPortal/app/models/disk_quota.rb index 45919a992..f7e41e909 100644 --- a/BrainPortal/app/models/disk_quota.rb +++ b/BrainPortal/app/models/disk_quota.rb @@ -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 @@ -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 ##################################################### diff --git a/BrainPortal/app/views/quotas/_disk_quotas_table.html.erb b/BrainPortal/app/views/quotas/_disk_quotas_table.html.erb index b142e2506..36c546774 100644 --- a/BrainPortal/app/views/quotas/_disk_quotas_table.html.erb +++ b/BrainPortal/app/views/quotas/_disk_quotas_table.html.erb @@ -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 diff --git a/BrainPortal/app/views/quotas/_disk_report.html.erb b/BrainPortal/app/views/quotas/_disk_report.html.erb index 657d2e6aa..cea231d95 100644 --- a/BrainPortal/app/views/quotas/_disk_report.html.erb +++ b/BrainPortal/app/views/quotas/_disk_report.html.erb @@ -26,6 +26,8 @@

diff --git a/BrainPortal/app/views/quotas/_disk_report_almost.html.erb b/BrainPortal/app/views/quotas/_disk_report_almost.html.erb new file mode 100644 index 000000000..d416773e7 --- /dev/null +++ b/BrainPortal/app/views/quotas/_disk_report_almost.html.erb @@ -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 . +# +-%> + +<% title 'Almost Exceeded Quotas' %> + +

+ + + + + + + + + + + + + + + +<% @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/ %> + + + + + + + + + + + + + + +<% end %> + +
UserDataProviderSizeSize quotaNumber of filesNumber of files quotaClose to QuotaExceededDetailsQuota record
><%= link_to_user_if_accessible(user_id) %>><%= link_to_data_provider_if_accessible(quota.data_provider_id) %>><%= colored_pretty_size(quota.cursize) %><%= pretty_quota_max_bytes(quota) %>><%= number_with_commas(quota.curfiles) %><%= pretty_quota_max_files(quota) %><%= almost.to_s.humanize %><%= exceeded.to_s.humanize %><%= + 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" + %> + + <% label = quota.is_for_resource? ? "(DP Quota)" : "(User Quota)" %> + <%= link_to("Show/Edit #{label}", quota_path(quota), :class => "action_link") %> +
+ diff --git a/BrainPortal/app/views/quotas/index.html.erb b/BrainPortal/app/views/quotas/index.html.erb index 9b9c4c131..4effa0ae7 100644 --- a/BrainPortal/app/views/quotas/index.html.erb +++ b/BrainPortal/app/views/quotas/index.html.erb @@ -42,7 +42,26 @@ <% if @mode == :disk %> <%= link_to "Switch to CPU Quotas", quotas_path(:mode => :cpu), :class => :button %> <% end %> - + + <% user_list = current_user.available_users.sort_by(&:login) %> + <% if @mode == :disk && user_list.size > 1 && current_user.has_role?(:admin_user) %> + + +
+ 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' + ) + %> +
+ + <% end %> <% if @mode == :disk %>