diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 90b513d49..15fc087a6 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -201,6 +201,9 @@ document.addEventListener("turbolinks:load", function(e) { // Testing section on source page Sources.init(); + // Approve/reject user curation buttons + Curation.init(); + var setStarButtonState = function (button) { if (button.data('starred')) { button.html(" "); diff --git a/app/assets/javascripts/curation.js b/app/assets/javascripts/curation.js new file mode 100644 index 000000000..bc396dda1 --- /dev/null +++ b/app/assets/javascripts/curation.js @@ -0,0 +1,36 @@ +const Curation = { + applyParam: function (select, param) { + const value = select.value; + const url = new URL(window.location.href); + if (!value) { + url.searchParams.delete(param); + } else { + url.searchParams.set(param, value); + } + window.location.replace(url.toString()); + }, + + curateUser: function (e) { + e.preventDefault(); + const url = $(this).parents('.curate-user-buttons').data('actionUrl'); + const panel = $(this).parents('.curate-user'); + panel.fadeOut('fast'); + + $.ajax({ + url: url, + method: 'PUT', + dataType: 'script', + data: { user: { role_id: $(this).data('roleId') } } + }).fail(function (e) { + panel.show(); + console.error(e); + alert('An error occurred while attempting to curate the user.'); + }); + + return false; + }, + + init: function () { + $('.curate-user-buttons .btn').click(Curation.curateUser); + } +} diff --git a/app/controllers/curator_controller.rb b/app/controllers/curator_controller.rb index 5eb4952d6..e325040c2 100644 --- a/app/controllers/curator_controller.rb +++ b/app/controllers/curator_controller.rb @@ -36,7 +36,16 @@ def topic_suggestions def users @role = Role.fetch(params[:role]) if current_user.is_admin? @role ||= Role.fetch('unverified_user') - @users = User.with_role(@role).order('created_at DESC') + @users = User.with_role(@role) + max_age = nil + if params[:max_age] + begin + max_age = ActiveSupport::Duration.parse(params[:max_age]) + rescue ArgumentError + end + end + @users = @users.where('users.created_at > ?', max_age.ago) if max_age + @users = @users.order('created_at DESC') if params[:with_content] @users = @users.includes(*User::CREATED_RESOURCE_TYPES).with_created_resources end diff --git a/app/helpers/curators_helper.rb b/app/helpers/curators_helper.rb index 52d97e201..3c912f57e 100644 --- a/app/helpers/curators_helper.rb +++ b/app/helpers/curators_helper.rb @@ -1,6 +1,13 @@ # The controller for actions related to the Ban model module CuratorsHelper + MAX_AGE_OPTIONS = [ + { period: nil, key: 'any' }.with_indifferent_access, + { period: 1.week, key: 'week' }.with_indifferent_access, + { period: 1.month, key: 'month' }.with_indifferent_access, + { period: 1.year, key: 'year' }.with_indifferent_access + ].freeze + def print_curation_action(action) resource, action = action.split('.') if action @@ -20,6 +27,14 @@ def role_options(selected_role, scope: User) options_for_select(array, selected_role.name) end + def max_age_options(selected_age = nil) + array = MAX_AGE_OPTIONS.map do |age| + [t("curation.users.filters.max_age.options.#{age[:key]}"), age[:period]&.iso8601] + end + + options_for_select(array, selected_age) + end + def recent_approvals PublicActivity::Activity.where(key: 'user.change_role').where('created_at > ?', 3.months.ago).order('created_at DESC').select do |activity| [Role.rejected.id, Role.approved.id].include?(activity.parameters[:new]) @@ -28,10 +43,10 @@ def recent_approvals def approval_message(role_id) if role_id == Role.approved.id - text = 'approved' + text = t('curation.users.activity.approved') css_class = 'text-success' else - text = 'rejected' + text = t('curation.users.activity.rejected') css_class = 'text-danger' end diff --git a/app/views/curation_mailer/user_requires_approval.html.erb b/app/views/curation_mailer/user_requires_approval.html.erb index 2ad705fff..69efadb20 100644 --- a/app/views/curation_mailer/user_requires_approval.html.erb +++ b/app/views/curation_mailer/user_requires_approval.html.erb @@ -23,4 +23,4 @@
  • Reject - Mark this user as a spammer, preventing them from creating further content on <%= TeSS::Config.site['title_short'] %>.
  • -<%= link_to('Click here to approve or reject this user', curate_users_url(with_content: true)) %> +<%= link_to('Click here to approve or reject this user', curate_users_url(with_content: true, max_age: 1.month.iso8601)) %> diff --git a/app/views/curation_mailer/user_requires_approval.text.erb b/app/views/curation_mailer/user_requires_approval.text.erb index 2471e7d50..5cff269bb 100644 --- a/app/views/curation_mailer/user_requires_approval.text.erb +++ b/app/views/curation_mailer/user_requires_approval.text.erb @@ -12,4 +12,4 @@ This user's resources are currently hidden. As a curator, you can decide whether "Reject" - Mark this user as a spammer, preventing them from creating further content on <%= TeSS::Config.site['title_short'] %>. To approve or reject this user, please visit: -<%= curate_users_url(with_content: true) %> +<%= curate_users_url(with_content: true, max_age: 1.month.iso8601) %> diff --git a/app/views/curator/users.html.erb b/app/views/curator/users.html.erb index f876b1d57..02a6f109a 100644 --- a/app/views/curator/users.html.erb +++ b/app/views/curator/users.html.erb @@ -1,132 +1,128 @@ <% filter_params = {} %> <% filter_params[:role] = params[:role] if params.key?(:role) %> <% filter_params[:with_content] = params[:with_content] if params.key?(:with_content) %> +<% filter_params[:max_age] = params[:max_age] if params.key?(:max_age) %> -
    -
    -
    Recent Curation Activity
    -
    - <% activities = recent_approvals %> - <% if activities.any? %> -
      - <% activities.each do |activity| %> -
    • - <%= link_to activity.trackable.name, activity.trackable, target: '_blank' -%> was - <%= approval_message(activity.parameters[:new]) -%> by <%= activity.owner.try(:username) -%> - <%= time_ago_in_words(activity.created_at) -%> ago. -
    • - <% end %> -
    - <% else %> - No recent approvals/rejections. - <% end %> +
    +
    +
    +
    <%= t('curation.users.recent_activity.title') %>
    +
    + <% activities = recent_approvals %> + <% if activities.any? %> +
      + <% activities.each do |activity| %> +
    • + <%= link_to activity.trackable.name, activity.trackable, target: '_blank' -%> - + <%= approval_message(activity.parameters[:new]) -%> + <%= t('curation.users.activity.info', + curator: activity.owner.try(:username), + time: time_ago_in_words(activity.created_at)) -%> +
    • + <% end %> +
    + <% else %> + <%= t('curation.users.recent_activity.empty') %> + <% end %> +
    -
    -
    - <% if @users.any? %> - <% @users.each do |user| %> -
    -
    - <%= link_to user.name, user, target: '_blank' %> +
    + <% if @users.any? %> + <% @users.each do |user| %> +
    +
    + <%= link_to user.name, user, target: '_blank' %> - <% if user.banned? %> - (<%= user.shadowbanned? ? 'shadowbanned' : 'banned'-%>) - <% end %> + <% if user.banned? %> + (<%= user.shadowbanned? ? 'shadowbanned' : 'banned'-%>) + <% end %> -
    - <%= link_to('Approve', '#', class: 'btn btn-xs btn-success', 'data-role-id' => Role.approved.id ) %> - <%= link_to('Reject', '#', class: 'btn btn-xs btn-danger', 'data-role-id' => Role.rejected.id ) %> +
    + <%= link_to('Approve', '#', class: 'btn btn-xs btn-success', 'data-role-id' => Role.approved.id ) %> + <%= link_to('Reject', '#', class: 'btn btn-xs btn-danger', 'data-role-id' => Role.rejected.id ) %> +
    -
    -
    - Registered <%= time_ago_in_words(user.created_at) -%> ago
    +
    + <%= t('curation.users.list.registered_time', time: time_ago_in_words(user.created_at)) -%>
    - Public email: - <% if user.profile.email.blank? %> - None specified - <% else %> - <%= mail_to user.profile.email %> - <% end %>
    + <%= Profile.human_attribute_name(:email) %>: + <% if user.profile.email.blank? %> + <%= t('curation.users.list.blank_attribute') %> + <% else %> + <%= mail_to user.profile.email %> + <% end %>
    - <%= User.human_attribute_name(:website) %>: - <% if user.profile.website.blank? %> - None specified - <% else %> - <%= link_to user.profile.website, user.profile.website, rel: 'nofollow' %> - <% end %>
    + <%= User.human_attribute_name(:website) %>: + <% if user.profile.website.blank? %> + <%= t('curation.users.list.blank_attribute') %> + <% else %> + <%= link_to user.profile.website, user.profile.website, rel: 'nofollow' %> + <% end %>
    - <% User::CREATED_RESOURCE_TYPES.each do |type| %> - <% count = user.send(type).count %> - <% if count > 0 %> - <% resources = user.send(type).order(created_at: :desc).first(3) %> - <%= resources.first.class.model_name.human -%> -
      - <% resources.each do |resource| %> -
    • - <%= link_to resource.title, resource, target: '_blank' %> - <% if resource.respond_to?(:url) %> -
      - URL: <%= link_to resource.url, resource.url, target: '_blank', rel: 'nofollow noreferrer noopener' -%> - <% end %> -
    • + <% User::CREATED_RESOURCE_TYPES.each do |type| %> + <% count = user.send(type).count %> + <% if count > 0 %> + <% resources = user.send(type).order(created_at: :desc).first(3) %> + <%= resources.first.class.model_name.human -%> +
        + <% resources.each do |resource| %> +
      • + <%= link_to resource.title, resource, target: '_blank' %> + <% if resource.respond_to?(:url) %> +
        + URL: <%= link_to resource.url, resource.url, target: '_blank', rel: 'nofollow noreferrer noopener' -%> + <% end %> +
      • + <% end %> +
      + <% if count > 3 %> + <%= link_to "See all #{pluralize(count, resources.first.class.model_name.human)}", + polymorphic_path(type, user: user.username), target: '_blank' -%>
      <% end %> -
    - <% if count > 3 %> - <%= link_to "See all #{pluralize(count, resources.first.class.model_name.human)}", - polymorphic_path(type, user: user.username), target: '_blank' -%>
    <% end %> <% end %> - <% end %> +
    -
    - <% end %> + <% end %> - <%= render partial: 'search/common/pagination_bar', locals: { resources: @users } %> - <% else %> - Could not find any <%= @role.title.downcase.pluralize-%> requiring approval. Another curator may have already taken action. - <% end %> + <%= render partial: 'search/common/pagination_bar', locals: { resources: @users } %> + <% else %> + <%= t('curation.users.list.empty', role: @role.title.downcase.pluralize)-%> + <% end %> +
    - - diff --git a/app/views/layouts/_user_menu.html.erb b/app/views/layouts/_user_menu.html.erb index 2830bb043..55d1008af 100644 --- a/app/views/layouts/_user_menu.html.erb +++ b/app/views/layouts/_user_menu.html.erb @@ -52,7 +52,7 @@ <% if is_admin || is_curator %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 8c0290563..3bdf4a41e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1133,4 +1133,33 @@ en: link: 'Link your ORCID' authenticate: 'Authenticate your ORCID' authentication_success: 'You have successfully authenticated your ORCID.' - authentication_failure: 'Failed to authenticate your ORCID.' \ No newline at end of file + authentication_failure: 'Failed to authenticate your ORCID.' + curation: + users: + title: Curate Users + recent_activity: + title: Recent Curation Activity + empty: No recent approvals/rejections. + list: + empty: Could not find any %{role} requiring approval. Another curator may have already taken action. + registered_time: Registered %{time} ago + blank_attribute: None specified + activity: + approved: approved + rejected: rejected + info: by %{curator} %{time} ago + filters: + with_role: + title: Role + hint: Show users with the selected role. + with_content: + title: With content? + hint: Only show users who have created content. + max_age: + title: Registered + hint: Only show users who registered within the selected period. + options: + any: Any time + week: Within the last week + month: Within the last month + year: Within the last year \ No newline at end of file diff --git a/test/controllers/curator_controller_test.rb b/test/controllers/curator_controller_test.rb index f13202306..a56c600f4 100644 --- a/test/controllers/curator_controller_test.rb +++ b/test/controllers/curator_controller_test.rb @@ -93,6 +93,34 @@ class CuratorControllerTest < ActionController::TestCase assert_includes assigns(:users), new_user end + test 'should allow filtering users based on registration date' do + sign_in users(:admin) + old_user = users(:unverified_user) + old_user.update_column(:created_at, 1.month.ago) + new_user = User.create!(username: 'neWuSer1996', email: 'neWUSer1996@email.com', password: '12345678', + processing_consent: '1', role: roles(:unverified_user)) + + get :users, params: { max_age: 'P1Y' } + + assert_response :success + assert_includes assigns(:users), old_user + assert_includes assigns(:users), new_user + + get :users, params: { max_age: 'P1W' } + + assert_response :success + refute_includes assigns(:users), old_user + assert_includes assigns(:users), new_user + + assert_nothing_raised do + get :users, params: { max_age: "🍌" } # Invalid max age is ignored + + assert_response :success + assert_includes assigns(:users), old_user + assert_includes assigns(:users), new_user + end + end + test 'should not get user curation page if regular user' do sign_in users(:regular_user) @@ -116,8 +144,8 @@ class CuratorControllerTest < ActionController::TestCase get :users assert_response :success - assert_select '#recent-user-curation-activity ul li', text: /#{approved.name}\s+was\s+approved\s+by\s+#{admin.username}/ - assert_select '#recent-user-curation-activity ul li', text: /#{rejected.name}\s+was\s+rejected\s+by\s+#{admin.username}/ + assert_select '#recent-user-curation-activity ul li', text: /#{approved.name}\s+-\s+approved\s+by\s+#{admin.username}/ + assert_select '#recent-user-curation-activity ul li', text: /#{rejected.name}\s+-\s+rejected\s+by\s+#{admin.username}/ end test 'should show all possible resource types under user' do